Files
Nomarchy/tools/check-theme-contrast.py
Bernardo Magri fd6e5f60e9
All checks were successful
Check / eval (push) Successful in 2m53s
fix(theme): generated waybar/rofi CSS invisible on on-surface palettes (item 27)
Follow-on to the swaync fix (a643391): the generated Waybar CSS colored
the module row and #window @subtext and dim states @muted, and the
generated rofi theme put @fg text on @surface chips — invisible where
palettes use those roles as "on-surface" (flexoki-light: subtext==base,
surface==text; gruvbox: muted/base = 1.27). flexoki-light has no
whole-swap, so it ran this CSS. Generated surfaces only — the summer
whole-swaps are hand-authored and untouched.

- waybar: secondary text → alpha(@text, 0.85); dim states (inactive
  workspaces, DND bell, muted volume) → alpha(@text, 0.5).
- rofi: @dim → text+"80", @surface → text+"1a" (#RRGGBBAA fg tints).
- check-theme-contrast.py pairings/docs now cover all three generated
  surfaces; status glyphs (good/warn/bad on base, worst 2.05) are
  deliberately not held to a text ratio — raising them is palette
  design, a human decision.

Verified: V0; V1 (checks.theme-contrast green over 21 themes;
downstream-template-home builds; rendered waybar style has zero
@subtext/@muted/@surface; rendered custom.rasi shows fg-derived
tints). Honesty: NOT visually verified — V3 queued (flexoki-light
eyeball + dark-theme regression spot-check).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:08:18 +01:00

71 lines
2.5 KiB
Python
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.
#!/usr/bin/env python3
"""Guard against invisible-text palettes (BACKLOG items 25 + 27).
Asserts, for every themes/*.json, the WCAG contrast ratio of each
hex-on-hex fg/bg pairing the generated swaync/waybar/rofi CSS uses.
Pairings built from alpha(@text) / #RRGGBBAA text tints are
contrast-by-construction and need no assert. Status glyph accents
(good/warn/bad on base) are deliberately NOT held to a text ratio —
several palettes sit at 2.02.7 there by design.
Wired as `checks.theme-contrast` in flake.nix; also runnable directly:
python3 tools/check-theme-contrast.py themes/
"""
import json
import sys
from pathlib import Path
# (fg role, bg role, minimum ratio, consumer). Text pairings only —
# borders (@accent/@bad on @base in swaync) are decorative and not held
# to a text ratio (miasma's bad-on-base is 2.3, fine for a border).
PAIRINGS = [
("text", "base", 4.5, "swaync body / waybar modules / rofi rows"),
# symmetric ratio also covers accent-on-base (waybar updates, rofi prompt)
("base", "accent", 3.0, "swaync button:hover / waybar active ws / rofi selected row"),
]
def channel(c: int) -> float:
c /= 255
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
def luminance(hexstr: str) -> float:
hexstr = hexstr.lstrip("#")
r, g, b = (int(hexstr[i:i + 2], 16) for i in (0, 2, 4))
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
def ratio(fg: str, bg: str) -> float:
hi, lo = sorted((luminance(fg), luminance(bg)), reverse=True)
return (hi + 0.05) / (lo + 0.05)
def main() -> int:
themes_dir = Path(sys.argv[1] if len(sys.argv) > 1 else "themes")
failures = []
themes = sorted(themes_dir.glob("*.json"))
if not themes:
print(f"no theme JSONs found under {themes_dir}", file=sys.stderr)
return 1
for path in themes:
colors = json.loads(path.read_text())["colors"]
for fg, bg, minimum, consumer in PAIRINGS:
r = ratio(colors[fg], colors[bg])
if r < minimum:
failures.append(
f"{path.stem}: {fg} on {bg} = {r:.2f} < {minimum}"
f" ({consumer}; {colors[fg]} on {colors[bg]})"
)
if failures:
print("theme contrast failures:", file=sys.stderr)
for f in failures:
print(f" {f}", file=sys.stderr)
return 1
print(f"{len(themes)} themes x {len(PAIRINGS)} pairings: all pass")
return 0
if __name__ == "__main__":
sys.exit(main())