diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-27 22:34:56 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 7ba54de4bd498c2a56ea3d0d43c88356759f7b1f (patch) | |
| tree | 2e661d1785ef6e74c158ca6b2e1aa96b763ec7bc /src/dump.zig | |
| parent | 52049344c1992850ea712e029eb5c4a88a7dfe19 (diff) | |
| download | pardes-7ba54de4bd498c2a56ea3d0d43c88356759f7b1f.tar.gz pardes-7ba54de4bd498c2a56ea3d0d43c88356759f7b1f.zip | |
Move saving and loading a whole editor out of pardes.zig into dump.zig
Pure move, no behaviour change (acme keeps dump and load in rows.c):
dumpState, restore, initFromDump and initDump go verbatim to the end of
dump.zig, after the dump format they read and write. Inside dump.zig the
moved code's `dump.` prefix drops, so `Pane` there is the dump record; the
one editor pane it names is spelled `pardes.panes.Pane`, and its other
`panes.X` references become `pardes.panes.X` because dump.zig's own tests
use `panes` as a local name.
The methods become free functions taking `p: *Pardes`: `p.dumpState()`
becomes `dump.dumpState(p)`, `core.restore(bytes)` becomes
`dump.restore(core, bytes)` and `Pardes.initFromDump(..)` becomes
`dump.initFromDump(..)`, in pardes.zig, the shells, layout.zig,
Terminal.zig, builtins.zig and the tests (38 receiver rewrites plus the
initFromDump calls). The tag-tail restore helpers stay with the tag code.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/dump.zig')
| -rw-r--r-- | src/dump.zig | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/src/dump.zig b/src/dump.zig index ff5d2166..0ce2d10e 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -6,6 +6,26 @@ const limits = @import("memory.zig").limits; // wholesale (ghostty and uucode log there too), and a corrupt dump's parse // diagnostic is the only thing that says WHICH field went bad. const log = std.log.scoped(.dump); +const pardes = @import("pardes.zig"); +const layout = @import("layout.zig"); +const filesystem = @import("fs.zig"); +const ctlfs = @import("ninep/tree.zig"); +const exec = @import("exec.zig"); +const body_layer = @import("body_layer.zig"); +const colors = @import("colors.zig"); +const themes = colors.themes; +const config = @import("config.zig"); +const pdf_enabled = pardes.pdf_enabled; +const platform = pardes.platform; +const isolated = pardes.isolated; +const terminal_panes = pardes.terminal_panes; +const MAX_PANES = pardes.MAX_PANES; +const MAX_COLS = pardes.MAX_COLS; +const column_weight_unit = layout.column_weight_unit; +const max_column_weight = layout.max_column_weight; +const effect_path_cap = pardes.effect_path_cap; +const Options = pardes.Options; +const Pardes = pardes.Pardes; /// Where the next dump goes. $PARDES_DUMP wins verbatim (the snapshot harness /// pins it for deterministic goldens); else $XDG_DATA_HOME|~/.local/share @@ -570,3 +590,221 @@ test "validation bounds pane restore state" { panes[0].tag_tail = oversized_tail[0..limits.max_tag_tail]; try validate(state); } + +// ---- a whole editor to and from a dump (acme keeps this in rows.c) ---- + +pub fn dumpState(p: *Pardes) !void { + const arena = p.scratch.allocator(); + var slot_to_pane: [MAX_PANES]?usize = @splat(null); + var dump_panes: [MAX_PANES]Pane = undefined; + var panes_len: usize = 0; + for (p.panes, 0..) |slot, id| { + const pane = slot orelse continue; + slot_to_pane[id] = panes_len; + const tag = try p.tagText(arena, pane); + const body = try body_layer.bodyText(p, arena, pane); + const scroll: usize = @intCast(@max(0, pane.scroll())); + var dp: Pane = if (pane.file) |*f| try pardes.panes.File.dumpPane( + arena, + pane, + f, + tag, + body, + scroll, + if (f.output) |o| pardes.panes.Output.word(o.from) else "", + if (f.output) |*o| o.arg() else "", + ) else if (pane.hasPdf()) blk: { + if (comptime !pdf_enabled) unreachable; + const pv = &pane.pdf.?; + break :blk try pardes.panes.Image.dumpPane(p, arena, pane, tag, body, pv.page, pv.path, &.{}); + } else if (pane.image) |iv| + try pardes.panes.Image.dumpPane(p, arena, pane, tag, body, scroll, iv.path, iv.raw) + else + try pardes.panes.Terminal.dumpPane(pane, arena, tag, body, scroll); + dp.tag_tail = if (pane.tag_init) pane.tagSlice() else null; + dump_panes[panes_len] = dp; + panes_len += 1; + } + + var columns: [MAX_COLS]Column = undefined; + var columns_len: usize = 0; + var column_ids: [MAX_COLS][MAX_PANES]usize = undefined; + for (0..p.ncol) |c| { + var ids_len: usize = 0; + for (0..p.col_n[c]) |k| if (slot_to_pane[p.col_panes[c][k]]) |compact| { + column_ids[c][ids_len] = compact; + ids_len += 1; + }; + if (ids_len > 0) { + columns[columns_len] = .{ + .tag = if (p.col_tags[c].custom) p.columnTag(c) else null, + .weight = @as(f32, @floatFromInt(p.col_weight[c])) / @as(f32, @floatFromInt(column_weight_unit)), + .panes = column_ids[c][0..ids_len], + }; + columns_len += 1; + } + } + + var mounts: [filesystem.max_mounts]Mount = undefined; + for (p.fs.mounts.items, 0..) |mount, i| mounts[i] = .{ .name = mount.name, .dial = mount.dial }; + const state: State = .{ + .screen = .{ .cols = p.screen_w, .rows = p.screen_h }, + .active = slot_to_pane[p.active] orelse 0, + .topbar = config.topbar_str, + .topbar_custom = if (p.global_tag.custom) p.global_tag.slice("") else null, + .theme = p.theme().name, + .locations_config = p.locations_config, + .tree_context_tag_style = p.settings.tree_context_tag_style, + .columns = columns[0..columns_len], + .panes = dump_panes[0..panes_len], + .mounts = mounts[0..p.fs.mounts.items.len], + }; + try validate(state); + var out: std.Io.Writer.Allocating = .init(p.gpa); + errdefer out.deinit(); + try std.zon.stringify.serialize(state, .{ .whitespace = true }, &out.writer); + if (p.dump_out) |d| p.gpa.free(d); + p.dump_out = try out.toOwnedSlice(); + p.emit(.write_dump); +} + +pub fn restore(p: *Pardes, zon_bytes: []const u8) !*Pardes { + var opts = p.opts; + opts.cols = p.screen_w; + opts.rows = p.screen_h; + return initDump(p.gpa, opts, zon_bytes, p); +} + +pub fn initFromDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8) !*Pardes { + return initDump(gpa, opts, zon_bytes, null); +} + +fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes) !*Pardes { + const image_gpa = opts.image_allocator orelse gpa; + const pdf_gpa = opts.pdf_allocator orelse gpa; + const tree_sitter_gpa = opts.tree_sitter_allocator orelse gpa; + const p = try gpa.create(Pardes); + p.* = .{ + .gpa = gpa, + .image_gpa = image_gpa, + .pdf_gpa = pdf_gpa, + .tree_sitter_gpa = tree_sitter_gpa, + .opts = opts, + .fs = .{}, + .screen_w = opts.cols, + .screen_h = opts.rows, + .scratch = .init(gpa), + .frame_arena = .init(opts.frame_allocator orelse gpa), + .fallback = .{ .gpa = gpa }, + }; + errdefer p.deinit(); + if (previous) |old| { + p.next_serial = old.next_serial; + p.lsp_seq = old.lsp_seq; + p.pipe_seq = old.pipe_seq; + p.cell_pixels = old.cell_pixels; + p.row_metrics = old.row_metrics; + p.native_images = old.native_images; + } + for (opts.mounts) |mount| try p.fs.mount(gpa, mount.name, mount.dial); + p.opts.mounts = &.{}; + var parsed = try readZon(gpa, zon_bytes, "load"); + defer parsed.deinit(); + const st = parsed.value; + p.locations_config = st.locations_config; + p.settings.tree_context_tag_style = st.tree_context_tag_style; + for (st.mounts) |mount| { + var already_mounted = false; + for (p.fs.mounts.items) |existing| { + if (!std.mem.eql(u8, mount.name, existing.name)) continue; + if (!std.mem.eql(u8, mount.dial, existing.dial)) return error.MountConflict; + already_mounted = true; + break; + } + if (!already_mounted) try p.fs.mount(gpa, mount.name, mount.dial); + } + + for (themes, 0..) |t, i| { + if (std.mem.eql(u8, t.name, st.theme)) p.settings.theme = @intCast(i); + } + for (st.panes, 0..) |src, i| { + if (i >= MAX_PANES) break; + const pane: *pardes.panes.Pane = switch (src.kind) { + .terminal => terminal: { + const t = src.terminal.?; + // A host that runs shells gets them back: the old history + // above a new shell in the directory it was in. The web + // shell and an isolated build only ever replay it. + const live = comptime (terminal_panes and platform != .web and !isolated); + const revive = live and t.cwd.len <= effect_path_cap; + const restored = try pardes.panes.Terminal.restore(p, src, revive); + p.installPane(i, restored); + try restored.setOwnedCwd(t.cwd); + if (std.mem.startsWith(u8, src.tag, "TTY ")) restored.mode = .tty; + if (revive) p.emit(.{ .spawn = .{ .pane = @intCast(i), .cwd = .from(t.cwd) } }); + break :terminal restored; + }, + .file => try pardes.panes.File.restore(p, i, src), + .image => restore_image: { + const im = src.image.?; + if (pardes.panes.Pdf.isPath(im.path)) { + var raw: []u8 = if (im.bytes_b64.len > 0) + try decodeBytes(gpa, im.bytes_b64) + else + &.{}; + errdefer if (raw.len > 0) gpa.free(raw); + if (comptime pdf_enabled) { + if (pardes.panes.Pdf.openPane(p, i, im.path, src.scroll + 1) catch null) |restored| { + if (raw.len > 0) gpa.free(raw); + raw = &.{}; + restored.cols = @max(1, src.cols); + restored.rows = @max(1, src.rows); + break :restore_image restored; + } + } + // If the source path is unavailable, retain its bytes + // as an ordinary file rather than dropping the pane. + const path = try gpa.dupe(u8, im.path); + errdefer gpa.free(path); + const content = if (raw.len > 0) raw else try gpa.dupe(u8, ""); + raw = &.{}; + errdefer if (content.len > 0) gpa.free(content); + const history = try pardes.panes.File.History.create(gpa); + errdefer gpa.destroy(history); + const restored = try p.newDocPane(i); + restored.file = .{ .path = path, .content = content, .history = history }; + restored.cur_pinned = true; + restored.cols = @max(1, src.cols); + restored.rows = @max(1, src.rows); + p.emit(.{ .watch = .{ .pane = @intCast(i), .on = true, .mode = .baseline_disk } }); + break :restore_image restored; + } + break :restore_image try pardes.panes.Image.restore(p, i, src); + }, + }; + p.restoreDumpTail(pane, src); + pane.vweight = src.vweight; + pane.collapsed = src.collapsed; + } + p.ncol = @min(st.columns.len, MAX_COLS); + p.col_serial = @splat(0); + if (st.topbar_custom) |bar| _ = p.global_tag.set(bar); + for (st.columns[0..p.ncol], 0..) |col, c| { + if (col.tag) |bar| _ = p.col_tags[c].set(bar); + const scaled = @as(f64, @floatCast(col.weight)) * @as(f64, @floatFromInt(column_weight_unit)); + const bounded = @min(scaled, @as(f64, @floatFromInt(max_column_weight))); + p.col_weight[c] = @max(1, @as(u64, @intFromFloat(@round(bounded)))); + p.col_n[c] = @min(col.panes.len, MAX_PANES); + for (col.panes[0..p.col_n[c]], 0..) |pid, k| p.col_panes[c][k] = pid; + } + p.active = @min(st.active, MAX_PANES - 1); + p.sync(); + exec.applyStartupConfig(p); + colors.finishThemeInitialization(p); + p.sync(); + p.presentation.enabled = true; + _ = p.takeHaptic(); // see init: a restored session is not a gesture + try p.fs.log.buf.ensureTotalCapacityPrecise(p.gpa, p.fs.log.cap); + ctlfs.events.announce(p); + return p; +} |
