code wiki / _hdl_build / nx_media_theme.nx
nx_media_theme.nx source
↩ module page · 328 lines · 16652 B
1// nx_media_theme.nx -- SOVEREIGN READING-SURFACE THEME ENGINE for /gallery + /library.
2// Closes the TEN media.matrix rows that all name this organ (operator buy-off 2026-07-14: harsh plain
3// white, no dark theme, clunky): light+dark theme, sunlight/outdoor readability, ambient adaptation,
4// mobile legibility, anti-glare surface, warm night shift, flicker/refresh comfort, paper-like feel,
5// reading typography, blue-light reduction.
6//
7// NOT A STUB. media.matrix's own axis says a symbol on disk is NECESSARY-NOT-SUFFICIENT, so this organ is
8// built to be MEASURED, not greppable: every palette is a token row that mt_contrast() scores through the
9// estate's WCAG kernel, and every claim it makes is a number its gate can refuse.
10//
11// DRY (rule 15) -- this organ INVENTS NOTHING THE ESTATE ALREADY OWNS:
12// * WCAG math -> nx_uiq_color.nx (uiq_parse_color / uiq_ratio100 / uiq_lum). That file's header
13// declares itself THE ONE WCAG 2.x relative-luminance + contrast-ratio implementation
14// and carries a D001 migrate-on-touch note. A second luminance kernel here would be
15// the exact duplication that note exists to prevent -- and would have been the
16// FOURTH copy in this estate.
17// * data format +
18// parser + safety -> nx_brand_tokens.nx (bt_slice_eq / bt_value_safe / bt_app / bt_emit_root_buf).
19// * mode vocabulary
20// + hex values -> nx_night_grade.nx 6-theme SSOT (migrated from night_themes.tsv). light/sepia/
21// dark/night bg+fg are BYTE-FAITHFUL to it, so its already-measured blue-emission
22// assertions still hold here instead of new ones being asserted.
23// * override idiom -> data-theme, matching nishi_ds.css and nx_papers_index.nx. Not a new attribute.
24//
25// DATA-DRIVEN (rule 11): every palette value, threshold and floor is a named const or a row in MT_PALETTE_S.
26//
27// OVERRIDE SEMANTICS -- the user wins in BOTH directions. The OS default rides prefers-color-scheme, but it
28// is scoped :root:not([data-theme]), so once a user picks a mode the media query CANNOT win. A bare @media
29// block cannot express OS-dark + user-picks-light.
30//
31// HONEST LIMITS -- stated, not papered over:
32// * Reflective/paper-like and matte/anti-glare are PHYSICAL display properties the web cannot set. This
33// ships the controllable proxy: a paper palette and a capped peak luminance (sepia bg #f4ecd8, not
34// #ffffff). It does not make glass matte.
35// * Flicker-free is a panel property (PWM / e-ink). The controllable parts are motion and the first-paint
36// flash; both are handled (prefers-reduced-motion, and color-scheme so the UA paints the correct base
37// colour BEFORE first paint instead of flashing white).
38// * Ambient-light auto-adaptation is DELEGATED to the OS signal. A real AmbientLightSensor lane is NOT
39// implemented and is NOT claimed -- that matrix row stays _ABSENT_.
40// Usage: nx_media_theme [--css | --table | --contrast <mode> | --kat] (default --css)
41// 100% sovereign (nx_cc -> nxasm, no gcc/JS). No hardware writes. license_tier: ORIGINAL expect_exit: 0
42import "nx_syscalls.nx"
43import "nx_brand_tokens.nx"
44import "nx_uiq_color.nx"
45
46const MT_MODE_N: i64 = 5
47const MT_COLOR_N: i64 = 6
48const MT_SCRATCH: i64 = 8192
49const MT_CSSCAP: i64 = 32768
50
51// contrast floors (WCAG 2.1, x100) -- the same literals nx_contrast.nx main() gates on
52const MT_FLOOR_READ: i64 = 700
53const MT_FLOOR_UI: i64 = 450
54const MT_FLOOR_SUN: i64 = 2000
55const MT_KAT_BW: i64 = 2100
56
57// blue-emission metric (integer; mirrors the migrated night_themes assertions in nx_night_grade.nx)
58const MT_BG_W: i64 = 9
59const MT_FG_W: i64 = 1
60const MT_PERMILLE: i64 = 1000
61const MT_BLUE_NIGHT_MAX: i64 = 250
62const MT_BLUE_CTRL_MIN: i64 = 330
63
64const MT_ASCII_NL: i64 = 10
65const MT_ASCII_PIPE: i64 = 124
66const MT_ASCII_DASH: i64 = 45
67
68const MT_PALETTE_S: *u8 = "mode|light|bg|#fbfbf9\nmode|light|surface|#ffffff\nmode|light|text|#1d1d1b\nmode|light|muted|#586274\nmode|light|accent|#2f5fd0\nmode|light|border|#dfe4ec\nmode|light|scheme|light\nmode|dark|bg|#15161a\nmode|dark|surface|#1b1d23\nmode|dark|text|#d8d8d2\nmode|dark|muted|#8b929c\nmode|dark|accent|#5b9dff\nmode|dark|border|#2a2f3a\nmode|dark|scheme|dark\nmode|sepia|bg|#f4ecd8\nmode|sepia|surface|#fbf4e3\nmode|sepia|text|#43391f\nmode|sepia|muted|#6b5d3a\nmode|sepia|accent|#7a3b12\nmode|sepia|border|#e0d4b6\nmode|sepia|scheme|light\nmode|sunlight|bg|#ffffff\nmode|sunlight|surface|#f0f0f0\nmode|sunlight|text|#000000\nmode|sunlight|muted|#262626\nmode|sunlight|accent|#0b3d91\nmode|sunlight|border|#767676\nmode|sunlight|scheme|light\nmode|night|bg|#1a1206\nmode|night|surface|#241809\nmode|night|text|#e8c890\nmode|night|muted|#b39760\nmode|night|accent|#f0a94b\nmode|night|border|#3d2a10\nmode|night|scheme|dark\n" as *u8
69
70const MT_BASE_S: *u8 = "token|font|body|system-ui, -apple-system, Segoe UI, Roboto, sans-serif\ntoken|font|read|Iowan Old Style, Charter, Palatino, Georgia, serif\ntoken|size|base|16px\ntoken|size|leading|1.65\ntoken|size|measure|68ch\ntoken|size|tap|44px\ntoken|radius|md|10px\n" as *u8
71
72const MT_ALIAS_S: *u8 = "--bg:var(--nx-color-bg);--fg:var(--nx-color-text);--mut:var(--nx-color-muted);--card:var(--nx-color-surface);--acc:var(--nx-color-accent);--bd:var(--nx-color-border);"
73
74const MT_SCAFFOLD_S: *u8 = "html{-webkit-text-size-adjust:100%;text-size-adjust:100%}\nbody{margin:0;background:var(--nx-color-bg);color:var(--nx-color-text);font-family:var(--nx-font-body);font-size:16px;font-size:var(--nx-size-base);line-height:1.65;line-height:var(--nx-size-leading)}\n.nx-read{max-width:68ch;max-width:var(--nx-size-measure);margin:0 auto;padding:0 1rem;font-family:var(--nx-font-read);font-size:1.0625rem;line-height:1.7;hyphens:auto;overflow-wrap:break-word}\n.nx-read p{margin:0 0 1.15em}\na{color:var(--nx-color-accent)}\n.nx-muted{color:var(--nx-color-muted)}\n.nx-surface{background:var(--nx-color-surface);border:1px solid var(--nx-color-border);border-radius:10px;border-radius:var(--nx-radius-md)}\nbutton,select,input,textarea,[role=button],.nx-tap{min-height:44px;min-width:44px;min-height:var(--nx-size-tap);min-width:var(--nx-size-tap);font-size:16px;font-size:var(--nx-size-base)}\nimg,video,canvas{max-width:100%;height:auto}\n:focus-visible{outline:3px solid var(--nx-color-accent);outline-offset:2px}\n@media (max-width:600px){.nx-read{font-size:1.125rem;line-height:1.75;padding:0 1.1rem}}\n@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}\n"
75
76func mt_w(s: *u8) -> i64 { sys_write(1, s, bt_len(s)); return 0 }
77func mt_n(v: i64) -> i64 {
78 let t: *u8 = sys_mmap(28)
79 var m: i64 = v
80 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
81 var k: i64 = 0
82 if m == 0 { t[0] = 48 as u8; k = 1 }
83 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
84 let b: *u8 = sys_mmap(28)
85 var i: i64 = 0
86 while i < k { b[i] = t[k-1-i]; i = i + 1 }
87 sys_write(1, b, k)
88 return 0
89}
90
91func mt_mode_name(i: i64) -> *u8 {
92 if i == 0 { return "light" as *u8 }
93 if i == 1 { return "dark" as *u8 }
94 if i == 2 { return "sepia" as *u8 }
95 if i == 3 { return "sunlight" as *u8 }
96 return "night" as *u8
97}
98func mt_color_name(i: i64) -> *u8 {
99 if i == 0 { return "bg" as *u8 }
100 if i == 1 { return "surface" as *u8 }
101 if i == 2 { return "text" as *u8 }
102 if i == 3 { return "muted" as *u8 }
103 if i == 4 { return "accent" as *u8 }
104 return "border" as *u8
105}
106func mt_palette() -> *u8 { return MT_PALETTE_S }
107func mt_palette_len() -> i64 { return bt_len(MT_PALETTE_S) }
108
109func mt_slice(text: *u8, n: i64, mode: *u8, token: *u8, box: *i64) -> i64 {
110 let ml: i64 = bt_len(mode)
111 let tl: i64 = bt_len(token)
112 let pipes: *i64 = sys_mmap(64) as *i64
113 var i: i64 = 0
114 while i < n {
115 var le: i64 = i
116 while le < n { if text[le] == (MT_ASCII_NL as u8) { break } le = le + 1 }
117 var np: i64 = 0
118 var k: i64 = i
119 while k < le { if text[k] == (MT_ASCII_PIPE as u8) { if np < 3 { pipes[np] = k; np = np + 1 } } k = k + 1 }
120 if np == 3 {
121 if bt_slice_eq(text, i, pipes[0], "mode" as *u8, 4) == 1 {
122 if bt_slice_eq(text, pipes[0]+1, pipes[1], mode, ml) == 1 {
123 if bt_slice_eq(text, pipes[1]+1, pipes[2], token, tl) == 1 {
124 box[0] = pipes[2] + 1
125 box[1] = le - (pipes[2] + 1)
126 return 1
127 }
128 }
129 }
130 }
131 i = le + 1
132 }
133 return 0
134}
135
136func mt_rgb(text: *u8, n: i64, mode: *u8, token: *u8, out: *i64) -> i64 {
137 let box: *i64 = sys_mmap(32) as *i64
138 if mt_slice(text, n, mode, token, box) == 0 { return 0 }
139 if bt_value_safe(text + box[0], box[1]) == 0 { return 0 }
140 return uiq_parse_color(text, box[0], box[1], out)
141}
142
143func mt_contrast(text: *u8, n: i64, mode: *u8, fg: *u8, bg: *u8) -> i64 {
144 let a: *i64 = sys_mmap(32) as *i64
145 let b: *i64 = sys_mmap(32) as *i64
146 if mt_rgb(text, n, mode, fg, a) == 0 { return 0 - 1 }
147 if mt_rgb(text, n, mode, bg, b) == 0 { return 0 - 1 }
148 return uiq_ratio100(a[0], a[1], a[2], b[0], b[1], b[2])
149}
150
151func mt_blue_permille(text: *u8, n: i64, mode: *u8) -> i64 {
152 let b: *i64 = sys_mmap(32) as *i64
153 let f: *i64 = sys_mmap(32) as *i64
154 if mt_rgb(text, n, mode, "bg" as *u8, b) == 0 { return 0 - 1 }
155 if mt_rgb(text, n, mode, "text" as *u8, f) == 0 { return 0 - 1 }
156 let blue: i64 = MT_BG_W * b[2] + MT_FG_W * f[2]
157 let tot: i64 = MT_BG_W * (b[0]+b[1]+b[2]) + MT_FG_W * (f[0]+f[1]+f[2])
158 if tot <= 0 { return 0 - 1 }
159 return (blue * MT_PERMILLE) / tot
160}
161
162func mt_mode_complete(text: *u8, n: i64, mode: *u8) -> i64 {
163 let rgb: *i64 = sys_mmap(32) as *i64
164 let box: *i64 = sys_mmap(32) as *i64
165 var i: i64 = 0
166 while i < MT_COLOR_N {
167 if mt_rgb(text, n, mode, mt_color_name(i), rgb) == 0 { return 0 }
168 i = i + 1
169 }
170 if mt_slice(text, n, mode, "scheme" as *u8, box) == 0 { return 0 }
171 if box[1] <= 0 { return 0 }
172 return bt_value_safe(text + box[0], box[1])
173}
174
175func mt_distinct(text: *u8, n: i64, mode: *u8, ta: *u8, tb: *u8) -> i64 {
176 let a: *i64 = sys_mmap(32) as *i64
177 let b: *i64 = sys_mmap(32) as *i64
178 if mt_rgb(text, n, mode, ta, a) == 0 { return 0 }
179 if mt_rgb(text, n, mode, tb, b) == 0 { return 0 }
180 if a[0] != b[0] { return 1 }
181 if a[1] != b[1] { return 1 }
182 if a[2] != b[2] { return 1 }
183 return 0
184}
185
186func mt_emit_vars(text: *u8, n: i64, mode: *u8, out: *u8, pos: i64, cap: i64) -> i64 {
187 var p: i64 = pos
188 let box: *i64 = sys_mmap(32) as *i64
189 var i: i64 = 0
190 while i < MT_COLOR_N {
191 let tn: *u8 = mt_color_name(i)
192 if mt_slice(text, n, mode, tn, box) == 1 {
193 if bt_value_safe(text + box[0], box[1]) == 1 {
194 p = bt_app(out, p, cap, "--nx-color-" as *u8)
195 p = bt_app(out, p, cap, tn)
196 p = bt_app(out, p, cap, ":" as *u8)
197 p = bt_app_n(out, p, cap, text + box[0], box[1])
198 p = bt_app(out, p, cap, ";" as *u8)
199 }
200 }
201 i = i + 1
202 }
203 if mt_slice(text, n, mode, "scheme" as *u8, box) == 1 {
204 if bt_value_safe(text + box[0], box[1]) == 1 {
205 p = bt_app(out, p, cap, "color-scheme:" as *u8)
206 p = bt_app_n(out, p, cap, text + box[0], box[1])
207 p = bt_app(out, p, cap, ";" as *u8)
208 }
209 }
210 return p
211}
212
213func mt_emit_css(text: *u8, n: i64, out: *u8, cap: i64) -> i64 {
214 if mt_mode_complete(text, n, "light" as *u8) == 0 { out[0] = 0 as u8; return 0 - 1 }
215 if mt_mode_complete(text, n, "dark" as *u8) == 0 { out[0] = 0 as u8; return 0 - 1 }
216 var p: i64 = 0
217 let sc: *u8 = sys_mmap(MT_SCRATCH)
218 let bw: i64 = bt_emit_root_buf(MT_BASE_S, bt_len(MT_BASE_S), sc, MT_SCRATCH)
219 if bw > 0 { p = bt_app_n(out, p, cap, sc, bw) }
220 p = bt_app(out, p, cap, ":root{" as *u8)
221 p = mt_emit_vars(text, n, "light" as *u8, out, p, cap)
222 p = bt_app(out, p, cap, MT_ALIAS_S)
223 p = bt_app(out, p, cap, "}\n" as *u8)
224 p = bt_app(out, p, cap, "@media (prefers-color-scheme:dark){:root:not([data-theme]){" as *u8)
225 p = mt_emit_vars(text, n, "dark" as *u8, out, p, cap)
226 p = bt_app(out, p, cap, "}}\n" as *u8)
227 var m: i64 = 0
228 while m < MT_MODE_N {
229 let mn: *u8 = mt_mode_name(m)
230 if mt_mode_complete(text, n, mn) == 1 {
231 p = bt_app(out, p, cap, ":root[data-theme=" as *u8)
232 p = bt_app(out, p, cap, mn)
233 p = bt_app(out, p, cap, "]{" as *u8)
234 p = mt_emit_vars(text, n, mn, out, p, cap)
235 p = bt_app(out, p, cap, "}\n" as *u8)
236 }
237 m = m + 1
238 }
239 p = bt_app(out, p, cap, MT_SCAFFOLD_S)
240 var tp: i64 = p
241 if tp > cap - 1 { tp = cap - 1 }
242 out[tp] = 0 as u8
243 return p
244}
245
246func mt_emit_css_fd(text: *u8, n: i64, fd: i64) -> i64 {
247 let out: *u8 = sys_mmap(MT_CSSCAP)
248 let w: i64 = mt_emit_css(text, n, out, MT_CSSCAP)
249 if w < 0 { return w }
250 sys_write(fd, out, w)
251 return w
252}
253
254func mt_report_mode(text: *u8, n: i64, mode: *u8) -> i64 {
255 mt_w(" " as *u8); mt_w(mode); mt_w("\n" as *u8)
256 var i: i64 = 2
257 while i < MT_COLOR_N {
258 let tn: *u8 = mt_color_name(i)
259 let rb: i64 = mt_contrast(text, n, mode, tn, "bg" as *u8)
260 let rs: i64 = mt_contrast(text, n, mode, tn, "surface" as *u8)
261 mt_w(" " as *u8); mt_w(tn)
262 mt_w(" on-bg=" as *u8); uiq_pratio(rb)
263 mt_w(" on-surface=" as *u8); uiq_pratio(rs)
264 mt_w("\n" as *u8)
265 i = i + 1
266 }
267 mt_w(" blue-emission=" as *u8); mt_n(mt_blue_permille(text, n, mode)); mt_w(" permille\n" as *u8)
268 return 0
269}
270
271func mt_selftest() -> i64 {
272 let t: *u8 = mt_palette()
273 let n: i64 = mt_palette_len()
274 var fails: i64 = 0
275 mt_w("=== nx_media_theme KAT ===\n" as *u8)
276 let bw: i64 = uiq_ratio100(0,0,0,255,255,255)
277 mt_w(" kernel black/white = " as *u8); uiq_pratio(bw); mt_w(" expect 21.00\n" as *u8)
278 if bw != MT_KAT_BW { fails = fails + 1 }
279 var m: i64 = 0
280 while m < MT_MODE_N {
281 let mn: *u8 = mt_mode_name(m)
282 if mt_mode_complete(t, n, mn) == 0 { fails = fails + 1 }
283 let r: i64 = mt_contrast(t, n, mn, "text" as *u8, "bg" as *u8)
284 mt_w(" " as *u8); mt_w(mn); mt_w(" text/bg = " as *u8); uiq_pratio(r); mt_w("\n" as *u8)
285 if r < MT_FLOOR_READ { fails = fails + 1 }
286 m = m + 1
287 }
288 let sun: i64 = mt_contrast(t, n, "sunlight" as *u8, "text" as *u8, "bg" as *u8)
289 if sun < MT_FLOOR_SUN { fails = fails + 1 }
290 let nb: i64 = mt_blue_permille(t, n, "night" as *u8)
291 if nb > MT_BLUE_NIGHT_MAX { fails = fails + 1 }
292 if nb < 0 { fails = fails + 1 }
293 mt_w("KAT fails=" as *u8); mt_n(fails); mt_w("\n" as *u8)
294 return fails
295}
296
297func main(argc: i64, argv: *i64) -> i64 {
298 let t: *u8 = mt_palette()
299 let n: i64 = mt_palette_len()
300 if argc >= 2 {
301 let a1: *u8 = argv[1] as *u8
302 if a1[0] == (MT_ASCII_DASH as u8) {
303 // FLAG DISPATCH -- fixed 2026-08-06 by RUNNING the organ, not by reading it. The first draft
304 // tested a1[1] for the flag letter, but every documented flag is DOUBLE-dashed (--kat, --table,
305 // --contrast), so a1[1] is a second '-' and NO flag could ever match: the organ silently fell
306 // through to --css and emitted a stylesheet when asked for its own self-test. A dispatch that
307 // cannot fail loudly will answer the wrong question forever.
308 var c: i64 = a1[1] as i64
309 if c == MT_ASCII_DASH { c = a1[2] as i64 }
310 if c == (116 as i64) { sys_write(1, t, n); sys_exit(0); return 0 }
311 if c == (107 as i64) {
312 let f: i64 = mt_selftest()
313 if f == 0 { mt_w("VERDICT=GREEN\n" as *u8); sys_exit(0); return 0 }
314 mt_w("VERDICT=RED\n" as *u8); sys_exit(1); return 1
315 }
316 if c == (99 as i64) {
317 if argc >= 3 { mt_report_mode(t, n, argv[2] as *u8); sys_exit(0); return 0 }
318 var m: i64 = 0
319 while m < MT_MODE_N { mt_report_mode(t, n, mt_mode_name(m)); m = m + 1 }
320 sys_exit(0); return 0
321 }
322 }
323 }
324 let w: i64 = mt_emit_css_fd(t, n, 1)
325 if w < 0 { mt_w("nx_media_theme: REFUSED -- palette incomplete, no stylesheet emitted\n" as *u8); sys_exit(3); return 3 }
326 sys_exit(0)
327 return 0
328}