diff options
Diffstat (limited to 'src/main.zig')
| -rw-r--r-- | src/main.zig | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..7dcc923 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,632 @@ +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] +; + +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)); + } + } +} |
