summaryrefslogtreecommitdiff
path: root/src/dump.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/dump.zig')
-rw-r--r--src/dump.zig238
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;
+}