summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 22:48:21 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commitf0e162d287f44a562ff79e7c1d2cf81ace00c836 (patch)
tree098428d08600eef0a8eb28939cad955315dd3d95
parent6f42f51cd652e189408eace598f99ec25b9542f8 (diff)
downloadpardes-f0e162d287f44a562ff79e7c1d2cf81ace00c836.tar.gz
pardes-f0e162d287f44a562ff79e7c1d2cf81ace00c836.zip
Dumps and init files from before 0.0.4 load no more of their old tag formats, and three orphan test scripts go
A2: a restored pane's tag tail comes from the dump's tag_tail, which every dump since 0.0.4 writes; the 27 earlier default tails that were recognised to upgrade, restoreTail, tailClass, savedPrefix and restoreTailAt, pdf_view's and image's legacySavedPrefix, a terminal's TTY tag prefix and its raw output tail replay, and the ThemeSel, FontSel and ColumnTags words (init lines and dumped output names) go, with their tests; the CHANGELOG says so. A4: test/fs_soak.py, referenced nowhere (monkey9p.py does its work). A6: test/column_grip.py and test/look_placement.py, referenced nowhere; the six review scripts docs/ui-review.md names stay. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/CHANGELOG.md8
-rw-r--r--src/Output.zig3
-rw-r--r--src/dump.zig1
-rw-r--r--src/exec.zig50
-rw-r--r--src/image.zig8
-rw-r--r--src/pardes.zig159
-rw-r--r--src/pdf_view.zig18
-rw-r--r--src/tagline.zig135
-rw-r--r--src/terminal.zig33
-rw-r--r--test/column_grip.py132
-rw-r--r--test/fs_soak.py284
-rw-r--r--test/look_placement.py143
-rw-r--r--test/panes.zig22
-rw-r--r--test/pdf.zig1
14 files changed, 16 insertions, 981 deletions
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 50149551..a2f493df 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## Unreleased
+
+- Dumps and init files from before 0.0.4 no longer load their old tag
+ formats: a restored pane's tag tail is read only from `tag_tail`, and an old
+ terminal's `TTY` tag or raw output tail is not replayed. `ThemeSel`,
+ `FontSel` and `ColumnTags` are no longer recognised in init files or dumps
+ (`Themes` and `Fonts` are the words).
+
## 0.0.5
- Over a slow link the terminal build keeps at most one frame in flight: it
diff --git a/src/Output.zig b/src/Output.zig
index 8cf57591..5bc1f9fc 100644
--- a/src/Output.zig
+++ b/src/Output.zig
@@ -262,9 +262,6 @@ pub fn fromWord(w: []const u8) ?Origin {
if (std.mem.eql(u8, w, config.errors_buffer)) return .errors;
if (std.mem.eql(u8, w, config.pdf_links_buffer)) return .pdf_links;
if (std.mem.eql(u8, w, config.unsaved_buffer)) return .unsaved;
- // A dump from before the rename names them by their old words.
- if (std.mem.eql(u8, w, "ThemeSel")) return .{ .cmd = .Themes };
- if (builtins.capabilities.font_picker) if (std.mem.eql(u8, w, "FontSel")) return .{ .cmd = .Fonts };
if (std.meta.stringToEnum(Builtin, w)) |b|
if (builtins.registry.outputTraits(b) != null) return .{ .cmd = b };
if (std.meta.stringToEnum(lsp.Kind, w)) |k| return .{ .query = k };
diff --git a/src/dump.zig b/src/dump.zig
index 78c2bd7a..2d3c2593 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -905,7 +905,6 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo
restored.command_done = true;
restored.command_status = t.status;
}
- if (std.mem.startsWith(u8, src.tag, "TTY ")) restored.body.mode = .tty;
if (revive) p.emitSpawn(i, restored.serial, t.cwd);
break :terminal restored;
},
diff --git a/src/exec.zig b/src/exec.zig
index 221de105..8c062f66 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -870,58 +870,10 @@ pub fn applyStartupConfig(p: *Pardes) void {
defer p.announce = true;
var lines = std.mem.splitScalar(u8, p.opts.startup_config orelse return, '\n');
while (lines.next()) |line| {
- if (executeBuiltinLine(p, p.active, line)) continue;
- // A setting pardes no longer has is said, once, and changes nothing.
- const word = commandText(line);
- const head = word[0 .. std.mem.indexOfAny(u8, word, " \t") orelse word.len];
- if (std.mem.eql(u8, head, "ColumnTags"))
- p.setMessage(p.active, "ColumnTags: column tags are always shown now; the init line is ignored");
- // A word renamed runs as its new name, and says so, once a line.
- for ([_][2][]const u8{ .{ "ThemeSel", "Themes" }, .{ "FontSel", "Fonts" } }) |renamed| if (std.mem.eql(u8, head, renamed[0])) {
- _ = executeBuiltinLine(p, p.active, renamed[1]);
- var said: [64]u8 = undefined;
- p.setMessage(p.active, std.fmt.bufPrint(&said, "{s}: renamed to {s}", .{ renamed[0], renamed[1] }) catch "renamed");
- };
+ _ = executeBuiltinLine(p, p.active, line);
}
}
-test "an init file's ThemeSel runs as Themes and says it was renamed" {
- const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24, .startup_config = "ThemeSel\n" });
- defer p.deinit();
- var listed = false;
- for (p.panes) |slot| if (slot) |pane| if (pane.file) |f| if (f.output) |o| {
- listed = listed or std.meta.eql(o.from, panes.Output.Origin{ .cmd = .Themes });
- };
- try std.testing.expect(listed);
- // In the session's directory, not `/`.
- for (p.panes) |slot| if (slot) |pane| if (pane.file) |f| if (f.output != null)
- try std.testing.expectEqualStrings(p.sessionDir(), std.fs.path.dirname(f.path).?);
- var said = false;
- for (p.panes) |slot| if (slot) |pane| {
- said = said or std.mem.eql(u8, pane.msg[0..pane.msg_len], "ThemeSel: renamed to Themes");
- };
- try std.testing.expect(said);
- // The panes there at boot are the log's first records, before what the
- // init file said.
- const th = @import("ninep/testing.zig");
- const log = @intFromEnum(pardes.ctlfs.TopFile.log);
- const h = th.call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
- defer _ = th.call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h });
- const text = th.call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes;
- try std.testing.expectStringStartsWith(text, "new ");
-}
-
-test "an init file's retired ColumnTags line is said and ignored" {
- const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
- defer p.deinit();
- p.opts.startup_config = "ColumnTags\nWrap off\n";
- applyStartupConfig(p);
- try std.testing.expect(!p.settings.wrap);
- try std.testing.expect(p.columnBarHeight() > 0);
- const pane = p.panes[p.active].?;
- try std.testing.expectEqualStrings("ColumnTags: column tags are always shown now; the init line is ignored", pane.msg[0..pane.msg_len]);
-}
-
pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8) void {
runBuiltinFrom(p, b, id, txt, arg, false);
}
diff --git a/src/image.zig b/src/image.zig
index 7b081ec6..936fe19a 100644
--- a/src/image.zig
+++ b/src/image.zig
@@ -916,14 +916,6 @@ pub fn tagPrefix(arena: std.mem.Allocator, state: *const State) ![]u8 {
);
}
-pub fn legacySavedPrefix(state: *const State, saved_tag: []const u8) ?[]const u8 {
- const lead = config.tag_image ++ " ";
- if (!std.mem.startsWith(u8, saved_tag, lead)) return null;
- const path_at = lead.len;
- if (!std.mem.startsWith(u8, saved_tag[path_at..], state.path)) return null;
- return saved_tag[0 .. path_at + state.path.len];
-}
-
fn ensureDecoded(p: *pardes.Pardes, state: *State) void {
if (state.tried) return;
state.tried = true;
diff --git a/src/pardes.zig b/src/pardes.zig
index 3ecc80ae..151fe7b0 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -751,13 +751,6 @@ test "Collapse builtin keeps content and restores the pane from its default tag"
p.sync();
try std.testing.expect(!pane.collapsed);
try std.testing.expectEqual(height, p.rects[0].h);
-
- // Unedited defaults from the preceding version gain the new command.
- if (pane.tag.own) |own| p.gpa.free(own);
- pane.tag.own = null;
- tagline.restoreTailAt(p, pane, tagline.pre_collapse_file_pane_tail, "", .file);
- try std.testing.expect(pane.tag.own == null);
- try std.testing.expectEqualStrings(tagline.file_pane_tail, tagline.curTail(pane));
}
test "LocationsConfig command reports partial updates and survives restore" {
@@ -985,129 +978,6 @@ test "compact tag commands stay beside their own prefix across layout changes" {
try std.testing.expectEqualStrings(above_tag, try tagline.tagText(p, p.scratch.allocator(), above));
}
-test "legacy default tag tails upgrade while custom tails remain owned" {
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{});
- defer p.deinit();
-
- const terminal = p.panes[0].?;
- const terminal_old = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{
- try tagline.tagPrefix(p, terminal),
- tagline.legacy_pane_tail,
- });
- tagline.restoreTail(p, terminal, terminal_old);
- try std.testing.expect(terminal.tag.own == null);
- try std.testing.expectEqualStrings(tagline.terminal_pane_tail, tagline.curTail(terminal));
-
- const terminal_previous = try std.fmt.allocPrint(p.scratch.allocator(), "{s} New Del", .{
- try tagline.tagPrefix(p, terminal),
- });
- tagline.restoreTail(p, terminal, terminal_previous);
- try std.testing.expect(terminal.tag.own == null);
- try std.testing.expectEqualStrings(tagline.terminal_pane_tail, tagline.curTail(terminal));
-
- const terminal_prefilter = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{
- try tagline.tagPrefix(p, terminal),
- tagline.prev_terminal_pane_tail,
- });
- tagline.restoreTail(p, terminal, terminal_prefilter);
- try std.testing.expect(terminal.tag.own == null);
- try std.testing.expectEqualStrings(tagline.terminal_pane_tail, tagline.curTail(terminal));
-
- for ([_][]const u8{ tagline.pre_mode_terminal_pane_tail, tagline.pre_tty_first_terminal_pane_tail, tagline.pre_collapse_terminal_pane_tail }) |old_tail| {
- const old = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{ try tagline.tagPrefix(p, terminal), old_tail });
- tagline.restoreTail(p, terminal, old);
- try std.testing.expect(terminal.tag.own == null);
- try std.testing.expectEqualStrings(tagline.terminal_pane_tail, tagline.curTail(terminal));
- }
- const custom_mode = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s} Keep", .{ try tagline.tagPrefix(p, terminal), tagline.pre_mode_terminal_pane_tail });
- tagline.restoreTail(p, terminal, custom_mode);
- try std.testing.expect(terminal.tag.own != null);
- try std.testing.expectEqualStrings(tagline.pre_mode_terminal_pane_tail ++ " Keep", tagline.curTail(terminal));
-
- if (terminal.tag.own) |own| p.gpa.free(own);
- terminal.tag.own = null;
- const terminal_custom = try std.fmt.allocPrint(p.scratch.allocator(), "{s} Keep Del", .{
- try tagline.tagPrefix(p, terminal),
- });
- tagline.restoreTail(p, terminal, terminal_custom);
- try std.testing.expect(terminal.tag.own != null);
- try std.testing.expectEqualStrings(" Keep Del", tagline.curTail(terminal));
-
- if (terminal.tag.own) |own| p.gpa.free(own);
- terminal.tag.own = null;
- tagline.restoreDumpTail(p, terminal, .{
- .kind = .terminal,
- .tag = "TTY /historical/cwd Keep Del",
- .body = "",
- .terminal = .{ .cwd = "/historical/cwd" },
- });
- try std.testing.expect(terminal.tag.own != null);
- try std.testing.expectEqualStrings(" Keep Del", tagline.curTail(terminal));
-
- if (terminal.tag.own) |own| p.gpa.free(own);
- terminal.tag.own = null;
- tagline.restoreDumpTail(p, terminal, .{
- .kind = .terminal,
- .tag = "TTY /historical/cwd New Del",
- .body = "",
- .terminal = .{ .cwd = "/historical/cwd" },
- });
- try std.testing.expect(terminal.tag.own == null);
- try std.testing.expectEqualStrings(tagline.terminal_pane_tail, tagline.curTail(terminal));
-
- // This happens to be the historical FILE default, but on a terminal it is
- // user-owned text and must not be swallowed by migration.
- if (terminal.tag.own) |own| p.gpa.free(own);
- terminal.tag.own = null;
- const terminal_fileish = try std.fmt.allocPrint(p.scratch.allocator(), "{s} Save Del", .{
- try tagline.tagPrefix(p, terminal),
- });
- tagline.restoreTail(p, terminal, terminal_fileish);
- try std.testing.expect(terminal.tag.own != null);
- try std.testing.expectEqualStrings(" Save Del", tagline.curTail(terminal));
-
- // A savable file has a distinct old default. Save remains first after the
- // migration so the tag's established `:w<Tab>` route is unchanged.
- const file = try p.setTestFile("");
- const file_old = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{
- try tagline.tagPrefix(p, file),
- tagline.legacy_file_pane_tail,
- });
- tagline.restoreTail(p, file, file_old);
- try std.testing.expect(file.tag.own == null);
- try std.testing.expectEqualStrings(" Save Tty Collapse Del", tagline.curTail(file));
-
- const file_custom = try std.fmt.allocPrint(p.scratch.allocator(), "{s} Save Mine Del", .{
- try tagline.tagPrefix(p, file),
- });
- tagline.restoreTail(p, file, file_custom);
- try std.testing.expect(file.tag.own != null);
- try std.testing.expectEqualStrings(" Save Mine Del", tagline.curTail(file));
-
- // Conversely the generic pane's current default is a custom edit on a
- // savable file. Legacy recognition is scoped to the source pane class.
- if (file.tag.own) |own| p.gpa.free(own);
- file.tag.own = null;
- const file_generic = try std.fmt.allocPrint(p.scratch.allocator(), "{s} New Del", .{
- try tagline.tagPrefix(p, file),
- });
- tagline.restoreTail(p, file, file_generic);
- try std.testing.expect(file.tag.own != null);
- try std.testing.expectEqualStrings(" New Del", tagline.curTail(file));
-
- const image_doc = try panes.Image.create(p, 1, "/tmp/legacy image.ppm", &.{});
- try std.testing.expectEqualStrings(" Tty Collapse Del", tagline.curTail(image_doc));
- tagline.restoreDumpTail(p, image_doc, .{
- .kind = .image,
- .tag = "img /tmp/legacy image.ppm Keep Del",
- .body = "",
- .image = .{ .path = "/tmp/legacy image.ppm" },
- });
- try std.testing.expect(image_doc.tag.own != null);
- try std.testing.expectEqualStrings(" Keep Del", tagline.curTail(image_doc));
-}
-
test "Joincol folds the active column into its right neighbor, keeping its panes" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 100, .rows = 30 });
@@ -1434,14 +1304,6 @@ test "pane defaults keep creation of scratch panes in column tags" {
while (words.next()) |word| try std.testing.expect(!std.mem.eql(u8, word, "New"));
}
try std.testing.expectEqualStrings("New Tty Find Grep Joincol Delcol", tagline.columnTag(p, 0));
- const terminal = p.panes[0].?;
- tagline.restoreTailAt(p, terminal, tagline.pre_column_terminal_pane_tail, "", .terminal);
- try std.testing.expect(terminal.tag.own == null);
- try std.testing.expectEqualStrings(tagline.terminal_pane_tail, tagline.curTail(terminal));
- const file = try p.setTestFile("unchanged\n");
- tagline.restoreTailAt(p, file, tagline.pre_column_file_pane_tail, "", .file);
- try std.testing.expect(file.tag.own == null);
- try std.testing.expectEqualStrings(tagline.file_pane_tail, tagline.curTail(file));
}
test "an unsaved file's tag is its name alone: the grip, not a mark, says it is unsaved" {
@@ -2640,27 +2502,8 @@ test "Save reaches every tagline with text behind it and no other" {
out.file.?.output = .{ .from = .search };
try std.testing.expectEqualStrings(tagline.locations_pane_tail, tagline.curTail(out));
- // A dump written before Save reached output buffers carries the generic
- // tail. That is a DEFAULT and not something typed, so it upgrades.
- const dumped = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{
- try tagline.tagPrefix(p, out),
- tagline.pane_tail,
- });
- tagline.restoreTail(p, out, dumped);
- try std.testing.expect(out.tag.own == null);
- try std.testing.expectEqualStrings(tagline.locations_pane_tail, tagline.curTail(out));
-
- // A scratch is an output buffer that has worn the FILE tails all along,
- // so its own older default upgrades from that family too.
+ // A scratch is an output buffer that wears the FILE tails.
out.file.?.output = .{ .from = .{ .cmd = std.meta.stringToEnum(Builtin, "New").? } };
- if (out.tag.own) |own| p.gpa.free(own);
- out.tag.own = null;
- const scratch_dumped = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{
- try tagline.tagPrefix(p, out),
- tagline.prev_file_pane_tail,
- });
- tagline.restoreTail(p, out, scratch_dumped);
- try std.testing.expect(out.tag.own == null);
try std.testing.expectEqualStrings(" Save Tty Collapse Del", tagline.curTail(out));
// An image's bytes on disk already are what they are: nothing of the
diff --git a/src/pdf_view.zig b/src/pdf_view.zig
index 8f842cde..5646a92f 100644
--- a/src/pdf_view.zig
+++ b/src/pdf_view.zig
@@ -57,24 +57,6 @@ pub const RasterPolicy = struct {
match_viewport: bool,
};
-pub fn legacySavedPrefix(path: []const u8, saved_tag: []const u8) ?[]const u8 {
- if (std.mem.startsWith(u8, saved_tag, path) and std.mem.startsWith(u8, saved_tag[path.len..], " [")) {
- const start = path.len + 2;
- const end = std.mem.indexOfScalarPos(u8, saved_tag, start, ']') orelse return null;
- var parts = std.mem.splitScalar(u8, saved_tag[start..end], '/');
- _ = std.fmt.parseInt(usize, parts.next() orelse return null, 10) catch return null;
- _ = std.fmt.parseInt(usize, parts.next() orelse return null, 10) catch return null;
- if (parts.next() != null) return null;
- return saved_tag[0 .. end + 1];
- }
- if (!std.mem.startsWith(u8, saved_tag, "pdf ")) return null;
- const marker = " PdfSections ";
- const marker_at = std.mem.indexOf(u8, saved_tag, marker) orelse return null;
- const path_at = marker_at + marker.len;
- if (!std.mem.startsWith(u8, saved_tag[path_at..], path)) return null;
- return saved_tag[0 .. path_at + path.len];
-}
-
pub const TintKey = if (enabled) struct {
mode: TintMode,
colors: pdf.TintColors,
diff --git a/src/tagline.zig b/src/tagline.zig
index df093925..1923871a 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -23,43 +23,11 @@ pub const context_pane_tail = file_pane_tail ++ " TreeContext";
pub const locations_pane_tail = file_pane_tail ++ " LocationsConfig";
pub const terminal_pane_tail = " " ++ config.terminal_pane_builtins_str;
pub const command_pane_tail = " " ++ config.command_pane_builtins_str;
-/// The mark a tag carried after an unsaved file's name before the grip
-/// showed it; a dump from then still has it in its saved tags.
-const dirty_marker = " *";
-
/// A file with changes it has not saved.
pub fn dirty(pane: *const panes.Pane) bool {
const f = pane.file orelse return false;
return panes.Output.fileTraits(f.output).saves and (f.revision != f.saved_revision or f.disk_gone);
}
-pub const legacy_pane_tail = " Del";
-pub const legacy_file_pane_tail = " Save Del";
-// The defaults from the release before Newtty joined every tagline. Recognized
-// so a dump written then upgrades instead of keeping the old word as a tail.
-const prev_pane_tail = " New Del";
-pub const prev_file_pane_tail = " Save New Del";
-const legacy_terminal_pane_tail = " New Del Filter";
-// ...and the terminal default from the release before Save reached it, when a
-// scrollback was not yet something you could write to a path.
-pub const prev_terminal_pane_tail = " New Newtty Del Filter";
-const pre_tty_terminal_pane_tail = " Save New Newtty Del Filter";
-const pre_column_pane_tail = " New Newtty Del";
-pub const pre_column_file_pane_tail = " Save New Newtty Del";
-const pre_column_pdf_pane_tail = " New Newtty Del PdfSections PdfTint";
-pub const pre_column_terminal_pane_tail = " Save New Newtty Del Tty Filter";
-const pre_collapse_pane_tail = " Tty Del";
-pub const pre_collapse_file_pane_tail = " Save Tty Del";
-const pre_collapse_pdf_pane_tail = " Tty Del PdfSections PdfTint";
-pub const pre_collapse_terminal_pane_tail = " Save Tty Del Togglettymode Filter";
-pub const pre_tty_first_terminal_pane_tail = " Save Tty Del Togglettymode Filter Collapse";
-pub const pre_mode_terminal_pane_tail = " Tty Save Del Togglettymode Filter Collapse";
-// ...and the defaults from before the closing word moved to the end of every
-// tagline, where a click cannot land on it by overshooting the one before.
-const pre_close_last_pane_tail = " Tty Del Collapse";
-const pre_close_last_file_pane_tail = " Save Tty Del Collapse";
-const pre_close_last_pdf_pane_tail = " Tty Del PdfSections PdfTint Collapse";
-const pre_close_last_terminal_pane_tail = " Tty Save Del Mode Filter Collapse";
-
/// What a column's tag says until someone edits it.
pub const column_tag_default = "New Tty Find Grep Joincol Delcol";
@@ -420,106 +388,11 @@ pub fn enterHeader(p: *Pardes, column: ?usize) void {
t.normal.clear();
}
+/// A dump's tag tail, what was typed after the name: a dump from 0.0.4 on
+/// writes it, and an older one's saved tag is not read.
pub fn restoreDumpTail(p: *Pardes, pane: *Pane, src: dump.Pane) void {
- if (src.tag_tail) |tail| {
- pane.tag.own = p.gpa.dupe(u8, tail) catch return;
- return;
- }
- const tail_class = tailClass(pane);
- if (src.kind == .image and panes.Pdf.isPath(src.image.?.path)) {
- if (panes.Pdf.legacySavedPrefix(src.image.?.path, src.tag)) |prefix|
- return restoreTailAt(p, pane, src.tag, prefix, .generic);
- }
- if (src.kind == .terminal and std.mem.startsWith(u8, src.tag, "TTY ")) {
- const legacy = std.fmt.allocPrint(p.scratch.allocator(), "TTY {s}", .{src.terminal.?.cwd}) catch return;
- if (std.mem.startsWith(u8, src.tag, legacy))
- return restoreTailAt(p, pane, src.tag, legacy, .terminal);
- }
- const current = tagPrefix(p, pane) catch return;
- if (std.mem.startsWith(u8, src.tag, current))
- return restoreTailAt(p, pane, src.tag, savedPrefix(src.tag, current, tail_class), tail_class);
- if (src.kind != .image) return;
- const saved = src.image.?;
- if (pane.image) |*state| if (panes.Image.legacySavedPrefix(state, src.tag)) |legacy|
- return restoreTailAt(p, pane, src.tag, legacy, .generic);
- if (panes.Pdf.isPath(saved.path)) if (panes.Pdf.legacySavedPrefix(saved.path, src.tag)) |legacy|
- restoreTailAt(p, pane, src.tag, legacy, .generic);
-}
-
-pub fn restoreTail(p: *Pardes, pane: *Pane, saved_tag: []const u8) void {
- const pfx = tagPrefix(p, pane) catch return;
- const class = tailClass(pane);
- restoreTailAt(p, pane, saved_tag, savedPrefix(saved_tag, pfx, class), class);
-}
-
-const TailClass = enum { generic, file, output, terminal };
-
-fn tailClass(pane: *const Pane) TailClass {
- if (pane.file) |f| return if (f.output == null) .file else .output;
- if (pane.isTerminal()) return .terminal;
- return .generic;
-}
-
-fn savedPrefix(saved_tag: []const u8, live: []const u8, class: TailClass) []const u8 {
- if (class == .generic or class == .terminal) return live;
- if (!std.mem.startsWith(u8, saved_tag, live)) return live;
- const marked_len = live.len + dirty_marker.len;
- if (saved_tag.len >= marked_len and std.mem.eql(u8, saved_tag[live.len..marked_len], dirty_marker))
- return saved_tag[0..marked_len];
- return live;
-}
-
-pub fn restoreTailAt(
- p: *Pardes,
- pane: *Pane,
- saved_tag: []const u8,
- pfx: []const u8,
- class: TailClass,
-) void {
- if (!std.mem.startsWith(u8, saved_tag, pfx)) return;
- const rest = saved_tag[pfx.len..];
- const said = std.mem.trimStart(u8, rest, " ");
- const defaults: []const []const u8 = switch (class) {
- .generic => &.{ pdf_pane_tail, pane_tail, pre_close_last_pdf_pane_tail, pre_close_last_pane_tail, pre_collapse_pdf_pane_tail, pre_collapse_pane_tail, pre_column_pdf_pane_tail, pre_column_pane_tail, prev_pane_tail, legacy_pane_tail },
- .file => &.{ context_pane_tail, file_pane_tail, pre_close_last_file_pane_tail, pre_collapse_file_pane_tail, pre_column_file_pane_tail, prev_file_pane_tail, legacy_file_pane_tail },
- .output => &.{
- locations_pane_tail,
- file_pane_tail,
- pre_close_last_file_pane_tail,
- pre_close_last_pane_tail,
- prev_file_pane_tail,
- legacy_file_pane_tail,
- pane_tail,
- prev_pane_tail,
- legacy_pane_tail,
- pre_column_file_pane_tail,
- pre_column_pane_tail,
- pre_collapse_file_pane_tail,
- pre_collapse_pane_tail,
- },
- .terminal => &.{ terminal_pane_tail, pre_close_last_terminal_pane_tail, pre_close_last_pane_tail, pre_mode_terminal_pane_tail, pre_tty_first_terminal_pane_tail, pre_collapse_terminal_pane_tail, pre_column_terminal_pane_tail, pre_tty_terminal_pane_tail, prev_terminal_pane_tail, legacy_terminal_pane_tail, pane_tail, pre_collapse_pane_tail, pre_column_pane_tail, prev_pane_tail, legacy_pane_tail },
- };
- // A terminal's saved tag carries what was live about it -- the REPL id
- // after its directory, the shell in its Tty word -- which is neither
- // dumped nor true of the new shell: its tail is still the default.
- var plain = said;
- var plain_buf: [256]u8 = undefined;
- if (class == .terminal) {
- const first = plain[0 .. std.mem.indexOfScalar(u8, plain, ' ') orelse plain.len];
- if (std.mem.lastIndexOfScalar(u8, first, '-')) |dash| if (dash + 2 == first.len and first[dash + 1] >= 'a' and first[dash + 1] <= 'z') {
- for (@import("grammar_manifest.zig").all) |g| if (std.mem.eql(u8, g.name, first[0..dash])) {
- plain = std.mem.trimStart(u8, plain[first.len..], " ");
- break;
- };
- };
- if (std.mem.startsWith(u8, plain, "Tty+")) {
- const word_end = std.mem.indexOfScalar(u8, plain, ' ') orelse plain.len;
- plain = std.fmt.bufPrint(&plain_buf, "Tty{s}", .{plain[word_end..]}) catch plain;
- }
- }
- for (defaults) |default|
- if (std.mem.eql(u8, plain, std.mem.trimStart(u8, default, " "))) return;
- pane.tag.own = p.gpa.dupe(u8, rest) catch return;
+ const tail = src.tag_tail orelse return;
+ pane.tag.own = p.gpa.dupe(u8, tail) catch return;
}
/// The span a header's selection covers on line `row` of its text, in
diff --git a/src/terminal.zig b/src/terminal.zig
index c99a92a6..6bf46bef 100644
--- a/src/terminal.zig
+++ b/src/terminal.zig
@@ -509,13 +509,7 @@ pub fn restore(p: *Pardes, src: dump.Pane, live: bool) !*Pane {
pane.ovl = .{ .row = 0, .rows = 1, .text = try p.gpa.dupe(u8, terminal.stream) };
return pane;
}
- // An older dump's raw output tail cannot come back live: cut off mid
- // redraw, or written at sizes the pane no longer has, it replays as
- // garbage. Its rendered text can.
- const legacy_text = live and !terminal.stream_is_screen and terminal.stream.len > 0;
- const bytes = if (legacy_text)
- try std.mem.replaceOwned(u8, p.scratch.allocator(), terminal.stream, "\n", "\r\n")
- else if (terminal.stream_b64.len > 0)
+ const bytes = if (terminal.stream_b64.len > 0)
try dump.decodeBytes(p.scratch.allocator(), terminal.stream_b64)
else
&.{};
@@ -759,31 +753,6 @@ test "a restored terminal keeps its history above a marker and gets a shell wher
try std.testing.expect(old_at < mark_at);
}
-test "an older dump comes back live from its rendered text, not its raw output tail" {
- if (comptime !enabled) return error.SkipZigTest;
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 40, .rows = 12 });
- defer p.deinit();
- // A tail cut mid-redraw: relative moves from a cursor it never set.
- const tail = try dump.encodeBytes(gpa, "\x1b[7A\x1b[2Kgarbage\x1b[3B\x1b[9Gmore");
- defer gpa.free(tail);
- const restored = try restore(p, .{
- .kind = .terminal,
- .tag = "",
- .body = "",
- .cols = 40,
- .rows = 12,
- .terminal = .{ .stream = "$ ls\na.txt b.txt\n$ ", .stream_b64 = tail },
- }, true);
- defer gpa.destroy(restored);
- defer deinitEmulator(restored, gpa);
- defer deinitPendingCommand(restored);
- const text = try screenTextAlloc(restored, gpa);
- defer gpa.free(text);
- try std.testing.expect(std.mem.indexOf(u8, text, "$ ls\na.txt b.txt") != null);
- try std.testing.expect(std.mem.indexOf(u8, text, "garbage") == null);
-}
-
/// Record and parse one live pty read, invalidate its motion surface, and
/// follow it only when the body (possibly parked under a tag edit) is raw.
pub fn feedOutput(p: *Pardes, pane: *Pane, bytes: []const u8) void {
diff --git a/test/column_grip.py b/test/column_grip.py
deleted file mode 100644
index ac0341c3..00000000
--- a/test/column_grip.py
+++ /dev/null
@@ -1,132 +0,0 @@
-#!/usr/bin/env python3
-"""Drag a column grip in the real SDL window with pixel mouse events.
-
-WAYLAND_DISPLAY=/run/user/1000/wayland-1 \\
- python3 -B test/column_grip.py zig-out/bin/pardes-gui /tmp/pardes-column-grip [--pad 7]
-
-The window is hidden (PARDES_TEST). Mouse events enter as ESC]777;mouse
-sequences on stdin, which the GUI builds into SDL events and dispatches like
-its own, so pixel-to-cell and tag hit testing run as they do under a hand.
-9P observes the grid; each step leaves a PNG of the GPU frame in the output
-directory: grip-rest, grip-resize-drag, grip-reorder-drag, grip-done.
-"""
-import argparse
-import os
-from pathlib import Path
-import shutil
-import subprocess
-import time
-
-from fs import session
-from ui_review import require, wait_for
-
-
-def rows(client):
- frame = client.screen()
- cols = frame['cols']
- return [''.join(cell[0] for cell in frame['cells'][at:at + cols]) for at in range(0, len(frame['cells']), cols)]
-
-
-def rail(client):
- """The one column the drag preview's rail runs down, if any."""
- bottom = rows(client)[-1]
- found = [x for x, ch in enumerate(bottom) if ch == '╎']
- require(len(found) <= 1, f'more than one rail: {found}')
- return found[0] if found else None
-
-
-def grab(root, label):
- time.sleep(.5)
- def convert():
- shutil.copyfile(root / 'latest.ppm', root / (label + '.ppm'))
- return subprocess.run(['magick', str(root / (label + '.ppm')), str(root / (label + '.png'))],
- stderr=subprocess.DEVNULL).returncode == 0
- require(wait_for(convert, 8), 'GPU capture did not contain a complete image')
- return root / (label + '.png')
-
-
-def pixel(path, x, y):
- out = subprocess.run(['magick', str(path), '-format', f'%[pixel:p{{{x},{y}}}]', 'info:'],
- capture_output=True, text=True, check=True)
- return out.stdout.strip()
-
-
-def run(args):
- root = args.output.resolve()
- shutil.rmtree(root, ignore_errors=True)
- root.mkdir(parents=True, mode=0o700)
- (root / 'config/pardes').mkdir(parents=True)
- (root / 'config/pardes/init').write_text('Shell /bin/sh\n')
- (root / 'w.txt').write_text('alpha\nbeta\n')
- name = 'column-grip-' + str(os.getpid())
- launch = ['-c', 'stty cols 100 rows 30; exec "$@"', 'column-grip',
- str(args.binary.resolve()), '--9p=' + name, str(root / 'w.txt')]
- env = {'PARDES_TEST': '1', 'PARDES_TEST_CAPTURE_DIR': str(root), 'PARDES_TEST_COLS': '100',
- 'PARDES_TEST_ROWS': '30', 'PARDES_TEST_PAD': str(args.pad)}
- handles = {}
- with session('/bin/sh', root, name, socket_name=name, tty=True, launch=launch,
- inherited=env, terminal=handles) as (client, _):
- def send(data):
- os.write(handles['input_fd'], data.encode())
-
- def mouse(kind, button, col, row):
- # the middle of a grid cell, in window pixels
- send(f'\x1b]777;mouse;{kind};{button};{(col + .5) * cw};{(row + .5) * ch}\x07')
- time.sleep(.15)
-
- require(wait_for(lambda: 'Newcol' in rows(client)[0]), 'workspace tag never drew')
- send('\x1bn')
- time.sleep(.3)
- send('\x1bc')
- require(wait_for(lambda: rows(client)[1].count('New Tty') == 2), 'no second column')
- require(wait_for(lambda: (root / 'latest.ppm').exists(), 8), 'GPU capture missing')
- time.sleep(.5)
- with open(root / 'latest.ppm', 'rb') as ppm:
- ppm.readline()
- width, height = map(int, ppm.readline().split())
- frame = client.screen()
- cw = (width - args.pad) // frame['cols']
- ch = (height - args.pad) // frame['rows']
- edge = rows(client)[1].index('New Tty', 10) - 3
-
- rest = grab(root, 'grip-rest')
- mouse('down', 1, edge, 1)
- held = grab(root, 'grip-held')
- require(pixel(held, int((edge + 1) * cw), int(1.5 * ch)) != pixel(rest, int((edge + 1) * cw), int(1.5 * ch)),
- 'a grabbed grip looks the same as a resting one')
- require(rail(client) is None, 'a grip held in place previews a move')
-
- # Short of the left column's middle: the left edge follows, and the
- # rail is the seam cell left of where the rule will be.
- for col in range(edge, edge - 11, -1):
- mouse('motion', 1, col, 1)
- require(rail(client) == edge - 11, f'resize rail at {rail(client)}, wanted {edge - 11}')
- grab(root, 'grip-resize-drag')
- mouse('up', 1, edge - 10, 1)
- require(wait_for(lambda: rows(client)[1].index('New Tty', 10) - 3 == edge - 10), 'left edge did not follow the grip')
- edge -= 10
-
- # Over the left column's left half: it goes first, and the rail runs
- # just past the left window edge's scroll rail.
- mouse('down', 1, edge, 1)
- for col in range(edge, 4, -5):
- mouse('motion', 1, col, 10)
- require(rail(client) == 1, f'reorder rail at {rail(client)}, wanted 1')
- grab(root, 'grip-reorder-drag')
- mouse('up', 1, 5, 10)
- require(wait_for(lambda: 'alpha' in rows(client)[3][50:]), 'the column did not move first')
- require(rail(client) is None, 'the rail outlived the drag')
- grab(root, 'grip-done')
- print('ok', root)
-
-
-def main():
- parser = argparse.ArgumentParser()
- parser.add_argument('binary', type=Path)
- parser.add_argument('output', type=Path)
- parser.add_argument('--pad', type=int, default=0, help='leftover pixels past the grid (PARDES_TEST_PAD)')
- run(parser.parse_args())
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/fs_soak.py b/test/fs_soak.py
deleted file mode 100644
index a62f03e6..00000000
--- a/test/fs_soak.py
+++ /dev/null
@@ -1,284 +0,0 @@
-#!/usr/bin/env python3
-import argparse
-import json
-import os
-from pathlib import Path
-import random
-import socket
-import struct
-import tempfile
-import time
-
-from fs import new_pane, session
-from ninep import Client
-
-
-BODY_LIMIT = 4096
-SCREEN_LIMIT = 4 * 1024 * 1024
-FRAGMENTS = [
- b'const value = 42;\n', b'fn check() void { if (true) return; }\n',
- b'\tspaces and tabs\n', 'λ 界 e\u0301 👩\u200d🚀\n'.encode(), b'x',
-]
-OPERATIONS = ['append', 'range', 'replace', 'read', 'save', 'reload',
- 'cycle', 'reconnect', 'screen']
-
-
-def emit(**record):
- print(json.dumps(record, separators=(',', ':')), flush=True)
-
-
-def payload(rng, limit=256):
- result = bytearray()
- for _ in range(rng.randint(1, 12)):
- fragment = rng.choice(FRAGMENTS)
- if len(result) + len(fragment) <= limit:
- result.extend(fragment)
- return bytes(result) or b'x'[:limit]
-
-
-def check_bytes(actual, expected, label):
- if actual == expected:
- return
- at = next((i for i, pair in enumerate(zip(actual, expected))
- if pair[0] != pair[1]), min(len(actual), len(expected)))
- raise AssertionError(f'{label}: mismatch at byte {at}; '
- f'length {len(actual)} != {len(expected)}; '
- f'{actual[at:at + 32]!r} != {expected[at:at + 32]!r}')
-
-
-def read_fid(client, fid, limit, count):
- result = bytearray()
- while chunk := client.read_fid(fid, len(result), count):
- result.extend(chunk)
- if len(result) > limit:
- raise AssertionError(f'read exceeded fixture limit {limit}')
- return bytes(result)
-
-
-def check_body(client, pane, count=4096):
- fid = client.open(f'/pane/{pane["serial"]}/body')
- try:
- actual = read_fid(client, fid, BODY_LIMIT, count)
- finally:
- client.close(fid)
- check_bytes(actual, pane['body'], f'pane {pane["serial"]}')
-
-
-def check_screen(data):
- screen = json.loads(data)
- cols, rows = screen['cols'], screen['rows']
- assert cols > 0 and rows > 0 and cols * rows <= 65536, (cols, rows)
- assert len(screen['cells']) == cols * rows
- styles = screen['styles']
- assert styles
- for cell in screen['cells']:
- assert len(cell) == 2 and isinstance(cell[0], str), cell
- assert isinstance(cell[1], int) and 0 <= cell[1] < len(styles), cell
-
-
-def session_pid(client):
- if not hasattr(socket, 'SO_PEERCRED'):
- return None
- pid, uid, _ = struct.unpack('3i', client.socket.getsockopt(
- socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('3i')))
- assert pid > 0 and uid == os.getuid(), (pid, uid)
- return pid
-
-
-def check_session(client, control, pid):
- assert session_pid(control) == pid
- if pid is not None:
- actual = (session_pid(client) if client.socket.family == socket.AF_UNIX
- else int(client.read('/os/proc/self/stat').split()[0]))
- assert actual == pid, (actual, pid)
-
-
-def memory(pid):
- result = {'rss_bytes': None, 'cumulative_peak_rss_bytes': None}
- if pid is None:
- return result
- try:
- for line in Path(f'/proc/{pid}/status').read_text().splitlines():
- fields = line.split()
- if fields[0] == 'VmRSS:':
- result['rss_bytes'] = int(fields[1]) * 1024
- elif fields[0] == 'VmHWM:':
- result['cumulative_peak_rss_bytes'] = int(fields[1]) * 1024
- except OSError:
- pass
- return result
-
-
-def run(args):
- rng = random.Random(args.seed)
- started = time.monotonic()
- deadline = None
- batch = 0
- operation = 'start'
- operation_count = 0
- pid = None
- try:
- with tempfile.TemporaryDirectory(prefix='pardes-soak-') as directory:
- root = Path(directory)
- options = ['--9p-tcp=tcp!127.0.0.1!0'] if args.transport == 'tcp' else []
- with session(str(args.binary.resolve()), root, 'soak', *options,
- tty=args.tty) as (control, unix_address):
- pid = session_pid(control)
- address = unix_address
- if args.transport == 'tcp':
- listeners = [line.split('!') for line in
- control.read('/listeners').decode().splitlines()
- if line.startswith('tcp!')]
- assert len(listeners) == 1, listeners
- transport, host, port = listeners[0]
- assert transport == 'tcp' and host == '127.0.0.1' and 0 < int(port) < 65536, listeners
- address = (host, int(port))
- client = Client(address)
- panes = []
- try:
- check_session(client, control, pid)
- for slot in range(4):
- body = payload(rng)
- serial = 1 if slot == 0 else new_pane(client, body)
- base = f'/pane/{serial}'
- if slot == 0:
- client.write(base + '/body', body, truncate=True)
- path = root / f'pane-{slot}-0.zig'
- client.write(base + '/name', f'{path}\n'.encode())
- client.write(base + '/exec', b'Save\n')
- panes.append({'serial': serial, 'body': body, 'path': path, 'variant': 0})
- emit(kind='start', seed=args.seed, session_pid=pid,
- transport=args.transport, host='tty' if args.tty else 'detached',
- batches=args.batches, duration_seconds=args.duration,
- interval_seconds=args.interval, body_limit_bytes=BODY_LIMIT,
- live_panes=len(panes), **memory(pid))
- if args.duration is not None:
- deadline = time.monotonic() + args.duration
- while (args.batches is None or batch < args.batches) and (
- deadline is None or time.monotonic() < deadline):
- batch_started = time.monotonic()
- operations = OPERATIONS * 2
- rng.shuffle(operations)
- for operation in operations:
- slot = rng.randrange(len(panes))
- pane = panes[slot]
- base = f'/pane/{pane["serial"]}'
- if operation == 'append':
- addition = payload(rng, min(256, BODY_LIMIT - len(pane['body'])))
- client.write(base + '/body', addition)
- pane['body'] += addition
- elif operation == 'range':
- body = pane['body']
- # Keep range endpoints outside combining and ZWJ clusters.
- boundaries = [0] + [i for i in range(1, len(body))
- if body[i - 1] < 128 and body[i] < 128] + [len(body)]
- lo, hi = sorted((rng.choice(boundaries), rng.choice(boundaries)))
- replacement = payload(rng, min(256, BODY_LIMIT - len(body) + hi - lo))
- if replacement:
- client.write(base + '/addr', f'#{lo},#{hi}'.encode())
- client.write(base + '/data', replacement)
- pane['body'] = body[:lo] + replacement + body[hi:]
- elif operation == 'replace':
- pane['body'] = payload(rng, BODY_LIMIT)
- client.write(base + '/body', pane['body'], truncate=True)
- elif operation == 'read':
- check_body(client, pane, rng.choice([97, 251, 1024, 4096]))
- elif operation == 'save':
- pane['variant'] ^= 1
- pane['path'] = root / f'pane-{slot}-{pane["variant"]}.zig'
- client.write(base + '/name', f'{pane["path"]}\n'.encode())
- client.write(base + '/exec', b'Save\n')
- check_bytes(pane['path'].read_bytes(), pane['body'], 'saved file')
- check_bytes(client.read('/os' + str(pane['path'])), pane['body'], 'OS mount')
- elif operation == 'reload':
- pane['body'] = payload(rng)
- pane['path'].write_bytes(pane['body'])
- client.write(base + '/ctl', b'get\n')
- elif operation == 'cycle':
- old_serial = pane['serial']
- client.remove(base)
- pane['body'] = payload(rng)
- pane['serial'] = new_pane(client, pane['body'])
- assert pane['serial'] != old_serial
- base = f'/pane/{pane["serial"]}'
- client.write(base + '/name', f'{pane["path"]}\n'.encode())
- client.write(base + '/exec', b'Save\n')
- elif operation == 'reconnect':
- client.socket.close()
- client = Client(address)
- check_session(client, control, pid)
- check_body(control, pane)
- elif operation == 'screen':
- frozen = client.open('/screen')
- try:
- before = read_fid(client, frozen, SCREEN_LIMIT, 4096)
- check_screen(before)
- addition = payload(rng, min(64, BODY_LIMIT - len(pane['body'])))
- client.write(base + '/body', addition)
- pane['body'] += addition
- after = read_fid(client, frozen, SCREEN_LIMIT, rng.choice([251, 1024]))
- check_bytes(after, before, 'frozen screen')
- finally:
- client.close(frozen)
- fresh = client.open('/screen')
- try:
- check_screen(read_fid(client, fresh, SCREEN_LIMIT, 4096))
- finally:
- client.close(fresh)
- operation_count += 1
- check_body(client, pane)
- expected = {str(pane['serial']) for pane in panes}
- assert set(client.list('/pane')) == expected
- assert set(control.list('/pane')) == expected
- for pane in panes:
- check_body(client, pane)
- check_body(control, pane)
- batch += 1
- emit(kind='batch', seed=args.seed, batch=batch,
- duration_seconds=time.monotonic() - batch_started,
- elapsed_seconds=time.monotonic() - started,
- operations=len(operations), total_operations=operation_count,
- model_bytes=sum(len(pane['body']) for pane in panes), **memory(pid))
- delay = args.interval - (time.monotonic() - batch_started)
- if deadline is not None:
- delay = min(delay, deadline - time.monotonic())
- if delay > 0:
- time.sleep(delay)
- finally:
- client.socket.close()
- emit(kind='summary', seed=args.seed, batches=batch,
- transport=args.transport, host='tty' if args.tty else 'detached',
- operations=operation_count, duration_seconds=time.monotonic() - started,
- status='passed')
- except BaseException as error:
- emit(kind='failure', seed=args.seed, batch=batch + 1, operation=operation,
- transport=args.transport, host='tty' if args.tty else 'detached',
- operations=operation_count, session_pid=pid,
- elapsed_seconds=time.monotonic() - started, error=str(error))
- raise
-
-
-def main():
- parser = argparse.ArgumentParser(description='Deterministic valid-operation editor/9P soak; writes JSONL.')
- parser.add_argument('binary', type=Path)
- parser.add_argument('--seed', type=int, default=4200)
- parser.add_argument('--transport', choices=['unix', 'tcp'], default='unix')
- parser.add_argument('--tty', action='store_true', help='run an owned TTY host instead of a detached host')
- limit = parser.add_mutually_exclusive_group()
- limit.add_argument('--batches', type=int)
- limit.add_argument('--duration', type=float, help='seconds, stopping after the current batch')
- parser.add_argument('--interval', type=float, default=0, help='minimum seconds between batch starts')
- args = parser.parse_args()
- if args.batches is None and args.duration is None:
- args.batches = 100
- if args.batches is not None and args.batches < 1:
- parser.error('--batches must be positive')
- if args.duration is not None and not 0 < args.duration < float('inf'):
- parser.error('--duration must be finite and positive')
- if not 0 <= args.interval < float('inf'):
- parser.error('--interval must be finite and nonnegative')
- run(args)
-
-
-if __name__ == '__main__':
- main()
diff --git a/test/look_placement.py b/test/look_placement.py
deleted file mode 100644
index 8a31157f..00000000
--- a/test/look_placement.py
+++ /dev/null
@@ -1,143 +0,0 @@
-#!/usr/bin/env python3
-"""Check narrow-window first-document Look placement in owned SDL/TTY sessions.
-
-python3 -B test/look_placement.py zig-out/column-review/bin/pardes-gui
-python3 -B test/look_placement.py zig-out/column-review/bin/pardes --tty
-
-Starts the classic three-terminal layout, then opens the first document from
-the lower-left or right terminal. Screen-grid geometry must split only that
-terminal, immediately below it, and a repeated Look must reuse the file pane.
-Use --output DIR to retain grid observations and SDL GPU screenshots.
-"""
-import argparse
-from contextlib import nullcontext
-from pathlib import Path
-import tempfile
-
-from fs import session
-from ui_review import capture, panes, require, wait_for
-
-
-def geometry(client, markers):
- frame = client.screen()
- width = frame['cols']
- lines = [''.join(cell[0] for cell in frame['cells'][row * width:(row + 1) * width])
- for row in range(frame['rows'])]
- positions = {}
- for serial, marker in markers.items():
- hits = [(row, line.index(marker)) for row, line in enumerate(lines) if marker in line]
- if len(hits) != 1:
- return None
- row, offset = hits[0]
- column_x = 0 if offset < width // 2 else width // 2
- positions[serial] = (column_x, row)
- rectangles = {}
- for serial, (column_x, row) in positions.items():
- following = [other_y for other_x, other_y in positions.values()
- if other_x == column_x and other_y > row]
- end = min(following, default=frame['rows'])
- rectangles[serial] = (column_x, row, width // 2, end - row)
- return frame, rectangles
-
-
-def rectangle_text(frame, rect):
- x, y, width, height = rect
- return tuple(tuple(cell[0] for cell in frame['cells'][row * frame['cols'] + x:
- row * frame['cols'] + x + width])
- for row in range(y, y + height))
-
-
-def run_case(args, root, source_index):
- root.mkdir(parents=True, exist_ok=True, mode=0o700)
- root.chmod(0o700)
- config = root / 'config/pardes'
- config.mkdir(parents=True)
- (config / 'init').write_text('Shell /bin/sh\nTheme forge\n')
- target = root / 'opened.txt'
- contents = b'First document opened from this terminal.\n' * 40
- target.write_bytes(contents)
- name = 'look-placement'
- inherited = {} if args.tty else {
- 'PARDES_TEST': '1', 'PARDES_TEST_COLS': '150', 'PARDES_TEST_ROWS': '48',
- 'PARDES_TEST_CAPTURE_DIR': str(root), 'SDL_VIDEODRIVER': 'x11',
- }
- # No document argument: the regression concerns the very first document.
- launch = ['-c', 'stty cols 150 rows 48; exec "$@"', name,
- str(args.binary.resolve()), '--9p=' + name, '-n', '3']
- with session('/bin/sh', root, name, socket_name=name, tty=True,
- launch=launch, inherited=inherited) as (client, _):
- serials = list(panes(client))
- require(len(serials) == 3, 'classic fixture did not boot three terminals')
- markers = {}
- for index, serial in enumerate(serials):
- pane = f'/pane/{serial}'
- require('pty' in client.list(pane), 'fixture contains an existing document')
- marker = f'LP_{index}'
- markers[serial] = marker
- client.write(pane + '/tag', b'', truncate=True)
- client.write(pane + '/tag', f' {marker} Tty Del Collapse'.encode())
- # Establish non-pristine shells and a stable prompt before measuring.
- client.write(pane + '/pty/data', f"printf 'ready-{index}\\n'\r".encode())
- require(wait_for(lambda: f'\nready-{index}\n'.encode() in
- client.read(pane + '/body').replace(b'\r', b'')),
- 'terminal did not execute readiness command')
- before_frame, before = require(wait_for(lambda: geometry(client, markers)),
- 'terminal tags not visible in fixture')
- source = serials[source_index]
- source_rect = before[source]
- require(source_rect[3] >= 8, 'source terminal too short for a local split')
- require(before[serials[0]][:2] == (0, 2) and before[serials[1]][0] == 0 and
- before[serials[1]][1] > 2 and before[serials[2]][:2] == (75, 2),
- f'unexpected initial classic layout: {before}')
- client.write(f'/pane/{source}/look', f'{target}\n'.encode())
- opened = require(wait_for(lambda: next((serial for serial in panes(client)
- if serial not in serials), None)),
- 'Look did not open the first document')
- markers[opened] = 'opened.txt'
- after_frame, after = require(wait_for(lambda: geometry(client, markers)),
- 'Look pane tags not visible')
- require(len(panes(client)) == 4, 'Look removed a terminal or added extra panes')
- x, y, width, height = source_rect
- require(after[source][:3] == (x, y, width),
- f'Look moved its originating terminal: {source_rect} -> {after[source]}')
- require(after[opened][:3] == (x, y + after[source][3], width) and
- after[source][3] + after[opened][3] == height,
- f'Look did not split directly below its caller: {after}')
- for serial in serials:
- if serial == source:
- continue
- require(after[serial] == before[serial],
- f'Look resized or moved unrelated terminal {serial}: {before} -> {after}')
- require(rectangle_text(before_frame, before[serial]) ==
- rectangle_text(after_frame, after[serial]),
- f'Look changed unrelated terminal {serial} rendered text')
- require(client.read(f'/pane/{opened}/body') == contents,
- 'opened document contents differ')
- client.write(f'/pane/{source}/look', f'{target}:2\n'.encode())
- require(list(panes(client)) == serials + [opened], 'repeated Look created a duplicate pane')
- require(wait_for(lambda: geometry(client, markers)[1] == after),
- 'repeated Look changed layout')
- require(target.read_bytes() == contents, 'Look modified the fixture on disk')
- if args.output:
- capture(client, root, 'look-below-caller', not args.tty)
- print(f'{"TTY" if args.tty else "SDL"} source {"lower-left" if source_index == 1 else "right"}: '
- 'local split, unchanged other terminals, first-file contents and re-Look reuse passed', flush=True)
-
-
-def run(args):
- # Cache-rooted fixtures avoid relying on /tmp quota in developer sessions.
- cache = Path.home() / '.cache'
- cache.mkdir(exist_ok=True)
- context = nullcontext(str(args.output.resolve())) if args.output else tempfile.TemporaryDirectory(
- prefix='lp-', dir=cache)
- with context as directory:
- for source_index, label in [(1, 'left'), (2, 'right')]:
- run_case(args, Path(directory) / label, source_index)
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('binary', type=Path)
- parser.add_argument('--tty', action='store_true')
- parser.add_argument('--output', type=Path)
- run(parser.parse_args())
diff --git a/test/panes.zig b/test/panes.zig
index f7ca9fc5..d7fa3514 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -3134,7 +3134,6 @@ const ImageTests = struct {
const togglePalette = panes.Image.togglePalette;
const toggleAscii = panes.Image.toggleAscii;
const tagPrefix = panes.Image.tagPrefix;
- const legacySavedPrefix = panes.Image.legacySavedPrefix;
test "pane-local renderer choices are visible in the image tag" {
var state: State = .{ .path = @constCast("/tmp/picture.ppm") };
@@ -3147,10 +3146,6 @@ const ImageTests = struct {
config.tag_image ++ " petscii:on palette:terminal ascii:off /tmp/picture.ppm",
tag,
);
- try std.testing.expectEqualStrings(
- "img /tmp/picture.ppm",
- legacySavedPrefix(&state, "img /tmp/picture.ppm Keep Del").?,
- );
}
test "dump restore propagates malformed embedded image bytes" {
@@ -3173,7 +3168,6 @@ const PdfTests = struct {
const Point = panes.Pdf.Point;
const Document = panes.Pdf.Document;
const isPath = panes.Pdf.isPath;
- const legacySavedPrefix = panes.Pdf.legacySavedPrefix;
const State = panes.Pdf.State;
const probeWord = panes.Pdf.probeWord;
const pointAtGeometry = panes.Pdf.pointAtGeometry;
@@ -3186,26 +3180,10 @@ const PdfTests = struct {
try std.testing.expect(!isPath("pdf"));
}
- test "legacy PDF prefix is delimited by its stable marker and exact path" {
- const path = "/tmp/a document.pdf";
- const tag = "pdf 2/9 height PdfFit full PdfTint PdfSections " ++ path ++ " Keep Del";
- try std.testing.expectEqualStrings(
- "pdf 2/9 height PdfFit full PdfTint PdfSections " ++ path,
- legacySavedPrefix(path, tag).?,
- );
- }
-
test "feature-off PDF state is zero-sized" {
if (!enabled) try std.testing.expectEqual(@as(usize, 0), @sizeOf(State));
}
- test "compact PDF saved prefix separates page status from editable commands" {
- const path = "/tmp/a document.pdf";
- try std.testing.expectEqualStrings(path ++ " [2/9]", legacySavedPrefix(path, path ++ " [2/9] Keep Del").?);
- try std.testing.expect(legacySavedPrefix(path, path ++ " [oops] Keep Del") == null);
- try std.testing.expect(legacySavedPrefix(path, path ++ " [2/9/4] Keep Del") == null);
- }
-
test "PDF section rows preserve DFS ordinals and sanitise hierarchy" {
if (!enabled) return;
const entries = [_]pdf.OutlineEntry{
diff --git a/test/pdf.zig b/test/pdf.zig
index 4cb8f3dd..48869de1 100644
--- a/test/pdf.zig
+++ b/test/pdf.zig
@@ -287,6 +287,7 @@ test "PDF dump fallback remains a byte-preserving file" {
const saved_panes = [_]dump.Pane{.{
.kind = .image,
.tag = "pdf 3/9 height PdfFit full PdfTint PdfSections " ++ path ++ " Keep Del",
+ .tag_tail = " Keep Del",
.body = "",
.scroll = 2,
.cols = 80,