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}