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; const MINW: u16 = 10; const MINH: u16 = 3; const PREFIX_W: u16 = 5; const Rect = struct { x: u16, y: u16, w: u16, h: u16 }; const ReplayTerm = struct { term: ghostty_vt.Terminal, stream: ghostty_vt.TerminalStream, }; const Pane = struct { kind: dump.PaneKind, tag: []const u8, content: []const u8, owned_content: ?[]u8 = null, path: []const u8, terminal: ?ReplayTerm = null, image_view: ?image.View = null, image_bytes: []const u8 = &.{}, tty_mode: bool = false, highlights: []u8 = &.{}, scroll: usize, cols: u16, rows: u16, vweight: f32, fn isTerminal(self: *const Pane) bool { return self.terminal != null; } }; const Layout = struct { ncol: usize, col_weight: [dump.max_cols]f32, col_terms: [dump.max_cols][dump.max_panes]usize, col_n: [dump.max_cols]usize, }; const Drag = union(enum) { none, border_v: struct { left_col: usize, cur_x: u16 }, border_h: struct { col: usize, top_idx: usize, cur_y: u16 }, move: struct { id: usize, cur_x: u16, cur_y: u16 }, }; pub const Command = struct { prev: ?*Command = null, pub var value: union(enum) { close, nop, quit, tick, key_press: ui.Key, winsize: ui.Winsize, mouse: ui.Mouse, paste: []const u8, } = .nop; }; const Loop = ui.Loop(@TypeOf(Command.value)); const accent = paint.accent; const accent_dim = paint.accent_dim; const T = paint.T; const rgbColor = paint.rgbColor; const colorOpt = paint.colorOpt; const synStyle = paint.synStyle; const highlightFile = paint.highlightFile; const cellStyle = paint.cellStyle; fn printUsage(io: std.Io) !void { try std.Io.File.stdout().writeStreamingAll(io, "Usage: pardes-replay \n"); } fn lineCount(text: []const u8) usize { return std.mem.count(u8, text, "\n") + 1; } fn lineAt(text: []const u8, row: usize) []const u8 { var it = std.mem.splitScalar(u8, text, '\n'); var i: usize = 0; while (it.next()) |line| : (i += 1) { if (i == row) return line; } return ""; } fn clampScroll(p: *Pane) void { if (p.isTerminal()) return; const total = lineCount(p.content); const max_scroll = total -| @max(1, p.rows); if (p.scroll > max_scroll) p.scroll = max_scroll; } fn scrollBy(p: *Pane, delta: i32) void { if (p.terminal) |*rt| { rt.term.screens.active.scroll(.{ .delta_row = @intCast(delta) }); return; } const total = lineCount(p.content); const max_scroll: i64 = @intCast(total -| @max(1, p.rows)); const next = std.math.clamp(@as(i64, @intCast(p.scroll)) + delta, 0, max_scroll); p.scroll = @intCast(next); } fn promptFlags(rt: *ReplayTerm, flags: []bool) void { @memset(flags, false); var it = rt.term.screens.active.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); var i: usize = 0; while (it.next()) |pin| : (i += 1) { if (i >= flags.len) break; flags[i] = pin.rowAndCell().row.semantic_prompt != .none; } } fn terminalBodyText(alloc: std.mem.Allocator, p: *Pane) ![]const u8 { const rt = &p.terminal.?; const body = try rt.term.plainString(alloc); var flagbuf: [512]bool = undefined; const nrows: usize = @min(p.rows, flagbuf.len); const flags = flagbuf[0..nrows]; if (p.tty_mode) @memset(flags, false) else promptFlags(rt, flags); var out: std.ArrayList(u8) = .empty; errdefer out.deinit(alloc); var it = std.mem.splitAny(u8, body, "\n"); var i: usize = 0; while (i < nrows) : (i += 1) { if (i > 0) try out.append(alloc, '\n'); const raw = it.next() orelse ""; try out.appendSlice(alloc, if (flags[i]) "" else raw); } return out.toOwnedSlice(alloc); } fn layoutFindTerm(l: *const Layout, id: usize) ?struct { col: usize, idx: usize } { var c: usize = 0; while (c < l.ncol) : (c += 1) { var k: usize = 0; while (k < l.col_n[c]) : (k += 1) { if (l.col_terms[c][k] == id) return .{ .col = c, .idx = k }; } } return null; } fn layoutRemove(l: *Layout, id: usize) void { const f = layoutFindTerm(l, id) orelse return; const c = f.col; var k = f.idx; while (k + 1 < l.col_n[c]) : (k += 1) l.col_terms[c][k] = l.col_terms[c][k + 1]; l.col_n[c] -= 1; if (l.col_n[c] == 0 and l.ncol > 1) { l.col_weight[if (c > 0) c - 1 else c + 1] += l.col_weight[c]; var j = c; while (j + 1 < l.ncol) : (j += 1) { l.col_terms[j] = l.col_terms[j + 1]; l.col_n[j] = l.col_n[j + 1]; l.col_weight[j] = l.col_weight[j + 1]; } l.ncol -= 1; } } fn layoutInsert(l: *Layout, c: usize, idx: usize, id: usize) void { var k = l.col_n[c]; while (k > idx) : (k -= 1) l.col_terms[c][k] = l.col_terms[c][k - 1]; l.col_terms[c][idx] = id; l.col_n[c] += 1; } fn computeGeom( l: *const Layout, panes: []const Pane, sw: u16, sh: u16, rects: *[dump.max_panes]Rect, col_x: *[dump.max_cols]u16, col_w: *[dump.max_cols]u16, ) void { if (l.ncol == 0) return; var wsum: f32 = 0; for (0..l.ncol) |c| wsum += l.col_weight[c]; if (wsum <= 0) wsum = 1; var x: u16 = 0; for (0..l.ncol) |c| { const last = c + 1 == l.ncol; const cw: u16 = if (last) (sw -| x) else @max(1, @as(u16, @intFromFloat(@round(@as(f32, @floatFromInt(sw)) * l.col_weight[c] / wsum)))); col_x[c] = x; col_w[c] = cw; var vsum: f32 = 0; for (0..l.col_n[c]) |k| vsum += panes[l.col_terms[c][k]].vweight; if (vsum <= 0) vsum = 1; var y: u16 = TOPBAR_H; const avail_h = sh -| TOPBAR_H; for (0..l.col_n[c]) |k| { const id = l.col_terms[c][k]; const lastk = k + 1 == l.col_n[c]; const ch: u16 = if (lastk) (sh -| y) else @max(1, @as(u16, @intFromFloat(@round(@as(f32, @floatFromInt(avail_h)) * panes[id].vweight / vsum)))); rects[id] = .{ .x = x, .y = y, .w = cw, .h = ch }; y +|= ch; } x +|= cw; } } fn moveTerm( l: *Layout, rects: *const [dump.max_panes]Rect, col_x: *const [dump.max_cols]u16, col_w: *const [dump.max_cols]u16, id: usize, cur_x: u16, cur_y: u16, ) void { const src = layoutFindTerm(l, id) orelse return; var tc: usize = if (l.ncol > 0) l.ncol - 1 else 0; for (0..l.ncol) |c| { if (cur_x >= col_x[c] and cur_x < col_x[c] + col_w[c]) { tc = c; break; } } var ti: usize = 0; var k: usize = 0; while (k < l.col_n[tc]) : (k += 1) { const cid = l.col_terms[tc][k]; if (cid == id) continue; const r = rects[cid]; if (cur_y < r.y + r.h / 2) break; ti += 1; } const src_solo = l.col_n[src.col] == 1; if (tc == src.col and src_solo) return; layoutRemove(l, id); if (src_solo and src.col < tc) tc -= 1; if (tc >= l.ncol) tc = l.ncol - 1; if (ti > l.col_n[tc]) ti = l.col_n[tc]; layoutInsert(l, tc, ti, id); } fn renderPane(alloc: std.mem.Allocator, root: ui.Window, p: *Pane, r: Rect, active: bool) !void { if (r.w <= GUTTER or r.h == 0) return; const text = root.child(.{ .x_off = r.x + GUTTER, .y_off = r.y, .width = r.w - GUTTER, .height = r.h }); text.clear(); if (T().bg) |bg| text.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = rgbColor(bg) } }); const tagw = text.child(.{ .height = BOX_H }); tagw.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = rgbColor(T().tag_bg) } }); _ = tagw.printSegment(.{ .text = p.tag, .style = .{ .fg = rgbColor(T().tag_fg), .bg = rgbColor(T().tag_bg) } }, .{ .wrap = .none }); const body_h = if (r.h > BOX_H) r.h - BOX_H else 0; const body_w = r.w - GUTTER; const body_style: ui.Style = .{ .fg = colorOpt(T().fg), .bg = colorOpt(T().bg) }; if (p.image_view) |*iv| { if (body_h > 0) image.draw(iv, text.child(.{ .y_off = BOX_H, .height = body_h }), body_w, body_h); const gut = root.child(.{ .x_off = r.x, .y_off = r.y, .width = GUTTER, .height = r.h }); const box_bg = if (active) accent else accent_dim; var gy: u16 = 0; while (gy < r.h) : (gy += 1) { const bg = if (gy < BOX_H) box_bg else T().scroll_track; var gx: u16 = 0; while (gx < GUTTER) : (gx += 1) { gut.writeCell(gx, gy, .{ .char = .{ .grapheme = " " }, .style = .{ .bg = rgbColor(bg) } }); } } return; } if (p.terminal != null) { const body = try terminalBodyText(alloc, p); var it = std.mem.splitAny(u8, body, "\n"); var vr: u16 = 0; while (it.next()) |line| : (vr += 1) { if (vr >= body_h) break; _ = text.printSegment(.{ .text = line, .style = body_style }, .{ .row_offset = BOX_H + vr, .wrap = .none }); } const rt = &p.terminal.?; vr = 0; while (vr < body_h and vr < p.rows) : (vr += 1) { var c: u16 = 0; while (c < body_w) : (c += 1) { const ci = rt.term.screens.active.pages.getCell(.{ .viewport = .{ .x = @intCast(c), .y = @intCast(vr) } }) orelse continue; if (ci.cell.wide == .spacer_tail) continue; if (text.readCell(c, BOX_H + vr)) |cell| { var nc = cell; nc.default = false; nc.style = cellStyle(ci); text.writeCell(c, BOX_H + vr, nc); } } } } else if (p.kind == .file) { var vr: u16 = 0; while (vr < body_h) : (vr += 1) { const abs = p.scroll + vr; const prefix = try std.fmt.allocPrint(alloc, "{d: >4} ", .{abs + 1}); _ = text.printSegment(.{ .text = prefix, .style = body_style }, .{ .row_offset = BOX_H + vr, .wrap = .none }); _ = text.printSegment(.{ .text = lineAt(p.content, abs), .style = body_style }, .{ .row_offset = BOX_H + vr, .col_offset = PREFIX_W, .wrap = .none }); } const ln_fg = rgbColor(T().lineno); vr = 0; while (vr < body_h) : (vr += 1) { var c: u16 = 0; while (c < PREFIX_W and c < body_w) : (c += 1) { if (text.readCell(c, BOX_H + vr)) |cell| { var nc = cell; nc.default = false; nc.style.fg = ln_fg; text.writeCell(c, BOX_H + vr, nc); } } } if (p.highlights.len > 0) { var lines = std.mem.splitScalar(u8, p.content, '\n'); var skip: usize = 0; while (skip < p.scroll) : (skip += 1) if (lines.next() == null) break; vr = 0; while (vr < body_h) : (vr += 1) { const line = lines.next() orelse break; const base = @intFromPtr(line.ptr) - @intFromPtr(p.content.ptr); var c: usize = 0; while (c < line.len and PREFIX_W + c < body_w and base + c < p.highlights.len) : (c += 1) { const ss = synStyle(@enumFromInt(p.highlights[base + c])) orelse continue; const cx: u16 = @intCast(PREFIX_W + c); if (text.readCell(cx, BOX_H + vr)) |cell| { var nc = cell; nc.default = false; nc.style.fg = rgbColor(ss.fg); nc.style.bold = ss.bold; text.writeCell(cx, BOX_H + vr, nc); } } } } } else { var vr: u16 = 0; while (vr < body_h) : (vr += 1) { const abs = p.scroll + vr; _ = text.printSegment(.{ .text = lineAt(p.content, abs), .style = body_style }, .{ .row_offset = BOX_H + vr, .wrap = .none }); } } const gut = root.child(.{ .x_off = r.x, .y_off = r.y, .width = GUTTER, .height = r.h }); const box_bg = if (active) accent else accent_dim; var gy: u16 = 0; while (gy < r.h) : (gy += 1) { const bg = if (gy < BOX_H) box_bg else T().scroll_track; var gx: u16 = 0; while (gx < GUTTER) : (gx += 1) { gut.writeCell(gx, gy, .{ .char = .{ .grapheme = " " }, .style = .{ .bg = rgbColor(bg) } }); } } const track_h: usize = if (r.h > BOX_H) r.h - BOX_H else 0; if (track_h > 0) { var sb_total: usize = undefined; var sb_off: usize = undefined; var sb_len: usize = undefined; if (p.terminal) |*rt| { const sb = rt.term.screens.active.pages.scrollbar(); sb_total = sb.total; sb_off = sb.offset; sb_len = sb.len; } else { sb_total = lineCount(p.content); sb_off = p.scroll; sb_len = p.rows; } const total = if (sb_total == 0) 1 else sb_total; const len = @max(1, (track_h * @max(1, sb_len)) / total); const pos = (track_h * sb_off) / total; var sy: usize = 0; while (sy < track_h) : (sy += 1) { if (sy < pos or sy >= pos + len) continue; var gx: u16 = 0; while (gx < GUTTER) : (gx += 1) { gut.writeCell(gx, @intCast(BOX_H + sy), .{ .char = .{ .grapheme = " " }, .style = .{ .bg = rgbColor(T().scroll_thumb) } }); } } } } fn render( vx: *ui.Vaxis, gpa: std.mem.Allocator, writer: *std.Io.Writer, state: dump.State, panes: []Pane, rects: *[dump.max_panes]Rect, screen_w: u16, active: usize, ) !void { var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); const alloc = arena.allocator(); const win = vx.window(); win.clear(); win.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = rgbColor(T().scroll_track) } }); for (panes, 0..) |*p, id| { if (p.image_view) |*iv| { image.ensureLoadedFromMemory(iv, p.image_bytes, vx, gpa, writer); image.ensureGrid(iv, gpa, p.cols, p.rows); } try renderPane(alloc, win, p, rects[id], id == active); } const bar = win.child(.{ .x_off = 0, .y_off = 0, .width = screen_w, .height = TOPBAR_H }); bar.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = rgbColor(T().tag_bg) } }); const topbar = if (state.topbar.len > 0) state.topbar else "pardes replay"; _ = bar.printSegment(.{ .text = topbar, .style = .{ .fg = rgbColor(T().tag_fg), .bg = rgbColor(T().tag_bg) } }, .{ .wrap = .none }); try vx.render(writer); } fn paneAt(rects: *const [dump.max_panes]Rect, pane_count: usize, col: u16, row: u16) ?usize { for (0..pane_count) |i| { const r = rects[i]; if (col >= r.x and col < r.x + r.w and row >= r.y and row < r.y + r.h) return i; } return null; } fn deinitPanes(gpa: std.mem.Allocator, panes: []Pane) void { for (panes) |*p| { if (p.terminal) |*rt| { rt.stream.deinit(); rt.term.deinit(gpa); } if (p.image_view) |*iv| image.deinitView(iv, gpa); if (p.highlights.len > 0) gpa.free(p.highlights); if (p.owned_content) |bytes| gpa.free(bytes); } } 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, .tag = src.tag, .content = src.body, .path = "", .tty_mode = std.mem.startsWith(u8, src.tag, "sy "), .scroll = src.scroll, .cols = src.cols, .rows = src.rows, .vweight = src.vweight, }; switch (src.kind) { .file => if (src.file) |f| { out.path = f.path; if (f.content_b64.len > 0) { const bytes = try dump.decodeBytes(gpa, f.content_b64); out.owned_content = bytes; out.content = bytes; } else { out.content = f.content; } out.highlights = highlightFile(gpa, out.path, out.content) catch &.{}; }, .terminal => if (src.terminal) |t| { out.path = t.cwd; if (t.stream_b64.len > 0) { const bytes = try dump.decodeBytes(gpa, t.stream_b64); out.owned_content = bytes; out.content = bytes; out.terminal = .{ .term = try ghostty_vt.Terminal.init(gpa, .{ .cols = @max(1, src.cols), .rows = @max(1, src.rows), .max_scrollback = terminalScrollbackLimit(src), }), .stream = undefined, }; if (out.terminal) |*rt| { rt.stream = rt.term.vtStream(); rt.stream.nextSlice(bytes); rt.term.screens.active.scroll(.active); if (src.scroll > 0) rt.term.screens.active.scroll(.{ .delta_row = -@as(isize, @intCast(src.scroll)) }); } } else { out.content = if (t.stream.len > 0) t.stream else src.body; } }, .image => if (src.image) |im| { out.path = im.path; out.content = src.body; if (im.bytes_b64.len > 0) { const bytes = try dump.decodeBytes(gpa, im.bytes_b64); out.owned_content = bytes; out.image_bytes = bytes; } out.image_view = .{ .path = im.path }; }, } clampScroll(out); } 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, 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; } 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); 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, }); } self.loop = .init(io, &self.tty, &self.vx); try self.loop.start(); try self.loop.installResizeHandler(); 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); } fn run(self: *App) !void { while (try self.step(try self.loop.nextEvent())) {} } 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; } fn step(self: *App, event: @TypeOf(Command.value)) !bool { switch (event) { .winsize => |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)), 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(&self.panes[id], 1); } else if (mouse.button == .wheel_up and mouse.type == .press) { 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 < 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 < 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 < 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) { self.drag = .{ .border_h = .{ .col = cc, .top_idx = k, .cur_y = mrow } }; handled = true; break :outer; } } } } if (!handled) if (hovered) |id| { self.active = id; const r = self.rects[id]; if (mcol < r.x + GUTTER and mrow < r.y + BOX_H) { 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(&self.panes[id], -local); } }; } else if (mouse.type == .press) { if (hovered) |id| self.active = id; } else if (mouse.type == .drag) { switch (self.drag) { .border_v => |*d| { const c = d.left_col; 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 < 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; } }, .move => |*d| { d.cur_x = mcol; d.cur_y = mrow; }, .none => {}, } } else if (mouse.type == .release) { switch (self.drag) { .border_v => |d| { const c = d.left_col; 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 = 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 < 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 = 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(&self.layout, &self.rects, &self.col_x, &self.col_w, d.id, d.cur_x, d.cur_y), .none => {}, } self.drag = .none; } }, .key_press => |key| { if (key.matches('q', .{}) or key.matches(ui.Key.escape, .{}) or key.matches('c', .{ .ctrl = true })) return false; }, .paste => |text| self.gpa.free(text), .quit, .close => return false, else => {}, } 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(self.gpa, cols, rows) catch {}; } p.cols = cols; p.rows = rows; clampScroll(p); } 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); }