From e5a973d8c59906f12e0182fcf9f527ada479d2e9 Mon Sep 17 00:00:00 2001 From: Adam Gausmann Date: Fri, 17 Jan 2025 17:40:22 -0600 Subject: [PATCH] Use attrs schemas for host configs --- .gitignore | 1 + Pipfile | 2 + Pipfile.lock | 4 +- hosts/adventurer.toml | 10 +- hosts/concord.toml | 2 +- hosts/default.toml | 19 -- hosts/faithful.toml | 4 +- hosts/liberation.toml | 8 +- hosts/ondeck.toml | 2 +- install.py | 244 +++++++++++++++------ templates/.config/alacritty/alacritty.toml | 6 +- templates/.config/i3status/config | 10 +- templates/.config/niri/config.kdl | 14 +- templates/.config/sway/config | 24 +- templates/.config/swayidle/config | 2 +- templates/.config/swaylock/config | 13 +- templates/.config/waybar/config.jsonc | 16 +- templates/.config/waybar/style.css | 2 +- templates/.config/wofi/style.css | 2 +- templates/.config/wpaperd/config.toml | 8 +- templates/.ssh/config | 2 +- 21 files changed, 227 insertions(+), 168 deletions(-) delete mode 100644 hosts/default.toml diff --git a/.gitignore b/.gitignore index 827aa16..88c1deb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__ *.pyc http_cache.sqlite +test_output diff --git a/Pipfile b/Pipfile index c194278..f9c78d1 100644 --- a/Pipfile +++ b/Pipfile @@ -9,5 +9,7 @@ toml = "*" pyyaml = "*" requests = "*" requests-cache = "*" +attrs = "*" +cattrs = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 00f7e3a..c2f5b4e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "50f1560a571ae9cbd1a971a155ec3ad2ffcbf9c2c641e7cd71f59795d7b18dd7" + "sha256": "3b96d3b70c9cd62e42cf05545b406ce7aba31fe6b71050e7f6f71dfe3afe7624" }, "pipfile-spec": 6, "requires": {}, @@ -19,6 +19,7 @@ "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308" ], + "index": "pypi", "markers": "python_version >= '3.8'", "version": "==24.3.0" }, @@ -27,6 +28,7 @@ "sha256:67c7495b760168d931a10233f979b28dc04daf853b30752246f4f8471c6d68d0", "sha256:8028cfe1ff5382df59dd36474a86e02d817b06eaf8af84555441bac915d2ef85" ], + "index": "pypi", "markers": "python_version >= '3.8'", "version": "==24.1.2" }, diff --git a/hosts/adventurer.toml b/hosts/adventurer.toml index 3ab89f0..c357492 100644 --- a/hosts/adventurer.toml +++ b/hosts/adventurer.toml @@ -1,29 +1,29 @@ wireless = ["wlp5s0"] ethernet = ["enp6s0"] # hwmon2 = k10temp, temp1 = Tctl -temperature-path = "/sys/class/hwmon/hwmon2/temp1_input" +temperature_path = "/sys/class/hwmon/hwmon2/temp1_input" [[outputs]] match = "*" -background = ["~/.local/share/backgrounds/adventurer/full.jpg", "fill"] +background_path = "~/.local/share/backgrounds/adventurer/full.jpg" [[outputs]] match = "ASUSTek COMPUTER INC VG258 M1LMQS004947" position = [0, 1080] mode = "1920x1080@165Hz" -background = ["~/.local/share/backgrounds/adventurer/l.jpg", "fill"] +background_path = "~/.local/share/backgrounds/adventurer/l.jpg" [[outputs]] match = "BNQ BenQ GL2580 46J02196SL0" position = [1920, 1080] mode = "1920x1080@60Hz" -background = ["~/.local/share/backgrounds/adventurer/r.jpg", "fill"] +background_path = "~/.local/share/backgrounds/adventurer/r.jpg" [[outputs]] match = "BNQ BenQ GL2580 46J02151SL0" position = [960, 0] mode = "1920x1080@60Hz" -background = ["~/.local/share/backgrounds/adventurer/t.jpg", "fill"] +background_path = "~/.local/share/backgrounds/adventurer/t.jpg" [[outputs]] match = "Pioneer Electronic Corporation AV Receiver Unknown" diff --git a/hosts/concord.toml b/hosts/concord.toml index c46811e..482a571 100644 --- a/hosts/concord.toml +++ b/hosts/concord.toml @@ -3,7 +3,7 @@ ethernet = ["eno1"] [[outputs]] match = "*" -background = ["~/.local/share/backgrounds/concord/full.jpg", "fill"] +background_path = "~/.local/share/backgrounds/concord/full.jpg" [[outputs]] match = "Pioneer Electronic Corporation AV Receiver Unknown" diff --git a/hosts/default.toml b/hosts/default.toml deleted file mode 100644 index 781d4e2..0000000 --- a/hosts/default.toml +++ /dev/null @@ -1,19 +0,0 @@ -is-virtual = false -base16-scheme = "seti" -wireless = [] -ethernet = [] -disks = ["/"] -system-font = "Fira Sans" -system-mono-font = "Fira Mono" -#temperature-path = "" -terminal = "alacritty" -lock-cmd = "swaylock -c 000000" -display-on-cmd = "wlopm --on '*'" -display-off-cmd = "wlopm --off '*'" -use-jump-host = false - -inputs = [] - -[[outputs]] -match = "*" -background = ["~/.local/share/backgrounds/sway-dark-1920x1080.png", "fill"] diff --git a/hosts/faithful.toml b/hosts/faithful.toml index 4db7ae8..7547453 100644 --- a/hosts/faithful.toml +++ b/hosts/faithful.toml @@ -1,7 +1,7 @@ wireless = ["wlp4s0"] ethernet = ["enp2s0"] -temperature-path = "/sys/class/hwmon/hwmon5/temp1_input" +temperature_path = "/sys/class/hwmon/hwmon5/temp1_input" [[outputs]] match = "*" -background = ["~/.local/share/backgrounds/faithful/full.jpg", "fill"] +background_path = "~/.local/share/backgrounds/faithful/full.jpg" diff --git a/hosts/liberation.toml b/hosts/liberation.toml index ab651a1..8b6267a 100644 --- a/hosts/liberation.toml +++ b/hosts/liberation.toml @@ -1,12 +1,12 @@ wireless = ["wlp170s0"] #ethernet = ["enp2s0"] -temperature-path = "/sys/class/hwmon/hwmon4/temp1_input" +temperature_path = "/sys/class/hwmon/hwmon4/temp1_input" [[outputs]] match = "eDP-1" -background = ["~/.local/share/backgrounds/sway-dark-1920x1080.png", "fill"] -scale = "1.25" +background_path = "~/.local/share/backgrounds/sway-dark-1920x1080.png" +scale = 1.25 [[inputs]] match = "2362:628:PIXA3854:00_093A:0274_Touchpad" -tap = "enabled" +tap = true diff --git a/hosts/ondeck.toml b/hosts/ondeck.toml index 7b7a030..6234a55 100644 --- a/hosts/ondeck.toml +++ b/hosts/ondeck.toml @@ -1,4 +1,4 @@ wireless = ["wlan0"] ethernet = ["eth0"] disk = ["/"] -temperature-path = "/sys/class/hwmon/hwmon0/temp1_input" +temperature_path = "/sys/class/hwmon/hwmon0/temp1_input" diff --git a/install.py b/install.py index 9d0f3ff..271ae29 100755 --- a/install.py +++ b/install.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 import argparse +from dataclasses import dataclass from datetime import timedelta +from enum import Enum import os import shutil import socket @@ -11,6 +13,8 @@ from functools import partial from pathlib import Path from typing import List +from attrs import define, field +import cattrs import mako.lookup import mako.template import requests_cache @@ -48,6 +52,133 @@ def is_outdated(src: List[Path], dst: Path) -> bool: return any(a_src.stat().st_mtime > dst_modified for a_src in src if a_src.exists()) +class BackgroundMode(Enum): + Fill = "fill" + + @property + def sway_value(self) -> str: + match self: + case self.Fill: + return "fill" + + @property + def wpaperd_value(self) -> str: + match self: + case self.Fill: + return "center" + + +@define +class InputConfig: + match: str + tap: bool | None = None + + @property + def sway_lines(self) -> str: + lines = [] + if self.tap is not None: + lines.append(f" tap {'enabled' if self.tap else 'disabled'}") + + return "\n".join(lines) + + +@define +class OutputConfig: + match: str + position: list | None = None + mode: str | None = None + scale: float | None = None + background_path: str | None = None + background_mode: BackgroundMode = BackgroundMode.Fill + device: str | None = None + + @property + def sway_lines(self) -> str: + lines = [] + if self.position: + lines.append(f" position {' '.join(map(str, self.position))}") + if self.mode: + lines.append(f" mode '{self.mode}'") + if self.background_path: + lines.append( + f" background '{self.background_path}' {self.background_mode.sway_value}" + ) + + return "\n".join(lines) + + @property + def swaylock_image_line(self) -> str | None: + if (self.match != "*" and self.device is None) or self.background_path is None: + return None + + if self.match == "*": + return f"image={self.background_path}" + return f"image={self.device}:{self.background_path}" + + @property + def niri_lines(self) -> str: + lines = [] + if self.position: + lines.append(f" position x={self.position[0]} y={self.position[1]}") + if self.mode: + lines.append(f" mode \"{self.mode.removesuffix('Hz')}\"") + if self.scale: + lines.append(f" scale {self.scale}") + + return "\n".join(lines) + + @property + def wpaperd_config_fragment(self) -> dict: + if self.background_path is None: + return {} + + key = self.match + if key == "*": + key = "any" + + return { + key: { + "path": self.background_path, + "mode": self.background_mode.wpaperd_value, + } + } + + +@define +class HostConfig: + name: str + is_virtual: bool = False + base16_scheme: str = "seti" + wireless: list[str] = field(factory=list) + ethernet: list[str] = field(factory=list) + disks: list[str] = field(factory=lambda: ["/"]) + system_font: str = "Fira Sans" + system_mono_font: str = "Fira Mono" + temperature_path: str | None = None + terminal: str = "alacritty" + lock_cmd: str = "swaylock -c 000000" + display_on_cmd: str = "wlopm --on *" + display_off_cmd: str = "wlopm --off *" + use_jump_host: bool = False + inputs: list[InputConfig] = field(factory=list) + outputs: list[OutputConfig] = field(factory=list) + + @property + def swaylock_images(self) -> str: + return "\n".join( + line for output in self.outputs if (line := output.swaylock_image_line) + ) + + @property + def wpaperd_config(self) -> str: + config = {} + + for output in self.outputs: + config.update(output.wpaperd_config_fragment) + + return toml.dumps(config) + + def main(): parser = argparse.ArgumentParser( description="Generates and installs dotfiles for this host.", @@ -85,86 +216,59 @@ def main(): 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) - with open(default_host_filename) as host_file: - host_config = toml.load(host_file) - + host_toml = { + "name": args.hostname, + } if host_filename.exists(): with open(host_filename) as host_file: - host_config.update(toml.load(host_file)) + host_toml.update(toml.load(host_file)) - host_config["name"] = args.hostname - - # Preprocess output configs for sway - for input in host_config.get("inputs", []): - # Generate config lines for sway template - lines = [] - for key in input: - if key == "match": - continue - if isinstance(input[key], list): - val = " ".join(repr(elem) for elem in input[key]) - else: - val = repr(input[key]) - lines.append(f"{key} {val}") - - input["sway-lines"] = lines - - 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 + host_config = cattrs.structure(host_toml, HostConfig) + for output in host_config.outputs: # 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 '' - if line.startswith("Output") and output["match"] in line: - output["device"] = line.split()[1] - break + # and if sway or niri is running, but that is usually the case... + if output.match == "*": + continue - 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 "" () - 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." - ) + 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 '' + if line.startswith("Output") and output.match in line: + output.device = line.split()[1] + break - 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.") + 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 "" () + 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.") lookup = mako.lookup.TemplateLookup( directories=[ @@ -200,9 +304,7 @@ def main(): output = template.render( host=host_config, home=args.home, - get_base16=partial( - get_base16, host_config.get("base16-scheme", "default-dark") - ), + get_base16=partial(get_base16, host_config.base16_scheme), ) output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w+") as output_file: diff --git a/templates/.config/alacritty/alacritty.toml b/templates/.config/alacritty/alacritty.toml index d8ebcd9..e71c82b 100644 --- a/templates/.config/alacritty/alacritty.toml +++ b/templates/.config/alacritty/alacritty.toml @@ -1,9 +1,5 @@ [font] -% if host['is-virtual']: -size = 10 -% else: -size = 12 -%endif +size = ${10 if host.is_virtual else 12} [font.bold] family = "Fira Mono" diff --git a/templates/.config/i3status/config b/templates/.config/i3status/config index 6bbf804..e897be3 100644 --- a/templates/.config/i3status/config +++ b/templates/.config/i3status/config @@ -5,7 +5,7 @@ general { ${get_base16('i3status')} -% for iface in host['wireless']: +% for iface in host.wireless: wireless ${iface} { format_up = "${iface} %ip %essid %quality" format_down = "${iface} down" @@ -13,7 +13,7 @@ wireless ${iface} { order += "wireless ${iface}" % endfor -% for iface in host['ethernet']: +% for iface in host.ethernet: ethernet ${iface} { format_up = "${iface} %ip" format_down = "${iface} down" @@ -38,7 +38,7 @@ battery all { } order += "battery all" -% for disk in host['disks']: +% for disk in host.disks: disk "${disk}" { format = "${disk} %avail" } @@ -59,10 +59,10 @@ memory { } order += "memory" -% if 'temperature-path' in host: +% if host.temperature_path: cpu_temperature 0 { format = "temp %degrees°C" - path = "${host['temperature-path']}" + path = "${host.temperature_path}" } order += "cpu_temperature 0" % endif diff --git a/templates/.config/niri/config.kdl b/templates/.config/niri/config.kdl index 441ebe0..5a9c4b9 100644 --- a/templates/.config/niri/config.kdl +++ b/templates/.config/niri/config.kdl @@ -58,17 +58,9 @@ input { // focus-follows-mouse max-scroll-amount="0%" } -% for output in host.get('outputs', []): -output "${output['match']}" { -% if 'mode' in output: - mode "${output['mode'].removesuffix('Hz')}" -% endif -% if 'scale' in output: - scale ${output['scale']} -% endif -% if 'position' in output: - position x=${output['position'][0]} y=${output['position'][1]} -% endif +% for output in host.outputs: +output "${output.match}" { +${output.niri_lines} } % endfor diff --git a/templates/.config/sway/config b/templates/.config/sway/config index 8763a40..c52f708 100644 --- a/templates/.config/sway/config +++ b/templates/.config/sway/config @@ -37,19 +37,15 @@ client.urgent $base08 $base08 $base00 $base08 $base08 client.placeholder $base01 $base01 $base05 $base01 $base01 client.background $base07 -% for output in host['outputs']: -output ${repr(output['match'])} { -% for line in output['sway-lines']: - ${line} -% endfor +% for output in host.outputs: +output ${repr(output.match)} { +${output.sway_lines} } % endfor -% for input in host['inputs']: -input ${repr(input['match'])} { -% for line in input['sway-lines']: - ${line} -% endfor +% for input in host.inputs: +input ${repr(input.match)} { +${input.sway_lines} } %endfor @@ -77,7 +73,7 @@ exec dunst exec udiskie exec fcitx5 -font pango:${host['system-mono-font']} 8 +font pango:${host.system_mono_font} 8 focus_follows_mouse no floating_modifier $mod @@ -86,14 +82,14 @@ bindsym $mod+Shift+c reload bindsym $mod+Shift+r restart bindsym $mod+Shift+e exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -b 'Yes, exit sway' 'swaymsg exit' bindsym $mod+Shift+q kill -bindsym $mod+Shift+p exec ${host['lock-cmd']} +bindsym $mod+Shift+p exec ${host.lock_cmd} # Blank individual displays bindsym $mod+o output - dpms off # Unblank all displays bindsym $mod+Shift+o output * dpms on -bindsym $mod+Return exec ${host['terminal']} +bindsym $mod+Return exec ${host.terminal} bindsym $mod+d exec wofi --show drun bindsym $mod+Shift+d exec wofi --show run bindsym $mod+p exec wofi-pass @@ -185,7 +181,7 @@ bindsym XF86AudioPlay exec playerctl play-pause bar { tray_output none status_command i3status - font pango:${host['system-mono-font']} 10 + font pango:${host.system_mono_font} 10 ${get_base16('i3', 'bar-colors')} diff --git a/templates/.config/swayidle/config b/templates/.config/swayidle/config index f681388..fc2e0a6 100644 --- a/templates/.config/swayidle/config +++ b/templates/.config/swayidle/config @@ -1 +1 @@ -timeout 10 'pgrep swaylock && ${host['display-off-cmd']}' resume 'pgrep swaylock && ${host['display-on-cmd']}' +timeout 10 'pgrep swaylock && ${host.display_off_cmd}' resume 'pgrep swaylock && ${host.display_on_cmd}' diff --git a/templates/.config/swaylock/config b/templates/.config/swaylock/config index a52a915..62ae49b 100644 --- a/templates/.config/swaylock/config +++ b/templates/.config/swaylock/config @@ -2,14 +2,7 @@ ignore-empty-password indicator-idle-visible color=000000 scaling=fill -font=${host['system-font']} +font=${host.system_font} -% for output in host['outputs']: -% if 'background' in output and (output['match'] == '*' or 'device' in output): -% if output['match'] == '*': -image=${output['background'][0]} -% else: -image=${output['device']}:${output['background'][0]} -% endif -% endif -% endfor + +${host.swaylock_images} diff --git a/templates/.config/waybar/config.jsonc b/templates/.config/waybar/config.jsonc index 0f13473..786d8cd 100644 --- a/templates/.config/waybar/config.jsonc +++ b/templates/.config/waybar/config.jsonc @@ -12,7 +12,7 @@ ], "modules-right": [ -% for iface in host.get('wireless', []) + host.get('ethernet', []): +% for iface in host.wireless + host.ethernet: "network#${iface}", % endfor @@ -21,7 +21,7 @@ "cpu", "memory", -% if 'temperature-path' in host: +% if host.temperature_path: "temperature", % endif @@ -29,7 +29,7 @@ "clock#local" ], -% for iface in host.get('wireless', []): +% for iface in host.wireless: "network#${iface}": { "interface": "${iface}", "format": "\uf1eb \uf058", @@ -39,7 +39,7 @@ }, % endfor -% for iface in host.get('ethernet', []): +% for iface in host.ethernet: "network#${iface}": { "interface": "${iface}", "format": "\uf6ff \uf058", @@ -59,14 +59,14 @@ // TODO: make drawer that expands orthogonally, outside of the bar "orientation": "inherit", "modules": [ -% for disk in host.get('disks', ['/']): +% for disk in host.disks: "disk#${disk}", % endfor ], "drawer": {} }, -% for disk in host.get('disks', ['/']): +% for disk in host.disks: "disk#${disk}": { "path": "${disk}", "format": "${disk} {free}", @@ -83,10 +83,10 @@ "tooltip-format": "{used}GiB / {total}GiB" }, -% if 'temperature-path' in host: +% if host.temperature_path: "temperature": { "format": "{temperatureC}°C", - "hwmon-path": "${host['temperature-path']}" + "hwmon-path": "${host.temperature_path}" }, % endif diff --git a/templates/.config/waybar/style.css b/templates/.config/waybar/style.css index 36733ab..477364d 100644 --- a/templates/.config/waybar/style.css +++ b/templates/.config/waybar/style.css @@ -1,6 +1,6 @@ * { /* `otf-font-awesome` is required to be installed for icons */ - font-family: '${host["system-mono-font"]}', 'Font Awesome 6 Free'; + font-family: '${host.system_mono_font}', 'Font Awesome 6 Free'; font-size: 13px; } diff --git a/templates/.config/wofi/style.css b/templates/.config/wofi/style.css index a8527ec..5b46f8a 100644 --- a/templates/.config/wofi/style.css +++ b/templates/.config/wofi/style.css @@ -1,7 +1,7 @@ window { background-color: --wofi-color1; color: --wofi-color5; - font-family: '${host['system-mono-font']}'; + font-family: '${host.system_mono_font}'; } #input { diff --git a/templates/.config/wpaperd/config.toml b/templates/.config/wpaperd/config.toml index 67ff0fd..11be19c 100644 --- a/templates/.config/wpaperd/config.toml +++ b/templates/.config/wpaperd/config.toml @@ -1,7 +1 @@ -% for output in host.get('outputs', []): -% if 'background' in output: -["${'any' if output['match'] == '*' else output['match']}"] -path = "${output['background'][0]}" -mode = "${output['background'][1].replace('fill', 'center')}" -%endif -% endfor +${host.wpaperd_config} diff --git a/templates/.ssh/config b/templates/.ssh/config index 5e80247..b310070 100644 --- a/templates/.ssh/config +++ b/templates/.ssh/config @@ -3,7 +3,7 @@ AddKeysToAgent confirm Host *.mst.edu User um-ad\ajgq56 -% if host['use-jump-host']: +% if host.use_jump_host: Host *.flock.wg ProxyJump goose@reliant.gaussian.dev % endif