summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.agents/skills/pardes-9p/SKILL.md105
-rw-r--r--build.zig.zon4
-rw-r--r--docs/config.md22
-rw-r--r--docs/fs.md204
-rw-r--r--docs/helix-keys.md2
-rw-r--r--docs/macos.md4
-rw-r--r--docs/selections.md27
-rw-r--r--docs/tags.md120
-rw-r--r--docs/themes.md1
-rw-r--r--docs/ui-review.md3
-rw-r--r--src/9p_io.zig40
-rw-r--r--src/CHANGELOG.md18
-rw-r--r--src/File.zig9
-rw-r--r--src/Messages.zig2
-rw-r--r--src/Mini.zig2
-rw-r--r--src/Output.zig10
-rw-r--r--src/Terminal.zig50
-rw-r--r--src/body_layer.zig13
-rw-r--r--src/builtins.zig117
-rw-r--r--src/colors.zig4
-rw-r--r--src/config.zig48
-rw-r--r--src/detached/server.zig16
-rw-r--r--src/draw.zig81
-rw-r--r--src/dump.zig51
-rw-r--r--src/edit.zig214
-rw-r--r--src/exec.zig185
-rw-r--r--src/fs-help.txt16
-rw-r--r--src/fs.zig11
-rw-r--r--src/gui/gui.zig86
-rw-r--r--src/host_io.zig247
-rw-r--r--src/layout.zig238
-rw-r--r--src/look.zig203
-rw-r--r--src/macos.zig22
-rw-r--r--src/mouse.zig126
-rw-r--r--src/ninep/addr.zig56
-rw-r--r--src/ninep/ctl.zig252
-rw-r--r--src/ninep/events.zig33
-rw-r--r--src/ninep/pane.zig73
-rw-r--r--src/ninep/pty.zig101
-rw-r--r--src/ninep/testing.zig9
-rw-r--r--src/ninep/tree.zig56
-rw-r--r--src/panes.zig38
-rw-r--r--src/pardes.zig747
-rw-r--r--src/pdf_view.zig2
-rw-r--r--src/regexp.zig52
-rw-r--r--src/sam_edit.zig717
-rw-r--r--src/surface.zig4
-rw-r--r--src/tagline.zig146
-rw-r--r--src/themes/acme.zig2
-rw-r--r--src/tty/tty.zig11
-rw-r--r--src/web/app.mjs3
-rw-r--r--test/appearance_gallery.py4
-rw-r--r--test/column_drag.zig5
-rw-r--r--test/column_tags.py5
-rw-r--r--test/fs.py31
-rw-r--r--test/gui-goldens.txt19
-rw-r--r--test/gui_golden.py3
-rw-r--r--test/panes.zig385
-rw-r--r--test/pdf.zig6
-rw-r--r--test/snapshot.zig3
-rw-r--r--test/snapshots/badutf.golden5
-rw-r--r--test/snapshots/builtins.golden105
-rw-r--r--test/snapshots/builtins.snap4
-rw-r--r--test/snapshots/chordcut.golden134
-rw-r--r--test/snapshots/chordcut.snap121
-rw-r--r--test/snapshots/cmdexit.golden6
-rw-r--r--test/snapshots/cmdexit.snap6
-rw-r--r--test/snapshots/cmdword.golden26
-rw-r--r--test/snapshots/colgrip.golden63
-rw-r--r--test/snapshots/cornerdrag-left.golden107
-rw-r--r--test/snapshots/cornerdrag.golden134
-rw-r--r--test/snapshots/delask.golden18
-rw-r--r--test/snapshots/delask.snap10
-rw-r--r--test/snapshots/doccol.golden32
-rw-r--r--test/snapshots/doccol.snap18
-rw-r--r--test/snapshots/dump.golden24
-rw-r--r--test/snapshots/dump.snap4
-rw-r--r--test/snapshots/edit.golden23
-rw-r--r--test/snapshots/emptycol.golden17
-rw-r--r--test/snapshots/emptycol.snap22
-rw-r--r--test/snapshots/exec-bigdrag.golden94
-rw-r--r--test/snapshots/fileedit.golden2
-rw-r--r--test/snapshots/filewatch.golden74
-rw-r--r--test/snapshots/filewatch.snap12
-rw-r--r--test/snapshots/find.golden79
-rw-r--r--test/snapshots/find.snap22
-rw-r--r--test/snapshots/fsearch.golden114
-rw-r--r--test/snapshots/ghostty-mid.golden5
-rw-r--r--test/snapshots/ghostty-mid.snap8
-rw-r--r--test/snapshots/grep.golden24
-rw-r--r--test/snapshots/hscroll.golden29
-rw-r--r--test/snapshots/images.golden87
-rw-r--r--test/snapshots/jumps.golden110
-rw-r--r--test/snapshots/jumps.snap9
-rw-r--r--test/snapshots/layout-close.golden74
-rw-r--r--test/snapshots/layout-close.snap10
-rw-r--r--test/snapshots/layout-open.golden39
-rw-r--r--test/snapshots/layout-open.snap8
-rw-r--r--test/snapshots/leader.golden145
-rw-r--r--test/snapshots/leader.snap9
-rw-r--r--test/snapshots/load.golden27
-rw-r--r--test/snapshots/look-center.golden6
-rw-r--r--test/snapshots/look-dir.golden13
-rw-r--r--test/snapshots/look-file-1col.golden2
-rw-r--r--test/snapshots/look-file.golden4
-rw-r--r--test/snapshots/lookloaded.golden40
-rw-r--r--test/snapshots/lookpanes.golden134
-rw-r--r--test/snapshots/lookpanes.snap20
-rw-r--r--test/snapshots/lookrange.golden184
-rw-r--r--test/snapshots/lsp-client-edit.golden5
-rw-r--r--test/snapshots/lsp-client.golden27
-rw-r--r--test/snapshots/lsp-rename.golden4
-rw-r--r--test/snapshots/lsp.golden22
-rw-r--r--test/snapshots/lspcomplete.golden30
-rw-r--r--test/snapshots/lspcompletemoved.golden2
-rw-r--r--test/snapshots/lsprelpath.golden153
-rw-r--r--test/snapshots/lsprelpath.snap6
-rw-r--r--test/snapshots/mini.golden157
-rw-r--r--test/snapshots/modal.golden59
-rw-r--r--test/snapshots/msg.golden2
-rw-r--r--test/snapshots/multicursor.golden4
-rw-r--r--test/snapshots/nested.golden17
-rw-r--r--test/snapshots/ninep.golden16
-rw-r--r--test/snapshots/pipe.golden3
-rw-r--r--test/snapshots/psearch.golden39
-rw-r--r--test/snapshots/respawn.golden8
-rw-r--r--test/snapshots/respawn.snap4
-rw-r--r--test/snapshots/rsearch.golden72
-rw-r--r--test/snapshots/rsearch.snap50
-rw-r--r--test/snapshots/savepane.golden30
-rw-r--r--test/snapshots/scroll.golden43
-rw-r--r--test/snapshots/scroll.snap8
-rw-r--r--test/snapshots/scrolloff.golden96
-rw-r--r--test/snapshots/scrolloff.snap9
-rw-r--r--test/snapshots/selarg.golden17
-rw-r--r--test/snapshots/selarg.snap30
-rw-r--r--test/snapshots/selregex.golden3
-rw-r--r--test/snapshots/shellset.golden9
-rw-r--r--test/snapshots/shrinktag.golden32
-rw-r--r--test/snapshots/splitparent.golden20
-rw-r--r--test/snapshots/splitparent.snap2
-rw-r--r--test/snapshots/stepgrain.golden96
-rw-r--r--test/snapshots/tagalign.golden95
-rw-r--r--test/snapshots/tagalign.snap4
-rw-r--r--test/snapshots/tagalign2col.golden41
-rw-r--r--test/snapshots/tagbottom.golden12
-rw-r--r--test/snapshots/tagbottom.snap8
-rw-r--r--test/snapshots/tagcmd.golden6
-rw-r--r--test/snapshots/tagnav.golden81
-rw-r--r--test/snapshots/tagpath.golden10
-rw-r--r--test/snapshots/theme.golden368
-rw-r--r--test/snapshots/themesel.golden321
-rw-r--r--test/snapshots/tinywin.golden23
-rw-r--r--test/snapshots/topbarexec.golden42
-rw-r--r--test/snapshots/topbarexec.snap6
-rw-r--r--test/snapshots/tty.golden51
-rw-r--r--test/snapshots/ttyclick.golden17
-rw-r--r--test/snapshots/ttyedit.golden41
-rw-r--r--test/snapshots/ttyedit.snap4
-rw-r--r--test/snapshots/ttyfollow.golden40
-rw-r--r--test/snapshots/ttyhelp.golden37
-rw-r--r--test/snapshots/ttylook.golden63
-rw-r--r--test/snapshots/ttypaste.golden11
-rw-r--r--test/snapshots/ttyprompt.golden25
-rw-r--r--test/snapshots/ttytaken.golden18
-rw-r--r--test/snapshots/winch.golden18
-rw-r--r--test/snapshots/windownav.golden62
-rw-r--r--test/snapshots/windowops.golden137
-rw-r--r--test/snapshots/wrap.golden43
-rw-r--r--test/snapshots/yankpaste.golden2
170 files changed, 6600 insertions, 3388 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index 0643e209..39a6b1ce 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -16,9 +16,10 @@ relative to that root unless linked.
Check `$NINE_MOUNT` first. `9ns --mntgen` sets it for every process it starts
(an interactive shell on this machine runs inside one), so a set `$NINE_MOUNT`
means the posted-9P registry is mounted there, usually `/mnt/9p`, and each
-running editor is a directory `$NINE_MOUNT/pardes/<pid>/`. Inside a pane,
-`$PARDES_9P` is that session's socket (`/run/user/1000/pardes-9p-<pid>.sock`),
-which names the directory, and `$PARDES_PANE` is the calling pane's serial:
+running editor is a directory `$NINE_MOUNT/pardes/<pid>/` (a `--detach=NAME`
+session's is `pardes/NAME/`). Inside a pane, `$PARDES_9P` is that session's
+socket (`/run/user/1000/pardes-9p-<pid or NAME>.sock`), which names the
+directory either way, and `$PARDES_PANE` is the calling pane's serial:
```sh
[ -n "$NINE_MOUNT" ] || echo 'no 9P mount: use the Python client below'
@@ -39,8 +40,9 @@ the `pid`, `version` and `panes` of a session new enough to serve it.
machine. It is not the per-pane kernel mount the `Tty9p` builtin makes, which
gives one pane's shell `$PARDES_MOUNT`; see [docs/v9fs.md](../../../docs/v9fs.md)
for that. Either mountpoint serves the same tree. A `9ns --unix SOCKET -- cmd`
-mount exists only inside `cmd`'s private namespace; to share one, use the
-registry (`9ns --mntgen`).
+mount exists only inside `cmd`'s private namespace, and there `$NINE_MOUNT` is
+the session's own root (`$NINE_MOUNT/index`), not a registry; to share one,
+use the registry (`9ns --mntgen`).
A running session serves whatever binary started it. If a listing does not
match this document, that session predates the change; restart it.
@@ -51,7 +53,10 @@ match this document, that session predates the change; restart it.
$m/README the served guide, worth reading first
$m/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name
$m/status pid, version, panes
-$m/look write a line = a right click on it at the active pane
+$m/look write a line = a right click on it at the active pane (file:/re/, file:#n, :/re/
+ select by address, as acme's look, from the file's dot: file:0/re/ for the
+ first match; file:N selects the line; a miss changes nothing, logs err, and
+ look reads back empty)
$m/exec write a line = a middle click: an editor command word, or a shell line
$m/pane/<n>/pty/run write one line, read `exit N` + its output, or `busy` / `error ...`, on the same open:
exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3
@@ -59,24 +64,28 @@ $m/pane/<n>/pty/run write one line, read `exit N` + its output, or `busy` / `e
$m/log recent events, then EOF: new|del|rename|save <serial> <name>, msg <serial|-> <text>,
run <serial> <line>, exit <serial> <N|?>, send <from> <to> <repl-id>,
dump|restore <path>, err <serial|-> <file>: <why>
- (exec 3<>$m/log; echo follow >&3; cat <&3 waits for new ones; tail -f does not)
+ (exec 3<>$m/log; echo follow >&3; cat <&3 waits for new ones; tail -f does not;
+ while read -r line <&3; do ...; done loses nothing)
$m/screen the rendered screen as JSON, frozen per open
$m/listeners this session's dial addresses
$m/focus the serial of the pane with the keyboard (empty while a column/workspace tag has it);
echo a serial into it to move the keyboard (a folded pane stays folded)
$m/ctl the settings, one a line as a write takes them; write a setting or a session builtin
- (Newcol, Dump, Theme x; Exit QUITS the editor, Kill [word...] stops the
+ (Newcol makes an empty column, Dump, Theme x; Exit QUITS the editor, Kill [word...] stops the
commands pardes started (command panes, lines it typed into shells), a word
- matching a command line's first word; Exit and Restore refuse once per
- unsaved pane, `<name>: Modified (Exit again to discard)`, and the same
+ matching a command line's first word; Exit and Restore refuse once,
+ naming every unsaved pane, `<a>, <b>: Modified (Exit again to discard them all)`, and the same
word again DISCARDS that text -- not a retry, unlike lock's `file in use`;
Kill signals only the foreground job, so of `sleep 30; echo done` the
echo still runs; Joincol needs a column to the right of the keyboard's);
a pane's builtins (Del, Save f, Collapse, which folds that pane, and the
column word Delcol, which closes that pane's column) go to $m/pane/<n>/ctl
$m/commands every builtin: `Word`, `Word arg`, then `root` or `pane` (which ctl takes it)
-$m/pane/new open it to make a pane (a scratch named <dir>/+New), read names it;
- rmdir $m/pane/<n> closes it; closing a column's last pane leaves a +New there
+$m/pane/new open it to make a pane (a scratch named <dir>/+New), read names it; it goes in
+ the ACTIVE column (the one last typed or clicked in, or Newcol's), filling it if
+ empty, else taking the bottom half of its last pane (ctl `Placement pardes`: the old rules);
+ rmdir $m/pane/<n> closes it; a column's last pane leaves the column EMPTY
+ (focus reads empty, the log says only del), and the session's LAST pane QUITS it
$m/pane/<n>/errors write-only: text appended to the +Errors pane of the pane's directory
$m/os/ the host filesystem
```
@@ -136,9 +145,13 @@ but never a 9P write: a script sends code by writing the REPL pane's
`pty/data`, and runs a command from such a file with `Exec <text>` or the
tag. A bound terminal's `ctl` line ends with its id (`python-a`); an event
record written back from such a file's body goes to its REPL, as the click
-would. `Repl -` unbinds; a bare `Repl` says the binding. Multi-line code
+would. `Repl -` unbinds; a bare `Repl` says the binding. With several
+REPLs bound for a language an exec asks which, logged `ask <serial> repl a b`
+(Del's side from the keyboard is `ask <serial> del k j`): answer with
+`echo 'answer a' > $m/pane/<serial>/ctl`, or `answer -` to send nothing. Multi-line code
written to `pty/data` should be a bracketed paste, `\e[200~<code>\e[201~`
-then `\r`: sent line by line, a blank line ends a Python block, and Python
+then `\r`, and a second `\r` when the code's last line is indented (a `def`
+or `for` body): one Enter leaves such a block open. Sent line by line, a blank line ends a Python block, and Python
3.14's REPL auto-indents each line it is typed. Every refused 9P write adds an `err <serial|->
<file>: <why>` record to `$m/log`; through a mount the write itself only says
`Invalid argument`.
@@ -147,6 +160,28 @@ then `\r`: sent line by line, a blank line ends a Python block, and Python
For a file or scratch pane, with `pane=$m/pane/<serial>`:
+Rename everywhere, or any sam edit, is one write: `echo 'Edit ,x/foo/c/bar/'
+> $pane/ctl`. `Edit` takes sam's command language (acme's): addresses, `x y g
+v c a i d s p = m t u` and `{ }` (commands in braces one to a line, so from
+exec or a tag, not a one-line ctl write). Its changes are one undo step, and
+one that fails changes nothing and fails the write with acme's words (`Edit:
+no substitution`), logged as `err`. A block goes in one write to the pane's
+`ctl`, the root `ctl` (the active pane) or `exec`: an `Edit` line takes the
+lines after it until its `{` closes or its `a`/`c`/`i` text ends with `.`.
+bash's builtin `printf` writes line by line, so use a heredoc or `env printf`:
+
+```sh
+cat > $pane/ctl <<'END'
+Edit ,x/area_of/{
+i/[/
+a/]/
+}
+END
+env printf 'Edit ,x/foo/{\ni/</\na/>/\n}\n' > $pane/ctl
+```
+ `p` and `=` print to the directory's
+`+Errors`. Not there: `b B D e r w f X Y`, `< | >`, and `\1`-`\9` in `s`.
+
| Operation | Shell | Python client |
|---|---|---|
| Read text | `cat $pane/body` | `client.read(pane + '/body')` |
@@ -161,16 +196,20 @@ inserts after the first: write `addr` again before each replacement.
Truncating `data` is pardes's own (acme ignores OTRUNC and always inserts).
| Read the selection | `cat $pane/dot` (offsets), `cat $pane/sel` (text) | the same two reads |
| Select the addressed range | `cp $pane/addr $pane/dot` | `client.write(pane + '/dot', client.read(pane + '/addr'))` |
-| Reload from disk | `echo get > $pane/ctl` | `client.write(pane + '/ctl', b'get\n')` |
+| Reload from disk | `echo get > $pane/ctl` (refused once while there are unsaved edits; again discards) | `client.write(pane + '/ctl', b'get\n')` |
| Close it | `rmdir $pane` | `client.remove(pane)` |
`addr`, `dot` and `limit` each read the pair of offsets they also accept, which
is why copying one onto another is all that acme's `addr=dot`, `dot=addr` and
`limit=addr` ever were; a write may also be an address expression (`#0,#5`,
-`/pattern/`, `2+1`), whose regexps are mvzr's searched as sam searches:
+`/pattern/`, `2+1`, or pardes's `12:5`: line 12, byte column 5, composing as
+`12:5,14:1`), whose regexps are mvzr's searched as sam searches:
`^`/`$` match at any line's start and end, `.` and `[^...]` never match a
newline, the leftmost match wins (the first alternative there, not the
-longest). An expression is evaluated from the current address (the last one
+longest). In a pattern with `\n`, `^` works only first (`^def .*\n` finds
+every def line) and `$` only just before a `\n`; anywhere else the pattern is
+refused, not silently unmatched. `^` inside an alternation (`^def|^ `) holds
+only where the search starts: fine in `x` over lines or `g`, not mid-line. An expression is evaluated from the current address (the last one
written, or just past the last `data` write): `.` is that address, not the
selection, `/re/` searches on from its end and wraps unless `limit` is set,
`?re?` or `-/re/` searches back, `#100,#50` fails `addresses out of order`,
@@ -179,16 +218,24 @@ past a step budget (about 300 ms) fails with `regular expression search
gave up, ...`. A failed address says
why (`no match for regexp`, `address out of range`) and leaves no address:
`data` refuses until the next good one, so a missed target is never
-written at the old one. Moving `dot` scrolls the pane to it. `limit` bounds a
-search and reads empty until set; truncate it to lift it.
+written at the old one. Moving `dot` scrolls the pane to it. `limit` bounds
+only the end of a forward search, as in acme, and reads empty until set;
+truncate it to lift it. In `12:5` the column counts bytes from 1 and clamps
+past the end of the line; a line past the end is `address out of range`.
+A refused write repeated the same way is one `err` line counted, `(x4)`.
`dirty`, `mark` and `scroll` read `0` or `1` and take `0` or `1`: whether the
buffer differs from its file, whether a write pushes an undo point, and whether
-a write scrolls. Truncating `tag` clears the part of the tag you may edit.
+a write scrolls. `tag` reads the path, then the text you may edit: `> tag`
+replaces that text (default words too), `>> tag` appends to it.
Address state belongs to the pane, not to a client: it keeps the last range
-written until someone writes or truncates it, so writing an address and reading
-it back evaluates it, and two clients addressing the same pane will interfere.
+written until someone writes another, so writing an address and reading it back
+evaluates it, and two clients addressing the same pane will interfere. Neither
+an open nor a `>` resets it (acme resets it on the first open): each `echo /re/
+> addr` searches on from the last address, so a find-and-replace loop advances.
+Write `0` to start from the top. The search wraps, so stop a find-all loop when
+the address comes back to where it began, or set `limit`.
`$pane/ctl` reads acme's window status line — serial, tag length, body length,
a reserved zero, the dirty flag, the width in cells, the font and the tab
width, then `current` or `notcurrent` — and takes `get` (reload from disk),
@@ -252,15 +299,14 @@ A Restore puts a new editor under every client: the Restore write is
answered, then every connection is hung up (their fids name the old
editor's panes); dial again, and the new log names the restored panes then
`restore <path>` -- the authority, since a slow client may see the cut
-before the answer. Through a 9ns mount the Restore write itself usually
-fails with ECONNRESET although the Restore went ahead: 9ns fails a request
-whose reply is already in when the hang-up breaks its next send. Trust the
-log. `Dump` writes `pardes-<date>-<time>.zon`, the time in UTC, under
+before the answer. A 9ns older than cloud9 2a7137c could fail the Restore
+write with ECONNRESET although the Restore went ahead; trust the log. `Dump` writes `pardes-<date>-<time>.zon`, the time in UTC, under
`DumpDir` (`$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`) and logs
`dump <path>`.
Read the event implementation before building an interceptor. Close handles in
-`finally`, and disconnect after a socket timeout. The service shares four
-connection slots and 32 screen/terminal-history snapshot handles.
+`finally`, and disconnect after a socket timeout. The service shares sixteen
+connection slots (the next client's version gets `too many connections`)
+and 32 screen/terminal-history snapshot handles.
A file's qid version is the pane's revision for `body`, `data` and `xdata`, so
`stat` sees an edit land without reading the text; it stays zero elsewhere.
@@ -274,7 +320,8 @@ Only terminal panes have `pty/`. Write keystroke bytes to `pty/data`, not
command. For an interactive application, send its actual input bytes; `b'\x03'`
is Ctrl-C, and Ctrl-U is `b'\x15'` where that application supports it. These go
to the child terminal, not to Pardes key bindings. `pty/ctl` takes `winsize C R`,
-`sig INT` and `exec`, one per line.
+`sig INT` and `exec`, one per line. `pty/status` reads one line: the pty's cols,
+rows and busy (1 while a command runs or text is typed at the prompt).
`client.screen()` returns `cols`, `rows`, `cursor`, `styles` and row-major
`cells` of `[grapheme, style_index]`. Reconstruct rows using `cols`; resolve
diff --git a/build.zig.zon b/build.zig.zon
index 4cfba514..877dfacb 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -9,8 +9,8 @@
// read-only HTTPS URL is what a manifest can carry. Re-pin with
// `zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#<commit>`.
.cloud9 = .{
- .url = "git+https://git.sr.ht/~gbrls/cloud9#a8c7a7158cc9d3bde78e34db6de11c084fde3d65",
- .hash = "cloud9-0.1.0-yt86qjLsHQC_9qrMuOQEcDOan4KTH6ermuxZvm0kgwpL",
+ .url = "git+https://git.sr.ht/~gbrls/cloud9#f35b7ed09d0a170e4ffe55b5286c28f74c9a8760",
+ .hash = "cloud9-0.1.0-yt86quEKHgCwewf5XRlEZji1JCmdsfufsrZYGZqzHj5_",
},
// ZLS as a LIBRARY, not a language server: src/lsp_zls.zig imports the
// `zls` module its build.zig publishes and calls the analyser in
diff --git a/docs/config.md b/docs/config.md
index 921f817e..0332d854 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -111,9 +111,16 @@ themes and all imported names remain available.
`FocusTint` toggles the focused pane and column tag tints; it is on by default.
Workspace, column and pane command text can be edited directly; see
[editable tags](tags.md) for naming and saved-workspace behavior.
-`ColumnTags` toggles the column command row in both GUI and TTY; it is on by
-default. Add `ColumnTags` to startup configuration to reclaim that row on a
-compact screen. Hiding it preserves your custom column commands.
+The column command row is always shown; an old `ColumnTags` init line is
+ignored with a message.
+`Placement acme` (the default) puts new panes where acme would, in the active
+column; `Placement pardes` brings back pardes's own rules, which open a first
+document in a column of its own. See [where new panes go](tags.md#where-new-panes-go).
+`BootShell keep` (the default) leaves a shell alone when a document is dragged
+into the left column beside it, whatever it holds; `BootShell replace` closes
+such a shell there when it is the column's only one and nobody has typed
+into it (no scrollback, cursor still on the first prompt line), the boot's
+placeholder giving its rows to the document. Bare `BootShell` flips it.
`Verbose` toggles the message-row announcement every builtin makes of its own
name before it runs; it is on by default, and the builtins that own the message
row themselves (`Msg`) never announce. Turning it off leaves the row to the
@@ -314,7 +321,7 @@ name, thirteen required RGB roles (`sel_bg`, `sel_fg`, `tag_bg`, `tag_fg`,
`box`, `box_dim`, `kw`, `str`, `num`, `comment`, `lineno`, `scroll_track`,
`scroll_thumb`), nullable `bg`/`fg`, and either a 16-color RGB `palette` or
`null`. Optional nullable RGB roles extend this format:
-`tag_active_bg`, `tag_active_fg`, `tag_name_fg`, `tag_active_name_fg`, `border`, `lineno_active`, `search_bg`, `search_fg`,
+`tag_active_bg`, `tag_active_fg`, `tag_name_fg`, `tag_active_name_fg`, `border`, `empty_col`, `lineno_active`, `search_bg`, `search_fg`,
`diagnostic_error`, `diagnostic_warning`, `diagnostic_info`,
`diagnostic_hint`, `tag_sel_bg`, `tag_rule`, `box_border` and `box_dirty`;
`sweep_bg` and `sweep_fg` take three triples, and `rule_px` and `rail_px` a
@@ -626,7 +633,10 @@ writes a timestamped file in the default directory.
A restored terminal comes back live: its recorded output (the last MiB of it)
is replayed as history, a dim `── restored history ──` line marks where it
-ends, and a new shell starts below it in the directory the old one was in. A
+ends, and a new shell starts below it in the directory the old one was in,
+the same shell it ran (a `Tty bash` pane comes back running bash). A
view left scrolled back stays where it was. Only the shell is new; nothing the
-old one was running is restarted. The web shell, which has no ptys, still
+old one was running is restarted. A command pane comes back finished, with
+what it printed and its `exit N`, and is not run again (a command still
+running when dumped comes back as `exit ?`). The web shell, which has no ptys, still
shows the history alone.
diff --git a/docs/fs.md b/docs/fs.md
index 86f44501..45b04675 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -45,8 +45,10 @@ IPv4/IPv6 addresses, not DNS names. Listener port zero chooses a free port;
All connections have session access, including `os`. TCP is unencrypted.
QUIC uses an ephemeral TLS identity without peer verification or login.
It carries 9P2000 on one bidirectional stream with ALPN `pardes-9p`.
-Unix and TCP connections share four slots served by cloud9's `std.Io`
-runner; QUIC has four of its own on the editor's poll loop. OpenSSL's
+Unix and TCP connections share sixteen slots served by cloud9's `std.Io`
+runner; QUIC has sixteen of its own on the editor's poll loop. A client
+that finds every Unix/TCP slot taken gets an Rerror `too many connections`
+to its Tversion, and the log an `err - 9p: too many connections` record. OpenSSL's
internal buffers are separate, dynamically allocated memory.
[Plan9port's client](https://9fans.github.io/plan9port/man/man1/9p.html) can
@@ -59,15 +61,23 @@ private namespace:
9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"'
```
+(Under `9ns --unix`, `$NINE_MOUNT` is that session's root itself.)
+
A `9ns --unix` mount lives in the private namespace of the command it runs,
and nothing outside that command sees it. The mount everyone on the machine
shares is the registry one, `9ns --mntgen` (default `/mnt/9p`): every running
editor posts itself there, so `$NINE_MOUNT/pardes/<pid>/` is that editor's tree
-for any process. 9ns exports `$NINE_MOUNT` to everything it starts, so a
-script checks that variable to know the mount is there, and takes `<pid>` from
-the name of `$PARDES_9P` (`pardes-9p-<pid>.sock`). A new pane made through `pane/new` is a scratch named
-`<dir>/+New` until it is given a name, and closing a column's last pane
-leaves such a `+New` in its place (`Delcol` closes the column).
+for any process, and `$NINE_MOUNT/pardes/NAME/` a `--detach=NAME` session's.
+9ns exports `$NINE_MOUNT` to everything it starts, so a script checks that
+variable to know the mount is there, and takes the name from `$PARDES_9P`
+(`pardes-9p-<pid or NAME>.sock`). A new pane made through `pane/new` is a scratch named
+`<dir>/+New` until it is given a name. A column may hold no pane, as in acme:
+`Newcol` makes one empty, and closing a column's last pane leaves it empty
+with its tag holding the keyboard (`focus` reads empty) and logs only the
+`del`. `pane/new` places its pane as acme's makenewwindow(nil) does: in the
+active column, filling it when it is empty, else taking the bottom half of its
+last pane ([where new panes go](tags.md#where-new-panes-go)). `Delcol` closes the column. Closing the
+session's last pane quits pardes; see [tags](tags.md#empty-columns).
For [Linux v9fs](https://www.kernel.org/doc/html/latest/filesystems/9p.html),
use `version=9p2000,cache=none,access=any` and `trans=unix`, or `trans=tcp`
@@ -117,9 +127,11 @@ asks the same first -- `Dump` writes `pardes-<date>-<time>.zon` (UTC) in
and logs `dump <path>`, and `Restore` with no path takes the last one; a
Restore puts a new editor under every client, so the write of it is
answered and then every connection is hung up, their fids naming the old
-editor's panes (through a 9ns mount the write usually fails with
-ECONNRESET all the same: 9ns fails a request whose reply is already in once
-the hang-up breaks its next send, so trust the log): dial again, and the new
+editor's panes (a 9ns older than cloud9 2a7137c could fail the write with
+ECONNRESET all the same, when another request's send met the hang-up
+before the answer was read; the log is the authority): dial again -- a 9ns
+mount is one such connection, so after a Restore stop it and start 9ns
+again, or every file under it fails -- and the new
log names the restored panes and
`restore <path>`. The answer has 200 ms to leave before the cut, so a slow
client may see only the cut; the log's `restore <path>` is what says the
@@ -129,22 +141,25 @@ never needed); and `Kill`, which
does not quit but stops commands, as acme's does: bare, every command pardes
started, and `Kill make ls`, those whose line begins with one of the words. A
command pardes started is a command pane's, until its child exits -- Kill
-sends its whole process group SIGTERM -- or a line it typed into a terminal (a
+sends SIGTERM to its running job and to its shell, so the whole line stops
+(of `sleep 30; echo done` the `echo` never runs) -- or a line it typed into
+a terminal (a
word written to `exec`, a middle click on one, a `pty/run`), from its shell's
start mark (C) to its end mark (D), where Kill sends its foreground job SIGTERM -- acme posts the
"kill" note, which ends a process -- and never signals the shell itself;
in a shell running without job control (`set +m`) the job shares the
shell's group, so there is none to signal: Kill says `Kill: no job to
signal`, and a write of it to `ctl` fails with that; with nothing running
-it says `Kill: nothing running`; and only the
-foreground job, so of `sleep 30; echo done` the `echo` still runs once the
-`sleep` is stopped --
+it says `Kill: nothing running`; and, of a typed line, only the
+foreground job, so of `sleep 30; echo done` typed at a prompt the `echo`
+still runs once the `sleep` is stopped --
and reads every setting there is, one a line, in the words a write of it
takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir` bare
for the default directory, `LocationsConfig ...`), so writing what it reads
back changes nothing; platform and startup facts are `/status`'s and the
Config window's, not settings. A pane's `ctl` takes the builtins that act on
-a pane (`Del`, `Save f`, `Collapse`, which folds that pane, `Find pat`)
+a pane (`Del`, `Save f`, `Collapse`, which folds that pane, `Undo` and
+`Redo`, which step its body through its edits, `Find pat`)
beside acme's `get`, `lock` and `unlock`. The column words are pane words
too, acting on the column that pane is in: `Delcol`, `DelAbove`, `DelBelow`
and the focus moves `Left`/`Right`/`Up`/`Down` from it. `Joincol` and
@@ -162,8 +177,8 @@ argument to a builtin that takes none, or none to one that needs it (`Msg`,
`bad value in control message "X"` for a setting's value it does not take
(for `Theme`, naming the themes that share the name's first letter, since
all of them, `ThemeSel`'s list, are too many for an error);
-and `not a session control message "X"` or `not a window control message
-"X"` for a word of the other ctl. 9ns maps them all to EINVAL, and a write
+and `not a session control message "X": write it to pane/<n>/ctl` or `not
+a window control message "X": write it to /ctl` for a word of the other ctl. 9ns maps them all to EINVAL, and a write
refused here has done nothing. A line that then fails as it runs fails the
write with the error the editor reports for it and the line, e.g. `Mount:
AlreadyMounted "Mount peer /tmp/s"` (EIO), and `control message needs its
@@ -240,23 +255,32 @@ same tree without leaving the process.
`src/builtins.zig` (`Save`, `Del`, `New`, `Newcol`, `Mount NAME DIAL`,
`Unmount NAME`, `Dump`, `Restore`, `Msg TEXT`, `Find`, `Grep`, `Tty`, ...),
or anything else, a command line. Written at a terminal at its prompt it
- is typed into that shell. From anywhere else -- a file, a scratch, a tag,
+ is typed into that shell (and a terminal whose shell exits, `exit` typed
+ or run, closes its pane). From anywhere else -- a file, a scratch, a tag,
a terminal whose tty a program holds -- it runs as a command pane: a
terminal whose child is the root ctl's `Shell` (fish unless set) run
- with `-c` and the line, in the pane's directory,
+ with `-c` and the line, in the pane's directory, with job control on
+ (bash, sh, dash, zsh, ksh `-m`; fish `status job-control full`),
which shows its output and then `exit N` (its tag reads `<dir> (<line>)
running`, then `exit N`), and stays. The command is over when its process
exits, as in acme, not when its terminal closes: a job it left in the
background prints on below `exit N` until it lets go of the pty, and a
- command that lets go of its terminal early runs on to its own exit. The directory's next command runs in
- that pane once it is done, below what it showed, after a `% <line>` line;
+ command that lets go of its terminal early runs on to its own exit. A
+ background job outlives the command: job control gives it a process
+ group of its own, so the hangup the kernel sends the terminal's
+ foreground group when the shell exits misses it. It survives the pane
+ closing too, but its writes to the terminal then fail, so start one that
+ must keep writing with `nohup` or its output redirected. The directory's
+ next command runs in that pane once it is done and nothing holds its pty, below what it showed, after a `% <line>` line;
one still running gets a second pane. Before the next command the pane
leaves any alternate screen and turns off the modes a program left on
(mouse reports, bracketed paste, a hidden cursor); a command that clears
the screen and its scrollback (`clear`, ED3) erases the history above it. A command pane's own exec starts
the next command there too. The log says `run <serial> <line>` and `exit
- <serial> <N|?>`; `exec` reads back the command pane's serial; Kill ends
- its whole process group; a line is at most 1 KB. A misspelled word is a
+ <serial> <N|?>`; `exec` reads back the command pane's serial; Kill stops
+ its whole line; a line is at most 1 KB. To run a command again, execute
+ its line again from its directory: `echo 'make test' > pane/<n>/exec` on
+ the command pane runs it there, below the last run. A misspelled word is a
command that says so and ends `exit 127`. `echo Tty > pane/<n>/ctl` makes
an interactive terminal in that pane's directory.
@@ -287,7 +311,9 @@ the tag is how to run `make` from that file), `Exec <text>` run by name
the text, looked at or clicked (`# @`pytest -x`` in a script). A 9P `exec`
is no gesture and is never sent: a script writes to the REPL pane's
`pty/data`, multi-line code as a bracketed paste (`\e[200~<code>\e[201~`
-then `\r`), since line by line a blank line ends a Python block and Python
+then `\r`, and a second `\r` when its last line is indented: one Enter
+leaves a block open, pasted or typed; a middle click sends that second one
+itself), since line by line a blank line ends a Python block and Python
3.14's REPL auto-indents each line typed into it. The REPL gets the text
wherever it is -- at a `pdb` or `input()` prompt too. Bindings are not dumped, so a Restore leaves none.
@@ -298,7 +324,23 @@ write with EINVAL; a command that fails inside the editor is reported on the
message row, not as a write error. Reading any of these files answers the
serials of the panes the last command created, or, when it created none, the
pane a look focused or the pane an exec acted on (even one it closed), one
-per line.
+per line; a look that found text answers the pane the text is selected in,
+not the hits buffer it opened.
+
+A look takes acme's addresses after a colon (editors/acme/look.c:450): a
+line written to `look` as `file:/re/`, `file:#n`, `file:$` or any address
+opens (or finds) the file and selects what the address names. **The
+address is evaluated from the file's dot**, as acme's is: `file:/re/`
+finds the next match after the current selection, not the first in the
+file. For the first, start at the top: `file:0/re/` (or `file:#0/re/`).
+`:addr` does the same in the pane itself, and a pattern may hold blanks
+(`calc.py:/return a/`). `file:12` selects line 12, its newline included,
+as acme's does; `file:12:5` puts the caret at line 12, column 5. A bare
+`/re/` is a path, as in acme, and failing that a search for its text. A
+look that finds nothing, an address that does not evaluate, or a line past
+the file's end (`calc.py:99`) says so on the message row and in the log
+(`err <serial> look: ...`), focuses and opens nothing, keeps the
+selection, and leaves `look` reading back empty.
`/pane/<n>/name` reads the pane's file name (a terminal's directory) and
writing it renames the buffer; a relative name resolves against the pane's
@@ -324,7 +366,14 @@ length, a reserved zero, the dirty flag, the width in cells, the font and the
tab width — followed by rio's `current` or `notcurrent` (rio(4), `wctl`):
whether the pane has the keyboard. It takes the pane's builtins (below),
`get`, which reloads the buffer from the name it
-carries, and acme's `lock` and `unlock` (editors/acme/xfid.c:603-611), for an
+carries (unsaved edits are refused once, `<name>: Modified (get again to
+discard)`, as acme's get asks winclean, exec.c:513), `answer <choice>`
+for the question the pane asks on its notice band, which the log names as
+`ask <serial> <what> <choices>` -- `ask 4 del k j` for Del's side from the
+keyboard (`k` the pane above takes the rows, `j` the one below), `ask 4
+repl a b` for which bound REPL takes an exec (a REPL's letter) -- `answer -`
+taking it back as Esc does (with no question standing, `answer` is refused),
+and acme's `lock` and `unlock` (editors/acme/xfid.c:603-611), for an
edit of several writes to `addr` and `data` that another client must not
land in the middle of. As in acme the lock binds only the clients that take
it: a `lock` while another open holds it fails at once with `file in use`
@@ -342,10 +391,30 @@ exec 3>&-`.
The three range files `addr`, `dot` and `limit` each read the pair of offsets
they also accept, so copying one onto another is all that acme's `addr=dot`,
`dot=addr` and `limit=addr` ever were. A write is either that pair or an
-address expression (`#0,#5`, `/pattern/`, `2+1`); `addr` selects what `data`
+address expression (`#0,#5`, `/pattern/`, `2+1`, and pardes's own `12:5`,
+below); `addr` selects what `data`
and `xdata` read or replace, `dot` is the editor's own selection and moving it
-scrolls the pane into view, and `limit` bounds a search and reads empty until
-it is set. Truncating a range file empties it; truncating `limit` lifts it.
+scrolls the pane into view, and `limit` bounds only the end of a forward
+search, as acme's does, and reads empty until it is set. Truncating `dot` empties it, truncating `limit` lifts it, and
+truncating `addr` leaves it as it is (below).
+
+A rename everywhere, or any other sam edit, is one write to the pane's
+`ctl`: `Edit ,x/foo/c/bar/` runs acme's Edit (docs/tags.md) on the body as
+one undo step; a failure fails the write with acme's words and changes
+nothing. A write is one message a line, except that an `Edit` line takes
+the lines after it while its `{` group is open or its `a`, `c` or `i` text
+block waits for its `.` line, on a pane's `ctl`, the root's (at the active
+pane) and `exec` alike; an unclosed group is refused (``unmatched `{'``).
+bash's builtin `printf` writes a line at a time, so write a block with a
+heredoc or `env printf`.
+
+`line:col` is a pardes extension to sam's addresses, the spelling Look
+takes in `file:12:5`: `12:5` is the point at line 12, column 5, and it
+composes like any simple address (`12:5,14:1`, `12:5+#3`). The column is
+in bytes from 1, as Look's is, clamped to the end of the line and snapped
+back to the start of the character it falls in; a line past the end, or
+column 0, is `address out of range`. sam would read `12:5` as a syntax
+error.
Truncating `data` or `xdata` deletes the range `addr` names and nothing
else, so a shell's `echo NEW > data` replaces that range, `: > data`
deletes it, and `>>` inserts at it; only truncating `body` empties the
@@ -355,15 +424,31 @@ leaves `addr` just past what it wrote, so a second `echo x > data` deletes
the empty range there and inserts after the first rather than replacing it
again; write `addr` before each replacement.
`addr` belongs to the pane rather than to a client and keeps what was written
-until someone writes or truncates it, so writing an address and reading it
-back evaluates it, which is what acme(4) promises of its own `addr`.
+until someone writes another, so writing an address and reading it back
+evaluates it, which is what acme(4) promises of its own `addr`. Unlike acme,
+neither an open nor a truncation resets it: acme sets it to `#0` when the
+first client opens `addr` (editors/acme/xfid.c:105-108), which suits a
+client that holds the fid, but a shell opens the file anew for every
+`echo /re/ > addr` and so would search from the top each time and never
+advance. Here each such write searches on from the last address, as `>>`
+does; write `0` to start again from the top. A search wraps at the end of
+the text, so a find-all loop stops when the address comes back to where it
+began, or bounds itself with `limit`.
The regular expressions are mvzr's (sets, `\d`/`\w`/`\s`, `{m,n}` and
lazy `*?` included), searched the way sam searches (editors/acme/regx.c): as
lines, so `^` and `$` match at the start and end of any line, `.` and a
negated class never match a newline, and `$` also matches at the end of a
text with no final newline. A pattern that names a newline (`\n`) runs over
-the whole text instead, its `.` kept to one line. An expression is
+the whole text instead, its `.` kept to one line; there a leading `^` still
+matches at every line start, and `$` may stand just before a `\n` (where it
+changes nothing). Any other `^` or `$` in such a pattern, `(^|\n)def` or
+`a\nb$`, is refused with `in a pattern with \n, ^ can only come first and $
+only just before a \n`, since mvzr would read it as the start or end of the
+whole text: never a search that silently finds nothing. Within a line, `^`
+inside an alternation (`^def|^ `) matches only where the search starts, so it
+works from a line's start (an `x` over lines, a `g` on one) and not from its
+middle: an mvzr limit. An expression is
evaluated from the current address, the range last written to `addr` (or
left by the last `data` write, just past it), as acme evaluates it from
`w->addr` (xfid.c:446): `.` is that address, not the selection (`dot` is
@@ -395,7 +480,7 @@ expression`, `regular expression search gave up, ...`, or sam's
`addresses out of order` for a range that ends before it starts
(`#100,#50`), which acme lets through. A failed write to `addr` leaves no
address at all, where acme
-keeps the old one: until an address is written or `addr` is truncated,
+keeps the old one: until a good address is written,
reading `addr`, and reading, writing or truncating `data` and `xdata`, fail
with `no address: the last one written to addr failed`, so a script that
missed its target cannot then write at the last one.
@@ -405,13 +490,15 @@ The three flag files `dirty`, `mark` and `scroll` read `0` or `1` and take
an undo point (writing `1` pushes one now), and whether a write scrolls the
pane.
-`tag` reads the whole tag as the pane shows it: the computed path, dirty
-marker or PDF page, then the text you may edit. A write appends to that text,
+`tag` reads the whole tag as the pane shows it: the computed path or PDF
+page (no mark for unsaved text: the grip shows that, and `dirty` says it),
+then the text you may edit. A write appends to that text,
newlines included, and a tag with more than one line takes a row per line on
screen; truncating `tag` clears it, as acme's `cleartag` does -- the default
words (`Del`, `Put` and the rest) with it, since they are that text until
you edit it, so `echo Make > tag` leaves only `Make`; append with `>>` to
-keep them. The clearing is an edit of the tag like a typed one and its undo
+keep them, with `printf ' Make' >> tag`: `echo` ends its word with a
+newline, which starts a new line of the tag. The clearing is an edit of the tag like a typed one and its undo
history is kept: `u` in the tag brings back the text it cleared, words
included.
@@ -429,9 +516,13 @@ an open renders its frame; stat the entry.
`/log` is one ring (64 KiB) that records whether or not anyone reads it:
`new`, `del`, `rename` (a terminal's too, as its shell changes directory,
-since a terminal is named by its directory) and `save <serial> <name>`,
+since a terminal is named by its directory), `exit <serial> <N>` before
+the `del` of a terminal whose shell exited by itself, `ask <serial> <what>
+<choices>` when a pane asks a question (answered by `answer` on its ctl),
+and `save <serial> <name>`,
`dump <path>` when a Dump is written and `restore <path>` in a Restore's
-new log after its panes' `new`s, and `msg <serial|-> <text>`
+new log after its panes' `new`s, then `restored <old> <new>` for each pane,
+mapping the serial it had to the one it has now, and `msg <serial|-> <text>`
for every line the editor says, repeats included (with `verbose` on, that
includes each builtin announcing itself as it runs), and `err <serial|->
<file>: <why>` for every write or truncation the tree refused or that
@@ -454,11 +545,17 @@ pane is being made can precede that pane's `new`; panes present at boot are
recorded before anything else.
Control characters in a record become spaces, so a record is one line.
(An `event` record is not: acme's `<origin><action><q0> <q1> <flag> <n>
-<text>\n`, whose text may hold newlines; read `n` bytes of it rather than
-up to a newline -- bytes here, where acme counts runes. Every offset and
+<text>\n`, whose text may hold newlines. The origin is `E` (a 9P write to body or tag), `F` (other
+files, the editor's own lines), `K` (the keyboard) or `M` (the mouse); the
+action's case says where: `x`/`l` a click executed or looked at in the tag,
+`X`/`L` in the body, `I`/`D` text put in or taken out of the body, `i`/`d`
+of the tag. Read `n` bytes of the text, not up to a newline -- bytes here, where acme counts runes. Every offset and
count pardes serves is in bytes, `#n` and `q0`/`q1` too; the event count
follows them rather than switch alone, so an acme library reads pardes
-correctly for ASCII text and not beyond it.) An
+correctly for ASCII text and not beyond it. A `#n` that falls inside a
+character, a multibyte rune or a grapheme cluster, snaps back to where that
+character starts, and `addr` reads back the snapped offset. A click in a
+tag gives offsets into the whole tag as `tag` reads it, the path first.) An
open freezes the ring's text the way `/screen` freezes a frame: reads walk it
and end. Writing `follow` to that same open makes reads past it wait for the
next record, one per read; a follower the ring outran reads `lost N` first.
@@ -499,8 +596,14 @@ are read so that the answer costs the editor a bounded amount. `exit ?` is a
command whose end mark carried no status, which is not a success; `error out
of memory` is an answer that could not be made. The header is always the
whole first line. It reads
-`busy` at once when a command is running or text is typed at the prompt,
-which is also when the third field of `pty/status` reads 1. A line written
+`busy` at once when a command is running or text is typed at the prompt
+-- a run is a line typed at the shell's prompt, so a program holding the
+terminal (a REPL, `less`) takes none: write to `pty/data` for it --
+which is also when the third field of `pty/status` reads 1. `pty/status` is
+one line, three right-aligned fields and a newline: the pty's columns and
+rows, then busy (0 or 1). The size, and `pty/ctl`'s `winsize` read back, is
+what `winsize C R` last set, until the pane itself resizes and gives the pty
+its grid again. A line written
before a new terminal's shell has drawn its first prompt is not busy: it
waits for that prompt (a respawn meanwhile keeps it waiting for the new
shell's) and is sent then, so the first command a script gives a fresh
@@ -509,8 +612,13 @@ could not instrument, or a startup that hangs) leaves such a line waiting
for ever: cancel the read (interrupt it, or close the open) to give up;
`error not run` when the shell refused
the line without running it (a fish syntax error; the line is taken back off
-the prompt); `error shell gone` when the pane closed or its shell was
-replaced; `error no prompt marks` for a shell pardes could not instrument;
+the prompt): the shell's marks say only that it did not run, so the answer
+carries no reason or code, and the shell's own complaint is in the pane's
+body (`tail body`); `exit N` and what it printed when the line ended the shell
+itself (`exit 3`, or `echo bye; exit 3`): its terminal closes, and a read
+of the run's open still answers after the pane is gone; `error shell gone`
+when the pane closed or its shell was replaced, or the shell went without
+an exit status to tell; `error no prompt marks` for a shell pardes could not instrument;
`error command done; not a shell` (or `error a command runs here, not a
shell`) on a command pane, whose child is its command.
It relies on the OSC 133 marks pardes injects into bash and fish, tagged
@@ -519,7 +627,11 @@ line on an open whose command still runs fails the write. `exec zsh` or a
continuation prompt never reports an end; cancel the read. A read of `run`
waits in pardes, so through 9ns it needs 9ns's concurrent requests or it
holds up the rest of the mount. A record
-longer than a read comes in pieces, so a shell's `read` loop works. `tail -f`
+longer than a read comes in pieces, so a shell's `read` loop works: `exec
+3<>$m/log; echo follow >&3; while read -r line <&3; do ...; done`. bash's
+`read` takes a chunk, keeps one line and seeks back to just past it; a
+followed log, `event` and `pty/data` answer a read at an offset inside
+their last answer from that answer again, so no record is lost. `tail -f`
never writes `follow`, so it sees nothing new: use the follow open instead. `/screen` returns JSON with
`cols`, `rows`, `cursor`, a `styles` table, and row-major `cells` of
`[grapheme, style_index]`. Each open freezes one frame until close. A
diff --git a/docs/helix-keys.md b/docs/helix-keys.md
index c703a3da..8874bf08 100644
--- a/docs/helix-keys.md
+++ b/docs/helix-keys.md
@@ -127,7 +127,7 @@ language-backend queries, and the shell pipe.
| `Enter` (normal) | acme **look** chord: EXPLICIT selection, else file-ish word under cursor | pardes-specific, keep (helix normal-mode Enter unbound). Covers helix `gf`. Implicit motion residue falls back to the cursor word | pardes-specific |
| `Tab` (normal) | acme **execute** chord | pardes-specific, keep; explicit-selection rule as Enter | pardes-specific |
| `:` (normal, body or tag) | in the body, focuses the pane's tag in normal mode at its remembered cursor (the first time, on `Save`); in the tag, goes back to the body; in a column or workspace tag, back to the active pane. The tag's normal and insert modes ARE the body's: every motion, selection, edit and undo works there, and `0` goes to the line's start, the path's. Tab runs the word under the cursor or the selection and Enter looks it up (in a column or workspace tag Enter runs it too), and either hands the keyboard back to the body first. Clicks choose a new cursor position and type into the tag. | Each tag keeps its own cursor during the session. The computed path/marker/page is reachable and yankable but read-only: an edit into it is refused, and typing into a file's path drafts a new name. File-name changes are staged as described in [editable tags](tags.md). | pardes-specific |
-| `Ctrl-w k` / `SPC w k` (pane with nothing above) | focuses its column's tag, then the workspace tag; with `ColumnTags` disabled it goes directly to the workspace. `Ctrl-w j` walks back to the panes, `Ctrl-w h`/`l` walk the column tags. Headers edit exactly as a pane tag does. | Column commands target that column's active pane, or its first pane when coming from elsewhere. Workspace and column text are independently editable and persist in dumps. See [editable tags](tags.md). | pardes-specific |
+| `Ctrl-w k` / `SPC w k` (pane with nothing above) | focuses its column's tag, then the workspace tag. `Ctrl-w j` walks back to the panes, `Ctrl-w h`/`l` walk the column tags. Headers edit exactly as a pane tag does. | Column commands target that column's active pane, or its first pane when coming from elsewhere. Workspace and column text are independently editable and persist in dumps. See [editable tags](tags.md). | pardes-specific |
| `Ctrl-w` + `h/j/k/l`/arrows | directional pane focus prefix — editor normal mode only | pardes' own window handling (helix window mode skipped, section C). Runs the SAME `Left`/`Down`/`Up`/`Right` builtins `SPC w h/j/k/l` runs; Raw **tty** mode forwards Ctrl-w to the child. Insert mode owns `Ctrl-w` = delete-word-back, so a tag being TYPED into swallows it; from a tag in normal mode it moves focus to the neighbour's BODY, as from a body, and from the top pane `Up` reaches the column and workspace tags | pardes-specific |
| `Alt-n` | new terminal below (outside raw tty) | shadows helix `Alt-n` (select next sibling), which pardes spells `Alt-right` alone | pardes-specific |
| `Alt-c` | move active terminal to a fresh column (outside raw tty) | helix `Alt-c` is change-noyank; the pardes window op wins (do-not-touch contract). `Alt-d` + `i` covers the behavior | waived (`alt-c-window-op`) |
diff --git a/docs/macos.md b/docs/macos.md
index 5166e2d5..cf7091e6 100644
--- a/docs/macos.md
+++ b/docs/macos.md
@@ -355,8 +355,8 @@ The dirty half needed one core watermark. `File` counted `revision` but never
recorded which edit was last *written*, so no shell could derive "unsaved" —
`File.saved_revision` is that watermark, advanced by `Save` at the moment the
write is asked for and by a successful external-file reload whose bytes
-already came from disk. The core renders the same answer as ` *` after the
-path in every tagline, while this host also puts it in the native close button.
+already came from disk. The core shows the same answer on each pane's grip
+button, while this host also puts it in the native close button.
Save is marked at ask-time rather than on completion because `save_file`
carries none back, which makes both indicators exactly as honest as the Save
request.
diff --git a/docs/selections.md b/docs/selections.md
index 5cc1df1a..0817c215 100644
--- a/docs/selections.md
+++ b/docs/selections.md
@@ -121,3 +121,30 @@ the replay.
line is its own haystack, so `^`/`$` hold at every line and a class like
`\s` does not reach the newline unless the pattern names `\n`.
- Undo restores only the primary (above).
+
+## The mouse
+
+A B1, B2 or B3 sweep selects a stream, as acme's does (libframe
+frselect.c:115): from the press to the end of its line, the lines between
+whole, the last from its start to the release, the character under the
+pointer included. A plain B1 click puts the caret on its cell. A second
+plain click at the same cell within half a second is acme's double-click
+(plan9port acme text.c:1407, textdoubleclick): just after `{ [ ( < «` or
+just before their closers it selects up to the match, nested pairs
+counted; at a line's start or end the whole line; just inside `' " \``
+the quoted text; anywhere else the word (letters, digits, `_` and any
+non-ASCII character). It works in bodies, tags and a terminal in normal
+mode; a terminal whose program has the tty gets its own clicks.
+
+A B2 or B3 pressed while B1 holds a sweep is acme's chord: B1-B2 cuts,
+B1-B3 pastes over it. Everything done while B1 stays down is one undo
+step, as acme marks the file once per B1 hold (text.c:881, textselect), so
+B1-B2 then B1-B3 is a copy: the text cut and pasted back, the cut text
+left in the register.
+
+A B1 sweep held past a pane's top (on its tag) or bottom (past it, or on
+its last row when the pane reaches the screen's bottom, which the pointer
+cannot leave) scrolls the body a line a tick, as acme's frselect scrolls,
+the sweep's end under the pointer and its start on its text. A sweep that
+scrolled ends as the text's own selection, from where it began to where it
+ended. B2 and B3 sweeps do not scroll.
diff --git a/docs/tags.md b/docs/tags.md
index f1e4a420..aa680c48 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -1,8 +1,9 @@
# Editable tags
Pardes has three levels of command text: the workspace tag, one tag per
-column, and each pane's tag. Column commands run in that column's active
-pane (or its first pane when focus comes from another column). This makes
+column, and each pane's tag. Column commands act on that column and run in
+its active pane (or its first pane when focus comes from another column); a
+column with no pane is described under [empty columns](#empty-columns). This makes
`New`, `Tty`, `Find`, and `Grep` available beside the work they act on.
`New` appears only in the column tag by default; pane tags keep their own
save, terminal, close, and collapse commands. `Tty` opens a new embedded terminal.
@@ -43,6 +44,21 @@ Its text and terminal process are kept;
the command stays available in the visible tag. Every default pane tag includes
`Collapse`, including file, terminal, image, and PDF panes.
+`Edit` is acme's: the rest of the line is sam's command language, run on
+the pane's body (`Edit ,x/foo/c/bar/` renames every `foo`), from a tag, a
+pane's `ctl` or `exec`. Addresses are the `addr` file's, `line:col`
+included; the commands are `x y g v c a i d s p = m t`, `u` alone, and `{ }`
+with a command per line. All its changes are one undo step, applied only
+if every command ran; an error says why in acme's words and changes
+nothing. `p` and `=` print to the directory's `+Errors`. Left out: the file
+commands `b B D e r w f X Y`, the pipes `< | >`, and `\1`-`\9` in `s`,
+since mvzr keeps no submatches; acme applies changes that come out of
+sequence with a warning, pardes refuses the Edit.
+
+`Undo` and `Redo` are acme's: typed or clicked in a pane's tag, or written
+to its `ctl`, they step the body back and forward through its edits, as the
+`u` and `U` keys do. They are not in the default tags.
+
`Del` closes a pane and gives its rows to one neighbor; the rest of the
column keeps its heights. `Del k` (or `DelAbove`) gives them to the nearest
expanded pane above, `Del j` (or `DelBelow`) to the one below, each falling
@@ -56,21 +72,33 @@ are passed over: they only keep a weight for later.
A tag is a text like a pane's body: pane tags, column tags and the
workspace tag all edit with the body's own normal and insert modes, undo
-included, and may hold more than one line. A tag with a newline in it takes
-a row per line, up to eight: a pane's tag leaves its body at least one row
-and a collapsed pane shows only the first line; the column and workspace
-tags take no more than a third of the screen, and the panes below move down
-to make room. The path, the dirty marker and a PDF's page at the start of a
+included. A pane's tag may hold more than one line, and a line wider than
+the pane wraps onto more rows, as acme's tag does: it takes a row per shown
+line, up to eight, and leaves its body at least one row. A word the wrap
+breaks across rows is still one word to a click. A collapsed pane shows
+only the first row.
+
+A pane tag starts expanded, showing all its rows, and can be collapsed to
+one row, as acme's Tagup does. In the tag's insert mode Up on its first row
+collapses it and Down on its last row expands it; Alt-Up and Alt-Down do
+the same in either mode. acme expands or collapses on any arrow key typed in
+the tag (Up collapses, the others expand); here arrows keep moving the
+cursor, and only these keys at the tag's edges change its height. The column and workspace tags
+are one line, as acme's (cols.c:244 gives a column tag one font height): a
+newline typed, pasted or written into one becomes a space, and a dump that
+holds a column or workspace tag of several lines, from before, comes back
+with its lines joined by spaces. The path and a PDF's page at the start of a
pane tag are computed, never stored, and read-only. The keyboard reaches
them as the mouse does: `0` goes to the line's start, the path's, as in
acme, and motions select and yank across the path and the commands. An edit
that would change them is refused and leaves the cursor where it was; typing
into a file's path instead drafts a new name, as clicking it does. When they
-grow or shrink (a rename, the dirty marker, a PDF's page) the cursor keeps
+grow or shrink (a rename, a PDF's page) the cursor keeps
its place in the text after them.
-Left-click a tag or a header to type into it at the click, in insert mode;
-tags reveal the caret horizontally when text is wider than their column.
+Left-click a tag or a header to type into it at the click, in insert mode.
+A pane tag wraps instead of scrolling sideways; a column or workspace tag
+reveals the caret horizontally when its text is wider than its column.
Esc is normal mode, where everything a body's normal mode does works. There
Tab executes the word under the cursor (or an explicit selection), and Enter
looks it up in a pane's tag but runs it in a column or workspace tag, whose
@@ -137,7 +165,7 @@ upgrade to `Mode`; customized command text is preserved.
## Saved workspaces
`Dump` and `Restore` preserve customized workspace and column tags, including
-intentionally empty tags. New columns start with the standard column tag.
+intentionally empty tags, and columns that hold no pane. New columns start with the standard column tag.
Closing a column keeps surviving columns' tags; `Joincol` keeps the destination
column's tag. Old dumps without these optional fields retain the defaults.
The automatic `Restore` shortcut does not overwrite a customized workspace
@@ -148,7 +176,75 @@ than three rows it is omitted so a pane still has room. SDL and TTY share the
same tag text, editing and layout; SDL additionally uses compact font sizing
and subtle pixel separators.
-`ColumnTags` toggles the column row (on by default) without deleting its text.
+The column row is always shown. An old `ColumnTags` line in an init file is
+ignored, with a message saying so.
`FocusTint` controls active-column and active-pane emphasis. SDL also honors
the shared bold, underline, and strikethrough attributes, including diagnostic
underlines and the optional `SyntaxBold` keyword weight.
+
+Unsaved text shows on the pane's grip button, as acme's modbutton does,
+not in the tag: the tag names the file and nothing more. The `dirty` file
+and `index`'s flag say the same to a script.
+
+## Empty columns
+
+A column can hold no pane, as acme's can: its tag stands over blank space in
+the theme's `empty_col` colour, white in the acme theme as acme paints it
+(cols.c:186-188), and the frame's border fill in themes that do not set it. `Newcol` makes an
+empty column right of the keyboard's and gives its tag the keyboard. Closing a
+column's last pane (`Del`, `Del k`/`Del j`, a shell exiting, a drag to another
+column) leaves the column empty where it was, and the keyboard goes to its tag
+if it was on that pane. Only `Delcol` and `Joincol` take a column away.
+
+`Delcol` and `Joincol` from a column's tag act on that column; `Delcol`
+written to a pane's ctl closes that pane's column. A pane dragged onto an
+empty column fills it.
+
+## Where new panes go
+
+Every new pane goes through one placement, chosen by the `Placement` setting:
+`acme` (the default) or `pardes`. `Placement pardes` or `Placement acme` sets
+it, in an init file, a tag or the root ctl; bare `Placement` flips it; `SPC c
+p` is its leader path, and `Config` reports it.
+
+`Placement acme` is acme's makenewwindow (util.c:449-495). The core keeps
+acme's *active column* (activecol, dat.c:37): the column last typed in
+(acme.c:487), clicked in with the select button (acme.c:659), dropped into by
+a grip (acme.c:640), whose tag was given the keyboard (`Newcol`, an emptied
+column, `Ctrl-w k`), or that was given the last new pane (util.c:467). A Look
+click moves the keyboard but not the active column, as button 3 does not in
+acme. A new pane goes into the column a command's tag belongs to when it came
+from a column tag, else the active column, else the keyboard's pane's, and
+never into a new column:
+
+- an empty column it takes whole (util.c:468-469);
+- from a tag, or 9P's `pane/new` (acme's `t->w == nil`), it takes the bottom
+ half of the column's last pane (coladd, cols.c:62-65);
+- from a pane's text (a Look, `Tty`, `Alt-n`, a Grep or Find listing), it goes
+ right under the text of the pane with the most blank rows when that is more
+ than 15 rows, or more than 3 and more than half the biggest pane
+ (util.c:482-486); otherwise it halves the biggest pane, or the asking pane
+ when that is in the column and not much smaller (util.c:487-491);
+- `New` goes into its own column, the bottom half of its last pane
+ (look.c:921-923);
+- a command pane or a `+Errors` pane goes to the last column, the bottom
+ half of its last pane (util.c:94-98).
+
+`Placement pardes` is what pardes did before: an empty column whose tag asked,
+or has the keyboard, is filled; a scratch goes right under the pane that asked;
+a shell under it or the nearest pane with room; a document beside the last one
+read, or in a column of its own on the left when there is none and the column
+is at least 200 cells wide; a command pane at the foot of the last column.
+
+`BootShell replace` brings back one more placeholder: a document dragged into
+the left column closes the column's lone shell if nobody has typed into it.
+`BootShell keep`, the default, never closes a pane for another one.
+
+Down from an empty column's tag stays there, and the pane-to-pane keys pass
+over an empty column; Left and Right from a tag walk every column's tag.
+
+One divergence from acme: acme keeps running when its last window closes,
+every column empty. Pardes quits when the session's last pane closes, as
+`Delcol` of the last column always has. A key, a prompt or a command in
+pardes runs in a pane, so a session without one would have nothing to run
+them in.
diff --git a/docs/themes.md b/docs/themes.md
index 6b218b13..e028e806 100644
--- a/docs/themes.md
+++ b/docs/themes.md
@@ -108,6 +108,7 @@ so existing exported themes remain valid.
| `column_box` | Column grip while held, and its drag rail | `num` |
| `column_box_dim` | Column drag grip at rest, in every focus state | Equal mix of `column_box` and `tag_bg` |
| `border` | Quiet separators | `scroll_track` |
+| `empty_col` | A column with no pane, under its tag (acme: white) | `border` |
| `lineno_active` | Restrained current line number foreground | `lineno` |
| `search_bg`, `search_fg` | Search matches, independent of selection | `sel_bg`, `sel_fg` |
| `diagnostic_error` | Error text | `fg`, or `tag_fg` for an inherited foreground |
diff --git a/docs/ui-review.md b/docs/ui-review.md
index 58a42e61..1180a20e 100644
--- a/docs/ui-review.md
+++ b/docs/ui-review.md
@@ -98,8 +98,7 @@ tests and 797 SDL tests; SDL image/PDF and Kitty PDF rendering harnesses pass.
## Column and editable-tag follow-up
The follow-up adds editable workspace and column command rows, compact pane
-tags, staged buffer-name edits, caret reveal for long tags, and `ColumnTags`
-to reclaim the extra row when needed. See [editable tags](tags.md) for the
+tags, staged buffer-name edits, and caret reveal for long tags. See [editable tags](tags.md) for the
exact interaction and save-target rules.
`test/column_tags.py` exercises isolated SDL and TTY sessions against the same
diff --git a/src/9p_io.zig b/src/9p_io.zig
index 2ca11a87..651b3d0a 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -64,7 +64,9 @@ const prefix = "pardes-9p-";
pub const msize = ninep.msize;
-pub const max_conns = 4;
+/// Scripts, a mount and an agent or two at once; each slot holds its
+/// buffers (a few msize) whether used or not.
+pub const max_conns = 16;
extern "c" fn inet_pton(family: c_int, src: [*:0]const u8, dst: *anyopaque) c_int;
@@ -176,6 +178,8 @@ pub const Listener = struct {
stopping: std.atomic.Value(bool) = .init(false),
/// A connection wrote the Restore and has yet to answer it.
restore_writer: std.atomic.Value(bool) = .init(false),
+ /// Clients turned away with every slot taken, since the log last said so.
+ refused: std.atomic.Value(u32) = .init(0),
/// Connections accepted so far, and the count each slot's connection
/// was accepted at: a Restore cuts those accepted before it (`reset`),
/// and a new client in a freed slot has a later stamp. Written on the
@@ -217,6 +221,14 @@ pub const Listener = struct {
/// answers. A request that would change a pane while the editor is out
/// in a syscall mid-step is parked in the engine instead, and retried
/// when the turn is next given up between steps (`wakeParked`).
+ /// On the accepting task, without the turn: counted, and logged with
+ /// the turn (`answerHeld`).
+ fn onRefused(ctx: ?*anyopaque) void {
+ const l = of(ctx);
+ _ = l.refused.fetchAdd(1, .acq_rel);
+ l.kick();
+ }
+
fn onOpened(ctx: ?*anyopaque, conn: *Runner.Conn) void {
const l = of(ctx);
l.accepted[conn.index].store(l.accepts.fetchAdd(1, .acq_rel) + 1, .release);
@@ -296,7 +308,15 @@ pub const Listener = struct {
// Set with the turn still held, so `reset` sees it and waits.
if (restoring) l.restore_writer.store(true, .release);
defer if (restoring) l.restore_writer.store(false, .release);
+ core.fs.late_failure_len = 0;
if (core.effects_len != 0) pardes.turn.awaitSettled(epoch);
+ // ponytail: one slot, so a failure of another client's effects that
+ // settle in the same wait is told to this write too.
+ if (l.core == core and core.fs.late_failure_len != 0) {
+ const failed = pardes.ctlfs.failText(req.tag, pardes.ctlfs.E.IO, core.fs.late_failure[0..core.fs.late_failure_len]);
+ pardes.ctlfs.events.noteError(core, req, failed);
+ return conn.reply(&failed, "");
+ }
// The Restore's own write is answered once it is done, and before
// `reset` hangs every connection up, this one too, so the writer
// hears that it happened rather than a cut; a failed one leaves the
@@ -323,6 +343,11 @@ pub const Listener = struct {
const l = of(ctx);
if (l.stopping.load(.acquire)) return;
const core = l.core;
+ const refused = l.refused.swap(0, .acq_rel);
+ if (refused != 0) {
+ var why: [64]u8 = undefined;
+ pardes.ctlfs.events.notePath(core, "err -", std.fmt.bufPrint(&why, "9p: too many connections ({d} turned away)", .{refused}) catch "9p: too many connections");
+ }
if (!core.fs.news) return;
core.fs.news = false;
for (&core.fs.opens) |*o| {
@@ -816,7 +841,7 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes, named: [
l.runner.init(.{
.io = io,
.root = pardes.ctlfs.root,
- .handler = .{ .ctx = l, .serve = Listener.onServe, .opened = Listener.onOpened },
+ .handler = .{ .ctx = l, .serve = Listener.onServe, .opened = Listener.onOpened, .refused = Listener.onRefused },
.greet_timeout_ms = Listener.greet_deadline_ms,
});
const entry_name = if (named.len != 0) named else fallback;
@@ -1278,7 +1303,8 @@ test "Unix TCP and QUIC share one listener through reads writes reconnects and r
var directory: [64:0]u8 = undefined;
_ = try std.fmt.bufPrintSentinel(&directory, "/tmp/pardes-tcp-XXXXXX", .{}, 0);
if (mkdtemp(&directory) == null) return error.TempDirectoryFailed;
- defer _ = rmdir(&directory);
+ // What the listener posted and left inside goes with it.
+ defer std.Io.Dir.cwd().deleteTree(testing.io, std.mem.sliceTo(&directory, 0)) catch |err| std.debug.print("deleteTree: {s}\n", .{@errorName(err)});
const old_runtime = if (libc.getenv("XDG_RUNTIME_DIR")) |v| try gpa.dupeZ(u8, std.mem.span(v)) else null;
defer {
if (old_runtime) |v| {
@@ -1348,7 +1374,7 @@ test "Unix TCP and QUIC share one listener through reads writes reconnects and r
}
try testing.expect(l.path().len != 0);
try testing.expectEqual(tcp, l.tcp_address != null);
- try testing.expectEqual(@as(usize, 4), l.runner.conns.len);
+ try testing.expectEqual(@as(usize, max_conns), l.runner.conns.len);
try testing.expect(l.watcher == null);
const port = (if (tcp) l.tcp_address else l.quic_address).?.getPort();
try testing.expect(port != 0);
@@ -1389,7 +1415,8 @@ test "a change waits while the editor is out mid-step, a read does not, and the
var directory: [64:0]u8 = undefined;
_ = try std.fmt.bufPrintSentinel(&directory, "/tmp/pardes-park-XXXXXX", .{}, 0);
if (mkdtemp(&directory) == null) return error.TempDirectoryFailed;
- defer _ = rmdir(&directory);
+ // What the listener posted and left inside goes with it.
+ defer std.Io.Dir.cwd().deleteTree(testing.io, std.mem.sliceTo(&directory, 0)) catch |err| std.debug.print("deleteTree: {s}\n", .{@errorName(err)});
const old_runtime = if (libc.getenv("XDG_RUNTIME_DIR")) |v| try gpa.dupeZ(u8, std.mem.span(v)) else null;
defer {
if (old_runtime) |v| {
@@ -1458,7 +1485,8 @@ test "a held read is answered when the log has news, and a flushed one spends no
var directory: [64:0]u8 = undefined;
_ = try std.fmt.bufPrintSentinel(&directory, "/tmp/pardes-held-XXXXXX", .{}, 0);
if (mkdtemp(&directory) == null) return error.TempDirectoryFailed;
- defer _ = rmdir(&directory);
+ // What the listener posted and left inside goes with it.
+ defer std.Io.Dir.cwd().deleteTree(testing.io, std.mem.sliceTo(&directory, 0)) catch |err| std.debug.print("deleteTree: {s}\n", .{@errorName(err)});
const old_runtime = if (libc.getenv("XDG_RUNTIME_DIR")) |v| try gpa.dupeZ(u8, std.mem.span(v)) else null;
defer {
if (old_runtime) |v| {
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index f8451baf..142287b2 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -2,6 +2,24 @@
## 0.0.3
+- A column can be empty, as in acme. `Newcol` makes an empty column and
+ gives its tag the keyboard; closing a column's last pane, or dragging it
+ away, leaves the column where it was with nothing in it, drawn as its tag
+ over blank space. The `+New` placeholder that used to stand in an emptied
+ column, and the machinery that swapped it out when something else arrived,
+ are gone. Closing the session's last pane now quits.
+- New panes go where acme's makenewwindow puts them: into the active column
+ (the one last typed or clicked in), filling it when it is empty, else under
+ the text of a pane with room to spare, else halving the biggest pane; never
+ into a new column. `Placement pardes` brings back the old rules, which open a
+ first document in a column of its own.
+- A document dragged into the left column no longer closes the untouched
+ boot shell there; `BootShell replace` brings that back.
+- Column tags are always shown: the `ColumnTags` setting is gone, and an
+ init file's `ColumnTags` line is ignored with a message.
+- Carrying a pane or a column by its grip shows acme's box cursor in the SDL
+ shell and a move cursor on the web.
+
- `Del` takes a side. `Del k` and `DelAbove` give the closed pane's rows to
the expanded pane above it, `Del j` and `DelBelow` to the one below. A bare
`Del` from the keyboard on a pane with panes both above and below asks on
diff --git a/src/File.zig b/src/File.zig
index b8368482..929e33bf 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -53,6 +53,10 @@ pub const State = struct {
/// Revision last known to match disk, after Save or an external reload.
/// Equal means the screen matches disk.
saved_revision: u32 = 0,
+ /// The text at `saved_revision`, hashed as the first edit after it
+ /// replaces it: an edit that brings the text back to it is clean again,
+ /// as an undo to the save point is in acme.
+ saved_hash: ?u64 = null,
watch_after_save: bool = false,
/// Seconds of the last content change, for the filesystem's stat; zero until edited.
mtime: u32 = 0,
@@ -859,9 +863,14 @@ pub fn setContent(p: *Pardes, f: *State, new: []u8) void {
if (f.mini) |*mini| mini.deinit(p.gpa);
f.mini = null;
const starts: []usize = if (f.line_starts.len > 0) editedLineStarts(p.gpa, f.line_starts, f.content, new) catch &.{} else &.{};
+ // ponytail: a whole-text hash per edit; a rolling hash when files grow large.
+ if (f.revision == f.saved_revision or f.saved_hash == null) f.saved_hash = std.hash.Wyhash.hash(0, f.content);
p.gpa.free(f.content);
f.content = new;
f.revision +%= 1;
+ if (f.saved_hash) |h| if (std.hash.Wyhash.hash(0, new) == h) {
+ f.saved_revision = f.revision;
+ };
f.mtime = pardes.ctlfs.events.now();
if (f.line_starts.len > 0) p.gpa.free(f.line_starts);
f.line_starts = starts;
diff --git a/src/Messages.zig b/src/Messages.zig
index 800218b7..8b6c795d 100644
--- a/src/Messages.zig
+++ b/src/Messages.zig
@@ -410,7 +410,7 @@ pub fn blendRgb(from: [3]u8, to: [3]u8, amount: f32) [3]u8 {
fn logMessage(p: *Pardes, id: usize, text: []const u8) void {
if (text.len == 0) return;
- const serial: u32 = if (id < MAX_PANES) if (p.panes[id]) |pane| pane.serial else 0 else 0;
+ const serial: u32 = if (p.fs.session_write) 0 else if (id < MAX_PANES) if (p.panes[id]) |pane| pane.serial else 0 else 0;
const kept = text[0..@min(text.len, LoggedMessage.cap)];
// /log hears every one: a client that retried and failed the same way
// is waiting on that second line. Only the +Messages view collapses.
diff --git a/src/Mini.zig b/src/Mini.zig
index e118e738..ac9a8e85 100644
--- a/src/Mini.zig
+++ b/src/Mini.zig
@@ -188,7 +188,7 @@ pub fn open(p: *Pardes, id: usize, argument: []const u8) !void {
.syntax_dirty = false,
};
pane.body.cur_pinned = true;
- exec.placeDoc(p, id, free, pane);
+ exec.placeNew(p, id, id, free, .doc);
p.active = free;
}
diff --git a/src/Output.zig b/src/Output.zig
index 201b5491..2f462fd1 100644
--- a/src/Output.zig
+++ b/src/Output.zig
@@ -479,7 +479,7 @@ pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []
const free = p.freeSlot() orelse return error.NoPaneSlots;
const np = try open(p, free, dir, from, arg, result_content);
np.file.?.location_rows = result_rows;
- exec.placeDoc(p, id, free, np);
+ exec.placeNew(p, id, id, free, .doc);
p.active = id;
if (traits(from).steps) {
pane.search_pane = free;
@@ -558,8 +558,8 @@ fn openStepped(p: *Pardes, id: usize, from: Origin, content: []u8) !void {
const pane = p.panes[id] orelse return error.MissingPane;
const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
const free = p.freeSlot() orelse return error.NoPaneSlots;
- const np = try open(p, free, dir, from, "", content);
- exec.placeDoc(p, id, free, np);
+ _ = try open(p, free, dir, from, "", content);
+ exec.placeNew(p, id, id, free, .doc);
p.active = id;
pane.search_pane = free;
pane.search_row = null;
@@ -777,7 +777,7 @@ fn openRead(p: *Pardes, id: usize, from: Origin, arg: []const u8, content: []u8)
}
const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
const free = p.freeSlot() orelse return error.NoPaneSlots;
- const np = try open(p, free, dir, from, arg, content);
- exec.placeDoc(p, id, free, np);
+ _ = try open(p, free, dir, from, arg, content);
+ exec.placeNew(p, id, id, free, if (from == .errors) .errors else .doc);
p.active = free;
}
diff --git a/src/Terminal.zig b/src/Terminal.zig
index 91c23305..76be29d0 100644
--- a/src/Terminal.zig
+++ b/src/Terminal.zig
@@ -659,6 +659,36 @@ test "replay restores terminal cells from the retained stream" {
try std.testing.expectEqualSlices(u8, try replayBytes(pane, gpa), try replayBytes(restored, gpa));
}
+test "a restored command pane comes back finished, its command not run again" {
+ if (comptime !enabled or pardes.isolated) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 60, .rows = 16 });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const cmd = p.freeSlot().?;
+ _ = try p.newCommand(cmd, "/tmp", "make test");
+ feedOutput(p, p.panes[cmd].?, "ok\r\n");
+ p.update(.{ .exited = .{ .pane = @intCast(cmd), .status = 2 } });
+ try dump.dumpState(p);
+ const restored = try dump.initFromDump(gpa, .{ .tty_only = true, .cols = 60, .rows = 16 }, p.dump_out.?);
+ defer restored.deinit();
+ var spawns: usize = 0;
+ while (restored.nextEffect()) |effect| switch (effect) {
+ .spawn => spawns += 1,
+ else => {},
+ };
+ try std.testing.expectEqual(@as(usize, 1), spawns); // the shell, not the command
+ const again = for (restored.panes) |slot| {
+ if (slot) |pane| if (pane.command != null) break pane;
+ } else return error.CommandPaneLost;
+ try std.testing.expectEqualStrings("make test", again.command.?);
+ try std.testing.expect(again.command_done);
+ try std.testing.expectEqual(@as(?u8, 2), again.command_status);
+ const text = try screenTextAlloc(again, gpa);
+ defer gpa.free(text);
+ try std.testing.expect(std.mem.indexOf(u8, text, "ok") != null);
+}
+
test "a restored terminal keeps its history above a marker and gets a shell where it was" {
if (comptime !enabled or pardes.isolated) return error.SkipZigTest;
const gpa = std.testing.allocator;
@@ -668,6 +698,7 @@ test "a restored terminal keeps its history above a marker and gets a shell wher
const pane = p.panes[0].?;
feedOutput(p, pane, "old output\r\n$ ls\r\n");
try pane.setOwnedCwd("/tmp");
+ pane.shell = try gpa.dupe(u8, "bash"); // `Tty bash`: it comes back as bash
try dump.dumpState(p);
const restored = try dump.initFromDump(gpa, .{ .tty_only = true, .cols = 40, .rows = 12 }, p.dump_out.?);
defer restored.deinit();
@@ -678,6 +709,8 @@ test "a restored terminal keeps its history above a marker and gets a shell wher
};
try std.testing.expect(spawned);
const again = restored.panes[0].?;
+ try std.testing.expectEqualStrings("bash", again.shell.?);
+ try std.testing.expect(again.command == null);
try std.testing.expectEqual(.spawn, again.pending_command.wait);
const text = try screenTextAlloc(again, gpa);
defer gpa.free(text);
@@ -961,6 +994,15 @@ pub const CommandOutput = struct {
/// What the newest finished command printed: the screen text between its
/// C and D marks, read from at most `max_rows` rows above its end, so a
/// command that printed the whole history costs no more than its tail.
+/// The shell exited in the middle of a command (its `exit 3`): no end mark
+/// will come, so the command's output ends where the cursor is.
+pub fn endOutputHere(pane: *Pane) void {
+ if (comptime !enabled) return;
+ const state = pane.terminal orelse return;
+ const marks = &state.stream.handler;
+ if (marks.phase == .running and marks.out_end == null) marks.pinCursor(&marks.out_end, true);
+}
+
pub fn commandOutput(pane: *Pane, gpa: std.mem.Allocator, max_rows: usize) !CommandOutput {
// Unless there is text to read, whether the nothing it printed is the
// whole of it.
@@ -1427,6 +1469,9 @@ pub fn dumpPane(
.collapsed = pane.collapsed,
.terminal = .{
.cwd = try arena.dupe(u8, pane.cwdSlice()),
+ .shell = try arena.dupe(u8, pane.shell orelse ""),
+ .command = try arena.dupe(u8, pane.command orelse ""),
+ .status = pane.command_status,
.stream = try arena.dupe(u8, text),
.stream_b64 = &.{},
.cursor = .{ .col = 0, .row = 0 },
@@ -1476,6 +1521,9 @@ pub fn dumpPane(
.collapsed = pane.collapsed,
.terminal = .{
.cwd = try arena.dupe(u8, pane.cwdSlice()),
+ .shell = try arena.dupe(u8, pane.shell orelse ""),
+ .command = try arena.dupe(u8, pane.command orelse ""),
+ .status = pane.command_status,
.stream = stream[0..len],
.stream_b64 = try dump.encodeBytes(arena, try screenVt(pane, arena)),
.stream_is_screen = true,
@@ -2111,6 +2159,8 @@ test "terminal Filter keys indexed truecolor OSC and background-only cells throu
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
+ pane.tag_expanded = false; // a one-row tag keeps this small screen's rows
+ p.sync();
try testing.expect(pane.tty_filter);
pane.tty_filter = false;
diff --git a/src/body_layer.zig b/src/body_layer.zig
index 875f2e36..6e1fe8f1 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -72,7 +72,7 @@ fn paintTerminalSelection(p: *Pardes, s: *Surface, pane: *Pane, r: Rect, rows: [
hi = @min(hi, line.len);
if (lo >= hi) continue;
var col = panes.File.rawDisplayCol(line, lo);
- const end = panes.File.rawDisplayCol(line, hi);
+ const end = if (row.to_edge and hi == line.len) width else panes.File.rawDisplayCol(line, hi);
while (col < end and col < width) : (col += 1) {
const cell = s.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(visible)));
cell.default = false;
@@ -103,7 +103,7 @@ fn paintSourceSelection(p: *Pardes, s: *Surface, pane: *Pane, r: Rect, rows: []c
const hi = @min(end, row.hi);
if (lo >= hi) continue;
var col = prefix + @as(i32, @intCast(panes.File.rawDisplayCol(line[start..], lo - start)));
- const last = prefix + @as(i32, @intCast(panes.File.rawDisplayCol(line[start..], hi - start)));
+ const last = if (row.to_edge and hi == end) @as(i32, width) else prefix + @as(i32, @intCast(panes.File.rawDisplayCol(line[start..], hi - start)));
while (col < last and col < width) : (col += 1) {
const cell = s.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(visible)));
cell.default = false;
@@ -130,13 +130,12 @@ pub fn paintPointerSelection(
fg: ?[3]u8,
) void {
if (pane.pointerSelection(sl)) |rows| return paintSourceSelection(p, s, pane, r, rows, bg, fg);
- const r0 = @min(sl.r0, sl.r1);
- const r1 = @max(sl.r0, sl.r1);
- const c0 = @max(0, @min(sl.c0, sl.c1));
- const c1 = @max(0, @max(sl.c0, sl.c1));
var row: u16 = 0;
while (row < r.h) : (row += 1) {
- if (@as(i32, row) < r0 or @as(i32, row) > r1 or row < from_row) continue;
+ if (row < from_row) continue;
+ const cols = edit.sweepCols(sl, row) orelse continue;
+ const c0: i32 = if (cols.first) cols.lo else 0;
+ const c1: i32 = cols.hi orelse std.math.maxInt(i16);
const sy = if (row < pane.tag_rows) tag_y + row else body_y + row - pane.tag_rows;
// File line numbers occupy a gutter only in the body. The tag is
// row zero in Sel space and starts at its real first text cell.
diff --git a/src/builtins.zig b/src/builtins.zig
index c25311e3..657f49d0 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -171,7 +171,7 @@ pub const registry = struct {
inline for (comptime settingList(), manualBuiltinCount()..) |setting, i|
if (@intFromEnum(b) == i) return switch (setting.action) {
// a switch flips bare, and DumpDir bare is the default
- .toggle, .transition, .scene, .dump_dir, .lift => false,
+ .toggle, .transition, .scene, .dump_dir, .choice, .lift => false,
.shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .shader_animation, .motion, .inactive_dim, .grip_width => true,
};
unreachable;
@@ -242,6 +242,28 @@ test "Exit asks once about unsaved text, and quits when asked again" {
try std.testing.expect(p.quit);
}
+test "Exit over a ctl write names every pane it asks about, and the next Exit discards them all" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ const first = try p.setTestFile("saved\n");
+ first.file.?.saved_revision = first.file.?.revision -% 1;
+ p.newScratchBelow(p.active);
+ const second = p.panes[p.active].?;
+ panes.File.setContent(p, &second.file.?, try std.testing.allocator.dupe(u8, "x" ** 120));
+ try std.testing.expect(second.file.?.revision != second.file.?.saved_revision);
+ p.fs.no_prompt = true;
+ defer p.fs.no_prompt = false;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
+ try std.testing.expect(!p.quit);
+ const said = p.fs.failure[0..p.fs.failure_len];
+ try std.testing.expect(std.mem.indexOf(u8, said, first.file.?.path) != null);
+ try std.testing.expect(std.mem.indexOf(u8, said, second.file.?.path) != null);
+ try std.testing.expect(std.mem.endsWith(u8, said, ": Modified (Exit again to discard them all)"));
+ p.fs.failure_len = 0;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
+ try std.testing.expect(p.quit);
+}
+
/// A dump file that exists, for a Restore to get past reading it.
fn testDump(buf: []u8) ![]const u8 {
var tmp = std.testing.tmpDir(.{});
@@ -421,6 +443,10 @@ pub const Exec = struct {
/// worth the question, as winclean does not ask for one.
fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
var modified = false;
+ // Every pane asked about is named in the one refusal a ctl write gets,
+ // as acme's rowclean names them all (wind.c:511).
+ var all_buf: [256]u8 = undefined;
+ var all = std.Io.Writer.fixed(&all_buf);
for (c.p.panes, 0..) |slot, id| {
const pane = slot orelse continue;
const f = pane.file orelse continue;
@@ -430,8 +456,15 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
pane.discard_warned = .{ .revision = f.revision, .by = asking };
var buf: [limits.host_path_cap + 48]u8 = undefined;
c.p.reportFailure(id, std.fmt.bufPrint(&buf, "{s}: Modified ({s} again to discard)", .{ f.path, @tagName(asking) }) catch f.path);
+ all.print("{s}{s}", .{ if (modified) ", " else "", f.path }) catch {};
modified = true;
}
+ if (modified and c.p.fs.no_prompt) {
+ all.print(": Modified ({s} again to discard them all)", .{@tagName(asking)}) catch {};
+ const said = @import("Messages.zig").clip(all.buffered(), c.p.fs.failure.len);
+ @memcpy(c.p.fs.failure[0..said.len], said);
+ c.p.fs.failure_len = @intCast(said.len);
+ }
return modified;
}
@@ -787,11 +820,11 @@ pub const New = struct {
}
};
-/// The same empty scratch, opened in a fresh column beside the calling pane.
+/// An empty column right of the keyboard's, its tag taking the keyboard.
pub const Newcol = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
- c.p.newScratchColumn(c.id);
+ c.p.newColumn(c.id);
}
};
@@ -823,6 +856,8 @@ pub const Del = struct {
{
c.pane.prompt = .del_side;
c.p.active = c.id;
+ var rec: [32]u8 = undefined;
+ pardes.ctlfs.events.notePath(c.p, "ask", std.fmt.bufPrint(&rec, "{d} del k j", .{c.pane.serial}) catch "");
// The question says Del already.
if (c.pane.msg_announcement) c.pane.msg_len = 0;
return;
@@ -852,6 +887,51 @@ pub const Togglettymode = struct {
}
};
+/// acme's Edit (exec.c `edit`, edit.c): sam's command language over the
+/// pane's body, from a tag, a pane ctl or exec. One undo step; a failure
+/// changes nothing and says why. src/sam_edit.zig has what is left out.
+pub const Edit = struct {
+ pub const takes_arg = true;
+ pub const requires_arg = true;
+ pub fn run(c: Ctx) void {
+ const sam = @import("sam_edit.zig");
+ const ninep_pane = @import("ninep/pane.zig");
+ const f = ninep_pane.fileOf(c.pane) orelse return c.p.reportFailure(c.id, "Edit: only a text pane is edited");
+ const arena = c.p.scratch.allocator();
+ var why: sam.Why = .{};
+ var said: [260]u8 = undefined;
+ const res = sam.run(arena, f.content, ninep_pane.dotOf(c.pane), f.path, c.arg orelse "", &why) catch |err| switch (err) {
+ error.Edit => return c.p.reportFailure(c.id, std.fmt.bufPrint(&said, "Edit: {s}", .{why.text()}) catch "Edit: failed"),
+ error.OutOfMemory => return c.p.reportError(c.id, "Edit", err),
+ };
+ if (res.undo != 0) {
+ for (0..@abs(res.undo)) |_| if (res.undo > 0) panes.File.undo(c.p, c.pane) else panes.File.redo(c.p, c.pane);
+ return;
+ }
+ if (res.ops.len > 0) {
+ const new = sam.apply(c.p.gpa, f.content, res.ops) catch |err| return c.p.reportError(c.id, "Edit", err);
+ panes.File.pushUndo(c.p, c.pane);
+ panes.File.setContent(c.p, f, new);
+ }
+ ninep_pane.setDot(c.pane, sam.moveDot(res.dot, res.ops));
+ if (res.printed.len > 0) _ = ninep_pane.appendErrors(c.p, c.id, res.printed);
+ }
+};
+
+/// acme's Undo and Redo (exec.c `undo`): step the pane's body back or
+/// forward through its edits, from a tag, a pane ctl or exec.
+pub const Undo = struct {
+ pub fn run(c: Ctx) void {
+ edit.doUndo(c.p, &c.pane.body);
+ }
+};
+
+pub const Redo = struct {
+ pub fn run(c: Ctx) void {
+ edit.doRedo(c.p, &c.pane.body);
+ }
+};
+
/// Fold a pane to its tag, or restore its share of the column.
pub const Collapse = struct {
pub fn run(c: Ctx) void {
@@ -861,7 +941,10 @@ pub const Collapse = struct {
pub const Delcol = struct {
pub fn run(c: Ctx) void {
- c.p.removeColumn(c.id) catch |err| c.p.reportError(c.id, "close column", err);
+ // The column of the tag it came from, else the pane's: a pane ctl's
+ // Delcol closes that pane's column wherever the keyboard is.
+ const col = c.p.exec_column orelse (layout.findPane(c.p, c.id) orelse return).col;
+ c.p.removeColumn(col) catch |err| c.p.reportError(c.id, "close column", err);
}
};
@@ -973,9 +1056,9 @@ pub const Joincol = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
// The keyboard's column joins the one to its right; the last has none.
- const at = layout.findPane(c.p, c.p.active) orelse return;
- if (at.col + 1 >= c.p.ncol) return c.p.reportFailure(c.id, "Joincol: no column to the right");
- layout.joinCol(c.p);
+ const col = c.p.keyboardColumn(c.p.active) orelse return;
+ if (col + 1 >= c.p.ncol) return c.p.reportFailure(c.id, "Joincol: no column to the right");
+ layout.joinCol(c.p, col);
}
};
@@ -983,8 +1066,8 @@ pub const Tutor = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
const free = c.p.freeSlot() orelse return;
- const nt = c.p.openTutorView(free) catch return;
- exec.placeDoc(c.p, c.id, free, nt); // a doc like any other
+ _ = c.p.openTutorView(free) catch return;
+ exec.placeNew(c.p, c.id, c.id, free, .doc); // a doc like any other
}
};
@@ -1097,8 +1180,10 @@ pub const Find = struct {
pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true };
pub fn run(c: Ctx) void {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
- if (pat.len > 0) return look.runSearch(c.p, c.id, pat, .find, .top) catch |err|
- c.p.reportError(c.id, "find", err);
+ if (pat.len > 0) {
+ _ = look.runSearch(c.p, c.id, pat, .find, .top, true) catch |err| c.p.reportError(c.id, "find", err);
+ return;
+ }
c.p.startPrompt(c.pane, &c.pane.body, .{ .search = config.find_marker });
}
};
@@ -1111,8 +1196,10 @@ pub const Grep = struct {
pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true, .locations = true };
pub fn run(c: Ctx) void {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
- if (pat.len > 0) return look.runSearch(c.p, c.id, pat, .grep, .top) catch |err|
- c.p.reportError(c.id, "grep", err);
+ if (pat.len > 0) {
+ _ = look.runSearch(c.p, c.id, pat, .grep, .top, true) catch |err| c.p.reportError(c.id, "grep", err);
+ return;
+ }
c.p.startPrompt(c.pane, &c.pane.body, .{ .search = config.grep_marker });
}
};
@@ -1141,8 +1228,10 @@ pub const Down = struct {
return tagline.enterHeader(c.p, if (column) |at| at.col else 0);
// Onto the top pane of the column the header stands over.
const col = c.p.header_column orelse if (column) |at| at.col else return;
+ // An empty column's tag has nothing below it.
+ if (col < c.p.ncol and c.p.col_n[col] == 0) return;
tagline.exitHeader(c.p);
- if (col < c.p.ncol and c.p.col_n[col] > 0) c.p.active = c.p.col_panes[col][0];
+ if (col < c.p.ncol) c.p.active = c.p.col_panes[col][0];
return;
}
_ = layout.focusDir(c.p, c.id, .down);
diff --git a/src/colors.zig b/src/colors.zig
index 25aa838f..224591d3 100644
--- a/src/colors.zig
+++ b/src/colors.zig
@@ -54,6 +54,8 @@ pub const Theme = struct {
column_box: ?[3]u8 = null,
column_box_dim: ?[3]u8 = null,
border: ?[3]u8 = null,
+ /// What an empty column shows under its tag; the border fill without it.
+ empty_col: ?[3]u8 = null,
search_bg: ?[3]u8 = null,
search_fg: ?[3]u8 = null,
diagnostic_error: ?[3]u8 = null,
@@ -169,6 +171,7 @@ pub const ChromeTheme = struct {
tag_name_fg: [3]u8,
tag_active_name_fg: [3]u8,
border: [3]u8,
+ empty_col: [3]u8,
search_bg: [3]u8,
search_fg: [3]u8,
diagnostic_error: [3]u8,
@@ -192,6 +195,7 @@ pub const ChromeTheme = struct {
.tag_name_fg = th.tag_name_fg orelse th.tag_fg,
.tag_active_name_fg = th.tag_active_name_fg orelse (th.tag_name_fg orelse (th.tag_active_fg orelse th.tag_fg)),
.border = th.border orelse th.scroll_track,
+ .empty_col = th.empty_col orelse (th.border orelse th.scroll_track),
.search_bg = th.search_bg orelse th.sel_bg,
.search_fg = th.search_fg orelse th.sel_fg,
.diagnostic_error = th.diagnostic_error orelse (th.fg orelse th.tag_fg),
diff --git a/src/config.zig b/src/config.zig
index ce051be0..24c48770 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -49,6 +49,9 @@ pub const leader_path = paths: {
.Mode = null,
.Togglettymode = null,
.Collapse = null,
+ .Undo = null,
+ .Edit = null,
+ .Redo = null,
.Repl = null,
.Kill = null,
.Exit = null,
@@ -70,11 +73,12 @@ pub const leader_path = paths: {
.Newcol = "cn",
.Delcol = "cd",
.Joincol = "cj",
+ .Placement = "cp",
+ .BootShell = null,
.Debug = "td",
.Msg = null,
.Colors = "tc",
.FocusTint = null,
- .ColumnTags = null,
.SyntaxBold = null,
.Verbose = null,
.TreeContextTagStyle = null,
@@ -617,7 +621,6 @@ pub const Runtime = struct {
/// menu bar) turn it off and carry its commands there instead; the core
/// keeps the row as the acme tradition everywhere else.
workspace_tag: bool = true,
- column_tags: bool = true,
syntax_bold: bool = false,
/// A font's programming ligatures (`->`, `!=`) draw across their cells.
/// Only a shell that shapes text has the setting (`Capabilities.ligatures`).
@@ -683,8 +686,19 @@ pub const Runtime = struct {
/// never, while the window has the focus, or always (ghostty's
/// custom-shader-animation).
shader_animation: ShaderAnimation = .on,
+ /// Where a new pane goes (exec.placeNew): `acme`, acme's makenewwindow,
+ /// or `pardes`, the rules pardes had before it.
+ placement: Placement = .acme,
+ /// A document dragged into the left column where a lone shell nobody
+ /// has typed into stands: `keep` leaves the shell, `replace` closes it
+ /// (exec.evictLonePristineTty).
+ boot_shell: BootShell = .keep,
pub const ShaderAnimation = enum { off, on, always };
+ pub const Placement = enum { acme, pardes };
+ pub const BootShell = enum { keep, replace };
+ /// A setting that is one of a few words, each an enum field of Runtime.
+ pub const Choice = enum { placement, boot_shell };
/// shadow: a soft drop shadow. rim: a light hairline along the top of
/// the focused pane's tag rows. auto: a shadow on a light page; on a
@@ -849,7 +863,7 @@ pub const Runtime = struct {
/// The message timings a setting word sets, each in milliseconds.
pub const MessageTime = enum { linger, fall, dissolve };
- pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, column_tags, syntax_bold, verbose, message_animation, ligatures };
+ pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, syntax_bold, verbose, message_animation, ligatures };
/// The bundled post pass, a Shadertoy file in shaders/post/.
pub const Scene = enum { crt };
@@ -876,6 +890,7 @@ pub const Runtime = struct {
/// it of files.
shader,
shader_animation,
+ choice: Choice,
};
pub const Setting = struct {
@@ -899,7 +914,6 @@ pub const Runtime = struct {
.{ .word = "Tagbottom", .action = .{ .toggle = .tag_bottom } },
.{ .word = "Debug", .action = .{ .toggle = .debug } },
.{ .word = "FocusTint", .action = .{ .toggle = .focus_tint } },
- .{ .word = "ColumnTags", .action = .{ .toggle = .column_tags } },
.{ .word = "SyntaxBold", .action = .{ .toggle = .syntax_bold } },
.{ .word = "Verbose", .action = .{ .toggle = .verbose } },
.{ .word = "MessageAnimation", .action = .{ .toggle = .message_animation } },
@@ -932,6 +946,8 @@ pub const Runtime = struct {
.{ .word = "Crt", .action = .{ .scene = .crt }, .availability = .scene_shaders },
.{ .word = "Shader", .action = .shader, .availability = .scene_shaders },
.{ .word = "ShaderAnimation", .action = .shader_animation, .availability = .scene_shaders },
+ .{ .word = "Placement", .action = .{ .choice = .placement } },
+ .{ .word = "BootShell", .action = .{ .choice = .boot_shell } },
};
pub fn find(name: []const u8) ?Setting {
@@ -1092,6 +1108,18 @@ pub const Runtime = struct {
const text = std.mem.trim(u8, argument orelse return false, " \t\r\n");
state.shader_animation = std.meta.stringToEnum(ShaderAnimation, text) orelse return false;
},
+ // Bare, it steps to the next word, as a tag word should.
+ .choice => |which| switch (which) {
+ inline else => |field| {
+ const now = &@field(state, @tagName(field));
+ const T = @TypeOf(now.*);
+ const text = std.mem.trim(u8, argument orelse "", " \t\r\n");
+ now.* = if (text.len == 0)
+ @enumFromInt((@as(usize, @intFromEnum(now.*)) + 1) % std.enums.values(T).len)
+ else
+ std.meta.stringToEnum(T, text) orelse return false;
+ },
+ },
.lift => {
const text = std.mem.trim(u8, argument orelse "", " \t\r\n");
state.lift = if (text.len == 0)
@@ -1286,6 +1314,9 @@ pub const Runtime = struct {
try out.print("{s} {s}\n", .{ setting.word, @tagName(state.shader_animation) })
else
try out.print("{s} unsupported\n", .{setting.word}),
+ .choice => |which| switch (which) {
+ inline else => |field| try out.print("{s} {s}\n", .{ setting.word, @tagName(@field(state, @tagName(field))) }),
+ },
.lift => if (setting.enabled(context.capabilities))
try out.print("{s} {s}\n", .{ setting.word, @tagName(state.lift) })
else
@@ -1560,6 +1591,10 @@ pub const Runtime = struct {
.dump_dir => "~/dumps",
.shader => "~/crt.glsl",
.shader_animation => "always",
+ .choice => |which| switch (which) {
+ .placement => "pardes",
+ .boot_shell => "replace",
+ },
.lift => "rim",
.motion => "playful",
.inactive_dim => "35",
@@ -1579,7 +1614,6 @@ pub const Runtime = struct {
.tag_bottom => "Tagbottom on\n",
.debug => "Debug on\n",
.focus_tint => "FocusTint off\n",
- .column_tags => "ColumnTags off\n",
.syntax_bold => "SyntaxBold on\n",
.verbose => "Verbose off\n",
.message_animation => "MessageAnimation off\n",
@@ -1614,6 +1648,10 @@ pub const Runtime = struct {
},
.shader => "Shader ~/crt.glsl\n",
.shader_animation => "ShaderAnimation always\n",
+ .choice => |which| switch (which) {
+ .placement => "Placement pardes\n",
+ .boot_shell => "BootShell replace\n",
+ },
.lift => "Lift rim\n",
.motion => "Motion playful\n",
.inactive_dim => "InactiveDim 35\n",
diff --git a/src/detached/server.zig b/src/detached/server.zig
index 5374bf73..857591d4 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -533,7 +533,7 @@ pub const Session = struct {
const pt = s.ptys[pane];
// A command whose exit is recorded is reaped: its group may be another's.
if (pt.fd < 0 or pt.cmd.exited) return;
- host_io.signalTty(pt.pid, pt.fd, sig);
+ host_io.signalTty(pt.pid, pt.fd, sig, pt.cmd.watched);
}
fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {
@@ -555,7 +555,7 @@ pub const Session = struct {
fn writeFile(ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void {
const s = of(ctx);
filesystem.write(s.core, path, bytes) catch |err|
- return s.core.saveFailed(pane, "save", err);
+ return s.core.saveFailed(pane, path, err);
if (s.core.panes[pane]) |pn| if (pn.file) |f| if (std.mem.eql(u8, f.path, path)) {
if (s.watches[pane]) |*w| if (w.serial == pn.serial) switch (w.generation) {
.text => w.generation = .{ .text = std.hash.Wyhash.hash(0, bytes) },
@@ -706,16 +706,14 @@ pub const Session = struct {
// it would hang up one that runs on without it.
// Its exit, if it came first, is told now its output is in.
if (pt.cmd.watched) return host_io.commandEof(s.core, &s.ptys, pane, s, closeWatched);
- // Unwatched, a command's exit is read here, as its end.
+ // Unwatched, a command's exit is read here, as its end, and a
+ // shell's, for a run waiting on it and the log.
const unwatched = if (s.core.panes[pane]) |pn| pn.command != null else false;
- var status: ?u8 = null;
- if (unwatched) {
- status = host_io.exitStatus(pt.pid, 100);
- if (status != null) pt.pid = 0; // reaped: no shell to retire
- }
+ const status = host_io.exitStatus(pt.pid, 100);
+ if (status != null) pt.pid = 0; // reaped: no shell to retire
s.closePty(pane);
s.harvest();
- if (unwatched) s.core.update(.{ .exited = .{ .pane = pane, .status = status } });
+ if (unwatched or status != null) s.core.update(.{ .exited = .{ .pane = pane, .status = status } });
s.core.update(.{ .eof = .{ .pane = pane } });
}
diff --git a/src/draw.zig b/src/draw.zig
index 9df76cfd..963d61c1 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -366,6 +366,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
// the pointer per-text (the macOS I-beam) read this as "target" and
// afford it the arrow instead.
if (p.look_hover_preview != null) s.pointer_shape = .target;
+ if (p.drag == .move or p.drag == .column_move) s.pointer_shape = .box;
for (&s.body_layers) |*layer| layer.rows = 0;
for (&s.tag_layers) |*layer| layer.rows = 0;
s.nimages = 0;
@@ -401,6 +402,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
// Separators have their own role; a scrollbar need not frame the page.
const whole = s.regions[0].rect;
s.fill(whole.x, whole.y, whole.w, whole.h, .{ .bg = .{ .rgb = chrome.border } });
+ for (0..p.ncol) |c| if (p.col_n[c] == 0) s.fill(p.col_x[c], p.topBarHeight() + p.columnBarHeight(), p.col_w[c], s.rows -| (p.topBarHeight() + p.columnBarHeight()), .{ .bg = .{ .rgb = chrome.empty_col } });
for (&p.panes, 0..) |*slot, id| {
const pane = slot.* orelse continue;
@@ -947,9 +949,9 @@ fn joinPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r
if (r.w <= TAG_TEXT_INSET) return paintPaneTag(p, s, arena, pane, r, id, active, capacity);
const chrome = p.chromeTheme();
const region = s.region(.tag, id).?;
- const tag = try tagline.tagText(p, arena, pane);
+ const tag = (try tagline.shownTag(p, arena, pane)).text;
const rows = pane.tag_rows;
- // As wide as the widest line it shows, and never narrower than its
+ // As wide as the widest row it shows, and never narrower than its
// viewport.
var widest: usize = 0;
var split = std.mem.splitScalar(u8, tag, '\n');
@@ -1093,8 +1095,10 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa
// A row per line shown, from `tag_top`: the tag's first line carries
// the prefix, the rest are the tag's own text alone.
const top = pane.tag_top;
+ // Its lines wrapped at its width (tagline.shownTag): a row per row shown.
+ const shown = try tagline.shownTag(p, arena, pane);
var lines: [MAX_TAG_ROWS][]const u8 = @splat("");
- var split = std.mem.splitScalar(u8, whole, '\n');
+ var split = std.mem.splitScalar(u8, shown.text, '\n');
for (0..top) |_| _ = split.next();
for (lines[0..rows]) |*line| line.* = split.next() orelse "";
// The keyboard's caret, as a row and a byte offset in that row's
@@ -1103,42 +1107,45 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa
const Caret = struct { row: usize, at: usize };
const caret: ?Caret = if (!active)
null
- else if (pane.prompt == .name)
- .{ .row = 0, .at = @min(lines[0].len, @as(usize, @intCast(@max(0, pane.input.cur_col)))) }
- else if (pane.focus == .tag and pane.prompt == .none and pane.tag.cur_row >= top and pane.tag.cur_row < top + rows) caret: {
- const row: usize = @intCast(pane.tag.cur_row - top);
- break :caret .{ .row = row, .at = @min(lines[row].len, @as(usize, @intCast(@max(0, pane.tag.cur_col)))) };
+ else if (pane.prompt == .name) caret: {
+ // A drafted name is the prefix: its caret is on row 0 of the tag,
+ // the rows it wrapped onto included.
+ const at = modal.positionAt(shown.text, shown.fromReal(@intCast(@max(0, pane.input.cur_col))));
+ if (at.row < top or at.row >= top + rows) break :caret null;
+ break :caret .{ .row = at.row - top, .at = @min(lines[at.row - top].len, at.col) };
+ }
+ else if (pane.focus == .tag and pane.prompt == .none) caret: {
+ const real = modal.offsetAt(whole, .{ .row = @intCast(@max(0, pane.tag.cur_row)), .col = @intCast(@max(0, pane.tag.cur_col)) });
+ const at = modal.positionAt(shown.text, shown.fromReal(real));
+ if (at.row < top or at.row >= top + rows) break :caret null;
+ const row: usize = at.row - top;
+ break :caret .{ .row = row, .at = @min(lines[row].len, at.col) };
} else null;
- if (caret) |at| {
- const line = lines[at.row];
- const col = panes.File.rawDisplayCol(line, at.at);
- var scroll: usize = pane.tag_scroll;
- if (col < scroll) scroll = col;
- if (col >= scroll + capacity) scroll = col - capacity + 1;
- var raw = panes.File.rawAtDisplay(line, scroll);
- scroll = panes.File.rawDisplayCol(line, raw);
- if (col >= scroll + capacity) {
- raw = modal.nextGrapheme(line, raw);
- scroll = panes.File.rawDisplayCol(line, raw);
- }
- // A line past a u16 of columns keeps its caret off the band.
- pane.tag_scroll = @intCast(@min(scroll, std.math.maxInt(u16)));
- } else if (pane.focus != .tag) pane.tag_scroll = 0;
+ // Its rows are wrapped to fit: a pane tag never scrolls sideways.
+ _ = capacity;
+ pane.tag_scroll = 0;
for (lines[0..rows], 0..) |line, row| {
// The scroll is the caret's line's: a shorter row shows nothing.
const start = @min(line.len, panes.File.rawAtDisplay(line, pane.tag_scroll));
_ = s.print(tx, tag_y + @as(u16, @intCast(row)), tw, line[start..], tag_style);
}
- const tag = lines[0];
- if (top == 0) if (tagTintRange(pane, tag, prefix_len)) |name| {
- const lo = @max(name.lo, panes.File.rawAtDisplay(tag, pane.tag_scroll));
- const hi = @min(name.hi, tag.len);
- if (lo < hi) {
- var col = panes.File.rawDisplayCol(tag, lo) -| pane.tag_scroll;
- const end = panes.File.rawDisplayCol(tag, hi) -| pane.tag_scroll;
- const name_fg = if (active and p.settings.focus_tint) chrome.tag_active_name_fg else chrome.tag_name_fg;
+ // The name's tint, in the tag's first line, over whichever rows it
+ // wrapped onto.
+ const first_line = whole[0 .. std.mem.indexOfScalar(u8, whole, '\n') orelse whole.len];
+ if (tagTintRange(pane, first_line, prefix_len)) |name| if (name.lo < name.hi) {
+ const name_fg = if (active and p.settings.focus_tint) chrome.tag_active_name_fg else chrome.tag_name_fg;
+ const a = modal.positionAt(shown.text, shown.fromReal(name.lo));
+ const b = modal.positionAt(shown.text, shown.fromReal(@min(name.hi, first_line.len)));
+ var row = @max(a.row, top);
+ while (row <= b.row and row < top + rows) : (row += 1) {
+ const line = lines[row - top];
+ const from = if (row == a.row) a.col else 0;
+ const to = if (row == b.row) b.col else line.len;
+ var col = panes.File.rawDisplayCol(line, @min(from, line.len));
+ const end = panes.File.rawDisplayCol(line, @min(to, line.len));
+ const y = tag_y + @as(u16, @intCast(row - top));
while (col < end and col < tw) : (col += 1)
- s.at(tx + @as(u16, @intCast(col)), tag_y).style.fg = .{ .rgb = name_fg };
+ s.at(tx + @as(u16, @intCast(col)), y).style.fg = .{ .rgb = name_fg };
}
};
if (p.look_hover_preview) |preview| {
@@ -1156,8 +1163,8 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa
const lo = @min(range.anchor, range.head);
const hi = @max(range.anchor, range.head);
if (modal.nextGrapheme(whole, lo) >= hi) continue;
- const first = modal.positionAt(whole, lo);
- const last = modal.positionAt(whole, hi);
+ const first = modal.positionAt(shown.text, shown.fromReal(lo));
+ const last = modal.positionAt(shown.text, shown.fromReal(hi));
var row = @max(first.row, top);
while (row <= last.row and row < top + rows) : (row += 1) {
const line = lines[row - top];
@@ -1299,14 +1306,14 @@ test "place puts every part where the painters and layers put it, in every layou
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
defer p.deinit();
_ = try p.setTestFile("placed\n" ** 80);
- p.newScratchColumn(0);
+ p.newColumn(0);
+ p.newScratchIn(0, p.activeColumn().?);
p.newScratchBelow(0);
p.sync();
var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
defer arena.deinit();
for (0..16) |step| {
p.settings.tag_bottom = step & 1 != 0;
- p.settings.column_tags = step & 2 != 0;
if (step & 4 != 0) try std.testing.expect(p.executeBuiltinLine(p.active, "Msg a placed notice"));
if (step == 8) try std.testing.expect(p.executeBuiltinLine(1, "Collapse"));
if (step == 12) try std.testing.expect(p.executeBuiltinLine(1, "Collapse"));
@@ -1661,7 +1668,7 @@ test "a monkey over notices, prompts, tags and tiny panes at a gui's metrics nev
var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
defer arena.deinit();
const words = [_][]const u8{ "error:", "\u{754c}", "x", "Shader /tmp/a.glsl:", "e\u{301}", "\t", "long-" ** 20, " " };
- const commands = [_][]const u8{ "New", "Newcol", "Collapse", "Tagbottom", "ColumnTags", "TreeContext", "Wrap", "Find", "Del", "FocusTint", "Debug" };
+ const commands = [_][]const u8{ "New", "Newcol", "Collapse", "Tagbottom", "TreeContext", "Wrap", "Find", "Del", "FocusTint", "Debug" };
var text: std.ArrayListUnmanaged(u8) = .empty;
defer text.deinit(std.testing.allocator);
for (0..steps) |_| {
diff --git a/src/dump.zig b/src/dump.zig
index 954a3270..ef442ddb 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -99,6 +99,13 @@ pub const PaneKind = enum {
pub const Terminal = struct {
cwd: []const u8 = "",
+ /// The shell it runs when not the configured one (`Tty bash`); empty,
+ /// the configured one, as in dumps from before.
+ shell: []const u8 = "",
+ /// A command pane's line and how it ended (null: unknown, or still
+ /// running when dumped); empty for an interactive terminal.
+ command: []const u8 = "",
+ status: ?u8 = null,
/// The rendered scrollback and screen as plain text, one row per line.
stream: []const u8 = "",
stream_b64: []const u8 = "",
@@ -161,6 +168,9 @@ pub const Pane = struct {
rows: u16 = 0,
vweight: f32 = 1,
collapsed: bool = false,
+ /// Its serial when dumped (0 in older dumps): a Restore logs
+ /// `restored <old> <new>` for it.
+ serial: u32 = 0,
/// Exact editable tail when the user touched it. Null retains the live
/// default; a present empty slice deliberately restores an empty tail.
tag_tail: ?[]const u8 = null,
@@ -217,7 +227,8 @@ pub fn validate(state: State) !void {
}
for (state.columns) |col| {
if (!std.math.isFinite(col.weight) or col.weight <= 0) return error.BadDumpColumns;
- if (col.panes.len == 0 or col.panes.len > max_panes) return error.BadDumpColumns;
+ // An empty column is a column, as in acme's dump (rows.c:639).
+ if (col.panes.len > max_panes) return error.BadDumpColumns;
for (col.panes) |pane| {
if (pane >= state.panes.len) return error.BadDumpPaneRef;
}
@@ -625,6 +636,7 @@ pub fn dumpState(p: *Pardes) !void {
else
try pardes.panes.Terminal.dumpPane(pane, arena, tag, body, scroll);
dp.tag_tail = pane.tag.own;
+ dp.serial = pane.serial;
dump_panes[panes_len] = dp;
panes_len += 1;
}
@@ -638,14 +650,12 @@ pub fn dumpState(p: *Pardes) !void {
column_ids[c][ids_len] = compact;
ids_len += 1;
};
- if (ids_len > 0) {
- columns[columns_len] = .{
- .tag = p.col_tags[c].own,
- .weight = @as(f32, @floatFromInt(p.col_weight[c])) / @as(f32, @floatFromInt(column_weight_unit)),
- .panes = column_ids[c][0..ids_len],
- };
- columns_len += 1;
- }
+ columns[columns_len] = .{
+ .tag = p.col_tags[c].own,
+ .weight = @as(f32, @floatFromInt(p.col_weight[c])) / @as(f32, @floatFromInt(column_weight_unit)),
+ .panes = column_ids[c][0..ids_len],
+ };
+ columns_len += 1;
}
var mounts: [filesystem.max_mounts]Mount = undefined;
@@ -677,16 +687,22 @@ pub fn restore(p: *Pardes, zon_bytes: []const u8, from: []const u8) !*Pardes {
var opts = p.opts;
opts.cols = p.screen_w;
opts.rows = p.screen_h;
- const replacement = try initDump(p.gpa, opts, zon_bytes, p);
+ var old: [MAX_PANES]u32 = @splat(0);
+ const replacement = try initDump(p.gpa, opts, zon_bytes, p, &old);
pardes.ctlfs.events.notePath(replacement, "restore", from);
+ // Which serial each pane had, for a script that held the old ones.
+ for (replacement.panes, old) |slot, was| if (slot) |pane| if (was != 0) {
+ var rec: [32]u8 = undefined;
+ pardes.ctlfs.events.notePath(replacement, "restored", std.fmt.bufPrint(&rec, "{d} {d}", .{ was, pane.serial }) catch continue);
+ };
return replacement;
}
pub fn initFromDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8) !*Pardes {
- return initDump(gpa, opts, zon_bytes, null);
+ return initDump(gpa, opts, zon_bytes, null, null);
}
-fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes) !*Pardes {
+fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes, old_serials: ?*[MAX_PANES]u32) !*Pardes {
const image_gpa = opts.image_allocator orelse gpa;
const pdf_gpa = opts.pdf_allocator orelse gpa;
const tree_sitter_gpa = opts.tree_sitter_allocator orelse gpa;
@@ -736,17 +752,26 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo
}
for (st.panes, 0..) |src, i| {
if (i >= MAX_PANES) break;
+ if (old_serials) |o| o[i] = src.serial;
const pane: *pardes.panes.Pane = switch (src.kind) {
.terminal => terminal: {
const t = src.terminal.?;
// A host that runs shells gets them back: the old history
// above a new shell in the directory it was in. The web
// shell and an isolated build only ever replay it.
+ // A command pane comes back finished, showing what it said
+ // and how it ended: its command is not run again.
const live = comptime (terminal_panes and platform != .web and !isolated);
- const revive = live and t.cwd.len <= effect_path_cap;
+ const revive = live and t.cwd.len <= effect_path_cap and t.command.len == 0;
const restored = try pardes.panes.Terminal.restore(p, src, revive);
p.installPane(i, restored);
try restored.setOwnedCwd(t.cwd);
+ if (t.shell.len > 0) restored.shell = try gpa.dupe(u8, t.shell);
+ if (t.command.len > 0) {
+ restored.command = try gpa.dupe(u8, t.command);
+ restored.command_done = true;
+ restored.command_status = t.status;
+ }
if (std.mem.startsWith(u8, src.tag, "TTY ")) restored.body.mode = .tty;
if (revive) p.emit(.{ .spawn = .{ .pane = @intCast(i), .serial = restored.serial, .cwd = .from(t.cwd) } });
break :terminal restored;
diff --git a/src/edit.zig b/src/edit.zig
index 84bbb292..a194ef48 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -202,7 +202,7 @@ pub fn clipYank(p: *Pardes, pane: *Pane, main_only: bool) void {
/// (the tag's lines first, the ones it shows)
pub fn paneText(p: *Pardes, pane: *Pane) ![]u8 {
const arena = p.scratch.allocator();
- const tag = try tagline.tagText(p, arena, pane);
+ const tag = (try tagline.shownTag(p, arena, pane)).text;
const from = modal.lineStartOffset(tag, pane.tag_top);
const cut = modal.lineStartOffset(tag, pane.tag_top + pane.tag_rows);
const shown = if (cut < tag.len) tag[from .. cut - 1] else tag[from..];
@@ -234,10 +234,13 @@ pub fn capturePointerSelection(p: *Pardes, pane: *Pane, slot: usize) !void {
errdefer p.gpa.free(rows);
const raw_text = if (raw) try p.gpa.dupe(u8, try selectionText(p, pane, gesture)) else null;
const prefix: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0;
- const left = @max(0, @min(gesture.c0, gesture.c1));
- const right = @max(0, @max(gesture.c0, gesture.c1));
for (rows, 0..) |*row, index| {
const visible = first + @as(i32, @intCast(index));
+ // A stream: only the first row starts where the press was; the
+ // others start after the line numbers, and all but the last run on.
+ const cols = sweepCols(gesture, visible + @as(i32, pane.tag_rows)).?;
+ const left: i32 = if (cols.first) cols.lo else prefix;
+ const right: i32 = cols.hi orelse std.math.maxInt(i32) / 2;
var source = pane.wrapAt(visible);
if (raw) source = .{ .line = panes.Terminal.gridOffset(pane) + visible, .at = 0 };
const line = if (raw) modal.lineSlice(body, @intCast(visible)) else pointerSourceLine(pane, terminal_lines, source.line);
@@ -250,7 +253,8 @@ pub fn capturePointerSelection(p: *Pardes, pane: *Pane, slot: usize) !void {
row.* = .{
.row = source.line,
.lo = lo,
- .hi = if (right < prefix) lo else @min(end, modal.nextGrapheme(line, at)),
+ .hi = if (right < prefix) lo else if (cols.hi == null) end else @min(end, modal.nextGrapheme(line, at)),
+ .to_edge = cols.hi == null,
.raw_terminal = raw,
.prompt_bytes = if (raw) panes.Terminal.promptPrefixBytes(pane, source.line, line) else 0,
};
@@ -294,40 +298,55 @@ pub fn selectionText(p: *Pardes, pane: *Pane, sl: Pane.Sel) ![]const u8 {
}
return result;
}
- const r0 = @min(sl.r0, sl.r1);
- const r1 = @max(sl.r0, sl.r1);
- const c0: usize = @intCast(@max(0, @min(sl.c0, sl.c1)));
- const c1: usize = @intCast(@max(0, @max(sl.c0, sl.c1)));
const text = try paneText(p, pane);
- var total: usize = 0;
- var selected: usize = 0;
- var count_it = std.mem.splitAny(u8, text, "\n");
- var count_row: i32 = 0;
- while (count_it.next()) |line| : (count_row += 1) {
- if (count_row < r0 or count_row > r1) continue;
- const b0 = @min(panes.File.renderedLineByteCol(pane, count_row, line, c0), line.len);
- const b1 = modal.nextGrapheme(line, @min(panes.File.renderedLineByteCol(pane, count_row, line, c1), line.len));
- total += b1 - b0 + @intFromBool(selected > 0);
- selected += 1;
- }
- const out = try arena.alloc(u8, total);
- var at: usize = 0;
+ const shown = try tagline.shownTag(p, arena, pane);
+ var out: std.ArrayList(u8) = .empty;
var it = std.mem.splitAny(u8, text, "\n");
var v: i32 = 0;
var first = true;
while (it.next()) |line| : (v += 1) {
- if (v < r0 or v > r1) continue;
- if (!first) {
- out[at] = '\n';
- at += 1;
- }
+ const cols = sweepCols(sl, v) orelse continue;
+ // A row the tag's wrapping began carries on its text: no newline.
+ const soft = v > 0 and v < pane.tag_rows and std.mem.indexOfScalar(
+ usize,
+ shown.soft,
+ modal.lineStartOffset(shown.text, @as(usize, @intCast(v)) + pane.tag_top) -| 1,
+ ) != null;
+ if (!first and !soft) try out.append(arena, '\n');
first = false;
- const b0 = @min(panes.File.renderedLineByteCol(pane, v, line, c0), line.len);
- const b1 = modal.nextGrapheme(line, @min(panes.File.renderedLineByteCol(pane, v, line, c1), line.len));
- @memcpy(out[at..][0 .. b1 - b0], line[b0..b1]);
- at += b1 - b0;
+ // Past the first row a body row starts after its line numbers.
+ const gutter: i32 = if (pane.file != null and v >= pane.tag_rows) panes.File.gutterWidth(pane) else 0;
+ const lo: usize = @intCast(if (cols.first) cols.lo else gutter);
+ const b0 = @min(panes.File.renderedLineByteCol(pane, v, line, lo), line.len);
+ const b1 = if (cols.hi) |hi|
+ @max(b0, modal.nextGrapheme(line, @min(panes.File.renderedLineByteCol(pane, v, line, @intCast(hi)), line.len)))
+ else
+ line.len;
+ try out.appendSlice(arena, line[b0..b1]);
}
- return out;
+ return out.items;
+}
+
+/// A sweep's columns on `row`, in its Sel space: acme's stream, not a
+/// rectangle (libframe/frselect.c:115-140, frselectpaint): the first row
+/// from the press to its end, the rows between whole, the last from its
+/// start to the release. `hi` null runs to the end of the row.
+pub const SweepCols = struct { lo: i32, hi: ?i32, first: bool };
+
+pub fn sweepCols(sl: Pane.Sel, row: i32) ?SweepCols {
+ const start, const end = sweepEnds(sl);
+ if (row < start[0] or row > end[0]) return null;
+ return .{
+ .lo = if (row == start[0]) @max(0, start[1]) else 0,
+ .hi = if (row == end[0]) @max(0, end[1]) else null,
+ .first = row == start[0],
+ };
+}
+
+/// The sweep's two ends in reading order, each (row, col).
+pub fn sweepEnds(sl: Pane.Sel) struct { [2]i32, [2]i32 } {
+ const fwd = sl.r0 < sl.r1 or (sl.r0 == sl.r1 and sl.c0 <= sl.c1);
+ return if (fwd) .{ .{ sl.r0, sl.c0 }, .{ sl.r1, sl.c1 } } else .{ .{ sl.r1, sl.c1 }, .{ sl.r0, sl.c0 } };
}
pub fn spanHas(r: i32, c: i32, ar: i32, ac: i32, br: i32, bc: i32) bool {
@@ -738,6 +757,19 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
Key.left, Key.right, Key.up, Key.down => {
const lines = paneCursorLines(p, t) catch return;
const cur0 = t.toModalCursor();
+ // In a pane's tag, Up on its first line collapses it and Down
+ // on its last expands it, where they would move nothing; acme's
+ // arrows in a tag always do (plan9port text.c:776, Tagdown/Tagup).
+ if (t.what == .tag) if (t.pane()) |pane| {
+ if (key.cp == Key.up and cur0.row == 0) {
+ pane.tag_expanded = false;
+ return;
+ }
+ if (key.cp == Key.down and cur0.row + 1 >= lines.len) {
+ pane.tag_expanded = true;
+ return;
+ }
+ };
const nc = switch (key.cp) {
Key.left => modal.charLeft(lines, cur0),
Key.right => modal.charRight(lines, cur0),
@@ -2000,7 +2032,8 @@ pub fn pushUndo(p: *Pardes, t: *Text) void {
// one keystroke, one undo step — even when it edited at ten cursors.
if (p.multi_on and !p.multi_first) return;
switch (t.what) {
- .body => {},
+ // A B1 chord took its one undo point when it began (mouse.zig).
+ .body => if (p.chord_hold) return,
.tag, .column, .workspace => return t.remember(p.gpa, if (t.own) |own| .{ .text = own } else null),
// a prompt's answer is typed once and gone; it keeps no history
.input => return,
@@ -2215,3 +2248,120 @@ test "only the SPC clipboard commands cross to the system clipboard" {
p.update(.{ .key = .{ .cp = 'l' } });
try std.testing.expect(p.clip_pending == null);
}
+
+/// acme's double-click (plan9port acme text.c:1407, textdoubleclick): from
+/// the point `q` (before the character clicked), the text up to the match
+/// of a bracket just before it or just after it (text.c:1386, `{[(<«` and
+/// their closers), the whole line at a line's start or end, the quoted text
+/// next to `' " \``, else the word, acme's isalnum (util.c:328) run.
+pub fn doubleClickRange(text: []const u8, q_in: usize) [2]usize {
+ const sets = [_][2][]const u21{
+ .{ &.{ '{', '[', '(', '<', 0xab }, &.{ '}', ']', ')', '>', 0xbb } },
+ .{ &.{'\n'}, &.{'\n'} },
+ .{ &.{ '\'', '"', '`' }, &.{ '\'', '"', '`' } },
+ };
+ const q = @min(q_in, text.len);
+ for (sets) |set| {
+ const left, const right = set;
+ // the character to the left, looking right
+ const before = if (q == 0) .{ @as(u21, '\n'), @as(usize, 0) } else runeBefore(text, q);
+ if (std.mem.indexOfScalar(u21, left, before[0])) |k| {
+ var at = q;
+ if (clickMatch(text, before[0], right[k], true, &at)) {
+ const closer = if (before[0] == '\n') 0 else runeLen(right[k]);
+ return .{ q, at - closer };
+ }
+ return .{ q, q };
+ }
+ // the character to the right, looking left
+ const after = if (q == text.len) @as(u21, '\n') else runeAt(text, q);
+ if (std.mem.indexOfScalar(u21, right, after)) |k| {
+ var at = q;
+ if (clickMatch(text, after, left[k], false, &at)) {
+ const q1 = q + @as(usize, @intFromBool(q < text.len and after == '\n'));
+ var q0 = at;
+ if (after != '\n' or at != 0 or (text.len > 0 and text[0] == '\n')) q0 += runeLen(left[k]);
+ return .{ @min(q0, q1), q1 };
+ }
+ return .{ q, q };
+ }
+ }
+ var lo = q;
+ var hi = q;
+ while (hi < text.len and isAlnum(runeAt(text, hi))) hi += std.unicode.utf8ByteSequenceLength(text[hi]) catch 1;
+ while (lo > 0 and isAlnum(runeBefore(text, lo)[0])) lo = runeBefore(text, lo)[1];
+ return .{ lo, hi };
+}
+
+/// acme's textclickmatch (text.c:1457): the nesting match of `cl` by `cr`
+/// from `q`, leaving `q` just past it (forward) or on it (back).
+fn clickMatch(text: []const u8, cl: u21, cr: u21, forward: bool, q: *usize) bool {
+ var nest: usize = 1;
+ while (true) {
+ var c: u21 = undefined;
+ if (forward) {
+ if (q.* >= text.len) break;
+ c = runeAt(text, q.*);
+ q.* += std.unicode.utf8ByteSequenceLength(text[q.*]) catch 1;
+ } else {
+ if (q.* == 0) break;
+ const b = runeBefore(text, q.*);
+ q.* = b[1];
+ c = b[0];
+ }
+ if (c == cr) {
+ nest -= 1;
+ if (nest == 0) return true;
+ } else if (c == cl) nest += 1;
+ }
+ return cl == '\n' and nest == 1;
+}
+
+fn runeLen(c: u21) usize {
+ return std.unicode.utf8CodepointSequenceLength(c) catch 1;
+}
+
+fn runeAt(text: []const u8, at: usize) u21 {
+ const n = std.unicode.utf8ByteSequenceLength(text[at]) catch return text[at];
+ if (at + n > text.len) return text[at];
+ return std.unicode.utf8Decode(text[at..][0..n]) catch text[at];
+}
+
+/// The rune ending at `at`, and where it starts.
+fn runeBefore(text: []const u8, at: usize) struct { u21, usize } {
+ var s = at - 1;
+ while (s > 0 and text[s] & 0xC0 == 0x80 and at - s < 4) s -= 1;
+ return .{ runeAt(text, s), s };
+}
+
+fn isAlnum(c: u21) bool {
+ if (c <= ' ') return false;
+ if (c >= 0x7F and c <= 0xA0) return false;
+ if (c < 0x80 and std.mem.indexOfScalar(u8, "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", @intCast(c)) != null) return false;
+ return true;
+}
+
+test "double-click selects as acme's does" {
+ const t = "fn foo_bar(a, b) {\n x\n}\n";
+ const Case = struct { q: usize, want: []const u8 };
+ for ([_]Case{
+ .{ .q = 4, .want = "foo_bar" }, // in a word
+ .{ .q = 11, .want = "a, b" }, // just after (
+ .{ .q = 15, .want = "a, b" }, // just before )
+ .{ .q = 18, .want = "\n x\n" }, // just after {
+ .{ .q = 0, .want = "fn foo_bar(a, b) {\n" }, // a line's start
+ }) |c| {
+ const r = doubleClickRange(t, c.q);
+ try std.testing.expectEqualStrings(c.want, t[r[0]..r[1]]);
+ }
+ const quoted = "say \"hello there\" now";
+ const r = doubleClickRange(quoted, 5);
+ try std.testing.expectEqualStrings("hello there", quoted[r[0]..r[1]]);
+ const french = "«oui»";
+ const f = doubleClickRange(french, 2);
+ try std.testing.expectEqualStrings("oui", french[f[0]..f[1]]);
+ // a line's end: the whole line, its newline included
+ const lines = "one\ntwo\n";
+ const e = doubleClickRange(lines, 7);
+ try std.testing.expectEqualStrings("two\n", lines[e[0]..e[1]]);
+}
diff --git a/src/exec.zig b/src/exec.zig
index ee7c31e9..56e6a2a9 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -322,18 +322,15 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane {
nt.greet = !v9fs;
nt.v9fs_on_spawn = v9fs;
nt.body.mode = .tty;
- const parent = layout.splitParent(p, from);
- const f = layout.findPane(p, parent).?;
- layout.insert(p, f.col, f.idx + 1, free);
- layout.splitBelow(p, parent, nt);
- p.active = free;
- p.stillborn_joiner = nt.serial;
+ placeNew(p, from, from, free, .shell);
return nt;
}
-/// when a doc lands in `col`, a lone pristine shell there is clutter — drop
-/// it; absorbVWeight hands its space to the doc
+/// With `BootShell replace`, a doc landing in `col` closes a lone pristine
+/// shell there, the boot's placeholder, and absorbVWeight hands its space to
+/// the doc. With `keep`, the default, the shell stays like any other pane.
pub fn evictLonePristineTty(p: *Pardes, col: usize, keep_id: usize) void {
+ if (p.settings.boot_shell == .keep) return;
var n_tty: usize = 0;
var tty_id: usize = 0;
for (0..p.col_n[col]) |k| {
@@ -357,29 +354,6 @@ pub fn evictLonePristineTty(p: *Pardes, col: usize, keep_id: usize) void {
if (p.active == tty_id) p.active = keep_id;
}
-/// A pane that just joined a column whose only other pane is stillborn —
-/// an empty scratch nobody typed into, body or tag (what Newcol makes and
-/// what a closed column's last pane leaves behind) — takes the column
-/// whole: the placeholder was only holding the space for it. Run at the
-/// end of a step: the pane that asked for the joiner is often the
-/// placeholder itself, and its caller is still holding it.
-pub fn replaceStillborn(p: *Pardes) void {
- const serial = p.stillborn_joiner orelse return;
- p.stillborn_joiner = null;
- const new_id = p.paneBySerial(serial) orelse return;
- const f = layout.findPane(p, new_id) orelse return;
- if (p.col_n[f.col] != 2) return;
- const old_id = p.col_panes[f.col][1 - f.idx];
- const old = p.panes[old_id] orelse return;
- const file = old.file orelse return;
- const out = file.output orelse return;
- if (out.from != .cmd or out.from.cmd != .New) return;
- if (file.revision != 0 or file.content.len != 0) return;
- if (old.tag.own) |own| if (!std.mem.eql(u8, own, tagline.defaultTail(old))) return;
- layout.compute(p); // the joiner may have no rect yet; absorb snaps to rows
- p.removePane(old_id, new_id) catch |err| return p.reportError(old_id, "close", err);
-}
-
// ---- REPLs ----
//
// A terminal is bound as a language's REPL by `Repl python` (builtins.zig).
@@ -461,9 +435,21 @@ fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed {
src.question_len = @intCast(w.buffered().len);
src.prompt = .repl_choice;
p.active = from;
+ var rec: [96]u8 = undefined;
+ var rw = std.Io.Writer.fixed(&rec);
+ rw.print("{d} repl", .{src.serial}) catch {};
+ for (bound[0..n]) |i| rw.print(" {c}", .{p.panes[i].?.repl.?.letter}) catch {};
+ pardes.ctlfs.events.notePath(p, "ask", rw.buffered());
return .asked;
}
+fn pythonBlockOpen(text: []const u8) bool {
+ const trimmed = std.mem.trimEnd(u8, text, " \t\n");
+ const start = if (std.mem.lastIndexOfScalar(u8, trimmed, '\n')) |nl| nl + 1 else 0;
+ const last = trimmed[start..];
+ return last.len > 0 and (last[0] == ' ' or last[0] == '\t' or last[last.len - 1] == ':');
+}
+
/// Types `text` into the REPL in `to`, bracketed if its program asked for
/// that, then Enter; says so on `from`'s message row and in the log.
pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void {
@@ -491,6 +477,11 @@ pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void {
}
edit.typeToTty(p, to, dst, clean[0..n]);
p.emitWrite(to, "\r");
+ // A Python block whose last line is indented, or a bare header, is not
+ // over at one Enter, pasted into 3.13's REPL or typed into the old one:
+ // the blank line a second Enter gives ends it.
+ if ((if (dst.repl) |r| r.lang == python_lang else false) and pythonBlockOpen(clean[0..n]))
+ p.emitWrite(to, "\r");
// Line by line, a blank line inside a Python block ends the block.
if (!panes.Terminal.bracketedPaste(dst) and !dst.repl_warned and (if (dst.repl) |r| r.lang == python_lang else false) and
std.mem.indexOfScalar(u8, text, '\n') != null)
@@ -552,7 +543,7 @@ pub fn executeFrom(p: *Pardes, id: usize, txt: []const u8, from_body: bool) ?usi
/// Runs `line` in a command pane for `from`'s directory: `from` itself if
/// it is one whose command is done, else the directory's command pane that
-/// is done, else a new one below the last pane of the rightmost column.
+/// is done, else a new one, placed as placeNew places a command pane.
/// A pane reused keeps what it showed: the new command runs below a line
/// naming it, as acme appends to +Errors and never clears it (util.c:213).
fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
@@ -607,13 +598,7 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
return null;
};
echoCommand(p, pane, line);
- const rc = if (p.ncol > 0) p.ncol - 1 else 0;
- if (p.col_n[rc] > 0) {
- const parent = layout.splitParent(p, p.col_panes[rc][p.col_n[rc] - 1]);
- const f = layout.findPane(p, parent).?;
- layout.splitBelow(p, parent, pane);
- layout.insert(p, f.col, f.idx + 1, free);
- } else layout.insert(p, rc, p.col_n[rc], free);
+ placeNew(p, from, from, free, .command);
noteRun(p, pane, "run", line);
return free;
}
@@ -634,7 +619,7 @@ fn echoCommand(p: *Pardes, pane: *Pane, line: []const u8) void {
/// The whole line, one record as `msg` carries its text: its control
/// characters, a newline too, become spaces (events.pushLog). A new pane is
/// announced first, so `new` comes before the `run` in it.
-fn noteRun(p: *Pardes, pane: *Pane, what: []const u8, said: []const u8) void {
+pub fn noteRun(p: *Pardes, pane: *Pane, what: []const u8, said: []const u8) void {
pardes.ctlfs.events.announce(p);
var buf: [command_max + 16]u8 = undefined;
pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d} {s}", .{ pane.serial, std.mem.trim(u8, said, " \t\r\n") }) catch return);
@@ -691,7 +676,24 @@ pub fn applyStartupConfig(p: *Pardes) void {
p.announce = false;
defer p.announce = true;
var lines = std.mem.splitScalar(u8, p.opts.startup_config orelse return, '\n');
- while (lines.next()) |line| _ = executeBuiltinLine(p, p.active, line);
+ while (lines.next()) |line| {
+ if (executeBuiltinLine(p, p.active, line)) continue;
+ // A setting pardes no longer has is said, once, and changes nothing.
+ const word = commandText(line);
+ if (std.mem.eql(u8, word[0 .. std.mem.indexOfAny(u8, word, " \t") orelse word.len], "ColumnTags"))
+ p.setMessage(p.active, "ColumnTags: column tags are always shown now; the init line is ignored");
+ }
+}
+
+test "an init file's retired ColumnTags line is said and ignored" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ defer p.deinit();
+ p.opts.startup_config = "ColumnTags\nWrap off\n";
+ applyStartupConfig(p);
+ try std.testing.expect(!p.settings.wrap);
+ try std.testing.expect(p.columnBarHeight() > 0);
+ const pane = p.panes[p.active].?;
+ try std.testing.expectEqualStrings("ColumnTags: column tags are always shown now; the init line is ignored", pane.msg[0..pane.msg_len]);
}
pub fn runBuiltin(p: *Pardes, b: Builtin, id: usize, txt: []const u8, arg: ?[]const u8) void {
@@ -718,7 +720,6 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]
const previous_context_style = p.settings.tree_context_tag_style;
const previous_transition = p.settings.panel_transition;
const previous_tagline_percent = p.settings.font.tagline_percent;
- const previous_column_tags = p.settings.column_tags;
switch (setting.action) {
.theme => {
const want = std.mem.trim(u8, arg orelse return, " \t\r\n");
@@ -768,20 +769,98 @@ pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]
p.settings.panel_transition != previous_transition;
const tagline_metrics_changed = setting.action == .tagline_size and
p.settings.font.tagline_percent != previous_tagline_percent;
- if (p.settings.column_tags != previous_column_tags) {
- if (p.header_column != null) tagline.exitHeader(p);
- }
// Idle frames keep no frozen grid while transitions are off; the one
// just drawn is the baseline the first transition starts from.
if (transition_changed and previous_transition == .off)
p.presentation.capturePrevious(p.gpa, &p.surface);
- if (transition_changed or tagline_metrics_changed or p.settings.column_tags != previous_column_tags) {
+ if (transition_changed or tagline_metrics_changed) {
p.abandonPanelAnimations();
}
}
-pub fn placeDoc(p: *Pardes, from_id: usize, free: usize, nt: *Pane) void {
- p.stillborn_joiner = nt.serial;
+/// What a new pane is, and who asked: each rule places them differently.
+/// `scratch` is New's, `fs` 9P's pane/new, `errors` a +Errors pane.
+pub const NewPane = enum { scratch, fs, shell, doc, command, errors };
+
+/// Put new pane `id` in the layout: every new pane comes through here, by
+/// the `Placement` setting, and every one but a command pane takes the
+/// keyboard. `from` is the pane that asked, `origin` the text it asked
+/// from: null when a column tag, the workspace tag or 9P asked (acme's
+/// t->w == nil).
+pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPane) void {
+ const asked = if (p.exec_column != null) null else origin;
+ switch (p.settings.placement) {
+ .acme => switch (kind) {
+ // New: the bottom half of the last pane in its own column
+ // (look.c:921-923, et->col).
+ .scratch => layout.acmePlace(p, p.exec_column orelse columnOf(p, from), null, id),
+ // +Errors, a command's output: the last column (util.c:94-98).
+ .command, .errors => layout.acmePlace(p, p.ncol - 1, null, id),
+ // makenewwindow(nil) for 9P's new (acme.c:877), else (t).
+ .fs => layout.acmePlace(p, acmeColumn(p, from), null, id),
+ .shell, .doc => layout.acmePlace(p, acmeColumn(p, from), asked, id),
+ },
+ .pardes => pardesPlace(p, from, id, kind),
+ }
+ if (kind == .command) return;
+ tagline.exitHeader(p);
+ p.active = id;
+ if (layout.findPane(p, id)) |f| p.setActiveColumn(f.col);
+}
+
+/// acme's makenewwindow column (util.c:456-467): the column a command came
+/// from (its tag's), else the active column, else the keyboard's pane's,
+/// else the asking pane's.
+fn acmeColumn(p: *Pardes, from: usize) usize {
+ if (p.exec_column) |c| return c;
+ if (p.activeColumn()) |c| return c;
+ if (layout.findPane(p, p.active)) |f| return f.col;
+ return columnOf(p, from);
+}
+
+fn columnOf(p: *Pardes, id: usize) usize {
+ return if (layout.findPane(p, id)) |f| f.col else p.ncol - 1;
+}
+
+/// The placements pardes had before it took acme's: an empty column whose
+/// tag asked, or has the keyboard, is filled; a scratch goes under the
+/// asking pane, a shell under it or the nearest pane with room, a document
+/// beside the last one read or in a column of its own, and a command pane
+/// at the foot of the last column.
+fn pardesPlace(p: *Pardes, from: usize, id: usize, kind: NewPane) void {
+ const empty = if (p.exec_column orelse if (p.header_focus) p.header_column else null) |c|
+ (if (c < p.ncol and p.col_n[c] == 0) c else null)
+ else
+ null;
+ if (empty) |c| {
+ layout.insert(p, c, 0, id);
+ p.panes[id].?.vweight = 1;
+ return;
+ }
+ const nt = p.panes[id].?;
+ switch (kind) {
+ .scratch, .fs => if (layout.findPane(p, from)) |sf| {
+ layout.insert(p, sf.col, sf.idx + 1, id);
+ layout.splitBelow(p, from, nt);
+ },
+ .shell => belowParent(p, from, id),
+ .doc, .errors => pardesDoc(p, from, id),
+ .command => {
+ const rc = p.ncol - 1;
+ if (p.col_n[rc] > 0) belowParent(p, p.col_panes[rc][p.col_n[rc] - 1], id) else layout.insert(p, rc, 0, id);
+ },
+ }
+}
+
+fn belowParent(p: *Pardes, from: usize, id: usize) void {
+ const parent = layout.splitParent(p, from);
+ const f = layout.findPane(p, parent).?;
+ layout.insert(p, f.col, f.idx + 1, id);
+ layout.splitBelow(p, parent, p.panes[id].?);
+}
+
+fn pardesDoc(p: *Pardes, from_id: usize, free: usize) void {
+ const nt = p.panes[free].?;
const doc = if (nt.file) |f| panes.Output.fileTraits(f.output).doc else true; // an image is a doc
var src_id: ?usize = null;
if (p.panes[from_id]) |src| if (if (src.file) |f| panes.Output.fileTraits(f.output).doc else src.image != null or src.hasPdf()) {
@@ -808,22 +887,14 @@ pub fn placeDoc(p: *Pardes, from_id: usize, free: usize, nt: *Pane) void {
if (!doc) if (layout.findPane(p, from_id)) |sf| {
layout.insert(p, sf.col, sf.idx + 1, free);
layout.splitBelow(p, from_id, nt); // NOT splitParent: no bystander pays
- p.active = free;
return;
};
if (src_id == null and doc and p.ncol < MAX_COLS and
layout.columnFitsHalves(p, from_id, 100) and layout.splitColumn(p, from_id, free, true))
- {
- p.active = free;
return;
- }
// Without room for a first-document column, split the Look origin
// just like Tty. Never displace (or evict) a top-left bystander.
- const src = layout.splitParent(p, src_id orelse from_id);
- const sf = layout.findPane(p, src).?;
- layout.insert(p, sf.col, sf.idx + 1, free);
- layout.splitBelow(p, src, nt);
- p.active = free;
+ belowParent(p, src_id orelse from_id, free);
}
test "the acme chords act once per selection, not once on the primary" {
diff --git a/src/fs-help.txt b/src/fs-help.txt
index 57ef7b62..0121a831 100644
--- a/src/fs-help.txt
+++ b/src/fs-help.txt
@@ -8,15 +8,15 @@ exec write a line: a middle click, an editor command word or a shell line
log events: new/del/rename/save <serial> <name>, run/exit/send <serial> ..., dump/restore <path>, msg, err
screen rendered screen as JSON, frozen from open to close
listeners the session's dial addresses
-focus the serial of the pane with the keyboard; write a serial to give it the keyboard
-ctl settings, one a line; or Newcol, Dump, Exit (quits; asks once), Kill [word] (stops what pardes ran, or what starts with word)
+focus the serial of the pane with the keyboard (empty on a column tag); write a serial to give it the keyboard
+ctl settings, one a line; or Newcol (an empty column), Dump, Exit (quits, asks once), Kill [w] (stops what pardes ran, w: by first word)
commands every builtin: its word, arg if it takes one, and root or pane: which ctl takes it
-pane/new open it to make a pane (named +New); the read answers that pane's serial
+pane/new open it to make a pane (named +New) in the active column, filling it if empty; the read answers its serial
pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors (write-only:
appended to its directory's +Errors pane) event look exec, pty/ on terminals; rmdir closes
os/, src/ the host filesystem; the editor's own sources (-Dembed-sources=true builds only)
-Below, $m is the mount: $NINE_MOUNT/pardes/<pid> when 9ns --mntgen set $NINE_MOUNT; PARDES_MOUNT in a Tty9p shell.
+Below, $m is the mount: $NINE_MOUNT/pardes/<pid, or NAME of --detach=NAME>; PARDES_MOUNT in a Tty9p shell.
cat $m/index which panes exist
n=$(cat $m/pane/new) make a pane, take its serial
@@ -24,7 +24,7 @@ Below, $m is the mount: $NINE_MOUNT/pardes/<pid> when 9ns --mntgen set $NINE_MOU
printf 'text\n' >> $m/pane/$n/body append to a pane (> replaces it all)
echo notes.txt > $m/pane/$n/name; echo Save > $m/pane/$n/ctl rename, then save it
echo 'Msg hello' > $m/exec show text in the editor
- echo '/^fn/' > $m/pane/$n/addr; printf NEW > $m/pane/$n/data replace what addr matched
+ echo 'Edit ,x/foo/c/bar/' > $m/pane/$n/ctl rename every foo, one undo step (sam's commands)
cp $m/pane/$n/addr $m/pane/$n/dot; cat $m/pane/$n/sel select the range, read it
exec 3<>$m/log; echo follow >&3; cat <&3 what happened lately, then wait for more
cat $m/pane/$n/event hold it open to own that pane's clicks
@@ -35,11 +35,11 @@ Below, $m is the mount: $NINE_MOUNT/pardes/<pid> when 9ns --mntgen set $NINE_MOU
Pitfalls, one each:
Each open of pane/new makes another pane (ls, stat, find never do); rmdir closes even a dirty one.
- addr takes #n,#m or /regexp/ from the current address (^ $ per line); a failed one refuses data.
+ addr: #n,#m, 12:5 or /re/ from the current address (^ $ per line); > never resets it, 0 does; a miss refuses data.
data: > replaces the addr range, : > deletes it, a 2nd > inserts (addr moved); body: > empties all.
- tag reads the path, then its own text; writes append; truncating clears it, default words too.
+ tag reads the path, then its own text; > replaces that text (default words too), >> appends.
A terminal's body is a history snapshot frozen per open; pty/data is the live stream.
exec: a non-builtin line runs as a command pane (ctl's Shell -c; exit N shown, logged), a shell's typed in.
Through a mount a malformed write is EINVAL, a well-formed one that fails EIO: log's err says why.
- lock/unlock need a held ctl fd (exec 3>ctl), a held lock fails: retry. A column's last pane -> +New.
+ lock/unlock need a held ctl fd (exec 3>ctl), a held lock fails: retry. A column's last pane leaves it empty; the last pane of all quits.
Repl python on a terminal's ctl: a .py body's clicks go to it (Repl - unbinds, Repl says it); tags stay commands.
diff --git a/src/fs.zig b/src/fs.zig
index b2e2751d..0a4473d6 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -598,7 +598,8 @@ test "virtual body writes can read their input from the same pane" {
const path = try std.fmt.bufPrint(&path_buffer, "/virtual/pane/{d}/body", .{pane.serial});
try write(p, path, pane.file.?.content);
try std.testing.expectEqualStrings("same pane contents\n", pane.file.?.content);
- try std.testing.expectEqual(@as(usize, 2), pane.file.?.history.undo_len);
+ // The truncation and the write are one undo step.
+ try std.testing.expectEqual(@as(usize, 1), pane.file.?.history.undo_len);
const events = &p.panes[p.active].?.fs.events;
try std.testing.expect(std.mem.startsWith(u8, events.peek().?, "ED"));
events.pop();
@@ -1260,9 +1261,17 @@ pub const Namespace = struct {
/// the first error one reports (`failure`) fails the write, as acme's
/// ctl answers a command's error (editors/acme/xfid.c:700).
no_prompt: bool = false,
+ /// The root ctl is running a session builtin: its messages are the
+ /// session's, logged `msg -`, though shown on the active pane.
+ session_write: bool = false,
refused: bool = false,
failure: [256]u8 = undefined,
failure_len: u16 = 0,
+ /// What went wrong performing a write's effects after it was answered
+ /// by the core (a save the host could not do): the 9P write, which
+ /// waits for them, fails with it (src/9p_io.zig).
+ late_failure: [256]u8 = undefined,
+ late_failure_len: u16 = 0,
/// A refusal that quotes the message it refuses, as Plan 9's cmderror
/// does (kernel/misc/parse.c:82); answered at once (src/9p_io.zig).
ename: [320]u8 = undefined,
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 9748d4c0..5dfacee9 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -73,6 +73,24 @@ const p9_arrow_set = [32]u8{
0x7F, 0xFC, 0x7F, 0xFE, 0x7F, 0xFC, 0x73, 0xF8,
0x61, 0xF0, 0x60, 0xE0, 0x40, 0x40, 0x00, 0x00,
};
+/// acme's boxcursor (acme.c:956), while a grip carries a pane or column.
+const p9_box_clr = [32]u8{
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F,
+ 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+};
+const p9_box_set = [32]u8{
+ 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE,
+ 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E,
+ 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E, 0x70, 0x0E,
+ 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x00,
+};
+const p9_box_mask = blk: {
+ var m: [32]u8 = undefined;
+ for (&m, p9_box_set, p9_box_clr) |*b, s, cl| b.* = s | cl;
+ break :blk m;
+};
const p9_arrow_mask = blk: {
var m: [32]u8 = undefined;
for (&m, p9_arrow_set, p9_arrow_clr) |*b, s, cl| b.* = s | cl;
@@ -1849,6 +1867,7 @@ const Gui = struct {
pointer_present: bool = false,
arrow_cursor: ?*c.SDL_Cursor = null,
link_cursor: ?*c.SDL_Cursor = null,
+ box_cursor: ?*c.SDL_Cursor = null,
pointer_shape: pardes.Surface.PointerShape = .arrow,
pointer_mapped: bool = false,
pointer_cell: ?MouseCell = null,
@@ -2307,6 +2326,9 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
defer if (arrow_cursor) |cursor| c.SDL_DestroyCursor(cursor);
const link_cursor = c.SDL_CreateSystemCursor(c.SDL_SYSTEM_CURSOR_POINTER);
defer if (link_cursor) |cursor| c.SDL_DestroyCursor(cursor);
+ // Its hot spot the middle, as acme's {-7, -7} offset puts it.
+ const box_cursor = c.SDL_CreateCursor(&p9_box_set, &p9_box_mask, 16, 16, 7, 7);
+ defer if (box_cursor) |cursor| c.SDL_DestroyCursor(cursor);
if (arrow_cursor) |cur| {
_ = c.SDL_SetCursor(cur);
} else log.err("SDL_CreateCursor: {s}", .{c.SDL_GetError()});
@@ -2442,6 +2464,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
.window = window,
.arrow_cursor = arrow_cursor,
.link_cursor = link_cursor,
+ .box_cursor = box_cursor,
.device = device,
.swapchain_format = swapchain_format,
.pipeline = pipeline,
@@ -3758,9 +3781,10 @@ test "tagline pointer mapping retains physical grips and carries logical text co
try std.testing.expect(grip.tag_hit == null);
try std.testing.expectEqual(r.x + config.GUTTER - 1, grip.col);
- const body = gridCellAtDimensions(core, x, (@as(f32, @floatFromInt(tag_y + 1)) + 0.5) * body_h, body_w, body_h, tag_w);
+ const body_y = tag_y + core.panes[core.active].?.tag_rows; // a long tag wraps
+ const body = gridCellAtDimensions(core, x, (@as(f32, @floatFromInt(body_y)) + 0.5) * body_h, body_w, body_h, tag_w);
try std.testing.expectEqual(pixelCell(x, 10), body.col);
- try std.testing.expectEqual(tag_y + 1, body.row);
+ try std.testing.expectEqual(body_y, body.row);
try std.testing.expect(body.tag_hit == null);
const topbar = gridCellAtDimensions(core, 3.5 * tag_w, 0.5 * body_h, body_w, body_h, tag_w);
@@ -4075,15 +4099,13 @@ const Shell = struct {
host_io.commandEof(s.core, s.ptys, e.pane, s, closeWatched);
break :eof;
}
- // Unwatched, a command's exit is read here, as its end.
+ // Unwatched, a command's exit is read here, as its end,
+ // and a shell's, for a run waiting on it and the log.
const unwatched = if (s.core.panes[e.pane]) |pane| pane.command != null else false;
- var status: ?u8 = null;
- if (unwatched) {
- status = host_io.exitStatus(pt.pid, 100);
- if (status != null) pt.pid = 0; // reaped: no shell to retire
- }
+ const status = host_io.exitStatus(pt.pid, 100);
+ if (status != null) pt.pid = 0; // reaped: no shell to retire
s.closePty(e.pane);
- if (unwatched) s.core.update(.{ .exited = .{ .pane = e.pane, .status = status } });
+ if (unwatched or status != null) s.core.update(.{ .exited = .{ .pane = e.pane, .status = status } });
s.core.update(.{ .eof = .{ .pane = e.pane } });
}
s.saw_event = true;
@@ -4428,7 +4450,7 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void {
fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void {
const s = shellOf(ctx);
// A command whose exit is recorded is reaped: its group may be another's.
- if (s.ptys[pane]) |pt| if (pt.fd >= 0 and !pt.cmd.exited) host_io.signalTty(pt.pid, pt.fd, sig);
+ if (s.ptys[pane]) |pt| if (pt.fd >= 0 and !pt.cmd.exited) host_io.signalTty(pt.pid, pt.fd, sig, pt.cmd.watched);
}
fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {
@@ -4448,7 +4470,7 @@ fn killJob(ctx: ?*anyopaque, pane: u8) bool {
fn writeFile(ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void {
const s = shellOf(ctx);
filesystem.write(s.core, path, bytes) catch |err|
- return s.core.saveFailed(pane, "save", err);
+ return s.core.saveFailed(pane, path, err);
if (s.core.panes[pane]) |pane_state| if (pane_state.file) |f| {
if (std.mem.eql(u8, f.path, path))
if (s.watches[pane]) |*w| if (w.serial == pane_state.serial) switch (w.generation) {
@@ -5296,7 +5318,11 @@ fn renderFrame(
) !bool {
if (surface.pointer_shape != g.pointer_shape) {
g.pointer_shape = surface.pointer_shape;
- const cursor = if (g.pointer_shape == .link) g.link_cursor else g.arrow_cursor orelse c.SDL_GetDefaultCursor();
+ const cursor = switch (g.pointer_shape) {
+ .link => g.link_cursor,
+ .box => g.box_cursor orelse g.arrow_cursor,
+ else => g.arrow_cursor orelse c.SDL_GetDefaultCursor(),
+ };
if (cursor) |value| _ = c.SDL_SetCursor(value);
}
g.scene_target_failed = false;
@@ -5842,7 +5868,6 @@ test "the layer cover is the per-cell layer scan, over a core's layouts and rand
var notices = false;
for (0..32) |step| {
core.settings.tag_bottom = step & 1 != 0;
- core.settings.column_tags = step & 2 != 0;
if (step & 3 == 0) try std.testing.expect(core.executeBuiltinLine(2, "Collapse"));
core.leader_on = step & 4 != 0;
if (step & 8 != 0) {
@@ -8300,6 +8325,41 @@ test "the rule between stacked panes stays in the tag band's slack, at any tagli
}
}
+test "an empty column is ruled on both sides, as between two filled ones" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ _ = try core.newShell(1, "");
+ try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
+ // Newcol from the left pane: an empty column between the two filled ones.
+ core.newColumn(0);
+ pardes.test_api.sync(core);
+ try std.testing.expectEqual(@as(usize, 3), core.ncol);
+ try std.testing.expectEqual(@as(usize, 0), core.col_n[1]);
+ const s = try core.render(core.scratch.allocator());
+ var g: Gui = undefined;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.tagline_width = 6;
+ g.tagline_height = 12;
+ const layout = fixedCellLayout(&g);
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, layout, 800, 480, true);
+ const border = [3]f32{ @as(f32, @floatFromInt(s.chrome.border[0])) / 255, @as(f32, @floatFromInt(s.chrome.border[1])) / 255, @as(f32, @floatFromInt(s.chrome.border[2])) / 255 };
+ // Its left edge and its right edge (the next column's left): each a
+ // rule `rule_px` wide in the border colour, down to the window's foot.
+ for ([_]usize{ 1, 2 }) |column| {
+ const px: f32 = @floatFromInt(@as(u32, core.col_x[column]) * 10);
+ const rule = for (g.decor.items) |item| {
+ if (@abs(item.x0 - (px / 800 * 2 - 1)) < 1e-4 and @abs(item.x1 - ((px + @as(f32, @floatFromInt(s.chrome.rule_px))) / 800 * 2 - 1)) < 1e-4 and
+ std.mem.eql(f32, &.{ item.br, item.bg, item.bb }, &border)) break item;
+ } else return error.TestExpectedColumnRule;
+ try std.testing.expectEqual(@as(f32, -1), rule.y1);
+ }
+}
+
test "decor is read from the regions, each pane's in its own group" {
const gpa = std.testing.allocator;
const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
diff --git a/src/host_io.zig b/src/host_io.zig
index a019e15d..df78f73c 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -637,35 +637,55 @@ pub const Shell = struct {
\\
;
- const rc_path_capacity = 64;
+ const rc_path_capacity = 256;
- // Private files live until host teardown. Lengths keep this value movable.
+ /// The prompt files a shell sources at start. Under $XDG_RUNTIME_DIR
+ /// (private to the user) there is one file per content, named by its
+ /// hash and shared by every pardes, so none is left behind by a pardes
+ /// that was killed: 72K of them piled up in /tmp when each host wrote
+ /// its own. Without that directory each host writes private mkstemp
+ /// files in /tmp and removes them at teardown, as before.
pub const PromptFiles = struct {
bash_path: [rc_path_capacity:0]u8 = @splat(0),
- bash_len: u8 = 0,
+ bash_len: u16 = 0,
+ bash_owned: bool = false,
fish_path: [rc_path_capacity:0]u8 = @splat(0),
- fish_len: u8 = 0,
- fish_command: [rc_path_capacity + "source ".len:0]u8 = @splat(0),
- fish_command_len: u8 = 0,
+ fish_len: u16 = 0,
+ fish_owned: bool = false,
+ fish_command: [rc_path_capacity + "source ''".len:0]u8 = @splat(0),
+ fish_command_len: u16 = 0,
pub fn init() PromptFiles {
var rcs: PromptFiles = .{};
- rcs.bash_len = stage(&rcs.bash_path, "/tmp/pardes-osc133-bash-XXXXXX", bash_rc);
- rcs.fish_len = stage(&rcs.fish_path, "/tmp/pardes-osc133-fish-XXXXXX", fish_rc);
+ const bash = place(&rcs.bash_path, "bash", bash_rc);
+ rcs.bash_len = bash.len;
+ rcs.bash_owned = bash.owned;
+ const fish = place(&rcs.fish_path, "fish", fish_rc);
+ rcs.fish_len = fish.len;
+ rcs.fish_owned = fish.owned;
if (rcs.fishPath()) |path| {
- const command = std.fmt.bufPrintSentinel(&rcs.fish_command, "source {s}", .{path}, 0) catch {
- _ = libc.unlink(path.ptr);
+ // Quoted for fish; a path a quote would break is not used.
+ const command = if (std.mem.indexOfAny(u8, path, "'\\") != null)
+ error.NoSpaceLeft
+ else
+ std.fmt.bufPrintSentinel(&rcs.fish_command, "source '{s}'", .{path}, 0);
+ const ok = command catch {
+ if (rcs.fish_owned) _ = libc.unlink(path.ptr);
rcs.fish_len = 0;
return rcs;
};
- rcs.fish_command_len = @intCast(command.len);
+ rcs.fish_command_len = @intCast(ok.len);
}
return rcs;
}
pub fn deinit(rcs: *PromptFiles) void {
- if (rcs.bashPath()) |path| _ = libc.unlink(path.ptr);
- if (rcs.fishPath()) |path| _ = libc.unlink(path.ptr);
+ if (rcs.bashPath()) |path| if (rcs.bash_owned) {
+ _ = libc.unlink(path.ptr);
+ };
+ if (rcs.fishPath()) |path| if (rcs.fish_owned) {
+ _ = libc.unlink(path.ptr);
+ };
rcs.bash_len = 0;
rcs.fish_len = 0;
rcs.fish_command_len = 0;
@@ -687,8 +707,34 @@ pub const Shell = struct {
}
};
+ /// The shared file for `contents` under $XDG_RUNTIME_DIR, written
+ /// beside it and renamed into place when missing or different, so a
+ /// shell never reads half of one; failing that, a private /tmp file.
+ fn place(path_buf: *[rc_path_capacity:0]u8, name: []const u8, contents: []const u8) struct { len: u16, owned: bool } {
+ shared: {
+ const dir = std.mem.span(std.c.getenv("XDG_RUNTIME_DIR") orelse break :shared);
+ if (dir.len == 0 or dir[0] != '/') break :shared;
+ const final = std.fmt.bufPrintSentinel(path_buf, "{s}/pardes-osc133-{s}-{x:0>16}", .{ dir, name, std.hash.Wyhash.hash(0, contents) }, 0) catch break :shared;
+ var have: [2048]u8 = undefined;
+ if (readSmall(final, &have)) |got| if (std.mem.eql(u8, got, contents)) return .{ .len = @intCast(final.len), .owned = false };
+ var tmp_buf: [rc_path_capacity:0]u8 = @splat(0);
+ var template_buf: [rc_path_capacity]u8 = undefined;
+ const tmp_template = std.fmt.bufPrint(&template_buf, "{s}.XXXXXX", .{final}) catch break :shared;
+ const n = stage(&tmp_buf, tmp_template, contents);
+ if (n == 0) break :shared;
+ if (libc.rename(tmp_buf[0..n :0].ptr, final.ptr) != 0) {
+ _ = libc.unlink(tmp_buf[0..n :0].ptr);
+ break :shared;
+ }
+ return .{ .len = @intCast(final.len), .owned = false };
+ }
+ var tmp_template: [64]u8 = undefined;
+ const template = std.fmt.bufPrint(&tmp_template, "/tmp/pardes-osc133-{s}-XXXXXX", .{name}) catch return .{ .len = 0, .owned = false };
+ return .{ .len = stage(path_buf, template, contents), .owned = true };
+ }
+
// Publish a path only after its private 0600 file is fully written and closed.
- fn stage(path_buf: *[rc_path_capacity:0]u8, template: []const u8, contents: []const u8) u8 {
+ fn stage(path_buf: *[rc_path_capacity:0]u8, template: []const u8, contents: []const u8) u16 {
const path = std.fmt.bufPrintSentinel(path_buf, "{s}", .{template}, 0) catch return 0;
const fd = mkstemp(path.ptr);
if (fd < 0) return 0;
@@ -746,7 +792,7 @@ pub const Shell = struct {
pub const Spawn = struct {
path: [*:0]const u8,
- argv: [4:null]?[*:0]const u8,
+ argv: [6:null]?[*:0]const u8,
};
// Resolve in the parent. The path buffer and prompt files must survive through exec.
@@ -757,7 +803,24 @@ pub const Shell = struct {
.fish => if (prompt_rcs.fishCommand()) |command| .{ "-C", command.ptr } else .{ null, null },
.none => .{ null, null },
};
- return .{ .path = path, .argv = .{ path, marks[0], marks[1], null } };
+ return .{ .path = path, .argv = .{ path, marks[0], marks[1], null, null, null } };
+ }
+
+ /// A command pane's shell runs its one line with job control on, so a
+ /// job the line puts in the background has a process group of its own.
+ /// The shell leads the terminal's session, and when it exits the kernel
+ /// hangs up the terminal's foreground group: with job control off that
+ /// is the shell's own, the job's too, and the job died with the command
+ /// (nohup'd or not; only setsid got out, and not always in time). A
+ /// shell pardes does not know runs the line as it is.
+ pub fn commandArgv(path: [*:0]const u8, line: [*:0]const u8) [6:null]?[*:0]const u8 {
+ const bin = std.mem.span(path);
+ const base = if (std.mem.lastIndexOfScalar(u8, bin, '/')) |s| bin[s + 1 ..] else bin;
+ const posix_shells = [_][]const u8{ "bash", "sh", "dash", "zsh", "ksh", "mksh", "yash" };
+ for (posix_shells) |name| if (std.mem.eql(u8, base, name))
+ return .{ path, "-m", "-c", line, null, null };
+ if (family(bin) == .fish) return .{ path, "-C", "status job-control full", "-c", line, null };
+ return .{ path, "-c", line, null, null, null };
}
/// A shell by its path or its name in the usual directories, or null.
@@ -794,6 +857,20 @@ pub const Shell = struct {
return @ptrCast(buf);
}
+ test "a command pane's line runs with job control on in the shells that have it" {
+ const want = struct {
+ fn check(path: [*:0]const u8, words: []const []const u8) !void {
+ const argv = commandArgv(path, "sleep 9 &");
+ for (words, 0..) |w, i| try std.testing.expectEqualStrings(w, std.mem.span(argv[i].?));
+ try std.testing.expect(argv[words.len] == null);
+ }
+ };
+ try want.check("/usr/bin/bash", &.{ "/usr/bin/bash", "-m", "-c", "sleep 9 &" });
+ try want.check("/bin/sh", &.{ "/bin/sh", "-m", "-c", "sleep 9 &" });
+ try want.check("/usr/bin/fish", &.{ "/usr/bin/fish", "-C", "status job-control full", "-c", "sleep 9 &" });
+ try want.check("/usr/bin/nu", &.{ "/usr/bin/nu", "-c", "sleep 9 &" });
+ }
+
test "a path is taken at its word, a name is looked up, and both pick their own marks" {
if (builtin.os.tag == .windows) return;
var buf: [std.fs.max_path_bytes]u8 = undefined;
@@ -817,52 +894,55 @@ pub const Shell = struct {
try std.testing.expect(libc.access(gone.path, X_OK) == 0);
}
- test "prompt rc owners have private complete files and clean them up" {
+ test "prompt files are one per content under the runtime dir, or private and cleaned up without it" {
if (builtin.os.tag == .windows) return;
- var original = PromptFiles.init();
- var a = original;
- original = .{};
- original.deinit();
- defer a.deinit();
+ var saved_buf: [std.fs.max_path_bytes:0]u8 = @splat(0);
+ const saved: ?[:0]const u8 = if (std.c.getenv("XDG_RUNTIME_DIR")) |v| std.fmt.bufPrintSentinel(&saved_buf, "{s}", .{std.mem.span(v)}, 0) catch null else null;
+ defer _ = if (saved) |v| setenv("XDG_RUNTIME_DIR", v.ptr, 1) else unsetenv("XDG_RUNTIME_DIR");
+
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ var dir_buf: [std.fs.max_path_bytes:0]u8 = @splat(0);
+ const dir_len = try tmp.dir.realPath(std.testing.io, &dir_buf);
+ _ = setenv("XDG_RUNTIME_DIR", dir_buf[0..dir_len :0].ptr, 1);
+
+ // Shared: the same file for every owner, left in place, rewritten
+ // whole when what is there is not its content.
+ var a = PromptFiles.init();
var b = PromptFiles.init();
- defer b.deinit();
const a_bash = a.bashPath() orelse return error.TempCreateFailed;
- const b_bash = b.bashPath() orelse return error.TempCreateFailed;
- const a_fish = a.fishPath() orelse return error.TempCreateFailed;
- try std.testing.expect(!std.mem.eql(u8, a_bash, b_bash));
+ try std.testing.expectEqualStrings(a_bash, b.bashPath().?);
+ try std.testing.expect(std.mem.startsWith(u8, a_bash, dir_buf[0..dir_len]));
const fish_command = a.fishCommand() orelse return error.MissingFishCommand;
- try std.testing.expectEqualStrings("source ", fish_command[0.."source ".len]);
- try std.testing.expectEqualStrings(a_fish, fish_command["source ".len..]);
- for ([_][]const u8{ a_bash, b_bash, a_fish }) |path| {
- const stat = try std.Io.Dir.cwd().statFile(std.testing.io, path, .{});
- try std.testing.expectEqual(std.Io.File.Kind.file, stat.kind);
- try std.testing.expectEqual(0, stat.permissions.toMode() & 0o077);
- }
- var fish_buf: [fish_rc.len]u8 = undefined;
- try std.testing.expectEqualStrings(fish_rc, readSmall(a_fish, &fish_buf) orelse return error.ReadFailed);
-
- var buf: [bash_rc.len]u8 = undefined;
- const fd = libc.open(a_bash.ptr, .{ .ACCMODE = .RDONLY });
- if (fd < 0) return error.OpenFailed;
- defer _ = libc.close(fd);
- var len: usize = 0;
- while (len < buf.len) {
- const n = libc.read(fd, buf[len..].ptr, buf.len - len);
- if (n < 0) {
- if (libc.errno(n) == .INTR) continue;
- return error.ReadFailed;
- }
- if (n == 0) break;
- len += @intCast(n);
- }
- try std.testing.expectEqualStrings(bash_rc, buf[0..len]);
-
- var removed: [rc_path_capacity:0]u8 = @splat(0);
- @memcpy(removed[0..a_bash.len], a_bash);
- removed[a_bash.len] = 0;
+ var want: [rc_path_capacity + 16]u8 = undefined;
+ try std.testing.expectEqualStrings(try std.fmt.bufPrint(&want, "source '{s}'", .{a.fishPath().?}), fish_command);
+ var buf: [2048]u8 = undefined;
+ try std.testing.expectEqualStrings(bash_rc, readSmall(a_bash, &buf) orelse return error.ReadFailed);
+ try std.testing.expectEqualStrings(fish_rc, readSmall(a.fishPath().?, &buf) orelse return error.ReadFailed);
+ const stat = try std.Io.Dir.cwd().statFile(std.testing.io, a_bash, .{});
+ try std.testing.expectEqual(0, stat.permissions.toMode() & 0o077);
+ var kept: [rc_path_capacity:0]u8 = @splat(0);
+ @memcpy(kept[0..a_bash.len], a_bash);
a.deinit();
- try std.testing.expect(libc.access(&removed, 0) < 0);
- try std.testing.expectEqualStrings(bash_rc, readSmall(b_bash, &buf) orelse return error.ReadFailed);
+ b.deinit();
+ try std.testing.expect(libc.access(&kept, 0) == 0);
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = std.fs.path.basename(kept[0..a_bash.len]), .data = "half" });
+ var c = PromptFiles.init();
+ defer c.deinit();
+ try std.testing.expectEqualStrings(bash_rc, readSmall(c.bashPath().?, &buf) orelse return error.ReadFailed);
+
+ // No runtime dir: private files, one per owner, gone at teardown.
+ _ = unsetenv("XDG_RUNTIME_DIR");
+ var d = PromptFiles.init();
+ var e = PromptFiles.init();
+ defer e.deinit();
+ const d_bash = d.bashPath() orelse return error.TempCreateFailed;
+ try std.testing.expect(!std.mem.eql(u8, d_bash, e.bashPath().?));
+ try std.testing.expect(std.mem.startsWith(u8, d_bash, "/tmp/pardes-osc133-bash-"));
+ @memcpy(kept[0..d_bash.len], d_bash);
+ kept[d_bash.len] = 0;
+ d.deinit();
+ try std.testing.expect(libc.access(&kept, 0) < 0);
}
};
@@ -1021,7 +1101,7 @@ pub fn forkShell(
if (line.len > exec_command_max or std.mem.indexOfScalar(u8, line, 0) != null) return error.InvalidCommand;
@memcpy(command_buf[0..line.len], line);
command_buf[line.len] = 0;
- spawn.argv = .{ spawn.path, "-c", command_buf[0..line.len :0].ptr, null };
+ spawn.argv = Shell.commandArgv(spawn.path, command_buf[0..line.len :0].ptr);
}
var helper_buf: [4096]u8 = undefined;
const mounted = one_line == null and if (core) |c| if (c.panes[pane]) |pn| pn.v9fs_on_spawn else false else false;
@@ -1590,7 +1670,11 @@ fn reapShell(pid: libc.pid_t, grace_ms: u32) void {
while (libc.waitpid(pid, null, 0) < 0 and libc.errno(-1) == .INTR) {}
}
-pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySignal) void {
+/// The tty's foreground job is signalled. A command pane's shell (`command`)
+/// is its command too: its line runs with job control on, so the job
+/// running is in a group of its own, and the shell's group goes as well, or
+/// the rest of the line would run on after the job it was on.
+pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySignal, command: bool) void {
const sig = switch (which) {
.int => libc.SIG.INT,
.term => libc.SIG.TERM,
@@ -1599,11 +1683,9 @@ pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySigna
.kill => libc.SIG.KILL,
};
const fg = tcgetpgrp(master_fd);
- if (fg > 0) {
- _ = libc.kill(-fg, sig);
- return;
- }
- if (shell_pid > 0) _ = libc.kill(shell_pid, sig);
+ if (fg > 0) _ = libc.kill(-fg, sig);
+ if (command and shell_pid > 0 and fg != shell_pid) _ = libc.kill(-shell_pid, sig);
+ if (fg <= 0 and !command and shell_pid > 0) _ = libc.kill(shell_pid, sig);
}
/// Kill: SIGTERM to the job in the tty's foreground, never to the shell.
@@ -1994,6 +2076,43 @@ test "a command's exit is its process's, told by its watcher, whether or not its
}
}
+test "a command's background job outlives it and its pty, and Kill stops its whole line" {
+ if (comptime !tty_probe_platform) return error.SkipZigTest;
+ if (!haveFile("/bin/sh")) return error.SkipZigTest;
+ const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer core.deinit();
+ while (core.nextEffect()) |_| {}
+ const rcs: Shell.PromptFiles = .{};
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)];
+ const Case = struct { line: []const u8, kill: bool, made: bool };
+ for ([_]Case{
+ // The shell leads the pty's session; its exit hangs up only the
+ // foreground group, which the job, in a group of its own, is not.
+ .{ .line = "(sleep 0.4; touch left) & exit 0", .kill = false, .made = true },
+ // The job running is in its own group; the shell goes too.
+ .{ .line = "sleep 0.4; touch killed", .kill = true, .made = false },
+ }) |c| {
+ const id = core.freeSlot().?;
+ _ = try core.newCommand(id, dir, c.line);
+ while (core.nextEffect()) |_| {}
+ const child = try forkShell(core, id, &rcs, "/bin/sh", dir, 24, 80, null);
+ if (c.kill) {
+ sleepMs(150);
+ signalTty(child.pid, child.file.handle, .term, true);
+ }
+ _ = exitStatus(child.pid, 2000);
+ _ = libc.close(child.file.handle); // the pane closes: the pty hangs up
+ sleepMs(800);
+ const name = if (c.kill) "killed" else "left";
+ const made = if (tmp.dir.statFile(std.testing.io, name, .{})) |_| true else |_| false;
+ try std.testing.expectEqual(c.made, made);
+ try core.removePane(id, null);
+ }
+}
+
test "a retired shell that ignores the hangup is killed and reaped, not left a zombie" {
const pid = libc.fork();
if (pid == 0) {
diff --git a/src/layout.zig b/src/layout.zig
index f0e6fd16..c58f3bcf 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -19,7 +19,9 @@ pub const max_column_weight: u64 = std.math.maxInt(u64) / MAX_COLS;
pub const MovePlacement = struct {
preview_col: usize,
- above_id: usize,
+ /// The pane the moved one lands below; null when the column is empty
+ /// and the moved pane takes it whole.
+ above_id: ?usize,
row: u16,
above_y: u16,
above_h: u16,
@@ -60,6 +62,16 @@ pub fn focusDir(p: *Pardes, from: usize, dir: enum { left, right, up, down }) bo
return true;
}
+/// Column `c`'s serial, handed out the first time it is asked for.
+pub fn columnSerial(p: *Pardes, c: usize) u32 {
+ if (p.col_serial[c] == 0) {
+ p.col_serial[c] = p.next_column_serial;
+ p.next_column_serial +%= 1;
+ if (p.next_column_serial == 0) p.next_column_serial = 1;
+ }
+ return p.col_serial[c];
+}
+
pub fn columnBySerial(p: *const Pardes, serial: u32) ?usize {
if (serial == 0) return null;
for (0..p.ncol) |column| if (p.col_serial[column] == serial) return column;
@@ -178,6 +190,7 @@ pub fn movePlacement(p: *Pardes, id: usize, cur_x: u16, cur_y: u16) ?MovePlaceme
heights[sib] +|= p.rects[id].h;
}
var y: u16 = p.topBarHeight() + p.columnBarHeight();
+ if (p.col_n[tc] == 0) return .{ .preview_col = tc, .above_id = null, .row = y, .above_y = y, .above_h = 0 };
var last: ?MovePlacement = null;
for (0..p.col_n[tc]) |k| {
const pid = p.col_panes[tc][k];
@@ -213,18 +226,23 @@ pub fn movePane(p: *Pardes, id: usize, cur_x: u16, cur_y: u16) void {
}
}
removePane(p, id);
- if (source_multi and src.col < p.ncol and p.col_n[src.col] > 0) {
+ if (source_multi) {
const sib = if (src.idx > 0) p.col_panes[src.col][src.idx - 1] else p.col_panes[src.col][src.idx];
heights[sib] +|= p.rects[id].h;
}
- const af = findPane(p, placement.above_id) orelse return;
+ const above = placement.above_id orelse {
+ insert(p, placement.preview_col, 0, id);
+ if (source_multi) setColumnWeights(p, src.col, &heights);
+ return;
+ };
+ const af = findPane(p, above) orelse return;
const upper_h = @max(1, placement.row -| placement.above_y);
const lower_h = @max(1, placement.above_h -| upper_h);
- heights[placement.above_id] = upper_h;
+ heights[above] = upper_h;
heights[id] = lower_h;
insert(p, af.col, af.idx + 1, id);
setColumnWeights(p, af.col, &heights);
- if (source_multi and src.col < p.ncol and src.col != af.col) setColumnWeights(p, src.col, &heights);
+ if (source_multi and src.col != af.col) setColumnWeights(p, src.col, &heights);
}
pub fn setColumnWeights(p: *Pardes, col: usize, heights: *const [MAX_PANES]u16) void {
@@ -278,39 +296,23 @@ pub fn insert(p: *Pardes, c: usize, idx: usize, id: usize) void {
p.col_n[c] += 1;
}
+/// Take a pane out of its column. The column stays, empty if this was its
+/// last pane, as acme's colclose leaves it (cols.c:186): only Delcol and
+/// Joincol take a column away.
pub fn removePane(p: *Pardes, id: usize) void {
const f = findPane(p, id) orelse return;
const c = f.col;
var k = f.idx;
while (k + 1 < p.col_n[c]) : (k += 1) p.col_panes[c][k] = p.col_panes[c][k + 1];
p.col_n[c] -= 1;
- if (p.col_n[c] == 0) {
- tagline.exitHeader(p);
- if (p.ncol > 1) p.col_weight[if (c > 0) c - 1 else c + 1] +|= p.col_weight[c];
- p.col_tags[c].deinit(p.gpa);
- var j = c;
- while (j + 1 < p.ncol) : (j += 1) {
- p.col_panes[j] = p.col_panes[j + 1];
- p.col_n[j] = p.col_n[j + 1];
- p.col_weight[j] = p.col_weight[j + 1];
- p.col_tags[j] = p.col_tags[j + 1];
- p.col_serial[j] = p.col_serial[j + 1];
- }
- p.ncol -= 1;
- p.col_tags[p.ncol] = .{ .what = .column, .gpa = p.gpa };
- p.col_serial[p.ncol] = 0;
- }
}
-pub fn joinCol(p: *Pardes) void {
- const f = findPane(p, p.active) orelse return;
- const c = f.col;
- if (c + 1 >= p.ncol) return;
+/// Close column `c` once its panes are gone, its width to the one left of
+/// it (the first column's to its right), as acme's rowclose gives it.
+pub fn dropColumn(p: *Pardes, c: usize) void {
+ std.debug.assert(p.col_n[c] == 0);
tagline.exitHeader(p);
- const dst = c + 1;
- p.col_weight[dst] +|= p.col_weight[c];
- for (0..p.col_n[c]) |k| p.col_panes[dst][p.col_n[dst] + k] = p.col_panes[c][k];
- p.col_n[dst] += p.col_n[c];
+ if (p.ncol > 1) p.col_weight[if (c > 0) c - 1 else c + 1] +|= p.col_weight[c];
p.col_tags[c].deinit(p.gpa);
var j = c;
while (j + 1 < p.ncol) : (j += 1) {
@@ -325,42 +327,145 @@ pub fn joinCol(p: *Pardes) void {
p.col_serial[p.ncol] = 0;
}
+/// Fold column `c` into the one on its right, its panes below theirs.
+pub fn joinCol(p: *Pardes, c: usize) void {
+ if (c + 1 >= p.ncol) return;
+ const dst = c + 1;
+ for (0..p.col_n[c]) |k| p.col_panes[dst][p.col_n[dst] + k] = p.col_panes[c][k];
+ p.col_n[dst] += p.col_n[c];
+ p.col_n[c] = 0;
+ // The width goes right, where the panes went.
+ p.col_weight[dst] +|= p.col_weight[c];
+ p.col_weight[c] = 0;
+ dropColumn(p, c);
+}
+
+/// Put new pane `id` in column `c` as acme's makenewwindow does
+/// (util.c:449-495): an empty column it takes whole; asked from a tag or
+/// from 9P (`origin` null, acme's t->w == nil) it takes the bottom half of
+/// the column's last pane (coladd with y < 0, cols.c:62-65). Asked from a
+/// pane's text, it goes under the text of the pane with the most blank rows
+/// when there are many, else it halves the biggest pane, or `origin` when
+/// that is in the column and not much smaller.
+pub fn acmePlace(p: *Pardes, c: usize, origin: ?usize, id: usize) void {
+ compute(p);
+ const n = p.col_n[c];
+ if (n == 0) {
+ insert(p, c, 0, id);
+ p.panes[id].?.vweight = 1;
+ return;
+ }
+ var big: ?usize = null;
+ var empty: ?usize = null;
+ for (p.col_panes[c][0..n]) |k| {
+ const pane = p.panes[k] orelse continue;
+ if (pane.collapsed) continue;
+ // `>=`: at a tie the one nearer the bottom, as acme picks.
+ if (big == null or bodyRows(p, k) >= bodyRows(p, big.?)) big = k;
+ if (empty == null or blankRows(p, k) >= blankRows(p, empty.?)) empty = k;
+ }
+ const v0 = big orelse {
+ // Every pane folded: the new one goes last.
+ insert(p, c, n, id);
+ return;
+ };
+ const y: u16 = y: {
+ const o = origin orelse {
+ var last = v0;
+ for (p.col_panes[c][0..n]) |k| if (!p.panes[k].?.collapsed) {
+ last = k;
+ };
+ break :y bodyTop(p, last) + bodyRows(p, last) / 2;
+ };
+ const e = empty.?;
+ const el = blankRows(p, e);
+ if (el > 15 or (el > 3 and el > (bodyRows(p, v0) -| 1) / 2))
+ break :y bodyTop(p, e) + bodyRows(p, e) - el;
+ const mine = if (findPane(p, o)) |f| f.col == c and !p.panes[o].?.collapsed else false;
+ const v = if (mine and @as(u32, p.rects[o].h) * 3 > @as(u32, p.rects[v0].h) * 2) o else v0;
+ break :y p.rects[v].y + p.rects[v].h / 2;
+ };
+ splitAt(p, c, id, y);
+}
+
+/// acme's coladd at row `y` (cols.c:66-126): the new pane goes under the
+/// pane that `y` falls in, which keeps its rows above `y`.
+fn splitAt(p: *Pardes, c: usize, id: usize, y: u16) void {
+ const n = p.col_n[c];
+ var k: usize = 0;
+ while (k + 1 < n and y >= p.rects[p.col_panes[c][k]].y + p.rects[p.col_panes[c][k]].h) k += 1;
+ const v = p.col_panes[c][k];
+ const r = p.rects[v];
+ snapColWeights(p, c);
+ insert(p, c, k + 1, id);
+ const pane = p.panes[id].?;
+ // Each keeps a tag and a body row, as acme's minht does.
+ const lo = r.y +| BOX_H + 1;
+ const hi = (r.y + r.h) -| (BOX_H + 1);
+ const row = splitRowForExtent(r.y, r.h, if (lo <= hi) std.math.clamp(y, lo, hi) else r.y + r.h / 2) orelse {
+ pane.vweight = BOX_H + 1;
+ return;
+ };
+ // `y` fell in an unfolded pane: acmePlace picks from those.
+ p.panes[v].?.vweight = @floatFromInt(@max(1, row - r.y));
+ pane.vweight = @floatFromInt(@max(1, (r.y + r.h) - row));
+}
+
+fn bodyTop(p: *const Pardes, id: usize) u16 {
+ return p.rects[id].y + p.panes[id].?.tag_rows;
+}
+
+fn bodyRows(p: *const Pardes, id: usize) u16 {
+ return p.rects[id].h -| p.panes[id].?.tag_rows;
+}
+
+/// The rows at the bottom of a pane's body that show nothing: acme's
+/// maxlines - nlines. A picture or a PDF page fills its body.
+fn blankRows(p: *const Pardes, id: usize) u16 {
+ const pane = p.panes[id].?;
+ const rows = bodyRows(p, id);
+ const used: usize = if (pane.file) |*f|
+ // The empty line after a final newline shows nothing.
+ (panes.File.nlines(p.gpa, f) -| @intFromBool(std.mem.endsWith(u8, f.content, "\n"))) -| f.scroll
+ else if (pane.isTerminal())
+ @as(usize, panes.Terminal.gridCursor(pane).y) + 1
+ else
+ rows;
+ return rows -| @as(u16, @intCast(@min(used, rows)));
+}
+
pub fn canSplitColumn(p: *Pardes, source_id: usize) bool {
- if (p.ncol >= MAX_COLS or source_id >= MAX_PANES or p.panes[source_id] == null) return false;
+ if (source_id >= MAX_PANES or p.panes[source_id] == null) return false;
const source = findPane(p, source_id) orelse return false;
+ return canSplitCol(p, source.col);
+}
+
+fn canSplitCol(p: *Pardes, col: usize) bool {
+ if (p.ncol >= MAX_COLS or col >= p.ncol) return false;
// Refresh derived widths: public layout surgery may be chained between
// syncs, and a cached width must never admit a now-too-narrow split.
compute(p);
- if (p.col_w[source.col] < config.MINW * 2) return false;
+ if (p.col_w[col] < config.MINW * 2) return false;
- const weight = p.col_weight[source.col];
+ const weight = p.col_weight[col];
if (weight >= 2 and weight % 2 == 0) return true;
for (0..p.ncol) |column| if (p.col_weight[column] > std.math.maxInt(u64) / 2)
return false;
return weight > 0;
}
-pub fn splitColumn(p: *Pardes, source_id: usize, id: usize, before: bool) bool {
- if (id >= MAX_PANES or p.panes[id] == null) return false;
- if (!canSplitColumn(p, source_id)) return false;
- const source = findPane(p, source_id) orelse return false;
- const source_col = source.col;
- var old_weight = p.col_weight[source_col];
- const needs_rebase = old_weight < 2 or old_weight % 2 != 0;
- if (needs_rebase) old_weight *= 2;
- if (id == source_id) {
- if (p.col_n[source_col] <= 1) return false;
- _ = absorbVWeight(p, id, null);
- removePane(p, id);
- } else if (findPane(p, id) != null) return false;
-
- if (needs_rebase) {
+/// An empty column beside column `col`, with half its width: acme's Newcol
+/// (rowadd, rows.c:51). The new column's index, or null with no room.
+pub fn insertColumn(p: *Pardes, col: usize, before: bool) ?usize {
+ if (!canSplitCol(p, col)) return null;
+ var old_weight = p.col_weight[col];
+ if (old_weight < 2 or old_weight % 2 != 0) {
+ old_weight *= 2;
for (0..p.ncol) |column| p.col_weight[column] *= 2;
}
const source_weight = old_weight / 2;
- const new_weight = old_weight - source_weight;
- p.col_weight[source_col] = source_weight;
- const c = source_col + @intFromBool(!before);
+ p.col_weight[col] = source_weight;
+ const c = col + @intFromBool(!before);
tagline.exitHeader(p);
var j = p.ncol;
while (j > c) : (j -= 1) {
@@ -370,12 +475,27 @@ pub fn splitColumn(p: *Pardes, source_id: usize, id: usize, before: bool) bool {
p.col_tags[j] = p.col_tags[j - 1];
p.col_serial[j] = p.col_serial[j - 1];
}
- p.col_weight[c] = new_weight;
+ p.col_weight[c] = old_weight - source_weight;
p.col_tags[c] = .{ .what = .column, .gpa = p.gpa };
p.col_serial[c] = 0;
- p.col_panes[c][0] = id;
- p.col_n[c] = 1;
+ p.col_n[c] = 0;
p.ncol += 1;
+ return c;
+}
+
+/// Pane `id` alone in a new column beside `source_id`'s; `id` may be the
+/// source itself when its column keeps another pane.
+pub fn splitColumn(p: *Pardes, source_id: usize, id: usize, before: bool) bool {
+ if (id >= MAX_PANES or p.panes[id] == null) return false;
+ if (!canSplitColumn(p, source_id)) return false;
+ const source = findPane(p, source_id) orelse return false;
+ if (id == source_id) {
+ if (p.col_n[source.col] <= 1) return false;
+ _ = absorbVWeight(p, id, null);
+ removePane(p, id);
+ } else if (findPane(p, id) != null) return false;
+ const c = insertColumn(p, source.col, before).?;
+ insert(p, c, 0, id);
return true;
}
@@ -551,11 +671,7 @@ fn computeCollapsedColumn(p: *Pardes, c: usize, x: u16, width: u16, folded: usiz
pub fn compute(p: *Pardes) void {
p.rects = @splat(.{});
if (p.ncol == 0) return;
- for (0..p.ncol) |column| if (p.col_serial[column] == 0) {
- p.col_serial[column] = p.next_column_serial;
- p.next_column_serial +%= 1;
- if (p.next_column_serial == 0) p.next_column_serial = 1;
- };
+ for (0..p.ncol) |column| _ = columnSerial(p, column);
var wsum: u128 = 0;
for (0..p.ncol) |c| wsum += p.col_weight[c];
if (wsum == 0) wsum = 1;
@@ -688,7 +804,6 @@ test "collapse local transfers remain bounded through repeated tiny window resiz
}
for (0..180) |step| {
p.screen_h = @intCast((step * 17) % 81);
- p.settings.column_tags = step % 2 == 0;
p.settings.tag_bottom = step % 3 == 0;
compute(p);
toggleCollapse(p, step % 5);
@@ -743,7 +858,7 @@ test "collapse single and all panes leave unused column space" {
try std.testing.expectEqual(p.screen_h - 1, p.rects[1].y);
}
-test "collapse survives tiny resizes column tags and TagBottom" {
+test "collapse survives tiny resizes and TagBottom" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 32 });
defer p.deinit();
_ = try p.newShell(1, "");
@@ -753,8 +868,7 @@ test "collapse survives tiny resizes column tags and TagBottom" {
p.panes[1].?.collapsed = true;
for ([_]bool{ false, true }) |bottom| {
p.settings.tag_bottom = bottom;
- for ([_]bool{ false, true }) |column_tags| {
- p.settings.column_tags = column_tags;
+ {
for (0..50) |height| {
p.screen_h = @intCast(height);
compute(p);
diff --git a/src/look.zig b/src/look.zig
index 2e27a001..d90d27b8 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -10,6 +10,7 @@ const platform_has_fs = fs.platform_has_fs;
const layout = @import("layout.zig");
const animation = @import("animation.zig");
const modal = @import("modal.zig");
+const tagline = @import("tagline.zig");
const filesystem = @import("fs.zig");
const panes = @import("panes.zig");
const mouse = @import("mouse.zig");
@@ -466,6 +467,7 @@ pub fn expandedWord(line: []const u8, col: usize) ?ExpandedWord {
pub fn expandedSel(p: *Pardes, pane: *Pane, at: Pane.Sel) ?Pane.Sel {
var sl = at;
if (sl.c0 != sl.c1 or sl.r0 != sl.r1) return sl;
+ if (sl.r0 >= 0 and sl.r0 < pane.tag_rows) return tagWordSel(p, pane, sl);
const text = edit.paneText(p, pane) catch return null;
var it = std.mem.splitAny(u8, text, "\n");
var v: i32 = 0;
@@ -481,6 +483,34 @@ pub fn expandedSel(p: *Pardes, pane: *Pane, at: Pane.Sel) ?Pane.Sel {
return null;
}
+/// A tag word is expanded in the tag's own text, so a word the wrapping
+/// broke across rows is still one word, as acme's frame wraps a tag
+/// without breaking its text.
+fn tagWordSel(p: *Pardes, pane: *Pane, at: Pane.Sel) ?Pane.Sel {
+ const arena = p.scratch.allocator();
+ const shown = tagline.shownTag(p, arena, pane) catch return null;
+ const whole = tagline.tagText(p, arena, pane) catch return null;
+ const row: usize = @as(usize, @intCast(at.r0)) + pane.tag_top;
+ const shown_line = modal.lineSlice(shown.text, row);
+ const real = shown.toReal(modal.lineStartOffset(shown.text, row) + panes.File.rawAtDisplay(shown_line, @intCast(@max(0, at.c0))));
+ const start = if (std.mem.lastIndexOfScalar(u8, whole[0..@min(real, whole.len)], '\n')) |nl| nl + 1 else 0;
+ const end = std.mem.indexOfScalarPos(u8, whole, start, '\n') orelse whole.len;
+ const b = expandedWord(whole[start..end], real - start) orelse return null;
+ const lo = shownCell(shown.text, shown.fromReal(start + b.lo), pane.tag_top);
+ const hi = shownCell(shown.text, shown.fromReal(start + b.hi - 1), pane.tag_top);
+ return .{ .state = at.state, .r0 = lo.row, .c0 = lo.col, .r1 = hi.row, .c1 = hi.col };
+}
+
+fn shownCell(text: []const u8, off: usize, top: usize) struct { row: i32, col: i32 } {
+ const before = text[0..@min(off, text.len)];
+ const row = std.mem.count(u8, before, "\n");
+ const line_start = if (std.mem.lastIndexOfScalar(u8, before, '\n')) |nl| nl + 1 else 0;
+ return .{
+ .row = @as(i32, @intCast(row)) - @as(i32, @intCast(top)),
+ .col = @intCast(panes.File.rawDisplayCol(text[line_start..], off - line_start)),
+ };
+}
+
/// the word under the modal cursor as a pane-local selection (paneText
/// coords: row 0 is the tag; file panes carry the line-number prefix)
pub fn cursorWordSel(p: *Pardes, pane: *Pane) Pane.Sel {
@@ -518,7 +548,7 @@ pub fn submitSearch(p: *Pardes, id: usize) void {
.grep
else
.text;
- runSearch(p, id, armed[slash + 1 ..], kind, .top) catch |err| return p.reportError(id, "search", err);
+ _ = runSearch(p, id, armed[slash + 1 ..], kind, .top, true) catch |err| return p.reportError(id, "search", err);
if (kind == .text) lookFirstHit(p, id);
}
@@ -538,10 +568,13 @@ fn lookFirstHit(p: *Pardes, id: usize) void {
exec.runBuiltin(p, config.look_cmd, rid, "", txt);
}
-pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start: SearchStart) !void {
- const pane = p.panes[id] orelse return;
+/// Fills the results buffer, and answers how many rows a text or PDF
+/// search found (0 for Find and Grep, which do not count). With
+/// `open_empty` false a text search that found nothing opens no buffer.
+pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start: SearchStart, open_empty: bool) !usize {
+ const pane = p.panes[id] orelse return 0;
const pat = std.mem.trim(u8, pat_raw, " \t\r\n");
- if (pat.len == 0) return;
+ if (pat.len == 0) return 0;
const arena = p.scratch.allocator();
// where the pane lives: a file's directory, a shell's cwd — the walk
// root, and the directory the results buffer is named in.
@@ -617,6 +650,7 @@ pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start
nrows += 1;
}
}
+ if (!open_empty and kind == .text and nrows == 0) return 0;
const content = try p.gpa.dupe(u8, out[0..out_len]);
// the buffer records WHICH search filled it, pattern and all: Find and
// Grep are builtins (words you can execute), the bare `/` is a key
@@ -626,6 +660,7 @@ pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start
.grep => .{ .cmd = .Grep },
};
try panes.Output.fillResults(p, id, dir, from, pat, content, anchor);
+ return nrows;
}
pub fn searchStep(p: *Pardes, id: usize, delta: i32) bool {
@@ -963,7 +998,8 @@ pub fn focusPaneLine(p: *Pardes, id: usize, at: Spot, landing: enum { center, ke
if (pane.file) |*f| {
if (at.line > panes.File.nlines(p.gpa, f)) return;
if (landing == .center) {
- const next = (at.line - 1) -| pane.rows / 2; // center, clamp at top
+ // center, clamp at top; a text that fits is shown whole
+ const next = if (pane.textFits()) 0 else (at.line - 1) -| pane.rows / 2;
if (next != f.scroll) {
f.scroll = next;
f.syntax_dirty = true;
@@ -1137,6 +1173,86 @@ pub fn followPdfLink(p: *Pardes, id: usize, probe: *const panes.Pdf.WordProbe) v
}
}
+/// A look that found nothing: said on the message row, logged as an `err`
+/// record, and marked so that /look reads back nothing (ninep/ctl.zig run).
+fn missed(p: *Pardes, id: usize, comptime fmt: []const u8, args: anytype) void {
+ p.look_missed = true;
+ var said: [160]u8 = undefined;
+ const why = std.fmt.bufPrint(&said, fmt, args) catch "no match";
+ var msg: [176]u8 = undefined;
+ p.setMessage(id, std.fmt.bufPrint(&msg, "Look: {s}", .{why}) catch "Look: no match");
+ const serial = if (p.panes[id]) |pane| pane.serial else 0;
+ var rec: [200]u8 = undefined;
+ pardes.ctlfs.events.notePath(p, "err", std.fmt.bufPrint(&rec, "{d} look: {s}", .{ serial, why }) catch return);
+}
+
+/// After a look at `path:N` focused its pane: a line past the end focuses
+/// nothing, leaves no pane it opened, and is a miss; a bare line number
+/// selects the line, its newline included, as acme's does.
+fn settleLine(p: *Pardes, id: usize, path: []const u8, at: Spot, before: usize, opened: ?usize) void {
+ const shown = p.panes[p.active] orelse return;
+ const f = if (shown.file) |*file| file else return;
+ if (at.line > panes.File.nlines(p.gpa, f)) {
+ if (opened) |free| p.removePane(free, null) catch {};
+ if (p.panes[before] != null) p.active = before;
+ return missed(p, id, "{s} has no line {d}", .{ path, at.line });
+ }
+ if (at.line > 0 and at.col == 0 and at.end_line == 0) {
+ const start = modal.lineStartOffset(f.content, at.line - 1);
+ const next = std.mem.indexOfScalarPos(u8, f.content, start, '\n');
+ const end = if (next) |i| i + 1 else f.content.len;
+ @import("ninep/pane.zig").setDot(shown, .{ .q0 = @intCast(start), .q1 = @intCast(end) });
+ }
+}
+
+/// `file:addr` or `:addr`, acme's look.c:450-543: a colon followed by an
+/// address character (addr.c:28, isaddrc) that is not a line spot this
+/// parser already reads (`file:12`, `file:12:5`). Not a URL.
+fn addressed(tok: []const u8) ?struct { path: []const u8, expr: []const u8 } {
+ if (std.mem.indexOf(u8, tok, "://") != null) return null;
+ const colon = std.mem.indexOfScalar(u8, tok, ':') orelse return null;
+ const expr = tok[colon + 1 ..];
+ // A written look is a whole line: the path has no blanks, but a
+ // pattern in the address may (`calc.py:/return a/`).
+ if (std.mem.indexOfAny(u8, tok[0..colon], " \t") != null) return null;
+ if (std.mem.indexOfAny(u8, expr, " \t") != null and std.mem.indexOfAny(u8, expr, "/?") == null) return null;
+ if (expr.len == 0 or std.mem.indexOfScalar(u8, "0123456789+-/$.#,;", expr[0]) == null) return null;
+ if (std.ascii.isDigit(expr[0])) {
+ const pl = parsePathLine(tok);
+ if (pl.at.line != 0 and pl.end == tok.len) return null;
+ }
+ return .{ .path = tok[0..colon], .expr = expr };
+}
+
+/// Opens (or finds) `path` as Look would, `id`'s own pane when it is empty,
+/// and selects what `expr` addresses there from its dot. An address that
+/// does not evaluate moves nothing and says why (acme: no jump).
+fn lookAddress(p: *Pardes, id: usize, path: []const u8, expr: []const u8) void {
+ const ninep_pane = @import("ninep/pane.zig");
+ const Addr = @import("ninep/addr.zig").Addr;
+ var target = id;
+ if (path.len > 0) {
+ const before = p.active;
+ lookAt(p, id, path);
+ target = p.active;
+ // The path opened nothing (a search, or no such file): Look said so.
+ if (target == before and target != id) return;
+ }
+ const pane = p.panes[target] orelse return;
+ const f = pane.file orelse return;
+ if (path.len > 0 and (f.output != null or !std.mem.endsWith(u8, f.path, std.fs.path.basename(path)))) return;
+ var a: Addr = .{ .text = f.content, .lim = null, .expr = expr };
+ const r = a.address(ninep_pane.dotOf(pane)) orelse {
+ return missed(p, id, "{s}", .{a.err});
+ };
+ if (a.i < expr.len or r.q0 > r.q1) {
+ return missed(p, id, "{s}", .{if (r.q0 > r.q1) "addresses out of order" else "bad address syntax"});
+ }
+ clearNavigationSelection(p, pane);
+ ninep_pane.setDot(pane, r);
+ p.active = target;
+}
+
pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
const pane = p.panes[id] orelse return;
p.noteHaptic(.look);
@@ -1149,37 +1265,45 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
// The operand may borrow selected terminal/PDF text released below.
const txt = p.scratch.allocator().dupe(u8, operand) catch return;
const trimmed = std.mem.trim(u8, txt, " \t\r\n");
- clearNavigationSelection(p, pane);
+ // acme's `file:addr` and `:addr` (look.c:450-543): an address after
+ // the colon, evaluated in that file from its dot (openfile, look.c:675).
+ if (addressed(trimmed)) |ad| return lookAddress(p, id, ad.path, ad.expr);
const pl = parsePathLine(trimmed);
+ if (comptime pdf_enabled) if (pane.hasPdf()) clearNavigationSelection(p, pane);
if (comptime pdf_enabled) if (panes.Pdf.lookSection(p, id, pl.path, pl.at)) return;
var realbuf: [4096]u8 = undefined;
const found = resolveLookTarget(p, id, txt, &realbuf);
+ // A line past the end of an open pane is a miss (the target's line is
+ // already clamped, so the one written is asked).
+ if (found == .pane and found.pane.id < MAX_PANES) if (p.panes[found.pane.id]) |shown| if (shown.file) |*f| if (pl.at.line > panes.File.nlines(p.gpa, f))
+ return missed(p, id, "{s} has no line {d}", .{ trimmed, pl.at.line });
+ // A miss changes nothing, the selection included; anything else starts
+ // from a clean one.
+ if (found != .none) clearNavigationSelection(p, pane);
switch (found) {
.none => {
if (pane.image != null or pane.body.mode == .tty) return;
- runSearch(p, id, trimmed, .text, .cursor) catch |err| {
+ const rows = runSearch(p, id, trimmed, .text, .cursor, false) catch |err| {
p.reportError(id, "search", err);
return;
};
+ if (rows == 0) return missed(p, id, "no match for \"{s}\"", .{@import("Messages.zig").clip(trimmed, 48)});
+ clearNavigationSelection(p, pane);
const at = pane.search_row;
_ = searchStep(p, id, 1);
if (at != null and pane.search_row == at) {
pane.search_row = null;
_ = searchStep(p, id, 1);
}
- // A path that is not there searches for its text, and a search
- // that found nothing says so rather than leave an empty buffer.
- const results = if (pane.search_pane) |rid| p.panes[rid] else null;
- if (results) |r| if (r.file) |f| if (f.content.len == 0) {
- var said: [96]u8 = undefined;
- p.setMessage(id, std.fmt.bufPrint(&said, "Look: no match for \"{s}\"", .{@import("Messages.zig").clip(trimmed, 48)}) catch "Look: no match");
- };
+
},
.pane => |t| {
if (t.id >= MAX_PANES) return;
const target = p.panes[t.id] orelse return;
+ const before = p.active;
clearNavigationSelection(p, target);
focusPaneLine(p, t.id, t.at, .center);
+ settleLine(p, id, trimmed, t.at, before, null);
},
.url => |u| if (u.len <= 256) p.emit(.{ .open_link = .from(u) }),
.dir => |dir| {
@@ -1194,43 +1318,48 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
const nt = p.newShell(free, dir) catch |err| return p.reportError(id, "look", err);
nt.greet = true;
- const src = layout.splitParent(p, id);
- const f = layout.findPane(p, src).?;
- layout.insert(p, f.col, f.idx + 1, free);
- layout.splitBelow(p, src, nt);
- p.active = free;
- p.stillborn_joiner = nt.serial;
+ exec.placeNew(p, id, id, free, .shell);
},
.file => |target| {
if (comptime pdf_enabled) if (target.kind == .pdf) {
if (focusPaneByPath(p, target.path, target.at)) return;
const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
- const nt = panes.Pdf.openPane(p, free, target.path, target.at.line) catch |err|
+ _ = panes.Pdf.openPane(p, free, target.path, target.at.line) catch |err|
return p.reportError(id, "look", err);
- exec.placeDoc(p, id, free, nt);
+ exec.placeNew(p, id, id, free, .doc);
return;
};
+ // An open pane on this path too short for the line: a miss that
+ // touches nothing.
+ if (openPaneTarget(p, target.path, target.at)) |open| if (p.panes[open.pane.id].?.file) |*f|
+ if (target.at.line > panes.File.nlines(p.gpa, f))
+ return missed(p, id, "{s} has no line {d}", .{ target.path, target.at.line });
// focus an existing pane on this path (rescrolled), else open
- if (focusPaneByPath(p, target.path, target.at)) return;
- const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
- const nt = panes.File.open(p, free, target.path, target.at.line) catch |err|
- return p.reportError(id, "look", err);
- if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1);
- exec.placeDoc(p, id, free, nt);
- layout.compute(p);
- nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2;
- if (target.at.end_line != 0) {
- const centered = nt.file.?.scroll;
- selectSpan(p, nt, target.at);
- nt.file.?.scroll = centered;
+ const before = p.active;
+ var opened: ?usize = null;
+ if (!focusPaneByPath(p, target.path, target.at)) {
+ const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
+ const nt = panes.File.open(p, free, target.path, target.at.line) catch |err|
+ return p.reportError(id, "look", err);
+ if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1);
+ exec.placeNew(p, id, id, free, .doc);
+ layout.compute(p);
+ nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2;
+ if (target.at.end_line != 0) {
+ const centered = nt.file.?.scroll;
+ selectSpan(p, nt, target.at);
+ nt.file.?.scroll = centered;
+ }
+ opened = free;
}
+ settleLine(p, id, target.path, target.at, before, opened);
},
.image => |target| {
if (focusPaneByPath(p, target.path, .{})) return;
const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots);
- const nt = panes.Image.create(p, free, target.path, &.{}) catch |err|
+ _ = panes.Image.create(p, free, target.path, &.{}) catch |err|
return p.reportError(id, "look", err);
- exec.placeDoc(p, id, free, nt);
+ exec.placeNew(p, id, id, free, .doc);
},
}
}
@@ -1244,6 +1373,8 @@ test "Look hover waits without mutating the pane and input cancels it" {
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = try p.setTestFile("alpha beta gamma\n");
+ pane.tag_expanded = false; // a one-row tag keeps this small screen's rows
+ p.sync();
while (p.nextEffect()) |_| {}
const other_id = p.freeSlot() orelse return error.NoSparePaneForHoverTest;
_ = try p.newShell(other_id, "");
diff --git a/src/macos.zig b/src/macos.zig
index acc344d2..678b155d 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -1313,15 +1313,15 @@ fn drainInbox(st: *State) bool {
host_io.commandEof(st.core, &st.ptys, e.pane, st, closeWatched);
continue;
}
- // Unwatched, a command's exit is read here, as its end.
- if (st.core.panes[e.pane]) |pane| if (pane.command != null) {
- const status = host_io.exitStatus(pt.pid, 100);
- if (status != null) pt.pid = 0; // reaped: no shell to retire
- reap(st, e.pane);
- st.core.update(.{ .exited = .{ .pane = e.pane, .status = status } });
- st.core.update(.{ .eof = .{ .pane = e.pane } });
- continue;
- };
+ // Unwatched, a command's exit is read here, as its end,
+ // and a shell's, for a run waiting on it and the log.
+ const command = if (st.core.panes[e.pane]) |pane| pane.command != null else false;
+ const status = host_io.exitStatus(pt.pid, 100);
+ if (status != null) pt.pid = 0; // reaped: no shell to retire
+ reap(st, e.pane);
+ if (command or status != null) st.core.update(.{ .exited = .{ .pane = e.pane, .status = status } });
+ st.core.update(.{ .eof = .{ .pane = e.pane } });
+ continue;
}
reap(st, e.pane);
st.core.update(.{ .eof = .{ .pane = e.pane } });
@@ -2469,7 +2469,7 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void {
fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void {
const st = hostState(ctx);
// A command whose exit is recorded is reaped: its group may be another's.
- if (st.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig);
+ if (st.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig, pt.cmd.watched);
}
fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {
@@ -2487,7 +2487,7 @@ fn killJob(ctx: ?*anyopaque, pane: u8) bool {
fn writeFile(ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void {
const st = hostState(ctx);
filesystem.write(st.core, path, bytes) catch |err|
- return st.core.saveFailed(pane, "save", err);
+ return st.core.saveFailed(pane, path, err);
st.file_watches.restampText(pane, path, std.hash.Wyhash.hash(0, bytes));
var mbuf: [256]u8 = undefined;
st.core.setMessage(pane, message.stamp(&mbuf, "saved", path));
diff --git a/src/mouse.zig b/src/mouse.zig
index 22eb3270..36786612 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -48,6 +48,14 @@ pub const Drag = union(enum) {
button: Mouse.Button,
chorded: bool = false,
ctrl: bool = false,
+ /// A B1 body sweep held past the pane's top (-1) or bottom (1): the
+ /// body scrolls a line a tick, as acme's frselect scrolls
+ /// (libframe frselect.c, the `scroll` callback while the button is
+ /// held outside the frame).
+ edge: i2 = 0,
+ /// Where the sweep began in the text, once it has scrolled: its
+ /// press row may be off the screen.
+ anchor: ?modal.Cursor = null,
/// This gesture began over a usable native PDF raster. The payload is
/// zero-bit when PDF support is absent.
pdf: panes.Pdf.PointerDrag = .{},
@@ -337,6 +345,9 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
wordAtCol(bar, at);
const word = p.scratch.allocator().dupe(u8, selected) catch return;
tagline.exitHeader(p);
+ // A column tag's command acts on that column, empty or not.
+ p.exec_column = column;
+ defer p.exec_column = null;
if (word.len > 0) {
if (m.button == config.exec_button) {
const named = std.meta.stringToEnum(Builtin, word);
@@ -450,6 +461,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
const on_tag = mrow >= tag_y and mrow < tag_y + pane.tag_rows;
if (on_tag and mcol < r.x + TAG_TEXT_INSET) return;
p.active = id;
+ p.noteTyped(); // acme's button 1 sets activecol (acme.c:659)
pane.closePrompt();
const pointed = edit.pointerTextSelection(p, id, mcol, mrow, m.body_hit, p.pointer_tag_hit) orelse return;
pane.clearPointerSelection(sel_slot);
@@ -621,6 +633,17 @@ pub fn dragUpdate(p: *Pardes, mcol: u16, mrow: u16, body_hit: ?Mouse.BodyHit) vo
// from the pointer.
const body_y = p.bodyTop(pane, r);
const body_h = r.h -| pane.tag_rows;
+ s.edge = if (s.button != config.select_button or pane.sel[b].r0 < tag_rows or pane.file == null or pane.hasPdf() or body_h == 0)
+ 0
+ else if (mrow < body_y)
+ -1
+ else if (mrow >= body_y + body_h or
+ // The pointer cannot leave the screen: at a bottom pane,
+ // its last row is the edge.
+ (body_y + body_h >= p.screen_h and mrow + 1 >= body_y + body_h))
+ 1
+ else
+ 0;
pane.sel[b].r1 = if (pane.sel[b].r0 < tag_rows)
tag_line
else if (body_h == 0)
@@ -666,11 +689,12 @@ fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand:
const pane = p.panes[id] orelse return .{};
if (on_tag) {
const tag = tagline.tagText(p, p.scratch.allocator(), pane) catch return .{};
+ const shown = tagline.shownTag(p, p.scratch.allocator(), pane) catch return .{};
const sel = operand.expanded orelse operand.preview orelse return .{};
const lead = if (sel.r0 < sel.r1 or (sel.r0 == sel.r1 and sel.c0 <= sel.c1)) sel else Pane.Sel{ .r0 = sel.r1, .c0 = sel.c1 };
const row: usize = @as(usize, @intCast(@max(0, lead.r0))) + pane.tag_top;
- const start = modal.lineStartOffset(tag, row);
- const at = start + panes.File.rawAtDisplay(modal.lineSlice(tag, row), @intCast(@max(0, lead.c0)));
+ const start = modal.lineStartOffset(shown.text, row);
+ const at = shown.toReal(start + panes.File.rawAtDisplay(modal.lineSlice(shown.text, row), @intCast(@max(0, lead.c0))));
const q0: u32 = @intCast(@min(at, tag.len));
return .{ .q0 = q0, .q1 = @intCast(@min(q0 + text.len, tag.len)) };
}
@@ -691,12 +715,13 @@ fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand:
const start: ?modal.Cursor = if (operand.file_word) |w|
.{ .row = @intCast(@max(0, w.row)), .col = @intCast(@max(0, w.lo)) }
else if (operand.expanded orelse operand.preview) |sel| lead: {
- // A selection's leading corner in reading order, converted the way
+ // A selection's start in reading order, converted the way
// `pointerOperand` converts the click itself.
- const top = @min(sel.r0, sel.r1) - @as(i32, pane.tag_rows);
+ const lead = edit.sweepEnds(sel)[0];
+ const top = lead[0] - @as(i32, pane.tag_rows);
if (top < 0) break :lead null;
const w = pane.wrapAt(top);
- const col = edit.paneByteAtDisplay(p, &pane.body, w.line, w.at, @min(sel.c0, sel.c1) - panes.File.gutterWidth(pane));
+ const col = edit.paneByteAtDisplay(p, &pane.body, w.line, w.at, lead[1] - panes.File.gutterWidth(pane));
break :lead .{
.row = @intCast(@max(0, w.line)),
.col = @intCast(@max(0, col)),
@@ -730,10 +755,11 @@ fn mirrorTtySelection(p: *Pardes, pane: *Pane) void {
const rows = pane.pointerSelection(gesture) orelse return;
const text = pane.rawPointerText(gesture) orelse return;
if (rows.len == 0) return;
- const endpoint = rows[if (gesture.r1 >= gesture.r0) rows.len - 1 else 0];
+ const forward = gesture.r1 > gesture.r0 or (gesture.r1 == gesture.r0 and gesture.c1 >= gesture.c0);
+ const endpoint = rows[if (forward) rows.len - 1 else 0];
const body = body_layer.bodyText(p, p.scratch.allocator(), pane) catch return;
const line = modal.lineSlice(body, @intCast(@max(0, endpoint.row - panes.Terminal.gridOffset(pane))));
- const byte = if (gesture.c1 >= gesture.c0) modal.prevGrapheme(line, @min(endpoint.hi, line.len)) else endpoint.lo;
+ const byte = if (forward) modal.prevGrapheme(line, @min(endpoint.hi, line.len)) else endpoint.lo;
pane.body.cur_row = pane.surfRow(endpoint.row);
var shown = std.mem.trimEnd(u8, line[@min(endpoint.prompt_bytes, line.len)..], " \t");
if (pane.ovl) |overlay| {
@@ -753,11 +779,70 @@ fn mirrorTtySelection(p: *Pardes, pane: *Pane) void {
edit.setYank(p, text);
}
+/// acme's double-click (text.c:881 textselect, plan9port text.c:1407
+/// textdoubleclick): a second plain B1 click at the same cell within half a
+/// second selects what edit.doubleClickRange says, from the caret the click
+/// left. A plain click still puts the caret on its cell.
+fn doubleClick(p: *Pardes, id: usize, t: *panes.Text, on_tag: bool, row: i32, col: i32) void {
+ const now = clockMs();
+ const prev = p.last_click;
+ p.last_click = .{ .id = id, .on_tag = on_tag, .row = row, .col = col, .ms = now };
+ const c = prev orelse return;
+ if (c.id != id or c.on_tag != on_tag or c.row != row or c.col != col or now - c.ms > 500) return;
+ p.last_click = null;
+ const text = edit.flatSurface(p, t) catch return;
+ const q = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) });
+ const r = edit.doubleClickRange(text, q);
+ if (r[1] <= r[0]) return;
+ const a = modal.positionAt(text, r[0]);
+ const h = modal.positionAt(text, modal.prevGrapheme(text, r[1]));
+ t.vsel = .{ .active = true, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true };
+ t.cur_row = @intCast(h.row);
+ t.cur_col = @intCast(h.col);
+ t.cur_pinned = true;
+}
+
+/// On each tick while a B1 body sweep is held past an edge: the body
+/// scrolls a line toward it, the sweep's end stays under the pointer and
+/// its start stays on its text (Drag.select.anchor).
+pub fn edgeScroll(p: *Pardes) void {
+ if (p.drag != .select) return;
+ const s = &p.drag.select;
+ if (s.edge == 0) return;
+ const pane = p.panes[s.id] orelse return;
+ const sl = &pane.sel[sel_slot];
+ if (s.anchor == null) s.anchor = sweepPoint(p, pane, sl.r0, sl.c0) orelse return;
+ const before = pane.scroll();
+ pane.scrollBy(s.edge);
+ // The start's row follows its text while it is on the screen, and
+ // stays on the body's edge row once it is off (the anchor keeps it).
+ const top: i32 = pane.tag_rows;
+ sl.r0 = std.math.clamp(sl.r0 - (pane.scroll() - before), top, top + @as(i32, pane.rows) - 1);
+}
+
+/// The text position under a body cell of the sweep's space.
+fn sweepPoint(p: *Pardes, pane: *panes.Pane, r: i32, c: i32) ?modal.Cursor {
+ const visible = r - @as(i32, pane.tag_rows);
+ if (visible < 0) return null;
+ const w = pane.wrapAt(visible);
+ if (w.line < 0) return null;
+ const col = edit.paneByteAtDisplay(p, &pane.body, w.line, w.at, c - (if (pane.file != null) @as(i32, panes.File.gutterWidth(pane)) else 0));
+ return .{ .row = @intCast(w.line), .col = @intCast(@max(0, col)) };
+}
+
+fn clockMs() i64 {
+ if (comptime !pardes.hosted) return 0;
+ var ts: std.c.timespec = undefined;
+ if (std.c.clock_gettime(.MONOTONIC, &ts) != 0) return 0;
+ return @as(i64, ts.sec) * 1000 + @divTrunc(@as(i64, ts.nsec), 1_000_000);
+}
+
fn dragRelease(p: *Pardes, button: Mouse.Button) void {
// A different button's release belongs to a chord or another host
// stream; only the button which began this gesture may end it.
if (p.drag.button() != button) return;
defer p.drag = .none;
+ defer p.chord_hold = false;
switch (p.drag) {
.border_v => |d| {
const c = d.left_col;
@@ -775,13 +860,17 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
layout.applyRowSplit(p, d.col, d.top_idx, d.cur_y),
.column_move => |d| if (layout.columnBySerial(p, d.serial)) |column| switch (layout.columnDrop(p, column, d.cur_x, d.grab)) {
.stay => {},
- .reorder => |target| layout.reorderColumn(p, column, target),
+ .reorder => |target| {
+ layout.reorderColumn(p, column, target);
+ p.setActiveColumn(target); // a grip drag sets activecol (acme.c:640)
+ },
.edge => |x| layout.setColumnPairWidth(p, column - 1, x - p.col_x[column - 1]),
},
.move => |d| {
layout.movePane(p, d.id, d.cur_x, d.cur_y);
- if (p.panes[d.id]) |mp| p.stillborn_joiner = mp.serial;
- // a file moved into the left column evicts a lone unused shell
+ if (layout.findPane(p, d.id)) |f| p.setActiveColumn(f.col);
+ // BootShell replace: a file moved into the left column closes a
+ // lone unused shell there
if (p.panes[d.id]) |mt| if (mt.file != null) {
if (layout.findPane(p, d.id)) |f| if (f.col == 0)
exec.evictLonePristineTty(p, 0, d.id);
@@ -836,10 +925,11 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
} else if (tagline.tagOffsetAt(p, pane, row, col)) |cursor| {
pane.tag.cur_row = @intCast(cursor.row);
pane.tag.cur_col = @intCast(cursor.col);
+ if (!dragged) doubleClick(p, s.id, &pane.tag, true, sl.r1, sl.c1);
}
return;
}
- if (dragged) edit.capturePointerSelection(p, pane, sel_slot) catch {};
+ if (dragged and s.anchor == null) edit.capturePointerSelection(p, pane, sel_slot) catch {};
if (dragged and pane.body.mode == .tty) mirrorTtySelection(p, pane);
const body_vis = sl.r1 - @as(i32, pane.tag_rows);
if (body_vis >= 0 and pane.body.mode != .tty) {
@@ -861,6 +951,15 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
pane.body.select = false;
pane.body.restore_cursor = false;
pane.body.sticky_col = -1;
+ // A ctrl-click is a definition jump, not half a double-click.
+ if (!dragged and !s.ctrl) doubleClick(p, s.id, &pane.body, false, sl.r1, sl.c1);
+ if (s.ctrl) p.last_click = null;
+ // A sweep that scrolled has its start off the screen: it
+ // is kept as the text's own selection, start to end.
+ if (dragged) if (s.anchor) |a| {
+ pane.sel[sel_slot].state = .none;
+ pane.body.vsel = .{ .active = true, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true };
+ };
if (s.ctrl) p.lspRequest(s.id, .definition, "");
}
} else {
@@ -993,6 +1092,11 @@ fn chordCutPaste(p: *Pardes, cut: bool) void {
}
if (!s.chorded) {
s.chorded = true;
+ // Everything done while B1 stays down is one undo step, as acme
+ // marks the file once per B1 hold (text.c:881 textselect, `state`
+ // and its one filemark): B1-B2 then B1-B3 is a copy.
+ edit.pushUndo(p, &pane.body);
+ p.chord_hold = true;
const sl = pane.sel[sel_slot];
pane.sel[sel_slot].state = .none;
const pfx: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0;
diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig
index 0537c17a..1e47183b 100644
--- a/src/ninep/addr.zig
+++ b/src/ninep/addr.zig
@@ -91,6 +91,26 @@ pub const Addr = struct {
if (d < '0' or d > '9') break;
n = @min(n * 10 + (d - '0'), std.math.maxInt(u32));
}
+ // pardes's `line:col`, Look's `file:12:5` spelling (look.zig
+ // parsePathLine): the point at that column of that line, the
+ // column in bytes from 1, clamped to the line's end and snapped
+ // to a character's start as Look's cursor is. Not sam's.
+ if (size == .line and dir == 0 and a.i + 1 < a.expr.len and a.expr[a.i] == ':' and
+ a.expr[a.i + 1] >= '0' and a.expr[a.i + 1] <= '9')
+ {
+ a.i += 1;
+ var col: u64 = 0;
+ while (a.i < a.expr.len and a.expr[a.i] >= '0' and a.expr[a.i] <= '9') : (a.i += 1)
+ col = @min(col * 10 + (a.expr[a.i] - '0'), std.math.maxInt(u32));
+ const lines = modal.cursorLineCount(a.text);
+ if (n == 0 or n > lines or col == 0) {
+ a.err = e_range;
+ return null;
+ }
+ const q = clip(modal.offsetAt(a.text, .{ .row = @intCast(n - 1), .col = @intCast(col - 1) }));
+ r = .{ .q0 = q, .q1 = q };
+ continue;
+ }
r = a.number(r, @intCast(n), dir, size) orelse return null;
dir = 0;
size = .line;
@@ -202,8 +222,8 @@ pub const Addr = struct {
a.err = "no previous regular expression";
return null;
}
- var rx = regexp_.Regex.compile(pat) catch {
- a.err = e_regexp;
+ var rx = regexp_.Regex.compile(pat) catch |err| {
+ a.err = if (err == error.Anchor) regexp_.Regex.e_anchor else e_regexp;
return null;
};
const found = if (back) found: {
@@ -340,13 +360,34 @@ test "a failed address leaves none, so data refuses rather than act at the last
try testing.expectEqualStrings(pane_files.e_addr_failed, th.rd(p, addr, 0, 64).reply.ename);
try testing.expectEqual(E.INVAL, th.call(p, .{ .tag = 1, .op = .setattr, .node = Node.of(serial, .xdata), .truncate = true }).errno());
try testing.expectEqualStrings("one\ntwo\n", p.panes[0].?.file.?.content);
- // A good address, or a truncated addr, gives it one again.
+ // A good address gives it one again; truncating addr does not.
_ = th.wr(p, addr, "#0,#3");
try testing.expectEqual(Status.ok, th.wr(p, data, "ONE").reply.status);
try testing.expectEqualStrings("ONE\ntwo\n", p.panes[0].?.file.?.content);
_ = th.wr(p, addr, "99");
_ = th.call(p, .{ .tag = 1, .op = .setattr, .node = addr, .truncate = true });
- try testing.expectEqual(Status.ok, th.wr(p, data, "0").reply.status);
+ try testing.expectEqualStrings(pane_files.e_addr_failed, th.wr(p, data, "0").reply.ename);
+}
+
+test "a shell's > on addr searches on from the last address, and 0 starts over" {
+ const p = try th.withFile(testing.allocator, "foo x foo y foo\n");
+ defer p.deinit();
+ const serial = th.serialOf(p);
+ const addr = Node.of(serial, .addr);
+ const data = Node.of(serial, .data);
+ // echo /foo/ > addr; printf foobar > data -- twice over, as a loop does.
+ for (0..3) |_| {
+ _ = th.call(p, .{ .tag = 1, .op = .setattr, .node = addr, .truncate = true });
+ try testing.expectEqual(Status.ok, th.wr(p, addr, "/foo/").reply.status);
+ _ = th.call(p, .{ .tag = 1, .op = .setattr, .node = data, .truncate = true });
+ try testing.expectEqual(Status.ok, th.wr(p, data, "foobar").reply.status);
+ }
+ try testing.expectEqualStrings("foobar x foobar y foobar\n", p.panes[0].?.file.?.content);
+ // .+#1 moves on from where the last write left the address.
+ _ = th.wr(p, addr, "0");
+ _ = th.call(p, .{ .tag = 1, .op = .setattr, .node = addr, .truncate = true });
+ try testing.expectEqual(Status.ok, th.wr(p, addr, ".+#1").reply.status);
+ try testing.expectEqualStrings(" 1 1 ", th.rd(p, addr, 0, 64).bytes);
}
test "the address language, form by form" {
@@ -372,6 +413,11 @@ test "the address language, form by form" {
.{ .expr = "/two/", .q0 = 4, .q1 = 7 },
.{ .expr = "/t.o/", .q0 = 4, .q1 = 7 },
.{ .expr = "1\n", .q0 = 0, .q1 = 4 },
+ // pardes's line:col, composing like any simple address.
+ .{ .expr = "2:2", .q0 = 5, .q1 = 5 },
+ .{ .expr = "2:99", .q0 = 7, .q1 = 7 },
+ .{ .expr = "2:2,3:1", .q0 = 5, .q1 = 8 },
+ .{ .expr = "2:2+#3", .q0 = 8, .q1 = 8 },
}) |c| {
_ = th.wr(p, addr, "#0");
const w = th.wr(p, addr, c.expr);
@@ -420,7 +466,7 @@ test "the address language, form by form" {
for ([_][2][]const u8{
.{ "zzz", e_syntax }, .{ "/nomatch/", e_no_match }, .{ "99", e_range },
.{ "#999", e_range }, .{ "/a[/", e_regexp }, .{ "/(a/", e_regexp },
- .{ "/*a/", e_regexp },
+ .{ "/*a/", e_regexp }, .{ "99:1", e_range }, .{ "2:0", e_range },
}) |c| {
_ = th.wr(p, addr, "#0");
try testing.expectEqualStrings(c[1], th.wr(p, addr, c[0]).reply.ename);
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index b28e02fa..b4144fd2 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -69,13 +69,19 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
const before = Serials.snapshot(p);
// An exec answers the pane it went to: a command pane it reused, or
// the terminal it typed into.
+ p.look_missed = false;
const went: ?usize = if (exec) pardes.exec.execute(p, id, text) else blk: {
look.lookAt(p, id, text);
break :blk null;
};
+ // A look that found nothing answers nothing, not the pane before it.
+ if (!exec and p.look_missed) return;
const after = Serials.snapshot(p);
var created = false;
for (after.list[0..after.len]) |serial| if (std.mem.indexOfScalar(u32, before.list[0..before.len], serial) == null) {
+ // A look's hits buffer is how it found the text, not where: it
+ // answers the pane the hit is selected in.
+ if (!exec) if (p.paneBySerial(serial)) |made| if (p.panes[made].?.file) |f| if (f.output) |o| if (o.from == .search) continue;
noteResult(p, serial);
created = true;
};
@@ -84,6 +90,37 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
noteResult(p, if (exec) context else (p.panes[p.active] orelse return).serial);
}
+/// A write's messages, a line each, except that an `Edit` line takes the
+/// lines after it while its `{` group or `a`/`c`/`i` text block is open
+/// (sam_edit.needsMore): a block written whole reaches Edit whole, never
+/// as lines of their own.
+const Messages = struct {
+ p: *Pardes,
+ data: []const u8,
+ at: usize = 0,
+
+ fn init(p: *Pardes, data: []const u8) Messages {
+ return .{ .p = p, .data = data };
+ }
+
+ fn next(m: *Messages) ?[]const u8 {
+ if (m.at >= m.data.len) return null;
+ const start = m.at;
+ var end = std.mem.indexOfScalarPos(u8, m.data, start, '\n') orelse m.data.len;
+ m.at = end + 1;
+ const first = std.mem.trim(u8, m.data[start..end], " \t\r");
+ const edit = "Edit";
+ if (!std.mem.startsWith(u8, first, edit) or (first.len > edit.len and first[edit.len] != ' ' and first[edit.len] != '\t'))
+ return first;
+ const sam = @import("../sam_edit.zig");
+ while (m.at < m.data.len and sam.needsMore(m.p.scratch.allocator(), std.mem.trim(u8, m.data[start..end], " \t\r")[edit.len..])) {
+ end = std.mem.indexOfScalarPos(u8, m.data, m.at, '\n') orelse m.data.len;
+ m.at = end + 1;
+ }
+ return std.mem.trim(u8, m.data[start..end], " \t\r");
+ }
+};
+
// ---- look and exec ----
/// One command per line, checked whole before any line runs: a line is the
@@ -93,11 +130,11 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply {
for ([2]bool{ false, true }) |apply| {
if (apply) p.fs.results_len = 0;
- var it = std.mem.splitScalar(u8, req.data, '\n');
- while (it.next()) |raw| {
- const text = std.mem.trim(u8, raw, " \t\r");
+ var it = Messages.init(p, req.data);
+ while (it.next()) |text| {
if (text.len == 0) continue;
- for (text) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL);
+ // Only an Edit block holds newlines (Messages).
+ for (text) |c| if (c < ' ' and c != '\t' and c != '\n') return Reply.fail(req.tag, E.INVAL);
if (!apply) continue;
const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active;
if (p.panes[id] == null) return Reply.fail(req.tag, E.NOENT);
@@ -205,6 +242,13 @@ fn refuse(p: *Pardes, req: Req, why: []const u8, line: []const u8) Reply {
return tree.failText(req.tag, E.INVAL, text);
}
+/// `refuse`, saying which ctl takes the message instead.
+fn refuseTo(p: *Pardes, req: Req, why: []const u8, line: []const u8, ctl: []const u8) Reply {
+ const room = p.fs.ename.len -| (why.len + ctl.len + 18);
+ const text = std.fmt.bufPrint(&p.fs.ename, "{s} \"{s}\": write it to {s}", .{ why, line[0..@min(line.len, room)], ctl }) catch why;
+ return tree.failText(req.tag, E.INVAL, text);
+}
+
/// Checks one line written to a ctl as a builtin of `scope` before any line
/// of the write runs: a word the registry knows, of this ctl's scope, given
/// an argument if and only if it takes or requires one, and for a setting a
@@ -216,8 +260,9 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ?
const word = line[0 .. sp orelse line.len];
const arg = if (sp) |s| std.mem.trim(u8, line[s + 1 ..], " \t") else "";
const b = std.meta.stringToEnum(Builtin, word) orelse return refuse(p, req, "unknown control message", line);
- if (builtins.registry.scope(b) != scope)
- return refuse(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line);
+ // Edit is the one pane word the root takes too: at the active pane.
+ if (builtins.registry.scope(b) != scope and b != .Edit)
+ return refuseTo(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line, if (scope == .pane) "/ctl" else "pane/<n>/ctl");
if (arg.len > 0 and !builtins.registry.takesArg(b)) return refuse(p, req, "wrong #args in control message", line);
if (arg.len == 0 and builtins.registry.requiresArg(b)) return refuse(p, req, "wrong #args in control message", line);
const setting = config.Runtime.find(word) orelse return null;
@@ -298,6 +343,9 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void {
.tagline_size => try w.print("{s} {d}\n", .{ word, s.font.tagline_percent }),
.window_opacity => try w.print("{s} {d}\n", .{ word, s.window_opacity }),
.window_blur => try w.print("{s} {d}\n", .{ word, s.window_blur }),
+ .choice => |which| switch (which) {
+ inline else => |field| try w.print("{s} {s}\n", .{ word, @tagName(@field(s, @tagName(field))) }),
+ },
.message_ms => |which| try w.print("{s} {d}\n", .{ word, @field(s, "message_" ++ @tagName(which) ++ "_ms") }),
// Bare, it names the default directory.
.dump_dir => if (s.dump_dir.len > 0) try w.print("{s} {s}\n", .{ word, s.dump_dir.get() }) else try w.print("{s}\n", .{word}),
@@ -336,15 +384,16 @@ pub fn readRoot(p: *Pardes, req: Req) Reply {
pub fn writeRoot(p: *Pardes, req: Req) Reply {
for ([2]bool{ false, true }) |apply| {
if (apply) p.fs.results_len = 0;
- var it = std.mem.splitScalar(u8, req.data, '\n');
- while (it.next()) |raw| {
- const line = std.mem.trim(u8, raw, " \t\r");
+ var it = Messages.init(p, req.data);
+ while (it.next()) |line| {
if (line.len == 0) continue;
if (!apply) {
if (checkBuiltin(p, req, line, .session)) |refusal| return refusal;
continue;
}
if (p.panes[p.active] == null) return Reply.fail(req.tag, E.NOENT);
+ p.fs.session_write = true;
+ defer p.fs.session_write = false;
if (runBuiltin(p, req, p.active, line)) |refusal| return refusal;
}
}
@@ -445,25 +494,33 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply {
for ([2]bool{ false, true }) |apply| {
if (apply) p.fs.results_len = 0;
var held = !other and pane.fs.lock != null;
- var it = std.mem.splitScalar(u8, req.data, '\n');
- while (it.next()) |raw| {
- const line = std.mem.trim(u8, raw, " \t\r");
+ var it = Messages.init(p, req.data);
+ while (it.next()) |line| {
if (line.len == 0) continue;
// A line before this one may have closed the pane (`Del`).
if (apply and p.paneBySerial(serial) == null) return Reply.fail(req.tag, E.NOENT);
if (std.mem.eql(u8, line, "get")) {
if (!apply) continue;
+ if (getRefused(p, pane)) |said| return tree.failText(req.tag, E.IO, said);
const errno = get(p, pane);
if (errno != 0) return Reply.fail(req.tag, errno);
} else if (std.mem.eql(u8, line, "lock")) {
if (mine == null) return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl);
- if (other) return tree.failText(req.tag, E.BUSY, tree.e_in_use);
+ if (other) return tree.failText(req.tag, E.BUSY, e_locked);
held = true;
if (apply) pane.fs.lock = mine;
} else if (std.mem.eql(u8, line, "unlock")) {
if (!held) return tree.failText(req.tag, E.INVAL, e_not_locked);
held = false;
if (apply) pane.fs.lock = null;
+ } else if (std.mem.startsWith(u8, line, "answer ")) {
+ // The pane's question (its `ask` in the log), answered.
+ const choice = std.mem.trim(u8, line["answer ".len..], " ");
+ if (choice.len != 1) return refuse(p, req, "answer takes one choice, or -", line);
+ const id = p.paneBySerial(serial).?;
+ if (p.panes[id].?.prompt != .del_side and p.panes[id].?.prompt != .repl_choice)
+ return refuse(p, req, "no question asked", line);
+ if (apply) _ = p.answerQuestion(id, if (choice[0] == '-') null else choice[0]);
} else if (!apply) {
if (checkBuiltin(p, req, line, .pane)) |refusal| return refusal;
} else if (runBuiltin(p, req, p.paneBySerial(serial).?, line)) |refusal| {
@@ -475,6 +532,23 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply {
}
const e_not_locked = "window not locked by this open";
+/// Who holds it is another open of this ctl, whose client pardes cannot name.
+const e_locked = tree.e_in_use ++ ": another open of this ctl holds the lock";
+
+/// acme's get asks winclean first (exec.c:513): unsaved text is warned
+/// about once, and the same get again, nothing edited since, discards it.
+fn getRefused(p: *Pardes, pane: *Pane) ?[]const u8 {
+ const f = pane_files.fileOf(pane) orelse return null;
+ if (!panes.Output.fileTraits(f.output).saves or f.revision == f.saved_revision) return null;
+ // Dirty by a rename alone is no text to lose: only edits are asked about.
+ const saved = f.saved_hash orelse return null;
+ if (std.hash.Wyhash.hash(0, f.content) == saved) return null;
+ if (pane.discard_warned) |w| if (w.revision == f.revision and w.by == .get) return null;
+ pane.discard_warned = .{ .revision = f.revision, .by = .get };
+ const said = std.fmt.bufPrint(&p.fs.ename, "{s}: Modified (get again to discard)", .{f.path}) catch "Modified (get again to discard)";
+ p.setMessage(p.paneBySerial(pane.serial).?, said);
+ return said;
+}
fn get(p: *Pardes, pane: *Pane) u16 {
const f = pane_files.fileOf(pane) orelse return 0;
@@ -486,6 +560,7 @@ fn get(p: *Pardes, pane: *Pane) u16 {
panes.File.pushUndo(p, pane);
panes.File.setContent(p, f, bytes);
f.saved_revision = f.revision;
+ f.saved_hash = std.hash.Wyhash.hash(0, f.content);
return 0;
}
@@ -546,10 +621,23 @@ test "commands lists every builtin with its argument and the ctl that takes it"
try testing.expect(words.next() == null);
}
try testing.expectEqual(std.enums.values(Builtin).len, lines);
- for ([_][]const u8{ "\nDel arg pane\n", "\nNewcol root\n", "\nSave arg pane\n", "\nVerbose arg root\n" }) |row|
+ for ([_][]const u8{ "\nDel arg pane\n", "\nNewcol root\n", "\nSave arg pane\n", "\nVerbose arg root\n", "\nUndo pane\n", "\nRedo pane\n" }) |row|
try testing.expect(std.mem.indexOf(u8, listed.bytes, row) != null);
}
+test "Undo and Redo on a pane's ctl step its body through its edits" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const body = Node.of(serial, .body);
+ try testing.expectEqual(Status.ok, wr(p, body, "two\n").reply.status);
+ try testing.expectEqualStrings("one\ntwo\n", rd(p, body, 0, 64).bytes);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status);
+ try testing.expectEqualStrings("one\n", rd(p, body, 0, 64).bytes);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Redo").reply.status);
+ try testing.expectEqualStrings("one\ntwo\n", rd(p, body, 0, 64).bytes);
+}
+
test "focus reads the pane with the keyboard, and a serial written to it moves the keyboard there" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();
@@ -604,8 +692,8 @@ test "the pane ctl takes acme's verbs and the pane's builtins, and refuses the r
var want: [64]u8 = undefined;
try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "unknown control message \"{s}\"", .{bad}), refused.reply.ename);
}
- try testing.expectEqualStrings("not a window control message \"Newcol\"", wr(p, ctl_node, "Newcol").reply.ename);
- try testing.expectEqualStrings("not a window control message \"Verbose off\"", wr(p, ctl_node, "Verbose off").reply.ename);
+ try testing.expectEqualStrings("not a window control message \"Newcol\": write it to /ctl", wr(p, ctl_node, "Newcol").reply.ename);
+ try testing.expectEqualStrings("not a window control message \"Verbose off\": write it to /ctl", wr(p, ctl_node, "Verbose off").reply.ename);
try testing.expectEqualStrings("wrong #args in control message \"Collapse now\"", wr(p, ctl_node, "Collapse now").reply.ename);
// Checked whole first: a bad line anywhere and nothing ran.
try testing.expectEqual(E.INVAL, wr(p, ctl_node, "Msg first\nbogus").errno());
@@ -657,7 +745,7 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi
try testing.expectEqual(Status.ok, wr(p, root_ctl, "Newcol\n").reply.status);
try testing.expectEqual(columns + 1, p.ncol);
- try testing.expectEqualStrings("not a session control message \"Del\"", wr(p, root_ctl, "Del").reply.ename);
+ try testing.expectEqualStrings("not a session control message \"Del\": write it to pane/<n>/ctl", wr(p, root_ctl, "Del").reply.ename);
try testing.expectEqualStrings("unknown control message \"Nonsense 1\"", wr(p, root_ctl, "Nonsense 1").reply.ename);
try testing.expectEqualStrings("bad value in control message \"Verbose maybe\"", wr(p, root_ctl, "Verbose maybe").reply.ename);
const no_theme = wr(p, root_ctl, "Theme no-such-theme").reply.ename;
@@ -700,7 +788,7 @@ test "a second lock is refused until the holder unlocks or closes, and binds nob
try testing.expectEqual(Status.ok, w.ctl(p, ctl_node, a, "lock\n").reply.status);
// Another open's lock is refused at once, and so is its unlock; writes
// to the pane's other files are not.
- try testing.expectEqualStrings(tree.e_in_use, w.ctl(p, ctl_node, b, "lock\n").reply.ename);
+ try testing.expectEqualStrings(e_locked, w.ctl(p, ctl_node, b, "lock\n").reply.ename);
try testing.expectEqual(E.BUSY, w.ctl(p, ctl_node, b, "lock\n").errno());
try testing.expectEqualStrings(e_not_locked, w.ctl(p, ctl_node, b, "unlock\n").reply.ename);
try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .addr), "1").reply.status);
@@ -730,6 +818,112 @@ test "a second lock is refused until the holder unlocks or closes, and binds nob
for (p.fs.opens) |o| try testing.expect(o.node == 0);
}
+test "Edit through a pane's ctl renames every foo in one undo step, and a failed one changes nothing" {
+ const p = try withFile(testing.allocator, "foo x foo y foo\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const ctl_node = Node.of(serial, .ctl);
+ const body = Node.of(serial, .body);
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "Edit ,x/foo/c/foobar/\n").reply.status);
+ try testing.expectEqualStrings("foobar x foobar y foobar\n", rd(p, body, 0, 64).bytes);
+ const failed = wr(p, ctl_node, "Edit ,s/nothing/x/\n");
+ try testing.expectEqual(E.IO, failed.errno());
+ try testing.expectStringStartsWith(failed.reply.ename, "Edit: no substitution");
+ try testing.expectEqualStrings("foobar x foobar y foobar\n", rd(p, body, 0, 64).bytes);
+ // The whole rename is one undo step.
+ try testing.expectEqual(Status.ok, wr(p, ctl_node, "Undo").reply.status);
+ try testing.expectEqualStrings("foo x foo y foo\n", rd(p, body, 0, 64).bytes);
+}
+
+test "a write of an Edit block carries it whole, to a pane's ctl, the root's ctl and exec" {
+ const p = try withFile(testing.allocator, "foo x\nfoo y\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const body = Node.of(serial, .body);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Edit ,x/foo/{\ni/[/\na/]/\n}\n").reply.status);
+ try testing.expectEqualStrings("[foo] x\n[foo] y\n", rd(p, body, 0, 64).bytes);
+ p.active = p.paneBySerial(serial).?;
+ try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Edit ,x/\\[/{\nc/</\n}\n").reply.status);
+ try testing.expectEqualStrings("<foo] x\n<foo] y\n", rd(p, body, 0, 64).bytes);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "Edit $a\nend\n.\n").reply.status);
+ try testing.expectEqualStrings("<foo] x\n<foo] y\nend\n", rd(p, body, 0, 64).bytes);
+ // An unclosed group is refused, and nothing after it runs as a line.
+ try testing.expectEqual(E.IO, wr(p, Node.of(serial, .ctl), "Edit ,x/foo/{\nd\n").errno());
+ try testing.expectEqualStrings("<foo] x\n<foo] y\nend\n", rd(p, body, 0, 64).bytes);
+}
+
+test "dirty goes back to 0 when the text is the saved text again" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const dirty = Node.of(serial, .dirty);
+ try testing.expectEqualStrings("0\n", rd(p, dirty, 0, 8).bytes);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .body), "two\n").reply.status);
+ try testing.expectEqualStrings("1\n", rd(p, dirty, 0, 8).bytes);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status);
+ try testing.expectEqualStrings("0\n", rd(p, dirty, 0, 8).bytes);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Redo").reply.status);
+ try testing.expectEqualStrings("1\n", rd(p, dirty, 0, 8).bytes);
+}
+
+test "a message from a root ctl write is the session's in the log" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ p.update(.tick); // the pane's `new` is logged before the log is opened
+ while (p.nextEffect()) |_| {}
+ const log = @intFromEnum(tree.TopFile.log);
+ const f = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
+ const frozen = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = f, .size = 8192 }).bytes.len;
+ _ = call(p, .{ .tag = 3, .op = .write, .node = log, .handle = f, .data = "follow" });
+ try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Kill\n").reply.status);
+ // The builtin's name, then what it said: both the session's.
+ try testing.expectEqualStrings("msg - Kill\n", call(p, .{ .tag = 4, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).bytes);
+ try testing.expectEqualStrings("msg - Kill: nothing running\n", call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).bytes);
+}
+
+test "a look takes acme's :addr and file:addr, and a miss changes nothing" {
+ const gpa = testing.allocator;
+ const p = try withFile(gpa, "foo x foo y\nbar\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const look_node = Node.of(serial, .look);
+ const dot = Node.of(serial, .dot);
+ // :addr is an address in this pane, evaluated from its dot.
+ try testing.expectEqual(Status.ok, wr(p, look_node, ":/y/\n").reply.status);
+ try testing.expectEqualStrings(" 10 11 ", rd(p, dot, 0, 64).bytes);
+ try testing.expectEqual(Status.ok, wr(p, look_node, ":#4\n").reply.status);
+ try testing.expectEqualStrings(" 4 4 ", rd(p, dot, 0, 64).bytes);
+ try testing.expectEqual(Status.ok, wr(p, look_node, ":/foo/\n").reply.status); // from #4: the second foo
+ try testing.expectEqualStrings(" 6 9 ", rd(p, dot, 0, 64).bytes);
+ // file:addr names the pane's own file here.
+ var line: [256]u8 = undefined;
+ const path = p.panes[0].?.file.?.path;
+ try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:/bar/\n", .{path})).reply.status);
+ try testing.expectEqualStrings(" 12 15 ", rd(p, dot, 0, 64).bytes);
+ // A miss: no buffer, the selection kept, and it says so.
+ const panes_before = p.panes.len - std.mem.count(?*pardes.Pane, &p.panes, &.{null});
+ try testing.expectEqual(Status.ok, wr(p, look_node, "zzqq\n").reply.status);
+ try testing.expectEqual(panes_before, p.panes.len - std.mem.count(?*pardes.Pane, &p.panes, &.{null}));
+ try testing.expectEqualStrings(" 12 15 ", rd(p, dot, 0, 64).bytes);
+ try testing.expectStringStartsWith(p.panes[0].?.msg[0..p.panes[0].?.msg_len], "Look: no match");
+ // A hit answers the pane the text is selected in, not its hits buffer.
+ try testing.expectEqual(Status.ok, wr(p, look_node, "foo\n").reply.status);
+ var own: [16]u8 = undefined;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&own, "{d}\n", .{serial}), rd(p, look_node, 0, 64).bytes);
+ // A pattern with a blank in it; 0/re/ searches from the top, whatever dot is.
+ try testing.expectEqual(Status.ok, wr(p, look_node, ":/foo y/\n").reply.status);
+ try testing.expectEqualStrings(" 6 11 ", rd(p, dot, 0, 64).bytes);
+ try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:0/foo/\n", .{path})).reply.status);
+ try testing.expectEqualStrings(" 0 3 ", rd(p, dot, 0, 64).bytes);
+ // file:N selects the line; a line past the end moves nothing and reads back empty.
+ try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:2\n", .{path})).reply.status);
+ try testing.expectEqualStrings(" 12 16 ", rd(p, dot, 0, 64).bytes);
+ try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:99\n", .{path})).reply.status);
+ try testing.expectEqualStrings(" 12 16 ", rd(p, dot, 0, 64).bytes);
+ try testing.expectEqualStrings("", rd(p, look_node, 0, 64).bytes);
+ try testing.expectStringStartsWith(p.panes[0].?.msg[0..p.panes[0].?.msg_len], "Look: ");
+}
+
test "exec runs a builtin at the pane and records the pane it acted on" {
const gpa = testing.allocator;
const p = try withFile(gpa, "Msg fs-ran\n");
@@ -886,6 +1080,10 @@ test "get reloads the pane from disk and reports a missing file without losing d
_ = wr(p, Node.of(serial, .name), try std.fmt.bufPrint(&name, "{s}/missing.txt\n", .{directory}));
_ = wr(p, Node.of(serial, .body), "unsaved\n");
+ // Unsaved text is asked about once, as acme's get asks winclean.
+ const asked = wr(p, ctl_node, "get\n");
+ try testing.expectEqual(E.IO, asked.errno());
+ try testing.expect(std.mem.endsWith(u8, asked.reply.ename, ": Modified (get again to discard)"));
try testing.expectEqual(E.NOENT, wr(p, ctl_node, "get\n").errno());
try testing.expect(p.paneBySerial(serial) != null);
try testing.expectEqualStrings("from disk\nunsaved\n", pane.file.?.content);
@@ -999,7 +1197,7 @@ test "Joincol with no column to the right and Theme with no such theme say so" {
try testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "no theme \"drak\"") != null);
}
-test "a look that fills a column's stillborn scratch is answered with the scratch already gone" {
+test "a look after Newcol fills the empty column the keyboard is on" {
const gpa = testing.allocator;
const p = try withFile(gpa, "x\n");
defer p.deinit();
@@ -1009,14 +1207,12 @@ test "a look that fills a column's stillborn scratch is answered with the scratc
var dir: [4096]u8 = undefined;
const at = dir[0..try tmp.dir.realPath(testing.io, &dir)];
var line: [4200]u8 = undefined;
- // Newcol makes a column of one empty +New; a file looked from it
- // joins that column and takes the scratch's place.
+ // Newcol makes an empty column and gives its tag the keyboard; no pane
+ // has it, so focus reads empty.
try testing.expectEqual(tree.Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Newcol\n").reply.status);
- const scratch = p.panes[p.active].?.serial;
- try testing.expectEqual(tree.Status.ok, wr(p, Node.of(scratch, .look), try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{at})).reply.status);
- try testing.expect(p.paneBySerial(scratch) == null);
- var want: [32]u8 = undefined;
- const gone = try std.fmt.bufPrint(&want, "{d} ", .{scratch});
- var rows = std.mem.splitScalar(u8, rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, '\n');
- while (rows.next()) |row| try testing.expect(!std.mem.startsWith(u8, row, gone));
+ try testing.expectEqual(@as(usize, 0), p.col_n[p.ncol - 1]);
+ try testing.expectEqualStrings("", rd(p, @intFromEnum(tree.TopFile.focus), 0, 64).bytes);
+ try testing.expectEqual(tree.Status.ok, wr(p, @intFromEnum(tree.TopFile.look), try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{at})).reply.status);
+ try testing.expectEqual(@as(usize, 1), p.col_n[p.ncol - 1]);
+ try testing.expectEqual(p.active, p.col_panes[p.ncol - 1][0]);
}
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index ffdea95b..0e489be7 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -565,6 +565,32 @@ const serialOf = th.serialOf;
const Node = tree.Node;
const Status = tree.Status;
+test "a stream read at an offset inside its last answer gets the rest of it again, as bash's read seeks back" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ p.update(.tick);
+ while (p.nextEffect()) |_| {}
+ const log = @intFromEnum(tree.TopFile.log);
+ const f = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
+ const frozen: u64 = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = f, .size = 8192 }).bytes.len;
+ _ = call(p, .{ .tag = 3, .op = .write, .node = log, .handle = f, .data = "follow" });
+ notePath(p, "dump", "/a");
+ notePath(p, "dump", "/b");
+ const Read = struct {
+ fn at(pp: *Pardes, h: u32, off: u64) []const u8 {
+ return call(pp, .{ .tag = 4, .op = .read, .node = @intFromEnum(tree.TopFile.log), .handle = h, .off = off, .size = 128 }).bytes;
+ }
+ };
+ try testing.expectEqualStrings("dump /a\n", Read.at(p, f, frozen));
+ // bash kept "dump" and seeked back to just past it.
+ try testing.expectEqualStrings(" /a\n", Read.at(p, f, frozen + 4));
+ // At the end of what it was given: the next record.
+ try testing.expectEqualStrings("dump /b\n", Read.at(p, f, frozen + 8));
+ // A client that reads every record at one offset still gets new ones.
+ notePath(p, "dump", "/c");
+ try testing.expectEqualStrings("dump /c\n", Read.at(p, f, frozen + 8));
+}
+
test "event records are acme's bytes, one per read, and .again when empty" {
const gpa = testing.allocator;
const p = try withFile(gpa, "Msg fs-ran\n");
@@ -908,8 +934,13 @@ test "a Dump written and a Restore made are in the log, with their files" {
const log = try freezeLog(core);
defer core.gpa.free(log.bytes);
var want: [64]u8 = undefined;
- try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "{s} /tmp/pardes.dump.zon\n", .{what})));
+ try testing.expect(std.mem.indexOf(u8, log.bytes, try std.fmt.bufPrint(&want, "{s} /tmp/pardes.dump.zon\n", .{what})) != null);
}
+ // The replacement's last says which serial each pane had.
+ const log = try freezeLog(restored);
+ defer restored.gpa.free(log.bytes);
+ var want: [64]u8 = undefined;
+ try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "restore /tmp/pardes.dump.zon\nrestored {d} {d}\n", .{ p.panes[0].?.serial, restored.panes[0].?.serial })));
}
test "opens of the log share the open records, and a closed one frees its record" {
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index d2b2a791..13381066 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -29,14 +29,15 @@ const Node = tree.Node;
/// Filesystem state a pane carries beside its editor state.
pub const State = struct {
/// The range `data` and `xdata` read and write through. acme clears it
- /// when the first client opens `addr` (editors/acme/xfid.c:105), which
- /// suits a client that holds the fid open and leaves a shell reading back
- /// `0 0` from the address it just wrote. Here it is the pane's own
- /// register, cleared by truncating the file, so that `cp addr dot` and
+ /// when the first client opens `addr` (editors/acme/xfid.c:105-108),
+ /// which suits a client that holds the fid open; a shell cannot, and
+ /// would evaluate every `echo /re/ > addr` from #0 and never advance.
+ /// Here it is the pane's own register, cleared by neither an open nor
+ /// a truncation: `0` or `,` starts over, and `cp addr dot` and
/// `cat addr` answer what was written.
addr: Range = .{},
/// The last address written to `addr` failed, so there is none: `data`
- /// and `xdata` refuse until one is written or `addr` is truncated,
+ /// and `xdata` refuse until a good one is written,
/// rather than act at the address before it, which acme would do.
addr_failed: bool = false,
limit: ?Range = null,
@@ -46,6 +47,10 @@ pub const State = struct {
event_reader: bool = false,
events: events.Queue = .{},
nomark: bool = false,
+ /// The revision a truncation of `data`, `xdata` or `body` left: the
+ /// write that follows it at once joins its undo step, so a shell's
+ /// `printf x > data` is one step, as acme's replace is.
+ joined: ?u32 = null,
noscroll: bool = false,
tag_snap: std.ArrayList(u8) = .empty,
/// An open reads pty/data, so output queues for it; a second is refused.
@@ -58,6 +63,9 @@ pub const State = struct {
lock: ?u32 = null,
/// The host started a shell it could not teach to mark its prompts.
unmarked: bool = false,
+ /// The pty's size as pty/ctl's winsize last set it, until the pane's own
+ /// grid resizes and gives the pty its size again.
+ winsize: ?[2]u16 = null,
/// Installed during this update; /log hears about it once the update ends
/// and the pane has its file, name and place.
unannounced: bool = false,
@@ -189,7 +197,9 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const
@memcpy(new[0..lo], f.content[0..lo]);
@memcpy(new[lo..][0..take], bytes[0..take]);
@memcpy(new[lo + take ..], f.content[hi..]);
- if (!pane.fs.nomark) panes.File.pushUndo(p, pane);
+ const join = pane.fs.joined == f.revision;
+ pane.fs.joined = null;
+ if (!pane.fs.nomark and !join) panes.File.pushUndo(p, pane);
panes.File.setContent(p, f, new);
return take;
}
@@ -507,7 +517,7 @@ fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply {
fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply {
const word = std.mem.trim(u8, req.data, " \t\r\n");
- if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return tree.failText(req.tag, E.INVAL, "takes 1 or 0");
+ if (!std.mem.eql(u8, word, "1") and !std.mem.eql(u8, word, "0")) return tree.failText(req.tag, E.INVAL, "bad flag: takes 1 or 0");
const on = word[0] == '1';
const pf = &pane.fs;
switch (file) {
@@ -515,9 +525,12 @@ fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply {
const f = fileOf(pane) orelse return Reply.fail(req.tag, E.INVAL);
f.saved_revision = if (on) f.revision -% 1 else f.revision;
},
+ // acme's nomark joins the writes after it into one undo step. The
+ // step starts here: the text as it is now is what Undo brings back,
+ // and turning mark on again only ends it.
.mark => {
+ if (!on and !pf.nomark) panes.File.pushUndo(p, pane);
pf.nomark = !on;
- if (on) panes.File.pushUndo(p, pane);
},
.scroll => pf.noscroll = !on,
else => unreachable,
@@ -592,11 +605,11 @@ pub fn appendErrors(p: *Pardes, id: usize, text: []const u8) ?usize {
}
const free = p.freeSlot() orelse return null;
const content = p.gpa.dupe(u8, text) catch return null;
- const np = panes.Output.open(p, free, dir, .errors, "", content) catch {
+ _ = panes.Output.open(p, free, dir, .errors, "", content) catch {
p.gpa.free(content);
return null;
};
- exec.placeDoc(p, id, free, np);
+ exec.placeNew(p, id, id, free, .errors);
return text.len;
}
@@ -610,6 +623,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status {
switch (file) {
.body => if (fileOf(pane) != null) {
_ = spliceBody(p, pane, 0, bodyOf(pane).len, "") orelse return .err;
+ pf.joined = fileOf(pane).?.revision;
pf.addr = .{};
setDot(pane, .{});
},
@@ -619,6 +633,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status {
const q1 = @max(q0, pf.addr.q1);
const before = dotOf(pane);
_ = spliceBody(p, pane, q0, q1, "") orelse return .err;
+ pf.joined = fileOf(pane).?.revision;
setDot(pane, .{ .q0 = shiftOne(before.q0, q0, q1 - q0, 0), .q1 = shiftOne(before.q1, q0, q1 - q0, 0) });
pf.addr = .{ .q0 = q0, .q1 = q0 };
},
@@ -634,10 +649,10 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status {
pane.tag.vsel.active = false;
pane.tag.nsel = 0;
},
- .addr => {
- pf.addr = .{};
- pf.addr_failed = false;
- },
+ // A shell's `>` truncates before it writes: the address written
+ // is evaluated from where the last one left off, as with `>>`.
+ // `0` (or `,`) is how to start over. See State.addr.
+ .addr => {},
.limit => pf.limit = null,
.dot => if (fileOf(pane) != null) setDot(pane, .{}),
else => {},
@@ -875,7 +890,7 @@ test "the flag files read and write the state acme kept behind ctl verbs" {
try testing.expectEqual(@as(u64, 2), call(p, .{ .tag = 1, .op = .getattr, .node = node }).reply.attr.size);
for ([_][]const u8{ "", "yes", "2", "0 1", "true" }) |bad| {
try testing.expectEqual(E.INVAL, wr(p, node, bad).errno());
- try testing.expectEqualStrings("takes 1 or 0", wr(p, node, bad).reply.ename);
+ try testing.expectEqualStrings("bad flag: takes 1 or 0", wr(p, node, bad).reply.ename);
}
}
@@ -905,6 +920,34 @@ test "the flag files read and write the state acme kept behind ctl verbs" {
try testing.expectEqual(E.INVAL, wr(term, Node.of(serialOf(term), .dirty), "1").errno());
}
+test "a replace through data, truncate then write, is one undo step" {
+ const p = try withFile(testing.allocator, "one two\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ _ = wr(p, Node.of(serial, .addr), "/two/");
+ _ = call(p, .{ .tag = 1, .op = .setattr, .node = Node.of(serial, .data), .truncate = true });
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .data), "2").reply.status);
+ try testing.expectEqualStrings("one 2\n", rd(p, Node.of(serial, .body), 0, 64).bytes);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status);
+ try testing.expectEqualStrings("one two\n", rd(p, Node.of(serial, .body), 0, 64).bytes);
+}
+
+test "writes under mark 0 are one undo step, even with no history before them" {
+ const p = try withFile(testing.allocator, "one\n");
+ defer p.deinit();
+ const serial = serialOf(p);
+ const body = Node.of(serial, .body);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .mark), "0").reply.status);
+ _ = wr(p, body, "two\n");
+ _ = wr(p, body, "three\n");
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .mark), "1").reply.status);
+ try testing.expectEqualStrings("one\ntwo\nthree\n", rd(p, body, 0, 64).bytes);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status);
+ try testing.expectEqualStrings("one\n", rd(p, body, 0, 64).bytes);
+ try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Redo").reply.status);
+ try testing.expectEqualStrings("one\ntwo\nthree\n", rd(p, body, 0, 64).bytes);
+}
+
test "truncating the tag clears its editable tail" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index 3599b612..393516ce 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -60,6 +60,7 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool {
if (words.next() != null) return false;
if (!apply) return true;
p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } });
+ pane.fs.winsize = .{ cols, rows };
},
.sig => {
const which = signalNamed(words.next() orelse return false) orelse return false;
@@ -79,13 +80,20 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool {
/// ctl reads back its state in the words it takes, as a Plan 9 ctl does.
pub fn readCtl(p: *Pardes, req: Req, pane: *Pane) Reply {
- p.fs.stage(p.gpa).print(p.gpa, "winsize {d} {d}\n", .{ pane.cols, pane.rows }) catch
+ const size = winsize(pane);
+ p.fs.stage(p.gpa).print(p.gpa, "winsize {d} {d}\n", .{ size[0], size[1] }) catch
return Reply.fail(req.tag, E.NOMEM);
return tree.stagedReply(p, req);
}
-/// Three right-aligned fields: cols, rows and busy: whether a run written now
-/// would be told `busy` (a command runs, or text is typed at the prompt).
+/// The pty's size: what ctl's winsize set, else the pane's grid.
+fn winsize(pane: *const Pane) [2]u16 {
+ return pane.fs.winsize orelse .{ pane.cols, pane.rows };
+}
+
+/// Three right-aligned fields: the pty's cols and rows, and busy: whether a
+/// run written now would be told `busy` (a command runs, or text is typed at
+/// the prompt). A newline ends the line, as a Plan 9 status file's does.
pub const status_len: u64 = 36;
pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
@@ -97,7 +105,8 @@ pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
const busy = p.hostTtyTaken(id) or (!pane.fs.unmarked and (waitingRun(p, pane) != null or
(if (marks) |m| m.prompts > 0 and (m.phase != .input or !pardes.panes.Terminal.promptInputEmpty(pane)) else false)));
const out = p.fs.stage(p.gpa);
- out.print(p.gpa, "{d:>11} {d:>11} {d:>11} ", .{ pane.cols, pane.rows, @intFromBool(busy) }) catch {};
+ const size = winsize(pane);
+ out.print(p.gpa, "{d:>11} {d:>11} {d:>11}\n", .{ size[0], size[1], @intFromBool(busy) }) catch {};
return tree.stagedReply(p, req);
}
@@ -287,6 +296,12 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void {
const done = marks.finished orelse return;
if (done.seq != slot.want) return;
pf.run = null;
+ finish(p, slot, pane, done.status);
+}
+
+/// Answers a run whose command ended with `status`: the header and the
+/// output it printed.
+fn finish(p: *Pardes, slot: *Run, pane: *Pane, status_code: ?i32) void {
const printed = pardes.panes.Terminal.commandOutput(pane, p.gpa, output_rows) catch
return answer(p, slot, "error out of memory", .{});
defer p.gpa.free(printed.text);
@@ -305,7 +320,7 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void {
}
// A D that carries no status says nothing of how the command went.
var code: [16]u8 = undefined;
- const status = if (done.status) |s| std.fmt.bufPrint(&code, "{d}", .{s}) catch unreachable else "?";
+ const status = if (status_code) |s| std.fmt.bufPrint(&code, "{d}", .{s}) catch unreachable else "?";
// The header is the whole first line, so a count there can never be
// mistaken for output; `cut` with no count: its start is not there to
// count from.
@@ -317,6 +332,22 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void {
answer(p, slot, "exit {s}", .{status});
}
+/// The shell itself exited (a run's `exit 3`, or one typed): a run waiting
+/// on the command it ran answers with the shell's status and what it
+/// printed, and the log says `exit` before the pane's `del`. Unknown, the
+/// status is not guessed: the run hears `error shell gone` when the pane
+/// goes.
+pub fn shellExited(p: *Pardes, pane: *Pane, status: ?u8) void {
+ const code = status orelse return;
+ if (waitingRun(p, pane)) |slot| if (slot.phase == .sent) {
+ pane.fs.run = null;
+ pardes.panes.Terminal.endOutputHere(pane);
+ finish(p, slot, pane, code);
+ };
+ var buf: [4]u8 = undefined;
+ pardes.exec.noteRun(p, pane, "exit", std.fmt.bufPrint(&buf, "{d}", .{code}) catch unreachable);
+}
+
/// The pane closed or its shell was replaced: the command's end will never
/// be reported, so the run says so instead of waiting forever. A line not
/// sent yet waits on for the replacement's first prompt.
@@ -466,6 +497,12 @@ test "every pty/ctl verb, and every refusal" {
try testing.expectEqual(@as(u16, 44), ws.winsize.?.rows);
try testing.expectEqual(cols, pane.cols);
try testing.expectEqual(rows, pane.rows);
+ // ctl and status read back the pty's size, until the pane resizes.
+ try testing.expectEqualStrings("winsize 132 44\n", rd(p, ctl, 0, 64).bytes);
+ try testing.expect(std.mem.startsWith(u8, rd(p, Node.of(serialOf(p), .pty_status), 0, 64).bytes, " 132 44 "));
+ p.update(.{ .resize = .{ .cols = 76, .rows = 24 } });
+ try testing.expect(pane.cols != 132);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&said, "winsize {d} {d}\n", .{ pane.cols, pane.rows }), rd(p, ctl, 0, 64).bytes);
for ([_]struct { line: []const u8, want: pardes.PtySignal }{
.{ .line = "sig INT", .want = .int },
@@ -527,6 +564,8 @@ test "owned cwd pty exec rejects long paths before applying its batch" {
const p = try withTerm(testing.allocator);
defer p.deinit();
const pane = p.panes[0].?;
+ // A one-row tag: the long path would wrap it onto rows and resize the pty.
+ pane.tag_expanded = false;
const ctl = Node.of(pane.serial, .pty_ctl);
var path: [1025]u8 = @splat('d');
path[0] = '/';
@@ -554,7 +593,7 @@ test "pty/status reports the grid and who holds the tty" {
const a = rd(p, status, 0, 256);
try testing.expectEqual(Status.ok, a.reply.status);
var want: [64]u8 = undefined;
- const whole = try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11} ", .{ pane.cols, pane.rows, 0 });
+ const whole = try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11}\n", .{ pane.cols, pane.rows, 0 });
try testing.expectEqualStrings(whole, a.bytes);
try testing.expectEqual(@as(usize, 3 * 12), a.bytes.len);
try testing.expectEqualStrings(whole[12..], rd(p, status, 12, 256).bytes);
@@ -563,7 +602,7 @@ test "pty/status reports the grid and who holds the tty" {
p.host = .{ .ctx = &probe, .vtable = &th.FakeTty.vtable };
const held = rd(p, status, 0, 256);
try testing.expectEqualStrings(
- try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11} ", .{ pane.cols, pane.rows, 1 }),
+ try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11}\n", .{ pane.cols, pane.rows, 1 }),
held.bytes,
);
}
@@ -645,12 +684,12 @@ test "run answers how its command ended once the shell is back at a prompt" {
// Before its first prompt the shell cannot take a line yet: the line
// waits for it, and pty/status does not call the shell busy meanwhile.
const status = Node.of(serialOf(p), .pty_status);
- try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 0 "));
+ try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 0\n"));
const early = call(p, .{ .tag = 1, .op = .open, .node = node });
const waiting = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = early.reply.handle, .data = "true\n" });
try testing.expectEqualStrings("", waiting.pty());
try testing.expectEqual(Status.again, call(p, .{ .tag = 3, .op = .read, .node = node, .handle = early.reply.handle, .size = 64 }).reply.status);
- try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 1 "));
+ try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 1\n"));
// A respawn before that prompt keeps it waiting for the new shell's.
p.acknowledgeShell(0, "/bin/sh", true);
try testing.expectEqual(Status.again, call(p, .{ .tag = 3, .op = .read, .node = node, .handle = early.reply.handle, .size = 64 }).reply.status);
@@ -746,8 +785,52 @@ test "run answers how its command ended once the shell is back at a prompt" {
_ = call(p, .{ .tag = 18, .op = .write, .node = node, .handle = gone.reply.handle, .data = "sleep 9\n" });
try p.removePane(0, null);
try testing.expectEqualStrings("error shell gone\n", call(p, .{ .tag = 19, .op = .read, .node = node, .handle = gone.reply.handle, .size = 64 }).bytes);
+ // It still stats while the run is open (a reader's fstat), on the
+ // open or on the file's name, and not once it is released.
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 19, .op = .getattr, .node = node, .handle = gone.reply.handle }).reply.status);
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 19, .op = .getattr, .node = node }).reply.status);
_ = call(p, .{ .tag = 20, .op = .release, .node = node, .handle = gone.reply.handle });
for (p.fs.opens) |o| try testing.expect(o.node == 0);
+ try testing.expectEqual(E.NOENT, call(p, .{ .tag = 21, .op = .getattr, .node = node }).errno());
+}
+
+test "a shell that exits under a run answers its status, and the log says exit before del" {
+ const gpa = testing.allocator;
+ const p = try withTerm(gpa);
+ defer p.deinit();
+ const serial = serialOf(p);
+ const node = Node.of(serial, .pty_run);
+ const prompt = "\x1b]133;A;cl=line;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07";
+ p.update(.{ .output = .{ .pane = 0, .bytes = prompt } });
+ while (p.nextEffect()) |_| {}
+ const log = @intFromEnum(tree.TopFile.log);
+ const f = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
+ const frozen = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = f, .size = 1 << 16 }).bytes.len;
+ _ = call(p, .{ .tag = 3, .op = .write, .node = log, .handle = f, .data = "follow" });
+
+ const r = call(p, .{ .tag = 4, .op = .open, .node = node });
+ _ = call(p, .{ .tag = 5, .op = .write, .node = node, .handle = r.reply.handle, .data = "echo bye; exit 3\n" });
+ p.update(.{ .output = .{ .pane = 0, .bytes = "echo bye; exit 3\r\n\x1b]133;C;aid=pardes\x07bye\r\n" } });
+ p.update(.{ .exited = .{ .pane = 0, .status = 3 } });
+ p.update(.{ .eof = .{ .pane = 0 } });
+ while (p.nextEffect()) |_| {}
+ try testing.expect(p.paneBySerial(serial) == null);
+ try testing.expectEqualStrings("exit 3\nbye\n", call(p, .{ .tag = 6, .op = .read, .node = node, .handle = r.reply.handle, .size = 64 }).bytes);
+ var want: [128]u8 = undefined;
+ // A followed log reads a record at a time: the exit, then the del.
+ var records: [2][]const u8 = undefined;
+ var at = frozen;
+ for (&records) |*rec| {
+ rec.* = try gpa.dupe(u8, call(p, .{ .tag = 7, .op = .read, .node = log, .handle = f, .off = at, .size = 4096 }).bytes);
+ at += rec.len;
+ }
+ defer for (records) |rec| gpa.free(rec);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "exit {d} 3\n", .{serial}), records[0]);
+ try testing.expect(std.mem.startsWith(u8, records[1], try std.fmt.bufPrint(&want, "del {d} ", .{serial})));
+ // A write to its pty/data now finds no pane.
+ try testing.expectEqual(E.NOENT, wr(p, Node.of(serial, .pty_data), "x").errno());
+ _ = call(p, .{ .tag = 8, .op = .release, .node = node, .handle = r.reply.handle });
+ _ = call(p, .{ .tag = 9, .op = .release, .node = log, .handle = f });
}
test "run on a command pane says no shell is there" {
diff --git a/src/ninep/testing.zig b/src/ninep/testing.zig
index fce77f10..927552e2 100644
--- a/src/ninep/testing.zig
+++ b/src/ninep/testing.zig
@@ -56,6 +56,15 @@ pub fn rd(p: *Pardes, node: u64, off: u64, size: u32) Answer {
return call(p, .{ .tag = 1, .op = .read, .node = node, .off = off, .size = size });
}
+/// Whether the log, read whole from a fresh open, holds `needle`.
+pub fn logHas(p: *Pardes, needle: []const u8) bool {
+ const log = @intFromEnum(tree.TopFile.log);
+ const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
+ defer _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h });
+ const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes;
+ return std.mem.indexOf(u8, text, needle) != null;
+}
+
pub fn wr(p: *Pardes, node: u64, data: []const u8) Answer {
return call(p, .{ .tag = 2, .op = .write, .node = node, .data = data });
}
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index f118e4b4..ad95f6bf 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -130,6 +130,13 @@ pub const Open = struct {
/// place of the older. The listener fills it in and answers it
/// (src/9p_io.zig, `answerHeld`).
held: ?Held = null,
+ /// The last answer a stream open (a followed log, event, pty/data) gave,
+ /// and the offset it was read at. A read at an offset inside it is
+ /// answered from it again: bash's `read` takes a chunk, keeps a line and
+ /// lseeks back to just past it, and a stream has already moved on.
+ replay: ?Replay = null,
+
+ pub const Replay = struct { off: u64, bytes: []u8 };
/// `asker` is the connection the read came on, which only the listener
/// knows.
@@ -145,6 +152,7 @@ pub const Open = struct {
},
.event, .pty_data, .ctl => {},
}
+ if (o.replay) |r| gpa.free(r.bytes);
o.* = .{};
}
};
@@ -377,7 +385,7 @@ pub fn handle(p: *Pardes, req: Req) Reply {
};
return switch (req.op) {
.lookup => lookup(p, req, target),
- .getattr => .{ .tag = req.tag, .attr = attrOf(p, target) orelse return Reply.fail(req.tag, E.NOENT) },
+ .getattr => .{ .tag = req.tag, .attr = attrOf(p, target) orelse runAttr(p, req, target) orelse return Reply.fail(req.tag, E.NOENT) },
.setattr => setattr(p, req, target),
.open => open(p, req, target),
.release => release(p, req),
@@ -387,6 +395,24 @@ pub fn handle(p: *Pardes, req: Req) Reply {
};
}
+/// A run's answer outlives its pane, so its file still stats while a run
+/// is open on it (a reader's fstat, cat's), as acme's fid keeps its window.
+/// The stat names the open when it came on one; a mount's comes on the
+/// file's walked name, and serials are never reused, so any run open on
+/// this node is the reader's.
+fn runAttr(p: *Pardes, req: Req, target: Target) ?Reply.Attr {
+ const t = switch (target) {
+ .pane => |t| t,
+ .top => return null,
+ };
+ if (t.file != .pty_run) return null;
+ const run = if (req.handle != 0) openOf(p, req) else for (&p.fs.opens) |*o| {
+ if (o.node == req.node and o.what == .run) break o;
+ } else null;
+ if ((run orelse return null).what != .run) return null;
+ return .{ .name = t.file.fileName(), .node = Node.of(t.serial, t.file), .mode = t.file.mode(), .mtime = p.fs.started };
+}
+
fn attrOf(p: *Pardes, target: Target) ?Reply.Attr {
switch (target) {
.top => |f| return .{
@@ -569,7 +595,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply {
// closing it leaves the pane alone.
.new => {
const slot = p.freeSlot() orelse return Reply.fail(req.tag, E.NFILE);
- p.newScratchBelow(p.active);
+ p.newScratchFor(p.active, .fs);
const made = p.panes[slot] orelse return Reply.fail(req.tag, E.NFILE);
return .{ .tag = req.tag, .handle = made.serial };
},
@@ -692,6 +718,32 @@ fn setattr(p: *Pardes, req: Req, target: Target) Reply {
}
fn read(p: *Pardes, req: Req, target: Target) Reply {
+ const stream = if (openOf(p, req)) |o| switch (o.what) {
+ .log => o,
+ .event => |reader| if (reader) o else null,
+ .pty_data => o,
+ else => null,
+ } else null;
+ const s = stream orelse return readFile(p, req, target);
+ // Strictly inside: a client that reads every record at one offset (the
+ // offset is ignored on a stream, as in Plan 9) always gets new ones.
+ if (s.replay) |r| if (req.off > r.off and req.off < r.off + r.bytes.len) {
+ const from = r.bytes[@intCast(req.off - r.off)..];
+ const n = @min(from.len, req.size);
+ p.fs.stage(p.gpa).appendSlice(p.gpa, from[0..n]) catch return Reply.fail(req.tag, E.NOMEM);
+ return .{ .tag = req.tag, .payload = .{ .staged = @intCast(n) } };
+ };
+ const reply = readFile(p, req, target);
+ if (reply.status != .ok) return reply;
+ const bytes = p.fsPayload(reply);
+ if (bytes.len == 0) return reply;
+ const kept = p.gpa.dupe(u8, bytes) catch return reply;
+ if (s.replay) |r| p.gpa.free(r.bytes);
+ s.replay = .{ .off = req.off, .bytes = kept };
+ return reply;
+}
+
+fn readFile(p: *Pardes, req: Req, target: Target) Reply {
switch (target) {
.top => |f| return switch (f) {
.README => help: {
diff --git a/src/panes.zig b/src/panes.zig
index 8049da4e..8ca2bb4d 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -25,10 +25,12 @@ pub const EditText = struct { text: []u8, row0: i32 };
pub const Pane = struct {
/// The words that throw unsaved text away (builtins.zig warnModified).
- pub const Discarding = enum { Exit, Restore };
+ pub const Discarding = enum { Exit, Restore, get };
- /// One mouse selection (block-shaped), per button. c/r are text-area relative;
- /// r counts from the tag's first row (the body starts at Pane.tag_rows).
+ /// One mouse sweep, per button: the press (c0, r0) and where it is now
+ /// (c1, r1), a stream between them as acme's (edit.sweepCols). c/r are
+ /// text-area relative; r counts from the tag's first row (the body
+ /// starts at Pane.tag_rows).
pub const Sel = struct {
state: enum { none, dragging, done } = .none,
source_id: u64 = 0,
@@ -38,12 +40,15 @@ pub const Pane = struct {
r1: i32 = 0,
};
- /// A rectangle keeps one source span per rendered row. Wrapped segments
- /// remain separate rows when copied, even when they share a source line.
+ /// A captured sweep keeps one source span per rendered row. Wrapped
+ /// segments remain separate rows when copied, even when they share a
+ /// source line.
pub const PointerRow = struct {
row: i32,
lo: usize,
hi: usize,
+ /// Not the sweep's last row: its highlight runs to the right edge.
+ to_edge: bool = false,
prefix: [32]u8 = @splat(0),
prefix_len: u8 = 0,
raw_terminal: bool = false,
@@ -181,6 +186,11 @@ pub const Pane = struct {
/// scrolls to keep its caret in view while it has the keyboard; pointer
/// rows count from this line, not the tag's first.
tag_top: u16 = 0,
+ /// The tag shows all its rows (acme's `tagexpand`, on by default); off,
+ /// one row, as acme's Tagup leaves it. Down on the
+ /// tag's last line and Up on its first, typing, or Alt-Down and Alt-Up
+ /// in the tag, as acme's arrows in a tag (plan9port text.c:776).
+ tag_expanded: bool = true,
/// How long the tag's computed prefix was when its positions were last
/// placed. They count from the start of the prefix, so when it grows or
/// shrinks (a rename, the dirty marker, a PDF's page) what follows it
@@ -512,6 +522,16 @@ pub const Pane = struct {
}
}
+ /// Every line of the file's text fits in the body's rows; the empty
+ /// line after a final newline is no text.
+ pub fn textFits(pane: *Pane) bool {
+ if (pane.file) |*f| {
+ const lines = File.nlines(pane.gpa, f) - @intFromBool(std.mem.endsWith(u8, f.content, "\n"));
+ return lines <= pane.rows;
+ }
+ return false;
+ }
+
pub fn ensureCursorVisible(pane: *Pane) void {
const off = pane.scroll();
var in_context = false;
@@ -539,7 +559,13 @@ pub const Pane = struct {
pane.scrollBy(pane.body.cur_row - margin - off);
} else if (pane.body.cur_row > last - margin) {
var to = pane.body.cur_row + margin;
- if (pane.file) |*f| to = @min(to, @as(i32, @intCast(File.nlines(pane.gpa, f) -| 1)));
+ if (pane.file) |*f| {
+ // The margin only reveals text: a pane its whole text
+ // fits in never scrolls for it. A longer one keeps
+ // helix's clamp at the buffer's last line.
+ const lines: i32 = @intCast(File.nlines(pane.gpa, f));
+ to = @min(to, if (off == 0 and pane.textFits()) @max(pane.body.cur_row, lines - 1 - @intFromBool(std.mem.endsWith(u8, f.content, "\n"))) else lines - 1);
+ }
if (comptime Pdf.enabled) {
if (pane.pdf) |pv|
to = @min(to, @as(i32, @intCast(modal.lineCount(pv.text) -| 1)));
diff --git a/src/pardes.zig b/src/pardes.zig
index eac21a14..6294b6cb 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -452,6 +452,7 @@ test {
_ = @import("tagline.zig");
_ = @import("exec.zig");
_ = @import("look.zig");
+ _ = @import("sam_edit.zig");
_ = @import("mouse.zig");
_ = @import("normal.zig");
_ = @import("regexp.zig");
@@ -1091,13 +1092,13 @@ test "Joincol folds the active column into its right neighbor, keeping its panes
try std.testing.expectEqual(@as(usize, 2), p.ncol);
p.active = 0; // the left column is current
- layout.joinCol(p);
+ layout.joinCol(p, 0);
try std.testing.expectEqual(@as(usize, 1), p.ncol);
const lf = layout.findPane(p, 0) orelse return error.LostLeftPane;
const rf = layout.findPane(p, right) orelse return error.LostRightPane;
try std.testing.expectEqual(lf.col, rf.col);
- layout.joinCol(p); // no right neighbor left: inert
+ layout.joinCol(p, 0); // no right neighbor left: inert
try std.testing.expectEqual(@as(usize, 1), p.ncol);
}
@@ -1178,6 +1179,8 @@ test "first document fallback splits like Tty from the originating pane" {
defer p.deinit();
const tty = try Pardes.init(std.testing.allocator, .{ .cols = width, .rows = 62, .shells = 3 });
defer tty.deinit();
+ p.settings.placement = .pardes;
+ tty.settings.placement = .pardes;
if (collapsed) {
layout.toggleCollapse(p, from);
layout.toggleCollapse(tty, from);
@@ -1190,8 +1193,8 @@ test "first document fallback splits like Tty from the originating pane" {
// The Look origin need not be the currently active pane.
p.active = (from + 1) % 3;
const free = p.freeSlot().?;
- const doc = try p.openTutorView(free);
- exec.placeDoc(p, from, free, doc);
+ _ = try p.openTutorView(free);
+ exec.placeNew(p, from, from, free, .doc);
_ = exec.spawnTty(tty, from);
p.sync();
tty.sync();
@@ -1214,9 +1217,10 @@ test "first document keeps its originating shell and only opens a column when it
for ([_]u16{ 199, 200 }) |width| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = width, .rows = 32 });
defer p.deinit();
+ p.settings.placement = .pardes;
const shell = p.panes[0].?;
- const doc = try p.openTutorView(1);
- exec.placeDoc(p, 0, 1, doc);
+ _ = try p.openTutorView(1);
+ exec.placeNew(p, 0, 0, 1, .doc);
p.sync();
try std.testing.expectEqual(shell, p.panes[0].?);
try std.testing.expectEqual(@as(usize, 1), p.active);
@@ -1232,6 +1236,167 @@ test "first document keeps its originating shell and only opens a column when it
}
}
+test "acme placement: an empty active column takes the new pane whole" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 32 });
+ defer p.deinit();
+ try std.testing.expectEqual(config.Runtime.Placement.acme, p.settings.placement);
+ p.newColumn(0); // its tag has the keyboard: it is the active column
+ tagline.exitHeader(p);
+ p.sync();
+ // A Look from the shell's text still lands in the active column, as
+ // acme's makenewwindow puts it in activecol first (util.c:456).
+ _ = try p.openTutorView(1);
+ exec.placeNew(p, 0, 0, 1, .doc);
+ try std.testing.expectEqualSlices(usize, &.{1}, p.col_panes[1][0..p.col_n[1]]);
+ try std.testing.expectEqual(@as(usize, 1), p.active);
+ try std.testing.expectEqual(@as(?usize, 1), p.activeColumn());
+ // Never a new column.
+ try std.testing.expectEqual(@as(usize, 2), p.ncol);
+}
+
+test "acme placement: under a pane's text when it has many blank rows, else halving the biggest" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 60 });
+ defer p.deinit();
+ const top = p.active;
+ _ = try p.setTestFile("one\ntwo\nthree\n");
+ p.sync();
+ p.setActiveColumn(0);
+ // Most of the pane is blank: a document looked from it starts under its
+ // three lines (makenewwindow, util.c:482-486).
+ const second = p.freeSlot().?;
+ _ = try p.openTutorView(second);
+ exec.placeNew(p, top, top, second, .doc);
+ p.sync();
+ const text_end = p.bodyTop(p.panes[top].?, p.rects[top]) + 3;
+ try std.testing.expectEqual(text_end, p.rects[second].y);
+ // Fill both with text: no blank space is big, so the biggest pane is
+ // halved.
+ const full = "x\n" ** 80;
+ for ([_]usize{ top, second }) |id| panes.File.setContent(p, &p.panes[id].?.file.?, try p.gpa.dupe(u8, full));
+ p.sync();
+ const big = if (p.rects[second].h >= p.rects[top].h) second else top;
+ const was = p.rects[big];
+ const third = p.freeSlot().?;
+ _ = try p.openTutorView(third);
+ exec.placeNew(p, second, second, third, .doc);
+ p.sync();
+ try std.testing.expectEqual(big, p.col_panes[0][layout.findPane(p, third).?.idx - 1]);
+ try std.testing.expectEqual(was.y + was.h / 2, p.rects[third].y);
+ try std.testing.expectEqual(was.y + was.h, p.rects[third].y + p.rects[third].h);
+}
+
+test "acme placement: New and 9P's new take the bottom half of the last pane" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 40 });
+ defer p.deinit();
+ p.newScratchBelow(p.active);
+ const last = p.active;
+ p.sync();
+ const was = p.rects[last];
+ p.newScratchFor(last, .fs);
+ const made = p.active;
+ p.sync();
+ try std.testing.expectEqual(layout.findPane(p, last).?.idx + 1, layout.findPane(p, made).?.idx);
+ try std.testing.expectEqual(was.y + was.h, p.rects[made].y + p.rects[made].h);
+ try std.testing.expect(p.rects[made].y > was.y + BOX_H);
+}
+
+test "Placement pardes brings back the old rules, and says so in Config" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 40 });
+ defer p.deinit();
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Placement pardes"));
+ try std.testing.expectEqual(config.Runtime.Placement.pardes, p.settings.placement);
+ const top = p.active;
+ p.sync();
+ const was = p.rects[top];
+ p.newScratchBelow(top);
+ p.sync();
+ // Directly under the asking pane, as New always put it.
+ try std.testing.expectEqual(@as(usize, 1), layout.findPane(p, p.active).?.idx);
+ try std.testing.expectEqual(was.y + was.h, p.rects[p.active].y + p.rects[p.active].h);
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Placement"));
+ try std.testing.expectEqual(config.Runtime.Placement.acme, p.settings.placement);
+}
+
+test "an empty column shows its theme's empty_col under its tag: acme's white, else the border" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 20 });
+ defer p.deinit();
+ p.newColumn(0);
+ tagline.exitHeader(p);
+ inline for (.{ "acme", "dark" }) |name| {
+ try std.testing.expect(p.executeBuiltinLine(0, "Theme " ++ name));
+ p.chrome_animation.displayed = colors.ChromeTheme.fromTheme(p.theme());
+ p.sync();
+ var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena.deinit();
+ const s = try p.render(arena.allocator());
+ const cell = s.at(p.col_x[1] + 3, p.screen_h - 2);
+ const want = p.chromeTheme().empty_col;
+ try std.testing.expectEqual(want, cell.style.bg.rgb);
+ if (std.mem.eql(u8, name, "acme")) try std.testing.expectEqual([3]u8{ 0xff, 0xff, 0xff }, want) else try std.testing.expectEqual(p.chromeTheme().border, want);
+ }
+}
+
+test "BootShell keep leaves the untouched boot shell a dragged document joins; replace closes it" {
+ inline for (.{ "keep", "replace" }) |word| {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
+ defer p.deinit();
+ const shell = p.active;
+ try std.testing.expect(p.panes[shell].?.isTerminal());
+ try std.testing.expect(p.executeBuiltinLine(shell, "BootShell " ++ word));
+ _ = try p.openTutorView(1);
+ layout.insert(p, 0, 1, 1);
+ p.sync();
+ exec.evictLonePristineTty(p, 0, 1);
+ p.sync();
+ const kept = std.mem.eql(u8, word, "keep");
+ try std.testing.expectEqual(kept, p.panes[shell] != null);
+ try std.testing.expectEqual(@as(usize, if (kept) 2 else 1), p.col_n[0]);
+ }
+}
+
+test "a pane its whole text fits in never scrolls to keep a margin" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 30 });
+ defer p.deinit();
+ const pane = try p.setTestFile("one\ntwo\nthree\n");
+ // A second pane under it, as acme's placement puts one: right under the
+ // three lines, which leaves the file exactly three body rows.
+ const below = p.freeSlot().?;
+ _ = try p.openTutorView(below);
+ exec.placeNew(p, 0, 0, below, .doc);
+ p.active = 0;
+ p.sync();
+ try std.testing.expectEqual(@as(u16, 3), p.rects[0].h - pane.tag_rows);
+ for ("jj") |c| p.update(.{ .key = .{ .cp = c, .text = &.{c} } });
+ p.sync();
+ try std.testing.expectEqual(@as(i32, 2), pane.body.cur_row);
+ try std.testing.expectEqual(@as(i32, 0), pane.scroll());
+}
+
+test "dragging a column's only pane to another leaves that column in place, empty" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 32 });
+ defer p.deinit();
+ p.newColumn(0);
+ p.newScratchIn(0, p.activeColumn().?);
+ const moved = p.active;
+ p.sync();
+ const weights = p.col_weight;
+ const tag_serial = p.col_serial[1];
+ layout.movePane(p, moved, p.col_x[0] + 4, p.rects[0].y + p.rects[0].h / 2);
+ p.sync();
+ // acme's coldragwin: colclose then coladd (cols.c:516-517); the column
+ // left behind keeps its place, width and tag.
+ try std.testing.expectEqual(@as(usize, 2), p.ncol);
+ try std.testing.expectEqual(@as(usize, 0), p.col_n[1]);
+ try std.testing.expectEqual(tag_serial, p.col_serial[1]);
+ try std.testing.expectEqualDeep(weights, p.col_weight);
+ try std.testing.expectEqual(@as(usize, 2), p.col_n[0]);
+ // ...and a pane dragged into the empty column takes it whole.
+ layout.movePane(p, moved, p.col_x[1] + 4, p.screen_h / 2);
+ p.sync();
+ try std.testing.expectEqualSlices(usize, &.{moved}, p.col_panes[1][0..p.col_n[1]]);
+ try std.testing.expectEqual(@as(usize, 1), p.col_n[0]);
+}
+
test "pane defaults keep creation of scratch panes in column tags" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
@@ -1250,37 +1415,19 @@ test "pane defaults keep creation of scratch panes in column tags" {
try std.testing.expectEqualStrings(tagline.file_pane_tail, tagline.curTail(file));
}
-test "an unsaved file marker sits between its path and builtins until Save" {
+test "an unsaved file's tag is its name alone: the grip, not a mark, says it is unsaved" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 100, .rows = 30 });
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = try p.setTestFile("before\n");
-
const clean = try tagline.tagText(p, p.scratch.allocator(), pane);
- try std.testing.expect(std.mem.indexOf(u8, clean, "/test.txt *") == null);
-
panes.File.setContent(p, &pane.file.?, try gpa.dupe(u8, "after\n"));
const dirty = try tagline.tagText(p, p.scratch.allocator(), pane);
- const marker_at = std.mem.indexOf(u8, dirty, "/test.txt *") orelse return error.MissingDirtyMarker;
- const save_at = std.mem.indexOf(u8, dirty, "Save") orelse return error.MissingSaveBuiltin;
- try std.testing.expect(marker_at < save_at);
-
- try std.testing.expect(p.executeBuiltinLine(0, "Save"));
- while (p.nextEffect()) |effect| p.perform(effect);
- const saved = try tagline.tagText(p, p.scratch.allocator(), pane);
- try std.testing.expect(std.mem.indexOf(u8, saved, "/test.txt *") == null);
-
- panes.File.changed(p, 0, "external\n");
- const reloaded = try tagline.tagText(p, p.scratch.allocator(), pane);
- try std.testing.expect(std.mem.indexOf(u8, reloaded, "/test.txt *") == null);
-
- // A generated output is file-shaped and Save can write it to a path, but
- // there is no file of its own for it to be dirty against.
- pane.file.?.output = .{ .from = .search };
- panes.File.setContent(p, &pane.file.?, try gpa.dupe(u8, "result\n"));
- const output = try tagline.tagText(p, p.scratch.allocator(), pane);
- try std.testing.expect(std.mem.indexOf(u8, output, "/test.txt *") == null);
+ try std.testing.expectEqualStrings(clean, dirty);
+ try std.testing.expect(std.mem.indexOf(u8, dirty, " *") == null);
+ // The dirty file and index's flag still say it.
+ try std.testing.expect(ctlfs.pane.dirtyOf(pane));
}
test "unknown Exec from a document runs as a command pane in its directory, typed into no terminal" {
@@ -1491,6 +1638,11 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe
_ = exec.executeFrom(p, 0, "a = 1\r\nb = \x1b[201~2", true);
Sent.drain(p);
try std.testing.expectEqualStrings("a = 1\rb = [201~2\r", Sent.said(1));
+ // A block left open by its indented last line gets the blank line that
+ // ends it: a second Enter.
+ _ = exec.executeFrom(p, 0, "def f():\n return 1", true);
+ Sent.drain(p);
+ try std.testing.expectEqualStrings("def f():\r return 1\r\r", Sent.said(1));
// Several selections executed at once: each is sent, not the first.
{
const text = try edit.flatSurface(p, &file.body);
@@ -1526,6 +1678,16 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe
Sent.drain(p);
try std.testing.expectEqualStrings("print(2)\r", Sent.said(2));
try std.testing.expect(Sent.len[1] == 0 and file.prompt == .none);
+ // The question is in the log, and a script answers it on the ctl.
+ {
+ const th = @import("ninep/testing.zig");
+ _ = exec.executeFrom(p, 0, "print(5)", true);
+ var want: [32]u8 = undefined;
+ try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "ask {d} repl a b\n", .{file.serial})));
+ const answered = th.wr(p, ctlfs.Node.of(file.serial, .ctl), "answer a\n");
+ try std.testing.expectEqual(ctlfs.Status.ok, answered.reply.status);
+ try std.testing.expectEqualStrings("print(5)\r", answered.pty());
+ }
_ = exec.executeFrom(p, 0, "print(3)", true);
p.update(.{ .key = .{ .cp = Key.escape } });
Sent.drain(p);
@@ -2261,7 +2423,7 @@ test "a save the host could not do leaves the pane dirty" {
const Refusing = struct {
fn writeFile(ctx: ?*anyopaque, pane: u8, _: []const u8, _: []const u8) void {
const core: *Pardes = @ptrCast(@alignCast(ctx.?));
- core.saveFailed(pane, "save", error.PermissionDenied);
+ core.saveFailed(pane, "/x/y.txt", error.PermissionDenied);
}
};
const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 100, .rows = 30 });
@@ -3046,7 +3208,7 @@ test "column tags follow their column through split and close" {
try std.testing.expectEqualStrings("Left Find", tagline.columnTag(p, 0));
p.col_tags[1].own = try p.gpa.dupe(u8, "Right Grep");
p.active = p.col_panes[0][0];
- layout.joinCol(p);
+ layout.joinCol(p, 0);
try std.testing.expectEqual(@as(usize, 1), p.ncol);
try std.testing.expectEqualStrings("Right Grep", tagline.columnTag(p, 0));
}
@@ -3055,7 +3217,8 @@ test "column tags render edit hover and scroll from the column edge" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 20, .tty_only = true });
defer p.deinit();
_ = try p.setTestFile("untouched\n");
- p.newScratchColumn(0);
+ p.newColumn(0);
+ p.newScratchIn(0, p.activeColumn().?);
p.sync();
p.acknowledgePanelPresentation(&.{});
var frame = std.heap.ArenaAllocator.init(std.testing.allocator);
@@ -3111,7 +3274,8 @@ test "column tag first-cell execution keeps New local to the clicked column" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 20, .tty_only = true });
defer p.deinit();
_ = try p.setTestFile("untouched\n");
- p.newScratchColumn(0);
+ p.newColumn(0);
+ p.newScratchIn(0, p.activeColumn().?);
p.sync();
for (0..2) |column| {
p.active = p.col_panes[1 - column][0];
@@ -3147,7 +3311,7 @@ test "header clicks beyond text append and long UTF-8 tags reveal their caret" {
try std.testing.expectEqual(@as(usize, 0), p.header_scroll);
}
-test "a column tag with a newline in it takes a row per line and pushes its panes down" {
+test "a newline typed into a column tag is a space: the tag stays one row" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 50, .rows = 12, .tty_only = true });
defer p.deinit();
_ = try p.setTestFile("body\n");
@@ -3157,26 +3321,9 @@ test "a column tag with a newline in it takes a row per line and pushes its pane
p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = 40, .row = TOPBAR_H } });
p.update(.{ .key = .{ .cp = Key.enter } });
p.update(.{ .key = .{ .cp = 'S', .text = "Snarf" } });
- try std.testing.expectEqualStrings("New\nSnarf", tagline.columnTag(p, 0));
- try std.testing.expectEqual(@as(u16, 2), p.columnBarHeight());
- const frame = try p.render(p.scratch.allocator());
- try std.testing.expectEqual(TOPBAR_H + 2, p.rects[0].y);
- try std.testing.expectEqual(TOPBAR_H + 1, frame.cursor.?.y);
- try std.testing.expectEqual(@as(u8, 'S'), frame.at(TAG_TEXT_INSET, TOPBAR_H + 1).text[0]);
-
- // A click on the second row lands on the second line.
- p.update(.{ .key = .{ .cp = Key.escape } });
- p.acknowledgePanelPresentation(&.{});
- p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET + 1, .row = TOPBAR_H + 1 } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET + 1, .row = TOPBAR_H + 1 } });
- try std.testing.expectEqual(@as(i32, 1), p.col_tags[0].cur_row);
- try std.testing.expectEqual(@as(i32, 1), p.col_tags[0].cur_col);
-
- // Taking the newline back out gives the panes their row again.
- p.update(.{ .key = .{ .cp = Key.home } });
- p.update(.{ .key = .{ .cp = Key.backspace } });
- try std.testing.expectEqualStrings("NewSnarf", tagline.columnTag(p, 0));
_ = try p.render(p.scratch.allocator());
+ try std.testing.expectEqualStrings("New Snarf", tagline.columnTag(p, 0));
+ try std.testing.expectEqual(COLBAR_H, p.columnBarHeight());
try std.testing.expectEqual(TOPBAR_H + COLBAR_H, p.rects[0].y);
}
@@ -3188,7 +3335,7 @@ test "tiny screens keep pane space before adding a column tag" {
_ = try p.render(p.scratch.allocator());
}
-test "ColumnTags reclaims the row and releases hidden header focus without losing commands" {
+test "a screen too short for the column row releases a column tag's focus without losing its commands" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 60, .rows = 12, .tty_only = true });
defer p.deinit();
const pane = try p.setTestFile("unchanged body\n");
@@ -3200,24 +3347,7 @@ test "ColumnTags reclaims the row and releases hidden header focus without losin
p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET, .row = TOPBAR_H } });
try std.testing.expect(p.header_focus and p.header_column != null);
- p.update(.{ .command = "ColumnTags" });
- try std.testing.expect(!p.settings.column_tags);
- try std.testing.expectEqual(@as(u16, 0), p.columnBarHeight());
- try std.testing.expectEqual(TOPBAR_H, p.rects[0].y);
- try std.testing.expectEqual(shown.h + COLBAR_H, p.rects[0].h);
- try std.testing.expect(!p.header_focus and p.header_column == null);
- try std.testing.expect(!p.header_drag);
- try std.testing.expectEqualStrings("New Help", tagline.columnTag(p, 0));
-
- p.update(.{ .command = "ColumnTags" });
- try std.testing.expect(p.settings.column_tags);
- try std.testing.expectEqual(shown, p.rects[0]);
- p.acknowledgePanelPresentation(&.{});
- p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET, .row = TOPBAR_H } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET, .row = TOPBAR_H } });
- try std.testing.expect(p.header_column != null);
p.update(.{ .resize = .{ .cols = 60, .rows = 2 } });
- try std.testing.expect(p.settings.column_tags);
try std.testing.expectEqual(@as(u16, 0), p.columnBarHeight());
try std.testing.expectEqual(TOPBAR_H, p.rects[0].y);
try std.testing.expect(!p.header_focus and p.header_column == null);
@@ -3267,15 +3397,17 @@ test "Enter in a header runs the word under its cursor, as the execute key does"
p.global_tag.cur_col = @intCast(std.mem.indexOf(u8, config.topbar_str, "Newcol").?);
p.update(.{ .key = .{ .cp = Key.enter } });
try std.testing.expectEqual(@as(usize, 2), p.ncol);
- // and the header lets go of the keyboard, as a pane tag does
- try std.testing.expect(!p.header_focus);
+ // and the workspace tag lets go of the keyboard: the new, empty
+ // column's tag has it
+ try std.testing.expect(p.header_focus and p.header_column.? == 1);
}
test "window keys walk from a pane up through the column and workspace tags and back" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 20, .tty_only = true });
defer p.deinit();
_ = try p.setTestFile("body\n");
- p.newScratchColumn(0);
+ p.newColumn(0);
+ p.newScratchIn(0, p.activeColumn().?);
p.sync();
p.active = p.col_panes[0][0];
const window = struct {
@@ -3562,6 +3694,7 @@ test "Newcol refuses an unsplittable restored weight before spawning" {
const pane = p.panes[id].?;
try p.deinitPane(pane);
layout.removePane(p, id);
+ layout.dropColumn(p, id);
p.panes[id] = null;
}
while (p.nextEffect()) |_| {}
@@ -4104,9 +4237,15 @@ pub const Pardes = struct {
look_walk_owner: ?u32 = null,
/// hands out Pane.serial; monotonic, never reused
next_serial: u32 = 0,
- /// The pane (by serial) that joined a column this step; `replaceStillborn`
- /// checks it once the step is over and nothing holds the column's panes.
- stillborn_joiner: ?u32 = null,
+ /// The column whose tag the running command came from, while it runs:
+ /// the column acme's commands take from their Text's col.
+ exec_column: ?usize = null,
+ /// acme's activecol (dat.c:37), by column serial, 0 for none: the
+ /// column last typed in (acme.c:487), clicked in with the select button
+ /// (acme.c:659), dropped into by a grip (acme.c:640), or given a new
+ /// pane (util.c:467). Where the acme placement puts new panes
+ /// (exec.placeNew). A Look click moves the keyboard but not this.
+ active_column: u32 = 0,
settings: config.Runtime = .{ .font = .{ .tagline_percent = config.gui_tagline_font_percent } },
locations_config: locations_config.Config = .{},
locations_cache: @import("locations_cache.zig").Cache = .{},
@@ -4141,6 +4280,13 @@ pub const Pardes = struct {
pointer_tag_hit: ?TagHit = null,
raw_hover_intent: bool = false,
pointer_inside: bool = false,
+ /// The last plain B1 click, for acme's double-click (text.c:881,
+ /// textselect: a second click at the same point within 500 ms).
+ last_click: ?struct { id: usize, on_tag: bool, row: i32, col: i32, ms: i64 } = null,
+ /// A B1 chord's edits share the undo point it took (mouse.zig).
+ chord_hold: bool = false,
+ /// The look just run found nothing (look.zig missed).
+ look_missed: bool = false,
look_hover_wait: ?look.LookHoverWait = null,
look_hover_preview: ?look.LookHoverPreview = null,
pdf_hover_preview: if (pdf_enabled) ?look.PdfWordPreview else void = if (pdf_enabled) null else {},
@@ -4356,10 +4502,12 @@ pub const Pardes = struct {
p.panes[0].?.body.mode = .tty;
p.oneColumn();
// A bare tty is still an editor: it opens with somewhere to put
- // text under the shell. The shell keeps the keyboard.
+ // text under the shell. The shell keeps the keyboard. A boot
+ // layout, like .classic's, not a placement.
if (p.boot == .tty) {
- p.newScratchBelow(0);
- p.active = 0;
+ const np = try p.newScratch(0, 1);
+ layout.insert(p, 0, 1, 1);
+ layout.splitBelow(p, 0, np);
}
},
.classic => {
@@ -4466,24 +4614,21 @@ pub const Pardes = struct {
return a;
}
- /// The workspace tag's height: a row per line of it while the setting
- /// keeps it, zero once a native shell takes its commands over. Layout and
- /// hit testing read this rather than the constant, the way they read
- /// `columnBarHeight`.
+ /// The workspace tag's height: one row while the setting keeps it, as
+ /// acme's row tag is, zero once a native shell takes its commands over.
+ /// Layout and hit testing read this rather than the constant, the way
+ /// they read `columnBarHeight`.
pub fn topBarHeight(p: *const Pardes) u16 {
if (!p.settings.workspace_tag) return 0;
- const lines = if (p.global_tag.own) |own| std.mem.count(u8, own, "\n") + 1 else TOPBAR_H;
- return @intCast(@min(lines, MAX_TAG_ROWS, @max(TOPBAR_H, p.screen_h / 3)));
+ return TOPBAR_H;
}
- /// The column tags' height: a row per line of the tallest one.
+ /// The column tags' height: one row, as acme's (cols.c:244, colresize,
+ /// gives a column tag one font height); a newline in one is a space
+ /// (tagline.flattenHeaders).
pub fn columnBarHeight(p: *const Pardes) u16 {
- if (!p.settings.column_tags or p.screen_h < 3) return 0;
- var lines: usize = COLBAR_H;
- for (p.col_tags[0..p.ncol]) |t| if (t.own) |own| {
- lines = @max(lines, std.mem.count(u8, own, "\n") + 1);
- };
- return @intCast(@min(lines, MAX_TAG_ROWS, @max(COLBAR_H, p.screen_h / 3)));
+ if (p.screen_h < 3) return 0;
+ return COLBAR_H;
}
fn detachCwds(p: *Pardes, parents: []const *Pane) !void {
@@ -4618,34 +4763,56 @@ pub const Pardes = struct {
return np;
}
- /// The empty pane that stands in for one that closed, keeping its column
- /// alive and its directory. Unlike `newScratch` it inherits nothing: the
- /// pane it replaces is on its way out.
- fn newScratchIn(p: *Pardes, free: usize, dir: []const u8) !*Pane {
- const content = try p.gpa.dupe(u8, "");
- errdefer p.gpa.free(content);
- const np = try panes.Output.open(p, free, dir, .{ .cmd = .New }, "", content);
- np.body.cur_pinned = true;
- return np;
- }
-
- /// New: a scratch below the calling pane, in its column.
+ /// New: an empty scratch in `from_id`'s directory, placed as exec.placeNew
+ /// places it.
pub fn newScratchBelow(p: *Pardes, from_id: usize) void {
- const free = p.freeSlot() orelse return;
- const sf = layout.findPane(p, from_id) orelse return;
- const np = p.newScratch(from_id, free) catch return;
- layout.insert(p, sf.col, sf.idx + 1, free);
- layout.splitBelow(p, from_id, np);
- p.active = free;
+ p.newScratchFor(from_id, .scratch);
}
- /// Newcol: a scratch in a fresh column beside the calling pane.
- pub fn newScratchColumn(p: *Pardes, from_id: usize) void {
+ /// A scratch for New (`.scratch`) or 9P's pane/new (`.fs`).
+ pub fn newScratchFor(p: *Pardes, from_id: usize, kind: exec.NewPane) void {
const free = p.freeSlot() orelse return;
- if (!layout.canSplitColumn(p, from_id)) return;
_ = p.newScratch(from_id, free) catch return;
- std.debug.assert(layout.splitColumn(p, from_id, free, false));
- p.active = free;
+ exec.placeNew(p, from_id, from_id, free, kind);
+ }
+
+ /// Newcol: an empty column right of the calling pane's, the keyboard
+ /// on its tag, for the next pane made to fill.
+ pub fn newColumn(p: *Pardes, from_id: usize) void {
+ const col = p.keyboardColumn(from_id) orelse return;
+ const c = layout.insertColumn(p, col, false) orelse return;
+ tagline.enterHeader(p, c);
+ }
+
+ pub fn setActiveColumn(p: *Pardes, c: usize) void {
+ if (c < p.ncol) p.active_column = layout.columnSerial(p, c);
+ }
+
+ /// The active column, while it is still there.
+ pub fn activeColumn(p: *const Pardes) ?usize {
+ return layout.columnBySerial(p, p.active_column);
+ }
+
+ /// The column the keyboard is in: its tag's, or its pane's.
+ pub fn noteTyped(p: *Pardes) void {
+ if (p.header_focus) {
+ if (p.header_column) |c| p.setActiveColumn(c);
+ } else if (layout.findPane(p, p.active)) |f| p.setActiveColumn(f.col);
+ }
+
+ /// New, run from column `c`'s tag.
+ pub fn newScratchIn(p: *Pardes, from_id: usize, c: usize) void {
+ p.exec_column = c;
+ defer p.exec_column = null;
+ p.newScratchBelow(from_id);
+ }
+
+ /// The column a command acts on: the one whose tag it came from, else
+ /// the one whose tag has the keyboard, else `id`'s.
+ pub fn keyboardColumn(p: *Pardes, id: usize) ?usize {
+ if (p.exec_column) |c| return c;
+ if (p.header_focus) if (p.header_column) |c| return c;
+ return if (layout.findPane(p, id)) |f| f.col else null;
}
pub fn freeSlot(p: *Pardes) ?usize {
@@ -4916,15 +5083,23 @@ pub const Pardes = struct {
if (p.host.vtable.write_file) |f| return f(p.host.ctx, pane, path, bytes);
// The in-process filesystem reports the same way a real host does, so
// an OOM here leaves the pane dirty rather than looking saved.
- if (!p.fallback.writeFile(path, bytes)) p.saveFailed(pane, "save", error.OutOfMemory);
+ if (!p.fallback.writeFile(path, bytes)) p.saveFailed(pane, path, error.OutOfMemory);
}
- pub fn saveFailed(p: *Pardes, id: u8, what: []const u8, err: anyerror) void {
+ /// A write of `path` the host could not do: the pane stays dirty, the
+ /// message row says `Save <path>: <why>`, and the 9P write that asked
+ /// for it, waiting on the save, fails with that (`fs.late_failure`).
+ pub fn saveFailed(p: *Pardes, id: u8, path: []const u8, err: anyerror) void {
if (p.panes[id]) |pane| if (pane.file) |*f| {
// The `-%` spelling fs.zig already uses for "make this dirty".
f.saved_revision = f.revision -% 1;
};
- p.reportError(id, what, err);
+ var what: [limits.host_path_cap + 8]u8 = undefined;
+ p.reportError(id, std.fmt.bufPrint(&what, "Save {s}", .{path}) catch "Save", err);
+ const said = if (p.panes[id]) |pane| pane.msg[0..pane.msg_len] else "Save failed";
+ const kept = @import("Messages.zig").clip(said, p.fs.late_failure.len);
+ @memcpy(p.fs.late_failure[0..kept.len], kept);
+ p.fs.late_failure_len = @intCast(kept.len);
}
/// The path a watch is about: a real file's, or a PDF's.
@@ -5091,10 +5266,6 @@ pub const Pardes = struct {
look.cancelLookHover(p);
const reply = ctlfs.handle(p, req);
if (reply.status == .err and (req.op == .write or req.op == .setattr)) ctlfs.events.noteError(p, req, reply);
- // A pane that joined a column holding only a stillborn scratch takes
- // it now, before the answer: one read of index after the look shows
- // the replacement done, and the log has its del with the new.
- exec.replaceStillborn(p);
// The request was a whole step of its own, so it settles the way a
// step does: the cursor and scroll reconciled, the scripted panes
// told, and the panes it made announced to /log now rather than at
@@ -5106,7 +5277,6 @@ pub const Pardes = struct {
}
pub fn update(p: *Pardes, ev: Event) void {
- defer exec.replaceStillborn(p);
// A tick with nothing animating is the one event that cannot change
// the screen.
p.needs_frame = p.needs_frame or ev != .tick;
@@ -5191,8 +5361,11 @@ pub const Pardes = struct {
panes.Terminal.feedOutput(p, pane, o.bytes);
ctlfs.pty.noteMarks(p, o.pane, pane);
},
- .exited => |e| if (p.panes[e.pane]) |pane| if (pane.command != null and !pane.command_done) {
- exec.commandDone(p, e.pane, e.status);
+ .exited => |e| if (p.panes[e.pane]) |pane| {
+ if (pane.command == null)
+ ctlfs.pty.shellExited(p, pane, e.status)
+ else if (!pane.command_done)
+ exec.commandDone(p, e.pane, e.status);
},
.eof => |e| if (p.panes[e.pane]) |pane| if (pane.command != null) {
// Output is over; the command is over when its child exits.
@@ -5236,6 +5409,7 @@ pub const Pardes = struct {
p.pointer_tag_hit = null;
},
.tick => {
+ mouse.edgeScroll(p);
p.chrome_animation.advance();
p.presentation.advance();
look.advanceLookHover(p);
@@ -5320,36 +5494,66 @@ pub const Pardes = struct {
}
}
- fn handleKey(p: *Pardes, key: Key) void {
- if (p.macro.recording != 0 and p.macro.replaying == 0) Macro.append(&p.macro.recorded, p.gpa, key);
- // A question waiting on a pane takes the next key, whatever mode the
- // pane or a header edit is in: that key is its answer.
- for (p.panes, 0..) |slot, id| {
- const asked = slot orelse continue;
- if (asked.prompt == .repl_choice) {
+ /// Answers the question pane `id` asks on its notice band, logged as
+ /// `ask <serial> <what> <choices>`: Del's side (`k` above, `j` below) or
+ /// the REPL to send to (its letter). Null, or a choice not offered,
+ /// takes the question back. A key answers it, or `answer` on the pane's
+ /// ctl; false when the pane asks nothing.
+ pub fn answerQuestion(p: *Pardes, id: usize, choice: ?u8) bool {
+ const asked = p.panes[id] orelse return false;
+ switch (asked.prompt) {
+ .repl_choice => {
asked.prompt = .none;
- const ask = p.repl_ask orelse return;
+ const ask = p.repl_ask orelse return true;
p.repl_ask = null;
defer p.gpa.free(ask.text);
- // Its letter sends it there; anything else takes it back.
- if (key.ctrl or key.alt or key.cp > 'z' or key.cp < 'a') return;
- const serial = ask.offered[key.cp - 'a'];
- if (serial == 0) return;
+ const letter = choice orelse return true;
+ if (letter < 'a' or letter > 'z') return true;
+ const serial = ask.offered[letter - 'a'];
+ if (serial == 0) return true;
// The pane offered, still that REPL and still running: it
// may have closed or been rebound while the question stood.
if (p.paneBySerial(serial)) |to| if (p.panes[to].?.repl) |r|
- if (r.lang == ask.lang and r.letter == key.cp and exec.replLive(p, to))
- return exec.sendToRepl(p, id, to, ask.text);
+ if (r.lang == ask.lang and r.letter == letter and exec.replLive(p, to)) {
+ exec.sendToRepl(p, id, to, ask.text);
+ return true;
+ };
p.setMessage(id, "Repl: the REPL chosen is gone; nothing sent");
- return;
+ },
+ .del_side => {
+ asked.prompt = .none;
+ if (choice == 'k') exec.runBuiltin(p, .Del, id, "", "k");
+ if (choice == 'j') exec.runBuiltin(p, .Del, id, "", "j");
+ },
+ else => return false,
+ }
+ return true;
+ }
+
+ fn handleKey(p: *Pardes, key: Key) void {
+ // A double-click is two clicks with nothing between them.
+ p.last_click = null;
+ if (p.macro.recording != 0 and p.macro.replaying == 0) Macro.append(&p.macro.recorded, p.gpa, key);
+ // A question waiting on a pane takes the next key, whatever mode the
+ // pane or a header edit is in: that key is its answer.
+ for (p.panes, 0..) |slot, id| {
+ const asked = slot orelse continue;
+ switch (asked.prompt) {
+ // Its letter sends it there; anything else takes it back.
+ .repl_choice => _ = p.answerQuestion(id, if (key.ctrl or key.alt or key.cp > 'z' or key.cp < 'a') null else @intCast(key.cp)),
+ .del_side => _ = p.answerQuestion(id, if (hit(key, config.del_above)) 'k' else if (hit(key, config.del_below)) 'j' else null),
+ else => continue,
}
- if (asked.prompt != .del_side) continue;
- asked.prompt = .none;
- if (hit(key, config.del_above)) exec.runBuiltin(p, .Del, id, "", "k");
- if (hit(key, config.del_below)) exec.runBuiltin(p, .Del, id, "", "j");
return;
}
const pane = p.panes[p.active] orelse return;
+ p.noteTyped();
+ // Alt-Down and Alt-Up in a pane's tag expand and collapse it, in
+ // either mode: acme's Tagdown and Tagup (plan9port text.c:776).
+ if (pane.focus == .tag and key.alt and !key.ctrl and (key.cp == Key.down or key.cp == Key.up) and tagline.focusedHeader(p) == null) {
+ pane.tag_expanded = key.cp == Key.down;
+ return;
+ }
// An open prompt takes the keys even when it answers for a header.
const header = if (pane.prompt != .none) null else tagline.focusedHeader(p);
if (header == null and pane.isTerminal() and pane.body.mode == .tty and pane.focus == .body and pane.prompt == .none) {
@@ -5398,16 +5602,11 @@ pub const Pardes = struct {
// global window ops (any mode): Alt-n new terminal below, Alt-c move
// the active terminal into a fresh column
if (hit(key, config.new_shell_below)) {
- tagline.exitHeader(p);
const free = p.freeSlot() orelse return;
const nt = p.newShell(free, "") catch return;
nt.greet = true;
- const src = layout.splitParent(p, p.active);
- const f = layout.findPane(p, src).?;
- layout.insert(p, f.col, f.idx + 1, free);
- layout.splitBelow(p, src, nt);
- p.active = free;
- p.stillborn_joiner = nt.serial;
+ // From a tag, as a 9P new is: acme's t->w == nil.
+ exec.placeNew(p, p.active, if (p.header_focus) null else p.active, free, .shell);
return;
}
for (config.jump_keys) |jk| {
@@ -5486,6 +5685,9 @@ pub const Pardes = struct {
const b = config.wordBounds(shown, modal.selectionCursor(shown, range));
break :word shown[b.lo..b.hi];
};
+ // A column tag's command acts on that column, empty or not.
+ p.exec_column = if (header != null) p.header_column else null;
+ defer p.exec_column = null;
// Out of the tag or header FIRST: Del frees the pane, Kill
// and Restore the whole session.
if (header != null) tagline.exitHeader(p) else pane.focus = .body;
@@ -5950,59 +6152,38 @@ pub const Pardes = struct {
// ---- dump / load (acme-style: `pardes -l state.zon` restores a session) ----
/// Close a pane; its rows go to `to`, or where layout.absorbVWeight
- /// sends them when that is null. Focus on it follows its rows: to the
- /// pane that grew into them, or to the scratch left standing in an
- /// emptied column. Every route that closes a pane comes through here or
- /// removeColumn, so none picks focus for itself, and none by recency.
+ /// sends them when that is null. Focus on it follows its rows to the
+ /// pane that grew into them. A column outlives its panes, as acme's
+ /// does (cols.c:186): closing its last one leaves it empty, the keyboard
+ /// on its tag; only Delcol takes a column away. The session's last pane
+ /// closing ends the session. Every route that closes a pane comes
+ /// through here or removeColumn, so none picks focus for itself, and
+ /// none by recency.
pub fn removePane(p: *Pardes, id: usize, to: ?usize) !void {
const pane = p.panes[id] orelse return;
- // A column outlives its panes. Closing the last one leaves an empty
- // pane standing in its place and in its directory; only Delcol ever
- // takes a column away.
const place = layout.findPane(p, id);
- const last_in_column = if (place) |f| p.col_n[f.col] == 1 else false;
- var dir_buf: [limits.host_path_cap]u8 = undefined;
- var dir: []const u8 = "";
- if (last_in_column) {
- const had = paneDir(pane);
- const n = @min(had.len, dir_buf.len);
- @memcpy(dir_buf[0..n], had[0..n]);
- dir = dir_buf[0..n];
- }
+ const emptied = if (place) |f| (if (p.col_n[f.col] == 1) f.col else null) else null;
// Before anything is torn down, so a failure here changes nothing.
try p.deinitPane(pane);
- // ...and after it, so the stand-in inherits none of the closing pane.
- const heir: ?usize = heir: {
- if (!last_in_column) break :heir null;
- const f = place.?;
- const free = p.freeSlot() orelse break :heir null;
- _ = p.newScratchIn(free, dir) catch break :heir null;
- layout.insert(p, f.col, f.idx + 1, free);
- break :heir free;
- };
const gainer = layout.absorbVWeight(p, id, to);
- const beside = if (last_in_column) p.besideColumn(place.?.col, id) else null;
+ const beside = if (emptied) |c| p.besideColumn(c, p.rects[id]) else null;
layout.removePane(p, id);
p.panes[id] = null;
- if (p.active == id) p.active = heir orelse gainer orelse beside orelse p.anyPane() orelse {
- p.quit = true;
- p.emit(.quit);
- return;
- };
+ if (p.active != id) return;
+ p.active = gainer orelse beside orelse p.anyPane() orelse return p.quitEmpty();
+ if (emptied) |c| if (!p.header_focus) tagline.enterHeader(p, c);
}
- pub fn removeColumn(p: *Pardes, id: usize) !void {
- const place = layout.findPane(p, id) orelse return;
- const had_focus = layout.findPane(p, p.active);
- const beside = if (had_focus != null and had_focus.?.col == place.col)
- p.besideColumn(place.col, p.active)
- else
- null;
+ /// Delcol: close column `col` and every pane in it.
+ pub fn removeColumn(p: *Pardes, col: usize) !void {
+ if (col >= p.ncol) return;
+ const had_focus = p.keyboardColumn(p.active) == col;
+ const beside = if (had_focus) p.besideColumn(col, if (p.header_focus) null else p.rects[p.active]) else null;
var ids: [MAX_PANES]usize = undefined;
var parents: [MAX_PANES]*Pane = undefined;
- const count = p.col_n[place.col];
+ const count = p.col_n[col];
for (0..count) |i| {
- ids[i] = p.col_panes[place.col][i];
+ ids[i] = p.col_panes[col][i];
parents[i] = p.panes[ids[i]].?;
}
try p.detachCwds(parents[0..count]);
@@ -6011,11 +6192,14 @@ pub const Pardes = struct {
layout.removePane(p, closed);
p.panes[closed] = null;
}
- if (p.panes[p.active] == null) p.active = beside orelse p.anyPane() orelse {
- p.quit = true;
- p.emit(.quit);
- return;
- };
+ layout.dropColumn(p, col);
+ if (p.panes[p.active] == null) p.active = beside orelse p.anyPane() orelse return p.quitEmpty();
+ }
+
+ /// No pane is left: the session is over.
+ fn quitEmpty(p: *Pardes) void {
+ p.quit = true;
+ p.emit(.quit);
}
/// The pane Last should fall back to when the jumplist has nothing: the
@@ -6034,14 +6218,13 @@ pub const Pardes = struct {
return null;
}
- /// The pane that takes over `id`'s place when column `col` goes: in the
- /// column layout.removePane gives the width to, the one level with the
- /// middle of `id`. Asked before the column goes, while rects still hold.
- fn besideColumn(p: *const Pardes, col: usize, id: usize) ?usize {
+ /// The pane that takes over the keyboard from column `col`: in the
+ /// column layout.dropColumn gives the width to, the one level with the
+ /// middle of `r` (of the screen without one). Asked while rects hold.
+ fn besideColumn(p: *const Pardes, col: usize, r: ?Rect) ?usize {
if (p.ncol < 2) return null;
const next = if (col > 0) col - 1 else col + 1;
- const r = p.rects[id];
- const mid = r.y + r.h / 2;
+ const mid = if (r) |at| at.y + at.h / 2 else p.screen_h / 2;
var pick: ?usize = null;
for (p.col_panes[next][0..p.col_n[next]]) |k| {
if (p.panes[k] == null) continue;
@@ -6122,6 +6305,7 @@ pub const Pardes = struct {
/// Recompute geometry, push grid-size changes to each emulator + pty, fire
/// deferred greetings. The mirror of the prototype's loop epilogue.
pub fn sync(p: *Pardes) void {
+ tagline.flattenHeaders(p);
if (p.header_column != null and p.columnBarHeight() == 0) {
tagline.exitHeader(p);
}
@@ -6172,11 +6356,30 @@ pub const Pardes = struct {
}
pane.tag_lead = lead;
}
- const lines = std.mem.count(u8, tagline.curTail(pane), "\n") + 1;
- pane.tag_rows = @intCast(@min(lines, MAX_TAG_ROWS, @max(BOX_H, r.h -| 1)));
+ // A row per shown row: the tag's lines wrapped at its width.
+ var wrap_room: [32 * 1024]u8 = undefined;
+ var wrap_fba = std.heap.FixedBufferAllocator.init(&wrap_room);
+ const wa = wrap_fba.allocator();
+ var lines = std.mem.count(u8, tagline.curTail(pane), "\n") + 1;
+ var wanted = lines;
+ var caret_row: usize = @intCast(@max(0, pane.tag.cur_row));
+ if (tagline.prefixIn(wa, pane)) |prefix| {
+ if (std.mem.concat(wa, u8, &.{ prefix, tagline.curTail(pane) })) |whole| {
+ if (tagline.wrapTag(wa, whole, tagline.tagWidth(p, id))) |shown| {
+ lines = shown.rows();
+ // Collapsed, one row, as acme's Tagup sets taglines to 1
+ // (plan9port text.c:786).
+ wanted = if (pane.tag_expanded) lines else 1;
+ const cur = modal.offsetAt(whole, .{ .row = caret_row, .col = @intCast(@max(0, pane.tag.cur_col)) });
+ caret_row = std.mem.count(u8, shown.text[0..@min(shown.fromReal(cur), shown.text.len)], "\n");
+ } else |_| {}
+ } else |_| {}
+ } else |_| {}
+ if (!pane.tag_expanded) wanted = 1;
+ pane.tag_rows = @intCast(@min(wanted, MAX_TAG_ROWS, @max(BOX_H, r.h -| 1)));
if (pane.collapsed) pane.tag_rows = BOX_H;
if (pane.focus == .tag and pane.prompt != .name) {
- const cur: usize = @intCast(@max(0, pane.tag.cur_row));
+ const cur: usize = caret_row;
var top: usize = pane.tag_top;
if (cur < top) top = cur;
if (cur >= top + pane.tag_rows) top = cur + 1 - pane.tag_rows;
@@ -6191,6 +6394,7 @@ pub const Pardes = struct {
panes.Terminal.resizeGrid(pane, p.gpa, cols, rows);
p.shell_rows.markStale(pane); // reflow moved every row
p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } });
+ pane.fs.winsize = null;
}
pane.cols = cols;
pane.rows = rows;
@@ -6660,43 +6864,67 @@ test "a prompt is typed on its own line in the notice band and leaves the tag ro
try std.testing.expect(pane.prompt == .none and pane.input.own == null);
}
-test "a stillborn pane gives its column to the first pane that joins it" {
+test "Newcol makes an empty column, and the next pane made from its tag fills it" {
if (platform == .web) return;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 40 });
defer p.deinit();
- try std.testing.expect(p.executeBuiltinLine(0, "Newcol"));
- const blank = p.active;
- p.sync(); // Tty splits by the rects a frame has laid out
- try std.testing.expect(p.executeBuiltinLine(blank, "Tty"));
+ const first = p.active;
+ const panes_before = p.col_n[0];
+ try std.testing.expect(p.executeBuiltinLine(first, "Newcol"));
+ try std.testing.expectEqual(@as(usize, 2), p.ncol);
+ try std.testing.expectEqual(@as(usize, 0), p.col_n[1]);
+ try std.testing.expectEqual(panes_before, p.col_n[0]);
+ // The keyboard is on the new column's tag; no pane has it.
+ try std.testing.expect(p.header_focus and p.header_column.? == 1);
+ try std.testing.expectEqual(first, p.active);
+ p.sync();
+ try std.testing.expect(p.executeBuiltinLine(first, "Tty"));
const shell = p.active;
- p.update(.tick); // the step ends: the placeholder goes
- try std.testing.expect(p.panes[blank] == null);
- try std.testing.expectEqual(@as(usize, 1), p.col_n[layout.findPane(p, shell).?.col]);
- try std.testing.expectEqual(shell, p.active);
+ try std.testing.expect(shell != first);
+ try std.testing.expectEqual(@as(usize, 1), p.col_n[1]);
+ try std.testing.expectEqual(shell, p.col_panes[1][0]);
+ try std.testing.expect(!p.header_focus);
- // Typed into, body or tag, it is a pane like any other.
- try std.testing.expect(p.executeBuiltinLine(shell, "Newcol"));
- const typed = p.active;
+ // The next Tty splits as it always has: nothing is empty any more.
p.sync();
- for ("ix") |c| p.update(.{ .key = .{ .cp = c, .text = &.{c} } });
- try std.testing.expect(p.executeBuiltinLine(typed, "Tty"));
- p.update(.tick);
- try std.testing.expect(p.panes[typed] != null);
- try std.testing.expectEqual(@as(usize, 2), p.col_n[layout.findPane(p, typed).?.col]);
+ try std.testing.expect(p.executeBuiltinLine(shell, "Tty"));
+ try std.testing.expectEqual(@as(usize, 2), p.col_n[1]);
+}
- try std.testing.expect(p.executeBuiltinLine(shell, "Newcol"));
- const tagged = p.active;
+test "a column tag's command acts on that column" {
+ if (platform == .web) return;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 40 });
+ defer p.deinit();
+ const first = p.active;
+ try std.testing.expect(p.executeBuiltinLine(first, "Newcol"));
+ try std.testing.expect(p.executeBuiltinLine(first, "Newcol"));
+ try std.testing.expectEqual(@as(usize, 3), p.ncol);
p.sync();
- try p.panes[tagged].?.appendTag(" make");
- try std.testing.expect(p.executeBuiltinLine(tagged, "Tty"));
- p.update(.tick);
- try std.testing.expect(p.panes[tagged] != null);
+ // New from the middle column's tag lands in it, not below `first`.
+ p.exec_column = 1;
+ try std.testing.expect(p.executeBuiltinLine(first, "New"));
+ p.exec_column = null;
+ try std.testing.expectEqual(@as(usize, 1), p.col_n[1]);
+ try std.testing.expectEqual(p.active, p.col_panes[1][0]);
+ // Delcol from the empty right column's tag closes that one.
+ p.exec_column = 2;
+ try std.testing.expect(p.executeBuiltinLine(first, "Delcol"));
+ p.exec_column = null;
+ try std.testing.expectEqual(@as(usize, 2), p.ncol);
+ try std.testing.expectEqual(@as(usize, 1), p.col_n[1]);
+ // A pane's own Delcol closes its column whatever tag has the keyboard.
+ try std.testing.expect(p.executeBuiltinLine(first, "Newcol"));
+ try std.testing.expect(p.header_focus);
+ try std.testing.expect(p.executeBuiltinLine(p.col_panes[1][0], "Delcol"));
+ try std.testing.expectEqual(@as(usize, 2), p.ncol);
+ try std.testing.expectEqual(@as(usize, 0), p.col_n[1]);
}
test "Del from a key asks which neighbour takes the rows; nothing else asks" {
if (platform == .web) return;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 40 });
defer p.deinit();
+ p.settings.placement = .pardes; // the neighbours below are what is under test
var frame = std.heap.ArenaAllocator.init(std.testing.allocator);
defer frame.deinit();
const top = p.active;
@@ -6759,6 +6987,28 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" {
try std.testing.expectEqual(top, p.active);
try std.testing.expectEqual(h[2], p.rects[low].h);
+ // A script answers the question on the pane's ctl, as the log's `ask`
+ // names it: `answer -` takes it back, a side closes the pane toward it.
+ {
+ const th = @import("ninep/testing.zig");
+ p.newScratchBelow(top);
+ const asked = p.active;
+ p.sync();
+ const serial = p.panes[asked].?.serial;
+ const ctl = ctlfs.Node.of(serial, .ctl);
+ try std.testing.expectEqual(ctlfs.E.INVAL, th.wr(p, ctl, "answer k\n").errno());
+ p.update(.{ .key = .{ .cp = ' ' } });
+ p.update(.{ .key = .{ .cp = 'd' } });
+ var want: [32]u8 = undefined;
+ try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "ask {d} del k j\n", .{serial})));
+ try std.testing.expectEqual(ctlfs.Status.ok, th.wr(p, ctl, "answer -\n").reply.status);
+ try std.testing.expect(p.panes[asked] != null and p.panes[asked].?.prompt == .none);
+ p.update(.{ .key = .{ .cp = ' ' } });
+ p.update(.{ .key = .{ .cp = 'd' } });
+ try std.testing.expectEqual(ctlfs.Status.ok, th.wr(p, ctl, "answer k\n").reply.status);
+ try std.testing.expect(p.paneBySerial(serial) == null);
+ }
+
// Bare Del from anything but a key closes at once and gives the rows up,
// as it always has.
p.newScratchBelow(top);
@@ -6793,7 +7043,8 @@ test "focus on a closed pane goes to the pane that takes its place, not the one
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 40 });
defer p.deinit();
const shell = p.active;
- p.newScratchColumn(shell);
+ p.newColumn(shell);
+ p.newScratchIn(shell, p.activeColumn().?);
const right = p.active;
p.sync();
p.newScratchBelow(shell);
@@ -6825,45 +7076,50 @@ test "focus on a closed pane goes to the pane that takes its place, not the one
p.sync();
p.active = left_low;
p.sync();
- try p.removeColumn(left_low);
+ try p.removeColumn(layout.findPane(p, left_low).?.col);
p.sync();
try std.testing.expectEqual(right_low, p.active);
}
-test "closing the last pane in a column leaves an empty one in its place" {
+test "closing the last pane in a column leaves the column empty, its tag focused" {
if (platform == .web) return;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 24 });
defer p.deinit();
const shell = p.active;
- p.newScratchColumn(shell);
+ p.newColumn(shell);
+ p.newScratchIn(shell, p.activeColumn().?);
const lone = p.active;
try std.testing.expect(lone != shell);
try std.testing.expectEqual(@as(usize, 2), p.ncol);
const at = layout.findPane(p, lone).?;
try std.testing.expectEqual(@as(usize, 1), p.col_n[at.col]);
- const had = try std.testing.allocator.dupe(u8, p.panes[lone].?.cwdSlice());
- defer std.testing.allocator.free(had);
+ p.sync();
try p.removePane(lone, null);
p.sync();
- // The column outlives the pane: still two columns, and the one that was
- // emptied holds a fresh empty pane in the same directory.
+ // The column outlives the pane, as acme's does: still two columns, the
+ // emptied one with no pane and the keyboard on its tag.
try std.testing.expectEqual(@as(usize, 2), p.ncol);
- try std.testing.expectEqual(@as(usize, 1), p.col_n[at.col]);
- const heir = p.col_panes[at.col][0];
- try std.testing.expect(heir != lone);
+ try std.testing.expectEqual(@as(usize, 0), p.col_n[at.col]);
try std.testing.expect(p.panes[lone] == null);
- const hp = p.panes[heir].?;
- try std.testing.expectEqualStrings("", hp.file.?.content);
- try std.testing.expectEqualStrings(had, hp.cwdSlice());
- try std.testing.expectEqual(heir, p.active);
+ try std.testing.expect(p.header_focus and p.header_column.? == at.col);
+ try std.testing.expectEqual(shell, p.active);
try std.testing.expect(!p.quit);
+ // Down from it goes nowhere: nothing is below.
+ try std.testing.expect(p.executeBuiltinLine(shell, "Down"));
+ try std.testing.expect(p.header_focus and p.header_column.? == at.col);
+
// ...and Delcol is what actually takes the column away.
- try p.removeColumn(heir);
+ try p.removeColumn(at.col);
p.sync();
try std.testing.expectEqual(@as(usize, 1), p.ncol);
+ try std.testing.expectEqual(shell, p.active);
+
+ // The session's last pane closing ends it.
+ try p.removePane(shell, null);
+ try std.testing.expect(p.quit);
}
test "the bare tty startup opens a text pane under the shell, unfocused" {
@@ -7029,6 +7285,7 @@ test "hopping between two panes does not grow the jump stack" {
if (platform == .web) return;
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .cols = 80, .rows = 24, .file = "mise.toml" });
+ p.settings.placement = .pardes; // two panes of room, not acme's fit to the doc's two lines
defer p.deinit();
p.update(.{ .resize = .{ .cols = 80, .rows = 24 } });
p.update(.{ .key = .{ .cp = 'n', .alt = true } }); // a shell under the doc
diff --git a/src/pdf_view.zig b/src/pdf_view.zig
index 13c02231..58dd2c3c 100644
--- a/src/pdf_view.zig
+++ b/src/pdf_view.zig
@@ -2045,7 +2045,7 @@ pub fn openSections(core: *pardes.Pardes, id: usize) void {
.serial = result.serial,
.revision = result.file.?.revision,
};
- exec.placeDoc(core, id, free, result);
+ exec.placeNew(core, id, id, free, .doc);
layout.compute(core);
core.active = id;
pane.search_pane = free;
diff --git a/src/regexp.zig b/src/regexp.zig
index fe68585d..249763c2 100644
--- a/src/regexp.zig
+++ b/src/regexp.zig
@@ -41,7 +41,12 @@ pub const Regex = struct {
/// patterns take 45-80 ms, and a Debug build is ten times slower.
pub const budget: u64 = if (builtin.mode == .Debug) 4_000_000 else 32_000_000;
- pub fn compile(pat: []const u8) error{Bad}!Regex {
+ pub const e_anchor = "in a pattern with \\n, ^ can only come first and $ only just before a \\n";
+
+ /// `Anchor`: a pattern that names a newline has `^` other than first,
+ /// or `$` other than just before a `\n`, which mvzr would read as the
+ /// ends of the whole text and so never match where sam would.
+ pub fn compile(pat: []const u8) error{ Bad, Anchor }!Regex {
if (pat.len == 0) return error.Bad;
var buf: [256]u8 = undefined;
var len: usize = 0;
@@ -62,12 +67,22 @@ pub const Regex = struct {
i += 1;
piece = pat[i - 1 .. i + 1];
if (pat[i] == 'n') spans = true;
+ // mvzr slices two hex digits after `\x` without looking,
+ // so a short one panics it: refuse it here.
+ if (pat[i] == 'x' and (i + 2 >= pat.len or
+ !std.ascii.isHex(pat[i + 1]) or !std.ascii.isHex(pat[i + 2]))) return error.Bad;
} else if (in_class) {
in_class = c != ']';
} else if (c == '[') {
in_class = true;
} else if (c == '.' and spans) {
piece = "[^\\n]";
+ } else if (spans and c == '^' and i != 0) {
+ return error.Anchor;
+ } else if (spans and c == '$') {
+ // `x$\n` is `x\n`; any other `$` would be the text's end.
+ if (std.mem.startsWith(u8, pat[i + 1 ..], "\\n")) continue;
+ return error.Anchor;
}
if (!emit) continue;
if (len + piece.len > buf.len) return error.Bad;
@@ -91,6 +106,20 @@ pub const Regex = struct {
mvzr.steps_left = rx.steps;
mvzr.exhausted = false;
defer rx.steps = mvzr.steps_left;
+ // A `^` pattern that spans lines: mvzr's `^` is its haystack's start,
+ // so each line start from `from` on is tried as one.
+ // ponytail: a search per line start, each to `hi`; the step budget
+ // bounds it.
+ if (rx.spans and rx.bol) {
+ var s = if (from == 0 or text[from - 1] == '\n') from else (std.mem.indexOfScalarPos(u8, text[0..hi], from, '\n') orelse return null) + 1;
+ while (s <= last and s <= hi) {
+ const hit = rx.re.match(text[s..hi]);
+ if (mvzr.exhausted) return error.TooSlow;
+ if (hit) |m| if (m.start == 0) return .{ .start = s, .end = s + m.end };
+ s = (std.mem.indexOfScalarPos(u8, text[0..hi], s, '\n') orelse return null) + 1;
+ }
+ return null;
+ }
var start: usize = if (rx.spans) 0 else if (std.mem.lastIndexOfScalar(u8, text[0..from], '\n')) |nl| nl + 1 else 0;
var at = from - start;
// `^` cannot match in the middle of a line.
@@ -136,6 +165,20 @@ test "lines are haystacks: ^ and $ at each line, . never a newline, \\n spans li
try std.testing.expectEqual(c.end, m.end);
}
_ = try Regex.compile("a.*a\\nq");
+ // `^` at every line start and `$` before a newline, when a pattern
+ // spans lines; anywhere else they are refused, never silently wrong.
+ const defs = "x = 1\ndef a\n\ndef b\n";
+ var def = try Regex.compile("^def .*\\n");
+ const d = (try def.find(defs, 0, defs.len, defs.len)).?;
+ try std.testing.expectEqual(@as(usize, 6), d.start);
+ try std.testing.expectEqual(@as(usize, 12), d.end);
+ try std.testing.expectEqual(@as(usize, 13), (try def.find(defs, 7, defs.len, defs.len)).?.start);
+ var blank = try Regex.compile("^\\n");
+ try std.testing.expectEqual(@as(usize, 12), (try blank.find(defs, 0, defs.len, defs.len)).?.start);
+ var dollar = try Regex.compile("1$\\n");
+ try std.testing.expectEqual(@as(usize, 4), (try dollar.find(defs, 0, defs.len, defs.len)).?.start);
+ try std.testing.expectError(error.Anchor, Regex.compile("(^|\\n)def"));
+ try std.testing.expectError(error.Anchor, Regex.compile("a$\\nb$"));
var none = try Regex.compile("zzz");
try std.testing.expect(try none.find(text, 0, text.len, text.len) == null);
try std.testing.expectError(error.Bad, Regex.compile(""));
@@ -185,3 +228,10 @@ test "the match is the leftmost, however long the line" {
try std.testing.expectEqual(@as(usize, 2), f.start);
try std.testing.expectEqual(@as(usize, 7), f.end);
}
+
+test "a \\x without two hex digits is refused, not handed to mvzr to panic on" {
+ for ([_][]const u8{ "\\x", "a\\x1", "\\x1b[\\x", "[\\x]", "\\xg1" }) |pat|
+ try std.testing.expectError(error.Bad, Regex.compile(pat));
+ var rx = try Regex.compile("\\x41");
+ try std.testing.expectEqual(@as(usize, 1), (try rx.find("xA", 0, 1, 2)).?.start);
+}
diff --git a/src/sam_edit.zig b/src/sam_edit.zig
new file mode 100644
index 00000000..679823da
--- /dev/null
+++ b/src/sam_edit.zig
@@ -0,0 +1,717 @@
+//! acme's Edit (editors/acme/edit.c, ecmd.c, elog.c): sam's command
+//! language over one pane's body. The commands are parsed as edit.c parses
+//! them, run as ecmd.c runs them against the text as it was before the Edit,
+//! and their changes collected in order, as elog.c logs them, then applied
+//! at once: an Edit is one undo step, and one that fails changes nothing.
+//!
+//! Addresses are the `addr` file's (src/ninep/addr.zig), so they take
+//! pardes's `line:col` too; regular expressions are src/regexp.zig's, mvzr
+//! searched as sam searches, with its step budget.
+//!
+//! Left out: the file commands (`b B D e r w f X Y`), the pipes (`< | >`),
+//! the `'` and `"` addresses, and `\1`..`\9` in `s` (mvzr has no
+//! submatches). `u` undoes or redoes only as a whole Edit. acme warns about
+//! changes out of sequence (elog.c:130) and applies them anyway; here the
+//! Edit is refused.
+const std = @import("std");
+const regexp = @import("regexp.zig");
+const addr_lang = @import("ninep/addr.zig");
+
+pub const Range = addr_lang.Range;
+
+pub const Op = struct { q0: u32, q1: u32, text: []const u8 };
+
+/// What an Edit came to: the changes in order, the dot it leaves (in the
+/// text before them), and what `p` and `=` printed.
+pub const Result = struct {
+ ops: []const Op,
+ dot: Range,
+ printed: []const u8,
+ /// A bare `u`/`u-3`: undo (positive) or redo (negative) that many.
+ undo: i32 = 0,
+};
+
+pub const Failure = error{ Edit, OutOfMemory };
+
+/// Why an Edit failed, in acme's words, when `run` answered error.Edit.
+pub const Why = struct {
+ buf: [200]u8 = undefined,
+ len: usize = 0,
+ pub fn text(w: *const Why) []const u8 {
+ return w.buf[0..w.len];
+ }
+};
+
+const Cmd = struct {
+ c: u8,
+ addr: ?[]const u8 = null,
+ re: ?[]const u8 = null,
+ text: []const u8 = "",
+ sub: ?*Cmd = null,
+ next: ?*Cmd = null,
+ num: i32 = 0,
+ global: bool = false,
+ to: ?[]const u8 = null,
+};
+
+/// Runs `command` over `text` from `dot`, the file named `name` (for `=`).
+/// Everything returned lives in `arena`.
+pub fn run(arena: std.mem.Allocator, text: []const u8, dot: Range, name: []const u8, command: []const u8, why: *Why) Failure!Result {
+ var src = try arena.alloc(u8, command.len + 1);
+ @memcpy(src[0..command.len], command);
+ src[command.len] = '\n';
+ if (command.len > 0 and command[command.len - 1] == '\n') src = src[0..command.len];
+ var ps: Parser = .{ .arena = arena, .src = src, .why = why };
+ var first: ?*Cmd = null;
+ var last: ?*Cmd = null;
+ while (try ps.parse(0)) |c| {
+ if (last) |l| l.next = c else first = c;
+ last = c;
+ }
+ const cmds = first orelse return .{ .ops = &.{}, .dot = dot, .printed = "" };
+ // acme's u runs at once (ecmd.c:532), outside the edit log; here it is
+ // an Edit of its own.
+ if (cmds.c == 'u') {
+ if (cmds.next != null) return fail(why, "u is only an Edit of its own here", .{});
+ return .{ .ops = &.{}, .dot = dot, .printed = "", .undo = cmds.num };
+ }
+ var ex: Exec = .{ .arena = arena, .text = text, .name = name, .why = why };
+ var d = dot;
+ var it: ?*Cmd = cmds;
+ while (it) |c| : (it = c.next) d = try ex.exec(c, d);
+ return .{ .ops = ex.ops.items, .dot = d, .printed = ex.out.items };
+}
+
+/// Whether `command` stops inside a `{` group or an `a`, `c` or `i` text
+/// block still waiting for its `.` line: a writer of lines (a ctl, exec)
+/// hands an Edit the lines after it until it does not.
+pub fn needsMore(arena: std.mem.Allocator, command: []const u8) bool {
+ var why: Why = .{};
+ const src = std.fmt.allocPrint(arena, "{s}\n", .{command}) catch return false;
+ var ps: Parser = .{ .arena = arena, .src = src, .why = &why };
+ while (true) {
+ const c = ps.parse(0) catch return std.mem.eql(u8, why.text(), "unmatched `{'") or ps.open_text;
+ if (c == null) return ps.open_text;
+ }
+}
+
+fn fail(why: *Why, comptime fmt: []const u8, args: anytype) Failure {
+ why.len = if (std.fmt.bufPrint(&why.buf, fmt, args)) |s| s.len else |_| 0;
+ return error.Edit;
+}
+
+// ---- parsing: edit.c ----
+
+const Parser = struct {
+ arena: std.mem.Allocator,
+ src: []const u8,
+ i: usize = 0,
+ /// The last pattern given, for an empty one (edit.c:562, getregexp).
+ lastpat: []const u8 = "",
+ /// The last `parse` that answered null met a `}`, not the end.
+ braced: bool = false,
+ /// A text block of `a`, `c` or `i` ran out before its `.` line.
+ open_text: bool = false,
+ why: *Why,
+
+ fn getch(ps: *Parser) ?u8 {
+ if (ps.i >= ps.src.len) return null;
+ ps.i += 1;
+ return ps.src[ps.i - 1];
+ }
+
+ fn nextc(ps: *const Parser) ?u8 {
+ return if (ps.i < ps.src.len) ps.src[ps.i] else null;
+ }
+
+ fn skipbl(ps: *Parser) ?u8 {
+ while (ps.nextc()) |c| {
+ if (c != ' ' and c != '\t') return c;
+ ps.i += 1;
+ }
+ return null;
+ }
+
+ fn atnl(ps: *Parser) Failure!void {
+ _ = ps.skipbl();
+ const c = ps.getch() orelse '\n';
+ if (c != '\n') return fail(ps.why, "newline expected (saw {c})", .{c});
+ }
+
+ fn okdelim(ps: *Parser, c: u8) Failure!void {
+ if (c == '\\' or std.ascii.isAlphanumeric(c)) return fail(ps.why, "bad delimiter {c}", .{c});
+ }
+
+ /// edit.c:216: a count, 1 when there is none; `-` allowed when signok > 1.
+ fn getnum(ps: *Parser, signok: u8) i32 {
+ var sign: i32 = 1;
+ if (signok > 1 and ps.nextc() == '-') {
+ sign = -1;
+ ps.i += 1;
+ }
+ const c = ps.nextc() orelse return sign;
+ if (c < '0' or c > '9') return sign;
+ var n: i32 = 0;
+ while (ps.nextc()) |d| {
+ if (d < '0' or d > '9') break;
+ n = n *| 10 +| (d - '0');
+ ps.i += 1;
+ }
+ return sign * n;
+ }
+
+ /// An address's text, blanks dropped, or null: the syntax of edit.c's
+ /// compoundaddr and simpleaddr, evaluated later by addr.zig.
+ fn address(ps: *Parser) Failure!?[]const u8 {
+ var out: std.ArrayList(u8) = .empty;
+ while (true) {
+ const c = ps.skipbl() orelse break;
+ switch (c) {
+ '0'...'9', '#', '.', '$', '+', '-', ',', ';' => {
+ ps.i += 1;
+ try out.append(ps.arena, c);
+ // pardes's line:col
+ if (c >= '0' and c <= '9' and ps.nextc() == ':' and ps.i + 1 < ps.src.len and
+ ps.src[ps.i + 1] >= '0' and ps.src[ps.i + 1] <= '9')
+ {
+ ps.i += 1;
+ try out.append(ps.arena, ':');
+ }
+ },
+ '/', '?' => {
+ ps.i += 1;
+ try out.append(ps.arena, c);
+ while (ps.nextc()) |d| {
+ if (d == '\n') break;
+ ps.i += 1;
+ try out.append(ps.arena, d);
+ if (d == '\\') {
+ if (ps.nextc()) |e| if (e != '\n') {
+ ps.i += 1;
+ try out.append(ps.arena, e);
+ };
+ continue;
+ }
+ if (d == c) break;
+ }
+ },
+ '\'' => return fail(ps.why, "can't handle '", .{}),
+ '"' => return fail(ps.why, "file addresses are not supported", .{}),
+ else => break,
+ }
+ }
+ return if (out.items.len == 0) null else out.items;
+ }
+
+ /// edit.c:562: up to the delimiter; `\delim` is the delimiter, `\\` stays.
+ fn getregexp(ps: *Parser, delim: u8) Failure![]const u8 {
+ var buf: std.ArrayList(u8) = .empty;
+ while (true) {
+ var c = ps.getch() orelse break;
+ if (c == '\\') {
+ if (ps.nextc() == delim) {
+ c = ps.getch().?;
+ } else if (ps.nextc() == '\\') {
+ try buf.append(ps.arena, c);
+ c = ps.getch().?;
+ }
+ } else if (c == delim or c == '\n') {
+ if (c == '\n') ps.i -= 1;
+ break;
+ }
+ try buf.append(ps.arena, c);
+ }
+ if (buf.items.len > 0) ps.lastpat = buf.items;
+ if (ps.lastpat.len == 0) return fail(ps.why, "no regular expression defined", .{});
+ return ps.lastpat;
+ }
+
+ /// edit.c:388: `\n` is a newline; for `s` every other escape is kept
+ /// for it to read (`\&`, `\\`).
+ fn getrhs(ps: *Parser, delim: u8, cmd: u8) Failure![]const u8 {
+ var s: std.ArrayList(u8) = .empty;
+ while (ps.getch()) |c0| {
+ var c = c0;
+ if (c == delim or c == '\n') {
+ ps.i -= 1;
+ break;
+ }
+ if (c == '\\') {
+ c = ps.getch() orelse return fail(ps.why, "bad right hand side", .{});
+ if (c == '\n') {
+ ps.i -= 1;
+ c = '\\';
+ } else if (c == 'n') {
+ c = '\n';
+ } else if (c != delim and (cmd == 's' or c != '\\')) {
+ try s.append(ps.arena, '\\');
+ }
+ }
+ try s.append(ps.arena, c);
+ }
+ return s.items;
+ }
+
+ /// edit.c:425: `/text/` on the line, or lines up to one that is `.`.
+ fn collecttext(ps: *Parser) Failure![]const u8 {
+ if (ps.skipbl() == '\n') {
+ ps.i += 1;
+ var s: std.ArrayList(u8) = .empty;
+ while (ps.i < ps.src.len) {
+ const end = std.mem.indexOfScalarPos(u8, ps.src, ps.i, '\n') orelse ps.src.len;
+ const line = ps.src[ps.i..end];
+ ps.i = @min(end + 1, ps.src.len);
+ if (std.mem.eql(u8, line, ".")) return s.items;
+ try s.appendSlice(ps.arena, line);
+ try s.append(ps.arena, '\n');
+ }
+ ps.open_text = true;
+ return s.items;
+ }
+ const delim = ps.getch() orelse return "";
+ try ps.okdelim(delim);
+ const s = try ps.getrhs(delim, 'a');
+ if (ps.nextc() == delim) ps.i += 1;
+ try ps.atnl();
+ return s;
+ }
+
+ /// edit.c:467, parsecmd.
+ fn parse(ps: *Parser, nest: u32) Failure!?*Cmd {
+ const a = try ps.address();
+ _ = ps.skipbl() orelse return null;
+ const c = ps.getch() orelse return null;
+ const cmd = try ps.arena.create(Cmd);
+ cmd.* = .{ .c = c, .addr = a };
+ switch (c) {
+ '\n' => {},
+ 'a', 'c', 'i' => cmd.text = try ps.collecttext(),
+ 'd', 'p' => try ps.atnl(),
+ '=' => {
+ _ = ps.skipbl();
+ const start = ps.i;
+ while (ps.nextc()) |d| : (ps.i += 1) if (d == '\n') break;
+ cmd.text = std.mem.trimEnd(u8, ps.src[start..ps.i], " \t");
+ ps.i = @min(ps.i + 1, ps.src.len);
+ if (cmd.text.len != 0 and !std.mem.eql(u8, cmd.text, "#")) return fail(ps.why, "newline expected", .{});
+ },
+ 's' => {
+ cmd.num = ps.getnum(1);
+ _ = ps.skipbl();
+ const delim = ps.getch() orelse '\n';
+ if (delim == '\n') return fail(ps.why, "no address", .{});
+ try ps.okdelim(delim);
+ cmd.re = try ps.getregexp(delim);
+ cmd.text = try ps.getrhs(delim, 's');
+ if (ps.nextc() == delim) {
+ ps.i += 1;
+ if (ps.nextc() == 'g') {
+ ps.i += 1;
+ cmd.global = true;
+ } else if (ps.nextc()) |d| if (d >= '1' and d <= '9') {
+ // `s/a/b/2`, the count after, as some write it for
+ // sam's `s2/a/b/`.
+ cmd.num = ps.getnum(1);
+ };
+ }
+ try ps.atnl();
+ },
+ 'x', 'y', 'g', 'v' => {
+ // x without a pattern is every line (ecmd.c:568, linelooper).
+ const n = ps.nextc();
+ if (c != 'x' or (n != ' ' and n != '\t' and n != '\n')) {
+ _ = ps.skipbl();
+ const delim = ps.getch() orelse '\n';
+ if (delim == '\n') return fail(ps.why, "no address", .{});
+ try ps.okdelim(delim);
+ cmd.re = try ps.getregexp(delim);
+ }
+ if (ps.skipbl() == '\n') {
+ ps.i += 1;
+ const p = try ps.arena.create(Cmd);
+ p.* = .{ .c = 'p' };
+ cmd.sub = p;
+ } else {
+ cmd.sub = (try ps.parse(nest)) orelse return fail(ps.why, "defcmd", .{});
+ }
+ },
+ 'm', 't' => {
+ cmd.to = (try ps.address()) orelse return fail(ps.why, "bad address", .{});
+ try ps.atnl();
+ },
+ 'u' => {
+ if (a != null) return fail(ps.why, "command takes no address", .{});
+ cmd.num = ps.getnum(2);
+ try ps.atnl();
+ },
+ '{' => {
+ var last: ?*Cmd = null;
+ while (true) {
+ if (ps.skipbl() == '\n') ps.i += 1;
+ ps.braced = false;
+ const next = (try ps.parse(nest + 1)) orelse {
+ // sam reads on for the `}`; an Edit has no more to read.
+ if (!ps.braced) return fail(ps.why, "unmatched `{{'", .{});
+ break;
+ };
+ if (last) |l| l.next = next else cmd.sub = next;
+ last = next;
+ }
+ },
+ '}' => {
+ ps.braced = true;
+ try ps.atnl();
+ if (nest == 0) return fail(ps.why, "right brace with no left brace", .{});
+ return null;
+ },
+ 'b', 'B', 'D', 'e', 'r', 'w', 'f', 'X', 'Y', '<', '|', '>' => return fail(ps.why, "{c} is not supported in pardes", .{c}),
+ else => return fail(ps.why, "unknown command {c}", .{c}),
+ }
+ return cmd;
+ }
+};
+
+// ---- running: ecmd.c, logging: elog.c ----
+
+const Exec = struct {
+ arena: std.mem.Allocator,
+ text: []const u8,
+ name: []const u8,
+ why: *Why,
+ ops: std.ArrayList(Op) = .empty,
+ out: std.ArrayList(u8) = .empty,
+ nest: u32 = 0,
+
+ fn clip(n: usize) u32 {
+ return std.math.cast(u32, n) orelse std.math.maxInt(u32);
+ }
+
+ /// An address from `dot`, or `dot` when there is none (aDot).
+ fn address(ex: *Exec, expr: ?[]const u8, dot: Range) Failure!Range {
+ const e = expr orelse return dot;
+ var a: addr_lang.Addr = .{ .text = ex.text, .lim = null, .expr = e };
+ const r = a.address(dot) orelse return fail(ex.why, "{s}", .{a.err});
+ if (a.i < e.len) return fail(ex.why, "bad address syntax", .{});
+ if (r.q0 > r.q1) return fail(ex.why, "addresses out of order", .{});
+ return r;
+ }
+
+ /// elog.c's order: each change at or after the end of the one before,
+ /// since they all address the text as it was.
+ fn log(ex: *Exec, q0: u32, q1: u32, text: []const u8) Failure!void {
+ if (q0 == q1 and text.len == 0) return;
+ if (ex.ops.items.len > 0) {
+ const prev = ex.ops.items[ex.ops.items.len - 1];
+ if (q0 < prev.q1 or q0 < prev.q0) return fail(ex.why, "changes out of sequence", .{});
+ }
+ try ex.ops.append(ex.arena, .{ .q0 = q0, .q1 = q1, .text = text });
+ }
+
+ fn compile(ex: *Exec, pat: []const u8, c: u8) Failure!regexp.Regex {
+ return regexp.Regex.compile(pat) catch |err| switch (err) {
+ error.Anchor => fail(ex.why, "{s}", .{regexp.Regex.e_anchor}),
+ error.Bad => fail(ex.why, "bad regexp in {c} command", .{c}),
+ };
+ }
+
+ fn find(ex: *Exec, rx: *regexp.Regex, from: usize, hi: usize) Failure!?regexp.Regex.Match {
+ if (from > hi) return null;
+ return rx.find(ex.text, from, hi, hi) catch fail(ex.why, "{s}", .{addr_lang.e_slow});
+ }
+
+ /// ecmd.c:62, cmdexec: runs `c` from `dot`, answering the dot it leaves.
+ fn exec(ex: *Exec, c: *Cmd, dot: Range) Failure!Range {
+ const r = try ex.address(c.addr, dot);
+ switch (c.c) {
+ '\n' => return r,
+ 'a' => {
+ try ex.log(r.q1, r.q1, c.text);
+ return .{ .q0 = r.q1, .q1 = r.q1 };
+ },
+ 'i' => {
+ try ex.log(r.q0, r.q0, c.text);
+ return .{ .q0 = r.q0, .q1 = r.q0 };
+ },
+ 'c' => {
+ try ex.log(r.q0, r.q1, c.text);
+ return .{ .q0 = r.q0, .q1 = r.q0 };
+ },
+ 'd' => {
+ try ex.log(r.q0, r.q1, "");
+ return .{ .q0 = r.q0, .q1 = r.q0 };
+ },
+ 'p' => {
+ try ex.out.appendSlice(ex.arena, ex.text[r.q0..r.q1]);
+ return r;
+ },
+ '=' => {
+ // ecmd.c:665, printposn
+ if (c.text.len > 0) {
+ try ex.out.print(ex.arena, "{s}:#{d}", .{ ex.name, r.q0 });
+ if (r.q1 != r.q0) try ex.out.print(ex.arena, ",#{d}", .{r.q1});
+ } else {
+ const l1 = 1 + std.mem.count(u8, ex.text[0..r.q0], "\n");
+ var l2 = l1 + std.mem.count(u8, ex.text[r.q0..r.q1], "\n");
+ if (r.q1 > r.q0 and ex.text[r.q1 - 1] == '\n') l2 -= 1;
+ try ex.out.print(ex.arena, "{s}:{d}", .{ ex.name, l1 });
+ if (l2 != l1) try ex.out.print(ex.arena, ",{d}", .{l2});
+ }
+ try ex.out.append(ex.arena, '\n');
+ return r;
+ },
+ 's' => return ex.substitute(c, r),
+ 'x', 'y' => {
+ const ranges = if (c.re) |pat| try ex.looper(pat, r, c.c == 'x') else try ex.lines(r);
+ ex.nest += 1;
+ defer ex.nest -= 1;
+ var d = r;
+ for (ranges) |each| d = try ex.exec(c.sub.?, each);
+ return d;
+ },
+ 'g', 'v' => {
+ var rx = try ex.compile(c.re.?, c.c);
+ const hit = (try ex.find(&rx, r.q0, r.q1)) != null;
+ if (hit != (c.c == 'v')) return ex.exec(c.sub.?, r);
+ return r;
+ },
+ 'm', 't' => {
+ const to = try ex.address(c.to, dot);
+ const bytes = ex.text[r.q0..r.q1];
+ if (c.c == 't') {
+ try ex.log(to.q1, to.q1, bytes);
+ } else if (r.q1 <= to.q0) {
+ // ecmd.c:408, move
+ try ex.log(r.q0, r.q1, "");
+ try ex.log(to.q1, to.q1, bytes);
+ } else if (r.q0 >= to.q1) {
+ try ex.log(to.q1, to.q1, bytes);
+ try ex.log(r.q0, r.q1, "");
+ } else if (!(r.q0 == to.q0 and r.q1 == to.q1)) {
+ return fail(ex.why, "move overlaps itself", .{});
+ }
+ return r;
+ },
+ 'u' => return fail(ex.why, "u is only an Edit of its own here", .{}),
+ '{' => {
+ var it = c.sub;
+ while (it) |each| : (it = each.next) _ = try ex.exec(each, r);
+ return r;
+ },
+ else => return fail(ex.why, "unknown command {c}", .{c.c}),
+ }
+ }
+
+ /// ecmd.c:443, s_cmd.
+ fn substitute(ex: *Exec, c: *Cmd, r: Range) Failure!Range {
+ var rx = try ex.compile(c.re.?, 's');
+ var n = c.num;
+ var op: ?usize = null;
+ var p1: usize = r.q0;
+ var did = false;
+ while (p1 <= r.q1) {
+ const m = (try ex.find(&rx, p1, r.q1)) orelse break;
+ if (m.start == m.end) {
+ if (op != null and m.start == op.?) {
+ p1 += 1;
+ continue;
+ }
+ p1 = m.end + 1;
+ } else p1 = m.end;
+ op = m.end;
+ n -= 1;
+ if (n > 0) continue;
+ var buf: std.ArrayList(u8) = .empty;
+ var i: usize = 0;
+ while (i < c.text.len) : (i += 1) {
+ const ch = c.text[i];
+ if (ch == '\\' and i + 1 < c.text.len) {
+ i += 1;
+ const e = c.text[i];
+ if (e >= '1' and e <= '9') return fail(ex.why, "no \\{c}: mvzr keeps no submatches", .{e});
+ try buf.append(ex.arena, e);
+ } else if (ch == '&') {
+ try buf.appendSlice(ex.arena, ex.text[m.start..m.end]);
+ } else try buf.append(ex.arena, ch);
+ }
+ try ex.log(clip(m.start), clip(m.end), buf.items);
+ did = true;
+ if (!c.global) break;
+ }
+ if (!did and ex.nest == 0) return fail(ex.why, "no substitution", .{});
+ return r;
+ }
+
+ /// ecmd.c:796, looper: the matches (x) or what lies between them (y).
+ fn looper(ex: *Exec, pat: []const u8, r: Range, xy: bool) Failure![]Range {
+ var rx = try ex.compile(pat, if (xy) 'x' else 'y');
+ var out: std.ArrayList(Range) = .empty;
+ var op: ?usize = if (xy) null else r.q0;
+ var p: usize = r.q0;
+ while (p <= r.q1) {
+ var tr: Range = undefined;
+ var sel_end: usize = undefined;
+ if (try ex.find(&rx, p, r.q1)) |m| {
+ if (m.start == m.end) {
+ if (op != null and m.start == op.?) {
+ p += 1;
+ continue;
+ }
+ p = m.end + 1;
+ } else p = m.end;
+ tr = if (xy) .{ .q0 = clip(m.start), .q1 = clip(m.end) } else .{ .q0 = clip(op.?), .q1 = clip(m.start) };
+ sel_end = m.end;
+ } else {
+ // no match, but y still takes what is left
+ if (xy or op.? > r.q1) break;
+ tr = .{ .q0 = clip(op.?), .q1 = r.q1 };
+ p = @as(usize, r.q1) + 1;
+ sel_end = r.q1;
+ }
+ op = sel_end;
+ try out.append(ex.arena, tr);
+ }
+ return out.items;
+ }
+
+ /// ecmd.c:840, linelooper: each line in the range, the first from where
+ /// the range starts, the last cut where it ends.
+ fn lines(ex: *Exec, r: Range) Failure![]Range {
+ var out: std.ArrayList(Range) = .empty;
+ var p: usize = r.q0;
+ while (p < r.q1) {
+ const nl = std.mem.indexOfScalarPos(u8, ex.text, p, '\n');
+ const e = @min(if (nl) |i| i + 1 else ex.text.len, r.q1);
+ if (e <= p) break;
+ try out.append(ex.arena, .{ .q0 = clip(p), .q1 = clip(e) });
+ p = e;
+ }
+ return out.items;
+ }
+};
+
+/// The text after `ops`, which are in order and do not overlap.
+pub fn apply(gpa: std.mem.Allocator, text: []const u8, ops: []const Op) ![]u8 {
+ var n: usize = text.len;
+ for (ops) |o| n = n - (o.q1 - o.q0) + o.text.len;
+ const out = try gpa.alloc(u8, n);
+ var at: usize = 0;
+ var from: usize = 0;
+ for (ops) |o| {
+ const keep = text[from..o.q0];
+ @memcpy(out[at..][0..keep.len], keep);
+ at += keep.len;
+ @memcpy(out[at..][0..o.text.len], o.text);
+ at += o.text.len;
+ from = o.q1;
+ }
+ @memcpy(out[at..], text[from..]);
+ return out;
+}
+
+/// Where `dot` lands after `ops`: each end moved by the changes before it;
+/// an empty dot where a change starts selects what it put there, as
+/// elogapply does (elog.c:283).
+pub fn moveDot(dot: Range, ops: []const Op) Range {
+ var shift: i64 = 0;
+ for (ops) |o| {
+ if (dot.q0 == dot.q1 and o.q0 == dot.q0 and (o.text.len > 0)) {
+ const q: u32 = @intCast(@as(i64, dot.q0) + shift);
+ return .{ .q0 = q, .q1 = q + @as(u32, @intCast(o.text.len)) };
+ }
+ if (o.q1 > dot.q0) break;
+ shift += @as(i64, @intCast(o.text.len)) - @as(i64, o.q1 - o.q0);
+ }
+ const q0: u32 = @intCast(@as(i64, dot.q0) + shift);
+ var shift1: i64 = 0;
+ for (ops) |o| {
+ if (o.q1 > dot.q1 or (o.q0 == o.q1 and o.q0 == dot.q1)) break;
+ shift1 += @as(i64, @intCast(o.text.len)) - @as(i64, o.q1 - o.q0);
+ }
+ const q1: u32 = @intCast(@max(@as(i64, q0), @as(i64, dot.q1) + shift1));
+ return .{ .q0 = q0, .q1 = q1 };
+}
+
+fn edited(text: []const u8, command: []const u8) ![]u8 {
+ var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena_state.deinit();
+ var why: Why = .{};
+ const res = run(arena_state.allocator(), text, .{}, "t", command, &why) catch |err| {
+ if (err == error.Edit) std.debug.print("Edit: {s}\n", .{why.text()});
+ return err;
+ };
+ return apply(std.testing.allocator, text, res.ops);
+}
+
+fn expectEdit(text: []const u8, command: []const u8, want: []const u8) !void {
+ const got = try edited(text, command);
+ defer std.testing.allocator.free(got);
+ try std.testing.expectEqualStrings(want, got);
+}
+
+test "sam's classic commands" {
+ try expectEdit("foo x foo y foo\n", ",x/foo/c/foobar/", "foobar x foobar y foobar\n");
+ try expectEdit("a b\nc d\n", ",x/ /c/_/", "a_b\nc_d\n");
+ // As in acme (ecmd.c:810), y also takes the empty stretch after the last match.
+ try expectEdit("one\ntwo\nthree\n", ",y/\\n/c/X/", "X\nX\nX\nX");
+ try expectEdit("aaa\n", ",s/a/b/g", "bbb\n");
+ try expectEdit("aaa\n", ",s/a/b/", "baa\n");
+ try expectEdit("aaa\n", ",s2/a/b/", "aba\n");
+ try expectEdit("aaa\n", ",s/a/b/2", "aba\n");
+ try expectEdit("cat dog\n", ",s/[a-z]+/<&>/g", "<cat> <dog>\n");
+ // x within x, g within x.
+ try expectEdit("int a;\nfloat b;\nint c;\n", ",x/.*\\n/g/^int/x/[a-z];/c/X;/", "int X;\nfloat b;\nint X;\n");
+ try expectEdit("keep 1\ndrop 2\nkeep 3\n", ",x g/drop/d", "keep 1\nkeep 3\n");
+ try expectEdit("keep 1\ndrop 2\nkeep 3\n", ",x v/keep/d", "keep 1\nkeep 3\n");
+ // Lines, a/i, grouping, m and t.
+ try expectEdit("a\nb\n", ",x a/;/", "a\n;b\n;");
+ try expectEdit("a\nb\n", "1 i/>/", ">a\nb\n");
+ try expectEdit("abc\n", "/b/{\ni/[/\na/]/\n}", "a[b]c\n");
+ try expectEdit("one\ntwo\n", "1 m $", "two\none\n");
+ try expectEdit("one\ntwo\n", "1 t $", "one\ntwo\none\n");
+ try expectEdit("one\ntwo\nthree\n", "2 d", "one\nthree\n");
+ // ^ at every line start in a pattern that spans lines (dogfood round 5).
+ try expectEdit("x = 1\ndef a\ndef b\n", ",x/^def .*\\n/a/X\\n/", "x = 1\ndef a\nX\ndef b\nX\n");
+ // pardes's line:col.
+ try expectEdit("one\ntwo\n", "2:2 i/-/", "one\nt-wo\n");
+}
+
+test "an Edit that fails halfway changes nothing, and says why in acme's words" {
+ var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena_state.deinit();
+ for ([_][2][]const u8{
+ .{ ",x/foo/c/bar/\n/nomatch/d", "no match for regexp" },
+ .{ ",s/zzz/y/", "no substitution" },
+ .{ "k", "unknown command k" },
+ .{ ",x/a/{\na/1/\ni/2/\n}", "changes out of sequence" },
+ .{ "}", "right brace with no left brace" },
+ .{ ",x/foo/{", "unmatched `{'" },
+ .{ ",x/foo/{\nd", "unmatched `{'" },
+ .{ "w /tmp/x", "w is not supported in pardes" },
+ .{ ",s/(a)/\\1/", "no \\1: mvzr keeps no submatches" },
+ .{ "1 m 1,2", "move overlaps itself" },
+ .{ ",x/(^|\\n)foo/d", "in a pattern with \\n, ^ can only come first and $ only just before a \\n" },
+ }) |c| {
+ var why: Why = .{};
+ try std.testing.expectError(error.Edit, run(arena_state.allocator(), "foo a\nfoo\n", .{}, "t", c[0], &why));
+ try std.testing.expectEqualStrings(c[1], why.text());
+ }
+}
+
+test "an Edit wants the lines after it until its braces and text blocks close" {
+ var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena_state.deinit();
+ const a = arena_state.allocator();
+ try std.testing.expect(needsMore(a, ",x/foo/{"));
+ try std.testing.expect(needsMore(a, ",x/foo/{\ni/[/"));
+ try std.testing.expect(!needsMore(a, ",x/foo/{\ni/[/\n}"));
+ try std.testing.expect(needsMore(a, "$a\nline one"));
+ try std.testing.expect(!needsMore(a, "$a\nline one\n."));
+ try std.testing.expect(!needsMore(a, ",s/{/x/"));
+ try std.testing.expect(!needsMore(a, ",x/foo/c/bar/"));
+}
+
+test "the dot an Edit leaves selects what a change put where it stood" {
+ const ops = [_]Op{.{ .q0 = 2, .q1 = 4, .text = "XYZ" }};
+ try std.testing.expectEqual(Range{ .q0 = 2, .q1 = 5 }, moveDot(.{ .q0 = 2, .q1 = 2 }, &ops));
+ try std.testing.expectEqual(Range{ .q0 = 6, .q1 = 7 }, moveDot(.{ .q0 = 5, .q1 = 6 }, &ops));
+}
diff --git a/src/surface.zig b/src/surface.zig
index bc0a019d..15de128f 100644
--- a/src/surface.zig
+++ b/src/surface.zig
@@ -423,7 +423,9 @@ pub const Chrome = struct {
/// The canonical interface: what a frame of pardes IS. The tty shell writes
/// these cells to vaxis one-to-one; the SDL shells rasterize them.
pub const Surface = struct {
- pub const PointerShape = enum(u8) { arrow, link, target };
+ /// `box` is acme's boxcursor, shown while a pane or a column is carried
+ /// by its grip (cols.c:485, rows.c:151).
+ pub const PointerShape = enum(u8) { arrow, link, target, box };
pointer_shape: PointerShape = .arrow,
body_layers: [MAX_PANES]Layer = @splat(.{}),
tag_layers: [MAX_TAG_LAYERS]Layer = @splat(.{}),
diff --git a/src/tagline.zig b/src/tagline.zig
index d0907cbd..68fa2645 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -23,6 +23,8 @@ pub const context_pane_tail = file_pane_tail ++ " TreeContext";
pub const locations_pane_tail = file_pane_tail ++ " LocationsConfig";
pub const terminal_pane_tail = " " ++ config.terminal_pane_builtins_str;
pub const command_pane_tail = " " ++ config.command_pane_builtins_str;
+/// The mark a tag carried after an unsaved file's name before the grip
+/// showed it; a dump from then still has it in its saved tags.
const dirty_marker = " *";
/// A file with changes it has not saved.
@@ -140,12 +142,9 @@ pub fn pathPrefix(arena: std.mem.Allocator, pane: *Pane) ![]const u8 {
var idbuf: [48]u8 = undefined;
return std.fmt.allocPrint(arena, "{s} {s}", .{ pane.cwdSlice(), exec.replId(&idbuf, pane) });
}
- if (pane.file) |f| {
- const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path;
- if (dirty(pane))
- return std.fmt.allocPrint(arena, "{s}{s}", .{ name, dirty_marker });
- return name;
- }
+ // Unsaved text shows on the grip, as acme's modbutton does, not as a
+ // mark in the text: the prefix is the name alone.
+ if (pane.file) |f| return if (pane.prompt == .name) pane.input.own orelse f.path else f.path;
return pane.cwdSlice();
}
@@ -213,13 +212,6 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void {
t.cur_row = @intCast(cursor.row);
t.cur_col = @intCast(cursor.col);
t.cur_pinned = true;
- // Reveal the suffix together with its commands, not merely the
- // first command letter at the viewport's far right edge.
- if (p.paneIdOf(pane)) |id| {
- const line = whole[0 .. std.mem.indexOfScalar(u8, whole, '\n') orelse whole.len];
- const width = p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET);
- pane.tag_scroll = @intCast(panes.File.displayWidth(line) + 1 -| width);
- }
}
// The text may have changed since (a 9P write, a rename), so the
// remembered cursor is pulled back inside it. It is the cursor that
@@ -239,12 +231,83 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void {
pane.focus = .tag;
}
-/// Where display column `col` of the tag's shown row `row` (tag scroll
-/// included) puts its cursor: the tag as shown, prefix and all.
+/// A pane's tag as it is shown: its lines, each wrapped onto more rows at
+/// the tag's width as acme's frame wraps a tag at its right edge (plan9port
+/// acme wind.c:140, wintaglines, counts the frame's lines). `soft` are the
+/// offsets in `text` of the newlines the wrapping put in.
+pub const Shown = struct {
+ text: []u8,
+ soft: []const usize,
+
+ /// The tag's own offset of a shown one.
+ pub fn toReal(s: Shown, off: usize) usize {
+ var n: usize = 0;
+ for (s.soft) |b| {
+ if (b < off) n += 1 else break;
+ }
+ return off - n;
+ }
+
+ /// The shown offset of the tag's own one.
+ pub fn fromReal(s: Shown, real: usize) usize {
+ var off = real;
+ for (s.soft) |b| {
+ if (b <= off) off += 1 else break;
+ }
+ return off;
+ }
+
+ pub fn rows(s: Shown) usize {
+ return modal.cursorLineCount(s.text);
+ }
+};
+
+/// `whole` wrapped at `width` display columns.
+pub fn wrapTag(arena: std.mem.Allocator, whole: []const u8, width: usize) !Shown {
+ var out: std.ArrayList(u8) = .empty;
+ var soft: std.ArrayList(usize) = .empty;
+ const w = @max(1, width);
+ var lines = std.mem.splitScalar(u8, whole, '\n');
+ var first = true;
+ while (lines.next()) |line| {
+ if (!first) try out.append(arena, '\n');
+ first = false;
+ var rest = line;
+ while (panes.File.displayWidth(rest) > w) {
+ var cut = modal.graphemeStart(rest, panes.File.rawAtDisplay(rest, w));
+ if (cut == 0) cut = modal.nextGrapheme(rest, 0);
+ try out.appendSlice(arena, rest[0..cut]);
+ try soft.append(arena, out.items.len);
+ try out.append(arena, '\n');
+ rest = rest[cut..];
+ }
+ try out.appendSlice(arena, rest);
+ }
+ return .{ .text = out.items, .soft = soft.items };
+}
+
+/// The tag's width for wrapping: its columns, one kept for the caret past
+/// a full row's end.
+pub fn tagWidth(p: *const Pardes, id: usize) usize {
+ return @max(1, @as(usize, p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET)) -| 1);
+}
+
+pub fn shownTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) !Shown {
+ const whole = try tagText(p, arena, pane);
+ const id = p.paneIdOf(pane) orelse return .{ .text = whole, .soft = &.{} };
+ return wrapTag(arena, whole, tagWidth(p, id));
+}
+
+/// Where display column `col` of the tag's shown row `row` puts its
+/// cursor: the tag as shown, prefix and all, wrapped rows included.
pub fn tagOffsetAt(p: *Pardes, pane: *Pane, row: usize, col: usize) ?modal.Cursor {
+ const arena = p.scratch.allocator();
+ const shown = shownTag(p, arena, pane) catch return null;
+ const whole = tagText(p, arena, pane) catch return null;
const at = row + pane.tag_top;
- const line = modal.lineSlice(tagText(p, p.scratch.allocator(), pane) catch return null, at);
- return .{ .row = at, .col = modal.graphemeStart(line, panes.File.rawAtDisplay(line, col)) };
+ const line = modal.lineSlice(shown.text, at);
+ const off = modal.lineStartOffset(shown.text, at) + modal.graphemeStart(line, panes.File.rawAtDisplay(line, col));
+ return modal.positionAt(whole, @min(shown.toReal(off), whole.len));
}
/// Whether display column `col` of the tag's shown row `row` is on a name
@@ -327,6 +390,7 @@ pub fn enterHeader(p: *Pardes, column: ?usize) void {
if (p.panes[p.active]) |pane| pane.focus = .body;
p.header_focus = true;
p.header_column = column;
+ if (column) |c| p.setActiveColumn(c);
const t = focusedHeader(p) orelse return exitHeader(p);
const text = edit.flatSurface(p, t) catch "";
const rows = modal.cursorLineCount(text);
@@ -459,3 +523,51 @@ pub fn headerSelection(t: *const Text, text: []const u8, row: usize) ?struct { l
if (hi <= start or lo >= end) return null;
return .{ .lo = @max(lo, start) - start, .hi = @min(hi, end) - start };
}
+
+/// Column and workspace tags are one line, as acme's: a newline typed,
+/// pasted or restored from a dump into one becomes a space, the caret and
+/// the selection kept on the same characters.
+pub fn flattenHeaders(p: *Pardes) void {
+ for (p.col_tags[0..p.ncol]) |*t| flatten(t);
+ flatten(&p.global_tag);
+}
+
+fn flatten(t: *Text) void {
+ const own = t.own orelse return;
+ if (std.mem.indexOfScalar(u8, own, '\n') == null) return;
+ const cur = modal.offsetAt(own, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) });
+ const anchor = modal.offsetAt(own, .{ .row = @intCast(@max(0, t.vsel.row)), .col = @intCast(@max(0, t.vsel.col)) });
+ std.mem.replaceScalar(u8, own, '\n', ' ');
+ t.cur_row = 0;
+ t.cur_col = @intCast(cur);
+ t.vsel.row = 0;
+ t.vsel.col = @intCast(anchor);
+ t.nsel = 0;
+}
+
+test "a column or workspace tag is one line: a newline in it is a space" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 50, .rows = 12, .tty_only = true });
+ defer p.deinit();
+ p.col_tags[0].own = try p.gpa.dupe(u8, "New\nSnarf");
+ p.col_tags[0].cur_row = 1;
+ p.col_tags[0].cur_col = 2;
+ p.global_tag.own = try p.gpa.dupe(u8, "Newcol\nExit");
+ p.sync();
+ try std.testing.expectEqualStrings("New Snarf", columnTag(p, 0));
+ try std.testing.expectEqualStrings("Newcol Exit", p.global_tag.own.?);
+ try std.testing.expectEqual(@as(i32, 0), p.col_tags[0].cur_row);
+ try std.testing.expectEqual(@as(i32, 6), p.col_tags[0].cur_col);
+ try std.testing.expectEqual(pardes.COLBAR_H, p.columnBarHeight());
+ try std.testing.expectEqual(pardes.TOPBAR_H, p.topBarHeight());
+}
+
+test "a pane tag wraps a long line onto rows, and offsets map both ways" {
+ var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena_state.deinit();
+ const shown = try wrapTag(arena_state.allocator(), "/a/b.txt Del Snarf Undo Put\nMore", 10);
+ try std.testing.expectEqualStrings("/a/b.txt D\nel Snarf U\nndo Put\nMore", shown.text);
+ try std.testing.expectEqual(@as(usize, 4), shown.rows());
+ try std.testing.expectEqual(@as(usize, 10), shown.toReal(11)); // the 'e' of Del
+ try std.testing.expectEqual(@as(usize, 11), shown.fromReal(10));
+}
+
diff --git a/src/themes/acme.zig b/src/themes/acme.zig
index 17ef4224..621ffa23 100644
--- a/src/themes/acme.zig
+++ b/src/themes/acme.zig
@@ -35,6 +35,8 @@ pub const theme = .{
.lineno = .{ 0x99, 0x99, 0x80 },
.scroll_track = .{ 0x99, 0x99, 0x4c },
.scroll_thumb = .{ 0xff, 0xff, 0xea },
+ // acme paints a column with no window white (cols.c:187).
+ .empty_col = .{ 0xff, 0xff, 0xff },
.border = .{ 0x00, 0x00, 0x00 },
.tag_rule = .{ 0x88, 0x88, 0xcc },
.rule_px = 2,
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 822c2d7f..96aec9a9 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -1050,12 +1050,13 @@ const Shell = struct {
return true;
}
_ = libc.close(pt.file.handle);
- // Unwatched, a command's exit is read here, as its end.
+ // Unwatched, a command's exit is read here, as its end,
+ // and a shell's, for a run waiting on it and the log.
const command = if (core.panes[e.id]) |pn| pn.command != null else false;
- const status = if (command) host_io.exitStatus(pt.pid, 100) else null;
+ const status = host_io.exitStatus(pt.pid, 100);
if (status == null) host_io.retireShell(pt.pid);
s.ptys[e.id] = null;
- if (command) core.update(.{ .exited = .{ .pane = @intCast(e.id), .status = status } });
+ if (command or status != null) core.update(.{ .exited = .{ .pane = @intCast(e.id), .status = status } });
}
core.update(.{ .eof = .{ .pane = @intCast(e.id) } });
},
@@ -1272,7 +1273,7 @@ const Shell = struct {
const s = of(ctx);
// A command whose exit is recorded is reaped: its group may be
// another's by now.
- if (s.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig);
+ if (s.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig, pt.cmd.watched);
}
/// The pane is gone: hang its pty up, which the kernel passes on to the
@@ -1314,7 +1315,7 @@ const Shell = struct {
fn writeFile(ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void {
const s = of(ctx);
filesystem.write(s.core, path, bytes) catch |err|
- return s.core.saveFailed(pane, "save", err);
+ return s.core.saveFailed(pane, path, err);
if (s.core.panes[pane]) |pn| if (pn.file) |f| if (std.mem.eql(u8, f.path, path)) {
if (s.watches[pane]) |*w| if (w.serial == pn.serial) switch (w.generation) {
.text => w.generation = .{ .text = std.hash.Wyhash.hash(0, bytes) },
diff --git a/src/web/app.mjs b/src/web/app.mjs
index 03829178..c053d663 100644
--- a/src/web/app.mjs
+++ b/src/web/app.mjs
@@ -42,7 +42,8 @@ export class DomRenderer {
render() {
const count = this.wasm.pardes_frame();
- this.root.style.cursor = this.wasm.pardes_pointer_shape() === 1 ? "pointer" : "text";
+ const shape = this.wasm.pardes_pointer_shape();
+ this.root.style.cursor = shape === 1 ? "pointer" : shape === 3 ? "move" : "text";
const cols = this.wasm.pardes_frame_cols();
const rows = this.wasm.pardes_frame_rows();
if (count !== cols * rows) throw new Error(`bad frame: ${count} cells for ${cols}x${rows}`);
diff --git a/test/appearance_gallery.py b/test/appearance_gallery.py
index 48c0ea1b..bc9a7570 100644
--- a/test/appearance_gallery.py
+++ b/test/appearance_gallery.py
@@ -78,8 +78,8 @@ def run(args):
launch=launch, inherited=inherited, terminal=handles) as (client, _):
source = next(serial for serial, tag in panes(client).items() if str(source_path) in tag)
before = set(panes(client))
- command(client, source, 'Newcol')
- right = require(wait_for(lambda: next((s for s in panes(client) if s not in before), None)), 'right column missing')
+ command(client, source, 'Newcol') # empty, its tag with the keyboard
+ right = int(client.read('/pane/new'))
client.write(f'/pane/{right}/name', f'{root / "Review"}\n'.encode())
client.write(f'/pane/{right}/body', PREFERENCE_NOTES if args.theme_selector else NOTES, truncate=True)
client.write(f'/pane/{right}/dirty', b'0\n')
diff --git a/test/column_drag.zig b/test/column_drag.zig
index 3aea4286..89508800 100644
--- a/test/column_drag.zig
+++ b/test/column_drag.zig
@@ -46,7 +46,6 @@ test "column grip drag moves only its interval and keeps tag caret with the colu
defer p.deinit();
_ = try p.setTestFile("body\n");
p.presentation.enabled = false;
- p.settings.column_tags = true;
p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 };
for (0..3) |_| {
try std.testing.expect(p.executeBuiltinLine(p.active, "New"));
@@ -127,7 +126,6 @@ test "column grip is separate from compact text and the resize seam" {
defer p.deinit();
_ = try p.setTestFile("body\n");
p.presentation.enabled = false;
- p.settings.column_tags = true;
p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 };
try std.testing.expect(p.executeBuiltinLine(0, "New"));
try std.testing.expect(layout.splitColumn(p, p.active, p.active, false));
@@ -181,7 +179,6 @@ test "column grips stay blank, and muted across focus and hover until grabbed" {
defer p.deinit();
_ = try p.setTestFile("left\n");
p.presentation.enabled = false;
- p.settings.column_tags = true;
p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 };
try std.testing.expect(p.executeBuiltinLine(0, "New"));
try std.testing.expect(layout.splitColumn(p, p.active, p.active, false));
@@ -219,7 +216,6 @@ test "column grip dropped in its own place moves the column's left edge" {
defer p.deinit();
_ = try p.setTestFile("body\n");
p.presentation.enabled = false;
- p.settings.column_tags = true;
try std.testing.expect(p.executeBuiltinLine(0, "New"));
try std.testing.expect(layout.splitColumn(p, p.active, p.active, false));
p.update(.tick);
@@ -284,7 +280,6 @@ test "column grip drag previews where the column lands, in the seam beside its r
defer p.deinit();
_ = try p.setTestFile("body\n");
p.presentation.enabled = false;
- p.settings.column_tags = true;
for (0..2) |_| {
try std.testing.expect(p.executeBuiltinLine(p.active, "New"));
try std.testing.expect(layout.splitColumn(p, p.active, p.active, false));
diff --git a/test/column_tags.py b/test/column_tags.py
index fe3a8699..ba638b7d 100644
--- a/test/column_tags.py
+++ b/test/column_tags.py
@@ -138,7 +138,10 @@ def run(args):
before = panes(client)
keys.click(*require(locate(client, 'Newcol', row=0), 'Newcol missing'), button=1)
- right = require(wait_for(lambda: new_serial(client, before)), 'Newcol did not create its pane')
+ # Newcol makes an empty column and gives its tag the keyboard; the
+ # pane made next fills it.
+ require(wait_for(lambda: client.read('/focus').strip() == b''), 'Newcol did not focus its empty column')
+ right = int(client.read('/pane/new'))
right_name = root / 'right.rs'
client.write(f'/pane/{right}/name', f'{right_name}\n'.encode())
right_body = b'// Column-local scratch\nfn main() {\n let ready = true;\n}\n'
diff --git a/test/fs.py b/test/fs.py
index 53a9d4df..67edf01e 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -360,6 +360,32 @@ def discovery(binary, embedded=False):
# Through a mount both read as EINVAL; the log says which and why.
assert f'err {scratch} addr: no match for regexp\n'.encode() in client.read('/log')
client.remove(f'/pane/{scratch}')
+ # A save the host cannot do fails the write that asked for it,
+ # and the log's err record names the path and why.
+ unsaved = new_pane(client, b'unsaved\n')
+ try:
+ 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 f'err {unsaved} ctl: Save /nonexistent-pardes-dir/x.txt: '.encode() in client.read('/log'), client.read('/log')
+ client.remove(f'/pane/{unsaved}')
+
+ # Sixteen connections fill the slots; the next is told why.
+ extra = [Client(address) for _ in range(15)]
+ try:
+ try:
+ Client(address).__exit__()
+ raise AssertionError('a seventeenth connection was served')
+ except OSError as refused:
+ assert 'too many connections' in str(refused), refused
+ finally:
+ for other in extra:
+ other.__exit__()
+ deadline = time.monotonic() + 5
+ while b'err - 9p: too many connections' not in client.read('/log'):
+ assert time.monotonic() < deadline, client.read('/log')
+ time.sleep(.01)
print('9P discovery: listing/stat/find are inert; new, remove, look, exec, name, sel, log, ctl lock, focus, the ctl split and commands behave')
@@ -636,7 +662,10 @@ def test(binary, quic=False):
raise AssertionError('Restore left an old body fid usable')
with Client(address) as restored:
# The new log names its panes, then where they came from.
- assert restored.read('/log').endswith(b'\nrestore ' + str(saved).encode() + b'\n'), restored.read('/log')
+ log = restored.read('/log')
+ assert b'\nrestore ' + str(saved).encode() + b'\n' in log, log
+ # then which serial each pane had: restored <old> <new>
+ assert log.endswith(b'\n'.join(b'restored %d %d' % (old, new) for old, new in zip(sorted(old_ids), sorted(int(r.split()[0]) for r in restored.read('/index').splitlines()))) + b'\n'), log
restored_ids = [int(row.split()[0]) for row in restored.read('/index').splitlines()]
assert len(restored_ids) == 2 and old_ids.isdisjoint(restored_ids), restored_ids
file_id, control_id = restored_ids
diff --git a/test/gui-goldens.txt b/test/gui-goldens.txt
index c4f11cc8..586c8985 100644
--- a/test/gui-goldens.txt
+++ b/test/gui-goldens.txt
@@ -4,15 +4,14 @@
04-context-rows-mid-thumb e7b9f5d6da06143fff79b9f2686c29050556173f97c759a4119a201542f993ef
05-body-selection 06b7cbcf368bb4371873b26f70286fb2b5ada4293b758fa73458157a248c7aa6
06-tag-focus-selection a781c95cebb8c47a5c5168e1b7f9747f44a0aad043cc4eec1338bb1f08d0a340
-07-prompt-notice f2a39f0d1136d818e57d693873256f2134d89f10805d4078eaaf16a59bece1ce
-08-wide-glyph-edges ad7439148a7705aac6f5f3de3f6a47f9863dccffeba561c70a77e9b9838c5847
+07-prompt-notice 03bdefcd99ccbf4849d2c2f6b561cee6c24263a1be70d55ecb9550a815506e0e
+08-wide-glyph-edges 9c7449be42c46ebbdbdcf156fbfbbe23d6e451720afde966659b1bd96b6886bc
09-collapsed-focus-tint 807bd7621d54948b84cf2965ef463d3928d050259e8ee2d27472b1d3d378f91e
10-tagbottom 03086852528d9c8a76f5926a7e7ee371200da617db279f42cbbb6825b5f41fce
-11-column-tags 28ac090425c28515893f184b8c1fd0ba1dc3f05c0385372bb23a0b9e2abd080e
-12-multiline-tag 72be302d73e36e6d51d940300d557e34b081b94ce7ec4e50e3bbd56968ed0d58
-13-image 162e6404d5578d1a8c34a8abd152b91d54588e8ddba540e88d48396560c06f71
-14-theme-ink 42b756fd9f1fba42004ac40983bc6c45445fd7d039a970c11378cac652e00d89
-15-theme-acme-light 5cecfb32915c217c5d39492b8708532bd826a8a55ad6b8bd49d61ed5cee29ab9
-16-debug 51628eb1d8dce799256e336d081d8510fe70b7a3f28f9efefcbca7126583b348
-17-terminal 65794362c9e76fa2930e85e888f17216d24cace7475e29da3610c0dab411887b
-18-mid-transition d365b05255d82a5cddd4c829493595a67b49db9ac67178f8499175c5cc0f706a
+12-multiline-tag cd12edf85d9f472fc79aac91669f5d37619e0cc0ceedd1fe22d100faa127b685
+13-image f9412d7bbe0ebdb53bc62145c51ad3028d2f07cc3e9d2e52a3aeb89de7db6da3
+14-theme-ink da6369bd141a2d71aabb91854e82da88026926e0c931d89947645c090896b5d3
+15-theme-acme-light ca5252f5117ac8d6c679cef917c9f316c9ca311fc7a8dcbac2796b4fc04d3b64
+16-debug 591bb7c45b4b9c46cd2458330b4fa3962afc6dac827fa6f5e20e927a72c586e6
+17-terminal 38299427891e31de0b5597159501c161474620eb26fd7708a6396f0fd685209e
+18-mid-transition c914231ea8c32446a0975cfbe338cb3b248a86f3d58c6bc56af6c3e800906302
diff --git a/test/gui_golden.py b/test/gui_golden.py
index d97009c9..99e8c636 100644
--- a/test/gui_golden.py
+++ b/test/gui_golden.py
@@ -177,9 +177,6 @@ def run(args):
client.write('/ctl', b'Tagbottom\n')
scene('10-tagbottom')
client.write('/ctl', b'Tagbottom\n')
- client.write('/ctl', b'ColumnTags\n')
- scene('11-column-tags')
- client.write('/ctl', b'ColumnTags\n')
client.write(f'/pane/{first}/tag', b' Second\nthird line of the tag')
scene('12-multiline-tag')
client.write(f'/pane/{first}/look', f'{image}\n'.encode())
diff --git a/test/panes.zig b/test/panes.zig
index 40b4b6ba..f00b35c4 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -86,6 +86,8 @@ const TagLayerTests = struct {
const source_y = @as(f32, @floatFromInt(layer.viewport.y)) + 0.5;
const painted_x = shown.x + (x / 10 - track.to.x) / track.to.w * shown.w;
const painted_y = shown.y + (source_y - track.to.y) / track.to.h * shown.h;
+ // Each is a first click, whatever cell the one before hit.
+ p.last_click = null;
for ([_]pardes.Mouse.Kind{ .press, .release }) |kind| {
var event = mouse(layer, x, kind);
// Hosts inverse the exact painted pixel into the tag envelope;
@@ -103,7 +105,6 @@ const TagLayerTests = struct {
defer p.deinit();
_ = try p.setTestFile("body\n");
p.presentation.enabled = false;
- p.settings.column_tags = true;
p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 };
try std.testing.expect(p.executeBuiltinLine(0, "New"));
const second = p.active;
@@ -163,6 +164,23 @@ const TagLayerTests = struct {
}
};
+
+// Rows relative to where the pane's body and tag are, whatever height its
+// tag has (a tag wraps onto more rows when it is long).
+fn paneAt(p: *Pardes, r: anytype) *pardes.Pane {
+ for (p.panes, 0..) |slot, id| if (slot) |pane| if (std.meta.eql(p.rects[id], r)) return pane;
+ unreachable;
+}
+fn bodyY(p: *Pardes, r: anytype) u16 {
+ return p.bodyTop(paneAt(p, r), r);
+}
+fn bodyH(p: *Pardes, r: anytype) u16 {
+ return r.h -| paneAt(p, r).tag_rows;
+}
+fn tagY(p: *Pardes, r: anytype) u16 {
+ return p.tagTop(paneAt(p, r), r);
+}
+
test "a tag with a newline in it is as tall as its lines, and each line is typed into and pointed at" {
for ([_]bool{ false, true }) |bottom| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 14 });
@@ -267,22 +285,14 @@ test "a tag with more lines than rows scrolls to keep its caret in view" {
try std.testing.expectEqual(tag_y, top.cursor.?.y);
try std.testing.expectEqualStrings("/", top.at(x, tag_y).grapheme());
- // A column tag does the same in its rows.
+ // A column tag is one row, as acme's: its lines are joined.
many.clearRetainingCapacity();
try many.appendSlice(std.testing.allocator, "New");
for (1..21) |n| try many.print(std.testing.allocator, "\nC{d}", .{n});
p.col_tags[0].own = try p.gpa.dupe(u8, many.items);
p.update(.tick);
- const rows = p.columnBarHeight();
- try std.testing.expectEqual(@as(u16, pardes.MAX_TAG_ROWS), rows);
- pardes.tagline.enterHeader(p, 0);
- p.update(.{ .key = .{ .cp = 'g' } });
- p.update(.{ .key = .{ .cp = 'e' } });
- const header = try p.render(frame.allocator());
- const header_row = p.topBarHeight() + rows - 1;
- try std.testing.expectEqual(header_row, header.cursor.?.y);
- try std.testing.expectEqualStrings("C", header.at(p.col_x[0] + pardes.TAG_TEXT_INSET, header_row).grapheme());
- try std.testing.expectEqualStrings("2", header.at(p.col_x[0] + pardes.TAG_TEXT_INSET + 1, header_row).grapheme());
+ try std.testing.expectEqual(pardes.COLBAR_H, p.columnBarHeight());
+ try std.testing.expect(std.mem.indexOfScalar(u8, p.col_tags[0].own.?, '\n') == null);
}
const TagNavigationTests = struct {
@@ -413,7 +423,7 @@ const TagNavigationTests = struct {
defer frame.deinit();
_ = try p.render(frame.allocator());
const rect = p.rects[0];
- const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
+ const y = tagY(p, rect);
const prefix: u16 = @intCast(panes.File.displayWidth(try pardes.tagline.tagPrefix(p, first)));
ClickCursorTests.click(p, .left, rect.x + pardes.TAG_TEXT_INSET + prefix + 6, y);
try std.testing.expectEqual(@as(usize, 0), p.active);
@@ -595,7 +605,8 @@ const BodyHitTests = struct {
p.update(.{ .mouse = mouse(layer, last - 1, prefix + 11, .press) });
// Release at a different source row without an intermediate drag event.
p.update(.{ .mouse = mouse(layer, last, prefix + 14, .release) });
- try std.testing.expectEqualStrings("body\nbody", pardes.test_api.heldSelection(p, 0).?);
+ // A stream: the last row from its start, indentation and all.
+ try std.testing.expect(std.mem.endsWith(u8, pardes.test_api.heldSelection(p, 0).?, "// body") and std.mem.startsWith(u8, pardes.test_api.heldSelection(p, 0).?, "body\n"));
p.update(.{ .mouse = mouse(layer, last, 10, .press) });
p.update(.{ .mouse = mouse(layer, last, 10, .release) });
try std.testing.expectEqual(expected, pane.body.cur_row);
@@ -779,6 +790,8 @@ const TreeContextTests = struct {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 48, .rows = 14 });
defer p.deinit();
const pane = try setZigFile(p, "pub fn @\"界name\"(\n\tvalue: usize,\n) void {\n" ++ " _ = value;\n" ** 12 ++ "}\n");
+ pane.tag_expanded = false; // a one-row tag keeps this small screen's rows
+ p.sync();
p.settings.wrap = false;
p.settings.colors = false;
try std.testing.expect(p.executeBuiltinLine(0, "TreeContext"));
@@ -816,6 +829,8 @@ const TreeContextTests = struct {
defer p.deinit();
const body = " // 012345678901234567890123456789界tail";
const pane = try setZigFile(p, "pub fn aLongDeclarationNameThatCannotFit() void {\n // before\n" ++ body ++ "\n}\n");
+ pane.tag_expanded = false; // a one-row tag keeps this small screen's rows
+ p.sync();
p.settings.wrap = true;
try std.testing.expect(p.executeBuiltinLine(0, "TreeContext"));
pane.scrollBy(2);
@@ -873,7 +888,7 @@ const TtySelectionTests = struct {
try noChildInput(p);
}
- test "tty selection preserves exact rectangle through yank clipboard and mode changes" {
+ test "tty selection preserves its exact stream through yank clipboard and mode changes" {
if (comptime !panes.Terminal.enabled) return error.SkipZigTest;
for ([_]bool{ false, true }) |prompt| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 });
@@ -894,7 +909,7 @@ const TtySelectionTests = struct {
const x = rect.x + config.GUTTER;
const y = p.bodyTop(pane, rect);
JumpSelectionTests.drag(p, x + @as(u16, if (prompt) 0 else 1), y, x + 3, y + @as(u16, if (prompt) 0 else 1));
- const expected = if (prompt) "$ ab" else "lph\nrav";
+ const expected = if (prompt) "$ ab" else "lpha\nbrav";
try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode);
try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?);
try std.testing.expectEqualStrings(expected, p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank);
@@ -907,9 +922,13 @@ const TtySelectionTests = struct {
try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode);
const normal = try p.render(frame.allocator());
if (!prompt) {
- const selection_bg: pardes.Color = .{ .rgb = p.theme().sel_bg };
- try std.testing.expect(!std.meta.eql(selection_bg, normal.at(x + 4, y).style.bg));
- try std.testing.expect(!std.meta.eql(selection_bg, normal.at(x, y + 1).style.bg));
+ // A stream: the first row to its edge, the last from its
+ // start; (x + 1, y) is surely in it, whatever its tint.
+ const selected = normal.at(x + 1, y).style.bg;
+ try std.testing.expect(std.meta.eql(selected, normal.at(x + 4, y).style.bg));
+ try std.testing.expect(std.meta.eql(selected, normal.at(x, y + 1).style.bg));
+ try std.testing.expect(!std.meta.eql(selected, normal.at(x + 4, y + 1).style.bg));
+ try std.testing.expect(!std.meta.eql(selected, normal.at(x, y).style.bg));
}
try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?);
p.update(.{ .key = .{ .cp = 'y' } });
@@ -954,14 +973,14 @@ const TtySelectionTests = struct {
_ = try p.render(frame.allocator());
const source = p.rects[0];
const sx = source.x + config.GUTTER;
- const sy = if (p.settings.tag_bottom) source.y else source.y + pardes.BOX_H;
+ const sy = bodyY(p, source);
JumpSelectionTests.drag(p, sx + 1, sy, sx + 3, sy + 1);
- try std.testing.expectEqualStrings("lph\nrav", p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank);
+ try std.testing.expectEqualStrings("lpha\nbrav", p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank);
try noChildInput(p);
const target = p.rects[1];
const tx = target.x + config.GUTTER + 2;
- const ty = if (p.settings.tag_bottom) target.y else target.y + pardes.BOX_H;
+ const ty = bodyY(p, target);
p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = tx, .row = ty } });
p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = tx, .row = ty } });
p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = tx, .row = ty } });
@@ -976,13 +995,13 @@ const TtySelectionTests = struct {
}
const expected = try std.fmt.allocPrint(p.gpa, "{s}{s}", .{
if (reports_mouse) "\x1b[<0;3;1M\x1b[<0;3;1m" else "",
- if (bracketed) "\x1b[200~lph\nrav\x1b[201~" else "lph\rrav",
+ if (bracketed) "\x1b[200~lpha\nbrav\x1b[201~" else "lpha\rbrav",
});
defer p.gpa.free(expected);
try std.testing.expectEqualStrings(expected, written.written());
try std.testing.expectEqual(panes.Text.Mode.tty, p.panes[0].?.body.mode);
try std.testing.expectEqual(panes.Text.Mode.tty, p.panes[1].?.body.mode);
- try std.testing.expectEqualStrings("lph\nrav", pardes.test_api.heldSelection(p, 0).?);
+ try std.testing.expectEqualStrings("lpha\nbrav", pardes.test_api.heldSelection(p, 0).?);
}
}
}
@@ -1060,6 +1079,107 @@ const ClickCursorTests = struct {
p.update(.{ .mouse = .{ .button = button, .kind = .release, .col = x, .row = y } });
}
+ test "a B1 sweep held past a pane's bottom scrolls the body and keeps its start" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 });
+ defer p.deinit();
+ var text: std.ArrayList(u8) = .empty;
+ defer text.deinit(std.testing.allocator);
+ for (0..60) |i| try text.print(std.testing.allocator, "line{d}\n", .{i});
+ const pane = try p.setTestFile(text.items);
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[0];
+ const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
+ const y = p.bodyTop(pane, rect);
+ // The pane reaches the screen's bottom: its last row is the edge.
+ const below = rect.y + rect.h - 1;
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = x, .row = y + 1 } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = x + 2, .row = below } });
+ for (0..20) |_| p.update(.tick);
+ try std.testing.expect(pane.scroll() >= 20);
+ _ = try p.render(frame.allocator()); // a frame between the ticks and the release
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = x + 2, .row = below } });
+ const got = pardes.edit.currentSelText(p, pane).?;
+ try std.testing.expect(std.mem.startsWith(u8, got, "line1\nline2\n"));
+ try std.testing.expect(std.mem.count(u8, got, "\n") > 20);
+ }
+
+ test "everything a B1 chord does while B1 is down is one undo step" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 });
+ defer p.deinit();
+ const pane = try p.setTestFile("one two three\n");
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[0];
+ const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
+ const y = p.bodyTop(pane, rect);
+ const M = struct {
+ fn ev(pp: *Pardes, b: pardes.Mouse.Button, k: pardes.Mouse.Kind, cx: u16, cy: u16) void {
+ pp.update(.{ .mouse = .{ .button = b, .kind = k, .col = cx, .row = cy } });
+ }
+ };
+ // B1-B2 then B1-B3: cut, then paste back: a copy, one step.
+ const before = pane.file.?.history.undo_len;
+ M.ev(p, .left, .press, x + 4, y);
+ M.ev(p, .left, .drag, x + 6, y);
+ M.ev(p, .middle, .press, x + 6, y);
+ M.ev(p, .middle, .release, x + 6, y);
+ M.ev(p, .right, .press, x + 6, y);
+ M.ev(p, .right, .release, x + 6, y);
+ M.ev(p, .left, .release, x + 6, y);
+ try std.testing.expectEqualStrings("one two three\n", pane.file.?.content);
+ try std.testing.expectEqualStrings("two", p.registers.text(p.gpa, '"').?);
+ try std.testing.expect(pane.file.?.history.undo_len <= before + 1);
+ // A cut alone comes back with one Undo.
+ M.ev(p, .left, .press, x + 4, y);
+ M.ev(p, .left, .drag, x + 6, y);
+ M.ev(p, .middle, .press, x + 6, y);
+ M.ev(p, .middle, .release, x + 6, y);
+ M.ev(p, .left, .release, x + 6, y);
+ try std.testing.expectEqualStrings("one three\n", pane.file.?.content);
+ try std.testing.expect(p.executeBuiltinLine(0, "Undo"));
+ try std.testing.expectEqualStrings("one two three\n", pane.file.?.content);
+ }
+
+ test "a double-click selects a word, a line, a bracketed or quoted text, as acme's" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 });
+ defer p.deinit();
+ const pane = try p.setTestFile("call foo_bar(a, b)\nsay \"hi there\"\n");
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[0];
+ const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
+ const y = p.bodyTop(pane, rect);
+ const Want = struct { col: u16, row: u16, text: []const u8 };
+ for ([_]Want{
+ .{ .col = 6, .row = 0, .text = "foo_bar" },
+ .{ .col = 13, .row = 0, .text = "a, b" },
+ .{ .col = 0, .row = 0, .text = "call foo_bar(a, b)\n" },
+ .{ .col = 5, .row = 1, .text = "hi there" },
+ }) |w| {
+ click(p, .left, x + w.col, y + w.row);
+ click(p, .left, x + w.col, y + w.row);
+ try std.testing.expectEqualStrings(w.text, pardes.edit.currentSelText(p, pane).?);
+ // A third click is a plain click again: the caret, no selection.
+ click(p, .left, x + w.col, y + w.row);
+ try std.testing.expect(!pane.body.vsel.active);
+ }
+ // In the tag too.
+ const tag_text = try pardes.tagline.tagText(p, frame.allocator(), pane);
+ const del: u16 = @intCast(std.mem.indexOf(u8, tag_text, "Del").? + 1);
+ const tag_x = rect.x + pardes.TAG_TEXT_INSET + del;
+ click(p, .left, tag_x, p.tagTop(pane, rect));
+ click(p, .left, tag_x, p.tagTop(pane, rect));
+ try std.testing.expect(pane.tag.vsel.active);
+ const tag_now = try pardes.tagline.tagText(p, frame.allocator(), pane);
+ const a = modal.offsetAt(tag_now, .{ .row = @intCast(pane.tag.vsel.row), .col = @intCast(pane.tag.vsel.col) });
+ const h = modal.offsetAt(tag_now, .{ .row = @intCast(pane.tag.cur_row), .col = @intCast(pane.tag.cur_col) });
+ try std.testing.expectEqualStrings("Del", tag_now[a .. h + 1]);
+ }
+
test "click cursor and insertion agree beyond line and file ends" {
for ([_]bool{ false, true }) |already_insert| {
for ([_]bool{ false, true }) |bottom_tag| {
@@ -1080,7 +1200,7 @@ const ClickCursorTests = struct {
_ = try p.render(frame.allocator());
const rect = p.rects[0];
const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
- const y = if (bottom_tag) rect.y else rect.y + pardes.BOX_H;
+ const y = bodyY(p, rect);
click(p, .left, x + 15, y + case.click_row);
try std.testing.expectEqual(case.row, pane.body.cur_row);
try std.testing.expectEqual(case.col, pane.body.cur_col);
@@ -1149,7 +1269,7 @@ const ClickCursorTests = struct {
_ = try p.render(frame.allocator());
const rect = p.rects[0];
const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
- const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
+ const y = bodyY(p, rect);
JumpSelectionTests.drag(p, x + 1, y + 4, x + 3, y + 4);
try std.testing.expectEqualStrings("lph", pardes.test_api.heldSelection(p, 0).?);
for (0..2) |_| p.update(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = x + 2, .row = y + 4 } });
@@ -1253,9 +1373,42 @@ const JumpSelectionTests = struct {
try std.testing.expectEqual(@as(i32, 0), pane.body.cur_col);
}
- test "jump selection clears copied rectangles in both history directions" {
+ test "a B1 sweep over several lines is a stream, as acme's, in a body and a tag" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 });
+ defer p.deinit();
+ const pane = try p.setTestFile("alpha\nbravo\ncharlie\n");
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const rect = p.rects[0];
+ const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane);
+ const y = p.bodyTop(pane, rect);
+ // From the press to the end of its line, the line between whole, the
+ // last from its start: no line numbers, no rectangle.
+ drag(p, x + 2, y, x + 1, y + 2);
+ try std.testing.expectEqualStrings("pha\nbravo\nch", pardes.test_api.heldSelection(p, 0).?);
+ // Upward, the same text.
+ drag(p, x + 1, y + 2, x + 2, y);
+ try std.testing.expectEqualStrings("pha\nbravo\nch", pardes.test_api.heldSelection(p, 0).?);
+ // A tag of several rows sweeps the same way.
+ if (pane.tag.own) |own| p.gpa.free(own);
+ pane.tag.own = try p.gpa.dupe(u8, "Save Del\nLook Edit");
+ p.sync();
+ _ = try p.render(frame.allocator());
+ p.sync();
+ try std.testing.expectEqual(@as(u16, 2), pane.tag_rows);
+ const sl: panes.Pane.Sel = .{ .state = .done, .r0 = 0, .c0 = 3, .r1 = 1, .c1 = 1 };
+ const text = try pardes.edit.paneText(p, pane);
+ const swept = try pardes.edit.selectionText(p, pane, sl);
+ const first_line = text[0 .. std.mem.indexOfScalar(u8, text, '\n') orelse text.len];
+ const second = std.mem.sliceTo(text[first_line.len + 1 ..], '\n');
+ const want = try std.fmt.allocPrint(frame.allocator(), "{s}\n{s}", .{ first_line[3..], second[0..@min(2, second.len)] });
+ try std.testing.expectEqualStrings(want, swept);
+ }
+
+ test "jump selection clears copied sweeps in both history directions" {
for ([_]bool{ false, true }) |reverse|
- try exercise("alpha\nbravo\n", false, reverse, 1, 3, "lph\nrav");
+ try exercise("alpha\nbravo\n", false, reverse, 1, 3, "lpha\nbrav");
}
test "jump selection clears wrapped Unicode and tab rectangles" {
@@ -1325,22 +1478,29 @@ const TagNameTintTests = struct {
var arena = std.heap.ArenaAllocator.init(p.gpa);
defer arena.deinit();
const surface = try p.render(arena.allocator());
- const text = try pardes.tagline.tagText(p, arena.allocator(), pane);
+ // The tag as shown: its rows wrapped, each offset mapped back to the
+ // tag's own to say whether it is in the name.
+ const shown = try pardes.tagline.shownTag(p, arena.allocator(), pane);
const base = std.mem.lastIndexOfScalar(u8, name, '/') orelse return error.MissingDirectory;
const rect = p.rects[id];
- const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
const left = rect.x + pardes.TAG_TEXT_INSET;
var visible_name: usize = 0;
- var at: usize = 0;
- while (at < text.len) : (at = modal.nextGrapheme(text, at)) {
- const start: i32 = @as(i32, @intCast(panes.File.rawDisplayCol(text, at))) - pane.tag_scroll;
- const end = start + @as(i32, @intCast(panes.File.displayWidth(text[at..modal.nextGrapheme(text, at)])));
- var col = @max(0, start);
- while (col < end and col < rect.w - pardes.TAG_TEXT_INSET) : (col += 1) {
- const in_name = at > base and at < name.len;
- const fg = if (in_name) name_fg else tag_fg;
- try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(left + @as(u16, @intCast(col)), y).style.fg);
- if (in_name) visible_name += 1;
+ for (0..pane.tag_rows) |row| {
+ const line_start = modal.lineStartOffset(shown.text, row + pane.tag_top);
+ const line = modal.lineSlice(shown.text, row + pane.tag_top);
+ const y = tagY(p, rect) + @as(u16, @intCast(row));
+ var at: usize = 0;
+ while (at < line.len) : (at = modal.nextGrapheme(line, at)) {
+ const real = shown.toReal(line_start + at);
+ const start: i32 = @intCast(panes.File.rawDisplayCol(line, at));
+ const end = start + @as(i32, @intCast(panes.File.displayWidth(line[at..modal.nextGrapheme(line, at)])));
+ var col = start;
+ while (col < end and col < rect.w - pardes.TAG_TEXT_INSET) : (col += 1) {
+ const in_name = real > base and real < name.len;
+ const fg = if (in_name) name_fg else tag_fg;
+ try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(left + @as(u16, @intCast(col)), y).style.fg);
+ if (in_name) visible_name += 1;
+ }
}
}
return visible_name;
@@ -1366,7 +1526,7 @@ const TagNameTintTests = struct {
}
}
- test "tag filename tint follows scrolled Unicode drafted names and yields to selection" {
+ test "tag filename tint follows wrapped Unicode drafted names and yields to selection" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 38, .rows = 14 });
defer p.deinit();
const pane = try p.setTestFile("body\n");
@@ -1376,17 +1536,20 @@ const TagNameTintTests = struct {
pardes.tagline.draftName(p, pane, 0);
redraft(pane, name);
pane.input.cur_col = @intCast(std.mem.indexOf(u8, name, "界").?);
+ p.sync(); // the drafted name's rows
try std.testing.expect((try check(p, 0, name)) > 0);
- try std.testing.expect(pane.tag_scroll > 0);
- // A sweep over the wide glyph wears the selection over the tint.
- const col: i32 = @intCast(panes.File.rawDisplayCol(name, @intCast(pane.input.cur_col)));
- pane.sel[0] = .{ .state = .done, .c0 = col, .c1 = col + 1, .r0 = 0, .r1 = 0 };
- pane.tag_scroll += 4; // bring both cells of the selected wide glyph into view
+ try std.testing.expect(pane.tag_rows > 1); // the long name wraps
+ // A sweep over the wide glyph, on the row it wrapped to, wears the
+ // selection over the tint.
var arena = std.heap.ArenaAllocator.init(p.gpa);
defer arena.deinit();
+ const shown = try pardes.tagline.shownTag(p, arena.allocator(), pane);
+ const pos = modal.positionAt(shown.text, shown.fromReal(@intCast(pane.input.cur_col)));
+ const col: i32 = @intCast(panes.File.rawDisplayCol(modal.lineSlice(shown.text, pos.row), pos.col));
+ pane.sel[0] = .{ .state = .done, .c0 = col, .c1 = col + 1, .r0 = @intCast(pos.row), .r1 = @intCast(pos.row) };
const surface = try p.render(arena.allocator());
- const x = p.rects[0].x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(col - pane.tag_scroll));
- const y = if (p.settings.tag_bottom) p.rects[0].y + p.rects[0].h - pardes.BOX_H else p.rects[0].y;
+ const x = p.rects[0].x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(col));
+ const y = tagY(p, p.rects[0]) + @as(u16, @intCast(pos.row));
try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().sel_fg }, surface.at(x, y).style.fg);
try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().sel_fg }, surface.at(x + 1, y).style.fg);
}
@@ -1409,7 +1572,7 @@ const TagNameTintTests = struct {
p.settings.focus_tint = active;
const surface = try p.render(arena.allocator());
const x = p.rects[0].x + pardes.TAG_TEXT_INSET;
- const y = if (p.settings.tag_bottom) p.rects[0].y + p.rects[0].h - pardes.BOX_H else p.rects[0].y;
+ const y = tagY(p, p.rects[0]);
const fg = if (active) active_fg else tag_fg;
try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(x + 1, y).style.fg);
try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(x + 5, y).style.fg);
@@ -1428,7 +1591,7 @@ fn redraft(pane: *Pane, name: []const u8) void {
/// Click the tagline at the end of the path: a name draft, caret there.
fn clickPathEnd(p: *Pardes, pane: *Pane) void {
const rect = p.rects[p.active];
- const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
+ const y = tagY(p, rect);
const x = rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(pane.file.?.path) - 1));
ClickCursorTests.click(p, .left, x, y);
std.debug.assert(pane.prompt == .name);
@@ -1570,7 +1733,7 @@ test "paste into pane commands and prompts never inserts into the body" {
try std.testing.expect(std.mem.indexOf(u8, pardes.tagline.curTail(pane), " Custom\nCommand") != null);
}
-test "long pane tags reveal commands and map visible clicks to whole tag addresses" {
+test "a long pane tag wraps onto rows, and a click on a wrapped row maps to the whole tag's address" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 50, .rows = 12 });
defer p.deinit();
const pane = try p.setTestFile("body\n");
@@ -1582,13 +1745,15 @@ test "long pane tags reveal commands and map visible clicks to whole tag address
var arena = std.heap.ArenaAllocator.init(p.gpa);
defer arena.deinit();
const surface = try p.render(arena.allocator());
- try std.testing.expect(pane.tag_scroll > 0);
+ try std.testing.expect(pane.tag_rows > 1);
try std.testing.expect(surface.cursor.?.x < p.rects[0].x + p.rects[0].w);
const text = try pardes.tagline.tagText(p, p.gpa, pane);
defer p.gpa.free(text);
- const offset = std.mem.indexOf(u8, text, "SyntaxBold").?;
- const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + panes.File.rawDisplayCol(text, offset) - pane.tag_scroll);
- const y = p.rects[0].y;
+ const shown = try pardes.tagline.shownTag(p, arena.allocator(), pane);
+ const at = shown.fromReal(std.mem.indexOf(u8, text, "SyntaxBold").?);
+ const pos = modal.positionAt(shown.text, at);
+ const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + panes.File.rawDisplayCol(modal.lineSlice(shown.text, pos.row), pos.col));
+ const y = p.tagTop(pane, p.rects[0]) + @as(u16, @intCast(pos.row));
const before = p.settings.syntax_bold;
p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .press } });
p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .release } });
@@ -1596,6 +1761,32 @@ test "long pane tags reveal commands and map visible clicks to whole tag address
try std.testing.expectEqualStrings(text, try pardes.tagline.tagText(p, p.scratch.allocator(), pane));
}
+test "a word the tag's wrapping breaks across rows is still one word to a click" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 50, .rows = 12 });
+ defer p.deinit();
+ const pane = try p.setTestFile("body\n");
+ try pane.appendTag(" SyntaxBold");
+ var arena = std.heap.ArenaAllocator.init(p.gpa);
+ defer arena.deinit();
+ // Lengthen the path until the wrap splits the word after "Synt".
+ var pad: usize = 1;
+ const shown = while (pad < 80) : (pad += 1) {
+ p.gpa.free(pane.file.?.path);
+ const path = try std.fmt.allocPrint(arena.allocator(), "/{s}/f.zig", .{("a" ** 80)[0..pad]});
+ pane.file.?.path = try p.gpa.dupe(u8, path);
+ p.sync();
+ const s = try pardes.tagline.shownTag(p, arena.allocator(), pane);
+ if (std.mem.indexOf(u8, s.text, "Synt\n") != null) break s;
+ } else return error.NoSplit;
+ const pos = modal.positionAt(shown.text, std.mem.indexOf(u8, shown.text, "Synt").?);
+ const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + pos.col + 1);
+ const y = p.tagTop(pane, p.rects[0]) + @as(u16, @intCast(pos.row));
+ const before = p.settings.syntax_bold;
+ p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .press } });
+ p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .release } });
+ try std.testing.expectEqual(!before, p.settings.syntax_bold);
+}
+
test "terminal overlay recoloring never materializes unrelated history" {
const testing = std.testing;
const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 32, .rows = 8 });
@@ -1618,7 +1809,7 @@ test "terminal overlay recoloring never materializes unrelated history" {
const surface = try p.render(frame.allocator());
try testing.expect(p.shell_rows.pane == null);
const rect = p.rects[0];
- const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
+ const body_y = bodyY(p, rect);
const cell = surface.at(rect.x + config.GUTTER, body_y);
try testing.expectEqualStrings("c", cell.grapheme());
try testing.expectEqual(pardes.Color{ .rgb = p.theme().palette.?[2] }, cell.style.fg);
@@ -1666,7 +1857,7 @@ test "terminal output keeps following new rows after scrollback eviction" {
const surface = try p.render(frame.allocator());
const rect = p.rects[0];
const x = rect.x + config.GUTTER;
- const y = (if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H) + pane.rows - 2;
+ const y = (bodyY(p, rect)) + pane.rows - 2;
for (last, 0..) |byte, col| {
const cell = surface.at(x + @as(u16, @intCast(col)), y);
try std.testing.expectEqualStrings(&.{byte}, cell.grapheme());
@@ -2319,7 +2510,7 @@ test "unplaced terminal panes keep their allocated grid until placement" {
p.update(.tick);
try std.testing.expect(p.rects[id].w > 0 and p.rects[id].h > pardes.BOX_H);
try std.testing.expectEqual(p.rects[id].w - config.GUTTER, pane.cols);
- try std.testing.expectEqual(p.rects[id].h - pardes.BOX_H, pane.rows);
+ try std.testing.expectEqual(bodyH(p, p.rects[id]), pane.rows);
}
test "document panes ignore terminal output without allocating terminal state" {
@@ -2672,7 +2863,7 @@ const OutputTests = struct {
layout.compute(p);
const surface = try p.render(frame.allocator());
const rect = p.rects[pane.search_pane.?];
- const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
+ const body_y = bodyY(p, rect);
for (file.location_rows, 0..) |row, index| {
if (!row.declaration) continue;
const line = modal.lineSlice(file.content, index);
@@ -2720,7 +2911,7 @@ const OutputTests = struct {
_ = try p.render(frame.allocator());
const rect = p.rects[id];
const x = rect.x + config.GUTTER + panes.File.gutterWidth(help) + @as(u16, @intCast(at - line_start + 2));
- const y = (if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H) + @as(u16, @intCast(row));
+ const y = (bodyY(p, rect)) + @as(u16, @intCast(row));
p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .press, .col = x, .row = y } });
p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .release, .col = x, .row = y } });
try std.testing.expect(p.active != id);
@@ -2790,7 +2981,7 @@ const OutputTests = struct {
p.acknowledgePanelPresentation(&.{});
const rect = p.rects[id];
const x = rect.x + config.GUTTER + panes.File.gutterWidth(result);
- const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
+ const y = bodyY(p, rect);
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = x, .row = y } });
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .drag, .col = x + 2, .row = y + 1 } });
try std.testing.expect(p.drag == .select);
@@ -3105,8 +3296,8 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
- const rows: usize = r.h - pardes.BOX_H;
+ const body_y = bodyY(p, r);
+ const rows: usize = bodyH(p, r);
const cols: usize = r.w -| config.GUTTER;
const Snap = struct { text: [][7]u8, len: []u8, style: []pardes.CellStyle };
@@ -3346,7 +3537,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
const red: pardes.Color = .{ .index = 1 };
const green: pardes.Color = .{ .index = 2 };
const blue: pardes.Color = .{ .index = 4 };
@@ -3401,7 +3592,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
p.shell_rows.stale = true;
const s = try p.render(frame.allocator());
@@ -3449,8 +3640,8 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
- const body_h = r.h - pardes.BOX_H;
+ const body_y = bodyY(p, r);
+ const body_h = bodyH(p, r);
p.shell_rows.stale = true;
const s = try p.render(frame.allocator());
@@ -3488,7 +3679,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
const red: pardes.Color = .{ .index = 1 };
const green: pardes.Color = .{ .index = 2 };
const blue: pardes.Color = .{ .index = 4 };
@@ -3525,7 +3716,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
const red: pardes.Color = .{ .index = 1 };
// Covers far more rows than the grid was ever dumped for, so the anchor
@@ -3607,7 +3798,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
p.shell_rows.stale = true;
const s = try p.render(frame.allocator());
@@ -3685,8 +3876,8 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
- const rows: usize = r.h - pardes.BOX_H;
+ const body_y = bodyY(p, r);
+ const rows: usize = bodyH(p, r);
const cols: usize = r.w -| config.GUTTER;
// What tty mode shows with nothing left behind: the reference.
@@ -3737,6 +3928,8 @@ const TerminalTests = struct {
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
+ pane.tag_expanded = false; // a one-row tag keeps this small screen's rows
+ p.sync();
pane.tty_filter = filter;
pane.body.mode = mode;
p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[32m\u{754C}\x1b[41m\x1b[K\x1b[7G\x1b[44m\x1b[K\r\n" } });
@@ -3745,7 +3938,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
p.shell_rows.stale = true;
const s = try p.render(frame.allocator());
@@ -3781,7 +3974,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
p.shell_rows.stale = true;
const s = try p.render(frame.allocator());
@@ -3814,10 +4007,10 @@ const TerminalTests = struct {
const s = try p.render(frame.allocator());
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
var bad: usize = 0;
- for (0..r.h -| pardes.BOX_H) |vr| {
+ for (0..bodyH(p, r)) |vr| {
var buf: [16]u8 = undefined;
var n: usize = 0;
for (0..10) |c| {
@@ -3857,7 +4050,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
p.update(.{ .output = .{ .pane = 0, .bytes = "plain text\r\n" } });
p.shell_rows.stale = true;
@@ -3895,7 +4088,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
// The state two ordinary edits reach: one at the bottom, one that split a
// line further up. The buffer now spans rows 2..6 and diverges at BOTH
@@ -3933,7 +4126,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
// The state three keystrokes reach on any blank shell row: type a character
// and delete it, and the buffer holds NO text while still standing in for
@@ -3958,6 +4151,8 @@ const TerminalTests = struct {
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
+ pane.tag_expanded = false; // a one-row tag keeps this small screen's rows
+ p.sync();
pane.tty_filter = false;
pane.body.mode = .tty;
@@ -3967,7 +4162,7 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
const blue: pardes.Color = .{ .index = 4 };
p.shell_rows.stale = true;
@@ -4010,12 +4205,12 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
const s = try p.render(frame.allocator());
const want = pardes.Color{ .index = 33 };
var seen = false;
- for (0..@as(usize, r.h -| pardes.BOX_H)) |vr| {
+ for (0..@as(usize, bodyH(p, r))) |vr| {
var buf: [15]u8 = undefined;
for (0..15) |c| {
const g = s.at(tx + @as(u16, @intCast(c)), body_y + @as(u16, @intCast(vr))).grapheme();
@@ -4063,11 +4258,11 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
const s = try p.render(frame.allocator());
var seen = false;
- for (0..@as(usize, r.h -| pardes.BOX_H)) |vr| {
+ for (0..@as(usize, bodyH(p, r))) |vr| {
var buf: [12]u8 = undefined;
for (0..12) |c| {
const g = s.at(tx + @as(u16, @intCast(c)), body_y + @as(u16, @intCast(vr))).grapheme();
@@ -4117,8 +4312,8 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
- const body_h = r.h -| pardes.BOX_H;
+ const body_y = bodyY(p, r);
+ const body_h = bodyH(p, r);
var step: usize = 0;
while (step < 12) : (step += 1) {
@@ -4212,11 +4407,11 @@ const TerminalTests = struct {
defer frame.deinit();
const r = p.rects[0];
const tx = r.x + config.GUTTER;
- const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H;
+ const body_y = bodyY(p, r);
const s = try p.render(frame.allocator());
var seen: usize = 0;
- for (0..@as(usize, r.h -| pardes.BOX_H)) |vr| {
+ for (0..@as(usize, bodyH(p, r))) |vr| {
var buf: [12]u8 = undefined;
for (0..12) |c| {
const g = s.at(tx + @as(u16, @intCast(c)), body_y + @as(u16, @intCast(vr))).grapheme();
@@ -4294,7 +4489,7 @@ test "cancelled normal gestures leave no count or subprefix after mouse and focu
switch (gesture) {
.body, .tag => {
const rect = p.rects[0];
- const tag_y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
+ const tag_y = tagY(p, rect);
const body_y = p.bodyTop(pane, rect);
const x = if (gesture == .tag)
rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(try pardes.tagline.tagPrefix(p, pane)))) + 3
@@ -4424,9 +4619,9 @@ test "growing a file pane colors every newly visible Zig keyword" {
_ = frame.reset(.retain_capacity);
const surface = try p.render(frame.allocator());
const rect = p.rects[0];
- const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
+ const body_y = bodyY(p, rect);
var seen: usize = 0;
- for (body_y..body_y + rect.h - pardes.BOX_H) |y| {
+ for (body_y..body_y + bodyH(p, rect)) |y| {
for (rect.x + config.GUTTER..rect.x + rect.w - 1) |x| {
const first = surface.at(@intCast(x), @intCast(y));
const second = surface.at(@intCast(x + 1), @intCast(y));
@@ -4467,6 +4662,8 @@ test "Zig keywords keep every rendered byte colored across line-number widths" {
const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = view.cols, .rows = 10 });
defer p.deinit();
const pane = try p.setTestFile(source);
+ pane.tag_expanded = false; // a one-row tag keeps this small screen's rows
+ p.sync();
const file = &pane.file.?;
gpa.free(file.path);
file.path = try gpa.dupe(u8, "/highlight.zig");
@@ -4483,8 +4680,8 @@ test "Zig keywords keep every rendered byte colored across line-number widths" {
var seen: usize = 0;
var if_position: ?struct { x: u16, y: u16 } = null;
const rect = p.rects[0];
- const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
- for (body_y..body_y + rect.h - pardes.BOX_H) |y| {
+ const body_y = bodyY(p, rect);
+ for (body_y..body_y + bodyH(p, rect)) |y| {
for (rect.x + config.GUTTER..rect.x + rect.w - 1) |x| {
const first = surface.at(@intCast(x), @intCast(y));
const second = surface.at(@intCast(x + 1), @intCast(y));
@@ -4533,7 +4730,7 @@ test "terminal overlays preserve trailing blank styles without coloring inserted
defer frame.deinit();
const rect = p.rects[0];
const x = rect.x + config.GUTTER;
- const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
+ const y = bodyY(p, rect);
const palette = p.theme().palette.?;
const colors = [_]pardes.Color{ .{ .rgb = palette[1] }, .{ .rgb = palette[2] }, .{ .rgb = palette[4] } };
const original = try p.render(frame.allocator());
diff --git a/test/pdf.zig b/test/pdf.zig
index 47f8a9e8..4cb8f3dd 100644
--- a/test/pdf.zig
+++ b/test/pdf.zig
@@ -343,7 +343,7 @@ test "PdfSections Look follows the exact owning PDF, not an equal path" {
const duplicate_id = p.freeSlot() orelse return error.NoDuplicatePdfSlot;
const duplicate = try panes.Pdf.openPane(p, duplicate_id, path, 0);
- pardes.exec.placeDoc(p, 0, duplicate_id, duplicate);
+ pardes.exec.placeNew(p, 0, 0, duplicate_id, .doc);
layout.compute(p);
panes.Pdf.openSections(p, duplicate_id);
const duplicate_output_id = duplicate.search_pane orelse return error.MissingDuplicatePdfSections;
@@ -883,7 +883,7 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" {
try std.testing.expect(pane.pdf.?.document_scroll_y > before_height_scroll);
const revision_before_search = active_raster.revision;
- try pardes.look.runSearch(p, 0, "Pardes", .text, .top);
+ _ = try pardes.look.runSearch(p, 0, "Pardes", .text, .top, true);
try std.testing.expectEqualStrings("Pardes", pane.pdf.?.search_query);
try std.testing.expectEqual(revision_before_search, active_raster.revision);
const results_id = pane.search_pane orelse return error.MissingPdfSearchResults;
@@ -1579,7 +1579,7 @@ test "PDF n/N addresses and reveals distinct MuPDF hits on one page" {
p.locations_config.context = 2;
p.locations_config.tscontext = true;
- try pardes.look.runSearch(p, 0, "Pardes", .text, .top);
+ _ = try pardes.look.runSearch(p, 0, "Pardes", .text, .top, true);
const results_id = pane.search_pane orelse return error.MissingPdfSearchResults;
const results = p.panes[results_id].?.file.?;
var found: [3]bool = @splat(false);
diff --git a/test/snapshot.zig b/test/snapshot.zig
index 53553657..2fa8e89c 100644
--- a/test/snapshot.zig
+++ b/test/snapshot.zig
@@ -264,6 +264,9 @@ pub fn main(init: std.process.Init) !void {
});
std.process.exit(1);
}
+ // Every retry passed: the captures of the failed attempts explain
+ // nothing any more, and /tmp is not where they should pile up.
+ if (retry_dir) |d| std.Io.Dir.cwd().deleteTree(init.io, d) catch {};
std.debug.print("all {d} snapshot scripts ok\n", .{scripts.len});
}
diff --git a/test/snapshots/badutf.golden b/test/snapshots/badutf.golden
index 0980612a..4490531a 100644
--- a/test/snapshots/badutf.golden
+++ b/test/snapshots/badutf.golden
@@ -7,10 +7,11 @@
| 3 truncated �� here
| 4 end
| 5
-== snap hscroll-midglyph grid=50x31 cursor=46,3
+== snap hscroll-midglyph grid=50x31 cursor=46,4
|Newcol Joincol Find Grep Help Changelog Tutor Dump
| New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/badutf/cwd/wide.txt Save Tty C
+| /tmp/pardes-snap/badutf/cwd/wide.txt Save Tty
+| Collapse Del
| 1 日本語 日本語 日本語 日本語 日本語 tail
| 2
| 3
diff --git a/test/snapshots/builtins.golden b/test/snapshots/builtins.golden
index de614ea2..00f53a1b 100644
--- a/test/snapshots/builtins.golden
+++ b/test/snapshots/builtins.golden
@@ -9,37 +9,74 @@
| 5 line 5
| 6 line 6
| 7
-== snap window-group grid=120x67 cursor=7,36
-|35: /tmp/pardes-snap/builtins/cwd/+Help Save Tty Collapse Del
-|36: 1 pardes builtins under SPC w
-|37: 2
-|38: 3 SPC w h Left C-w h, C-w left
-|39: 4 SPC w j Down C-w j, C-w down
-|40: 5 SPC w k Up C-w k, C-w up
-|41: 6 SPC w l Right C-w l, C-w right
-|42: 7
-== snap docs-group grid=120x67 cursor=7,36
-|36: 1 pardes builtins under SPC h
-|38: 3 SPC h c Changelog topbar
-|39: 4 SPC h m Messages
-|40: 5 SPC h t Tutor topbar
-|41: 6
-|42:
-== snap tutor-from-index grid=120x67 cursor=7,20
-|19: /Tutor Save Tty Collapse Del
-|20: 1
-|21: 2 PARDES TUTOR
-|22: 3 tmux + vi + acme, from scratch.
-|23: 4
-|24: 5 The model is acme's: EVERYTHING is text, editable and executable
-|25: 6 the same way. Modal editing (helix-style) lives on top of that.
-|26: 7 Press j until you reach the introduction.
-|27: 8
-|28: 9
-|29: 10 =================================================================
-|30: 11 = INTRODUCTION =
-|31: 12 =================================================================
-|32: 13
-|33: 14 A pane is just text on screen. There is no separate "buffer" type —
-|34: 15 what you see is what you edit. Some panes happen to be live terminals;
-|36: 1 pardes builtins under SPC h Tutor
+== snap window-group grid=120x67 cursor=7,10
+|9: /tmp/pardes-snap/builtins/cwd/+Help Save Tty Collapse Del
+|10: 1 pardes builtins under SPC w
+|11: 2
+|12: 3 SPC w h Left C-w h, C-w left
+|13: 4 SPC w j Down C-w j, C-w down
+|14: 5 SPC w k Up C-w k, C-w up
+|15: 6 SPC w l Right C-w l, C-w right
+|16: 7
+== snap docs-group grid=120x67 cursor=7,10
+|10: 1 pardes builtins under SPC h
+|12: 3 SPC h c Changelog topbar
+|13: 4 SPC h m Messages
+|14: 5 SPC h t Tutor topbar
+|15: 6
+|16:
+== snap tutor-from-index grid=120x67 cursor=7,16
+|10: 1 pardes builtins under SPC h Tutor
+|14: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+|15: /Tutor Save Tty Collapse Del
+|16: 1
+|17: 2 PARDES TUTOR
+|18: 3 tmux + vi + acme, from scratch.
+|19: 4
+|20: 5 The model is acme's: EVERYTHING is text, editable and executable
+|21: 6 the same way. Modal editing (helix-style) lives on top of that.
+|22: 7 Press j until you reach the introduction.
+|23: 8
+|24: 9
+|25: 10 =================================================================
+|26: 11 = INTRODUCTION =
+|27: 12 =================================================================
+|28: 13
+|29: 14 A pane is just text on screen. There is no separate "buffer" type —
+|30: 15 what you see is what you edit. Some panes happen to be live terminals;
+|31: 16 most are text you move a cursor over. Modal editing is layered on top.
+|32: 17
+|33: 18 Three modes (the BOX at the pane's top-left corner shows which):
+|34: 19 NORMAL block cursor, keys move and edit. The box is BLANK.
+|35: 20 ^ INSERT keys type text at the cursor.
+|36: 21 $ TTY keys go straight to the shell. (terminals only)
+|37: 22
+|38: 23 A bare `pardes` opens one shell already in TTY. Give it a file or a
+|39: 24 directory and you start in NORMAL.
+|40: 25
+|41: 26 SIX PARTS, ordered by what is most different from editors you know:
+|42: 27 1 — THE MOUSE. Acme's three buttons; nothing like vim.
+|43: 28 2 — PANES. Moving between them. Esc, Shift-Esc, Ctrl-w.
+|44: 29 3 — THE TTY. Raw input, prompt-aware Esc, and the mode tag.
+|45: 30 4 — DETACHED. The core outliving the terminal showing it.
+|46: 31 5 — THE KEYS. Helix-style modal, and where it differs.
+|47: 32 6 — THE REST. PDFs, images, the language backend, scripting.
+|48: 33
+|49: 34 PRACTICE BLOCKS (part 5): the "# keys:" line lists keystrokes
+|50: 35 (space-separated; esc/enter/bs are special, the rest type each char).
+|51: 36 The lines under "# before" are what you practice on; "# after" is what
+|52: 37 you should end up with. They are here to be TYPED and nothing runs
+|53: 38 them. What pins the real behaviour is `zig build hxdiff`, which replays
+|54: 39 test/hxcases against goldens recorded from a real helix, and
+|55: 40 `zig build hxparity`, which runs each case twice — once in a file pane,
+|56: 41 once in a shell — and demands the two agree.
+|57: 42
+|58: 43 Hold j to reach part 1.
+|59: 44
+|60: 45
+|61: 46 =================================================================
+|62: 47 = PART 1 — THE MOUSE (acme chording) =
+|63: 48 =================================================================
+|64: 49
+|65: 50 Three buttons, three verbs:
+|66: 51 LEFT select, and focus the pane. Also pins the cursor.
diff --git a/test/snapshots/builtins.snap b/test/snapshots/builtins.snap
index 8bf70f36..849c83d3 100644
--- a/test/snapshots/builtins.snap
+++ b/test/snapshots/builtins.snap
@@ -49,8 +49,8 @@ key space h ?
wait 10000 Tutor
stable 700 15000
snap docs-group
-press middle @Tutor 41
-release middle @Tutor 41
+press middle @Tutor 15
+release middle @Tutor 15
wait 10000 PARDES TUTOR
stable 700 15000
snap tutor-from-index
diff --git a/test/snapshots/chordcut.golden b/test/snapshots/chordcut.golden
index bff929b3..d6ea6879 100644
--- a/test/snapshots/chordcut.golden
+++ b/test/snapshots/chordcut.golden
@@ -1,11 +1,13 @@
-== snap opened grid=120x31 cursor=7,7
+== snap opened grid=120x31 cursor=7,9
|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/chordcut/cwd Tty+bash Save Mode Filter C /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C
+| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter
+| Collapse Del Collapse Del
| ls ls
| f.txt g.txt f.txt g.txt
|
-| /tmp/pardes-snap/chordcut/cwd/f.txt Save Tty Collapse Del
+| /tmp/pardes-snap/chordcut/cwd/f.txt Save Tty Collapse De
+| l
| 1 line 1
| 2 line 2
| 3 line 3
@@ -14,75 +16,79 @@
| 6
|
|
-|
-|
-| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C
+| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter
+| Collapse Del
| ls
| f.txt g.txt
-== snap cut grid=120x31 cursor=7,8
-|6: * /tmp/pardes-snap/chordcut/cwd/f.txt * Save Tty Collapse D
-|8: 2 2
-== snap copied grid=120x31 cursor=10,9
-== snap pointpaste grid=120x31 cursor=16,10
-|10: 4 line 4line
-== snap replaced grid=120x31 cursor=11,11
-|11: 5 lline 5
-== snap cancelexec grid=120x31 cursor=25,8
-|8: 2 2echo B''OO g.txt
-== snap execafter grid=120x31 cursor=25,8
-|6: * /tmp/pardes-snap/chordcut/cwd/f.txt * Save Tty Collapse D /tmp/pardes-snap/chordcut/cwd (echo B''OO) exit 0 Kill Sa
-|7: 1 line 1 Exec % echo B''OO
-|8: 2 2echo B''OO g.txt BOO
-|9: 3 line 3 exit 0
-== snap cancellook grid=120x31 cursor=25,8
-|7: 1 line 1 % echo B''OO
-== snap lookafter grid=120x31 cursor=7,13
-|12: /tmp/pardes-snap/chordcut/cwd/g.txt Save Tty Collapse Del
-|13: 1 line 1
-|14: 2 line 2
-|15: 3
+== snap cut grid=120x31 cursor=7,10
+|7: * /tmp/pardes-snap/chordcut/cwd/f.txt Save Tty Collapse De
+|10: 2 2
+== snap copied grid=120x31 cursor=10,11
+== snap pointpaste grid=120x31 cursor=16,12
+|12: 4 line 4line
+== snap replaced grid=120x31 cursor=11,13
+|13: 5 lline 5
+== snap cancelexec grid=120x31 cursor=25,10
+|10: 2 2echo B''OO g.txt
+== snap execafter grid=120x31 cursor=25,10
+|9: 1 line 1 Exec
+|17: /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/chordcut/cwd (echo B''OO) exit 0 Kill S
+|18: Collapse Del ave Collapse Del
+|19: ls % echo B''OO
+|20: f.txt g.txt BOO
+|21: exit 0
+== snap cancellook grid=120x31 cursor=25,10
+|9: 1 line 1
+== snap lookafter grid=120x31 cursor=7,24
+|22: /tmp/pardes-snap/chordcut/cwd/g.txt Save Tty Collapse De
+|23: l
+|24: 1 line 1
+|25: 2 line 2
+|26: 3
== style chordsel grid=120x31
|0: 0-70 #b2c3b8,#1d2824, 71-119 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-59 d,#26362d, 60-61 #ffffff,#887455, 62-62 d,#1d2824, 63-94 #b2c3b8,#1d2824, 95-119 d,#1d2824,
-|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-40 #d3b0d9,#1d2824, 41-59 #b2c3b8,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-92 #b2c3b8,#1d2824, 93-100 #d3b0d9,#1d2824, 101-119 #b2c3b8,#1d2824,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-40 #d3b0d9,#1d2824, 41-58 #b2c3b8,#1d2824, 59-59 d,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-92 #b2c3b8,#1d2824, 93-100 #d3b0d9,#1d2824, 101-118 #b2c3b8,#1d2824, 119-119 d,#1d2824,
+|3: 0-2 d,#1d2824, 3-14 #b2c3b8,#1d2824, 15-62 d,#1d2824, 63-74 #b2c3b8,#1d2824, 75-119 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|5: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410,
-|6: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-37 #d3b0d9,#1d2824, 38-59 #b2c3b8,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-119 #b2c3b8,#1d2824,
-|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-24 #e5eee5,#0d1410, 25-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|6: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410,
+|7: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-37 #d3b0d9,#1d2824, 38-58 #b2c3b8,#1d2824, 59-59 d,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|8: 0-2 d,#1d2824, 3-3 #b2c3b8,#1d2824, 4-59 d,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|11: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|12: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-37 #dfbce5,#26362d, 38-59 #becfc4,#26362d, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #f5f7ee,#3c614a, 11-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-24 #e5eee5,#0d1410, 25-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-40 #d3b0d9,#1d2824, 41-59 #b2c3b8,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-40 #d3b0d9,#1d2824, 41-58 #b2c3b8,#1d2824, 59-59 d,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-118 #b2c3b8,#1d2824, 119-119 d,#1d2824,
+|18: 0-2 d,#1d2824, 3-14 #b2c3b8,#1d2824, 15-62 d,#1d2824, 63-78 #b2c3b8,#1d2824, 79-119 d,#1d2824,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|20: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|22: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-37 #dfbce5,#26362d, 38-58 #becfc4,#26362d, 59-59 d,#26362d, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|23: 0-2 d,#26362d, 3-3 #becfc4,#26362d, 4-59 d,#26362d, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #f5f7ee,#3c614a, 11-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
== style clickdismiss grid=120x31
-|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-== snap ttyprobe grid=120x31 cursor=42,22
-|12: * /tmp/pardes-snap/chordcut/cwd/g.txt * Save Tty Collapse D
-|17: $ /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C
-|18: $ ls
-|20: $ stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[?
-|21: 1006h'; cat -v
-|22: ^[[<0;38;6M^[[<0;38;6m^[[200~line^[[201~
-== snap ttyprompt grid=120x31 cursor=8,24
-|23: $
-|24: $ line
+|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+== snap ttyprobe grid=120x31 cursor=42,23
+|17: $ /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/chordcut/cwd (echo B''OO) exit 0 Kill S
+|19: $ ls % echo B''OO
+|21: $ stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[? exit 0
+|22: 1006h'; cat -v
+|23: ^[[<0;38;6M^[[<0;38;6m^[[200~line^[[201~
+|24:
+|25:
+|26:
+== snap ttyprompt grid=120x31 cursor=8,25
+|24: $
+|25: $ line
diff --git a/test/snapshots/chordcut.snap b/test/snapshots/chordcut.snap
index 56170cce..36aad675 100644
--- a/test/snapshots/chordcut.snap
+++ b/test/snapshots/chordcut.snap
@@ -29,42 +29,42 @@ wait 10000 line 3
stable 700 15000
snap opened
# 1-2 Cut: left-drag over "line" on line 2, tap middle while holding
-press left 8 9
-drag left 11 9
-press middle 11 9
-release middle 11 9
-release left 11 9
+press left 8 11
+drag left 11 11
+press middle 11 11
+release middle 11 11
+release left 11 11
stable 400 5000
snap cut
# 1-2 then 1-3 in one hold = copy: line 3 keeps its text, register loaded
-press left 8 10
-drag left 11 10
-press middle 11 10
-release middle 11 10
-press right 11 10
-release right 11 10
-release left 11 10
+press left 8 12
+drag left 11 12
+press middle 11 12
+release middle 11 12
+press right 11 12
+release right 11 12
+release left 11 12
stable 400 5000
snap copied
# 1-3 at a bare click point: hold left past the end of line 4, tap right —
# the register splices in at the click
-press left 14 11
-press right 14 11
-release right 14 11
-release left 14 11
+press left 14 13
+press right 14 13
+release right 14 13
+release left 14 13
stable 400 5000
snap pointpaste
# 1-3 over a fresh selection replaces it with the register
-press left 9 12
-drag left 11 12
-press right 11 12
-release right 11 12
-release left 11 12
+press left 9 14
+drag left 11 14
+press right 11 14
+release right 11 14
+release left 11 14
stable 400 5000
snap replaced
# arm the cancel tests: append `echo B''OO g.txt` to line 2
-press left 10 9
-release left 10 9
+press left 10 11
+release left 10 11
stable 400 5000
key i
text echo B''OO g.txt
@@ -73,67 +73,72 @@ settle 100
stable 400 5000
# 2-3 cancel (acme textselect2): tap right during the middle execute-drag
# over `echo B''OO` — the command never runs (no BOO anywhere)
-press middle 10 9
-drag middle 19 9
-press right 19 9
-release right 19 9
-release middle 19 9
+press middle 10 11
+drag middle 19 11
+press right 19 11
+release right 19 11
+release middle 19 11
stable 700 8000
snap cancelexec
# the same drag released clean proves the word was live: BOO appears
-press middle 10 9
-drag middle 19 9
-release middle 19 9
+press middle 10 11
+drag middle 19 11
+release middle 19 11
wait 10000 BOO
stable 700 10000
snap execafter
# 3-1 cancel (acme textselect3): tap left during the right look-drag over
# g.txt — no pane opens
-press right 21 9
-drag right 25 9
-press left 25 9
-release left 25 9
-release right 25 9
+press right 21 11
+drag right 25 11
+press left 25 11
+release left 25 11
+release right 25 11
stable 700 8000
snap cancellook
# the same look released clean opens g.txt
-press right 21 9
-drag right 25 9
-release right 25 9
+press right 21 11
+drag right 25 11
+release right 25 11
wait 10000 line 2
stable 700 10000
snap lookafter
# a fresh left click dismisses the chord's leftover selection instead of
# dragging the anchored span to the click: copy `line` on g.txt line 1
# (styled snap shows the selection), then click the next row (it's gone)
-press left 8 14
-drag left 11 14
-press middle 11 14
-release middle 11 14
-press right 11 14
-release right 11 14
-release left 11 14
+press left 8 25
+drag left 11 25
+press middle 11 25
+release middle 11 25
+press right 11 25
+release right 11 25
+release left 11 25
stable 400 5000
snapstyle chordsel
-press left 8 15
-release left 8 15
+press left 8 26
+release left 8 26
stable 400 5000
snapstyle clickdismiss
+# g.txt opened under the lower shell's text and left it three rows: close it,
+# and the shell grows back into its rows, for what follows to run in
+press middle 59 23
+release middle 59 23
+stable 700 15000
# tty paste chord: a mouse-listening app gets the click forwarded (SGR,
# body-relative) then the register between bracketed-paste markers —
# cat -v prints the received bytes visibly
-press left 35 22
-release left 35 22
+press left 35 23
+release left 35 23
stable 400 5000
key c-b
stable 600 8000
text stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[?1006h'; cat -v
key enter
stable 700 8000
-press left 40 24
-press right 40 24
-release right 40 24
-release left 40 24
+press left 40 25
+press right 40 25
+release right 40 25
+release left 40 25
stable 700 8000
snap ttyprobe
# a plain prompt (mouse modes off) gets no click bytes — just the paste;
@@ -143,9 +148,9 @@ stable 600 8000
text stty sane; printf '\x1b[?1000l\x1b[?1006l'
key enter
stable 600 8000
-press left 40 24
-press right 40 24
-release right 40 24
-release left 40 24
+press left 40 25
+press right 40 25
+release right 40 25
+release left 40 25
stable 700 8000
snap ttyprompt
diff --git a/test/snapshots/cmdexit.golden b/test/snapshots/cmdexit.golden
index de95c383..c5c314b9 100644
--- a/test/snapshots/cmdexit.golden
+++ b/test/snapshots/cmdexit.golden
@@ -10,6 +10,6 @@
|19: BGSTARTED
|20: exit 0
== snap letgo grid=120x31 cursor=7,3
-|17: /tmp/pardes-snap/cmdexit/cwd (exec </dev/null >/dev/null 2>&1; sleep 1; false) exit 1 Kill Save Collapse Del
-|21: % exec </dev/null >/dev/null 2>&1; sleep 1; false
-|22: exit 1
+|24: /tmp/pardes-snap/cmdexit/cwd (exec </dev/null >/dev/null 2>&1; sleep 1; false) exit 1 Kill Save Collapse Del
+|25: % exec </dev/null >/dev/null 2>&1; sleep 1; false
+|26: exit 1
diff --git a/test/snapshots/cmdexit.snap b/test/snapshots/cmdexit.snap
index ff60a881..e9f39bfd 100644
--- a/test/snapshots/cmdexit.snap
+++ b/test/snapshots/cmdexit.snap
@@ -16,8 +16,10 @@ release middle 34 4
wait 10000 exit 0
stable 700 15000
snap background
-# line 2, in the same pane now its command is done: its end of file comes at
-# once, and its exit a second later, from `false`
+# line 2, in a second pane: the first's command is done but its background
+# `sleep 30` lives on (job control gives it its own group) and still holds
+# that pane's pty; here the end of file comes at once, and the exit a second
+# later, from `false`
press middle 7 5
drag middle 54 5
release middle 54 5
diff --git a/test/snapshots/cmdword.golden b/test/snapshots/cmdword.golden
index eec32bcb..748bf06b 100644
--- a/test/snapshots/cmdword.golden
+++ b/test/snapshots/cmdword.golden
@@ -13,16 +13,16 @@
|21: % echo ZZ''L
|22: ZZL
|23: exit 0
-== snap lookword grid=120x31 cursor=7,11
-|10: /tmp/pardes-snap/cmdword/cwd/p.txt Save Tty Collapse Del
-|11: 1 just text
-== snap nested grid=120x31 cursor=4,9
-|6: /tmp/pardes-snap/cmdword/cwd Tty+bash Save Mode Filter Collapse Del
-|7: ls
-|8: cmds.txt p.txt zzmark.txt
-== snap execword grid=120x31 cursor=4,9
-|6: /tmp/pardes-snap/cmdword/cwd Tty+bash Save Mode Filter Collapse Del Exec
-== snap tagcmd grid=120x31 cursor=4,7
-|6: /tmp/pardes-snap/cmdword/cwd Tty+bash Save Mode Filter Collapse Del Exec @`echo ZZ''T`
-|7: echo ZZ''T
-|8: ZZT
+== snap lookword grid=120x31 cursor=7,6
+|5: /tmp/pardes-snap/cmdword/cwd/p.txt Save Tty Collapse Del
+|6: 1 just text
+== snap nested grid=120x31 cursor=4,11
+|8: /tmp/pardes-snap/cmdword/cwd Tty+bash Save Mode Filter Collapse Del
+|9: ls
+|10: cmds.txt p.txt zzmark.txt
+== snap execword grid=120x31 cursor=4,11
+|8: /tmp/pardes-snap/cmdword/cwd Tty+bash Save Mode Filter Collapse Del Exec
+== snap tagcmd grid=120x31 cursor=4,11
+|8: /tmp/pardes-snap/cmdword/cwd Tty+bash Save Mode Filter Collapse Del Exec @`echo ZZ''T`
+|11: echo ZZ''T
+|12: ZZT
diff --git a/test/snapshots/colgrip.golden b/test/snapshots/colgrip.golden
index dd16074d..95a2390f 100644
--- a/test/snapshots/colgrip.golden
+++ b/test/snapshots/colgrip.golden
@@ -1,11 +1,11 @@
== style rest grid=100x24
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-49 d,#1d2824, 50-51 #ffffff,#887455, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d,
-|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-89 #dfbce5,#26362d, 90-99 #becfc4,#26362d,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-89 #dfbce5,#26362d, 90-98 #becfc4,#26362d, 99-99 d,#26362d,
+|3: 0-2 d,#1d2824, 3-12 #b2c3b8,#1d2824, 13-49 d,#1d2824, 50-52 d,#26362d, 53-73 #becfc4,#26362d, 74-99 d,#26362d,
+|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
@@ -25,10 +25,11 @@
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
== style held grid=100x24
|1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-49 d,#1d2824, 50-51 #000000,#f4c086, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d,
-== snap resize-drag grid=100x24 cursor=54,5
+== snap resize-drag grid=100x24 cursor=54,6
|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/colgrip/cwd/w.txt S╎ve Tty Col /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode
+| /tmp/pardes-snap/colgrip/cwd/w.txt S╎ve Tty Co /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mod
+| llapse Del ╎ e Filter Collapse Del
| 1 x ╎ ls
| ╎ w.txt
| ╎
@@ -49,14 +50,13 @@
| ╎
| ╎
| ╎
-| ╎
== style resize-drag grid=100x24
|1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-38 d,#1d2824, 39-39 #f4c086,#1d2824, 40-49 d,#1d2824, 50-51 #000000,#f4c086, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d,
-|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-38 #b2c3b8,#1d2824, 39-39 #f4c086,#1d2824, 40-49 #b2c3b8,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-89 #dfbce5,#26362d, 90-99 #becfc4,#26362d,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-38 #b2c3b8,#1d2824, 39-39 #f4c086,#1d2824, 40-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-89 #dfbce5,#26362d, 90-98 #becfc4,#26362d, 99-99 d,#26362d,
+|3: 0-2 d,#1d2824, 3-12 #b2c3b8,#1d2824, 13-38 d,#1d2824, 39-39 #f4c086,#1d2824, 40-49 d,#1d2824, 50-52 d,#26362d, 53-73 #becfc4,#26362d, 74-99 d,#26362d,
+|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
@@ -74,12 +74,12 @@
|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-== snap resize-done grid=100x24 cursor=44,5
+== snap resize-done grid=100x24 cursor=44,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/colgrip/cwd/w.txt Sa /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter Co
-|3: 1 x ls
-|4: w.txt
-|5:
+|2: /tmp/pardes-snap/colgrip/cwd/w.txt S /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter C
+|3: ave Tty Collapse Del ollapse Del
+|4: 1 x ls
+|5: w.txt
|6:
|7:
|8:
@@ -98,10 +98,9 @@
|21:
|22:
|23:
-== snap reorder-drag grid=100x24 cursor=44,5
-|3: ╎ 1 x ls
-|4: ╎ w.txt
-|5: ╎
+== snap reorder-drag grid=100x24 cursor=44,6
+|4: ╎ 1 x ls
+|5: ╎ w.txt
|6: ╎
|7: ╎
|8: ╎
@@ -122,11 +121,11 @@
|23: ╎
== style reorder-drag grid=100x24
|1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-39 d,#1d2824, 40-41 #000000,#f4c086, 42-42 d,#26362d, 43-74 #becfc4,#26362d, 75-99 d,#26362d,
-|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-39 #b2c3b8,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-71 #becfc4,#26362d, 72-79 #dfbce5,#26362d, 80-99 #becfc4,#26362d,
-|3: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-|4: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-|5: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-99 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-38 #b2c3b8,#1d2824, 39-39 d,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-71 #becfc4,#26362d, 72-79 #dfbce5,#26362d, 80-98 #becfc4,#26362d, 99-99 d,#26362d,
+|3: 0-2 d,#1d2824, 3-22 #b2c3b8,#1d2824, 23-39 d,#1d2824, 40-42 d,#26362d, 43-53 #becfc4,#26362d, 54-99 d,#26362d,
+|4: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|6: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-99 d,#0d1410,
|7: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
@@ -144,12 +143,12 @@
|21: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|22: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|23: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-== snap reorder-done grid=100x24 cursor=4,5
+== snap reorder-done grid=100x24 cursor=4,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter Co /tmp/pardes-snap/colgrip/cwd/w.txt Sa
-|3: ls 1 x
-|4: w.txt
-|5:
+|2: /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter C /tmp/pardes-snap/colgrip/cwd/w.txt S
+|3: ollapse Del ave Tty Collapse Del
+|4: ls 1 x
+|5: w.txt
|6:
|7:
|8:
diff --git a/test/snapshots/cornerdrag-left.golden b/test/snapshots/cornerdrag-left.golden
index c9ccf859..e0d2ab51 100644
--- a/test/snapshots/cornerdrag-left.golden
+++ b/test/snapshots/cornerdrag-left.golden
@@ -1,11 +1,13 @@
-== snap right-split grid=100x31 cursor=54,9
+== snap right-split grid=100x31 cursor=54,11
|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/cornerdrag-left/cwd Tty+bash S /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
+| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
+| Save Mode Filter Collapse Del Save Mode Filter Collapse Del
| ls ls
| w.txt w.txt
|
-| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
+| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
+| Save Mode Filter Collapse Del
| ls
| w.txt
|
@@ -14,22 +16,21 @@
|
|
|
-|
-|
-| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
+| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
+| Save Mode Filter Collapse Del
| ls
| w.txt
-== snap hover-corner grid=100x31 cursor=54,9
+== snap hover-corner grid=100x31 cursor=54,11
|1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
-|3: ls ╎ ls
-|4: w.txt ╎ w.txt
-|5: ╎
-|6: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
-|7: ╎ ls
-|8: ╎ w.txt
-|9: ╎
-|10: ╎
+|2: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
+|3: Save Mode Filter Collapse Del ╎ Save Mode Filter Collapse Del
+|4: ls ╎ ls
+|5: w.txt ╎ w.txt
+|6: ╎
+|7: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
+|8: ╎ Save Mode Filter Collapse Del
+|9: ╎ ls
+|10: ╎ w.txt
|11: ╎
|12: ╎
|13: ╎
@@ -37,9 +38,9 @@
|15: ╎
|16: ╎
|17: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash ╎
-|18: ls ╎
-|19: w.txt ╎
-|20: ╎
+|18: Save Mode Filter Collapse Del ╎
+|19: ls ╎
+|20: w.txt ╎
|21: ╎
|22: ╎
|23: ╎
@@ -50,28 +51,28 @@
|28: ╎
|29: ╎
|30: ╎
-== snap hover-left-corner grid=100x31 cursor=54,9
-== snap corner-drag grid=100x31 cursor=54,9
+== snap hover-left-corner grid=100x31 cursor=54,11
+== snap corner-drag grid=100x31 cursor=54,11
|1: New Tty Find Grep Joincol ╎elcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash S /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
-|3: ls ╎ ls
-|4: w.txt ╎ w.txt
-|5: ╎
-|6: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
-|7: ╎ ls
-|8: ╎ w.txt
-|9: ╎
-|10: ╎
+|2: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
+|3: Save Mode Filter Collapse ╎el Save Mode Filter Collapse Del
+|4: ls ╎ ls
+|5: w.txt ╎ w.txt
+|6: ╎
+|7: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
+|8: ╎ Save Mode Filter Collapse Del
+|9: ╎ ls
+|10: ╎ w.txt
|11: ╎
-|12: ╎╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+|12: ╎
|13: ╎
|14: ╎
|15: ╎
|16: ╎
-|17: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash S
-|18: ls ╎
-|19: w.txt ╎
-|20: ╎
+|17: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash
+|18: Save Mode Filter Collapse ╎el
+|19: ls ╎
+|20: w.txt ╎
|21: ╎
|22: ╎
|23: ╎
@@ -82,28 +83,28 @@
|28: ╎
|29: ╎
|30: ╎
-== snap corner-done grid=100x31 cursor=34,16
+== snap corner-done grid=100x31 cursor=34,11
|1: New Tty Find Grep Joincol D New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Coll
-|3: ls ls
-|4: w.txt w.txt
-|5:
-|6:
-|7:
-|8:
-|9:
-|10:
+|2: /tmp/pardes-snap/cornerdra /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Col
+|3: g-left/cwd Tty+bash Save M lapse Del
+|4: ode Filter Collapse Del ls
+|5: ls w.txt
+|6: w.txt
+|7: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Col
+|8: lapse Del
+|9: ls
+|10: w.txt
|11:
|12:
-|13: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Coll
-|14: ls
-|15: w.txt
+|13:
+|14:
+|15:
|16:
-|17: /tmp/pardes-snap/cornerdrag
-|18: ls
-|19: w.txt
-|20:
-|21:
+|17: /tmp/pardes-snap/cornerdra
+|18: g-left/cwd Tty+bash Save M
+|19: ode Filter Collapse Del
+|20: ls
+|21: w.txt
|22:
|23:
|24:
diff --git a/test/snapshots/cornerdrag.golden b/test/snapshots/cornerdrag.golden
index fc212219..d70e41e4 100644
--- a/test/snapshots/cornerdrag.golden
+++ b/test/snapshots/cornerdrag.golden
@@ -1,7 +1,8 @@
-== snap hover-corner grid=100x31 cursor=4,5
+== snap hover-corner grid=100x31 cursor=4,6
|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/cornerdrag/cwd Tty+bash Save ╎ /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save M
+| /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save ╎ /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save
+| Mode Filter Collapse Del ╎ Mode Filter Collapse Del
| ls ╎ ls
| w.txt ╎ w.txt
| ╎
@@ -15,8 +16,8 @@
| ╎
| ╎
| ╎
-| ╎
| /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save ╎
+| Mode Filter Collapse Del ╎
| ls ╎
| w.txt ╎
| ╎
@@ -29,13 +30,12 @@
| ╎
| ╎
| ╎
-| ╎
-== snap edge-drag grid=100x31 cursor=4,5
+== snap edge-drag grid=100x31 cursor=4,6
|1: New Tty Find Grep Joincol ╎elcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save M /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save M
-|3: ls ╎ ls
-|4: w.txt ╎ w.txt
-|5: ╎
+|2: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save
+|3: Mode Filter Collapse Del ╎ Mode Filter Collapse Del
+|4: ls ╎ ls
+|5: w.txt ╎ w.txt
|6: ╎
|7: ╎
|8: ╎
@@ -47,10 +47,10 @@
|14: ╎
|15: ╎
|16: ╎
-|17: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save M
-|18: ls ╎
-|19: w.txt ╎
-|20: ╎
+|17: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save
+|18: Mode Filter Collapse Del ╎
+|19: ls ╎
+|20: w.txt ╎
|21: ╎
|22: ╎
|23: ╎
@@ -61,13 +61,13 @@
|28: ╎
|29: ╎
|30: ╎
-== snap edge-done grid=100x31 cursor=4,5
+== snap edge-done grid=100x31 cursor=4,7
|1: New Tty Find Grep Joincol D New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter Collapse
-|3: ls ls
-|4: w.txt w.txt
-|5:
-|6:
+|2: /tmp/pardes-snap/cornerdra /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter Collapse
+|3: g/cwd Tty+bash Save Mode F Del
+|4: ilter Collapse Del ls
+|5: ls w.txt
+|6: w.txt
|7:
|8:
|9:
@@ -78,11 +78,11 @@
|14:
|15:
|16:
-|17: /tmp/pardes-snap/cornerdrag
-|18: ls
-|19: w.txt
-|20:
-|21:
+|17: /tmp/pardes-snap/cornerdra
+|18: g/cwd Tty+bash Save Mode F
+|19: ilter Collapse Del
+|20: ls
+|21: w.txt
|22:
|23:
|24:
@@ -92,13 +92,13 @@
|28:
|29:
|30:
-== snap corner-drag grid=100x31 cursor=4,5
+== snap corner-drag grid=100x31 cursor=4,7
|1: New Tty Find Grep Joincol D New Tty Find Grep Joincol ╎elcol
-|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save Mode Filter Collapse
-|3: ls ls ╎
-|4: w.txt w.txt ╎
-|5: ╎
-|6: ╎
+|2: /tmp/pardes-snap/cornerdra /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save Mode Filter Collapse
+|3: g/cwd Tty+bash Save Mode F Del ╎
+|4: ilter Collapse Del ls ╎
+|5: ls w.txt ╎
+|6: w.txt ╎
|7: ╎
|8: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|9: ╎
@@ -109,11 +109,11 @@
|14: ╎
|15: ╎
|16: ╎
-|17: /tmp/pardes-snap/cornerdrag ╎
-|18: ls ╎
-|19: w.txt ╎
-|20: ╎
-|21: ╎
+|17: /tmp/pardes-snap/cornerdra ╎
+|18: g/cwd Tty+bash Save Mode F ╎
+|19: ilter Collapse Del ╎
+|20: ls ╎
+|21: w.txt ╎
|22: ╎
|23: ╎
|24: ╎
@@ -123,19 +123,19 @@
|28: ╎
|29: ╎
|30: ╎
-== snap corner-done grid=100x31 cursor=4,5
+== snap corner-done grid=100x31 cursor=4,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag/cwd Tty+b
-|3: ls ls
-|4: w.txt w.txt
-|5:
+|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte /tmp/pardes-snap/cornerdrag/cwd Tty+
+|3: r Collapse Del bash Save Mode Filter Collapse Del
+|4: ls ls
+|5: w.txt w.txt
|6:
|7:
|8:
-|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter
-|10: ls
-|11: w.txt
-|12:
+|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte
+|10: r Collapse Del
+|11: ls
+|12: w.txt
|13:
|14:
|15:
@@ -154,19 +154,19 @@
|28:
|29:
|30:
-== snap clamp-x grid=100x31 cursor=4,5
+== snap clamp-x grid=100x31 cursor=4,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol D╎lcol
-|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag╎cwd Tty+b
-|3: ls ls ╎
-|4: w.txt w.txt ╎
-|5: ╎
+|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte /tmp/pardes-snap/cornerdrag╎cwd Tty+
+|3: r Collapse Del bash Save Mode Filter Colla╎se Del
+|4: ls ls ╎
+|5: w.txt w.txt ╎
|6: ╎
|7: ╎
|8: ╎
-|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter ╎
-|10: ls ╎
-|11: w.txt ╎
-|12: ╎
+|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte ╎
+|10: r Collapse Del ╎
+|11: ls ╎
+|12: w.txt ╎
|13: ╎
|14: ╎
|15: ╎
@@ -185,19 +185,19 @@
|28: ╎
|29: ╎
|30: ╎
-== snap clamp-y grid=100x31 cursor=4,5
+== snap clamp-y grid=100x31 cursor=4,6
|1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte╎ /tmp/pardes-snap/cornerdrag/cwd Tty+b
-|3: ls ╎ ls
-|4: w.txt ╎ w.txt
-|5: ╎
+|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte╎ /tmp/pardes-snap/cornerdrag/cwd Tty+
+|3: r Collapse Del ╎ bash Save Mode Filter Collapse Del
+|4: ls ╎ ls
+|5: w.txt ╎ w.txt
|6: ╎
|7: ╎
|8: ╎
|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte╎
-|10: ls ╎
-|11: w.txt ╎
-|12: ╎
+|10: r Collapse Del ╎
+|11: ls ╎
+|12: w.txt ╎
|13: ╎
|14: ╎
|15: ╎
@@ -216,12 +216,12 @@
|28: ╎
|29: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|30: ╎
-== snap clamp-done grid=100x31 cursor=4,5
+== snap clamp-done grid=100x31 cursor=4,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag/cwd Tty+b
-|3: ls ls
-|4: w.txt w.txt
-|5:
+|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte /tmp/pardes-snap/cornerdrag/cwd Tty+
+|3: r Collapse Del bash Save Mode Filter Collapse Del
+|4: ls ls
+|5: w.txt w.txt
|6:
|7:
|8:
@@ -246,4 +246,4 @@
|27:
|28:
|29:
-|30: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter
+|30: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte
diff --git a/test/snapshots/delask.golden b/test/snapshots/delask.golden
index 35562f00..ed5dd66a 100644
--- a/test/snapshots/delask.golden
+++ b/test/snapshots/delask.golden
@@ -18,7 +18,7 @@
|
|
|
-| * /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del
+| * /tmp/pardes-snap/delask/cwd/+New Save Tty Collapse Del
| 1 middle-pane
|
|
@@ -27,7 +27,7 @@
|
|
|
-| * /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del
+| * /tmp/pardes-snap/delask/cwd/+New Save Tty Collapse Del
| 1 bottom-pane Up
== snap asked grid=100x36 cursor=18,20
|20: 1 middle-pane Del: give space to k above j below
@@ -40,17 +40,17 @@
|29:
== snap typed grid=100x36 cursor=27,20
|20: 1 bottom-pane DelBelow
-|28: * /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del
+|28: * /tmp/pardes-snap/delask/cwd/+New Save Tty Collapse Del
|29: 1 third-pane
== snap below-took-click grid=100x36 cursor=17,20
|20: 1 third-pane
|28:
|29:
-== snap four grid=100x36 cursor=7,12
+== snap four grid=100x36 cursor=7,29
|3: 1 top-marker New
-|11: /tmp/pardes-snap/delask/cwd/+New Save Tty Collapse Del
-|12: 1
-== snap mouse-del grid=100x36 cursor=9,3
+|28: /tmp/pardes-snap/delask/cwd/+New Save Tty Collapse Del
+|29: 1
+== snap mouse-del grid=100x36 cursor=7,29
|3: 1 top-marker
-|11:
-|12:
+|19:
+|20:
diff --git a/test/snapshots/delask.snap b/test/snapshots/delask.snap
index 04bc850c..9e120f9f 100644
--- a/test/snapshots/delask.snap
+++ b/test/snapshots/delask.snap
@@ -57,15 +57,17 @@ press middle @DelBelow 21
release middle @DelBelow 21
stable 700 10000
snap below-took-click
-# a fresh pane between the file and the third one, closed by a middle click
-# on its tag's Del: no question, and the rows go up as they always have
+# a fresh pane at the foot of the column (acme's New takes the bottom half of
+# the last pane), and the third one, now between the file and it, closed by a
+# middle click on its tag's Del: no question, and the rows go up as they
+# always have
press left 10 5
release left 10 5
stable 400 5000
key space f n
stable 400 5000
snap four
-press middle @Del 12
-release middle @Del 12
+press middle @Del 20
+release middle @Del 20
stable 700 10000
snap mouse-del
diff --git a/test/snapshots/doccol.golden b/test/snapshots/doccol.golden
index fe21d23a..078f1c04 100644
--- a/test/snapshots/doccol.golden
+++ b/test/snapshots/doccol.golden
@@ -1,21 +1,25 @@
-== snap boot grid=100x31 cursor=7,3
+== snap boot grid=100x31 cursor=7,4
|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/doccol/cwd/a.txt Save Tty Coll /tmp/pardes-snap/doccol/cwd Tty+bash Save Mode
+| /tmp/pardes-snap/doccol/cwd/a.txt Save Tty Col /tmp/pardes-snap/doccol/cwd Tty+bash Save Mode
+| lapse Del Filter Collapse Del
| 1 AAA ls
| a.txt b.txt c.txt
-== snap two-docs grid=100x31 cursor=7,18
-|17: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Coll
-|18: 1 BBB
-|19: 2 B-TWO
-== snap moved grid=100x31 cursor=57,21
+== snap two-docs grid=100x31 cursor=7,19
+|17: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Col
+|18: lapse Del
+|19: 1 BBB
+|20: 2 B-TWO
+== snap moved grid=100x31 cursor=57,22
|17:
|18:
|19:
-|20: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Coll
-|21: 1 BBB
-|22: 2 B-TWO
-== snap worked grid=100x31 cursor=59,22
-== snap third grid=100x31 cursor=57,27
-|26: /tmp/pardes-snap/doccol/cwd/c.txt Save Tty Coll
-|27: 1 CCC
+|20: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Col
+|21: lapse Del
+|22: 1 BBB
+|23: 2 B-TWO
+== snap worked grid=100x31 cursor=59,23
+== snap third grid=100x31 cursor=57,28
+|26: /tmp/pardes-snap/doccol/cwd/c.txt Save Tty Col
+|27: lapse Del
+|28: 1 CCC
diff --git a/test/snapshots/doccol.snap b/test/snapshots/doccol.snap
index 2135c533..3a9befa3 100644
--- a/test/snapshots/doccol.snap
+++ b/test/snapshots/doccol.snap
@@ -17,6 +17,8 @@ file c.txt CCC
# 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
+# pins the pardes placement rule (Placement pardes), which it covers
+config Placement pardes
start 31 100 a.txt
wait 8000 AAA
stable 700 20000
@@ -29,8 +31,8 @@ stable 400 5000
snap boot
# look b.txt in the shell's ls output: the second doc splits the first, so
# both docs are in the left column and a.txt is the lower-numbered pane
-press right 60 5
-release right 60 5
+press right 60 6
+release right 60 6
wait 10000 BBB
stable 700 20000
snap two-docs
@@ -43,17 +45,17 @@ stable 700 15000
snap moved
# use a.txt, then b.txt: what counts is the doc last USED, so a.txt being the
# most recent for a moment must not survive the click into b.txt
-press left 5 4
-release left 5 4
+press left 5 5
+release left 5 5
stable 400 5000
-press left 60 22
-release left 60 22
+press left 60 23
+release left 60 23
key j
stable 400 5000
snap worked
# look c.txt from the shell: below b.txt, in the right column
-press right 67 5
-release right 67 5
+press right 67 6
+release right 67 6
wait 10000 CCC
stable 700 20000
snap third
diff --git a/test/snapshots/dump.golden b/test/snapshots/dump.golden
index f061e7be..deed689c 100644
--- a/test/snapshots/dump.golden
+++ b/test/snapshots/dump.golden
@@ -27,15 +27,19 @@
.{
.kind = .terminal,
.tag = "/tmp/pardes-snap/dump/cwd Tty+bash Save Mode Filter Collapse Del",
- .body = "ls\nd.txt\n\n\n\n typed-marker\n\n\n\n\n\n\n\n",
+ .body = "ls\nd.txt\n\n\n\n typed-marker\n\n\n\n\n\n\n",
.scroll = 0,
.cols = 48,
- .rows = 14,
+ .rows = 13,
.vweight = 1,
.collapsed = false,
+ .serial = 1,
.tag_tail = null,
.terminal = .{
.cwd = "/tmp/pardes-snap/dump/cwd",
+ .shell = "",
+ .command = "",
+ .status = null,
.stream = "ls\nd.txt\n",
.stream_b64 = <elided>
.stream_is_screen = true,
@@ -47,15 +51,19 @@
.{
.kind = .terminal,
.tag = "/tmp/pardes-snap/dump/cwd Tty+bash Save Mode Filter Collapse Del",
- .body = "ls\nd.txt\n\n\n\n\n\n\n\n\n\n\n",
+ .body = "ls\nd.txt\n\n\n\n\n\n\n\n\n\n",
.scroll = 0,
.cols = 48,
- .rows = 13,
+ .rows = 12,
.vweight = 1,
.collapsed = false,
+ .serial = 2,
.tag_tail = null,
.terminal = .{
.cwd = "/tmp/pardes-snap/dump/cwd",
+ .shell = "",
+ .command = "",
+ .status = null,
.stream = "ls\nd.txt\n",
.stream_b64 = <elided>
.stream_is_screen = true,
@@ -67,15 +75,19 @@
.{
.kind = .terminal,
.tag = "/tmp/pardes-snap/dump/cwd Tty+bash Save Mode Filter Collapse Del",
- .body = "ls\nd.txt\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",
+ .body = "ls\nd.txt\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",
.scroll = 0,
.cols = 48,
- .rows = 28,
+ .rows = 27,
.vweight = 1,
.collapsed = false,
+ .serial = 3,
.tag_tail = null,
.terminal = .{
.cwd = "/tmp/pardes-snap/dump/cwd",
+ .shell = "",
+ .command = "",
+ .status = null,
.stream = "ls\nd.txt\n",
.stream_b64 = <elided>
.stream_is_screen = true,
diff --git a/test/snapshots/dump.snap b/test/snapshots/dump.snap
index 485c4932..29e7b09e 100644
--- a/test/snapshots/dump.snap
+++ b/test/snapshots/dump.snap
@@ -3,8 +3,8 @@ file d.txt x
start 31 100 -n 3
wait 8000 d.txt
stable 700 20000
-press left 5 9
-release left 5 9
+press left 5 10
+release left 5 10
stable 400 5000
key i
text typed-marker
diff --git a/test/snapshots/edit.golden b/test/snapshots/edit.golden
index 63388124..d4ec9581 100644
--- a/test/snapshots/edit.golden
+++ b/test/snapshots/edit.golden
@@ -1,14 +1,14 @@
== snap inserted grid=100x31 cursor=16,10
|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/edit/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi
+| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode F /tmp/pardes-snap/edit/cwd Tty+bash Save Mode F
+| ilter Collapse Del ilter Collapse Del
| ls ls
| e.txt e.txt
|
|
|
|
-|
| hello-insert
|
|
@@ -16,17 +16,18 @@
|
|
|
-| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi
+| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode F
+| ilter Collapse Del
| ls
| e.txt
== style inserted grid=100x31
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-86 #d3b0d9,#1d2824, 87-99 #b2c3b8,#1d2824,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-86 #d3b0d9,#1d2824, 87-98 #b2c3b8,#1d2824, 99-99 d,#1d2824,
+|3: 0-2 d,#26362d, 3-20 #becfc4,#26362d, 21-49 d,#26362d, 50-52 d,#1d2824, 53-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|5: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|6: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
@@ -37,11 +38,11 @@
|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|18: 0-2 d,#1d2824, 3-20 #b2c3b8,#1d2824, 21-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
diff --git a/test/snapshots/emptycol.golden b/test/snapshots/emptycol.golden
new file mode 100644
index 00000000..37415cd7
--- /dev/null
+++ b/test/snapshots/emptycol.golden
@@ -0,0 +1,17 @@
+== snap newcol grid=100x24 cursor=53,1
+|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/emptycol/cwd/notes.txt Save T
+| ty Collapse Del
+| 1 one
+| 2 two
+== snap filled grid=100x24 cursor=54,6
+|2: /tmp/pardes-snap/emptycol/cwd/notes.txt Save T /tmp/pardes-snap/emptycol/cwd Tty+bash Save Mo
+|3: ty Collapse Del de Filter Collapse Del
+|4: 1 one ls
+|5: 2 two notes.txt
+== snap emptied grid=100x24 cursor=53,1
+|2: /tmp/pardes-snap/emptycol/cwd/notes.txt Save T
+|3: ty Collapse Del
+|4: 1 one
+|5: 2 two
diff --git a/test/snapshots/emptycol.snap b/test/snapshots/emptycol.snap
new file mode 100644
index 00000000..edf61eed
--- /dev/null
+++ b/test/snapshots/emptycol.snap
@@ -0,0 +1,22 @@
+# A column can hold no pane, as acme's can (cols.c:186). Newcol makes one
+# empty, its tag over blank space and holding the keyboard; the next pane
+# made while it is the active column fills it whole (makenewwindow,
+# util.c:468-469); and a shell that exits there leaves it empty again, the
+# keyboard back on its tag.
+file notes.txt one\ntwo
+config Verbose off
+start 24 100 notes.txt
+wait 8000 notes.txt
+stable 700 20000
+key space c n
+stable 400 5000
+snap newcol
+key a-n
+stable 700 15000
+snap filled
+key c-b
+stable 400 5000
+text exit
+key enter
+stable 700 10000
+snap emptied
diff --git a/test/snapshots/exec-bigdrag.golden b/test/snapshots/exec-bigdrag.golden
index 87d3b9bb..c74fd8f9 100644
--- a/test/snapshots/exec-bigdrag.golden
+++ b/test/snapshots/exec-bigdrag.golden
@@ -2,60 +2,60 @@
|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/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del
-| ls bash: line: command not found
-| big.txt 4line 2705line 2706line 2707line 2708line 2709line 2710line 2711line 2712line 2713line 2714lin
-| bash: 4line: command not found
-| 15line 2716line 2717line 2718line 2719line 2720line 2721line 2722line 2723line 2724line 2725li
-| bash: 15line: command not found
-| 726line 2727line 2728line 2729line 2730line 2731line 2732line 2733line 2734line 2735line 2736l
-| bash: 726line: command not found
-| 2737line 2738line 2739line 2740line 2741line 2742line 2743line 2744line 2745line 2746line 2747
-| bash: 2737line: command not found
-| 2748line 2749line 2750line 2751line 2752line 2753line 2754line 2755line 2756line 2757line 275
-| bash: 2748line: command not found
-| e 2759line 2760line 2761line 2762line 2763line 2764line 2765line 2766line 2767line 2768line 27
+| ls 2
+| big.txt bash: 91line: command not found
+| 802line 2803line 2804line 2805line 2806line 2807line 2808line 2809line 2810line 2811line 2812lin
+| e
+| bash: 802line: command not found
+| 2813line 2814line 2815line 2816line 2817line 2818line 2819line 2820line 2821line 2822line 2823li
+| ne
+| bash: 2813line: command not found
+| 2824line 2825line 2826line 2827line 2828line 2829line 2830line 2831line 2832line 2833line 2834l
+| in
+| bash: 2824line: command not found
+| e 2835line 2836line 2837line 2838line 2839line 2840line 2841line 2842line 2843line 2844line 2845
+| li
| bash: e: command not found
-| ne 2770line 2771line 2772line 2773line 2774line 2775line 2776line 2777line 2778line 2779line 2
+| ne 2846line 2847line 2848line 2849line 2850line 2851line 2852line 2853line 2854line 2855line 285
+| 6l
| bash: ne: command not found
-| ine 2781line 2782line 2783line 2784line 2785line 2786line 2787line 2788line 2789line 2790line
+| ine 2857line 2858line 2859line 2860line 2861line 2862line 2863line 2864line 2865line 2866line 28
+| 67
| bash: ine: command not found
-| line 2792line 2793line 2794line 2795line 2796line 2797line 2798line 2799line 2800line 2801line
+| line 2868line 2869line 2870line 2871line 2872line 2873line 2874line 2875line 2876line 2877line 2
+| 87
| bash: line: command not found
-| 2line 2803line 2804line 2805line 2806line 2807line 2808line 2809line 2810line 2811line 2812lin
-| bash: 2line: command not found
-| 13line 2814line 2815line 2816line 2817line 2818line 2819line 2820line 2821line 2822line 2823li
-| bash: 13line: command not found
-| 824line 2825line 2826line 2827line 2828line 2829line 2830line 2831line 2832line 2833line 2834l
-| bash: 824line: command not found
-| 2835line 2836line 2837line 2838line 2839line 2840line 2841line 2842line 2843line 2844line 2845
-| bash: 2835line: command not found
-| 2846line 2847line 2848line 2849line 2850line 2851line 2852line 2853line 2854line 2855line 285
-| bash: 2846line: command not found
-| /tmp/pardes-snap/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del e 2857line 2858line 2859line 2860line 2861line 2862line 2863line 2864line 2865line 2866line 28
-| ls bash: e: command not found
-| big.txt ne 2868line 2869line 2870line 2871line 2872line 2873line 2874line 2875line 2876line 2877line 2
-| bash: ne: command not found
-| ine 2879line 2880line 2881line 2882line 2883line 2884line 2885line 2886line 2887line 2888line
-| bash: ine: command not found
-| liecho alive-o''kne 2890line 2891line 2892line 2893line 2894line 2895line 2896line 2897line 2898li
-| bash: line: command not found
-| 0line 2901line 2902line 2903line 2904line 2905line 2906line 2907line 2908line 2909line 2910lin
-| bash: 0line: command not found
-| 11line 2912line 2913line 2914line 2915line 2916line 2917line 2918line 2919line 2920line 2921li
-| bash: 11line: command not found
-| 922line 2923line 2924line 2925line 2926line 2927line 2928line 2929line 2930line 2931line 2932l
-| bash: 922line: command not found
-| 2933line 2934line 2935line 2936line 2937line 2938line 2939line 2940line 2941line 2942line 2943
-| bash: 2933line: command not found
-| 2944line 2945line 2946line 2947line 2948line 2949line 2950line 2951line 2952line 2953line 295
-| bash: 2944line: command not found
-| e 2955line 2956line 2957line 2958line 2959line 2960line 2961line 2962line 2963line 2964line 29
+| 8line 2879line 2880line 2881line 2882line 2883line 2884line 2885line 2886line 2887line 2888line
+| 28
+| bash: 8line: command not found
+| 89line 2890line 2891line 2892line 2893line 2894line 2895line 2896line 2897line 2898line 2899line
+| 2
+| bash: 89line: command not found
+| /tmp/pardes-snap/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del 900line 2901line 2902line 2903line 2904line 2905line 2906line 2907line 2908line 2909line 2910lin
+| ls e
+| big.txt bash: 900line: command not found
+| 2911line 2912line 2913line 2914line 2915line 2916line 2917line 2918line 2919line 2920line 2921li
+| ne
+| bash: 2911line: command not found
+| 2echo alive-o''k922line 2923line 2924line 2925line 2926line 2927line 2928line 2929line 2930line 2
+| in
+| bash: 2922line: command not found
+| e 2933line 2934line 2935line 2936line 2937line 2938line 2939line 2940line 2941line 2942line 2943
+| li
| bash: e: command not found
-| ne 2966line 2967line 2968line 2969line 2970line 2971line 2972line 2973line 2974line 2975line 2
+| ne 2944line 2945line 2946line 2947line 2948line 2949line 2950line 2951line 2952line 2953line 295
+| 4l
| bash: ne: command not found
-| ine 2977line 2978line 2979line 2980line 2981line 2982line 2983line 2984line 2985line 2986line
+| ine 2955line 2956line 2957line 2958line 2959line 2960line 2961line 2962line 2963line 2964line 29
+| 65
| bash: ine: command not found
-| line 2988line 2989line 2990line 2991line 2992line 2993line 2994line 2995line 2996line 2997line
+| line 2966line 2967line 2968line 2969line 2970line 2971line 2972line 2973line 2974line 2975line 2
+| 97
| bash: line: command not found
+| 6line 2977line 2978line 2979line 2980line 2981line 2982line 2983line 2984line 2985line 2986line
+| 29
+| bash: 6line: command not found
+| 87line 2988line 2989line 2990line 2991line 2992line 2993line 2994line 2995line 2996line 2997line
+| bash: 87line: command not found
| echo alive-o''k
| alive-ok
diff --git a/test/snapshots/fileedit.golden b/test/snapshots/fileedit.golden
index 8a9555e0..4c651d3c 100644
--- a/test/snapshots/fileedit.golden
+++ b/test/snapshots/fileedit.golden
@@ -5,7 +5,7 @@
| ls ls
| f.txt f.txt
|
-| * /tmp/pardes-snap/fileedit/cwd/f.txt * Save Tty Collapse Del
+| * /tmp/pardes-snap/fileedit/cwd/f.txt Save Tty Collapse Del
| 1 liEDIT''MEne 1
| 2 line 2
| 3 line 3
diff --git a/test/snapshots/filewatch.golden b/test/snapshots/filewatch.golden
index ba9bce7c..f19168b1 100644
--- a/test/snapshots/filewatch.golden
+++ b/test/snapshots/filewatch.golden
@@ -1,21 +1,23 @@
-== snap edited grid=210x31 cursor=15,3
+== snap edited grid=210x31 cursor=15,7
|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/filewatch/cwd/w.txt * Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del
-| 1 liMYEDITne 1 ls
-| 2 line 2 sub w.txt
+| New Tty Find Grep Joincol Delcol
+| /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del
+| ls
+| sub w.txt
+|
+| * /tmp/pardes-snap/filewatch/cwd/w.txt Save Tty Collapse Del
+| 1 liMYEDITne 1
+| 2 line 2
| 3 /tmp/pardes-snap/filewatch/cwd/sub/s.txt
| 4 line 4
| 5 line 5
-| 6 line 6 w.txt
+| 6 line 6
| 7 line 7
| 8 line 8
| 9 line 9
| 10 line 10
| 11 line 11
| 12 line 12
-| 13
-|
| /tmp/pardes-snap/filewatch/cwd/sub/s.txt Save Tty Collapse Del
| 1 line 1
| 2 line 2
@@ -26,14 +28,10 @@
| 7 line 7
| 8 line 8
| 9
-== snap reloaded grid=210x31 cursor=15,3
-|2: /tmp/pardes-snap/filewatch/cwd/w.txt Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del
-|3: 1 EXTERNAL --:--:-- reloaded /tmp/pardes-snap/filewatch/cwd/w.txt ls
-|4: 2 sub w.txt
-|5:
-|6:
-|7:
-|8: w.txt
+== 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:
@@ -41,29 +39,31 @@
|13:
|14:
|15:
-== snap reloaded_sub grid=210x31 cursor=15,3
-|18: 1 RENAMED --:--:-- reloaded /tmp/pardes-snap/filewatch/cwd/sub/s.txt
-|19: 2
-|20:
-|21:
+|16:
+|17:
+|18:
+== snap reloaded_sub grid=210x31 cursor=15,7
+|20: 1 RENAMED --:--:-- reloaded /tmp/pardes-snap/filewatch/cwd/sub/s.txt
+|21: 2
|22:
|23:
|24:
|25:
|26:
-== snap undone grid=210x31 cursor=15,3
-|2: * /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del /tmp/pardes-snap/filewatch/cwd Tty+bash Save Mode Filter Collapse Del
-|3: 1 liMYEDITne 1 ls
-|4: 2 line 2 sub w.txt
-|5: 3 /tmp/pardes-snap/filewatch/cwd/sub/s.txt
-|6: 4 line 4
-|7: 5 line 5
-|8: 6 line 6 w.txt
-|9: 7 line 7
-|10: 8 line 8
-|11: 9 line 9
-|12: 10 line 10
-|13: 11 line 11
-|14: 12 line 12
-|15: 13
-|18: 1 RENAMED
+|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 38b6ac8e..9e6e6a13 100644
--- a/test/snapshots/filewatch.snap
+++ b/test/snapshots/filewatch.snap
@@ -39,13 +39,13 @@ wait 10000 line 4
stable 700 15000
# ...and open sub/s.txt from the path sitting on line 3 of that pane, so the
# second watched pane is in a DIFFERENT directory from the first
-press right 20 6
-release right 20 6
+press right 20 10
+release right 20 10
wait 10000 line 8
stable 700 15000
# an UNSAVED edit on line 1 of w.txt
-press left 10 4
-release left 10 4
+press left 10 8
+release left 10 8
stable 400 5000
key i
text MYEDIT
@@ -63,8 +63,8 @@ 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 4
-release left 10 4
+press left 10 8
+release left 10 8
stable 400 5000
key u
stable 400 5000
diff --git a/test/snapshots/find.golden b/test/snapshots/find.golden
index b71da767..e6e1bac1 100644
--- a/test/snapshots/find.golden
+++ b/test/snapshots/find.golden
@@ -11,25 +11,26 @@
|4:
== snap results grid=120x41 cursor=7,3
|3: 1 Find
-|37: /tmp/pardes-snap/find/cwd/+Search Save Tty Collapse Del
-|38: 1 sub/ZQMARK-beta.txt
-|39: 2 sub/nest/zqmark-gamma.txt
-|40: 3 zqmark-alpha.txt
-== snap first grid=120x41 cursor=7,21
-|20: /tmp/pardes-snap/find/cwd/sub/ZQMARK-beta.txt Save Tty Collapse Del
-|21: 1 beta body
+|5: /tmp/pardes-snap/find/cwd/+Search Save Tty Collapse Del
+|6: 1 sub/ZQMARK-beta.txt
+|7: 2 sub/nest/zqmark-gamma.txt
+|8: 3 zqmark-alpha.txt
+|9: 4
+== snap first grid=120x41 cursor=7,10
+|9: /tmp/pardes-snap/find/cwd/sub/ZQMARK-beta.txt Save Tty Collapse Del
+|10: 1 beta body
== style first grid=120x41
|0: 0-70 #b2c3b8,#1d2824, 71-119 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-119 d,#26362d,
|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-58 #b2c3b8,#1d2824, 59-119 d,#1d2824,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-119 d,#0d1410,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
-|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
-|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
-|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
-|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
-|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
+|5: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-35 #d3b0d9,#1d2824, 36-57 #b2c3b8,#1d2824, 58-119 d,#1d2824,
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-25 #e5eee5,#0d1410, 26-119 d,#0d1410,
+|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-31 #e5eee5,#0d1410, 32-119 d,#0d1410,
+|8: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-22 #e5eee5,#0d1410, 23-119 d,#0d1410,
+|9: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-47 #dfbce5,#26362d, 48-69 #becfc4,#26362d, 70-119 d,#26362d,
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-15 #e5eee5,#0d1410, 16-119 d,#0d1410,
|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
@@ -39,8 +40,8 @@
|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
-|20: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-47 #dfbce5,#26362d, 48-69 #becfc4,#26362d, 70-119 d,#26362d,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-15 #e5eee5,#0d1410, 16-119 d,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
+|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
@@ -56,39 +57,31 @@
|34: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
|35: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
|36: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
-|37: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-35 #d3b0d9,#1d2824, 36-57 #b2c3b8,#1d2824, 58-119 d,#1d2824,
-|38: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-25 #e5eee5,#0d1410, 26-119 d,#0d1410,
-|39: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-31 #e5eee5,#0d1410, 32-119 d,#0d1410,
-|40: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-22 #e5eee5,#0d1410, 23-119 d,#0d1410,
-== snap second grid=120x41 cursor=7,30
-|29: /tmp/pardes-snap/find/cwd/sub/nest/zqmark-gamma.txt Save Tty Collapse Del
-|30: 1 gamma body
-== snap third grid=120x41 cursor=7,34
-|33: /tmp/pardes-snap/find/cwd/zqmark-alpha.txt Save Tty Collapse Del
-|34: 1 alpha body
-|38: 2 sub/nest/zqmark-gamma.txt
-|39: 3 zqmark-alpha.txt
-|40: 4
-== snap back grid=120x41 cursor=7,30
-|38: 1 sub/ZQMARK-beta.txt
-|39: 2 sub/nest/zqmark-gamma.txt
-|40: 3 zqmark-alpha.txt
-== snap word grid=120x41 cursor=7,30
+|37: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
+|38: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
+|39: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
+|40: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-119 d,#0d1410,
+== snap second grid=120x41 cursor=7,13
+|12: /tmp/pardes-snap/find/cwd/sub/nest/zqmark-gamma.txt Save Tty Collapse Del
+|13: 1 gamma body
+== snap third grid=120x41 cursor=7,16
+|15: /tmp/pardes-snap/find/cwd/zqmark-alpha.txt Save Tty Collapse Del
+|16: 1 alpha body
+== snap back grid=120x41 cursor=7,13
+== snap word grid=120x41 cursor=7,13
|3: 1 Find Find
|4: Find /
-== snap cancel grid=120x41 cursor=7,39
+== snap cancel grid=120x41 cursor=7,7
|3: 1 Find Find /
|4:
-|30: 1 gamma body Last
-== snap nomatch grid=120x41 cursor=7,39
+|13: 1 gamma body Last
+== snap nomatch grid=120x41 cursor=7,8
|3: 1 Find
+|13: 1 gamma body
|18: /tmp/pardes-snap/find/cwd/+Search Save Tty Collapse Del
|19: 1
-|30: 1 gamma body
-|38: 2 sub/nest/zqmark-gamma.txt
-|39: 3 zqmark-alpha.txt
-|40: 4
== snap refit grid=120x41 cursor=7,3
-== snap topbar grid=120x41 cursor=12,34
-|35: /tmp/pardes-snap/find/cwd/+Search Save Tty Collapse Del
-|36: 1 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
diff --git a/test/snapshots/find.snap b/test/snapshots/find.snap
index fe1ecad3..09826a79 100644
--- a/test/snapshots/find.snap
+++ b/test/snapshots/find.snap
@@ -5,7 +5,9 @@
# one with no code of their own, and the look chord — Enter, the same one that
# opens a path anywhere else — is what OPENS it. Focus then follows the file
# that opened, but the look was made FROM the results buffer, so the next n
-# resumes stepping that same list. The walk skips .git, matches the NAME
+# resumes stepping that same list. Where each pane lands is acme's placement
+# (docs/tags.md, where new panes go): under the text of the pane with the
+# most blank rows. The walk skips .git, matches the NAME
# case-insensitively, and sorts (readdir order is not stable). `SPC f s` is
# Save, which moved out of `w` to make this group.
dirmk sub
@@ -37,10 +39,8 @@ key enter
wait 10000 zqmark-alpha.txt
stable 700 15000
snap results
-# n SELECTS row 1 and Enter looks it — a path, so the file OPENS. The look
-# comes FROM the results buffer, but that is an output pane and not a
-# document, so the file joins the docs above it (below here.txt) instead of
-# stacking under the list: the +Search stays at the foot of the column.
+# n SELECTS row 1 and Enter looks it — a path, so the file OPENS, under the
+# list's text, and each file after it under the one before.
key n
key enter
wait 10000 beta body
@@ -81,9 +81,7 @@ snap cancel
press left 8 4
release left 8 4
stable 400 5000
-# a no-match walk opens an empty list of its own. It is exactly as tall as its
-# results — one blank row plus its tag — and it stacks above the earlier one,
-# whose height is untouched.
+# a no-match walk opens an empty list of its own, under the last file's text.
key space
key f
key f
@@ -118,12 +116,12 @@ snap refit
# Find is a TOPBAR word too: the same
# builtin the leader path runs, reached with the mouse. Chorded — a selection
# held when it runs IS the argument — so dragging over `alpha` and clicking
-# Find walks for that name with nothing typed and no input armed. Row 34 is
+# Find walks for that name with nothing typed and no input armed. Row 17 is
# `alpha body`: the three found files stack in the order the looks above
# OPENED them, which is beta, gamma, alpha down the column.
-press left 8 35
-drag left 13 35
-release left 13 35
+press left 8 17
+drag left 13 17
+release left 13 17
stable 400 5000
press middle @Find 1
release middle @Find 1
diff --git a/test/snapshots/fsearch.golden b/test/snapshots/fsearch.golden
index 2d4fcb64..d88fd59f 100644
--- a/test/snapshots/fsearch.golden
+++ b/test/snapshots/fsearch.golden
@@ -23,11 +23,14 @@
|7: 1 l1 /MARK
== snap results grid=180x31 cursor=10,8
|7: 1 l1
-|12: /tmp/pardes-snap/fsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
-|13: 1 g.txt:2:1-4
-|14: 2 MARK a
-|15: 3 g.txt:6:1-4
-|16: 4 MARK b
+|21: /tmp/pardes-snap/fsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
+|22: 1 g.txt:2:1-4
+|23: 2 MARK a
+|24: 3 g.txt:6:1-4
+|25: 4 MARK b
+|26: 5 g.txt:10:1-4
+|27: 6 MARK c
+|28: 7
== style results grid=180x31
|0: 0-70 #b2c3b8,#1d2824, 71-179 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-89 d,#26362d, 90-91 #ffffff,#887455, 92-92 d,#1d2824, 93-124 #b2c3b8,#1d2824, 125-179 d,#1d2824,
@@ -38,59 +41,56 @@
|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-36 #dfbce5,#26362d, 37-58 #becfc4,#26362d, 59-89 d,#26362d, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #f5f7ee,#3c614a, 11-12 #e5eee5,#0d1410, 13-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|9: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|10: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|11: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|12: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-38 #d3b0d9,#1d2824, 39-76 #b2c3b8,#1d2824, 77-89 d,#1d2824, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-17 #59675c,#0d1410, 18-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f7e4a5,#5e4923, 11-12 #e5eee5,#0d1410, 13-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|15: 0-0 d,#152019, 1-1 d,#0d1410, 2-17 #59675c,#0d1410, 18-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|16: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f7e4a5,#5e4923, 11-12 #e5eee5,#0d1410, 13-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|15: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|16: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-39 #d3b0d9,#1d2824, 40-69 #b2c3b8,#1d2824, 70-89 d,#1d2824, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
|20: 0-0 d,#608269, 1-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-89 #e5eee5,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
-== snap second grid=180x31 cursor=10,9
-|7: 4 l4
-|8: 5 l5
-|9: 6 MARK b
-|10: 7 l7
-|11: 8 l8
-== snap third grid=180x31 cursor=10,9
-|7: 8 l8
-|8: 9 l9
-|9: 10 MARK c
-|10: 11 l11
-|11: 12 l12
-|13: 3 g.txt:6:1-4
-|14: 4 MARK b
-|15: 5 g.txt:10:1-4
-|16: 6 MARK c
-== snap back grid=180x31 cursor=10,9
-|7: 4 l4
-|8: 5 l5
-|9: 6 MARK b
-|10: 7 l7
-|11: 8 l8
-|13: 2 MARK a
-|14: 3 g.txt:6:1-4
-|15: 4 MARK b
-|16: 5 g.txt:10:1-4
-== snap cancel grid=180x31 cursor=10,9
-== snap nomatch grid=180x31 cursor=7,15
-|10: /tmp/pardes-snap/fsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
-|11: 1
-|13: 3 g.txt:6:1-4
-|14: 4 MARK b
-|15: 5 g.txt:10:1-4
-|16: 6 MARK c
+|21: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-38 #d3b0d9,#1d2824, 39-76 #b2c3b8,#1d2824, 77-89 d,#1d2824, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-17 #59675c,#0d1410, 18-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f7e4a5,#5e4923, 11-12 #e5eee5,#0d1410, 13-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-17 #59675c,#0d1410, 18-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f7e4a5,#5e4923, 11-12 #e5eee5,#0d1410, 13-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-18 #59675c,#0d1410, 19-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f7e4a5,#5e4923, 11-12 #e5eee5,#0d1410, 13-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410, 90-90 d,#608269, 91-91 d,#0d1410, 92-179 #e5eee5,#0d1410,
+== snap second grid=180x31 cursor=10,12
+== snap third grid=180x31 cursor=10,12
+|7: 5 l5
+|8: 6 MARK b
+|9: 7 l7
+|10: 8 l8
+|11: 9 l9
+|12: 10 MARK c
+|13: 11 l11
+|14: 12 l12
+|15:
+|16:
+== snap back grid=180x31 cursor=10,12
+|7: 1 l1
+|8: 2 MARK a
+|9: 3 l3
+|10: 4 l4
+|11: 5 l5
+|12: 6 MARK b
+|13: 7 l7
+|14: 8 l8
+|15: 9 l9
+|16: 10 MARK c
+== snap cancel grid=180x31 cursor=10,12
+== snap nomatch grid=180x31 cursor=7,26
+|11: /tmp/pardes-snap/fsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
+|12: 1
+|13:
+|14:
+|15:
+|16:
diff --git a/test/snapshots/ghostty-mid.golden b/test/snapshots/ghostty-mid.golden
index 965f6896..05d63bc8 100644
--- a/test/snapshots/ghostty-mid.golden
+++ b/test/snapshots/ghostty-mid.golden
@@ -1,7 +1,8 @@
-== snap ghostty-mid grid=100x31 cursor=4,7
+== snap ghostty-mid grid=100x31 cursor=4,8
|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/ghostty-mid/cwd Tty+bash Save /tmp/pardes-snap/ghostty-mid/cwd Tty+bash Save
+| Mode Filter Collapse Del Mode Filter Collapse Del
| ZZPAD ls
| ./g.sh g.txt
| ./g.sh
@@ -15,7 +16,7 @@
|
|
|
-|
| /tmp/pardes-snap/ghostty-mid/cwd Tty+bash Save
+| Mode Filter Collapse Del
| ls
| g.txt
diff --git a/test/snapshots/ghostty-mid.snap b/test/snapshots/ghostty-mid.snap
index cb3088b4..c20db2f7 100644
--- a/test/snapshots/ghostty-mid.snap
+++ b/test/snapshots/ghostty-mid.snap
@@ -23,10 +23,10 @@ stable 700 10000
key c-b
stable 600 8000
send \x1b[I
-send \x1b[<35;10;9M\x1b[<35;7;7M\x1b[<35;4;5M
-send \x1b[<1;4;5M
-send \x1b[<1;4;5m
-send \x1b[<35;6;5M
+send \x1b[<35;10;10M\x1b[<35;7;8M\x1b[<35;4;6M
+send \x1b[<1;4;6M
+send \x1b[<1;4;6m
+send \x1b[<35;6;6M
wait 10000 GHOSTOK
stable 700 10000
snap ghostty-mid
diff --git a/test/snapshots/grep.golden b/test/snapshots/grep.golden
index 29b18db0..65b2fd5b 100644
--- a/test/snapshots/grep.golden
+++ b/test/snapshots/grep.golden
@@ -23,20 +23,18 @@
|7: 1 hello TARGET one Grep /TARGET
== snap results grid=180x31 cursor=7,7
|7: 1 hello TARGET one
-|13: /tmp/pardes-snap/grep/cwd/+Search Save Tty Collapse Del LocationsConfig
-|14: 1 a.txt:1:7-12 hello TARGET one
-|15: 2 a.txt:3:6-11 last TARGET line
-|16: 3 sub/b.txt:2:1-6 TARGET inside sub
+|21: /tmp/pardes-snap/grep/cwd/+Search Save Tty Collapse Del LocationsConfig
+|22: 1 a.txt:1:7-12 hello TARGET one
+|23: 2 a.txt:3:6-11 last TARGET line
+|24: 3 sub/b.txt:2:1-6 TARGET inside sub
+|25: 4
== snap first grid=180x31 cursor=18,7
-== snap opened-hit grid=180x31 cursor=12,11
-|9: /tmp/pardes-snap/grep/cwd/sub/b.txt Save Tty Collapse Del
-|10: 1 deep
-|11: 2 TARGET inside sub
-|14: 2 a.txt:3:6-11 last TARGET line
-|15: 3 sub/b.txt:2:1-6 TARGET inside sub
-|16: 4
+== snap opened-hit grid=180x31 cursor=12,12
+|10: /tmp/pardes-snap/grep/cwd/sub/b.txt Save Tty Collapse Del
+|11: 1 deep
+|12: 2 TARGET inside sub
== snap nomatch grid=180x31 cursor=2,4
-|11: /tmp/pardes-snap/grep/cwd/sub/+Search Save Tty Collapse Del LocationsConfig
-|12: 1
+|25: /tmp/pardes-snap/grep/cwd/sub/+Search Save Tty Collapse Del LocationsConfig
+|26: 1
== snap chordsel grid=180x31 cursor=19,7
== snap chord grid=180x31 cursor=19,7
diff --git a/test/snapshots/hscroll.golden b/test/snapshots/hscroll.golden
index 4e9fd9af..8ce8b968 100644
--- a/test/snapshots/hscroll.golden
+++ b/test/snapshots/hscroll.golden
@@ -1,11 +1,13 @@
-== snap opened grid=90x31 cursor=7,7
+== snap opened grid=90x31 cursor=7,9
|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/hscroll/cwd Tty+bash Save /tmp/pardes-snap/hscroll/cwd Tty+bash Save
+| /tmp/pardes-snap/hscroll/cwd Tty+bash Sav /tmp/pardes-snap/hscroll/cwd Tty+bash Sav
+| e Mode Filter Collapse Del e Mode Filter Collapse Del
| ls ls
| wide.txt wide.txt
|
-| /tmp/pardes-snap/hscroll/cwd/wide.txt Save
+| /tmp/pardes-snap/hscroll/cwd/wide.txt Sav
+| e Tty Collapse Del
| 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp
|
|
@@ -14,16 +16,15 @@
|
|
|
-|
-|
-| /tmp/pardes-snap/hscroll/cwd Tty+bash Save
+| /tmp/pardes-snap/hscroll/cwd Tty+bash Sav
+| e Mode Filter Collapse Del
| ls
| wide.txt
-== snap wheeled grid=90x31 cursor=7,7
-|7: 1 ND_A_MARKER_WAY_OUT_RIGHT_padpadpadpad
-== snap clamped grid=90x31 cursor=7,7
-|7: 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp
-== snap dollar grid=90x31 cursor=41,7
-|7: 1 padpadpadpadpadpadpadpadpadpad_TAIL
-== snap zero grid=90x31 cursor=7,7
-|7: 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp
+== snap wheeled grid=90x31 cursor=7,9
+|9: 1 ND_A_MARKER_WAY_OUT_RIGHT_padpadpadpad
+== snap clamped grid=90x31 cursor=7,9
+|9: 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp
+== snap dollar grid=90x31 cursor=41,9
+|9: 1 padpadpadpadpadpadpadpadpadpad_TAIL
+== snap zero grid=90x31 cursor=7,9
+|9: 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp
diff --git a/test/snapshots/images.golden b/test/snapshots/images.golden
index 11d4ae3b..46caaff7 100644
--- a/test/snapshots/images.golden
+++ b/test/snapshots/images.golden
@@ -5,17 +5,17 @@
| ls ls
| pic.ppm pic.ppm
|
-| img petscii:off palette:commodore ascii:on /tmp/pardes-snap/images/cwd/p
-| 92@7%5Gm$D4▘🬵%🬨3'🬥🬆🬻
-| 🬥3▖╱{=CY,▌*)^M<▀🬎🬻
-| X▄jN.&🬺j*w3┼"🬥🬆🬻
-| ╱╳PxGXPV*q"(🬆~
-| HF[🬗█▀&t0^🬆🬻
-| 🬴X7─27/*🬆🬻
-| .g🬬🬎ks▛🬻
-| ▚i,V🬆🬹
-| mu🬆🬻
-| 🬆🬻
+| img petscii:off palette:commodore ascii:on /tmp/pardes-snap/images/cwd/
+| pic.ppm Tty Collapse Del
+| x🬱\╲🬌\~▄🬥4▟g*🬺🬶_🬆🬻
+| >A8🬌Cq&.\H&🬹🬜C▛🬻
+| ><e'🬷y🬙▄Fso🬴🬆🬹
+| k[🬻🬟🬜7f🬜$>🬎🬻
+| 4▜>gi▟9🬙🬆🬻
+| ╱i🬹─B▜🬆🬻
+| @🬥🬺v🬆🬻
+| g🬷🬆🬻
+| 🬆🬻
| /tmp/pardes-snap/images/cwd Tty+bash Save Mode Filter Collapse Del
| ls
| pic.ppm
@@ -26,17 +26,17 @@
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|5: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410,
-|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-73 #becfc4,#26362d, 74-74 #dfbce5,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|7: 0-0 d,#152019, 1-27 d,#0d1410, 28-29 #959595,#6c6c6c, 30-30 #6c6c6c,#959595, 31-34 #959595,#6c6c6c, 35-35 #6c6c6c,#959595, 36-37 #959595,#6c6c6c, 38-38 #6c6c6c,#959595, 39-39 #959595,#6c6c6c, 40-40 #6c6c6c,#959595, 41-41 #959595,#6c6c6c, 42-42 #6c6c6c,#959595, 43-44 #959595,#6c6c6c, 45-45 #6c6c6c,#959595, 46-46 #959595,#6c5eb5, 47-47 #352879,#6c6c6c, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|8: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #6c6c6c,#959595, 29-29 #959595,#6c6c6c, 30-30 #6c6c6c,#959595, 31-34 #959595,#6c6c6c, 35-38 #6c6c6c,#959595, 39-42 #959595,#6c6c6c, 43-43 #6c6c6c,#959595, 44-44 #6c6c6c,#352879, 45-45 #352879,#959595, 46-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|9: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #959595,#6c6c6c, 29-29 #6c6c6c,#959595, 30-33 #959595,#6c6c6c, 34-34 #6c6c6c,#959595, 35-38 #959595,#6c6c6c, 39-39 #6c6c6c,#959595, 40-40 #959595,#6c6c6c, 41-41 #6c6c6c,#959595, 42-42 #959595,#6c5eb5, 43-43 #352879,#6c6c6c, 44-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|10: 0-0 d,#152019, 1-27 d,#0d1410, 28-29 #959595,#6c6c6c, 30-30 #6c6c6c,#959595, 31-33 #959595,#6c6c6c, 34-34 #6c6c6c,#959595, 35-39 #959595,#6c6c6c, 40-40 #959595,#6c5eb5, 41-41 #6c6c6c,#352879, 42-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|11: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #6c6c6c,#959595, 29-29 #959595,#6c6c6c, 30-34 #6c6c6c,#959595, 35-36 #959595,#6c6c6c, 37-37 #6c6c6c,#959595, 38-38 #6c6c6c,#6c5eb5, 39-39 #352879,#6c6c6c, 40-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|12: 0-0 d,#152019, 1-27 d,#0d1410, 28-29 #6c6c6c,#959595, 30-31 #959595,#6c6c6c, 32-32 #6c6c6c,#959595, 33-34 #959595,#6c6c6c, 35-35 #6c6c6c,#959595, 36-36 #6c6c6c,#6c5eb5, 37-37 #352879,#6c6c6c, 38-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|13: 0-0 d,#152019, 1-27 d,#0d1410, 28-29 #959595,#6c6c6c, 30-32 #6c6c6c,#959595, 33-33 #959595,#6c6c6c, 34-34 #6c6c6c,#352879, 35-35 #352879,#6c6c6c, 36-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|14: 0-0 d,#152019, 1-27 d,#0d1410, 28-31 #959595,#6c6c6c, 32-32 #959595,#6c5eb5, 33-33 #352879,#6c5eb5, 34-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|15: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #6c6c6c,#959595, 29-29 #959595,#6c6c6c, 30-30 #6c6c6c,#6c5eb5, 31-31 #352879,#6c6c6c, 32-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|16: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #6c6c6c,#6c5eb5, 29-29 #352879,#6c6c6c, 30-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-73 #becfc4,#26362d, 74-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|7: 0-2 d,#26362d, 3-9 #dfbce5,#26362d, 10-26 #becfc4,#26362d, 27-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|8: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 #959595,#6c6c6c, 31-31 #6c6c6c,#959595, 32-32 #959595,#6c6c6c, 33-33 #6c6c6c,#959595, 34-35 #959595,#6c6c6c, 36-36 #6c6c6c,#959595, 37-37 #959595,#6c6c6c, 38-40 #6c6c6c,#959595, 41-41 #959595,#6c6c6c, 42-43 #6c6c6c,#959595, 44-44 #959595,#6c6c6c, 45-45 #6c6c6c,#6c5eb5, 46-46 #352879,#959595, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|9: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 #959595,#6c6c6c, 30-31 #6c6c6c,#959595, 32-35 #959595,#6c6c6c, 36-36 #6c6c6c,#959595, 37-38 #959595,#6c6c6c, 39-42 #6c6c6c,#959595, 43-43 #6c6c6c,#352879, 44-44 #352879,#6c6c6c, 45-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|10: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 #959595,#6c6c6c, 31-31 #6c6c6c,#959595, 32-32 #959595,#6c6c6c, 33-33 #6c6c6c,#959595, 34-34 #959595,#6c6c6c, 35-36 #6c6c6c,#959595, 37-39 #959595,#6c6c6c, 40-40 #6c6c6c,#959595, 41-41 #6c6c6c,#6c5eb5, 42-42 #352879,#6c5eb5, 43-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|11: 0-0 d,#152019, 1-28 d,#0d1410, 29-33 #6c6c6c,#959595, 34-35 #959595,#6c6c6c, 36-36 #6c6c6c,#959595, 37-38 #959595,#6c6c6c, 39-39 #6c6c6c,#352879, 40-40 #352879,#959595, 41-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|12: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 #6c6c6c,#959595, 31-33 #959595,#6c6c6c, 34-34 #6c6c6c,#959595, 35-35 #959595,#6c6c6c, 36-36 #6c6c6c,#959595, 37-37 #959595,#6c5eb5, 38-38 #352879,#6c6c6c, 39-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|13: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 #959595,#6c6c6c, 31-31 #6c6c6c,#959595, 32-33 #959595,#6c6c6c, 34-34 #6c6c6c,#959595, 35-35 #6c6c6c,#6c5eb5, 36-36 #352879,#6c6c6c, 37-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|14: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 #959595,#6c6c6c, 30-32 #6c6c6c,#959595, 33-33 #959595,#6c5eb5, 34-34 #352879,#6c6c6c, 35-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|15: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 #6c6c6c,#959595, 31-31 #6c6c6c,#6c5eb5, 32-32 #352879,#6c6c6c, 33-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|16: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 #6c6c6c,#6c5eb5, 30-30 #352879,#6c6c6c, 31-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-68 #b2c3b8,#1d2824, 69-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
@@ -52,29 +52,28 @@
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
== style term-palette grid=150x31
-|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-72 #becfc4,#26362d, 73-74 #dfbce5,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|7: 0-0 d,#152019, 1-65 d,#0d1410, 66-66 d,#1d2824, 67-73 #b2c3b8,#1d2824, 74-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|8: 0-0 d,#152019, 1-28 d,#0d1410, 29-31 p8,p4, 32-33 p4,p8, 34-37 p8,p4, 38-38 p4,p8, 39-44 p8,p4, 45-45 p8,p12, 46-46 p12,p8, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|9: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 p8,p4, 30-30 p4,p8, 31-35 p8,p4, 36-36 p5,p8, 37-37 p8,p4, 38-38 p4,p8, 39-41 p8,p4, 42-42 p4,p8, 43-43 p8,p12, 44-45 p12,p8, 46-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|10: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 p8,p4, 31-32 p4,p8, 33-40 p8,p4, 41-41 p8,p12, 42-43 p12,p8, 44-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|11: 0-0 d,#152019, 1-28 d,#0d1410, 29-31 p8,p4, 32-32 p4,p8, 33-34 p8,p4, 35-37 p4,p8, 38-38 p8,p4, 39-39 p8,p12, 40-41 p12,p8, 42-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|12: 0-0 d,#152019, 1-28 d,#0d1410, 29-36 p8,p4, 37-37 p8,p12, 38-39 p12,p8, 40-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|13: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 p4,p8, 31-34 p8,p4, 35-35 p8,p12, 36-36 p12,p8, 37-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|14: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 p4,p8, 30-31 p8,p4, 32-32 p4,p8, 33-33 p8,p12, 34-35 p12,p8, 36-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|15: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 p4,p8, 30-30 p8,p4, 31-31 p8,p12, 32-32 p12,p8, 33-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|16: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 p8,p12, 30-30 p12,p8, 31-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-72 #becfc4,#26362d, 73-73 #dfbce5,#26362d, 74-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|7: 0-2 d,#26362d, 3-8 #dfbce5,#26362d, 9-25 #becfc4,#26362d, 26-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|8: 0-0 d,#152019, 1-65 d,#0d1410, 66-66 d,#1d2824, 67-73 #b2c3b8,#1d2824, 74-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|9: 0-0 d,#152019, 1-29 d,#0d1410, 30-33 p8,p4, 34-34 p4,p8, 35-35 p8,p4, 36-36 p4,p8, 37-43 p8,p4, 44-44 p8,p12, 45-45 p12,p8, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|10: 0-0 d,#152019, 1-29 d,#0d1410, 30-30 p4,p8, 31-31 p8,p4, 32-32 p4,p8, 33-41 p8,p4, 42-43 p12,p8, 44-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|11: 0-0 d,#152019, 1-29 d,#0d1410, 30-34 p8,p4, 35-36 p4,p8, 37-37 p8,p4, 38-39 p4,p8, 40-40 p8,p12, 41-41 p12,p8, 42-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|12: 0-0 d,#152019, 1-29 d,#0d1410, 30-31 p8,p4, 32-32 p4,p8, 33-34 p8,p4, 35-35 p4,p8, 36-37 p8,p4, 38-38 p8,p12, 39-39 p12,p8, 40-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|13: 0-0 d,#152019, 1-29 d,#0d1410, 30-31 p8,p4, 32-32 p4,p8, 33-35 p8,p4, 36-36 p8,p12, 37-37 p12,p8, 38-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|14: 0-0 d,#152019, 1-29 d,#0d1410, 30-32 p8,p4, 33-33 p4,p8, 34-34 p8,p12, 35-35 p12,p8, 36-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|15: 0-0 d,#152019, 1-29 d,#0d1410, 30-30 p4,p8, 31-31 p8,p4, 32-32 p8,p12, 33-33 p12,p8, 34-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|16: 0-0 d,#152019, 1-29 d,#0d1410, 30-30 p8,p12, 31-31 p12,p8, 32-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
== snap no-ascii grid=150x31 cursor=11,8
-|6: img petscii:off palette:terminal ascii:off /tmp/pardes-snap/images/cwd/p
-|7: Ascii
-|8: █🬝█╲███▙🬸🬬🬻█████🬝🬵
-|9: ███████🬻██🬸█🬝█🬝▄█
-|10: ████████████🬝▄█
-|11: ███🬸██████🬝▄█
-|12: ████████🬝🬵█
-|13: ╱█████🬝▄
-|14: █🬬█╲🬝🬵█
-|15: 🬺█🬝🬵
-|16: 🬝▄
+|6: img petscii:off palette:terminal ascii:off /tmp/pardes-snap/images/cwd/
+|8: Ascii
+|9: ██████████████🬝🬹
+|10: ██████🬛█████🬝▄
+|11: ██████████🬝▄
+|12: ██╱█████🬝▄
+|13: █🬸╲███🬝🬹
+|14: ████🬝▄
+|15: │█🬝▄
+|16: 🬝🬹
== snap tag-edit grid=400x31 cursor=87,6
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
diff --git a/test/snapshots/jumps.golden b/test/snapshots/jumps.golden
index 75e39f9b..96ba2cc5 100644
--- a/test/snapshots/jumps.golden
+++ b/test/snapshots/jumps.golden
@@ -133,11 +133,21 @@
|3: 86 line 86 ls Last
== snap jumplist grid=150x31 cursor=7,17
|3: 86 line 86 Jumplist ls
-|26: /tmp/pardes-snap/jumps/cwd/+Jumps Save Tty Collapse Del LocationsConfig
-|27: 1 @p1:3:3 /tmp/pardes-snap/jumps/cwd
-|28: 2 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1
-|29: 3 @p1:3:3 /tmp/pardes-snap/jumps/cwd
-|30: 4 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100
+|16: /tmp/pardes-snap/jumps/cwd/+Jumps Save Tty Collapse Del LocationsConfig
+|17: 1 @p1: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
+|20: 4 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100
+|21: 5
+|22:
+|23:
+|24:
+|25:
+|26:
+|27:
+|28:
+|29:
+|30:
== snap step1-shell grid=150x31 cursor=79,5
|3: 86 line 86 ls
== style step1-shell grid=150x31
@@ -149,29 +159,29 @@
|5: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410,
|6: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|7: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|8: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|9: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|10: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|11: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|12: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|13: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|14: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|15: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|16: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|17: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|18: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|19: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|20: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|21: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|22: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|23: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|24: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|25: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|26: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-35 #d3b0d9,#1d2824, 36-73 #b2c3b8,#1d2824, 74-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-13 #59675c,#0d1410, 14-40 #e5eee5,#0d1410, 41-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-44 #59675c,#0d1410, 45-51 #e5eee5,#0d1410, 52-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-13 #59675c,#0d1410, 14-40 #e5eee5,#0d1410, 41-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|30: 0-0 d,#152019, 1-1 d,#0d1410, 2-46 #59675c,#0d1410, 47-55 #e5eee5,#0d1410, 56-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|16: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-35 #d3b0d9,#1d2824, 36-73 #b2c3b8,#1d2824, 74-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-13 #59675c,#0d1410, 14-40 #e5eee5,#0d1410, 41-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-44 #59675c,#0d1410, 45-51 #e5eee5,#0d1410, 52-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-13 #59675c,#0d1410, 14-40 #e5eee5,#0d1410, 41-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-46 #59675c,#0d1410, 47-55 #e5eee5,#0d1410, 56-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
== snap step2-file grid=150x31 cursor=7,3
|3: 1 line 1 ls
|4: 2 line 2 big.txt
@@ -186,43 +196,19 @@
|13: 11 line 11
|14: 12 line 12
|15: 13 line 13
-|16: 14 line 14
-|17: 15 line 15
-|18: 16 line 16
-|19: 17 line 17
-|20: 18 line 18
-|21: 19 line 19
-|22: 20 line 20
-|23: 21 line 21
-|24: 22 line 22
-|25: 23 line 23
-== snap step4-line100 grid=150x31 cursor=7,14
-|3: 89 line 89 ls
-|4: 90 line 90 big.txt
-|5: 91 line 91
-|6: 92 line 92
-|7: 93 line 93
-|8: 94 line 94
-|9: 95 line 95
-|10: 96 line 96
-|11: 97 line 97
-|12: 98 line 98
-|13: 99 line 99
-|14: 100 line 100
-|15: 101 line 101
-|16: 102 line 102
-|17: 103 line 103
-|18: 104 line 104
-|19: 105 line 105
-|20: 106 line 106
-|21: 107 line 107
-|22: 108 line 108
-|23: 109 line 109
-|24: 110 line 110
-|25: 111 line 111
-|27: 2 /tmp/pardes-snap/jumps/cwd/big.txt:1:1 line 1
-|28: 3 @p1:3:3 /tmp/pardes-snap/jumps/cwd
-|29: 4 /tmp/pardes-snap/jumps/cwd/big.txt:100:1 line 100
-|30: 5
+== snap step4-line100 grid=150x31 cursor=7,9
+|3: 94 line 94 ls
+|4: 95 line 95 big.txt
+|5: 96 line 96
+|6: 97 line 97
+|7: 98 line 98
+|8: 99 line 99
+|9: 100 line 100
+|10: 101 line 101
+|11: 102 line 102
+|12: 103 line 103
+|13: 104 line 104
+|14: 105 line 105
+|15: 106 line 106
== snap step-back grid=150x31 cursor=79,5
-== snap look-row grid=150x31 cursor=7,14
+== snap look-row grid=150x31 cursor=7,9
diff --git a/test/snapshots/jumps.snap b/test/snapshots/jumps.snap
index e4816f88..9ba0139d 100644
--- a/test/snapshots/jumps.snap
+++ b/test/snapshots/jumps.snap
@@ -110,10 +110,9 @@ stable 700 15000
snap step-back
# and an ordinary LOOK on a row goes there too — the leading word IS a
# location, so the right-click look resolves it like any other path. Row 4
-# again, this time by clicking it: the walk above scrolled the list by one, so
-# the last four screen rows are its rows 2 through 5 and row 4 is the second
-# from the bottom.
-press right 16 30
-release right 16 30
+# again, this time by clicking it: the list shows every row, and its row 4 is
+# screen row 21.
+press right 16 21
+release right 16 21
stable 700 15000
snap look-row
diff --git a/test/snapshots/layout-close.golden b/test/snapshots/layout-close.golden
index a7d159b5..e080d13e 100644
--- a/test/snapshots/layout-close.golden
+++ b/test/snapshots/layout-close.golden
@@ -1,48 +1,70 @@
-== snap built grid=140x41 cursor=4,17
+== snap built grid=140x41 cursor=4,16
|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/layout-close/cwd Tty+bash Save Mode Filter Collaps /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
+| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
+| se Del se Del
| ls ls
| marker.txt marker.txt
|
-| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
+| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
+| se Del
| ls
| marker.txt
|
-| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
+| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
+| se Del
| ls
| marker.txt
|
-| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
-| ls
-| marker.txt
|
|
|
|
|
-| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
+| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
+| se Del
| ls
| marker.txt
-== snap altc grid=140x41 cursor=39,5
+|
+| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
+| se Del
+| ls
+| marker.txt
+== snap altc grid=140x41 cursor=39,7
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/layout-close/cw /tmp/pardes-snap/layout-close/cw /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
-|3: ls ls ls
-|4: marker.txt marker.txt marker.txt
-|6: /tmp/pardes-snap/layout-close/cw
-|10: /tmp/pardes-snap/layout-close/cw
+|2: /tmp/pardes-snap/layout-close/c /tmp/pardes-snap/layout-close/c /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
+|3: wd Tty+bash Save Mode Filter Co wd Tty+bash Save Mode Filter Co se Del
+|4: llapse Del llapse Del ls
+|5: marker.txt ls marker.txt
+|6: marker.txt
+|7: /tmp/pardes-snap/layout-close/c
+|8: wd Tty+bash Save Mode Filter Co
+|9: llapse Del
+|10: ls
+|11: marker.txt
+|12:
+|13:
|14:
|15:
-|16:
-|22: /tmp/pardes-snap/layout-close/cw
-== snap del grid=140x41 cursor=4,8
+|22: /tmp/pardes-snap/layout-close/c
+|23: wd Tty+bash Save Mode Filter Co
+|24: llapse Del
+|27: /tmp/pardes-snap/layout-close/c
+|28: wd Tty+bash Save Mode Filter Co
+|29: llapse Del
+|30: ls
+|31: marker.txt
+== snap del grid=140x41 cursor=6,8
+|8: wd kd Tty+bash Save Mode Filter
+|9: Collapse Del
+== snap exit grid=140x41 cursor=4,12
+|22:
+|23:
+|24:
+|25:
+== snap colclose grid=140x41 cursor=38,1
+|2: /tmp/pardes-snap/layout-close/c /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
+|3: wd Tty+bash Save Mode Filter Co se Del
+|4: llapse Del ls
+|5: marker.txt marker.txt
|6:
-|7:
-|8:
-== snap exit grid=140x41 cursor=4,8
-|10:
-|11:
-|12:
-== snap colclose grid=140x41 cursor=42,3
-|3: ls 1 ls
-|4: marker.txt marker.txt
diff --git a/test/snapshots/layout-close.snap b/test/snapshots/layout-close.snap
index 4ae3e31f..3218cfdb 100644
--- a/test/snapshots/layout-close.snap
+++ b/test/snapshots/layout-close.snap
@@ -1,6 +1,6 @@
# every pane close hands its space to ONE sibling, the rest of the column
# keeps its sizes: Alt-c move-out, tag Del, and shell exit (pty EOF); an
-# emptied column hands its width to one neighbor
+# emptied column stays, as acme's does, its tag over blank space
file marker.txt x
start 41 140 -n 3
wait 8000 marker.txt
@@ -40,8 +40,8 @@ snap del
press left 40 9
release left 40 9
stable 400 5000
-press left 5 15
-release left 5 15
+press left 5 24
+release left 5 24
stable 400 5000
key c-b
stable 400 5000
@@ -49,8 +49,8 @@ text exit
key enter
stable 700 10000
snap exit
-# exit the moved-out pane's shell: its column empties and returns its width to
-# the source column it was carved from; the untouched right column stays put
+# exit the moved-out pane's shell: its column stays where it was, empty, the
+# keyboard on its tag; the other columns keep their widths
press left 40 9
release left 40 9
stable 400 5000
diff --git a/test/snapshots/layout-open.golden b/test/snapshots/layout-open.golden
index 79492382..c48f476e 100644
--- a/test/snapshots/layout-open.golden
+++ b/test/snapshots/layout-open.golden
@@ -2,11 +2,11 @@
|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/layout-open/cwd Tty+bash Save /tmp/pardes-snap/layout-open/cwd Tty+bash Save
+| Mode Filter Collapse Del Mode Filter Collapse Del
| ls ls
| sub sub
|
|
-|
| echo one; echo two
|
|
@@ -17,21 +17,26 @@
|
|
| /tmp/pardes-snap/layout-open/cwd Tty+bash Save
+| Mode Filter Collapse Del
| ls
| sub
-== snap altn-left grid=100x31 cursor=4,9
-|6: /tmp/pardes-snap/layout-open/cwd Tty+bash Save
-|7: ls
-|8: sub
-== snap altn-right grid=100x31 cursor=54,9
-|6: /tmp/pardes-snap/layout-open/cwd Tty+bash Save /tmp/pardes-snap/layout-open/cwd Tty+bash Save
-|7: ls ls
-|8: sub sub
-== snap file-open grid=100x31 cursor=7,22
-|21: /tmp/pardes-snap/layout-open/cwd/sub/f.txt Save
-|22: 1 echo 'TTY'OK
-== snap ttydir grid=100x31 cursor=7,22
-|10: /tmp/pardes-snap/layout-open/cwd/sub (echo 'TTY
-|11: % echo 'TTY'OK
-|12: TTYOK
-|13: exit 0
+== snap altn-left grid=100x31 cursor=4,11
+|7: /tmp/pardes-snap/layout-open/cwd Tty+bash Save
+|8: Mode Filter Collapse Del
+|9: ls
+|10: sub
+== snap altn-right grid=100x31 cursor=54,11
+|7: /tmp/pardes-snap/layout-open/cwd Tty+bash Save /tmp/pardes-snap/layout-open/cwd Tty+bash Save
+|8: Mode Filter Collapse Del Mode Filter Collapse Del
+|9: ls ls
+|10: sub sub
+== snap file-open grid=100x31 cursor=7,24
+|22: /tmp/pardes-snap/layout-open/cwd/sub/f.txt Sav
+|23: e Tty Collapse Del
+|24: 1 echo 'TTY'OK
+== snap ttydir grid=100x31 cursor=7,24
+|20: sub /tmp/pardes-snap/layout-open/cwd/sub (echo 'TT
+|21: Y'OK) exit 0 Kill Save Collapse Del
+|22: /tmp/pardes-snap/layout-open/cwd/sub/f.txt Sav % echo 'TTY'OK
+|23: e Tty Collapse Del TTYOK
+|24: 1 echo 'TTY'OK exit 0
diff --git a/test/snapshots/layout-open.snap b/test/snapshots/layout-open.snap
index 04770f27..43cf56dd 100644
--- a/test/snapshots/layout-open.snap
+++ b/test/snapshots/layout-open.snap
@@ -47,10 +47,10 @@ snap file-open
# execute the file's command (middle drag over line 1): it runs as a command
# pane for sub/, forked at the bottom of the rightmost column, splitting only
# the pane above it
-press middle 8 23
-drag middle 19 23
-release middle 19 23
+press middle 8 25
+drag middle 19 25
+release middle 19 25
wait 10000 TTYOK
-motion 40 25
+motion 40 27
stable 700 10000
snap ttydir
diff --git a/test/snapshots/leader.golden b/test/snapshots/leader.golden
index 18210edf..6c154c6c 100644
--- a/test/snapshots/leader.golden
+++ b/test/snapshots/leader.golden
@@ -8,27 +8,54 @@
== snap abandoned grid=100x41 cursor=7,3
|3: 1 Help
== snap unmapped grid=100x41 cursor=7,3
-== snap help-group grid=100x41 cursor=7,23
-|22: /tmp/pardes-snap/leader/cwd/+Help Save Tty Collapse Del
-|23: 1 pardes builtins under SPC h
-|24: 2
-|25: 3 SPC h c Changelog topbar
-|26: 4 SPC h m Messages
-|27: 5 SPC h t Tutor topbar
-|28: 6
-== snap tutor grid=100x41 cursor=7,13
-|12: /Tutor Save Tty Collapse Del
-|13: 1
-|14: 2 PARDES TUTOR
-|15: 3 tmux + vi + acme, from scratch.
-|16: 4
-|17: 5 The model is acme's: EVERYTHING is text, editable and executable
-|18: 6 the same way. Modal editing (helix-style) lives on top of that.
-|19: 7 Press j until you reach the introduction.
-|20: 8
-|21: 9
-|23: 1 pardes builtins under SPC h Tutor
+== snap help-group grid=100x41 cursor=7,6
+|5: /tmp/pardes-snap/leader/cwd/+Help Save Tty Collapse Del
+|6: 1 pardes builtins under SPC h
+|7: 2
+|8: 3 SPC h c Changelog topbar
+|9: 4 SPC h m Messages
+|10: 5 SPC h t Tutor topbar
+|11: 6
+== snap tutor grid=100x41 cursor=7,12
+|6: 1 pardes builtins under SPC h Tutor
+|11: /Tutor Save Tty Collapse Del
+|12: 1
+|13: 2 PARDES TUTOR
+|14: 3 tmux + vi + acme, from scratch.
+|15: 4
+|16: 5 The model is acme's: EVERYTHING is text, editable and executable
+|17: 6 the same way. Modal editing (helix-style) lives on top of that.
+|18: 7 Press j until you reach the introduction.
+|19: 8
+|20: 9
+|21: 10 =================================================================
+|22: 11 = INTRODUCTION =
+|23: 12 =================================================================
+|24: 13
+|25: 14 A pane is just text on screen. There is no separate "buffer" type —
+|26: 15 what you see is what you edit. Some panes happen to be live terminals;
+|27: 16 most are text you move a cursor over. Modal editing is layered on top.
+|28: 17
+|29: 18 Three modes (the BOX at the pane's top-left corner shows which):
+|30: 19 NORMAL block cursor, keys move and edit. The box is BLANK.
+|31: 20 ^ INSERT keys type text at the cursor.
+|32: 21 $ TTY keys go straight to the shell. (terminals only)
+|33: 22
+|34: 23 A bare `pardes` opens one shell already in TTY. Give it a file or a
+|35: 24 directory and you start in NORMAL.
+|36: 25
+|37: 26 SIX PARTS, ordered by what is most different from editors you know:
+|38: 27 1 — THE MOUSE. Acme's three buttons; nothing like vim.
+|39: 28 2 — PANES. Moving between them. Esc, Shift-Esc, Ctrl-w.
+|40: 29 3 — THE TTY. Raw input, prompt-aware Esc, and the mode tag.
== snap deleted grid=100x41 cursor=7,3
+|5:
+|6:
+|7:
+|8:
+|9:
+|10:
+|11:
|12:
|13:
|14:
@@ -39,32 +66,6 @@
|19:
|20:
|21:
-|23: 1 pardes builtins under SPC h
-== snap help-window grid=100x41 cursor=7,23
-|23: 1 pardes builtins under SPC w
-|25: 3 SPC w h Left C-w h, C-w left
-|26: 4 SPC w j Down C-w j, C-w down
-|27: 5 SPC w k Up C-w k, C-w up
-|28: 6 SPC w l Right C-w l, C-w right
-|29: 7
-== snap help-language grid=100x41 cursor=7,23
-|23: 1 pardes builtins under SPC l
-|25: 3 SPC l C Callees
-|26: 4 SPC l D WsDiagnostics
-|27: 5 SPC l S WsSymbols
-|28: 6 SPC l T Subtypes
-|29: 7 SPC l a CodeAction
-|30: 8 SPC l c Callers
-|31: 9 SPC l d Diagnostics
-|32: 10 SPC l h SelectRefs
-|33: 11 SPC l i Lspinfo
-|34: 12 SPC l k Hover
-|35: 13 SPC l r Rename
-|36: 14 SPC l s Symbols
-|37: 15 SPC l t Supertypes
-|38: 16 SPC l w Lspwhy
-|39: 17
-== snap del-output grid=100x41 cursor=7,3
|22:
|23:
|24:
@@ -78,8 +79,60 @@
|32:
|33:
|34:
+|35: /tmp/pardes-snap/leader/cwd/+Help Save Tty Collapse Del
+|36: 1 pardes builtins under SPC h
+|37: 2
+|38: 3 SPC h c Changelog topbar
+|39: 4 SPC h m Messages
+|40: 5 SPC h t Tutor topbar
+== snap help-window grid=100x41 cursor=7,6
+|5: /tmp/pardes-snap/leader/cwd/+Help Save Tty Collapse Del
+|6: 1 pardes builtins under SPC w
+|7: 2
+|8: 3 SPC w h Left C-w h, C-w left
+|9: 4 SPC w j Down C-w j, C-w down
+|10: 5 SPC w k Up C-w k, C-w up
+|11: 6 SPC w l Right C-w l, C-w right
+|12: 7
|35:
|36:
|37:
|38:
|39:
+|40:
+== snap help-language grid=100x41 cursor=7,6
+|6: 1 pardes builtins under SPC l
+|8: 3 SPC l C Callees
+|9: 4 SPC l D WsDiagnostics
+|10: 5 SPC l S WsSymbols
+|11: 6 SPC l T Subtypes
+|12: 7 SPC l a CodeAction
+|13: 8 SPC l c Callers
+|14: 9 SPC l d Diagnostics
+|15: 10 SPC l h SelectRefs
+|16: 11 SPC l i Lspinfo
+|17: 12 SPC l k Hover
+|18: 13 SPC l r Rename
+|19: 14 SPC l s Symbols
+|20: 15 SPC l t Supertypes
+|21: 16 SPC l w Lspwhy
+|22: 17
+== snap del-output grid=100x41 cursor=7,3
+|5:
+|6:
+|7:
+|8:
+|9:
+|10:
+|11:
+|12:
+|13:
+|14:
+|15:
+|16:
+|17:
+|18:
+|19:
+|20:
+|21:
+|22:
diff --git a/test/snapshots/leader.snap b/test/snapshots/leader.snap
index 78594a47..d8bbf233 100644
--- a/test/snapshots/leader.snap
+++ b/test/snapshots/leader.snap
@@ -32,11 +32,16 @@ key space h t
wait 10000 PARDES TUTOR
stable 700 15000
snap tutor
-# the tutor has panes above and below it, so SPC d asks: k gives its rows up
+# the tutor is at the foot of its column (acme's placement put it under the
+# +Help's text), so SPC d has nothing to ask and closes it at once
key space d
-key k
stable 700 15000
snap deleted
+# ...and the +Help it squeezed goes too, so the full index below opens with
+# room to show it
+key c-w j
+key space d
+stable 700 15000
key space ?
wait 10000 pardes builtins, and how to run each:
diff --git a/test/snapshots/load.golden b/test/snapshots/load.golden
index 3a3c596a..9dfbcecd 100644
--- a/test/snapshots/load.golden
+++ b/test/snapshots/load.golden
@@ -1,7 +1,8 @@
== snap loaded grid=100x31 cursor=4,16
|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/load/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi
+| /tmp/pardes-snap/load/cwd Tty+bash Save Mode F /tmp/pardes-snap/load/cwd Tty+bash Save Mode F
+| ilter Collapse Del ilter Collapse Del
| $ echo LOAD''MARK; seq 1 40; echo E''ND $ ls
| LOADMARK d.txt
| 1 $
@@ -15,8 +16,8 @@
| 9
| 10
| 11
-| 12
-| /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi
+| /tmp/pardes-snap/load/cwd Tty+bash Save Mode F
+| ilter Collapse Del
| $ ls
| d.txt
| $
@@ -24,13 +25,13 @@
== style loaded grid=100x31
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-86 #d3b0d9,#1d2824, 87-99 #b2c3b8,#1d2824,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-86 #d3b0d9,#1d2824, 87-98 #b2c3b8,#1d2824, 99-99 d,#1d2824,
+|3: 0-2 d,#26362d, 3-20 #becfc4,#26362d, 21-49 d,#26362d, 50-52 d,#1d2824, 53-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-73 #e5eee5,#0d1410,f 74-99 #e5eee5,#0d1410,
-|7: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
-|8: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|7: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-73 #e5eee5,#0d1410,f 74-99 #e5eee5,#0d1410,
+|8: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
|9: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|10: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|11: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
@@ -39,13 +40,13 @@
|14: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|15: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|16: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|18: 0-2 d,#1d2824, 3-20 #b2c3b8,#1d2824, 21-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-23 #e5eee5,#0d1410,f 24-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|22: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-23 #e5eee5,#0d1410,f 24-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|23: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
diff --git a/test/snapshots/look-center.golden b/test/snapshots/look-center.golden
index 3eaa819b..e5db101e 100644
--- a/test/snapshots/look-center.golden
+++ b/test/snapshots/look-center.golden
@@ -1,4 +1,4 @@
-== snap center-open grid=150x31 cursor=7,12
+== snap center-open grid=150x31 cursor=15,12
|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/look-center/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del
@@ -19,7 +19,7 @@
| /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del
| ls
| big.txt
-== snap center-dedup grid=150x31 cursor=7,12
+== snap center-dedup grid=150x31 cursor=15,12
|7: 145 line 145
|8: 146 line 146 big.txt:150 big.txt:2
|9: 147 line 147
@@ -30,7 +30,7 @@
|14: 152 line 152
|15: 153 line 153
|16: 154 line 154
-== snap center-clamp grid=150x31 cursor=7,8
+== snap center-clamp grid=150x31 cursor=13,8
|7: 1 line 1
|8: 2 line 2 big.txt:150 big.txt:2
|9: 3 line 3
diff --git a/test/snapshots/look-dir.golden b/test/snapshots/look-dir.golden
index 42627413..a946d119 100644
--- a/test/snapshots/look-dir.golden
+++ b/test/snapshots/look-dir.golden
@@ -1,11 +1,13 @@
-== snap dir-open grid=100x31 cursor=4,9
+== snap dir-open grid=100x31 cursor=4,11
|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/look-dir/cwd Tty+bash Save Mod /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod
+| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mo /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mo
+| de Filter Collapse Del de Filter Collapse Del
| ls ls
| sub top.txt sub top.txt
|
-| /tmp/pardes-snap/look-dir/cwd/sub Tty+bash Save
+| /tmp/pardes-snap/look-dir/cwd/sub Tty+bash Sav
+| e Mode Filter Collapse Del
| ls
| inner.txt
|
@@ -14,8 +16,7 @@
|
|
|
-|
-|
-| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod
+| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mo
+| de Filter Collapse Del
| ls
| sub top.txt
diff --git a/test/snapshots/look-file-1col.golden b/test/snapshots/look-file-1col.golden
index f70f787b..45f9f20f 100644
--- a/test/snapshots/look-file-1col.golden
+++ b/test/snapshots/look-file-1col.golden
@@ -1,4 +1,4 @@
-== snap file-open-1col grid=100x31 cursor=7,19
+== snap file-open-1col grid=100x31 cursor=14,19
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/look-file-1col/cwd Tty+bash Save Mode Filter Collapse Del
diff --git a/test/snapshots/look-file.golden b/test/snapshots/look-file.golden
index 19b7899d..f0690264 100644
--- a/test/snapshots/look-file.golden
+++ b/test/snapshots/look-file.golden
@@ -1,4 +1,4 @@
-== snap file-open grid=150x31 cursor=7,12
+== snap file-open grid=150x31 cursor=14,12
|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/look-file/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/look-file/cwd Tty+bash Save Mode Filter Collapse Del
@@ -32,7 +32,7 @@
|9: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|10: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|11: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
-|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-14 #f5f7ee,#3c614a, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|13: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|14: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|15: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
diff --git a/test/snapshots/lookloaded.golden b/test/snapshots/lookloaded.golden
index 8883609a..462affe8 100644
--- a/test/snapshots/lookloaded.golden
+++ b/test/snapshots/lookloaded.golden
@@ -1,12 +1,13 @@
-== snap file-open grid=100x31 cursor=7,12
+== snap file-open grid=100x31 cursor=14,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/lookloaded/cwd Tty+bash Save M /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M
+| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save /tmp/pardes-snap/lookloaded/cwd Tty+bash Save
+| Mode Filter Collapse Del Mode Filter Collapse Del
| ls ls
| big.txt big.txt
|
-| /tmp/pardes-snap/lookloaded/cwd/big.txt Save Tt
-| 35 line 35
+| /tmp/pardes-snap/lookloaded/cwd/big.txt Save T
+| ty Collapse Del
| 36 line 36
| 37 line 37
| 38 line 38
@@ -15,21 +16,20 @@
| 41 line 41
| 42 line 42
| 43 line 43
-| 44 line 44
-| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M
+| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save
+| Mode Filter Collapse Del
| ls
| big.txt
-== snap removed grid=100x31 cursor=54,7
-|5: rm big.txt; echo GO''NE
-|6: /tmp/pardes-snap/lookloaded/cwd/big.txt Save Tt GONE
-== snap look-noFS grid=100x31 cursor=7,12
-|7: 5 line 5
-|8: 6 line 6 big.txt:10
-|9: 7 line 7
-|10: 8 line 8
-|11: 9 line 9
-|12: 10 line 10
-|13: 11 line 11
-|14: 12 line 12
-|15: 13 line 13
-|16: 14 line 14
+== snap removed grid=100x31 cursor=54,8
+|6: rm big.txt; echo GO''NE
+|7: /tmp/pardes-snap/lookloaded/cwd/big.txt Save T GONE
+== snap look-noFS grid=100x31 cursor=14,13
+|8: ty Collapse Del big.txt:10
+|9: 6 line 6
+|10: 7 line 7
+|11: 8 line 8
+|12: 9 line 9
+|13: 10 line 10
+|14: 11 line 11
+|15: 12 line 12
+|16: 13 line 13
diff --git a/test/snapshots/lookpanes.golden b/test/snapshots/lookpanes.golden
index bfaf1d04..79cbb368 100644
--- a/test/snapshots/lookpanes.golden
+++ b/test/snapshots/lookpanes.golden
@@ -1,74 +1,80 @@
== snap shells grid=130x51 cursor=122,4
|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/lookpanes/cwd/beta/bmark.txt Save Tty Collaps /tmp/pardes-snap/lookpanes/cwd Tty+bash Save Mode Filter Colla
-| 1 both.txt ls
-| 2 b-marker alpha beta zzboot.txt zqonly.txt pick.txt twin.txt
+| /tmp/pardes-snap/lookpanes/cwd/beta/bmark.txt Save Tty Collap /tmp/pardes-snap/lookpanes/cwd Tty+bash Save Mode Filter Coll
+| se Del apse Del
+| 1 both.txt ls zqonly.txt pick.txt twin.txt
+| 2 b-marker alpha beta zzboot.txt
|
-| /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filter
+| /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filte
+| r Collapse Del
| ls
| amark.txt both.txt pick.txt twin.txt zqonly.txt
-== snap other-dir grid=130x51 cursor=7,28
-|27: /tmp/pardes-snap/lookpanes/cwd/alpha/zqonly.txt Save Tty Colla
-|28: 1 ONLY-IN-ALPHA
-== snap clicked-wins grid=130x51 cursor=7,16
-|15: /tmp/pardes-snap/lookpanes/cwd/beta/both.txt Save Tty Collapse
-|16: 1 BOTH-FROM-BETA
-== snap mru-alpha grid=130x51 cursor=7,22
-|21: /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collaps
-|22: 1 PICK-FROM-ALPHA
-== snap mru-beta grid=130x51 cursor=7,10
-|9: /tmp/pardes-snap/lookpanes/cwd/beta/twin.txt Save Tty Collapse
-|10: 1 TWIN-FROM-BETA
+== snap other-dir grid=130x51 cursor=7,8
+|6: /tmp/pardes-snap/lookpanes/cwd/alpha/zqonly.txt Save Tty Coll
+|7: apse Del /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filte
+|8: 1 ONLY-IN-ALPHA r Collapse Del
+== snap clicked-wins grid=130x51 cursor=7,11
+|9: /tmp/pardes-snap/lookpanes/cwd/beta/both.txt Save Tty Collaps ls
+|10: e Del amark.txt both.txt pick.txt twin.txt zqonly.txt
+|11: 1 BOTH-FROM-BETA
+== snap mru-alpha grid=130x51 cursor=72,14
+|12: /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collap
+|13: se 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 Collaps /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collap
+|13: e Del se Del
+|14: 1 TWIN-FROM-BETA 1 PICK-FROM-ALPHA
== style mru-beta grid=130x51
|0: 0-70 #b2c3b8,#1d2824, 71-129 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-64 d,#26362d, 65-66 #ffffff,#887455, 67-67 d,#1d2824, 68-99 #b2c3b8,#1d2824, 100-129 d,#1d2824,
-|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-47 #d3b0d9,#1d2824, 48-64 #b2c3b8,#1d2824, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-98 #b2c3b8,#1d2824, 99-106 #d3b0d9,#1d2824, 107-129 #b2c3b8,#1d2824,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
+|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-47 #d3b0d9,#1d2824, 48-63 #b2c3b8,#1d2824, 64-64 d,#1d2824, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-98 #b2c3b8,#1d2824, 99-106 #d3b0d9,#1d2824, 107-128 #b2c3b8,#1d2824, 129-129 d,#1d2824,
+|3: 0-2 d,#1d2824, 3-8 #b2c3b8,#1d2824, 9-67 d,#1d2824, 68-75 #b2c3b8,#1d2824, 76-129 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-121 #e5eee5,#0d1410, 122-129 d,#0d1410,
-|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-129 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824,
-|7: 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,
-|8: 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,
-|9: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-38 #becfc4,#26362d, 39-46 #dfbce5,#26362d, 47-64 #becfc4,#26362d, 65-65 d,#608269, 66-129 d,#0d1410,
-|10: 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-129 #e5eee5,#0d1410,
-|11: 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,
-|12: 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,
-|13: 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,
-|14: 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,
-|15: 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,
-|16: 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-66 d,#0d1410, 67-129 #e5eee5,#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-129 #e5eee5,#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-129 #e5eee5,#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-129 #e5eee5,#0d1410,
-|20: 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,
-|21: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-39 #b2c3b8,#1d2824, 40-47 #d3b0d9,#1d2824, 48-64 #b2c3b8,#1d2824, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-21 #e5eee5,#0d1410, 22-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
-|23: 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,
-|24: 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,
-|25: 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,
-|26: 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,
-|27: 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-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
-|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
-|29: 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,
-|30: 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,
-|31: 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,
-|32: 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,
-|33: 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,
-|34: 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,
-|35: 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,
-|36: 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,
-|37: 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,
-|38: 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,
-|39: 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,
-|40: 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,
-|41: 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,
-|42: 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,
-|43: 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,
-|44: 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,
-|45: 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,
-|46: 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,
-|47: 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,
-|48: 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,
-|49: 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,
-|50: 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,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
+|6: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-39 #b2c3b8,#1d2824, 40-49 #d3b0d9,#1d2824, 50-63 #b2c3b8,#1d2824, 64-64 d,#1d2824, 65-65 d,#608269, 66-129 d,#0d1410,
+|7: 0-2 d,#1d2824, 3-10 #b2c3b8,#1d2824, 11-64 d,#1d2824, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-128 #b2c3b8,#1d2824, 129-129 d,#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-81 #b2c3b8,#1d2824, 82-129 d,#1d2824,
+|9: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-46 #d3b0d9,#1d2824, 47-63 #b2c3b8,#1d2824, 64-64 d,#1d2824, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
+|10: 0-2 d,#1d2824, 3-7 #b2c3b8,#1d2824, 8-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,#26362d, 3-38 #becfc4,#26362d, 39-46 #dfbce5,#26362d, 47-63 #becfc4,#26362d, 64-64 d,#26362d, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-128 #b2c3b8,#1d2824, 129-129 d,#1d2824,
+|13: 0-2 d,#26362d, 3-7 #becfc4,#26362d, 8-64 d,#26362d, 65-67 d,#1d2824, 68-73 #b2c3b8,#1d2824, 74-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,
+|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,
+|20: 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,
+|21: 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,
+|22: 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,
+|23: 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,
+|24: 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,
+|25: 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,
+|26: 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,
+|27: 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,
+|28: 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,
+|29: 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,
+|30: 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,
+|31: 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,
+|32: 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,
+|33: 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,
+|34: 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,
+|35: 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,
+|36: 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,
+|37: 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,
+|38: 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,
+|39: 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,
+|40: 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,
+|41: 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,
+|42: 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,
+|43: 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,
+|44: 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,
+|45: 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,
+|46: 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,
+|47: 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,
+|48: 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,
+|49: 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,
+|50: 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/lookpanes.snap b/test/snapshots/lookpanes.snap
index 8ada3f1b..e1db907a 100644
--- a/test/snapshots/lookpanes.snap
+++ b/test/snapshots/lookpanes.snap
@@ -35,8 +35,8 @@ key space w h
stable 400 5000
# an ordinary dir look on the shell's own ls output: a shell in ./alpha,
# split below it
-press right 70 5
-release right 70 5
+press right 70 6
+release right 70 6
wait 10000 amark.txt
stable 700 20000
# type the words to look into what is left of the workdir shell (past its own
@@ -51,8 +51,8 @@ settle 100
stable 400 5000
snap shells
# make the beta pane the most recently focused, so the walk has to pass it
-press left 10 4
-release left 10 4
+press left 10 5
+release left 10 5
stable 700 10000
# zqonly.txt is only under alpha: the clicked shell's cwd fails, beta fails,
# the alpha shell answers. Before, this word became a search.
@@ -64,15 +64,15 @@ snap other-dir
# both.txt is in BOTH dirs and alpha is now the most recent (its file pane
# just opened), but the CLICK still wins: looked inside the beta pane it
# opens beta's copy
-press right 10 4
-release right 10 4
+press right 10 5
+release right 10 5
wait 10000 BOTH-FROM-BETA
stable 700 20000
snap clicked-wins
# from a pane whose own dir holds neither copy the access order decides:
# focus the alpha shell, look pick.txt (in both dirs) -> alpha's
-press left 70 13
-release left 70 13
+press left 70 14
+release left 70 14
stable 700 10000
press right 109 5
release right 109 5
@@ -81,8 +81,8 @@ stable 700 20000
snap mru-alpha
# focus the beta pane instead and the same look flips: twin.txt is in both
# dirs, beta is most recent, beta's copy opens
-press left 10 4
-release left 10 4
+press left 10 5
+release left 10 5
stable 700 10000
press right 118 5
release right 118 5
diff --git a/test/snapshots/lookrange.golden b/test/snapshots/lookrange.golden
index 3db2246d..7d802b0b 100644
--- a/test/snapshots/lookrange.golden
+++ b/test/snapshots/lookrange.golden
@@ -6,17 +6,17 @@
| 2 r.txt:7:4-9
| 3 r.txt:3:2-5:3
| 4 r.txt:8:6-99:9
-== snap wholelines grid=150x31 cursor=11,22
-|17: /tmp/pardes-snap/lookrange/cwd/r.txt Save Tty Collapse Del
-|18: 1 aaaa
-|19: 2 bbbb
-|20: 3 cccc dccc
-|21: 4 dddd
-|22: 5 eeee
-|23: 6 ffff
-|24: 7 gg NEEDLE gg
-|25: 8 hhhh NEEDLE
-|26: 9 iiii
+== snap wholelines grid=150x31 cursor=11,12
+|7: /tmp/pardes-snap/lookrange/cwd/r.txt Save Tty Collapse Del
+|8: 1 aaaa
+|9: 2 bbbb
+|10: 3 cccc dccc
+|11: 4 dddd
+|12: 5 eeee
+|13: 6 ffff
+|14: 7 gg NEEDLE gg
+|15: 8 hhhh NEEDLE
+|16: 9 iiii
== style wholelines grid=150x31
|0: 0-70 #b2c3b8,#1d2824, 71-149 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-149 d,#26362d,
@@ -25,117 +25,71 @@
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-149 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-149 d,#0d1410,
|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-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,
-|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-|17: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-33 #becfc4,#26362d, 34-38 #dfbce5,#26362d, 39-60 #becfc4,#26362d, 61-149 d,#26362d,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 #f5f7ee,#3c614a,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 #f5f7ee,#3c614a,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-11 #f5f7ee,#3c614a, 12-149 d,#0d1410,
-|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
-|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-149 d,#0d1410,
-|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|7: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-33 #becfc4,#26362d, 34-38 #dfbce5,#26362d, 39-60 #becfc4,#26362d, 61-149 d,#26362d,
+|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 #f5f7ee,#3c614a,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 #f5f7ee,#3c614a,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-11 #f5f7ee,#3c614a, 12-149 d,#0d1410,
+|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
+|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-149 d,#0d1410,
+|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-== snap onelinespan grid=150x31 cursor=15,24
+== snap onelinespan grid=150x31 cursor=15,14
== style onelinespan grid=150x31
-|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-149 d,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-9 #e5eee5,#0d1410, 10-15 #f5f7ee,#3c614a, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
-== snap fullspan grid=150x31 cursor=9,22
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-149 d,#0d1410,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-9 #e5eee5,#0d1410, 10-15 #f5f7ee,#3c614a, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
+== snap fullspan grid=150x31 cursor=9,12
== style fullspan grid=150x31
-|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-149 #f5f7ee,#3c614a,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 #f5f7ee,#3c614a,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-9 #f5f7ee,#3c614a, 10-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
-== snap stale grid=150x31 cursor=10,25
-|18: 2 bbbb
-|19: 3 cccc dccc
-|20: 4 dddd
-|21: 5 eeee
-|22: 6 ffff
-|23: 7 gg NEEDLE gg
-|24: 8 hhhh NEEDLE
-|25: 9 iiii
-|26:
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-149 #f5f7ee,#3c614a,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 #f5f7ee,#3c614a,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-9 #f5f7ee,#3c614a, 10-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
+== snap stale grid=150x31 cursor=10,16
+|6: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
== style stale grid=150x31
-|18: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-149 d,#0d1410,
-|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
-|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-11 #e5eee5,#0d1410, 12-149 #f5f7ee,#3c614a,
-|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #f5f7ee,#3c614a, 11-149 d,#0d1410,
-|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-== snap hit1 grid=150x31 cursor=15,24
-|18: 1 aaaa
-|19: 2 bbbb
-|20: 3 cccc dccc
-|21: 4 dddd
-|22: 5 eeee
-|23: 6 ffff
-|24: 7 gg NEEDLE gg
-|25: 8 hhhh NEEDLE
-|26: /tmp/pardes-snap/lookrange/cwd/+Search Save Tty Collapse Del LocationsConfig
-|27: 1 r.txt:7:4-9
-|28: 2 gg NEEDLE gg
-|29: 3 r.txt:8:6-11
-|30: 4 hhhh NEEDLE
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-149 d,#0d1410,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-11 #e5eee5,#0d1410, 12-149 #f5f7ee,#3c614a,
+|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #f5f7ee,#3c614a, 11-149 d,#0d1410,
+== snap hit1 grid=150x31 cursor=15,14
+|17: /tmp/pardes-snap/lookrange/cwd/+Search Save Tty Collapse Del LocationsConfig
+|18: 1 r.txt:7:4-9
+|19: 2 gg NEEDLE gg
+|20: 3 r.txt:8:6-11
+|21: 4 hhhh NEEDLE
+|22: 5
== style hit1 grid=150x31
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-149 d,#0d1410,
-|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-9 #e5eee5,#0d1410, 10-15 #f5f7ee,#3c614a, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
-|25: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-149 d,#0d1410,
-|26: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-40 #d3b0d9,#1d2824, 41-78 #b2c3b8,#1d2824, 79-149 d,#1d2824,
-|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-17 #59675c,#0d1410, 18-149 d,#0d1410,
-|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-9 #e5eee5,#0d1410, 10-15 #f7e4a5,#5e4923, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
-|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-18 #59675c,#0d1410, 19-149 d,#0d1410,
-|30: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-11 #e5eee5,#0d1410, 12-17 #f7e4a5,#5e4923, 18-149 d,#0d1410,
-== snap hit2 grid=150x31 cursor=17,22
-|18: 4 dddd
-|19: 5 eeee
-|20: 6 ffff
-|21: 7 gg NEEDLE gg
-|22: 8 hhhh NEEDLE
-|23: 9 iiii
-|24:
-|25:
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-9 #e5eee5,#0d1410, 10-15 #f5f7ee,#3c614a, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
+|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-149 d,#0d1410,
+|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-40 #d3b0d9,#1d2824, 41-78 #b2c3b8,#1d2824, 79-149 d,#1d2824,
+|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-17 #59675c,#0d1410, 18-149 d,#0d1410,
+|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-9 #e5eee5,#0d1410, 10-15 #f7e4a5,#5e4923, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-18 #59675c,#0d1410, 19-149 d,#0d1410,
+|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-11 #e5eee5,#0d1410, 12-17 #f7e4a5,#5e4923, 18-149 d,#0d1410,
+== snap hit2 grid=150x31 cursor=17,15
== style hit2 grid=150x31
-|18: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|19: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-11 #e5eee5,#0d1410, 12-17 #f5f7ee,#3c614a, 18-149 d,#0d1410,
-|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-== snap back grid=150x31 cursor=15,22
-|18: 3 cccc dccc
-|19: 4 dddd
-|20: 5 eeee
-|21: 6 ffff
-|22: 7 gg NEEDLE gg
-|23: 8 hhhh NEEDLE
-|24: 9 iiii
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
+|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-11 #e5eee5,#0d1410, 12-17 #f5f7ee,#3c614a, 18-149 d,#0d1410,
+== snap back grid=150x31 cursor=15,14
== style back grid=150x31
-|18: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-149 d,#0d1410,
-|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-9 #e5eee5,#0d1410, 10-15 #f5f7ee,#3c614a, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
-|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-149 d,#0d1410,
-|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-149 d,#0d1410,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-9 #e5eee5,#0d1410, 10-15 #f5f7ee,#3c614a, 16-18 #e5eee5,#0d1410, 19-149 d,#0d1410,
+|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-149 d,#0d1410,
diff --git a/test/snapshots/lsp-client-edit.golden b/test/snapshots/lsp-client-edit.golden
index 7fe973f9..29ec05af 100644
--- a/test/snapshots/lsp-client-edit.golden
+++ b/test/snapshots/lsp-client-edit.golden
@@ -1,7 +1,7 @@
== snap formatted grid=100x25 cursor=7,4
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| * /tmp/pardes-snap/lsp-client-edit/cwd/main.rs * Save Tty Collapse Del TreeContext
+| * /tmp/pardes-snap/lsp-client-edit/cwd/main.rs Save Tty Collapse Del TreeContext
| 1 // formatted by mock formatted 1 range(s)
| 2 mod lib;
| 3
@@ -12,6 +12,7 @@
| 8 }
| 9
== snap unformatted grid=100x25 cursor=7,3
+|2: /tmp/pardes-snap/lsp-client-edit/cwd/main.rs Save Tty Collapse Del TreeContext
|3: 1 mod lib;
|4: 2
|5: 3 fn main() {
@@ -22,10 +23,12 @@
|10: 8
|11:
== snap renamed grid=100x25 cursor=12,5
+|2: * /tmp/pardes-snap/lsp-client-edit/cwd/main.rs Save Tty Collapse Del TreeContext
|3: 1 mod lib; renamed 2 range(s)
|6: 4 let x = sum(1, 2);
|7: 5 let y = sum(3, 4);
== snap unrenamed grid=100x25 cursor=12,5
+|2: /tmp/pardes-snap/lsp-client-edit/cwd/main.rs Save Tty Collapse Del TreeContext
|3: 1 mod lib;
|6: 4 let x = add(1, 2);
|7: 5 let y = add(3, 4);
diff --git a/test/snapshots/lsp-client.golden b/test/snapshots/lsp-client.golden
index 3dfcdd28..17eb0d40 100644
--- a/test/snapshots/lsp-client.golden
+++ b/test/snapshots/lsp-client.golden
@@ -1,4 +1,4 @@
-== snap gd-jumped grid=100x25 cursor=16,16
+== snap gd-jumped grid=100x25 cursor=16,12
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/lsp-client/cwd/main.rs Save Tty Collapse Del TreeContext
@@ -9,10 +9,6 @@
| 5 let y = add(3, 4);
| 6 let _ = x + y;
| 7 }
-| 8
-|
-|
-|
| /tmp/pardes-snap/lsp-client/cwd/lib.rs Save Tty Collapse Del TreeContext
| 1 // mock fixture
| 2 pub fn add(a: i32, b: i32) -> i32 {
@@ -21,18 +17,13 @@
| 5
== snap gr-list grid=100x25 cursor=21,6
|3: 1 mod lib;
-|20: /tmp/pardes-snap/lsp-client/cwd/+Search Save Tty Collapse Del LocationsConfig
-|21: 1 lib.rs:2:8-10
-|22: 2 pub fn add(a: i32, b: i32) -> i32 {
-|23: 3 main.rs:4:13-15
-|24: 4 let x = add(1, 2);
+|15: /tmp/pardes-snap/lsp-client/cwd/+Search Save Tty Collapse Del LocationsConfig
+|16: 1 lib.rs:2:8-10
+|17: 2 pub fn add(a: i32, b: i32) -> i32 {
+|18: 3 main.rs:4:13-15
+|19: 4 let x = add(1, 2);
+|20: 5 main.rs:5:13-15
+|21: 6 let y = add(3, 4);
+|22: 7
== snap gr-opened grid=100x25 cursor=21,7
-|21: 3 main.rs:4:13-15
-|22: 4 let x = add(1, 2);
-|23: 5 main.rs:5:13-15
-|24: 6 let y = add(3, 4);
== snap gr-back grid=100x25 cursor=21,6
-|21: 2 pub fn add(a: i32, b: i32) -> i32 {
-|22: 3 main.rs:4:13-15
-|23: 4 let x = add(1, 2);
-|24: 5 main.rs:5:13-15
diff --git a/test/snapshots/lsp-rename.golden b/test/snapshots/lsp-rename.golden
index 9151d1f3..03fb2311 100644
--- a/test/snapshots/lsp-rename.golden
+++ b/test/snapshots/lsp-rename.golden
@@ -1,7 +1,7 @@
== snap renamed grid=110x25 cursor=17,10
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| * /tmp/pardes-snap/lsp-rename/cwd/rename.zig * Save Tty Collapse Del TreeContext
+| * /tmp/pardes-snap/lsp-rename/cwd/rename.zig Save Tty Collapse Del TreeContext
| 1 const std = @import("std"); renamed 2 range(s)
| 2
| 3 fn renamed_helper(x: u32) u32 {
@@ -18,9 +18,11 @@
| 14 }
| 15
== snap undone grid=110x25 cursor=17,10
+|2: /tmp/pardes-snap/lsp-rename/cwd/rename.zig Save Tty Collapse Del TreeContext
|3: 1 const std = @import("std");
|5: 3 fn helper(x: u32) u32 {
|10: 8 _ = helper(41);
== snap redone grid=110x25 cursor=17,10
+|2: * /tmp/pardes-snap/lsp-rename/cwd/rename.zig Save Tty Collapse Del TreeContext
|5: 3 fn renamed_helper(x: u32) u32 {
|10: 8 _ = renamed_helper(41);
diff --git a/test/snapshots/lsp.golden b/test/snapshots/lsp.golden
index d6ffcc73..984a7abf 100644
--- a/test/snapshots/lsp.golden
+++ b/test/snapshots/lsp.golden
@@ -16,18 +16,20 @@
== snap definition grid=120x41 cursor=15,5
== snap hover grid=120x41 cursor=15,5
|3: 1 const std = @import("std"); Hover
-|38: /tmp/pardes-snap/lsp/cwd/+Hover Save Tty Collapse Del
-|39: 1 fn helper(x: u32) u32
-|40: 2 (fn (u32) u32)
+|12: /tmp/pardes-snap/lsp/cwd/+Hover Save Tty Collapse Del
+|13: 1 fn helper(x: u32) u32
+|14: 2 (fn (u32) u32)
+|15: 3
== snap symbols grid=120x41 cursor=15,5
|3: 1 const std = @import("std"); Symbols
-|31: /tmp/pardes-snap/lsp/cwd/+Search Save Tty Collapse Del LocationsConfig
-|32: 1 a.zig:1:7-9
-|33: 2 std
-|34: 3 a.zig:3:4-9
-|35: 4 helper fn helper(x: u32) u32
-|36: 5 a.zig:7:8-11
-|37: 6 main fn main() void
+|15: /tmp/pardes-snap/lsp/cwd/+Search Save Tty Collapse Del LocationsConfig
+|16: 1 a.zig:1:7-9
+|17: 2 std
+|18: 3 a.zig:3:4-9
+|19: 4 helper fn helper(x: u32) u32
+|20: 5 a.zig:7:8-11
+|21: 6 main fn main() void
+|22: 7
== snap step grid=120x41 cursor=15,3
|3: 1 const std = @import("std");
== snap nothing grid=120x41 cursor=7,5
diff --git a/test/snapshots/lspcomplete.golden b/test/snapshots/lspcomplete.golden
index 0a58a231..0b29034f 100644
--- a/test/snapshots/lspcomplete.golden
+++ b/test/snapshots/lspcomplete.golden
@@ -16,29 +16,30 @@
| 12 // note. see below
| 13
== snap indent grid=120x41 cursor=11,12
-|2: ^* /tmp/pardes-snap/lspcomplete/cwd/a.zig * Save Tty Collapse Del TreeContext
+|2: ^* /tmp/pardes-snap/lspcomplete/cwd/a.zig Save Tty Collapse Del TreeContext
|12: 10 }
== snap commentindent grid=120x41 cursor=19,14
|14: 12 // note. see below
== snap athedot grid=120x41 cursor=16,10
|10: 8 .
== snap completion grid=120x41 cursor=16,10
-|38: /tmp/pardes-snap/lspcomplete/cwd/+Search Save Tty Collapse Del
-|39: 1 a.zig:2:5-13 verdigris verdigris,
-|40: 2 a.zig:3:5-12 cinnabar cinnabar,
+|15: /tmp/pardes-snap/lspcomplete/cwd/+Search Save Tty Collapse Del
+|16: 1 a.zig:2:5-13 verdigris verdigris,
+|17: 2 a.zig:3:5-12 cinnabar cinnabar,
+|18: 3
== snap refill grid=120x41 cursor=16,10
== snap step grid=120x41 cursor=19,4
-|2: * /tmp/pardes-snap/lspcomplete/cwd/a.zig * Save Tty Collapse Del TreeContext
+|2: * /tmp/pardes-snap/lspcomplete/cwd/a.zig Save Tty Collapse Del TreeContext
== snap txtindent grid=120x41 cursor=15,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^* /tmp/pardes-snap/lspcomplete/cwd/notes.txt * Save Tty Collapse Del
+|^* /tmp/pardes-snap/lspcomplete/cwd/notes.txt Save Tty Collapse Del
| 1 abc. def ghi
| 2
== snap nosemi grid=100x19 cursor=29,9
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^* /tmp/pardes-snap/lspcomplete/cwd/c.zig * Save Tty Collapse Del TreeContext
+|^* /tmp/pardes-snap/lspcomplete/cwd/c.zig Save Tty Collapse Del TreeContext
| 1 const Shade = enum {
| 2 verdigris,
| 3 cinnabar,
@@ -47,18 +48,14 @@
| 6 pub fn nosemi() void {
| 7 const z: Shade = .
| 8 }
-| 9
-|
-|
-|
-|
| /tmp/pardes-snap/lspcomplete/cwd/+Search Save Tty Collapse Del
| 1 c.zig:2:5-13 verdigris verdigris,
| 2 c.zig:3:5-12 cinnabar cinnabar,
+| 3
== snap nobrace grid=100x19 cursor=16,10
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^* /tmp/pardes-snap/lspcomplete/cwd/d.zig * Save Tty Collapse Del TreeContext
+|^* /tmp/pardes-snap/lspcomplete/cwd/d.zig Save Tty Collapse Del TreeContext
| 1 const Shade = enum {
| 2 verdigris,
| 3 cinnabar,
@@ -66,12 +63,7 @@
| 5
| 6 pub fn nobrace(s: Shade) void {
| 7 switch (s) {
-| 8 .
-| 9 }
-| 10
-|
-|
-|
| /tmp/pardes-snap/lspcomplete/cwd/+Search Save Tty Collapse Del
| 1 d.zig:2:5-13 verdigris verdigris,
| 2 d.zig:3:5-12 cinnabar cinnabar,
+| 3
diff --git a/test/snapshots/lspcompletemoved.golden b/test/snapshots/lspcompletemoved.golden
index 797bcd11..a033ed56 100644
--- a/test/snapshots/lspcompletemoved.golden
+++ b/test/snapshots/lspcompletemoved.golden
@@ -1,7 +1,7 @@
== snap typedthrough grid=120x21 cursor=16,14
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^* /tmp/pardes-snap/lspcompletemoved/cwd/a.zig * Save Tty Collapse Del TreeContext
+|^* /tmp/pardes-snap/lspcompletemoved/cwd/a.zig Save Tty Collapse Del TreeContext
| 1 const Shade = enum {
| 2 verdigris,
| 3 cinnabar,
diff --git a/test/snapshots/lsprelpath.golden b/test/snapshots/lsprelpath.golden
index e2201d5c..c8121b5a 100644
--- a/test/snapshots/lsprelpath.golden
+++ b/test/snapshots/lsprelpath.golden
@@ -1,7 +1,7 @@
== snap absrow grid=100x35 cursor=16,7
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^* /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig * Save Tty Collapse Del TreeContext
+|^* /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig Save Tty Collapse Del TreeContext
| 1 const shade = @import("../shade.zig");
| 2
| 3 pub fn pick(s: shade.Shade) u8 {
@@ -9,62 +9,41 @@
| 5 .
| 6 };
| 7 }
-| 8
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search Save Tty Collapse Del
| 1 /tmp/pardes-snap/lsprelpath/cwd/shade.zig:2:5-13 verdigris verdigris,
| 2 /tmp/pardes-snap/lsprelpath/cwd/shade.zig:3:5-12 cinnabar cinnabar,
-== snap abslook grid=100x35 cursor=19,19
-|2: * /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig * Save Tty Collapse Del TreeContext
-|17: /tmp/pardes-snap/lsprelpath/cwd/shade.zig Save Tty Collapse Del TreeContext
-|18: 1 pub const Shade = enum {
-|19: 2 verdigris,
-|20: 3 cinnabar,
-|21: 4 };
-|22: 5
+| 3
+== snap abslook grid=100x35 cursor=19,15
+|2: * /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig Save Tty Collapse Del TreeContext
+|13: /tmp/pardes-snap/lsprelpath/cwd/shade.zig Save Tty Collapse Del TreeContext
+|14: 1 pub const Shade = enum {
+|15: 2 verdigris,
+|16: 3 cinnabar,
+|17: 4 };
+|18: 5
== style abslook grid=100x35
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-99 d,#26362d,
-|2: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
+|2: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-82 #b2c3b8,#1d2824, 83-99 d,#1d2824,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-27 #c9a7ed,#0d1410, 28-28 #e5eee5,#0d1410, 29-42 #b8d88a,#0d1410, 43-44 #e5eee5,#0d1410, 45-99 d,#0d1410,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-12 #c9a7ed,#0d1410, 13-38 #e5eee5,#0d1410, 39-99 d,#0d1410,
|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-16 #c9a7ed,#0d1410, 17-17 #e5eee5,#0d1410, 18-23 #c9a7ed,#0d1410, 24-29 #e5eee5,#0d1410, 30-99 d,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-99 d,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-99 d,#0d1410,
-|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-99 d,#0d1410,
-|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|17: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-43 #dfbce5,#26362d, 44-77 #becfc4,#26362d, 78-99 d,#26362d,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-24 #e5eee5,#0d1410, 25-28 #c9a7ed,#0d1410, 29-30 #e5eee5,#0d1410, 31-99 d,#0d1410,
-|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #e5eee5,#0d1410, 11-19 #f5f7ee,#3c614a, 20-20 #e5eee5,#0d1410, 21-99 d,#0d1410,
-|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-99 d,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-99 d,#0d1410,
+|9: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-99 d,#0d1410,
+|10: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-45 #d3b0d9,#1d2824, 46-67 #b2c3b8,#1d2824, 68-99 d,#1d2824,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-54 #59675c,#0d1410, 55-76 #e5eee5,#0d1410, 77-99 d,#0d1410,
+|12: 0-0 d,#152019, 1-1 d,#0d1410, 2-54 #59675c,#0d1410, 55-74 #e5eee5,#0d1410, 75-99 d,#0d1410,
+|13: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-43 #dfbce5,#26362d, 44-77 #becfc4,#26362d, 78-99 d,#26362d,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-24 #e5eee5,#0d1410, 25-28 #c9a7ed,#0d1410, 29-30 #e5eee5,#0d1410, 31-99 d,#0d1410,
+|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #e5eee5,#0d1410, 11-19 #f5f7ee,#3c614a, 20-20 #e5eee5,#0d1410, 21-99 d,#0d1410,
+|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-99 d,#0d1410,
+|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-99 d,#0d1410,
+|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|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,
@@ -75,13 +54,13 @@
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|31: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|32: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-45 #d3b0d9,#1d2824, 46-67 #b2c3b8,#1d2824, 68-99 d,#1d2824,
-|33: 0-0 d,#608269, 1-1 d,#0d1410, 2-54 #59675c,#0d1410, 55-76 #e5eee5,#0d1410, 77-99 d,#0d1410,
-|34: 0-0 d,#152019, 1-1 d,#0d1410, 2-54 #59675c,#0d1410, 55-74 #e5eee5,#0d1410, 75-99 d,#0d1410,
+|32: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+|33: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+|34: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
== snap subrow grid=100x35 cursor=16,7
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-|^* /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig * Save Tty Collapse Del TreeContext
+|^* /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig Save Tty Collapse Del TreeContext
| 1 const tint = @import("inner/tint.zig");
| 2
| 3 pub fn hue(t: tint.Tint) u8 {
@@ -89,62 +68,41 @@
| 5 .
| 6 };
| 7 }
-| 8
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
| /tmp/pardes-snap/lsprelpath/cwd/sub/+Search Save Tty Collapse Del
| 1 inner/tint.zig:2:5-9 ochre ochre,
| 2 inner/tint.zig:3:5-10 indigo indigo,
-== snap sublook grid=100x35 cursor=15,19
-|2: * /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig * Save Tty Collapse Del TreeContext
-|17: /tmp/pardes-snap/lsprelpath/cwd/sub/inner/tint.zig Save Tty Collapse Del TreeContext
-|18: 1 pub const Tint = enum {
-|19: 2 ochre,
-|20: 3 indigo,
-|21: 4 };
-|22: 5
+| 3
+== snap sublook grid=100x35 cursor=15,15
+|2: * /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig Save Tty Collapse Del TreeContext
+|13: /tmp/pardes-snap/lsprelpath/cwd/sub/inner/tint.zig Save Tty Collapse Del TreeContext
+|14: 1 pub const Tint = enum {
+|15: 2 ochre,
+|16: 3 indigo,
+|17: 4 };
+|18: 5
== style sublook grid=100x35
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-99 d,#26362d,
-|2: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
+|2: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-82 #b2c3b8,#1d2824, 83-99 d,#1d2824,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-26 #c9a7ed,#0d1410, 27-27 #e5eee5,#0d1410, 28-43 #b8d88a,#0d1410, 44-45 #e5eee5,#0d1410, 46-99 d,#0d1410,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-12 #c9a7ed,#0d1410, 13-35 #e5eee5,#0d1410, 36-99 d,#0d1410,
|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-16 #c9a7ed,#0d1410, 17-17 #e5eee5,#0d1410, 18-23 #c9a7ed,#0d1410, 24-29 #e5eee5,#0d1410, 30-99 d,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-99 d,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-99 d,#0d1410,
-|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-99 d,#0d1410,
-|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|17: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-44 #becfc4,#26362d, 45-52 #dfbce5,#26362d, 53-86 #becfc4,#26362d, 87-99 d,#26362d,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-23 #e5eee5,#0d1410, 24-27 #c9a7ed,#0d1410, 28-29 #e5eee5,#0d1410, 30-99 d,#0d1410,
-|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #e5eee5,#0d1410, 11-15 #f5f7ee,#3c614a, 16-16 #e5eee5,#0d1410, 17-99 d,#0d1410,
-|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-99 d,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-99 d,#0d1410,
+|9: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-99 d,#0d1410,
+|10: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-45 #d3b0d9,#1d2824, 46-67 #b2c3b8,#1d2824, 68-99 d,#1d2824,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-26 #59675c,#0d1410, 27-40 #e5eee5,#0d1410, 41-99 d,#0d1410,
+|12: 0-0 d,#152019, 1-1 d,#0d1410, 2-27 #59675c,#0d1410, 28-43 #e5eee5,#0d1410, 44-99 d,#0d1410,
+|13: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-44 #becfc4,#26362d, 45-52 #dfbce5,#26362d, 53-86 #becfc4,#26362d, 87-99 d,#26362d,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-23 #e5eee5,#0d1410, 24-27 #c9a7ed,#0d1410, 28-29 #e5eee5,#0d1410, 30-99 d,#0d1410,
+|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #e5eee5,#0d1410, 11-15 #f5f7ee,#3c614a, 16-16 #e5eee5,#0d1410, 17-99 d,#0d1410,
+|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-99 d,#0d1410,
+|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-99 d,#0d1410,
+|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|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,
@@ -155,7 +113,8 @@
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|31: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
-|32: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-45 #d3b0d9,#1d2824, 46-67 #b2c3b8,#1d2824, 68-99 d,#1d2824,
-|33: 0-0 d,#608269, 1-1 d,#0d1410, 2-26 #59675c,#0d1410, 27-40 #e5eee5,#0d1410, 41-99 d,#0d1410,
-|34: 0-0 d,#152019, 1-1 d,#0d1410, 2-27 #59675c,#0d1410, 28-43 #e5eee5,#0d1410, 44-99 d,#0d1410,
-== snap clicked grid=100x35 cursor=16,20
+|32: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+|33: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+|34: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
+== snap clicked grid=100x35 cursor=16,16
+|12: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
diff --git a/test/snapshots/lsprelpath.snap b/test/snapshots/lsprelpath.snap
index 247390fe..1d662f8a 100644
--- a/test/snapshots/lsprelpath.snap
+++ b/test/snapshots/lsprelpath.snap
@@ -76,14 +76,14 @@ key enter
stable 700 15000
snap sublook
snapstyle sublook
-# The mouse spelling, on the OTHER row (grid row 33, 1-based click row 34). A
+# The mouse spelling, on the OTHER row (grid row 12, 1-based click row 13). A
# right click expands to the file-ish word under the cursor
# (config.wordBounds), which has to take `inner/tint.zig:3:5-10` WHOLE — the
# separator inside a stripped path is a file char, so the scan runs back over
# `inner/` and stops on the space of the line-number prefix. A look focuses
# what it opens, so the header's cursor is the assertion: the end of `indigo`
# on line 3 of tint.zig.
-press right 12 35
-release right 12 35
+press right 12 13
+release right 12 13
stable 700 15000
snap clicked
diff --git a/test/snapshots/mini.golden b/test/snapshots/mini.golden
index cb51368d..b75bbd2b 100644
--- a/test/snapshots/mini.golden
+++ b/test/snapshots/mini.golden
@@ -1,17 +1,9 @@
-== snap opened grid=90x23 cursor=7,14
+== snap opened grid=90x23 cursor=7,6
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/mini/cwd/commands.txt Save Tty Collapse Del
| 1 @`Mini demo.zig` Mini
|
-|
-|
-|
-|
-|
-|
-|
-|
| /tmp/pardes-snap/mini/cwd/Mini demo.zig Save Tty Collapse Del
| 1 ⠭⠍⣥⢩⣭⡭⣭⣬⣌⢭⣭⠌⠬⠅⠍⠁
| 2 ⣂⢀⣉⣉⢉⣈⢁⣀⣀⢀⣀⣀⣀⢀⣀⣀⣀⣀⣀
@@ -23,25 +15,25 @@
|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-40 #d3b0d9,#1d2824, 41-62 #b2c3b8,#1d2824, 63-89 d,#1d2824,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-22 #e5eee5,#0d1410, 23-83 d,#0d1410, 84-84 d,#1d2824, 85-88 #b2c3b8,#1d2824, 89-89 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
-|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
-|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
-|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
+|5: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-41 #dfbce5,#26362d, 42-63 #becfc4,#26362d, 64-89 d,#26362d,
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-8 #e5eee5,#0d1410, 9-9 #c9a7ed,#0d1410, 10-12 #e5eee5,#0d1410, 13-15 #c9a7ed,#0d1410, 16-21 #b8d88a,#0d1410, 22-22 #e5eee5,#0d1410, 23-89 d,#0d1410,
+|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #b9f480,#0d1410, 9-11 #c9a7ed,#0d1410, 12-25 #b9f480,#0d1410, 26-89 d,#0d1410,
+|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-19 #f8c082,#0d1410, 20-20 #e5eee5,#0d1410, 21-89 d,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
-|13: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-41 #dfbce5,#26362d, 42-63 #becfc4,#26362d, 64-89 d,#26362d,
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-8 #e5eee5,#0d1410, 9-9 #c9a7ed,#0d1410, 10-12 #e5eee5,#0d1410, 13-15 #c9a7ed,#0d1410, 16-21 #b8d88a,#0d1410, 22-22 #e5eee5,#0d1410, 23-89 d,#0d1410,
-|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #b9f480,#0d1410, 9-11 #c9a7ed,#0d1410, 12-25 #b9f480,#0d1410, 26-89 d,#0d1410,
-|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-19 #f8c082,#0d1410, 20-20 #e5eee5,#0d1410, 21-89 d,#0d1410,
+|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
+|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
+|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
-== 9p opened grid=90x23 cursor=7,14,block
+== 9p opened grid=90x23 cursor=7,6,block
|0: Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
@0: 0-70 #b2c3b8,#1d2824,,tagline 71-89 d,#1d2824,,tagline
|1: New Tty Find Grep Joincol Delcol
@@ -52,15 +44,18 @@
@3: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-22 #e5eee5,#0d1410,,body 23-83 d,#0d1410,,body 84-84 d,#1d2824,,tagline 85-88 #b2c3b8,#1d2824,,tagline 89-89 d,#1d2824,,tagline
|4:
@4: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
-|5:
-@5: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
-|6:
-@6: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
-|7:
-@7: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
-|8:
-@8: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
-|9:
+|5: /tmp/pardes-snap/mini/cwd/Mini demo.zig Save Tty Collapse Del
+@5: 0-1 #000000,#add38a,,tagline 2-2 d,#26362d,,tagline 3-28 #becfc4,#26362d,,tagline 29-41 #dfbce5,#26362d,,tagline 42-63 #becfc4,#26362d,,tagline 64-89 d,#26362d,,tagline
+|6: 1 ⠭⠍⣥⢩⣭⡭⣭⣬⣌⢭⣭⠌⠬⠅⠍⠁
++6: 7="⠭" 8="⠍" 9="⣥" 10="⢩" 11="⣭" 12="⡭" 13="⣭" 14="⣬" 15="⣌" 16="⢭" 17="⣭" 18="⠌" 19="⠬" 20="⠅" 21="⠍" 22="⠁"
+@6: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #616f64,#0d1410,b,body 7-8 #e5eee5,#0d1410,,body 9-9 #c9a7ed,#0d1410,,body 10-12 #e5eee5,#0d1410,,body 13-15 #c9a7ed,#0d1410,,body 16-21 #b8d88a,#0d1410,,body 22-22 #e5eee5,#0d1410,,body 23-89 d,#0d1410,,body
+|7: 2 ⣂⢀⣉⣉⢉⣈⢁⣀⣀⢀⣀⣀⣀⢀⣀⣀⣀⣀⣀
++7: 7="⣂" 8="⢀" 9="⣉" 10="⣉" 11="⢉" 12="⣈" 13="⢁" 14="⣀" 15="⣀" 16="⢀" 17="⣀" 18="⣀" 19="⣀" 20="⢀" 21="⣀" 22="⣀" 23="⣀" 24="⣀" 25="⣀"
+@7: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-8 #b9f480,#0d1410,,body 9-11 #c9a7ed,#0d1410,,body 12-25 #b9f480,#0d1410,,body 26-89 d,#0d1410,,body
+|8: 3 ⠉⠉⠁⠉⠉⠉⠈⠈⠉⠉⠉⠉⠉⠉
++8: 7="⠉" 8="⠉" 9="⠁" 10="⠉" 11="⠉" 12="⠉" 13="⠈" 14="⠈" 15="⠉" 16="⠉" 17="⠉" 18="⠉" 19="⠉" 20="⠉"
+@8: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-17 #e5eee5,#0d1410,,body 18-19 #f8c082,#0d1410,,body 20-20 #e5eee5,#0d1410,,body 21-89 d,#0d1410,,body
+|9: 4
@9: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
|10:
@10: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
@@ -68,18 +63,15 @@
@11: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
|12:
@12: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
-|13: /tmp/pardes-snap/mini/cwd/Mini demo.zig Save Tty Collapse Del
-@13: 0-1 #000000,#add38a,,tagline 2-2 d,#26362d,,tagline 3-28 #becfc4,#26362d,,tagline 29-41 #dfbce5,#26362d,,tagline 42-63 #becfc4,#26362d,,tagline 64-89 d,#26362d,,tagline
-|14: 1 ⠭⠍⣥⢩⣭⡭⣭⣬⣌⢭⣭⠌⠬⠅⠍⠁
-+14: 7="⠭" 8="⠍" 9="⣥" 10="⢩" 11="⣭" 12="⡭" 13="⣭" 14="⣬" 15="⣌" 16="⢭" 17="⣭" 18="⠌" 19="⠬" 20="⠅" 21="⠍" 22="⠁"
-@14: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #616f64,#0d1410,b,body 7-8 #e5eee5,#0d1410,,body 9-9 #c9a7ed,#0d1410,,body 10-12 #e5eee5,#0d1410,,body 13-15 #c9a7ed,#0d1410,,body 16-21 #b8d88a,#0d1410,,body 22-22 #e5eee5,#0d1410,,body 23-89 d,#0d1410,,body
-|15: 2 ⣂⢀⣉⣉⢉⣈⢁⣀⣀⢀⣀⣀⣀⢀⣀⣀⣀⣀⣀
-+15: 7="⣂" 8="⢀" 9="⣉" 10="⣉" 11="⢉" 12="⣈" 13="⢁" 14="⣀" 15="⣀" 16="⢀" 17="⣀" 18="⣀" 19="⣀" 20="⢀" 21="⣀" 22="⣀" 23="⣀" 24="⣀" 25="⣀"
-@15: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-8 #b9f480,#0d1410,,body 9-11 #c9a7ed,#0d1410,,body 12-25 #b9f480,#0d1410,,body 26-89 d,#0d1410,,body
-|16: 3 ⠉⠉⠁⠉⠉⠉⠈⠈⠉⠉⠉⠉⠉⠉
-+16: 7="⠉" 8="⠉" 9="⠁" 10="⠉" 11="⠉" 12="⠉" 13="⠈" 14="⠈" 15="⠉" 16="⠉" 17="⠉" 18="⠉" 19="⠉" 20="⠉"
-@16: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-17 #e5eee5,#0d1410,,body 18-19 #f8c082,#0d1410,,body 20-20 #e5eee5,#0d1410,,body 21-89 d,#0d1410,,body
-|17: 4
+|13:
+@13: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
+|14:
+@14: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
+|15:
+@15: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
+|16:
+@16: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
+|17:
@17: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
|18:
@18: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
@@ -93,47 +85,38 @@
@22: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
== style colors-off grid=90x23
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-22 #e5eee5,#0d1410, 23-89 d,#0d1410,
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-22 #e5eee5,#0d1410, 23-81 d,#0d1410, 82-82 d,#1d2824, 83-88 #b2c3b8,#1d2824, 89-89 d,#1d2824,
-|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-25 #e5eee5,#0d1410, 26-89 d,#0d1410,
-|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-89 d,#0d1410,
-== 9p colors-off grid=90x23 cursor=7,14,block
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-22 #e5eee5,#0d1410, 23-81 d,#0d1410, 82-82 d,#1d2824, 83-88 #b2c3b8,#1d2824, 89-89 d,#1d2824,
+|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-25 #e5eee5,#0d1410, 26-89 d,#0d1410,
+|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-89 d,#0d1410,
+== 9p colors-off grid=90x23 cursor=7,6,block
|3: 1 @`Mini demo.zig`
@3: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-22 #e5eee5,#0d1410,,body 23-89 d,#0d1410,,body
-|14: 1 ⠭⠍⣥⢩⣭⡭⣭⣬⣌⢭⣭⠌⠬⠅⠍⠁ Colors
-+14: 7="⠭" 8="⠍" 9="⣥" 10="⢩" 11="⣭" 12="⡭" 13="⣭" 14="⣬" 15="⣌" 16="⢭" 17="⣭" 18="⠌" 19="⠬" 20="⠅" 21="⠍" 22="⠁"
-@14: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #616f64,#0d1410,b,body 7-22 #e5eee5,#0d1410,,body 23-81 d,#0d1410,,body 82-82 d,#1d2824,,tagline 83-88 #b2c3b8,#1d2824,,tagline 89-89 d,#1d2824,,tagline
-|15: 2 ⣂⢀⣉⣉⢉⣈⢁⣀⣀⢀⣀⣀⣀⢀⣀⣀⣀⣀⣀
-+15: 7="⣂" 8="⢀" 9="⣉" 10="⣉" 11="⢉" 12="⣈" 13="⢁" 14="⣀" 15="⣀" 16="⢀" 17="⣀" 18="⣀" 19="⣀" 20="⢀" 21="⣀" 22="⣀" 23="⣀" 24="⣀" 25="⣀"
-@15: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-25 #e5eee5,#0d1410,,body 26-89 d,#0d1410,,body
-|16: 3 ⠉⠉⠁⠉⠉⠉⠈⠈⠉⠉⠉⠉⠉⠉
-+16: 7="⠉" 8="⠉" 9="⠁" 10="⠉" 11="⠉" 12="⠉" 13="⠈" 14="⠈" 15="⠉" 16="⠉" 17="⠉" 18="⠉" 19="⠉" 20="⠉"
-@16: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-20 #e5eee5,#0d1410,,body 21-89 d,#0d1410,,body
+|6: 1 ⠭⠍⣥⢩⣭⡭⣭⣬⣌⢭⣭⠌⠬⠅⠍⠁ Colors
++6: 7="⠭" 8="⠍" 9="⣥" 10="⢩" 11="⣭" 12="⡭" 13="⣭" 14="⣬" 15="⣌" 16="⢭" 17="⣭" 18="⠌" 19="⠬" 20="⠅" 21="⠍" 22="⠁"
+@6: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #616f64,#0d1410,b,body 7-22 #e5eee5,#0d1410,,body 23-81 d,#0d1410,,body 82-82 d,#1d2824,,tagline 83-88 #b2c3b8,#1d2824,,tagline 89-89 d,#1d2824,,tagline
+|7: 2 ⣂⢀⣉⣉⢉⣈⢁⣀⣀⢀⣀⣀⣀⢀⣀⣀⣀⣀⣀
++7: 7="⣂" 8="⢀" 9="⣉" 10="⣉" 11="⢉" 12="⣈" 13="⢁" 14="⣀" 15="⣀" 16="⢀" 17="⣀" 18="⣀" 19="⣀" 20="⢀" 21="⣀" 22="⣀" 23="⣀" 24="⣀" 25="⣀"
+@7: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-25 #e5eee5,#0d1410,,body 26-89 d,#0d1410,,body
+|8: 3 ⠉⠉⠁⠉⠉⠉⠈⠈⠉⠉⠉⠉⠉⠉
++8: 7="⠉" 8="⠉" 9="⠁" 10="⠉" 11="⠉" 12="⠉" 13="⠈" 14="⠈" 15="⠉" 16="⠉" 17="⠉" 18="⠉" 19="⠉" 20="⠉"
+@8: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-20 #e5eee5,#0d1410,,body 21-89 d,#0d1410,,body
== style colors-on grid=90x23
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-8 #e5eee5,#0d1410, 9-9 #c9a7ed,#0d1410, 10-12 #e5eee5,#0d1410, 13-15 #c9a7ed,#0d1410, 16-21 #b8d88a,#0d1410, 22-22 #e5eee5,#0d1410, 23-81 d,#0d1410, 82-82 d,#1d2824, 83-88 #b2c3b8,#1d2824, 89-89 d,#1d2824,
-|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #b9f480,#0d1410, 9-11 #c9a7ed,#0d1410, 12-25 #b9f480,#0d1410, 26-89 d,#0d1410,
-|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-19 #f8c082,#0d1410, 20-20 #e5eee5,#0d1410, 21-89 d,#0d1410,
-== snap resized grid=105x27 cursor=7,16
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-8 #e5eee5,#0d1410, 9-9 #c9a7ed,#0d1410, 10-12 #e5eee5,#0d1410, 13-15 #c9a7ed,#0d1410, 16-21 #b8d88a,#0d1410, 22-22 #e5eee5,#0d1410, 23-81 d,#0d1410, 82-82 d,#1d2824, 83-88 #b2c3b8,#1d2824, 89-89 d,#1d2824,
+|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #b9f480,#0d1410, 9-11 #c9a7ed,#0d1410, 12-25 #b9f480,#0d1410, 26-89 d,#0d1410,
+|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-19 #f8c082,#0d1410, 20-20 #e5eee5,#0d1410, 21-89 d,#0d1410,
+== snap resized grid=105x27 cursor=7,7
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/mini/cwd/commands.txt Save Tty Collapse Del
| 1 @`Mini demo.zig`
|
|
-|
-|
-|
-|
-|
-|
-|
-|
-|
| /tmp/pardes-snap/mini/cwd/Mini demo.zig Save Tty Collapse Del
| 1 ⠭⠍⣥⢩⣭⡭⣭⣬⣌⢭⣭⠌⠬⠅⠍⠁ Colors
| 2 ⣂⢀⣉⣉⢉⣈⢁⣀⣀⢀⣀⣀⣀⢀⣀⣀⣀⣀⣀
| 3 ⠉⠉⠁⠉⠉⠉⠈⠈⠉⠉⠉⠉⠉⠉
| 4
-== 9p resized grid=105x27 cursor=7,16,block
+== 9p resized grid=105x27 cursor=7,7,block
|0: Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
@0: 0-70 #b2c3b8,#1d2824,,tagline 71-104 d,#1d2824,,tagline
|1: New Tty Find Grep Joincol Delcol
@@ -146,15 +129,18 @@
@4: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
|5:
@5: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
-|6:
-@6: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
-|7:
-@7: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
-|8:
-@8: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
-|9:
-@9: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
-|10:
+|6: /tmp/pardes-snap/mini/cwd/Mini demo.zig Save Tty Collapse Del
+@6: 0-1 #000000,#add38a,,tagline 2-2 d,#26362d,,tagline 3-28 #becfc4,#26362d,,tagline 29-41 #dfbce5,#26362d,,tagline 42-63 #becfc4,#26362d,,tagline 64-104 d,#26362d,,tagline
+|7: 1 ⠭⠍⣥⢩⣭⡭⣭⣬⣌⢭⣭⠌⠬⠅⠍⠁ Colors
++7: 7="⠭" 8="⠍" 9="⣥" 10="⢩" 11="⣭" 12="⡭" 13="⣭" 14="⣬" 15="⣌" 16="⢭" 17="⣭" 18="⠌" 19="⠬" 20="⠅" 21="⠍" 22="⠁"
+@7: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #616f64,#0d1410,b,body 7-8 #e5eee5,#0d1410,,body 9-9 #c9a7ed,#0d1410,,body 10-12 #e5eee5,#0d1410,,body 13-15 #c9a7ed,#0d1410,,body 16-21 #b8d88a,#0d1410,,body 22-22 #e5eee5,#0d1410,,body 23-96 d,#0d1410,,body 97-97 d,#1d2824,,tagline 98-103 #b2c3b8,#1d2824,,tagline 104-104 d,#1d2824,,tagline
+|8: 2 ⣂⢀⣉⣉⢉⣈⢁⣀⣀⢀⣀⣀⣀⢀⣀⣀⣀⣀⣀
++8: 7="⣂" 8="⢀" 9="⣉" 10="⣉" 11="⢉" 12="⣈" 13="⢁" 14="⣀" 15="⣀" 16="⢀" 17="⣀" 18="⣀" 19="⣀" 20="⢀" 21="⣀" 22="⣀" 23="⣀" 24="⣀" 25="⣀"
+@8: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-8 #b9f480,#0d1410,,body 9-11 #c9a7ed,#0d1410,,body 12-25 #b9f480,#0d1410,,body 26-104 d,#0d1410,,body
+|9: 3 ⠉⠉⠁⠉⠉⠉⠈⠈⠉⠉⠉⠉⠉⠉
++9: 7="⠉" 8="⠉" 9="⠁" 10="⠉" 11="⠉" 12="⠉" 13="⠈" 14="⠈" 15="⠉" 16="⠉" 17="⠉" 18="⠉" 19="⠉" 20="⠉"
+@9: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-17 #e5eee5,#0d1410,,body 18-19 #f8c082,#0d1410,,body 20-20 #e5eee5,#0d1410,,body 21-104 d,#0d1410,,body
+|10: 4
@10: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
|11:
@11: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
@@ -164,18 +150,15 @@
@13: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
|14:
@14: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
-|15: /tmp/pardes-snap/mini/cwd/Mini demo.zig Save Tty Collapse Del
-@15: 0-1 #000000,#add38a,,tagline 2-2 d,#26362d,,tagline 3-28 #becfc4,#26362d,,tagline 29-41 #dfbce5,#26362d,,tagline 42-63 #becfc4,#26362d,,tagline 64-104 d,#26362d,,tagline
-|16: 1 ⠭⠍⣥⢩⣭⡭⣭⣬⣌⢭⣭⠌⠬⠅⠍⠁ Colors
-+16: 7="⠭" 8="⠍" 9="⣥" 10="⢩" 11="⣭" 12="⡭" 13="⣭" 14="⣬" 15="⣌" 16="⢭" 17="⣭" 18="⠌" 19="⠬" 20="⠅" 21="⠍" 22="⠁"
-@16: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #616f64,#0d1410,b,body 7-8 #e5eee5,#0d1410,,body 9-9 #c9a7ed,#0d1410,,body 10-12 #e5eee5,#0d1410,,body 13-15 #c9a7ed,#0d1410,,body 16-21 #b8d88a,#0d1410,,body 22-22 #e5eee5,#0d1410,,body 23-96 d,#0d1410,,body 97-97 d,#1d2824,,tagline 98-103 #b2c3b8,#1d2824,,tagline 104-104 d,#1d2824,,tagline
-|17: 2 ⣂⢀⣉⣉⢉⣈⢁⣀⣀⢀⣀⣀⣀⢀⣀⣀⣀⣀⣀
-+17: 7="⣂" 8="⢀" 9="⣉" 10="⣉" 11="⢉" 12="⣈" 13="⢁" 14="⣀" 15="⣀" 16="⢀" 17="⣀" 18="⣀" 19="⣀" 20="⢀" 21="⣀" 22="⣀" 23="⣀" 24="⣀" 25="⣀"
-@17: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-8 #b9f480,#0d1410,,body 9-11 #c9a7ed,#0d1410,,body 12-25 #b9f480,#0d1410,,body 26-104 d,#0d1410,,body
-|18: 3 ⠉⠉⠁⠉⠉⠉⠈⠈⠉⠉⠉⠉⠉⠉
-+18: 7="⠉" 8="⠉" 9="⠁" 10="⠉" 11="⠉" 12="⠉" 13="⠈" 14="⠈" 15="⠉" 16="⠉" 17="⠉" 18="⠉" 19="⠉" 20="⠉"
-@18: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-17 #e5eee5,#0d1410,,body 18-19 #f8c082,#0d1410,,body 20-20 #e5eee5,#0d1410,,body 21-104 d,#0d1410,,body
-|19: 4
+|15:
+@15: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
+|16:
+@16: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
+|17:
+@17: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
+|18:
+@18: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
+|19:
@19: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
|20:
@20: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-104 d,#0d1410,,body
diff --git a/test/snapshots/modal.golden b/test/snapshots/modal.golden
index 0147d5c8..2a329bf8 100644
--- a/test/snapshots/modal.golden
+++ b/test/snapshots/modal.golden
@@ -1,7 +1,8 @@
-== snap top grid=100x31 cursor=2,3
+== snap top grid=100x31 cursor=2,4
|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/modal/cwd Tty+bash Save Mode F /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F
+| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode /tmp/pardes-snap/modal/cwd Tty+bash Save Mode
+| Filter Collapse Del Filter Collapse Del
| ls ls
| words.txt words.txt
| printf 'one two three\nfour five six\nseven ei
@@ -15,35 +16,35 @@
|
|
|
-|
-| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F
+| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode
+| Filter Collapse Del
| ls
| words.txt
-== snap moved grid=100x31 cursor=8,5
-== snap xsel grid=100x31 cursor=48,5
+== snap moved grid=100x31 cursor=8,6
+== snap xsel grid=100x31 cursor=48,6
== style xsel grid=100x31
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-99 #b2c3b8,#1d2824,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-98 #b2c3b8,#1d2824, 99-99 d,#1d2824,
+|3: 0-2 d,#26362d, 3-21 #becfc4,#26362d, 22-49 d,#26362d, 50-52 d,#1d2824, 53-71 #b2c3b8,#1d2824, 72-99 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-48 #f5f7ee,#3c614a, 49-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-48 #f5f7ee,#3c614a, 49-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|10: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|11: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|18: 0-2 d,#1d2824, 3-21 #b2c3b8,#1d2824, 22-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
@@ -53,17 +54,17 @@
|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-== snap xsel2 grid=100x31 cursor=13,6
-== snap yanked grid=100x31 cursor=13,6
-== snap vsel grid=100x31 cursor=14,7
+== snap xsel2 grid=100x31 cursor=13,7
+== snap yanked grid=100x31 cursor=13,7
+== snap vsel grid=100x31 cursor=14,8
== style vsel grid=100x31
-|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #f5f7ee,#3c614a, 50-50 d,#608269, 51-99 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #f5f7ee,#3c614a, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-14 #f5f7ee,#3c614a, 15-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-== snap typed grid=100x31 cursor=9,20
-|20: TYPED
-== snap undone grid=100x31 cursor=4,20
-|20:
-== snap redone grid=100x31 cursor=9,20
-|20: TYPED
-== snap bottom grid=100x31 cursor=2,20
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #f5f7ee,#3c614a, 50-50 d,#608269, 51-99 d,#0d1410,
+|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #f5f7ee,#3c614a, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-14 #f5f7ee,#3c614a, 15-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+== snap typed grid=100x31 cursor=9,21
+|21: TYPED
+== snap undone grid=100x31 cursor=4,21
+|21:
+== snap redone grid=100x31 cursor=9,21
+|21: TYPED
+== snap bottom grid=100x31 cursor=2,21
diff --git a/test/snapshots/msg.golden b/test/snapshots/msg.golden
index 293e137c..ff7b39a0 100644
--- a/test/snapshots/msg.golden
+++ b/test/snapshots/msg.golden
@@ -45,7 +45,7 @@
|3: 1 AGAIN
|4: 2
== snap msgbuiltin grid=70x17 cursor=29,5
-|2: * /tmp/pardes-snap/msg/cwd/a.txt * Save Tty Collapse Del
+|2: * /tmp/pardes-snap/msg/cwd/a.txt Save Tty Collapse Del
|3: 1 AGAIN hand-written report
|5: 3 Msg hand-written report
== snap msgtail grid=70x17 cursor=32,7
diff --git a/test/snapshots/multicursor.golden b/test/snapshots/multicursor.golden
index 010199b1..a32a7fc3 100644
--- a/test/snapshots/multicursor.golden
+++ b/test/snapshots/multicursor.golden
@@ -52,13 +52,13 @@
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
== snap edited-at-three grid=150x31 cursor=9,9
-|6: * /tmp/pardes-snap/multicursor/cwd/f.txt * Save Tty Collapse Del
+|6: * /tmp/pardes-snap/multicursor/cwd/f.txt Save Tty Collapse Del
|7: 1 >>line 1
|8: 2 >>line 2
|9: 3 >>line 3
== snap three-selections grid=150x31 cursor=8,9
== style three-selections grid=150x31
-|6: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-35 #becfc4,#26362d, 36-40 #dfbce5,#26362d, 41-64 #becfc4,#26362d, 65-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
+|6: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-35 #becfc4,#26362d, 36-40 #dfbce5,#26362d, 41-62 #becfc4,#26362d, 63-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #f5f7ee,#243a2d, 8-8 #3c614a,#f5f7ee, 9-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #f5f7ee,#243a2d, 8-8 #3c614a,#f5f7ee, 9-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-8 #f5f7ee,#3c614a, 9-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410,
diff --git a/test/snapshots/nested.golden b/test/snapshots/nested.golden
index 3485b4be..e5244e71 100644
--- a/test/snapshots/nested.golden
+++ b/test/snapshots/nested.golden
@@ -1,13 +1,13 @@
-== snap file-arg grid=100x31 cursor=7,27
+== snap file-arg grid=100x31 cursor=7,6
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
|$ /tmp/pardes-snap/nested/cwd Tty+bash Save Mode Filter Collapse Del
| $ $(readlink /proc/$PPID/exe) nested.txt
| $
-|
-|
-|
-|
+| /tmp/pardes-snap/nested/cwd/nested.txt Save Tty Collapse Del
+| 1 hello-from-nested
+| 2 second-nested-line
+| 3
|
|
|
@@ -22,13 +22,6 @@
|
| /+New Save Tty Collapse Del
| 1
-|
-|
-|
-| /tmp/pardes-snap/nested/cwd/nested.txt Save Tty Collapse Del
-| 1 hello-from-nested
-| 2 second-nested-line
-| 3
== snap no-arg grid=100x31 cursor=4,7
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
diff --git a/test/snapshots/ninep.golden b/test/snapshots/ninep.golden
index 5391a552..82bdb39c 100644
--- a/test/snapshots/ninep.golden
+++ b/test/snapshots/ninep.golden
@@ -73,7 +73,7 @@
|7: 5 return 7;
@7: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-19 #e5eee5,#0d1410,,body 20-89 d,#0d1410,,body
== snap edited-with-colors-off grid=90x15 cursor=7,4
-|2: * /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
+|2: * /tmp/pardes-snap/ninep/cwd/demo.zig Save Tty Collapse Del TreeContext
|3: 1 // café λ 界
|4: 2 const greeting = "café λ 界 é";
|5: 3
@@ -83,8 +83,8 @@
|9: 7 }
|10: 8
== 9p edited-with-colors-off grid=90x15 cursor=7,4,block
-|2: * /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
-@2: 0-0 #000000,#add38a,,tagline 1-1 #000000,#f9cb70,b,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-73 #becfc4,#26362d,,tagline 74-89 d,#26362d,,tagline
+|2: * /tmp/pardes-snap/ninep/cwd/demo.zig Save Tty Collapse Del TreeContext
+@2: 0-0 #000000,#add38a,,tagline 1-1 #000000,#f9cb70,b,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-71 #becfc4,#26362d,,tagline 72-89 d,#26362d,,tagline
|3: 1 // café λ 界
+3: 13="é" 15="λ" 17="界" 18=""
@3: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-18 #e5eee5,#0d1410,,body 19-89 d,#0d1410,,body
@@ -104,7 +104,7 @@
|10: 8
@10: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body
== style colors-on grid=90x15
-|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-73 #becfc4,#26362d, 74-89 d,#26362d,
+|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-71 #becfc4,#26362d, 72-89 d,#26362d,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #b9f480,#0d1410, 18-18 d,d, 19-81 d,#0d1410, 82-82 d,#1d2824, 83-88 #b2c3b8,#1d2824, 89-89 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-23 #e5eee5,#0d1410, 24-32 #b8d88a,#0d1410, 33-33 d,d, 34-36 #b8d88a,#0d1410, 37-37 #e5eee5,#0d1410, 38-89 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410,
@@ -128,7 +128,7 @@
== style resized grid=105x20
|0: 0-70 #b2c3b8,#1d2824, 71-104 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-104 d,#26362d,
-|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-73 #becfc4,#26362d, 74-104 d,#26362d,
+|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-71 #becfc4,#26362d, 72-104 d,#26362d,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #b9f480,#0d1410, 18-18 d,d, 19-96 d,#0d1410, 97-97 d,#1d2824, 98-103 #b2c3b8,#1d2824, 104-104 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-23 #e5eee5,#0d1410, 24-32 #b8d88a,#0d1410, 33-33 d,d, 34-36 #b8d88a,#0d1410, 37-37 #e5eee5,#0d1410, 38-104 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-104 d,#0d1410,
@@ -151,8 +151,8 @@
@0: 0-70 #b2c3b8,#1d2824,,tagline 71-104 d,#1d2824,,tagline
|1: New Tty Find Grep Joincol Delcol
@1: 0-1 #ffffff,#887455,,tagline 2-2 d,#26362d,,tagline 3-34 #becfc4,#26362d,,tagline 35-104 d,#26362d,,tagline
-|2: * /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
-@2: 0-0 #000000,#add38a,,tagline 1-1 #000000,#f9cb70,b,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-73 #becfc4,#26362d,,tagline 74-104 d,#26362d,,tagline
+|2: * /tmp/pardes-snap/ninep/cwd/demo.zig Save Tty Collapse Del TreeContext
+@2: 0-0 #000000,#add38a,,tagline 1-1 #000000,#f9cb70,b,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-71 #becfc4,#26362d,,tagline 72-104 d,#26362d,,tagline
|3: 1 // café λ 界 Colors
+3: 13="é" 15="λ" 17="界" 18=""
@3: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-18 #b9f480,#0d1410,,body 19-96 d,#0d1410,,body 97-97 d,#1d2824,,tagline 98-103 #b2c3b8,#1d2824,,tagline 104-104 d,#1d2824,,tagline
@@ -192,7 +192,7 @@
== snap resized grid=105x20 cursor=7,4
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| * /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext
+| * /tmp/pardes-snap/ninep/cwd/demo.zig Save Tty Collapse Del TreeContext
| 1 // café λ 界 Colors
| 2 const greeting = "café λ 界 é";
| 3
diff --git a/test/snapshots/pipe.golden b/test/snapshots/pipe.golden
index 208986a8..b7a8da86 100644
--- a/test/snapshots/pipe.golden
+++ b/test/snapshots/pipe.golden
@@ -5,7 +5,8 @@
| 1 alpha beta |tr a-z A-Z
| 2 gamma delta
== snap filtered grid=180x13 cursor=17,3
-|2: * /tmp/pardes-snap/pipe/cwd/pipe.txt * Save Tty Collapse Del
+|2: * /tmp/pardes-snap/pipe/cwd/pipe.txt Save Tty Collapse Del
|3: 1 ALPHA BETA
== snap undone grid=180x13 cursor=17,3
+|2: /tmp/pardes-snap/pipe/cwd/pipe.txt Save Tty Collapse Del
|3: 1 alpha beta
diff --git a/test/snapshots/psearch.golden b/test/snapshots/psearch.golden
index 459b667b..228096ba 100644
--- a/test/snapshots/psearch.golden
+++ b/test/snapshots/psearch.golden
@@ -26,13 +26,14 @@
|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'
-|14: /tmp/pardes-snap/psearch/cwd/+Search Save Tty Collapse Del LocationsConfig
-|15: 1 @p0:2:3-6
-|16: 2 x MARK a
-|17: 3 @p0:4:3-6
-|18: 4 x MARK b
-|19: 5 @p0:6:3-6
-|20: 6 x MARK c
+|10: /tmp/pardes-snap/psearch/cwd/+Search Save Tty Collapse Del LocationsConfig
+|11: 1 @p0:2:3-6
+|12: 2 x MARK a
+|13: 3 @p0:4:3-6
+|14: 4 x MARK b
+|15: 5 @p0:6:3-6
+|16: 6 x MARK c
+|17: 7
== style results grid=140x31
|0: 0-70 #b2c3b8,#1d2824, 71-139 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-139 d,#26362d,
@@ -44,17 +45,17 @@
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-139 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-139 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-139 d,#0d1410,
-|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-139 #e5eee5,#0d1410,
-|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-139 #e5eee5,#0d1410,
-|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-139 #e5eee5,#0d1410,
-|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-139 #e5eee5,#0d1410,
-|14: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-38 #d3b0d9,#1d2824, 39-76 #b2c3b8,#1d2824, 77-139 d,#1d2824,
+|10: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-38 #d3b0d9,#1d2824, 39-76 #b2c3b8,#1d2824, 77-139 d,#1d2824,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-15 #59675c,#0d1410, 16-139 d,#0d1410,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-12 #f7e4a5,#5e4923, 13-14 #e5eee5,#0d1410, 15-139 d,#0d1410,
+|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-15 #59675c,#0d1410, 16-139 d,#0d1410,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-12 #f7e4a5,#5e4923, 13-14 #e5eee5,#0d1410, 15-139 d,#0d1410,
|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-15 #59675c,#0d1410, 16-139 d,#0d1410,
|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-12 #f7e4a5,#5e4923, 13-14 #e5eee5,#0d1410, 15-139 d,#0d1410,
-|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-15 #59675c,#0d1410, 16-139 d,#0d1410,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-12 #f7e4a5,#5e4923, 13-14 #e5eee5,#0d1410, 15-139 d,#0d1410,
-|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-15 #59675c,#0d1410, 16-139 d,#0d1410,
-|20: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-8 #e5eee5,#0d1410, 9-12 #f7e4a5,#5e4923, 13-14 #e5eee5,#0d1410, 15-139 d,#0d1410,
+|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-139 d,#0d1410,
+|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,
|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,
@@ -67,9 +68,9 @@
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-139 d,#0d1410,
== snap second grid=140x31 cursor=7,6
== snap back grid=140x31 cursor=7,4
-== snap nomatch grid=140x31 cursor=7,17
-|12: /tmp/pardes-snap/psearch/cwd/+Search Save Tty Collapse Del LocationsConfig
-|13: 1
+== snap nomatch grid=140x31 cursor=7,13
+|24: /tmp/pardes-snap/psearch/cwd/+Search Save Tty Collapse Del LocationsConfig
+|25: 1
== snap ttyinert grid=140x31 cursor=4,9
|2: $ /tmp/pardes-snap/psearch/cwd Tty+bash Save Mode Filter Collapse Del
|3: $ printf 'x MA''RK a\nzz\nx MA''RK b\nzz\nx MA''RK c\n'
diff --git a/test/snapshots/respawn.golden b/test/snapshots/respawn.golden
index d6d21a81..e8f94978 100644
--- a/test/snapshots/respawn.golden
+++ b/test/snapshots/respawn.golden
@@ -1,10 +1,12 @@
-== snap respawn grid=140x31 cursor=4,9
+== snap respawn grid=140x31 cursor=4,11
|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/respawn/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del
+| /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse De /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse De
+| l l
| ls ls
|
-|$ /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del
+|$ /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse De
+| l
| $ ls
| $ echo RESP''AWNOK
| RESPAWNOK
diff --git a/test/snapshots/respawn.snap b/test/snapshots/respawn.snap
index eac74267..e0ebea13 100644
--- a/test/snapshots/respawn.snap
+++ b/test/snapshots/respawn.snap
@@ -6,8 +6,8 @@ wait 8000 Newcol Joincol
stable 700 20000
# middle-click Del on the lower-left pane's tag: the pane id is freed while
# its shell process stays alive in the slot
-press middle @Del 18
-release middle @Del 18
+press middle @Collapse+9 18
+release middle @Collapse+9 18
stable 700 10000
# Alt-n reuses the freed id -> spawn effect lands on the occupied slot
key a-n
diff --git a/test/snapshots/rsearch.golden b/test/snapshots/rsearch.golden
index 8312e1bc..efbd81c9 100644
--- a/test/snapshots/rsearch.golden
+++ b/test/snapshots/rsearch.golden
@@ -1,36 +1,48 @@
-== snap opened grid=210x31 cursor=7,3
+== snap opened grid=210x31 cursor=7,7
|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/rsearch/cwd/code.txt Save Tty Collapse Del /tmp/pardes-snap/rsearch/cwd Tty+bash Save Mode Filter Collapse Del
-| 1 run count_x now ls
-| 2 open sub/dep.txt:3 here code.txt sub
+| New Tty Find Grep Joincol Delcol
+| /tmp/pardes-snap/rsearch/cwd Tty+bash Save Mode Filter Collapse Del
+| ls
+| code.txt sub
+|
+| /tmp/pardes-snap/rsearch/cwd/code.txt Save Tty Collapse Del
+| 1 run count_x now
+| 2 open sub/dep.txt:3 here
| 3 count_x again
| 4 pick two words
-|
-| code.txt
-== snap search grid=210x31 cursor=13,5
-|26: /tmp/pardes-snap/rsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
-|27: 1 code.txt:1:5-11
-|28: 2 run count_x now
-|29: 3 code.txt:3:1-7
-|30: 4 count_x again
-== snap select grid=210x31 cursor=14,6
-|23: /tmp/pardes-snap/rsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
-|24: 1 code.txt:4:1-8
-|25: 2 pick two words
-== snap path-open grid=210x31 cursor=7,16
-|13: /tmp/pardes-snap/rsearch/cwd/sub/dep.txt Save Tty Collapse Del
-|14: 1 d1
-|15: 2 d2
-|16: 3 DEPTARGET
-|17: 4 d4
-|18: 5 d5
-|19: 6 d6
-== snap step1 grid=210x31 cursor=13,5
-== snap step2 grid=210x31 cursor=13,5
-== snap step3 grid=210x31 cursor=13,5
-== snap selclick grid=210x31 cursor=14,6
-== snap outside grid=210x31 cursor=20,6
+== snap search grid=210x31 cursor=13,9
+|11: /tmp/pardes-snap/rsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
+|12: 1 code.txt:1:5-11
+|13: 2 run count_x now
+|14: 3 code.txt:3:1-7
+|15: 4 count_x again
+|16: 5
+== snap select grid=210x31 cursor=14,10
+|10: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+|16: /tmp/pardes-snap/rsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
+|17: 1 code.txt:4:1-8
+|18: 2 pick two words
+|19: 3
+== snap path-open grid=210x31 cursor=16,22
+|10: 4 pick two words
+|19: /tmp/pardes-snap/rsearch/cwd/sub/dep.txt Save Tty Collapse Del
+|20: 1 d1
+|21: 2 d2
+|22: 3 DEPTARGET
+|23: 4 d4
+|24: 5 d5
+|25: 6 d6
+== snap step1 grid=210x31 cursor=13,9
+== snap step2 grid=210x31 cursor=13,9
+== snap step3 grid=210x31 cursor=13,9
+== snap selclick grid=210x31 cursor=14,10
+|11:
+|12:
+|13:
+|14:
+|15: /tmp/pardes-snap/rsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
+== snap outside grid=210x31 cursor=14,10
|10: /tmp/pardes-snap/rsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
|11: 1 code.txt:4:10-14
|12: 2 pick two words
+|13: 3
diff --git a/test/snapshots/rsearch.snap b/test/snapshots/rsearch.snap
index 30459488..0ce257bd 100644
--- a/test/snapshots/rsearch.snap
+++ b/test/snapshots/rsearch.snap
@@ -28,10 +28,11 @@ release right 6 9
wait 10000 count_x
stable 700 15000
snap opened
+# code.txt opens under the shell's text, its line 1 on screen row 8.
# right-CLICK the identifier `count_x` (line 1, the 'n' at col 15). It names no
# file, so the click searches for it: the results list both lines containing it.
-press right 15 4
-release right 15 4
+press right 15 8
+release right 15 8
# Locations stack the target under its line now: two rows, two waits.
wait 10000 code.txt:1:5-11
wait 10000 run count_x now
@@ -39,9 +40,9 @@ stable 700 15000
snap search
# right-SELECT `pick two` (line 4, sweep cols 8..15): a multi-word span passes
# through verbatim (the space is kept), unlike a click's single-word expansion.
-press right 8 7
-drag right 15 7
-release right 15 7
+press right 8 11
+drag right 15 11
+release right 15 11
wait 10000 code.txt:4:1-8
wait 10000 pick two words
stable 700 15000
@@ -49,45 +50,52 @@ snap select
# right-CLICK the path `sub/dep.txt:3` (line 2, col 18): isFileChar keeps / . :
# so the click expands the whole token and it RESOLVES — a look, not a search —
# opening dep.txt scrolled to line 3 (DEPTARGET). The "don't break paths" case.
-press right 18 5
-release right 18 5
+press right 18 9
+release right 18 9
wait 10000 DEPTARGET
stable 700 15000
snap path-open
# A click does not merely LIST the hits, it goes to one: this re-search of
# `count_x` (line 1, col 15) also STEPS onto hit 1, so the cursor lands in the
-# match — line 1 col 5, not the column that was clicked.
-press right 15 4
-release right 15 4
+# match, not where the click was.
+press right 15 8
+release right 15 8
stable 700 15000
snap step1
# ...and the same word clicked in its OTHER place (line 3, col 10) opens NO
# second +Search: the list is already armed on this pattern, so the click is
# `n`. Cursor moves to hit 2, line 3.
-press right 10 6
-release right 10 6
+press right 10 10
+release right 10 10
stable 700 15000
snap step2
# past the last hit the ring wraps: back to hit 1, line 1.
-press right 10 6
-release right 10 6
+press right 10 10
+release right 10 10
stable 700 15000
snap step3
+# acme sized code.txt to its four lines, so line 4 is on its last row, and a
+# pane's last row is its resize handle: a press there would drag the seam, not
+# sweep text. Drag that handle down four rows first.
+press left 40 11
+drag left 40 15
+release left 40 15
+stable 700 15000
# A LEFT sweep over `pick two` (line 4, cols 8..15) and then a right CLICK
# INSIDE it: the click takes the whole selection — the search is for the
# two-word span (`4:1-8`), which the file-ish word expansion could never
# produce from the single word under the click.
-press left 8 7
-drag left 15 7
-release left 15 7
+press left 8 11
+drag left 15 11
+release left 15 11
stable 400 5000
-press right 11 7
-release right 11 7
+press right 11 11
+release right 11 11
stable 700 15000
snap selclick
# ...and a right click OUTSIDE the selection is the ordinary word click again:
# `words` (line 4, col 19), not the span left lying in the pane.
-press right 19 7
-release right 19 7
+press right 19 11
+release right 19 11
stable 700 15000
snap outside
diff --git a/test/snapshots/savepane.golden b/test/snapshots/savepane.golden
index f1a47696..8660a437 100644
--- a/test/snapshots/savepane.golden
+++ b/test/snapshots/savepane.golden
@@ -1,18 +1,30 @@
-== snap scratch grid=100x31 cursor=20,6
+== snap scratch grid=100x31 cursor=20,18
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/savepane/cwd Tty+bash Save Mode Filter Collapse Del
| ls
|
-| * /tmp/pardes-snap/savepane/cwd/+New * Save Tty Collapse Del
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| * /tmp/pardes-snap/savepane/cwd/+New Save Tty Collapse Del
| 1 scratch bytes
-== snap prompt grid=100x31 cursor=99,7
-|6: 1 scratch bytes Save
-|7: Save /tmp/pardes-snap/savepane/cwd/
-== snap saved grid=100x31 cursor=20,6
-|5: /tmp/pardes-snap/savepane/cwd/saved.txt Save Tty Collapse Del
-|6: 1 scratch bytes --:--:-- saved /tmp/pardes-snap/savepane/cwd/saved.txt
-|7:
+== snap prompt grid=100x31 cursor=99,19
+|18: 1 scratch bytes Save
+|19: Save /tmp/pardes-snap/savepane/cwd/
+== snap saved grid=100x31 cursor=20,18
+|17: /tmp/pardes-snap/savepane/cwd/saved.txt Save Tty Collapse Del
+|18: 1 scratch bytes --:--:-- saved /tmp/pardes-snap/savepane/cwd/saved.txt
+|19:
== file bytes
scratch bytes
== end file
diff --git a/test/snapshots/scroll.golden b/test/snapshots/scroll.golden
index 5960bdc5..19db2e0a 100644
--- a/test/snapshots/scroll.golden
+++ b/test/snapshots/scroll.golden
@@ -2,9 +2,9 @@
|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/scroll/cwd Tty+bash Save Mode /tmp/pardes-snap/scroll/cwd Tty+bash Save Mode
-| 188 ls
-| 189 s.txt
-| 190
+| Filter Collapse Del Filter Collapse Del
+| 189 ls
+| 190 s.txt
| 191
| 192
| 193
@@ -17,12 +17,12 @@
| 200
|
| /tmp/pardes-snap/scroll/cwd Tty+bash Save Mode
+| Filter Collapse Del
| ls
| s.txt
== snap wheel3 grid=100x31 cursor=4,16
-|3: 185 ls
-|4: 186 s.txt
-|5: 187
+|4: 186 ls
+|5: 187 s.txt
|6: 188
|7: 189
|8: 190
@@ -35,9 +35,8 @@
|15: 197
|16: 198
== snap gutter-up grid=100x31 cursor=4,16
-|3: 178 ls
-|4: 179 s.txt
-|5: 180
+|4: 179 ls
+|5: 180 s.txt
|6: 181
|7: 182
|8: 183
@@ -50,9 +49,8 @@
|15: 190
|16: 191
== snap gutter-down grid=100x31 cursor=4,16
-|3: 180 ls
-|4: 181 s.txt
-|5: 182
+|4: 181 ls
+|5: 182 s.txt
|6: 183
|7: 184
|8: 185
@@ -65,9 +63,8 @@
|15: 192
|16: 193
== snap wheel-down grid=100x31 cursor=4,16
-|3: 181 ls
-|4: 182 s.txt
-|5: 183
+|4: 182 ls
+|5: 183 s.txt
|6: 184
|7: 185
|8: 186
@@ -82,11 +79,11 @@
== style wheel-down grid=100x31
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-30 #becfc4,#26362d, 31-38 #dfbce5,#26362d, 39-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-80 #b2c3b8,#1d2824, 81-88 #d3b0d9,#1d2824, 89-99 #b2c3b8,#1d2824,
-|3: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-30 #becfc4,#26362d, 31-38 #dfbce5,#26362d, 39-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-80 #b2c3b8,#1d2824, 81-88 #d3b0d9,#1d2824, 89-98 #b2c3b8,#1d2824, 99-99 d,#1d2824,
+|3: 0-2 d,#26362d, 3-22 #becfc4,#26362d, 23-49 d,#26362d, 50-52 d,#1d2824, 53-72 #b2c3b8,#1d2824, 73-99 d,#1d2824,
|4: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|5: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
-|6: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|5: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|6: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
|7: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|8: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|9: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
@@ -97,11 +94,11 @@
|14: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|16: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|18: 0-2 d,#1d2824, 3-22 #b2c3b8,#1d2824, 23-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
diff --git a/test/snapshots/scroll.snap b/test/snapshots/scroll.snap
index d8d3e665..4b723756 100644
--- a/test/snapshots/scroll.snap
+++ b/test/snapshots/scroll.snap
@@ -17,12 +17,12 @@ wheel up 10 8
wheel up 10 8
stable 500 8000
snap wheel3
-press left 1 11
-release left 1 11
+press left 1 12
+release left 1 12
stable 500 8000
snap gutter-up
-press right 1 6
-release right 1 6
+press right 1 7
+release right 1 7
stable 500 8000
snap gutter-down
wheel down 10 8
diff --git a/test/snapshots/scrolloff.golden b/test/snapshots/scrolloff.golden
index 3201ec6c..17ca7e7b 100644
--- a/test/snapshots/scrolloff.golden
+++ b/test/snapshots/scrolloff.golden
@@ -1,9 +1,13 @@
-== snap top grid=210x12 cursor=7,3
+== snap top grid=210x16 cursor=7,7
|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/scrolloff/cwd/g.txt Save Tty Collapse Del /tmp/pardes-snap/scrolloff/cwd Tty+bash Save Mode Filter Collapse Del
-| 1 line 1 ls
-| 2 line 2 g.txt
+| New Tty Find Grep Joincol Delcol
+| /tmp/pardes-snap/scrolloff/cwd Tty+bash Save Mode Filter Collapse Del
+| ls
+| g.txt
+|
+| /tmp/pardes-snap/scrolloff/cwd/g.txt Save Tty Collapse Del
+| 1 line 1
+| 2 line 2
| 3 line 3
| 4 line 4
| 5 line 5
@@ -11,54 +15,54 @@
| 7 line 7
| 8 line 8
| 9 line 9
-== snap edge grid=210x12 cursor=7,8
-== snap scrolled1 grid=210x12 cursor=7,8
-|3: 2 line 2 ls
-|4: 3 line 3 g.txt
-|5: 4 line 4
-|6: 5 line 5
+== snap edge grid=210x16 cursor=7,12
+== snap scrolled1 grid=210x16 cursor=7,12
+|7: 2 line 2
+|8: 3 line 3
+|9: 4 line 4
+|10: 5 line 5
+|11: 6 line 6
+|12: 7 line 7
+|13: 8 line 8
+|14: 9 line 9
+|15: 10 line 10
+== snap jdown grid=210x16 cursor=7,12
|7: 6 line 6
|8: 7 line 7
|9: 8 line 8
|10: 9 line 9
|11: 10 line 10
-== snap jdown grid=210x12 cursor=7,8
-|3: 6 line 6 ls
-|4: 7 line 7 g.txt
-|5: 8 line 8
-|6: 9 line 9
-|7: 10 line 10
-|8: 11 line 11
-|9: 12 line 12
-|10: 13 line 13
-|11: 14 line 14
-== snap end grid=210x12 cursor=7,10
-|3: 33 line 33 ls
-|4: 34 line 34 g.txt
-|5: 35 line 35
-|6: 36 line 36
-|7: 37 line 37
-|8: 38 line 38
-|9: 39 line 39
-|10: 40 line 40
-|11: 41
-== snap kup grid=210x12 cursor=7,6
-|3: 29 line 29 ls
-|4: 30 line 30 g.txt
-|5: 31 line 31
-|6: 32 line 32
+|12: 11 line 11
+|13: 12 line 12
+|14: 13 line 13
+|15: 14 line 14
+== snap end grid=210x16 cursor=7,14
|7: 33 line 33
|8: 34 line 34
|9: 35 line 35
|10: 36 line 36
|11: 37 line 37
-== snap top2 grid=210x12 cursor=7,3
-|3: 1 line 1 ls
-|4: 2 line 2 g.txt
-|5: 3 line 3
-|6: 4 line 4
-|7: 5 line 5
-|8: 6 line 6
-|9: 7 line 7
-|10: 8 line 8
-|11: 9 line 9
+|12: 38 line 38
+|13: 39 line 39
+|14: 40 line 40
+|15: 41
+== snap kup grid=210x16 cursor=7,10
+|7: 29 line 29
+|8: 30 line 30
+|9: 31 line 31
+|10: 32 line 32
+|11: 33 line 33
+|12: 34 line 34
+|13: 35 line 35
+|14: 36 line 36
+|15: 37 line 37
+== snap top2 grid=210x16 cursor=7,7
+|7: 1 line 1
+|8: 2 line 2
+|9: 3 line 3
+|10: 4 line 4
+|11: 5 line 5
+|12: 6 line 6
+|13: 7 line 7
+|14: 8 line 8
+|15: 9 line 9
diff --git a/test/snapshots/scrolloff.snap b/test/snapshots/scrolloff.snap
index caed9d47..3dbad2b8 100644
--- a/test/snapshots/scrolloff.snap
+++ b/test/snapshots/scrolloff.snap
@@ -1,16 +1,17 @@
# scrolloff: keyboard motion keeps 3 context rows above/below the cursor,
# clamped at file boundaries (cursor may reach the top/bottom body row there)
lines g.txt 40
-start 12 210 -n 1
+start 16 210 -n 1
wait 8000 g.txt
stable 700 20000
-# right-click the g.txt name in the greeting ls -> file pane, 9 body rows
+# right-click the g.txt name in the greeting ls -> file pane under the shell's
+# text, 9 body rows
press right 4 5
release right 4 5
wait 10000 line 3
stable 700 15000
-press left 10 4
-release left 10 4
+press left 10 9
+release left 10 9
stable 400 5000
# (b) gg: cursor CAN sit on the top body row, no forced scroll
key g g
diff --git a/test/snapshots/selarg.golden b/test/snapshots/selarg.golden
index cc4cdec1..ef240e31 100644
--- a/test/snapshots/selarg.golden
+++ b/test/snapshots/selarg.golden
@@ -1,7 +1,8 @@
-== snap kept grid=100x31 cursor=6,3
+== snap kept grid=100x31 cursor=6,4
|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/selarg/cwd Tty+bash Save Mode /tmp/pardes-snap/selarg/cwd Tty+bash Save Mode
+| Filter Collapse Del Filter Collapse Del
| ZZARG ZZVEE ./c.sh
|
|
@@ -15,13 +16,13 @@
|
|
|
-|
| /tmp/pardes-snap/selarg/cwd Tty+bash Save Mode
+| Filter Collapse Del
| ls
| s.txt
-== snap crosspane grid=100x31 cursor=6,3
-|4: ./c.sh ZZARG
-|5: CHORDARG:ZZARG
-== snap vselarg grid=100x31 cursor=12,3
-|6: ./c.sh ZZVEE
-|7: CHORDARG:ZZVEE
+== snap crosspane grid=100x31 cursor=6,4
+|5: ./c.sh ZZARG
+|6: CHORDARG:ZZARG
+== snap vselarg grid=100x31 cursor=12,4
+|7: ./c.sh ZZVEE
+|8: CHORDARG:ZZVEE
diff --git a/test/snapshots/selarg.snap b/test/snapshots/selarg.snap
index 1df7ee12..8aa59bbd 100644
--- a/test/snapshots/selarg.snap
+++ b/test/snapshots/selarg.snap
@@ -16,8 +16,8 @@ stable 700 15000
key c-b
stable 600 8000
# pane B (top right): create the arg-printing script and show its path
-press left 60 6
-release left 60 6
+press left 60 7
+release left 60 7
stable 400 5000
key c-b
stable 600 8000
@@ -28,31 +28,31 @@ stable 700 15000
key c-b
stable 600 8000
# keep a left selection over ZZARG in pane A (this also focuses A)
-press left 3 4
-drag left 7 4
-release left 7 4
+press left 3 5
+drag left 7 5
+release left 7 5
stable 400 5000
snap kept
# chord in pane B: middle on ./c.sh + left tap (B has NO selection: the
# argument comes from pane A's kept left selection) + middle release runs
-press middle 55 4
-press left 55 4
-release middle 55 4
-release left 55 4
+press middle 55 5
+press left 55 5
+release middle 55 5
+release left 55 5
wait 10000 CHORDARG:ZZARG
stable 700 10000
snap crosspane
# now an explicit modal selection: plain-click ZZVEE in A (drops A's kept
# left selection), select it with v e, chord in B again
-press left 9 4
-release left 9 4
+press left 9 5
+release left 9 5
stable 400 5000
text ve
stable 400 5000
-press middle 55 4
-press left 55 4
-release middle 55 4
-release left 55 4
+press middle 55 5
+press left 55 5
+release middle 55 5
+release left 55 5
wait 10000 CHORDARG:ZZVEE
stable 700 10000
snap vselarg
diff --git a/test/snapshots/selregex.golden b/test/snapshots/selregex.golden
index fa29b661..9715a5ec 100644
--- a/test/snapshots/selregex.golden
+++ b/test/snapshots/selregex.golden
@@ -51,11 +51,12 @@
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f5f7ee,#243a2d, 11-11 #3c614a,#f5f7ee, 12-18 #e5eee5,#0d1410, 19-19 #f8c082,#0d1410, 20-20 #e5eee5,#0d1410, 21-159 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f5f7ee,#243a2d, 11-11 #3c614a,#f5f7ee, 12-20 #e5eee5,#0d1410, 21-21 #f8c082,#0d1410, 22-22 #e5eee5,#0d1410, 23-159 d,#0d1410,
== snap commented grid=160x25 cursor=14,3
-|2: * /tmp/pardes-snap/selregex/cwd/m.zig * Save Tty Collapse Del TreeContext
+|2: * /tmp/pardes-snap/selregex/cwd/m.zig Save Tty Collapse Del TreeContext
|3: 1 // const one = 1;
|4: 2 // const two = 2;
|5: 3 // const three = 3;
== snap uncommented grid=160x25 cursor=11,3
+|2: /tmp/pardes-snap/selregex/cwd/m.zig Save Tty Collapse Del TreeContext
|3: 1 const one = 1;
|4: 2 const two = 2;
|5: 3 const three = 3;
diff --git a/test/snapshots/shellset.golden b/test/snapshots/shellset.golden
index 85fcb474..718842a4 100644
--- a/test/snapshots/shellset.golden
+++ b/test/snapshots/shellset.golden
@@ -5,8 +5,9 @@
| 1 Shell zznosuchshell
== snap builtin-ran grid=90x21 cursor=25,3
|3: 1 Shell zznosuchshell Shell
-== snap fallback-shell grid=90x21 cursor=49,5
+== 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
-|3: 1 Shell zznosuchshell ls
-|4: cmd.txt zmarkfile.txt
+|2: /tmp/pardes-snap/shellset/cwd/cmd.txt Sav /tmp/pardes-snap/shellset/cwd Tty+bash Sa
+|3: e Tty Collapse Del ve Mode Filter Collapse Del
+|4: 1 Shell zznosuchshell ls
+|5: cmd.txt zmarkfile.txt
diff --git a/test/snapshots/shrinktag.golden b/test/snapshots/shrinktag.golden
index 8ed65105..70d36c6a 100644
--- a/test/snapshots/shrinktag.golden
+++ b/test/snapshots/shrinktag.golden
@@ -1,12 +1,12 @@
== snap before grid=100x31 cursor=2,9
|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/shrinktag/cwd Tty+bash Save Mo /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
+| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M
+| ode Filter Collapse Del ode Filter Collapse Del
| ls ls
| w.txt w.txt
|
|
-|
| echo one; echo two
|
|
@@ -16,29 +16,35 @@
|
|
|
-| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
+| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M
+| ode Filter Collapse Del
| ls
| w.txt
== snap shrunk grid=100x31 cursor=2,9
-|3: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo ls
-|4: ls w.txt
-|5: w.txt
+|3: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M ode Filter Collapse Del
+|4: ode Filter Collapse Del ls
+|5: ls w.txt
+|6: w.txt
|8:
|17:
|18:
|19:
+|20:
== snap move-preview grid=100x31 cursor=2,9
|8: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
== snap restored grid=100x31 cursor=2,9
-|3: ls ls
-|4: w.txt w.txt
-|5:
+|3: ode Filter Collapse Del ode Filter Collapse Del
+|4: ls ls
+|5: w.txt w.txt
+|6:
|8: echo one; echo two
-|17: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
-|18: ls
-|19: w.txt
+|17: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M
+|18: ode Filter Collapse Del
+|19: ls
+|20: w.txt
== snap bottom-shrunk grid=100x31 cursor=2,9
|17:
|18:
|19:
-|30: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
+|20:
+|30: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M
diff --git a/test/snapshots/splitparent.golden b/test/snapshots/splitparent.golden
index 55488973..5159c1d6 100644
--- a/test/snapshots/splitparent.golden
+++ b/test/snapshots/splitparent.golden
@@ -1,26 +1,30 @@
-== snap shrunk grid=100x31 cursor=4,4
+== snap shrunk grid=100x31 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 ls
+| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del
+| Mode Filter Collapse Del ls
| ls
-== snap swap-parent grid=100x31 cursor=4,8
+== snap swap-parent grid=100x31 cursor=4,9
+|5:
|6: /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-|7: ls
+|7: Mode Filter Collapse Del
+|8: ls
== snap tallest grid=100x6 cursor=54,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,19
+== snap regrown grid=100x31 cursor=54,20
|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 ls
-| ls
+| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del
+| Mode Filter Collapse Del ls
|
| /tmp/pardes-snap/splitparent/cwd Tty+bash Save
+| Mode Filter Collapse Del
| ls
|
|
@@ -30,6 +34,6 @@
|
|
|
-|
| /tmp/pardes-snap/splitparent/cwd Tty+bash Save
+| Mode Filter Collapse Del
| ls
diff --git a/test/snapshots/splitparent.snap b/test/snapshots/splitparent.snap
index 1fd4e935..3ec42b14 100644
--- a/test/snapshots/splitparent.snap
+++ b/test/snapshots/splitparent.snap
@@ -1,6 +1,8 @@
# a new pane never opens under a too-short parent: Alt-n from a tag-only pane
# splits the tall sibling in its column instead; when NO pane could leave the
# newcomer 2 body rows, the tallest one anywhere is split
+# 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
diff --git a/test/snapshots/stepgrain.golden b/test/snapshots/stepgrain.golden
index 6d3ba91f..c59a82fb 100644
--- a/test/snapshots/stepgrain.golden
+++ b/test/snapshots/stepgrain.golden
@@ -4,25 +4,6 @@
| /tmp/pardes-snap/stepgrain/cwd/a.txt Save Tty Collapse Del
| 1 hello ZQMARK one
| 2 see also sub/b.txt for ZQMARK two
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
| /tmp/pardes-snap/stepgrain/cwd/+Search Save Tty Collapse Del LocationsConfig
| 1 a.txt:1:7-12
| 2 hello ZQMARK one
@@ -30,20 +11,21 @@
| 4 see also sub/b.txt for ZQMARK two
| 5 sub/b.txt:1:1-6
| 6 ZQMARK inside sub
-== snap step-row1 grid=150x31 cursor=7,25
+| 7
+== snap step-row1 grid=150x31 cursor=7,6
== style step-row1 grid=150x31
|0: 0-70 #b2c3b8,#1d2824, 71-149 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-149 d,#26362d,
|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-38 #d3b0d9,#1d2824, 39-60 #b2c3b8,#1d2824, 61-149 d,#1d2824,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-22 #e5eee5,#0d1410, 23-149 d,#0d1410,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-39 #e5eee5,#0d1410, 40-149 d,#0d1410,
-|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-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,
+|5: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-33 #becfc4,#26362d, 34-40 #dfbce5,#26362d, 41-78 #becfc4,#26362d, 79-149 d,#26362d,
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-18 #f5f7ee,#3c614a, 19-149 d,#0d1410,
+|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-18 #f7e4a5,#5e4923, 19-22 #e5eee5,#0d1410, 23-149 d,#0d1410,
+|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-19 #59675c,#0d1410, 20-149 d,#0d1410,
+|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-29 #e5eee5,#0d1410, 30-35 #f7e4a5,#5e4923, 36-39 #e5eee5,#0d1410, 40-149 d,#0d1410,
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-21 #59675c,#0d1410, 22-149 d,#0d1410,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #f7e4a5,#5e4923, 13-23 #e5eee5,#0d1410, 24-149 d,#0d1410,
|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
@@ -56,40 +38,30 @@
|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
-|24: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-33 #becfc4,#26362d, 34-40 #dfbce5,#26362d, 41-78 #becfc4,#26362d, 79-149 d,#26362d,
-|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-18 #f5f7ee,#3c614a, 19-149 d,#0d1410,
-|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-18 #f7e4a5,#5e4923, 19-22 #e5eee5,#0d1410, 23-149 d,#0d1410,
-|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-19 #59675c,#0d1410, 20-149 d,#0d1410,
-|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-29 #e5eee5,#0d1410, 30-35 #f7e4a5,#5e4923, 36-39 #e5eee5,#0d1410, 40-149 d,#0d1410,
-|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-21 #59675c,#0d1410, 22-149 d,#0d1410,
-|30: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #f7e4a5,#5e4923, 13-23 #e5eee5,#0d1410, 24-149 d,#0d1410,
-== snap step-row2 grid=150x31 cursor=7,27
+|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|26: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-149 d,#0d1410,
+== snap step-row2 grid=150x31 cursor=7,8
== style step-row2 grid=150x31
-|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-18 #59675c,#0d1410, 19-149 d,#0d1410,
-|27: 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,28
-|25: 2 hello ZQMARK one
-|26: 3 a.txt:2:24-29
-|27: 4 see also sub/b.txt for ZQMARK two
-|28: 5 sub/b.txt:1:1-6
-|29: 6 ZQMARK inside sub
-|30: 7
-== snap step-wrap grid=150x31 cursor=7,25
-|25: 1 a.txt:1:7-12
-|26: 2 hello ZQMARK one
-|27: 3 a.txt:2:24-29
-|28: 4 see also sub/b.txt for ZQMARK two
-|29: 5 sub/b.txt:1:1-6
-|30: 6 ZQMARK inside sub
-== snap blank-in-path grid=150x31 cursor=7,23
-|22: /tmp/pardes-snap/stepgrain/cwd/+Search Save Tty Collapse Del
-|23: 1 two words.txt
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-18 #59675c,#0d1410, 19-149 d,#0d1410,
+|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
== style blank-in-path grid=150x31
-|22: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-33 #becfc4,#26362d, 34-40 #dfbce5,#26362d, 41-62 #becfc4,#26362d, 63-149 d,#26362d,
-|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-19 #f5f7ee,#3c614a, 20-149 d,#0d1410,
-|24: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-40 #d3b0d9,#1d2824, 41-78 #b2c3b8,#1d2824, 79-149 d,#1d2824,
-|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #f5f7ee,#3c614a, 19-149 d,#0d1410,
-|27: 0-0 d,#608269, 1-1 d,#0d1410, 2-19 #59675c,#0d1410, 20-149 d,#0d1410,
-== snap blank-opened grid=150x31 cursor=7,13
-|12: /tmp/pardes-snap/stepgrain/cwd/two words.txt Save Tty Collapse Del
-|13: 1
+|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,#26362d, 3-33 #becfc4,#26362d, 34-40 #dfbce5,#26362d, 41-62 #becfc4,#26362d, 63-149 d,#26362d,
+|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
diff --git a/test/snapshots/tagalign.golden b/test/snapshots/tagalign.golden
index 6873c19a..f6a8f9d5 100644
--- a/test/snapshots/tagalign.golden
+++ b/test/snapshots/tagalign.golden
@@ -1,63 +1,30 @@
-== snap aligned grid=100x31 cursor=4,20
+== snap aligned grid=100x31 cursor=4,9
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty Collapse Del
| 1 alpha
| 2 beta
| 3 gamma
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del
| ls
| deep
-== snap fits grid=87x31 cursor=4,20
+== snap fits grid=87x31 cursor=4,9
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty Co
+| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty C
+| ollapse Del
| 1 alpha
| 2 beta
-| 3 gamma
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del
| ls
| deep
-== snap overby1 grid=86x31 cursor=4,20
+== snap overby1 grid=86x31 cursor=4,9
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty C
+| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty
+| Collapse Del
| 1 alpha
| 2 beta
-| 3 gamma
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del
| ls
| deep
@@ -68,46 +35,26 @@
| 1 alpha
| 2 beta
| 3 gamma
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del
| ls
| deep
-== snap revealed grid=70x31 cursor=69,2
+== snap revealed grid=70x31 cursor=31,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exi
| New Tty Find Grep Joincol Delcol
-| d/deep/nested-project-directory/document.txt Save Tty Collapse Del
+| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/docume
+| nt.txt Save Tty Collapse Del
| 1 alpha
| 2 beta
-| 3 gamma
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse De
+| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse D
+| el
| ls
| deep
-== snap alone grid=70x31 cursor=4,5
-|2: /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse De
-|3: ls
-|4: deep
-|5:
-|17:
-|18:
-|19:
+== snap alone grid=70x31 cursor=4,6
+|2: /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse D
+|3: el
+|4: ls
+|5: deep
+|6:
+|7:
+|8:
+|9:
diff --git a/test/snapshots/tagalign.snap b/test/snapshots/tagalign.snap
index a77415d9..3bb3922e 100644
--- a/test/snapshots/tagalign.snap
+++ b/test/snapshots/tagalign.snap
@@ -32,7 +32,7 @@ key :
key A
stable 400 5000
snap revealed
-press middle @Del 3
-release middle @Del 3
+press middle @Del 4
+release middle @Del 4
stable 700 15000
snap alone
diff --git a/test/snapshots/tagalign2col.golden b/test/snapshots/tagalign2col.golden
index 25d42e37..f5306f42 100644
--- a/test/snapshots/tagalign2col.golden
+++ b/test/snapshots/tagalign2col.golden
@@ -1,25 +1,26 @@
-== snap onecol grid=160x31 cursor=7,3
+== snap onecol grid=160x31 cursor=7,4
|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/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse De /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del
-| 1 alpha ls Left
-| 2 beta deep
+| /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse D /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del
+| el ls Left
+| 1 alpha deep
+| 2 beta
| 3 gamma
-== snap aligned grid=160x31 cursor=4,20
-|3: 1 alpha ls
-|17: /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del
-|18: ls
-|19: deep
-== snap widened grid=160x31 cursor=4,20
+== snap aligned grid=160x31 cursor=4,10
+|3: el ls
+|7: /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del
+|8: ls
+|9: deep
+== snap widened grid=160x31 cursor=4,10
|1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse Del ╎ /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filt
-|3: 1 alpha ╎ ls
-|4: 2 beta ╎ deep
-|5: 3 gamma ╎
+|2: /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse Del ╎ /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Fil
+|3: 1 alpha ╎ ter Collapse Del
+|4: 2 beta ╎ ls
+|5: 3 gamma ╎ deep
|6: ╎
-|7: ╎
-|8: ╎
-|9: ╎
+|7: /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del ╎
+|8: ls ╎
+|9: deep ╎
|10: ╎
|11: ╎
|12: ╎
@@ -27,9 +28,9 @@
|14: ╎
|15: ╎
|16: ╎
-|17: /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del ╎
-|18: ls ╎
-|19: deep ╎
+|17: ╎
+|18: ╎
+|19: ╎
|20: ╎
|21: ╎
|22: ╎
diff --git a/test/snapshots/tagbottom.golden b/test/snapshots/tagbottom.golden
index 637d3143..a86bb8fb 100644
--- a/test/snapshots/tagbottom.golden
+++ b/test/snapshots/tagbottom.golden
@@ -115,10 +115,11 @@
|27: 26 zzz
|28:
|30: /tmp/pardes-snap/tagbottom/cwd/top.txt Save Tty Collapse Del bottom.txt
-== snap look-bottom-tag grid=100x31 cursor=7,17
-|16: /tmp/pardes-snap/tagbottom/cwd/top.txt Save Tty Collapse Del bottom.txt
-|17: 1 ZZONE
-|18: 2 ZZTWO
+== snap look-bottom-tag grid=100x31 cursor=7,16
+|15: /tmp/pardes-snap/tagbottom/cwd/top.txt Save Tty Collapse Del bottom.txt
+|16: 1 ZZONE
+|17: 2 ZZTWO
+|18:
|19:
|20:
|21:
@@ -139,7 +140,6 @@
|15:
|16:
|17:
-|18:
== snap gutter-click grid=100x31 cursor=9,5
|2: 2 bbb
|3: 3 ccc
@@ -164,7 +164,7 @@
|8: 7 mmm
|9: 8 nnn
|10: 9 ooo
-|11: * /tmp/pardes-snap/tagbottom/cwd/top.txt * Save Tty Collapse Del bottom.txt
+|11: * /tmp/pardes-snap/tagbottom/cwd/top.txt Save Tty Collapse Del bottom.txt
|12: 1 ZZONE
== file cut-buffer
aaa
diff --git a/test/snapshots/tagbottom.snap b/test/snapshots/tagbottom.snap
index 67bb2ead..a70ace1f 100644
--- a/test/snapshots/tagbottom.snap
+++ b/test/snapshots/tagbottom.snap
@@ -70,14 +70,14 @@ snap body-cursor
# The h-resize seam. With top taglines it is the upper pane's LAST row; here
# that row is the upper pane's TAG, so the handle is the lower pane's FIRST row
# (grid 16, click row 17). A no-drag click on it must leave both sizes alone.
-press left 40 18
-release left 40 18
+press left 40 17
+release left 40 17
motion 40 25
stable 500 8000
snap seam-noop
-# ...and the real drag, five rows up: the upper pane keeps 10 rows, the lower
+# ...and the real drag, four rows up: the upper pane keeps 10 rows, the lower
# grows to 19. Both taglines follow their own pane's bottom.
-press left 40 18
+press left 40 17
drag left 40 13
release left 40 13
motion 40 25
diff --git a/test/snapshots/tagcmd.golden b/test/snapshots/tagcmd.golden
index 83106d0d..8f1dc8d2 100644
--- a/test/snapshots/tagcmd.golden
+++ b/test/snapshots/tagcmd.golden
@@ -1,7 +1,7 @@
== snap edited grid=100x31 cursor=13,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| * /tmp/pardes-snap/tagcmd/cwd/c.txt * Save Tty Collapse Del
+| * /tmp/pardes-snap/tagcmd/cwd/c.txt Save Tty Collapse Del
| 1 ZZEDITline 1
| 2 line 2
| 3 line 3
@@ -9,8 +9,8 @@
| 5 line 5
| 6 line 6
| 7
-== snap colon grid=100x31 cursor=39,2
-== snap word grid=100x31 cursor=43,2
+== snap colon grid=100x31 cursor=37,2
+== snap word grid=100x31 cursor=41,2
== snap saved grid=100x31 cursor=13,3
|2: /tmp/pardes-snap/tagcmd/cwd/c.txt Save Tty Collapse Del
|3: 1 ZZEDITline 1 --:--:-- saved /tmp/pardes-snap/tagcmd/cwd/c.txt
diff --git a/test/snapshots/tagnav.golden b/test/snapshots/tagnav.golden
index 929ae828..dd982322 100644
--- a/test/snapshots/tagnav.golden
+++ b/test/snapshots/tagnav.golden
@@ -1,65 +1,52 @@
-== snap start grid=100x31 cursor=7,3
+== snap start grid=100x31 cursor=7,4
|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/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
+| /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Col /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
+| lapse Del Filter Collapse Del
| 1 alpha ls
| 2 beta w.txt
| 3 gamma
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
-|
| /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
+| Filter Collapse Del
| ls Up
| w.txt
-== snap colon grid=100x31 cursor=28,2
-|2: es-snap/tagnav/cwd/w.txt Save Tty Collapse Del /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
-|18: ls
-== snap word grid=100x31 cursor=32,2
-== snap in-tag grid=100x31 cursor=34,2
-== snap right grid=100x31 cursor=54,5
-|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
-|3: 1 alpha Right ls
-== snap right-tag grid=100x31 cursor=70,2
-|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll nav/cwd Tty+bash Save Mode Filter Collapse Del
-|3: 1 alpha ls
+== snap colon grid=100x31 cursor=37,2
+|9: ls
+== snap word grid=100x31 cursor=41,2
+== snap in-tag grid=100x31 cursor=43,2
+== snap right grid=100x31 cursor=54,6
+|4: 1 alpha Right ls
+== snap right-tag grid=100x31 cursor=90,2
+|4: 1 alpha ls
== snap left-tag grid=100x31 cursor=43,2
-|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
-== snap back-to-body grid=100x31 cursor=7,3
-== snap tty-on grid=100x31 cursor=54,5
-|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll$ /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
-|3: 1 alpha $ ls
-|5: 3 gamma $
+== snap back-to-body grid=100x31 cursor=7,4
+== snap tty-on grid=100x31 cursor=54,6
+|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Col $ /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
+|4: 1 alpha $ ls
+|6: 3 gamma $
== snap tty-tag grid=100x31 cursor=95,2
-== snap tty-restored grid=100x31 cursor=54,5
+== snap tty-restored grid=100x31 cursor=54,6
== snap column-tag grid=100x31 cursor=3,1
-|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
-|3: 1 alpha Up ls
-|5: 3 gamma
+|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Col /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
+|4: 1 alpha Up ls
+|6: 3 gamma
== snap column-word grid=100x31 cursor=6,1
-|3: 1 alpha ls
+|4: 1 alpha ls
== snap workspace-tag grid=100x31 cursor=0,0
-|3: 1 alpha Up ls
+|4: 1 alpha Up ls
== snap topbar-dollar grid=100x31 cursor=70,0
-|3: 1 alpha ls
+|4: 1 alpha ls
== snap topbar-dump-word grid=100x31 cursor=46,0
-== snap topbar-exec grid=100x31 cursor=7,3
+== snap topbar-exec grid=100x31 cursor=7,4
|0: Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit Restore /tmp/pardes-snap/tag
-|3: 1 alpha Dump ls
+|4: 1 alpha Dump ls
== snap topbar-restore-word grid=100x31 cursor=72,0
-|3: 1 alpha ls
-== snap topbar-restored grid=100x31 cursor=7,3
+|4: 1 alpha ls
+== snap topbar-restored grid=100x31 cursor=7,4
|0: Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
-|3: 1 alpha $ ls
-|5: 3 gamma $
-|6: ── restored history ──
-|18: $ ls
-|20: $
-|21: ── restored history ──
+|4: 1 alpha $ ls
+|6: 3 gamma $
+|7: /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode ── restored history ──
+|9: $ ls
+|11: $
+|12: ── restored history ──
diff --git a/test/snapshots/tagpath.golden b/test/snapshots/tagpath.golden
index 2363bcd6..30699276 100644
--- a/test/snapshots/tagpath.golden
+++ b/test/snapshots/tagpath.golden
@@ -43,15 +43,15 @@
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
== snap yanked-path grid=100x31 cursor=41,3
-|2: * /tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
+|2: * /tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del
|3: 1 l/tmp/pardes-snap/tagpath/cwd/c.txtine 1
== snap drag-cross grid=100x31 cursor=41,2
== style drag-cross grid=100x31
-|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-10 #becfc4,#26362d, 11-41 #f5f7ee,#3c614a, 42-60 #becfc4,#26362d, 61-99 d,#26362d,
+|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-10 #becfc4,#26362d, 11-41 #f5f7ee,#3c614a, 42-58 #becfc4,#26362d, 59-99 d,#26362d,
|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-41 #f5f7ee,#3c614a, 42-46 #e5eee5,#0d1410, 47-99 d,#0d1410,
== snap name-draft grid=100x31 cursor=11,2
-|2: * /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
+|2: * /renamed/tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del
== snap name-cancelled grid=100x31 cursor=41,3
-|2: * /tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
+|2: * /tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del
== snap name-committed grid=100x31 cursor=41,3
-|2: * /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
+|2: * /renamed/tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del
diff --git a/test/snapshots/theme.golden b/test/snapshots/theme.golden
index 5e7a3dc4..a81c4232 100644
--- a/test/snapshots/theme.golden
+++ b/test/snapshots/theme.golden
@@ -1,7 +1,8 @@
-== snap orchard grid=100x31 cursor=4,5
+== snap orchard grid=100x31 cursor=4,6
|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/theme/cwd Tty+bash Save Mode F /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F
+| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode /tmp/pardes-snap/theme/cwd Tty+bash Save Mode
+| Filter Collapse Del Filter Collapse Del
| ls ls
| t.txt t.txt
|
@@ -15,18 +16,18 @@
|
|
|
-|
-| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F
+| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode
+| Filter Collapse Del
| ls
| t.txt
== style orchard grid=100x31
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-99 #b2c3b8,#1d2824,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-98 #b2c3b8,#1d2824, 99-99 d,#1d2824,
+|3: 0-2 d,#26362d, 3-21 #becfc4,#26362d, 22-49 d,#26362d, 50-52 d,#1d2824, 53-71 #b2c3b8,#1d2824, 72-99 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|5: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|6: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
@@ -37,11 +38,11 @@
|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|18: 0-2 d,#1d2824, 3-21 #b2c3b8,#1d2824, 22-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
@@ -51,15 +52,15 @@
|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
-== snap dusk grid=100x31 cursor=4,5
+== snap dusk grid=100x31 cursor=4,6
== style dusk grid=100x31
|0: 0-50 #c6b6c5,#2c2630, 51-59 #fff0eb,#654053, 60-70 #c6b6c5,#2c2630, 71-99 d,#2c2630,
|1: 0-1 #ffffff,#757a5f, 2-2 d,#3b2f3b, 3-34 #d2c2d1,#3b2f3b, 35-49 d,#3b2f3b, 50-51 #ffffff,#757a5f, 52-52 d,#2c2630, 53-84 #c6b6c5,#2c2630, 85-99 d,#2c2630,
-|2: 0-1 #000000,#edb092, 2-2 d,#3b2f3b, 3-29 #d2c2d1,#3b2f3b, 30-37 #a1d5c4,#3b2f3b, 38-49 #d2c2d1,#3b2f3b, 50-51 #ffffff,#745469, 52-52 d,#2c2630, 53-79 #c6b6c5,#2c2630, 80-87 #96c9b8,#2c2630, 88-99 #c6b6c5,#2c2630,
-|3: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
+|2: 0-1 #000000,#edb092, 2-2 d,#3b2f3b, 3-29 #d2c2d1,#3b2f3b, 30-37 #a1d5c4,#3b2f3b, 38-48 #d2c2d1,#3b2f3b, 49-49 d,#3b2f3b, 50-51 #ffffff,#745469, 52-52 d,#2c2630, 53-79 #c6b6c5,#2c2630, 80-87 #96c9b8,#2c2630, 88-98 #c6b6c5,#2c2630, 99-99 d,#2c2630,
+|3: 0-2 d,#3b2f3b, 3-21 #d2c2d1,#3b2f3b, 22-49 d,#3b2f3b, 50-52 d,#2c2630, 53-71 #c6b6c5,#2c2630, 72-99 d,#2c2630,
|4: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
-|5: 0-0 d,#8e667c, 1-49 d,#39323b, 50-50 d,#8e667c, 51-99 d,#39323b,
-|6: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
+|5: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
+|6: 0-0 d,#8e667c, 1-49 d,#39323b, 50-50 d,#8e667c, 51-99 d,#39323b,
|7: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
|8: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
|9: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
@@ -70,11 +71,11 @@
|14: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
|15: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
|16: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
-|17: 0-1 #ffffff,#745469, 2-2 d,#2c2630, 3-29 #c6b6c5,#2c2630, 30-37 #96c9b8,#2c2630, 38-49 #c6b6c5,#2c2630, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
-|18: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
+|17: 0-1 #ffffff,#745469, 2-2 d,#2c2630, 3-29 #c6b6c5,#2c2630, 30-37 #96c9b8,#2c2630, 38-48 #c6b6c5,#2c2630, 49-49 d,#2c2630, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
+|18: 0-2 d,#2c2630, 3-21 #c6b6c5,#2c2630, 22-49 d,#2c2630, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
|19: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
-|20: 0-0 d,#8e667c, 1-49 d,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
-|21: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
+|20: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
+|21: 0-0 d,#8e667c, 1-49 d,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
|22: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
|23: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
|24: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
@@ -84,15 +85,15 @@
|28: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
|29: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
|30: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b,
-== snap ink grid=100x31 cursor=4,5
+== snap ink grid=100x31 cursor=4,6
== style ink grid=100x31
|0: 0-50 #d5e4f1,#15212c, 51-59 #ffffff,#1b547c, 60-70 #d5e4f1,#15212c, 71-99 d,#15212c,
|1: 0-1 #ffffff,#8f7d5b, 2-2 d,#223342, 3-34 #e1eef9,#223342, 35-49 d,#223342, 50-51 #ffffff,#8f7d5b, 52-52 d,#15212c, 53-84 #d5e4f1,#15212c, 85-99 d,#15212c,
-|2: 0-1 #000000,#86d8ff, 2-2 d,#223342, 3-29 #e1eef9,#223342, 30-37 #ffeab8,#223342, 38-49 #e1eef9,#223342, 50-51 #ffffff,#5f7990, 52-52 d,#15212c, 53-79 #d5e4f1,#15212c, 80-87 #f5e0ac,#15212c, 88-99 #d5e4f1,#15212c,
-|3: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
+|2: 0-1 #000000,#86d8ff, 2-2 d,#223342, 3-29 #e1eef9,#223342, 30-37 #ffeab8,#223342, 38-48 #e1eef9,#223342, 49-49 d,#223342, 50-51 #ffffff,#5f7990, 52-52 d,#15212c, 53-79 #d5e4f1,#15212c, 80-87 #f5e0ac,#15212c, 88-98 #d5e4f1,#15212c, 99-99 d,#15212c,
+|3: 0-2 d,#223342, 3-21 #e1eef9,#223342, 22-49 d,#223342, 50-52 d,#15212c, 53-71 #d5e4f1,#15212c, 72-99 d,#15212c,
|4: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
-|5: 0-0 d,#86a8c2, 1-49 d,#000000, 50-50 d,#86a8c2, 51-99 d,#000000,
-|6: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
+|5: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
+|6: 0-0 d,#86a8c2, 1-49 d,#000000, 50-50 d,#86a8c2, 51-99 d,#000000,
|7: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
|8: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
|9: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
@@ -103,11 +104,11 @@
|14: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
|15: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
|16: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
-|17: 0-1 #ffffff,#5f7990, 2-2 d,#15212c, 3-29 #d5e4f1,#15212c, 30-37 #f5e0ac,#15212c, 38-49 #d5e4f1,#15212c, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
-|18: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
+|17: 0-1 #ffffff,#5f7990, 2-2 d,#15212c, 3-29 #d5e4f1,#15212c, 30-37 #f5e0ac,#15212c, 38-48 #d5e4f1,#15212c, 49-49 d,#15212c, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
+|18: 0-2 d,#15212c, 3-21 #d5e4f1,#15212c, 22-49 d,#15212c, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
|19: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
-|20: 0-0 d,#86a8c2, 1-49 d,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
-|21: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
+|20: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
+|21: 0-0 d,#86a8c2, 1-49 d,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
|22: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
|23: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
|24: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
@@ -117,15 +118,15 @@
|28: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
|29: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
|30: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000,
-== snap paper grid=100x31 cursor=4,5
+== snap paper grid=100x31 cursor=4,6
== style paper grid=100x31
|0: 0-50 #52625a,#e8eae3, 51-59 #203e31,#c5e2d0, 60-70 #52625a,#e8eae3, 71-99 d,#e8eae3,
|1: 0-1 #000000,#bda27e, 2-2 d,#dce5dc, 3-34 #4a5a52,#dce5dc, 35-49 d,#dce5dc, 50-51 #000000,#bda27e, 52-52 d,#e8eae3, 53-84 #52625a,#e8eae3, 85-99 d,#e8eae3,
-|2: 0-1 #ffffff,#367157, 2-2 d,#dce5dc, 3-29 #4a5a52,#dce5dc, 30-37 #69447a,#dce5dc, 38-49 #4a5a52,#dce5dc, 50-51 #000000,#8fa594, 52-52 d,#e8eae3, 53-79 #52625a,#e8eae3, 80-87 #724c83,#e8eae3, 88-99 #52625a,#e8eae3,
-|3: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
+|2: 0-1 #ffffff,#367157, 2-2 d,#dce5dc, 3-29 #4a5a52,#dce5dc, 30-37 #69447a,#dce5dc, 38-48 #4a5a52,#dce5dc, 49-49 d,#dce5dc, 50-51 #000000,#8fa594, 52-52 d,#e8eae3, 53-79 #52625a,#e8eae3, 80-87 #724c83,#e8eae3, 88-98 #52625a,#e8eae3, 99-99 d,#e8eae3,
+|3: 0-2 d,#dce5dc, 3-21 #4a5a52,#dce5dc, 22-49 d,#dce5dc, 50-52 d,#e8eae3, 53-71 #52625a,#e8eae3, 72-99 d,#e8eae3,
|4: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
-|5: 0-0 d,#869d8a, 1-49 d,#f5f3ed, 50-50 d,#869d8a, 51-99 d,#f5f3ed,
-|6: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
+|5: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
+|6: 0-0 d,#869d8a, 1-49 d,#f5f3ed, 50-50 d,#869d8a, 51-99 d,#f5f3ed,
|7: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
|8: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
|9: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
@@ -136,11 +137,11 @@
|14: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
|15: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
|16: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
-|17: 0-1 #000000,#8fa594, 2-2 d,#e8eae3, 3-29 #52625a,#e8eae3, 30-37 #724c83,#e8eae3, 38-49 #52625a,#e8eae3, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
-|18: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
+|17: 0-1 #000000,#8fa594, 2-2 d,#e8eae3, 3-29 #52625a,#e8eae3, 30-37 #724c83,#e8eae3, 38-48 #52625a,#e8eae3, 49-49 d,#e8eae3, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
+|18: 0-2 d,#e8eae3, 3-21 #52625a,#e8eae3, 22-49 d,#e8eae3, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
|19: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
-|20: 0-0 d,#869d8a, 1-49 d,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
-|21: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
+|20: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
+|21: 0-0 d,#869d8a, 1-49 d,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
|22: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
|23: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
|24: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
@@ -150,15 +151,15 @@
|28: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
|29: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
|30: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed,
-== snap daybreak grid=100x31 cursor=4,5
+== snap daybreak grid=100x31 cursor=4,6
== style daybreak grid=100x31
|0: 0-50 #29465e,#edf2f7, 51-59 #10364f,#b9dffb, 60-70 #29465e,#edf2f7, 71-99 d,#edf2f7,
|1: 0-1 #000000,#b79a7a, 2-2 d,#dfeaf3, 3-34 #213e56,#dfeaf3, 35-49 d,#dfeaf3, 50-51 #000000,#b79a7a, 52-52 d,#edf2f7, 53-84 #29465e,#edf2f7, 85-99 d,#edf2f7,
-|2: 0-1 #ffffff,#006198, 2-2 d,#dfeaf3, 3-29 #213e56,#dfeaf3, 30-37 #52255f,#dfeaf3, 38-49 #213e56,#dfeaf3, 50-51 #000000,#7b9eb7, 52-52 d,#edf2f7, 53-79 #29465e,#edf2f7, 80-87 #5a2d67,#edf2f7, 88-99 #29465e,#edf2f7,
-|3: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
+|2: 0-1 #ffffff,#006198, 2-2 d,#dfeaf3, 3-29 #213e56,#dfeaf3, 30-37 #52255f,#dfeaf3, 38-48 #213e56,#dfeaf3, 49-49 d,#dfeaf3, 50-51 #000000,#7b9eb7, 52-52 d,#edf2f7, 53-79 #29465e,#edf2f7, 80-87 #5a2d67,#edf2f7, 88-98 #29465e,#edf2f7, 99-99 d,#edf2f7,
+|3: 0-2 d,#dfeaf3, 3-21 #213e56,#dfeaf3, 22-49 d,#dfeaf3, 50-52 d,#edf2f7, 53-71 #29465e,#edf2f7, 72-99 d,#edf2f7,
|4: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
-|5: 0-0 d,#6685a0, 1-49 d,#ffffff, 50-50 d,#6685a0, 51-99 d,#ffffff,
-|6: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
+|5: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
+|6: 0-0 d,#6685a0, 1-49 d,#ffffff, 50-50 d,#6685a0, 51-99 d,#ffffff,
|7: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
|8: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
|9: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
@@ -169,11 +170,11 @@
|14: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
|15: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
|16: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
-|17: 0-1 #000000,#7b9eb7, 2-2 d,#edf2f7, 3-29 #29465e,#edf2f7, 30-37 #5a2d67,#edf2f7, 38-49 #29465e,#edf2f7, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
-|18: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
+|17: 0-1 #000000,#7b9eb7, 2-2 d,#edf2f7, 3-29 #29465e,#edf2f7, 30-37 #5a2d67,#edf2f7, 38-48 #29465e,#edf2f7, 49-49 d,#edf2f7, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
+|18: 0-2 d,#edf2f7, 3-21 #29465e,#edf2f7, 22-49 d,#edf2f7, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
|19: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
-|20: 0-0 d,#6685a0, 1-49 d,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
-|21: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
+|20: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
+|21: 0-0 d,#6685a0, 1-49 d,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
|22: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
|23: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
|24: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
@@ -183,15 +184,15 @@
|28: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
|29: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
|30: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff,
-== snap atelier grid=100x31 cursor=4,5
+== snap atelier grid=100x31 cursor=4,6
== style atelier grid=100x31
|0: 0-50 #3d6055,#e4f0ed, 51-59 #35492d,#edf2a5, 60-70 #3d6055,#e4f0ed, 71-99 d,#e4f0ed,
|1: 0-1 #000000,#bca481, 2-2 d,#d3e7df, 3-34 #35584d,#d3e7df, 35-49 d,#d3e7df, 50-51 #000000,#bca481, 52-52 d,#e4f0ed, 53-84 #3d6055,#e4f0ed, 85-99 d,#e4f0ed,
-|2: 0-1 #ffffff,#36826b, 2-2 d,#d3e7df, 3-29 #35584d,#d3e7df, 30-37 #653f6b,#d3e7df, 38-49 #35584d,#d3e7df, 50-51 #000000,#95baa2, 52-52 d,#e4f0ed, 53-79 #3d6055,#e4f0ed, 80-87 #6e4774,#e4f0ed, 88-99 #3d6055,#e4f0ed,
-|3: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
+|2: 0-1 #ffffff,#36826b, 2-2 d,#d3e7df, 3-29 #35584d,#d3e7df, 30-37 #653f6b,#d3e7df, 38-48 #35584d,#d3e7df, 49-49 d,#d3e7df, 50-51 #000000,#95baa2, 52-52 d,#e4f0ed, 53-79 #3d6055,#e4f0ed, 80-87 #6e4774,#e4f0ed, 88-98 #3d6055,#e4f0ed, 99-99 d,#e4f0ed,
+|3: 0-2 d,#d3e7df, 3-21 #35584d,#d3e7df, 22-49 d,#d3e7df, 50-52 d,#e4f0ed, 53-71 #3d6055,#e4f0ed, 72-99 d,#e4f0ed,
|4: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
-|5: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
-|6: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
+|5: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
+|6: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
|7: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
|8: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
|9: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
@@ -202,11 +203,11 @@
|14: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
|15: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
|16: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
-|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-37 #6e4774,#e4f0ed, 38-49 #3d6055,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
-|18: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
+|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-37 #6e4774,#e4f0ed, 38-48 #3d6055,#e4f0ed, 49-49 d,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
+|18: 0-2 d,#e4f0ed, 3-21 #3d6055,#e4f0ed, 22-49 d,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
|19: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
-|20: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
-|21: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
+|20: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
+|21: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
|22: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
|23: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
|24: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
@@ -216,12 +217,11 @@
|28: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
|29: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
|30: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb,
-== snap colors-off grid=100x31 cursor=4,5
-|3: ls Colors ls
+== snap colors-off grid=100x31 cursor=4,6
+|4: ls Colors ls
== style colors-off grid=100x31
-|3: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-3 #293f35,#fffdeb, 4-41 d,#fffdeb, 42-42 d,#e4f0ed, 43-48 #3d6055,#e4f0ed, 49-49 d,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-53 #293f35,#fffdeb, 54-99 d,#fffdeb,
-|4: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #293f35,#fffdeb, 7-49 d,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-56 #293f35,#fffdeb, 57-99 d,#fffdeb,
-|6: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
+|4: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-3 #293f35,#fffdeb, 4-41 d,#fffdeb, 42-42 d,#e4f0ed, 43-48 #3d6055,#e4f0ed, 49-49 d,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-53 #293f35,#fffdeb, 54-99 d,#fffdeb,
+|5: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #293f35,#fffdeb, 7-49 d,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-56 #293f35,#fffdeb, 57-99 d,#fffdeb,
|7: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
|8: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
|9: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
@@ -232,10 +232,10 @@
|14: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
|15: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
|16: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
-|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-37 #6e4774,#e4f0ed, 38-49 #3d6055,#e4f0ed, 50-50 d,#9fae76, 51-99 d,#fffdeb,
-|18: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-3 #293f35,#fffdeb, 4-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
-|19: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #293f35,#fffdeb, 7-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
-|20: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
+|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-37 #6e4774,#e4f0ed, 38-48 #3d6055,#e4f0ed, 49-49 d,#e4f0ed, 50-50 d,#9fae76, 51-99 d,#fffdeb,
+|18: 0-2 d,#e4f0ed, 3-21 #3d6055,#e4f0ed, 22-49 d,#e4f0ed, 50-50 d,#9fae76, 51-99 d,#fffdeb,
+|19: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-3 #293f35,#fffdeb, 4-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
+|20: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #293f35,#fffdeb, 7-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
|21: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
|22: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
|23: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
@@ -246,16 +246,16 @@
|28: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
|29: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
|30: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb,
-== snap forge grid=100x31 cursor=4,5
-|3: ls ls
+== snap forge grid=100x31 cursor=4,6
+|4: ls ls
== style forge grid=100x31
|0: 0-50 #c5c9d2,#22252b, 51-59 #e2eef4,#1c5172, 60-70 #c5c9d2,#22252b, 71-99 d,#22252b,
|1: 0-1 #ffffff,#6a7b67, 2-2 d,#2d3440, 3-34 #d1d5de,#2d3440, 35-49 d,#2d3440, 50-51 #ffffff,#6a7b67, 52-52 d,#22252b, 53-84 #c5c9d2,#22252b, 85-99 d,#22252b,
-|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-37 #f8cba7,#2d3440, 38-49 #d1d5de,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-87 #ecbf9b,#22252b, 88-99 #c5c9d2,#22252b,
-|3: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
+|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-37 #f8cba7,#2d3440, 38-48 #d1d5de,#2d3440, 49-49 d,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-87 #ecbf9b,#22252b, 88-98 #c5c9d2,#22252b, 99-99 d,#22252b,
+|3: 0-2 d,#2d3440, 3-21 #d1d5de,#2d3440, 22-49 d,#2d3440, 50-52 d,#22252b, 53-71 #c5c9d2,#22252b, 72-99 d,#22252b,
|4: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
-|5: 0-0 d,#708095, 1-49 d,#0c0c0e, 50-50 d,#708095, 51-99 d,#0c0c0e,
-|6: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
+|5: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
+|6: 0-0 d,#708095, 1-49 d,#0c0c0e, 50-50 d,#708095, 51-99 d,#0c0c0e,
|7: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
|8: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
|9: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
@@ -266,11 +266,11 @@
|14: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
|15: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
|16: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
-|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-37 #ecbf9b,#22252b, 38-49 #c5c9d2,#22252b, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
-|18: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
+|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-37 #ecbf9b,#22252b, 38-48 #c5c9d2,#22252b, 49-49 d,#22252b, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
+|18: 0-2 d,#22252b, 3-21 #c5c9d2,#22252b, 22-49 d,#22252b, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
|19: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
-|20: 0-0 d,#708095, 1-49 d,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
-|21: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
+|20: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
+|21: 0-0 d,#708095, 1-49 d,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
|22: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
|23: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
|24: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
@@ -280,15 +280,15 @@
|28: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
|29: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
|30: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e,
-== snap lagoon grid=100x31 cursor=4,5
+== snap lagoon grid=100x31 cursor=4,6
== style lagoon grid=100x31
|0: 0-50 #bbd7cb,#29443f, 51-59 #e6f2f3,#325f6c, 60-70 #bbd7cb,#29443f, 71-99 d,#29443f,
|1: 0-1 #ffffff,#9a7662, 2-2 d,#34574d, 3-34 #c7e3d7,#34574d, 35-49 d,#34574d, 50-51 #ffffff,#9a7662, 52-52 d,#29443f, 53-84 #bbd7cb,#29443f, 85-99 d,#29443f,
-|2: 0-1 #000000,#81cfb7, 2-2 d,#34574d, 3-29 #c7e3d7,#34574d, 30-37 #f0cef8,#34574d, 38-49 #c7e3d7,#34574d, 50-51 #ffffff,#548372, 52-52 d,#29443f, 53-79 #bbd7cb,#29443f, 80-87 #e4c2ec,#29443f, 88-99 #bbd7cb,#29443f,
-|3: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
+|2: 0-1 #000000,#81cfb7, 2-2 d,#34574d, 3-29 #c7e3d7,#34574d, 30-37 #f0cef8,#34574d, 38-48 #c7e3d7,#34574d, 49-49 d,#34574d, 50-51 #ffffff,#548372, 52-52 d,#29443f, 53-79 #bbd7cb,#29443f, 80-87 #e4c2ec,#29443f, 88-98 #bbd7cb,#29443f, 99-99 d,#29443f,
+|3: 0-2 d,#34574d, 3-21 #c7e3d7,#34574d, 22-49 d,#34574d, 50-52 d,#29443f, 53-71 #bbd7cb,#29443f, 72-99 d,#29443f,
|4: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
-|5: 0-0 d,#6e9b8b, 1-49 d,#34464c, 50-50 d,#6e9b8b, 51-99 d,#34464c,
-|6: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
+|5: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
+|6: 0-0 d,#6e9b8b, 1-49 d,#34464c, 50-50 d,#6e9b8b, 51-99 d,#34464c,
|7: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
|8: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
|9: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
@@ -299,11 +299,11 @@
|14: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
|15: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
|16: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
-|17: 0-1 #ffffff,#548372, 2-2 d,#29443f, 3-29 #bbd7cb,#29443f, 30-37 #e4c2ec,#29443f, 38-49 #bbd7cb,#29443f, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
-|18: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
+|17: 0-1 #ffffff,#548372, 2-2 d,#29443f, 3-29 #bbd7cb,#29443f, 30-37 #e4c2ec,#29443f, 38-48 #bbd7cb,#29443f, 49-49 d,#29443f, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
+|18: 0-2 d,#29443f, 3-21 #bbd7cb,#29443f, 22-49 d,#29443f, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
|19: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
-|20: 0-0 d,#6e9b8b, 1-49 d,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
-|21: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
+|20: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
+|21: 0-0 d,#6e9b8b, 1-49 d,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
|22: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
|23: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
|24: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
@@ -313,15 +313,15 @@
|28: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
|29: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
|30: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c,
-== snap solarium grid=100x31 cursor=4,5
+== snap solarium grid=100x31 cursor=4,6
== style solarium grid=100x31
|0: 0-50 #c7d4bd,#123f43, 51-59 #eee8d5,#1f5c64, 60-70 #c7d4bd,#123f43, 71-99 d,#123f43,
|1: 0-1 #ffffff,#347a9a, 2-2 d,#214e50, 3-34 #d3e0c9,#214e50, 35-49 d,#214e50, 50-51 #ffffff,#347a9a, 52-52 d,#123f43, 53-84 #c7d4bd,#123f43, 85-99 d,#123f43,
-|2: 0-1 #000000,#e1c266, 2-2 d,#214e50, 3-29 #d3e0c9,#214e50, 30-37 #f5cdec,#214e50, 38-49 #d3e0c9,#214e50, 50-51 #ffffff,#467f76, 52-52 d,#123f43, 53-79 #c7d4bd,#123f43, 80-87 #e9c1e0,#123f43, 88-99 #c7d4bd,#123f43,
-|3: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
+|2: 0-1 #000000,#e1c266, 2-2 d,#214e50, 3-29 #d3e0c9,#214e50, 30-37 #f5cdec,#214e50, 38-48 #d3e0c9,#214e50, 49-49 d,#214e50, 50-51 #ffffff,#467f76, 52-52 d,#123f43, 53-79 #c7d4bd,#123f43, 80-87 #e9c1e0,#123f43, 88-98 #c7d4bd,#123f43, 99-99 d,#123f43,
+|3: 0-2 d,#214e50, 3-21 #d3e0c9,#214e50, 22-49 d,#214e50, 50-52 d,#123f43, 53-71 #c7d4bd,#123f43, 72-99 d,#123f43,
|4: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
-|5: 0-0 d,#65978c, 1-49 d,#173e45, 50-50 d,#65978c, 51-99 d,#173e45,
-|6: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
+|5: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
+|6: 0-0 d,#65978c, 1-49 d,#173e45, 50-50 d,#65978c, 51-99 d,#173e45,
|7: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
|8: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
|9: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
@@ -332,11 +332,11 @@
|14: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
|15: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
|16: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
-|17: 0-1 #ffffff,#467f76, 2-2 d,#123f43, 3-29 #c7d4bd,#123f43, 30-37 #e9c1e0,#123f43, 38-49 #c7d4bd,#123f43, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
-|18: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
+|17: 0-1 #ffffff,#467f76, 2-2 d,#123f43, 3-29 #c7d4bd,#123f43, 30-37 #e9c1e0,#123f43, 38-48 #c7d4bd,#123f43, 49-49 d,#123f43, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
+|18: 0-2 d,#123f43, 3-21 #c7d4bd,#123f43, 22-49 d,#123f43, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
|19: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
-|20: 0-0 d,#65978c, 1-49 d,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
-|21: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
+|20: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
+|21: 0-0 d,#65978c, 1-49 d,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
|22: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
|23: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
|24: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
@@ -346,15 +346,15 @@
|28: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
|29: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
|30: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45,
-== snap spectrum grid=100x31 cursor=4,5
+== snap spectrum grid=100x31 cursor=4,6
== style spectrum grid=100x31
|0: 0-50 #ddd8bb,#3b3830, 51-59 #fcfcfa,#513f60, 60-70 #ddd8bb,#3b3830, 71-99 d,#3b3830,
|1: 0-1 #ffffff,#706596, 2-2 d,#4d4736, 3-34 #e9e4c7,#4d4736, 35-49 d,#4d4736, 50-51 #ffffff,#706596, 52-52 d,#3b3830, 53-84 #ddd8bb,#3b3830, 85-99 d,#3b3830,
-|2: 0-1 #000000,#ffd866, 2-2 d,#4d4736, 3-29 #e9e4c7,#4d4736, 30-37 #d7e3ff,#4d4736, 38-49 #e9e4c7,#4d4736, 50-51 #ffffff,#92844f, 52-52 d,#3b3830, 53-79 #ddd8bb,#3b3830, 80-87 #c5d7ff,#3b3830, 88-99 #ddd8bb,#3b3830,
-|3: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
+|2: 0-1 #000000,#ffd866, 2-2 d,#4d4736, 3-29 #e9e4c7,#4d4736, 30-37 #d7e3ff,#4d4736, 38-48 #e9e4c7,#4d4736, 49-49 d,#4d4736, 50-51 #ffffff,#92844f, 52-52 d,#3b3830, 53-79 #ddd8bb,#3b3830, 80-87 #c5d7ff,#3b3830, 88-98 #ddd8bb,#3b3830, 99-99 d,#3b3830,
+|3: 0-2 d,#4d4736, 3-21 #e9e4c7,#4d4736, 22-49 d,#4d4736, 50-52 d,#3b3830, 53-71 #ddd8bb,#3b3830, 72-99 d,#3b3830,
|4: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
-|5: 0-0 d,#948d6f, 1-49 d,#100e11, 50-50 d,#948d6f, 51-99 d,#100e11,
-|6: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
+|5: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
+|6: 0-0 d,#948d6f, 1-49 d,#100e11, 50-50 d,#948d6f, 51-99 d,#100e11,
|7: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
|8: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
|9: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
@@ -365,11 +365,11 @@
|14: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
|15: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
|16: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
-|17: 0-1 #ffffff,#92844f, 2-2 d,#3b3830, 3-29 #ddd8bb,#3b3830, 30-37 #c5d7ff,#3b3830, 38-49 #ddd8bb,#3b3830, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
-|18: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
+|17: 0-1 #ffffff,#92844f, 2-2 d,#3b3830, 3-29 #ddd8bb,#3b3830, 30-37 #c5d7ff,#3b3830, 38-48 #ddd8bb,#3b3830, 49-49 d,#3b3830, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
+|18: 0-2 d,#3b3830, 3-21 #ddd8bb,#3b3830, 22-49 d,#3b3830, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
|19: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
-|20: 0-0 d,#948d6f, 1-49 d,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
-|21: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
+|20: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
+|21: 0-0 d,#948d6f, 1-49 d,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
|22: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
|23: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
|24: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
@@ -379,15 +379,15 @@
|28: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
|29: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
|30: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11,
-== snap harvest grid=100x31 cursor=4,5
+== snap harvest grid=100x31 cursor=4,6
== style harvest grid=100x31
|0: 0-50 #d5d8b8,#343b2e, 51-59 #f3f2cc,#4f5834, 60-70 #d5d8b8,#343b2e, 71-99 d,#343b2e,
|1: 0-1 #ffffff,#9b583f, 2-2 d,#424d38, 3-34 #e1e4c4,#424d38, 35-49 d,#424d38, 50-51 #ffffff,#9b583f, 52-52 d,#343b2e, 53-84 #d5d8b8,#343b2e, 85-99 d,#343b2e,
-|2: 0-1 #000000,#d8bd83, 2-2 d,#424d38, 3-29 #e1e4c4,#424d38, 30-37 #c5e7f7,#424d38, 38-49 #e1e4c4,#424d38, 50-51 #ffffff,#7b7e4e, 52-52 d,#343b2e, 53-79 #d5d8b8,#343b2e, 80-87 #badbeb,#343b2e, 88-99 #d5d8b8,#343b2e,
-|3: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
+|2: 0-1 #000000,#d8bd83, 2-2 d,#424d38, 3-29 #e1e4c4,#424d38, 30-37 #c5e7f7,#424d38, 38-48 #e1e4c4,#424d38, 49-49 d,#424d38, 50-51 #ffffff,#7b7e4e, 52-52 d,#343b2e, 53-79 #d5d8b8,#343b2e, 80-87 #badbeb,#343b2e, 88-98 #d5d8b8,#343b2e, 99-99 d,#343b2e,
+|3: 0-2 d,#424d38, 3-21 #e1e4c4,#424d38, 22-49 d,#424d38, 50-52 d,#343b2e, 53-71 #d5d8b8,#343b2e, 72-99 d,#343b2e,
|4: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
-|5: 0-0 d,#888e5e, 1-49 d,#100e0b, 50-50 d,#888e5e, 51-99 d,#100e0b,
-|6: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
+|5: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
+|6: 0-0 d,#888e5e, 1-49 d,#100e0b, 50-50 d,#888e5e, 51-99 d,#100e0b,
|7: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
|8: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
|9: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
@@ -398,11 +398,11 @@
|14: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
|15: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
|16: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
-|17: 0-1 #ffffff,#7b7e4e, 2-2 d,#343b2e, 3-29 #d5d8b8,#343b2e, 30-37 #badbeb,#343b2e, 38-49 #d5d8b8,#343b2e, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
-|18: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
+|17: 0-1 #ffffff,#7b7e4e, 2-2 d,#343b2e, 3-29 #d5d8b8,#343b2e, 30-37 #badbeb,#343b2e, 38-48 #d5d8b8,#343b2e, 49-49 d,#343b2e, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
+|18: 0-2 d,#343b2e, 3-21 #d5d8b8,#343b2e, 22-49 d,#343b2e, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
|19: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
-|20: 0-0 d,#888e5e, 1-49 d,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
-|21: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
+|20: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
+|21: 0-0 d,#888e5e, 1-49 d,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
|22: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
|23: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
|24: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
@@ -412,15 +412,15 @@
|28: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
|29: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
|30: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b,
-== snap clay grid=100x31 cursor=4,5
+== snap clay grid=100x31 cursor=4,6
== style clay grid=100x31
|0: 0-50 #d7d6b6,#343f36, 51-59 #fbf1c7,#505c38, 60-70 #d7d6b6,#343f36, 71-99 d,#343f36,
|1: 0-1 #ffffff,#875e65, 2-2 d,#424f40, 3-34 #e3e2c2,#424f40, 35-49 d,#424f40, 50-51 #ffffff,#875e65, 52-52 d,#343f36, 53-84 #d7d6b6,#343f36, 85-99 d,#343f36,
-|2: 0-1 #000000,#8ac874, 2-2 d,#424f40, 3-29 #e3e2c2,#424f40, 30-37 #b5ecec,#424f40, 38-49 #e3e2c2,#424f40, 50-51 #ffffff,#687c4d, 52-52 d,#343f36, 53-79 #d7d6b6,#343f36, 80-87 #aae0e0,#343f36, 88-99 #d7d6b6,#343f36,
-|3: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
+|2: 0-1 #000000,#8ac874, 2-2 d,#424f40, 3-29 #e3e2c2,#424f40, 30-37 #b5ecec,#424f40, 38-48 #e3e2c2,#424f40, 49-49 d,#424f40, 50-51 #ffffff,#687c4d, 52-52 d,#343f36, 53-79 #d7d6b6,#343f36, 80-87 #aae0e0,#343f36, 88-98 #d7d6b6,#343f36, 99-99 d,#343f36,
+|3: 0-2 d,#424f40, 3-21 #e3e2c2,#424f40, 22-49 d,#424f40, 50-52 d,#343f36, 53-71 #d7d6b6,#343f36, 72-99 d,#343f36,
|4: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
-|5: 0-0 d,#8c9467, 1-49 d,#10100e, 50-50 d,#8c9467, 51-99 d,#10100e,
-|6: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
+|5: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
+|6: 0-0 d,#8c9467, 1-49 d,#10100e, 50-50 d,#8c9467, 51-99 d,#10100e,
|7: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
|8: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
|9: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
@@ -431,11 +431,11 @@
|14: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
|15: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
|16: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
-|17: 0-1 #ffffff,#687c4d, 2-2 d,#343f36, 3-29 #d7d6b6,#343f36, 30-37 #aae0e0,#343f36, 38-49 #d7d6b6,#343f36, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
-|18: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
+|17: 0-1 #ffffff,#687c4d, 2-2 d,#343f36, 3-29 #d7d6b6,#343f36, 30-37 #aae0e0,#343f36, 38-48 #d7d6b6,#343f36, 49-49 d,#343f36, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
+|18: 0-2 d,#343f36, 3-21 #d7d6b6,#343f36, 22-49 d,#343f36, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
|19: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
-|20: 0-0 d,#8c9467, 1-49 d,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
-|21: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
+|20: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
+|21: 0-0 d,#8c9467, 1-49 d,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
|22: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
|23: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
|24: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
@@ -445,15 +445,15 @@
|28: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
|29: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
|30: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e,
-== snap forge-black grid=100x31 cursor=4,5
+== snap forge-black grid=100x31 cursor=4,6
== style forge-black grid=100x31
|0: 0-50 #c5c9d2,#22252b, 51-59 #e2eef4,#1c5172, 60-70 #c5c9d2,#22252b, 71-99 d,#22252b,
|1: 0-1 #ffffff,#6a7b67, 2-2 d,#2d3440, 3-34 #d1d5de,#2d3440, 35-49 d,#2d3440, 50-51 #ffffff,#6a7b67, 52-52 d,#22252b, 53-84 #c5c9d2,#22252b, 85-99 d,#22252b,
-|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-37 #f8cba7,#2d3440, 38-49 #d1d5de,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-87 #ecbf9b,#22252b, 88-99 #c5c9d2,#22252b,
-|3: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
+|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-37 #f8cba7,#2d3440, 38-48 #d1d5de,#2d3440, 49-49 d,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-87 #ecbf9b,#22252b, 88-98 #c5c9d2,#22252b, 99-99 d,#22252b,
+|3: 0-2 d,#2d3440, 3-21 #d1d5de,#2d3440, 22-49 d,#2d3440, 50-52 d,#22252b, 53-71 #c5c9d2,#22252b, 72-99 d,#22252b,
|4: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
-|5: 0-0 d,#708095, 1-49 d,#000000, 50-50 d,#708095, 51-99 d,#000000,
-|6: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
+|5: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
+|6: 0-0 d,#708095, 1-49 d,#000000, 50-50 d,#708095, 51-99 d,#000000,
|7: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
|8: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
|9: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
@@ -464,11 +464,11 @@
|14: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
|15: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
|16: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
-|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-37 #ecbf9b,#22252b, 38-49 #c5c9d2,#22252b, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
-|18: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
+|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-37 #ecbf9b,#22252b, 38-48 #c5c9d2,#22252b, 49-49 d,#22252b, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
+|18: 0-2 d,#22252b, 3-21 #c5c9d2,#22252b, 22-49 d,#22252b, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
|19: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
-|20: 0-0 d,#708095, 1-49 d,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
-|21: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
+|20: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
+|21: 0-0 d,#708095, 1-49 d,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
|22: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
|23: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
|24: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
@@ -478,15 +478,15 @@
|28: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
|29: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
|30: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000,
-== snap forge-soft grid=100x31 cursor=4,5
+== snap forge-soft grid=100x31 cursor=4,6
== style forge-soft grid=100x31
|0: 0-50 #c5c9d2,#2a2d34, 51-59 #e2eef4,#1c5172, 60-70 #c5c9d2,#2a2d34, 71-99 d,#2a2d34,
|1: 0-1 #ffffff,#6d7f6c, 2-2 d,#373e49, 3-34 #d1d5de,#373e49, 35-49 d,#373e49, 50-51 #ffffff,#6d7f6c, 52-52 d,#2a2d34, 53-84 #c5c9d2,#2a2d34, 85-99 d,#2a2d34,
-|2: 0-1 #000000,#9cc3f7, 2-2 d,#373e49, 3-29 #d1d5de,#373e49, 30-37 #f8cba7,#373e49, 38-49 #d1d5de,#373e49, 50-51 #ffffff,#55647b, 52-52 d,#2a2d34, 53-79 #c5c9d2,#2a2d34, 80-87 #ecbf9b,#2a2d34, 88-99 #c5c9d2,#2a2d34,
-|3: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
+|2: 0-1 #000000,#9cc3f7, 2-2 d,#373e49, 3-29 #d1d5de,#373e49, 30-37 #f8cba7,#373e49, 38-48 #d1d5de,#373e49, 49-49 d,#373e49, 50-51 #ffffff,#55647b, 52-52 d,#2a2d34, 53-79 #c5c9d2,#2a2d34, 80-87 #ecbf9b,#2a2d34, 88-98 #c5c9d2,#2a2d34, 99-99 d,#2a2d34,
+|3: 0-2 d,#373e49, 3-21 #d1d5de,#373e49, 22-49 d,#373e49, 50-52 d,#2a2d34, 53-71 #c5c9d2,#2a2d34, 72-99 d,#2a2d34,
|4: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
-|5: 0-0 d,#708095, 1-49 d,#35383e, 50-50 d,#708095, 51-99 d,#35383e,
-|6: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
+|5: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
+|6: 0-0 d,#708095, 1-49 d,#35383e, 50-50 d,#708095, 51-99 d,#35383e,
|7: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
|8: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
|9: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
@@ -497,11 +497,11 @@
|14: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
|15: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
|16: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
-|17: 0-1 #ffffff,#55647b, 2-2 d,#2a2d34, 3-29 #c5c9d2,#2a2d34, 30-37 #ecbf9b,#2a2d34, 38-49 #c5c9d2,#2a2d34, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
-|18: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
+|17: 0-1 #ffffff,#55647b, 2-2 d,#2a2d34, 3-29 #c5c9d2,#2a2d34, 30-37 #ecbf9b,#2a2d34, 38-48 #c5c9d2,#2a2d34, 49-49 d,#2a2d34, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
+|18: 0-2 d,#2a2d34, 3-21 #c5c9d2,#2a2d34, 22-49 d,#2a2d34, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
|19: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
-|20: 0-0 d,#708095, 1-49 d,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
-|21: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
+|20: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
+|21: 0-0 d,#708095, 1-49 d,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
|22: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
|23: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
|24: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
@@ -511,15 +511,15 @@
|28: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
|29: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
|30: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e,
-== snap orchard-black grid=100x31 cursor=4,5
+== snap orchard-black grid=100x31 cursor=4,6
== style orchard-black grid=100x31
|0: 0-50 #b2c3b8,#1d2824, 51-59 #f5f7ee,#3c614a, 60-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824,
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-99 #b2c3b8,#1d2824,
-|3: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
+|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-98 #b2c3b8,#1d2824, 99-99 d,#1d2824,
+|3: 0-2 d,#26362d, 3-21 #becfc4,#26362d, 22-49 d,#26362d, 50-52 d,#1d2824, 53-71 #b2c3b8,#1d2824, 72-99 d,#1d2824,
|4: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
-|5: 0-0 d,#608269, 1-49 d,#000000, 50-50 d,#608269, 51-99 d,#000000,
-|6: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
+|5: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
+|6: 0-0 d,#608269, 1-49 d,#000000, 50-50 d,#608269, 51-99 d,#000000,
|7: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
|8: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
|9: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
@@ -530,11 +530,11 @@
|14: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
|15: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
|16: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
-|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
-|18: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
+|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
+|18: 0-2 d,#1d2824, 3-21 #b2c3b8,#1d2824, 22-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
|19: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
-|20: 0-0 d,#608269, 1-49 d,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
-|21: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
+|20: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
+|21: 0-0 d,#608269, 1-49 d,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
|22: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
|23: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
|24: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
@@ -544,15 +544,15 @@
|28: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
|29: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
|30: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000,
-== snap helix grid=100x31 cursor=4,5
+== snap helix grid=100x31 cursor=4,6
== style helix grid=100x31
|0: 0-50 #949494,#121212, 51-59 #080808,#80a0ff, 60-70 #949494,#121212, 71-99 d,#121212,
|1: 0-1 #ffffff,#826f46, 2-2 d,#121212, 3-34 #949494,#121212, 35-49 d,#121212, 50-51 #ffffff,#826f46, 52-52 d,#121212, 53-84 #949494,#121212, 85-99 d,#121212,
-|2: 0-1 #ffffff,#626262, 2-2 d,#121212, 3-49 #949494,#121212, 50-51 #ffffff,#262626, 52-52 d,#121212, 53-99 #949494,#121212,
-|3: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
+|2: 0-1 #ffffff,#626262, 2-2 d,#121212, 3-48 #949494,#121212, 49-49 d,#121212, 50-51 #ffffff,#262626, 52-52 d,#121212, 53-98 #949494,#121212, 99-99 d,#121212,
+|3: 0-2 d,#121212, 3-21 #949494,#121212, 22-52 d,#121212, 53-71 #949494,#121212, 72-99 d,#121212,
|4: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
-|5: 0-0 d,#444444, 1-49 d,#080808, 50-50 d,#444444, 51-99 d,#080808,
-|6: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
+|5: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
+|6: 0-0 d,#444444, 1-49 d,#080808, 50-50 d,#444444, 51-99 d,#080808,
|7: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
|8: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
|9: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
@@ -563,11 +563,11 @@
|14: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
|15: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
|16: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
-|17: 0-1 #ffffff,#262626, 2-2 d,#121212, 3-49 #949494,#121212, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
-|18: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
+|17: 0-1 #ffffff,#262626, 2-2 d,#121212, 3-48 #949494,#121212, 49-49 d,#121212, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
+|18: 0-2 d,#121212, 3-21 #949494,#121212, 22-49 d,#121212, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
|19: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
-|20: 0-0 d,#444444, 1-49 d,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
-|21: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
+|20: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
+|21: 0-0 d,#444444, 1-49 d,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
|22: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
|23: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
|24: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
@@ -577,15 +577,15 @@
|28: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
|29: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
|30: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808,
-== snap dark grid=100x31 cursor=4,5
+== snap dark grid=100x31 cursor=4,6
== style dark grid=100x31
|0: 0-50 #969696,#2c2a3e, 51-59 #d8c8a8,#513c17, 60-70 #969696,#2c2a3e, 71-99 d,#2c2a3e,
|1: 0-1 #ffffff,#867a4e, 2-2 d,#2c2a3e, 3-34 #969696,#2c2a3e, 35-49 d,#2c2a3e, 50-51 #ffffff,#867a4e, 52-52 d,#2c2a3e, 53-84 #969696,#2c2a3e, 85-99 d,#2c2a3e,
-|2: 0-1 #ffffff,#663fe0, 2-2 d,#2c2a3e, 3-49 #969696,#2c2a3e, 50-51 #ffffff,#353a58, 52-52 d,#2c2a3e, 53-99 #969696,#2c2a3e,
-|3: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
+|2: 0-1 #ffffff,#663fe0, 2-2 d,#2c2a3e, 3-48 #969696,#2c2a3e, 49-49 d,#2c2a3e, 50-51 #ffffff,#353a58, 52-52 d,#2c2a3e, 53-98 #969696,#2c2a3e, 99-99 d,#2c2a3e,
+|3: 0-2 d,#2c2a3e, 3-21 #969696,#2c2a3e, 22-52 d,#2c2a3e, 53-71 #969696,#2c2a3e, 72-99 d,#2c2a3e,
|4: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
-|5: 0-0 d,#525252, 1-49 d,d, 50-50 d,#525252, 51-99 d,d,
-|6: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
+|5: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
+|6: 0-0 d,#525252, 1-49 d,d, 50-50 d,#525252, 51-99 d,d,
|7: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
|8: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
|9: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
@@ -596,11 +596,11 @@
|14: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
|15: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
|16: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
-|17: 0-1 #ffffff,#353a58, 2-2 d,#2c2a3e, 3-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
-|18: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
+|17: 0-1 #ffffff,#353a58, 2-2 d,#2c2a3e, 3-48 #969696,#2c2a3e, 49-49 d,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
+|18: 0-2 d,#2c2a3e, 3-21 #969696,#2c2a3e, 22-49 d,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
|19: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
-|20: 0-0 d,#525252, 1-49 d,d, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
-|21: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
+|20: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
+|21: 0-0 d,#525252, 1-49 d,d, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
|22: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
|23: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
|24: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
@@ -610,15 +610,15 @@
|28: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
|29: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
|30: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e,
-== snap acme grid=100x31 cursor=4,5
+== snap acme grid=100x31 cursor=4,6
== style acme grid=100x31
|0: 0-50 #000000,#eaffff, 51-59 #000000,#9eeeee, 60-70 #000000,#eaffff, 71-99 d,#eaffff,
|1: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-49 d,#eaffff, 50-51 #000000,#8888cc, 52-52 d,#eaffff, 53-84 #000000,#eaffff, 85-99 d,#eaffff,
-|2: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-49 #000000,#eaffff, 50-51 #000000,#b9c3e5, 52-52 d,#eaffff, 53-99 #000000,#eaffff,
-|3: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
+|2: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-48 #000000,#eaffff, 49-49 d,#eaffff, 50-51 #000000,#b9c3e5, 52-52 d,#eaffff, 53-98 #000000,#eaffff, 99-99 d,#eaffff,
+|3: 0-2 d,#eaffff, 3-21 #000000,#eaffff, 22-52 d,#eaffff, 53-71 #000000,#eaffff, 72-99 d,#eaffff,
|4: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
-|5: 0-99 d,#ffffea,
-|6: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
+|5: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
+|6: 0-99 d,#ffffea,
|7: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|8: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|9: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
@@ -629,11 +629,11 @@
|14: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|15: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|16: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
-|17: 0-1 #000000,#b9c3e5, 2-2 d,#eaffff, 3-49 #000000,#eaffff, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
-|18: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
+|17: 0-1 #000000,#b9c3e5, 2-2 d,#eaffff, 3-48 #000000,#eaffff, 49-49 d,#eaffff, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
+|18: 0-2 d,#eaffff, 3-21 #000000,#eaffff, 22-49 d,#eaffff, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|19: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
-|20: 0-51 d,#ffffea, 52-99 #000000,#ffffea,
-|21: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
+|20: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
+|21: 0-51 d,#ffffea, 52-99 #000000,#ffffea,
|22: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|23: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|24: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
@@ -643,15 +643,15 @@
|28: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|29: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|30: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
-== snap generated grid=100x31 cursor=4,5
+== snap generated grid=100x31 cursor=4,6
== style generated grid=100x31
|0: 0-50 #e0e0e0,#1e262e, 51-59 #e0e0e0,#303840, 60-70 #e0e0e0,#1e262e, 71-99 d,#1e262e,
|1: 0-1 #ffffff,#4f7b96, 2-2 d,#1e262e, 3-34 #e0e0e0,#1e262e, 35-49 d,#1e262e, 50-51 #ffffff,#4f7b96, 52-52 d,#1e262e, 53-84 #e0e0e0,#1e262e, 85-99 d,#1e262e,
-|2: 0-1 #000000,#ffa060, 2-2 d,#1e262e, 3-49 #e0e0e0,#1e262e, 50-51 #ffffff,#303840, 52-52 d,#1e262e, 53-99 #e0e0e0,#1e262e,
-|3: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
+|2: 0-1 #000000,#ffa060, 2-2 d,#1e262e, 3-48 #e0e0e0,#1e262e, 49-49 d,#1e262e, 50-51 #ffffff,#303840, 52-52 d,#1e262e, 53-98 #e0e0e0,#1e262e, 99-99 d,#1e262e,
+|3: 0-2 d,#1e262e, 3-21 #e0e0e0,#1e262e, 22-52 d,#1e262e, 53-71 #e0e0e0,#1e262e, 72-99 d,#1e262e,
|4: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|5: 0-0 d,#303840, 1-49 d,#161e26, 50-50 d,#303840, 51-99 d,#161e26,
-|6: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
+|5: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
+|6: 0-0 d,#303840, 1-49 d,#161e26, 50-50 d,#303840, 51-99 d,#161e26,
|7: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
|8: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
|9: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
@@ -662,11 +662,11 @@
|14: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
|15: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
|16: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|17: 0-1 #ffffff,#303840, 2-2 d,#1e262e, 3-49 #e0e0e0,#1e262e, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|18: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
+|17: 0-1 #ffffff,#303840, 2-2 d,#1e262e, 3-48 #e0e0e0,#1e262e, 49-49 d,#1e262e, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
+|18: 0-2 d,#1e262e, 3-21 #e0e0e0,#1e262e, 22-49 d,#1e262e, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
|19: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|20: 0-0 d,#303840, 1-49 d,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|21: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
+|20: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
+|21: 0-0 d,#303840, 1-49 d,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
|22: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
|23: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
|24: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
diff --git a/test/snapshots/themesel.golden b/test/snapshots/themesel.golden
index 65a4f563..978baeb1 100644
--- a/test/snapshots/themesel.golden
+++ b/test/snapshots/themesel.golden
@@ -32,23 +32,24 @@
| 28 line 28
== snap list grid=100x31 cursor=7,3
|3: 1 line 1 ThemeSel
-|17: /tmp/pardes-snap/themesel/cwd/+Themes Save Tty Collapse Del
-|18: 1 # Pardes themes
-|19: 2 Theme orchard
-|20: 3 Theme dusk
-|21: 4 Theme ink
-|22: 5 Theme paper
-|23: 6 Theme daybreak
-|24: 7 Theme atelier
-|25: 8 Theme forge
-|26: 9 Theme lagoon
-|27: 10 Theme solarium
-|28: 11 Theme spectrum
-|29: 12 Theme harvest
-|30: 13 Theme clay
-== snap step-orchard grid=100x31 cursor=7,19
+|16: /tmp/pardes-snap/themesel/cwd/+Themes Save Tty Collapse Del
+|17: 1 # Pardes themes
+|18: 2 Theme orchard
+|19: 3 Theme dusk
+|20: 4 Theme ink
+|21: 5 Theme paper
+|22: 6 Theme daybreak
+|23: 7 Theme atelier
+|24: 8 Theme forge
+|25: 9 Theme lagoon
+|26: 10 Theme solarium
+|27: 11 Theme spectrum
+|28: 12 Theme harvest
+|29: 13 Theme clay
+|30: 14 Theme forge_black
+== snap step-orchard grid=100x31 cursor=7,18
|3: 1 line 1
-|18: 1 # Pardes themes Theme
+|17: 1 # Pardes themes Theme
== style step-orchard grid=100x31
|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-99 d,#26362d,
@@ -66,22 +67,22 @@
|13: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-99 d,#0d1410,
|14: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-99 d,#0d1410,
|15: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-99 d,#0d1410,
-|16: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-99 d,#0d1410,
-|17: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-39 #dfbce5,#26362d, 40-61 #becfc4,#26362d, 62-99 d,#26362d,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-21 #e5eee5,#0d1410, 22-92 d,#0d1410, 93-93 d,#1d2824, 94-98 #b2c3b8,#1d2824, 99-99 d,#1d2824,
-|19: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-19 #e5eee5,#0d1410, 20-99 d,#0d1410,
-|20: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-99 d,#0d1410,
-|21: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-99 d,#0d1410,
-|22: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-99 d,#0d1410,
-|23: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-99 d,#0d1410,
-|24: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-99 d,#0d1410,
-|25: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-99 d,#0d1410,
-|26: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-99 d,#0d1410,
+|16: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-39 #dfbce5,#26362d, 40-61 #becfc4,#26362d, 62-99 d,#26362d,
+|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-21 #e5eee5,#0d1410, 22-92 d,#0d1410, 93-93 d,#1d2824, 94-98 #b2c3b8,#1d2824, 99-99 d,#1d2824,
+|18: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-19 #e5eee5,#0d1410, 20-99 d,#0d1410,
+|19: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-99 d,#0d1410,
+|20: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-15 #e5eee5,#0d1410, 16-99 d,#0d1410,
+|21: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-99 d,#0d1410,
+|22: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-99 d,#0d1410,
+|23: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-99 d,#0d1410,
+|24: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #e5eee5,#0d1410, 18-99 d,#0d1410,
+|25: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-18 #e5eee5,#0d1410, 19-99 d,#0d1410,
+|26: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-99 d,#0d1410,
|27: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-99 d,#0d1410,
-|28: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-99 d,#0d1410,
-|29: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-99 d,#0d1410,
-|30: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-99 d,#0d1410,
-== snap step-dusk grid=100x31 cursor=7,20
+|28: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-99 d,#0d1410,
+|29: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-99 d,#0d1410,
+|30: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-23 #e5eee5,#0d1410, 24-99 d,#0d1410,
+== snap step-dusk grid=100x31 cursor=7,19
== style step-dusk grid=100x31
|0: 0-70 #c6b6c5,#2c2630, 71-99 d,#2c2630,
|1: 0-1 #ffffff,#757a5f, 2-2 d,#3b2f3b, 3-34 #d2c2d1,#3b2f3b, 35-99 d,#3b2f3b,
@@ -99,22 +100,22 @@
|13: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-13 #c3b6c1,#39323b, 14-99 d,#39323b,
|14: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-13 #c3b6c1,#39323b, 14-99 d,#39323b,
|15: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-13 #c3b6c1,#39323b, 14-99 d,#39323b,
-|16: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-13 #c3b6c1,#39323b, 14-99 d,#39323b,
-|17: 0-1 #000000,#edb092, 2-2 d,#3b2f3b, 3-32 #d2c2d1,#3b2f3b, 33-39 #a1d5c4,#3b2f3b, 40-61 #d2c2d1,#3b2f3b, 62-99 d,#3b2f3b,
-|18: 0-0 d,#8e667c, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-21 #c3b6c1,#39323b, 22-92 d,#39323b, 93-93 d,#2c2630, 94-98 #c6b6c5,#2c2630, 99-99 d,#2c2630,
-|19: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-19 #c3b6c1,#39323b, 20-99 d,#39323b,
-|20: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #8a7986,#39323b,b 7-16 #c3b6c1,#39323b, 17-99 d,#39323b,
-|21: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-15 #c3b6c1,#39323b, 16-99 d,#39323b,
-|22: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-17 #c3b6c1,#39323b, 18-99 d,#39323b,
-|23: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-20 #c3b6c1,#39323b, 21-99 d,#39323b,
-|24: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-19 #c3b6c1,#39323b, 20-99 d,#39323b,
-|25: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-17 #c3b6c1,#39323b, 18-99 d,#39323b,
-|26: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-18 #c3b6c1,#39323b, 19-99 d,#39323b,
+|16: 0-1 #000000,#edb092, 2-2 d,#3b2f3b, 3-32 #d2c2d1,#3b2f3b, 33-39 #a1d5c4,#3b2f3b, 40-61 #d2c2d1,#3b2f3b, 62-99 d,#3b2f3b,
+|17: 0-0 d,#8e667c, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-21 #c3b6c1,#39323b, 22-92 d,#39323b, 93-93 d,#2c2630, 94-98 #c6b6c5,#2c2630, 99-99 d,#2c2630,
+|18: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-19 #c3b6c1,#39323b, 20-99 d,#39323b,
+|19: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #8a7986,#39323b,b 7-16 #c3b6c1,#39323b, 17-99 d,#39323b,
+|20: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-15 #c3b6c1,#39323b, 16-99 d,#39323b,
+|21: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-17 #c3b6c1,#39323b, 18-99 d,#39323b,
+|22: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-20 #c3b6c1,#39323b, 21-99 d,#39323b,
+|23: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-19 #c3b6c1,#39323b, 20-99 d,#39323b,
+|24: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-17 #c3b6c1,#39323b, 18-99 d,#39323b,
+|25: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-18 #c3b6c1,#39323b, 19-99 d,#39323b,
+|26: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-20 #c3b6c1,#39323b, 21-99 d,#39323b,
|27: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-20 #c3b6c1,#39323b, 21-99 d,#39323b,
-|28: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-20 #c3b6c1,#39323b, 21-99 d,#39323b,
-|29: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-19 #c3b6c1,#39323b, 20-99 d,#39323b,
-|30: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-16 #c3b6c1,#39323b, 17-99 d,#39323b,
-== snap step-atelier grid=100x31 cursor=7,24
+|28: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-19 #c3b6c1,#39323b, 20-99 d,#39323b,
+|29: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-16 #c3b6c1,#39323b, 17-99 d,#39323b,
+|30: 0-0 d,#3d3540, 1-1 d,#39323b, 2-6 #82717e,#39323b, 7-23 #c3b6c1,#39323b, 24-99 d,#39323b,
+== snap step-atelier grid=100x31 cursor=7,23
== style step-atelier grid=100x31
|0: 0-70 #3d6055,#e4f0ed, 71-99 d,#e4f0ed,
|1: 0-1 #000000,#bca481, 2-2 d,#d3e7df, 3-34 #35584d,#d3e7df, 35-99 d,#d3e7df,
@@ -132,22 +133,22 @@
|13: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-13 #293f35,#fffdeb, 14-99 d,#fffdeb,
|14: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-13 #293f35,#fffdeb, 14-99 d,#fffdeb,
|15: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-13 #293f35,#fffdeb, 14-99 d,#fffdeb,
-|16: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-13 #293f35,#fffdeb, 14-99 d,#fffdeb,
-|17: 0-1 #ffffff,#36826b, 2-2 d,#d3e7df, 3-32 #35584d,#d3e7df, 33-39 #653f6b,#d3e7df, 40-61 #35584d,#d3e7df, 62-99 d,#d3e7df,
-|18: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-21 #293f35,#fffdeb, 22-92 d,#fffdeb, 93-93 d,#e4f0ed, 94-98 #3d6055,#e4f0ed, 99-99 d,#e4f0ed,
-|19: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-19 #293f35,#fffdeb, 20-99 d,#fffdeb,
-|20: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-16 #293f35,#fffdeb, 17-99 d,#fffdeb,
-|21: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-15 #293f35,#fffdeb, 16-99 d,#fffdeb,
-|22: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-17 #293f35,#fffdeb, 18-99 d,#fffdeb,
-|23: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-20 #293f35,#fffdeb, 21-99 d,#fffdeb,
-|24: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #889078,#fffdeb,b 7-19 #293f35,#fffdeb, 20-99 d,#fffdeb,
-|25: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-17 #293f35,#fffdeb, 18-99 d,#fffdeb,
-|26: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-18 #293f35,#fffdeb, 19-99 d,#fffdeb,
+|16: 0-1 #ffffff,#36826b, 2-2 d,#d3e7df, 3-32 #35584d,#d3e7df, 33-39 #653f6b,#d3e7df, 40-61 #35584d,#d3e7df, 62-99 d,#d3e7df,
+|17: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-21 #293f35,#fffdeb, 22-92 d,#fffdeb, 93-93 d,#e4f0ed, 94-98 #3d6055,#e4f0ed, 99-99 d,#e4f0ed,
+|18: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-19 #293f35,#fffdeb, 20-99 d,#fffdeb,
+|19: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-16 #293f35,#fffdeb, 17-99 d,#fffdeb,
+|20: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-15 #293f35,#fffdeb, 16-99 d,#fffdeb,
+|21: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-17 #293f35,#fffdeb, 18-99 d,#fffdeb,
+|22: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-20 #293f35,#fffdeb, 21-99 d,#fffdeb,
+|23: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #889078,#fffdeb,b 7-19 #293f35,#fffdeb, 20-99 d,#fffdeb,
+|24: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-17 #293f35,#fffdeb, 18-99 d,#fffdeb,
+|25: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-18 #293f35,#fffdeb, 19-99 d,#fffdeb,
+|26: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-20 #293f35,#fffdeb, 21-99 d,#fffdeb,
|27: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-20 #293f35,#fffdeb, 21-99 d,#fffdeb,
-|28: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-20 #293f35,#fffdeb, 21-99 d,#fffdeb,
-|29: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-19 #293f35,#fffdeb, 20-99 d,#fffdeb,
-|30: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-16 #293f35,#fffdeb, 17-99 d,#fffdeb,
-== snap step-forge grid=100x31 cursor=7,25
+|28: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-19 #293f35,#fffdeb, 20-99 d,#fffdeb,
+|29: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-16 #293f35,#fffdeb, 17-99 d,#fffdeb,
+|30: 0-0 d,#eeeccf, 1-1 d,#fffdeb, 2-6 #909880,#fffdeb, 7-23 #293f35,#fffdeb, 24-99 d,#fffdeb,
+== snap step-forge grid=100x31 cursor=7,24
== style step-forge grid=100x31
|0: 0-70 #c5c9d2,#22252b, 71-99 d,#22252b,
|1: 0-1 #ffffff,#6a7b67, 2-2 d,#2d3440, 3-34 #d1d5de,#2d3440, 35-99 d,#2d3440,
@@ -165,22 +166,22 @@
|13: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-13 #e5e5e5,#0c0c0e, 14-99 d,#0c0c0e,
|14: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-13 #e5e5e5,#0c0c0e, 14-99 d,#0c0c0e,
|15: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-13 #e5e5e5,#0c0c0e, 14-99 d,#0c0c0e,
-|16: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-13 #e5e5e5,#0c0c0e, 14-99 d,#0c0c0e,
-|17: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-32 #d1d5de,#2d3440, 33-39 #f8cba7,#2d3440, 40-61 #d1d5de,#2d3440, 62-99 d,#2d3440,
-|18: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-21 #e5e5e5,#0c0c0e, 22-92 d,#0c0c0e, 93-93 d,#22252b, 94-98 #c5c9d2,#22252b, 99-99 d,#22252b,
-|19: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-19 #e5e5e5,#0c0c0e, 20-99 d,#0c0c0e,
-|20: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-16 #e5e5e5,#0c0c0e, 17-99 d,#0c0c0e,
-|21: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-15 #e5e5e5,#0c0c0e, 16-99 d,#0c0c0e,
-|22: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-17 #e5e5e5,#0c0c0e, 18-99 d,#0c0c0e,
-|23: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-20 #e5e5e5,#0c0c0e, 21-99 d,#0c0c0e,
-|24: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-19 #e5e5e5,#0c0c0e, 20-99 d,#0c0c0e,
-|25: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #6a6d74,#0c0c0e,b 7-17 #e5e5e5,#0c0c0e, 18-99 d,#0c0c0e,
-|26: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-18 #e5e5e5,#0c0c0e, 19-99 d,#0c0c0e,
+|16: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-32 #d1d5de,#2d3440, 33-39 #f8cba7,#2d3440, 40-61 #d1d5de,#2d3440, 62-99 d,#2d3440,
+|17: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-21 #e5e5e5,#0c0c0e, 22-92 d,#0c0c0e, 93-93 d,#22252b, 94-98 #c5c9d2,#22252b, 99-99 d,#22252b,
+|18: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-19 #e5e5e5,#0c0c0e, 20-99 d,#0c0c0e,
+|19: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-16 #e5e5e5,#0c0c0e, 17-99 d,#0c0c0e,
+|20: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-15 #e5e5e5,#0c0c0e, 16-99 d,#0c0c0e,
+|21: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-17 #e5e5e5,#0c0c0e, 18-99 d,#0c0c0e,
+|22: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-20 #e5e5e5,#0c0c0e, 21-99 d,#0c0c0e,
+|23: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-19 #e5e5e5,#0c0c0e, 20-99 d,#0c0c0e,
+|24: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #6a6d74,#0c0c0e,b 7-17 #e5e5e5,#0c0c0e, 18-99 d,#0c0c0e,
+|25: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-18 #e5e5e5,#0c0c0e, 19-99 d,#0c0c0e,
+|26: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-20 #e5e5e5,#0c0c0e, 21-99 d,#0c0c0e,
|27: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-20 #e5e5e5,#0c0c0e, 21-99 d,#0c0c0e,
-|28: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-20 #e5e5e5,#0c0c0e, 21-99 d,#0c0c0e,
-|29: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-19 #e5e5e5,#0c0c0e, 20-99 d,#0c0c0e,
-|30: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-16 #e5e5e5,#0c0c0e, 17-99 d,#0c0c0e,
-== snap step-lagoon grid=100x31 cursor=7,26
+|28: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-19 #e5e5e5,#0c0c0e, 20-99 d,#0c0c0e,
+|29: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-16 #e5e5e5,#0c0c0e, 17-99 d,#0c0c0e,
+|30: 0-0 d,#1a1c21, 1-1 d,#0c0c0e, 2-6 #62656c,#0c0c0e, 7-23 #e5e5e5,#0c0c0e, 24-99 d,#0c0c0e,
+== snap step-lagoon grid=100x31 cursor=7,25
== style step-lagoon grid=100x31
|0: 0-70 #bbd7cb,#29443f, 71-99 d,#29443f,
|1: 0-1 #ffffff,#9a7662, 2-2 d,#34574d, 3-34 #c7e3d7,#34574d, 35-99 d,#34574d,
@@ -198,22 +199,22 @@
|13: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-13 #bac9ce,#34464c, 14-99 d,#34464c,
|14: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-13 #bac9ce,#34464c, 14-99 d,#34464c,
|15: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-13 #bac9ce,#34464c, 14-99 d,#34464c,
-|16: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-13 #bac9ce,#34464c, 14-99 d,#34464c,
-|17: 0-1 #000000,#81cfb7, 2-2 d,#34574d, 3-32 #c7e3d7,#34574d, 33-39 #f0cef8,#34574d, 40-61 #c7e3d7,#34574d, 62-99 d,#34574d,
-|18: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-21 #bac9ce,#34464c, 22-92 d,#34464c, 93-93 d,#29443f, 94-98 #bbd7cb,#29443f, 99-99 d,#29443f,
-|19: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-19 #bac9ce,#34464c, 20-99 d,#34464c,
-|20: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-16 #bac9ce,#34464c, 17-99 d,#34464c,
-|21: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-15 #bac9ce,#34464c, 16-99 d,#34464c,
-|22: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-17 #bac9ce,#34464c, 18-99 d,#34464c,
-|23: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-20 #bac9ce,#34464c, 21-99 d,#34464c,
-|24: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-19 #bac9ce,#34464c, 20-99 d,#34464c,
-|25: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-17 #bac9ce,#34464c, 18-99 d,#34464c,
-|26: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #83999c,#34464c,b 7-18 #bac9ce,#34464c, 19-99 d,#34464c,
+|16: 0-1 #000000,#81cfb7, 2-2 d,#34574d, 3-32 #c7e3d7,#34574d, 33-39 #f0cef8,#34574d, 40-61 #c7e3d7,#34574d, 62-99 d,#34574d,
+|17: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-21 #bac9ce,#34464c, 22-92 d,#34464c, 93-93 d,#29443f, 94-98 #bbd7cb,#29443f, 99-99 d,#29443f,
+|18: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-19 #bac9ce,#34464c, 20-99 d,#34464c,
+|19: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-16 #bac9ce,#34464c, 17-99 d,#34464c,
+|20: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-15 #bac9ce,#34464c, 16-99 d,#34464c,
+|21: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-17 #bac9ce,#34464c, 18-99 d,#34464c,
+|22: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-20 #bac9ce,#34464c, 21-99 d,#34464c,
+|23: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-19 #bac9ce,#34464c, 20-99 d,#34464c,
+|24: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-17 #bac9ce,#34464c, 18-99 d,#34464c,
+|25: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #83999c,#34464c,b 7-18 #bac9ce,#34464c, 19-99 d,#34464c,
+|26: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-20 #bac9ce,#34464c, 21-99 d,#34464c,
|27: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-20 #bac9ce,#34464c, 21-99 d,#34464c,
-|28: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-20 #bac9ce,#34464c, 21-99 d,#34464c,
-|29: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-19 #bac9ce,#34464c, 20-99 d,#34464c,
-|30: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-16 #bac9ce,#34464c, 17-99 d,#34464c,
-== snap step-solarium grid=100x31 cursor=7,27
+|28: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-19 #bac9ce,#34464c, 20-99 d,#34464c,
+|29: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-16 #bac9ce,#34464c, 17-99 d,#34464c,
+|30: 0-0 d,#3a4c50, 1-1 d,#34464c, 2-6 #7b9194,#34464c, 7-23 #bac9ce,#34464c, 24-99 d,#34464c,
+== snap step-solarium grid=100x31 cursor=7,26
== style step-solarium grid=100x31
|0: 0-70 #c7d4bd,#123f43, 71-99 d,#123f43,
|1: 0-1 #ffffff,#347a9a, 2-2 d,#214e50, 3-34 #d3e0c9,#214e50, 35-99 d,#214e50,
@@ -231,35 +232,22 @@
|13: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-13 #a7b7ae,#173e45, 14-99 d,#173e45,
|14: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-13 #a7b7ae,#173e45, 14-99 d,#173e45,
|15: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-13 #a7b7ae,#173e45, 14-99 d,#173e45,
-|16: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-13 #a7b7ae,#173e45, 14-99 d,#173e45,
-|17: 0-1 #000000,#e1c266, 2-2 d,#214e50, 3-32 #d3e0c9,#214e50, 33-39 #f5cdec,#214e50, 40-61 #d3e0c9,#214e50, 62-99 d,#214e50,
-|18: 0-0 d,#65978c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-21 #a7b7ae,#173e45, 22-92 d,#173e45, 93-93 d,#123f43, 94-98 #c7d4bd,#123f43, 99-99 d,#123f43,
-|19: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-19 #a7b7ae,#173e45, 20-99 d,#173e45,
-|20: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-16 #a7b7ae,#173e45, 17-99 d,#173e45,
-|21: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-15 #a7b7ae,#173e45, 16-99 d,#173e45,
-|22: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-17 #a7b7ae,#173e45, 18-99 d,#173e45,
-|23: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-20 #a7b7ae,#173e45, 21-99 d,#173e45,
-|24: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-19 #a7b7ae,#173e45, 20-99 d,#173e45,
-|25: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-17 #a7b7ae,#173e45, 18-99 d,#173e45,
-|26: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-18 #a7b7ae,#173e45, 19-99 d,#173e45,
-|27: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #6c8684,#173e45,b 7-20 #a7b7ae,#173e45, 21-99 d,#173e45,
-|28: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-20 #a7b7ae,#173e45, 21-99 d,#173e45,
-|29: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-19 #a7b7ae,#173e45, 20-99 d,#173e45,
-|30: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-16 #a7b7ae,#173e45, 17-99 d,#173e45,
+|16: 0-1 #000000,#e1c266, 2-2 d,#214e50, 3-32 #d3e0c9,#214e50, 33-39 #f5cdec,#214e50, 40-61 #d3e0c9,#214e50, 62-99 d,#214e50,
+|17: 0-0 d,#65978c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-21 #a7b7ae,#173e45, 22-92 d,#173e45, 93-93 d,#123f43, 94-98 #c7d4bd,#123f43, 99-99 d,#123f43,
+|18: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-19 #a7b7ae,#173e45, 20-99 d,#173e45,
+|19: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-16 #a7b7ae,#173e45, 17-99 d,#173e45,
+|20: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-15 #a7b7ae,#173e45, 16-99 d,#173e45,
+|21: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-17 #a7b7ae,#173e45, 18-99 d,#173e45,
+|22: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-20 #a7b7ae,#173e45, 21-99 d,#173e45,
+|23: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-19 #a7b7ae,#173e45, 20-99 d,#173e45,
+|24: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-17 #a7b7ae,#173e45, 18-99 d,#173e45,
+|25: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-18 #a7b7ae,#173e45, 19-99 d,#173e45,
+|26: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #6c8684,#173e45,b 7-20 #a7b7ae,#173e45, 21-99 d,#173e45,
+|27: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-20 #a7b7ae,#173e45, 21-99 d,#173e45,
+|28: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-19 #a7b7ae,#173e45, 20-99 d,#173e45,
+|29: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-16 #a7b7ae,#173e45, 17-99 d,#173e45,
+|30: 0-0 d,#1c454c, 1-1 d,#173e45, 2-6 #647e7c,#173e45, 7-23 #a7b7ae,#173e45, 24-99 d,#173e45,
== snap step-spectrum grid=100x31 cursor=7,27
-|18: 2 Theme orchard Theme
-|19: 3 Theme dusk
-|20: 4 Theme ink
-|21: 5 Theme paper
-|22: 6 Theme daybreak
-|23: 7 Theme atelier
-|24: 8 Theme forge
-|25: 9 Theme lagoon
-|26: 10 Theme solarium
-|27: 11 Theme spectrum
-|28: 12 Theme harvest
-|29: 13 Theme clay
-|30: 14 Theme forge_black
== style step-spectrum grid=100x31
|0: 0-70 #ddd8bb,#3b3830, 71-99 d,#3b3830,
|1: 0-1 #ffffff,#706596, 2-2 d,#4d4736, 3-34 #e9e4c7,#4d4736, 35-99 d,#4d4736,
@@ -277,9 +265,9 @@
|13: 0-0 d,#211d23, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-13 #fcfcfa,#100e11, 14-99 d,#100e11,
|14: 0-0 d,#211d23, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-13 #fcfcfa,#100e11, 14-99 d,#100e11,
|15: 0-0 d,#211d23, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-13 #fcfcfa,#100e11, 14-99 d,#100e11,
-|16: 0-0 d,#211d23, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-13 #fcfcfa,#100e11, 14-99 d,#100e11,
-|17: 0-1 #000000,#ffd866, 2-2 d,#4d4736, 3-32 #e9e4c7,#4d4736, 33-39 #d7e3ff,#4d4736, 40-61 #e9e4c7,#4d4736, 62-99 d,#4d4736,
-|18: 0-0 d,#948d6f, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-19 #fcfcfa,#100e11, 20-92 d,#100e11, 93-93 d,#3b3830, 94-98 #ddd8bb,#3b3830, 99-99 d,#3b3830,
+|16: 0-1 #000000,#ffd866, 2-2 d,#4d4736, 3-32 #e9e4c7,#4d4736, 33-39 #d7e3ff,#4d4736, 40-61 #e9e4c7,#4d4736, 62-99 d,#4d4736,
+|17: 0-0 d,#948d6f, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-21 #fcfcfa,#100e11, 22-92 d,#100e11, 93-93 d,#3b3830, 94-98 #ddd8bb,#3b3830, 99-99 d,#3b3830,
+|18: 0-0 d,#211d23, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-19 #fcfcfa,#100e11, 20-99 d,#100e11,
|19: 0-0 d,#211d23, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-16 #fcfcfa,#100e11, 17-99 d,#100e11,
|20: 0-0 d,#211d23, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-15 #fcfcfa,#100e11, 16-99 d,#100e11,
|21: 0-0 d,#211d23, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-17 #fcfcfa,#100e11, 18-99 d,#100e11,
@@ -293,7 +281,8 @@
|29: 0-0 d,#211d23, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-16 #fcfcfa,#100e11, 17-99 d,#100e11,
|30: 0-0 d,#211d23, 1-1 d,#100e11, 2-6 #68616b,#100e11, 7-23 #fcfcfa,#100e11, 24-99 d,#100e11,
== snap step-harvest grid=100x31 cursor=7,27
-|18: 3 Theme dusk Theme
+|17: 2 Theme orchard Theme
+|18: 3 Theme dusk
|19: 4 Theme ink
|20: 5 Theme paper
|21: 6 Theme daybreak
@@ -323,9 +312,9 @@
|13: 0-0 d,#201d17, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-13 #f3f2cc,#100e0b, 14-99 d,#100e0b,
|14: 0-0 d,#201d17, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-13 #f3f2cc,#100e0b, 14-99 d,#100e0b,
|15: 0-0 d,#201d17, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-13 #f3f2cc,#100e0b, 14-99 d,#100e0b,
-|16: 0-0 d,#201d17, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-13 #f3f2cc,#100e0b, 14-99 d,#100e0b,
-|17: 0-1 #000000,#d8bd83, 2-2 d,#424d38, 3-32 #e1e4c4,#424d38, 33-39 #c5e7f7,#424d38, 40-61 #e1e4c4,#424d38, 62-99 d,#424d38,
-|18: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-16 #f3f2cc,#100e0b, 17-92 d,#100e0b, 93-93 d,#343b2e, 94-98 #d5d8b8,#343b2e, 99-99 d,#343b2e,
+|16: 0-1 #000000,#d8bd83, 2-2 d,#424d38, 3-32 #e1e4c4,#424d38, 33-39 #c5e7f7,#424d38, 40-61 #e1e4c4,#424d38, 62-99 d,#424d38,
+|17: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-19 #f3f2cc,#100e0b, 20-92 d,#100e0b, 93-93 d,#343b2e, 94-98 #d5d8b8,#343b2e, 99-99 d,#343b2e,
+|18: 0-0 d,#201d17, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-16 #f3f2cc,#100e0b, 17-99 d,#100e0b,
|19: 0-0 d,#201d17, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-15 #f3f2cc,#100e0b, 16-99 d,#100e0b,
|20: 0-0 d,#201d17, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-17 #f3f2cc,#100e0b, 18-99 d,#100e0b,
|21: 0-0 d,#201d17, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-20 #f3f2cc,#100e0b, 21-99 d,#100e0b,
@@ -339,7 +328,8 @@
|29: 0-0 d,#201d17, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-23 #f3f2cc,#100e0b, 24-99 d,#100e0b,
|30: 0-0 d,#201d17, 1-1 d,#100e0b, 2-6 #6a6456,#100e0b, 7-22 #f3f2cc,#100e0b, 23-99 d,#100e0b,
== snap step-clay grid=100x31 cursor=7,27
-|18: 4 Theme ink Theme
+|17: 3 Theme dusk Theme
+|18: 4 Theme ink
|19: 5 Theme paper
|20: 6 Theme daybreak
|21: 7 Theme atelier
@@ -369,9 +359,9 @@
|13: 0-0 d,#222119, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-13 #f3e5bc,#10100e, 14-99 d,#10100e,
|14: 0-0 d,#222119, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-13 #f3e5bc,#10100e, 14-99 d,#10100e,
|15: 0-0 d,#222119, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-13 #f3e5bc,#10100e, 14-99 d,#10100e,
-|16: 0-0 d,#222119, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-13 #f3e5bc,#10100e, 14-99 d,#10100e,
-|17: 0-1 #000000,#8ac874, 2-2 d,#424f40, 3-32 #e3e2c2,#424f40, 33-39 #b5ecec,#424f40, 40-61 #e3e2c2,#424f40, 62-99 d,#424f40,
-|18: 0-0 d,#8c9467, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-15 #f3e5bc,#10100e, 16-92 d,#10100e, 93-93 d,#343f36, 94-98 #d7d6b6,#343f36, 99-99 d,#343f36,
+|16: 0-1 #000000,#8ac874, 2-2 d,#424f40, 3-32 #e3e2c2,#424f40, 33-39 #b5ecec,#424f40, 40-61 #e3e2c2,#424f40, 62-99 d,#424f40,
+|17: 0-0 d,#8c9467, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-16 #f3e5bc,#10100e, 17-92 d,#10100e, 93-93 d,#343f36, 94-98 #d7d6b6,#343f36, 99-99 d,#343f36,
+|18: 0-0 d,#222119, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-15 #f3e5bc,#10100e, 16-99 d,#10100e,
|19: 0-0 d,#222119, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-17 #f3e5bc,#10100e, 18-99 d,#10100e,
|20: 0-0 d,#222119, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-20 #f3e5bc,#10100e, 21-99 d,#10100e,
|21: 0-0 d,#222119, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-19 #f3e5bc,#10100e, 20-99 d,#10100e,
@@ -385,7 +375,8 @@
|29: 0-0 d,#222119, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-22 #f3e5bc,#10100e, 23-99 d,#10100e,
|30: 0-0 d,#222119, 1-1 d,#10100e, 2-6 #696455,#10100e, 7-25 #f3e5bc,#10100e, 26-99 d,#10100e,
== snap step-forge-black grid=100x31 cursor=7,27
-|18: 5 Theme paper Theme
+|17: 4 Theme ink Theme
+|18: 5 Theme paper
|19: 6 Theme daybreak
|20: 7 Theme atelier
|21: 8 Theme forge
@@ -415,9 +406,9 @@
|13: 0-0 d,#14161a, 1-1 d,#000000, 2-6 #62656c,#000000, 7-13 #f6f6f6,#000000, 14-99 d,#000000,
|14: 0-0 d,#14161a, 1-1 d,#000000, 2-6 #62656c,#000000, 7-13 #f6f6f6,#000000, 14-99 d,#000000,
|15: 0-0 d,#14161a, 1-1 d,#000000, 2-6 #62656c,#000000, 7-13 #f6f6f6,#000000, 14-99 d,#000000,
-|16: 0-0 d,#14161a, 1-1 d,#000000, 2-6 #62656c,#000000, 7-13 #f6f6f6,#000000, 14-99 d,#000000,
-|17: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-32 #d1d5de,#2d3440, 33-39 #f8cba7,#2d3440, 40-61 #d1d5de,#2d3440, 62-99 d,#2d3440,
-|18: 0-0 d,#708095, 1-1 d,#000000, 2-6 #62656c,#000000, 7-17 #f6f6f6,#000000, 18-92 d,#000000, 93-93 d,#22252b, 94-98 #c5c9d2,#22252b, 99-99 d,#22252b,
+|16: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-32 #d1d5de,#2d3440, 33-39 #f8cba7,#2d3440, 40-61 #d1d5de,#2d3440, 62-99 d,#2d3440,
+|17: 0-0 d,#708095, 1-1 d,#000000, 2-6 #62656c,#000000, 7-15 #f6f6f6,#000000, 16-92 d,#000000, 93-93 d,#22252b, 94-98 #c5c9d2,#22252b, 99-99 d,#22252b,
+|18: 0-0 d,#14161a, 1-1 d,#000000, 2-6 #62656c,#000000, 7-17 #f6f6f6,#000000, 18-99 d,#000000,
|19: 0-0 d,#14161a, 1-1 d,#000000, 2-6 #62656c,#000000, 7-20 #f6f6f6,#000000, 21-99 d,#000000,
|20: 0-0 d,#14161a, 1-1 d,#000000, 2-6 #62656c,#000000, 7-19 #f6f6f6,#000000, 20-99 d,#000000,
|21: 0-0 d,#14161a, 1-1 d,#000000, 2-6 #62656c,#000000, 7-17 #f6f6f6,#000000, 18-99 d,#000000,
@@ -431,7 +422,8 @@
|29: 0-0 d,#14161a, 1-1 d,#000000, 2-6 #62656c,#000000, 7-25 #f6f6f6,#000000, 26-99 d,#000000,
|30: 0-0 d,#14161a, 1-1 d,#000000, 2-6 #62656c,#000000, 7-99 d,#000000,
== snap step-forge-soft grid=100x31 cursor=7,27
-|18: 6 Theme daybreak Theme
+|17: 5 Theme paper Theme
+|18: 6 Theme daybreak
|19: 7 Theme atelier
|20: 8 Theme forge
|21: 9 Theme lagoon
@@ -461,9 +453,9 @@
|13: 0-0 d,#3d4047, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-13 #bfc3cc,#35383e, 14-99 d,#35383e,
|14: 0-0 d,#3d4047, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-13 #bfc3cc,#35383e, 14-99 d,#35383e,
|15: 0-0 d,#3d4047, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-13 #bfc3cc,#35383e, 14-99 d,#35383e,
-|16: 0-0 d,#3d4047, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-13 #bfc3cc,#35383e, 14-99 d,#35383e,
-|17: 0-1 #000000,#9cc3f7, 2-2 d,#373e49, 3-32 #d1d5de,#373e49, 33-39 #f8cba7,#373e49, 40-61 #d1d5de,#373e49, 62-99 d,#373e49,
-|18: 0-0 d,#708095, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-20 #bfc3cc,#35383e, 21-92 d,#35383e, 93-93 d,#2a2d34, 94-98 #c5c9d2,#2a2d34, 99-99 d,#2a2d34,
+|16: 0-1 #000000,#9cc3f7, 2-2 d,#373e49, 3-32 #d1d5de,#373e49, 33-39 #f8cba7,#373e49, 40-61 #d1d5de,#373e49, 62-99 d,#373e49,
+|17: 0-0 d,#708095, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-17 #bfc3cc,#35383e, 18-92 d,#35383e, 93-93 d,#2a2d34, 94-98 #c5c9d2,#2a2d34, 99-99 d,#2a2d34,
+|18: 0-0 d,#3d4047, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-20 #bfc3cc,#35383e, 21-99 d,#35383e,
|19: 0-0 d,#3d4047, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-19 #bfc3cc,#35383e, 20-99 d,#35383e,
|20: 0-0 d,#3d4047, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-17 #bfc3cc,#35383e, 18-99 d,#35383e,
|21: 0-0 d,#3d4047, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-18 #bfc3cc,#35383e, 19-99 d,#35383e,
@@ -477,7 +469,8 @@
|29: 0-0 d,#3d4047, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-99 d,#35383e,
|30: 0-0 d,#3d4047, 1-1 d,#35383e, 2-6 #868991,#35383e, 7-34 #bfc3cc,#35383e, 35-99 d,#35383e,
== snap step-orchard-black grid=100x31 cursor=7,27
-|18: 7 Theme atelier Theme
+|17: 6 Theme daybreak Theme
+|18: 7 Theme atelier
|19: 8 Theme forge
|20: 9 Theme lagoon
|21: 10 Theme solarium
@@ -507,9 +500,9 @@
|13: 0-0 d,#111813, 1-1 d,#000000, 2-6 #59675c,#000000, 7-13 #f2f8ef,#000000, 14-99 d,#000000,
|14: 0-0 d,#111813, 1-1 d,#000000, 2-6 #59675c,#000000, 7-13 #f2f8ef,#000000, 14-99 d,#000000,
|15: 0-0 d,#111813, 1-1 d,#000000, 2-6 #59675c,#000000, 7-13 #f2f8ef,#000000, 14-99 d,#000000,
-|16: 0-0 d,#111813, 1-1 d,#000000, 2-6 #59675c,#000000, 7-13 #f2f8ef,#000000, 14-99 d,#000000,
-|17: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-39 #dfbce5,#26362d, 40-61 #becfc4,#26362d, 62-99 d,#26362d,
-|18: 0-0 d,#608269, 1-1 d,#000000, 2-6 #59675c,#000000, 7-19 #f2f8ef,#000000, 20-92 d,#000000, 93-93 d,#1d2824, 94-98 #b2c3b8,#1d2824, 99-99 d,#1d2824,
+|16: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-39 #dfbce5,#26362d, 40-61 #becfc4,#26362d, 62-99 d,#26362d,
+|17: 0-0 d,#608269, 1-1 d,#000000, 2-6 #59675c,#000000, 7-20 #f2f8ef,#000000, 21-92 d,#000000, 93-93 d,#1d2824, 94-98 #b2c3b8,#1d2824, 99-99 d,#1d2824,
+|18: 0-0 d,#111813, 1-1 d,#000000, 2-6 #59675c,#000000, 7-19 #f2f8ef,#000000, 20-99 d,#000000,
|19: 0-0 d,#111813, 1-1 d,#000000, 2-6 #59675c,#000000, 7-17 #f2f8ef,#000000, 18-99 d,#000000,
|20: 0-0 d,#111813, 1-1 d,#000000, 2-6 #59675c,#000000, 7-18 #f2f8ef,#000000, 19-99 d,#000000,
|21: 0-0 d,#111813, 1-1 d,#000000, 2-6 #59675c,#000000, 7-20 #f2f8ef,#000000, 21-99 d,#000000,
@@ -523,7 +516,8 @@
|29: 0-0 d,#111813, 1-1 d,#000000, 2-6 #59675c,#000000, 7-34 #f2f8ef,#000000, 35-99 d,#000000,
|30: 0-0 d,#111813, 1-1 d,#000000, 2-6 #59675c,#000000, 7-17 #f2f8ef,#000000, 18-99 d,#000000,
== snap step-helix grid=100x31 cursor=7,27
-|18: 10 Theme solarium Theme
+|17: 9 Theme lagoon Theme
+|18: 10 Theme solarium
|19: 11 Theme spectrum
|20: 12 Theme harvest
|21: 13 Theme clay
@@ -553,9 +547,9 @@
|13: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-99 d,#080808,
|14: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-99 d,#080808,
|15: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-99 d,#080808,
-|16: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-13 #bdbdbd,#080808, 14-99 d,#080808,
-|17: 0-1 #ffffff,#626262, 2-2 d,#121212, 3-61 #949494,#121212, 62-99 d,#121212,
-|18: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-20 #bdbdbd,#080808, 21-92 d,#080808, 93-93 d,#121212, 94-98 #949494,#121212, 99-99 d,#121212,
+|16: 0-1 #ffffff,#626262, 2-2 d,#121212, 3-61 #949494,#121212, 62-99 d,#121212,
+|17: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-18 #bdbdbd,#080808, 19-92 d,#080808, 93-93 d,#121212, 94-98 #949494,#121212, 99-99 d,#121212,
+|18: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-20 #bdbdbd,#080808, 21-99 d,#080808,
|19: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-20 #bdbdbd,#080808, 21-99 d,#080808,
|20: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-19 #bdbdbd,#080808, 20-99 d,#080808,
|21: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-16 #bdbdbd,#080808, 17-99 d,#080808,
@@ -569,7 +563,8 @@
|29: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-16 #bdbdbd,#080808, 17-99 d,#080808,
|30: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-16 #bdbdbd,#080808, 17-99 d,#080808,
== snap step-dark grid=100x31 cursor=7,27
-|18: 11 Theme spectrum Theme
+|17: 10 Theme solarium Theme
+|18: 11 Theme spectrum
|19: 12 Theme harvest
|20: 13 Theme clay
|21: 14 Theme forge_black
@@ -599,9 +594,9 @@
|13: 0-0 d,#2a2a2a, 1-1 d,d, 2-6 #5a5a5a,d, 7-99 d,d,
|14: 0-0 d,#2a2a2a, 1-1 d,d, 2-6 #5a5a5a,d, 7-99 d,d,
|15: 0-0 d,#2a2a2a, 1-1 d,d, 2-6 #5a5a5a,d, 7-99 d,d,
-|16: 0-0 d,#2a2a2a, 1-1 d,d, 2-6 #5a5a5a,d, 7-99 d,d,
-|17: 0-1 #ffffff,#663fe0, 2-2 d,#2c2a3e, 3-61 #969696,#2c2a3e, 62-99 d,#2c2a3e,
-|18: 0-0 d,#525252, 1-1 d,d, 2-6 #5a5a5a,d, 7-92 d,d, 93-93 d,#2c2a3e, 94-98 #969696,#2c2a3e, 99-99 d,#2c2a3e,
+|16: 0-1 #ffffff,#663fe0, 2-2 d,#2c2a3e, 3-61 #969696,#2c2a3e, 62-99 d,#2c2a3e,
+|17: 0-0 d,#525252, 1-1 d,d, 2-6 #5a5a5a,d, 7-92 d,d, 93-93 d,#2c2a3e, 94-98 #969696,#2c2a3e, 99-99 d,#2c2a3e,
+|18: 0-0 d,#2a2a2a, 1-1 d,d, 2-6 #5a5a5a,d, 7-99 d,d,
|19: 0-0 d,#2a2a2a, 1-1 d,d, 2-6 #5a5a5a,d, 7-99 d,d,
|20: 0-0 d,#2a2a2a, 1-1 d,d, 2-6 #5a5a5a,d, 7-99 d,d,
|21: 0-0 d,#2a2a2a, 1-1 d,d, 2-6 #5a5a5a,d, 7-99 d,d,
@@ -615,7 +610,8 @@
|29: 0-0 d,#2a2a2a, 1-1 d,d, 2-6 #5a5a5a,d, 7-99 d,d,
|30: 0-0 d,#2a2a2a, 1-1 d,d, 2-6 #5a5a5a,d, 7-99 d,d,
== snap step-acme grid=100x31 cursor=7,27
-|18: 12 Theme harvest Theme
+|17: 11 Theme spectrum Theme
+|18: 12 Theme harvest
|19: 13 Theme clay
|20: 14 Theme forge_black
|21: 15 Theme forge_soft
@@ -645,9 +641,9 @@
|13: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
|14: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
|15: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
-|16: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
-|17: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff,
-|18: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-19 #000000,#ffffea, 20-92 d,#ffffea, 93-93 d,#eaffff, 94-98 #000000,#eaffff, 99-99 d,#eaffff,
+|16: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff,
+|17: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-20 #000000,#ffffea, 21-92 d,#ffffea, 93-93 d,#eaffff, 94-98 #000000,#eaffff, 99-99 d,#eaffff,
+|18: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-19 #000000,#ffffea, 20-99 d,#ffffea,
|19: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea,
|20: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-23 #000000,#ffffea, 24-99 d,#ffffea,
|21: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-22 #000000,#ffffea, 23-99 d,#ffffea,
@@ -661,7 +657,8 @@
|29: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-22 #000000,#ffffea, 23-99 d,#ffffea,
|30: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-24 #000000,#ffffea, 25-99 d,#ffffea,
== snap step-generated grid=100x31 cursor=7,27
-|18: 13 Theme clay Theme
+|17: 12 Theme harvest Theme
+|18: 13 Theme clay
|19: 14 Theme forge_black
|20: 15 Theme forge_soft
|21: 16 Theme orchard_black
@@ -691,9 +688,9 @@
|13: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-13 #e0e0e0,#161e26, 14-99 d,#161e26,
|14: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-13 #e0e0e0,#161e26, 14-99 d,#161e26,
|15: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-13 #e0e0e0,#161e26, 14-99 d,#161e26,
-|16: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-13 #e0e0e0,#161e26, 14-99 d,#161e26,
-|17: 0-1 #000000,#ffa060, 2-2 d,#1e262e, 3-61 #e0e0e0,#1e262e, 62-99 d,#1e262e,
-|18: 0-0 d,#303840, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-16 #e0e0e0,#161e26, 17-92 d,#161e26, 93-93 d,#1e262e, 94-98 #e0e0e0,#1e262e, 99-99 d,#1e262e,
+|16: 0-1 #000000,#ffa060, 2-2 d,#1e262e, 3-61 #e0e0e0,#1e262e, 62-99 d,#1e262e,
+|17: 0-0 d,#303840, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-19 #e0e0e0,#161e26, 20-92 d,#161e26, 93-93 d,#1e262e, 94-98 #e0e0e0,#1e262e, 99-99 d,#1e262e,
+|18: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-16 #e0e0e0,#161e26, 17-99 d,#161e26,
|19: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-23 #e0e0e0,#161e26, 24-99 d,#161e26,
|20: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-22 #e0e0e0,#161e26, 23-99 d,#161e26,
|21: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-25 #e0e0e0,#161e26, 26-99 d,#161e26,
@@ -724,9 +721,9 @@
|13: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
|14: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
|15: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
-|16: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea,
-|17: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff,
-|18: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-92 d,#ffffea, 93-93 d,#eaffff, 94-98 #000000,#eaffff, 99-99 d,#eaffff,
+|16: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff,
+|17: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-19 #000000,#ffffea, 20-92 d,#ffffea, 93-93 d,#eaffff, 94-98 #000000,#eaffff, 99-99 d,#eaffff,
+|18: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea,
|19: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-23 #000000,#ffffea, 24-99 d,#ffffea,
|20: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-22 #000000,#ffffea, 23-99 d,#ffffea,
|21: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-25 #000000,#ffffea, 26-99 d,#ffffea,
@@ -740,6 +737,7 @@
|29: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-24 #000000,#ffffea, 25-99 d,#ffffea,
|30: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-25 #000000,#ffffea, 26-99 d,#ffffea,
== snap tail grid=100x31 cursor=7,29
+|17: 234 Theme wolf_alabaster_dark
|18: 235 Theme wolf_alabaster_dark_bg
|19: 236 Theme wolf_alabaster_dark_mono
|20: 237 Theme wolf_alabaster_light
@@ -754,6 +752,7 @@
|29: 246 Theme zenburn
|30: 247
== snap picked grid=100x31 cursor=7,3
+|16: 14 line 14
|17: 15 line 15
|18: 16 line 16
|19: 17 line 17
diff --git a/test/snapshots/tinywin.golden b/test/snapshots/tinywin.golden
index 9b5d06d4..082e75c1 100644
--- a/test/snapshots/tinywin.golden
+++ b/test/snapshots/tinywin.golden
@@ -25,7 +25,7 @@
== 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
+|$ /tmp/pardes-snap/tinywin/cwd Tty+bash Save Mode Filter C
| /+New Save Tty Collapse Del
== snap columns grid=100x31 cursor=23,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
@@ -55,13 +55,13 @@
|Newc
| N
|$ /
-| ^[
-| n^
-| [n
-| c
-| n
-| cn
-| c
+| t
+| m
+| p
+| /
+| p
+| a
+| r
| $
| ^[
| n^
@@ -74,3 +74,10 @@
| cn
|
| /
+| +
+| N
+| e
+| w
+|
+| S
+| a
diff --git a/test/snapshots/topbarexec.golden b/test/snapshots/topbarexec.golden
index 83a12f52..a621dd18 100644
--- a/test/snapshots/topbarexec.golden
+++ b/test/snapshots/topbarexec.golden
@@ -1,7 +1,8 @@
-== snap selected grid=90x21 cursor=14,4
+== snap selected grid=90x21 cursor=14,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/topbarexec/cwd Tty+bash S /tmp/pardes-snap/topbarexec/cwd Tty+bash S
+| /tmp/pardes-snap/topbarexec/cwd Tty+bash /tmp/pardes-snap/topbarexec/cwd Tty+bash
+| Save Mode Filter Collapse Del Save Mode Filter Collapse Del
| ls ls
| zmarkfile.txt zmarkfile.txt
|
@@ -10,30 +11,29 @@
|
|
|
-|
-| /tmp/pardes-snap/topbarexec/cwd Tty+bash S
+| /tmp/pardes-snap/topbarexec/cwd Tty+bash
+| Save Mode Filter Collapse Del
| ls
| zmarkfile.txt
-== snap noarg-runs grid=90x21 cursor=14,4
+== snap noarg-runs grid=90x21 cursor=14,5
|1: New Tty Find Grep Joincol Delcol New ╭────────────────────────────────────╮
-|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp│pardes resizes=1 │
-|3: ls ls │theme orchard │
-|4: zmarkfile.txt zmark│font default │
-|5: │terms 3 cols 2 │
-|6: │active #0 43x9 │
-|7: │scroll 0/9 │
+|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash /tmp│pardes resizes=1 │
+|3: Save Mode Filter Collapse Del Save│theme orchard │
+|4: ls ls │font default │
+|5: zmarkfile.txt zmark│terms 3 cols 2 │
+|6: │active #0 43x8 │
+|7: │scroll 0/8 │
|8: │pinch 1.0000 │
|9: │tscroll 0.0000 │
|10: ╰────────────────────────────────────╯
-== snap arg-still-spliced grid=90x21 cursor=14,4
+== snap arg-still-spliced grid=90x21 cursor=14,5
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp/pardes-snap/topbarexec/cwd Tty+bash S
-|3: ls ls
-|4: zmarkfile.txt zmarkfile.txt
-|5:
+|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash /tmp/pardes-snap/topbarexec/cwd Tty+bash
+|3: Save Mode Filter Collapse Del Save Mode Filter Collapse Del
+|4: ls ls
+|5: zmarkfile.txt zmarkfile.txt
|6:
-|7:
-|8:
-|9:
-|10: /tmp/pardes-snap/topbarexec/cwd/+Search Sa
-|11: 1 zmarkfile.txt
+|7: /tmp/pardes-snap/topbarexec/cwd/+Search S
+|8: ave Tty Collapse Del
+|9: 1 zmarkfile.txt
+|10: 2
diff --git a/test/snapshots/topbarexec.snap b/test/snapshots/topbarexec.snap
index 516a23b3..81581b9d 100644
--- a/test/snapshots/topbarexec.snap
+++ b/test/snapshots/topbarexec.snap
@@ -19,9 +19,9 @@ wait 8000 zmarkfile.txt
stable 700 20000
# sweep `zmarkfile.txt` out of the shell's own greeting: an ordinary left drag,
# and the kind of stale highlight that is simply lying around in real use
-press left 3 5
-drag left 15 5
-release left 15 5
+press left 3 6
+drag left 15 6
+release left 15 6
stable 700 15000
snap selected
# Debug takes NO argument. Middle-clicked with that sweep live it must open the
diff --git a/test/snapshots/tty.golden b/test/snapshots/tty.golden
index 6c17c625..4034be54 100644
--- a/test/snapshots/tty.golden
+++ b/test/snapshots/tty.golden
@@ -1,7 +1,8 @@
-== snap tty-on grid=100x31 cursor=4,5
+== snap tty-on grid=100x31 cursor=4,6
|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/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil
+|$ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi
+| lter Collapse Del lter Collapse Del
| $ ls ls
| m.txt m.txt
| $
@@ -15,28 +16,28 @@
|
|
|
-|
-| /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil
+| /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi
+| lter Collapse Del
| ls
| m.txt
-== snap tty-echo grid=100x31 cursor=4,7
-|5: $ echo SNAP''OK
-|6: SNAPOK
-|7: $
-== snap tty-off grid=100x31 cursor=4,7
-|2: /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil
-|3: ls ls
-|5: echo SNAP''OK
-|7:
-== snap shift-esc-on grid=100x31 cursor=4,7
-|2: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil
-|3: $ ls ls
-|5: $ echo SNAP''OK
-|7: $
-== snap shift-esc-nodoc grid=100x31 cursor=4,20
-|3: $ ls Last ls
-== snap ctrl-b-off grid=100x31 cursor=4,20
-|3: $ ls ls
-|17: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil
-|18: $ ls
-|20: $
+== snap tty-echo grid=100x31 cursor=4,8
+|6: $ echo SNAP''OK
+|7: SNAPOK
+|8: $
+== snap tty-off grid=100x31 cursor=4,8
+|2: /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi
+|4: ls ls
+|6: echo SNAP''OK
+|8:
+== snap shift-esc-on grid=100x31 cursor=4,8
+|2: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi
+|4: $ ls ls
+|6: $ echo SNAP''OK
+|8: $
+== snap shift-esc-nodoc grid=100x31 cursor=4,21
+|4: $ ls Last ls
+== snap ctrl-b-off grid=100x31 cursor=4,21
+|4: $ ls ls
+|17: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi
+|19: $ ls
+|21: $
diff --git a/test/snapshots/ttyclick.golden b/test/snapshots/ttyclick.golden
index 5233b8a9..75eb9b30 100644
--- a/test/snapshots/ttyclick.golden
+++ b/test/snapshots/ttyclick.golden
@@ -1,7 +1,8 @@
-== snap tty-bodyclick grid=100x31 cursor=4,5
+== snap tty-bodyclick grid=100x31 cursor=4,6
|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/ttyclick/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod
+|$ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo
+| de Filter Collapse Del de Filter Collapse Del
| $ ls ls
| probe.txt probe.txt
| $
@@ -15,13 +16,13 @@
|
|
|
-|
-| /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod
+| /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo
+| de Filter Collapse Del
| ls
| probe.txt
== snap tty-tagedit grid=100x31 cursor=33,2
-== snap tty-tagexit grid=100x31 cursor=4,5
+== snap tty-tagexit grid=100x31 cursor=4,6
== snap ins-bodyclick grid=100x31 cursor=11,4
-|2: ^ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod
-|3: ls ls
-|5:
+|2: ^ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo
+|4: ls ls
+|6:
diff --git a/test/snapshots/ttyedit.golden b/test/snapshots/ttyedit.golden
index 144c97a0..5e25c810 100644
--- a/test/snapshots/ttyedit.golden
+++ b/test/snapshots/ttyedit.golden
@@ -1,7 +1,8 @@
-== snap before grid=100x31 cursor=7,7
+== snap before grid=100x31 cursor=7,8
|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/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode
+| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod
+| e Filter Collapse Del e Filter Collapse Del
| ls ls
| words.txt words.txt
| printf 'one two three\nfour five six\nseven ei
@@ -15,28 +16,28 @@
|
|
|
-|
-| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode
+| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod
+| e Filter Collapse Del
| ls
| words.txt
-== snap split grid=100x31 cursor=2,8
-|2: ^ /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode
-|7: one t
-|8: wo three
-|9: four five six
-|10: seven eight nine
-== snap opened grid=100x31 cursor=6,9
-|2: /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode
-|9: OPEN
+== snap split grid=100x31 cursor=2,9
+|2: ^ /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod
+|8: one t
+|9: wo three
+|10: four five six
+|11: seven eight nine
+== snap opened grid=100x31 cursor=6,10
+|2: /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod
+|10: OPEN
+|11: four five six
+|12: seven eight nine
+== snap joined grid=100x31 cursor=10,9
+|9: wo threeOPEN
|10: four five six
|11: seven eight nine
-== snap joined grid=100x31 cursor=10,8
-|8: wo threeOPEN
+|12:
+== snap undone grid=100x31 cursor=7,8
+|8: one two three
|9: four five six
|10: seven eight nine
|11:
-== snap undone grid=100x31 cursor=7,7
-|7: one two three
-|8: four five six
-|9: seven eight nine
-|10:
diff --git a/test/snapshots/ttyedit.snap b/test/snapshots/ttyedit.snap
index c453d230..6ee3ca03 100644
--- a/test/snapshots/ttyedit.snap
+++ b/test/snapshots/ttyedit.snap
@@ -17,8 +17,8 @@ stable 700 10000
key c-b
stable 600 8000
# click into the middle of the first output row
-press left 8 8
-release left 8 8
+press left 8 9
+release left 8 9
stable 400 5000
snap before
# Enter splits the row there: "one t" stays, "wo three" becomes a real line
diff --git a/test/snapshots/ttyfollow.golden b/test/snapshots/ttyfollow.golden
index 692ecf8d..bb040593 100644
--- a/test/snapshots/ttyfollow.golden
+++ b/test/snapshots/ttyfollow.golden
@@ -1,10 +1,10 @@
== snap held-pre grid=100x31 cursor=4,16
|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/ttyfollow/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo
-| 185 ls
-| 186 s.txt
-| 187
+| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M
+| ode Filter Collapse Del ode Filter Collapse Del
+| 186 ls
+| 187 s.txt
| 188
| 189
| 190
@@ -16,25 +16,25 @@
| 196
| 197
| 198
-| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo
+| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M
+| ode Filter Collapse Del
| ls
| s.txt
-== snap held-post grid=100x31 cursor=54,8
-|2: /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo$ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo
-|3: 185 $ ls
-|5: 187 $ touch go; until test -f done1; do sleep 0.05;
-|6: 188 done; echo SYN''CED
-|7: 189 SYNCED
-|8: 190 $
+== snap held-post grid=100x31 cursor=54,9
+|2: /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M
+|4: 186 $ ls
+|6: 188 $ touch go; until test -f done1; do sleep 0.05;
+|7: 189 done; echo SYN''CED
+|8: 190 SYNCED
+|9: 191 $
== snap tty-follows grid=100x31 cursor=4,16
-|2: $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo$ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo
-|3: 292 $ ls
-|4: 293 s.txt
-|5: 294 $ touch go; until test -f done1; do sleep 0.05;
-|6: 295 done; echo SYN''CED
-|7: 296 SYNCED
-|8: 297 $
-|9: 298
+|2: $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M
+|4: 293 $ ls
+|5: 294 s.txt
+|6: 295 $ touch go; until test -f done1; do sleep 0.05;
+|7: 296 done; echo SYN''CED
+|8: 297 SYNCED
+|9: 298 $
|10: 299
|11: 300
|12: [1]+ Done ( until test -f
diff --git a/test/snapshots/ttyhelp.golden b/test/snapshots/ttyhelp.golden
index 828da2fb..cd7cb946 100644
--- a/test/snapshots/ttyhelp.golden
+++ b/test/snapshots/ttyhelp.golden
@@ -22,22 +22,21 @@
|
| /+New Save Tty Collapse Del
| 1
-== snap helped grid=100x31 cursor=7,5
-|4: /tmp/pardes-snap/ttyhelp/cwd/+Help Save Tty Collapse Del
-|5: 1 pardes builtins, and how to run each:
-|6: 2 SPC and its keys, a chord, a button, the
-|7: 3 topbar - or the name, executed anywhere.
-|8: 4
-|9: 5 SPC ? Help topbar
-|10: 6 SPC P ClipPasteBefore
-|11: 7 SPC R ClipReplace
-|12: 8 SPC Y ClipYankMain
-|13: 9 SPC a a PanelAscii
-|14: 10 SPC a c PanelCurtain
-|15: 11 SPC a d PanelDissolve
-|16: 12 SPC a e PanelEdges
-|17: 13 SPC a f PanelFall
-|18: 14 SPC a r PanelScramble
-|19: 15 SPC a s PanelSlide
-|20: 16 SPC a t PanelType
-== snap leaderworks grid=100x31 cursor=7,5
+== snap helped grid=100x31 cursor=7,6
+|5: /tmp/pardes-snap/ttyhelp/cwd/+Help Save Tty Collapse Del
+|6: 1 pardes builtins, and how to run each:
+|7: 2 SPC and its keys, a chord, a button, the
+|8: 3 topbar - or the name, executed anywhere.
+|9: 4
+|10: 5 SPC ? Help topbar
+|11: 6 SPC P ClipPasteBefore
+|12: 7 SPC R ClipReplace
+|13: 8 SPC Y ClipYankMain
+|14: 9 SPC a a PanelAscii
+|15: 10 SPC a c PanelCurtain
+|16: 11 SPC a d PanelDissolve
+|17: 12 SPC a e PanelEdges
+|18: 13 SPC a f PanelFall
+|19: 14 SPC a r PanelScramble
+|20: 15 SPC a s PanelSlide
+== snap leaderworks grid=100x31 cursor=7,6
diff --git a/test/snapshots/ttylook.golden b/test/snapshots/ttylook.golden
index 6bfe5470..14c7c0c0 100644
--- a/test/snapshots/ttylook.golden
+++ b/test/snapshots/ttylook.golden
@@ -1,7 +1,8 @@
-== snap tok1 grid=120x31 cursor=2,4
+== snap tok1 grid=120x31 cursor=2,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/ttylook/cwd Tty+bash Save Mode Filter Co /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co
+| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter C /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter C
+| ollapse Del ollapse Del
| ls ls
| big.txt plain.txt sub big.txt plain.txt sub
|
@@ -15,18 +16,18 @@
|
|
|
-|
-| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co
+| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter C
+| ollapse Del
| ls
| big.txt plain.txt sub
== style tok1 grid=120x31
|0: 0-70 #b2c3b8,#1d2824, 71-119 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-59 d,#26362d, 60-61 #ffffff,#887455, 62-62 d,#1d2824, 63-94 #b2c3b8,#1d2824, 95-119 d,#1d2824,
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-39 #dfbce5,#26362d, 40-59 #becfc4,#26362d, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-91 #b2c3b8,#1d2824, 92-99 #d3b0d9,#1d2824, 100-119 #b2c3b8,#1d2824,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-8 #f5f7ee,#3c614a, 9-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|5: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-39 #dfbce5,#26362d, 40-58 #becfc4,#26362d, 59-59 d,#26362d, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-91 #b2c3b8,#1d2824, 92-99 #d3b0d9,#1d2824, 100-118 #b2c3b8,#1d2824, 119-119 d,#1d2824,
+|3: 0-2 d,#26362d, 3-13 #becfc4,#26362d, 14-59 d,#26362d, 60-62 d,#1d2824, 63-73 #b2c3b8,#1d2824, 74-119 d,#1d2824,
+|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-8 #f5f7ee,#3c614a, 9-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|6: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
@@ -37,11 +38,11 @@
|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-39 #d3b0d9,#1d2824, 40-59 #b2c3b8,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-39 #d3b0d9,#1d2824, 40-58 #b2c3b8,#1d2824, 59-59 d,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|18: 0-2 d,#1d2824, 3-13 #b2c3b8,#1d2824, 14-59 d,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|20: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
@@ -51,22 +52,20 @@
|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410,
-== snap tok2 grid=120x31 cursor=11,4
-== snap tokback grid=120x31 cursor=2,4
-== snap insert grid=120x31 cursor=4,4
-|4: nnbig.txt plain.txt sub big.txt plain.txt sub
-== snap tok3 grid=120x31 cursor=22,4
-|4: big.txt plain.txt sub big.txt plain.txt sub
-== snap diropen grid=120x31 cursor=4,8
-|6: /tmp/pardes-snap/ttylook/cwd/sub Tty+bash Save Mode Filte
-|7: ls
-== snap noop grid=120x31 cursor=2,4
-== snap fileopen grid=120x31 cursor=7,10
-|9: /tmp/pardes-snap/ttylook/cwd/big.txt Save Tty Collapse De
-|10: 1 line 1
-|11: 2 line 2
-|12: 3 line 3
-|13: 4 line 4
-|14: 5 line 5
-|15: 6 line 6
-|16: 7 line 7
+== snap tok2 grid=120x31 cursor=11,5
+== snap tokback grid=120x31 cursor=2,5
+== snap insert grid=120x31 cursor=4,5
+|5: nnbig.txt plain.txt sub big.txt plain.txt sub
+== snap tok3 grid=120x31 cursor=22,5
+|5: big.txt plain.txt sub big.txt plain.txt sub
+== snap diropen grid=120x31 cursor=4,10
+|7: /tmp/pardes-snap/ttylook/cwd/sub Tty+bash Save Mode Filt
+|8: er Collapse Del
+|9: ls
+== snap noop grid=120x31 cursor=2,5
+== snap fileopen grid=120x31 cursor=3,4
+|22: /tmp/pardes-snap/ttylook/cwd/+Search Save Tty Collapse D
+|23: el LocationsConfig
+|24: 1 @p0:1:1-2
+|25: 2 ls
+|26: 3
diff --git a/test/snapshots/ttypaste.golden b/test/snapshots/ttypaste.golden
index 448cbce3..6945faec 100644
--- a/test/snapshots/ttypaste.golden
+++ b/test/snapshots/ttypaste.golden
@@ -1,7 +1,8 @@
-== snap bracketed-paste-reached-the-shell grid=100x31 cursor=18,5
+== snap bracketed-paste-reached-the-shell grid=100x31 cursor=18,6
|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/ttypaste/cwd Tty+bash Save Mod /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod
+|$ /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mo /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mo
+| de Filter Collapse Del de Filter Collapse Del
| $ ls ls
| src.txt src.txt
| $ echo BRACKETED
@@ -15,8 +16,8 @@
|
|
|
-|
-| /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod
+| /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mo
+| de Filter Collapse Del
| ls
| src.txt
-== snap ctrl-v-with-empty-register-is-inert grid=100x31 cursor=18,5
+== snap ctrl-v-with-empty-register-is-inert grid=100x31 cursor=18,6
diff --git a/test/snapshots/ttyprompt.golden b/test/snapshots/ttyprompt.golden
index bfdfcd4b..1bbaacf4 100644
--- a/test/snapshots/ttyprompt.golden
+++ b/test/snapshots/ttyprompt.golden
@@ -1,7 +1,8 @@
-== snap greeted grid=100x31 cursor=4,5
+== snap greeted grid=100x31 cursor=4,6
|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/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo
+| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M
+| ode Filter Collapse Del ode Filter Collapse Del
| ls ls
| probe.txt probe.txt
|
@@ -15,15 +16,15 @@
|
|
|
-|
-| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo
+| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M
+| ode Filter Collapse Del
| ls
| probe.txt
-== snap in-tty grid=100x31 cursor=17,5
-|2: $ /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo
-|3: $ ls ls
-|5: $ echo ABCDEFGH
-== snap out-of-tty grid=100x31 cursor=17,5
-|2: /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo
-|3: ls ls
-|5: echo ABCDEFGH
+== snap in-tty grid=100x31 cursor=17,6
+|2: $ /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M
+|4: $ ls ls
+|6: $ echo ABCDEFGH
+== snap out-of-tty grid=100x31 cursor=17,6
+|2: /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M
+|4: ls ls
+|6: echo ABCDEFGH
diff --git a/test/snapshots/ttytaken.golden b/test/snapshots/ttytaken.golden
index d3aba990..d0f2eee9 100644
--- a/test/snapshots/ttytaken.golden
+++ b/test/snapshots/ttytaken.golden
@@ -41,8 +41,18 @@
== snap spawned grid=210x31 cursor=109,2
|2: /tmp/pardes-snap/ttytaken/cwd Tty+bash Save Mode Filter Collapse Del @`echo TAKEN''ONE` @`echo TAKEN''TWO`
|3: line 1 Exec
-|26: :
-|27: /tmp/pardes-snap/ttytaken/cwd (echo TAKEN''TWO) exit 0 Kill Save Collapse Del
-|28: TAKENTWO
-|29: exit 0
+|16: :
+|17: /tmp/pardes-snap/ttytaken/cwd (echo TAKEN''TWO) exit 0 Kill Save Collapse Del
+|18: % echo TAKEN''TWO
+|19: TAKENTWO
+|20: exit 0
+|21:
+|22:
+|23:
+|24:
+|25:
+|26:
+|27:
+|28:
+|29:
|30:
diff --git a/test/snapshots/winch.golden b/test/snapshots/winch.golden
index 2e960cc6..f009aa5d 100644
--- a/test/snapshots/winch.golden
+++ b/test/snapshots/winch.golden
@@ -1,7 +1,8 @@
-== snap storm grid=100x31 cursor=4,7
+== snap storm grid=100x31 cursor=4,8
|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/winch/cwd Tty+bash Save Mode F /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F
+|$ /tmp/pardes-snap/winch/cwd Tty+bash Save Mode /tmp/pardes-snap/winch/cwd Tty+bash Save Mode
+| Filter Collapse Del Filter Collapse Del
| $ ls ls
| $ for i in $(seq 600); do kill -WINCH $PPID; don
| e; echo SUR''VIVED
@@ -15,13 +16,14 @@
|
|
|
-|
-| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F
+| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode
+| Filter Collapse Del
| ls
-== snap resized grid=106x35 cursor=4,7
+== snap resized grid=106x35 cursor=4,8
|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/winch/cwd Tty+bash Save Mode Filt /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt
+|$ /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Fil
+| ter Collapse Del ter Collapse Del
| $ ls ls
| $ for i in $(seq 600); do kill -WINCH $PPID; done;
| echo SUR''VIVED
@@ -37,6 +39,6 @@
|
|
|
-|
-| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt
+| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Fil
+| ter Collapse Del
| ls
diff --git a/test/snapshots/windownav.golden b/test/snapshots/windownav.golden
index f89dbe80..2a3319f9 100644
--- a/test/snapshots/windownav.golden
+++ b/test/snapshots/windownav.golden
@@ -1,35 +1,37 @@
-== snap start grid=100x31 cursor=7,3
+== snap start grid=100x31 cursor=7,4
|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/windownav/cwd/w.txt Save Tty C /tmp/pardes-snap/windownav/cwd Tty+bash Save Mo
+| /tmp/pardes-snap/windownav/cwd/w.txt Save Tty /tmp/pardes-snap/windownav/cwd Tty+bash Save M
+| Collapse Del ode Filter Collapse Del
| 1 alpha ls Left
| 2 beta w.txt
| 3 gamma
-== snap to-term grid=100x31 cursor=54,5
-|3: 1 alpha Last ls
-== snap back-to-file grid=100x31 cursor=7,3
-|3: 1 alpha ls Last
-== snap esc-to-term grid=100x31 cursor=54,5
-|3: 1 alpha Last ls
-== snap esc-back-to-file grid=100x31 cursor=7,3
-|3: 1 alpha ls Last
-== snap altn grid=100x31 cursor=4,20
-|3: 1 alpha ls
-|17: /tmp/pardes-snap/windownav/cwd Tty+bash Save Mo
-|18: ls
-|19: w.txt
-== snap up grid=100x31 cursor=7,3
-|18: ls Up
-== snap right grid=100x31 cursor=54,5
-|3: 1 alpha Right ls
-|18: ls
-== snap left grid=100x31 cursor=7,3
-|3: 1 alpha ls Left
-== snap down grid=100x31 cursor=4,20
-|3: 1 alpha Down ls
-== snap toggle-doc grid=100x31 cursor=7,3
-|3: 1 alpha ls
-|18: ls Last
-== snap toggle-back grid=100x31 cursor=4,20
-|3: 1 alpha Last ls
-|18: ls
+== snap to-term grid=100x31 cursor=54,6
+|4: 1 alpha Last ls
+== snap back-to-file grid=100x31 cursor=7,4
+|4: 1 alpha ls Last
+== snap esc-to-term grid=100x31 cursor=54,6
+|4: 1 alpha Last ls
+== snap esc-back-to-file grid=100x31 cursor=7,4
+|4: 1 alpha ls Last
+== snap altn grid=100x31 cursor=4,11
+|4: 1 alpha ls
+|7: /tmp/pardes-snap/windownav/cwd Tty+bash Save M
+|8: ode Filter Collapse Del
+|9: ls
+|10: w.txt
+== snap up grid=100x31 cursor=7,4
+|9: ls Up
+== snap right grid=100x31 cursor=54,6
+|4: 1 alpha Right ls
+|9: ls
+== snap left grid=100x31 cursor=7,4
+|4: 1 alpha ls Left
+== snap down grid=100x31 cursor=4,11
+|4: 1 alpha Down ls
+== snap toggle-doc grid=100x31 cursor=7,4
+|4: 1 alpha ls
+|9: ls Last
+== snap toggle-back grid=100x31 cursor=4,11
+|4: 1 alpha Last ls
+|9: ls
diff --git a/test/snapshots/windowops.golden b/test/snapshots/windowops.golden
index 63a7574a..98454854 100644
--- a/test/snapshots/windowops.golden
+++ b/test/snapshots/windowops.golden
@@ -1,7 +1,8 @@
-== snap hover-v grid=100x31 cursor=4,5
+== snap hover-v grid=100x31 cursor=4,6
|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/windowops/cwd Tty+bash Save M╎ /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo
+| /tmp/pardes-snap/windowops/cwd Tty+bash Save M╎ /tmp/pardes-snap/windowops/cwd Tty+bash Save M
+| ode Filter Collapse Del ╎ ode Filter Collapse Del
| ls ╎ ls
| w.txt ╎ w.txt
| ╎
@@ -15,8 +16,8 @@
| ╎
| ╎
| ╎
-| ╎
| /tmp/pardes-snap/windowops/cwd Tty+bash Save M╎
+| ode Filter Collapse Del ╎
| ls ╎
| w.txt ╎
| ╎
@@ -29,13 +30,12 @@
| ╎
| ╎
| ╎
-| ╎
-== snap hover-h grid=100x31 cursor=4,5
+== snap hover-h grid=100x31 cursor=4,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo
-|3: ls ls
-|4: w.txt w.txt
-|5:
+|2: /tmp/pardes-snap/windowops/cwd Tty+bash Save M /tmp/pardes-snap/windowops/cwd Tty+bash Save M
+|3: ode Filter Collapse Del ode Filter Collapse Del
+|4: ls ls
+|5: w.txt w.txt
|6:
|7:
|8:
@@ -47,10 +47,10 @@
|14:
|15:
|16:
-|17: /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo
-|18: ls
-|19: w.txt
-|20:
+|17: /tmp/pardes-snap/windowops/cwd Tty+bash Save M
+|18: ode Filter Collapse Del
+|19: ls
+|20: w.txt
|21:
|22:
|23:
@@ -61,12 +61,12 @@
|28:
|29:
|30:
-== snap border-drag grid=100x31 cursor=4,5
+== snap border-drag grid=100x31 cursor=4,6
|1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save Mo /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo
-|3: ls ╎ ls
-|4: w.txt ╎ w.txt
-|5: ╎
+|2: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save M /tmp/pardes-snap/windowops/cwd Tty+bash Save M
+|3: ode Filter Collapse Del ╎ ode Filter Collapse Del
+|4: ls ╎ ls
+|5: w.txt ╎ w.txt
|6: ╎
|7: ╎
|8: ╎
@@ -78,10 +78,10 @@
|14: ╎
|15: ╎
|16: ╎
-|17: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save Mo
-|18: ls ╎
-|19: w.txt ╎
-|20: ╎
+|17: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save M
+|18: ode Filter Collapse Del ╎
+|19: ls ╎
+|20: w.txt ╎
|21: ╎
|22: ╎
|23: ╎
@@ -92,18 +92,20 @@
|28: ╎
|29: ╎
|30: ╎
-== snap border-done grid=100x31 cursor=4,5
+== snap border-done grid=100x31 cursor=4,6
|1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol
|2: /tmp/pardes-snap/windowops/cwd Tty+b╎ /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter
-|3: ls ╎ ls
-|4: w.txt ╎ w.txt
+|3: ash Save Mode Filter Collapse Del ╎ Collapse Del
+|4: ls ╎ ls
+|5: w.txt ╎ w.txt
|17: /tmp/pardes-snap/windowops/cwd Tty+b╎
-== snap hborder-done grid=100x31 cursor=4,5
+|18: ash Save Mode Filter Collapse Del ╎
+== snap hborder-done grid=100x31 cursor=4,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter
-|3: ls ls
-|4: w.txt w.txt
-|5:
+|2: /tmp/pardes-snap/windowops/cwd Tty+b /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter
+|3: ash Save Mode Filter Collapse Del Collapse Del
+|4: ls ls
+|5: w.txt w.txt
|6:
|7:
|8:
@@ -111,10 +113,10 @@
|10:
|11:
|12: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-|13: /tmp/pardes-snap/windowops/cwd Tty+ba
-|14: ls
-|15: w.txt
-|16:
+|13: /tmp/pardes-snap/windowops/cwd Tty+b
+|14: ash Save Mode Filter Collapse Del
+|15: ls
+|16: w.txt
|17:
|18:
|19:
@@ -129,51 +131,52 @@
|28:
|29:
|30:
-== snap move-preview grid=100x31 cursor=4,16
+== snap move-preview grid=100x31 cursor=4,17
|12:
-== snap moved grid=100x31 cursor=4,16
-== snap altn grid=100x31 cursor=4,20
-|17: /tmp/pardes-snap/windowops/cwd Tty+ba
-|18: ls
-|19: w.txt
-== snap focus-right grid=100x31 cursor=44,5
-|18: ls Right
-== snap debug-on grid=100x31 cursor=44,5
+== snap moved grid=100x31 cursor=4,17
+== snap altn grid=100x31 cursor=4,22
+|18: /tmp/pardes-snap/windowops/cwd Tty+b
+|19: ash Save Mode Filter Collapse Del
+|20: ls
+|21: w.txt
+== snap focus-right grid=100x31 cursor=44,6
+|20: ls Right
+== snap debug-on grid=100x31 cursor=44,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep J╭────────────────────────────────────╮
-|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/wi│pardes resizes=1 │
-|3: ls ls │theme orchard │
-|4: w.txt w.txt │font default │
-|5: │terms 4 cols 2 │
-|6: │active #2 58x28 │
-|7: │scroll 0/28 │
+|2: /tmp/pardes-snap/windowops/cwd Tty+b /tmp/pardes-snap/wi│pardes resizes=1 │
+|3: ash Save Mode Filter Collapse Del Collapse Del │theme orchard │
+|4: ls ls │font default │
+|5: w.txt w.txt │terms 4 cols 2 │
+|6: │active #2 58x27 │
+|7: │scroll 0/27 │
|8: │pinch 1.0000 │
|9: │tscroll 0.0000 │
|10: ╰────────────────────────────────────╯
-|18: ls
+|20: ls
== style debug-on grid=100x31
|0: 0-60 #b2c3b8,#1d2824, 61-65 #f5f7ee,#3c614a, 66-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
|1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-39 d,#1d2824, 40-41 #ffffff,#887455, 42-42 d,#26362d, 43-61 #becfc4,#26362d, 62-99 d,d,
-|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-39 #d3b0d9,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-61 #becfc4,#26362d, 62-62 d,d, 63-79 #b2c3b8,d, 80-98 d,#152019, 99-99 d,d,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-76 #b2c3b8,d, 77-98 d,#152019, 99-99 d,d,
+|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-38 #d3b0d9,#1d2824, 39-39 d,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-61 #becfc4,#26362d, 62-62 d,d, 63-79 #b2c3b8,d, 80-98 d,#152019, 99-99 d,d,
+|3: 0-2 d,#1d2824, 3-5 #d3b0d9,#1d2824, 6-35 #b2c3b8,#1d2824, 36-39 d,#1d2824, 40-42 d,#26362d, 43-55 #becfc4,#26362d, 56-61 d,#26362d, 62-62 d,d, 63-76 #b2c3b8,d, 77-98 d,#152019, 99-99 d,d,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-76 #b2c3b8,d, 77-98 d,#152019, 99-99 d,d,
-|5: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-61 d,#0d1410, 62-62 d,d, 63-77 #b2c3b8,d, 78-98 d,#152019, 99-99 d,d,
-|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-78 #b2c3b8,d, 79-98 d,#152019, 99-99 d,d,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-77 #b2c3b8,d, 78-98 d,#152019, 99-99 d,d,
+|6: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-61 d,#0d1410, 62-62 d,d, 63-78 #b2c3b8,d, 79-98 d,#152019, 99-99 d,d,
|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-73 #b2c3b8,d, 74-98 d,#152019, 99-99 d,d,
|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-75 #b2c3b8,d, 76-98 d,#152019, 99-99 d,d,
|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-77 #b2c3b8,d, 78-98 d,#152019, 99-99 d,d,
|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-99 d,d,
|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-|13: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-39 #d3b0d9,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|13: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-38 #d3b0d9,#1d2824, 39-39 d,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|14: 0-2 d,#1d2824, 3-5 #d3b0d9,#1d2824, 6-35 #b2c3b8,#1d2824, 36-39 d,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-|16: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-39 #d3b0d9,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-|20: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|17: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|18: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-38 #d3b0d9,#1d2824, 39-39 d,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|19: 0-2 d,#1d2824, 3-5 #d3b0d9,#1d2824, 6-35 #b2c3b8,#1d2824, 36-39 d,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|22: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
@@ -182,12 +185,12 @@
|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|29: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
|30: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
-== snap debug-off grid=100x31 cursor=44,5
+== snap debug-off grid=100x31 cursor=44,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter
-|3: ls ls
-|4: w.txt w.txt
-|5:
+|2: /tmp/pardes-snap/windowops/cwd Tty+b /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter
+|3: ash Save Mode Filter Collapse Del Collapse Del
+|4: ls ls
+|5: w.txt w.txt
|6:
|7:
|8:
diff --git a/test/snapshots/wrap.golden b/test/snapshots/wrap.golden
index 8ef0ca9f..df41535b 100644
--- a/test/snapshots/wrap.golden
+++ b/test/snapshots/wrap.golden
@@ -1,7 +1,8 @@
-== snap on-narrow grid=48x15 cursor=7,3
+== snap on-narrow grid=48x15 cursor=7,4
|Newcol Joincol Find Grep Help Changelog Tutor Du
| New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/wrap/cwd/long.txt Save Tty C
+| /tmp/pardes-snap/wrap/cwd/long.txt Save Tty
+| Collapse Del
| 1 0---------1---------2---------3---------↩
| 4---------5---------6---------7---------↩
| 8---------9---------A---------B---------↩
@@ -11,28 +12,28 @@
| 3 third line
| 4
== snap clicked grid=48x15 cursor=17,4
-== snap logical-j grid=48x15 cursor=18,8
-== snap visual-gj grid=48x15 cursor=10,4
-== snap visual-gj-again grid=48x15 cursor=10,5
-== snap visual-gk grid=48x15 cursor=10,4
-== snap visual-count-gj grid=48x15 cursor=10,6
-== snap off grid=48x15 cursor=10,6
-|3: 1 0---------1---------2---------3---- Wrap
-|4: 2 second line
-|5: 3 third line
-|6: 4
-|7:
+== snap logical-j grid=48x15 cursor=17,9
+== snap visual-gj grid=48x15 cursor=10,5
+== snap visual-gj-again grid=48x15 cursor=10,6
+== snap visual-gk grid=48x15 cursor=10,5
+== snap visual-count-gj grid=48x15 cursor=10,7
+== snap off grid=48x15 cursor=10,7
+|4: 1 0---------1---------2---------3---- Wrap
+|5: 2 second line
+|6: 3 third line
+|7: 4
|8:
|9:
|10:
-== snap on-again grid=48x15 cursor=10,6
-|4: 4---------5---------6---------7---------↩
-|5: 8---------9---------A---------B---------↩
-|6: C---------D---------E---------F---------↩
-|7: G---------H---------I---------J---------
-|8: 2 second line
-|9: 3 third line
-|10: 4
+|11:
+== snap on-again grid=48x15 cursor=10,7
+|5: 4---------5---------6---------7---------↩
+|6: 8---------9---------A---------B---------↩
+|7: C---------D---------E---------F---------↩
+|8: G---------H---------I---------J---------
+|9: 2 second line
+|10: 3 third line
+|11: 4
== snap on-wide grid=128x15 cursor=7,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
diff --git a/test/snapshots/yankpaste.golden b/test/snapshots/yankpaste.golden
index 12e7a74e..22cc787f 100644
--- a/test/snapshots/yankpaste.golden
+++ b/test/snapshots/yankpaste.golden
@@ -1,7 +1,7 @@
== snap linewise-below grid=100x31 cursor=13,6
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol
-| * /tmp/pardes-snap/yankpaste/cwd/f.txt * Save Tty Collapse Del
+| * /tmp/pardes-snap/yankpaste/cwd/f.txt Save Tty Collapse Del
| 1 line 1
| 2 line 2
| 3 line 3