From 9695cb098c784792af79c22d46a17f7bc7247d33 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 22 Sep 2026 17:43:57 -0300 Subject: Repaint PDF highlights by row, send rasters by shared memory, and animate messages PDF highlights (hover preview, search, selection) are baked into page rasters, and any change re-rendered the whole page with MuPDF; the TTY then re-sent it as base64 (4.7 MB a page), the GUI as a new texture. Worse, a pointer motion over a PDF invalidated the page even when no preview was shown, so every motion paid that. Now: - A raster whose baked highlight set equals the wanted one is left alone. - A highlighted page keeps its clean rows (before highlights and tint); a change repaints only the rows of quads that differ, running MuPDF's highlight pass (pardes_pdf_paint_highlights) over those clean rows and tinting them: the operations a full render performs, so the pixels are identical. MuPDF band renders are NOT bit-identical to a whole page (edge rows, resampled images), so they are never used to patch; the comment claiming otherwise is corrected. - ImagePlace.patch hands shells the changed rows; the GUI uploads just those rows into the texture it holds. - The TTY probes kitty shared memory (t=s) with an id vaxis never reaches and sends rasters as a /dev/shm name when the terminal reads it; direct base64 otherwise (ssh). - Shells that take row patches (GUI, TTY with shm) repaint a selection while it is dragged instead of only on release. Latency elsewhere: - TTY: an animating frame no longer sleeps 16 ms blind; a tick thread posts into the input queue, so input inside the frame is handled at once. - TTY and GUI: queued pointer motions coalesce to the last. - GUI: a skipped swapchain image re-arms the frame (3 retries); animations still tick while nothing presents. - Editing: the line index is carried across an edit instead of rebuilt from a scan of the whole file per keystroke. Messages fall into their row (ease-in; the GUI slides the band out from under the tagline, a terminal fades it), stay until the next input as before, linger MessageLinger ms (default 800), and dissolve (ease-out). MessageAnimation toggles it; both are settings, in Config and startup files. The snapshot harness pins the old behaviour. The detached server now ticks animations. A restored terminal comes back live: the old screen and scrollback (dumped as clean VT by ghostty's formatter, replayed at the new size; older dumps fall back to their rendered text), a dim "restored history" marker, then a new shell in the directory it was in. Right-click on a line number in a file pane looks at that line (a sticky context header's number included). Measured with an external pty driver (TTY), an in-process fence trace (GUI, PARDES_TEST_LATENCY), and test/pdf_pointer_bench.zig (pixel identity against the baseline and a whole-page oracle); balanced A/A/B rounds, paired per-round statistics. Messages stack: each event gets its own row and its own fall, linger and dissolve; a line keeps its row until it leaves and a new one fills the first free row. Announcements and statuses are replaced in place, not stacked. MessageFall, MessageDissolve and DumpDir are settings Config reports. Co-Authored-By: Claude Opus 5.5 (1M context) --- test/output.zig | 3 +- test/panes.zig | 37 +++++++ test/pdf_pointer_bench.zig | 224 ++++++++++++++++++++++++++++++++++++++++++ test/snapshot.zig | 4 +- test/snapshots/builtins.snap | 4 + test/snapshots/dump.golden | 3 + test/snapshots/load.golden | 18 ++-- test/snapshots/restore.golden | 6 +- 8 files changed, 285 insertions(+), 14 deletions(-) create mode 100644 test/pdf_pointer_bench.zig (limited to 'test') 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(©)); +} + +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 = + .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 = + .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 = + .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 ── -- cgit v1.3