Files
dotfiles/docs/superpowers/plans/2026-06-07-theme-centralization-and-bluetooth.md
funman300 1e3adbc842 feat: centralize theme palette + add waybar bluetooth module
Theme centralization:
- theme/colors.json is now the single source of truth for the palette.
- New theme/generate-theme.py renders, at install time, the three derived
  artifacts into ~/.config: colors.css (waybar/wofi @import), mako config,
  and hyprlock-colors.conf (hyprlang $tn_* variables).
- theme/colors.css removed from the repo (now generated); install.sh calls
  the generator instead of the old inline mako snippet.
- hyprlock.conf sources the generated color file and uses $tn_* variables
  instead of hardcoded rgba() values.
- Generator covered by theme/test_generate_theme.py.

Bluetooth:
- Added waybar built-in bluetooth module (config.jsonc + style.css), with
  bluez/bluez-utils in packages.txt.

Includes design spec and implementation plan under docs/superpowers/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:45:01 -07:00

19 KiB
Raw Permalink Blame History

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.jsonsource of truth (unchanged; 12 base colors).
  • theme/generate-theme.pynew generator. Pure render functions + a main() that writes outputs. One responsibility: turn the palette into config artifacts.
  • theme/test_generate_theme.pynew test for the generator's pure functions.
  • theme/colors.cssdeleted from repo (now generated into ~/.config/theme/colors.css).
  • install.shmodified: call the generator, drop the inline mako snippet and the colors.css symlink.
  • hyprlock/hyprlock.confmodified: source= the generated color file, replace hardcoded rgba(...) with $tn_* vars.
  • waybar/config.jsoncmodified: add bluetooth module.
  • waybar/style.cssmodified: add #bluetooth style rules.
  • packages.txtmodified: ensure bluez/bluez-utils present.

Two independent features (theme: Tasks 16; 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

# 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
# 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
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__):

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:

# (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
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__):

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):

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
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__):

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:

# (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:

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
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, 5054)

  • 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:

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
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 3242 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:

ln -sf "$(pwd)/theme/colors.css" ~/.config/theme/colors.css

and delete the entire inline block:

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:

echo "==> Generating theme artifacts from colors.json"
python3 "$(pwd)/theme/generate-theme.py"
  • Step 3: Delete the now-generated colors.css from the repo
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:

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
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:

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":

    "network",
    "bluetooth",
    "battery",

And add this module block (e.g. after the network block):

  "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:

#bluetooth {
  padding: 0 12px;
}

#bluetooth.connected {
  color: @tn-blue;
}

#bluetooth.disabled, #bluetooth.off {
  color: @tn-fg-muted;
}
  • Step 5: Verify the module renders

Run:

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
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.pyOK
  • 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.