diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-02 21:41:13 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | 81992cc92743a98066ee1a9025bc74f91cf0e967 (patch) | |
| tree | 12c07f68f8ad030f6c6f8a2b3415998d0a87eee7 /replay.zig | |
| parent | bead9f0b78380d1428f40fdab339c1c8f88fa626 (diff) | |
| download | pardes-81992cc92743a98066ee1a9025bc74f91cf0e967.tar.gz pardes-81992cc92743a98066ee1a9025bc74f91cf0e967.zip | |
storage and replay
Diffstat (limited to 'replay.zig')
| -rw-r--r-- | replay.zig | 712 |
1 files changed, 712 insertions, 0 deletions
diff --git a/replay.zig b/replay.zig new file mode 100644 index 00000000..75227c42 --- /dev/null +++ b/replay.zig @@ -0,0 +1,712 @@ +const std = @import("std"); +const ui = @import("ui"); +const ghostty_vt = @import("ghostty-vt"); +const dump = @import("dump.zig"); +const paint = @import("paint.zig"); +const image = @import("image.zig"); + +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 <dump.zon>\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 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 = 16 * 1024 * 1024, + }), + .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); +} + +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 state = try dump.readFile(io, gpa, args[1]); + defer dump.free(gpa, state); + _ = paint.setThemeByName(state.theme); + image.start(io, gpa); + defer image.stop(); + + 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); + + 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; + } + + 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; + + 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 {}; + + var loop: Loop = .init(io, &tty, &vx); + try loop.start(); + defer loop.stop(); + try loop.installResizeHandler(); + + 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); + + var drag: Drag = .none; + while (true) { + const event = try loop.nextEvent(); + switch (event) { + .winsize => |ws| { + screen_w = ws.cols; + screen_h = ws.rows; + try vx.resize(gpa, 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); + + if (mouse.button == .wheel_down and mouse.type == .press) { + if (hovered) |id| scrollBy(&panes[id], 1); + } else if (mouse.button == .wheel_up and mouse.type == .press) { + if (hovered) |id| scrollBy(&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 } }; + 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; + var k: usize = 0; + while (k + 1 < layout.col_n[cc]) : (k += 1) { + const id = layout.col_terms[cc][k]; + const r = rects[id]; + if (mrow == r.y + r.h -| 1) { + drag = .{ .border_h = .{ .col = cc, .top_idx = k, .cur_y = mrow } }; + handled = true; + break :outer; + } + } + } + } + if (!handled) if (hovered) |id| { + active = id; + const r = rects[id]; + if (mcol < r.x + GUTTER and mrow < r.y + BOX_H) { + 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); + } + }; + } else if (mouse.type == .press) { + if (hovered) |id| active = id; + } else if (mouse.type == .drag) { + switch (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; + 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; + 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 (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]); + 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]; + } + }, + .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); + 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; + } + }, + .move => |d| moveTerm(&layout, &rects, &col_x, &col_w, d.id, d.cur_x, d.cur_y), + .none => {}, + } + drag = .none; + } + }, + .key_press => |key| { + if (key.matches('q', .{}) or key.matches(ui.Key.escape, .{}) or key.matches('c', .{ .ctrl = true })) break; + }, + .paste => |text| gpa.free(text), + .quit, .close => break, + else => {}, + } + + computeGeom(&layout, panes, screen_w, screen_h, &rects, &col_x, &col_w); + for (panes, 0..) |*p, id| { + const r = 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 {}; + } + p.cols = cols; + p.rows = rows; + clampScroll(p); + } + try render(&vx, gpa, tty.writer(), state, panes, &rects, screen_w, active); + } +} |
