diff options
Diffstat (limited to 'src/dump.zig')
| -rw-r--r-- | src/dump.zig | 127 |
1 files changed, 118 insertions, 9 deletions
diff --git a/src/dump.zig b/src/dump.zig index ef442ddb..88a95994 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -53,27 +53,34 @@ pub fn outPath(buf: *[1024:0]u8, dir_setting: []const u8) ?[:0]const u8 { ds.getSecondsIntoMinute(), }, 0) catch null; } +/// The host's environment; a freestanding core (web, the board) has none +/// and no libc to ask, so only a DumpDir with no `~/` names a directory there. +fn hostEnv(name: [*:0]const u8) ?[*:0]u8 { + if (builtin.os.tag == .freestanding) return null; + return std.c.getenv(name); +} + /// Where dumps go and relative Restore names are looked up: the `DumpDir` /// setting when set (a leading `~/` is the home directory), else the default. pub fn directory(buf: []u8, dir_setting: []const u8) ?[]const u8 { if (dir_setting.len == 0) return defaultDirectory(buf); - if (std.mem.startsWith(u8, dir_setting, "~/")) if (std.c.getenv("HOME")) |h| + if (std.mem.startsWith(u8, dir_setting, "~/")) if (hostEnv("HOME")) |h| return std.fmt.bufPrint(buf, "{s}/{s}", .{ std.mem.span(h), dir_setting[2..] }) catch null; return std.fmt.bufPrint(buf, "{s}", .{dir_setting}) catch null; } /// The standard dump directory, without creating it or generating a filename. pub fn defaultDirectory(buf: []u8) ?[]const u8 { - if (std.c.getenv("XDG_DATA_HOME")) |x| + if (hostEnv("XDG_DATA_HOME")) |x| return std.fmt.bufPrint(buf, "{s}/pardes", .{std.mem.span(x)}) catch null; - if (std.c.getenv("HOME")) |h| + if (hostEnv("HOME")) |h| return std.fmt.bufPrint(buf, "{s}/.local/share/pardes", .{std.mem.span(h)}) catch null; return null; } pub const magic = "pardes-dump"; pub const version: u32 = 1; -pub const max_panes: usize = 16; +pub const max_panes: usize = MAX_PANES; pub const max_cols: usize = 6; /// Output arguments are typed in the same bounded one-line tag storage. Keep /// the schema limit named independently so a dump reader can validate it @@ -142,6 +149,9 @@ pub const File = struct { origin_arg: []const u8 = "", mini_source: []const u8 = "", mini_colors_b64: []const u8 = "", + /// Its dot, q0 and q1 in bytes, as acme's dump keeps a window's; null + /// in older dumps. + dot: ?[2]u32 = null, }; pub const ImagePalette = enum { @@ -180,6 +190,9 @@ pub const Pane = struct { }; pub const Column = struct { + /// Its serial when dumped (0 in older dumps): a Restore logs + /// `restoredcol <old> <new>` for it. + serial: u32 = 0, tag: ?[]const u8 = null, weight: f32 = 1, panes: []const usize = &.{}, @@ -200,6 +213,11 @@ pub const State = struct { columns: []const Column = &.{}, panes: []const Pane = &.{}, mounts: []const Mount = &.{}, + /// The settings that differ from a fresh session's, each a line as the + /// root ctl takes it (`Placement pardes`); the theme and the locations + /// config are kept on their own, and the font is the frontend's. Absent + /// in older dumps. + settings: []const []const u8 = &.{}, }; /// A header is a text like any other: it may hold newlines, but not a @@ -305,6 +323,15 @@ pub const Parsed = struct { } }; +/// Whether `bytes` are a dump a Restore could load, saying nothing: a +/// Restore asks before it warns about unsaved text or replaces anything. +pub fn check(gpa: std.mem.Allocator, bytes: []const u8) !void { + var arena: std.heap.ArenaAllocator = .init(gpa); + defer arena.deinit(); + const source = try arena.allocator().dupeZ(u8, bytes); + try validate(try std.zon.parse.fromSliceAlloc(State, arena.allocator(), source, null, .{ .free_on_error = false })); +} + pub fn readZon(gpa: std.mem.Allocator, bytes: []const u8, label: []const u8) !Parsed { var parsed: Parsed = .{ .value = undefined, .arena = .init(gpa) }; errdefer parsed.deinit(); @@ -636,6 +663,10 @@ pub fn dumpState(p: *Pardes) !void { else try pardes.panes.Terminal.dumpPane(pane, arena, tag, body, scroll); dp.tag_tail = pane.tag.own; + if (dp.file) |*df| { + const dot = ctlfs.pane.dotOf(pane); + df.dot = .{ dot.q0, dot.q1 }; + } dp.serial = pane.serial; dump_panes[panes_len] = dp; panes_len += 1; @@ -651,6 +682,7 @@ pub fn dumpState(p: *Pardes) !void { ids_len += 1; }; columns[columns_len] = .{ + .serial = layout.columnSerial(p, c), .tag = p.col_tags[c].own, .weight = @as(f32, @floatFromInt(p.col_weight[c])) / @as(f32, @floatFromInt(column_weight_unit)), .panes = column_ids[c][0..ids_len], @@ -671,6 +703,7 @@ pub fn dumpState(p: *Pardes) !void { .columns = columns[0..columns_len], .panes = dump_panes[0..panes_len], .mounts = mounts[0..p.fs.mounts.items.len], + .settings = try changedSettings(p, arena), }; try validate(state); var out: std.Io.Writer.Allocating = .init(p.gpa); @@ -681,20 +714,55 @@ pub fn dumpState(p: *Pardes) !void { p.emit(.write_dump); } +/// The setting lines that differ from a fresh session's, as the root ctl +/// reads them, less the ones a dump keeps apart. +fn changedSettings(p: *Pardes, arena: std.mem.Allocator) ![]const []const u8 { + var now: std.Io.Writer.Allocating = .init(arena); + var fresh: std.Io.Writer.Allocating = .init(arena); + const defaults: config.Runtime = .{}; + try pardes.ctlfs.ctl.writeSettingsOf(p, &p.settings, &now.writer); + try pardes.ctlfs.ctl.writeSettingsOf(p, &defaults, &fresh.writer); + var out: std.ArrayList([]const u8) = .empty; + var lines = std.mem.splitScalar(u8, now.written(), '\n'); + while (lines.next()) |line| { + if (line.len == 0) continue; + const word = line[0 .. std.mem.indexOfScalar(u8, line, ' ') orelse line.len]; + if (std.mem.eql(u8, word, "Theme") or std.mem.eql(u8, word, "Font")) continue; + var same = std.mem.splitScalar(u8, fresh.written(), '\n'); + while (same.next()) |other| { + if (std.mem.eql(u8, other, line)) break; + } else try out.append(arena, line); + } + return out.items; +} + +/// Applies a dump's setting lines, each as the root ctl would. +fn applySettings(p: *Pardes, lines: []const []const u8) void { + for (lines) |line| { + const sp = std.mem.indexOfScalar(u8, line, ' '); + const setting = config.Runtime.find(line[0 .. sp orelse line.len]) orelse continue; + _ = p.settings.apply(setting, if (sp) |s| line[s + 1 ..] else null); + } +} + /// The replacement core for a Restore of the dump at `from`, whose log says /// so after its panes' `new`s, for a client that reconnects to read. pub fn restore(p: *Pardes, zon_bytes: []const u8, from: []const u8) !*Pardes { var opts = p.opts; opts.cols = p.screen_w; opts.rows = p.screen_h; - var old: [MAX_PANES]u32 = @splat(0); + var old: OldSerials = .{}; const replacement = try initDump(p.gpa, opts, zon_bytes, p, &old); pardes.ctlfs.events.notePath(replacement, "restore", from); - // Which serial each pane had, for a script that held the old ones. - for (replacement.panes, old) |slot, was| if (slot) |pane| if (was != 0) { + // Which serial each pane and column had, for a script that held them. + for (replacement.panes, old.panes) |slot, was| if (slot) |pane| if (was != 0) { var rec: [32]u8 = undefined; pardes.ctlfs.events.notePath(replacement, "restored", std.fmt.bufPrint(&rec, "{d} {d}", .{ was, pane.serial }) catch continue); }; + for (old.cols[0..replacement.ncol], 0..) |was, c| if (was != 0) { + var rec: [32]u8 = undefined; + pardes.ctlfs.events.notePath(replacement, "restoredcol", std.fmt.bufPrint(&rec, "{d} {d}", .{ was, layout.columnSerial(replacement, c) }) catch continue); + }; return replacement; } @@ -702,7 +770,13 @@ pub fn initFromDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8 return initDump(gpa, opts, zon_bytes, null, null); } -fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes, old_serials: ?*[MAX_PANES]u32) !*Pardes { +/// The serials a dump's panes and columns had, read as it is restored. +const OldSerials = struct { + panes: [MAX_PANES]u32 = @splat(0), + cols: [MAX_COLS]u32 = @splat(0), +}; + +fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes, old_serials: ?*OldSerials) !*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; @@ -736,6 +810,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo const st = parsed.value; p.locations_config = st.locations_config; p.settings.tree_context_tag_style = st.tree_context_tag_style; + applySettings(p, st.settings); for (st.mounts) |mount| { var already_mounted = false; for (p.fs.mounts.items) |existing| { @@ -752,7 +827,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo } for (st.panes, 0..) |src, i| { if (i >= MAX_PANES) break; - if (old_serials) |o| o[i] = src.serial; + if (old_serials) |o| o.panes[i] = src.serial; const pane: *pardes.panes.Pane = switch (src.kind) { .terminal => terminal: { const t = src.terminal.?; @@ -822,6 +897,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo p.col_serial = @splat(0); if (st.topbar_custom) |bar| p.global_tag.own = try p.gpa.dupe(u8, bar); for (st.columns[0..p.ncol], 0..) |col, c| { + if (old_serials) |o| o.cols[c] = col.serial; if (col.tag) |bar| p.col_tags[c].own = try p.gpa.dupe(u8, bar); const scaled = @as(f64, @floatCast(col.weight)) * @as(f64, @floatFromInt(column_weight_unit)); const bounded = @min(scaled, @as(f64, @floatFromInt(max_column_weight))); @@ -840,3 +916,36 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo ctlfs.events.announce(p); return p; } + +test "a dump keeps the settings that differ from a fresh session's, and a restore applies them" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true }); + defer p.deinit(); + try std.testing.expect(p.executeBuiltinLine(p.active, "Placement pardes")); + try std.testing.expect(p.executeBuiltinLine(p.active, "Verbose off")); + try dumpState(p); + try std.testing.expect(std.mem.indexOf(u8, p.dump_out.?, "\"Placement pardes\"") != null); + // What is left at its default is not written. + try std.testing.expect(std.mem.indexOf(u8, p.dump_out.?, "\"BootShell keep\"") == null); + const restored = try initFromDump(gpa, .{ .tty_only = true }, p.dump_out.?); + defer restored.deinit(); + try std.testing.expectEqual(config.Runtime.Placement.pardes, restored.settings.placement); + try std.testing.expect(!restored.settings.verbose); +} + +test "a restored pane keeps its dot, as acme's dump keeps a window's" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true }); + defer p.deinit(); + const pane = try p.setTestFile("one two three\nfour\n"); + ctlfs.pane.setDot(pane, .{ .q0 = 4, .q1 = 7 }); + try dumpState(p); + const restored = try initFromDump(gpa, .{ .tty_only = true }, p.dump_out.?); + defer restored.deinit(); + const again = for (restored.panes) |slot| { + if (slot) |candidate| if (candidate.file != null) break candidate; + } else return error.FileLost; + const dot = ctlfs.pane.dotOf(again); + try std.testing.expectEqual(@as(u32, 4), dot.q0); + try std.testing.expectEqual(@as(u32, 7), dot.q1); +} |
