diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-02 21:56:01 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | 589269c0b3ca74a29f5889fa6e781e59cef7efdb (patch) | |
| tree | ecd2ce3060d06353f3b6302e70abf784295ff40f /replay.zig | |
| parent | 81992cc92743a98066ee1a9025bc74f91cf0e967 (diff) | |
| download | pardes-589269c0b3ca74a29f5889fa6e781e59cef7efdb.tar.gz pardes-589269c0b3ca74a29f5889fa6e781e59cef7efdb.zip | |
wasm replay ui
Diffstat (limited to 'replay.zig')
| -rw-r--r-- | replay.zig | 345 |
1 files changed, 239 insertions, 106 deletions
@@ -1,10 +1,30 @@ const std = @import("std"); +const builtin = @import("builtin"); const ui = @import("ui"); const ghostty_vt = @import("ghostty-vt"); +const replay_config = @import("replay_config"); const dump = @import("dump.zig"); const paint = @import("paint.zig"); const image = @import("image.zig"); +const is_emscripten = builtin.os.tag == .emscripten; +const embedded_dump = if (replay_config.embedded_dump) @embedFile("replay.dump.zon") else ""; +const wasm_gpa_buffer_size = 1024 * 1024 * 1024; + +var wasm_gpa_buffer: [if (is_emscripten) wasm_gpa_buffer_size else 0]u8 align(16) = undefined; +var wasm_gpa_fba: std.heap.FixedBufferAllocator = undefined; + +extern "c" fn emscripten_console_error(utf8String: [*:0]const u8) void; + +pub const std_options_debug_threaded_io: ?*std.Io.Threaded = if (is_emscripten) + null +else + std.Io.Threaded.global_single_threaded; +pub const std_options_debug_io: std.Io = if (is_emscripten) + std.Io.failing +else + std_options_debug_threaded_io.?.io(); + const TOPBAR_H: u16 = 1; const GUTTER: u16 = 2; const BOX_H: u16 = 1; @@ -455,6 +475,15 @@ fn deinitPanes(gpa: std.mem.Allocator, panes: []Pane) void { } } +fn terminalScrollbackLimit(src: dump.Pane) usize { + if (!is_emscripten) return 16 * 1024 * 1024; + const cols: usize = @max(1, src.cols); + const rows: usize = @max(1, src.rows); + const visible_and_scrolled = rows + src.scroll + 8; + const estimated = visible_and_scrolled * cols * 128; + return std.math.clamp(estimated, 64 * 1024, 1024 * 1024); +} + fn buildPane(gpa: std.mem.Allocator, src: dump.Pane, out: *Pane) !void { out.* = .{ .kind = src.kind, @@ -490,7 +519,7 @@ fn buildPane(gpa: std.mem.Allocator, src: dump.Pane, out: *Pane) !void { .term = try ghostty_vt.Terminal.init(gpa, .{ .cols = @max(1, src.cols), .rows = @max(1, src.rows), - .max_scrollback = 16 * 1024 * 1024, + .max_scrollback = terminalScrollbackLimit(src), }), .stream = undefined, }; @@ -518,97 +547,148 @@ fn buildPane(gpa: std.mem.Allocator, src: dump.Pane, out: *Pane) !void { clampScroll(out); } -pub fn main(init: std.process.Init) !void { - const io = init.io; - const gpa = init.gpa; - const args = try init.minimal.args.toSlice(init.arena.allocator()); - if (args.len != 2) { - try printUsage(io); - return error.BadArgs; - } +const App = struct { + io: std.Io, + gpa: std.mem.Allocator, + state: dump.State, + panes_buf: [dump.max_panes]Pane, + panes: []Pane, + layout: Layout, + screen_w: u16, + screen_h: u16, + active: usize, + tty_buf: [0x10000]u8, + tty: ui.Tty, + vx: ui.Vaxis, + loop: Loop, + rects: [dump.max_panes]Rect, + col_x: [dump.max_cols]u16, + col_w: [dump.max_cols]u16, + drag: Drag = .none, - const state = try dump.readFile(io, gpa, args[1]); - defer dump.free(gpa, state); - _ = paint.setThemeByName(state.theme); - image.start(io, gpa); - defer image.stop(); + fn init( + self: *App, + io: std.Io, + gpa: std.mem.Allocator, + env_map: *std.process.Environ.Map, + args: []const [:0]const u8, + ) !void { + if (!replay_config.embedded_dump and args.len != 2) { + try printUsage(io); + return error.BadArgs; + } - var panes_buf: [dump.max_panes]Pane = undefined; - for (state.panes, 0..) |src, i| try buildPane(gpa, src, &panes_buf[i]); - var panes = panes_buf[0..state.panes.len]; - defer deinitPanes(gpa, panes); + self.io = io; + self.gpa = gpa; + self.state = if (replay_config.embedded_dump) + try dump.readZonZ(gpa, embedded_dump, "embedded replay dump") + else + try dump.readFile(io, gpa, args[1]); + _ = paint.setThemeByName(self.state.theme); + image.start(io, gpa); - var layout: Layout = .{ - .ncol = state.columns.len, - .col_weight = @splat(1), - .col_terms = undefined, - .col_n = @splat(0), - }; - for (state.columns, 0..) |col, c| { - layout.col_weight[c] = col.weight; - layout.col_n[c] = col.panes.len; - for (col.panes, 0..) |pane, k| layout.col_terms[c][k] = pane; - } + for (self.state.panes, 0..) |src, i| { + try buildPane(gpa, src, &self.panes_buf[i]); + } + self.panes = self.panes_buf[0..self.state.panes.len]; + + self.layout = .{ + .ncol = self.state.columns.len, + .col_weight = @splat(1), + .col_terms = undefined, + .col_n = @splat(0), + }; + for (self.state.columns, 0..) |col, c| { + self.layout.col_weight[c] = col.weight; + self.layout.col_n[c] = col.panes.len; + for (col.panes, 0..) |pane, k| self.layout.col_terms[c][k] = pane; + } + + self.screen_w = if (self.state.screen.cols > 0) self.state.screen.cols else 80; + self.screen_h = if (self.state.screen.rows > 0) self.state.screen.rows else 24; + self.active = self.state.active; + self.drag = .none; + + self.tty = try ui.Tty.init(io, &self.tty_buf); + self.vx = try ui.init(io, gpa, env_map, .{}); + try self.vx.setMouseMode(self.tty.writer(), true); + try self.vx.enterAltScreen(self.tty.writer()); + if (is_emscripten) { + try self.vx.resize(gpa, self.tty.writer(), .{ + .cols = self.screen_w, + .rows = self.screen_h, + .x_pixel = 0, + .y_pixel = 0, + }); + } - var screen_w: u16 = if (state.screen.cols > 0) state.screen.cols else 80; - var screen_h: u16 = if (state.screen.rows > 0) state.screen.rows else 24; - var active = state.active; + self.loop = .init(io, &self.tty, &self.vx); + try self.loop.start(); + try self.loop.installResizeHandler(); - var tty_buf: [0x10000]u8 = undefined; - var tty = try ui.Tty.init(io, &tty_buf); - var vx = try ui.init(io, gpa, init.environ_map, .{}); - defer vx.deinit(gpa, tty.writer()); - try vx.setMouseMode(tty.writer(), true); - try vx.enterAltScreen(tty.writer()); - defer vx.exitAltScreen(tty.writer()) catch {}; + computeGeom(&self.layout, self.panes, self.screen_w, self.screen_h, &self.rects, &self.col_x, &self.col_w); + } + + fn deinit(self: *App) void { + self.loop.stop(); + self.vx.exitAltScreen(self.tty.writer()) catch {}; + self.vx.deinit(self.gpa, self.tty.writer()); + deinitPanes(self.gpa, self.panes); + image.stop(); + dump.free(self.gpa, self.state); + } - var loop: Loop = .init(io, &tty, &vx); - try loop.start(); - defer loop.stop(); - try loop.installResizeHandler(); + fn run(self: *App) !void { + while (try self.step(try self.loop.nextEvent())) {} + } - var rects: [dump.max_panes]Rect = undefined; - var col_x: [dump.max_cols]u16 = undefined; - var col_w: [dump.max_cols]u16 = undefined; - computeGeom(&layout, panes, screen_w, screen_h, &rects, &col_x, &col_w); + fn frame(self: *App) !bool { + var had_event = false; + while (try self.loop.tryEvent()) |event| { + had_event = true; + if (!try self.step(event)) return false; + } + if (!had_event) return try self.step(.tick); + return true; + } - var drag: Drag = .none; - while (true) { - const event = try loop.nextEvent(); + fn step(self: *App, event: @TypeOf(Command.value)) !bool { switch (event) { .winsize => |ws| { - screen_w = ws.cols; - screen_h = ws.rows; - try vx.resize(gpa, tty.writer(), ws); + if (!is_emscripten) { + self.screen_w = ws.cols; + self.screen_h = ws.rows; + try self.vx.resize(self.gpa, self.tty.writer(), ws); + } }, .mouse => |mouse| { - const mcol: u16 = if (mouse.col < 0) 0 else @min(@as(u16, @intCast(mouse.col)), screen_w -| 1); - const mrow: u16 = if (mouse.row < 0) 0 else @min(@as(u16, @intCast(mouse.row)), screen_h -| 1); - const hovered = paneAt(&rects, panes.len, mcol, mrow); + const mcol: u16 = if (mouse.col < 0) 0 else @min(@as(u16, @intCast(mouse.col)), self.screen_w -| 1); + const mrow: u16 = if (mouse.row < 0) 0 else @min(@as(u16, @intCast(mouse.row)), self.screen_h -| 1); + const hovered = paneAt(&self.rects, self.panes.len, mcol, mrow); if (mouse.button == .wheel_down and mouse.type == .press) { - if (hovered) |id| scrollBy(&panes[id], 1); + if (hovered) |id| scrollBy(&self.panes[id], 1); } else if (mouse.button == .wheel_up and mouse.type == .press) { - if (hovered) |id| scrollBy(&panes[id], -1); + if (hovered) |id| scrollBy(&self.panes[id], -1); } else if (mouse.type == .press and mouse.button == .left) { var handled = false; var c: usize = 0; - while (c + 1 < layout.ncol and !handled) : (c += 1) { - if (mcol == col_x[c] + col_w[c] -| 1) { - drag = .{ .border_v = .{ .left_col = c, .cur_x = mcol } }; + while (c + 1 < self.layout.ncol and !handled) : (c += 1) { + if (mcol == self.col_x[c] + self.col_w[c] -| 1) { + self.drag = .{ .border_v = .{ .left_col = c, .cur_x = mcol } }; handled = true; } } if (!handled) { var cc: usize = 0; - outer: while (cc < layout.ncol) : (cc += 1) { - if (mcol < col_x[cc] or mcol >= col_x[cc] + col_w[cc]) continue; + outer: while (cc < self.layout.ncol) : (cc += 1) { + if (mcol < self.col_x[cc] or mcol >= self.col_x[cc] + self.col_w[cc]) continue; var k: usize = 0; - while (k + 1 < layout.col_n[cc]) : (k += 1) { - const id = layout.col_terms[cc][k]; - const r = rects[id]; + while (k + 1 < self.layout.col_n[cc]) : (k += 1) { + const id = self.layout.col_terms[cc][k]; + const r = self.rects[id]; if (mrow == r.y + r.h -| 1) { - drag = .{ .border_h = .{ .col = cc, .top_idx = k, .cur_y = mrow } }; + self.drag = .{ .border_h = .{ .col = cc, .top_idx = k, .cur_y = mrow } }; handled = true; break :outer; } @@ -616,35 +696,35 @@ pub fn main(init: std.process.Init) !void { } } if (!handled) if (hovered) |id| { - active = id; - const r = rects[id]; + self.active = id; + const r = self.rects[id]; if (mcol < r.x + GUTTER and mrow < r.y + BOX_H) { - drag = .{ .move = .{ .id = id, .cur_x = mcol, .cur_y = mrow } }; + self.drag = .{ .move = .{ .id = id, .cur_x = mcol, .cur_y = mrow } }; } else if (mcol < r.x + GUTTER) { const local: i32 = @as(i32, mrow) - @as(i32, r.y + BOX_H); - scrollBy(&panes[id], -local); + scrollBy(&self.panes[id], -local); } }; } else if (mouse.type == .press) { - if (hovered) |id| active = id; + if (hovered) |id| self.active = id; } else if (mouse.type == .drag) { - switch (drag) { + switch (self.drag) { .border_v => |*d| { const c = d.left_col; - if (c + 1 < layout.ncol) { - const lo = col_x[c] + MINW; - const hi = col_x[c] +| col_w[c] +| col_w[c + 1] -| MINW; + if (c + 1 < self.layout.ncol) { + const lo = self.col_x[c] + MINW; + const hi = self.col_x[c] +| self.col_w[c] +| self.col_w[c + 1] -| MINW; d.cur_x = if (lo <= hi) std.math.clamp(mcol, lo, hi) else mcol; } }, .border_h => |*d| { const c = d.col; const k = d.top_idx; - if (k + 1 < layout.col_n[c]) { - const a = layout.col_terms[c][k]; - const b = layout.col_terms[c][k + 1]; - const lo = rects[a].y + MINH; - const hi = rects[a].y +| rects[a].h +| rects[b].h -| MINH; + if (k + 1 < self.layout.col_n[c]) { + const a = self.layout.col_terms[c][k]; + const b = self.layout.col_terms[c][k + 1]; + const lo = self.rects[a].y + MINH; + const hi = self.rects[a].y +| self.rects[a].h +| self.rects[b].h -| MINH; d.cur_y = if (lo <= hi) std.math.clamp(mrow, lo, hi) else mrow; } }, @@ -655,58 +735,111 @@ pub fn main(init: std.process.Init) !void { .none => {}, } } else if (mouse.type == .release) { - switch (drag) { + switch (self.drag) { .border_v => |d| { const c = d.left_col; - if (c + 1 < layout.ncol) { - const combined: f32 = @floatFromInt(col_w[c] + col_w[c + 1]); - var nl: f32 = @floatFromInt((d.cur_x + 1) -| col_x[c]); + if (c + 1 < self.layout.ncol) { + const combined: f32 = @floatFromInt(self.col_w[c] + self.col_w[c + 1]); + var nl: f32 = @floatFromInt((d.cur_x + 1) -| self.col_x[c]); nl = std.math.clamp(nl, @as(f32, MINW), @max(@as(f32, MINW), combined - @as(f32, MINW))); - const pair = layout.col_weight[c] + layout.col_weight[c + 1]; - layout.col_weight[c] = pair * (nl / combined); - layout.col_weight[c + 1] = pair - layout.col_weight[c]; + const pair = self.layout.col_weight[c] + self.layout.col_weight[c + 1]; + self.layout.col_weight[c] = pair * (nl / combined); + self.layout.col_weight[c + 1] = pair - self.layout.col_weight[c]; } }, .border_h => |d| { const c = d.col; const k = d.top_idx; - if (k + 1 < layout.col_n[c]) { - const a = layout.col_terms[c][k]; - const b = layout.col_terms[c][k + 1]; - const combined: f32 = @floatFromInt(rects[a].h + rects[b].h); - var nt: f32 = @floatFromInt((d.cur_y + 1) -| rects[a].y); + if (k + 1 < self.layout.col_n[c]) { + const a = self.layout.col_terms[c][k]; + const b = self.layout.col_terms[c][k + 1]; + const combined: f32 = @floatFromInt(self.rects[a].h + self.rects[b].h); + var nt: f32 = @floatFromInt((d.cur_y + 1) -| self.rects[a].y); nt = std.math.clamp(nt, @as(f32, MINH), @max(@as(f32, MINH), combined - @as(f32, MINH))); - const pair = panes[a].vweight + panes[b].vweight; - panes[a].vweight = pair * (nt / combined); - panes[b].vweight = pair - panes[a].vweight; + const pair = self.panes[a].vweight + self.panes[b].vweight; + self.panes[a].vweight = pair * (nt / combined); + self.panes[b].vweight = pair - self.panes[a].vweight; } }, - .move => |d| moveTerm(&layout, &rects, &col_x, &col_w, d.id, d.cur_x, d.cur_y), + .move => |d| moveTerm(&self.layout, &self.rects, &self.col_x, &self.col_w, d.id, d.cur_x, d.cur_y), .none => {}, } - drag = .none; + self.drag = .none; } }, .key_press => |key| { - if (key.matches('q', .{}) or key.matches(ui.Key.escape, .{}) or key.matches('c', .{ .ctrl = true })) break; + if (key.matches('q', .{}) or key.matches(ui.Key.escape, .{}) or key.matches('c', .{ .ctrl = true })) return false; }, - .paste => |text| gpa.free(text), - .quit, .close => break, + .paste => |text| self.gpa.free(text), + .quit, .close => return false, else => {}, } - computeGeom(&layout, panes, screen_w, screen_h, &rects, &col_x, &col_w); - for (panes, 0..) |*p, id| { - const r = rects[id]; + computeGeom(&self.layout, self.panes, self.screen_w, self.screen_h, &self.rects, &self.col_x, &self.col_w); + for (self.panes, 0..) |*p, id| { + const r = self.rects[id]; const cols = @max(1, r.w -| GUTTER); const rows = @max(1, r.h -| BOX_H); if (p.terminal) |*rt| { - if (cols != p.cols or rows != p.rows) rt.term.resize(gpa, cols, rows) catch {}; + if (cols != p.cols or rows != p.rows) rt.term.resize(self.gpa, cols, rows) catch {}; } p.cols = cols; p.rows = rows; clampScroll(p); } - try render(&vx, gpa, tty.writer(), state, panes, &rects, screen_w, active); + try render(&self.vx, self.gpa, self.tty.writer(), self.state, self.panes, &self.rects, self.screen_w, self.active); + return true; } +}; + +fn wasmFrame(arg: ?*anyopaque) callconv(.c) void { + const app: *App = @ptrCast(@alignCast(arg.?)); + if (app.frame() catch |err| { + std.log.err("replay frame failed: {s}", .{@errorName(err)}); + std.os.emscripten.emscripten_cancel_main_loop(); + app.deinit(); + app.gpa.destroy(app); + return; + }) return; + std.os.emscripten.emscripten_cancel_main_loop(); + app.deinit(); + app.gpa.destroy(app); +} + +pub const main = if (is_emscripten) wasmMain else nativeMain; + +comptime { + if (is_emscripten) @export(&emscriptenMain, .{ .name = "main" }); +} + +fn nativeMain(init: std.process.Init) !void { + const args = try init.minimal.args.toSlice(init.arena.allocator()); + var app: App = undefined; + try app.init(init.io, init.gpa, init.environ_map, args); + defer app.deinit(); + try app.run(); +} + +fn emscriptenMain(argc: c_int, argv: [*]?[*:0]u8) callconv(.c) c_int { + _ = argc; + _ = argv; + wasmMain() catch |err| { + var buf: [128]u8 = undefined; + const msg = std.fmt.bufPrintZ(&buf, "pardes replay wasm init failed: {s}", .{@errorName(err)}) catch "pardes replay wasm init failed"; + emscripten_console_error(msg.ptr); + return 1; + }; + return 0; +} + +fn wasmMain() !void { + wasm_gpa_fba = std.heap.FixedBufferAllocator.init(&wasm_gpa_buffer); + const gpa = wasm_gpa_fba.allocator(); + var env_map = std.process.Environ.Map.init(gpa); + defer env_map.deinit(); + + const app = try gpa.create(App); + errdefer gpa.destroy(app); + try app.init(std.Io.failing, gpa, &env_map, &.{}); + std.os.emscripten.emscripten_set_main_loop_arg(wasmFrame, app, 0, 0); } |
