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