summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/output.zig3
-rw-r--r--test/panes.zig37
-rw-r--r--test/pdf_pointer_bench.zig224
-rw-r--r--test/snapshot.zig4
-rw-r--r--test/snapshots/builtins.snap4
-rw-r--r--test/snapshots/dump.golden3
-rw-r--r--test/snapshots/load.golden18
-rw-r--r--test/snapshots/restore.golden6
8 files changed, 285 insertions, 14 deletions
diff --git a/test/output.zig b/test/output.zig
index ad33954f..7c80e105 100644
--- a/test/output.zig
+++ b/test/output.zig
@@ -1089,7 +1089,8 @@ test "identical result refresh keeps warm content caches without allocation" {
try std.testing.expectEqual(revision + 1, file.revision);
try std.testing.expectEqualStrings("short\n", file.content);
try std.testing.expectEqualStrings("changed", file.output.?.arg());
- try std.testing.expectEqual(@as(usize, 0), file.line_starts.len);
+ // The warm index is carried through the edit, not dropped.
+ try std.testing.expectEqualSlices(usize, &.{ 0, 6 }, file.line_starts);
try std.testing.expectEqual(@as(usize, 0), file.highlights.len);
try std.testing.expect(file.syntax_dirty);
try std.testing.expect(!result.vsel.active and !result.select);
diff --git a/test/panes.zig b/test/panes.zig
index 8e71e69d..2d3aed43 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -394,6 +394,42 @@ const BodyHitTests = struct {
}
};
+const LineNumberLookTests = struct {
+ test "right-click on a line number looks at that line, sticky context headers included" {
+ if (!syntax.enabled) return error.SkipZigTest;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 64, .rows = 18 });
+ defer p.deinit();
+ p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 8, .tagline_h = 10 };
+ const pane = try TreeContextTests.setZigFile(p, "const Box = struct {\n pub fn work(\n value: usize,\n ) void {\n" ++ " // body\n" ** 60 ++ " }\n};\n");
+ try std.testing.expect(p.executeBuiltinLine(0, "TreeContext"));
+ pane.scrollBy(30);
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const layer = &p.surface.body_layers[0];
+ try std.testing.expect(layer.context_rows >= 2);
+ // A header row's number is its declaration's own source line, not the
+ // body line it sits over (`pub fn work(` is line 1); a body row's is
+ // its own.
+ try std.testing.expectEqual(@as(i32, 1), pane.wrapAt(1).line);
+ const rows = [_]u16{ 1, pane.context_rows + 3 };
+ for (rows) |row| {
+ // Each Look recentres the view; come back under the headers.
+ pane.scrollBy(30 - pane.scroll());
+ _ = frame.reset(.retain_capacity);
+ _ = try p.render(frame.allocator());
+ const expected = pane.wrapAt(row).line;
+ var press = BodyHitTests.mouse(&p.surface.body_layers[0], row, 1, .press);
+ press.button = .right;
+ var release = BodyHitTests.mouse(&p.surface.body_layers[0], row, 1, .release);
+ release.button = .right;
+ p.update(.{ .mouse = press });
+ p.update(.{ .mouse = release });
+ try std.testing.expectEqual(expected, pane.cur_row);
+ }
+ }
+};
+
const TreeContextTests = struct {
const source = "const Box = struct {\n" ++
" pub fn work(\n" ++
@@ -4069,6 +4105,7 @@ test {
_ = TagNameTintTests;
_ = TreeContextTests;
_ = BodyHitTests;
+ _ = LineNumberLookTests;
_ = TtySelectionTests;
_ = ClickCursorTests;
_ = JumpSelectionTests;
diff --git a/test/pdf_pointer_bench.zig b/test/pdf_pointer_bench.zig
new file mode 100644
index 00000000..f1d61229
--- /dev/null
+++ b/test/pdf_pointer_bench.zig
@@ -0,0 +1,224 @@
+//! Pointer gestures over a native PDF pane: hovering words (the Look preview
+//! highlight), and dragging a selection to its release.
+//!
+//! zig build pdf-pointer-bench -Doptimize=ReleaseFast -- [--label L] [--reps N] [--path FILE]
+//! [--cell WxH] [--scroll NOTCHES] [--verify 1] [--live 1]
+//!
+//! Every frame is timed the way a shell pump spends it: the event through
+//! `Pardes.update`, one `render()` into a reset arena, the effect drain. A
+//! hover step is its motion frame plus every tick frame the preview animates
+//! before settling, which is when the highlight lands. Stdout is CSV,
+//! `label,scenario:kind,round,ns`, one row per frame and one per step
+//! (`kind=step`), for an external statistics pass.
+//!
+//! `visual` hashes every presented raster in full, with its placement, on the
+//! frame each step settles on. It is the no-regression proof: a change to how highlights
+//! are painted may change what it costs, never one presented pixel.
+const std = @import("std");
+const pardes = @import("pardes");
+const pdf = @import("mupdf");
+
+pub const std_options: std.Options = .{ .log_level = .err };
+
+const gpa = std.heap.smp_allocator;
+const cols: u16 = 120;
+const rows: u16 = 40;
+
+const Config = struct {
+ path: []const u8 = "docs/design.pdf",
+ label: []const u8 = "run",
+ reps: usize = 5,
+ /// Wheel notches before the gestures, off the sparse title page.
+ scroll: usize = 40,
+ /// 8x16 is the Kitty policy's cell; the SDL shell's is closer to 16x35.
+ cell_w: u16 = 8,
+ cell_h: u16 = 16,
+ /// Re-render every raster that claims to hold the current highlights and
+ /// compare it row by row: exactness without a baseline build.
+ verify: bool = false,
+ /// A shell that takes row patches: selections repaint while dragged.
+ live: bool = false,
+};
+
+pub fn main(init: std.process.Init) !void {
+ const args = try init.minimal.args.toSlice(init.arena.allocator());
+ var config: Config = .{};
+ var i: usize = 1;
+ while (i + 1 < args.len) : (i += 2) {
+ const value = args[i + 1];
+ if (std.mem.eql(u8, args[i], "--path")) config.path = value else if (std.mem.eql(u8, args[i], "--label")) config.label = value else if (std.mem.eql(u8, args[i], "--reps")) config.reps = try std.fmt.parseUnsigned(usize, value, 10) else if (std.mem.eql(u8, args[i], "--scroll")) config.scroll = try std.fmt.parseUnsigned(usize, value, 10) else if (std.mem.eql(u8, args[i], "--verify")) config.verify = std.mem.eql(u8, value, "1") else if (std.mem.eql(u8, args[i], "--live")) config.live = std.mem.eql(u8, value, "1") else if (std.mem.eql(u8, args[i], "--cell")) {
+ var it = std.mem.splitScalar(u8, value, 'x');
+ config.cell_w = try std.fmt.parseUnsigned(u16, it.next() orelse "", 10);
+ config.cell_h = try std.fmt.parseUnsigned(u16, it.next() orelse "", 10);
+ } else return error.UnknownOption;
+ }
+ if (i != args.len) return error.MissingValue;
+
+ var visual: ?u64 = null;
+ for (0..config.reps) |round| {
+ const got = try measure(config, round);
+ if (visual) |want| if (want != got) {
+ std.debug.print("pdf-pointer-bench: visual changed between rounds: {x} vs {x}\n", .{ want, got });
+ return error.Unstable;
+ };
+ visual = got;
+ }
+ std.debug.print("pdf-pointer-bench {s}: visual={x:0>16}\n", .{ config.label, visual.? });
+}
+
+const Run = struct {
+ core: *pardes.Pardes,
+ arena: std.heap.ArenaAllocator,
+ config: Config,
+ round: usize,
+ visual: u64 = 0x7061_7264_6573_5050,
+ step_ns: u64 = 0,
+ frames: usize = 0,
+ steps: usize = 0,
+ last: ?*const pardes.Surface = null,
+
+ fn frame(run: *Run, scenario: []const u8, kind: []const u8, event: pardes.Event) !void {
+ const started = nowNs();
+ run.core.update(event);
+ while (run.core.nextEffect()) |_| {}
+ _ = run.arena.reset(.retain_capacity);
+ const surface = try run.core.render(run.arena.allocator());
+ while (run.core.nextEffect()) |_| {}
+ const elapsed = nowNs() - started;
+ run.step_ns += elapsed;
+ run.frames += 1;
+ print("{s},{s}:{s},{d},{d}\n", .{ run.config.label, scenario, kind, run.round, elapsed });
+ run.last = surface;
+ if (run.config.verify) try verify(run.core, run.arena.allocator());
+ }
+
+ /// The step, then the tick frames its animation asks for (a hover's
+ /// preview arrives on one of them), as a pump with nothing else queued.
+ fn step(run: *Run, scenario: []const u8, kind: []const u8, event: pardes.Event) !void {
+ run.step_ns = 0;
+ try run.frame(scenario, kind, event);
+ var ticks: usize = 0;
+ while (run.core.animationActive() and ticks < 32) : (ticks += 1)
+ try run.frame(scenario, "tick", .tick);
+ print("{s},{s}:step,{d},{d}\n", .{ run.config.label, scenario, run.round, run.step_ns });
+ // Only the settled frame is the invariant: how many frames a preview
+ // takes to arrive is a setting (look_preview_delay_frames).
+ run.observe(run.last.?);
+ run.steps += 1;
+ }
+
+ fn observe(run: *Run, surface: *const pardes.Surface) void {
+ for (surface.images[0..surface.nimages]) |maybe| {
+ const place = maybe orelse continue;
+ run.visual = mix(run.visual, [_]u64{ place.pane, place.x, place.y, place.w, place.h, place.native.page, place.iw, place.ih });
+ if (place.native.geometry) |g| run.visual = mix(run.visual, [_]u64{ g.src.x, g.src.y, g.src.w, g.src.h, g.dst.x, g.dst.y, g.dst.w, g.dst.h });
+ run.visual = std.hash.Wyhash.hash(run.visual, place.rgba);
+ }
+ }
+};
+
+var verify_failures: usize = 0;
+
+fn verify(core: *pardes.Pardes, arena: std.mem.Allocator) !void {
+ const Pdf = pardes.panes.Pdf;
+ const pane = core.panes[0] orelse return;
+ const state = &(pane.pdf orelse return);
+ const highlights = try Pdf.buildHighlights(state, arena, Pdf.highlightInput(core, 0, pane));
+ for (state.rasters[0..state.rasters_len]) |*r| {
+ if (!r.valid or r.rgba.len == 0 or r.tint_key == null) continue;
+ const want = highlights.forPage(r.page);
+ if (comptime !@hasField(@TypeOf(r.*), "baked")) return else if (!std.mem.eql(u8, std.mem.sliceAsBytes(want), std.mem.sliceAsBytes(r.baked))) continue;
+ const shape = try state.document.measureRenderAt(r.page, r.request);
+ // The oracle is the WHOLE page: a band's own edge rows can differ.
+ const whole = try arena.alloc(u8, shape.len);
+ try state.document.renderIntoAtWithPaper(r.page, r.request, shape, shape.wholePage(), want, whole, false);
+ try pdf.tintRgba(whole, r.tint_key.?.mode, r.tint_key.?.colors);
+ const fresh = whole[r.band_y * shape.stride ..][0 .. r.band_h * shape.stride];
+ var first: ?usize = null;
+ var last: usize = 0;
+ var count: usize = 0;
+ for (0..r.band_h) |y| {
+ const row = fresh[y * shape.stride ..][0..shape.stride];
+ if (std.mem.eql(u8, row, r.rgba[y * shape.stride ..][0..shape.stride])) continue;
+ if (first == null) first = y;
+ last = y;
+ count += 1;
+ }
+ if (first) |f| {
+ verify_failures += 1;
+ if (verify_failures <= 20) std.debug.print("verify: page {d} rows {d}..{d} ({d} differ), highlights {d}, band {d}+{d}\n", .{ r.page, f + r.band_y, last + r.band_y, count, want.len, r.band_y, r.band_h });
+ }
+ }
+}
+
+fn measure(config: Config, round: usize) !u64 {
+ const core = try pardes.Pardes.init(gpa, .{ .file = config.path, .cols = cols, .rows = rows });
+ defer core.deinit();
+ core.native_images = true;
+ if (comptime @hasField(pardes.Pardes, "native_image_patches")) core.native_image_patches = config.live;
+ var run: Run = .{ .core = core, .arena = .init(gpa), .config = config, .round = round };
+ defer run.arena.deinit();
+ const cell: pardes.CellPixels = .{ .w = config.cell_w, .h = config.cell_h };
+ core.update(.{ .resize = .{ .cols = cols, .rows = rows, .cell_pixels = cell } });
+ _ = try core.render(run.arena.allocator());
+ const pane = core.panes[0] orelse return error.NoPane;
+ if (pane.pdf == null) return error.NotPdf;
+ const rect = core.rects[0];
+ const mid_col = rect.x + rect.w / 2;
+ const mid_row = rect.y + rect.h / 2;
+ for (0..config.scroll) |_| {
+ core.update(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = mid_col, .row = mid_row } });
+ _ = run.arena.reset(.retain_capacity);
+ _ = try core.render(run.arena.allocator());
+ while (core.nextEffect()) |_| {}
+ }
+
+ const top = rect.y + 4;
+ const bottom = rect.y + rect.h -| 3;
+ const left = rect.x + 4;
+ const right = rect.x + rect.w -| 4;
+
+ // Hover: walk four rows of the body, a word-ish stride at a time.
+ var row: u16 = top;
+ while (row < bottom) : (row += (bottom - top) / 4) {
+ var col: u16 = left;
+ while (col < right) : (col += 3)
+ try run.step("hover", "motion", .{ .mouse = .{ .button = .none, .kind = .motion, .col = col, .row = row } });
+ }
+ // Leave the page so the last preview is withdrawn like a real exit.
+ try run.step("hover", "leave", .pointer_leave);
+
+ // Select: press, drag across three lines, release; five times down the page.
+ row = top;
+ while (row + 3 < bottom) : (row += (bottom - top) / 5) {
+ try run.step("select", "press", .{ .mouse = .{ .button = .left, .kind = .press, .col = left, .row = row } });
+ var n: u16 = 1;
+ while (n <= 24) : (n += 1) {
+ const col = left + (right - left) * (n % 8) / 8;
+ try run.step("select", "drag", .{ .mouse = .{ .button = .left, .kind = .drag, .col = col, .row = row + n / 8 } });
+ }
+ try run.step("select", "release", .{ .mouse = .{ .button = .left, .kind = .release, .col = right, .row = row + 3 } });
+ }
+ return mix(run.visual, @as(u64, run.steps));
+}
+
+fn print(comptime format: []const u8, args: anytype) void {
+ var buf: [256]u8 = undefined;
+ const line = std.fmt.bufPrint(&buf, format, args) catch return;
+ _ = std.c.write(1, line.ptr, line.len);
+}
+
+fn mix(seed: u64, value: anytype) u64 {
+ var copy = value;
+ return std.hash.Wyhash.hash(seed, std.mem.asBytes(&copy));
+}
+
+fn nowNs() u64 {
+ var ts: std.c.timespec = undefined;
+ _ = std.c.clock_gettime(.MONOTONIC, &ts);
+ return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec));
+}
+
+comptime {
+ _ = pdf.Document;
+}
diff --git a/test/snapshot.zig b/test/snapshot.zig
index 7cc5eb91..bdabb56e 100644
--- a/test/snapshot.zig
+++ b/test/snapshot.zig
@@ -569,7 +569,9 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const
try mkdir(arena, config_home, false);
const pardes_config = try std.fmt.allocPrint(arena, "{s}/pardes", .{config_home});
try mkdir(arena, pardes_config, false);
- try eh.writeFile(try std.fmt.allocPrintSentinel(arena, "{s}/init", .{pardes_config}, 0), "Shell bash\n");
+ // Messages leave the moment they are dismissed, as goldens have always
+ // recorded: a linger or a dissolve is time, and time cannot be a golden.
+ try eh.writeFile(try std.fmt.allocPrintSentinel(arena, "{s}/init", .{pardes_config}, 0), "Shell bash\nMessageAnimation off\nMessageLinger 0\n");
_ = setenv("TERM", "xterm-256color", 1);
_ = setenv("LC_ALL", "C", 1);
// a wall clock cannot live in a golden; everything that would print one
diff --git a/test/snapshots/builtins.snap b/test/snapshots/builtins.snap
index cd47209f..02547b91 100644
--- a/test/snapshots/builtins.snap
+++ b/test/snapshots/builtins.snap
@@ -19,6 +19,10 @@ wheel up 110 50
wheel up 110 50
wheel up 110 50
wheel up 110 50
+wheel up 110 50
+wheel up 110 50
+wheel up 110 50
+wheel up 110 50
wait 10000 Look enter, right-click
wait 10000 Exec tab, middle-click
wait 10000 Filter
diff --git a/test/snapshots/dump.golden b/test/snapshots/dump.golden
index 8f920136..e466dbf1 100644
--- a/test/snapshots/dump.golden
+++ b/test/snapshots/dump.golden
@@ -38,6 +38,7 @@
.cwd = "/tmp/pardes-snap/dump/cwd",
.stream = "ls\nd.txt\n",
.stream_b64 = <elided>
+ .stream_is_screen = true,
.cursor = .{ .col = 2, .row = 2 },
},
.file = null,
@@ -57,6 +58,7 @@
.cwd = "/tmp/pardes-snap/dump/cwd",
.stream = "ls\nd.txt\n",
.stream_b64 = <elided>
+ .stream_is_screen = true,
.cursor = .{ .col = 2, .row = 2 },
},
.file = null,
@@ -76,6 +78,7 @@
.cwd = "/tmp/pardes-snap/dump/cwd",
.stream = "ls\nd.txt\n",
.stream_b64 = <elided>
+ .stream_is_screen = true,
.cursor = .{ .col = 2, .row = 2 },
},
.file = null,
diff --git a/test/snapshots/load.golden b/test/snapshots/load.golden
index 994ab83e..32024cec 100644
--- a/test/snapshots/load.golden
+++ b/test/snapshots/load.golden
@@ -2,10 +2,10 @@
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill
| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/load/cwd Tty Save Mode Filter /tmp/pardes-snap/load/cwd Tty Save Mode Filter
-| echo LOAD''MARK; seq 1 40; echo E''ND ls
+| $ echo LOAD''MARK; seq 1 40; echo E''ND $ ls
| LOADMARK d.txt
-| 1
-| 2
+| 1 $
+| 2 ── restored history ──
| 3
| 4
| 5
@@ -17,8 +17,10 @@
| 11
| 12
| /tmp/pardes-snap/load/cwd Tty Save Mode Filter
-| ls
+| $ ls
| d.txt
+| $
+| ── restored history ──
== style loaded grid=100x31
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
@@ -26,8 +28,8 @@
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|7: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-73 #e5eee5,#0d1410,f 74-99 #e5eee5,#0d1410,
+|7: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
|8: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|9: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|10: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
@@ -41,8 +43,8 @@
|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-23 #e5eee5,#0d1410,f 24-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|22: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
diff --git a/test/snapshots/restore.golden b/test/snapshots/restore.golden
index 11d92288..01b52984 100644
--- a/test/snapshots/restore.golden
+++ b/test/snapshots/restore.golden
@@ -6,9 +6,7 @@
| $ echo KEEP''ME
| KEEPME
| $
-== snap restored grid=100x31 cursor=4,6
+== snap restored grid=100x31 cursor=4,8
|0: Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill
|2: /tmp/pardes-snap/restore/cwd Tty Save Mode Filter Collapse Del
-|3: ls
-|4: echo KEEP''ME
-|6:
+|7: ── restored history ──