feat(themes): #62 whole-swap CSS self-containment CI gate
Some checks failed
Check / eval (push) Has been cancelled
Some checks failed
Check / eval (push) Has been cancelled
Add tools/check-theme-wholeswap.py and checks.theme-wholeswap: every themes/*/waybar.css must @define-color each @name it references (raw readFile, no palette prepend — neon-glass class), and every preset needs preview.png + non-empty backgrounds/. README: four waybar.css themes (drop quarantined neon-glass). Close #62. Verified: V0 (script + negative fixture + checks.theme-wholeswap + flake check --no-build).
This commit is contained in:
111
tools/check-theme-wholeswap.py
Normal file
111
tools/check-theme-wholeswap.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Whole-swap theme asset + CSS self-containment (BACKLOG #62).
|
||||
|
||||
waybar.nix applies themes/<slug>/waybar.css *raw* — no palette @define-color
|
||||
block is prepended. A whole-swap that references @text without defining it
|
||||
is broken at apply time (neon-glass, 2026-07-09). This gate fails that class
|
||||
of bug before it ships.
|
||||
|
||||
Also asserts every themes/*.json has preview.png + non-empty backgrounds/.
|
||||
|
||||
Wired as `checks.theme-wholeswap` in flake.nix; runnable directly:
|
||||
python3 tools/check-theme-wholeswap.py themes/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# CSS at-rules that look like @name but are not GTK/CSS color names.
|
||||
AT_RULES = frozenset(
|
||||
{
|
||||
"keyframes",
|
||||
"media",
|
||||
"import",
|
||||
"supports",
|
||||
"font-face",
|
||||
"charset",
|
||||
"page",
|
||||
"define-color",
|
||||
"layer",
|
||||
"container",
|
||||
"property",
|
||||
"scope",
|
||||
}
|
||||
)
|
||||
|
||||
COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)
|
||||
DEFINE_RE = re.compile(r"@define-color\s+([A-Za-z_][\w-]*)")
|
||||
# Color uses: @name, alpha(@name, …) — same token shape as defines.
|
||||
AT_REF_RE = re.compile(r"@([A-Za-z_][\w-]*)")
|
||||
|
||||
|
||||
def strip_comments(css: str) -> str:
|
||||
return COMMENT_RE.sub("", css)
|
||||
|
||||
|
||||
def check_waybar_css(path: Path, failures: list[str]) -> None:
|
||||
raw = path.read_text(encoding="utf-8")
|
||||
css = strip_comments(raw)
|
||||
defs = set(DEFINE_RE.findall(css))
|
||||
refs = set(AT_REF_RE.findall(css)) - AT_RULES - defs
|
||||
# define-color lines also match AT_REF_RE for the rule name only via
|
||||
# @define-color — already in AT_RULES. Color names on define lines
|
||||
# appear as the capture of DEFINE_RE only when after define-color;
|
||||
# AT_REF_RE on "@define-color foo" yields "define-color", not "foo".
|
||||
# On "color: @text" yields "text". Good.
|
||||
if refs:
|
||||
missing = ", ".join(sorted(refs))
|
||||
failures.append(f"{path}: undefined color refs: {missing}")
|
||||
|
||||
|
||||
def check_assets(themes_dir: Path, failures: list[str]) -> None:
|
||||
jsons = sorted(themes_dir.glob("*.json"))
|
||||
if not jsons:
|
||||
failures.append(f"{themes_dir}: no themes/*.json found")
|
||||
return
|
||||
for jp in jsons:
|
||||
slug = jp.stem
|
||||
tdir = themes_dir / slug
|
||||
preview = tdir / "preview.png"
|
||||
if not preview.is_file():
|
||||
failures.append(f"{slug}: missing {preview.relative_to(themes_dir)}")
|
||||
bg = tdir / "backgrounds"
|
||||
if not bg.is_dir():
|
||||
failures.append(f"{slug}: missing backgrounds/")
|
||||
elif not any(bg.iterdir()):
|
||||
failures.append(f"{slug}: backgrounds/ is empty")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
print(f"usage: {sys.argv[0]} <themes-dir>", file=sys.stderr)
|
||||
return 2
|
||||
themes_dir = Path(sys.argv[1])
|
||||
if not themes_dir.is_dir():
|
||||
print(f"not a directory: {themes_dir}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
failures: list[str] = []
|
||||
check_assets(themes_dir, failures)
|
||||
for css in sorted(themes_dir.glob("*/waybar.css")):
|
||||
check_waybar_css(css, failures)
|
||||
|
||||
if failures:
|
||||
for f in failures:
|
||||
print(f"theme-wholeswap: {f}", file=sys.stderr)
|
||||
print(f"theme-wholeswap: {len(failures)} failure(s)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
n_css = len(list(themes_dir.glob("*/waybar.css")))
|
||||
n_json = len(list(themes_dir.glob("*.json")))
|
||||
print(
|
||||
f"theme-wholeswap: ok — {n_json} presets (preview+backgrounds), "
|
||||
f"{n_css} whole-swap waybar.css self-contained"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user