// cmatrix — Matrix-style falling glyphs in the terminal via libncurses. // // Exercises a broad slice of ww: // - @symbol FFI to libncurses (functions + struct-ptr params) // - struct types + struct literals (incl. `aerr{}` to tag a named // `!void` variant) // - alloc/free via os.alloc / os.free (page allocator, no GC) // - slice indexing — `str` palette is (*u8, len); rng_glyph uses [i] // - enum types (theme, action) // - tagged-union return types with two and three variants // - `?` to propagate the error variant up the stack // - `!` to unwrap the success variant or abort on the error variant // - `match` (incl. nested match) to dispatch over (T | E1 | …) // - `yield` to turn `match` into an expression (single-return funcs) // - `switch` over a key code // - `let` type inference — annotations only where the RHS is ambiguous // (uninitialised decls) or carries a load-bearing narrowing cast // // Build: // ww build cmatrix.ww -L /usr/lib -l ncurses // // Keys while running: // q / Q quit // space pause / resume // 1 2 3 4 change speed (1 = slowest, 4 = fastest) // 0 invalid speed — flashes an error overlay // r g b switch trail colour to red / green / blue package cmatrix; import os; import time; import fmt; // ---- libncurses FFI ---------------------------------------------------- @symbol("initscr") fn nc_initscr() *void; @symbol("endwin") fn nc_endwin() i32; @symbol("noecho") fn nc_noecho() i32; @symbol("cbreak") fn nc_cbreak() i32; @symbol("curs_set") fn nc_curs_set(v: i32) i32; @symbol("start_color") fn nc_start_color() i32; @symbol("use_default_colors") fn nc_use_default_colors() i32; @symbol("init_pair") fn nc_init_pair(pair: i16, fg: i16, bg: i16) i32; @symbol("COLOR_PAIR") fn nc_color_pair(n: i32) i32; @symbol("attron") fn nc_attron(attrs: i32) i32; @symbol("attroff") fn nc_attroff(attrs: i32) i32; @symbol("mvaddch") fn nc_mvaddch(y: i32, x: i32, c: u32) i32; @symbol("mvaddstr") fn nc_mvaddstr(y: i32, x: i32, s: *u8) i32; @symbol("erase") fn nc_erase() i32; @symbol("refresh") fn nc_refresh() i32; @symbol("getch") fn nc_getch() i32; @symbol("nodelay") fn nc_nodelay(win: *void, flag: i32) i32; @symbol("keypad") fn nc_keypad(win: *void, flag: i32) i32; @symbol("napms") fn nc_napms(ms: i32) i32; @symbol("getmaxx") fn nc_getmaxx(win: *void) i32; @symbol("getmaxy") fn nc_getmaxy(win: *void) i32; // ncurses palette indices (from ). def COLOR_RED: i16 = 1; def COLOR_GREEN: i16 = 2; def COLOR_BLUE: i16 = 4; def COLOR_WHITE: i16 = 7; def A_BOLD: i32 = 2097152; // (1 << 21) from // Local color-pair ids. def CP_DIM: i32 = 1; def CP_BODY: i32 = 2; def CP_BRIGHT: i32 = 3; def CP_HEAD: i32 = 4; // Per-column trail buffer width. The head pushes glyphs down through // MAX_TRAIL slots; cells past the tail are erased to space. def MAX_TRAIL: i32 = 24; // Number of frames the '0' error overlay stays on screen. def FLASH_FRAMES: i32 = 25; // Glyph palette — printable ASCII. `str` is the canonical ww slice // shape (*u8 + len); rng_glyph treats it as one. def GLYPHS: str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*+=-_;:.<>?/"; // ---- xorshift64 prng --------------------------------------------------- type rng = struct { state: u64 }; fn rng_next(r: *rng) u64 = { let x = r.state; x ^= x << 13u64; x ^= x >> 7u64; x ^= x << 17u64; r.state = x; return x; }; fn rng_range(r: *rng, lo: i32, hi: i32) i32 = { if (hi <= lo) { return lo; }; let span = (hi - lo): u64; return lo + ((rng_next(r) % span): i32); }; fn rng_glyph(r: *rng) u8 = { let pool = GLYPHS; let i = (rng_next(r) % (pool.len: u64)): i32; return pool[i]; }; // ---- theme — enum picked by 'r' / 'g' / 'b' --------------------------- type theme = enum i32 { GREEN = 0, RED = 1, BLUE = 2, }; fn theme_color(t: theme) i16 = { if (t == theme.RED) { return COLOR_RED; }; if (t == theme.BLUE) { return COLOR_BLUE; }; return COLOR_GREEN; }; // apply_theme — re-init the three trail color pairs against `t`. The // HEAD pair stays white. Subsequent paint_col calls pick up the new // colours automatically because ncurses indirects through pair-id. fn apply_theme(t: theme) void = { let base = theme_color(t); nc_init_pair(CP_DIM: i16, base, -1: i16); nc_init_pair(CP_BODY: i16, base, -1: i16); nc_init_pair(CP_BRIGHT: i16, base, -1: i16); }; // ---- cmat — column state as parallel arrays --------------------------- // // w6c's element-size analysis returns 8 for any non-primitive slice // element, so an array of `column` structs can't be indexed correctly. // We hold each per-column field in its own primitive-typed buffer plus // one rectangular u8 buffer of glyphs (column-major: glyphs[c*MAX_TRAIL // + i] is the glyph at row head[c]-i in column c). type cmat = struct { w: i32, h: i32, head: *i32, length: *i32, speed: *i32, counter: *i32, glyphs: *u8, gspeed: i32, // uniform speed override, -1 = random per column gtheme: i32, // current theme.* value flash: i32, // frames remaining for the '0' error overlay }; // 56 bytes by natural alignment; rounded to 64 to avoid trap #1 in // selfhost/CLAUDE.md (amalloc < struct size silently corrupts). def CMAT_SZ: u64 = 64u64; // ---- error variants --------------------------------------------------- // initerr — non-recoverable setup failure. `!` flags this as an error // variant — `?`-propagation picks it as the failure half of any // (T | initerr) shape. type initerr = !str; // speederr — the user pressed '0', which is "no speed at all". We track // it separately from "this wasn't a digit" (the void variant of the // same union) so the key handler can distinguish "show error overlay" // from "ignore this key". Constructed via the `speederr{}` struct-lit // form; for `!void` aliases there's no payload to pass. type speederr = !void; // ---- ncurses bring-up + tear-down ------------------------------------- fn setup() (*void | initerr) = { let scr = nc_initscr(); if (scr == nil) { return "initscr failed": initerr; }; nc_noecho(); nc_cbreak(); nc_curs_set(0); nc_nodelay(scr, 1); nc_keypad(scr, 1); if (nc_start_color() != 0) { nc_endwin(); return "start_color failed": initerr; }; nc_use_default_colors(); nc_init_pair(CP_HEAD: i16, COLOR_WHITE, -1: i16); apply_theme(theme.GREEN); return scr; }; // ---- clock helper ----------------------------------------------------- fn now_ns() u64 = { let i = time.now(time.clock.monotonic); let v = i.sec * 1000000000i64 + i.nsec; return v: u64; }; // ---- cmat allocate / free --------------------------------------------- fn cmat_alloc(w: i32, h: i32, r: *rng) *cmat = { let m = os.alloc(CMAT_SZ): *cmat; m.w = w; m.h = h; m.gspeed = -1; m.gtheme = theme.GREEN: i32; m.flash = 0; let n4 = (w: u64) * 4u64; m.head = os.alloc(n4): *i32; m.length = os.alloc(n4): *i32; m.speed = os.alloc(n4): *i32; m.counter = os.alloc(n4): *i32; m.glyphs = os.alloc((w: u64) * (MAX_TRAIL: u64)): *u8; let c = 0; for (c < w) { m.head[c] = -rng_range(r, 0, h); m.length[c] = rng_range(r, 4, MAX_TRAIL); m.speed[c] = rng_range(r, 0, 4); m.counter[c] = 0; let i = 0; for (i < MAX_TRAIL) { m.glyphs[c * MAX_TRAIL + i] = rng_glyph(r); i += 1; }; c += 1; }; return m; }; fn cmat_free(m: *cmat) void = { let n4 = (m.w: u64) * 4u64; os.free(m.head: *void, n4); os.free(m.length: *void, n4); os.free(m.speed: *void, n4); os.free(m.counter: *void, n4); os.free(m.glyphs: *void, (m.w: u64) * (MAX_TRAIL: u64)); os.free(m: *void, CMAT_SZ); }; // ---- per-column tick + paint ------------------------------------------ // fresh_speed — pick a per-column speed honouring the global override. fn fresh_speed(m: *cmat, r: *rng) i32 = { if (m.gspeed >= 0) { return m.gspeed; }; return rng_range(r, 0, 4); }; fn step_col(m: *cmat, c: i32, r: *rng) void = { if (m.counter[c] < m.speed[c]) { m.counter[c] = m.counter[c] + 1; return; }; m.counter[c] = 0; let base = c * MAX_TRAIL; let i = m.length[c] - 1; for (i > 0) { m.glyphs[base + i] = m.glyphs[base + i - 1]; i -= 1; }; m.glyphs[base + 0] = rng_glyph(r); m.head[c] = m.head[c] + 1; let off = m.head[c] - m.length[c]; if (off >= 0) { if (off < m.h) { nc_mvaddch(off, c, 32u32); // ' ' }; }; if (m.head[c] - m.length[c] >= m.h) { m.head[c] = -rng_range(r, 0, m.h / 2); m.length[c] = rng_range(r, 4, MAX_TRAIL); m.speed[c] = fresh_speed(m, r); m.counter[c] = 0; }; }; fn paint_col(m: *cmat, c: i32) void = { let base = c * MAX_TRAIL; let len = m.length[c]; let head = m.head[c]; let i = 0; for (i < len) { let y = head - i; if (y < 0) { break; }; if (y >= m.h) { i += 1; continue; }; let pair = CP_BODY; let attr = 0; if (i == 0) { pair = CP_HEAD; attr = A_BOLD; } else if (i == 1) { pair = CP_BRIGHT; attr = A_BOLD; } else if (i > len - 4) { pair = CP_DIM; }; let mask = nc_color_pair(pair) | attr; nc_attron(mask); nc_mvaddch(y, c, m.glyphs[base + i]: u32); nc_attroff(mask); i += 1; }; }; // paint_flash — error overlay shown while m.flash > 0. fn paint_flash(m: *cmat) void = { if (m.flash <= 0) { return; }; let msg = " [error] '0' is not a valid speed — use 1..4 "; let attr = nc_color_pair(CP_HEAD) | A_BOLD; nc_attron(attr); nc_mvaddstr(m.h - 1, 0, msg.ptr); nc_attroff(attr); m.flash -= 1; }; // ---- keyboard --------------------------------------------------------- type action = enum i32 { NONE = 0, QUIT = 1, PAUSE = 2, }; // poll_key — non-blocking. (i32 | void) is the Hare-shaped optional // shape; match handles both. fn poll_key() (i32 | void) = { let c = nc_getch(); if (c == -1) { return; }; return c; }; // classify — uses `switch` to fold the 'top-level' control keys into // the action enum. Other digits and letters fall through to NONE and // are dispatched by the more specific parsers below. fn classify(c: i32) action = { switch (c) { case 113, 81: return action.QUIT; // 'q' / 'Q' case 32: return action.PAUSE; // space }; return action.NONE; }; // digit_to_delay — three-variant tagged union: // i32 — successful parse; the value is a frame-delay (0..6) // speederr — the user pressed '0', a known-invalid speed // void — the key wasn't a digit at all (caller can ignore) fn digit_to_delay(c: i32) (i32 | speederr | void) = { if (c == 48) { return speederr{}; }; // '0' invalid if (c == 49) { return 6; }; // '1' slowest if (c == 50) { return 3; }; // '2' if (c == 51) { return 1; }; // '3' if (c == 52) { return 0; }; // '4' fastest return; // not a digit }; // key_to_theme — 'r'/'g'/'b' map to a theme variant; everything else // yields void. fn key_to_theme(c: i32) (theme | void) = { if (c == 114) { return theme.RED; }; // 'r' if (c == 103) { return theme.GREEN; }; // 'g' if (c == 98) { return theme.BLUE; }; // 'b' return; }; // apply_speed — set every column's per-advance delay to `d`. Also // records the global override so respawned columns adopt the new speed. fn apply_speed(m: *cmat, d: i32) void = { m.gspeed = d; let c = 0; for (c < m.w) { m.speed[c] = d; m.counter[c] = 0; c += 1; }; }; // dispatch_extra — handle key codes that aren't QUIT/PAUSE. Returns // `true` iff the caller should erase the screen (the trail re-colour // after a theme switch leaves stale cells behind otherwise). // // Both matches are *expressions* — every arm `yield`s the erase-flag, // and the whole `match (...)` is the function's single return value. // The outer void arm yields the inner match-expression, so dispatch // stays declarative even with three levels of dispatch. fn dispatch_extra(m: *cmat, k: i32) bool = { return match (digit_to_delay(k)) { case let d: i32 => { apply_speed(m, d); yield false; }; case let e: speederr => { m.flash = FLASH_FRAMES; yield false; }; case void => yield match (key_to_theme(k)) { case let t: theme => { m.gtheme = t: i32; apply_theme(t); yield true; }; case void => yield false; }; }; }; // ---- run -------------------------------------------------------------- fn run(seed: u64) (i32 | initerr) = { // `?` propagation — if setup returns initerr, run returns initerr. let scr = setup()?; let w = nc_getmaxx(scr); let h = nc_getmaxy(scr); if (w < 8 || h < 4) { nc_endwin(); return "terminal too small": initerr; }; let r = rng { state = seed }; if (r.state == 0u64) { r.state = 14695981039346656037u64; }; let m = cmat_alloc(w, h, &r); nc_erase(); let running = true; let paused = false; let frames = 0; for (running) { let evt = poll_key(); // outer match — dispatch on the variant. match (evt) { case let k: i32 => { let a = classify(k); if (a == action.QUIT) { running = false; }; if (a == action.PAUSE) { paused = !paused; }; if (a == action.NONE) { if (dispatch_extra(m, k)) { nc_erase(); }; }; }; case void => { }; }; if (running) { if (!paused) { let c = 0; for (c < m.w) { step_col(m, c, &r); paint_col(m, c); c += 1; }; }; paint_flash(m); nc_refresh(); frames += 1; nc_napms(60); }; }; cmat_free(m); nc_endwin(); return frames; }; // ---- entry ------------------------------------------------------------ export fn main() i32 = { // Clock is a pre-init dependency; time.now aborts on // syscall failure (EINVAL / EFAULT are programmer errors). let seed = now_ns(); // match-as-expression: every arm `yield`s the exit code, and the // whole match resolves to a single i32. One return site for the // function — Hare-shaped, no early returns scattered across arms. let r = run(seed); let code = match (r) { case let v: i32 => yield 0; case let e: initerr => { nc_endwin(); fmt.errorln("cmatrix: setup failed"); yield 1; }; }; return code; };