diff options
Diffstat (limited to 'tests.zig')
| -rw-r--r-- | tests.zig | 1061 |
1 files changed, 0 insertions, 1061 deletions
diff --git a/tests.zig b/tests.zig deleted file mode 100644 index 6080aa3b..00000000 --- a/tests.zig +++ /dev/null @@ -1,1061 +0,0 @@ -const std = @import("std"); -const h = @import("e2e_harness.zig"); -const Harness = h.Harness; -const config = @import("config"); -const dump = @import("dump.zig"); - -const ls_ready = "tests.zig"; - -// E2E tests for pardes. The harness forks the app in a pty and parses its -// output with a ghostty terminal emulator, so all assertions read the 2D screen -// GRID (plainString) — not raw byte streams. This is decoupled from the app's -// rendering (vaxis diff-rendering, repaints, escape sequences): the grid is -// always current after pump/waitFor. -// -// Input is sent as raw bytes (keystrokes, SGR mouse sequences) that the app's -// vaxis parses. The harness terminal only reads output. -// -// Mouse events use SGR pixel mode: \x1b[<button;col;row M (press) / m (release). -// Cols/rows are 1-based screen coords. Button 0=left, 1=middle, 2=right. -// A drag is press at start + release at end (with different coords). - -// Row index (0-based) of the first screen line containing `needle`, or null. -fn rowOf(text: []const u8, needle: []const u8) ?usize { - var it = std.mem.splitScalar(u8, text, '\n'); - var i: usize = 0; - while (it.next()) |line| : (i += 1) { - if (std.mem.indexOf(u8, line, needle) != null) return i; - } - return null; -} - -// Column (0-based byte offset within its line) of the first `needle` on screen. -// The grid is monospace ASCII here, so the byte offset is the screen column. -fn colOf(text: []const u8, needle: []const u8) ?usize { - var it = std.mem.splitScalar(u8, text, '\n'); - while (it.next()) |line| { - if (std.mem.indexOf(u8, line, needle)) |off| return off; - } - return null; -} - -fn clearRightColumnTerminal(hs: *Harness) !void { - try hs.send("\x1b[<0;60;20M\x1b[<0;60;20m"); // focus the right-column terminal - try hs.pump(300); - try hs.send("\x02"); // normal -> tty - try hs.pump(400); - try hs.send("clear\r"); - try hs.pump(800); - try hs.send("\x02"); // tty -> normal - try hs.pump(400); -} - -fn writeReplayFixture(io: std.Io, gpa: std.mem.Allocator, path: []const u8) !void { - const file_content = - "const std = @import(\"std\");\n" ++ - "\n" ++ - "// utf8: \xe2\x80\x94\n"; - const content_b64 = try dump.encodeBytes(gpa, file_content); - defer gpa.free(content_b64); - const panes = [_]dump.Pane{.{ - .kind = .file, - .tag = "nm /tmp/replay_fixture.zig Save Del", - .body = "", - .cols = 78, - .rows = 22, - .file = .{ - .path = "/tmp/replay_fixture.zig", - .content = file_content, - .content_b64 = content_b64, - }, - }}; - const col_panes = [_]usize{0}; - const columns = [_]dump.Column{.{ .weight = 1, .panes = &col_panes }}; - try dump.writeFile(io, gpa, path, .{ - .screen = .{ .cols = 80, .rows = 24 }, - .topbar = "Kill Newcol Tutor Debug Colors NextColor Dump", - .theme = "dark", - .columns = &columns, - .panes = &panes, - }); -} - -fn writeReplayScrollFixture(io: std.Io, gpa: std.mem.Allocator, path: []const u8) !void { - var file_content: std.ArrayList(u8) = .empty; - defer file_content.deinit(gpa); - var line_no: usize = 1; - while (line_no <= 80) : (line_no += 1) { - const line = try std.fmt.allocPrint(gpa, "replay scroll line {d}\n", .{line_no}); - defer gpa.free(line); - try file_content.appendSlice(gpa, line); - } - const content_b64 = try dump.encodeBytes(gpa, file_content.items); - defer gpa.free(content_b64); - const panes = [_]dump.Pane{.{ - .kind = .file, - .tag = "nm /tmp/replay_scroll_fixture.zig Save Del", - .body = "", - .cols = 78, - .rows = 22, - .file = .{ - .path = "/tmp/replay_scroll_fixture.zig", - .content = file_content.items, - .content_b64 = content_b64, - }, - }}; - const col_panes = [_]usize{0}; - const columns = [_]dump.Column{.{ .weight = 1, .panes = &col_panes }}; - try dump.writeFile(io, gpa, path, .{ - .screen = .{ .cols = 80, .rows = 24 }, - .topbar = "Kill Newcol Tutor Debug Colors NextColor Dump", - .theme = "dark", - .columns = &columns, - .panes = &panes, - }); -} - -fn writeReplayHoverFixture(io: std.Io, gpa: std.mem.Allocator, path: []const u8) !void { - const panes = [_]dump.Pane{ - .{ - .kind = .file, - .tag = "nm /tmp/replay_left.txt Save Del", - .body = "", - .cols = 48, - .rows = 22, - .file = .{ - .path = "/tmp/replay_left.txt", - .content = "left\n", - }, - }, - .{ - .kind = .file, - .tag = "nm /tmp/replay_right.txt Save Del", - .body = "", - .cols = 48, - .rows = 22, - .file = .{ - .path = "/tmp/replay_right.txt", - .content = "right\n", - }, - }, - }; - const left_col = [_]usize{0}; - const right_col = [_]usize{1}; - const columns = [_]dump.Column{ - .{ .weight = 1, .panes = &left_col }, - .{ .weight = 1, .panes = &right_col }, - }; - try dump.writeFile(io, gpa, path, .{ - .screen = .{ .cols = 100, .rows = 24 }, - .topbar = "Kill Newcol Tutor Debug Colors NextColor Dump", - .theme = "dark", - .columns = &columns, - .panes = &panes, - }); -} - -fn writeReplayImageFixture(io: std.Io, gpa: std.mem.Allocator, path: []const u8) !void { - const png_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="; - const panes = [_]dump.Pane{.{ - .kind = .image, - .tag = "img /tmp/replay.png Petscii Del", - .body = "", - .cols = 78, - .rows = 22, - .image = .{ - .path = "/tmp/replay.png", - .bytes_b64 = png_b64, - }, - }}; - const col_panes = [_]usize{0}; - const columns = [_]dump.Column{.{ .weight = 1, .panes = &col_panes }}; - try dump.writeFile(io, gpa, path, .{ - .screen = .{ .cols = 80, .rows = 24 }, - .topbar = "Kill Newcol Tutor Debug Colors NextColor Dump", - .theme = "dark", - .columns = &columns, - .panes = &panes, - }); -} - -pub fn main(init: std.process.Init) !void { - const gpa = init.gpa; - const io = init.io; - const args = try init.minimal.args.toSlice(init.arena.allocator()); - if (args.len != 3) { - std.debug.print("usage: {s} <pardes-exe> <pardes-replay-exe>\n", .{args[0]}); - return error.BadArgs; - } - const exe = args[1]; - const replay_exe = args[2]; - - var hs = try Harness.init(gpa, exe.ptr, 24, 80); - hs.trace = config.trace; - defer hs.deinit(); - - // 0. The stats overlay is off by default; turn it on via the "Debug" command in - // the global tagbar (top row). "Debug" sits at cols 18-22, so SGR col ~20, - // row 1. This also exercises the global tagbar + the Debug toggle. Give the - // app a moment to come up and render the bar first. - try hs.pump(800); - try hs.send("\x1b[<1;20;1M\x1b[<1;20;1m"); // toggle the Debug overlay on - - // 1. The app posts an initial winsize on startup. Its winsize handler bumps - // the resize counter to 1 and repaints, so "resizes=1" shows in the debug - // overlay. Confirms the app is up and handled its first size. - try hs.expectWaitFor("resizes=1", 6000, "app never handled its initial winsize (no 'resizes=1')"); - try hs.expectWaitFor(ls_ready, 8000, "new terminals did not auto-ls their directory on open"); - - // 1b. Dump is a global-tagbar builtin. It writes a backend-independent ZON - // snapshot with compact pane ids, layout columns, rendered pane text, and - // pane backing contents. - std.Io.Dir.deleteFileAbsolute(io, dump.default_path) catch {}; - try hs.send("\x1b[<1;43;1M\x1b[<1;43;1m"); // middle-click "Dump" in the topbar - try hs.pump(800); - { - const dumped = dump.readFile(io, gpa, dump.default_path) catch { - hs.dump("Dump did not write a parseable /tmp/pardes-dump.zon"); - return error.ExpectFailed; - }; - defer dump.free(gpa, dumped); - if (dumped.panes.len == 0 or dumped.columns.len == 0 or dumped.active >= dumped.panes.len) { - hs.dump("Dump wrote an invalid empty layout"); - return error.ExpectFailed; - } - if (std.mem.indexOf(u8, dumped.topbar, "Dump") == null) { - hs.dump("Dump snapshot did not include the main tagline"); - return error.ExpectFailed; - } - if (!std.mem.eql(u8, dumped.theme, "dark")) { - hs.dump("Dump snapshot did not include the active theme"); - return error.ExpectFailed; - } - if (dumped.panes[0].kind != .terminal or dumped.panes[0].terminal == null) { - hs.dump("Dump snapshot did not capture the terminal pane stream"); - return error.ExpectFailed; - } - if (dumped.panes[0].terminal.?.stream_b64.len == 0) { - hs.dump("Dump snapshot did not include base64 terminal bytes"); - return error.ExpectFailed; - } - const tty_bytes = try dump.decodeBytes(gpa, dumped.panes[0].terminal.?.stream_b64); - defer gpa.free(tty_bytes); - if (std.mem.indexOf(u8, tty_bytes, ls_ready) == null) { - hs.dump("Dump terminal byte stream does not replay the visible ls output"); - return error.ExpectFailed; - } - } - { - var replay_h = try Harness.initArgs(gpa, replay_exe.ptr, 24, 80, dump.default_path); - replay_h.trace = config.trace; - defer replay_h.deinit(); - try replay_h.pump(1200); - try replay_h.expectContains("Kill Newcol", "replay did not render the dump topbar"); - try replay_h.expectContains(ls_ready, "replay did not rebuild the terminal from the dumped byte stream"); - try replay_h.expectNotContains("\xc2\xaa", "replay rendered corrupt prefix bytes"); - } - { - const fixture_path = "/tmp/pardes-replay-fixture.zon"; - try writeReplayFixture(io, gpa, fixture_path); - var replay_file = try Harness.initArgs(gpa, replay_exe.ptr, 24, 80, fixture_path); - replay_file.trace = config.trace; - defer replay_file.deinit(); - try replay_file.pump(1200); - try replay_file.expectContains(" 1 const std", "replay file pane line numbers are broken"); - try replay_file.expectNotContains("\xc2\xaa", "replay file line numbers used invalid stack bytes"); - } - { - const fixture_path = "/tmp/pardes-replay-hover.zon"; - try writeReplayHoverFixture(io, gpa, fixture_path); - var replay_hover = try Harness.initArgs(gpa, replay_exe.ptr, 24, 100, fixture_path); - replay_hover.trace = config.trace; - defer replay_hover.deinit(); - try replay_hover.pump(1200); - try replay_hover.send("\x1b[<35;50;10M"); - try replay_hover.pump(400); - try replay_hover.expectContains("╎", "replay hover did not overlay the dashed resize handle"); - } - { - const fixture_path = "/tmp/pardes-replay-scroll-fixture.zon"; - try writeReplayScrollFixture(io, gpa, fixture_path); - var replay_scroll = try Harness.initArgs(gpa, replay_exe.ptr, 24, 80, fixture_path); - replay_scroll.trace = config.trace; - defer replay_scroll.deinit(); - try replay_scroll.pump(1200); - try replay_scroll.expectContains(" 1 replay scroll line 1", "replay scrollbar fixture did not start at the top"); - try replay_scroll.send("\x1b[<2;1;23M\x1b[<2;1;23m"); - try replay_scroll.pump(300); - try replay_scroll.expectContains(" 21 replay scroll line 21", "replay right-click scrollbar did not scroll down"); - try replay_scroll.expectNotContains(" 1 replay scroll line 1", "replay right-click scrollbar left the top line visible"); - try replay_scroll.send("\x1b[<0;1;23M\x1b[<0;1;23m"); - try replay_scroll.pump(300); - try replay_scroll.expectContains(" 1 replay scroll line 1", "replay left-click scrollbar did not scroll back up"); - } - { - const fixture_path = "/tmp/pardes-replay-image.zon"; - try writeReplayImageFixture(io, gpa, fixture_path); - var replay_image = try Harness.initArgs(gpa, replay_exe.ptr, 24, 80, fixture_path); - replay_image.trace = config.trace; - defer replay_image.deinit(); - try replay_image.pump(1200); - try replay_image.expectContains("/tmp/replay.png", "replay image pane did not render from dumped bytes"); - } - - // 2. Regression: grow the pty -> SIGWINCH -> app re-resizes. Counter bumps - // to 2 only if the resize handler is installed. - try hs.resize(30, 100); - try hs.expectWaitFor("resizes=2", 8000, "SIGWINCH was not turned into a resize (counter stuck at 1)"); - - // 2b. RECLAIMED GAP + HOVER HANDLE: no dead gap column between panes now — the - // resize handle is the LEFT pane's last column, and hovering it overlays a - // dash (keeping the cell's colors). 2 equal columns at 100 wide -> left width - // 50, last col = 49 (SGR 50). A no-button motion there paints the "╎". - try hs.send("\x1b[<35;50;10M"); // motion (no button) over the left pane's last col - try hs.pump(400); - try hs.expectContains("╎", "hovering the column boundary did not overlay the dashed resize handle"); - - // 3. Default is NORMAL mode: the tag shows "nm" (helix-style block cursor). - try hs.expectContains("nm /", "default tag is not 'nm' (normal mode not default)"); - - // 3b. ls-on-open: a fresh shell greets with its directory listing. The e2e app - // boots in the project dir, so a known file shows up as command output - // (visible even in normal mode — only prompt rows are hidden). - try hs.expectWaitFor(ls_ready, 8000, "new terminals did not auto-ls their directory on open"); - - // 3c. Normal-mode TAB = acme middle-click "execute" the word under the cursor. - // Drop a one-line executable, echo its path onto body row 0 (clear homes the - // cursor first), left-click the path to enter normal + pin the cursor on it, - // then TAB. It runs the word in place (no new pane) -> the script's marker. - try hs.send("\x02"); // normal -> tty (Ctrl-b) - try hs.pump(600); - try hs.send("printf '#!/bin/sh\\necho ZZTABRAN\\n' > /tmp/zztabcmd; chmod +x /tmp/zztabcmd; clear; echo /tmp/zztabcmd\r"); - try hs.pump(1200); - try hs.send("\x02"); // tty -> normal (Ctrl-b): the mouse no longer exits tty mode - try hs.pump(600); - try hs.send("\x1b[<0;8;3M\x1b[<0;8;3m"); // left-click the path on body row 0 -> pin the cursor on it (+1 row: global tagbar) - try hs.pump(600); - try hs.send("\t"); // TAB: execute "/tmp/zztabcmd" - try hs.expectWaitFor("ZZTABRAN", 8000, "normal-mode Tab did not execute the word under the cursor"); - try hs.pump(400); - - // 3c-bis. CENTRAL DISPATCH: a builtin executes from a PANE BODY, not only the - // tagbar. Echo "Debug" as output (visible in normal mode), click it, TAB it: - // the same .debug action runs through the one dispatcher and toggles the - // overlay OFF (step 0 turned it on) -> "resizes=" disappears. Proves onSelect - // matches builtins wherever the word appears (tty here, not the tagbar). - try hs.send("\x02"); // normal -> tty - try hs.pump(400); - try hs.send("clear; echo Debug\r"); - try hs.pump(900); - try hs.send("\x02"); // tty -> normal - try hs.pump(400); - try hs.send("\x1b[<0;5;3M\x1b[<0;5;3m"); // left-click "Debug" on body row 0 (col 3=body 0; GUTTER 2) - try hs.pump(400); - try hs.send("\t"); // TAB: execute the builtin "Debug" - try hs.pump(600); - try hs.expectNotContains("resizes=", "executing 'Debug' from a pane body did not toggle the overlay (central dispatch broken)"); - - // 3d. OUTPUT PADDING: a command composed (typed as an overlay) far below the - // prompt runs BELOW where it was typed, not back up at the prompt. The app - // pads the shell down with newlines to max(last-edit row, prompt row); the - // padding prompts are hidden in text mode, so the output flows under the - // composed line. Clear the pane (prompt -> row 0), type a command on a low - // row whose OUTPUT marker (PAD13Z) differs from its source (echo PAD...), - // run it, and assert the output row is BELOW the typed row. (Screen is 30x100 - // from step 2; terms[0] is the ~15-row top-left pane.) - try hs.send("\x02"); // normal -> tty - try hs.pump(400); - try hs.send("clear\r"); // prompt back to row 0 - try hs.pump(900); - try hs.send("\x02"); // tty -> normal - try hs.pump(400); - try hs.send("\x1b[<0;5;11M\x1b[<0;5;11m"); // left-click a low blank row -> normal + pin (+1 row: global tagbar) - try hs.pump(400); - try hs.send("i"); // insert - try hs.pump(400); - try hs.send("echo PAD$((6+7))Z"); // overlay; output is "PAD13Z" - try hs.pump(600); - try hs.send("\x1b"); // -> normal - try hs.pump(400); - try hs.send("\x1b[<1;5;11M\x1b[<33;21;11M\x1b[<1;21;11m"); // middle-drag-select the line -> run (padded) - try hs.expectWaitFor("PAD13Z", 8000, "padded command did not run"); - try hs.pump(600); - { - const text = try hs.screenText(); - defer gpa.free(text); - const typed = rowOf(text, "echo PAD") orelse { - hs.dump("composed line vanished before the padding check"); - return error.ExpectFailed; - }; - const outp = rowOf(text, "PAD13Z") orelse { - hs.dump("padded output not found on screen"); - return error.ExpectFailed; - }; - if (outp <= typed) { - hs.dump("output did not land below the composed command (padding failed)"); - return error.ExpectFailed; - } - // and it should land RIGHT under the composed line (tight alignment), not - // somewhere far below — the hidden padding prompts fill the gap above it. - if (outp - typed > 2) { - hs.dump("padded output landed too far below the composed line (misaligned)"); - return error.ExpectFailed; - } - } - // tidy up: drop the lingering overlay so it doesn't ride the scrollback into - // later steps (cursor is on the typed row; x selects it, d clears the run). - try hs.send("x"); - try hs.pump(300); - try hs.send("d"); - try hs.pump(300); - - // 4. Ctrl-b toggles raw TTY mode from normal and forwards keystrokes to the - // shell. tty is a hidden toggle (Ctrl-b), off the normal editing workflow. - const marker = "PARDES_E2E"; - try hs.send("\x02"); // normal -> tty (Ctrl-b) - try hs.pump(600); - try hs.send("echo " ++ marker ++ "\r"); - try hs.expectWaitFor(marker, 8000, "tty mode did not forward the marker to the shell"); - try hs.expectContains("sy /", "no 'sy' indicator after Ctrl-b (tty mode)"); - - // 4a. The mouse stays in tty mode: a body left-click (press+release) must NOT - // drop to normal — only Ctrl-b exits tty now. The tag stays "sy". - try hs.send("\x1b[<0;6;4M\x1b[<0;6;4m"); // left-click + release in the body - try hs.pump(500); - try hs.expectContains("sy /", "a left-click in tty mode wrongly returned to normal"); - - // 4b. Ctrl-b toggles back: tty -> normal. Send Ctrl-b again and confirm the - // tag flips off "sy" (raw tty) back to "nm" (normal). - try hs.send("\x02"); // tty -> normal (Ctrl-b) - try hs.pump(600); - try hs.expectContains("nm /", "Ctrl-b did not toggle tty back to normal"); - try hs.send("\x02"); // normal -> tty again for the prompt-hiding step - try hs.pump(600); - - // 5. Prompt hiding in TEXT mode. tty shows the real terminal (command line + - // output). BOTH normal and insert hide the prompt/command rows (the acme - // compose surface) while keeping output. Run a command in tty, Ctrl-b to - // NORMAL, and check: output present, the command's prompt row hidden. - try hs.send("echo CMDHIDEZZ\r"); - try hs.expectWaitFor("CMDHIDEZZ", 8000, "tty mode did not run the command"); - try hs.pump(800); // let tty-mode frames settle - try hs.send("\x02"); // tty -> normal (Ctrl-b): the mouse no longer exits tty mode - try hs.pump(600); - try hs.send("\x1b[<0;6;4M\x1b[<0;6;4m"); // left-click body to pin the cursor (already normal) (+1 row: global tagbar) - try hs.pump(600); - try hs.expectContains("CMDHIDEZZ", "command output not shown in normal mode"); - // The prompt row (with the typed command) is hidden in normal mode now too. We - // check "echo CMDHIDEZZ" (this pane's prompt row) rather than a shell prompt (e.g. "user@host") since a - // raw-tty pane elsewhere could show its own prompt. - try hs.expectNotContains("echo CMDHIDEZZ", "command line still visible in normal mode (prompt row not hidden)"); - - // 6. Insert-mode editing + middle-click execute. Click a blank row to pin the - // cursor at text col 0, type a computed echo, then middle-select those 15 - // cells and release. "Q7Z" appears only if the SELECTED typed text ran — - // proving selection reads the rendered screen. - try hs.send("\x1b[<0;3;15M\x1b[<0;3;15m"); // click blank row, col 0 -> normal + pin (+1 row: global tagbar) - try hs.pump(400); - try hs.send("i"); // normal -> insert so typing composes the command - try hs.pump(400); - try hs.send("echo Q$((3+4))Z"); - try hs.pump(600); // let the run build - try hs.send("\x1b[<1;3;15M\x1b[<33;17;15M\x1b[<1;17;15m"); // middle-select it (+1 row: global tagbar) - try hs.expectWaitFor("Q7Z", 8000, "middle-click did not execute the selected typed text"); - // leave insert -> normal so the next step can reach tty via Ctrl-b - try hs.send("\x1b"); - try hs.pump(400); - - // 6b. FOLLOW OUTPUT: new tty output scrolls the pane to the bottom, so a result - // shows even after you've scrolled up. Fill the pane, wheel UP off the - // bottom (LN40 disappears), then run a command — the pane snaps back and the - // new marker is visible. Done in tty mode so the active pane follows too. - try hs.send("\x02"); // normal -> tty - try hs.pump(400); - try hs.send("clear; for i in $(seq 1 40); do echo LN$i; done\r"); - try hs.expectWaitFor("LN40", 8000, "filler output never appeared"); - try hs.pump(500); - { - var w: usize = 0; - while (w < 30) : (w += 1) try hs.send("\x1b[<64;5;6M"); // wheel UP over the pane (+1 row: global tagbar) - } - try hs.pump(500); - try hs.expectNotContains("LN40", "wheel-up did not scroll the pane off the bottom"); - try hs.send("echo ZZFOLLOW\r"); - try hs.expectWaitFor("ZZFOLLOW", 8000, "new output did not scroll the pane back to the bottom"); - try hs.send("\x02"); // tty -> normal (step 7 re-enters via Ctrl-b) - try hs.pump(400); - - // 6c. NORMAL-MODE MOTION SCROLLS THE TTY like a file pane: the pane is at the - // bottom (ZZFOLLOW visible). `gg` rides the cursor to the top of the - // scrollback and the viewport follows, so the bottom marker scrolls off; - // `G` rides it back down and the marker returns. - try hs.send("gg"); - try hs.pump(500); - try hs.expectNotContains("ZZFOLLOW", "normal-mode gg did not scroll the tty up off the bottom"); - try hs.send("G"); - try hs.pump(500); - try hs.expectContains("ZZFOLLOW", "normal-mode G did not scroll the tty back to the bottom"); - - // 6d. YANK MIRRORS TO THE SYSTEM CLIPBOARD (OSC 52): select the line with `x` - // and yank with `y`; the app writes an OSC 52 clipboard-copy to the host. - // (The paste direction needs a host that answers OSC 52 reads, which this - // emulator can't, so it isn't covered here.) - try hs.send("xy"); - try hs.pump(400); - try hs.expectRawContains("\x1b]52;c;", "yank did not emit an OSC 52 system-clipboard copy"); - - // 7. Right-click a directory opens (acme "look") a terminal there and ls's - // it. Make a dir with a unique entry, then `clear` and echo its path so the - // path lands as contiguous output on body row 0. Right-click (no drag) on - // it: the new terminal lists the dir, so the unique entry appears only if - // the directory was actually opened. - try hs.send("\x02"); // normal -> tty (Ctrl-b) - try hs.pump(700); - try hs.send("rm -rf /tmp/pardesE2E; mkdir -p /tmp/pardesE2E/ZZUNIQDIR; clear; echo /tmp/pardesE2E\r"); - try hs.pump(2000); // let it run + settle - try hs.send("\x1b[<2;5;3M\x1b[<2;5;3m"); // right-click the path (+1 row: global tagbar) - try hs.expectWaitFor("ZZUNIQDIR", 20000, "right-click on a directory did not open+ls it"); - - // 8. Right-click a "path:line" opens a line-numbered FILE pane SCROLLED to - // that line. Build a tall file whose deep marker (line 150) is only - // reachable by scrolling, type the path:line on a blank row of the right - // column, then right-click it. The file pane opens, scrolls to line 150 -> - // the marker shows. - { - var fbuf: std.ArrayList(u8) = .empty; - defer fbuf.deinit(gpa); - var ln: usize = 1; - while (ln <= 200) : (ln += 1) - try fbuf.appendSlice(gpa, if (ln == 150) "ZZDEEPMARK\n" else "filler\n"); - try h.writeFile("/tmp/pardesE2Efile.txt", fbuf.items); - } - try hs.resize(30, 100); // clean geometry (right column starts ~col 51) - try hs.pump(700); // settle the reflow - try clearRightColumnTerminal(&hs); - try hs.send("\x1b[<0;60;26M\x1b[<0;60;26m"); // left-click a blank row well below the ls output, right column -> normal + pin (+1 row: global tagbar) - try hs.pump(400); - try hs.send("i"); // normal -> insert so typing composes the path:line - try hs.pump(400); - try hs.send("/tmp/pardesE2Efile.txt:150"); - try hs.pump(800); // build the run - try hs.send("\x1b[<2;70;26M\x1b[<2;70;26m"); // right-click on the typed path (+1 row: global tagbar) - try hs.expectWaitFor("ZZDEEPMARK", 20000, "right-click 'path:150' did not open the file scrolled to line 150"); - try hs.expectContains("Save Del", "file pane tag did not include the default Save command"); - try hs.expectNotContains("Delcol", "Delcol is still shown in the default pane tag"); - - // 9. Modal modes on the active FILE pane (left open & focused by step 8): - // `i` -> INSERT (tag shows "in"), Esc -> NORMAL ("nm"). Validates the - // 3-mode model end to end via the tag indicator. No content changes here. - try hs.send("i"); // normal -> insert - try hs.pump(600); - try hs.expectContains("in /", "no 'in' indicator after i (insert mode)"); - try hs.send("\x1b"); // insert -> normal - try hs.pump(600); - try hs.expectContains("nm /", "no 'nm' indicator after Esc (back to normal)"); - - // 10. Helix-style file editing on that same file pane (still focused, normal, - // cursor on the ZZDEEPMARK line). `o` opens a line below and enters insert; - // type a unique marker, Esc to normal, then `x` (select line) + `d` (delete - // selection) removes it. Assert the marker appears then is gone, while the - // surrounding file content survives — proving real content mutation. - try hs.send("o"); // open line below -> insert - try hs.pump(600); - try hs.send("ZZMODALNEW"); - try hs.pump(600); - try hs.send("\x1b"); // back to normal - try hs.pump(600); - try hs.expectContains("ZZMODALNEW", "'o' did not open a line / typed text not shown"); - // cursor sits on the ZZMODALNEW line: `x` selects it, `d` deletes it. - try hs.send("x"); - try hs.pump(400); - try hs.send("d"); - try hs.pump(800); - try hs.expectNotContains("ZZMODALNEW", "ZZMODALNEW still present after x+d (line delete failed)"); - // sanity: the deep marker (line 150, above the opened/deleted line) survives. - try hs.expectContains("ZZDEEPMARK", "ZZDEEPMARK vanished — file pane content was clobbered"); - - // 10b. Char-wise select (`v`) + delete, then undo/redo. Open a line, type a - // marker, `0` to the line start, `v` to start a char selection, `$` to - // extend to the line end, `d` to delete the range (gone), `u` to undo it - // (back), `U` to redo (gone again). Proves v-select, real char-range - // deletion, and the dumb undo/redo stack on a file pane. - try hs.send("o"); // open line below -> insert - try hs.pump(500); - try hs.send("ZZVSELMARK"); - try hs.pump(500); - try hs.send("\x1b"); // -> normal, cursor on the new line - try hs.pump(500); - try hs.expectContains("ZZVSELMARK", "char-select setup: typed marker not shown"); - try hs.send("0"); // line start - try hs.pump(300); - try hs.send("v"); // start char selection - try hs.pump(300); - try hs.send("$"); // extend the selection to the line end - try hs.pump(300); - try hs.send("d"); // delete the selected char range - try hs.pump(800); - try hs.expectNotContains("ZZVSELMARK", "v + $ + d did not delete the char selection"); - try hs.send("u"); // undo the delete - try hs.pump(800); - try hs.expectContains("ZZVSELMARK", "u (undo) did not restore the deleted text"); - try hs.send("U"); // redo the delete - try hs.pump(800); - try hs.expectNotContains("ZZVSELMARK", "U (redo) did not re-apply the delete"); - - // 10c. Save writes the current file pane back to disk. Add a durable marker, - // middle-click the default "Save" command in the file tag, then read the - // real file from /tmp and confirm the edit landed there. - try hs.send("o"); // open a new line below -> insert - try hs.pump(500); - try hs.send("ZZSAVEDISK"); - try hs.pump(500); - try hs.send("\x1b"); // -> normal - try hs.pump(500); - try hs.expectContains("ZZSAVEDISK", "save setup: marker not visible in the file pane"); - try hs.send("\x1b[<1;31;2M\x1b[<1;31;2m"); // middle-click "Save" in the file tag - try hs.pump(1000); - const saved = try std.Io.Dir.cwd().readFileAlloc(io, "/tmp/pardesE2Efile.txt", gpa, .limited(1 << 20)); - defer gpa.free(saved); - if (std.mem.indexOf(u8, saved, "ZZSAVEDISK") == null) { - hs.dump("Save command did not write the edited file content to disk"); - return error.SaveDidNotWriteDisk; - } - - // 10d. Focused save regression: saving a shorter edited buffer must replace the - // file exactly, not merely leave a marker somewhere on disk. This catches - // missed Save dispatch and non-truncating writes. - { - var hs_save = try Harness.init(gpa, exe.ptr, 24, 80); - hs_save.trace = config.trace; - defer hs_save.deinit(); - try h.writeFile("/tmp/pardesE2Esave.txt", "ZZORIG_SAVE_SHOULD_DISAPPEAR_AND_TAIL\n"); - try hs_save.expectWaitFor(ls_ready, 8000, "save regression app never auto-ls'd"); - try hs_save.resize(30, 100); - try hs_save.pump(700); - try clearRightColumnTerminal(&hs_save); - try hs_save.send("\x1b[<0;60;20M\x1b[<0;60;20m"); // pin a blank row, right column -> normal - try hs_save.pump(400); - try hs_save.send("i"); - try hs_save.pump(300); - try hs_save.send("/tmp/pardesE2Esave.txt"); - try hs_save.pump(500); - try hs_save.send("\x1b"); // -> normal - try hs_save.pump(400); - try hs_save.send("\x1b[<2;70;20M\x1b[<2;70;20m"); // right-click the path -> open file - try hs_save.expectWaitFor("ZZORIG_SAVE_SHOULD_DISAPPEAR", 20000, "save regression file did not open"); - try hs_save.send("x"); // select the original line - try hs_save.pump(300); - try hs_save.send("c"); // clear it and enter insert - try hs_save.pump(300); - try hs_save.send("ZZSAVED_EXACT"); - try hs_save.pump(500); - try hs_save.send("\x1b"); // -> normal - try hs_save.pump(500); - try hs_save.expectContains("ZZSAVED_EXACT", "save regression edit is not visible before saving"); - try hs_save.send("\x1b[<1;31;2M\x1b[<1;31;2m"); // middle-click "Save" in the file tag - try hs_save.pump(1000); - const exact = try std.Io.Dir.cwd().readFileAlloc(io, "/tmp/pardesE2Esave.txt", gpa, .limited(1 << 20)); - defer gpa.free(exact); - if (!std.mem.eql(u8, exact, "ZZSAVED_EXACT\n")) { - hs_save.dump("Save command did not replace the file with the exact edited bytes"); - std.debug.print("saved file bytes: {any}\n", .{exact}); - return error.SaveDidNotWriteExactDisk; - } - } - - // 11. The Tutor builtin now lives in the GLOBAL tagbar (top row, full width): - // "Kill Newcol Tutor Debug". Clicking "Tutor" there spawns the tutor as a - // file pane. The tutor is embedded in the binary (@embedFile), so its - // content (the PARDES TUTOR header + the acme mouse intro) appears. "Tutor" - // sits at cols 12-16 (0-based) -> SGR col ~14, the tagbar is screen row 1. - try hs.send("\x1b[<1;14;1M\x1b[<1;14;1m"); // middle-click "Tutor" in the global tagbar - try hs.expectWaitFor("PARDES TUTOR", 10000, "global-tagbar 'Tutor' did not spawn the tutor pane"); - try hs.expectContains("tmux + vi + acme", "tutor pane content is wrong (no tagline)"); - - // Tell the app to quit (ctrl-c is the app's own exit key). - try hs.send("\x03"); - - // 12. The --tty flag: start tmux-style as a single shell pane already in tty - // mode. Confirm the (only) pane's tag shows "sy" (tty) and that keystrokes - // go straight to the shell. A fresh app instance with the flag. - { - var hs2 = try Harness.initArgs(gpa, exe.ptr, 24, 80, "--tty"); - defer hs2.deinit(); - try hs2.expectWaitFor("sy /", 6000, "--tty did not start in tty mode (no 'sy' tag)"); - try hs2.send("echo ZZTTYFLAG\r"); - try hs2.expectWaitFor("ZZTTYFLAG", 8000, "--tty single shell did not forward input to the shell"); - } - - // 12b. -h/--help print usage and exit before entering the TUI. - { - var hsh = try Harness.initArgs(gpa, exe.ptr, 12, 80, "-h"); - defer hsh.deinit(); - try hsh.expectWaitFor("Usage: pardes", 4000, "-h did not print usage"); - try hsh.expectContains("--tty-toggle", "-h help did not mention --tty-toggle"); - } - { - var hsh2 = try Harness.initArgs(gpa, exe.ptr, 12, 80, "--help"); - defer hsh2.deinit(); - try hsh2.expectWaitFor("Usage: pardes", 4000, "--help did not print usage"); - } - - // 12c. The tty toggle can be moved off Ctrl-b. With --tty-toggle=g, Ctrl-b - // should no longer enter tty; Ctrl-g should. - { - var hsg = try Harness.initArgs(gpa, exe.ptr, 24, 80, "--tty-toggle=g"); - defer hsg.deinit(); - try hsg.expectWaitFor("nm /", 6000, "--tty-toggle=g app did not start in normal mode"); - try hsg.send("\x02"); // Ctrl-b: now just a normal-mode key, not the tty toggle - try hsg.pump(600); - try hsg.expectNotContains("sy /", "Ctrl-b still toggled tty after --tty-toggle=g"); - try hsg.send("\x07"); // Ctrl-g: configured tty toggle - try hsg.expectWaitFor("sy /", 6000, "Ctrl-g did not toggle tty after --tty-toggle=g"); - try hsg.send("\x07"); - try hsg.expectWaitFor("nm /", 6000, "Ctrl-g did not toggle tty back to normal"); - } - - // 13. Terminal char-mode editing end to end (fresh instance for a clean pane): - // insert an overlay run, backspace part of it, Ctrl-b to tty (overlay hidden) - // and back (overlay returns), then u (undo the whole insert) / U (redo). - { - var hs3 = try Harness.init(gpa, exe.ptr, 24, 80); - defer hs3.deinit(); - try hs3.expectWaitFor(ls_ready, 8000, "fresh terminal never auto-ls'd"); - // pin the cursor on the top-left pane and type a marker (insert mode). - try hs3.send("\x1b[<0;5;8M\x1b[<0;5;8m"); // left-click -> normal + pin (+1 row: global tagbar) - try hs3.pump(400); - try hs3.send("i"); - try hs3.pump(300); - try hs3.send("ZZTERMINSXYZ"); - try hs3.pump(500); - try hs3.expectContains("ZZTERMINSXYZ", "terminal insert did not show the typed overlay"); - // insert-mode backspace erases within the run: drop the trailing "XYZ". - try hs3.send("\x7f\x7f\x7f"); - try hs3.pump(500); - try hs3.expectNotContains("ZZTERMINSXYZ", "terminal insert-mode backspace did not erase typed text"); - try hs3.expectContains("ZZTERMINS", "backspace erased too much of the run"); - try hs3.send("\x1b"); // insert -> normal - try hs3.pump(400); - try hs3.send("\x02"); // normal -> tty: the overlay is hidden - try hs3.pump(700); - try hs3.expectNotContains("ZZTERMINS", "tty mode did not hide the inserted-text overlay"); - try hs3.send("\x02"); // tty -> normal: the overlay returns - try hs3.pump(700); - try hs3.expectContains("ZZTERMINS", "overlay did not return after leaving tty mode"); - try hs3.send("u"); // undo the whole insert session - try hs3.pump(600); - try hs3.expectNotContains("ZZTERMINS", "u (undo) did not remove the terminal insertion"); - try hs3.send("U"); // redo - try hs3.pump(600); - try hs3.expectContains("ZZTERMINS", "U (redo) did not restore the terminal insertion"); - } - - // 14. Normal-mode Tab/Enter act on the SELECTION when one is active (not the - // word under the cursor). Clear a fresh pane, type a computed command as an - // overlay, `x` to select the whole line, Tab -> the SELECTION runs. The - // output marker (ZZSEL5X) differs from the source ($((2+3))) so it only - // matches if the selection actually executed. - { - var hs4 = try Harness.init(gpa, exe.ptr, 24, 80); - defer hs4.deinit(); - try hs4.expectWaitFor(ls_ready, 8000, "fresh terminal never auto-ls'd"); - try hs4.send("\x02"); // normal -> tty - try hs4.pump(500); - try hs4.send("clear\r"); // blank the pane, prompt to row 0 - try hs4.pump(900); - try hs4.send("\x02"); // tty -> normal - try hs4.pump(400); - try hs4.send("\x1b[<0;2;6M\x1b[<0;2;6m"); // pin a blank low row near col 0 - try hs4.pump(400); - try hs4.send("i"); - try hs4.pump(300); - try hs4.send("echo ZZSEL$((2+3))X"); // overlay; output is "ZZSEL5X" - try hs4.pump(500); - try hs4.send("\x1b"); // -> normal - try hs4.pump(400); - try hs4.send("x"); // select the whole typed line - try hs4.pump(400); - try hs4.send("\t"); // Tab: execute the SELECTION (not the word under cursor) - try hs4.expectWaitFor("ZZSEL5X", 8000, "Tab on a selection did not execute the selected text"); - } - - // 15. Image panes open even without kitty graphics, rendering via the PETSCII - // fallback, and carry a "Petscii" toggle in their tagline. Write a small PPM, - // open it with an acme right-click, confirm the image pane + toggle appear, - // and pump frames (smoke-testing the glyph draw doesn't crash). - { - var hs5 = try Harness.init(gpa, exe.ptr, 24, 80); - defer hs5.deinit(); - try hs5.expectWaitFor(ls_ready, 8000, "fresh terminal never auto-ls'd"); - { - var ppm: std.ArrayList(u8) = .empty; - defer ppm.deinit(gpa); - try ppm.appendSlice(gpa, "P6\n64 64\n255\n"); // diagonal split -> per-cell glyphs - var y: usize = 0; - while (y < 64) : (y += 1) { - var x: usize = 0; - while (x < 64) : (x += 1) - try ppm.appendSlice(gpa, if (x + y < 64) &[_]u8{ 255, 255, 255 } else &[_]u8{ 0, 0, 255 }); - } - try h.writeFile("/tmp/pardesE2Epet.ppm", ppm.items); - } - try hs5.send("\x02"); // normal -> tty - try hs5.pump(500); - try hs5.send("clear\r"); // blank the pane - try hs5.pump(900); - try hs5.send("\x02"); // tty -> normal - try hs5.pump(400); - try hs5.send("\x1b[<0;5;8M\x1b[<0;5;8m"); // pin a blank row near col 0 - try hs5.pump(400); - try hs5.send("i"); - try hs5.pump(300); - try hs5.send("/tmp/pardesE2Epet.ppm"); - try hs5.pump(500); - try hs5.send("\x1b"); // -> normal - try hs5.pump(400); - try hs5.send("\x1b[<2;10;8M\x1b[<2;10;8m"); // right-click the typed path -> open image pane - try hs5.expectWaitFor("Petscii", 16000, "right-click on an image path did not open an image pane with the Petscii toggle"); - try hs5.pump(800); // let the petscii grid render across frames - // the diagonal split renders as real block glyphs — assert the full block - // shows (a blank body here would mean the grid didn't blit, e.g. a dangling - // grapheme slice into a stack-local cell). - try hs5.expectContains("█", "petscii image body is blank — no block glyphs rendered"); - } - - // 16. Opening a SECOND file SPLITS the existing file pane (acme-style minimal - // reflow) instead of stacking at the top of the column: the new file lands - // directly BELOW the source file, so A is above B. The OLD stack-at-top - // behavior would put B ABOVE A — so asserting rowOf(A) < rowOf(B) is a - // regression test for the split. - { - var hs6 = try Harness.init(gpa, exe.ptr, 24, 80); - defer hs6.deinit(); - try hs6.expectWaitFor(ls_ready, 8000, "fresh terminal never auto-ls'd"); - try h.writeFile("/tmp/pardesE2Efa.txt", "ZZFILEA\nfa1\nfa2\nfa3\nfa4\nfa5\nfa6\nfa7\nfa8\nfa9\n"); - try h.writeFile("/tmp/pardesE2Efb.txt", "ZZFILEB\nfb1\nfb2\nfb3\nfb4\nfb5\nfb6\nfb7\nfb8\nfb9\n"); - try hs6.resize(30, 100); // clean geometry: right column starts ~col 51 - try hs6.pump(700); - try clearRightColumnTerminal(&hs6); - // open file A from the right-column terminal: the FIRST doc pane, so it - // takes the stack-at-top fallback (no existing doc pane to split). - try hs6.send("\x1b[<0;60;20M\x1b[<0;60;20m"); // pin a blank row, right column -> normal - try hs6.pump(400); - try hs6.send("i"); - try hs6.pump(300); - try hs6.send("/tmp/pardesE2Efa.txt"); - try hs6.pump(500); - try hs6.send("\x1b"); // -> normal - try hs6.pump(400); - try hs6.send("\x1b[<2;70;20M\x1b[<2;70;20m"); // right-click the path -> open file A - try hs6.expectWaitFor("ZZFILEA", 20000, "right-click did not open file A"); - // open file B likewise: a doc pane now exists, so B SPLITS it (lands below A). - try hs6.send("\x1b[<0;60;22M\x1b[<0;60;22m"); // pin another blank row, right column - try hs6.pump(400); - try hs6.send("i"); - try hs6.pump(300); - try hs6.send("/tmp/pardesE2Efb.txt"); - try hs6.pump(500); - try hs6.send("\x1b"); - try hs6.pump(400); - try hs6.send("\x1b[<2;70;22M\x1b[<2;70;22m"); // right-click the path -> open file B - try hs6.expectWaitFor("ZZFILEB", 20000, "right-click did not open file B"); - // both markers sit on each file's TOP line (scroll 0). The split puts B - // under A -> A's row < B's row. Stack-at-top would invert this. - const text = try hs6.screenText(); - defer gpa.free(text); - const ra = rowOf(text, "ZZFILEA") orelse return error.NoFileAOnScreen; - const rb = rowOf(text, "ZZFILEB") orelse return error.NoFileBOnScreen; - if (!(ra < rb)) { - hs6.dump("second file did not split BELOW the first (B not under A)"); - return error.FileSplitOrderWrong; - } - } - - // 17. Deleting a pane that EMPTIES its column hands the column's width to a - // neighbor, so the OTHER columns keep their widths (only the neighbor - // grows). Regression for the extra horizontal movement: without the - // col-weight handoff, wsum drops and every surviving column reflows - // sideways. Build 3 columns, put a marker in the rightmost, delete the - // MIDDLE column's shell (exit), and assert the marker's screen column is - // unchanged (the middle column's width flowed LEFT, not to the right one). - { - var hs7 = try Harness.init(gpa, exe.ptr, 24, 80); - defer hs7.deinit(); - try hs7.expectWaitFor(ls_ready, 8000, "fresh terminal never auto-ls'd"); - try hs7.resize(30, 120); // 3 columns of ~40 each - try hs7.pump(700); - // add a 3rd column via the global-tagbar "Newcol" (cols 5-10, 0-based). - try hs7.send("\x1b[<1;8;1M\x1b[<1;8;1m"); // middle-click "Newcol" - try hs7.pump(2500); // let the new shell come up - // mark the rightmost column (now the active pane) with a unique token. - try hs7.send("\x02"); // normal -> tty - try hs7.pump(500); - try hs7.send("clear\r"); - try hs7.pump(800); - // split the token so it appears ONLY in the OUTPUT (at the pane's left edge), - // never in the command text — else colOf would match the prompt-relative echo. - try hs7.send("echo ZZFAR''COL\r"); - try hs7.expectWaitFor("ZZFARCOL", 12000, "new column's shell did not echo the marker"); - const before = try hs7.screenText(); - const x1 = colOf(before, "ZZFARCOL") orelse { - gpa.free(before); - return error.NoFarColBefore; - }; - gpa.free(before); - // focus the MIDDLE column (x 40..80) and exit its shell -> the column - // empties and collapses. pty_eof deletes the pane and reflows. - try hs7.send("\x1b[<0;58;12M\x1b[<0;58;12m"); // left-click inside the middle column - try hs7.pump(500); - try hs7.send("\x02"); // -> tty - try hs7.pump(400); - try hs7.send("exit\r"); // shell EOF -> pane removed -> middle column collapses - try hs7.pump(2000); // let pty_eof + the reflow settle - try hs7.expectContains("ZZFARCOL", "marker vanished after collapsing the middle column"); - const after = try hs7.screenText(); - defer gpa.free(after); - const x2 = colOf(after, "ZZFARCOL") orelse return error.NoFarColAfter; - // with the width handoff the rightmost column stays put (x2 ~= x1); without - // it, every column reflows and x2 shrinks by ~one column width. - if (x2 + 2 < x1) { - hs7.dump("rightmost column moved when the middle column collapsed (no width handoff)"); - std.debug.print("ZZFARCOL col: before={d} after={d}\n", .{ x1, x2 }); - return error.ColumnCollapseReflow; - } - } - - // 18. DEVICE-QUERY REPLIES: full-screen apps (nushell/reedline, helix, fzf — all - // crossterm) send a DSR cursor-position report (ESC[6n) at startup and BLOCK - // until the terminal answers. pardes must reply into the pty; without the - // write_pty effect the reply is dropped and the app spins forever ("nushell - // never opens"). Mode-independent. Run a command that (in raw mode, like a - // real app) emits ESC[6n and only echoes its marker AFTER it reads the reply - // — the marker appears only if pardes answered the query. - { - var hs8 = try Harness.init(gpa, exe.ptr, 24, 80); - defer hs8.deinit(); - try hs8.expectWaitFor(ls_ready, 8000, "fresh terminal never auto-ls'd"); - try hs8.send("\x02"); // normal -> tty - try hs8.pump(600); - // -icanon so the reply's bytes reach `read` without a newline; read until the - // reply terminator 'R' (ESC[<row>;<col>R) with a timeout, and print the marker - // ONLY if the read SUCCEEDS (a reply arrived). The marker is split with '' - // so it appears solely in the command's OUTPUT, never in the echoed command - // line — otherwise the on-screen command text would false-match it. - try hs8.send("stty -icanon -echo min 1 time 0; printf '\\033[6n'; read -r -d R -t 3 _c && printf 'ZZCPR''OK\\n'; stty sane\r"); - try hs8.expectWaitFor("ZZCPROK", 8000, "app blocked on its ESC[6n query — pardes dropped the reply (device queries not answered)"); - } - - // 19. PRIMARY-DA TERMINATOR (helix regression). crossterm probes capabilities with - // `ESC[?u ESC[c` (kitty-keyboard query + primary DA) and waits on the DA reply - // `ESC[?...c` as the TERMINATOR of the whole probe. Answering ESC[6n alone (test - // 18) is not enough: without primary DA, helix enters its alt screen and then - // hangs on a BLANK pane. Send the real probe, read until the DA reply's 'c', - // and print the split marker only on success. - { - var hs9 = try Harness.init(gpa, exe.ptr, 24, 80); - defer hs9.deinit(); - try hs9.expectWaitFor(ls_ready, 8000, "fresh terminal never auto-ls'd"); - try hs9.send("\x02"); // normal -> tty - try hs9.pump(600); - // read until 'c' (primary DA ends in 'c'); the kitty reply ESC[?0u has no 'c', - // so the read only completes once primary DA is answered. - try hs9.send("stty -icanon -echo min 1 time 0; printf '\\033[?u\\033[c'; read -r -d c -t 3 _r && printf 'ZZDA''OK\\n'; stty sane\r"); - try hs9.expectWaitFor("ZZDAOK", 8000, "app blocked on its ESC[c device-attributes probe — pardes never answered primary DA (helix hangs on a blank pane)"); - } - - // 20. KEPT LEFT SELECTION + MIDDLE+LEFT CHORD. A left-drag selection now PERSISTS - // after release (it clears only on a plain left click or a new left drag). The - // middle+left chord: press middle (on a command word), press left BEFORE - // releasing middle -> the KEPT left selection's text is passed as the command's - // argument. Here a script echoes CHORDARG:"$1"; left-select ZZCHORDARG, then - // chord the script -> output "CHORDARG:ZZCHORDARG" only if the kept selection - // fed the arg. Top-left pane body row 0 = SGR row 3; body col 0 = SGR col 3. - { - var hsc = try Harness.init(gpa, exe.ptr, 24, 80); - defer hsc.deinit(); - try hsc.expectWaitFor(ls_ready, 8000, "chord test: fresh terminal never auto-ls'd"); - try h.writeFile("/tmp/pardesE2Echord.sh", "#!/bin/sh\necho CHORDARG:\"$1\"\n"); - try hsc.send("\x02"); // normal -> tty - try hsc.pump(500); - try hsc.send("chmod +x /tmp/pardesE2Echord.sh; clear; printf 'ZZCHORDARG\\n/tmp/pardesE2Echord.sh\\n'\r"); - try hsc.expectWaitFor("/tmp/pardesE2Echord.sh", 8000, "chord test: command path not rendered"); - // left-drag-select "ZZCHORDARG" (10 chars, body cols 0-9 = SGR cols 3-12) on - // body row 0 (SGR row 3) -> kept (.done) after release. - try hsc.send("\x1b[<0;3;3M\x1b[<32;12;3M\x1b[<0;12;3m"); - try hsc.pump(400); - // middle+left chord: middle-press on the script path (body row 1 = SGR row 4), - // left-press (captures the kept selection as the arg), middle-release (runs it). - try hsc.send("\x1b[<1;8;4M\x1b[<0;8;4M\x1b[<1;8;4m"); - try hsc.expectWaitFor("CHORDARG:ZZCHORDARG", 8000, "middle+left chord did not pass the kept left selection as the command argument"); - } - - // 21. A plain LEFT CLICK (no drag) clears the kept left selection, so a later - // middle+left chord passes NO argument. Left-select ZZCLRARG, left-click a - // blank cell (clears), then chord the script -> output "CHORDARG:" (empty), - // with no ZZCLRARG after it. - { - var hscl = try Harness.init(gpa, exe.ptr, 24, 80); - defer hscl.deinit(); - try hscl.expectWaitFor(ls_ready, 8000, "clear test: fresh terminal never auto-ls'd"); - try h.writeFile("/tmp/pardesE2Echord.sh", "#!/bin/sh\necho CHORDARG:\"$1\"\n"); - try hscl.send("\x02"); - try hscl.pump(500); - try hscl.send("chmod +x /tmp/pardesE2Echord.sh; clear; printf 'ZZCLRARG\\n/tmp/pardesE2Echord.sh\\n'\r"); - try hscl.expectWaitFor("/tmp/pardesE2Echord.sh", 8000, "clear test: command path not rendered"); - // left-drag-select "ZZCLRARG" (8 chars, body cols 0-7 = SGR cols 3-10) -> kept. - try hscl.send("\x1b[<0;3;3M\x1b[<32;10;3M\x1b[<0;10;3m"); - try hscl.pump(400); - // plain left click on a blank row (body row 3 = SGR row 6), no drag -> clears. - try hscl.send("\x1b[<0;3;6M\x1b[<0;3;6m"); - try hscl.pump(400); - // chord the script; the kept selection is gone -> no arg -> "CHORDARG:" (empty). - try hscl.send("\x1b[<1;8;4M\x1b[<0;8;4M\x1b[<1;8;4m"); - try hscl.pump(900); - try hscl.expectContains("CHORDARG:", "clear test: chord did not run the script"); - try hscl.expectNotContains("CHORDARG:ZZCLRARG", "a plain left click did not clear the kept left selection (chord still passed it)"); - } - - // 22. A NEW left drag REPLACES the previous kept left selection. Left-select - // ZZTXTONE, then left-select ZZTXTTWO (replaces), then chord the script -> - // output "CHORDARG:ZZTXTTWO" (the NEW selection), NOT "CHORDARG:ZZTXTONE". - { - var hsr = try Harness.init(gpa, exe.ptr, 24, 80); - defer hsr.deinit(); - try hsr.expectWaitFor(ls_ready, 8000, "replace test: fresh terminal never auto-ls'd"); - try h.writeFile("/tmp/pardesE2Echord.sh", "#!/bin/sh\necho CHORDARG:\"$1\"\n"); - try hsr.send("\x02"); - try hsr.pump(500); - try hsr.send("chmod +x /tmp/pardesE2Echord.sh; clear; printf 'ZZTXTONE\\nZZTXTTWO\\n/tmp/pardesE2Echord.sh\\n'\r"); - try hsr.expectWaitFor("/tmp/pardesE2Echord.sh", 8000, "replace test: command path not rendered"); - // left-drag-select "ZZTXTONE" on body row 0 (SGR row 3; 8 chars -> SGR cols 3-10). - try hsr.send("\x1b[<0;3;3M\x1b[<32;10;3M\x1b[<0;10;3m"); - try hsr.pump(400); - // left-drag-select "ZZTXTTWO" on body row 1 (SGR row 4) -> REPLACES the kept one. - try hsr.send("\x1b[<0;3;4M\x1b[<32;10;4M\x1b[<0;10;4m"); - try hsr.pump(400); - // chord the script on body row 2 (SGR row 5) -> arg is the NEW selection ZZTXTTWO. - try hsr.send("\x1b[<1;8;5M\x1b[<0;8;5M\x1b[<1;8;5m"); - try hsr.expectWaitFor("CHORDARG:ZZTXTTWO", 8000, "a new left drag did not replace the old kept selection (chord used the wrong arg)"); - try hsr.expectNotContains("CHORDARG:ZZTXTONE", "the old kept selection leaked into the chord arg (not replaced)"); - } - - // 23. A BUILTIN with a chord arg ignores the arg (each builtin decides; none use - // it yet). Left-select some text, then chord middle-click "Debug" in the body - // -> the Debug overlay toggles on (arg ignored), so "resizes=" appears. - { - var hsb = try Harness.init(gpa, exe.ptr, 24, 80); - defer hsb.deinit(); - try hsb.expectWaitFor(ls_ready, 8000, "builtin-chord test: fresh terminal never auto-ls'd"); - try hsb.send("\x02"); - try hsb.pump(500); - try hsb.send("clear; printf 'ZZBUILTARG\\nDebug\\n'\r"); - try hsb.expectWaitFor("Debug", 8000, "builtin-chord test: 'Debug' word not rendered"); - // left-select "ZZBUILTARG" (10 chars, SGR cols 3-12) on body row 0 -> kept. - try hsb.send("\x1b[<0;3;3M\x1b[<32;12;3M\x1b[<0;12;3m"); - try hsb.pump(400); - // chord middle-click "Debug" on body row 1 (SGR row 4, body col 2 = SGR col 5). - try hsb.send("\x1b[<1;5;4M\x1b[<0;5;4M\x1b[<1;5;4m"); - try hsb.pump(900); - try hsb.expectContains("resizes=", "builtin with a chord arg did not toggle (arg broke builtin dispatch)"); - } -} |
