summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/fs.py99
-rw-r--r--test/fs_namespace.zig12
-rw-r--r--test/gui-goldens.txt8
-rw-r--r--test/macos_e2e.swift2
-rw-r--r--test/output.zig11
-rw-r--r--test/panes.zig53
-rw-r--r--test/selfmount.py33
-rw-r--r--test/snapshot.zig5
-rw-r--r--test/snapshots/argv.golden2
-rw-r--r--test/snapshots/cornerdrag.golden8
-rw-r--r--test/snapshots/filewatch.golden31
-rw-r--r--test/snapshots/filewatch.snap19
-rw-r--r--test/snapshots/jumps.golden4
-rw-r--r--test/snapshots/lookpanes.golden34
-rw-r--r--test/snapshots/nested.golden6
-rw-r--r--test/snapshots/psearch.golden10
-rw-r--r--test/snapshots/psearch.snap8
-rw-r--r--test/snapshots/reflow.golden2
-rw-r--r--test/snapshots/rsearch.snap3
-rw-r--r--test/snapshots/shellset.golden2
-rw-r--r--test/snapshots/shellset.snap12
-rw-r--r--test/snapshots/shrinktag.golden20
-rw-r--r--test/snapshots/shrinktag.snap20
-rw-r--r--test/snapshots/splitparent.golden42
-rw-r--r--test/snapshots/splitparent.snap18
-rw-r--r--test/snapshots/tinywin.golden18
-rw-r--r--test/snapshots/ttyhelp.golden2
-rw-r--r--test/snapshots/ttylook.golden2
-rw-r--r--test/snapshots/ttyonly.golden4
29 files changed, 319 insertions, 171 deletions
diff --git a/test/fs.py b/test/fs.py
index c5199034..89801a71 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -121,6 +121,10 @@ def look(client, path, source=None):
client.write(f'/pane/{serial}/look', f'{path}\n'.encode())
results = [int(row) for row in client.read('/look').split()]
assert len(results) == 1 and results[0] != serial, (path, results)
+ # The scratch it looked from goes: a column holds only so many panes,
+ # each its tag and two rows.
+ if source is None:
+ client.remove(f'/pane/{serial}')
return results[0]
@@ -129,6 +133,16 @@ def execute(client, serial, command):
client.write(f'/pane/{serial}/exec', f'{command}\n'.encode())
+def execute_refused(client, serial, command, words):
+ """A middle click on a builtin that fails: the write fails, saying why."""
+ try:
+ execute(client, serial, command)
+ except OSError as why:
+ assert words in str(why), (command, why)
+ return
+ raise AssertionError(f'{command!r} was taken')
+
+
def walk_tree(client, path='/', skip=('/log', '/os', '/screen')):
"""Stat every entry below `path`, as `find` does, without opening anything."""
seen = []
@@ -148,7 +162,8 @@ def discovery(binary, embedded=False):
top = client.list('/')
# A set: which files the root holds, not the order a listing gives.
assert set(top) - {'src', 'shaders'} == {'README', 'index', 'status', 'focus', 'ctl', 'commands',
- 'look', 'exec', 'log', 'screen', 'listeners', 'pane', 'os'}, top
+ 'look', 'exec', 'log', 'screen', 'listeners', 'layout', 'tag', 'tagexec',
+ 'pane', 'col', 'os'}, top
assert 'self' not in top and 'cons' not in top and 'new' not in top, top
assert ('src' in top) == embedded, (top, embedded)
before = client.read('/index')
@@ -180,7 +195,7 @@ def discovery(binary, embedded=False):
assert client.read('/pane/1/dirty') == b'0\n'
assert client.read('/pane/1/scroll') == b'1\n'
fixture = int(before.split()[0])
- assert before.split()[1:3] == [b'text', b'0'] and before.endswith(b'/discovery.txt\n'), before
+ assert before.split()[1:3] == [b'text', b'0'] and before.rsplit(b' ', 1)[0].endswith(b'/discovery.txt'), before
# /status reports the editor; /exec has nothing to report yet.
status = dict(line.split(maxsplit=1) for line in client.read('/status').decode().splitlines())
assert int(status['pid']) > 0 and status['version'] and int(status['panes']) >= 1, status
@@ -265,9 +280,13 @@ def discovery(binary, embedded=False):
except OSError as refused:
assert why in str(refused), refused
commands = client.read('/commands').decode().splitlines()
- assert 'Newcol root' in commands and 'Save arg pane' in commands, commands
- assert all(row.split()[-1] in ('root', 'pane') for row in commands), commands
- assert client.stat('/commands')['length'] == len('\n'.join(commands)) + 1
+ def row(start):
+ return any(r == start or r.startswith(start + ' ') for r in commands)
+ assert row('Newcol root') and row('Save arg pane') and row('Edit arg both'), commands
+ # word, `arg` if it takes one, its ctl, a setting's values, then `-- ` what it does
+ assert all(r.split()[2 if r.split()[1] == 'arg' else 1] in ('root', 'pane', 'both') for r in commands), commands
+ assert row('Placement arg root acme,pardes'), commands
+ assert client.stat('/commands')['length'] == len('\n'.join(commands).encode()) + 1 # bytes: a description may end in …
client.write(f'/pane/{first}/ctl', b'Msg said through ctl\n')
assert 'said through ctl' in ''.join(cell[0] for cell in client.screen()['cells'])
assert set(client.list('/pane')) == {'new', str(fixture), str(first), str(second)}
@@ -416,12 +435,20 @@ def run_file(binary):
assert run(client, term, b'fi\n') == b'error not run\n' # a syntax error runs nothing
assert run(client, term, b"echo 'open\n") == b'error not run\n' # nor does an open quote
assert run(client, term, b'true\n') == b'exit 0\n' # ...and the shell is clear after
+ # A pty asked for one row gets two: one loses the prompt's input
+ # mark, and the shell would read busy for ever.
+ client.write(f'/pane/{term}/pty/ctl', b'winsize 20 1\n')
+ time.sleep(.5)
+ assert run(client, term, b'true\n') == b'exit 0\n'
+ assert client.read(f'/pane/{term}/pty/ctl').split()[2] == b'2', client.read(f'/pane/{term}/pty/ctl')
+ client.write(f'/pane/{term}/pty/ctl', b'winsize 80 24\n')
with Client(address) as other:
slow = []
waiter = threading.Thread(target=lambda: slow.append(run(other, term, b'sleep 1\n')), daemon=True)
waiter.start()
time.sleep(.3)
- assert run(client, term, b'true\n') == b'busy\n'
+ # The job the other client ran holds the tty, and says so.
+ assert run(client, term, b'true\n') == b'busy: sleep is running\n'
waiter.join(5)
assert slow == [b'exit 0\n'], slow
closing = []
@@ -508,6 +535,26 @@ def python_repl_runs_one_line(binary):
record = b'MX0 0 0 10 print(6*7)\n'
client.rpc(118, struct.pack('<IQI', fid, 0, len(record)) + record)
assert wait_for('\n42'), body()
+ # After the session has sat idle, the Enter still waits for
+ # the paste: core time is as old as the last frame then.
+ for n in range(5):
+ time.sleep(2)
+ line = f'print({1000 + n})'.encode()
+ record = b'MX0 0 0 %d %s\n' % (len(line), line)
+ client.rpc(118, struct.pack('<IQI', fid, 0, len(record)) + record)
+ assert wait_for(f'\n{1000 + n}'), (n, body())
+ # More than one line, with and without a newline at the end,
+ # and a block with a statement after it: each runs whole,
+ # and the REPL is back at its prompt, not at `...`.
+ for code, outputs in [(b'print(11)\nprint(22)', ['11', '22']),
+ (b'print(33)\nprint(44)\n', ['33', '44']),
+ (b'for i in range(2):\n print(55 + i)\nprint(77)\n', ['55', '56', '77'])]:
+ record = b'MX0 0 0 %d %s\n' % (len(code), code)
+ client.rpc(118, struct.pack('<IQI', fid, 0, len(record)) + record)
+ for out in outputs:
+ assert wait_for(f'\n{out}\n'), (code, out, body())
+ time.sleep(.5)
+ assert body().rstrip().endswith('>>>'), (code, body()[-200:])
finally:
client.close(fid)
@@ -528,8 +575,38 @@ def signal_cleans_up(binary):
assert not address.exists() and not detached.exists(), (sig, list(root.iterdir()))
+def cut_writes(binary):
+ """A write bigger than a 9P message comes as several, cut anywhere: each
+ command line runs once, whole, an Edit block once it ends, and a last
+ line with no newline when the open closes. Never a fragment."""
+ with tempfile.TemporaryDirectory(prefix='pardes-cut-') as directory:
+ with session(binary, Path(directory), 'cut') as (client, _):
+ burst = b''.join(b'Msg burst-%04d\n' % i for i in range(1000))
+ assert len(burst) > client.msize
+ client.write('/exec', burst)
+ log = client.read('/log').decode()
+ said = [line.split(' ', 2)[2] for line in log.splitlines() if line.startswith('msg ') and 'burst-' in line]
+ assert said == ['burst-%04d' % i for i in range(1000)], said[:3] + said[-3:]
+ assert '\nrun ' not in log and 'exit 127' not in log, log[-400:]
+ client.write('/exec', b'Msg no-newline-at-end')
+ assert 'msg ' in client.read('/log').decode().split('no-newline-at-end')[0][-40:]
+ serial = new_pane(client, b'')
+ block = b'Edit ,a\n' + b''.join(b'line %05d of the block\n' % i for i in range(2500)) + b'.\n'
+ assert len(block) > 50000
+ client.write(f'/pane/{serial}/ctl', block)
+ body = client.read(f'/pane/{serial}/body').decode().splitlines()
+ assert body == ['line %05d of the block' % i for i in range(2500)], (len(body), body[:2])
+ # A detached session with no frontend is 160x50 until told.
+ screen = client.screen()
+ assert (screen['cols'], screen['rows']) == (160, 50), (screen['cols'], screen['rows'])
+ client.write('/ctl', b'size 132 40\n')
+ screen = client.screen()
+ assert (screen['cols'], screen['rows']) == (132, 40), (screen['cols'], screen['rows'])
+
+
def test(binary, quic=False):
started = time.monotonic()
+ cut_writes(binary)
run_file(binary)
exit_answers(binary)
unattended_messages_expire(binary)
@@ -665,8 +742,9 @@ def test(binary, quic=False):
execute(remote, control, 'Mount peer ' + str(address))
opened = look(remote, '/n/peer/pane/1/body', source=control)
assert remote.read(f'/pane/{opened}/body') == client.read('/pane/1/body')
- execute(remote, control, 'Mount peer ' + str(own_address))
- execute(remote, control, 'Unmount peer')
+ # A builtin that fails through exec fails the write (one rule).
+ execute_refused(remote, control, 'Mount peer ' + str(own_address), 'already mounted')
+ execute_refused(remote, control, 'Unmount peer', 'mount in use') # a pane is open on it
remote.write(f'/pane/{opened}/body', b'runtime mounted Save\n', truncate=True)
remote.write(f'/pane/{opened}/exec', b'Save\n')
assert client.read('/pane/1/body') == b'runtime mounted Save\n'
@@ -693,7 +771,7 @@ def test(binary, quic=False):
with session(binary, root, name, tty=tty) as (old, address):
old.write('/pane/1/body', b'dumped state\n', truncate=True)
control = new_pane(old, b'')
- execute(old, control, 'Restore ' + str(root / 'missing dump.zon'))
+ execute_refused(old, control, 'Restore ' + str(root / 'missing dump.zon'), 'no such file')
assert old.read('/pane/1/body') == b'dumped state\n'
execute(old, control, 'Mount own ' + str(address))
execute(old, control, 'Dump')
@@ -735,7 +813,7 @@ def test(binary, quic=False):
contents = restored.read(f'/pane/{file_id}/body')
assert contents == b'dumped state\n', (contents, saved.read_text(), restored.read('/index'))
assert restored.read(f'/pane/{control_id}/body') == b'', 'commands ran through exec, not the body'
- assert restored.screen()['cols'] == 80
+ assert restored.screen()['cols'] == (80 if tty else 160) # a detached one is 160x50
opened = look(restored, '/n/own/os' + str(root / (name + '.txt')), source=control_id)
assert restored.read(f'/pane/{opened}/body') == b'initial\n'
@@ -909,6 +987,7 @@ def test(binary, quic=False):
assert path.read_bytes() == b'written through long filename\n'
opened = look(client, '/n/os' + str(path))
assert client.read(f'/pane/{opened}/body') == path.read_bytes()
+ client.remove(f'/pane/{opened}') # a long name's tag takes rows
opened = look(client, '/n/os' + str(directory))
listing = client.read(f'/pane/{opened}/body')
for path in directory.iterdir():
diff --git a/test/fs_namespace.zig b/test/fs_namespace.zig
index 2f78ca1a..bd620c2d 100644
--- a/test/fs_namespace.zig
+++ b/test/fs_namespace.zig
@@ -6,9 +6,15 @@ test "runtime Mount and Unmount copy command arguments and change Look resolutio
const gpa = std.testing.allocator;
const p = try pardes.Pardes.init(gpa, .{ .tty_only = true });
defer p.deinit();
- var command = "Mount peer /tmp/a socket.sock".*;
- try std.testing.expect(p.executeBuiltinLine(0, &command));
- @memset(&command, 'x');
+ // No peer answers at the dial: Mount mounts nothing (it dials first).
+ try std.testing.expect(p.executeBuiltinLine(0, "Mount peer /tmp/a socket.sock"));
+ try std.testing.expectEqual(@as(usize, 0), p.fs.mounts.items.len);
+ // Mounted (as one that answered is), its arguments are copies.
+ var name = "peer".*;
+ var dial = "/tmp/a socket.sock".*;
+ try p.fs.mount(gpa, &name, &dial);
+ @memset(&name, 'x');
+ @memset(&dial, 'x');
try std.testing.expectEqual(@as(usize, 1), p.fs.mounts.items.len);
try std.testing.expectEqualStrings("peer", p.fs.mounts.items[0].name);
try std.testing.expectEqualStrings("/tmp/a socket.sock", p.fs.mounts.items[0].dial);
diff --git a/test/gui-goldens.txt b/test/gui-goldens.txt
index 263f1bcc..01bdfc7c 100644
--- a/test/gui-goldens.txt
+++ b/test/gui-goldens.txt
@@ -6,12 +6,12 @@
06-tag-focus-selection 94aff8499a6e6f399761ffdd56369d526fbe65232eb21c88addd12973af611aa
07-prompt-notice 041603b841605b0a9456cbbb9b3e06e219b0a08a58e2d38229e1c81366577a87
08-wide-glyph-edges 94b0beb7cc2f2a09259f4f7d57999001ef75ed53ea210a65a1510404bd161af9
-09-collapsed-focus-tint 83db5e1acb69424c8ecdb247fcf359ff6b010a529a628012991d74b4a6428b89
-10-tagbottom 3a7d5bc8b2e821ffb082ccd548c6f312947f614be80324fbd79b4182e34cf387
-12-multiline-tag 93cdcfa151dc823d3d13af270157dcca8f69ecfd0ace7e0b888c088ca8c3deaf
+09-collapsed-focus-tint 1d0e6e4b371bc3b2bba96d9177b91f240fa4bef38c62470dc078fa9feed119ac
+10-tagbottom f7175dfb338b75c5ac3457c8422dc0e352c46d51b2c52533ce32ef143fcf6c6b
+12-multiline-tag ddd7d775a246e91b88f0eff98b6f9bea8aa5d63dbfbe931bd830ccb9086bd5c4
13-image f609043fc0fcdb55369b836b61fcbc631f786cc2295a5ddd72c0d9a59b177a60
14-theme-ink 881b7e99f38ad4aa3c46d2c61e20598010175d7858712a67fa6638fc29f12e14
15-theme-acme-light ca5252f5117ac8d6c679cef917c9f316c9ca311fc7a8dcbac2796b4fc04d3b64
16-debug 591bb7c45b4b9c46cd2458330b4fa3962afc6dac827fa6f5e20e927a72c586e6
17-terminal 38299427891e31de0b5597159501c161474620eb26fd7708a6396f0fd685209e
-18-mid-transition ab31f5fde35367edda6c7f4e45bf4396596f242a60938f384df6c392bd3712d2
+18-mid-transition 80cf350f8736fa4b348225a3872572a799ce57c763939796b68e8c839418f8ee
diff --git a/test/macos_e2e.swift b/test/macos_e2e.swift
index 5f172f9d..2b0eedad 100644
--- a/test/macos_e2e.swift
+++ b/test/macos_e2e.swift
@@ -1251,7 +1251,7 @@ private func buildWorld(stem: String) throws -> String {
// sources $HOME/.bashrc (host_io.Shell) — so this is what pins the
// prompt to `$ ` and keeps the developer's history out of the capture.
try "PS1='$ '\nHISTFILE=\n".write(toFile: "\(home)/.bashrc", atomically: true, encoding: .utf8)
- // ...and the config is not empty, because the DEFAULT shell is fish, whose
+ // ...and the config is not empty, because the DEFAULT shell is $SHELL (fish, say), whose
// prompt carries a hostname and whose greeting carries a version. A golden
// taken against that is a golden for one machine.
try "Shell bash\n".write(toFile: "\(pardesConfig)/init", atomically: true, encoding: .utf8)
diff --git a/test/output.zig b/test/output.zig
index 95cdbfb7..478f1b4a 100644
--- a/test/output.zig
+++ b/test/output.zig
@@ -544,10 +544,11 @@ test "Config prints the startup path and refreshes its one output" {
for ([_][]const u8{
"Startup config: " ++ path ++ "\n",
"Theme orchard\n",
- "Shell requested (new panes) " ++ config.default_shell ++ " (default)\n",
"Shell effective (last spawn) (none)\n",
"Shell pending on\n",
}) |line| try std.testing.expect(std.mem.indexOf(u8, out.content, line) != null);
+ var default_line: [256]u8 = undefined;
+ try std.testing.expect(std.mem.indexOf(u8, out.content, try std.fmt.bufPrint(&default_line, "Shell requested (new panes) {s} (default)\n", .{config.defaultShell()})) != null);
try std.testing.expectEqualStrings(config.config_buffer, std.fs.path.basename(out.path));
try std.testing.expectEqual(panes.Output.Origin{ .cmd = .Config }, out.output.?.from);
@@ -861,6 +862,7 @@ test "n/N resumes the result output whose Look moved focus away" {
const p = try Pardes.init(std.testing.allocator, .{ .file = path, .cols = 80, .rows = 24 });
defer p.deinit();
p.locations_config.layout = .@"inline";
+ p.settings.look_word = .list; // the +Search listing is what is walked
try std.testing.expect(p.executeBuiltinLine(0, "Look target"));
const rid = p.panes[0].?.search_pane orelse return error.MissingResults;
@@ -1590,10 +1592,13 @@ test "stacked search and LSP results retain source tint navigation and dump owne
}
restored.active = id;
pardes.look.armLookWalk(restored, id);
- // The dot came back on the match it was on; the walk goes on.
+ // The dot came back on the match it was on; the walk goes on
+ // from it both ways, N wrapping to the last as it does live.
try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored.panes[id].?.body.cur_row);
- restored.update(.{ .key = .{ .cp = 'n' } });
+ restored.update(.{ .key = .{ .cp = 'N' } });
try std.testing.expectEqual(@as(i32, @intCast(matches[1])), restored.panes[id].?.body.cur_row);
+ restored.update(.{ .key = .{ .cp = 'n' } });
+ try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored.panes[id].?.body.cur_row);
try std.testing.expectEqualStrings(source_text, source.file.?.content);
}
}
diff --git a/test/panes.zig b/test/panes.zig
index 25f0f6db..3d2821aa 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -4853,7 +4853,7 @@ const LookResetTests = struct {
try std.testing.expectEqualStrings("needle", selected(pane));
}
- test "Look clears both source and existing destination selections before focus" {
+ test "Look clears the source's click highlight, not its dot, and the destination's selections before focus" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
defer p.deinit();
const source = try p.setTestFile("source\nlines\n");
@@ -4872,9 +4872,12 @@ const LookResetTests = struct {
try seed(destination, "previous destination");
pardes.look.lookAt(p, 0, source.pointer_selections[0].?.raw_text.?);
try std.testing.expectEqual(id, p.active);
- try cleared(source);
+ // The source keeps its dot (acme's look leaves it); the click's
+ // highlight goes.
+ for (source.sel) |selection| try std.testing.expectEqual(.none, selection.state);
+ for (source.pointer_selections) |selection| try std.testing.expect(selection == null);
+ try std.testing.expect(source.body.vsel.active);
try cleared(destination);
- try std.testing.expect(!source.body.vsel.active);
if (range) {
try std.testing.expectEqual(@as(i32, 1), destination.body.cur_row);
try std.testing.expectEqualStrings("rav", selected(destination));
@@ -4890,3 +4893,47 @@ const LookResetTests = struct {
test {
_ = LookResetTests;
}
+
+test "a terminal keeps its tag and two body rows through a squeeze, a drag and a smaller window; a text pane does not" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
+ defer p.deinit();
+ p.presentation.enabled = false;
+ p.sync();
+ while (p.nextEffect()) |_| {}
+ const term = for (p.panes, 0..) |slot, i| {
+ if (slot) |pane| if (pane.isTerminal()) break i;
+ } else return error.NoTerminal;
+ p.newScratchBelow(term);
+ p.sync();
+ while (p.nextEffect()) |_| {}
+ const col = layout.findPane(p, term).?.col;
+ const floor = p.panes[term].?.tag_rows + layout.min_body_rows;
+ // Squeezed by the weights: the others give it its floor.
+ p.panes[term].?.vweight = 0.01;
+ for (p.col_panes[col][0..p.col_n[col]]) |k| if (k != term) {
+ p.panes[k].?.vweight = 1000;
+ };
+ p.sync();
+ try std.testing.expectEqual(floor, p.rects[term].h);
+ var rows: ?u16 = null;
+ while (p.nextEffect()) |effect| if (effect == .resize_pty and effect.resize_pty.pane == term) {
+ rows = effect.resize_pty.rows;
+ };
+ try std.testing.expectEqual(@as(?u16, layout.min_body_rows), rows);
+ // A drag of the grip under it up to its tag stops at the floor; one
+ // the other way takes a text pane down to its tag alone.
+ const k = layout.findPane(p, term).?.idx;
+ if (k + 1 < p.col_n[col]) {
+ layout.applyRowSplit(p, col, k, p.rects[term].y);
+ p.sync();
+ try std.testing.expectEqual(floor, p.rects[term].h);
+ const text = p.col_panes[col][k + 1];
+ layout.applyRowSplit(p, col, k, p.screen_h);
+ p.sync();
+ try std.testing.expect(p.rects[text].h <= p.panes[text].?.tag_rows);
+ }
+ // A window too short for all: the terminal keeps its floor still.
+ p.update(.{ .resize = .{ .cols = 100, .rows = 10 } });
+ p.sync();
+ try std.testing.expect(p.rects[term].h >= p.panes[term].?.tag_rows + layout.min_body_rows);
+}
diff --git a/test/selfmount.py b/test/selfmount.py
index 90960bed..55809740 100644
--- a/test/selfmount.py
+++ b/test/selfmount.py
@@ -105,6 +105,39 @@ def run(binary):
check(client.read(f'/pane/{first}/body') == b'HELLO initial\n', f'Save wrote back through the mount into pane {first}')
text = ''.join(cell[0] for cell in client.screen()['cells'])
check('saved ' + mount in text, 'the editor reported the save')
+ # Through the kernel mount a write is cut at its msize: a
+ # 1000-line burst and a 50 KB Edit block each still run whole,
+ # line by line, and no piece of a line runs as a shell command.
+ # sh: bash's and fish's prompt files are not reachable in 9ns's
+ # namespace here, and a command pane needs no prompt marks.
+ client.write('/ctl', b'Shell sh\n')
+ # From the fixture's directory: a pane on the mount has one a
+ # shell cannot enter.
+ client.write('/focus', f'{first}\n'.encode())
+
+ def run(line):
+ """A command pane runs the line; answers its exit status."""
+ seen = client.read('/log').decode().count('\nexit ')
+ client.write('/exec', line)
+ deadline = time.monotonic() + 20
+ while time.monotonic() < deadline:
+ drain()
+ exits = [row for row in client.read('/log').decode().splitlines() if row.startswith('exit ')]
+ if len(exits) > seen:
+ return exits[-1].split()[2]
+ time.sleep(.1)
+ return 'timeout'
+ got = run(f"seq -f 'Msg mnt-%04g' 0 999 > {mount}/exec\n".encode())
+ check(got == '0', f'a 1000-line burst written through the mount ({got!r})')
+ log = client.read('/log').decode()
+ said = [line.split(' ', 2)[2] for line in log.splitlines() if line.startswith('msg ') and 'mnt-' in line]
+ check(said == ['mnt-%04d' % i for i in range(1000)], f'each line of it ran once, whole ({len(said)} said)')
+ check('exit 127' not in log, 'no piece of a line ran as a command')
+ scratch = int(client.read('/pane/new'))
+ maker = "import sys; sys.stdout.write('Edit ,a\\n' + ''.join('line %05d of the block\\n' % i for i in range(2500)) + '.\\n')"
+ check(run(f'python3 -c "{maker}" > {mount}/pane/{scratch}/ctl\n'.encode()) == '0', 'a 50 KB Edit block written through the mount')
+ body = client.read(f'/pane/{scratch}/body').decode().splitlines()
+ check(body == ['line %05d of the block' % i for i in range(2500)], f'it ran once, whole ({len(body)} lines)')
client.write('/exec', b'Msg still here\n')
text = ''.join(cell[0] for cell in client.screen()['cells'])
check('still here' in text, 'the editor answers after all of that')
diff --git a/test/snapshot.zig b/test/snapshot.zig
index 2fa8e89c..c49e7645 100644
--- a/test/snapshot.zig
+++ b/test/snapshot.zig
@@ -562,8 +562,9 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const
// XDG directory that points outside this per-snapshot fake home.
const config_home = try std.fmt.allocPrintSentinel(arena, "{s}/.config", .{home}, 0);
_ = setenv("XDG_CONFIG_HOME", config_home, 1);
- // ...and it is not empty: the DEFAULT shell is fish, whose prompt carries
- // a hostname and whose greeting carries a version, so a golden taken
+ // ...and it is not empty: the DEFAULT shell is $SHELL, whatever this
+ // machine's user logs in with (fish's prompt carries a hostname and its
+ // greeting a version), so a golden taken
// against it would be a golden for this machine. Every script here runs
// bash — the .bashrc above pins its prompt to `$ ` — and the fish path is
// covered by host_io.Shell's unit tests instead. This is an ordinary
diff --git a/test/snapshots/argv.golden b/test/snapshots/argv.golden
index ab143a88..e4d41d6c 100644
--- a/test/snapshots/argv.golden
+++ b/test/snapshots/argv.golden
@@ -20,7 +20,7 @@
|
|
|
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/argv/cwd/+New Save Tty Collapse Del
| 1
== snap filearg grid=100x31 cursor=7,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
diff --git a/test/snapshots/cornerdrag.golden b/test/snapshots/cornerdrag.golden
index 730cc2bf..30c1830a 100644
--- a/test/snapshots/cornerdrag.golden
+++ b/test/snapshots/cornerdrag.golden
@@ -243,7 +243,7 @@
|24:
|25:
|26:
-|27:
-|28:
-|29:
-|30: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter
+|27: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter
+|28: Collapse Del
+|29: w.txt
+|30:
diff --git a/test/snapshots/filewatch.golden b/test/snapshots/filewatch.golden
index f19168b1..589cfc3a 100644
--- a/test/snapshots/filewatch.golden
+++ b/test/snapshots/filewatch.golden
@@ -28,20 +28,8 @@
| 7 line 7
| 8 line 8
| 9
-== snap reloaded grid=210x31 cursor=15,7
-|6: /tmp/pardes-snap/filewatch/cwd/w.txt Save Tty Collapse Del
-|7: 1 EXTERNAL --:--:-- reloaded /tmp/pardes-snap/filewatch/cwd/w.txt
-|8: 2
-|9:
-|10:
-|11:
-|12:
-|13:
-|14:
-|15:
-|16:
-|17:
-|18:
+== snap kept grid=210x31 cursor=15,7
+|7: 1 liMYEDITne 1 /tmp/pardes-snap/filewatch/cwd/w.txt changed on disk (get reloads it, Save overwrites it)
== snap reloaded_sub grid=210x31 cursor=15,7
|20: 1 RENAMED --:--:-- reloaded /tmp/pardes-snap/filewatch/cwd/sub/s.txt
|21: 2
@@ -52,18 +40,3 @@
|26:
|27:
|28:
-== snap undone grid=210x31 cursor=15,7
-|6: * /tmp/pardes-snap/filewatch/cwd/w.txt Save Tty Collapse Del
-|7: 1 liMYEDITne 1
-|8: 2 line 2
-|9: 3 /tmp/pardes-snap/filewatch/cwd/sub/s.txt
-|10: 4 line 4
-|11: 5 line 5
-|12: 6 line 6
-|13: 7 line 7
-|14: 8 line 8
-|15: 9 line 9
-|16: 10 line 10
-|17: 11 line 11
-|18: 12 line 12
-|20: 1 RENAMED
diff --git a/test/snapshots/filewatch.snap b/test/snapshots/filewatch.snap
index 9e6e6a13..6664519f 100644
--- a/test/snapshots/filewatch.snap
+++ b/test/snapshots/filewatch.snap
@@ -1,6 +1,6 @@
-# external file updates: a write from outside pardes lands in the open pane,
-# and Undo brings the unsaved edit back. That is the whole contract — the
-# update is committed like any other edit, so nothing has to be merged.
+# external file updates: a write from outside pardes lands in an open pane
+# that has no unsaved edits, committed like any other edit; a pane WITH
+# unsaved edits keeps them and says the file changed on disk (File.changed).
#
# The writer must be a STRANGER to the editor. This script used to have a
# pardes PANE run the write, which proved much less than it looked: that
@@ -54,18 +54,13 @@ settle 100
stable 400 5000
snap edited
# ---- from here to `snap reloaded_sub`, pardes is sent NOTHING ----
+# w.txt has the unsaved edit: it is kept, and the change is said
run printf 'EXTERNAL\n' > w.txt
-wait 10000 EXTERNAL
+wait 10000 changed on disk
stable 700 10000
-snap reloaded
+snap kept
+# sub/s.txt has none: it takes the new text
run printf 'RENAMED\n' > sub/t && mv sub/t sub/s.txt
wait 10000 RENAMED
stable 700 10000
snap reloaded_sub
-# back in the file pane: one undo and the unsaved edit is there again
-press left 10 8
-release left 10 8
-stable 400 5000
-key u
-stable 400 5000
-snap undone
diff --git a/test/snapshots/jumps.golden b/test/snapshots/jumps.golden
index 310f00f9..0a2063b0 100644
--- a/test/snapshots/jumps.golden
+++ b/test/snapshots/jumps.golden
@@ -134,9 +134,9 @@
== snap jumplist grid=150x31 cursor=7,17
|3: 86 line 86 Jumplist ls
|16: /tmp/pardes-snap/jumps/cwd/+Jumps Save Tty Collapse Del LocationsConfig
-|17: 1 @p1:3:3 /tmp/pardes-snap/jumps/cwd
+|17: 1 @p2:3:3 /tmp/pardes-snap/jumps/cwd
|18: 2 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1
-|19: 3 @p1:3:3 /tmp/pardes-snap/jumps/cwd
+|19: 3 @p2:3:3 /tmp/pardes-snap/jumps/cwd
|20: 4 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100
|21: 5
|22:
diff --git a/test/snapshots/lookpanes.golden b/test/snapshots/lookpanes.golden
index a9088166..fbab6eb1 100644
--- a/test/snapshots/lookpanes.golden
+++ b/test/snapshots/lookpanes.golden
@@ -14,18 +14,18 @@
|6: /tmp/pardes-snap/lookpanes/cwd/alpha/zqonly.txt Save Tty Colla
|7: pse Del /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filter
|8: 1 ONLY-IN-ALPHA Collapse Del
-== snap clicked-wins grid=130x51 cursor=7,11
-|9: /tmp/pardes-snap/lookpanes/cwd/beta/both.txt Save Tty Collapse ls
-|10: Del amark.txt both.txt pick.txt twin.txt zqonly.txt
-|11: 1 BOTH-FROM-BETA
+== snap clicked-wins grid=130x51 cursor=7,12
+|10: /tmp/pardes-snap/lookpanes/cwd/beta/both.txt Save Tty Collapse amark.txt both.txt pick.txt twin.txt zqonly.txt
+|11: Del
+|12: 1 BOTH-FROM-BETA
== snap mru-alpha grid=130x51 cursor=72,14
-|12: /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collaps
+|12: 1 BOTH-FROM-BETA /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collaps
|13: e Del
|14: 1 PICK-FROM-ALPHA
-== snap mru-beta grid=130x51 cursor=7,14
-|12: /tmp/pardes-snap/lookpanes/cwd/beta/twin.txt Save Tty Collapse /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collaps
-|13: Del e Del
-|14: 1 TWIN-FROM-BETA 1 PICK-FROM-ALPHA
+== snap mru-beta grid=130x51 cursor=7,16
+|14: /tmp/pardes-snap/lookpanes/cwd/beta/twin.txt Save Tty Collapse 1 PICK-FROM-ALPHA
+|15: Del
+|16: 1 TWIN-FROM-BETA
== style mru-beta grid=130x51
|0: 0-70 #b2c3b8,#1d2824, 71-129 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#314738, 3-34 #becfc4,#314738, 35-64 d,#314738, 65-66 #ffffff,#887455, 67-67 d,#1d2824, 68-99 #b2c3b8,#1d2824, 100-129 d,#1d2824,
@@ -36,14 +36,14 @@
|6: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-39 #b2c3b8,#1d2824, 40-49 #d3b0d9,#1d2824, 50-64 #b2c3b8,#1d2824, 65-65 d,#608269, 66-129 d,#0d1410,
|7: 0-2 d,#1d2824, 3-9 #b2c3b8,#1d2824, 10-64 d,#1d2824, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-64 d,#0d1410, 65-67 d,#1d2824, 68-80 #b2c3b8,#1d2824, 81-129 d,#1d2824,
-|9: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-46 #d3b0d9,#1d2824, 47-64 #b2c3b8,#1d2824, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
-|10: 0-2 d,#1d2824, 3-6 #b2c3b8,#1d2824, 7-64 d,#1d2824, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
-|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-129 d,#0d1410,
-|12: 0-1 #000000,#add38a, 2-2 d,#314738, 3-38 #becfc4,#314738, 39-46 #dfbce5,#314738, 47-64 #becfc4,#314738, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824,
-|13: 0-2 d,#314738, 3-6 #becfc4,#314738, 7-64 d,#314738, 65-67 d,#1d2824, 68-72 #b2c3b8,#1d2824, 73-129 d,#1d2824,
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-86 #e5eee5,#0d1410, 87-129 d,#0d1410,
-|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410,
-|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410,
+|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
+|10: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-46 #d3b0d9,#1d2824, 47-64 #b2c3b8,#1d2824, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
+|11: 0-2 d,#1d2824, 3-6 #b2c3b8,#1d2824, 7-64 d,#1d2824, 65-65 d,#608269, 66-129 d,#0d1410,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824,
+|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-67 d,#1d2824, 68-72 #b2c3b8,#1d2824, 73-129 d,#1d2824,
+|14: 0-1 #000000,#add38a, 2-2 d,#314738, 3-38 #becfc4,#314738, 39-46 #dfbce5,#314738, 47-64 #becfc4,#314738, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-86 #e5eee5,#0d1410, 87-129 d,#0d1410,
+|15: 0-2 d,#314738, 3-6 #becfc4,#314738, 7-64 d,#314738, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410,
+|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410,
|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410,
|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410,
diff --git a/test/snapshots/nested.golden b/test/snapshots/nested.golden
index e5244e71..9758c491 100644
--- a/test/snapshots/nested.golden
+++ b/test/snapshots/nested.golden
@@ -20,7 +20,7 @@
|
|
|
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/nested/cwd/+New Save Tty Collapse Del
| 1
== snap no-arg grid=100x31 cursor=4,7
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
@@ -44,7 +44,7 @@
|
|
|
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/nested/cwd/+New Save Tty Collapse Del
| 1
== snap dir-arg grid=100x31 cursor=4,8
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
@@ -68,5 +68,5 @@
|
|
|
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/nested/cwd/+New Save Tty Collapse Del
| 1
diff --git a/test/snapshots/psearch.golden b/test/snapshots/psearch.golden
index ee65ede2..20523962 100644
--- a/test/snapshots/psearch.golden
+++ b/test/snapshots/psearch.golden
@@ -20,18 +20,18 @@
|
|
|
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/psearch/cwd/+New Save Tty Collapse Del
| 1
== snap typing grid=140x31 cursor=139,3
|3: printf 'x MA''RK a\nzz\nx MA''RK b\nzz\nx MA''RK c\n' /MARK
== snap results grid=140x31 cursor=7,4
|3: printf 'x MA''RK a\nzz\nx MA''RK b\nzz\nx MA''RK c\n'
|10: /tmp/pardes-snap/psearch/cwd/+Search Save Tty Collapse Del LocationsConfig
-|11: 1 @p0:2:3-6
+|11: 1 @p1:2:3-6
|12: 2 x MARK a
-|13: 3 @p0:4:3-6
+|13: 3 @p1:4:3-6
|14: 4 x MARK b
-|15: 5 @p0:6:3-6
+|15: 5 @p1:6:3-6
|16: 6 x MARK c
|17: 7
== style results grid=140x31
@@ -56,7 +56,7 @@
|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-139 d,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-139 d,#0d1410,
|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-139 d,#0d1410,
-|21: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-3 #b2c3b8,#1d2824, 4-7 #d3b0d9,#1d2824, 8-29 #b2c3b8,#1d2824, 30-139 d,#1d2824,
+|21: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-35 #d3b0d9,#1d2824, 36-57 #b2c3b8,#1d2824, 58-139 d,#1d2824,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-139 d,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-139 d,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-139 d,#0d1410,
diff --git a/test/snapshots/psearch.snap b/test/snapshots/psearch.snap
index a46e2b9d..6e463119 100644
--- a/test/snapshots/psearch.snap
+++ b/test/snapshots/psearch.snap
@@ -1,7 +1,7 @@
# `/` search on a TERMINAL pane: the same tag input as a file pane, searching
# the shell's own scrollback (the plain text view Look reads, prompt rows
# hidden). Results land in a +Search OUTPUT BUFFER — a file pane with no file
-# behind it, so its tag has no Save — and point at `@p0:LINE:COL-ENDCOL`, the
+# behind it, so its tag has no Save — and point at `@p1:LINE:COL-ENDCOL` (the terminal's serial), the
# look target that names a live pane, since a terminal has no path. n/N SELECT
# the next/prev row in the buffer and open nothing; Enter — the look chord —
# is what scrolls the terminal to that line of its scrollback and selects the
@@ -31,12 +31,12 @@ stable 400 5000
text MARK
stable 400 5000
snap typing
-# Enter opens the results buffer — three @p0:LINE:COL rows — and goes to the
+# Enter opens the results buffer — three @p1:LINE:COL rows — and goes to the
# first of them: the terminal's cursor lands on that scrollback line, on the
# matched SPAN (cols 3..6, past the leading "x ") — not the start of the line,
# and selected rather than merely parked on. No `n` was pressed to get here.
key enter
-wait 10000 @p0:2:3
+wait 10000 @p1:2:3
stable 700 15000
snap results
snapstyle results
@@ -83,6 +83,6 @@ key c-b
stable 600 8000
press right 5 5
release right 5 5
-wait 10000 @p0:2:3
+wait 10000 @p1:2:3
stable 700 15000
snap rightsearch
diff --git a/test/snapshots/reflow.golden b/test/snapshots/reflow.golden
index ab8bfe9b..afd4b93f 100644
--- a/test/snapshots/reflow.golden
+++ b/test/snapshots/reflow.golden
@@ -16,5 +16,5 @@
| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
| yyyyyyyyyyyyyyyyyyyyyyyyyyyyyydone
| $
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/reflow/cwd/+New Save Tty Collapse Del
| 1
diff --git a/test/snapshots/rsearch.snap b/test/snapshots/rsearch.snap
index 8c81db1d..75d0212c 100644
--- a/test/snapshots/rsearch.snap
+++ b/test/snapshots/rsearch.snap
@@ -11,6 +11,9 @@ file code.txt run count_x now\nopen sub/dep.txt:3 here\ncount_x again\npick two
# the top of the body -- which moves every row this script clicks. What the
# verbs do is what is under test, not the announcement.
config Verbose off
+# the +Search listing is what this walks: LookWord list (search, acme's look3,
+# is the default; ctl.zig tests it)
+config LookWord list
start 31 210 -n 1
wait 8000 code.txt
stable 700 20000
diff --git a/test/snapshots/shellset.golden b/test/snapshots/shellset.golden
index 0af8f2e1..e2830801 100644
--- a/test/snapshots/shellset.golden
+++ b/test/snapshots/shellset.golden
@@ -4,7 +4,7 @@
| /tmp/pardes-snap/shellset/cwd/cmd.txt Save Tty Collapse Del
| 1 Shell zznosuchshell
== snap builtin-ran grid=90x21 cursor=25,3
-|3: 1 Shell zznosuchshell Shell
+|3: 1 Shel Shell: no shell "zznosuchshell" (a name on the usual paths, or a path to one)
== snap fallback-shell grid=90x21 cursor=49,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
|2: /tmp/pardes-snap/shellset/cwd/cmd.txt Save /tmp/pardes-snap/shellset/cwd Tty+bash Sav
diff --git a/test/snapshots/shellset.snap b/test/snapshots/shellset.snap
index d2bacf38..7d213a84 100644
--- a/test/snapshots/shellset.snap
+++ b/test/snapshots/shellset.snap
@@ -1,12 +1,12 @@
# The Shell builtin names the binary the NEXT terminal execs. What is pinned
# here is the case that has to hold on a machine you did not set up: the name
-# resolves to nothing, and the pane opens anyway.
+# resolves to nothing, so Shell refuses it and says why on the message row,
+# and the next pane opens anyway with the shell that was set before.
#
-# That is not a corner — it is the DEFAULT's failure mode. pardes ships
-# `Shell fish` as the default, and a box without fish must still get a working
-# shell rather than a pane whose child dies at exec and shows one EOF. So the
-# lookup falls back, and this is the proof: a name nothing resolves to, and the
-# new pane still greets and lists like any other.
+# A default $SHELL that names nothing executable falls back to /bin/sh
+# (config.defaultShell), and the host's lookup still falls back at spawn for
+# a shell that went away after it was set: a pane never gets a child that
+# dies at exec and shows one EOF.
#
# The rest of the feature is unit-tested rather than pinned here, on purpose.
# Which family a binary belongs to, what argv each one gets and where the
diff --git a/test/snapshots/shrinktag.golden b/test/snapshots/shrinktag.golden
index 552722f9..9aa29e11 100644
--- a/test/snapshots/shrinktag.golden
+++ b/test/snapshots/shrinktag.golden
@@ -21,23 +21,25 @@
| ls
| w.txt
== snap shrunk grid=100x31 cursor=2,9
-|3: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo de Filter Collapse Del
-|4: de Filter Collapse Del ls
-|5: ls w.txt
-|6: w.txt
-|8:
+|4: w.txt ls
+|5: w.txt
+|6: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
+|7: de Filter Collapse Del
+|8: ls
+|9: w.txt
|17:
|18:
|19:
|20:
== snap move-preview grid=100x31 cursor=2,9
-|8: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+|8: ls ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
== snap restored grid=100x31 cursor=2,9
-|3: de Filter Collapse Del de Filter Collapse Del
|4: ls ls
|5: w.txt w.txt
|6:
+|7:
|8: echo one; echo two
+|9:
|17: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
|18: de Filter Collapse Del
|19: ls
@@ -47,4 +49,6 @@
|18:
|19:
|20:
-|30: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
+|27: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
+|28: de Filter Collapse Del
+|29: w.txt
diff --git a/test/snapshots/shrinktag.snap b/test/snapshots/shrinktag.snap
index 31edad14..5264be74 100644
--- a/test/snapshots/shrinktag.snap
+++ b/test/snapshots/shrinktag.snap
@@ -1,8 +1,8 @@
-# a pane can shrink to just its tagline: the pane below dragged by its grip up
-# to the tag row (0 body rows) -- a terminal has no rule between panes, so
-# the grip is the handle, as acme's button is -- the gutter box still grabs a
-# move, and dragging back down restores the body intact (the hidden pane's
-# pty/vt is never resized)
+# a terminal shrinks only to its tag and 2 body rows (a text pane would go
+# to its tag alone): the pane below dragged by its grip up to the tag row
+# stops there -- a terminal has no rule between panes, so the grip is the
+# handle, as acme's button is -- the gutter box still grabs a move, and
+# dragging back down restores the body intact
file w.txt x
start 31 100 -n 3
wait 8000 w.txt
@@ -18,7 +18,7 @@ key esc
settle 100
stable 400 5000
snap before
-# drag the lower pane's grip up under pane 0's tag row: tag-only pane
+# drag the lower pane's grip up under pane 0's tag row: it keeps 2 body rows
press left 1 18
drag left 1 3
release left 1 3
@@ -34,14 +34,16 @@ snap move-preview
drag left 20 3
release left 20 3
stable 400 5000
-# re-enlarge: the lower pane's grip, now under pane 0's tag, pulled back down
-press left 1 4
+# re-enlarge: the lower pane's grip, now under pane 0's 2 body rows, pulled
+# back down
+press left 1 7
drag left 1 18
release left 1 18
motion 40 25
stable 500 8000
snap restored
-# the bottom pane can shrink to ITS tag too: its grip dragged past the bottom
+# the bottom pane stops at ITS tag and 2 rows too: its grip dragged past the
+# bottom
press left 1 18
drag left 1 31
release left 1 31
diff --git a/test/snapshots/splitparent.golden b/test/snapshots/splitparent.golden
index e64df572..e626cea7 100644
--- a/test/snapshots/splitparent.golden
+++ b/test/snapshots/splitparent.golden
@@ -2,38 +2,34 @@
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del
-| Mode Filter Collapse Del ls
+| Mode Filter Collapse Del Mode Filter Collapse Del
+| ls ls
+|
+| /tmp/pardes-snap/splitparent/cwd Tty+bash Save
+| Mode Filter Collapse Del
| ls
-== snap swap-parent grid=100x31 cursor=4,9
-|5:
-|6: /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-|7: Mode Filter Collapse Del
-|8: ls
-== snap tallest grid=100x6 cursor=54,5
+== snap swap-parent grid=100x31 cursor=4,13
+|10: /tmp/pardes-snap/splitparent/cwd Tty+bash Save
+|11: Mode Filter Collapse Del
+|12: ls
+== snap tallest grid=100x6 cursor=4,5
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-| /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-| /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-== snap regrown grid=100x31 cursor=54,20
+| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del
+| /tmp/pardes-snap/splitparent/cwd Tty+bash Save ls
+| e in that column: each keeps its tag and 2 rows
+== snap regrown grid=100x31 cursor=4,13
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del
-| Mode Filter Collapse Del ls
+| Mode Filter Collapse Del Mode Filter Collapse Del
+| ls ls
|
| /tmp/pardes-snap/splitparent/cwd Tty+bash Save
| Mode Filter Collapse Del
| ls
|
-|
-|
-|
-|
-|
-|
-|
-| /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-| Mode Filter Collapse Del
-| ls
+| /tmp/pardes-snap/splitparent/cwd Tty+bash Save
+| Mode Filter Collapse Del
+| e in that column: each keeps its tag and 2 rows
diff --git a/test/snapshots/splitparent.snap b/test/snapshots/splitparent.snap
index b9038af2..93ed9e21 100644
--- a/test/snapshots/splitparent.snap
+++ b/test/snapshots/splitparent.snap
@@ -1,25 +1,29 @@
-# a new pane never opens under a too-short parent: Alt-n from a tag-only pane
+# a new pane never opens under a too-short parent: Alt-n from a pane too short to halve
# splits the tall sibling in its column instead; when NO pane could leave the
-# newcomer 2 body rows, the tallest one anywhere is split
+# newcomer 2 body rows, the tallest one anywhere is split, and when that one
+# cannot keep its own tag and 2 rows either, the pane is refused
# pins the pardes placement rule (Placement pardes), which it covers
config Placement pardes
start 31 100 -n 3
stable 700 20000
-# shrink pane 0 (top-left) to just its tagline: the pane below's grip dragged
-# up under it (a terminal has no rule between panes; the grip is the handle)
+# shrink pane 0 (top-left), a terminal, as far as it goes, its tag and 2 body
+# rows: the pane below's grip dragged up under its tag (a terminal has no
+# rule between panes; the grip is the handle)
press left 1 18
drag left 1 3
release left 1 3
motion 40 25
stable 500 8000
snap shrunk
-# Alt-n from the tag-only pane: the new shell opens below the tall sibling
+# Alt-n from the short pane: the new shell opens below the tall sibling
# underneath it, not as another 1-row sliver
key a-n
stable 700 15000
snap swap-parent
-# squeeze the grid until no pane is 5 rows tall, then Alt-n again: the
-# tallest pane (right column) is split even though the active one is left
+# squeeze the grid until no pane can give a new one its tag and 2 rows, then
+# Alt-n again: the new shell is refused, and the message row says why (before
+# every pane had to keep only a tag and one row, the tallest pane anywhere was
+# split here)
resize 6 100
stable 700 15000
key a-n
diff --git a/test/snapshots/tinywin.golden b/test/snapshots/tinywin.golden
index d24ab0c5..c847a2f1 100644
--- a/test/snapshots/tinywin.golden
+++ b/test/snapshots/tinywin.golden
@@ -20,13 +20,13 @@
|
|
|
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/tinywin/cwd/+New Save Tty Collapse Del
| 1
== snap shrunk grid=60x4 cursor=59,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor
| New Tty Find Grep Joincol Delcol
|$ /tmp/pardes-snap/tinywin/cwd Tty+bash Save Mode Filter Co
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/tinywin/cwd/+New Save Tty Collapse Del
== snap columns grid=100x31 cursor=23,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
@@ -49,7 +49,7 @@
|
|
|
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/tinywin/cwd/+New Save Tty Collapse Del
| 1
== snap narrow grid=4x31 cursor=2,20
|Newc
@@ -74,10 +74,10 @@
| cn
|
| /
-| +
-| N
-| e
-| w
-|
-| S
+| t
+| m
+| p
+| /
+| p
| a
+| r
diff --git a/test/snapshots/ttyhelp.golden b/test/snapshots/ttyhelp.golden
index cd7cb946..8b8d4195 100644
--- a/test/snapshots/ttyhelp.golden
+++ b/test/snapshots/ttyhelp.golden
@@ -20,7 +20,7 @@
|
|
|
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/ttyhelp/cwd/+New Save Tty Collapse Del
| 1
== snap helped grid=100x31 cursor=7,6
|5: /tmp/pardes-snap/ttyhelp/cwd/+Help Save Tty Collapse Del
diff --git a/test/snapshots/ttylook.golden b/test/snapshots/ttylook.golden
index 05cace52..f2f37f9d 100644
--- a/test/snapshots/ttylook.golden
+++ b/test/snapshots/ttylook.golden
@@ -66,6 +66,6 @@
== snap fileopen grid=120x31 cursor=3,4
|22: /tmp/pardes-snap/ttylook/cwd/+Search Save Tty Collapse De
|23: l LocationsConfig
-|24: 1 @p0:1:1-2
+|24: 1 @p1:1:1-2
|25: 2 ls
|26: 3
diff --git a/test/snapshots/ttyonly.golden b/test/snapshots/ttyonly.golden
index 85a61d35..77f7dbb7 100644
--- a/test/snapshots/ttyonly.golden
+++ b/test/snapshots/ttyonly.golden
@@ -20,7 +20,7 @@
|
|
|
-| /+New Save Tty Collapse Del
+| /tmp/pardes-snap/ttyonly/cwd/+New Save Tty Collapse Del
| 1
== style ttyonly grid=100x31
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
@@ -44,7 +44,7 @@
|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-99 #e5eee5,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-99 #e5eee5,#0d1410,
|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-99 #e5eee5,#0d1410,
-|21: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-3 #b2c3b8,#1d2824, 4-7 #d3b0d9,#1d2824, 8-29 #b2c3b8,#1d2824, 30-99 d,#1d2824,
+|21: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-35 #d3b0d9,#1d2824, 36-57 #b2c3b8,#1d2824, 58-99 d,#1d2824,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,