diff options
| -rw-r--r-- | src/CHANGELOG.md | 8 | ||||
| -rw-r--r-- | src/Output.zig | 3 | ||||
| -rw-r--r-- | src/dump.zig | 1 | ||||
| -rw-r--r-- | src/exec.zig | 50 | ||||
| -rw-r--r-- | src/image.zig | 8 | ||||
| -rw-r--r-- | src/pardes.zig | 159 | ||||
| -rw-r--r-- | src/pdf_view.zig | 18 | ||||
| -rw-r--r-- | src/tagline.zig | 135 | ||||
| -rw-r--r-- | src/terminal.zig | 33 | ||||
| -rw-r--r-- | test/column_grip.py | 132 | ||||
| -rw-r--r-- | test/fs_soak.py | 284 | ||||
| -rw-r--r-- | test/look_placement.py | 143 | ||||
| -rw-r--r-- | test/panes.zig | 22 | ||||
| -rw-r--r-- | test/pdf.zig | 1 |
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, |
