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:
@@ -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) |
|
| `waybar.jsonc` | whole-swap for the bar *layout* (must be plain JSON) |
|
||||||
| `rofi.rasi` | **whole-swap**: replaces the generated launcher/menu theme entirely |
|
| `rofi.rasi` | **whole-swap**: replaces the generated launcher/menu theme entirely |
|
||||||
|
|
||||||
Five themes ship a `waybar.css` identity (summer-day, summer-night,
|
Four themes ship a `waybar.css` identity (summer-day, summer-night,
|
||||||
executive-slate, boreal, neon-glass). Custom user themes can live in
|
executive-slate, boreal). Custom user themes can live in
|
||||||
`$NOMARCHY_PATH/themes/` (preset lookup) and `nomarchy.themesDir` (eval-time
|
`$NOMARCHY_PATH/themes/` (preset lookup) and `nomarchy.themesDir` (eval-time
|
||||||
asset probe).
|
asset probe).
|
||||||
|
|
||||||
|
|||||||
@@ -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 —
|
small multi-file; `[blocked:hw]` only to confirm on non-BAT* hardware —
|
||||||
logic still ships at V1.
|
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)
|
### 63. Generated Waybar: identity-bar affordances (powermenu + logo)
|
||||||
Whole-swap bars ship `custom/powermenu` and a left logo → main menu; the
|
Whole-swap bars ship `custom/powermenu` and a left logo → main menu; the
|
||||||
generated `waybar.nix` bar has neither. Optional parity add for
|
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;
|
executive-slate, and the generated style have. Functional OK;
|
||||||
visual hierarchy weak. Cost: CSS pass; V3.
|
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)
|
### Per-theme design audit (2026-07-09)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
## 2026-07-10 — #61 Review Source line tracks offline/online
|
||||||
- **Task:** NEXT #61 — Review panel Source must match the cache.nixos.org
|
- **Task:** NEXT #61 — Review panel Source must match the cache.nixos.org
|
||||||
probe (`OFFLINE`), not always claim “no network needed”.
|
probe (`OFFLINE`), not always claim “no network needed”.
|
||||||
|
|||||||
11
flake.nix
11
flake.nix
@@ -155,6 +155,17 @@
|
|||||||
touch $out
|
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.*`
|
# The README option tables must track the live `nomarchy.*`
|
||||||
# surface. The surface is walked at eval time from the four
|
# surface. The surface is walked at eval time from the four
|
||||||
# option-declaring modules (their lazy `config` halves stay
|
# option-declaring modules (their lazy `config` halves stay
|
||||||
|
|||||||
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