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 /main.zig | |
| parent | bead9f0b78380d1428f40fdab339c1c8f88fa626 (diff) | |
| download | pardes-81992cc92743a98066ee1a9025bc74f91cf0e967.tar.gz pardes-81992cc92743a98066ee1a9025bc74f91cf0e967.zip | |
storage and replay
Diffstat (limited to 'main.zig')
| -rw-r--r-- | main.zig | 472 |
1 files changed, 146 insertions, 326 deletions
@@ -4,37 +4,8 @@ const ghostty_vt = @import("ghostty-vt"); const Term = @import("term.zig"); const modal = @import("modal.zig"); const image = @import("image.zig"); -const ts = @import("tree-sitter"); -const ts_queries = @import("ts_queries"); - -// grammars whose parser.c we link (see build.zig). The query strings ride along -// in the ts_queries module. -extern fn tree_sitter_ada() callconv(.c) *const ts.Language; -extern fn tree_sitter_bash() callconv(.c) *const ts.Language; -extern fn tree_sitter_c() callconv(.c) *const ts.Language; -extern fn tree_sitter_c_sharp() callconv(.c) *const ts.Language; -extern fn tree_sitter_clojure() callconv(.c) *const ts.Language; -extern fn tree_sitter_cpp() callconv(.c) *const ts.Language; -extern fn tree_sitter_css() callconv(.c) *const ts.Language; -extern fn tree_sitter_elixir() callconv(.c) *const ts.Language; -extern fn tree_sitter_erlang() callconv(.c) *const ts.Language; -extern fn tree_sitter_fortran() callconv(.c) *const ts.Language; -extern fn tree_sitter_go() callconv(.c) *const ts.Language; -extern fn tree_sitter_haskell() callconv(.c) *const ts.Language; -extern fn tree_sitter_html() callconv(.c) *const ts.Language; -extern fn tree_sitter_java() callconv(.c) *const ts.Language; -extern fn tree_sitter_javascript() callconv(.c) *const ts.Language; -extern fn tree_sitter_json() callconv(.c) *const ts.Language; -extern fn tree_sitter_kotlin() callconv(.c) *const ts.Language; -extern fn tree_sitter_ocaml() callconv(.c) *const ts.Language; -extern fn tree_sitter_pascal() callconv(.c) *const ts.Language; -extern fn tree_sitter_php() callconv(.c) *const ts.Language; -extern fn tree_sitter_powershell() callconv(.c) *const ts.Language; -extern fn tree_sitter_python() callconv(.c) *const ts.Language; -extern fn tree_sitter_ruby() callconv(.c) *const ts.Language; -extern fn tree_sitter_rust() callconv(.c) *const ts.Language; -extern fn tree_sitter_scala() callconv(.c) *const ts.Language; -extern fn tree_sitter_zig() callconv(.c) *const ts.Language; +const dump = @import("dump.zig"); +const paint = @import("paint.zig"); // A TUI can't let std.log hit stderr — it's the same tty as our alt-screen, so the // message lands on top of the UI (this is the stray text you saw when opening yazi: @@ -224,10 +195,8 @@ const gray = struct { const lit = [3]u8{ 0x6e, 0x6e, 0x6e }; // dashed resize preview const txt = [3]u8{ 0x96, 0x96, 0x96 }; // muted text }; -// the drag button: an indigo-purple accent (brighter when the window is focused), -// dimming to a dark grey-blue when idle. -const accent = [3]u8{ 0x6e, 0x4f, 0xd0 }; -const accent_dim = [3]u8{ 0x38, 0x3c, 0x54 }; +const accent = paint.accent; +const accent_dim = paint.accent_dim; // per-button selection highlight: distinct (muted) colors so left/middle/right // selections are easy to tell apart against the otherwise-monochrome chrome. const SEL_BG = [3][3]u8{ .{ 0x2c, 0x46, 0x74 }, .{ 0x2c, 0x60, 0x38 }, .{ 0x64, 0x2c, 0x58 } }; @@ -237,6 +206,13 @@ const sel_fg = [3]u8{ 0xd2, 0xd2, 0xd2 }; const msel_bg = [3]u8{ 0x4a, 0x3a, 0x1e }; const msel_fg = [3]u8{ 0xd8, 0xc8, 0xa8 }; +const T = paint.T; +const rgbColor = paint.rgbColor; +const colorOpt = paint.colorOpt; +const synStyle = paint.synStyle; +const highlightFile = paint.highlightFile; +const cellStyle = paint.cellStyle; + // Master switch for the per-cell recolor passes (tty ansi + file syntax). Off = // the plain-text render path, exactly as before colors existed; the recolor loops // are skipped so there's no per-frame cost. Toggled by the "Colors" tagbar command. @@ -246,99 +222,13 @@ var colors_on: bool = true; // central command dispatcher can flip it; the render reads it. var show_debug: bool = false; -// Color themes. Chrome (scrollbar/box/selection/edit colors above) stays fixed; -// only the page background, text, tag bar and syntax colors swap. `bg`/`fg` are -// null in the dark theme so it keeps the terminal's own default colors (i.e. looks -// exactly as it did before themes existed); the acme light theme paints an explicit -// pale-yellow page with black text. Cycle with the "NextColor" tagbar command. -const Theme = struct { - name: []const u8, - bg: ?[3]u8, // body background (null = terminal default, no paint) - fg: ?[3]u8, // body text (null = terminal default) - tag_bg: [3]u8, - tag_fg: [3]u8, - // syntax (keyword + comment render bold) - kw: [3]u8, - str: [3]u8, - num: [3]u8, - comment: [3]u8, - lineno: [3]u8, // muted file-pane line-number gutter - scroll_track: [3]u8, // scrollbar strip - scroll_thumb: [3]u8, // scrollbar visible-portion indicator - // the 16 ANSI colors for terminal-pane output; null = pass palette indices - // through to the real terminal (dark keeps the terminal's own colors). - palette: ?[16][3]u8, -}; -const themes = [_]Theme{ - .{ - .name = "dark", - .bg = null, - .fg = null, - .tag_bg = .{ 0x2c, 0x2a, 0x3e }, // dim gray, slight indigo tint - .tag_fg = .{ 0x96, 0x96, 0x96 }, - .kw = .{ 0xcc, 0x70, 0xd8 }, // magenta - .str = .{ 0x8a, 0xb8, 0x7a }, // green - .num = .{ 0xd0, 0xc0, 0x70 }, // yellow - .comment = .{ 0xf0, 0xf0, 0xf0 }, // bold white - .lineno = .{ 0x5a, 0x5a, 0x5a }, // dim gray gutter - .scroll_track = .{ 0x2a, 0x2a, 0x2a }, // gray.bar (unchanged dark scrollbar) - .scroll_thumb = .{ 0x52, 0x52, 0x52 }, // gray.mid - .palette = null, // terminal-native ANSI colors - }, - .{ - .name = "acme", - .bg = .{ 0xff, 0xff, 0xea }, // classic acme pale yellow - .fg = .{ 0x00, 0x00, 0x00 }, - .tag_bg = .{ 0xea, 0xff, 0xff }, // acme pale cyan - .tag_fg = .{ 0x00, 0x00, 0x00 }, - .kw = .{ 0x88, 0x22, 0x99 }, - .str = .{ 0x00, 0x77, 0x33 }, - .num = .{ 0xa0, 0x52, 0x00 }, - .comment = .{ 0x55, 0x55, 0x55 }, // bold, darkened to read on yellow - .lineno = .{ 0x99, 0x99, 0x80 }, // muted gutter, reads on the pale page - .scroll_track = .{ 0x99, 0x99, 0x4c }, // acme olive strip (BORD) - .scroll_thumb = .{ 0xff, 0xff, 0xea }, // acme pale-yellow scroller (BACK) - // darkened ANSI palette so shell output (ls colors, prompts) reads on the - // pale-yellow page; 7/15 (the "whites") map to gray/black, not invisible. - .palette = .{ - .{ 0x00, 0x00, 0x00 }, .{ 0xaa, 0x22, 0x22 }, .{ 0x22, 0x80, 0x22 }, .{ 0x88, 0x66, 0x00 }, - .{ 0x22, 0x44, 0xbb }, .{ 0x99, 0x33, 0x99 }, .{ 0x11, 0x88, 0x88 }, .{ 0x55, 0x55, 0x55 }, - .{ 0x88, 0x88, 0x88 }, .{ 0xcc, 0x33, 0x33 }, .{ 0x33, 0xaa, 0x33 }, .{ 0xaa, 0x77, 0x00 }, - .{ 0x33, 0x55, 0xdd }, .{ 0xbb, 0x44, 0xbb }, .{ 0x22, 0xaa, 0xaa }, .{ 0x00, 0x00, 0x00 }, - }, - }, -}; -var theme_idx: usize = 0; -fn T() *const Theme { - return &themes[theme_idx]; -} -fn rgbColor(c: [3]u8) vaxis.Color { - return .{ .rgb = c }; -} -fn colorOpt(c: ?[3]u8) vaxis.Color { - return if (c) |x| .{ .rgb = x } else .default; -} - -// tree-sitter syntax classes; also the per-byte style id stored in File.highlights -// (0 = none). synStyle maps each to the active theme's color (null for .none). -const Syn = enum(u8) { none, keyword, string, number, comment }; -fn synStyle(s: Syn) ?struct { fg: [3]u8, bold: bool } { - return switch (s) { - .none => null, - .keyword => .{ .fg = T().kw, .bold = true }, - .string => .{ .fg = T().str, .bold = false }, - .number => .{ .fg = T().num, .bold = false }, - .comment => .{ .fg = T().comment, .bold = true }, - }; -} - // Builtins. Executing the name (middle-click / Tab) runs the action through the ONE // dispatcher (onSelect + actOnSelection), no matter where the name is shown — a pane // body, a file, the tagbar. onSelect matches against the whole list, so a name works // wherever it appears. Window-specific ones (Save, Del, Delcol) live in each // pane's tag or can be typed/executed; window-agnostic ones (Kill, Newcol, Tutor, // Debug, Colors, NextColor) in the tagbar. -const SelectAction = enum { none, send, newcol, delcol, del, save, tutor, kill, debug, colors, nextcolor }; +const SelectAction = enum { none, send, newcol, delcol, del, save, tutor, kill, debug, colors, nextcolor, dump }; const Builtin = struct { name: []const u8, action: SelectAction }; const builtins = [_]Builtin{ .{ .name = "Kill", .action = .kill }, @@ -347,6 +237,7 @@ const builtins = [_]Builtin{ .{ .name = "Debug", .action = .debug }, .{ .name = "Colors", .action = .colors }, .{ .name = "NextColor", .action = .nextcolor }, + .{ .name = "Dump", .action = .dump }, .{ .name = "Save", .action = .save }, .{ .name = "Del", .action = .del }, .{ .name = "Delcol", .action = .delcol }, @@ -360,7 +251,7 @@ const pane_tail = " " ++ pane_builtins_str; const file_pane_tail = " " ++ file_pane_builtins_str; // the global tagbar: window-agnostic commands. Kill closes everything; Debug toggles // the stats overlay, Colors the ansi/syntax recolor, NextColor cycles the theme. -const topbar_str = "Kill Newcol Tutor Debug Colors NextColor"; +const topbar_str = "Kill Newcol Tutor Debug Colors NextColor Dump"; // Path of the generated bash rc that adds OSC 133 semantic prompts (shell // integration). bash --rcfile reads it for interactive shells; it sources the @@ -788,6 +679,7 @@ fn feedPtyRead(gpa: std.mem.Allocator, terms: *[MAX_TERMS]?*Term, active: usize, defer gpa.free(pr.bytes); if (pr.id >= MAX_TERMS) return; if (terms[pr.id]) |t| { + t.tty_stream.appendSlice(gpa, pr.bytes) catch {}; t.stream.nextSlice(pr.bytes); // follow new output to the bottom, so a command's result shows even in a // pane you didn't focus. Exception: the pane you're actively navigating @@ -888,154 +780,6 @@ fn readFileAlloc(gpa: std.mem.Allocator, path: [*:0]const u8) ![]u8 { return buf.toOwnedSlice(gpa); } -// Map a tree-sitter capture name (e.g. "keyword.function", "string.special") to a -// syntax color, by substring — the grammars' highlights.scm use many sub-captures -// we don't care to distinguish. Returns .none for captures we don't color. -fn synFor(name: []const u8) Syn { - for ([_]struct { []const u8, Syn }{ - .{ "comment", .comment }, - .{ "string", .string }, - .{ "character", .string }, - .{ "number", .number }, - .{ "float", .number }, - .{ "boolean", .number }, - .{ "keyword", .keyword }, - .{ "include", .keyword }, - .{ "operator", .keyword }, - .{ "conditional", .keyword }, - .{ "repeat", .keyword }, - }) |m| { - if (std.mem.indexOf(u8, name, m[0]) != null) return m[1]; - } - return .none; -} - -fn extIs(ext: []const u8, comptime choices: anytype) bool { - inline for (choices) |choice| { - if (std.ascii.eqlIgnoreCase(ext, choice)) return true; - } - return false; -} - -// Syntax-highlight `content` with tree-sitter: returns a gpa-owned byte per content -// byte (a `Syn` id; 0 = none), or empty when the extension has no grammar. Caller -// frees (stored in File.highlights; see term.zig). Best-effort — any parse/query -// failure just yields fewer styled bytes, never an error. -fn highlightFile(gpa: std.mem.Allocator, path: []const u8, content: []const u8) ![]u8 { - const ext = std.fs.path.extension(path); - var lang: *const ts.Language = undefined; - var query_src: []const u8 = undefined; - if (extIs(ext, .{ ".adb", ".ads", ".ada" })) { - lang = tree_sitter_ada(); - query_src = ts_queries.ada_highlights; - } else if (extIs(ext, .{ ".sh", ".bash", ".zsh" })) { - lang = tree_sitter_bash(); - query_src = ts_queries.bash_highlights; - } else if (extIs(ext, .{ ".c", ".h" })) { - lang = tree_sitter_c(); - query_src = ts_queries.c_highlights; - } else if (extIs(ext, .{ ".cs", ".csx" })) { - lang = tree_sitter_c_sharp(); - query_src = ts_queries.c_sharp_highlights; - } else if (extIs(ext, .{ ".clj", ".cljs", ".cljc", ".edn" })) { - lang = tree_sitter_clojure(); - query_src = ts_queries.clojure_highlights; - } else if (extIs(ext, .{ ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx" })) { - lang = tree_sitter_cpp(); - query_src = ts_queries.cpp_highlights; - } else if (extIs(ext, .{".css"})) { - lang = tree_sitter_css(); - query_src = ts_queries.css_highlights; - } else if (extIs(ext, .{ ".ex", ".exs" })) { - lang = tree_sitter_elixir(); - query_src = ts_queries.elixir_highlights; - } else if (extIs(ext, .{ ".erl", ".hrl" })) { - lang = tree_sitter_erlang(); - query_src = ts_queries.erlang_highlights; - } else if (extIs(ext, .{ ".f", ".for", ".ftn", ".f90", ".f95", ".f03", ".f08" })) { - lang = tree_sitter_fortran(); - query_src = ts_queries.fortran_highlights; - } else if (extIs(ext, .{".go"})) { - lang = tree_sitter_go(); - query_src = ts_queries.go_highlights; - } else if (extIs(ext, .{ ".hs", ".lhs" })) { - lang = tree_sitter_haskell(); - query_src = ts_queries.haskell_highlights; - } else if (extIs(ext, .{ ".html", ".htm" })) { - lang = tree_sitter_html(); - query_src = ts_queries.html_highlights; - } else if (extIs(ext, .{".java"})) { - lang = tree_sitter_java(); - query_src = ts_queries.java_highlights; - } else if (extIs(ext, .{ ".js", ".jsx", ".mjs", ".cjs" })) { - lang = tree_sitter_javascript(); - query_src = ts_queries.javascript_highlights; - } else if (extIs(ext, .{".json"})) { - lang = tree_sitter_json(); - query_src = ts_queries.json_highlights; - } else if (extIs(ext, .{ ".kt", ".kts" })) { - lang = tree_sitter_kotlin(); - query_src = ts_queries.kotlin_highlights; - } else if (extIs(ext, .{ ".ml", ".mli" })) { - lang = tree_sitter_ocaml(); - query_src = ts_queries.ocaml_highlights; - } else if (extIs(ext, .{ ".pas", ".pp", ".p" })) { - lang = tree_sitter_pascal(); - query_src = ts_queries.pascal_highlights; - } else if (extIs(ext, .{ ".php", ".phtml", ".php3", ".php4", ".php5" })) { - lang = tree_sitter_php(); - query_src = ts_queries.php_highlights; - } else if (extIs(ext, .{ ".ps1", ".psm1", ".psd1" })) { - lang = tree_sitter_powershell(); - query_src = ts_queries.powershell_highlights; - } else if (extIs(ext, .{ ".py", ".pyw" })) { - lang = tree_sitter_python(); - query_src = ts_queries.python_highlights; - } else if (extIs(ext, .{ ".rb", ".rake" })) { - lang = tree_sitter_ruby(); - query_src = ts_queries.ruby_highlights; - } else if (extIs(ext, .{".rs"})) { - lang = tree_sitter_rust(); - query_src = ts_queries.rust_highlights; - } else if (extIs(ext, .{ ".scala", ".sc" })) { - lang = tree_sitter_scala(); - query_src = ts_queries.scala_highlights; - } else if (extIs(ext, .{ ".zig", ".zon" })) { - lang = tree_sitter_zig(); - query_src = ts_queries.zig_highlights; - } else return &.{}; - // note: no lang.destroy() — tree_sitter_*() return static singletons reused on - // every open; deleting one would use-after-free the next file's parse. - - const styles = try gpa.alloc(u8, content.len); - errdefer gpa.free(styles); - @memset(styles, 0); - - const parser = ts.Parser.create(); - defer parser.destroy(); - parser.setLanguage(lang) catch return styles; - const tree = parser.parseString(content, null) orelse return styles; - defer tree.destroy(); - - var error_offset: u32 = 0; - const query = ts.Query.create(lang, query_src, &error_offset) catch return styles; - defer query.destroy(); - const cursor = ts.QueryCursor.create(); - defer cursor.destroy(); - cursor.exec(query, tree.rootNode()); - - while (cursor.nextMatch()) |match| { - for (match.captures) |cap| { - const syn = synFor(query.captureNameForId(cap.index) orelse ""); - if (syn == .none) continue; - var b: usize = cap.node.startByte(); - const end = @min(@as(usize, cap.node.endByte()), styles.len); - while (b < end) : (b += 1) styles[b] = @intFromEnum(syn); - } - } - return styles; -} - // Replace a file pane's content (freeing the old buffer) and re-derive syntax // highlighting to match. Every file edit funnels through here, so highlights stay // in sync with content (and can never index past it). The reparse is skipped when @@ -1347,60 +1091,6 @@ fn computeGeom( } } -// A ghostty palette index → vaxis color. With a themed palette (light) the 16 -// ANSI colors resolve to the theme's rgb so shell output matches the page; without -// one (dark) they pass through as palette indices for the real terminal to render. -fn palColor(idx: u8) vaxis.Color { - if (T().palette) |p| if (idx < 16) return rgbColor(p[idx]); - return .{ .index = idx }; -} - -// Translate a ghostty cell color to vaxis. `default` (no explicit color) resolves -// to the theme's page fg/bg — so on the acme theme the terminal's own default text -// becomes black-on-yellow instead of the real terminal's colors. dark keeps null -// (→ vaxis .default → terminal native, unchanged). -fn ghostColor(color: ghostty_vt.Style.Color, is_bg: bool) vaxis.Color { - return switch (color) { - .none => colorOpt(if (is_bg) T().bg else T().fg), - .palette => |idx| palColor(idx), - .rgb => |rgb| .{ .rgb = .{ rgb.r, rgb.g, rgb.b } }, - }; -} - -// The vaxis style for one emulator cell: fg/bg/attrs translated from ghostty. -fn cellStyle(ci: ghostty_vt.PageList.Cell) vaxis.Style { - const style = ci.style(); - var vs: vaxis.Style = .{ - .fg = ghostColor(style.fg_color, false), - .bg = ghostColor(style.bg_color, true), - .bold = style.flags.bold, - .dim = style.flags.faint, - .italic = style.flags.italic, - .blink = style.flags.blink, - .reverse = style.flags.inverse, - .invisible = style.flags.invisible, - .strikethrough = style.flags.strikethrough, - .ul_style = switch (style.flags.underline) { - .none => .off, - .single => .single, - .double => .double, - .curly => .curly, - .dotted => .dotted, - .dashed => .dashed, - }, - }; - // a cell can carry its bg directly (palette/rgb) rather than via its style. - switch (ci.cell.content_tag) { - .bg_color_palette => vs.bg = palColor(ci.cell.content.color_palette.data), - .bg_color_rgb => { - const rgb = ci.cell.content.color_rgb; - vs.bg = .{ .rgb = .{ rgb.r, rgb.g, rgb.b } }; - }, - else => {}, - } - return vs; -} - // `alloc` must be a per-frame arena, NOT freed before vx.render(): vaxis is a // retained renderer and stores each cell's grapheme as a *slice* into the text // we hand it, so plainString has to outlive the render call. @@ -2833,7 +2523,10 @@ fn actOnSelection( .none => {}, .debug => show_debug = !show_debug, .colors => colors_on = !colors_on, - .nextcolor => theme_idx = (theme_idx + 1) % themes.len, + .nextcolor => paint.nextTheme(), + .dump => dumpSnapshot(io, gpa, terms, layout, active.*, sw, sh) catch |err| { + std.log.warn("dump {s}: {}", .{ dump.default_path, err }); + }, .kill => return true, // close everything: signal the main loop to quit .save => { if (t.file) |*f| saveFile(io, f) catch |err| { @@ -3863,6 +3556,133 @@ fn bodyText(alloc: std.mem.Allocator, t: *Term) ![]u8 { return out.toOwnedSlice(alloc); } +fn terminalReplayText(alloc: std.mem.Allocator, t: *Term) ![]u8 { + const full = try t.term.screens.active.dumpStringAlloc(alloc, .{ .screen = .{} }); + defer alloc.free(full); + + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(alloc); + var lines = std.mem.splitAny(u8, full, "\n"); + var prompts = t.term.screens.active.pages.rowIterator(.right_down, .{ .screen = .{} }, null); + var row: usize = 0; + while (lines.next()) |raw| : (row += 1) { + if (row > 0) try out.append(alloc, '\n'); + const is_prompt = if (t.mode != .tty) + if (prompts.next()) |pin| pin.rowAndCell().row.semantic_prompt != .none else false + else + false; + const line = if (is_prompt) "" else raw; + if (t.mode == .tty) { + try out.appendSlice(alloc, line); + } else { + try spliceRow(alloc, &out, t, @intCast(row), line); + } + } + return out.toOwnedSlice(alloc); +} + +fn dumpSnapshot( + io: std.Io, + gpa: std.mem.Allocator, + terms: *[MAX_TERMS]?*Term, + layout: *const Layout, + active: usize, + sw: u16, + sh: u16, +) !void { + var arena: std.heap.ArenaAllocator = .init(gpa); + defer arena.deinit(); + const alloc = arena.allocator(); + + var slot_to_pane: [MAX_TERMS]?usize = @splat(null); + var panes: std.ArrayList(dump.Pane) = .empty; + for (terms.*, 0..) |slot, id| { + const t = slot orelse continue; + const compact = panes.items.len; + slot_to_pane[id] = compact; + + const tag = try fullTag(alloc, t); + const body = try bodyText(alloc, t); + const scroll: usize = @intCast(@max(0, paneScroll(t))); + const pane: dump.Pane = if (t.file) |f| .{ + .kind = .file, + .tag = tag, + .body = body, + .scroll = scroll, + .cols = t.cols, + .rows = t.rows, + .vweight = t.vweight, + .file = .{ + .path = f.path, + .content = f.content, + .content_b64 = try dump.encodeBytes(alloc, f.content), + }, + } else if (t.image) |iv| blk: { + const pz = try std.fmt.allocPrintSentinel(alloc, "{s}", .{iv.path}, 0); + const bytes = readFileAlloc(alloc, pz) catch ""; + break :blk .{ + .kind = .image, + .tag = tag, + .body = body, + .scroll = scroll, + .cols = t.cols, + .rows = t.rows, + .vweight = t.vweight, + .image = .{ + .path = iv.path, + .bytes_b64 = if (bytes.len > 0) try dump.encodeBytes(alloc, bytes) else "", + }, + }; + } else blk: { + var cwdbuf: [1024]u8 = undefined; + const cwd = try alloc.dupe(u8, termCwd(t, &cwdbuf)); + const stream = try terminalReplayText(alloc, t); + break :blk .{ + .kind = .terminal, + .tag = tag, + .body = body, + .scroll = scroll, + .cols = t.cols, + .rows = t.rows, + .vweight = t.vweight, + .terminal = .{ + .cwd = cwd, + .stream = stream, + .stream_b64 = try dump.encodeBytes(alloc, t.tty_stream.items), + .cursor = .{ + .col = t.term.screens.active.cursor.x, + .row = t.term.screens.active.cursor.y, + }, + }, + }; + }; + try panes.append(alloc, pane); + } + + var columns: std.ArrayList(dump.Column) = .empty; + var c: usize = 0; + while (c < layout.ncol) : (c += 1) { + var ids: std.ArrayList(usize) = .empty; + var k: usize = 0; + while (k < layout.col_n[c]) : (k += 1) { + const old_id = layout.col_terms[c][k]; + if (slot_to_pane[old_id]) |compact| try ids.append(alloc, compact); + } + if (ids.items.len > 0) + try columns.append(alloc, .{ .weight = layout.col_weight[c], .panes = try ids.toOwnedSlice(alloc) }); + } + + const state: dump.State = .{ + .screen = .{ .cols = sw, .rows = sh }, + .active = slot_to_pane[active] orelse 0, + .topbar = topbar_str, + .theme = paint.currentThemeName(), + .columns = try columns.toOwnedSlice(alloc), + .panes = try panes.toOwnedSlice(alloc), + }; + try dump.writeFile(io, gpa, dump.default_path, state); +} + // The full rendered pane: tag at visual row 0, spliced body below. Caller frees. fn paneText(gpa: std.mem.Allocator, t: *Term) ![]u8 { const tag = try fullTag(gpa, t); |
