diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Terminal.zig | 4 | ||||
| -rw-r--r-- | src/builtins.zig | 3 | ||||
| -rw-r--r-- | src/detached/client.zig | 3 | ||||
| -rw-r--r-- | src/detached/server.zig | 7 | ||||
| -rw-r--r-- | src/dump.zig | 238 | ||||
| -rw-r--r-- | src/gui/gui.zig | 11 | ||||
| -rw-r--r-- | src/layout.zig | 5 | ||||
| -rw-r--r-- | src/macos.zig | 7 | ||||
| -rw-r--r-- | src/pardes.zig | 260 | ||||
| -rw-r--r-- | src/tty/tty.zig | 5 | ||||
| -rw-r--r-- | src/web.zig | 2 |
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, |
