# Theme Centralization & Bluetooth Module Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make `theme/colors.json` the single source of truth for the desktop palette, generating all derived configs (waybar/wofi CSS, mako, hyprlock colors) from it at install time, and add a bluetooth indicator to waybar. **Architecture:** A pure-function Python generator (`theme/generate-theme.py`) reads `colors.json` and renders three artifacts into `~/.config` at install time (matching how mako is already generated). `hyprlock.conf` switches to a sourced hyprlang color file using `$tn_*` variables. The hand-maintained `theme/colors.css` is deleted and becomes a generated artifact. Bluetooth uses waybar's built-in module. **Tech Stack:** Python 3 (stdlib only: `json`, `os`), waybar (JSONC + GTK CSS), hyprlang (hyprlock config), bash (install.sh). --- ## File Structure - `theme/colors.json` — **source of truth** (unchanged; 12 base colors). - `theme/generate-theme.py` — **new** generator. Pure render functions + a `main()` that writes outputs. One responsibility: turn the palette into config artifacts. - `theme/test_generate_theme.py` — **new** test for the generator's pure functions. - `theme/colors.css` — **deleted from repo** (now generated into `~/.config/theme/colors.css`). - `install.sh` — **modified**: call the generator, drop the inline mako snippet and the colors.css symlink. - `hyprlock/hyprlock.conf` — **modified**: `source=` the generated color file, replace hardcoded `rgba(...)` with `$tn_*` vars. - `waybar/config.jsonc` — **modified**: add `bluetooth` module. - `waybar/style.css` — **modified**: add `#bluetooth` style rules. - `packages.txt` — **modified**: ensure `bluez`/`bluez-utils` present. Two independent features (theme: Tasks 1–6; bluetooth: Task 7). Implement and verify separately. --- ## Task 1: Generator — palette loading and hex conversion **Files:** - Create: `theme/generate-theme.py` - Test: `theme/test_generate_theme.py` - [ ] **Step 1: Write the failing test** ```python # theme/test_generate_theme.py import importlib.util, os _spec = importlib.util.spec_from_file_location( "gen", os.path.join(os.path.dirname(__file__), "generate-theme.py")) gen = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(gen) def test_load_palette_reads_colors_json(): pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json")) assert pal["background"] == "#1d1f21" assert pal["blue"] == "#81a2be" def test_hex_to_hyprlock_rgba(): assert gen.hex_to_hyprlock_rgba("#81a2be") == "rgba(81a2beff)" assert gen.hex_to_hyprlock_rgba("#1d1f21") == "rgba(1d1f21ff)" def test_hex_to_rgb_tuple(): assert gen.hex_to_rgb("#1d1f21") == (29, 31, 33) if __name__ == "__main__": test_load_palette_reads_colors_json() test_hex_to_hyprlock_rgba() test_hex_to_rgb_tuple() print("OK") ``` - [ ] **Step 2: Run test to verify it fails** Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py` Expected: FAIL — `FileNotFoundError` / `AttributeError: module 'gen' has no attribute 'load_palette'` (generate-theme.py does not exist yet). - [ ] **Step 3: Write minimal implementation** ```python # theme/generate-theme.py """Generate desktop config artifacts from theme/colors.json (single source of truth).""" import json import os def load_palette(path): return json.load(open(path)) def hex_to_rgb(h): h = h.lstrip("#") return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) def hex_to_hyprlock_rgba(h): return f"rgba({h.lstrip('#')}ff)" ``` - [ ] **Step 4: Run test to verify it passes** Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py` Expected: `OK` - [ ] **Step 5: Commit** ```bash git add theme/generate-theme.py theme/test_generate_theme.py git commit -m "feat(theme): generator palette loading + hex conversion" ``` --- ## Task 2: Generator — render colors.css (with alpha variants) **Files:** - Modify: `theme/generate-theme.py` - Test: `theme/test_generate_theme.py` - [ ] **Step 1: Write the failing test** Add to `theme/test_generate_theme.py` (and add the call in `__main__`): ```python def test_render_colors_css(): pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json")) css = gen.render_colors_css(pal) # base colors assert "@define-color tn-bg #1d1f21;" in css assert "@define-color tn-bg-alt #282a2e;" in css assert "@define-color tn-bg-high #373b41;" in css assert "@define-color tn-fg #c5c8c6;" in css assert "@define-color tn-fg-dim #969896;" in css assert "@define-color tn-fg-muted #707880;" in css assert "@define-color tn-blue #81a2be;" in css assert "@define-color tn-green #b5bd68;" in css assert "@define-color tn-yellow #f0c674;" in css assert "@define-color tn-red #cc6666;" in css assert "@define-color tn-magenta #b294bb;" in css assert "@define-color tn-cyan #8abeb7;" in css # alpha variants computed from background (29,31,33) assert "@define-color tn-bg-a90 rgba(29, 31, 33, 0.90);" in css assert "@define-color tn-bg-a95 rgba(29, 31, 33, 0.95);" in css assert "@define-color tn-bg-a96 rgba(29, 31, 33, 0.96);" in css ``` - [ ] **Step 2: Run test to verify it fails** Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py` Expected: FAIL — `AttributeError: module 'gen' has no attribute 'render_colors_css'` - [ ] **Step 3: Write minimal implementation** Add to `theme/generate-theme.py`: ```python # (key in colors.json) -> (css variable suffix) _CSS_MAP = [ ("background", "bg"), ("background_alt", "bg-alt"), ("background_high", "bg-high"), ("foreground", "fg"), ("foreground_dim", "fg-dim"), ("foreground_muted", "fg-muted"), ("blue", "blue"), ("green", "green"), ("yellow", "yellow"), ("red", "red"), ("magenta", "magenta"), ("cyan", "cyan"), ] _ALPHA_VARIANTS = [("a90", "0.90"), ("a95", "0.95"), ("a96", "0.96")] def render_colors_css(pal): lines = ["/* Tomorrow Night - GENERATED from colors.json by generate-theme.py. DO NOT EDIT. */", ""] for key, suffix in _CSS_MAP: lines.append(f"@define-color tn-{suffix} {pal[key]};") lines.append("") r, g, b = hex_to_rgb(pal["background"]) for name, alpha in _ALPHA_VARIANTS: lines.append(f"@define-color tn-bg-{name} rgba({r}, {g}, {b}, {alpha});") return "\n".join(lines) + "\n" ``` - [ ] **Step 4: Run test to verify it passes** Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py` Expected: `OK` - [ ] **Step 5: Commit** ```bash git add theme/generate-theme.py theme/test_generate_theme.py git commit -m "feat(theme): render colors.css from palette" ``` --- ## Task 3: Generator — render mako config **Files:** - Modify: `theme/generate-theme.py` - Test: `theme/test_generate_theme.py` - [ ] **Step 1: Write the failing test** Add to `theme/test_generate_theme.py` (and call in `__main__`): ```python def test_render_mako_config(): pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json")) cfg = gen.render_mako_config(pal) assert "background-color=#1d1f21" in cfg assert "text-color=#c5c8c6" in cfg assert "border-size=2" in cfg assert "border-color=#81a2be" in cfg assert "default-timeout=4000" in cfg ``` - [ ] **Step 2: Run test to verify it fails** Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py` Expected: FAIL — `AttributeError: module 'gen' has no attribute 'render_mako_config'` - [ ] **Step 3: Write minimal implementation** Add to `theme/generate-theme.py` (preserves the exact output of the current inline install.sh snippet): ```python def render_mako_config(pal): return ( f"background-color={pal['background']}\n" f"text-color={pal['foreground']}\n" "border-size=2\n" f"border-color={pal['blue']}\n" "default-timeout=4000\n" ) ``` - [ ] **Step 4: Run test to verify it passes** Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py` Expected: `OK` - [ ] **Step 5: Commit** ```bash git add theme/generate-theme.py theme/test_generate_theme.py git commit -m "feat(theme): render mako config from palette" ``` --- ## Task 4: Generator — render hyprlock colors + `main()` writer **Files:** - Modify: `theme/generate-theme.py` - Test: `theme/test_generate_theme.py` - [ ] **Step 1: Write the failing test** Add to `theme/test_generate_theme.py` (and call in `__main__`): ```python def test_render_hyprlock_colors(): pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json")) out = gen.render_hyprlock_colors(pal) assert "$tn_fg = rgba(c5c8c6ff)" in out assert "$tn_fg_dim = rgba(969896ff)" in out assert "$tn_bg_alt = rgba(282a2eff)" in out assert "$tn_blue = rgba(81a2beff)" in out assert "$tn_red = rgba(cc6666ff)" in out assert "$tn_green = rgba(b5bd68ff)" in out ``` - [ ] **Step 2: Run test to verify it fails** Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py` Expected: FAIL — `AttributeError: module 'gen' has no attribute 'render_hyprlock_colors'` - [ ] **Step 3: Write minimal implementation** Add to `theme/generate-theme.py`. The `_HYPRLOCK_MAP` covers exactly the colors hyprlock.conf needs: ```python # (colors.json key) -> (hyprlock variable name) _HYPRLOCK_MAP = [ ("foreground", "tn_fg"), ("foreground_dim", "tn_fg_dim"), ("background_alt", "tn_bg_alt"), ("blue", "tn_blue"), ("red", "tn_red"), ("green", "tn_green"), ] def render_hyprlock_colors(pal): lines = ["# GENERATED from colors.json by generate-theme.py. DO NOT EDIT."] for key, var in _HYPRLOCK_MAP: lines.append(f"${var} = {hex_to_hyprlock_rgba(pal[key])}") return "\n".join(lines) + "\n" def main(): here = os.path.dirname(os.path.abspath(__file__)) pal = load_palette(os.path.join(here, "colors.json")) cfgdir = os.path.expanduser("~/.config") os.makedirs(os.path.join(cfgdir, "theme"), exist_ok=True) os.makedirs(os.path.join(cfgdir, "mako"), exist_ok=True) writes = { os.path.join(cfgdir, "theme", "colors.css"): render_colors_css(pal), os.path.join(cfgdir, "mako", "config"): render_mako_config(pal), os.path.join(cfgdir, "theme", "hyprlock-colors.conf"): render_hyprlock_colors(pal), } for path, content in writes.items(): with open(path, "w") as f: f.write(content) print(f" generated {path}") if __name__ == "__main__": main() ``` - [ ] **Step 4: Run test to verify it passes** Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py` Expected: `OK` - [ ] **Step 5: Verify the writer produces a colors.css matching the current one** Run: ```bash cd ~/Documents/dotfiles && python3 theme/generate-theme.py diff <(grep '@define-color' theme/colors.css) <(grep '@define-color' ~/.config/theme/colors.css) ``` Expected: no differences (the generated palette matches the committed hand-written one). If `~/.config/theme/colors.css` was a symlink to the repo file, `diff` shows nothing because they are the same file — that is fine; the symlink is removed in Task 6. - [ ] **Step 6: Commit** ```bash git add theme/generate-theme.py theme/test_generate_theme.py git commit -m "feat(theme): render hyprlock colors + main() writer" ``` --- ## Task 5: Convert hyprlock.conf to the shared palette **Files:** - Modify: `hyprlock/hyprlock.conf` (add `source=` line; replace 7 `rgba(...)` at lines 27, 38, 50–54) - [ ] **Step 1: Add the source line at the top of `hyprlock/hyprlock.conf`** Insert as the very first line (before `general {`): ``` source = ~/.config/theme/hyprlock-colors.conf ``` - [ ] **Step 2: Replace the hardcoded colors with variables** Apply these exact replacements in `hyprlock/hyprlock.conf`: | Line context | From | To | |---|---|---| | label (clock) | `color = rgba(c5c8c6ff)` | `color = $tn_fg` | | label (date) | `color = rgba(969896ff)` | `color = $tn_fg_dim` | | input-field | `inner_color = rgba(282a2eff)` | `inner_color = $tn_bg_alt` | | input-field | `outer_color = rgba(81a2beff)` | `outer_color = $tn_blue` | | input-field | `font_color = rgba(c5c8c6ff)` | `font_color = $tn_fg` | | input-field | `fail_color = rgba(cc6666ff)` | `fail_color = $tn_red` | | input-field | `check_color = rgba(b5bd68ff)` | `check_color = $tn_green` | - [ ] **Step 3: Generate the hyprlock color file and parse-test hyprlock** Run: ```bash cd ~/Documents/dotfiles && python3 theme/generate-theme.py cat ~/.config/theme/hyprlock-colors.conf # Parse test: launch with no grace, watch for parse errors, kill after 2s before interacting. timeout 2 hyprlock --verbose 2>&1 | grep -iE 'error|invalid|fail to parse|unknown' || echo "NO PARSE ERRORS" ``` Expected: the color file lists the 6 `$tn_*` variables; the parse test prints `NO PARSE ERRORS`. (hyprlock will lock the screen for ~2s — unlock with your password/fingerprint after `timeout` kills it. Do this when you can attend the screen.) - [ ] **Step 4: FALLBACK (only if Step 3 shows parse errors for `source=` or `$variables`)** If hyprlock v0.9.5 rejects `source=`/variables, abandon the source approach for hyprlock: 1. Revert `hyprlock/hyprlock.conf` changes: `git checkout hyprlock/hyprlock.conf` 2. Rename it to a template: `git mv hyprlock/hyprlock.conf hyprlock/hyprlock.conf.tmpl` 3. In `hyprlock.conf.tmpl`, replace the 7 `rgba(...)` values with `__TN_FG__`, `__TN_FG_DIM__`, `__TN_BG_ALT__`, `__TN_BLUE__`, `__TN_RED__`, `__TN_GREEN__` placeholders. 4. Add a `render_hyprlock_conf(pal, template_str)` function to the generator that `.replace()`s each placeholder with `hex_to_hyprlock_rgba(pal[key])`, plus a test asserting no `__` placeholders remain and `rgba(c5c8c6ff)` is present. 5. In `main()`, read the template and write the rendered file to `~/.config/hypr/hyprlock.conf`. 6. In `install.sh` (Task 6), remove the `ln -sf .../hyprlock/hyprlock.conf` symlink (now generated). Document in the commit message which path was taken. - [ ] **Step 5: Commit** ```bash git add hyprlock/hyprlock.conf git commit -m "feat(theme): hyprlock uses shared palette via sourced variables" ``` --- ## Task 6: Wire generator into install.sh; remove colors.css from repo **Files:** - Modify: `install.sh` (replace lines 32–42 inline python; remove line 29 colors.css symlink) - Delete: `theme/colors.css` - [ ] **Step 1: Remove the inline mako python block and the colors.css symlink** In `install.sh`, delete the colors.css symlink line: ```bash ln -sf "$(pwd)/theme/colors.css" ~/.config/theme/colors.css ``` and delete the entire inline block: ```bash python3 - <<'PYEOF' import json, os c = json.load(open("theme/colors.json")) config = f"""background-color={c['background']} text-color={c['foreground']} border-size=2 border-color={c['blue']} default-timeout=4000 """ open(os.path.expanduser("~/.config/mako/config"), "w").write(config) PYEOF ``` - [ ] **Step 2: Add the generator call** In `install.sh`, after the `mkdir -p ~/.config/theme` line (and after the wofi/waybar symlinks), add: ```bash echo "==> Generating theme artifacts from colors.json" python3 "$(pwd)/theme/generate-theme.py" ``` - [ ] **Step 3: Delete the now-generated colors.css from the repo** ```bash cd ~/Documents/dotfiles && git rm theme/colors.css # Remove any stale symlink so generation writes a real file: rm -f ~/.config/theme/colors.css ``` - [ ] **Step 4: Run install.sh's theming path and verify all artifacts regenerate** Run: ```bash cd ~/Documents/dotfiles && python3 theme/generate-theme.py test -f ~/.config/theme/colors.css && echo "colors.css OK" test -f ~/.config/mako/config && echo "mako OK" test -f ~/.config/theme/hyprlock-colors.conf && echo "hyprlock-colors OK" grep -q '@define-color tn-blue #81a2be;' ~/.config/theme/colors.css && echo "palette OK" waybar-restart ``` Expected: all four `OK` lines print, and waybar restarts with colors intact (no GTK CSS parse errors in its output). - [ ] **Step 5: Commit** ```bash git add install.sh git rm --cached theme/colors.css 2>/dev/null || true git commit -m "build(theme): generate all theme artifacts from colors.json in install.sh" ``` --- ## Task 7: Bluetooth waybar module **Files:** - Modify: `waybar/config.jsonc` (add `bluetooth` to `modules-right`; add module block) - Modify: `waybar/style.css` (add `#bluetooth` rules) - Modify: `packages.txt` (ensure `bluez`, `bluez-utils`) - [ ] **Step 1: Ensure bluez packages are present** In `packages.txt`, confirm/add (alongside the existing `blueman`): ``` bluez bluez-utils ``` - [ ] **Step 2: Ensure the bluetooth service is running (one-time, host setup)** Run: ```bash systemctl is-enabled bluetooth.service 2>/dev/null || sudo systemctl enable --now bluetooth.service systemctl is-active bluetooth.service ``` Expected: `active`. (The waybar module reads adapter state from the bluez daemon.) - [ ] **Step 3: Add the bluetooth module to waybar config** In `waybar/config.jsonc`, add `"bluetooth"` to `modules-right` immediately after `"network"`: ```jsonc "network", "bluetooth", "battery", ``` And add this module block (e.g. after the `network` block): ```jsonc "bluetooth": { "format": " ", "format-disabled": "", "format-off": "", "format-connected": " {num_connections}", "tooltip-format": "{controller_alias}\n{status}", "tooltip-format-connected": "{controller_alias}\n{num_connections} connected\n{device_enumerate}", "tooltip-format-enumerate-connected": "{device_alias}", "on-click": "blueman-manager", }, ``` - [ ] **Step 4: Add bluetooth styling** In `waybar/style.css`, add `#bluetooth` to the shared padding selector list and add state colors: ```css #bluetooth { padding: 0 12px; } #bluetooth.connected { color: @tn-blue; } #bluetooth.disabled, #bluetooth.off { color: @tn-fg-muted; } ``` - [ ] **Step 5: Verify the module renders** Run: ```bash waybar-restart ``` Expected: a bluetooth glyph appears between network and battery; clicking it opens `blueman-manager`; toggling bluetooth changes the glyph/color. No waybar errors in output. - [ ] **Step 6: Commit** ```bash git add waybar/config.jsonc waybar/style.css packages.txt git commit -m "feat(waybar): add bluetooth module" ``` --- ## Final Verification - [ ] Run the generator test suite: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py` → `OK` - [ ] Re-run generation and confirm idempotency: `python3 theme/generate-theme.py` twice → identical files, no errors. - [ ] `waybar-restart` → bar renders with correct colors + bluetooth module. - [ ] Open wofi (Mod+D) → styling intact. - [ ] Trigger a notification → mako uses palette colors. - [ ] Lock screen (Mod+Shift+E) → hyprlock colors correct; unlock works. - [ ] `grep -rn 'rgba(' hyprlock/hyprlock.conf` → no hardcoded colors remain (only `$tn_*` vars), unless the Task 5 fallback template path was taken. - [ ] Confirm `theme/colors.css` no longer exists in the repo: `git ls-files theme/colors.css` → empty.