From a6c4ab97de9f9624d4d4d37a4f63cae342e314fb Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 23:48:17 -0300 Subject: A dump keeps the settings changed from a fresh session's, and Restore sets them A session running with Placement pardes came back from Restore as acme: a dump kept the layout and the theme but no setting. It now keeps every setting line that differs from a fresh session's, as the root ctl reads it, and a Restore applies them; the theme stays where it was kept, the font the frontend's, and REPL bindings are still dropped. The dump golden gains the three the snapshot harness sets. Co-Authored-By: Claude Opus 5.5 --- src/dump.zig | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/ninep/ctl.zig | 18 +++++++++++------- 2 files changed, 65 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/dump.zig b/src/dump.zig index 5ebb3c8d..77e2c2ba 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -200,6 +200,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 @@ -680,6 +685,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); @@ -690,6 +696,37 @@ 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 { @@ -745,6 +782,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| { @@ -849,3 +887,19 @@ 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); +} diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 4faa7df7..11de3c1c 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -333,13 +333,22 @@ const e_prompt = "control message needs its argument"; /// What is not a setting -- platform, themes, where the config came from -- /// is /status's or the Config window's. fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void { - const s = &p.settings; + try writeSettingsOf(p, &p.settings, w); + try w.writeAll("LocationsConfig "); + try p.locations_config.write(w); + try w.writeByte('\n'); + if (comptime builtins.Pet.enabled) try w.print("Pet {s}\n", .{@tagName(p.settings.pet)}); +} + +/// The setting words of `s`, one a line, as the root ctl reads them; a Dump +/// keeps those that differ from a fresh session's (dump.zig). +pub fn writeSettingsOf(p: *Pardes, s: *const config.Runtime, w: *std.Io.Writer) !void { inline for (config.Runtime.settings) |setting| { if (comptime !setting.enabled(builtins.capabilities)) continue; const word = setting.word; switch (setting.action) { .toggle => |field| try w.print("{s} {s}\n", .{ word, if (@field(s, @tagName(field))) "on" else "off" }), - .theme => try w.print("{s} {s}\n", .{ word, p.theme().name }), + .theme => try w.print("{s} {s}\n", .{ word, if (s == &p.settings) p.theme().name else pardes.themes[s.theme].name }), .shell => { const chosen = s.shell.requested.get(); try w.print("{s} {s}\n", .{ word, if (chosen.len > 0) chosen else config.default_shell }); @@ -360,7 +369,6 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void { inline else => |field| try w.print("{s} {s}\n", .{ word, @tagName(@field(s, @tagName(field))) }), }, .message_ms => |which| try w.print("{s} {d}\n", .{ word, @field(s, "message_" ++ @tagName(which) ++ "_ms") }), - // Bare, it names the default directory. // The directory in effect, as Config reports it; bare only when // there is none (no home to put the default in). .dump_dir => { @@ -377,10 +385,6 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void { .grip_width => try w.print("{s} {d}\n", .{ word, s.grip_width }), } } - try w.writeAll("LocationsConfig "); - try p.locations_config.write(w); - try w.writeByte('\n'); - if (comptime builtins.Pet.enabled) try w.print("Pet {s}\n", .{@tagName(s.pet)}); } pub fn rootLen(p: *Pardes) u64 { -- cgit v1.3