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>
This commit is contained in:
funman300
2026-06-07 16:45:01 -07:00
parent 5305bb78c6
commit 1e3adbc842
11 changed files with 888 additions and 39 deletions
-20
View File
@@ -1,20 +0,0 @@
/* Tomorrow Night - shared color palette */
/* @import this file from waybar, wofi CSS */
@define-color tn-bg #1d1f21;
@define-color tn-bg-alt #282a2e;
@define-color tn-bg-high #373b41;
@define-color tn-fg #c5c8c6;
@define-color tn-fg-dim #969896;
@define-color tn-fg-muted #707880;
@define-color tn-blue #81a2be;
@define-color tn-green #b5bd68;
@define-color tn-yellow #f0c674;
@define-color tn-red #cc6666;
@define-color tn-magenta #b294bb;
@define-color tn-cyan #8abeb7;
/* Alpha variants */
@define-color tn-bg-a90 rgba(29, 31, 33, 0.90);
@define-color tn-bg-a95 rgba(29, 31, 33, 0.95);
@define-color tn-bg-a96 rgba(29, 31, 33, 0.96);
+85
View File
@@ -0,0 +1,85 @@
# theme/generate-theme.py
"""Generate desktop config artifacts from theme/colors.json (single source of truth)."""
import json
import os
def load_palette(path):
with open(path) as f:
return json.load(f)
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)"
# (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_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"
)
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"
# (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()
+75
View File
@@ -0,0 +1,75 @@
# 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)
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
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
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
if __name__ == "__main__":
test_load_palette_reads_colors_json()
test_hex_to_hyprlock_rgba()
test_hex_to_rgb_tuple()
test_render_colors_css()
test_render_mako_config()
test_render_hyprlock_colors()
print("OK")