Files
dotfiles/docs/superpowers/plans/2026-06-07-theme-centralization-and-bluetooth.md
T
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

550 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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**
```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, 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:
```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 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:
```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.