diff options
| -rw-r--r-- | docs/prototypes/gen_tokens.py | 180 | ||||
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 109 | ||||
| -rw-r--r-- | docs/prototypes/tokens-waybar.css | 34 | ||||
| -rw-r--r-- | docs/prototypes/tokens.el | 41 | ||||
| -rw-r--r-- | docs/prototypes/tokens.json | 46 | ||||
| -rw-r--r-- | tests/gallery-tokens/test_gen_tokens.py | 268 |
6 files changed, 638 insertions, 40 deletions
diff --git a/docs/prototypes/gen_tokens.py b/docs/prototypes/gen_tokens.py new file mode 100644 index 0000000..f966451 --- /dev/null +++ b/docs/prototypes/gen_tokens.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Single-source design-token generator for the panel widget gallery. + +tokens.json is the neutral source of truth for the gallery's design tokens +(palette, the amber family, glows, font, timing). This script reads it and +emits three target representations so the same look and feel travels across +every place the widgets get built: + + web CSS :root { --gold:#e2a038; --glow-hi:255,190,84; ... } + waybar GTK @define-color gold #e2a038; (GTK has no custom properties) + Emacs (defconst gallery-tokens '((gold . "#e2a038") ...)) for svg.el + +The three differ on purpose, which is the whole reason a generator earns its +keep instead of hand-maintaining three copies: + + - CSS custom props are hyphenated (--glow-hi) and store glow colors as bare + "r,g,b" triples so rgba(var(--glow-hi),.5) resolves with a variable alpha. + - GTK CSS has no custom properties; it uses @define-color with underscore + names and resolves glows to their source hex (GTK does alpha(@color,a)). + - Elisp uses a hyphenated-symbol alist of hex strings, which svg.el / + librsvg consume directly (a bare triple is not a color there). + +The amber hue is defined once, in tokens.json. Both its solid form (--gold) +and its glow form (--glow-hi, the rgb triple) are derived here, so retuning +the amber is a one-line edit to the source plus a regenerate. + +Usage: + python3 gen_tokens.py # regenerate all three targets in place + +The web CSS is written into panel-widget-gallery.html between the +`/* @tokens:start */` and `/* @tokens:end */` markers; the waybar and elisp +targets are written to tokens-waybar.css and tokens.el beside it. +""" + +import json +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +TOKENS_START = "/* @tokens:start */" +TOKENS_END = "/* @tokens:end */" + +# sections whose values are plain colors (searched by resolve_color, and the +# ones that become @define-color / alist color entries) +COLOR_SECTIONS = ("palette", "amber") + + +def hex_to_triple(h): + """'#ffbe54' -> '255,190,84'. Accepts 3- or 6-digit hex, '#' optional.""" + h = h.lstrip("#") + if len(h) == 3: + h = "".join(c * 2 for c in h) + if len(h) != 6: + raise ValueError(f"expected 3- or 6-digit hex, got {h!r}") + try: + r, g, b = (int(h[i:i + 2], 16) for i in (0, 2, 4)) + except ValueError: + raise ValueError(f"non-hex digits in {h!r}") + return f"{r},{g},{b}" + + +def resolve_color(tokens, key): + """Return the hex for a color named in palette or amber; KeyError if absent.""" + for section in COLOR_SECTIONS: + if key in tokens.get(section, {}): + return tokens[section][key] + raise KeyError(key) + + +def _el_str(v): + """Quote a value as an elisp string literal, escaping backslashes/quotes.""" + return '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"' + + +def emit_web_css(tokens): + """The inner :root block: hyphenated --vars, glows as rgb triples.""" + lines = [] + for section in COLOR_SECTIONS: + for k, v in tokens.get(section, {}).items(): + lines.append(f"--{k}:{v};") + for name, source in tokens.get("glow", {}).items(): + lines.append(f"--{name}:{hex_to_triple(resolve_color(tokens, source))};") + for k, v in tokens.get("font", {}).items(): + lines.append(f"--{k}:{v};") + for k, v in tokens.get("timing", {}).items(): + lines.append(f"--{k}:{v};") + return "\n".join(" " + line for line in lines) + + +def emit_waybar_gtk(tokens): + """GTK @define-color declarations; underscore names, glows resolved to hex.""" + def gtk(name): + return name.replace("-", "_") + + lines = [ + "/* generated from tokens.json by gen_tokens.py — do not edit by hand.", + " GTK CSS has no custom properties; reference these as @name, and use", + " alpha(@name, 0.5) where the web build uses rgba(var(--name),.5). */", + ] + for section in COLOR_SECTIONS: + for k, v in tokens.get(section, {}).items(): + lines.append(f"@define-color {gtk(k)} {v};") + for name, source in tokens.get("glow", {}).items(): + lines.append(f"@define-color {gtk(name)} {resolve_color(tokens, source)};") + return "\n".join(lines) + + +def emit_elisp(tokens): + """An alist of (name . hex) plus timing, for the future svg.el renderer.""" + pairs = [] + for section in COLOR_SECTIONS: + for k, v in tokens.get(section, {}).items(): + pairs.append(f"({k} . {_el_str(v)})") + for name, source in tokens.get("glow", {}).items(): + pairs.append(f"({name} . {_el_str(resolve_color(tokens, source))})") + for k, v in tokens.get("font", {}).items(): + pairs.append(f"({k} . {_el_str(v)})") + for k, v in tokens.get("timing", {}).items(): + pairs.append(f"({k} . {_el_str(v)})") + body = "\n ".join(pairs) + return ( + ";;; gallery-tokens.el --- generated from tokens.json -*- lexical-binding: t; -*-\n" + ";;; Commentary:\n" + ";; Generated by gen_tokens.py — do not edit by hand; edit tokens.json.\n" + ";;; Code:\n" + "(defconst gallery-tokens\n '(" + body + ")\n" + ' "Design tokens for the panel widget gallery.")\n' + "(provide 'gallery-tokens)\n" + ";;; gallery-tokens.el ends here\n" + ) + + +def replace_between_markers(text, start, end, block): + """Replace the text between the start and end marker lines with block. + + The marker lines themselves survive; only their interior is swapped. + Idempotent: re-running with the same block is a no-op. Raises ValueError + if either marker is missing or out of order. + """ + si = text.find(start) + ei = text.find(end) + if si == -1 or ei == -1 or ei < si: + raise ValueError("token markers not found (or out of order)") + start_line_end = text.find("\n", si) + if start_line_end == -1: + start_line_end = si + len(start) + end_line_start = text.rfind("\n", 0, ei) + 1 + return text[:start_line_end + 1] + block + "\n" + text[end_line_start:] + + +def main(tokens_path=None, html_path=None, waybar_path=None, elisp_path=None): + """Regenerate all three targets from tokens.json.""" + tokens_path = tokens_path or os.path.join(HERE, "tokens.json") + html_path = html_path or os.path.join(HERE, "panel-widget-gallery.html") + waybar_path = waybar_path or os.path.join(HERE, "tokens-waybar.css") + elisp_path = elisp_path or os.path.join(HERE, "tokens.el") + + with open(tokens_path) as f: + tokens = json.load(f) + + note = (" /* generated from tokens.json by gen_tokens.py — " + "edit tokens.json, then run: python3 gen_tokens.py */") + block = note + "\n" + emit_web_css(tokens) + + with open(html_path) as f: + html = f.read() + html = replace_between_markers(html, TOKENS_START, TOKENS_END, block) + with open(html_path, "w") as f: + f.write(html) + + with open(waybar_path, "w") as f: + f.write(emit_waybar_gtk(tokens) + "\n") + with open(elisp_path, "w") as f: + f.write(emit_elisp(tokens)) + + return {"html": html_path, "waybar": waybar_path, "elisp": elisp_path} + + +if __name__ == "__main__": # pragma: no cover + out = main() + print("regenerated:", ", ".join(f"{k}={v}" for k, v in out.items())) diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 5f1dcf3..1d95488 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -6,13 +6,42 @@ <title>Panel widget gallery — dupre instrument console (interactive)</title> <style> :root{ - --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917; - --gold:#e2a038; --gold-hi:#ffbe54; --silver:#bfc4d0; --cream:#f3e7c5; - --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d; - --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d; +/* @tokens:start */ + /* generated from tokens.json by gen_tokens.py — edit tokens.json, then run: python3 gen_tokens.py */ + --ground:#151311; + --panel:#100f0f; + --well:#0a0c0d; + --raise:#1a1917; + --silver:#bfc4d0; + --cream:#f3e7c5; + --steel:#969385; + --dim:#7c838a; + --slate:#424f5e; + --slate-hi:#54677d; + --wash:#2c2f32; + --pass:#74932f; + --fail:#cb6b4d; + --phos:#7fe0a0; + --phos-dim:#1c3626; + --vfd:#63e6c8; + --sevgrn:#57d357; + --sevred:#e2543f; + --sevoff:#1a1613; + --jewel-r:#ff5b45; + --jewel-a:#ffb43a; + --jewel-g:#6fce33; + --gold:#e2a038; + --gold-hi:#ffbe54; + --amber-grad-top:#f2c76a; + --amber-grad-mid:#d29638; + --amber-grad-bot:#8f671f; + --amber-edge:#7d5c16; + --amber-warn:#f0b552; + --glow-hi:255,190,84; + --glow-lo:226,160,56; --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace; - --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8; --sevgrn:#57d357; --sevred:#e2543f; - --sevoff:#1a1613; --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33; + --pulse-rate:1s; +/* @tokens:end */ } *{box-sizing:border-box;margin:0;padding:0} html{background:var(--ground)} @@ -43,10 +72,10 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} /* ---- shared primitives ---- */ .lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);box-shadow:0 0 6px 1px rgba(116,147,47,.55)} -.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(226,160,56,.6)} +.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(var(--glow-lo),.6)} .lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)} .lamp.off{background:var(--wash);box-shadow:none} -.lamp.busy{background:var(--gold);animation:pulse 1s ease-in-out infinite} +.lamp.busy{background:var(--gold);animation:pulse var(--pulse-rate) ease-in-out infinite} /* standard pulse: 1s ease-in-out — the norm for every pulsing / flashing element */ @keyframes pulse{50%{opacity:.25}} @@ -64,7 +93,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} border-radius:8px;padding:8px 12px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)} .key:hover{color:var(--gold);border-color:var(--gold)} .key:active{transform:translateY(1px)} -.key.on{color:var(--panel);background:linear-gradient(180deg,#f2c76a,var(--gold));border-color:var(--gold-hi);font-weight:700} +.key.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));border-color:var(--gold-hi);font-weight:700} .key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)} .key.off{opacity:.4} @@ -78,22 +107,22 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} /* fader */ .fader{width:150px;height:16px;position:relative;cursor:pointer;touch-action:none} .fader .slot{position:absolute;top:6px;left:0;right:0;height:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden} -.fader .fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,#8f671f,var(--gold))} +.fader .fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,var(--amber-grad-bot),var(--gold))} .fader .cap{position:absolute;top:1px;width:7px;height:14px;border-radius:2px;margin-left:-3.5px; - background:linear-gradient(180deg,#f2c76a,#d29638);border:1px solid #7d5c16;box-shadow:0 1px 2px rgba(0,0,0,.5)} + background:linear-gradient(180deg,var(--amber-grad-top),var(--amber-grad-mid));border:1px solid var(--amber-edge);box-shadow:0 1px 2px rgba(0,0,0,.5)} /* vertical fader */ .vfader{width:16px;height:64px;position:relative;cursor:pointer;touch-action:none} .vfader .slot{position:absolute;left:6px;top:0;bottom:0;width:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden} -.vfader .fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,#8f671f,var(--gold))} +.vfader .fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,var(--amber-grad-bot),var(--gold))} .vfader .cap{position:absolute;left:1px;height:7px;width:14px;border-radius:2px;margin-top:-3.5px; - background:linear-gradient(90deg,#d29638,#f2c76a);border:1px solid #7d5c16} + background:linear-gradient(90deg,var(--amber-grad-mid),var(--amber-grad-top));border:1px solid var(--amber-edge)} /* rotary knob */ .knob{width:52px;height:52px;border-radius:50%;position:relative;cursor:ns-resize;touch-action:none; background:radial-gradient(circle at 40% 35%,#2a2622,#141210);border:1px solid #3a352c; box-shadow:inset 0 2px 3px rgba(255,255,255,.05),0 3px 6px rgba(0,0,0,.5)} .knob .ind{position:absolute;left:50%;top:5px;width:2px;height:16px;background:var(--gold-hi); - margin-left:-1px;transform-origin:50% 21px;border-radius:1px;box-shadow:0 0 5px rgba(255,190,84,.6)} + margin-left:-1px;transform-origin:50% 21px;border-radius:1px;box-shadow:0 0 5px rgba(var(--glow-hi),.6)} /* needle gauge */ .gauge{width:96px;cursor:ns-resize;touch-action:none} @@ -101,7 +130,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .gauge .arc{position:absolute;inset:0 0 -48px 0;border:2px solid var(--wash);border-radius:50%} .gauge .tk{position:absolute;left:50%;bottom:0;width:1.5px;height:8px;background:var(--steel);transform-origin:50% 48px} .gauge .ndl{position:absolute;left:50%;bottom:0;width:2px;height:40px;background:var(--gold-hi); - transform-origin:50% 100%;transform:rotate(0deg);border-radius:2px;box-shadow:0 0 6px rgba(255,190,84,.5); + transform-origin:50% 100%;transform:rotate(0deg);border-radius:2px;box-shadow:0 0 6px rgba(var(--glow-hi),.5); transition:transform .12s cubic-bezier(.3,1.3,.5,1)} .gauge .hub{position:absolute;left:50%;bottom:-4px;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--gold)} .gauge .gv{color:var(--cream);text-align:center;font-size:12px;font-weight:700;margin-top:5px;font-variant-numeric:tabular-nums} @@ -129,7 +158,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} /* linear progress / fuel bar */ .bar{width:160px;height:12px;background:#0d0f10;border:1px solid #231f18;border-radius:6px;overflow:hidden;position:relative;cursor:pointer;touch-action:none} -.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,#8f671f,var(--gold));border-radius:6px} +.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,var(--amber-grad-bot),var(--gold));border-radius:6px} .bar.warn>span{background:linear-gradient(90deg,#a35a3f,var(--fail))} /* radial ring */ @@ -163,7 +192,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden} .seg button{font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 11px;cursor:pointer} .seg button:last-child{border-right:0} -.seg button.on{background:linear-gradient(180deg,#f2c76a,var(--gold));color:var(--panel);font-weight:700} +.seg button.on{background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));color:var(--panel);font-weight:700} /* engraved section label */ .engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer; @@ -189,15 +218,15 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .rotsel{position:relative;width:118px;height:74px} .rotsel>.knob{position:absolute;left:50%;top:20px;margin-left:-26px;cursor:pointer} .rotsel .pos{position:absolute;font-size:9px;color:var(--dim);transform:translate(-50%,-50%);letter-spacing:.02em} -.rotsel .pos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(255,190,84,.55)} +.rotsel .pos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.55)} /* slide-rule tuner dial */ .tuner{width:180px;height:46px;position:relative;border-radius:6px;overflow:hidden;cursor:pointer; background:linear-gradient(180deg,#191510,#0b0908);border:1px solid #2a251c; - box-shadow:inset 0 0 20px rgba(226,160,56,.12),inset 0 1px 0 rgba(255,255,255,.03)} + box-shadow:inset 0 0 20px rgba(var(--glow-lo),.12),inset 0 1px 0 rgba(255,255,255,.03)} .tuner .tick{position:absolute;top:6px;width:1px;height:11px;background:var(--steel);transform:translateX(-50%)} .tuner .mk{position:absolute;bottom:8px;transform:translateX(-50%);color:var(--steel);font-size:10px} -.tuner .mk.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(255,190,84,.6)} +.tuner .mk.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.6)} .tuner:focus-visible{outline:2px solid var(--gold);outline-offset:2px} .tuner .ndl{position:absolute;top:3px;bottom:3px;width:2px;margin-left:-1px;border-radius:1px; background:var(--fail);box-shadow:0 0 7px rgba(203,107,77,.85);transition:left .25s} @@ -222,7 +251,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} font-size:12px;letter-spacing:.06em;color:var(--dim)} .rocker .top{top:0;border-bottom:1px solid #0c0b0a;background:linear-gradient(180deg,#211d19,#181513)} .rocker .bot{bottom:0;background:linear-gradient(180deg,#141210,#100e0c);box-shadow:inset 0 3px 5px rgba(0,0,0,.55)} -.rocker.on .top{background:linear-gradient(180deg,#f2c76a,var(--gold));color:var(--panel);font-weight:700;box-shadow:0 0 10px rgba(255,190,84,.35)} +.rocker.on .top{background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));color:var(--panel);font-weight:700;box-shadow:0 0 10px rgba(var(--glow-hi),.35)} .rocker.on .bot{color:var(--steel)} .rocker:not(.on) .top{color:var(--steel)} .rocker:not(.on) .bot{background:linear-gradient(180deg,#2a1512,#1c0f0d);color:var(--fail);box-shadow:inset 0 3px 5px rgba(0,0,0,.55),0 0 8px rgba(203,107,77,.25)} @@ -233,13 +262,13 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} background:radial-gradient(circle at 45% 40%,#211d18,#0d0b09)} .reel::before{content:"";position:absolute;inset:9px;border-radius:50%;border:1px solid #4a443a;background:#161310} .reel i{position:absolute;left:50%;top:50%;width:2px;height:11px;background:var(--steel);margin:-5.5px 0 0 -1px;transform-origin:50% 5.5px} -.reel i:nth-child(1){background:var(--gold-hi);box-shadow:0 0 4px rgba(255,190,84,.55)} +.reel i:nth-child(1){background:var(--gold-hi);box-shadow:0 0 4px rgba(var(--glow-hi),.55)} .reel i:nth-child(2){transform:rotate(120deg)}.reel i:nth-child(3){transform:rotate(240deg)} .transport{display:flex;gap:5px} .tbtn{font:inherit;font-size:12px;color:var(--silver);cursor:pointer;width:30px;height:26px;display:grid;place-items:center; background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a;border-radius:6px} .tbtn:hover{color:var(--gold);border-color:var(--gold)} -.tbtn.on{color:var(--panel);background:linear-gradient(180deg,#f2c76a,var(--gold));border-color:var(--gold-hi)} +.tbtn.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));border-color:var(--gold-hi)} .tbtn.rec.on{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)} /* radio bank */ @@ -249,7 +278,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} background:linear-gradient(180deg,#211d19,#161310);box-shadow:inset 0 1px 0 rgba(255,255,255,.04)} .preset+.preset{margin-left:3px} .preset:hover{color:var(--silver)} -.preset.on{color:var(--panel);background:linear-gradient(180deg,#f2c76a,var(--gold));font-weight:700; +.preset.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));font-weight:700; box-shadow:inset 0 2px 4px rgba(0,0,0,.4)} /* concentric dual knob */ @@ -261,13 +290,13 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .dualknob .inner{width:34px;height:34px;border-radius:50%;position:absolute;left:15px;top:15px;cursor:ns-resize;touch-action:none; background:radial-gradient(circle at 40% 35%,#37322a,#1a1713);border:1px solid #4a443a;box-shadow:0 2px 4px rgba(0,0,0,.5)} .dualknob .inner .ind{position:absolute;left:50%;top:3px;width:2px;height:11px;background:var(--gold-hi);margin-left:-1px; - transform-origin:50% 14px;border-radius:1px;box-shadow:0 0 5px rgba(255,190,84,.6)} + transform-origin:50% 14px;border-radius:1px;box-shadow:0 0 5px rgba(var(--glow-hi),.6)} /* rotary encoder + LED ring */ .encoder{position:relative;width:66px;height:66px;display:grid;place-items:center;cursor:ns-resize;touch-action:none} .encoder .led{position:absolute;width:5px;height:5px;border-radius:50%;background:var(--wash); left:50%;top:50%;margin:-2.5px} -.encoder .led.on{background:var(--gold);box-shadow:0 0 5px 1px rgba(226,160,56,.7)} +.encoder .led.on{background:var(--gold);box-shadow:0 0 5px 1px rgba(var(--glow-lo),.7)} .encoder .knob{width:40px;height:40px;cursor:ns-resize} .encoder .knob .ind{transform-origin:50% 15px;top:4px;height:12px} @@ -279,9 +308,9 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .keylock .barrel{position:absolute;left:50%;top:50%;width:5px;height:5px;margin:-2.5px;border-radius:1px;background:#0a0908; box-shadow:0 0 2px rgba(0,0,0,.8)} .keylock .bit{position:absolute;left:50%;top:50%;width:3px;height:15px;margin:-15px 0 0 -1.5px;background:var(--gold-hi); - border-radius:1px;transform-origin:50% 100%;box-shadow:0 0 5px rgba(255,190,84,.55);transition:transform .2s} + border-radius:1px;transform-origin:50% 100%;box-shadow:0 0 5px rgba(var(--glow-hi),.55);transition:transform .2s} .keylock .kpos{position:absolute;font-size:9px;color:var(--dim);letter-spacing:.05em;transform:translate(-50%,-50%)} -.keylock .kpos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(255,190,84,.55)} +.keylock .kpos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.55)} /* crossfader */ .xfader{width:160px;height:22px;position:relative;cursor:pointer;touch-action:none} @@ -289,7 +318,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .xfader .detent{position:absolute;top:2px;left:50%;width:1px;height:18px;background:var(--steel);margin-left:-.5px;opacity:.7} .xfader .end{position:absolute;top:11px;font-size:9px;color:var(--steel);transform:translateY(-50%)} .xfader .cap{position:absolute;top:3px;width:9px;height:16px;border-radius:2px;margin-left:-4.5px;transition:left .05s; - background:linear-gradient(180deg,#f2c76a,#d29638);border:1px solid #7d5c16;box-shadow:0 1px 2px rgba(0,0,0,.5)} + background:linear-gradient(180deg,var(--amber-grad-top),var(--amber-grad-mid));border:1px solid var(--amber-edge);box-shadow:0 1px 2px rgba(0,0,0,.5)} /* thumbwheel */ .thumbw{display:flex;align-items:center;gap:10px} @@ -306,7 +335,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .dipsw{width:12px;height:26px;background:#0a1220;border-radius:2px;position:relative;cursor:pointer;border:1px solid #1c2c42} .dipsw i{position:absolute;left:1px;right:1px;height:11px;border-radius:1px;background:linear-gradient(180deg,#eae4d2,#b4ad98); bottom:1px;transition:bottom .12s,top .12s} -.dipsw.on i{bottom:auto;top:1px;background:linear-gradient(180deg,#f2c76a,var(--gold))} +.dipsw.on i{bottom:auto;top:1px;background:linear-gradient(180deg,var(--amber-grad-top),var(--gold))} /* jog / shuttle */ .jog{position:relative;width:74px;height:74px;cursor:ns-resize;touch-action:none} @@ -342,7 +371,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .crossm .face{position:relative;height:56px;overflow:hidden;cursor:ns-resize;touch-action:none} .crossm .arc{position:absolute;inset:2px 4px -56px;border:2px solid var(--wash);border-radius:50%} .crossm .nA{position:absolute;left:14px;bottom:2px;width:2px;height:52px;background:var(--gold-hi);transform-origin:50% 100%; - border-radius:2px;box-shadow:0 0 5px rgba(255,190,84,.5);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} + border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.5);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} .crossm .nB{position:absolute;right:14px;bottom:2px;width:2px;height:52px;background:var(--fail);transform-origin:50% 100%; border-radius:2px;box-shadow:0 0 5px rgba(203,107,77,.5);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} .crossm .lbl{display:flex;justify-content:space-between;color:var(--steel);font-size:9px;margin-top:2px} @@ -362,7 +391,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} box-shadow:inset 0 2px 5px rgba(0,0,0,.6)} .bourdon svg{position:absolute;inset:0} .bourdon .ndl{position:absolute;left:50%;top:50%;width:2px;height:32px;margin:-32px 0 0 -1px;background:var(--gold-hi); - transform-origin:50% 100%;border-radius:2px;box-shadow:0 0 5px rgba(255,190,84,.55);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} + transform-origin:50% 100%;border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.55);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} .bourdon .hub{position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px;border-radius:50%;background:var(--gold)} .bourdon .cap{position:absolute;left:0;right:0;bottom:12px;text-align:center;font-size:8px;letter-spacing:.16em;color:var(--steel)} @@ -374,7 +403,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .strip svg{position:absolute;inset:0;width:100%;height:100%} .strip polyline{fill:none;stroke:var(--gold);stroke-width:1.4} .strip .pen{position:absolute;right:2px;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold-hi); - box-shadow:0 0 6px rgba(255,190,84,.8);transition:top .08s linear} + box-shadow:0 0 6px rgba(var(--glow-hi),.8);transition:top .08s linear} /* correlation */ .corr{width:150px;cursor:pointer;touch-action:none} @@ -382,7 +411,7 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .corr .arc{position:absolute;inset:0 0 -44px;border:2px solid var(--wash);border-radius:50%} .corr .zero{position:absolute;left:50%;top:2px;width:1px;height:10px;background:var(--gold);margin-left:-.5px} .corr .ndl{position:absolute;left:50%;bottom:0;width:2px;height:38px;background:var(--gold-hi);transform-origin:50% 100%; - border-radius:2px;box-shadow:0 0 5px rgba(255,190,84,.5);transition:transform .1s ease-out} + border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.5);transition:transform .1s ease-out} .corr .lbl{display:flex;justify-content:space-between;color:var(--steel);font-size:9px;margin-top:2px} /* battery */ @@ -423,8 +452,8 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .annun{display:grid;grid-template-columns:repeat(3,1fr);gap:3px} .acell{font-size:8.5px;letter-spacing:.08em;text-align:center;color:var(--dim);padding:6px 4px;border-radius:3px;cursor:pointer; background:#141210;border:1px solid #262320;line-height:1.2} -.acell.warn{color:var(--panel);background:linear-gradient(180deg,#f0b552,var(--gold));font-weight:700;box-shadow:0 0 8px rgba(226,160,56,.4)} -.acell.fault{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));font-weight:700;box-shadow:0 0 8px rgba(203,107,77,.4);animation:pulse 1s ease-in-out infinite} +.acell.warn{color:var(--panel);background:linear-gradient(180deg,var(--amber-warn),var(--gold));font-weight:700;box-shadow:0 0 8px rgba(var(--glow-lo),.4)} +.acell.fault{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));font-weight:700;box-shadow:0 0 8px rgba(203,107,77,.4);animation:pulse var(--pulse-rate) ease-in-out infinite} /* jewel */ .jewel{width:24px;height:24px;border-radius:50%;position:relative;cursor:pointer; @@ -457,13 +486,13 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .clock .mh{position:absolute;left:50%;top:50%;width:2px;height:28px;margin:-28px 0 0 -1px;background:var(--silver); transform-origin:50% 100%;border-radius:2px} .clock .sh{position:absolute;left:50%;top:50%;width:1px;height:30px;margin:-30px 0 0 -.5px;background:var(--gold-hi); - transform-origin:50% 100%;box-shadow:0 0 4px rgba(255,190,84,.5)} + transform-origin:50% 100%;box-shadow:0 0 4px rgba(var(--glow-hi),.5)} .clock .pin{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold)} /* frequency-dial scale */ .freqscale{width:184px;height:44px;position:relative;border-radius:5px;overflow:hidden;cursor:pointer;touch-action:none; background:linear-gradient(180deg,#191510,#0b0908);border:1px solid #2a251c; - box-shadow:inset 0 0 16px rgba(226,160,56,.1)} + box-shadow:inset 0 0 16px rgba(var(--glow-lo),.1)} .freqscale .tick{position:absolute;top:5px;width:1px;background:var(--steel);transform:translateX(-50%)} .freqscale .mk{position:absolute;bottom:6px;transform:translateX(-50%);color:var(--steel);font-size:9px} .freqscale .band{position:absolute;bottom:2px;left:6px;color:var(--gold);font-size:8px;letter-spacing:.18em} @@ -476,8 +505,8 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .patch .row+.row{margin-top:9px} .patch .jack{width:13px;height:13px;border-radius:50%;cursor:pointer;background:radial-gradient(circle at 40% 35%,#3a352c,#0a0908 70%); border:1px solid #4a443a;box-shadow:inset 0 1px 2px rgba(0,0,0,.8)} -.patch .jack.hot{background:radial-gradient(circle at 40% 35%,#7d5c16,#1a1408 70%)} -.patch .jack.sel{border-color:var(--gold-hi);box-shadow:0 0 6px 1px rgba(255,190,84,.7)} +.patch .jack.hot{background:radial-gradient(circle at 40% 35%,var(--amber-edge),#1a1408 70%)} +.patch .jack.sel{border-color:var(--gold-hi);box-shadow:0 0 6px 1px rgba(var(--glow-hi),.7)} .patch svg{position:absolute;inset:0;pointer-events:none;width:100%;height:100%} .patch svg path{fill:none;stroke:var(--gold);stroke-width:2.4;opacity:.85;stroke-linecap:round} diff --git a/docs/prototypes/tokens-waybar.css b/docs/prototypes/tokens-waybar.css new file mode 100644 index 0000000..d18ba23 --- /dev/null +++ b/docs/prototypes/tokens-waybar.css @@ -0,0 +1,34 @@ +/* generated from tokens.json by gen_tokens.py — do not edit by hand. + GTK CSS has no custom properties; reference these as @name, and use + alpha(@name, 0.5) where the web build uses rgba(var(--name),.5). */ +@define-color ground #151311; +@define-color panel #100f0f; +@define-color well #0a0c0d; +@define-color raise #1a1917; +@define-color silver #bfc4d0; +@define-color cream #f3e7c5; +@define-color steel #969385; +@define-color dim #7c838a; +@define-color slate #424f5e; +@define-color slate_hi #54677d; +@define-color wash #2c2f32; +@define-color pass #74932f; +@define-color fail #cb6b4d; +@define-color phos #7fe0a0; +@define-color phos_dim #1c3626; +@define-color vfd #63e6c8; +@define-color sevgrn #57d357; +@define-color sevred #e2543f; +@define-color sevoff #1a1613; +@define-color jewel_r #ff5b45; +@define-color jewel_a #ffb43a; +@define-color jewel_g #6fce33; +@define-color gold #e2a038; +@define-color gold_hi #ffbe54; +@define-color amber_grad_top #f2c76a; +@define-color amber_grad_mid #d29638; +@define-color amber_grad_bot #8f671f; +@define-color amber_edge #7d5c16; +@define-color amber_warn #f0b552; +@define-color glow_hi #ffbe54; +@define-color glow_lo #e2a038; diff --git a/docs/prototypes/tokens.el b/docs/prototypes/tokens.el new file mode 100644 index 0000000..1fb6e52 --- /dev/null +++ b/docs/prototypes/tokens.el @@ -0,0 +1,41 @@ +;;; gallery-tokens.el --- generated from tokens.json -*- lexical-binding: t; -*- +;;; Commentary: +;; Generated by gen_tokens.py — do not edit by hand; edit tokens.json. +;;; Code: +(defconst gallery-tokens + '((ground . "#151311") + (panel . "#100f0f") + (well . "#0a0c0d") + (raise . "#1a1917") + (silver . "#bfc4d0") + (cream . "#f3e7c5") + (steel . "#969385") + (dim . "#7c838a") + (slate . "#424f5e") + (slate-hi . "#54677d") + (wash . "#2c2f32") + (pass . "#74932f") + (fail . "#cb6b4d") + (phos . "#7fe0a0") + (phos-dim . "#1c3626") + (vfd . "#63e6c8") + (sevgrn . "#57d357") + (sevred . "#e2543f") + (sevoff . "#1a1613") + (jewel-r . "#ff5b45") + (jewel-a . "#ffb43a") + (jewel-g . "#6fce33") + (gold . "#e2a038") + (gold-hi . "#ffbe54") + (amber-grad-top . "#f2c76a") + (amber-grad-mid . "#d29638") + (amber-grad-bot . "#8f671f") + (amber-edge . "#7d5c16") + (amber-warn . "#f0b552") + (glow-hi . "#ffbe54") + (glow-lo . "#e2a038") + (mono . "\"BerkeleyMono Nerd Font\",\"Berkeley Mono\",monospace") + (pulse-rate . "1s")) + "Design tokens for the panel widget gallery.") +(provide 'gallery-tokens) +;;; gallery-tokens.el ends here diff --git a/docs/prototypes/tokens.json b/docs/prototypes/tokens.json new file mode 100644 index 0000000..6974805 --- /dev/null +++ b/docs/prototypes/tokens.json @@ -0,0 +1,46 @@ +{ + "_note": "Single source of truth for the panel widget gallery's design tokens. Edit here, then run: python3 gen_tokens.py (regenerates the :root block in panel-widget-gallery.html plus tokens-waybar.css and tokens.el). The amber hue is defined once; its glow rgb triples are derived by the generator, so retuning amber is a one-line change here.", + "palette": { + "ground": "#151311", + "panel": "#100f0f", + "well": "#0a0c0d", + "raise": "#1a1917", + "silver": "#bfc4d0", + "cream": "#f3e7c5", + "steel": "#969385", + "dim": "#7c838a", + "slate": "#424f5e", + "slate-hi": "#54677d", + "wash": "#2c2f32", + "pass": "#74932f", + "fail": "#cb6b4d", + "phos": "#7fe0a0", + "phos-dim": "#1c3626", + "vfd": "#63e6c8", + "sevgrn": "#57d357", + "sevred": "#e2543f", + "sevoff": "#1a1613", + "jewel-r": "#ff5b45", + "jewel-a": "#ffb43a", + "jewel-g": "#6fce33" + }, + "amber": { + "gold": "#e2a038", + "gold-hi": "#ffbe54", + "amber-grad-top": "#f2c76a", + "amber-grad-mid": "#d29638", + "amber-grad-bot": "#8f671f", + "amber-edge": "#7d5c16", + "amber-warn": "#f0b552" + }, + "glow": { + "glow-hi": "gold-hi", + "glow-lo": "gold" + }, + "font": { + "mono": "\"BerkeleyMono Nerd Font\",\"Berkeley Mono\",monospace" + }, + "timing": { + "pulse-rate": "1s" + } +} diff --git a/tests/gallery-tokens/test_gen_tokens.py b/tests/gallery-tokens/test_gen_tokens.py new file mode 100644 index 0000000..414194e --- /dev/null +++ b/tests/gallery-tokens/test_gen_tokens.py @@ -0,0 +1,268 @@ +"""Tests for docs/prototypes/gen_tokens.py. + +gen_tokens.py is the single-source token generator for the panel widget +gallery. It reads docs/prototypes/tokens.json (the neutral source of truth +for the design tokens) and emits three target representations from it: + + - web CSS custom properties (:root { --gold:#e2a038; ... }) + - waybar GTK CSS (@define-color gold #e2a038; ...) + - Emacs elisp (an alist for the future svg.el renderer) + +The three differ on purpose: CSS uses hyphenated --vars and stores glow +colors as bare "r,g,b" triples (so rgba(var(--glow-hi),.5) works); GTK CSS +has no custom properties, so it uses @define-color with underscore names +and resolves glows to their source hex (GTK uses alpha(@color,a)); elisp +uses a hyphenated-symbol alist of hex strings. One source, three emitters +is the whole reason the generator earns its keep across the three targets. + +These tests import the REAL gen_tokens module from docs/prototypes/ (not a +copy) and exercise its functions directly against fixture token dicts, plus +one integration test that runs main() against temp copies. + +Run from repo root: + make test-unit + (or python3 -m unittest tests.gallery-tokens.test_gen_tokens) +""" + +import importlib.util +import json +import os +import tempfile +import unittest + + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +GEN = os.path.join(REPO_ROOT, "docs", "prototypes", "gen_tokens.py") +TOKENS_JSON = os.path.join(REPO_ROOT, "docs", "prototypes", "tokens.json") + + +def _load_module(): + """Import the real gen_tokens.py by path (no importable package name).""" + spec = importlib.util.spec_from_file_location("gen_tokens", GEN) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +gt = _load_module() + + +# A small deterministic fixture — not the real tokens.json, so these tests +# don't drift when the palette is retuned. +FIXTURE = { + "palette": {"ground": "#151311", "wash": "#2c2f32", "slate-hi": "#54677d"}, + "amber": {"gold": "#e2a038", "gold-hi": "#ffbe54", "amber-grad-top": "#f2c76a"}, + "glow": {"glow-hi": "gold-hi", "glow-lo": "gold"}, + "font": {"mono": '"Berkeley Mono",monospace'}, + "timing": {"pulse-rate": "1s"}, +} + + +class HexToTriple(unittest.TestCase): + def test_normal_values(self): + self.assertEqual(gt.hex_to_triple("#ffbe54"), "255,190,84") + self.assertEqual(gt.hex_to_triple("#e2a038"), "226,160,56") + + def test_boundary_black_and_white(self): + self.assertEqual(gt.hex_to_triple("#000000"), "0,0,0") + self.assertEqual(gt.hex_to_triple("#ffffff"), "255,255,255") + + def test_boundary_three_digit_shorthand(self): + self.assertEqual(gt.hex_to_triple("#fff"), "255,255,255") + self.assertEqual(gt.hex_to_triple("#0f0"), "0,255,0") + + def test_boundary_no_leading_hash(self): + self.assertEqual(gt.hex_to_triple("e2a038"), "226,160,56") + + def test_error_bad_chars(self): + with self.assertRaises(ValueError): + gt.hex_to_triple("#gggggg") + + def test_error_bad_length(self): + with self.assertRaises(ValueError): + gt.hex_to_triple("#12") + + +class ResolveColor(unittest.TestCase): + def test_finds_in_amber(self): + self.assertEqual(gt.resolve_color(FIXTURE, "gold"), "#e2a038") + + def test_finds_in_palette(self): + self.assertEqual(gt.resolve_color(FIXTURE, "wash"), "#2c2f32") + + def test_missing_raises(self): + with self.assertRaises(KeyError): + gt.resolve_color(FIXTURE, "nonexistent") + + +class EmitWebCss(unittest.TestCase): + def setUp(self): + self.css = gt.emit_web_css(FIXTURE) + + def test_solid_colors_are_hyphenated_vars(self): + self.assertIn("--gold:#e2a038;", self.css) + self.assertIn("--slate-hi:#54677d;", self.css) + self.assertIn("--amber-grad-top:#f2c76a;", self.css) + + def test_glow_stored_as_rgb_triple(self): + # so rgba(var(--glow-hi),.5) resolves; derived from the source hex + self.assertIn("--glow-hi:255,190,84;", self.css) + self.assertIn("--glow-lo:226,160,56;", self.css) + + def test_font_and_timing(self): + self.assertIn("--pulse-rate:1s;", self.css) + self.assertIn('--mono:"Berkeley Mono",monospace;', self.css) + + +class EmitWaybarGtk(unittest.TestCase): + def setUp(self): + self.gtk = gt.emit_waybar_gtk(FIXTURE) + + def test_uses_define_color_not_custom_props(self): + self.assertIn("@define-color gold #e2a038;", self.gtk) + self.assertNotIn("--gold", self.gtk) + self.assertNotIn(":root", self.gtk) + + def test_glow_resolved_to_source_hex(self): + # GTK has no bare triples; it uses alpha(@color,a), so glow -> hex + self.assertIn("@define-color glow_hi #ffbe54;", self.gtk) + self.assertIn("@define-color glow_lo #e2a038;", self.gtk) + + def test_names_use_underscores_not_hyphens(self): + self.assertIn("@define-color slate_hi #54677d;", self.gtk) + self.assertNotIn("slate-hi", self.gtk) + self.assertNotIn("amber-grad-top", self.gtk) + + +class EmitElisp(unittest.TestCase): + def setUp(self): + self.el = gt.emit_elisp(FIXTURE) + + def test_alist_of_hex_with_hyphenated_symbols(self): + self.assertIn('(gold . "#e2a038")', self.el) + self.assertIn('(slate-hi . "#54677d")', self.el) + + def test_glow_is_hex_for_svg(self): + # svg.el / librsvg wants a real color, not a bare triple + self.assertIn('(glow-hi . "#ffbe54")', self.el) + + def test_timing_included(self): + self.assertIn('(pulse-rate . "1s")', self.el) + + +class ReplaceBetweenMarkers(unittest.TestCase): + START = "/* @tokens:start */" + END = "/* @tokens:end */" + + def _wrap(self, inner): + return f"a\n{self.START}\n{inner}\n{self.END}\nb\n" + + def test_replaces_inner_block(self): + src = self._wrap("OLD") + out = gt.replace_between_markers(src, self.START, self.END, "NEW") + self.assertIn("NEW", out) + self.assertNotIn("OLD", out) + # markers and surrounding text survive + self.assertIn(self.START, out) + self.assertIn(self.END, out) + self.assertTrue(out.startswith("a\n")) + self.assertTrue(out.rstrip().endswith("b")) + + def test_idempotent(self): + src = self._wrap("OLD") + once = gt.replace_between_markers(src, self.START, self.END, "NEW") + twice = gt.replace_between_markers(once, self.START, self.END, "NEW") + self.assertEqual(once, twice) + + def test_missing_marker_raises(self): + with self.assertRaises(ValueError): + gt.replace_between_markers("no markers here", self.START, self.END, "NEW") + + def test_start_marker_on_final_line_without_newline(self): + # Defensive branch: no newline after the start marker. The guard must + # not let text[:si-of-newline+1] collapse to "" and wipe the prefix. + src = f"keep\n{self.START} {self.END}" + out = gt.replace_between_markers(src, self.START, self.END, "X") + self.assertTrue(out.startswith("keep\n")) + self.assertIn(self.END, out) + self.assertIn("X", out) + + +class RealTokensJson(unittest.TestCase): + """The committed tokens.json must stay well-formed and complete.""" + + def setUp(self): + with open(TOKENS_JSON) as f: + self.tokens = json.load(f) + + def test_has_required_sections(self): + for section in ("palette", "amber", "glow", "font", "timing"): + self.assertIn(section, self.tokens) + + def test_amber_defines_gold_and_glow_sources_resolve(self): + self.assertIn("gold", self.tokens["amber"]) + self.assertIn("gold-hi", self.tokens["amber"]) + for _name, source in self.tokens["glow"].items(): + # every glow source must resolve to a real color + gt.resolve_color(self.tokens, source) + + def test_pulse_rate_present(self): + self.assertIn("pulse-rate", self.tokens["timing"]) + + +class MainIntegration(unittest.TestCase): + """main() rewrites the html :root and writes the waybar + elisp files.""" + + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="gen-tokens-test-") + self.html = os.path.join(self.tmp, "gallery.html") + self.tokens_path = os.path.join(self.tmp, "tokens.json") + self.waybar = os.path.join(self.tmp, "tokens-waybar.css") + self.elisp = os.path.join(self.tmp, "tokens.el") + with open(self.tokens_path, "w") as f: + json.dump(FIXTURE, f) + with open(self.html, "w") as f: + f.write( + "<style>\n:root{\n" + "/* @tokens:start */\n" + "STALE\n" + "/* @tokens:end */\n" + "}\n</style>\n" + ) + + def tearDown(self): + import shutil + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_main_writes_all_three_targets(self): + gt.main( + tokens_path=self.tokens_path, + html_path=self.html, + waybar_path=self.waybar, + elisp_path=self.elisp, + ) + with open(self.html) as f: + html = f.read() + self.assertIn("--gold:#e2a038;", html) + self.assertNotIn("STALE", html) + self.assertTrue(os.path.exists(self.waybar)) + self.assertTrue(os.path.exists(self.elisp)) + with open(self.waybar) as f: + self.assertIn("@define-color gold #e2a038;", f.read()) + with open(self.elisp) as f: + self.assertIn('(gold . "#e2a038")', f.read()) + + def test_main_is_idempotent(self): + gt.main(tokens_path=self.tokens_path, html_path=self.html, + waybar_path=self.waybar, elisp_path=self.elisp) + with open(self.html) as f: + first = f.read() + gt.main(tokens_path=self.tokens_path, html_path=self.html, + waybar_path=self.waybar, elisp_path=self.elisp) + with open(self.html) as f: + second = f.read() + self.assertEqual(first, second) + + +if __name__ == "__main__": + unittest.main() |
