summaryrefslogtreecommitdiff
path: root/tests.zig
diff options
context:
space:
mode:
Diffstat (limited to 'tests.zig')
-rw-r--r--tests.zig1061
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)");
- }
-}