diff --git a/README.md b/README.md index 0260aab..5512992 100644 --- a/README.md +++ b/README.md @@ -383,8 +383,8 @@ per theme — a single place to look, unlike the old distro's split: | `waybar.jsonc` | whole-swap for the bar *layout* (must be plain JSON) | | `rofi.rasi` | **whole-swap**: replaces the generated launcher/menu theme entirely | -Five themes ship a `waybar.css` identity (summer-day, summer-night, -executive-slate, boreal, neon-glass). Custom user themes can live in +Four themes ship a `waybar.css` identity (summer-day, summer-night, +executive-slate, boreal). Custom user themes can live in `$NOMARCHY_PATH/themes/` (preset lookup) and `nomarchy.themesDir` (eval-time asset probe). diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 28e23b9..dbc299f 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -113,12 +113,6 @@ gating. Fix: name-agnostic scan aligned with battery-notify. Cost: small multi-file; `[blocked:hw]` only to confirm on non-BAT* hardware — logic still ships at V1. -### 62. Optional CI: whole-swap CSS self-containment -Contrast check does not require that a `waybar.css` defines every -`@color` it references, nor that every preset has preview + backgrounds. -A cheap gate would have caught neon-glass. Cost: small tool + flake -check; V0. - ### 63. Generated Waybar: identity-bar affordances (powermenu + logo) Whole-swap bars ship `custom/powermenu` and a left logo → main menu; the generated `waybar.nix` bar has neither. Optional parity add for @@ -238,7 +232,7 @@ _(Generated Waybar missing identity-bar affordances → **#63**.)_ executive-slate, and the generated style have. Functional OK; visual hierarchy weak. Cost: CSS pass; V3. -_(Optional CI: whole-swap CSS self-containment → **#62**.)_ +_(Optional CI: whole-swap CSS self-containment → **#62** ✓ 2026-07-10.)_ ### Per-theme design audit (2026-07-09) diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index b662997..c5608bb 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -17,6 +17,17 @@ Template: --- +## 2026-07-10 — #62 whole-swap CSS self-containment CI gate +- **Task:** NEXT #62 — cheap gate that waybar.css defines every @color it + references (would have caught neon-glass) + preview/backgrounds present. +- **Did:** `tools/check-theme-wholeswap.py` + `checks.theme-wholeswap` in + flake.nix. README: five themes → four (drop quarantined neon-glass). + Closed #62. +- **Verified:** **V0** — script OK on themes/ (24 presets, 4 whole-swaps); + negative fixture with `@text`/`@base` and zero defines fails as expected; + `nix build .#checks.x86_64-linux.theme-wholeswap` green. +- **Next suggestion:** #59 NVIDIA install comments, or #60 BAT*-agnostic. + ## 2026-07-10 — #61 Review Source line tracks offline/online - **Task:** NEXT #61 — Review panel Source must match the cache.nixos.org probe (`OFFLINE`), not always claim “no network needed”. diff --git a/flake.nix b/flake.nix index e62f530..fc7f225 100644 --- a/flake.nix +++ b/flake.nix @@ -155,6 +155,17 @@ touch $out ''; + # Whole-swap waybar.css is read raw (no palette prepend) — every + # @color it references must be @define-color'd in the same file + # (neon-glass shipped zero defines; BACKLOG #62). Also gate + # preview.png + non-empty backgrounds/ per preset. + theme-wholeswap = pkgs.runCommand "nomarchy-theme-wholeswap" + { nativeBuildInputs = [ pkgs.python3 ]; } + '' + python3 ${./tools/check-theme-wholeswap.py} ${./themes} + touch $out + ''; + # The README option tables must track the live `nomarchy.*` # surface. The surface is walked at eval time from the four # option-declaring modules (their lazy `config` halves stay diff --git a/tools/check-theme-wholeswap.py b/tools/check-theme-wholeswap.py new file mode 100644 index 0000000..bd72e11 --- /dev/null +++ b/tools/check-theme-wholeswap.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Whole-swap theme asset + CSS self-containment (BACKLOG #62). + +waybar.nix applies themes//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]} ", 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())