extern fn zm_enter() c_int; extern fn zm_leave() void; extern fn zm_install(lock: c_int) void; extern fn zm_stopped() c_int; extern fn zm_size(rows: *c_int, cols: *c_int) c_int; extern fn zm_wait(ms: c_int) c_int; extern fn zm_read(buf: [*]u8, n: usize) isize; extern fn zm_write(fd: c_int, buf: [*]const u8, n: usize) void; extern fn zm_seed() u64; extern fn calloc(n: usize, size: usize) ?*anyopaque; extern fn free(p: ?*anyopaque) void; extern fn exit(code: c_int) noreturn; const version_text = "zigmatrix version 0.1-alpine\n"; const usage_text = "usage: zigmatrix -[abBcfhlLnorsmVxk] [-u delay] [-C color] [-t tty] [-M message]\n"; const color_help = "Invalid color selection\n" ++ "Valid colors are green, red, blue, white, yellow, cyan, magenta and black.\n"; const clear_seq = "\x1b[0m\x1b[2J"; const Opts = struct { asynch: bool = false, bold: u2 = 0, oldstyle: bool = false, screensaver: bool = false, lock: bool = false, rainbow: bool = false, lambda: bool = false, changes: bool = false, japanese: bool = false, console: bool = false, update: u32 = 4, color: u8 = 2, }; const Charset = struct { min: i32, num: i32 }; var opt: Opts = .{}; var charset: Charset = .{ .min = 33, .num = 93 }; var paused = false; var raw_active = false; var msg_cps: []const u32 = &.{}; var g_argc: usize = 0; var g_argv: [*][*:0]const u8 = undefined; const BOLD: u8 = 8; const rainbow_colors = [6]u8{ 1, 3, 2, 6, 4, 5 }; fn writeAll(fd: c_int, bytes: []const u8) void { zm_write(fd, bytes.ptr, bytes.len); } fn alloc(comptime T: type, n: usize) error{OutOfMemory}![]T { const raw_ptr = calloc(n, @sizeOf(T)) orelse return error.OutOfMemory; const typed_ptr: [*]T = @ptrCast(@alignCast(raw_ptr)); return typed_ptr[0..n]; } fn release(s: anytype) void { free(@ptrCast(s.ptr)); } var rng_state: u64 = 0x2545F4914F6CDD1D; fn rnd(n: i32) i32 { rng_state +%= 0x9e3779b97f4a7c15; var z_val = rng_state; z_val = (z_val ^ (z_val >> 30)) *% 0xbf58476d1ce4e5b9; z_val = (z_val ^ (z_val >> 27)) *% 0x94d049bb133111eb; z_val ^= z_val >> 31; return @intCast((z_val >> 33) % @as(u64, @intCast(n))); } fn finish() noreturn { if (raw_active) { writeAll(1, "\x1b[0m\x1b[?25h\x1b[?1049l"); zm_leave(); } exit(0); } const Size = struct { lines: usize, cols: usize }; fn termSize() Size { var rows: c_int = 0; var cols: c_int = 0; if (zm_size(&rows, &cols) != 0 or rows <= 0 or cols <= 0) { return .{ .lines = 24, .cols = 80 }; } return .{ .lines = @intCast(rows), .cols = @intCast(cols) }; } fn putNum(out: []u8, op: usize, n: usize) usize { var tmp: [20]u8 = undefined; var k: usize = 0; var x = n; while (true) { tmp[k] = @intCast('0' + (x % 10)); k += 1; x /= 10; if (x == 0) break; } var o = op; while (k > 0) { k -= 1; out[o] = tmp[k]; o += 1; } return o; } fn moveTo(out: []u8, op: usize, r: usize, c: usize) usize { var o = op; out[o] = 0x1b; out[o + 1] = '['; o += 2; o = putNum(out, o, r + 1); out[o] = ';'; o += 1; o = putNum(out, o, c + 1); out[o] = 'H'; return o + 1; } fn setAttr(out: []u8, op: usize, at: u8) usize { const head: []const u8 = if ((at & BOLD) != 0) "\x1b[0;1;3" else "\x1b[0;3"; var o = op; @memcpy(out[o..][0..head.len], head); o += head.len; out[o] = @intCast('0' + (at & 7)); o += 1; const tail = "m"; @memcpy(out[o..][0..tail.len], tail); return o + tail.len; } fn putUtf8(out: []u8, op: usize, cp: u32) usize { if (cp < 0x80) { out[op] = @intCast(cp); return op + 1; } else if (cp < 0x800) { out[op] = @intCast(0xC0 | (cp >> 6)); out[op + 1] = @intCast(0x80 | (cp & 0x3F)); return op + 2; } else if (cp < 0x10000) { out[op] = @intCast(0xE0 | (cp >> 12)); out[op + 1] = @intCast(0x80 | ((cp >> 6) & 0x3F)); out[op + 2] = @intCast(0x80 | (cp & 0x3F)); return op + 3; } out[op] = @intCast(0xF0 | (cp >> 18)); out[op + 1] = @intCast(0x80 | ((cp >> 12) & 0x3F)); out[op + 2] = @intCast(0x80 | ((cp >> 6) & 0x3F)); out[op + 3] = @intCast(0x80 | (cp & 0x3F)); return op + 4; } fn glyph(v: i32) u32 { return if (opt.lambda) 0x3bb else @intCast(v); } const Sim = struct { lines: usize, cols: usize, val: []i32, head: []bool, length: []i32, spaces: []i32, updates: []i32, nxt_cp: []u32, nxt_at: []u8, scr_cp: []u32, scr_at: []u8, out: []u8, fn init(lines: usize, cols: usize) !Sim { const cells = lines * cols; return .{ .lines = lines, .cols = cols, .val = try alloc(i32, (lines + 1) * cols), .head = try alloc(bool, (lines + 1) * cols), .length = try alloc(i32, cols), .spaces = try alloc(i32, cols), .updates = try alloc(i32, cols), .nxt_cp = try alloc(u32, cells), .nxt_at = try alloc(u8, cells), .scr_cp = try alloc(u32, cells), .scr_at = try alloc(u8, cells), .out = try alloc(u8, cells * 32 + 64), }; } fn deinit(s: *Sim) void { release(s.val); release(s.head); release(s.length); release(s.spaces); release(s.updates); release(s.nxt_cp); release(s.nxt_at); release(s.scr_cp); release(s.scr_at); release(s.out); } fn reset(s: *Sim) void { @memset(s.val, -1); @memset(s.head, false); @memset(s.length, 0); @memset(s.spaces, 0); @memset(s.updates, 0); @memset(s.scr_cp, ' '); @memset(s.scr_at, 0); const lines_i: i32 = @intCast(s.lines); var j: usize = 0; while (j < s.cols) : (j += 2) { s.spaces[j] = rnd(lines_i) + 1; s.length[j] = rnd(@max(lines_i - 3, 1)) + 3; s.val[s.cols + j] = ' '; s.updates[j] = rnd(3) + 1; } } fn stepColumn(s: *Sim, j: usize) void { const cols = s.cols; const last = s.lines; const lines_i: i32 = @intCast(s.lines); const v = s.val; const hd = s.head; const cs = charset; if (opt.oldstyle) { var i: usize = last; while (i > 0) : (i -= 1) { v[i * cols + j] = v[(i - 1) * cols + j]; } const random = rnd(cs.num + 8) + cs.min; const below = v[cols + j]; if (below == ' ' or below == -1) { if (s.spaces[j] > 0) { v[j] = ' '; s.spaces[j] -= 1; } else { v[j] = rnd(cs.num) + cs.min; s.spaces[j] = rnd(lines_i) + 1; } } else if (random > cs.num and below != 1) { v[j] = ' '; hd[j] = false; } else { v[j] = rnd(cs.num) + cs.min; } return; } if (v[j] == -1 and v[cols + j] == ' ') { if (s.spaces[j] > 0) { s.spaces[j] -= 1; } else { s.length[j] = rnd(@max(lines_i - 3, 1)) + 3; v[j] = rnd(cs.num) + cs.min; hd[j] = true; s.spaces[j] = rnd(lines_i) + 1; } } var i: usize = 0; var first_done = false; while (i <= last) { while (i <= last and (v[i * cols + j] == ' ' or v[i * cols + j] == -1)) { i += 1; } if (i > last) break; const z = i; var y: i32 = 0; while (i <= last and v[i * cols + j] != ' ' and v[i * cols + j] != -1) { hd[i * cols + j] = false; if (opt.changes and rnd(8) == 0) { v[i * cols + j] = rnd(cs.num) + cs.min; } i += 1; y += 1; } if (i > last) { v[z * cols + j] = ' '; continue; } v[i * cols + j] = rnd(cs.num) + cs.min; hd[i * cols + j] = true; if (y > s.length[j] or first_done) { v[z * cols + j] = ' '; v[j] = -1; } first_done = true; i += 1; } } fn render(s: *Sim) void { const cols = s.cols; const y_offset: usize = if (opt.oldstyle) 0 else 1; var r: usize = 0; while (r < s.lines) : (r += 1) { const base = (r + y_offset) * cols; const nb = r * cols; var c: usize = 0; while (c < cols) : (c += 1) { var cp: u32 = ' '; var at: u8 = 0; if ((c & 1) == 0) { const v = s.val[base + c]; if (v != -1 and v != ' ') { cp = glyph(v); if (s.head[base + c] and !opt.rainbow) { at = 7 | (if (opt.bold != 0) BOLD else 0); } else { const col: u8 = if (opt.rainbow) rainbow_colors[@intCast(rnd(6))] else opt.color; const b = opt.bold == 2 or (opt.bold == 1 and (v & 1) == 0); at = col | (if (b) BOLD else 0); } } } s.nxt_cp[nb + c] = cp; s.nxt_at[nb + c] = at; } } } fn put(s: *Sim, r: isize, c: isize, cp: u32, at: u8) void { if (r < 0 or c < 0) return; const ru: usize = @intCast(r); const cu: usize = @intCast(c); if (ru >= s.lines or cu >= s.cols) return; const idx = ru * s.cols + cu; s.nxt_cp[idx] = cp; s.nxt_at[idx] = at; } fn overlay(s: *Sim) void { const ml: isize = @intCast(msg_cps.len); const my: isize = @intCast(s.lines / 2); const mx: isize = @as(isize, @intCast(s.cols / 2)) - @divTrunc(ml, 2); var rr: isize = my - 1; while (rr <= my + 1) : (rr += 1) { var cc: isize = mx - 2; while (cc < mx + ml + 2) : (cc += 1) { const inside = rr == my and cc >= mx and cc < mx + ml; const cp: u32 = if (inside) msg_cps[@intCast(cc - mx)] else ' '; const at: u8 = if (cp == ' ') 0 else 7 | BOLD; s.put(rr, cc, cp, at); } } } fn flush(s: *Sim) void { const out_buf = s.out; const cols = s.cols; var op: usize = 0; var cur_r: usize = ~@as(usize, 0); var cur_c: usize = 0; var cur_at: u16 = 0xffff; var r: usize = 0; while (r < s.lines) : (r += 1) { var c: usize = 0; while (c < cols) : (c += 1) { const idx = r * cols + c; const cp = s.nxt_cp[idx]; const at = s.nxt_at[idx]; if (cp == s.scr_cp[idx] and at == s.scr_at[idx]) continue; s.scr_cp[idx] = cp; s.scr_at[idx] = at; if (cur_r != r or cur_c != c) { op = moveTo(out_buf, op, r, c); cur_r = r; } if (cp != ' ' and cur_at != at) { op = setAttr(out_buf, op, at); cur_at = at; } op = putUtf8(out_buf, op, cp); cur_c = c + 1; if (cur_c == cols) cur_r = ~@as(usize, 0); } } if (op > 0) writeAll(1, out_buf[0..op]); } }; fn handleKey(b: u8) void { switch (b) { 'q', 3 => if (!opt.lock) finish(), 'a' => opt.asynch = !opt.asynch, 'b' => opt.bold = 1, 'B' => opt.bold = 2, 'n' => opt.bold = 0, 'p', 'P' => paused = !paused, '0'...'9' => opt.update = b - '0', '!' => opt.color = 1, '@' => opt.color = 2, '#' => opt.color = 3, '$' => opt.color = 4, '%' => opt.color = 5, '^' => opt.color = 6, '&' => opt.color = 7, else => {}, } } fn handleInput() void { if (zm_wait(0) == 0) return; var buf: [64]u8 = undefined; const got = zm_read(&buf, buf.len); if (got <= 0) return; const n: usize = @intCast(got); if (opt.screensaver and !opt.lock) finish(); var i: usize = 0; while (i < n) { const b = buf[i]; i += 1; if (b == 0x1b) { if (i < n and (buf[i] == '[' or buf[i] == 'O')) { i += 1; while (i < n and !(buf[i] >= 0x40 and buf[i] <= 0x7e)) { i += 1; } i += 1; } continue; } handleKey(b); } } fn spanZ(p: [*:0]const u8) []const u8 { var n: usize = 0; while (p[n] != 0) n += 1; return p[0..n]; } fn argAt(i: usize) []const u8 { return spanZ(g_argv[i]); } fn streq(a: []const u8, b: []const u8) bool { if (a.len != b.len) return false; for (a, b) |x, y| { if (x != y) return false; } return true; } fn parseUint(s: []const u8) ?u32 { if (s.len == 0) return null; var v: u32 = 0; for (s) |c| { if (c < '0' or c > '9') return null; v = v *| 10 +| (c - '0'); } return v; } fn badUsage() noreturn { writeAll(2, usage_text); exit(1); } fn badColor() noreturn { writeAll(2, color_help); exit(1); } fn parseColor(name: []const u8) ?u8 { const names = [_][]const u8{ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white" }; for (names, 0..) |n, i| { if (streq(n, name)) return @intCast(i); } return null; } fn setMessage(m: []const u8) void { if (m.len == 0) return; const buf = alloc(u32, m.len) catch return; var n: usize = 0; var i: usize = 0; while (i < m.len) { const b = m[i]; var cp: u32 = b; var extra: usize = 0; if (b >= 0xF0) { cp = b & 0x07; extra = 3; } else if (b >= 0xE0) { cp = b & 0x0F; extra = 2; } else if (b >= 0xC0) { cp = b & 0x1F; extra = 1; } i += 1; var k: usize = 0; while (k < extra and i < m.len) : (k += 1) { cp = (cp << 6) | (m[i] & 0x3F); i += 1; } buf[n] = cp; n += 1; } msg_cps = buf[0..n]; } fn parseArgs() void { var i: usize = 1; outer: while (i < g_argc) : (i += 1) { const a = argAt(i); if (a.len < 2 or a[0] != '-') continue; if (streq(a, "--")) break; var k: usize = 1; while (k < a.len) : (k += 1) { const ch = a[k]; switch (ch) { 'a' => opt.asynch = true, 'b' => opt.bold = 1, 'B' => opt.bold = 2, 'c' => opt.japanese = true, 'f' => {}, 'l', 'x' => opt.console = true, 'L' => opt.lock = true, 'n' => opt.bold = 0, 'o' => opt.oldstyle = true, 'r' => opt.rainbow = true, 'm' => opt.lambda = true, 'k' => opt.changes = true, 's' => opt.screensaver = true, 'v', 'V' => { writeAll(1, version_text); exit(0); }, 'h' => { writeAll(1, usage_text); exit(0); }, 'u', 'C', 'M', 't' => { var val: []const u8 = a[k + 1 ..]; if (val.len == 0) { i += 1; if (i >= g_argc) badUsage(); val = argAt(i); } switch (ch) { 'u' => opt.update = @min(parseUint(val) orelse badUsage(), 10), 'C' => opt.color = parseColor(val) orelse badColor(), 'M' => setMessage(val), else => {}, } continue :outer; }, else => badUsage(), } } } } pub export fn main(argc: c_int, argv: [*][*:0]const u8) c_int { g_argc = if (argc > 0) @intCast(argc) else 0; g_argv = argv; parseArgs(); if (opt.console) { charset = .{ .min = 166, .num = 51 }; } else if (opt.japanese) { charset = .{ .min = 0xFF66, .num = 56 }; } if (zm_enter() != 0) { writeAll(2, "zigmatrix: stdin is not a terminal\n"); return 1; } raw_active = true; zm_install(if (opt.lock) 1 else 0); rng_state = zm_seed(); writeAll(1, "\x1b[?1049h\x1b[?25l" ++ clear_seq); var size = termSize(); var sim = Sim.init(size.lines, size.cols) catch finish(); sim.reset(); var count: i32 = 0; while (true) { if (zm_stopped() != 0) finish(); handleInput(); const ns = termSize(); if (ns.lines != size.lines or ns.cols != size.cols) { size = ns; sim.deinit(); sim = Sim.init(size.lines, size.cols) catch finish(); sim.reset(); writeAll(1, clear_seq); } count += 1; if (count > 4) count = 1; if (!paused) { var j: usize = 0; while (j < sim.cols) : (j += 2) { if (count > sim.updates[j] or !opt.asynch) { sim.stepColumn(j); } } sim.render(); if (msg_cps.len > 0) { sim.overlay(); } sim.flush(); } if (opt.update > 0) { _ = zm_wait(@intCast(opt.update * 10)); } } }