1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
|
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[<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;
}
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const args = try init.minimal.args.toSlice(init.arena.allocator());
if (args.len != 2) {
std.debug.print("usage: {s} <pardes-exe>\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;
}
}
}
|