summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Terminal.zig4
-rw-r--r--src/builtins.zig3
-rw-r--r--src/detached/client.zig3
-rw-r--r--src/detached/server.zig7
-rw-r--r--src/dump.zig238
-rw-r--r--src/gui/gui.zig11
-rw-r--r--src/layout.zig5
-rw-r--r--src/macos.zig7
-rw-r--r--src/pardes.zig260
-rw-r--r--src/tty/tty.zig5
-rw-r--r--src/web.zig2
11 files changed, 287 insertions, 258 deletions
diff --git a/src/Terminal.zig b/src/Terminal.zig
index a8588952..cb752613 100644
--- a/src/Terminal.zig
+++ b/src/Terminal.zig
@@ -628,8 +628,8 @@ test "a restored terminal keeps its history above a marker and gets a shell wher
const pane = p.panes[0].?;
feedOutput(p, pane, "old output\r\n$ ls\r\n");
try pane.setOwnedCwd("/tmp");
- try p.dumpState();
- const restored = try Pardes.initFromDump(gpa, .{ .tty_only = true, .cols = 40, .rows = 12 }, p.dump_out.?);
+ try dump.dumpState(p);
+ const restored = try dump.initFromDump(gpa, .{ .tty_only = true, .cols = 40, .rows = 12 }, p.dump_out.?);
defer restored.deinit();
var spawned = false;
while (restored.nextEffect()) |effect| switch (effect) {
diff --git a/src/builtins.zig b/src/builtins.zig
index 19be62e2..743573e4 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -1,6 +1,7 @@
//! Command structs and runtime settings form the builtin registry; bindings live in config.zig.
const std = @import("std");
const pardes = @import("pardes.zig");
+const dump = @import("dump.zig");
const exec = @import("exec.zig");
const look = @import("look.zig");
const edit = @import("edit.zig");
@@ -239,7 +240,7 @@ pub const Kill = struct {
pub const Dump = struct {
pub fn run(c: Ctx) void {
- c.p.dumpState() catch {};
+ dump.dumpState(c.p) catch {};
}
};
diff --git a/src/detached/client.zig b/src/detached/client.zig
index a027c0bb..7cee790a 100644
--- a/src/detached/client.zig
+++ b/src/detached/client.zig
@@ -59,6 +59,7 @@
const std = @import("std");
const libc = std.c;
const pardes = @import("../pardes.zig");
+const dump = @import("../dump.zig");
const server = @import("server.zig");
const wire = @import("wire.zig");
@@ -842,7 +843,7 @@ test "detached Restore keeps attached frontends and follows queued frames with a
try testing.expectEqual(fd, h.session.clients[c.slot].fd);
try testing.expect(h.session.clients[c.slot].attached);
- try h.core.dumpState();
+ try dump.dumpState(h.core);
const saved = try testing.allocator.dupe(u8, h.core.dump_out.?);
defer testing.allocator.free(saved);
while (h.core.nextEffect()) |_| {}
diff --git a/src/detached/server.zig b/src/detached/server.zig
index e33354cf..b75fa5ad 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -4,6 +4,7 @@ const builtin = @import("builtin");
const libc = std.c;
const posix = std.posix;
const pardes = @import("../pardes.zig");
+const dump = @import("../dump.zig");
const wire = @import("wire.zig");
const host_io = @import("../host_io.zig");
@@ -233,7 +234,7 @@ pub const Session = struct {
}
pub fn restore(s: *Session, bytes: []const u8) !void {
- const replacement = try s.core.restore(bytes);
+ const replacement = try dump.restore(s.core, bytes);
s.cancelWorkers();
for (0..s.ptys.len) |pane| s.closePty(@intCast(pane));
s.harvest();
@@ -1129,7 +1130,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options, name: []const u8) !void
const core = if (options.load_path) |lp| blk: {
const bytes = try filesystem.readFile(gpa, lp);
defer gpa.free(bytes);
- break :blk try pardes.Pardes.initFromDump(allocs.pardes, options, bytes);
+ break :blk try pardes.dump.initFromDump(allocs.pardes, options, bytes);
} else try pardes.Pardes.init(allocs.pardes, options);
var session: Session = .{
.gpa = gpa,
@@ -1197,7 +1198,7 @@ test "detached queued results preserve current requests and are discarded before
try std.testing.expectEqual(current_id, s.lsp_task.?.id);
try std.testing.expectEqual(current_id, s.core.lsp_wait.?.id);
- try s.core.dumpState();
+ try dump.dumpState(s.core);
const saved = try gpa.dupe(u8, s.core.dump_out.?);
defer gpa.free(saved);
while (s.core.nextEffect()) |_| {}
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;
+}
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 86c69ff6..f044ca17 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -6,6 +6,7 @@ const libc = std.c;
const vaxis = @import("vaxis"); // test modes only: the stdin escape-seq parser
const ghostty_vt = @import("ghostty-vt"); // 256-color palette for .index cells
const pardes = @import("../pardes.zig");
+const dump = @import("../dump.zig");
const config = @import("../config.zig");
const look = @import("../look.zig");
const message = pardes.Messages.Message;
@@ -1298,7 +1299,7 @@ test "GUI PTY Restore joins a real reader waiting for queue space" {
const gpa = std.testing.allocator;
var core = try pardes.Pardes.init(gpa, .{ .tty_only = true });
defer core.deinit();
- try core.dumpState();
+ try dump.dumpState(core);
var workers: LspWorkers = .{};
var queue: Queue = .{ .gpa = gpa, .lsp_allocator = gpa, .lsp_workers = &workers, .sdl_wake = false };
defer queue.deinit();
@@ -1321,7 +1322,7 @@ test "GUI PTY Restore joins a real reader waiting for queue space" {
try std.testing.expect(host_io.writeFd(child.file.handle, "printf 'after-full'; exit\n"));
try PtyTests.waitBlocked(&queue);
const old_serial = core.panes[0].?.serial;
- const replacement = try core.restore(core.dump_out.?);
+ const replacement = try dump.restore(core, core.dump_out.?);
shell.stopPtys();
core.deinit();
core = replacement;
@@ -2453,7 +2454,7 @@ fn localSession(
var core = if (opts.load_path) |lp| blk: {
const bytes = try @import("../fs.zig").readFile(gpa, lp);
defer gpa.free(bytes);
- break :blk try pardes.Pardes.initFromDump(allocs.pardes, opts, bytes);
+ break :blk try pardes.dump.initFromDump(allocs.pardes, opts, bytes);
} else try pardes.Pardes.init(allocs.pardes, opts);
defer core.deinit();
core.native_images = true;
@@ -2559,7 +2560,7 @@ fn localSession(
break :blk;
};
defer gpa.free(bytes);
- const nc = core.restore(bytes) catch |err| {
+ const nc = dump.restore(core, bytes) catch |err| {
core.reportError(core.active, "Restore", err);
break :blk;
};
@@ -2772,7 +2773,7 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
const core = if (opts.load_path) |lp| blk: {
const bytes = try @import("../fs.zig").readFile(gpa, lp);
defer gpa.free(bytes);
- break :blk try pardes.Pardes.initFromDump(allocs.pardes, opts, bytes);
+ break :blk try pardes.dump.initFromDump(allocs.pardes, opts, bytes);
} else try pardes.Pardes.init(allocs.pardes, opts);
defer core.deinit();
if (opts.load_path == null)
diff --git a/src/layout.zig b/src/layout.zig
index 116a64b8..3d8932a8 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -1,5 +1,6 @@
const std = @import("std");
const pardes = @import("pardes.zig");
+const dump = @import("dump.zig");
const config = @import("config.zig");
const panes = @import("panes.zig");
const Pardes = pardes.Pardes;
@@ -1442,8 +1443,8 @@ test "collapse dump restores folded state and retained expansion weight" {
compute(p);
const original_height = p.rects[0].h;
toggleCollapse(p, 0);
- try p.dumpState();
- const restored = try Pardes.initFromDump(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 32 }, p.dump_out.?);
+ try dump.dumpState(p);
+ const restored = try dump.initFromDump(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 32 }, p.dump_out.?);
defer restored.deinit();
try std.testing.expect(restored.panes[0].?.collapsed);
try std.testing.expectEqual(@as(f32, @floatFromInt(original_height)), restored.panes[0].?.vweight);
diff --git a/src/macos.zig b/src/macos.zig
index f83fb8b4..78356810 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -6,6 +6,7 @@ const builtin = @import("builtin");
const posix = std.posix;
const libc = std.c;
const pardes = @import("pardes.zig");
+const dump = @import("dump.zig");
const mouse = @import("mouse.zig");
const colors = @import("colors.zig");
const look = @import("look.zig");
@@ -1344,7 +1345,7 @@ fn restoreCore(st: *State) bool {
return false;
};
defer st.gpa.free(bytes);
- const replacement = st.core.restore(bytes) catch |err| {
+ const replacement = dump.restore(st.core, bytes) catch |err| {
st.core.reportError(st.core.active, "Restore", err);
return false;
};
@@ -1411,7 +1412,7 @@ test "mac Restore keeps host state and rejects callbacks from the old core" {
const old_serial = core.panes[0].?.serial;
st.gens[0] = 23;
const old_watch = try st.file_watches.replace(gpa, 0, "/test.txt", old_serial, .{ .text = 0 });
- try core.dumpState();
+ try dump.dumpState(core);
try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "restore.zon", .data = core.dump_out.? });
while (core.nextEffect()) |_| {}
@@ -1501,7 +1502,7 @@ test "mac Restore cancels a PTY reader waiting for output capacity" {
defer st.config_arena.deinit();
defer st.file_watches.deinit(gpa);
defer st.inbox.close(gpa, io);
- try st.core.dumpState();
+ try dump.dumpState(st.core);
try tmp.dir.writeFile(io, .{ .sub_path = "restore.zon", .data = st.core.dump_out.? });
while (st.core.nextEffect()) |_| {}
for (0..inbox_output_limit) |_| try st.inbox.pushOutput(gpa, io, .{
diff --git a/src/pardes.zig b/src/pardes.zig
index 75766827..44f0d2ea 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -779,8 +779,8 @@ test "LocationsConfig command reports partial updates and survives restore" {
try std.testing.expectEqualStrings("LocationsConfig context:5 tscontext:on tslocations:off layout:stacked\n", file.content);
}
try std.testing.expectEqual(@as(usize, 1), reports);
- try p.dumpState();
- const restored = try Pardes.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
+ try dump.dumpState(p);
+ const restored = try dump.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
defer restored.deinit();
try std.testing.expectEqual(expected, restored.locations_config);
}
@@ -829,8 +829,8 @@ test "TreeContext tag style preserves message rows and its opt-out survives rest
try std.testing.expectEqual(@as(u16, 0), p.surface.body_layers[0].rows);
try std.testing.expectEqual(@as(u16, 0), pane.body_rows);
try std.testing.expectEqual(FontRole.body, p.surface.at(r.x + config.GUTTER, p.bodyTop(r)).style.font_role);
- try p.dumpState();
- const restored = try Pardes.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
+ try dump.dumpState(p);
+ const restored = try dump.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
defer restored.deinit();
try std.testing.expect(!restored.settings.tree_context_tag_style);
}
@@ -858,8 +858,8 @@ test "TreeContext command and custom tag survive a workspace round trip" {
try std.testing.expect(pane.file.?.tree_context);
try std.testing.expect(pane.appendTag(" Save Keep Del"));
pane.tag_init = true;
- try p.dumpState();
- const restored = try Pardes.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
+ try dump.dumpState(p);
+ const restored = try dump.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
defer restored.deinit();
const saved = restored.panes[0].?;
try std.testing.expect(saved.file.?.tree_context);
@@ -1397,7 +1397,7 @@ test "image dump restores source bytes renderer choices and exact custom tail" {
pane.image.?.ascii = false;
try std.testing.expect(pane.appendTag(" Keep Del"));
pane.tag_init = true;
- try p.dumpState();
+ try dump.dumpState(p);
var first_dump = try dump.readZon(gpa, p.dump_out.?, "image-first-dump");
defer first_dump.deinit();
@@ -1407,7 +1407,7 @@ test "image dump restores source bytes renderer choices and exact custom tail" {
try std.testing.expect(!first.panes[0].image.?.ascii);
try std.testing.expectEqualStrings(" Keep Del", first.panes[0].tag_tail.?);
- const restored = try Pardes.initFromDump(gpa, .{ .tty_only = true }, p.dump_out.?);
+ const restored = try dump.initFromDump(gpa, .{ .tty_only = true }, p.dump_out.?);
defer restored.deinit();
const restored_pane = restored.panes[0].?;
try std.testing.expect(restored_pane.image.?.glyph_art);
@@ -1417,7 +1417,7 @@ test "image dump restores source bytes renderer choices and exact custom tail" {
try std.testing.expectEqualStrings(" Keep Del", restored_pane.tagSlice());
try std.testing.expectEqualSlices(u8, source, restored_pane.image.?.raw);
- try restored.dumpState();
+ try dump.dumpState(restored);
var parsed = try dump.readZon(gpa, restored.dump_out.?, "image-redump");
defer parsed.deinit();
const redump = parsed.value;
@@ -1767,13 +1767,13 @@ test "owned cwd restore either copies the directory or preserves the old core" {
const p = try Pardes.init(allocator.allocator(), .{ .tty_only = true });
defer p.deinit();
p.setCwd(0, "/retained/terminal/directory");
- try p.dumpState();
+ try dump.dumpState(p);
const original = p.panes[0].?;
var completed = false;
for (0..256) |failure| {
allocator.has_induced_failure = false;
allocator.fail_index = allocator.alloc_index + failure;
- const replacement = p.restore(p.dump_out.?) catch {
+ const replacement = dump.restore(p, p.dump_out.?) catch {
try std.testing.expect(allocator.has_induced_failure);
try std.testing.expectEqual(original, p.panes[0].?);
try std.testing.expectEqualStrings("/retained/terminal/directory", original.cwdSlice());
@@ -2305,9 +2305,9 @@ test "startup config overrides restored styling before initFromDump returns" {
const source = try Pardes.init(std.testing.allocator, .{ .startup_config = "Theme dark\n" });
defer source.deinit();
try std.testing.expectEqualStrings("dark", source.theme().name);
- try source.dumpState();
+ try dump.dumpState(source);
- const restored = try Pardes.initFromDump(
+ const restored = try dump.initFromDump(
std.testing.allocator,
.{ .startup_config = "Theme acme\n" },
source.dump_out.?,
@@ -2324,9 +2324,9 @@ test "restored sessions animate layout changes after bootstrap" {
if (platform == .web) return;
const source = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 16 });
defer source.deinit();
- try source.dumpState();
+ try dump.dumpState(source);
- const restored = try Pardes.initFromDump(
+ const restored = try dump.initFromDump(
std.testing.allocator,
.{ .tty_only = true, .cols = 80, .rows = 16, .startup_config = "PanelSlide\n" },
source.dump_out.?,
@@ -2355,9 +2355,9 @@ test "restored terminals keep monotonic nonzero pane identities" {
// Filter is a fresh presentation default, not emulator/dump state. Even a
// pane toggled off in the source session comes back with the default on.
source.panes[1].?.tty_filter = false;
- try source.dumpState();
+ try dump.dumpState(source);
- const restored = try Pardes.initFromDump(gpa, .{}, source.dump_out.?);
+ const restored = try dump.initFromDump(gpa, .{}, source.dump_out.?);
defer restored.deinit();
var ids: [MAX_PANES]usize = undefined;
@@ -2721,14 +2721,14 @@ test "editable workspace and column tags replace selections and persist" {
p.update(.{ .paste = "Grep " });
try std.testing.expect(std.mem.startsWith(u8, p.columnTag(0), "Grep New"));
try std.testing.expectEqualStrings("untouched\n", p.panes[0].?.file.?.content);
- try p.dumpState();
- const restored = try p.restore(p.dump_out.?);
+ try dump.dumpState(p);
+ const restored = try dump.restore(p, p.dump_out.?);
defer restored.deinit();
try std.testing.expectEqualStrings(p.global_tag.slice(""), restored.global_tag.slice(""));
try std.testing.expectEqualStrings(p.columnTag(0), restored.columnTag(0));
try std.testing.expect(p.col_tags[0].set(""));
- try p.dumpState();
- const empty = try p.restore(p.dump_out.?);
+ try dump.dumpState(p);
+ const empty = try dump.restore(p, p.dump_out.?);
defer empty.deinit();
try std.testing.expectEqualStrings("", empty.columnTag(0));
}
@@ -5819,223 +5819,7 @@ pub const Pardes = struct {
// ---- dump / load (acme-style: `pardes -l state.zon` restores a session) ----
- 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]dump.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: dump.Pane = if (pane.file) |*f| try panes.File.dumpPane(
- arena,
- pane,
- f,
- tag,
- body,
- scroll,
- if (f.output) |o| 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 panes.Image.dumpPane(p, arena, pane, tag, body, pv.page, pv.path, &.{});
- } else if (pane.image) |iv|
- try panes.Image.dumpPane(p, arena, pane, tag, body, scroll, iv.path, iv.raw)
- else
- try 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]dump.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]dump.Mount = undefined;
- for (p.fs.mounts.items, 0..) |mount, i| mounts[i] = .{ .name = mount.name, .dial = mount.dial };
- const state: dump.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 dump.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 dump.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: *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 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 panes.File.restore(p, i, src),
- .image => restore_image: {
- const im = src.image.?;
- if (panes.Pdf.isPath(im.path)) {
- var raw: []u8 = if (im.bytes_b64.len > 0)
- try dump.decodeBytes(gpa, im.bytes_b64)
- else
- &.{};
- errdefer if (raw.len > 0) gpa.free(raw);
- if (comptime pdf_enabled) {
- if (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 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 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;
- }
-
- fn restoreDumpTail(p: *Pardes, pane: *Pane, src: dump.Pane) void {
+ pub fn restoreDumpTail(p: *Pardes, pane: *Pane, src: dump.Pane) void {
if (src.tag_tail) |tail| {
if (!pane.appendTag(tail)) return;
pane.tag_init = true;
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 89a476f2..fe968650 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -5,6 +5,7 @@ const posix = std.posix;
const libc = std.c;
const vaxis = @import("vaxis");
const pardes = @import("../pardes.zig");
+const dump = @import("../dump.zig");
const tracy = @import("../tracy.zig");
const look = @import("../look.zig");
const message = pardes.Messages.Message;
@@ -700,7 +701,7 @@ fn localSession(
var core = if (options.load_path) |lp| blk: {
const bytes = try filesystem.readFile(gpa, lp);
defer gpa.free(bytes);
- break :blk try pardes.Pardes.initFromDump(allocs.pardes, options, bytes);
+ break :blk try pardes.dump.initFromDump(allocs.pardes, options, bytes);
} else try pardes.Pardes.init(allocs.pardes, options);
defer core.deinit();
@@ -802,7 +803,7 @@ fn localSession(
break :blk;
};
defer gpa.free(bytes);
- const nc = core.restore(bytes) catch |err| {
+ const nc = dump.restore(core, bytes) catch |err| {
core.reportError(core.active, "Restore", err);
break :blk;
};
diff --git a/src/web.zig b/src/web.zig
index 4c2a9db8..b77d07a6 100644
--- a/src/web.zig
+++ b/src/web.zig
@@ -117,7 +117,7 @@ const State = struct {
pardes.syntax.start(allocs.tree_sitter);
errdefer pardes.syntax.stop();
- const core = try pardes.Pardes.initFromDump(allocs.pardes, .{
+ const core = try pardes.dump.initFromDump(allocs.pardes, .{
.cols = cols,
.rows = rows,
.frame_allocator = allocs.frame,