const std = @import("std"); const h = @import("e2e_harness.zig"); const Harness = h.Harness; const config = @import("config"); // 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[\n", .{args[0]}); return error.BadArgs; } const exe = args[1]; 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')"); // 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("build.zig", 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 "goblin@" 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", 12000, "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 hs.send("\x1b[<0;60;19M\x1b[<0;60;19m"); // 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;19M\x1b[<2;70;19m"); // right-click on the typed path (+1 row: global tagbar) try hs.expectWaitFor("ZZDEEPMARK", 12000, "right-click 'path:150' did not open the file scrolled to line 150"); // 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"); // 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"); } // 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("build.zig", 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("build.zig", 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("build.zig", 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", 10000, "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("build.zig", 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); // 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", 12000, "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", 12000, "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("build.zig", 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(1600); // 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", 8000, "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; } } }