dotfiles/install.py

217 lines
7.5 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
import argparse
from datetime import timedelta
import os
import shutil
2019-01-29 19:45:45 -06:00
import socket
import subprocess
import sys
2020-04-04 02:45:46 -05:00
from functools import partial
from pathlib import Path
from typing import List
2019-01-29 19:45:45 -06:00
import mako.lookup
import mako.template
import requests_cache
2019-01-29 19:45:45 -06:00
import toml
2020-04-04 02:45:46 -05:00
import yaml
http_session = requests_cache.CachedSession(expire_after=timedelta(days=1))
BASE16_TEMPLATES_URL = "https://raw.githubusercontent.com/chriskempson/base16-templates-source/master/list.yaml"
BASE16_TEMPLATES = yaml.safe_load(http_session.get(BASE16_TEMPLATES_URL).text)
2020-04-04 02:45:46 -05:00
2022-04-02 13:28:09 -05:00
# Pending https://github.com/chriskempson/base16-templates-source/pull/106
BASE16_TEMPLATES["wofi-colors"] = "https://github.com/agausmann/base16-wofi-colors"
2022-04-02 13:28:09 -05:00
2020-04-04 02:45:46 -05:00
def get_base16(scheme, app, template="default"):
2022-04-02 13:28:09 -05:00
base_url = BASE16_TEMPLATES[app]
if "github.com" in base_url:
base_url = (
base_url.replace("github.com", "raw.githubusercontent.com") + "/master/"
)
2022-04-02 13:28:09 -05:00
else:
base_url += "/blob/master/"
config = yaml.safe_load(http_session.get(base_url + "templates/config.yaml").text)
output = config[template]["output"]
extension = config[template]["extension"]
return http_session.get(base_url + output + "/base16-" + scheme + extension).text
2019-01-29 19:45:45 -06:00
def is_outdated(src: List[Path], dst: Path) -> bool:
if not dst.exists():
return True
dst_modified = dst.stat().st_mtime
return any(a_src.stat().st_mtime > dst_modified for a_src in src if a_src.exists())
def main():
parser = argparse.ArgumentParser(
description="Generates and installs dotfiles for this host.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-d",
"--dotfiles",
help="The base directory of the dotfiles repository.",
type=Path,
default=Path(sys.argv[0]).parent,
)
parser.add_argument(
"-n",
"--hostname",
help="The hostname or other identifying name of this system that will"
" be used to retrieve the host-specific configuration.",
default=os.environ.get("HOSTNAME") or socket.gethostname(),
)
parser.add_argument(
"-o",
"--home",
help="The home directory where generated dotfiles will be installed.",
2019-01-29 19:45:45 -06:00
type=Path,
default=os.environ.get("HOME") or Path.home(),
)
parser.add_argument(
"-f",
"--force",
help="Force overwrite all files even if they are not considered outdated.",
action="store_true",
)
args = parser.parse_args()
raw_dir = args.dotfiles / "raw"
templates_dir = args.dotfiles / "templates"
include_dir = args.dotfiles / "include"
default_host_filename = args.dotfiles / "hosts" / "default.toml"
host_filename = args.dotfiles / "hosts" / "{}.toml".format(args.hostname)
2019-01-29 19:45:45 -06:00
with open(default_host_filename) as host_file:
host_config = toml.load(host_file)
if host_filename.exists():
with open(host_filename) as host_file:
host_config.update(toml.load(host_file))
host_config["name"] = args.hostname
2019-01-29 19:45:45 -06:00
# Preprocess output configs for sway
for input in host_config.get("inputs", []):
2023-09-27 20:09:16 -05:00
# Generate config lines for sway template
lines = []
for key in input:
if key == "match":
2023-09-27 20:09:16 -05:00
continue
if isinstance(input[key], list):
val = " ".join(repr(elem) for elem in input[key])
2023-09-27 20:09:16 -05:00
else:
val = repr(input[key])
lines.append(f"{key} {val}")
2023-09-27 20:09:16 -05:00
input["sway-lines"] = lines
2023-09-27 20:09:16 -05:00
for output in host_config["outputs"]:
# Generate config lines for sway template
lines = []
for key in output:
if key == "match":
continue
if isinstance(output[key], list):
val = " ".join(repr(elem) for elem in output[key])
else:
val = repr(output[key])
lines.append(f"{key} {val}")
output["sway-lines"] = lines
# Attempt to resolve device names for swaylock template
# (Workaround https://github.com/swaywm/swaylock/issues/114)
#
# This will only work if this is run on the target host
# and if sway is running, but that is usually the case...
if output["match"] != "*":
try:
if "SWAYSOCK" in os.environ:
get_outputs = subprocess.check_output(
["swaymsg", "-t", "get_outputs", "-p"],
).decode("utf-8")
for line in get_outputs.splitlines():
# Line format: Output <device> '<match identifier>'
if line.startswith("Output") and output["match"] in line:
output["device"] = line.split()[1]
break
elif "NIRI_SOCKET" in os.environ:
get_outputs = subprocess.check_output(
["niri", "msg", "outputs"],
).decode("utf-8")
for line in get_outputs.splitlines():
# Line format: Output "<match identifier>" (<device>)
if line.startswith("Output") and output["match"] in line:
output["device"] = (
line.split()[-1].removeprefix("(").removesuffix(")")
)
break
else:
print(
"Could not find SWAYSOCK or NIRI_SOCKET, cannot retrieve output names."
)
print(
"Please re-run in sway or niri to finish configuring swaylock."
)
except subprocess.CalledProcessError:
print("Could not contact sway or niri to retrieve output names.")
print("Please re-run in sway or niri to finish configuring swaylock.")
2019-01-29 19:45:45 -06:00
lookup = mako.lookup.TemplateLookup(
directories=[
str(templates_dir),
str(include_dir),
],
)
for raw_path in raw_dir.glob("**/*"):
if not raw_path.is_file():
continue
rel_path = raw_path.relative_to(raw_dir)
output_path = args.home / rel_path
if args.force or is_outdated([raw_path], output_path):
print(rel_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(raw_path, output_path)
for template_path in templates_dir.glob("**/*"):
2019-01-29 19:45:45 -06:00
if not template_path.is_file():
continue
rel_path = template_path.relative_to(templates_dir)
2019-01-29 19:45:45 -06:00
output_path = args.home / template_path.relative_to(templates_dir)
if args.force or is_outdated([template_path, host_filename], output_path):
print(rel_path)
template = mako.template.Template(
filename=str(template_path),
strict_undefined=True,
lookup=lookup,
)
output = template.render(
host=host_config,
2022-04-03 19:09:57 -05:00
home=args.home,
get_base16=partial(
get_base16, host_config.get("base16-scheme", "default-dark")
),
)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w+") as output_file:
output_file.write(output)
# Copy permissions from original file
2023-05-11 20:30:18 -05:00
output_path.chmod(template_path.stat().st_mode & 0o777)
if __name__ == "__main__":
2020-04-04 02:45:46 -05:00
main()