1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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()))
|