diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/appearance_gallery.py | 8 | ||||
| -rw-r--r-- | test/fs.py | 223 | ||||
| -rw-r--r-- | test/gui-goldens.txt | 4 | ||||
| -rw-r--r-- | test/output.zig | 10 | ||||
| -rw-r--r-- | test/panes.zig | 70 | ||||
| -rw-r--r-- | test/snapshots/find.golden | 16 | ||||
| -rw-r--r-- | test/snapshots/shellset.golden | 2 | ||||
| -rw-r--r-- | test/snapshots/stepgrain.golden | 32 | ||||
| -rw-r--r-- | test/snapshots/themesel.golden | 2 | ||||
| -rw-r--r-- | test/snapshots/themesel.snap | 2 |
10 files changed, 325 insertions, 44 deletions
diff --git a/test/appearance_gallery.py b/test/appearance_gallery.py index bc9a7570..46b17dd9 100644 --- a/test/appearance_gallery.py +++ b/test/appearance_gallery.py @@ -51,7 +51,7 @@ Orchard: crisp woodland color Dusk: intentionally soft and warm Theme dusk -ThemeSel: Pardes themes come first. +Themes: Pardes themes come first. ''' @@ -159,16 +159,16 @@ def run(args): if args.theme_selector: command(client, terminal, 'Collapse') before = set(panes(client)) - command(client, right, 'ThemeSel') + command(client, right, 'Themes') selector = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), - 'ThemeSel missing') + 'Themes missing') body = client.read(f'/pane/{selector}/body').decode() native, legacy = body.split('# Legacy and imported themes', 1) for theme in ('forge_black', 'forge_soft', 'orchard_black'): require('Theme ' + theme + '\n' in native, 'native contrast variant below legacy heading') require('Theme helix\n' in legacy, 'legacy theme section missing') require('Theme orchard' in ''.join(c[0] for c in client.screen()['cells']), - 'ThemeSel did not open at native first entry') + 'Themes did not open at native first entry') take_frame('theme-selector') report['native_themes_first'] = True client.write(f'/pane/{selector}/exec', b'Del\n') @@ -161,7 +161,7 @@ def discovery(binary, embedded=False): with session(binary, root, 'discovery') as (client, address): 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', + assert set(top) - {'src', 'shaders'} == {'README', 'index', 'status', 'focus', 'ctl', 'commands', 'recent', '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 @@ -387,7 +387,7 @@ def discovery(binary, embedded=False): client.write(f'/pane/{unsaved}/ctl', b'Save /nonexistent-pardes-dir/x.txt\n') raise AssertionError('a Save into a missing directory succeeded') except OSError as refused: - assert 'Save /nonexistent-pardes-dir/x.txt: ' in str(refused), refused + assert 'Save /nonexistent-pardes-dir/x.txt: no such directory' in str(refused), refused assert f'err {unsaved} ctl: Save /nonexistent-pardes-dir/x.txt: '.encode() in client.read('/log'), client.read('/log') client.remove(f'/pane/{unsaved}') @@ -494,6 +494,222 @@ def unattended_messages_expire(binary): assert not shown(), 'an unattended message outlived MessageLinger' +def new_terminals_named_once(binary): + """A terminal started outside the session's directory is named by that + directory from its `new` on: no rename to the session's and back.""" + with tempfile.TemporaryDirectory(prefix='pardes-names-') as directory: + root = Path(directory) + (root / 'sub').mkdir() + with session(binary, root, 'names') as (client, _): + made = [look(client, str(root / 'sub'))] + before = {int(row.split()[0]) for row in client.read('/index').splitlines()} + client.write('/pane/1/ctl', b'Tty\n') + if shutil.which('fish'): + client.write('/pane/1/ctl', b'Tty fish\n') + made += [s for s in (int(row.split()[0]) for row in client.read('/index').splitlines()) if s not in before] + time.sleep(1.5) # their shells start and say where they are + # A shell started again by pty/ctl exec: named as it was. + client.write(f'/pane/{made[0]}/pty/ctl', b'exec\n') + time.sleep(1.5) + log = client.read('/log').decode() + for serial in made: + assert f'\nrename {serial} ' not in '\n' + log, (serial, log) + assert f'\nnew {serial} {root}' in '\n' + log, (serial, log) + # A shell that cannot start (its interpreter is not there): Tty + # fails the write with why and leaves no pane; pty/ctl exec + # fails with why and keeps its terminal. + bad = root / 'badsh' + bad.write_bytes(b'#!/nonexistent/interp\n') + bad.chmod(0o755) + panes = client.read('/index') + try: + client.write('/pane/1/ctl', f'Tty {bad}\n'.encode()) + raise AssertionError('Tty of a shell that cannot start was taken') + except OSError as refused: + assert 'shell' in str(refused), refused + time.sleep(.3) + assert client.read('/index').count(b'\n') == panes.count(b'\n'), client.read('/index') + client.write('/ctl', f'Shell {bad}\n'.encode()) + try: + client.write(f'/pane/{made[0]}/pty/ctl', b'exec\n') + raise AssertionError('pty/ctl exec of a shell that cannot start was taken') + except OSError as refused: + assert 'shell' in str(refused), refused + client.write('/ctl', b'Shell\n') + assert str(made[0]).encode() in b' '.join(r.split()[0] for r in client.read('/index').splitlines()) + + +def recent_survives_restart(binary): + """A file opened and closed is in /recent closed, and still is in the + next session: the list is kept under $XDG_STATE_HOME (here HOME's + .local/state) across restarts.""" + with tempfile.TemporaryDirectory(prefix='pardes-recent-') as directory: + root = Path(directory) + kept = root / 'kept.txt' + kept.write_bytes(b'one\ntwo\n') + with session(binary, root, 'recent1') as (client, _): + serial = look(client, f'{kept}:2') + assert f'open {kept}\n' in client.read('/recent').decode() + client.write(f'/pane/{serial}/ctl', b'Del\n') + assert f'closed {kept}\n' in client.read('/recent').decode(), client.read('/recent') + assert (root / '.local' / 'state' / 'pardes' / 'recent').exists() + with session(binary, root, 'recent2') as (client, _): + listed = client.read('/recent').decode() + assert f'closed {kept}\n' in listed, listed + # Recent shows it, and a look at its row opens it again. + client.write('/pane/1/exec', b'Recent\n') + rows = int(client.read('/pane/1/exec').split()[-1]) + row = next(r for r in client.read(f'/pane/{rows}/body').decode().splitlines() if r.startswith(f'{kept}:')) + client.write(f'/pane/{rows}/look', (row.split(' ')[0] + '\n').encode()) + assert f'open {kept}\n' in client.read('/recent').decode() + + +def workflow_canary(binary): + """One session driven as the dogfood agents drive it, asserting the log + and the read-backs at every step: the canary for the main paths (the + round-18 Grep that never found anything went through every narrower + test). Look files, Grep and Find hits and misses, a plain word looked + at, Edit in two files, Save, get, pty/run failing then passing, a + command pane and Kill, a REPL send, the layout and a column's files, + ask/answer, and Dump/Restore last.""" + started = time.monotonic() + with tempfile.TemporaryDirectory(prefix='pardes-flow-') as directory: + root = Path(directory) + (root / 'a.txt').write_bytes(b'alpha one\nshared word\n') + (root / 'b.txt').write_bytes(b'beta two\nshared word\n') + (root / 's.sh').write_bytes(b'echo from-file\n') + + def wait(ready, what, seconds=10): + deadline = time.monotonic() + seconds + while not ready(): + assert time.monotonic() < deadline, what + time.sleep(.05) + + with session(binary, root, 'flow') as (client, address): + def log(): + return client.read('/log').decode('utf-8', 'replace') + + def made_by(serial, line): + """The pane an exec at `serial` made, as exec reads it back.""" + client.write(f'/pane/{serial}/exec', line) + return int(client.read(f'/pane/{serial}/exec').split()[-1]) + + # Look opens files, and the log says new. + a = look(client, str(root / 'a.txt')) + b = look(client, str(root / 'b.txt')) + assert client.read(f'/pane/{a}/body') == b'alpha one\nshared word\n' + assert f'new {a} {root}/a.txt\n' in log() and f'new {b} {root}/b.txt\n' in log() + + # Grep and Find: a hit opens its +Search, rows and all; a miss + # fails ENOENT, opening nothing. + grep = made_by(a, b'Grep shared\n') + rows = client.read(f'/pane/{grep}/body').decode() + assert 'a.txt:2' in rows and 'b.txt:2' in rows, rows + client.remove(f'/pane/{grep}') + execute_refused(client, a, 'Grep zzqqnomatch', 'grep: no match') + found = made_by(a, b'Find b.txt\n') + assert 'b.txt' in client.read(f'/pane/{found}/body').decode() + client.remove(f'/pane/{found}') + execute_refused(client, a, 'Find zzqqnomatch', 'find: no match') + + # A plain word looked at is found in the pane, after its dot. + client.write(f'/pane/{a}/dot', b' 0 0 ') + client.write(f'/pane/{a}/look', b'shared\n') + assert client.read(f'/pane/{a}/dot').split() == [b'10', b'16'], client.read(f'/pane/{a}/dot') + + # Edit in two files, then Save each: on disk, and logged. + for pane in (a, b): + client.write(f'/pane/{pane}/ctl', b'Edit ,s/shared/SHARED/g\n') + assert client.read(f'/pane/{b}/body') == b'beta two\nSHARED word\n' + for pane in (a, b): + client.write(f'/pane/{pane}/exec', b'Save\n') + assert (root / 'a.txt').read_bytes() == b'alpha one\nSHARED word\n' + assert f'save {a} {root}/a.txt\n' in log() and f'save {b} {root}/b.txt\n' in log() + + # get reloads what changed on disk. + (root / 'b.txt').write_bytes(b'from disk\n') + client.write(f'/pane/{b}/ctl', b'get\n') + assert client.read(f'/pane/{b}/body') == b'from disk\n' + + # A terminal: pty/run failing then passing. + term = made_by(1, b'Tty\n') + + def run(line): + fid = client.open(f'/pane/{term}/pty/run', 2) + try: + client.rpc(118, struct.pack('<IQI', fid, 0, len(line)) + line) + return client.read_fid(fid) + finally: + client.close(fid) + deadline = time.monotonic() + 10 + while (answer := run(b'true\n')) != b'exit 0\n': + assert time.monotonic() < deadline, answer + time.sleep(.05) + assert run(b'false\n') == b'exit 1\n' + assert run(b'echo passed\n') == b'exit 0\npassed\n' + # (A run's answer is its record: the log has no err for these.) + assert f'err {term} pty/run' not in log(), log() + + # A command pane, and Kill: its exit is logged. + sleeper = made_by(a, b'sleep 30\n') + assert f'run {sleeper} sleep 30\n' in log() + client.write('/ctl', b'Kill sleep\n') + wait(lambda: f'\nexit {sleeper} ' in log(), 'Kill: no exit logged') + + # A REPL send: a program in the terminal (cat, which says back + # what it is sent; a nested shell reads as the shell itself) + # bound as bash's REPL, an exec on a .sh file's body (its event + # written back) goes there. + s = look(client, str(root / 's.sh')) + client.write(f'/pane/{term}/pty/data', b'cat\r') + wait(lambda: client.read(f'/pane/{term}/pty/status').split()[-1] == b'1', ('the REPL program did not start', client.read(f'/pane/{term}/body')[-300:])) + client.write(f'/pane/{term}/ctl', b'Repl bash\n') + fid = client.open(f'/pane/{s}/event', 2) + try: + record = b'MX0 0 0 14 echo from-file\n' + client.rpc(118, struct.pack('<IQI', fid, 0, len(record)) + record) + finally: + client.close(fid) + wait(lambda: f'\nsend {s} {term} ' in log(), ('no REPL send logged', log()[-800:])) + wait(lambda: client.read(f'/pane/{term}/body').count(b'echo from-file') >= 2, 'the REPL did not get it') + client.write(f'/pane/{term}/ctl', b'Repl -\n') + client.write(f'/pane/{term}/pty/data', b'\x04') + wait(lambda: run(b'true\n') == b'exit 0\n', 'the terminal did not come back to its shell') + + # The layout and a column's files: Newcol, its tag, rmdir. + assert len(client.read('/layout').splitlines()) >= 2 # a column, then `active` + client.write('/ctl', b'Newcol\n') + columns = [row for row in client.read('/layout').splitlines() if not row.startswith(b'active')] + assert len(columns) == 2, columns + empty = columns[-1].split()[0].decode() + assert b'New' in client.read(f'/col/{empty}/tag') + client.remove(f'/col/{empty}') + assert len([row for row in client.read('/layout').splitlines() if not row.startswith(b'active')]) == 1 + + # ask/answer: Save on a terminal asks for a path over 9P. + client.write(f'/pane/{term}/exec', b'Save\n') + wait(lambda: f'ask {term} save path\n' in log(), 'no ask logged') + client.write(f'/pane/{term}/ctl', f'answer {root}/scroll.txt\n'.encode()) + wait(lambda: (root / 'scroll.txt').exists(), 'the answered Save wrote nothing') + assert f'answer {term} {root}/scroll.txt\n' in log() + + # Dump, then Restore it: the new session names its panes. + client.write('/ctl', b'Dump\n') + saved = root / 'flow.dump.zon' + wait(saved.exists, 'Dump wrote nothing') + assert f'dump {saved}\n' in log() + try: + client.write('/ctl', f'Restore {saved}\n'.encode()) + except (EOFError, OSError): + pass + time.sleep(.5) + with Client(address) as restored: + wait(lambda: f'restore {saved}' in restored.read('/log').decode(), 'Restore not logged') + names = restored.read('/index').decode() + assert f'{root}/a.txt' in names and f'{root}/b.txt' in names, names + assert time.monotonic() - started < 30, 'the canary took too long' + + def python_repl_runs_one_line(binary): """A one-line exec sent to Python's own REPL (3.13's pyrepl, which asks for bracketed paste) runs at once, with no second Enter: the Enter @@ -610,6 +826,9 @@ def test(binary, quic=False): run_file(binary) exit_answers(binary) unattended_messages_expire(binary) + new_terminals_named_once(binary) + workflow_canary(binary) + recent_survives_restart(binary) python_repl_runs_one_line(binary) signal_cleans_up(binary) for options, message in [ diff --git a/test/gui-goldens.txt b/test/gui-goldens.txt index 01bdfc7c..2b364b37 100644 --- a/test/gui-goldens.txt +++ b/test/gui-goldens.txt @@ -5,7 +5,7 @@ 05-body-selection 96a158adac982f4141526d10956e19828632edc0f6f60d1161d1353e34797118 06-tag-focus-selection 94aff8499a6e6f399761ffdd56369d526fbe65232eb21c88addd12973af611aa 07-prompt-notice 041603b841605b0a9456cbbb9b3e06e219b0a08a58e2d38229e1c81366577a87 -08-wide-glyph-edges 94b0beb7cc2f2a09259f4f7d57999001ef75ed53ea210a65a1510404bd161af9 +08-wide-glyph-edges 4f15245cb2e14f4190940a79c61751bbb6efbf21c1e3ab423fb1469e0466697e 09-collapsed-focus-tint 1d0e6e4b371bc3b2bba96d9177b91f240fa4bef38c62470dc078fa9feed119ac 10-tagbottom f7175dfb338b75c5ac3457c8422dc0e352c46d51b2c52533ce32ef143fcf6c6b 12-multiline-tag ddd7d775a246e91b88f0eff98b6f9bea8aa5d63dbfbe931bd830ccb9086bd5c4 @@ -14,4 +14,4 @@ 15-theme-acme-light ca5252f5117ac8d6c679cef917c9f316c9ca311fc7a8dcbac2796b4fc04d3b64 16-debug 591bb7c45b4b9c46cd2458330b4fa3962afc6dac827fa6f5e20e927a72c586e6 17-terminal 38299427891e31de0b5597159501c161474620eb26fd7708a6396f0fd685209e -18-mid-transition 80cf350f8736fa4b348225a3872572a799ce57c763939796b68e8c839418f8ee +18-mid-transition 5d3e37d874988b460297e18df9508a77b602d5f675e28983c2c793871d4301f6 diff --git a/test/output.zig b/test/output.zig index d9d41e28..4332fd13 100644 --- a/test/output.zig +++ b/test/output.zig @@ -952,13 +952,13 @@ test "N exactly reverses n across output panes and the ring seam" { try std.testing.expect(saw_first and saw_second); } -test "ThemeSel lists the ring in sections and skips headings in both directions" { +test "Themes lists the ring in sections and skips headings in both directions" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 40 }); defer p.deinit(); const source = try p.setTestFile("notes\n"); pardes.colors.setThemeIndex(p, pardes.themes.len - 1); const original_theme = p.settings.theme; - try std.testing.expect(p.executeBuiltinLine(0, "ThemeSel")); + try std.testing.expect(p.executeBuiltinLine(0, "Themes")); const tid = source.search_pane orelse return error.MissingThemeList; const list = p.panes[tid].?; const content = list.file.?.content; @@ -997,12 +997,12 @@ test "ThemeSel lists the ring in sections and skips headings in both directions" for (pardes.themes[0 .. brk + 1]) |theme| { p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(tid, p.active); - const selected = selectedOutputText(list) orelse return error.MissingThemeSelection; + const selected = selectedOutputText(list) orelse return error.MissingThemesection; try std.testing.expectEqualStrings(theme.name, selected["Theme ".len..]); try std.testing.expectEqual(original_theme, p.settings.theme); } p.update(.{ .key = .{ .cp = 'N' } }); - const previous = selectedOutputText(list) orelse return error.MissingThemeSelection; + const previous = selectedOutputText(list) orelse return error.MissingThemesection; try std.testing.expectEqualStrings(pardes.themes[brk - 1].name, previous["Theme ".len..]); p.update(.{ .key = .{ .cp = Key.tab } }); try std.testing.expectEqual(brk - 1, p.settings.theme); @@ -1017,7 +1017,7 @@ test "n/N selects command outputs by whole row" { defer p.deinit(); p.update(.{ .resize = .{ .cols = 100, .rows = 40 } }); - try std.testing.expect(p.executeBuiltinLine(p.active, "ThemeSel")); + try std.testing.expect(p.executeBuiltinLine(p.active, "Themes")); const tid = p.panes[p.active].?.search_pane orelse return error.MissingThemeList; const themes = p.panes[tid].?; try std.testing.expect(panes.Output.fileTraits(themes.file.?.output).commands); diff --git a/test/panes.zig b/test/panes.zig index 3d2821aa..c1ae1666 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -1622,7 +1622,8 @@ test "pane filename drafts commit and cancel without changing body or command ta try std.testing.expectEqualStrings(tail, pardes.tagline.curTail(pane)); while (p.nextEffect()) |effect| try std.testing.expect(std.meta.activeTag(effect) != .save_file); } - try std.testing.expect(pane.file.?.revision != pane.file.?.saved_revision); + // A name alone is no edit: the pane stays clean. + try std.testing.expect(pane.file.?.revision == pane.file.?.saved_revision); } test "pane filename grapheme edits and invalid empty commits preserve the buffer" { @@ -2500,21 +2501,48 @@ test "Save on a scratch does not watch a path the host could not create" { const id = p.active; while (p.nextEffect()) |_| {} p.host = .{ .ctx = p, .vtable = &.{ .write_file = Refusing.write } }; + const file = &p.panes[id].?.file.?; + const scratch_name = try std.testing.allocator.dupe(u8, file.path); + defer std.testing.allocator.free(scratch_name); + const was_dirty = file.revision != file.saved_revision; try std.testing.expect(p.executeBuiltinLine(id, "Save /new-file.txt")); while (p.nextEffect()) |effect| p.perform(effect); - const file = &p.panes[id].?.file.?; - try std.testing.expect(file.output == null); - try std.testing.expect(file.watch_after_save); - try std.testing.expect(file.revision != file.saved_revision); + // Failed, it changed nothing: still the scratch, its name and dirty + // flag as they were, unwatched. + try std.testing.expect(file.output != null); + try std.testing.expectEqualStrings(scratch_name, file.path); + try std.testing.expectEqual(was_dirty, file.revision != file.saved_revision); try std.testing.expect(!p.fallback.watched[id]); p.host = .{}; - try std.testing.expect(p.executeBuiltinLine(id, "Save")); + try std.testing.expect(p.executeBuiltinLine(id, "Save /new-file.txt")); while (p.nextEffect()) |effect| p.perform(effect); - try std.testing.expect(!file.watch_after_save); + try std.testing.expect(file.output == null); + try std.testing.expectEqualStrings("/new-file.txt", file.path); try std.testing.expectEqual(file.revision, file.saved_revision); try std.testing.expect(p.fallback.watched[id]); } +test "a failed Save to another name leaves a clean named file clean" { + const Refusing = struct { + fn write(ctx: ?*anyopaque, id: u8, _: []const u8, _: []const u8) void { + const p: *Pardes = @ptrCast(@alignCast(ctx.?)); + p.saveFailed(id, "save", error.AccessDenied); + } + }; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 }); + defer p.deinit(); + const pane = try p.setTestFile("clean\n"); + while (p.nextEffect()) |_| {} + const file = &pane.file.?; + try std.testing.expectEqual(file.revision, file.saved_revision); + p.host = .{ .ctx = p, .vtable = &.{ .write_file = Refusing.write } }; + try std.testing.expect(p.executeBuiltinLine(0, "Save /root/x.txt")); + while (p.nextEffect()) |effect| p.perform(effect); + try std.testing.expectEqual(file.revision, file.saved_revision); + try std.testing.expectEqualStrings("/test.txt", file.path); + p.host = .{}; +} + test "unplaced terminal panes keep their allocated grid until placement" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 }); defer p.deinit(); @@ -3438,14 +3466,14 @@ const TerminalTests = struct { .write => return error.GreetedBeforePrompt, else => {}, }; - p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;A\x07prompt$ " } }); + p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;A;aid=pardes\x07prompt$ " } }); try std.testing.expect(!promptInputReady(pane)); while (p.nextEffect()) |effect| switch (effect) { .write => return error.GreetedDuringPrompt, else => {}, }; - p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;B\x07" } }); + p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;B;aid=pardes\x07" } }); try std.testing.expect(promptInputReady(pane)); var greeted = false; while (p.nextEffect()) |effect| switch (effect) { @@ -3456,6 +3484,30 @@ const TerminalTests = struct { try std.testing.expect(!pane.greet); } + test "the greeting is dropped, never typed later, once something ran or was typed first" { + if (pardes.platform == .web) return; + for ([_][]const u8{ + // A command ran before the first prompt was seen at rest. + "\x1b]133;A;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07\x1b]133;C;aid=pardes\x07out\r\n\x1b]133;D;0;aid=pardes\x07\x1b]133;A;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07", + // Someone typed at the first prompt. + "\x1b]133;A;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07vim", + }) |output| { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + const pane = p.panes[0].?; + while (p.nextEffect()) |_| {} + p.update(.{ .resize = .{ .cols = 80, .rows = 24 } }); + p.update(.{ .output = .{ .pane = 0, .bytes = output } }); + // and a later, quiet prompt changes nothing + p.update(.{ .output = .{ .pane = 0, .bytes = "\r\n\x1b]133;A;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07" } }); + while (p.nextEffect()) |effect| switch (effect) { + .write => |write| if (std.mem.eql(u8, write.bytes.slice(), "ls\r")) return error.GreetedLate, + else => {}, + }; + try std.testing.expect(!pane.greet); + } + } + test "fresh-shell commands preserve order and wait for OSC 133 B" { if (pardes.platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); diff --git a/test/snapshots/find.golden b/test/snapshots/find.golden index dec8ed1d..a6eae9cf 100644 --- a/test/snapshots/find.golden +++ b/test/snapshots/find.golden @@ -75,13 +75,17 @@ |3: 1 Find Find / |4: |13: 1 gamma body Last -== snap nomatch grid=120x41 cursor=7,8 +== snap nomatch grid=120x41 cursor=7,3 |3: 1 Find +|6: 1 +|7: +|8: |13: 1 gamma body -|18: /tmp/pardes-snap/find/cwd/+Search Save Tty Collapse Del -|19: 1 == snap refit grid=120x41 cursor=7,3 +|6: 1 sub/ZQMARK-beta.txt +|7: 2 sub/nest/zqmark-gamma.txt +|8: 3 zqmark-alpha.txt == snap topbar grid=120x41 cursor=12,16 -|21: /tmp/pardes-snap/find/cwd/+Search Save Tty Collapse Del -|22: 1 zqmark-alpha.txt -|23: 2 +|6: 1 zqmark-alpha.txt +|7: 2 +|8: diff --git a/test/snapshots/shellset.golden b/test/snapshots/shellset.golden index e2830801..db72bfc8 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 Shel Shell: no shell "zznosuchshell" (a name on the usual paths, or a path to one) +|3: hell: no shell "zznosuchshell", not found (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/stepgrain.golden b/test/snapshots/stepgrain.golden index ef3cc49b..7f18d246 100644 --- a/test/snapshots/stepgrain.golden +++ b/test/snapshots/stepgrain.golden @@ -51,17 +51,23 @@ |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-19 #f5f7ee,#3c614a, 20-149 d,#0d1410, == snap step-row3 grid=150x31 cursor=7,10 == snap step-wrap grid=150x31 cursor=7,6 -== snap blank-in-path grid=150x31 cursor=7,13 -|12: /tmp/pardes-snap/stepgrain/cwd/+Search Save Tty Collapse Del -|13: 1 two words.txt -|14: 2 +== snap blank-in-path grid=150x31 cursor=7,6 +|5: /tmp/pardes-snap/stepgrain/cwd/+Search Save Tty Collapse Del +|6: 1 two words.txt +|7: 2 +|8: +|9: +|10: +|11: +|12: == style blank-in-path grid=150x31 -|5: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-40 #d3b0d9,#1d2824, 41-78 #b2c3b8,#1d2824, 79-149 d,#1d2824, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #f5f7ee,#3c614a, 19-149 d,#0d1410, -|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-19 #59675c,#0d1410, 20-149 d,#0d1410, -|11: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #f7e4a5,#5e4923, 13-23 #e5eee5,#0d1410, 24-149 d,#0d1410, -|12: 0-1 #000000,#add38a, 2-2 d,#314738, 3-33 #becfc4,#314738, 34-40 #dfbce5,#314738, 41-62 #becfc4,#314738, 63-149 d,#314738, -|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-19 #f5f7ee,#3c614a, 20-149 d,#0d1410, -== snap blank-opened grid=150x31 cursor=7,16 -|15: /tmp/pardes-snap/stepgrain/cwd/two words.txt Save Tty Collapse Del -|16: 1 +|5: 0-1 #000000,#add38a, 2-2 d,#314738, 3-33 #becfc4,#314738, 34-40 #dfbce5,#314738, 41-62 #becfc4,#314738, 63-149 d,#314738, +|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-19 #f5f7ee,#3c614a, 20-149 d,#0d1410, +|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410, +|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410, +|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410, +|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410, +|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410, +== snap blank-opened grid=150x31 cursor=7,9 +|8: /tmp/pardes-snap/stepgrain/cwd/two words.txt Save Tty Collapse Del +|9: 1 diff --git a/test/snapshots/themesel.golden b/test/snapshots/themesel.golden index 75100f0f..f553fee9 100644 --- a/test/snapshots/themesel.golden +++ b/test/snapshots/themesel.golden @@ -31,7 +31,7 @@ | 27 line 27 | 28 line 28 == snap list grid=100x31 cursor=7,3 -|3: 1 line 1 ThemeSel +|3: 1 line 1 Themes |16: /tmp/pardes-snap/themesel/cwd/+Themes Save Tty Collapse Del |17: 1 # Pardes themes |18: 2 Theme orchard diff --git a/test/snapshots/themesel.snap b/test/snapshots/themesel.snap index fa34f030..01e4dc5e 100644 --- a/test/snapshots/themesel.snap +++ b/test/snapshots/themesel.snap @@ -1,7 +1,7 @@ # RULE: a results buffer is a list of things you can RUN, and its traits say # what a ROW IS. Every list before this one held LOCATIONS — a row with a # `path:LINE:COL` word inside it — so n/N select that word and Enter Looks it. -# `SPC t t` (ThemeSel) opens the first one whose rows are COMMANDS: `Theme +# `SPC t t` (Themes) opens the first one whose rows are COMMANDS: `Theme # <name>`, exactly the line you would type, with no path in it to pick out, so # n/N select the WHOLE ROW. One motion, two grains, and the same split either # way — the step only SELECTS, and Tab (Exec, or the middle button, which is |
