summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.agents/skills/pardes-9p/SKILL.md15
-rw-r--r--README.md5
-rw-r--r--build.zig35
-rw-r--r--docs/config.md17
-rw-r--r--docs/design.typ2
-rw-r--r--docs/effects.md11
-rw-r--r--docs/fs.md117
-rw-r--r--docs/mvzr-group-backtrack.md64
-rw-r--r--docs/tags.md33
-rw-r--r--src/File.zig8
-rw-r--r--src/Messages.zig12
-rw-r--r--src/Terminal.zig62
-rw-r--r--src/animation.zig201
-rw-r--r--src/builtins.zig91
-rw-r--r--src/config.zig63
-rw-r--r--src/detached/server.zig4
-rw-r--r--src/detached/wire.zig22
-rw-r--r--src/draw.zig182
-rw-r--r--src/dump.zig127
-rw-r--r--src/exec.zig29
-rw-r--r--src/fs-help.txt16
-rw-r--r--src/fs.zig24
-rw-r--r--src/gui/gui.zig97
-rw-r--r--src/layout.zig29
-rw-r--r--src/look.zig1
-rw-r--r--src/macos.zig4
-rw-r--r--src/mouse.zig59
-rw-r--r--src/ninep/cols.zig284
-rw-r--r--src/ninep/ctl.zig142
-rw-r--r--src/ninep/events.zig8
-rw-r--r--src/ninep/pane.zig61
-rw-r--r--src/ninep/screen.zig22
-rw-r--r--src/ninep/tree.zig197
-rw-r--r--src/panes.zig6
-rw-r--r--src/pardes.zig94
-rw-r--r--src/regexp.zig23
-rw-r--r--src/sam_edit.zig14
-rw-r--r--src/surface.zig10
-rw-r--r--src/tagline.zig8
-rw-r--r--src/tty/tty.zig4
-rw-r--r--src/tutor.txt9
-rw-r--r--test/fs.py65
-rw-r--r--test/output.zig15
-rw-r--r--test/panes.zig26
-rw-r--r--test/selfmount.py6
-rw-r--r--test/snapshots/badutf.golden4
-rw-r--r--test/snapshots/builtins.golden1
-rw-r--r--test/snapshots/chordcut.golden94
-rw-r--r--test/snapshots/chordcut.snap92
-rw-r--r--test/snapshots/colgrip.golden24
-rw-r--r--test/snapshots/cornerdrag-left.golden50
-rw-r--r--test/snapshots/cornerdrag.golden66
-rw-r--r--test/snapshots/doccol.golden16
-rw-r--r--test/snapshots/dump.golden7
-rw-r--r--test/snapshots/edit.golden16
-rw-r--r--test/snapshots/emptycol.golden12
-rw-r--r--test/snapshots/ghostty-mid.golden4
-rw-r--r--test/snapshots/hscroll.golden12
-rw-r--r--test/snapshots/images.golden14
-rw-r--r--test/snapshots/layout-close.golden56
-rw-r--r--test/snapshots/layout-open.golden20
-rw-r--r--test/snapshots/load.golden16
-rw-r--r--test/snapshots/look-dir.golden12
-rw-r--r--test/snapshots/lookloaded.golden16
-rw-r--r--test/snapshots/lookpanes.golden44
-rw-r--r--test/snapshots/lookrange.golden1
-rw-r--r--test/snapshots/lsprelpath.golden1
-rw-r--r--test/snapshots/modal.golden16
-rw-r--r--test/snapshots/respawn.golden8
-rw-r--r--test/snapshots/rsearch.golden17
-rw-r--r--test/snapshots/rsearch.snap9
-rw-r--r--test/snapshots/scroll.golden12
-rw-r--r--test/snapshots/selarg.golden4
-rw-r--r--test/snapshots/shellset.golden4
-rw-r--r--test/snapshots/shrinktag.golden20
-rw-r--r--test/snapshots/shrinktag.snap37
-rw-r--r--test/snapshots/splitparent.golden14
-rw-r--r--test/snapshots/splitparent.snap9
-rw-r--r--test/snapshots/tagalign.golden22
-rw-r--r--test/snapshots/tagalign2col.golden10
-rw-r--r--test/snapshots/tagbottom.golden4
-rw-r--r--test/snapshots/tagbottom.snap18
-rw-r--r--test/snapshots/tagnav.golden10
-rw-r--r--test/snapshots/theme.golden164
-rw-r--r--test/snapshots/tinywin.golden2
-rw-r--r--test/snapshots/topbarexec.golden20
-rw-r--r--test/snapshots/tty.golden14
-rw-r--r--test/snapshots/ttyclick.golden10
-rw-r--r--test/snapshots/ttyedit.golden12
-rw-r--r--test/snapshots/ttyfollow.golden12
-rw-r--r--test/snapshots/ttylook.golden24
-rw-r--r--test/snapshots/ttypaste.golden8
-rw-r--r--test/snapshots/ttyprompt.golden12
-rw-r--r--test/snapshots/winch.golden16
-rw-r--r--test/snapshots/windownav.golden8
-rw-r--r--test/snapshots/windowops.golden61
-rw-r--r--test/snapshots/windowops.snap13
-rw-r--r--test/snapshots/wrap.golden4
98 files changed, 2685 insertions, 811 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index 39a6b1ce..844e028f 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -24,6 +24,7 @@ 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'
s=${PARDES_9P##*/pardes-9p-}; m=$NINE_MOUNT/pardes/${s%.sock}
+# under `9ns --unix SOCKET -- cmd` the session is the mount itself: m=$NINE_MOUNT
cat "$m/index"
cat "$m/pane/$PARDES_PANE/body"
```
@@ -51,7 +52,7 @@ 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/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name, column serial
$m/status pid, version, panes
$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
@@ -81,6 +82,10 @@ $m/ctl the settings, one a line as a write takes them; write a setting or
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/layout one line per column: serial index x width current|notcurrent empty|full pane-serials...; active <serial>
+$m/tag the workspace tag (> replaces, >> appends, one line); $m/col/<serial>/tag a column's (serials stay, as panes' do)
+$m/col/<serial>/ctl Delcol, Joincol, New, Tty on that column; col/<serial>/exec a word as a click in its tag;
+ rmdir col/<serial> closes an empty column
$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);
@@ -149,9 +154,11 @@ 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`, 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
+written to `pty/data` should be a bracketed paste, `\e[200~<code>\e[201~`,
+then, in a separate write once the REPL has echoed the paste (Python 3.13+
+takes a `\r` read with the paste as part of it, even for one line), `\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`.
diff --git a/README.md b/README.md
index d2f3d995..49ecfa4d 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,10 @@ to refresh. Mini snapshots survive Dump/Restore without rereading the source.
On Linux, `Tty9p` (`SPC n 9`) opens a terminal with the session's 9P tree
mounted through kernel v9fs. It asks sudo in that pane, then starts your shell
as your normal user. Access the tree through `$PARDES_MOUNT`.
-See [mounted terminals](docs/v9fs.md).
+See [mounted terminals](docs/v9fs.md). Beyond acme's per-window files the tree
+serves the layout: `/layout` lists the columns, their places and panes, and
+`/tag` and `/col/<serial>/tag` are the workspace and column tags, editable like a
+window's ([the tree](docs/fs.md#the-served-tree)).
## Requirements
diff --git a/build.zig b/build.zig
index b3382d4b..e63b7f4b 100644
--- a/build.zig
+++ b/build.zig
@@ -467,7 +467,40 @@ pub fn build(b: *std.Build) void {
\\pub threadlocal var exhausted: bool = false;
\\
;
- const patched = b.fmt("{s}{s}{s}{s}", .{ src[0..at], spend, src[at..], budget });
+ const budgeted = b.fmt("{s}{s}{s}{s}", .{ src[0..at], spend, src[at..], budget });
+ // A group's greedy match never gave back to what follows the group:
+ // on `aab`, `(a+)ab` missed where `a+ab` matched. hasAlt counts the
+ // group's own `)` as an alternative, so every group with pattern
+ // after it takes matchGroup's alternatives branch, which tries each
+ // alternative's one (greedy) match and gives up. When the rest fails
+ // after it, try that alternative's shorter matches, longest first
+ // (docs/mvzr-group-backtrack.md is the report for upstream).
+ const group_anchor =
+ \\ const next_match = matchPattern(next_patt, sets, haystack, m1.i);
+ \\ if (next_match) |m2| {
+ \\ // Whole group matches, and we can use m2.j
+ \\ return m2;
+ \\ } else { // Try our next pattern, if any.
+ \\
+ ;
+ const group_backoff =
+ \\ const next_match = matchPattern(next_patt, sets, haystack, m1.i);
+ \\ if (next_match) |m2| {
+ \\ // Whole group matches, and we can use m2.j
+ \\ return m2;
+ \\ } else { // Try our next pattern, if any.
+ \\ // pardes's patch (its build.zig): back the group off.
+ \\ var e = m1.i;
+ \\ while (e > i) {
+ \\ e -= 1;
+ \\ const shorter = matchAlt(remaining_patt, sets, haystack[0..e], i) orelse continue;
+ \\ if (shorter.i != e) continue;
+ \\ if (matchPattern(next_patt, sets, haystack, e)) |whole| return whole;
+ \\ }
+ \\
+ ;
+ if (std.mem.count(u8, budgeted, group_anchor) != 1) @panic("mvzr's matchGroup changed: redo its group backtracking in build.zig");
+ const patched = std.mem.replaceOwned(u8, b.allocator, budgeted, group_anchor, group_backoff) catch @panic("OOM");
break :mvzr b.createModule(.{ .root_source_file = b.addWriteFiles().add("mvzr.zig", patched), .target = target, .optimize = optimize });
};
diff --git a/docs/config.md b/docs/config.md
index a9db1467..40d0fb24 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -120,7 +120,13 @@ document in a column of its own. See [where new panes go](tags.md#where-new-pane
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.
+placeholder giving its rows to the document. Bare `BootShell` flips it, as
+bare `Placement` does: a setting that chooses among words (`on`/`off`
+switches, `Placement`, `BootShell`, `Crt`, `Lift`, `Motion`,
+`ShaderAnimation`) steps to its
+next value when written bare, as its word clicked in a tag does, and a value
+it does not take is refused with the values it takes, which `/commands` also
+lists.
`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
@@ -545,6 +551,8 @@ Lift auto a shadow on a light page; on a dark one the others recede
Lift off
InactiveDim 30 the unfocused panes' text fades 30% toward its ground
Motion smooth off, crisp, smooth (the default), bouncy or playful
+CursorBlink on the cursor blinks, solid while typing and half a second
+ after, eased at each edge, solid after 10 idle seconds
GripWidth 150 the grip's button and the scrollbar under it, percent of the
theme's rail_px (12px at a 17px tagline; 50 to 300)
```
@@ -631,6 +639,13 @@ uses the argument as a path as before. Absolute paths and argument-free Restore
retain their existing behavior. `Dump` still honors `$PARDES_DUMP` and otherwise
writes a timestamped file in the default directory.
+A dump keeps each text pane's dot (its selection, or the caret), as acme's
+keeps a window's q0 and q1, and a Restore puts it back where the kept view
+shows it. It keeps the settings that differ from a fresh session's (`Placement
+pardes`, `Verbose off`, a shader), as the root ctl reads them, and a Restore
+sets them again; the theme is kept with the layout, and the font stays the
+frontend's. REPL bindings are not kept.
+
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,
diff --git a/docs/design.typ b/docs/design.typ
index a6bf56a3..639884a9 100644
--- a/docs/design.typ
+++ b/docs/design.typ
@@ -1960,7 +1960,7 @@ p/l/a`). Topbar:
`Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill` —
editable by left click, with keyboard command navigation and Exec/Look gestures.
An additional editable tag in each column supplies local New, Tty, Find,
-Grep and Joincol commands. ColumnTags hides that row when space is tight;
+Grep and Joincol commands; it is always shown, as acme's column tag is.
Colors and Crt left it for their leader paths.
*Panes.* Terminal: ghostty-vt, 16 MiB scrollback, OSC 133 prompt semantics,
diff --git a/docs/effects.md b/docs/effects.md
index 3f375b7d..08fd3d88 100644
--- a/docs/effects.md
+++ b/docs/effects.md
@@ -23,6 +23,7 @@ why. The verdict is the user's.
| Crt (bundled post pass) | `Crt 0..3` | kept, rewritten (no barrel) | fx-compare stills, live window | kept at the user's live look |
| Ripple, Glitch | — | removed | live window | dropped by the user after a live look |
| G1 lift | `Lift shadow\|rim\|auto` (off), `InactiveDim <percent>` | opt-in until the focus-lift default is decided | lift-shots-2 (off, shadow, rim, auto on acme, dusk, forge); frame series + mp4 per Motion flavour | shadow on acme: keep as is. Round 1 dropped glow and surface (surface lowered the focused text's contrast) and made rim a hairline just above the focused tag (light on a dark page, shade on a light one). auto on a dark page recedes the others (InactiveDim 30) |
+| G3 cursor | follows `Motion`; `CursorBlink on\|off` (on) | glide on by default through the default flavour (smooth); blink on (decision 6) | .scratch/render/cursor/cursor-flavours-grid.mp4 (and quarter speed), raw/<flavour>/<motion>/ | pending |
## Motion flavours
@@ -64,9 +65,13 @@ Which principles each motion uses:
spring over the move's length; anticipation (playful). A pane past its mark
never takes a neighbour's click, a rising one stays in its box, and a notice
past its row stays in its pane's body.
-- **Cursor** (G3, next): designed around the same set: glide on the spring,
- stretch along the path, an arc on long jumps, the trailing corners as the
- secondary action.
+- **Cursor** (G3): timing (the flavour's spring, 2.4x its pace: it follows
+ input), follow-through (bouncy and playful pass the target), squash and
+ stretch and secondary action (the trailing corners lag by the flavour's
+ follow, so a jump smears and collapses), arcs (a jump over 8 cells bows by
+ the flavour's arc), anticipation (playful's wind-up). A step of a cell or
+ less, or any move in insert mode, lands at once, so typing never trails;
+ a scroll carries the cursor with the text. Only the focused cursor moves.
Notes on G1: the lift and the dim run on one focus spring per pane, at the
diff --git a/docs/fs.md b/docs/fs.md
index 27a36248..d2f35505 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -92,7 +92,7 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
```
/README this guide, also src/fs-help.txt
-/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name
+/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name, column serial
/status pid, version and pane count
/look write a line: a right click on it at the active pane; read: the serials the last
look, exec or ctl write touched (made, else acted at)
@@ -102,16 +102,44 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC.
write follow to that open to wait for more
/screen rendered screen JSON; frozen per open handle
/listeners the session's dial addresses
-/focus the serial of the pane with the keyboard; write a serial to give it the keyboard
+/focus the serial of the pane with the keyboard; write a serial to give it the keyboard,
+ which makes its column the active one as a click there would
/ctl the settings, one a line as a write takes them; write a setting or a session builtin
-/commands every builtin: word, `arg` if it takes one, and `root` or `pane`, the ctl that takes it
-/pane/new open it to make a pane; the read answers that pane's serial
+/commands every builtin: word, `arg` if it takes one, `root` or `pane` (the ctl that takes it), a setting's values
+/layout one line per column, left to right: serial index x width current|notcurrent
+ empty|full pane-serials...; then active <serial>: the active column, where
+ pane/new and a look place a pane next (- when there is none)
+/tag the workspace tag; > replaces it, >> appends, one line
+/col/<n>/tag the tag of the column with serial n, the same way
+/col/<n>/ctl write Delcol, Joincol, New or Tty: each acts on that column, as from its tag
+/col/<n>/exec write a word: a middle click on it in that column's tag; rmdir col/<n> closes
+ an empty column (a column with panes is refused, ENOTEMPTY)
+/pane/new open it to make a pane; the read answers that pane's serial. A session holds
+ 64 panes (16 on the board); at that, every route that would open one -- this
+ open, look, exec, New, Tty -- fails with `no pane slots: 64 max` and an err
+ record, and look reads back empty
/pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll
errors event look exec, plus pty/{ctl,status,data} on terminals
/os/ the host filesystem
/src/ the editor's embedded sources, only when built with -Dembed-sources=true
```
+`/layout`, `/tag` and `/col` go past acme, which serves no column files --
+its columns are only where a window sits. They are here so a script can see
+where the panes are (`/index`'s last word is each one's column serial) and
+edit the tags a person clicks in: a column tag is one line, a newline
+written into it a space, and a truncating write (`echo Make > col/3/tag`)
+clears it and drops the newline that ends it, as a pane tag's does. A
+column is named by its serial, as a pane is: it stays while the column
+lives, whatever opens or closes beside it, and is never reused; /layout
+gives each column's serial and its index left to right, and the log says
+`newcol <serial>` and `delcol <serial>` as columns come and go, and after a
+Restore `restoredcol <old> <new>` for each column as `restored` does for
+panes. Joincol folds a column into the one on its right, which keeps its own
+serial and tag; the joined one's serial is gone (`delcol`). The root ctl
+takes no column word: its `Delcol` is refused, pointing at
+`col/<serial>/ctl`.
+
Control messages are split by what they act on, as acme keeps window verbs
on a window's ctl and webfs and upas/fs keep session settings on a root ctl.
Each builtin declares its scope in src/builtins.zig (`scope = .session`;
@@ -119,9 +147,11 @@ every setting is one, the rest act on a pane). `/ctl` takes the session's
builtins, one a line, at whichever pane has the keyboard as each runs --
`Newcol`, `Dump`, `Mount name dial`, `Theme ink`, `Verbose off`; `Exit`,
which quits the editor as acme's does (it refuses once, naming each pane with
-unsaved text, `<name>: Modified (Exit again to discard)`, and a second
-`Exit` with nothing edited since quits, throwing that text away; a scratch
-under 100 bytes is not asked about; `Restore`, which replaces every pane,
+unsaved text, a `+New` scratch of 100 bytes or more too, in one line,
+`<name>, <name>: Modified (Exit again to discard)`, and a second `Exit`
+with nothing edited since quits, throwing that text away; a scratch or a
+command's output under 100 bytes is not asked about, as acme's winclean
+asks about no small unnamed window; `Restore`, which replaces every pane,
asks the same first -- `Dump` writes `pardes-<date>-<time>.zon` (UTC) in
`DumpDir` (default `$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`)
and logs `dump <path>`, and `Restore` with no path takes the last one; a
@@ -151,14 +181,18 @@ 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, 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 --
+foreground job, after which what the rest of the line does is the shell's
+affair: of `sleep 30; echo done` typed at a prompt, fish goes on and runs
+the `echo`, and bash abandons the line, as each does when a job it waits
+on is killed --
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
+takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir
+<the directory in effect>`, `LocationsConfig ...`), so writing what it reads
+back changes nothing; a setting the frontend cannot show (`Lift`,
+`GripWidth` on a terminal) is refused as `Lift is GUI-only`; 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, `Undo` and
+a pane (`Del`, or `Del k`/`Del j` to give its rows to the pane above or
+below, `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`
@@ -193,8 +227,13 @@ performed what it asked for (a save written, a shell started). A click on
the same word, or the word written to `exec`, still opens its prompt.
`/commands` lists every builtin the registry holds, in registry order, one
-a line: its word, `arg` when it takes one, and `root` or `pane` for the ctl
-that takes it, e.g. `Newcol root`, `Save arg pane`, `Verbose arg root`. It is
+a line: its word, `arg` when it takes one, `root` or `pane` for the ctl
+that takes it, and for a setting that chooses among words those words,
+comma-joined, e.g. `Newcol root`, `Save arg pane`, `Verbose arg root on,off`,
+`Placement arg root acme,pardes`. Such a setting written bare steps to its
+next value, so a two-valued one flips (`Crt`, `Placement`, `BootShell`), as
+its word clicked in a tag does; a value it does not take is refused with
+`bad value in control message; takes ...` naming those it does. It is
generated from the registry, so it is always this build's own list.
`/focus` reads the serial of the pane with the keyboard, and a serial written
@@ -307,13 +346,17 @@ terminal's while a program other than its shell holds the tty -- after
Ctrl-D the shell would run the text, so nothing is sent and the pane says
so. Still commands, whatever is bound: a word in a tag (so
the tag is how to run `make` from that file), `Exec <text>` run by name
-(typed, a 2-1 chord onto `Exec`, a `ctl` line) and a command word @`cmd` in
+(typed, a 2-1 chord onto `Exec`, a `ctl` line) -- in a `.py` pane bound to
+a REPL, `Exec print(1)` runs `print(1)` as a command, never in the REPL --
+and a command word @`cmd` in
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`, 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
+`pty/data`, multi-line code as a bracketed paste (`\e[200~<code>\e[201~`,
+then `\r` in a write of its own once the REPL has echoed the paste --
+Python 3.13's REPL takes an Enter read with the paste as part of it, even a
+one-line one -- and a second `\r` when its last line is indented: one Enter
+leaves a block open, pasted or typed; a middle click does all of this
+itself, holding the Enter until the REPL answers the paste), 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.
@@ -344,7 +387,11 @@ 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
-directory. `body` appends on write and replaces on truncating open. `sel`
+directory. `body` appends on write and replaces on truncating open. A
+terminal's `body` is its history as plain text, frozen per open, in logical
+lines: a row the terminal wrapped is joined back to the row before it (the
+wrap is ghostty's, as `pty/run`'s output unwraps), and the last line ends
+with a newline. `sel`
reads the selected text and writing it replaces the selection. `errors`
appends to the directory's `+Errors` pane. Holding `event` open redirects the
pane's Look and Exec clicks to that client, and so does a line written to the
@@ -372,7 +419,9 @@ 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),
+taking it back as Esc does (a choice the question does not offer is refused,
+naming those it does, and the question stands; 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
@@ -496,9 +545,13 @@ 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, 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
+you edit it, so `echo Make > tag` leaves only `Make` -- a truncating write
+drops the one newline that ends what it wrote, which would draw an empty
+row, and keeps any other; append with `>>` to
+keep them, with `printf ' Make' >> tag`. The leading blank is needed: the
+tag reads back with no blank after its last word, so `printf Make >> tag`
+glues `Make` onto it; and `echo ' Make' >> tag` ends 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.
@@ -515,16 +568,21 @@ entries carry no sizes, and neither does `/screen`, which has no length until
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,
+`new`, `del`, `rename` (a terminal's too, as its shell changes directory --
+a new terminal is `new N /` until its shell says where it is, then renamed;
+it runs `ls` once at its first prompt, a greeting that shows the directory --
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, then `restored <old> <new>` for each pane,
-mapping the serial it had to the one it has now, and `msg <serial|-> <text>`
+mapping the serial it had to the one it has now, and `restoredcol <old>
+<new>` for each column, 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|->
+includes each builtin announcing itself as it runs, on purpose: the log says
+which ran; its serial is the pane it ran at, `-` when the keyboard was on a
+column or workspace tag), and `err <serial|->
<file>: <why>` for every write or truncation the tree refused or that
failed -- through a mount a shell sees only the errno its kernel mapped the
reply to, usually `Invalid argument`, and this is the reason (`err 3 addr:
@@ -547,7 +605,8 @@ 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. 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,
+action's case says where: `x`/`l` a click executed or looked at in the tag
+(its offsets count the tag's whole text, path included, as `tag` reads),
`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
diff --git a/docs/mvzr-group-backtrack.md b/docs/mvzr-group-backtrack.md
new file mode 100644
index 00000000..d074af62
--- /dev/null
+++ b/docs/mvzr-group-backtrack.md
@@ -0,0 +1,64 @@
+# mvzr: a repeat inside a group never gives back to what follows the group
+
+A report for upstream (https://github.com/mnemnion/mvzr), against trunk
+3efcfe3 (0.3.9). pardes patches its fetched copy at build time (build.zig)
+until it is fixed there.
+
+## What happens
+
+```zig
+const mvzr = @import("mvzr");
+// a+ab matches aab; the same with the repeat in a group does not
+std.debug.assert(mvzr.compile("a+ab").?.match("aab") != null);
+std.debug.assert(mvzr.compile("(a+)ab").?.match("aab") == null); // expected a match
+```
+
+The same for `(a*)ab` on `aab`, `(.+)_area` and `([a-z_]+)_area` on
+`the_total_area`. `\w+_area` and `(\w)+_area` match, as does
+`([a-z]+)_area` (whose class cannot eat the `_`, so no giving back is
+needed).
+
+## Why
+
+`hasAlt` returns true when its scan reaches the group's own closing
+`.right` at depth 0:
+
+```zig
+.right => {
+ if (pump == 0)
+ return true // the group's own `)`, not an alternative
+ else
+ pump -= 1;
+},
+```
+
+so every group with more pattern after it goes down `matchGroup`'s
+alternatives branch. That branch takes each alternative's single match (a
+greedy repeat's longest) and, when the pattern after the group fails from
+there, moves on to the next alternative; it never tries a shorter match of
+the same alternative. The alt-free branch has the same shape.
+
+## A fix
+
+Where the pattern after the group fails from an alternative's match, try
+that alternative's shorter matches, longest first, before the next
+alternative. pardes's patch, in `matchGroup`'s loop:
+
+```zig
+} else { // Try our next pattern, if any.
+ var e = m1.i;
+ while (e > i) {
+ e -= 1;
+ const shorter = matchAlt(remaining_patt, sets, haystack[0..e], i) orelse continue;
+ if (shorter.i != e) continue;
+ if (matchPattern(next_patt, sets, haystack, e)) |whole| return whole;
+ }
+ remaining_patt = m1.j;
+}
+```
+
+Matching the alternative against `haystack[0..e]` makes `$` and `\b` see a
+string end at `e`, so a group ending in either can accept a cut the full
+string would not; a continuation-passing matcher (each repeat trying the
+rest of the whole pattern) would be the full fix. `hasAlt` returning false
+at the group's own `)` is worth fixing too.
diff --git a/docs/tags.md b/docs/tags.md
index aa680c48..bfca4f0e 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -30,6 +30,15 @@ edge will land, beside the rule as a border drag's rail does. Only columns
between the old and new positions shift. Each column keeps its width, panes and
command text.
+Two panes in a column are resized as acme's windows are, by the grip: drag
+a pane's grip up or down its own column and its top follows, the pane above
+taking or giving the rows, either one down to its tag alone (acme's
+coldragwin keeps a window its tag line); drop it in another column and the
+pane moves there. In the GUI the 2 px rule between two panes also drags
+their seam. No row of text is a handle: a pane's last row takes a click like
+any other, and a terminal, which draws no rule, resizes by the grip. A
+column's right edge still drags its width.
+
When Look opens the first document and its source column is too narrow for
a new column, it splits below the originating pane, just like `Tty`, instead
of inserting at the top of the leftmost column. The originating terminal is
@@ -53,14 +62,23 @@ 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.
+sequence with a warning, pardes refuses the Edit. In `s`, `&` in the
+replacement is the matched text (`\&` a plain `&`); in `c`, `a` and `i` an
+`&` is only an `&`, as in sam. A pattern that finds nothing says so with the
+pattern (`no match for regexp /nomatch/`), and an `s` on an empty dot says
+`no substitution: dot is empty`.
`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
+column keeps its heights. A pane with unsaved text is refused once, as
+acme's Del warns (exec.c del, wind.c winclean): `<name>: Modified (Del again
+to discard)`, and the same `Del` again, nothing edited since, closes it and
+throws the text away; `Delcol` refuses a column holding such a pane the same
+way (exec.c delcol). Each word warns on its own, from a key, a tag, `exec`
+or a ctl, where the refusal fails the write with EIO. `Del k` (or `DelAbove`) gives them to the nearest
expanded pane above, `Del j` (or `DelBelow`) to the one below, each falling
back to the other side. Bare `Del` from the keyboard (`SPC d`, or Enter on
the word) on a pane with expanded panes both above and below asks on the
@@ -74,8 +92,10 @@ 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. 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
+line, up to eight, and leaves its body at least one row. A line exactly as
+wide as the pane stays one row, its caret at the tag's edge past the last
+character, as acme's tick is (in a terminal, on the last cell). 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
@@ -183,7 +203,10 @@ 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
+not in the tag: the tag names the file and nothing more. In a terminal the
+grip is two cells: the second is `*` while the pane holds unsaved text, and
+the first is the pane's mode: blank in normal mode, `^` in insert, `$` when
+the keyboard goes to a terminal's program (tty mode). The `dirty` file
and `index`'s flag say the same to a script.
## Empty columns
diff --git a/src/File.zig b/src/File.zig
index 929e33bf..6a1ac319 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -814,6 +814,14 @@ pub fn restore(p: *Pardes, id: usize, src: dump.Pane) !*Pane {
pane.file.?.tree_context = saved.tree_context and supportsContext(pane);
pane.body.cur_pinned = true;
pane.body.cur_row = @intCast(src.scroll);
+ // Its dot as it was, where the view the dump kept shows it.
+ if (saved.dot) |dot| if (dot[0] <= dot[1] and dot[1] <= content.len) {
+ const a = modal.positionAt(content, dot[0]);
+ const h = modal.positionAt(content, if (dot[1] > dot[0]) modal.prevGrapheme(content, dot[1]) else dot[0]);
+ pane.body.vsel = .{ .active = dot[1] > dot[0], .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true };
+ pane.body.cur_row = @intCast(h.row);
+ pane.body.cur_col = @intCast(h.col);
+ };
pane.cols = @max(1, src.cols);
pane.rows = @max(1, src.rows);
if (output == null and filesystem.localPath(path) != null) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = true, .mode = .baseline_disk } });
diff --git a/src/Messages.zig b/src/Messages.zig
index 8b6c795d..7d284e6b 100644
--- a/src/Messages.zig
+++ b/src/Messages.zig
@@ -445,10 +445,22 @@ pub fn messageLog(m: *const Messages, i: usize) ?*const LoggedMessage {
}
/// `<operation>: <the error in words>`: FileNotFound reads "file not found".
+/// What a route that would open a pane says when every slot is taken.
+pub const no_pane_slots = "no pane slots: {d} max";
+
pub fn reportError(p: *Pardes, id: usize, operation: []const u8, err: anyerror) void {
var buf: [256]u8 = undefined;
var w = std.Io.Writer.fixed(&buf);
w.print("{s}: ", .{operation}) catch {};
+ // Every route that opens a pane says the cap the same way.
+ if (err == error.NoPaneSlots) {
+ w.print(no_pane_slots, .{pardes.MAX_PANES}) catch {};
+ // A 9P look or exec that would open the pane fails with it too.
+ p.fs.no_pane_slot = true;
+ @memcpy(p.fs.ename[0..w.buffered().len], w.buffered());
+ p.fs.no_pane_slot_len = @intCast(w.buffered().len);
+ return reportFailure(p, id, w.buffered());
+ }
for (@errorName(err), 0..) |c, i| {
if (std.ascii.isUpper(c) and i > 0) w.writeByte(' ') catch break;
w.writeByte(std.ascii.toLower(c)) catch break;
diff --git a/src/Terminal.zig b/src/Terminal.zig
index 76be29d0..4808298e 100644
--- a/src/Terminal.zig
+++ b/src/Terminal.zig
@@ -385,26 +385,66 @@ pub fn mouseFormatSgr(pane: *const Pane) bool {
return state.vt.modes.get(.mouse_format_sgr);
}
-/// The whole scrollback as plain text, `gpa`-owned: what `Save` writes out.
+/// The whole scrollback as plain text, `gpa`-owned: what a terminal's body
+/// reads and `Save` writes out. Logical lines: rows the terminal wrapped
+/// are joined again (ghostty's wrap flags, as pty/run's output is), and
+/// the last line ends with a newline.
pub fn screenTextAlloc(pane: *Pane, gpa: std.mem.Allocator) ![]const u8 {
if (comptime !enabled) return &.{};
- const state = pane.terminal orelse return &.{};
- return state.vt.screens.active.dumpStringAlloc(gpa, .{ .screen = .{} });
+ var out: std.Io.Writer.Allocating = .init(gpa);
+ errdefer out.deinit();
+ try writeScreenText(pane, &out.writer);
+ return out.toOwnedSlice();
}
+fn writeScreenText(pane: *Pane, w: *std.Io.Writer) !void {
+ if (comptime !enabled) return;
+ const state = pane.terminal orelse return;
+ const screen = state.vt.screens.active;
+ const br = screen.pages.getBottomRight(.screen) orelse return;
+ var both: TeeWriter = .{ .a = w };
+ try screen.dumpString(&both.writer, .{ .tl = screen.pages.getTopLeft(.screen), .br = br, .unwrap = true });
+ if (both.count > 0 and both.final != '\n') try w.writeByte('\n');
+}
+
+/// Passes bytes through, remembering how many and the last one.
+const TeeWriter = struct {
+ a: *std.Io.Writer,
+ count: usize = 0,
+ final: u8 = 0,
+ writer: std.Io.Writer = .{ .vtable = &.{ .drain = drain }, .buffer = &.{} },
+
+ fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
+ const t: *TeeWriter = @alignCast(@fieldParentPtr("writer", w));
+ var n: usize = 0;
+ for (data[0 .. data.len - 1]) |bytes| {
+ try t.a.writeAll(bytes);
+ t.note(bytes);
+ n += bytes.len;
+ }
+ const pattern = data[data.len - 1];
+ for (0..splat) |_| {
+ try t.a.writeAll(pattern);
+ t.note(pattern);
+ n += pattern.len;
+ }
+ return n;
+ }
+
+ fn note(t: *TeeWriter, bytes: []const u8) void {
+ if (bytes.len == 0) return;
+ t.count += bytes.len;
+ t.final = bytes[bytes.len - 1];
+ }
+};
+
/// How long `screenTextAlloc` would be, counted without keeping it: the
/// length a terminal's body reads as, which its ctl line reports.
pub fn screenTextLen(pane: *Pane) usize {
if (comptime !enabled) return 0;
- const state = pane.terminal orelse return 0;
- const screen = state.vt.screens.active;
var buf: [256]u8 = undefined;
var counting: std.Io.Writer.Discarding = .init(&buf);
- screen.dumpString(&counting.writer, .{
- .tl = screen.pages.getTopLeft(.screen),
- .br = screen.pages.getBottomRight(.screen) orelse return 0,
- .unwrap = false,
- }) catch return 0;
+ writeScreenText(pane, &counting.writer) catch return 0;
return @intCast(counting.fullCount());
}
@@ -2505,6 +2545,8 @@ test "a prompt row hidden end to end paints nothing at all" {
defer p.deinit();
while (p.nextEffect()) |_| {}
const pane = p.panes[0].?;
+ pane.tag_expanded = false; // a 12x8 screen: the body, not the tag
+ p.sync();
pane.tty_filter = false;
pane.body.mode = .normal;
p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;A\x1b\\\x1b[32maaaaaaaaa\x1b]133;B\x1b\\\x1b[41;36m\u{754C}\x1b[0m\r\n" } });
diff --git a/src/animation.zig b/src/animation.zig
index 9b43644e..9002a2c6 100644
--- a/src/animation.zig
+++ b/src/animation.zig
@@ -870,6 +870,207 @@ test "a flavour's stretched pane lands exactly on its box, and a click finds the
}
}
+/// The focused cursor's quad (docs/render-pipeline.md §5.4), in grid cells:
+/// four corners, each on its own springs, so a jump smears along its path
+/// (the leading corners stiffer than the trailing ones, Motion.follow) and
+/// collapses at rest; a long jump bows (Motion.arc). A move of a cell or
+/// less, or any the caller calls a snap, lands at once.
+pub const CursorGlide = struct {
+ /// Top left, top right, bottom right, bottom left; x then y.
+ corners: [4][2]Spring = @splat(@splat(.{})),
+ target: Box = .{},
+ from_centre: [2]f32 = .{ 0, 0 },
+ placed: bool = false,
+
+ /// How much faster than the flavour's own pace a cursor moves: it
+ /// follows input (§8.1: settles in 90-150 ms on the default).
+ pub const pace: f32 = 2.4;
+ /// A jump longer than this, in cells, bows.
+ pub const arc_from: f32 = 8;
+
+ fn cornersOf(box: Box) [4][2]f32 {
+ return .{ .{ box.x, box.y }, .{ box.x + box.w, box.y }, .{ box.x + box.w, box.y + box.h }, .{ box.x, box.y + box.h } };
+ }
+
+ fn centre(box: Box) [2]f32 {
+ return .{ box.x + box.w / 2, box.y + box.h / 2 };
+ }
+
+ /// Heads for `box` at `now_ns`.
+ pub fn aim(glide: *CursorGlide, box: Box, now_ns: u64, motion: Motion, snap: bool) void {
+ // The same mark: nothing to do, unless a snap lands a glide under way.
+ if (glide.placed and std.meta.eql(box, glide.target) and !(snap and glide.moving())) return;
+ const to = cornersOf(box);
+ const from_c = if (glide.placed) glide.centreAt(now_ns) else centre(box);
+ const to_c = centre(box);
+ const dx = to_c[0] - from_c[0];
+ const dy = to_c[1] - from_c[1];
+ const jump = @abs(dx) > 1.01 or @abs(dy) > 1.01;
+ const land = !glide.placed or snap or motion.instant or !jump;
+ glide.from_centre = from_c;
+ glide.target = box;
+ glide.placed = true;
+ for (&glide.corners, to) |*corner, point| {
+ // Leading: the corner ahead of the move, on the side it goes.
+ const ahead = (point[0] - to_c[0]) * dx + (point[1] - to_c[1]) * dy;
+ var own = motion;
+ own.omega *= pace * (if (ahead > 0) 1 else motion.follow);
+ if (land) {
+ for (corner, point) |*axis, value| axis.* = .{ .omega = own.omega, .zeta = own.zeta, .target = value, .from = value };
+ continue;
+ }
+ for (corner, point) |*axis, value| axis.retarget(value, now_ns, own);
+ }
+ }
+
+ fn centreAt(glide: *const CursorGlide, now_ns: u64) [2]f32 {
+ var c: [2]f32 = .{ 0, 0 };
+ for (glide.corners) |corner| for (&c, corner) |*sum, axis| {
+ sum.* += axis.value(now_ns) / 4;
+ };
+ return c;
+ }
+
+ /// Text under the cursor moved by (dx, dy) cells (a scroll): the quad
+ /// moves with it, rather than gliding against text that jumped.
+ pub fn shift(glide: *CursorGlide, dx: f32, dy: f32) void {
+ for (&glide.corners) |*corner| for (corner, [2]f32{ dx, dy }) |*axis, d| {
+ axis.from += d;
+ axis.target += d;
+ };
+ glide.from_centre = .{ glide.from_centre[0] + dx, glide.from_centre[1] + dy };
+ glide.target.x += dx;
+ glide.target.y += dy;
+ }
+
+ /// The quad at `now_ns`, bowed on a long jump by the flavour's arc.
+ pub fn sample(glide: *const CursorGlide, now_ns: u64, motion: Motion) [4][2]f32 {
+ var out: [4][2]f32 = undefined;
+ for (&out, glide.corners) |*point, corner| for (point, corner) |*value, axis| {
+ value.* = axis.value(now_ns);
+ };
+ const to_c = centre(glide.target);
+ const dx = to_c[0] - glide.from_centre[0];
+ const dy = to_c[1] - glide.from_centre[1];
+ const length = @sqrt(dx * dx + dy * dy);
+ if (motion.arc == 0 or length < arc_from) return out;
+ const now_c = glide.centreAt(now_ns);
+ const left = @sqrt((to_c[0] - now_c[0]) * (to_c[0] - now_c[0]) + (to_c[1] - now_c[1]) * (to_c[1] - now_c[1]));
+ const travelled = std.math.clamp(1 - left / length, 0, 1);
+ const bow = motion.arc * length * 4 * travelled * (1 - travelled);
+ // Perpendicular to the path, to its left.
+ for (&out) |*point| {
+ point[0] += -dy / length * bow;
+ point[1] += dx / length * bow;
+ }
+ return out;
+ }
+
+ /// Steps the springs to `now_ns`; true while the quad still moves.
+ pub fn step(glide: *CursorGlide, now_ns: u64) bool {
+ var any = false;
+ for (&glide.corners) |*corner| for (corner) |*axis| {
+ if (axis.step(now_ns)) any = true;
+ };
+ return any;
+ }
+
+ /// How far, in cells, the furthest corner still is from its mark.
+ pub fn remaining(glide: *const CursorGlide, now_ns: u64) f32 {
+ var most: f32 = 0;
+ for (glide.corners) |corner| for (corner) |axis| {
+ most = @max(most, @abs(axis.value(now_ns) - axis.target));
+ };
+ return most;
+ }
+
+ pub fn moving(glide: *const CursorGlide) bool {
+ for (glide.corners) |corner| for (corner) |axis| {
+ if (!axis.settled) return true;
+ };
+ return false;
+ }
+};
+
+/// The cursor's opacity under blink (§5.4): solid while typing and for
+/// `hold_ns` after, then on and off with an eased edge, solid again after
+/// `idle_ns` of no input. `edge` says whether it is mid-edge (a frame is
+/// owed), `next_ns` when the next edge starts (a wake).
+pub const Blink = struct {
+ pub const hold_ns: u64 = 500 * std.time.ns_per_ms;
+ pub const idle_ns: u64 = 10 * std.time.ns_per_s;
+ pub const half_ns: u64 = 530 * std.time.ns_per_ms;
+ pub const edge_ns: u64 = 80 * std.time.ns_per_ms;
+
+ alpha: f32,
+ edge: bool,
+ next_ns: ?u64,
+
+ pub fn at(now_ns: u64, input_ns: u64) Blink {
+ const since = now_ns -| input_ns;
+ if (since < hold_ns) return .{ .alpha = 1, .edge = false, .next_ns = input_ns + hold_ns + half_ns - edge_ns };
+ if (since >= hold_ns + idle_ns) return .{ .alpha = 1, .edge = false, .next_ns = null };
+ const t = (since - hold_ns) % (2 * half_ns);
+ const cycle_start = now_ns - t;
+ // On for a half, easing out over its last `edge_ns`; off for a half,
+ // easing back in over its last.
+ if (t < half_ns - edge_ns) return .{ .alpha = 1, .edge = false, .next_ns = cycle_start + half_ns - edge_ns };
+ if (t < half_ns) return .{ .alpha = 1 - smooth(@as(f32, @floatFromInt(t - (half_ns - edge_ns))) / @as(f32, @floatFromInt(edge_ns))), .edge = true, .next_ns = null };
+ if (t < 2 * half_ns - edge_ns) return .{ .alpha = 0, .edge = false, .next_ns = cycle_start + 2 * half_ns - edge_ns };
+ return .{ .alpha = smooth(@as(f32, @floatFromInt(t - (2 * half_ns - edge_ns))) / @as(f32, @floatFromInt(edge_ns))), .edge = true, .next_ns = null };
+ }
+
+ fn smooth(x: f32) f32 {
+ const u = std.math.clamp(x, 0, 1);
+ return u * u * (3 - 2 * u);
+ }
+};
+
+test "a cursor snaps a cell at a time and glides a jump, smearing, bowing and landing exactly" {
+ const ms = std.time.ns_per_ms;
+ const smooth_motion = Motion.of(.smooth);
+ var glide: CursorGlide = .{};
+ const cell: Box = .{ .x = 3, .y = 4, .w = 1, .h = 1 };
+ glide.aim(cell, 0, smooth_motion, false);
+ try std.testing.expect(!glide.moving());
+ // One cell over: lands at once.
+ glide.aim(.{ .x = 4, .y = 4, .w = 1, .h = 1 }, ms, smooth_motion, false);
+ try std.testing.expect(!glide.moving());
+ // A jump of 30 cells: glides, the leading edge ahead of the trailing one.
+ const far: Box = .{ .x = 34, .y = 4, .w = 1, .h = 1 };
+ glide.aim(far, 2 * ms, smooth_motion, false);
+ try std.testing.expect(glide.moving());
+ const mid = glide.sample(40 * ms, smooth_motion);
+ const width = mid[1][0] - mid[0][0];
+ try std.testing.expect(width > 1.5);
+ // Bowed off the straight line on a long jump.
+ try std.testing.expect(@abs(mid[0][1] - 4) > 0.05);
+ var t: u64 = 2 * ms;
+ while (glide.step(t)) t += ms;
+ // Arrives within §8.1's 90-150 ms on the default, within a twentieth of
+ // a cell (under a pixel); settles to rest just after, landing exactly.
+ const arrived = glide.sample(152 * ms, smooth_motion);
+ try std.testing.expect(@abs(arrived[3][0] - 34) < 0.05 and @abs(arrived[1][0] - 35) < 0.05);
+ try std.testing.expect(t <= 220 * ms);
+ const rest = glide.sample(t, smooth_motion);
+ try std.testing.expectEqual([2]f32{ 34, 4 }, rest[0]);
+ try std.testing.expectEqual([2]f32{ 35, 5 }, rest[2]);
+ // A scroll carries it with the text.
+ glide.shift(0, -3);
+ try std.testing.expectEqual([2]f32{ 34, 1 }, glide.sample(t, smooth_motion)[0]);
+}
+
+test "a blink holds while typing, eases at its edges and stops when idle" {
+ const ms = std.time.ns_per_ms;
+ try std.testing.expectEqual(@as(f32, 1), Blink.at(100 * ms, 0).alpha);
+ const out = Blink.at(Blink.hold_ns + Blink.half_ns + 100 * ms, 0);
+ try std.testing.expectEqual(@as(f32, 0), out.alpha);
+ const edge = Blink.at(Blink.hold_ns + Blink.half_ns - Blink.edge_ns / 2, 0);
+ try std.testing.expect(edge.edge and edge.alpha > 0.2 and edge.alpha < 0.8);
+ try std.testing.expectEqual(@as(?u64, null), Blink.at(Blink.hold_ns + Blink.idle_ns + ms, 0).next_ns);
+ try std.testing.expectEqual(@as(f32, 1), Blink.at(Blink.hold_ns + Blink.idle_ns + ms, 0).alpha);
+}
+
/// A displayed value that fades from one target to the next over
/// `transition_steps` frames; the chrome colours are one.
pub fn Fade(comptime Value: type) type {
diff --git a/src/builtins.zig b/src/builtins.zig
index 657f49d0..32e3cd84 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -170,9 +170,9 @@ pub const registry = struct {
if (@intFromEnum(b) == i) return @hasDecl(T, "requires_arg") and T.requires_arg;
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, .choice, .lift => false,
- .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .shader_animation, .motion, .inactive_dim, .grip_width => true,
+ // a switch flips bare, a choice steps, and DumpDir bare is the default
+ .toggle, .transition, .scene, .dump_dir, .choice, .lift, .shader_animation, .motion => false,
+ .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .inactive_dim, .grip_width => true,
};
unreachable;
}
@@ -258,7 +258,7 @@ test "Exit over a ctl write names every pane it asks about, and the next Exit di
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)"));
+ try std.testing.expect(std.mem.endsWith(u8, said, ": Modified (Exit again to discard)"));
p.fs.failure_len = 0;
try std.testing.expect(p.executeBuiltinLine(p.active, "Exit"));
try std.testing.expect(p.quit);
@@ -267,7 +267,11 @@ test "Exit over a ctl write names every pane it asks about, and the next Exit di
/// A dump file that exists, for a Restore to get past reading it.
fn testDump(buf: []u8) ![]const u8 {
var tmp = std.testing.tmpDir(.{});
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "d.zon", .data = ".{}" });
+ // A real dump: a Restore reads it through before it asks anything.
+ const source = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer source.deinit();
+ try pardes.dump.dumpState(source);
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "d.zon", .data = source.dump_out.? });
var dir: [4096]u8 = undefined;
const at = dir[0..try tmp.dir.realPath(std.testing.io, &dir)];
return std.fmt.bufPrint(buf, "Restore {s}/d.zon", .{at});
@@ -442,6 +446,14 @@ pub const Exec = struct {
/// the person at the keyboard. A scratch of under 100 bytes is no loss
/// worth the question, as winclean does not ask for one.
fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
+ return warnModifiedIn(c, asking, .all);
+}
+
+/// Which panes a refusal asks about: every one (Exit, Restore), one (Del),
+/// or a column's (Delcol).
+const Asked = union(enum) { all, pane: usize, column: usize };
+
+fn warnModifiedIn(c: Ctx, asking: Pane.Discarding, which: Asked) 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).
@@ -449,9 +461,23 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
var all = std.Io.Writer.fixed(&all_buf);
for (c.p.panes, 0..) |slot, id| {
const pane = slot orelse continue;
+ switch (which) {
+ .all => {},
+ .pane => |only| if (id != only) continue,
+ .column => |col| if ((layout.findPane(c.p, id) orelse continue).col != col) continue,
+ }
const f = pane.file orelse continue;
- if (f.revision == f.saved_revision or !panes.Output.fileTraits(f.output).saves) continue;
+ // A +New scratch saves nowhere, but what is typed in it is as lost;
+ // under 100 bytes it is not asked about, as acme's winclean does not
+ // whine about a small unnamed window, nor about a command's output.
+ const scratch = if (f.output) |o| o.from == .cmd and o.from.cmd == .New else false;
+ if (f.revision == f.saved_revision or !(panes.Output.fileTraits(f.output).saves or scratch)) continue;
if (f.output != null and f.content.len < 100) continue;
+ // Dirty by a rename alone is no text to lose, as get says (ctl.zig).
+ // Nor is an empty buffer that never held a file.
+ if (f.saved_hash) |saved| {
+ if (std.hash.Wyhash.hash(0, f.content) == saved) continue;
+ } else if (f.content.len == 0) continue;
if (pane.discard_warned) |w| if (w.revision == f.revision and w.by == asking) continue;
pane.discard_warned = .{ .revision = f.revision, .by = asking };
var buf: [limits.host_path_cap + 48]u8 = undefined;
@@ -460,7 +486,7 @@ fn warnModified(c: Ctx, asking: Pane.Discarding) bool {
modified = true;
}
if (modified and c.p.fs.no_prompt) {
- all.print(": Modified ({s} again to discard them all)", .{@tagName(asking)}) catch {};
+ all.print(": Modified ({s} again to discard)", .{@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);
@@ -558,7 +584,12 @@ pub const Restore = struct {
};
return c.p.reportFailure(c.id, std.fmt.bufPrint(&buf, "Restore: {s}: {s}", .{ path, why }) catch "Restore: cannot read the dump");
};
- c.p.gpa.free(bytes);
+ defer c.p.gpa.free(bytes);
+ // Nor does a file that is not a dump, before any warning.
+ pardes.dump.check(c.p.gpa, bytes) catch |err| {
+ var buf: [limits.host_path_cap + 64]u8 = undefined;
+ return c.p.reportFailure(c.id, std.fmt.bufPrint(&buf, "Restore: {s}: not a pardes dump ({s})", .{ path, @errorName(err) }) catch "Restore: not a pardes dump");
+ };
}
// acme's Load adds a dump's windows to the ones there; a Restore
// replaces them all, so it asks what Exit asks first.
@@ -835,6 +866,9 @@ pub const Del = struct {
pub const takes_arg = true;
pub fn run(c: Ctx) void {
const side = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
+ // acme's Del asks winclean first (exec.c del): unsaved text is warned
+ // about once, and the same Del again, nothing edited since, closes.
+ if (warnModifiedIn(c, .Del, .{ .pane = c.id })) return;
const at = layout.findPane(c.p, c.id);
const above = if (at) |f| layout.expandedNeighbor(c.p, f.col, f.idx, .above) else null;
const below = if (at) |f| layout.expandedNeighbor(c.p, f.col, f.idx, .below) else null;
@@ -944,6 +978,9 @@ pub const Delcol = struct {
// 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;
+ // acme's Delcol refuses a column with unsaved text (exec.c delcol,
+ // cols.c colclean), once: the next Delcol closes it.
+ if (warnModifiedIn(c, .Delcol, .{ .column = col })) return;
c.p.removeColumn(col) catch |err| c.p.reportError(c.id, "close column", err);
}
};
@@ -1065,7 +1102,7 @@ pub const Joincol = struct {
pub const Tutor = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
- const free = c.p.freeSlot() orelse return;
+ const free = c.p.freeSlot() orelse return c.p.reportError(c.id, "Tutor", error.NoPaneSlots);
_ = c.p.openTutorView(free) catch return;
exec.placeNew(c.p, c.id, c.id, free, .doc); // a doc like any other
}
@@ -1724,3 +1761,39 @@ const Board = struct {
try std.testing.expect(20 != 0x20);
}
};
+
+test "Exit and Restore name a modified +New scratch as well as files" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ const file = try p.setTestFile("saved\n");
+ file.file.?.saved_revision = file.file.?.revision -% 1; // modified
+ p.newScratchBelow(p.active);
+ const scratch = &p.panes[p.active].?.file.?;
+ scratch.content = try p.gpa.realloc(scratch.content, 120);
+ @memset(scratch.content, 'n'); // past acme's 100 bytes, where it whines
+ scratch.revision +%= 1; // typed into, never saved
+ 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, "test.txt") != null);
+ try std.testing.expect(std.mem.indexOf(u8, said, config.scratch_buffer) != null);
+}
+
+test "Del and Delcol refuse a pane with unsaved text once, as acme's do, and close on the second" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("saved\n");
+ pane.file.?.saved_revision = pane.file.?.revision -% 1; // modified
+ const serial = pane.serial;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Delcol"));
+ try std.testing.expect(p.paneBySerial(serial) != null);
+ try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Delcol again to discard)"));
+ // Each word is warned on its own: Del asks too, then closes.
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Del"));
+ try std.testing.expect(p.paneBySerial(serial) != null);
+ try std.testing.expect(std.mem.endsWith(u8, pane.msg[0..pane.msg_len], ": Modified (Del again to discard)"));
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Del"));
+ try std.testing.expect(p.paneBySerial(serial) == null);
+}
diff --git a/src/config.zig b/src/config.zig
index 24c48770..fb38e4dd 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -119,6 +119,7 @@ pub const leader_path = paths: {
if (builtins.capabilities.window_opacity) table.set(.WindowOpacity, null);
if (builtins.capabilities.window_blur) table.set(.WindowBlur, null);
if (builtins.capabilities.ligatures) table.set(.Ligatures, null);
+ if (builtins.capabilities.lift) table.set(.CursorBlink, null);
if (builtins.capabilities.lift) table.set(.Lift, null);
table.set(.Motion, null);
table.set(.InactiveDim, null);
@@ -647,6 +648,10 @@ pub const Runtime = struct {
verbose: bool = true,
/// A message falls into its row and dissolves out of it.
message_animation: bool = true,
+ /// The cursor blinks, a pixel shell's (docs/render-pipeline.md §5.4):
+ /// solid while typing and for half a second after, eased at each edge,
+ /// solid again after ten idle seconds.
+ cursor_blink: bool = true,
/// How long a message stays after the key or click that dismisses it.
message_linger_ms: u16 = 800,
/// How long its ease-in fall into the row takes.
@@ -863,7 +868,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, 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, cursor_blink };
/// The bundled post pass, a Shadertoy file in shaders/post/.
pub const Scene = enum { crt };
@@ -918,6 +923,7 @@ pub const Runtime = struct {
.{ .word = "Verbose", .action = .{ .toggle = .verbose } },
.{ .word = "MessageAnimation", .action = .{ .toggle = .message_animation } },
.{ .word = "Ligatures", .action = .{ .toggle = .ligatures }, .availability = .ligatures },
+ .{ .word = "CursorBlink", .action = .{ .toggle = .cursor_blink }, .availability = .lift },
.{ .word = "Lift", .action = .lift, .availability = .lift },
.{ .word = "Motion", .action = .motion },
.{ .word = "InactiveDim", .action = .inactive_dim },
@@ -990,6 +996,9 @@ pub const Runtime = struct {
.toggle => |field| if (field == .ligatures) {
if (setting.availability != .ligatures)
@compileError("the ligatures toggle must use the ligatures capability");
+ } else if (field == .cursor_blink) {
+ if (setting.availability != .lift)
+ @compileError("CursorBlink must use the lift capability (a pixel shell's)");
} else if (setting.availability != null)
@compileError("unconditional toggles cannot carry a backend capability"),
else => if (setting.availability != null)
@@ -1104,20 +1113,12 @@ pub const Runtime = struct {
}
return state.post.set(null, path, null);
},
- .shader_animation => {
- const text = std.mem.trim(u8, argument orelse return false, " \t\r\n");
- state.shader_animation = std.meta.stringToEnum(ShaderAnimation, text) orelse return false;
- },
+ .shader_animation => state.shader_animation = stepped(ShaderAnimation, state.shader_animation, argument) 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;
+ now.* = stepped(@TypeOf(now.*), now.*, argument) orelse return false;
},
},
.lift => {
@@ -1129,10 +1130,7 @@ pub const Runtime = struct {
else
std.meta.stringToEnum(LiftStyle, text) orelse return false;
},
- .motion => {
- const text = std.mem.trim(u8, argument orelse return false, " \t\r\n");
- state.motion = std.meta.stringToEnum(animation.Motion.Flavour, text) orelse return false;
- },
+ .motion => state.motion = stepped(animation.Motion.Flavour, state.motion, argument) orelse return false,
.inactive_dim => {
const text = std.mem.trim(u8, argument orelse return false, " \t\r\n%");
const percent = std.fmt.parseInt(u8, text, 10) catch return false;
@@ -1150,6 +1148,40 @@ pub const Runtime = struct {
return true;
}
+ /// The values a setting that chooses among words takes, for a refusal to
+ /// name; null for one that takes a number, a path or a name. Bare, each
+ /// of these flips (a two-valued one) or steps to its next value.
+ pub fn choices(action: Action) ?[]const u8 {
+ const words = struct {
+ fn of(comptime T: type) []const u8 {
+ comptime var out: []const u8 = "";
+ inline for (std.meta.fieldNames(T), 0..) |name, i| out = out ++ (if (i > 0) ", " else "") ++ name;
+ return out;
+ }
+ };
+ return switch (action) {
+ .toggle, .transition => "on, off",
+ .choice => |which| switch (which) {
+ inline else => |field| comptime words.of(@FieldType(Runtime, @tagName(field))),
+ },
+ .lift => "on, " ++ comptime words.of(LiftStyle),
+ .motion => comptime words.of(animation.Motion.Flavour),
+ .scene => std.fmt.comptimePrint("on, off, 0-{d}", .{Post.max_level}),
+ .shader_animation => comptime words.of(ShaderAnimation),
+ else => null,
+ };
+ }
+
+ /// The value a choice word names, or bare, the one after `now`; null for
+ /// a word it does not take.
+ fn stepped(comptime T: type, now: T, argument: ?[]const u8) ?T {
+ const text = std.mem.trim(u8, argument orelse "", " \t\r\n");
+ if (text.len > 0) return std.meta.stringToEnum(T, text);
+ const all = std.enums.values(T);
+ const at = std.mem.indexOfScalar(T, all, now) orelse 0;
+ return all[(at + 1) % all.len];
+ }
+
/// What `on`, `off` or nothing after a switch word asks for: null flips
/// it, as a bare tagline word does.
fn switchedTo(argument: ?[]const u8) error{NotOnOrOff}!?bool {
@@ -1618,6 +1650,7 @@ pub const Runtime = struct {
.verbose => "Verbose off\n",
.message_animation => "MessageAnimation off\n",
.ligatures => "Ligatures off\n",
+ .cursor_blink => "CursorBlink off\n",
},
.shell => "Shell requested (new panes) fish\n",
.tagline_size => "TaglineSize 73\n",
diff --git a/src/detached/server.zig b/src/detached/server.zig
index 857591d4..8f45135a 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -569,8 +569,8 @@ pub const Session = struct {
fn writeDump(ctx: ?*anyopaque, bytes: []const u8) void {
const s = of(ctx);
var pbuf: [1024:0]u8 = undefined;
- const path = pardes.dump.outPath(&pbuf, s.core.settings.dump_dir.get()) orelse return;
- filesystem.write(s.core, path, bytes) catch |err| return s.core.reportError(0, "dump", err);
+ const path = pardes.dump.outPath(&pbuf, s.core.settings.dump_dir.get()) orelse return s.core.dumpFailed(s.core.settings.dump_dir.get(), error.NoDumpDirectory);
+ filesystem.write(s.core, path, bytes) catch |err| return s.core.dumpFailed(path, err);
s.core.setLastDump(path);
}
diff --git a/src/detached/wire.zig b/src/detached/wire.zig
index c0e17054..53a1a70d 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -13,7 +13,7 @@ pub const version: u16 = 8;
/// The Chrome palette is carried field by field as its declaration lists
/// them, so any change to it changes the wire: bump `version`, then this.
-const chrome_layout: u64 = 0x7e3d0952317bfee;
+const chrome_layout: u64 = 0xc91196fcdef144e2;
comptime {
@setEvalBranchQuota(10_000);
var hash = std.hash.Fnv1a_64.init();
@@ -286,6 +286,16 @@ pub const Frame = struct {
}
} else if (field.type == u8) {
rgb.* = try r.getByte();
+ } else if (field.type == u32) {
+ rgb.* = try r.getU32();
+ } else if (field.type == f32) {
+ rgb.* = @bitCast(try r.getU32());
+ if (!std.math.isFinite(rgb.*)) return error.BadValue;
+ } else if (field.type == [8]f32) {
+ for (rgb) |*value| {
+ value.* = @bitCast(try r.getU32());
+ if (!std.math.isFinite(value.*)) return error.BadValue;
+ }
} else for (rgb) |*channel| channel.* = try r.getByte();
}
try r.end();
@@ -747,7 +757,7 @@ pub fn layersBound(layers: []const pardes.Layer, tags: []const pardes.Layer, reg
const region_len = 1 + 1 + 4 + 8 + 1 + 2 + 2 + 1 + 2 + 4;
/// The palette: each colour three bytes, a theme's optional ones a flag
/// before them.
-const chrome_len = @typeInfo(pardes.Chrome).@"struct".fields.len * 4;
+const chrome_len = @typeInfo(pardes.Chrome).@"struct".fields.len * 4 + 8 * 4;
pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, pointer_shape: pardes.Surface.PointerShape, cells: []const pardes.Cell, prev: []const pardes.Cell, layers: []const pardes.Layer, tags: []const pardes.Layer, regions: []const pardes.Region, chrome: pardes.Chrome) Error![]const u8 {
// The protocol's ceiling, enforced by the ENCODER too, and BEFORE the
@@ -837,6 +847,12 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point
if (rgb) |value| for (value) |channel| try w.putByte(channel);
} else if (field.type == u8) {
try w.putByte(rgb);
+ } else if (field.type == u32) {
+ try w.putU32(rgb);
+ } else if (field.type == f32) {
+ try w.putU32(@bitCast(rgb));
+ } else if (field.type == [8]f32) {
+ for (rgb) |value| try w.putU32(@bitCast(value));
} else for (rgb) |channel| try w.putByte(channel);
}
}
@@ -1917,7 +1933,7 @@ test "detached wire carries compact body rows and exact pointer metrics" {
const placed = [_]pardes.Region{.{ .kind = .rail, .owner = 3, .serial = 42, .rect = .{ .x = 0, .y = 1, .w = 2, .h = 3 }, .active = true, .dirty = true, .thumb_y = 1, .thumb_h = 2, .clip = 1, .lift = 0.5 }};
const out = try gpa.alloc(u8, frameBound(2, 4) + layersBound(&.{layer}, &.{tag}, &placed));
defer gpa.free(out);
- const palette: pardes.Chrome = .{ .page = .{ 1, 2, 3 }, .border = .{ 4, 5, 6 }, .tag_focus_bg = .{ 7, 8, 9 }, .scroll_thumb = .{ 10, 11, 12 }, .rule_px = 3, .tag_rule = .{ 13, 14, 15 }, .rail_px = 12, .grip_border = .{ 16, 17, 18 }, .grip_focus_ring = .{ 19, 20, 21 }, .grip_dirty = .{ 22, 23, 24 }, .lift_style = 2 };
+ const palette: pardes.Chrome = .{ .page = .{ 1, 2, 3 }, .border = .{ 4, 5, 6 }, .tag_focus_bg = .{ 7, 8, 9 }, .scroll_thumb = .{ 10, 11, 12 }, .rule_px = 3, .tag_rule = .{ 13, 14, 15 }, .rail_px = 12, .grip_border = .{ 16, 17, 18 }, .grip_focus_ring = .{ 19, 20, 21 }, .grip_dirty = .{ 22, 23, 24 }, .lift_style = 2, .cursor_glide = 3, .cursor_alpha = 0.5, .cursor_idle_ms = 1234, .cursor_quad = .{ 1, 2, 3.5, 2, 3.5, 3, 1, 3 } };
const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}, &.{tag}, &placed, palette);
const packet = (try framed(encoded)).?;
const frame = (try decodeServer(packet.tag, packet.payload)).frame;
diff --git a/src/draw.zig b/src/draw.zig
index 963d61c1..a2005e59 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -184,6 +184,76 @@ fn inkOn(bg: [3]u8) [3]u8 {
return if (lum > 140) .{ 0x00, 0x00, 0x00 } else .{ 0xff, 0xff, 0xff };
}
+/// The focused cursor's box in grid cells, at the pitch it is drawn at: a
+/// tag's at the tagline's (Layer.Metrics), else a grid cell.
+fn focusedCursorBox(p: *const Pardes, s: *const Surface) ?animation.Box {
+ const cursor = s.cursor orelse return null;
+ const m = p.row_metrics;
+ const bw: f32 = @floatFromInt(@max(1, m.body_w));
+ const bh: f32 = @floatFromInt(@max(1, m.body_h));
+ const tw: f32 = @floatFromInt(@max(1, m.tagline_w));
+ const th: f32 = @floatFromInt(@max(1, @min(m.tagline_h, m.body_h)));
+ for (s.tagLayers()) |*layer| {
+ const caret = layer.cursor orelse continue;
+ if (layer.rows == 0 or layer.viewport.y + caret.y != cursor.y) continue;
+ if (cursor.x < layer.viewport.x or cursor.x >= layer.viewport.x + layer.viewport.w) continue;
+ const x = @as(f32, @floatFromInt(layer.viewport.x)) + @as(f32, @floatFromInt(caret.x)) * tw / bw;
+ const w = if (caret.bar) tw / bw / 8 else tw / bw;
+ return .{ .x = x, .y = @as(f32, @floatFromInt(cursor.y)) + (bh - th) / 2 / bh, .w = w, .h = th / bh };
+ }
+ return .{ .x = @floatFromInt(cursor.x), .y = @floatFromInt(cursor.y), .w = if (cursor.bar) 1.0 / 8.0 else 1, .h = 1 };
+}
+
+/// G3 (docs/render-pipeline.md §5.4): the focused cursor on its springs,
+/// at the Motion flavour's pace, a jump gliding and a step or a keystroke in
+/// insert mode landing at once; blinking unless typing. A pixel shell's: a
+/// grid's cursor is the terminal's own.
+fn glideCursor(p: *Pardes, s: *Surface) void {
+ s.chrome.cursor_glide = 0;
+ s.chrome.cursor_alpha = 1;
+ s.chrome.cursor_idle_ms = std.math.maxInt(u32);
+ if (!builtins.capabilities.lift) return;
+ const box = focusedCursorBox(p, s) orelse return;
+ const motion = animation.Motion.of(p.settings.motion);
+ var snap = if (s.cursor) |cursor| cursor.bar else false;
+ if (p.panes[p.active]) |pane| {
+ // A focus change lands at once: a glide between panes would be
+ // squashed against the new one's edge.
+ if (p.cursor_view.serial != pane.serial) snap = true;
+ // Typing never trails: insert mode lands every move.
+ if (pane.body.mode == .insert) snap = true;
+ // Scrolled (a file's view, a terminal's output): the quad moves with
+ // the text and lands, never gliding against text that jumped (every
+ // Enter at a prompt scrolls one and moves the other).
+ const line: i64 = if (pane.file) |f| @intCast(f.scroll) else if (pane.isTerminal()) panes.Terminal.gridOffset(pane) else 0;
+ if (p.cursor_view.serial == pane.serial and line != p.cursor_view.line) {
+ p.cursor_glide.shift(0, @floatFromInt(p.cursor_view.line - line));
+ snap = true;
+ }
+ p.cursor_view = .{ .serial = pane.serial, .line = line };
+ }
+ p.cursor_glide.aim(box, p.now_ns, motion, snap);
+ _ = p.cursor_glide.step(p.now_ns);
+ const moving = p.cursor_glide.moving();
+ s.chrome.cursor_glide = 2 | @as(u8, @intFromBool(moving));
+ // The shell blinks it, from how long ago the last key or click was.
+ if (p.settings.cursor_blink)
+ s.chrome.cursor_idle_ms = @intCast(@min(std.math.maxInt(u32) - 1, (p.now_ns -| p.input_ns) / std.time.ns_per_ms));
+ // A little less than solid while it glides, so the glyphs it passes
+ // stay legible (less on the long flavours), solid again as it lands.
+ const remaining = p.cursor_glide.remaining(p.now_ns);
+ const see_through: f32 = if (motion.zeta < 1) 0.7 else 0.85;
+ s.chrome.cursor_alpha = if (moving) see_through + (1 - see_through) * std.math.clamp(1 - remaining / 0.5, 0, 1) else 1;
+ const quad = p.cursor_glide.sample(p.now_ns, motion);
+ // Never outside the focused pane, however far a flavour overshoots or
+ // a scroll carried it: it squashes against the pane's edges.
+ const r = p.rects[p.active];
+ for (quad, 0..) |point, i| {
+ s.chrome.cursor_quad[i * 2] = std.math.clamp(point[0], @as(f32, @floatFromInt(r.x)), @as(f32, @floatFromInt(r.x + r.w)));
+ s.chrome.cursor_quad[i * 2 + 1] = std.math.clamp(point[1], @as(f32, @floatFromInt(r.y)), @as(f32, @floatFromInt(r.y + r.h)));
+ }
+}
+
/// The Lift style as drawn: `auto` is a shadow on a light page, and on a
/// dark one, where a shadow has nothing to darken, no lift at all (the other
/// panes recede instead, `inactiveDim`).
@@ -504,15 +574,8 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
s.addRegion(.{ .kind = .guide, .rect = .{ .x = p.hover_col, .y = p.topBarHeight(), .w = 1, .h = s.rows -| p.topBarHeight() } });
}
}
- for (0..p.ncol) |cc| {
- if (p.hover_col < p.col_x[cc] or p.hover_col >= p.col_x[cc] + p.col_w[cc]) continue;
- // the same seam the h-handle hit test picks (see there)
- if (mouse.seamIdxAt(p, cc, p.hover_row) != null) {
- var col = p.col_x[cc];
- while (col < p.col_x[cc] + p.col_w[cc]) : (col += 1) s.overlayDash(col, p.hover_row, "╌");
- s.addRegion(.{ .kind = .guide, .rect = .{ .x = p.col_x[cc], .y = p.hover_row, .w = p.col_w[cc], .h = 1 } });
- }
- }
+ // No hint over a pane's last row: a row of text is no handle; two
+ // panes resize by the rule between them or by the grip.
for (0..p.ncol -| 1) |cn| {
if (p.hover_col != p.col_x[cn] + p.col_w[cn] -| 1) continue;
if (mouse.seamIdxAt(p, cn, p.hover_row) != null) continue;
@@ -582,6 +645,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
}
}
};
+ glideCursor(p, s);
try p.presentation.submit(p, s);
return composeAsciiTransitions(p, arena, s);
}
@@ -987,9 +1051,11 @@ fn joinPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r
// The grid's row: the gap cell and the text, as far as the pane.
joinCells(s, region.rect.x, region.rect.y + @as(u16, @intCast(line)), storage[line * pitch ..][0..pitch], config.GUTTER, region.rect.w, tag_fg);
}
- if (temporary.cursor) |cursor| if (cursor.x - TAG_TEXT_INSET < r.w - TAG_TEXT_INSET) {
- s.cursor = .{ .x = r.x + cursor.x, .y = region.rect.y + cursor.y, .bar = cursor.bar };
- };
+ // Past a full row's end the layer's caret is at the tag's edge, as
+ // acme's tick is; the grid has no cell there, so it takes the last one.
+ if (temporary.cursor) |cursor| {
+ s.cursor = .{ .x = r.x + @min(cursor.x, r.w - 1), .y = region.rect.y + cursor.y, .bar = cursor.bar };
+ }
}
/// A notice chip, painted once as a tag layer at the tagline's pitch, and the
@@ -1653,6 +1719,98 @@ test "each Motion flavour reaches the lift as its own trajectory" {
try std.testing.expect(peaks[2] <= 1.001 and peaks[3] > 1.25 and peaks[4] > peaks[3]);
}
+test "the focused cursor glides a jump, lands a step at once, and is exact at rest" {
+ if (!builtins.capabilities.lift) return error.SkipZigTest;
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 30 });
+ defer p.deinit();
+ _ = try p.setTestFile("some text on a line\n" ** 20);
+ p.settings.cursor_blink = false;
+ var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena.deinit();
+ const ms = std.time.ns_per_ms;
+ var now: u64 = std.time.ns_per_s;
+ p.advance(now);
+ _ = try p.render(arena.allocator());
+ // A step down: at once.
+ p.update(.{ .key = .{ .cp = 'j' } });
+ now += 16 * ms;
+ p.advance(now);
+ _ = arena.reset(.retain_capacity);
+ var s = try p.render(arena.allocator());
+ try std.testing.expectEqual(@as(u8, 2), s.chrome.cursor_glide);
+ const before = s.cursor.?;
+ // To the last line: it glides, a quad partway, and asks for frames.
+ p.update(.{ .key = .{ .cp = 'g' } });
+ p.update(.{ .key = .{ .cp = 'e' } });
+ now += 16 * ms;
+ p.advance(now);
+ _ = arena.reset(.retain_capacity);
+ s = try p.render(arena.allocator());
+ const target = s.cursor.?;
+ try std.testing.expect(target.y > before.y + 5);
+ try std.testing.expectEqual(@as(u8, 3), s.chrome.cursor_glide);
+ try std.testing.expect(s.chrome.cursor_quad[1] < @as(f32, @floatFromInt(target.y)) - 1);
+ try std.testing.expect(p.continuous());
+ // Frame by frame to rest: gliding exactly while it moves (the shell then
+ // draws the quad, never the cells' cursor too), at under full alpha
+ // until it lands, solid as it does.
+ var frames: usize = 0;
+ while (p.continuous()) : (frames += 1) {
+ now += 7 * ms;
+ p.advance(now);
+ _ = arena.reset(.retain_capacity);
+ s = try p.render(arena.allocator());
+ try std.testing.expect(s.chrome.cursor_glide & 2 != 0);
+ try std.testing.expectEqual(p.cursor_glide.moving(), s.chrome.cursor_glide & 1 != 0);
+ if (s.chrome.cursor_glide & 1 != 0) try std.testing.expect(s.chrome.cursor_alpha <= 1 and s.chrome.cursor_alpha >= 0.7);
+ for (s.chrome.cursor_quad) |value| try std.testing.expect(std.math.isFinite(value));
+ }
+ try std.testing.expect(frames > 3);
+ try std.testing.expectEqual(@as(u8, 2), s.chrome.cursor_glide);
+ try std.testing.expectEqual(@as(f32, 1), s.chrome.cursor_alpha);
+ // At rest the quad is the cursor's own cell.
+ try std.testing.expectEqual(@as(f32, @floatFromInt(target.x)), s.chrome.cursor_quad[0]);
+ try std.testing.expectEqual(@as(f32, @floatFromInt(target.y)), s.chrome.cursor_quad[1]);
+ // A mode change mid-glide lands at once: insert mode never trails.
+ p.update(.{ .key = .{ .cp = 'g' } });
+ p.update(.{ .key = .{ .cp = 'g' } });
+ now += 7 * ms;
+ p.advance(now);
+ _ = arena.reset(.retain_capacity);
+ s = try p.render(arena.allocator());
+ try std.testing.expectEqual(@as(u8, 3), s.chrome.cursor_glide);
+ p.update(.{ .key = .{ .cp = 'i' } });
+ now += 7 * ms;
+ p.advance(now);
+ _ = arena.reset(.retain_capacity);
+ s = try p.render(arena.allocator());
+ try std.testing.expectEqual(@as(u8, 2), s.chrome.cursor_glide);
+}
+
+test "a terminal's cursor lands with scrolling output, never gliding against it" {
+ if (!builtins.capabilities.lift or !pardes.terminal_panes) return error.SkipZigTest;
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 12 });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const term = p.active;
+ try std.testing.expect(p.panes[term].?.isTerminal());
+ p.settings.cursor_blink = false;
+ var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena.deinit();
+ const ms = std.time.ns_per_ms;
+ var now: u64 = std.time.ns_per_s;
+ p.update(.{ .output = .{ .pane = @intCast(term), .bytes = "line\r\n" ** 20 ++ "$ some command typed" } });
+ p.advance(now);
+ _ = try p.render(arena.allocator());
+ // Enter: three lines of output scroll up, the cursor to a new prompt.
+ p.update(.{ .output = .{ .pane = @intCast(term), .bytes = "\r\nout\r\nout\r\nout\r\n$ " } });
+ now += 16 * ms;
+ p.advance(now);
+ _ = arena.reset(.retain_capacity);
+ const s = try p.render(arena.allocator());
+ try std.testing.expectEqual(@as(u8, 2), s.chrome.cursor_glide);
+}
+
test "a monkey over notices, prompts, tags and tiny panes at a gui's metrics never panics" {
// The snapshot harness runs the tty, whose tagline pitch is the body's,
// so it never reaches what a narrower tagline does to a notice's band:
diff --git a/src/dump.zig b/src/dump.zig
index ef442ddb..88a95994 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -53,27 +53,34 @@ pub fn outPath(buf: *[1024:0]u8, dir_setting: []const u8) ?[:0]const u8 {
ds.getSecondsIntoMinute(),
}, 0) catch null;
}
+/// The host's environment; a freestanding core (web, the board) has none
+/// and no libc to ask, so only a DumpDir with no `~/` names a directory there.
+fn hostEnv(name: [*:0]const u8) ?[*:0]u8 {
+ if (builtin.os.tag == .freestanding) return null;
+ return std.c.getenv(name);
+}
+
/// Where dumps go and relative Restore names are looked up: the `DumpDir`
/// setting when set (a leading `~/` is the home directory), else the default.
pub fn directory(buf: []u8, dir_setting: []const u8) ?[]const u8 {
if (dir_setting.len == 0) return defaultDirectory(buf);
- if (std.mem.startsWith(u8, dir_setting, "~/")) if (std.c.getenv("HOME")) |h|
+ if (std.mem.startsWith(u8, dir_setting, "~/")) if (hostEnv("HOME")) |h|
return std.fmt.bufPrint(buf, "{s}/{s}", .{ std.mem.span(h), dir_setting[2..] }) catch null;
return std.fmt.bufPrint(buf, "{s}", .{dir_setting}) catch null;
}
/// The standard dump directory, without creating it or generating a filename.
pub fn defaultDirectory(buf: []u8) ?[]const u8 {
- if (std.c.getenv("XDG_DATA_HOME")) |x|
+ if (hostEnv("XDG_DATA_HOME")) |x|
return std.fmt.bufPrint(buf, "{s}/pardes", .{std.mem.span(x)}) catch null;
- if (std.c.getenv("HOME")) |h|
+ if (hostEnv("HOME")) |h|
return std.fmt.bufPrint(buf, "{s}/.local/share/pardes", .{std.mem.span(h)}) catch null;
return null;
}
pub const magic = "pardes-dump";
pub const version: u32 = 1;
-pub const max_panes: usize = 16;
+pub const max_panes: usize = MAX_PANES;
pub const max_cols: usize = 6;
/// Output arguments are typed in the same bounded one-line tag storage. Keep
/// the schema limit named independently so a dump reader can validate it
@@ -142,6 +149,9 @@ pub const File = struct {
origin_arg: []const u8 = "",
mini_source: []const u8 = "",
mini_colors_b64: []const u8 = "",
+ /// Its dot, q0 and q1 in bytes, as acme's dump keeps a window's; null
+ /// in older dumps.
+ dot: ?[2]u32 = null,
};
pub const ImagePalette = enum {
@@ -180,6 +190,9 @@ pub const Pane = struct {
};
pub const Column = struct {
+ /// Its serial when dumped (0 in older dumps): a Restore logs
+ /// `restoredcol <old> <new>` for it.
+ serial: u32 = 0,
tag: ?[]const u8 = null,
weight: f32 = 1,
panes: []const usize = &.{},
@@ -200,6 +213,11 @@ pub const State = struct {
columns: []const Column = &.{},
panes: []const Pane = &.{},
mounts: []const Mount = &.{},
+ /// The settings that differ from a fresh session's, each a line as the
+ /// root ctl takes it (`Placement pardes`); the theme and the locations
+ /// config are kept on their own, and the font is the frontend's. Absent
+ /// in older dumps.
+ settings: []const []const u8 = &.{},
};
/// A header is a text like any other: it may hold newlines, but not a
@@ -305,6 +323,15 @@ pub const Parsed = struct {
}
};
+/// Whether `bytes` are a dump a Restore could load, saying nothing: a
+/// Restore asks before it warns about unsaved text or replaces anything.
+pub fn check(gpa: std.mem.Allocator, bytes: []const u8) !void {
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ const source = try arena.allocator().dupeZ(u8, bytes);
+ try validate(try std.zon.parse.fromSliceAlloc(State, arena.allocator(), source, null, .{ .free_on_error = false }));
+}
+
pub fn readZon(gpa: std.mem.Allocator, bytes: []const u8, label: []const u8) !Parsed {
var parsed: Parsed = .{ .value = undefined, .arena = .init(gpa) };
errdefer parsed.deinit();
@@ -636,6 +663,10 @@ pub fn dumpState(p: *Pardes) !void {
else
try pardes.panes.Terminal.dumpPane(pane, arena, tag, body, scroll);
dp.tag_tail = pane.tag.own;
+ if (dp.file) |*df| {
+ const dot = ctlfs.pane.dotOf(pane);
+ df.dot = .{ dot.q0, dot.q1 };
+ }
dp.serial = pane.serial;
dump_panes[panes_len] = dp;
panes_len += 1;
@@ -651,6 +682,7 @@ pub fn dumpState(p: *Pardes) !void {
ids_len += 1;
};
columns[columns_len] = .{
+ .serial = layout.columnSerial(p, c),
.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],
@@ -671,6 +703,7 @@ pub fn dumpState(p: *Pardes) !void {
.columns = columns[0..columns_len],
.panes = dump_panes[0..panes_len],
.mounts = mounts[0..p.fs.mounts.items.len],
+ .settings = try changedSettings(p, arena),
};
try validate(state);
var out: std.Io.Writer.Allocating = .init(p.gpa);
@@ -681,20 +714,55 @@ pub fn dumpState(p: *Pardes) !void {
p.emit(.write_dump);
}
+/// The setting lines that differ from a fresh session's, as the root ctl
+/// reads them, less the ones a dump keeps apart.
+fn changedSettings(p: *Pardes, arena: std.mem.Allocator) ![]const []const u8 {
+ var now: std.Io.Writer.Allocating = .init(arena);
+ var fresh: std.Io.Writer.Allocating = .init(arena);
+ const defaults: config.Runtime = .{};
+ try pardes.ctlfs.ctl.writeSettingsOf(p, &p.settings, &now.writer);
+ try pardes.ctlfs.ctl.writeSettingsOf(p, &defaults, &fresh.writer);
+ var out: std.ArrayList([]const u8) = .empty;
+ var lines = std.mem.splitScalar(u8, now.written(), '\n');
+ while (lines.next()) |line| {
+ if (line.len == 0) continue;
+ const word = line[0 .. std.mem.indexOfScalar(u8, line, ' ') orelse line.len];
+ if (std.mem.eql(u8, word, "Theme") or std.mem.eql(u8, word, "Font")) continue;
+ var same = std.mem.splitScalar(u8, fresh.written(), '\n');
+ while (same.next()) |other| {
+ if (std.mem.eql(u8, other, line)) break;
+ } else try out.append(arena, line);
+ }
+ return out.items;
+}
+
+/// Applies a dump's setting lines, each as the root ctl would.
+fn applySettings(p: *Pardes, lines: []const []const u8) void {
+ for (lines) |line| {
+ const sp = std.mem.indexOfScalar(u8, line, ' ');
+ const setting = config.Runtime.find(line[0 .. sp orelse line.len]) orelse continue;
+ _ = p.settings.apply(setting, if (sp) |s| line[s + 1 ..] else null);
+ }
+}
+
/// The replacement core for a Restore of the dump at `from`, whose log says
/// so after its panes' `new`s, for a client that reconnects to read.
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;
- var old: [MAX_PANES]u32 = @splat(0);
+ var old: OldSerials = .{};
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) {
+ // Which serial each pane and column had, for a script that held them.
+ for (replacement.panes, old.panes) |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);
};
+ for (old.cols[0..replacement.ncol], 0..) |was, c| if (was != 0) {
+ var rec: [32]u8 = undefined;
+ pardes.ctlfs.events.notePath(replacement, "restoredcol", std.fmt.bufPrint(&rec, "{d} {d}", .{ was, layout.columnSerial(replacement, c) }) catch continue);
+ };
return replacement;
}
@@ -702,7 +770,13 @@ pub fn initFromDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8
return initDump(gpa, opts, zon_bytes, null, null);
}
-fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes, old_serials: ?*[MAX_PANES]u32) !*Pardes {
+/// The serials a dump's panes and columns had, read as it is restored.
+const OldSerials = struct {
+ panes: [MAX_PANES]u32 = @splat(0),
+ cols: [MAX_COLS]u32 = @splat(0),
+};
+
+fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes, old_serials: ?*OldSerials) !*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,6 +810,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo
const st = parsed.value;
p.locations_config = st.locations_config;
p.settings.tree_context_tag_style = st.tree_context_tag_style;
+ applySettings(p, st.settings);
for (st.mounts) |mount| {
var already_mounted = false;
for (p.fs.mounts.items) |existing| {
@@ -752,7 +827,7 @@ 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;
+ if (old_serials) |o| o.panes[i] = src.serial;
const pane: *pardes.panes.Pane = switch (src.kind) {
.terminal => terminal: {
const t = src.terminal.?;
@@ -822,6 +897,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo
p.col_serial = @splat(0);
if (st.topbar_custom) |bar| p.global_tag.own = try p.gpa.dupe(u8, bar);
for (st.columns[0..p.ncol], 0..) |col, c| {
+ if (old_serials) |o| o.cols[c] = col.serial;
if (col.tag) |bar| p.col_tags[c].own = try p.gpa.dupe(u8, bar);
const scaled = @as(f64, @floatCast(col.weight)) * @as(f64, @floatFromInt(column_weight_unit));
const bounded = @min(scaled, @as(f64, @floatFromInt(max_column_weight)));
@@ -840,3 +916,36 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo
ctlfs.events.announce(p);
return p;
}
+
+test "a dump keeps the settings that differ from a fresh session's, and a restore applies them" {
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .tty_only = true });
+ defer p.deinit();
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Placement pardes"));
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Verbose off"));
+ try dumpState(p);
+ try std.testing.expect(std.mem.indexOf(u8, p.dump_out.?, "\"Placement pardes\"") != null);
+ // What is left at its default is not written.
+ try std.testing.expect(std.mem.indexOf(u8, p.dump_out.?, "\"BootShell keep\"") == null);
+ const restored = try initFromDump(gpa, .{ .tty_only = true }, p.dump_out.?);
+ defer restored.deinit();
+ try std.testing.expectEqual(config.Runtime.Placement.pardes, restored.settings.placement);
+ try std.testing.expect(!restored.settings.verbose);
+}
+
+test "a restored pane keeps its dot, as acme's dump keeps a window's" {
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("one two three\nfour\n");
+ ctlfs.pane.setDot(pane, .{ .q0 = 4, .q1 = 7 });
+ try dumpState(p);
+ const restored = try initFromDump(gpa, .{ .tty_only = true }, p.dump_out.?);
+ defer restored.deinit();
+ const again = for (restored.panes) |slot| {
+ if (slot) |candidate| if (candidate.file != null) break candidate;
+ } else return error.FileLost;
+ const dot = ctlfs.pane.dotOf(again);
+ try std.testing.expectEqual(@as(u32, 4), dot.q0);
+ try std.testing.expectEqual(@as(u32, 7), dot.q1);
+}
diff --git a/src/exec.zig b/src/exec.zig
index 56e6a2a9..d4480e00 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -312,7 +312,7 @@ pub fn spawnV9fsTty(p: *Pardes, from: usize) void {
fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane {
const src = p.panes[from] orelse return null;
const free = p.freeSlot() orelse {
- p.reportFailure(from, "Tty: no pane free");
+ p.reportError(from, "Tty", error.NoPaneSlots);
return null;
};
const nt = p.newShell(free, Pardes.paneDir(src)) catch |err| {
@@ -443,6 +443,18 @@ fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed {
return .asked;
}
+const repl_enter_wait_ns: u64 = 250 * std.time.ns_per_ms;
+
+/// Sends the Enters a paste into pane `id`'s REPL is owed: when the REPL
+/// has answered it (`answered`), or once the wait is up.
+pub fn flushReplEnter(p: *Pardes, id: usize, answered: bool) void {
+ const pane = p.panes[id] orelse return;
+ if (pane.repl_enter == 0) return;
+ if (!answered and p.stepped_ns < pane.repl_enter_due_ns) return;
+ for (0..pane.repl_enter) |_| p.emitWrite(id, "\r");
+ pane.repl_enter = 0;
+}
+
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;
@@ -475,13 +487,20 @@ pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void {
var said: [128]u8 = undefined;
return p.setMessage(from, std.fmt.bufPrint(&said, "Repl: {d} bytes is more than can be sent at once; send less", .{n}) catch "Repl: too long to send");
}
+ const pasted = panes.Terminal.bracketedPaste(dst);
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");
+ const enters: u8 = if ((if (dst.repl) |r| r.lang == python_lang else false) and pythonBlockOpen(clean[0..n])) 2 else 1;
+ if (pasted) {
+ // An Enter read with the paste is part of it: 3.13's REPL takes it
+ // as a newline in the pasted text, even a one-line one, and waits.
+ // It goes once the REPL has answered the paste (its next output),
+ // or after a moment if nothing comes.
+ dst.repl_enter = enters;
+ dst.repl_enter_due_ns = p.stepped_ns + repl_enter_wait_ns;
+ } else for (0..enters) |_| 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)
@@ -590,7 +609,7 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
return id;
}
const free = p.freeSlot() orelse {
- p.reportFailure(from, "exec: no pane free for the command");
+ p.reportError(from, "exec", error.NoPaneSlots);
return null;
};
const pane = p.newCommand(free, dir, line) catch |err| {
diff --git a/src/fs-help.txt b/src/fs-help.txt
index 0121a831..73a49e9b 100644
--- a/src/fs-help.txt
+++ b/src/fs-help.txt
@@ -1,22 +1,28 @@
Pardes control filesystem
=========================
README this guide
-index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name (+New if unnamed)
+index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name (+New if unnamed), column serial
status pid, version and pane count
look write a line: a right click on it at the active pane; read: the serials it touched
exec write a line: a middle click, an editor command word or a shell line; read the same
-log events: new/del/rename/save <serial> <name>, run/exit/send <serial> ..., dump/restore <path>, msg, err
+log events: new/del/rename/save <serial> <name>, newcol/delcol <column serial>, run/exit/send <serial> ...,
+ ask <serial> <what> <choices>, dump/restore <path>, restored/restoredcol <old> <new>, 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 (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
+layout one line per column: serial index x width current|notcurrent empty|full pane-serials...; then active <serial>
+tag the workspace tag: read it, > replaces it, >> appends (one line)
+col/<n>/tag the tag of the column with serial n, the same way (a serial stays while the column lives)
+col/<n>/ctl Delcol, Joincol, New or Tty on that column; col/<n>/exec a word as a click in its tag; rmdir col/<n> closes an empty one
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)
+os/, src/ the host filesystem; the editor's own sources
-Below, $m is the mount: $NINE_MOUNT/pardes/<pid, or NAME of --detach=NAME>; 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;
+$NINE_MOUNT itself under `9ns --unix SOCKET -- cmd`, which mounts one session as its root.
cat $m/index which panes exist
n=$(cat $m/pane/new) make a pane, take its serial
@@ -38,7 +44,7 @@ Pitfalls, one each:
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; > replaces that text (default words too), >> appends.
- A terminal's body is a history snapshot frozen per open; pty/data is the live stream.
+ A terminal's body is a history snapshot frozen per open, wrapped rows joined into lines; 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 leaves it empty; the last pane of all quits.
diff --git a/src/fs.zig b/src/fs.zig
index 0a4473d6..8be728a6 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -19,7 +19,14 @@ const Reply = tree.Reply;
const E = tree.E;
/// The served README; tree.zig answers it.
-pub const help = @embedFile("fs-help.txt");
+/// The tree's README; a build that embeds no sources lists no src/.
+pub const help = help: {
+ const raw = @embedFile("fs-help.txt");
+ if (limits.embedded_sources) break :help raw;
+ const both = "os/, src/ the host filesystem; the editor's own sources";
+ const at = std.mem.indexOf(u8, raw, both) orelse @compileError("fs-help.txt no longer lists os/, src/");
+ break :help raw[0..at] ++ "os/ the host filesystem" ++ raw[at + both.len ..];
+};
/// Host filesystem nodes: the root, and one hashed node per path a client walked to.
pub const os_root: u64 = (1 << 63) + 2;
@@ -491,7 +498,7 @@ test "bounded reads release terminal snapshots after success and size refusal" {
defer gpa.free(empty);
try std.testing.expectEqual(@as(usize, 0), empty.len);
p.update(.{ .output = .{ .pane = 0, .bytes = "limited terminal output" } });
- const expected = "limited terminal output";
+ const expected = "limited terminal output\n"; // a line ends with its newline
const bytes = try readLimit(p, path, expected.len);
defer gpa.free(bytes);
try std.testing.expectEqualStrings(expected, bytes);
@@ -1270,6 +1277,15 @@ pub const Namespace = struct {
/// 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).
+ /// A header tag truncated by an open not yet released (ninep/cols.zig):
+ /// 0 the workspace's, a column's its index + 1; and whether the newline
+ /// that ended its last write is held back.
+ header_rewrite: ?u32 = null,
+ /// A look or exec a 9P write made found every pane slot taken; the
+ /// write fails with what was said, kept at the head of `ename`.
+ no_pane_slot: bool = false,
+ no_pane_slot_len: u16 = 0,
+ header_held: bool = false,
late_failure: [256]u8 = undefined,
late_failure_len: u16 = 0,
/// A refusal that quotes the message it refuses, as Plan 9's cmderror
@@ -1355,3 +1371,7 @@ pub fn unmount(p: *Pardes, name: []const u8) !void {
test {
_ = tree;
}
+
+test "the tree's README lists src/ only when the build serves it" {
+ try std.testing.expectEqual(limits.embedded_sources, std.mem.indexOf(u8, help, "src/") != null);
+}
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 5dfacee9..491373a8 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1857,6 +1857,12 @@ const Gui = struct {
scroll_edge: []pardes.Cell = &.{}, // the row that just left the pane
scroll_edge_len: u16 = 0,
+ /// The cursor's blink (blinkNow): when the frame drawn last arrived,
+ /// a redraw owed for an edge, and this frame's cursor mode and alpha.
+ frame_arrival_ns: u64 = 0,
+ blink_due: bool = false,
+ no_blink: bool = false,
+ cursor_quad_alpha: f32 = 0,
scene_tex: ?*c.SDL_GPUTexture = null,
scene_tex_w: u32 = 0,
scene_tex_h: u32 = 0,
@@ -1972,7 +1978,11 @@ fn glyphForCodepoint(g: *const Gui, cp: u32) FaceGlyph {
}
const CellLayout = struct { w: f32, h: f32, x_off: f32, y_off: f32 };
-const MouseCell = struct { col: u16, row: u16, body_hit: ?pardes.Mouse.BodyHit = null, tag_hit: ?pardes.TagHit = null };
+const MouseCell = struct { col: u16, row: u16, body_hit: ?pardes.Mouse.BodyHit = null, tag_hit: ?pardes.TagHit = null, rule_above: bool = false };
+
+/// How near a row's top edge a press is on the rule drawn there between two
+/// panes: the rule's 2 px and a pixel either side.
+const rule_grab_px: f32 = 3;
const WindowGeometry = struct {
window_w: f32,
window_h: f32,
@@ -2648,6 +2658,8 @@ fn localSession(
.test_hz = if (std.c.getenv("PARDES_TEST_HZ")) |hz| @max(1, std.fmt.parseInt(u64, std.mem.span(hz), 10) catch 60) else 60,
};
defer shell.shutdownPtys();
+ // A virtual clock never blinks: goldens and feel series hold still.
+ g.no_blink = shell.test_clock != null;
const host = shell.host();
core.host = host;
@@ -3420,6 +3432,7 @@ fn dispatch(g: *Gui, in: *Input, sev: *const c.SDL_Event) void {
.row = mc.row,
.tag_hit = mc.tag_hit,
.body_hit = mc.body_hit,
+ .rule_above = mc.rule_above,
.ctrl = (c.SDL_GetModState() & c.SDL_KMOD_CTRL) != 0,
},
});
@@ -3720,6 +3733,8 @@ fn gridCellAtDimensions(
) MouseCell {
const row: u16 = @intFromFloat(@min(@floor(@max(y, 0) / @max(body_h, 1)), 10_000));
var point: MouseCell = .{ .col = @intFromFloat(@min(@floor(@max(x, 0) / @max(body_w, 1)), 10_000)), .row = row };
+ // On the rule over this row, which is where two panes meet if they do.
+ point.rule_above = row > 0 and @max(y, 0) - @as(f32, @floatFromInt(row)) * @max(body_h, 1) < rule_grab_px;
if (core) |p| if (p.presentation.pointerFractional(p.screen_w, p.screen_h, x / @max(body_w, 1), y / @max(body_h, 1))) |mapped| {
for (p.surface.tagLayers()) |*layer| {
if (layer.tagHit(mapped.x * body_w, mapped.y * body_h, body_w, body_h, tagline_w, @floatFromInt(p.row_metrics.tagline_h))) |hit| {
@@ -4179,7 +4194,14 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void {
@intCast(@max(1, refreshNs(g) / std.time.ns_per_ms))
else
16;
- const ms: c_int = if (latency_fd >= 0) 0 else if (minimized) -1 else if (timeout_ms != 0) @min(@as(c_int, @intCast(@min(timeout_ms, std.math.maxInt(c_int)))), if (polls) poll_ms else std.math.maxInt(c_int)) else if (polls) poll_ms else -1;
+ const waited: c_int = if (latency_fd >= 0) 0 else if (minimized) -1 else if (timeout_ms != 0) @min(@as(c_int, @intCast(@min(timeout_ms, std.math.maxInt(c_int)))), if (polls) poll_ms else std.math.maxInt(c_int)) else if (polls) poll_ms else -1;
+ // The cursor's blink is the shell's to time (Chrome.cursor_idle_ms):
+ // a frame a loop through an edge, else a wake at the next one; none
+ // while the window is minimized or unfocused. A virtual clock never
+ // blinks, so goldens hold still.
+ const blink = if (s.test_clock != null or minimized) null else blinkNow(g, if (s.surface) |frame| &frame.chrome else null);
+ const blink_ms: ?c_int = if (blink) |b| (if (b.edge) 1 else if (b.next_ns) |next| @as(c_int, @intCast(@min(std.math.maxInt(c_int), next / std.time.ns_per_ms + 1))) else null) else null;
+ const ms: c_int = if (blink_ms) |b| (if (waited < 0) b else @min(waited, b)) else waited;
// The wait is the 9P connections' turn with the core.
pardes.turn.rest();
const got = c.SDL_WaitEventTimeout(&sev, ms);
@@ -4189,6 +4211,9 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void {
// (PARDES_TEST_HZ), events or not, as a real display's present
// would; otherwise a wait that ran out moves it exactly to the
// core's wake.
+ // Through an edge, or woken for the next: the shell redraws the last
+ // frame with the new alpha, no core render (§5.5 level B).
+ if (blink_ms != null and (blink.?.edge or !got)) g.blink_due = true;
if (s.test_clock) |*virtual| {
if (core.continuous()) {
const display = virtual.* + std.time.ns_per_s / s.test_hz;
@@ -4273,6 +4298,27 @@ fn pollFrame(ctx: ?*anyopaque) void {
// early is on time: the wait that paces it is in whole milliseconds).
if (g.post.animating(core.settings.shader_animation) and !core.needs_frame and s.presented and
c.SDL_GetTicksNS() -| g.post.last_ns + std.time.ns_per_ms >= refreshNs(g)) redrawPost(g, s.gpa);
+ // A blink edge: the last frame again, the cursor's alpha moved on.
+ if (g.blink_due and !core.needs_frame and s.presented) if (s.surface) |frame| {
+ g.blink_due = false;
+ blink_redraws += 1;
+ s.presented = renderFrame(g, s.gpa, core, frame, core.settings.debug) catch false;
+ };
+}
+
+/// Frames drawn for a blink alone, with no core render (tests, tracing).
+var blink_redraws: u64 = 0;
+
+/// The blink now, from the frame's time since input and how long ago it
+/// arrived; null where nothing blinks (off, idle, an unfocused window).
+fn blinkNow(g: *const Gui, chrome: ?*const pardes.Chrome) ?pardes.animation.Blink {
+ const frame = chrome orelse return null;
+ if (g.no_blink or frame.cursor_glide & 2 == 0 or frame.cursor_idle_ms == std.math.maxInt(u32)) return null;
+ if (c.SDL_GetWindowFlags(g.window) & c.SDL_WINDOW_INPUT_FOCUS == 0) return null;
+ const since = @as(u64, frame.cursor_idle_ms) * std.time.ns_per_ms + (monoNs() -| g.frame_arrival_ns);
+ // Relative to now: `next_ns` is how long until the next edge.
+ const b = pardes.animation.Blink.at(since, 0);
+ return .{ .alpha = b.alpha, .edge = b.edge, .next_ns = if (b.next_ns) |next| next -| since else null };
}
/// One refresh of the display the window is on, in ns; a frame of the
@@ -4340,6 +4386,7 @@ fn present(ctx: ?*anyopaque, surface: *const pardes.Surface) void {
frame_started_ns = monoNs();
const frame = @constCast(surface);
s.surface = frame;
+ g.frame_arrival_ns = monoNs();
const scene_requested = g.post.ready();
if (!scene_requested) g.scene_failures = 0;
s.presented = renderFrame(
@@ -4485,8 +4532,8 @@ fn writeFile(ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) vo
fn writeDump(ctx: ?*anyopaque, bytes: []const u8) void {
const s = shellOf(ctx);
var pbuf: [1024:0]u8 = undefined;
- const path = pardes.dump.outPath(&pbuf, s.core.settings.dump_dir.get()) orelse return;
- filesystem.write(s.core, path, bytes) catch |err| return s.core.reportError(0, "dump", err);
+ const path = pardes.dump.outPath(&pbuf, s.core.settings.dump_dir.get()) orelse return s.core.dumpFailed(s.core.settings.dump_dir.get(), error.NoDumpDirectory);
+ filesystem.write(s.core, path, bytes) catch |err| return s.core.dumpFailed(path, err);
s.core.setLastDump(path);
}
@@ -5359,8 +5406,13 @@ fn renderFrame(
const layout = fixedCellLayout(g);
const chrome = &surface.chrome;
var groups = makeGroups(surface.panelTracks(), surface.hasPanelDiff());
- // Cursors stand still while panes move, as they always have.
- const cursors_shown = groups.tracks == 0;
+ // Cursors stand still while panes move, as they always have; while the
+ // focused one glides or blinks, it is the overlay's quad instead. One
+ // or the other every frame (none only at a blink's off half).
+ const blink_alpha: f32 = if (blinkNow(g, chrome)) |b| b.alpha else 1;
+ const quad_mode = chrome.cursor_glide & 2 != 0 and (chrome.cursor_glide & 1 != 0 or blink_alpha < 1);
+ g.cursor_quad_alpha = if (quad_mode) chrome.cursor_alpha * blink_alpha else 0;
+ const cursors_shown = groups.tracks == 0 and !quad_mode;
var overlay_count = buildOverlay(g, core, surface, sw, sh, debug_on);
if (overlay_count != 0 and !uploadOverlayGpu(g, cmd, overlay_count))
overlay_count = 0;
@@ -8498,10 +8550,43 @@ fn buildOverlay(
if (g.overlay_vertices.len == 0 or sw == 0 or sh == 0) return 0;
var builder = OverlayBuilder{ .vertices = g.overlay_vertices, .win_w = @floatFromInt(sw), .win_h = @floatFromInt(sh) };
appendWorkspacePet(g, core, surface, &builder);
+ appendCursorGlide(g, surface, &builder);
appendTouchOverlay(g, &builder, debug_on);
return @intCast(builder.len);
}
+/// G3: the focused cursor while it glides or blinks, its quad from the core
+/// (Chrome.cursor_quad, grid cells) in its cursor colour; a little less than
+/// solid, so the glyphs it passes over stay legible.
+fn appendCursorGlide(g: *Gui, surface: *const pardes.Surface, builder: *OverlayBuilder) void {
+ const chrome = &surface.chrome;
+ if (g.cursor_quad_alpha <= 0.001) return;
+ if (surface.panelTracks().len != 0) return;
+ const layout = fixedCellLayout(g);
+ var px: [4][2]f32 = undefined;
+ var centre: [2]f32 = .{ 0, 0 };
+ for (&px, 0..) |*point, i| {
+ point.* = .{ chrome.cursor_quad[i * 2] * layout.w, chrome.cursor_quad[i * 2 + 1] * layout.h };
+ centre[0] += point[0] / 4;
+ centre[1] += point[1] / 4;
+ }
+ // Its colour: the cursor the cells would draw nearest the quad.
+ var boxes: [max_cursors]CursorBox = undefined;
+ var rgb: [3]u8 = chrome.fg orelse fg_default;
+ var nearest: f32 = std.math.inf(f32);
+ for (boxes[0..cursorBoxes(g, surface, layout, builder.win_h, &boxes)]) |box| {
+ const dx = (box.x0 + box.x1) / 2 - centre[0];
+ const dy = (box.y0 + box.y1) / 2 - centre[1];
+ if (dx * dx + dy * dy < nearest) {
+ nearest = dx * dx + dy * dy;
+ rgb = box.rgb;
+ }
+ }
+ const color: OverlayColor = .{ .r = @as(f32, @floatFromInt(rgb[0])) / 255, .g = @as(f32, @floatFromInt(rgb[1])) / 255, .b = @as(f32, @floatFromInt(rgb[2])) / 255, .a = g.cursor_quad_alpha };
+ builder.addTri(px[0][0], px[0][1], px[1][0], px[1][1], px[2][0], px[2][1], color);
+ builder.addTri(px[0][0], px[0][1], px[2][0], px[2][1], px[3][0], px[3][1], color);
+}
+
fn appendWorkspacePet(g: *Gui, core: ?*const pardes.Pardes, surface: *const pardes.Surface, builder: *OverlayBuilder) void {
const p = core orelse return;
const kind: pet.Kind = @enumFromInt(@intFromEnum(p.settings.pet));
diff --git a/src/layout.zig b/src/layout.zig
index c58f3bcf..b9b24e79 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -162,8 +162,15 @@ pub fn targetColumn(p: *Pardes, cur_x: u16) usize {
}
pub fn splitRowForExtent(y: u16, h: u16, cur_y: u16) ?u16 {
+ return splitRowFor(y, h, cur_y, null);
+}
+
+/// As `splitRowForExtent`, each pane keeping at least `least` rows when
+/// given: a pane dragged by its grip within its own column is how a
+/// terminal resizes, and acme lets either window keep only its tag.
+fn splitRowFor(y: u16, h: u16, cur_y: u16, least: ?u16) ?u16 {
if (h < 2) return null;
- const min_each: u16 = if (h >= config.MINH * 2) config.MINH else 1;
+ const min_each: u16 = least orelse if (h >= config.MINH * 2) config.MINH else 1;
const lo = y +| min_each;
const hi = y + h - min_each;
if (lo > hi) return y + h / 2;
@@ -176,7 +183,11 @@ pub fn movePlacement(p: *Pardes, id: usize, cur_x: u16, cur_y: u16) ?MovePlaceme
if (tc == src.col and p.col_n[src.col] == 1) return null;
if (tc == src.col) {
const sr = p.rects[id];
- if (cur_y >= sr.y and cur_y < sr.y + sr.h) return null;
+ // Dropped on itself it stays, unless dragged down from its top with
+ // a pane above: its top moves down there, the pane above taking the
+ // rows, as acme's window dragged by its button does. With no rule
+ // between panes, that is how a terminal resizes them.
+ if (cur_y >= sr.y and cur_y < sr.y + sr.h and (src.idx == 0 or cur_y <= sr.y)) return null;
}
var heights: [MAX_PANES]u16 = @splat(0);
for (0..p.ncol) |c| {
@@ -196,7 +207,7 @@ pub fn movePlacement(p: *Pardes, id: usize, cur_x: u16, cur_y: u16) ?MovePlaceme
const pid = p.col_panes[tc][k];
if (pid == id) continue;
const h = heights[pid];
- const row = splitRowForExtent(y, h, cur_y) orelse {
+ const row = splitRowFor(y, h, cur_y, if (tc == src.col) BOX_H else null) orelse {
y +|= h;
continue;
};
@@ -280,7 +291,7 @@ pub fn applyRowSplit(p: *Pardes, cc: usize, k: usize, cur_y: u16) void {
b.vweight = pair - a.vweight;
}
-pub fn findPane(p: *Pardes, id: usize) ?struct { col: usize, idx: usize } {
+pub fn findPane(p: *const Pardes, id: usize) ?struct { col: usize, idx: usize } {
for (0..p.ncol) |c| {
for (0..p.col_n[c]) |k| {
if (p.col_panes[c][k] == id) return .{ .col = c, .idx = k };
@@ -311,6 +322,7 @@ pub fn removePane(p: *Pardes, id: usize) void {
/// 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);
+ const serial = columnSerial(p, c);
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);
@@ -325,6 +337,14 @@ pub fn dropColumn(p: *Pardes, c: usize) void {
p.ncol -= 1;
p.col_tags[p.ncol] = .{ .what = .column, .gpa = p.gpa };
p.col_serial[p.ncol] = 0;
+ noteColumn(p, "delcol", serial);
+}
+
+/// `newcol <serial>` or `delcol <serial>` in the log: the column's serial,
+/// as /layout and /col name it.
+fn noteColumn(p: *Pardes, what: []const u8, serial: u32) void {
+ var buf: [16]u8 = undefined;
+ pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d}", .{serial}) catch return);
}
/// Fold column `c` into the one on its right, its panes below theirs.
@@ -480,6 +500,7 @@ pub fn insertColumn(p: *Pardes, col: usize, before: bool) ?usize {
p.col_serial[c] = 0;
p.col_n[c] = 0;
p.ncol += 1;
+ noteColumn(p, "newcol", columnSerial(p, c));
return c;
}
diff --git a/src/look.zig b/src/look.zig
index d90d27b8..17746cd1 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -1378,6 +1378,7 @@ test "Look hover waits without mutating the pane and input cancels it" {
while (p.nextEffect()) |_| {}
const other_id = p.freeSlot() orelse return error.NoSparePaneForHoverTest;
_ = try p.newShell(other_id, "");
+ p.panes[other_id].?.tag_expanded = false; // the blank spot right of the file is its body, not its tag
try std.testing.expect(layout.splitColumn(p, 0, other_id, false));
p.sync();
while (p.nextEffect()) |_| {}
diff --git a/src/macos.zig b/src/macos.zig
index 678b155d..d6daf944 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -2496,8 +2496,8 @@ fn writeFile(ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) vo
fn writeDump(ctx: ?*anyopaque, bytes: []const u8) void {
const st = hostState(ctx);
var pbuf: [1024:0]u8 = undefined;
- const path = pardes.dump.outPath(&pbuf, st.core.settings.dump_dir.get()) orelse return;
- filesystem.write(st.core, path, bytes) catch |err| return st.core.reportError(0, "dump", err);
+ const path = pardes.dump.outPath(&pbuf, st.core.settings.dump_dir.get()) orelse return st.core.dumpFailed(st.core.settings.dump_dir.get(), error.NoDumpDirectory);
+ filesystem.write(st.core, path, bytes) catch |err| return st.core.dumpFailed(path, err);
st.core.setLastDump(path);
}
diff --git a/src/mouse.zig b/src/mouse.zig
index 36786612..1ca53017 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -97,13 +97,50 @@ fn cornerFixture(gpa: std.mem.Allocator) !*Pardes {
return p;
}
+test "a pane's last row of text is no resize handle; the rule over the next pane is" {
+ if (platform == .web) return;
+ const p = try cornerFixture(std.testing.allocator);
+ defer p.deinit();
+ const seam = seamRowOf(p, 1, 0);
+ const inside = p.col_x[1] + p.col_w[1] / 2;
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = inside, .row = seam } });
+ try std.testing.expect(p.drag != .border_h);
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = inside, .row = seam } });
+ try std.testing.expect(nudgeRightSeam(p, 2) == seam + 2);
+}
+
+test "a pane's grip dragged up or down its own column moves its top, down to a tag alone above" {
+ if (platform == .web) return;
+ const p = try cornerFixture(std.testing.allocator);
+ defer p.deinit();
+ const upper = p.col_panes[1][0];
+ const lower = p.col_panes[1][1];
+ const grip_x = p.rects[lower].x;
+ // Up, past the pane above's first row: it keeps one row, its tag.
+ const top = p.rects[upper].y;
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = grip_x, .row = p.rects[lower].y } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = grip_x, .row = top } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = grip_x, .row = top } });
+ p.sync();
+ try std.testing.expectEqual(@as(u16, 1), p.rects[upper].h);
+ try std.testing.expectEqual(lower, p.col_panes[1][1]);
+ // Down, inside itself: its top follows, the pane above taking the rows.
+ const to = p.rects[lower].y + 6;
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = grip_x, .row = p.rects[lower].y } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = grip_x, .row = to } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = grip_x, .row = to } });
+ p.sync();
+ try std.testing.expectEqual(to, p.rects[lower].y);
+}
+
fn nudgeRightSeam(p: *Pardes, delta: i32) u16 {
const from = seamRowOf(p, 1, 0);
const inside = p.col_x[1] + p.col_w[1] / 2;
const to: u16 = @intCast(@as(i32, from) + delta);
- p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = inside, .row = from } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = inside, .row = to } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = inside, .row = to } });
+ // On the rule a pixel frontend draws over the lower pane's first row.
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = inside, .row = from + 1, .rule_above = true } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = inside, .row = to + 1 } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = inside, .row = to + 1 } });
p.sync();
return seamRowOf(p, 1, 0);
}
@@ -437,13 +474,17 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
return;
}
}
- for (0..p.ncol) |cc| {
+ // Two panes are resized on the rule between them, never on a
+ // row of text; with no rule, by the grip, as acme moves a
+ // window by its button.
+ if (m.rule_above and mrow > 0) for (0..p.ncol) |cc| {
if (mcol < p.col_x[cc] or mcol >= p.col_x[cc] + p.col_w[cc]) continue;
- if (seamIdxAt(p, cc, mrow)) |k| {
- p.drag = .{ .border_h = .{ .col = cc, .top_idx = k, .cur_y = mrow } };
+ const seam = if (p.settings.tag_bottom) mrow else mrow - 1;
+ if (seamIdxAt(p, cc, seam)) |k| {
+ p.drag = .{ .border_h = .{ .col = cc, .top_idx = k, .cur_y = seam } };
return;
}
- }
+ };
const id = hovered orelse return;
const r = p.rects[id];
const pane = p.panes[id] orelse return;
@@ -597,7 +638,9 @@ pub fn dragUpdate(p: *Pardes, mcol: u16, mrow: u16, body_hit: ?Mouse.BodyHit) vo
if (k + 1 < p.col_n[cc]) {
const a = p.rects[p.col_panes[cc][k]];
const b = p.rects[p.col_panes[cc][k + 1]];
- d.cur_y = clampBorderRow(a.y, a.h, b.h, mrow, p.settings.tag_bottom);
+ // Held by the rule over the lower pane: the seam is the row above.
+ const seam = if (p.settings.tag_bottom) mrow else mrow -| 1;
+ d.cur_y = clampBorderRow(a.y, a.h, b.h, seam, p.settings.tag_bottom);
} else d.cur_y = mrow;
},
.column_move => |*d| d.cur_x = mcol,
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
new file mode 100644
index 00000000..b6dc7d26
--- /dev/null
+++ b/src/ninep/cols.zig
@@ -0,0 +1,284 @@
+//! The layout and the header tags: /layout, /tag (the workspace's) and
+//! /col/<serial>/tag (each column's; a column's serial stays while it lives,
+//! as a pane's does). acme serves no column files -- its columns
+//! are only a window's place -- so these go past it: a script can see where
+//! the panes are and edit the tags a person clicks in.
+const std = @import("std");
+const pardes = @import("../pardes.zig");
+const Pardes = pardes.Pardes;
+const tree = @import("tree.zig");
+const tagline = pardes.tagline;
+const layout = pardes.layout;
+const Text = pardes.panes.Text;
+const Req = tree.Req;
+const Reply = tree.Reply;
+const E = tree.E;
+
+/// The column with the keyboard: the header's, when a column tag has it,
+/// else the active pane's. Null when the workspace tag has it and no pane
+/// is active.
+pub fn activeColumn(p: *const Pardes) ?usize {
+ if (p.header_focus) if (p.header_column) |c| return if (c < p.ncol) c else null;
+ const at = layout.findPane(p, p.active) orelse return null;
+ return at.col;
+}
+
+/// One line a column: its serial (the name /col/<serial> and the log give
+/// it, which stays while it lives), its index left to right, x and width in
+/// cells, `current` or `notcurrent` (rio's words, for the column with the
+/// keyboard), `empty` or `full`, and its panes' serials, top to bottom;
+/// then `active <serial>`: the active column, where pane/new and a look
+/// place a pane next (acme's activecol), not always the keyboard's.
+pub fn writeLayout(p: *Pardes, w: *std.Io.Writer) !void {
+ const active = activeColumn(p);
+ for (0..p.ncol) |c| {
+ try w.print("{d} {d} {d} {d} {s} {s}", .{
+ layout.columnSerial(p, c), c, p.col_x[c], p.col_w[c],
+ if (active == c) "current" else "notcurrent", if (p.col_n[c] == 0) "empty" else "full",
+ });
+ for (p.col_panes[c][0..p.col_n[c]]) |id| if (p.panes[id]) |pane| try w.print(" {d}", .{pane.serial});
+ try w.writeByte('\n');
+ }
+ if (p.activeColumn() orelse active) |c| try w.print("active {d}\n", .{layout.columnSerial(p, c)}) else try w.writeAll("active -\n");
+}
+
+pub fn layoutLen(p: *Pardes) u64 {
+ var buf: [256]u8 = undefined;
+ var counting: std.Io.Writer.Discarding = .init(&buf);
+ writeLayout(p, &counting.writer) catch return 0;
+ return counting.fullCount();
+}
+
+/// A header by its node: the workspace's (`serial` null) or the column's
+/// with that serial; null when that column has closed.
+const Header = struct { text: *Text, col: ?usize };
+
+fn header(p: *Pardes, serial: ?u32) ?Header {
+ const s = serial orelse return .{ .text = &p.global_tag, .col = null };
+ const c = layout.columnBySerial(p, s) orelse return null;
+ return .{ .text = &p.col_tags[c], .col = c };
+}
+
+pub fn headerText(p: *Pardes, serial: ?u32) ?[]const u8 {
+ const h = header(p, serial) orelse return null;
+ return if (h.col) |c| tagline.columnTag(p, c) else tagline.topbar(p);
+}
+
+pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply {
+ const text = headerText(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
+ const out = p.fs.stage(p.gpa);
+ out.appendSlice(p.gpa, text) catch return Reply.fail(req.tag, E.NOMEM);
+ out.append(p.gpa, '\n') catch return Reply.fail(req.tag, E.NOMEM);
+ return tree.stagedReply(p, req);
+}
+
+/// A write appends, as a pane tag's does; the tag is one line, so a newline
+/// in it becomes a space (tagline.flattenHeaders), but after a truncation
+/// the newline that ends the write is dropped, as a pane tag drops it.
+pub fn write(p: *Pardes, req: Req, serial: ?u32) Reply {
+ const t = (header(p, serial) orelse return Reply.fail(req.tag, E.NOENT)).text;
+ if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 };
+ const was = headerText(p, serial).?;
+ var data = req.data;
+ const rewriting = p.fs.header_rewrite == rewriteKey(serial);
+ // A newline held from the write before goes in once more text follows.
+ const held: []const u8 = if (rewriting and p.fs.header_held) " " else "";
+ p.fs.header_held = false;
+ if (rewriting and data[data.len - 1] == '\n') {
+ data = data[0 .. data.len - 1];
+ p.fs.header_held = true;
+ }
+ const next = std.mem.concat(p.gpa, u8, &.{ was, held, data }) catch return Reply.fail(req.tag, E.NOMEM);
+ std.mem.replaceScalar(u8, next, '\n', ' ');
+ if (t.own) |own| p.gpa.free(own);
+ t.own = next;
+ return .{ .tag = req.tag, .written = @intCast(req.data.len) };
+}
+
+/// Truncating clears it, as a pane tag's `cleartag`: its default words go
+/// too, and `u` in it brings them back.
+pub fn truncate(p: *Pardes, serial: ?u32) tree.Status {
+ const t = (header(p, serial) orelse return .err).text;
+ const empty = p.gpa.alloc(u8, 0) catch return .err;
+ t.remember(p.gpa, if (t.own) |own| .{ .text = own } else null);
+ if (t.own) |own| p.gpa.free(own);
+ t.own = empty;
+ t.cur_row = 0;
+ t.cur_col = 0;
+ p.fs.header_rewrite = rewriteKey(serial);
+ p.fs.header_held = false;
+ return .ok;
+}
+
+/// Which header a truncation was of: the workspace's is 0 (no column's
+/// serial is), a column's its serial.
+fn rewriteKey(serial: ?u32) u32 {
+ return serial orelse 0;
+}
+
+pub fn released(p: *Pardes, serial: ?u32) void {
+ if (p.fs.header_rewrite != rewriteKey(serial)) return;
+ p.fs.header_rewrite = null;
+ p.fs.header_held = false;
+}
+
+/// A column's ctl: the words that act on a column, as its tag's clicks do --
+/// Delcol, Joincol, New, Tty -- one a line, each run as from that tag.
+pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply {
+ var lines = std.mem.splitScalar(u8, req.data, '\n');
+ while (lines.next()) |raw| {
+ const line = std.mem.trim(u8, raw, " \t\r");
+ if (line.len == 0) continue;
+ const word = line[0 .. std.mem.indexOfAny(u8, line, " \t") orelse line.len];
+ const takes = for ([_][]const u8{ "Delcol", "Joincol", "New", "Tty" }) |w| {
+ if (std.mem.eql(u8, w, word)) break true;
+ } else false;
+ if (!takes) return tree.failText(req.tag, E.INVAL, "a column's ctl takes Delcol, Joincol, New or Tty");
+ const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
+ p.exec_column = col;
+ defer p.exec_column = null;
+ if (pardes.ctlfs.ctl.runBuiltin(p, req, p.active, line)) |refusal| return refusal;
+ }
+ return .{ .tag = req.tag, .written = @intCast(req.data.len) };
+}
+
+/// A column's exec: each line run as a middle click on it in that column's
+/// tag, acting on that column.
+pub fn writeExec(p: *Pardes, req: Req, serial: u32) Reply {
+ var lines = std.mem.splitScalar(u8, req.data, '\n');
+ while (lines.next()) |raw| {
+ const line = std.mem.trim(u8, raw, " \t\r");
+ if (line.len == 0) continue;
+ for (line) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL);
+ const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
+ p.exec_column = col;
+ defer p.exec_column = null;
+ pardes.ctlfs.ctl.run(p, p.active, true, line);
+ }
+ return .{ .tag = req.tag, .written = @intCast(req.data.len) };
+}
+
+/// rmdir of a column closes it when it is empty; one with panes is
+/// refused, saying so (close them, or Delcol on its ctl, which asks first).
+pub fn remove(p: *Pardes, req: Req, serial: u32, file: tree.ColFile) Reply {
+ if (file != .dir) return Reply.fail(req.tag, E.PERM);
+ const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
+ if (p.col_n[col] > 0) return tree.failText(req.tag, E.NOTEMPTY, "column not empty: close its panes, or write Delcol to its ctl");
+ p.removeColumn(col) catch return Reply.fail(req.tag, E.IO);
+ return .{ .tag = req.tag };
+}
+
+// ---- tests ----
+
+const testing = std.testing;
+const th = @import("testing.zig");
+const Node = tree.Node;
+
+test "layout lists each column's place, panes and state, and the active one" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ p.newColumn(p.active);
+ p.sync();
+ const first_serial = layout.columnSerial(p, 0);
+ const new_serial = layout.columnSerial(p, 1);
+ try testing.expect(first_serial != new_serial);
+ const layout_node = @intFromEnum(tree.TopFile.layout);
+ const text = th.rd(p, layout_node, 0, 4096).bytes;
+ var want: [256]u8 = undefined;
+ const first = try std.fmt.bufPrint(&want, "{d} 0 {d} {d} notcurrent full {d}\n", .{ first_serial, p.col_x[0], p.col_w[0], p.panes[0].?.serial });
+ try testing.expect(std.mem.startsWith(u8, text, first));
+ // Newcol gives the new, empty column's tag the keyboard.
+ try testing.expect(std.mem.indexOf(u8, text, "current empty\n") != null);
+ try testing.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "active {d}\n", .{new_serial})));
+ try testing.expectEqual(@as(u64, text.len), th.call(p, .{ .tag = 1, .op = .getattr, .node = layout_node }).reply.attr.size);
+ // /index says which column each pane is in, by its serial, last.
+ try testing.expect(std.mem.endsWith(u8, th.rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, try std.fmt.bufPrint(&want, " {d}\n", .{first_serial})));
+ try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "newcol {d}\n", .{new_serial})));
+ // A column that closes takes its name with it; the others keep theirs.
+ try p.removeColumn(1);
+ try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "delcol {d}\n", .{new_serial})));
+ try testing.expectEqual(E.NOENT, th.look_up(p, @intFromEnum(tree.TopFile.col), try std.fmt.bufPrint(&want, "{d}", .{new_serial})).errno());
+ try testing.expectEqual(first_serial, layout.columnSerial(p, 0));
+}
+
+test "a column's tag and the workspace's read, replace and append as a pane tag does, on one line" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const col_dir = @intFromEnum(tree.TopFile.col);
+ const serial = layout.columnSerial(p, 0);
+ var name: [16]u8 = undefined;
+ try testing.expectEqual(tree.Status.ok, th.look_up(p, col_dir, try std.fmt.bufPrint(&name, "{d}", .{serial})).reply.status);
+ try testing.expectEqual(E.NOENT, th.look_up(p, col_dir, "0").errno());
+ try testing.expectEqual(E.NOENT, th.look_up(p, col_dir, "777").errno());
+ const tag = Node.ofCol(serial, .tag);
+ try testing.expectEqual(tree.Status.ok, th.look_up(p, Node.ofCol(serial, .dir), "tag").reply.status);
+ try testing.expectEqualStrings("New Tty Find Grep Joincol Delcol\n", th.rd(p, tag, 0, 4096).bytes);
+ // `echo Make > tag`: cleared, then the newline ending the write dropped.
+ _ = th.call(p, .{ .tag = 1, .op = .setattr, .node = tag, .truncate = true });
+ _ = th.wr(p, tag, "Make\n");
+ try testing.expectEqualStrings("Make\n", th.rd(p, tag, 0, 4096).bytes);
+ _ = th.call(p, .{ .tag = 2, .op = .release, .node = tag });
+ // `>>` appends; a newline in a one-line tag is a space.
+ _ = th.wr(p, tag, " Lint\nNow");
+ try testing.expectEqualStrings("Make Lint Now\n", th.rd(p, tag, 0, 4096).bytes);
+ const top = @intFromEnum(tree.TopFile.tag);
+ _ = th.call(p, .{ .tag = 3, .op = .setattr, .node = top, .truncate = true });
+ _ = th.wr(p, top, "Newcol Exit\n");
+ try testing.expectEqualStrings("Newcol Exit", tagline.topbar(p));
+}
+
+test "a focus write makes its pane's column the active one: layout says so and pane/new goes there" {
+ for ([_][]const u8{ "Placement acme", "Placement pardes" }) |placement| {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ try testing.expect(p.executeBuiltinLine(p.active, placement));
+ const left = p.panes[0].?.serial;
+ p.newColumn(p.active);
+ p.newScratchFor(p.active, .fs); // fills the new, right column
+ p.sync();
+ try testing.expectEqual(@as(usize, 1), layout.findPane(p, p.active).?.col);
+ var digits: [16]u8 = undefined;
+ _ = th.wr(p, @intFromEnum(tree.TopFile.focus), try std.fmt.bufPrint(&digits, "{d}\n", .{left}));
+ var want: [32]u8 = undefined;
+ const text = th.rd(p, @intFromEnum(tree.TopFile.layout), 0, 4096).bytes;
+ try testing.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "active {d}\n", .{layout.columnSerial(p, 0)})));
+ const made = th.call(p, .{ .tag = 1, .op = .open, .node = @intFromEnum(tree.TopFile.new) }).reply.handle;
+ try testing.expectEqual(@as(usize, 0), layout.findPane(p, p.paneBySerial(made).?).?.col);
+ }
+}
+
+test "a column's ctl and exec act on it as its tag would, and rmdir closes it only empty" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const first = layout.columnSerial(p, 0);
+ p.newColumn(p.active);
+ const empty = layout.columnSerial(p, 1);
+ // New on the empty column's ctl fills it, wherever the keyboard is.
+ try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(empty, .ctl), "New\n").reply.status);
+ try testing.expectEqual(@as(usize, 1), p.col_n[1]);
+ try testing.expectEqual(E.INVAL, th.wr(p, Node.ofCol(empty, .ctl), "Exit\n").errno());
+ // rmdir refuses a column with a pane, and says why.
+ const full = th.call(p, .{ .tag = 1, .op = .release, .node = Node.ofCol(empty, .dir), .remove = true });
+ try testing.expectEqual(E.NOTEMPTY, full.errno());
+ try testing.expect(th.logHas(p, "column not empty"));
+ // exec runs a word as a click in that column's tag: Delcol closes it.
+ try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(empty, .exec), "Delcol\n").reply.status);
+ try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, empty));
+ // An empty column goes on rmdir.
+ p.newColumn(p.active);
+ const made = layout.columnSerial(p, 1);
+ try testing.expectEqual(tree.Status.ok, th.call(p, .{ .tag = 2, .op = .release, .node = Node.ofCol(made, .dir), .remove = true }).reply.status);
+ try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, made));
+ try testing.expectEqual(first, layout.columnSerial(p, 0));
+ // Joincol folds a column into the one on its right, which keeps its own
+ // serial and tag.
+ p.newColumn(p.active);
+ const right = layout.columnSerial(p, 1);
+ _ = th.wr(p, Node.ofCol(right, .tag), " Mine");
+ try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(first, .ctl), "Joincol\n").reply.status);
+ try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, first));
+ try testing.expectEqual(@as(?usize, 0), layout.columnBySerial(p, right));
+ try testing.expect(std.mem.endsWith(u8, headerText(p, right).?, " Mine"));
+ // The root's ctl points a column word at the column's own.
+ try testing.expect(std.mem.endsWith(u8, th.wr(p, @intFromEnum(tree.TopFile.ctl), "Delcol\n").reply.ename, "write it to col/<serial>/ctl"));
+}
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index b4144fd2..2fdc47cc 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -51,7 +51,7 @@ fn noteResult(p: *Pardes, serial: u32) void {
/// Runs one look or exec from the pane `id`, then records the panes it
/// created; failing that, a look records the pane it focused and an exec
/// the pane it acted on (even one it closed).
-fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
+pub fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
const context = p.panes[id].?.serial;
// A pane an event reader holds hears its clicks instead of acting on
// them (editors/acme/look.c:53 and exec.c:173 send them to winevent),
@@ -138,7 +138,14 @@ pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply {
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);
+ p.fs.no_pane_slot = false;
run(p, id, exec, text);
+ // A pane it would have opened had no slot: the write fails, and
+ // a look reads back nothing.
+ if (p.fs.no_pane_slot) {
+ p.fs.results_len = 0;
+ return tree.failText(req.tag, E.IO, p.fs.ename[0..p.fs.no_pane_slot_len]);
+ }
}
}
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
@@ -259,10 +266,23 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ?
const sp = std.mem.indexOfAny(u8, line, " \t");
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);
+ const b = std.meta.stringToEnum(Builtin, word) orelse {
+ // A setting this build's frontend cannot show (Lift, GripWidth on a
+ // terminal) is known, and says so rather than sound misspelt.
+ if (config.Runtime.find(word) != null) {
+ var why: [64]u8 = undefined;
+ return refuse(p, req, std.fmt.bufPrint(&why, "{s} is GUI-only", .{word}) catch "GUI-only", line);
+ }
+ return refuse(p, req, "unknown 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");
+ return refuseTo(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line, if (scope == .pane)
+ "/ctl"
+ else if (b == .Delcol or b == .Joincol)
+ "col/<serial>/ctl" // a column's own ctl, the one /layout names
+ 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;
@@ -281,14 +301,19 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ?
break :probe probe.apply(setting, if (arg.len > 0) arg else null);
},
};
- return if (takes) null else refuse(p, req, "bad value in control message", line);
+ if (takes) return null;
+ var why: [104]u8 = undefined;
+ return refuse(p, req, if (config.Runtime.choices(setting.action)) |values|
+ std.fmt.bufPrint(&why, "bad value in control message; takes {s}", .{values}) catch "bad value in control message"
+ else
+ "bad value in control message", line);
}
/// Runs a checked control message at pane `id`, as a click on its word
/// would. Answers the refusal when it would have opened a prompt for an
/// argument, which nobody writing to a file is there to type, or when it
/// reported an error: `Mount: AlreadyMounted "Mount peer /tmp/s"`.
-fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply {
+pub fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply {
p.fs.no_prompt = true;
p.fs.refused = false;
p.fs.failure_len = 0;
@@ -307,7 +332,10 @@ fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply {
if (!created) noteResult(p, context);
if (p.fs.refused) return refuse(p, req, e_prompt, line);
if (p.fs.failure_len == 0) return null;
- const refusal = refuse(p, req, p.fs.failure[0..p.fs.failure_len], line);
+ // An Edit's failure names what failed (the pattern that missed): its
+ // line, a program, is not quoted after it.
+ const edit = std.mem.startsWith(u8, line, "Edit ") or std.mem.eql(u8, line, "Edit");
+ const refusal = if (edit) tree.failText(req.tag, E.IO, p.fs.failure[0..p.fs.failure_len]) else refuse(p, req, p.fs.failure[0..p.fs.failure_len], line);
return .{ .tag = req.tag, .status = .err, .errno = E.IO, .ename = refusal.ename };
}
@@ -320,13 +348,22 @@ const e_prompt = "control message needs its argument";
/// What is not a setting -- platform, themes, where the config came from --
/// is /status's or the Config window's.
fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void {
- const s = &p.settings;
+ try writeSettingsOf(p, &p.settings, w);
+ try w.writeAll("LocationsConfig ");
+ try p.locations_config.write(w);
+ try w.writeByte('\n');
+ if (comptime builtins.Pet.enabled) try w.print("Pet {s}\n", .{@tagName(p.settings.pet)});
+}
+
+/// The setting words of `s`, one a line, as the root ctl reads them; a Dump
+/// keeps those that differ from a fresh session's (dump.zig).
+pub fn writeSettingsOf(p: *Pardes, s: *const config.Runtime, w: *std.Io.Writer) !void {
inline for (config.Runtime.settings) |setting| {
if (comptime !setting.enabled(builtins.capabilities)) continue;
const word = setting.word;
switch (setting.action) {
.toggle => |field| try w.print("{s} {s}\n", .{ word, if (@field(s, @tagName(field))) "on" else "off" }),
- .theme => try w.print("{s} {s}\n", .{ word, p.theme().name }),
+ .theme => try w.print("{s} {s}\n", .{ word, if (s == &p.settings) p.theme().name else pardes.themes[s.theme].name }),
.shell => {
const chosen = s.shell.requested.get();
try w.print("{s} {s}\n", .{ word, if (chosen.len > 0) chosen else config.default_shell });
@@ -347,8 +384,12 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void {
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}),
+ // The directory in effect, as Config reports it; bare only when
+ // there is none (no home to put the default in).
+ .dump_dir => {
+ var buf: [900]u8 = undefined;
+ if (pardes.dump.directory(&buf, s.dump_dir.get())) |dir| try w.print("{s} {s}\n", .{ word, dir }) else try w.print("{s}\n", .{word});
+ },
.transition => |effect| try w.print("{s} {s}\n", .{ word, if (s.panel_transition == effect) "on" else "off" }),
.scene => |effect| if (s.post.level(effect) == 0) try w.print("{s} off\n", .{word}) else try w.print("{s} {d}\n", .{ word, s.post.level(effect) }),
.shader => for (s.post.list()) |entry| if (entry.scene == null) try w.print("{s} {s}\n", .{ word, entry.path.get() }),
@@ -359,10 +400,6 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void {
.grip_width => try w.print("{s} {d}\n", .{ word, s.grip_width }),
}
}
- try w.writeAll("LocationsConfig ");
- try p.locations_config.write(w);
- try w.writeByte('\n');
- if (comptime builtins.Pet.enabled) try w.print("Pet {s}\n", .{@tagName(s.pet)});
}
pub fn rootLen(p: *Pardes) u64 {
@@ -407,11 +444,20 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply {
/// itself, so a new builtin is listed the moment it exists.
fn writeCommands(w: *std.Io.Writer) !void {
for (std.enums.values(Builtin)) |b| {
- try w.print("{s}{s} {s}\n", .{
+ try w.print("{s}{s} {s}", .{
@tagName(b),
if (builtins.registry.takesArg(b)) " arg" else "",
if (builtins.registry.scope(b) == .session) "root" else "pane",
});
+ // A setting that chooses among words: the words, comma-joined. Bare,
+ // it steps to the next of them (a two-valued one flips).
+ if (config.Runtime.find(@tagName(b))) |setting| if (config.Runtime.choices(setting.action)) |values| {
+ try w.writeByte(' ');
+ var parts = std.mem.splitSequence(u8, values, ", ");
+ var first = true;
+ while (parts.next()) |v| : (first = false) try w.print("{s}{s}", .{ if (first) "" else ",", v });
+ };
+ try w.writeByte('\n');
}
}
@@ -448,9 +494,12 @@ pub fn writeFocus(p: *Pardes, req: Req) Reply {
const text = std.mem.trim(u8, req.data, " \t\r\n");
const serial = std.fmt.parseInt(u32, text, 10) catch return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl);
const id = p.paneBySerial(serial) orelse return tree.failText(req.tag, E.NOENT, e_no_pane);
- // The keyboard goes to the pane itself, off any header that had it.
+ // The keyboard goes to the pane itself, off any header that had it, and
+ // its column becomes the active one, as the user's click there would
+ // make it: pane/new and a look place there next, whatever Placement.
tagline.exitHeader(p);
p.active = id;
+ if (pardes.layout.findPane(p, id)) |at| p.setActiveColumn(at.col);
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
@@ -516,10 +565,15 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply {
} 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);
+ var offered: [64]u8 = undefined;
+ const choices = p.questionChoices(id, &offered) orelse return refuse(p, req, "no question asked", line);
+ // Only a choice the question offers answers it; anything
+ // else leaves it standing, and says what it takes.
+ if (!std.mem.eql(u8, choice, "-") and (choice.len != 1 or std.mem.indexOfScalar(u8, choices, choice[0]) == null)) {
+ var why: [96]u8 = undefined;
+ return refuse(p, req, std.fmt.bufPrint(&why, "answer takes {s} or -", .{choices}) catch "answer takes a choice or -", 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;
@@ -618,10 +672,11 @@ test "commands lists every builtin with its argument and the ctl that takes it"
const b = std.meta.stringToEnum(Builtin, words.next().?).?;
if (builtins.registry.takesArg(b)) try testing.expectEqualStrings("arg", words.next().?);
try testing.expectEqualStrings(if (builtins.registry.scope(b) == .session) "root" else "pane", words.next().?);
+ if (config.Runtime.find(@tagName(b))) |s| if (config.Runtime.choices(s.action) != null) try testing.expect(words.next() != null);
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", "\nUndo pane\n", "\nRedo pane\n" }) |row|
+ for ([_][]const u8{ "\nDel arg pane\n", "\nNewcol root\n", "\nSave arg pane\n", "\nVerbose arg root on,off\n", "\nPlacement arg root acme,pardes\n", "\nUndo pane\n", "\nRedo pane\n" }) |row|
try testing.expect(std.mem.indexOf(u8, listed.bytes, row) != null);
}
@@ -747,7 +802,8 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi
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);
+ try testing.expectEqualStrings("bad value in control message; takes on, off \"Verbose maybe\"", wr(p, root_ctl, "Verbose maybe").reply.ename);
+ try testing.expectEqualStrings("bad value in control message; takes acme, pardes \"Placement east\"", wr(p, root_ctl, "Placement east").reply.ename);
const no_theme = wr(p, root_ctl, "Theme no-such-theme").reply.ename;
try testing.expectStringStartsWith(no_theme, "bad value in control message; like it: n");
try testing.expect(std.mem.endsWith(u8, no_theme, "; ThemeSel lists all \"Theme no-such-theme\""));
@@ -1216,3 +1272,47 @@ test "a look after Newcol fills the empty column the keyboard is on" {
try testing.expectEqual(@as(usize, 1), p.col_n[p.ncol - 1]);
try testing.expectEqual(p.active, p.col_panes[p.ncol - 1][0]);
}
+
+test "a Restore of a file that is no dump fails the write before any warning, and a failed Dump fails its write" {
+ if (comptime !pardes.hosted) return error.SkipZigTest;
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const f = pane_files.fileOf(p.panes[0].?).?;
+ f.content = try p.gpa.realloc(f.content, 3);
+ @memcpy(f.content, "yy\n");
+ f.revision +%= 1; // unsaved: a Restore would warn about it first
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "notes.txt", .data = "just notes\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ var line_buf: [4200]u8 = undefined;
+ const line = try std.fmt.bufPrint(&line_buf, "Restore {s}/notes.txt\n", .{dir});
+ const root_ctl = @intFromEnum(tree.TopFile.ctl);
+ const refused = wr(p, root_ctl, line);
+ try testing.expectEqual(E.IO, refused.errno());
+ try testing.expect(std.mem.indexOf(u8, refused.reply.ename, "not a pardes dump") != null);
+ try testing.expect(p.restore_req == null);
+ try testing.expect(p.panes[0].?.discard_warned == null);
+
+ p.fs.late_failure_len = 0;
+ p.dumpFailed("/nowhere/pardes.dump.zon", error.FileNotFound);
+ try testing.expectEqualStrings("Dump /nowhere/pardes.dump.zon: FileNotFound", p.fs.late_failure[0..p.fs.late_failure_len]);
+}
+
+test "a setting this frontend cannot show says GUI-only, and DumpDir reads back the directory in effect" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const root_ctl = @intFromEnum(tree.TopFile.ctl);
+ if (!builtins.capabilities.lift) {
+ try testing.expectEqualStrings("Lift is GUI-only \"Lift on\"", wr(p, root_ctl, "Lift on\n").reply.ename);
+ try testing.expectEqualStrings("GripWidth is GUI-only \"GripWidth 120\"", wr(p, root_ctl, "GripWidth 120\n").reply.ename);
+ }
+ try testing.expectEqualStrings("unknown control message \"Liftt on\"", wr(p, root_ctl, "Liftt on\n").reply.ename);
+ var buf: [900]u8 = undefined;
+ if (pardes.dump.directory(&buf, "")) |dir| {
+ var want: [920]u8 = undefined;
+ const line = try std.fmt.bufPrint(&want, "\nDumpDir {s}\n", .{dir});
+ try testing.expect(std.mem.indexOf(u8, rd(p, root_ctl, 0, 1 << 16).bytes, line) != null);
+ }
+}
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 0e489be7..ccc61352 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -172,6 +172,10 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
if (req.node != pardes.filesystem.os_root and req.node & pardes.filesystem.os_node == 0 and req.node & tree.sources.archive_node == 0) {
if (tree.Node.target(req.node)) |t| switch (t) {
.top => |f| file = f.fileName(),
+ .col => |c| {
+ serial = "-";
+ file = std.fmt.bufPrint(&name, "col/{d}/tag", .{c.serial}) catch "col";
+ },
.pane => |pt| {
serial = std.fmt.bufPrint(&name, "{d}", .{pt.serial}) catch "-";
file = switch (pt.file) {
@@ -940,7 +944,9 @@ test "a Dump written and a Restore made are in the log, with their files" {
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 })));
+ try testing.expect(std.mem.indexOf(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 })) != null);
+ // ...and each column's: restoredcol <old> <new>.
+ try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "restoredcol {d} {d}\n", .{ pardes.layout.columnSerial(p, 0), pardes.layout.columnSerial(restored, 0) })));
}
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 13381066..c594c7a3 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -63,6 +63,11 @@ pub const State = struct {
lock: ?u32 = null,
/// The host started a shell it could not teach to mark its prompts.
unmarked: bool = false,
+ /// `tag` was truncated (`echo X > tag`): until that open is released,
+ /// the newline that ends a write is held back, so the one that ends the
+ /// whole of it is dropped and draws no empty row.
+ tag_rewrite: bool = false,
+ tag_held_newline: 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,
@@ -260,6 +265,13 @@ pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 {
// ---- the index ----
+/// The serial of the column a pane is in, as /layout and /col name it; the
+/// last word of its /index line.
+pub fn columnOf(p: *Pardes, id: usize) u32 {
+ const at = pardes.layout.findPane(p, id) orelse return 0;
+ return pardes.layout.columnSerial(p, at.col);
+}
+
pub fn indexLen(p: *Pardes) u64 {
var n: u64 = 0;
var last: u32 = 0;
@@ -269,6 +281,7 @@ pub fn indexLen(p: *Pardes) u64 {
var digits: [16]u8 = undefined;
n += (std.fmt.bufPrint(&digits, "{d}", .{serial}) catch unreachable).len;
n += 1 + kindOf(pane).len + 3 + nameOf(pane).len + 1;
+ n += 1 + (std.fmt.bufPrint(&digits, "{d}", .{columnOf(p, p.paneBySerial(serial).?)}) catch unreachable).len;
}
return n;
}
@@ -427,11 +440,23 @@ fn writeTag(req: Req, pane: *Pane) Reply {
if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 };
// The tag's own text grows by what is written, newlines and all: a tag
// is a text like any other (acme's tag file appends the same way).
+ const pf = &pane.fs;
const had = tagline.curTail(pane).len;
- const room = limits.max_tag_tail -| had;
+ const room = limits.max_tag_tail -| had -| @intFromBool(pf.tag_held_newline);
if (room == 0) return Reply.fail(req.tag, E.NOSPC);
const take = wholeUtf8(req.data[0..@min(req.data.len, room)]);
- pane.appendTag(req.data[0..take]) catch return Reply.fail(req.tag, E.NOMEM);
+ // A truncating write drops ONE trailing newline, its whole text's: a
+ // newline held from a write before goes in once more text follows it.
+ if (pf.tag_held_newline) {
+ pane.appendTag("\n") catch return Reply.fail(req.tag, E.NOMEM);
+ pf.tag_held_newline = false;
+ }
+ var text = req.data[0..take];
+ if (pf.tag_rewrite and text.len > 0 and text[text.len - 1] == '\n') {
+ text = text[0 .. text.len - 1];
+ pf.tag_held_newline = true;
+ }
+ pane.appendTag(text) catch return Reply.fail(req.tag, E.NOMEM);
return .{ .tag = req.tag, .written = @intCast(take) };
}
@@ -544,7 +569,10 @@ fn writeName(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
const name = std.mem.trim(u8, req.data, " \t\r\n");
if (name.len == 0) return Reply.fail(req.tag, E.INVAL);
for (name) |c| if (c < ' ') return Reply.fail(req.tag, E.INVAL);
- if (fileOf(pane) == null) return Reply.fail(req.tag, E.PERM);
+ if (fileOf(pane) == null) return tree.failText(req.tag, E.PERM, if (pane.isTerminal())
+ "rename not allowed: a terminal is named by its shell's directory; cd there, or Tty in another"
+ else
+ "rename not allowed: an image or PDF is named by the file it shows");
const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ pardes.Pardes.paneDir(pane), name }) catch
return Reply.fail(req.tag, E.NOMEM);
if (!std.fs.path.isAbsolute(full) or full.len >= 4096) return Reply.fail(req.tag, E.INVAL);
@@ -648,6 +676,8 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status {
pane.tag.cur_col = if (tagline.pathPrefix(p.scratch.allocator(), pane)) |prefix| @intCast(prefix.len) else |_| 0;
pane.tag.vsel.active = false;
pane.tag.nsel = 0;
+ pf.tag_rewrite = true;
+ pf.tag_held_newline = false;
},
// A shell's `>` truncates before it writes: the address written
// is evaluated from where the last one left off, as with `>>`.
@@ -681,7 +711,7 @@ test "index lists serial kind dirty flag and name and stat reports its length" {
const a = rd(p, index, 0, 4096);
try testing.expectEqual(Status.ok, a.reply.status);
var want: [64]u8 = undefined;
- const clean_row = try std.fmt.bufPrint(&want, "{d} text 0 /test.txt\n", .{pane.serial});
+ const clean_row = try std.fmt.bufPrint(&want, "{d} text 0 /test.txt {d}\n", .{ pane.serial, columnOf(p, 0) });
try testing.expect(std.mem.indexOf(u8, a.bytes, clean_row) != null);
const at = std.mem.indexOf(u8, a.bytes, clean_row).?;
const stat = call(p, .{ .tag = 1, .op = .getattr, .node = index });
@@ -690,7 +720,7 @@ test "index lists serial kind dirty flag and name and stat reports its length" {
pane.file.?.saved_revision = pane.file.?.revision -% 1;
var dirty_want: [64]u8 = undefined;
- const dirty_row = try std.fmt.bufPrint(&dirty_want, "{d} text 1 /test.txt\n", .{pane.serial});
+ const dirty_row = try std.fmt.bufPrint(&dirty_want, "{d} text 1 /test.txt {d}\n", .{ pane.serial, columnOf(p, 0) });
try testing.expect(std.mem.indexOf(u8, rd(p, index, 0, 4096).bytes, dirty_row) != null);
const kind = at + std.mem.indexOfScalar(u8, clean_row, ' ').? + 1;
try testing.expectEqualStrings("text", rd(p, index, kind, 4).bytes);
@@ -700,7 +730,7 @@ test "index lists serial kind dirty flag and name and stat reports its length" {
try testing.expectEqual(@as(u64, both.bytes.len), indexLen(p));
var expected: [4200]u8 = undefined;
const made = p.panes[p.paneBySerial(serial).?].?;
- try testing.expect(std.mem.endsWith(u8, both.bytes, try std.fmt.bufPrint(&expected, "\n{d} text 0 {s}\n", .{ serial, nameOf(made) })));
+ try testing.expect(std.mem.endsWith(u8, both.bytes, try std.fmt.bufPrint(&expected, "\n{d} text 0 {s} {d}\n", .{ serial, nameOf(made), columnOf(p, p.paneBySerial(serial).?) })));
}
test "a terminal is listed as term with its directory as name" {
@@ -712,10 +742,12 @@ test "a terminal is listed as term with its directory as name" {
try testing.expect(std.mem.indexOf(
u8,
rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes,
- try std.fmt.bufPrint(&want, "{d} term 0 /work/dir\n", .{pane.serial}),
+ try std.fmt.bufPrint(&want, "{d} term 0 /work/dir {d}\n", .{ pane.serial, columnOf(p, 0) }),
) != null);
try testing.expectEqualStrings("/work/dir\n", rd(p, Node.of(pane.serial, .name), 0, 4096).bytes);
- try testing.expectEqual(E.PERM, wr(p, Node.of(pane.serial, .name), "/elsewhere\n").errno());
+ const refused = wr(p, Node.of(pane.serial, .name), "/elsewhere\n");
+ try testing.expectEqual(E.PERM, refused.errno());
+ try testing.expect(std.mem.startsWith(u8, refused.reply.ename, "rename not allowed: a terminal is named"));
}
test "body reads at any offset and writes append" {
@@ -977,6 +1009,19 @@ test "truncating the tag clears its editable tail" {
// and a click on the G puts the tag's cursor on it
const at = tagline.tagOffsetAt(p, pane, 0, "/test.txt ".len).?;
try testing.expectEqualStrings("Get", whole[at.col..]);
+
+ // `echo Make > tag`: the newline ending the truncating write's text is
+ // dropped, one only; a newline between its writes stays.
+ _ = call(p, .{ .tag = 3, .op = .release, .node = tag });
+ _ = call(p, .{ .tag = 4, .op = .setattr, .node = tag, .truncate = true });
+ _ = wr(p, tag, "Make\n");
+ try testing.expectEqualStrings("Make", pane.tag.own.?);
+ _ = wr(p, tag, "More\n\n");
+ try testing.expectEqualStrings("Make\nMore\n", pane.tag.own.?);
+ _ = call(p, .{ .tag = 5, .op = .release, .node = tag });
+ // `>>` keeps what it is given.
+ _ = wr(p, tag, " Tail\n");
+ try testing.expectEqualStrings("Make\nMore\n Tail\n", pane.tag.own.?);
}
test "name reads the file name and writing it promotes a scratch without touching its body" {
diff --git a/src/ninep/screen.zig b/src/ninep/screen.zig
index 462ea40f..f496da03 100644
--- a/src/ninep/screen.zig
+++ b/src/ninep/screen.zig
@@ -146,7 +146,7 @@ test "terminal body handles keep one history snapshot across fragmented reads" {
try testing.expectEqualStrings("", call(p, .{ .tag = 4, .op = .read, .node = node, .handle = opened.reply.handle, .off = original.len, .size = 3 }).bytes);
_ = call(p, .{ .tag = 5, .op = .release, .node = node, .handle = opened.reply.handle });
const newer = call(p, .{ .tag = 6, .op = .open, .node = node });
- try testing.expectEqualStrings("new output", call(p, .{ .tag = 7, .op = .read, .node = node, .handle = newer.reply.handle, .size = 32 }).bytes);
+ try testing.expectEqualStrings("new output\n", call(p, .{ .tag = 7, .op = .read, .node = node, .handle = newer.reply.handle, .size = 32 }).bytes);
_ = call(p, .{ .tag = 8, .op = .release, .node = node, .handle = newer.reply.handle });
}
@@ -162,7 +162,7 @@ test "an empty terminal body snapshot stays empty while output continues" {
try testing.expectEqualStrings("", call(p, .{ .tag = 3, .op = .read, .node = node, .handle = opened.reply.handle, .size = 32 }).bytes);
_ = call(p, .{ .tag = 4, .op = .release, .node = node, .handle = opened.reply.handle });
const newer = call(p, .{ .tag = 5, .op = .open, .node = node });
- try testing.expectEqualStrings("new output", call(p, .{ .tag = 6, .op = .read, .node = node, .handle = newer.reply.handle, .size = 32 }).bytes);
+ try testing.expectEqualStrings("new output\n", call(p, .{ .tag = 6, .op = .read, .node = node, .handle = newer.reply.handle, .size = 32 }).bytes);
_ = call(p, .{ .tag = 7, .op = .release, .node = node, .handle = newer.reply.handle });
}
@@ -194,13 +194,13 @@ test "terminal body snapshots are lazy bounded and released after the pane close
try testing.expectEqual(E.NOMEM, failed.errno());
try testing.expect(p.fs.opens[handles[0] - 1].what.snapshot == null);
const first = call(p, .{ .tag = 4, .op = .read, .node = node, .handle = handles[0], .size = 32 });
- try testing.expectEqualStrings("latest output", first.bytes);
+ try testing.expectEqualStrings("latest output\n", first.bytes);
const saved = p.fs.opens[handles[0] - 1].what.snapshot.?;
failing = std.testing.FailingAllocator.init(gpa, .{ .fail_index = 0 });
p.gpa = failing.allocator();
const second = call(p, .{ .tag = 5, .op = .read, .node = node, .handle = handles[0], .off = 7, .size = 32 });
p.gpa = gpa;
- try testing.expectEqualStrings("output", second.bytes);
+ try testing.expectEqualStrings("output\n", second.bytes);
try testing.expect(!failing.has_induced_failure);
try testing.expectEqual(saved.ptr, p.fs.opens[handles[0] - 1].what.snapshot.?.ptr);
try testing.expectEqual(E.INVAL, call(p, .{ .tag = 6, .op = .read, .node = screen_node, .handle = handles[0], .size = 32 }).errno());
@@ -328,3 +328,17 @@ test "screen inspection preserves acknowledged presentation and the next real fr
}
}
}
+
+test "a terminal's body is its logical lines: a row the terminal wrapped is joined again" {
+ const p = try th.withTerm(testing.allocator);
+ defer p.deinit();
+ const pane = p.panes[0].?;
+ const long = "w" ** 50 ++ "x" ** 150; // wider than the pane: it wraps
+ try testing.expect(long.len > pane.cols);
+ p.update(.{ .output = .{ .pane = 0, .bytes = long ++ "\r\nnext" } });
+ while (p.nextEffect()) |_| {}
+ const node = Node.of(pane.serial, .body);
+ const body = th.rd(p, node, 0, 4096).bytes;
+ try testing.expectEqualStrings(long ++ "\nnext\n", body);
+ try testing.expectEqual(@as(u64, body.len), th.call(p, .{ .tag = 1, .op = .getattr, .node = node }).reply.attr.size);
+}
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index ad95f6bf..baf8c772 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -18,6 +18,8 @@ pub const events = @import("events.zig");
pub const pane = @import("pane.zig");
pub const addr = @import("addr.zig");
pub const ctl = @import("ctl.zig");
+pub const cols = @import("cols.zig");
+const layout = pardes.layout;
pub const pty = @import("pty.zig");
pub const sources = @import("sources.zig");
pub const screen = @import("screen.zig");
@@ -68,9 +70,11 @@ pub const e_shut_down = "window shut down";
/// Tremove closes a pane; nothing else in the tree is created or destroyed by
/// the protocol, and wstat stays a truncation. Making a pane is an open of
-/// /pane/new, which needs no feature of the engine's, so Tcreate is refused
-/// everywhere, as it is in acme (editors/acme/fsys.c, fsyscreate).
-pub const features: cloud9.fs.Features = .{ .remove = true };
+/// /pane/new, so Tcreate is refused everywhere, as it is in acme
+/// (editors/acme/fsys.c, fsyscreate) -- here rather than by the engine, so
+/// a create in a pane that has closed says the pane is gone (a shell's `>`
+/// to a closed pane's file creates when its lookup fails).
+pub const features: cloud9.fs.Features = .{ .remove = true, .create = true };
/// Whether a request must wait until no step is out in a syscall
/// (`pardes.turn`): what would change a pane such a step still points into.
@@ -169,7 +173,7 @@ pub fn openOf(p: *Pardes, req: Req) ?*Open {
// ---- nodes ----
-pub const TopFile = enum(u4) {
+pub const TopFile = enum(u5) {
root = 1,
README,
index,
@@ -184,6 +188,9 @@ pub const TopFile = enum(u4) {
focus,
ctl,
commands,
+ layout,
+ tag,
+ col,
pub fn fileName(f: TopFile) []const u8 {
return if (f == .root) "/" else @tagName(f);
@@ -191,14 +198,14 @@ pub const TopFile = enum(u4) {
pub fn mode(f: TopFile) u16 {
return switch (f) {
- .root, .pane => 0o755,
- .look, .exec, .log, .focus, .ctl => 0o666,
- .README, .index, .status, .screen, .listeners, .new, .commands => 0o444,
+ .root, .pane, .col => 0o755,
+ .look, .exec, .log, .focus, .ctl, .tag => 0o666,
+ .README, .index, .status, .screen, .listeners, .new, .commands, .layout => 0o444,
};
}
pub fn dir(f: TopFile) bool {
- return f == .root or f == .pane;
+ return f == .root or f == .pane or f == .col;
}
};
@@ -262,6 +269,21 @@ pub const PaneFile = enum(u5) {
}
};
+/// /col/<serial> and its tag: a column is named by its serial, which stays
+/// while it lives as a pane's does; this bit, past a pane serial's 32, keeps
+/// the two apart.
+pub const ColFile = enum(u5) {
+ dir = 0,
+ tag = 1,
+ ctl = 2,
+ exec = 3,
+
+ pub fn fileName(f: ColFile) []const u8 {
+ return @tagName(f);
+ }
+};
+const col_bit: u59 = 1 << 58;
+
pub const Node = packed struct(u64) {
file: u5 = 0,
serial: u59 = 0,
@@ -271,11 +293,19 @@ pub const Node = packed struct(u64) {
return @bitCast(Node{ .file = @intFromEnum(file), .serial = serial });
}
+ pub fn ofCol(serial: u32, file: ColFile) u64 {
+ return @bitCast(Node{ .file = @intFromEnum(file), .serial = col_bit | @as(u59, serial) });
+ }
+
pub fn target(node: u64) ?Target {
const n: Node = @bitCast(node);
if (n.serial == 0) {
return .{ .top = std.enums.fromInt(TopFile, n.file) orelse return null };
}
+ if (n.serial & col_bit != 0) return .{ .col = .{
+ .serial = std.math.cast(u32, n.serial & ~col_bit) orelse return null,
+ .file = std.enums.fromInt(ColFile, n.file) orelse return null,
+ } };
return .{ .pane = .{
.serial = std.math.cast(u32, n.serial) orelse return null,
.file = std.enums.fromInt(PaneFile, n.file) orelse return null,
@@ -286,6 +316,7 @@ pub const Node = packed struct(u64) {
pub const Target = union(enum) {
top: TopFile,
pane: struct { serial: u32, file: PaneFile },
+ col: struct { serial: u32, file: ColFile },
};
/// Opening /pane/new makes a pane and reading the open fid answers its
@@ -371,6 +402,11 @@ pub fn stagedReply(p: *Pardes, req: Req) Reply {
// ---- dispatch ----
pub fn handle(p: *Pardes, req: Req) Reply {
+ if (req.op == .open and req.create) {
+ if (Node.target(req.node)) |t| if (t == .pane and p.paneBySerial(t.pane.serial) == null)
+ return Reply.fail(req.tag, E.NOENT);
+ return Reply.fail(req.tag, E.PERM);
+ }
const host = req.node == fs.os_root or req.node & fs.os_node != 0;
const archive = req.node & sources.archive_node != 0;
// Only a pane directory is removed. The host tree and the embedded
@@ -381,7 +417,7 @@ pub fn handle(p: *Pardes, req: Req) Reply {
const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT);
if (req.op == .write or req.op == .setattr) p.fs.origin = switch (target) {
.pane => |t| @as(u8, if (t.file == .body or t.file == .tag) 'E' else 'F'),
- .top => 'F',
+ .top, .col => 'F',
};
return switch (req.op) {
.lookup => lookup(p, req, target),
@@ -403,7 +439,7 @@ pub fn handle(p: *Pardes, req: Req) Reply {
fn runAttr(p: *Pardes, req: Req, target: Target) ?Reply.Attr {
const t = switch (target) {
.pane => |t| t,
- .top => return null,
+ .top, .col => return null,
};
if (t.file != .pty_run) return null;
const run = if (req.handle != 0) openOf(p, req) else for (&p.fs.opens) |*o| {
@@ -422,6 +458,23 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr {
.mode = f.mode(),
.size = topSize(p, f),
.mtime = p.fs.started,
+ .append = f == .tag,
+ },
+ .col => |c| {
+ if (layout.columnBySerial(p, c.serial) == null) return null;
+ return .{
+ .name = if (c.file == .dir) (std.fmt.bufPrint(&p.fs.node_name, "{d}", .{c.serial}) catch unreachable) else c.file.fileName(),
+ .node = Node.ofCol(c.serial, c.file),
+ .dir = c.file == .dir,
+ .mode = switch (c.file) {
+ .dir => 0o755,
+ .ctl, .exec => 0o222,
+ .tag => 0o666,
+ },
+ .size = if (c.file == .tag) cols.headerText(p, c.serial).?.len + 1 else 0,
+ .mtime = p.fs.started,
+ .append = c.file == .tag,
+ };
},
.pane => |t| {
const id = p.paneBySerial(t.serial) orelse return null;
@@ -454,7 +507,9 @@ fn topSize(p: *Pardes, f: TopFile) u64 {
return switch (f) {
// ponytail: /screen has no length until an open renders its frame,
// and /pane/new none until an open has a pane to name.
- .root, .pane, .screen, .new => 0,
+ .root, .pane, .col, .screen, .new => 0,
+ .layout => cols.layoutLen(p),
+ .tag => cols.headerText(p, null).?.len + 1,
.index => pane.indexLen(p),
.README => fs.help.len,
.status => ctl.statusLen(p),
@@ -485,6 +540,7 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply {
.pty => Node.of(t.serial, .dir),
else => return Reply.fail(req.tag, E.NOTDIR),
},
+ .col => |c| if (c.file == .dir) @intFromEnum(TopFile.col) else return Reply.fail(req.tag, E.NOTDIR),
};
return handle(p, .{ .tag = req.tag, .op = .getattr, .node = parent });
}
@@ -507,8 +563,22 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply {
if (serial == 0) return Reply.fail(req.tag, E.NOENT);
break :pane Node.of(serial, .dir);
},
+ // A column is its serial, in decimal digits and never 0.
+ .col => col: {
+ if (name.len > 10) return Reply.fail(req.tag, E.NOENT);
+ for (name) |c| if (c < '0' or c > '9') return Reply.fail(req.tag, E.NOENT);
+ const serial = std.fmt.parseInt(u32, name, 10) catch return Reply.fail(req.tag, E.NOENT);
+ if (serial == 0) return Reply.fail(req.tag, E.NOENT);
+ break :col Node.ofCol(serial, .dir);
+ },
else => return Reply.fail(req.tag, E.NOTDIR),
},
+ .col => |c| col: {
+ if (c.file != .dir) return Reply.fail(req.tag, E.NOTDIR);
+ const f = std.meta.stringToEnum(ColFile, name) orelse return Reply.fail(req.tag, E.NOENT);
+ if (f == .dir) return Reply.fail(req.tag, E.NOENT);
+ break :col Node.ofCol(c.serial, f);
+ },
.pane => |t| pane: {
_ = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
const f = switch (t.file) {
@@ -548,7 +618,7 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
.root => {
// The guide, then what describes the session, then the
// files that act, then the directories.
- inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| {
+ inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.focus, TopFile.ctl, TopFile.commands, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.layout, TopFile.tag, TopFile.pane, TopFile.col }) |t| {
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName());
}
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, fs.os_root, true, "os");
@@ -568,8 +638,24 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
stageDirent(out, p.gpa, Node.of(serial, .dir), true, name);
}
},
+ .col => for (0..p.ncol) |c| {
+ if (skip > 0) {
+ skip -= 1;
+ continue;
+ }
+ var buf: [16]u8 = undefined;
+ const serial = layout.columnSerial(p, c);
+ stageDirent(out, p.gpa, Node.ofCol(serial, .dir), true, std.fmt.bufPrint(&buf, "{d}", .{serial}) catch unreachable);
+ },
else => return Reply.fail(req.tag, E.NOTDIR),
},
+ .col => |c| {
+ if (layout.columnBySerial(p, c.serial) == null) return Reply.fail(req.tag, E.NOENT);
+ if (c.file != .dir) return Reply.fail(req.tag, E.NOTDIR);
+ inline for (.{ ColFile.tag, ColFile.ctl, ColFile.exec }) |f| {
+ if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, Node.ofCol(c.serial, f), false, f.fileName());
+ }
+ },
.pane => |t| {
const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
const terminal = p.panes[id].?.isTerminal();
@@ -594,7 +680,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply {
// pane it made, so reading it twice answers the same one and
// closing it leaves the pane alone.
.new => {
- const slot = p.freeSlot() orelse return Reply.fail(req.tag, E.NFILE);
+ const slot = p.freeSlot() orelse return failText(req.tag, E.NFILE, std.fmt.bufPrint(&p.fs.ename, "pane/new: " ++ pardes.Messages.no_pane_slots, .{pardes.MAX_PANES}) catch "no pane slots");
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 };
@@ -603,6 +689,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply {
.log => .{ .log = .{} },
else => return .{ .tag = req.tag },
},
+ .col => |c| return if (layout.columnBySerial(p, c.serial) != null) .{ .tag = req.tag } else Reply.fail(req.tag, E.NOENT),
.pane => |t| what: {
const pn = p.panes[p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT)].?;
if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT);
@@ -659,6 +746,7 @@ fn remove(p: *Pardes, req: Req) Reply {
const t = switch (target) {
.pane => |t| t,
.top => return Reply.fail(req.tag, E.PERM),
+ .col => |c| return cols.remove(p, req, c.serial, c.file),
};
if (t.file != .dir) return Reply.fail(req.tag, E.PERM);
const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
@@ -676,8 +764,16 @@ fn release(p: *Pardes, req: Req) Reply {
/// its releases before the connection's slot is reused, so none outlives
/// the connection it came on.
fn releaseHandle(p: *Pardes, req: Req) void {
- const o = openOf(p, req) orelse return;
const t = Node.target(req.node).?;
+ // A truncating write to `tag` is over when its open goes (pane.zig
+ // writeTag): a newline it held back is dropped.
+ if (t == .pane and t.pane.file == .tag) if (p.paneBySerial(t.pane.serial)) |id| {
+ p.panes[id].?.fs.tag_rewrite = false;
+ p.panes[id].?.fs.tag_held_newline = false;
+ };
+ if (t == .col and t.col.file == .tag) cols.released(p, t.col.serial);
+ if (t == .top and t.top == .tag) cols.released(p, null);
+ const o = openOf(p, req) orelse return;
const of: ?*Pane = if (t == .pane) (if (p.paneBySerial(t.pane.serial)) |id| p.panes[id] else null) else null;
if (of) |pn| switch (o.what) {
.event => |reader| {
@@ -712,7 +808,8 @@ fn setattr(p: *Pardes, req: Req, target: Target) Reply {
return failText(req.tag, E.INVAL, pane.e_addr_failed);
if (pane.truncate(p, p.panes[id].?, t.file) != .ok) return Reply.fail(req.tag, E.NOMEM);
},
- .top => {},
+ .col => |c| if (c.file == .tag and cols.truncate(p, c.serial) != .ok) return Reply.fail(req.tag, E.NOENT),
+ .top => |f| if (f == .tag and cols.truncate(p, null) != .ok) return Reply.fail(req.tag, E.NOMEM),
};
return .{ .tag = req.tag, .attr = attrOf(p, target) orelse return Reply.fail(req.tag, E.NOENT) };
}
@@ -757,8 +854,9 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply {
var last: u32 = 0;
while (nextSerialAfter(p, last)) |serial| {
last = serial;
- const pn = p.panes[p.paneBySerial(serial).?].?;
- out.print(p.gpa, "{d} {s} {d} {s}\n", .{ serial, pane.kindOf(pn), @intFromBool(pane.dirtyOf(pn)), pane.nameOf(pn) }) catch {};
+ const id = p.paneBySerial(serial).?;
+ const pn = p.panes[id].?;
+ out.print(p.gpa, "{d} {s} {d} {s} {d}\n", .{ serial, pane.kindOf(pn), @intFromBool(pane.dirtyOf(pn)), pane.nameOf(pn), pane.columnOf(p, id) }) catch {};
}
break :index stagedReply(p, req);
},
@@ -770,6 +868,13 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply {
.focus => ctl.readFocus(p, req),
.ctl => ctl.readRoot(p, req),
.commands => ctl.readCommands(p, req),
+ .layout => layout: {
+ var text: std.Io.Writer.Allocating = .init(p.scratch.allocator());
+ cols.writeLayout(p, &text.writer) catch break :layout Reply.fail(req.tag, E.NOMEM);
+ p.fs.stage(p.gpa).appendSlice(p.gpa, text.written()) catch break :layout Reply.fail(req.tag, E.NOMEM);
+ break :layout stagedReply(p, req);
+ },
+ .tag => cols.read(p, req, null),
// The serial the open handed this fid, so that two reads of one
// fid answer the same pane: the read observes, the open acted.
.new => serial: {
@@ -778,7 +883,7 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply {
break :serial Reply.fail(req.tag, E.NOMEM);
break :serial stagedReply(p, req);
},
- .root, .pane => Reply.fail(req.tag, E.PERM),
+ .root, .pane, .col => Reply.fail(req.tag, E.PERM),
},
.pane => |t| {
// A run's answer outlives the pane it ran in.
@@ -794,6 +899,7 @@ fn readFile(p: *Pardes, req: Req, target: Target) Reply {
if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT);
return pane.read(p, req, id, pn, t.file);
},
+ .col => |c| return if (c.file == .tag) cols.read(p, req, c.serial) else Reply.fail(req.tag, E.PERM),
}
}
@@ -804,6 +910,7 @@ fn write(p: *Pardes, req: Req, target: Target) Reply {
.log => events.writeLog(p, req),
.focus => ctl.writeFocus(p, req),
.ctl => ctl.writeRoot(p, req),
+ .tag => cols.write(p, req, null),
else => Reply.fail(req.tag, E.PERM),
},
.pane => |t| {
@@ -812,6 +919,12 @@ fn write(p: *Pardes, req: Req, target: Target) Reply {
if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT);
return pane.write(p, req, id, pn, t.file);
},
+ .col => |c| return switch (c.file) {
+ .tag => cols.write(p, req, c.serial),
+ .ctl => cols.writeCtl(p, req, c.serial),
+ .exec => cols.writeExec(p, req, c.serial),
+ .dir => Reply.fail(req.tag, E.PERM),
+ },
}
}
@@ -823,6 +936,7 @@ test {
_ = addr;
_ = ctl;
_ = pty;
+ _ = cols;
_ = sources;
_ = screen;
}
@@ -955,12 +1069,13 @@ test "readdir lists the root and a pane directory without creating anything" {
@memcpy(top_bytes[0..top_reply.bytes.len], top_reply.bytes);
const top = th.dirents(top_bytes[0..top_reply.bytes.len], &buf);
try testing.expect(top.len >= 10);
- for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| {
+ for ([_][]const u8{ "README", "index", "status", "focus", "ctl", "commands", "look", "exec", "log", "screen", "listeners", "layout", "tag", "pane", "col", "os" }, 0..) |name, i| {
try testing.expectEqualStrings(name, top[i].name);
- try testing.expectEqual(i >= 11, top[i].dir);
+ try testing.expectEqual(i >= 13, top[i].dir);
}
- try testing.expectEqual(@intFromEnum(TopFile.pane), top[11].node);
- try testing.expectEqual(fs.os_root, top[12].node);
+ try testing.expectEqual(@intFromEnum(TopFile.pane), top[13].node);
+ try testing.expectEqual(@intFromEnum(TopFile.col), top[14].node);
+ try testing.expectEqual(fs.os_root, top[15].node);
var idbuf: [16]u8 = undefined;
try testing.expect(th.nameAt(top, try std.fmt.bufPrint(&idbuf, "{d}", .{serial})) == null);
try testing.expect(th.nameAt(top, "self") == null);
@@ -1099,10 +1214,11 @@ test "opening /pane/new makes a pane and removing one closes it" {
try testing.expect(p.paneBySerial(serial) != null);
try testing.expectEqualStrings("hi", p.panes[id].?.file.?.content);
- // The tree declares no create, so the engine refuses every Tcreate
- // (cloud9 fs.zig: `.tcreate => if (features.create) ... else fail(e_perm)`),
- // and `new` is the only name in /pane that is not a serial.
- try testing.expect(!features.create);
+ // Every create is refused, a pane's that has closed as gone, and `new`
+ // is the only name in /pane that is not a serial.
+ try testing.expectEqual(E.PERM, call(p, .{ .tag = 6, .op = .open, .node = panes_dir, .create = true, .data = "scratch" }).errno());
+ try testing.expectEqual(E.PERM, call(p, .{ .tag = 6, .op = .open, .node = Node.of(serial, .dir), .create = true, .data = "x" }).errno());
+ try testing.expectEqual(E.NOENT, call(p, .{ .tag = 6, .op = .open, .node = Node.of(999_999, .dir), .create = true, .data = "exec" }).errno());
try testing.expectEqual(E.NOENT, look_up(p, panes_dir, "scratch").errno());
try testing.expectEqual(E.NOENT, look_up(p, root, new_pane).errno());
// ...and it is listed, because the user asked to see it in `ls`.
@@ -1190,3 +1306,32 @@ test "editor paths resolve to the same nodes the wire serves" {
try testing.expect(resolveSelf(p, "pane/new/body") == null);
try testing.expectEqual(before, p.next_serial);
}
+
+test "at the pane cap, pane/new, look and New each say so, and look reads back empty" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const new = @intFromEnum(TopFile.new);
+ while (p.freeSlot() != null) try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .open, .node = new }).reply.status);
+ var want: [64]u8 = undefined;
+ const said = try std.fmt.bufPrint(&want, "no pane slots: {d} max", .{pardes.MAX_PANES});
+ const refused = call(p, .{ .tag = 2, .op = .open, .node = new });
+ try testing.expectEqual(E.NFILE, refused.errno());
+ try testing.expect(std.mem.endsWith(u8, refused.reply.ename, said));
+ try testing.expect(th.logHas(p, said));
+ // A look that would open a pane fails its write, and says the same.
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "f\n" });
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)];
+ var line: [4200]u8 = undefined;
+ const look_node = @intFromEnum(TopFile.look);
+ const looked = th.wr(p, look_node, try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{dir}));
+ try testing.expect(looked.errno() != 0);
+ try testing.expect(std.mem.indexOf(u8, looked.reply.ename, said) != null);
+ try testing.expectEqualStrings("", th.rd(p, look_node, 0, 64).bytes);
+ // New from a pane's ctl fails as well.
+ const ctl_node = Node.of(p.panes[0].?.serial, .ctl);
+ const made = th.wr(p, ctl_node, "New\n");
+ try testing.expect(std.mem.indexOf(u8, made.reply.ename, said) != null);
+}
diff --git a/src/panes.zig b/src/panes.zig
index 8ca2bb4d..7aea5898 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -25,7 +25,7 @@ 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, get };
+ pub const Discarding = enum { Exit, Restore, get, Del, Delcol };
/// 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
@@ -135,6 +135,10 @@ pub const Pane = struct {
repl: ?struct { lang: u8, letter: u8 } = null,
/// It has been told that its program takes no bracketed paste.
repl_warned: bool = false,
+ /// Enters owed to a REPL after a bracketed paste (exec.sendToRepl): sent
+ /// with its next output, the paste taken, or at `repl_enter_due_ns`.
+ repl_enter: u8 = 0,
+ repl_enter_due_ns: u64 = 0,
/// A question with its answers in it (`repl_choice`).
question: [96]u8 = undefined,
question_len: u8 = 0,
diff --git a/src/pardes.zig b/src/pardes.zig
index 6294b6cb..54fe2df0 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -250,7 +250,11 @@ pub const pdf_raster_policy: PdfRasterPolicy = switch (platform) {
pub const Pane = panes.Pane;
const Text = panes.Text;
-pub const MAX_PANES = 16;
+/// Pane slots: 64 on a desktop, where a session keeps many files and
+/// terminals open; the board keeps its 16. A slot costs its share of the
+/// fixed tables (MAX_PANES-sized arrays in the core and its surface) whether
+/// or not a pane is in it; a pane itself is allocated when it opens.
+pub const MAX_PANES = if (platform == .esp32p4) 16 else 64;
pub const Layer = @import("Layer.zig");
pub const RowMetrics = Layer.Metrics;
pub const TagHit = Layer.TagHit;
@@ -1638,6 +1642,24 @@ 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));
+ // Into a REPL that takes a bracketed paste (3.13's), the Enter waits
+ // for the REPL to answer the paste: read with it, it would be part of it.
+ p.update(.{ .output = .{ .pane = 1, .bytes = "\x1b[?2004h" } });
+ Sent.drain(p);
+ _ = exec.executeFrom(p, 0, "print(1)", true);
+ Sent.drain(p);
+ try std.testing.expectEqualStrings("\x1b[200~print(1)\x1b[201~", Sent.said(1));
+ p.update(.{ .output = .{ .pane = 1, .bytes = "print(1)" } });
+ Sent.drain(p);
+ try std.testing.expectEqualStrings("\r", Sent.said(1));
+ // Nothing answering, it goes when its wait is up.
+ _ = exec.executeFrom(p, 0, "print(2)", true);
+ p.stepped_ns += 300 * std.time.ns_per_ms;
+ p.update(.tick);
+ Sent.drain(p);
+ try std.testing.expect(std.mem.endsWith(u8, Sent.said(1), "\x1b[200~print(2)\x1b[201~\r"));
+ p.update(.{ .output = .{ .pane = 1, .bytes = "\x1b[?2004l" } });
+ Sent.len[1] = 0;
// 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);
@@ -1684,6 +1706,9 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe
_ = 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})));
+ // A choice not offered is refused, and the question stands.
+ try std.testing.expectEqual(ctlfs.E.INVAL, th.wr(p, ctlfs.Node.of(file.serial, .ctl), "answer z\n").errno());
+ try std.testing.expectEqual(Pane.Prompt.repl_choice, file.prompt);
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());
@@ -3462,6 +3487,11 @@ pub const Mouse = struct {
ctrl: bool = false,
body_hit: ?BodyHit = null,
tag_hit: ?TagHit = null,
+ /// A pixel frontend's press on the rule it draws at the top of this row,
+ /// which is where two panes meet if they do: the one grab that resizes
+ /// them there. A row of text never is one; a terminal, with no rule,
+ /// resizes by the grip.
+ rule_above: bool = false,
};
pub const Key = struct {
@@ -4260,6 +4290,12 @@ pub const Pardes = struct {
/// spring. `lift_serials` says whose it is, so a reused slot starts flat.
lifts: [MAX_PANES]animation.Spring = @splat(.{}),
lift_serials: [MAX_PANES]u32 = @splat(0),
+ /// The focused cursor's quad on its springs (animation.CursorGlide),
+ /// the pane and view origin it was last aimed in, and the last key or
+ /// click, which a blink holds for.
+ cursor_glide: animation.CursorGlide = .{},
+ cursor_view: struct { serial: u32 = 0, line: i64 = 0 } = .{},
+ input_ns: u64 = 0,
animate_theme_changes: bool = false,
native_images: bool = false,
/// The shell can take a changed image a few rows at a time (a texture row
@@ -4771,7 +4807,7 @@ pub const Pardes = struct {
/// 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;
+ const free = p.freeSlot() orelse return p.reportError(from_id, "New", error.NoPaneSlots);
_ = p.newScratch(from_id, free) catch return;
exec.placeNew(p, from_id, from_id, free, kind);
}
@@ -5102,6 +5138,20 @@ pub const Pardes = struct {
p.fs.late_failure_len = @intCast(kept.len);
}
+ /// A Dump the host could not write: said on the message row and in the
+ /// log, and the 9P write that asked for it fails with it, as a failed
+ /// Save's does.
+ pub fn dumpFailed(p: *Pardes, path: []const u8, err: anyerror) void {
+ var what: [limits.host_path_cap + 8]u8 = undefined;
+ const operation = std.fmt.bufPrint(&what, "Dump {s}", .{path}) catch "Dump";
+ p.reportError(p.active, operation, err);
+ var buf: [256]u8 = undefined;
+ const said = std.fmt.bufPrint(&buf, "{s}: {s}", .{ operation, @errorName(err) }) catch "Dump 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.
fn watchPath(p: *const Pardes, id: u8) ?[]const u8 {
const pane = p.panes[id] orelse return null;
@@ -5265,7 +5315,7 @@ pub const Pardes = struct {
p.raw_hover_intent = false;
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);
+ if (reply.status == .err and (req.op == .write or req.op == .setattr or (req.op == .open and reply.errno == ctlfs.E.NFILE) or (req.op == .release and req.remove))) ctlfs.events.noteError(p, req, reply);
// 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
@@ -5311,12 +5361,16 @@ pub const Pardes = struct {
}
switch (ev) {
.key, .mouse => {
+ if (ev == .key or ev.mouse.kind != .motion) p.input_ns = p.now_ns;
for (p.panes) |slot| {
if (slot) |pane| Messages.dismissMessage(p, pane);
}
p.clip_pending = null;
},
- .tick => Messages.advanceMessages(p),
+ .tick => {
+ Messages.advanceMessages(p);
+ for (0..p.panes.len) |id| exec.flushReplEnter(p, id, false);
+ },
else => {},
}
if (p.fs.listeners != 0) p.fs.origin = switch (ev) {
@@ -5360,6 +5414,7 @@ pub const Pardes = struct {
ctlfs.events.notePtyOutput(p, o.pane, o.bytes);
panes.Terminal.feedOutput(p, pane, o.bytes);
ctlfs.pty.noteMarks(p, o.pane, pane);
+ exec.flushReplEnter(p, o.pane, true);
},
.exited => |e| if (p.panes[e.pane]) |pane| {
if (pane.command == null)
@@ -5499,6 +5554,25 @@ pub const Pardes = struct {
/// 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.
+ /// The choices the question pane `id` asks offers, space-separated
+ /// (`k j`, `a b`), or null when it asks none.
+ pub fn questionChoices(p: *const Pardes, id: usize, buf: []u8) ?[]const u8 {
+ const asked = p.panes[id] orelse return null;
+ switch (asked.prompt) {
+ .del_side => return "k j",
+ .repl_choice => {
+ const ask = p.repl_ask orelse return null;
+ var w = std.Io.Writer.fixed(buf);
+ for (ask.offered, 0..) |serial, i| if (serial != 0) {
+ if (w.buffered().len > 0) w.writeByte(' ') catch {};
+ w.writeByte(@intCast('a' + i)) catch {};
+ };
+ return w.buffered();
+ },
+ else => return null,
+ }
+ }
+
pub fn answerQuestion(p: *Pardes, id: usize, choice: ?u8) bool {
const asked = p.panes[id] orelse return false;
switch (asked.prompt) {
@@ -5602,7 +5676,7 @@ 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)) {
- const free = p.freeSlot() orelse return;
+ const free = p.freeSlot() orelse return p.reportError(p.active, "Tty", error.NoPaneSlots);
const nt = p.newShell(free, "") catch return;
nt.greet = true;
// From a tag, as a 9P new is: acme's t->w == nil.
@@ -6438,16 +6512,22 @@ pub const Pardes = struct {
const wait = Messages.messageWake(p, pane) orelse continue;
frames = if (frames) |soonest| @min(soonest, wait) else wait;
};
+ // A paste's Enter waiting on its REPL goes at the end of its wait.
+ for (p.panes) |slot| if (slot) |pane| if (pane.repl_enter > 0) {
+ const wait = @max(1, (pane.repl_enter_due_ns -| p.stepped_ns) / animation.frame_ns);
+ frames = if (frames) |soonest| @min(soonest, wait) else wait;
+ };
return if (frames) |n| p.stepped_ns + n * animation.frame_ns else null;
}
+
/// A render-time animation is moving (a pane's lift): it is sampled at
/// each frame's own `now`, so while it moves the shell draws every loop
/// and its present (vsync) paces it, at the display's rate. `nextWake`
/// is for the stepped animations, which count frames of `frame_ns`.
pub fn continuous(p: *const Pardes) bool {
for (p.lifts) |spring| if (!spring.settled) return true;
- return false;
+ return p.cursor_glide.moving();
}
/// For shells that only need a yes or no: macOS and the board, which
@@ -7001,6 +7081,8 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" {
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.E.INVAL, th.wr(p, ctl, "answer x\n").errno());
+ try std.testing.expectEqual(Pane.Prompt.del_side, p.panes[asked].?.prompt);
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 = ' ' } });
diff --git a/src/regexp.zig b/src/regexp.zig
index 249763c2..97e3dc91 100644
--- a/src/regexp.zig
+++ b/src/regexp.zig
@@ -235,3 +235,26 @@ test "a \\x without two hex digits is refused, not handed to mvzr to panic on" {
var rx = try Regex.compile("\\x41");
try std.testing.expectEqual(@as(usize, 1), (try rx.find("xA", 0, 1, 2)).?.start);
}
+
+test "a repeat inside a group gives back to what follows the group" {
+ const Case = struct { pat: []const u8, text: []const u8, start: usize, end: usize };
+ for ([_]Case{
+ .{ .pat = "a+ab", .text = "aab", .start = 0, .end = 3 },
+ .{ .pat = "(a+)ab", .text = "aab", .start = 0, .end = 3 },
+ .{ .pat = "(a*)ab", .text = "aab", .start = 0, .end = 3 },
+ .{ .pat = "(.+)_area", .text = "the_total_area x", .start = 0, .end = 14 },
+ .{ .pat = "([a-z_]+)_area", .text = "the_total_area x", .start = 0, .end = 14 },
+ .{ .pat = "\\w+_area", .text = "the_total_area x", .start = 0, .end = 14 },
+ .{ .pat = "(\\w)+_area", .text = "the_total_area x", .start = 0, .end = 14 },
+ .{ .pat = "([a-z]+)_area", .text = "the_total_area x", .start = 4, .end = 14 },
+ .{ .pat = "x(ab|a)bc", .text = "xabc", .start = 0, .end = 4 },
+ }) |c| {
+ var rx = try Regex.compile(c.pat);
+ const m = (try rx.find(c.text, 0, c.text.len, c.text.len)) orelse {
+ std.debug.print("no match for {s}\n", .{c.pat});
+ return error.NoMatch;
+ };
+ try std.testing.expectEqual(c.start, m.start);
+ try std.testing.expectEqual(c.end, m.end);
+ }
+}
diff --git a/src/sam_edit.zig b/src/sam_edit.zig
index 679823da..29dec726 100644
--- a/src/sam_edit.zig
+++ b/src/sam_edit.zig
@@ -390,7 +390,11 @@ const Exec = struct {
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});
+ // A miss names the pattern that missed, not the whole Edit line.
+ const r = a.address(dot) orelse return if (std.mem.eql(u8, a.err, addr_lang.e_no_match))
+ fail(ex.why, "no match for regexp {s}", .{e})
+ else
+ 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;
@@ -537,7 +541,10 @@ const Exec = struct {
did = true;
if (!c.global) break;
}
- if (!did and ex.nest == 0) return fail(ex.why, "no substitution", .{});
+ if (!did and ex.nest == 0) return if (r.q0 == r.q1)
+ fail(ex.why, "no substitution: dot is empty", .{})
+ else
+ fail(ex.why, "no substitution", .{});
return r;
}
@@ -679,8 +686,9 @@ 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" },
+ .{ ",x/foo/c/bar/\n/nomatch/d", "no match for regexp /nomatch/" },
.{ ",s/zzz/y/", "no substitution" },
+ .{ "s/foo/y/", "no substitution: dot is empty" },
.{ "k", "unknown command k" },
.{ ",x/a/{\na/1/\ni/2/\n}", "changes out of sequence" },
.{ "}", "right brace with no left brace" },
diff --git a/src/surface.zig b/src/surface.zig
index 15de128f..523cba43 100644
--- a/src/surface.zig
+++ b/src/surface.zig
@@ -412,6 +412,16 @@ pub const Chrome = struct {
grip_focus_ring: [3]u8 = .{ 0, 0, 0 },
/// A dirty pane's button fill.
grip_dirty: [3]u8 = .{ 0, 0, 0 },
+ /// The focused cursor's quad (four corners, x then y, in grid cells, top
+ /// left clockwise): `cursor_glide` bit 1 says there is one, bit 0 that
+ /// it glides, when a pixel shell draws it at `cursor_alpha` in place of
+ /// its cursors (as it does while it blinks, from `cursor_idle_ms`, the
+ /// time since the last key or click when the frame was made; maxInt,
+ /// no blink).
+ cursor_glide: u8 = 0,
+ cursor_alpha: f32 = 1,
+ cursor_quad: [8]f32 = @splat(0),
+ cursor_idle_ms: u32 = std.math.maxInt(u32),
/// The rule between a pane's tag and its body; null is halfway between
/// the tag's ground and the page the shell actually draws.
tag_rule: ?[3]u8 = null,
diff --git a/src/tagline.zig b/src/tagline.zig
index 68fa2645..d024be4e 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -286,10 +286,12 @@ pub fn wrapTag(arena: std.mem.Allocator, whole: []const u8, width: usize) !Shown
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.
+/// The tag's width for wrapping: all its columns. A row the text fills
+/// exactly stays one row, as acme's frame wraps a box only when it is wider
+/// than what is left (libframe frutil.c _frcklinewrap), and the caret past
+/// its end sits at the edge (frptofchar.c clamps it to r.max.x).
pub fn tagWidth(p: *const Pardes, id: usize) usize {
- return @max(1, @as(usize, p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET)) -| 1);
+ return @max(1, @as(usize, p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET)));
}
pub fn shownTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) !Shown {
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 96aec9a9..754b4b96 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -1329,8 +1329,8 @@ const Shell = struct {
fn writeDump(ctx: ?*anyopaque, bytes: []const u8) void {
const s = of(ctx);
var pbuf: [1024:0]u8 = undefined;
- const path = pardes.dump.outPath(&pbuf, s.core.settings.dump_dir.get()) orelse return;
- filesystem.write(s.core, path, bytes) catch |err| return s.core.reportError(0, "dump", err);
+ const path = pardes.dump.outPath(&pbuf, s.core.settings.dump_dir.get()) orelse return s.core.dumpFailed(s.core.settings.dump_dir.get(), error.NoDumpDirectory);
+ filesystem.write(s.core, path, bytes) catch |err| return s.core.dumpFailed(path, err);
s.core.setLastDump(path);
}
diff --git a/src/tutor.txt b/src/tutor.txt
index 0893415e..c59deeae 100644
--- a/src/tutor.txt
+++ b/src/tutor.txt
@@ -132,6 +132,15 @@
DelAbove close it, giving the space to the pane above (Del k)
DelBelow close it, giving the space to the pane below (Del j)
+ RESIZE, AS ACME MOVES A WINDOW
+ the grip (the box left of a pane's tag) drag it: drop it in another
+ column to move the pane there, or up or down its own
+ column to move its top, the pane above taking or giving
+ the rows -- down to its tag alone
+ the rule in the GUI, the line between two panes drags their seam
+ a column's right edge drags the column's width
+ A pane's last row is text like any other, never a handle.
+
A new file opens in a new COLUMN only if both panes would still get
100 columns of width. Otherwise it stacks in the one you are in.
diff --git a/test/fs.py b/test/fs.py
index 3c1564b5..c5199034 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -3,6 +3,7 @@ import contextlib
import fcntl
import json
import os
+import shutil
import signal
from pathlib import Path
import subprocess
@@ -451,6 +452,66 @@ def exit_answers(binary):
raise AssertionError("Exit did not quit the editor")
+def unattended_messages_expire(binary):
+ """With nothing attached, a message leaves after MessageLinger on its own
+ clock: no key, no click, no other request comes to move it along."""
+ with tempfile.TemporaryDirectory(prefix='pardes-msg-') as directory:
+ with session(binary, Path(directory), 'msg') as (client, _):
+ client.write('/pane/1/ctl', b'Msg lingering-words\n')
+ def shown():
+ screen = json.loads(client.read('/screen'))
+ return 'lingering-words' in ''.join(cell[0] for cell in screen['cells'])
+ assert shown()
+ # Reading /screen is itself a request, so wait without asking.
+ time.sleep(2.5)
+ assert not shown(), 'an unattended message outlived MessageLinger'
+
+
+def python_repl_runs_one_line(binary):
+ """A one-line exec sent to Python's own REPL (3.13's pyrepl, which asks
+ for bracketed paste) runs at once, with no second Enter: the Enter
+ reaches it after the paste, not inside it. A real pty, a real python3."""
+ python = shutil.which('python3')
+ if python is None:
+ return
+ version = subprocess.run([python, '-c', 'import sys; print(sys.version_info >= (3, 13))'],
+ capture_output=True, text=True).stdout.strip()
+ if version != 'True':
+ return
+ with tempfile.TemporaryDirectory(prefix='pardes-repl-') as directory:
+ with session(binary, Path(directory), 'repl') as (client, _):
+ def panes(client):
+ return {int(row.split()[0]) for row in client.read('/index').splitlines()}
+ before = panes(client)
+ client.write('/pane/1/ctl', b'Tty\n')
+ deadline = time.monotonic() + 10
+ while time.monotonic() < deadline and panes(client) == before:
+ time.sleep(.05)
+ tty = max(panes(client) - before)
+ def body():
+ return client.read(f'/pane/{tty}/body').decode('utf-8', 'replace')
+ def wait_for(text, seconds=15):
+ deadline = time.monotonic() + seconds
+ while time.monotonic() < deadline:
+ if text in body():
+ return True
+ time.sleep(.1)
+ return False
+ assert wait_for('$') or wait_for('>'), body()
+ client.write(f'/pane/{tty}/pty/data', python.encode() + b' -q\r')
+ assert wait_for('>>>'), body()
+ client.write(f'/pane/{tty}/ctl', b'Repl python\n')
+ # An exec on the REPL's own body goes to the REPL, as its event
+ # record written back does: an empty range acts on its text.
+ fid = client.open(f'/pane/{tty}/event', 2)
+ try:
+ record = b'MX0 0 0 10 print(6*7)\n'
+ client.rpc(118, struct.pack('<IQI', fid, 0, len(record)) + record)
+ assert wait_for('\n42'), body()
+ finally:
+ client.close(fid)
+
+
def signal_cleans_up(binary):
"""A detached session told to stop by a signal unlinks its sockets."""
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]:
@@ -471,6 +532,8 @@ def test(binary, quic=False):
started = time.monotonic()
run_file(binary)
exit_answers(binary)
+ unattended_messages_expire(binary)
+ python_repl_runs_one_line(binary)
signal_cleans_up(binary)
for options, message in [
(['--fs'], b'no such option'),
@@ -665,7 +728,7 @@ def test(binary, quic=False):
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
+ assert (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'\nrestoredcol ') in log, 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/output.zig b/test/output.zig
index e49ec4ff..95cdbfb7 100644
--- a/test/output.zig
+++ b/test/output.zig
@@ -799,13 +799,16 @@ test "n/N skips formatted context and selects comment match locations" {
defer restored.deinit();
pardes.look.armLookWalk(restored, id);
const restored_result = restored.panes[id].?;
- restored.update(.{ .key = .{ .cp = 'n' } });
+ // The dot came back on the match it was on (the first), so the
+ // walk goes on from there.
try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored_result.body.cur_row);
- restored.update(.{ .key = .{ .cp = 'N' } });
+ restored.update(.{ .key = .{ .cp = 'n' } });
try std.testing.expectEqual(@as(i32, @intCast(matches[1])), restored_result.body.cur_row);
+ restored.update(.{ .key = .{ .cp = 'N' } });
+ try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored_result.body.cur_row);
const restored_file = &restored_result.file.?;
- const last = modal.lineSlice(restored_file.content, matches[1]);
- try std.testing.expectEqualStrings(last[0..restored_file.location_rows[matches[1]].location_end], selectedOutputText(restored_result) orelse "");
+ const last = modal.lineSlice(restored_file.content, matches[0]);
+ try std.testing.expectEqualStrings(last[0..restored_file.location_rows[matches[0]].location_end], selectedOutputText(restored_result) orelse "");
}
}
}
@@ -1587,7 +1590,9 @@ test "stacked search and LSP results retain source tint navigation and dump owne
}
restored.active = id;
pardes.look.armLookWalk(restored, id);
- restored.update(.{ .key = .{ .cp = 'N' } });
+ // The dot came back on the match it was on; the walk goes on.
+ try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored.panes[id].?.body.cur_row);
+ restored.update(.{ .key = .{ .cp = 'n' } });
try std.testing.expectEqual(@as(i32, @intCast(matches[1])), restored.panes[id].?.body.cur_row);
try std.testing.expectEqualStrings(source_text, source.file.?.content);
}
diff --git a/test/panes.zig b/test/panes.zig
index f00b35c4..25f0f6db 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -1733,6 +1733,32 @@ 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 "a tag that exactly fills its row stays one row, its caret at the edge as acme's tick" {
+ 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");
+ const width = pardes.tagline.tagWidth(p, 0);
+ // Pad the path until the whole tag is exactly the row.
+ const bare = try pardes.tagline.tagText(p, p.scratch.allocator(), pane);
+ const pad = width - bare.len + pane.file.?.path.len;
+ p.gpa.free(pane.file.?.path);
+ const path = try p.gpa.alloc(u8, pad);
+ @memset(path, 'a');
+ path[0] = '/';
+ pane.file.?.path = path;
+ try std.testing.expectEqual(width, (try pardes.tagline.tagText(p, p.scratch.allocator(), pane)).len);
+ p.update(.{ .key = .{ .cp = ':' } });
+ p.update(.{ .key = .{ .cp = 'A' } });
+ var arena = std.heap.ArenaAllocator.init(p.gpa);
+ defer arena.deinit();
+ const surface = try p.render(arena.allocator());
+ try std.testing.expectEqual(@as(u16, 1), pane.tag_rows);
+ // The layer's caret is past the last character; the grid's, on the pane's
+ // last cell, since it has none past it.
+ try std.testing.expectEqual(@as(u16, @intCast(width)), surface.tag_layers[0].cursor.?.x);
+ try std.testing.expectEqual(p.rects[0].x + p.rects[0].w - 1, surface.cursor.?.x);
+}
+
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();
diff --git a/test/selfmount.py b/test/selfmount.py
index cc889a93..90960bed 100644
--- a/test/selfmount.py
+++ b/test/selfmount.py
@@ -75,7 +75,7 @@ def run(binary):
mount = f'/mnt/9p/pardes/{name}'
with Client(sock) as client:
index = client.read('/index').decode()
- rows = [row.split(maxsplit=3) for row in index.splitlines()]
+ rows = [row.rsplit(maxsplit=1)[0].split(maxsplit=3) for row in index.splitlines()]
first = rows[0][0]
# A Look at the tree's own index: realpath, stat and read all go
# out through 9ns and back in, and the write returns when the
@@ -84,7 +84,7 @@ def run(binary):
client.write('/look', (mount + '/index\n').encode())
took = time.monotonic() - started
check(took < 5, f'look at the mounted index returned ({took:.2f}s)')
- rows = [row.split(maxsplit=3) for row in client.read('/index').decode().splitlines()]
+ rows = [row.rsplit(maxsplit=1)[0].split(maxsplit=3) for row in client.read('/index').decode().splitlines()]
opened = [row for row in rows if len(row) == 4 and row[3] == mount + '/index']
check(len(opened) == 1, 'a pane opened on the mounted index')
if opened:
@@ -94,7 +94,7 @@ def run(binary):
# and a Save: the save writes back through the mount into pane 1,
# and the exec returns once it has.
client.write('/look', f'{mount}/pane/{first}/body\n'.encode())
- rows = [row.split(maxsplit=3) for row in client.read('/index').decode().splitlines()]
+ rows = [row.rsplit(maxsplit=1)[0].split(maxsplit=3) for row in client.read('/index').decode().splitlines()]
mirror = [row for row in rows if len(row) == 4 and row[3] == f'{mount}/pane/{first}/body']
check(len(mirror) == 1, 'a pane opened on the mounted body')
if mirror:
diff --git a/test/snapshots/badutf.golden b/test/snapshots/badutf.golden
index 4490531a..d013a610 100644
--- a/test/snapshots/badutf.golden
+++ b/test/snapshots/badutf.golden
@@ -10,8 +10,8 @@
== 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
-| Collapse Del
+| /tmp/pardes-snap/badutf/cwd/wide.txt Save Tty C
+| ollapse Del
| 1 日本語 日本語 日本語 日本語 日本語 tail
| 2
| 3
diff --git a/test/snapshots/builtins.golden b/test/snapshots/builtins.golden
index 00f53a1b..f40d563e 100644
--- a/test/snapshots/builtins.golden
+++ b/test/snapshots/builtins.golden
@@ -27,7 +27,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
diff --git a/test/snapshots/chordcut.golden b/test/snapshots/chordcut.golden
index d6ea6879..4b26fa2b 100644
--- a/test/snapshots/chordcut.golden
+++ b/test/snapshots/chordcut.golden
@@ -1,13 +1,12 @@
-== snap opened grid=120x31 cursor=7,9
+== snap opened grid=120x31 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/chordcut/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter
-| Collapse Del Collapse Del
+| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C
+| ollapse Del ollapse Del
| ls ls
| f.txt g.txt f.txt g.txt
|
-| /tmp/pardes-snap/chordcut/cwd/f.txt Save Tty Collapse De
-| l
+| /tmp/pardes-snap/chordcut/cwd/f.txt Save Tty Collapse Del
| 1 line 1
| 2 line 2
| 3 line 3
@@ -16,79 +15,78 @@
| 6
|
|
-| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter
-| Collapse Del
+|
+| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C
+| ollapse Del
| ls
| f.txt g.txt
-== 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
+== snap cut grid=120x31 cursor=7,9
+|7: * /tmp/pardes-snap/chordcut/cwd/f.txt Save Tty Collapse Del
+|9: 2 2
+== snap copied grid=120x31 cursor=10,10
+== snap pointpaste grid=120x31 cursor=16,11
+|11: 4 line 4line
+== snap replaced grid=120x31 cursor=11,12
+|12: 5 lline 5
+== snap cancelexec grid=120x31 cursor=25,9
+|9: 2 2echo B''OO g.txt
+== snap execafter grid=120x31 cursor=25,9
+|8: 1 line 1 Exec
+|17: /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C /tmp/pardes-snap/chordcut/cwd (echo B''OO) exit 0 Kill Sa
+|18: ollapse Del ve 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
+== snap cancellook grid=120x31 cursor=25,9
+|8: 1 line 1
+== snap lookafter grid=120x31 cursor=7,23
+|22: /tmp/pardes-snap/chordcut/cwd/g.txt Save Tty Collapse Del
+|23: 1 line 1
+|24: 2 line 2
+|25: 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-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,
+|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-2 d,#1d2824, 3-13 #b2c3b8,#1d2824, 14-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-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-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,
+|7: 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-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-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 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-24 #e5eee5,#0d1410, 25-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-12 #e5eee5,#0d1410, 13-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-16 #e5eee5,#0d1410, 17-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-13 #e5eee5,#0d1410, 14-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-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-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,
+|17: 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-119 #b2c3b8,#1d2824,
+|18: 0-2 d,#1d2824, 3-13 #b2c3b8,#1d2824, 14-62 d,#1d2824, 63-77 #b2c3b8,#1d2824, 78-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-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,
+|22: 0-1 #000000,#add38a, 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,
+|23: 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,
+|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 #59675c,#0d1410, 7-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
-|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,
+|23: 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,
+|24: 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
+|17: $ /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C /tmp/pardes-snap/chordcut/cwd (echo B''OO) exit 0 Kill Sa
|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 36aad675..1baef7ec 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 10
+drag left 11 10
+press middle 11 10
+release middle 11 10
+release left 11 10
+stable 400 5000
+snap cut
+# 1-2 then 1-3 in one hold = copy: line 3 keeps its text, register loaded
press left 8 11
drag left 11 11
press middle 11 11
release middle 11 11
+press right 11 11
+release right 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 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 13
-press right 14 13
-release right 14 13
-release left 14 13
+press left 14 12
+press right 14 12
+release right 14 12
+release left 14 12
stable 400 5000
snap pointpaste
# 1-3 over a fresh selection replaces it with the register
-press left 9 14
-drag left 11 14
-press right 11 14
-release right 11 14
-release left 11 14
+press left 9 13
+drag left 11 13
+press right 11 13
+release right 11 13
+release left 11 13
stable 400 5000
snap replaced
# arm the cancel tests: append `echo B''OO g.txt` to line 2
-press left 10 11
-release left 10 11
+press left 10 10
+release left 10 10
stable 400 5000
key i
text echo B''OO g.txt
@@ -73,50 +73,50 @@ 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 11
-drag middle 19 11
-press right 19 11
-release right 19 11
-release middle 19 11
+press middle 10 10
+drag middle 19 10
+press right 19 10
+release right 19 10
+release middle 19 10
stable 700 8000
snap cancelexec
# the same drag released clean proves the word was live: BOO appears
-press middle 10 11
-drag middle 19 11
-release middle 19 11
+press middle 10 10
+drag middle 19 10
+release middle 19 10
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 11
-drag right 25 11
-press left 25 11
-release left 25 11
-release right 25 11
+press right 21 10
+drag right 25 10
+press left 25 10
+release left 25 10
+release right 25 10
stable 700 8000
snap cancellook
# the same look released clean opens g.txt
-press right 21 11
-drag right 25 11
-release right 25 11
+press right 21 10
+drag right 25 10
+release right 25 10
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 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
+press left 8 24
+drag left 11 24
+press middle 11 24
+release middle 11 24
+press right 11 24
+release right 11 24
+release left 11 24
stable 400 5000
snapstyle chordsel
-press left 8 26
-release left 8 26
+press left 8 25
+release left 8 25
stable 400 5000
snapstyle clickdismiss
# g.txt opened under the lower shell's text and left it three rows: close it,
diff --git a/test/snapshots/colgrip.golden b/test/snapshots/colgrip.golden
index 95a2390f..020661ea 100644
--- a/test/snapshots/colgrip.golden
+++ b/test/snapshots/colgrip.golden
@@ -1,8 +1,8 @@
== 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-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,
+|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-2 d,#1d2824, 3-11 #b2c3b8,#1d2824, 12-49 d,#1d2824, 50-52 d,#26362d, 53-72 #becfc4,#26362d, 73-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,
@@ -28,8 +28,8 @@
== 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 Co /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mod
-| llapse Del ╎ e Filter Collapse Del
+| /tmp/pardes-snap/colgrip/cwd/w.txt S╎ve Tty Col /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode
+| lapse Del ╎ Filter Collapse Del
| 1 x ╎ ls
| ╎ w.txt
| ╎
@@ -52,8 +52,8 @@
| ╎
== 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-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,
+|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-2 d,#1d2824, 3-11 #b2c3b8,#1d2824, 12-38 d,#1d2824, 39-39 #f4c086,#1d2824, 40-49 d,#1d2824, 50-52 d,#26362d, 53-72 #becfc4,#26362d, 73-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,
@@ -76,8 +76,8 @@
|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,6
|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
-|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
+|2: /tmp/pardes-snap/colgrip/cwd/w.txt Sa /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter Co
+|3: ve Tty Collapse Del llapse Del
|4: 1 x ls
|5: w.txt
|6:
@@ -121,8 +121,8 @@
|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-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,
+|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-2 d,#1d2824, 3-21 #b2c3b8,#1d2824, 22-39 d,#1d2824, 40-42 d,#26362d, 43-52 #becfc4,#26362d, 53-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,
@@ -145,8 +145,8 @@
|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,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 C /tmp/pardes-snap/colgrip/cwd/w.txt S
-|3: ollapse Del ave Tty Collapse Del
+|2: /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter Co /tmp/pardes-snap/colgrip/cwd/w.txt Sa
+|3: llapse Del ve Tty Collapse Del
|4: ls 1 x
|5: w.txt
|6:
diff --git a/test/snapshots/cornerdrag-left.golden b/test/snapshots/cornerdrag-left.golden
index e0d2ab51..22bdd5b7 100644
--- a/test/snapshots/cornerdrag-left.golden
+++ b/test/snapshots/cornerdrag-left.golden
@@ -1,13 +1,13 @@
== 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 /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
-| Save Mode Filter Collapse Del Save Mode Filter Collapse Del
+| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
+| ave Mode Filter Collapse Del ave Mode Filter Collapse Del
| ls ls
| w.txt w.txt
|
-| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
-| Save Mode Filter Collapse Del
+| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
+| ave Mode Filter Collapse Del
| ls
| w.txt
|
@@ -16,19 +16,19 @@
|
|
|
-| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
-| Save Mode Filter Collapse Del
+| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
+| ave Mode Filter Collapse Del
| ls
| w.txt
== 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
-|3: Save Mode Filter Collapse Del ╎ Save Mode Filter Collapse Del
+|2: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
+|3: ave Mode Filter Collapse Del ╎ ave 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
+|7: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
+|8: ╎ ave Mode Filter Collapse Del
|9: ╎ ls
|10: ╎ w.txt
|11: ╎
@@ -38,7 +38,7 @@
|15: ╎
|16: ╎
|17: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash ╎
-|18: Save Mode Filter Collapse Del ╎
+|18: ave Mode Filter Collapse Del ╎
|19: ls ╎
|20: w.txt ╎
|21: ╎
@@ -54,13 +54,13 @@
== 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 /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash
-|3: Save Mode Filter Collapse ╎el Save Mode Filter Collapse Del
+|2: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash S /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
+|3: ave Mode Filter Collapse D╎l ave 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
+|7: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S
+|8: ╎ ave Mode Filter Collapse Del
|9: ╎ ls
|10: ╎ w.txt
|11: ╎
@@ -69,8 +69,8 @@
|14: ╎
|15: ╎
|16: ╎
-|17: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash
-|18: Save Mode Filter Collapse ╎el
+|17: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash S
+|18: ave Mode Filter Collapse D╎l
|19: ls ╎
|20: w.txt ╎
|21: ╎
@@ -85,13 +85,13 @@
|30: ╎
== 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/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
+|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Coll
+|3: -left/cwd Tty+bash Save Mod apse Del
+|4: e 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
+|7: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Coll
+|8: apse Del
|9: ls
|10: w.txt
|11:
@@ -100,9 +100,9 @@
|14:
|15:
|16:
-|17: /tmp/pardes-snap/cornerdra
-|18: g-left/cwd Tty+bash Save M
-|19: ode Filter Collapse Del
+|17: /tmp/pardes-snap/cornerdrag
+|18: -left/cwd Tty+bash Save Mod
+|19: e Filter Collapse Del
|20: ls
|21: w.txt
|22:
diff --git a/test/snapshots/cornerdrag.golden b/test/snapshots/cornerdrag.golden
index d70e41e4..730cc2bf 100644
--- a/test/snapshots/cornerdrag.golden
+++ b/test/snapshots/cornerdrag.golden
@@ -1,8 +1,8 @@
== 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
-| Mode Filter Collapse Del ╎ Mode Filter Collapse Del
+| /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save ╎ /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save M
+| ode Filter Collapse Del ╎ ode Filter Collapse Del
| ls ╎ ls
| w.txt ╎ w.txt
| ╎
@@ -17,7 +17,7 @@
| ╎
| ╎
| /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save ╎
-| Mode Filter Collapse Del ╎
+| ode Filter Collapse Del ╎
| ls ╎
| w.txt ╎
| ╎
@@ -32,8 +32,8 @@
| ╎
== 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 /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save
-|3: Mode Filter Collapse Del ╎ Mode Filter Collapse Del
+|2: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save M /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save M
+|3: ode Filter Collapse Del ╎ ode Filter Collapse Del
|4: ls ╎ ls
|5: w.txt ╎ w.txt
|6: ╎
@@ -47,8 +47,8 @@
|14: ╎
|15: ╎
|16: ╎
-|17: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save
-|18: Mode Filter Collapse Del ╎
+|17: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save M
+|18: ode Filter Collapse Del ╎
|19: ls ╎
|20: w.txt ╎
|21: ╎
@@ -63,9 +63,9 @@
|30: ╎
== 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/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
+|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter Collapse
+|3: /cwd Tty+bash Save Mode Fil Del
+|4: ter Collapse Del ls
|5: ls w.txt
|6: w.txt
|7:
@@ -78,9 +78,9 @@
|14:
|15:
|16:
-|17: /tmp/pardes-snap/cornerdra
-|18: g/cwd Tty+bash Save Mode F
-|19: ilter Collapse Del
+|17: /tmp/pardes-snap/cornerdrag
+|18: /cwd Tty+bash Save Mode Fil
+|19: ter Collapse Del
|20: ls
|21: w.txt
|22:
@@ -94,9 +94,9 @@
|30:
== 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/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 ╎
+|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save Mode Filter Collapse
+|3: /cwd Tty+bash Save Mode Fil Del ╎
+|4: ter Collapse Del ls ╎
|5: ls w.txt ╎
|6: w.txt ╎
|7: ╎
@@ -109,9 +109,9 @@
|14: ╎
|15: ╎
|16: ╎
-|17: /tmp/pardes-snap/cornerdra ╎
-|18: g/cwd Tty+bash Save Mode F ╎
-|19: ilter Collapse Del ╎
+|17: /tmp/pardes-snap/cornerdrag ╎
+|18: /cwd Tty+bash Save Mode Fil ╎
+|19: ter Collapse Del ╎
|20: ls ╎
|21: w.txt ╎
|22: ╎
@@ -125,15 +125,15 @@
|30: ╎
== 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 Filte /tmp/pardes-snap/cornerdrag/cwd Tty+
-|3: r Collapse Del bash Save Mode Filter Collapse Del
+|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag/cwd Tty+b
+|3: Collapse Del ash 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: r Collapse Del
+|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter
+|10: Collapse Del
|11: ls
|12: w.txt
|13:
@@ -156,15 +156,15 @@
|30:
== 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 Filte /tmp/pardes-snap/cornerdrag╎cwd Tty+
-|3: r Collapse Del bash Save Mode Filter Colla╎se Del
+|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag╎cwd Tty+b
+|3: Collapse Del ash Save Mode Filter Collap╎e Del
|4: ls ls ╎
|5: w.txt w.txt ╎
|6: ╎
|7: ╎
|8: ╎
-|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte ╎
-|10: r Collapse Del ╎
+|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter ╎
+|10: Collapse Del ╎
|11: ls ╎
|12: w.txt ╎
|13: ╎
@@ -187,15 +187,15 @@
|30: ╎
== 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+
-|3: r Collapse Del ╎ bash Save Mode Filter Collapse Del
+|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte╎ /tmp/pardes-snap/cornerdrag/cwd Tty+b
+|3: Collapse Del ╎ ash 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: r Collapse Del ╎
+|10: Collapse Del ╎
|11: ls ╎
|12: w.txt ╎
|13: ╎
@@ -218,8 +218,8 @@
|30: ╎
== 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 Filte /tmp/pardes-snap/cornerdrag/cwd Tty+
-|3: r Collapse Del bash Save Mode Filter Collapse Del
+|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag/cwd Tty+b
+|3: Collapse Del ash Save Mode Filter Collapse Del
|4: ls ls
|5: w.txt w.txt
|6:
@@ -246,4 +246,4 @@
|27:
|28:
|29:
-|30: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte
+|30: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter
diff --git a/test/snapshots/doccol.golden b/test/snapshots/doccol.golden
index 078f1c04..db2b0074 100644
--- a/test/snapshots/doccol.golden
+++ b/test/snapshots/doccol.golden
@@ -1,25 +1,25 @@
== 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 Col /tmp/pardes-snap/doccol/cwd Tty+bash Save Mode
-| lapse Del Filter Collapse Del
+| /tmp/pardes-snap/doccol/cwd/a.txt Save Tty Coll /tmp/pardes-snap/doccol/cwd Tty+bash Save Mode
+| apse Del Filter Collapse Del
| 1 AAA ls
| a.txt b.txt c.txt
== snap two-docs grid=100x31 cursor=7,19
-|17: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Col
-|18: lapse Del
+|17: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Coll
+|18: apse 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 Col
-|21: lapse Del
+|20: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Coll
+|21: apse 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
+|26: /tmp/pardes-snap/doccol/cwd/c.txt Save Tty Coll
+|27: apse Del
|28: 1 CCC
diff --git a/test/snapshots/dump.golden b/test/snapshots/dump.golden
index deed689c..9220c450 100644
--- a/test/snapshots/dump.golden
+++ b/test/snapshots/dump.golden
@@ -15,10 +15,12 @@
},
.tree_context_tag_style = true,
.columns = .{ .{
+ .serial = 1,
.tag = null,
.weight = 1,
.panes = .{ 0, 1 },
}, .{
+ .serial = 2,
.tag = null,
.weight = 1,
.panes = .{2},
@@ -98,5 +100,10 @@
},
},
.mounts = .{},
+ .settings = .{
+ "MessageAnimation off",
+ "MessageLinger 0",
+ "Shell bash",
+ },
}
== end file
diff --git a/test/snapshots/edit.golden b/test/snapshots/edit.golden
index d4ec9581..c607989f 100644
--- a/test/snapshots/edit.golden
+++ b/test/snapshots/edit.golden
@@ -1,8 +1,8 @@
== 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 F /tmp/pardes-snap/edit/cwd Tty+bash Save Mode F
-| ilter Collapse Del ilter Collapse Del
+| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi
+| lter Collapse Del lter Collapse Del
| ls ls
| e.txt e.txt
|
@@ -16,15 +16,15 @@
|
|
|
-| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode F
-| ilter Collapse Del
+| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi
+| lter 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-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,
+|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-2 d,#26362d, 3-19 #becfc4,#26362d, 20-49 d,#26362d, 50-52 d,#1d2824, 53-69 #b2c3b8,#1d2824, 70-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-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
@@ -38,8 +38,8 @@
|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-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,
+|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-2 d,#1d2824, 3-19 #b2c3b8,#1d2824, 20-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-49 d,#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
index 37415cd7..1e3f916d 100644
--- a/test/snapshots/emptycol.golden
+++ b/test/snapshots/emptycol.golden
@@ -1,17 +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
+| /tmp/pardes-snap/emptycol/cwd/notes.txt Save Tt
+| y 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
+|2: /tmp/pardes-snap/emptycol/cwd/notes.txt Save Tt /tmp/pardes-snap/emptycol/cwd Tty+bash Save Mod
+|3: y Collapse Del e 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
+|2: /tmp/pardes-snap/emptycol/cwd/notes.txt Save Tt
+|3: y Collapse Del
|4: 1 one
|5: 2 two
diff --git a/test/snapshots/ghostty-mid.golden b/test/snapshots/ghostty-mid.golden
index 05d63bc8..ab117f8a 100644
--- a/test/snapshots/ghostty-mid.golden
+++ b/test/snapshots/ghostty-mid.golden
@@ -2,7 +2,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/ghostty-mid/cwd Tty+bash Save /tmp/pardes-snap/ghostty-mid/cwd Tty+bash Save
-| Mode Filter Collapse Del Mode Filter Collapse Del
+| Mode Filter Collapse Del Mode Filter Collapse Del
| ZZPAD ls
| ./g.sh g.txt
| ./g.sh
@@ -17,6 +17,6 @@
|
|
| /tmp/pardes-snap/ghostty-mid/cwd Tty+bash Save
-| Mode Filter Collapse Del
+| Mode Filter Collapse Del
| ls
| g.txt
diff --git a/test/snapshots/hscroll.golden b/test/snapshots/hscroll.golden
index 8ce8b968..ddd7bab7 100644
--- a/test/snapshots/hscroll.golden
+++ b/test/snapshots/hscroll.golden
@@ -1,13 +1,13 @@
== 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 Sav /tmp/pardes-snap/hscroll/cwd Tty+bash Sav
-| e Mode Filter Collapse Del e Mode Filter Collapse Del
+| /tmp/pardes-snap/hscroll/cwd Tty+bash Save /tmp/pardes-snap/hscroll/cwd Tty+bash Save
+| Mode Filter Collapse Del Mode Filter Collapse Del
| ls ls
| wide.txt wide.txt
|
-| /tmp/pardes-snap/hscroll/cwd/wide.txt Sav
-| e Tty Collapse Del
+| /tmp/pardes-snap/hscroll/cwd/wide.txt Save
+| Tty Collapse Del
| 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp
|
|
@@ -16,8 +16,8 @@
|
|
|
-| /tmp/pardes-snap/hscroll/cwd Tty+bash Sav
-| e Mode Filter Collapse Del
+| /tmp/pardes-snap/hscroll/cwd Tty+bash Save
+| Mode Filter Collapse Del
| ls
| wide.txt
== snap wheeled grid=90x31 cursor=7,9
diff --git a/test/snapshots/images.golden b/test/snapshots/images.golden
index 46caaff7..dfa49491 100644
--- a/test/snapshots/images.golden
+++ b/test/snapshots/images.golden
@@ -5,8 +5,8 @@
| ls ls
| pic.ppm pic.ppm
|
-| img petscii:off palette:commodore ascii:on /tmp/pardes-snap/images/cwd/
-| pic.ppm Tty Collapse Del
+| img petscii:off palette:commodore ascii:on /tmp/pardes-snap/images/cwd/p
+| ic.ppm Tty Collapse Del
| x🬱\╲🬌\~▄🬥4▟g*🬺🬶_🬆🬻
| >A8🬌Cq&.\H&🬹🬜C▛🬻
| ><e'🬷y🬙▄Fso🬴🬆🬹
@@ -26,8 +26,8 @@
|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 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,
+|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-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-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,
@@ -52,8 +52,8 @@
|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-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,
+|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-2 d,#26362d, 3-7 #dfbce5,#26362d, 8-24 #becfc4,#26362d, 25-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,
@@ -64,7 +64,7 @@
|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/
+|6: img petscii:off palette:terminal ascii:off /tmp/pardes-snap/images/cwd/p
|8: Ascii
|9: ██████████████🬝🬹
|10: ██████🬛█████🬝▄
diff --git a/test/snapshots/layout-close.golden b/test/snapshots/layout-close.golden
index e080d13e..d708d764 100644
--- a/test/snapshots/layout-close.golden
+++ b/test/snapshots/layout-close.golden
@@ -1,18 +1,18 @@
== 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 Collap /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
-| se Del se Del
+| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
+| e Del e Del
| ls ls
| marker.txt marker.txt
|
-| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
-| se Del
+| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
+| e Del
| ls
| marker.txt
|
-| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
-| se Del
+| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
+| e Del
| ls
| marker.txt
|
@@ -21,50 +21,50 @@
|
|
|
-| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
-| se Del
+| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
+| e Del
| ls
| marker.txt
|
-| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap
-| se Del
+| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
+| e 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/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
+|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: d Tty+bash Save Mode Filter Coll d Tty+bash Save Mode Filter Coll e Del
+|4: apse Del apse 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
+|7: /tmp/pardes-snap/layout-close/cw
+|8: d Tty+bash Save Mode Filter Coll
+|9: apse Del
|10: ls
|11: marker.txt
|12:
|13:
|14:
|15:
-|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
+|22: /tmp/pardes-snap/layout-close/cw
+|23: d Tty+bash Save Mode Filter Coll
+|24: apse Del
+|27: /tmp/pardes-snap/layout-close/cw
+|28: d Tty+bash Save Mode Filter Coll
+|29: apse 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 del grid=140x41 cursor=7,8
+|8: d dk Tty+bash Save Mode Filter C
+|9: ollapse 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
+|2: /tmp/pardes-snap/layout-close/cw /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps
+|3: d Tty+bash Save Mode Filter Coll e Del
+|4: apse Del ls
|5: marker.txt marker.txt
|6:
diff --git a/test/snapshots/layout-open.golden b/test/snapshots/layout-open.golden
index c48f476e..91d41790 100644
--- a/test/snapshots/layout-open.golden
+++ b/test/snapshots/layout-open.golden
@@ -2,7 +2,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/layout-open/cwd Tty+bash Save /tmp/pardes-snap/layout-open/cwd Tty+bash Save
-| Mode Filter Collapse Del Mode Filter Collapse Del
+| Mode Filter Collapse Del Mode Filter Collapse Del
| ls ls
| sub sub
|
@@ -17,26 +17,26 @@
|
|
| /tmp/pardes-snap/layout-open/cwd Tty+bash Save
-| Mode Filter Collapse Del
+| Mode Filter Collapse Del
| ls
| sub
== snap altn-left grid=100x31 cursor=4,11
|7: /tmp/pardes-snap/layout-open/cwd Tty+bash Save
-|8: Mode Filter Collapse Del
+|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
+|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
+|22: /tmp/pardes-snap/layout-open/cwd/sub/f.txt Save
+|23: 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
+|20: sub /tmp/pardes-snap/layout-open/cwd/sub (echo 'TTY
+|21: 'OK) exit 0 Kill Save Collapse Del
+|22: /tmp/pardes-snap/layout-open/cwd/sub/f.txt Save % echo 'TTY'OK
+|23: Tty Collapse Del TTYOK
|24: 1 echo 'TTY'OK exit 0
diff --git a/test/snapshots/load.golden b/test/snapshots/load.golden
index 9dfbcecd..60feb993 100644
--- a/test/snapshots/load.golden
+++ b/test/snapshots/load.golden
@@ -1,8 +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 F /tmp/pardes-snap/load/cwd Tty+bash Save Mode F
-| ilter Collapse Del ilter Collapse Del
+| /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi
+| lter Collapse Del lter Collapse Del
| $ echo LOAD''MARK; seq 1 40; echo E''ND $ ls
| LOADMARK d.txt
| 1 $
@@ -16,8 +16,8 @@
| 9
| 10
| 11
-| /tmp/pardes-snap/load/cwd Tty+bash Save Mode F
-| ilter Collapse Del
+| /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi
+| lter Collapse Del
| $ ls
| d.txt
| $
@@ -25,8 +25,8 @@
== 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-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,
+|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-2 d,#26362d, 3-19 #becfc4,#26362d, 20-49 d,#26362d, 50-52 d,#1d2824, 53-69 #b2c3b8,#1d2824, 70-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-99 #e5eee5,#0d1410,
@@ -40,8 +40,8 @@
|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-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,
+|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-2 d,#1d2824, 3-19 #b2c3b8,#1d2824, 20-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-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
diff --git a/test/snapshots/look-dir.golden b/test/snapshots/look-dir.golden
index a946d119..7f20fdc6 100644
--- a/test/snapshots/look-dir.golden
+++ b/test/snapshots/look-dir.golden
@@ -1,13 +1,13 @@
== 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 Mo /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mo
-| de Filter Collapse Del de Filter Collapse Del
+| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod
+| e Filter Collapse Del e Filter Collapse Del
| ls ls
| sub top.txt sub top.txt
|
-| /tmp/pardes-snap/look-dir/cwd/sub Tty+bash Sav
-| e Mode Filter Collapse Del
+| /tmp/pardes-snap/look-dir/cwd/sub Tty+bash Save
+| Mode Filter Collapse Del
| ls
| inner.txt
|
@@ -16,7 +16,7 @@
|
|
|
-| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mo
-| de Filter Collapse Del
+| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod
+| e Filter Collapse Del
| ls
| sub top.txt
diff --git a/test/snapshots/lookloaded.golden b/test/snapshots/lookloaded.golden
index 462affe8..6644a430 100644
--- a/test/snapshots/lookloaded.golden
+++ b/test/snapshots/lookloaded.golden
@@ -1,13 +1,13 @@
== 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 /tmp/pardes-snap/lookloaded/cwd Tty+bash Save
-| Mode Filter Collapse Del Mode Filter Collapse Del
+| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M
+| ode Filter Collapse Del ode Filter Collapse Del
| ls ls
| big.txt big.txt
|
-| /tmp/pardes-snap/lookloaded/cwd/big.txt Save T
-| ty Collapse Del
+| /tmp/pardes-snap/lookloaded/cwd/big.txt Save Tt
+| y Collapse Del
| 36 line 36
| 37 line 37
| 38 line 38
@@ -16,15 +16,15 @@
| 41 line 41
| 42 line 42
| 43 line 43
-| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save
-| Mode Filter Collapse Del
+| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M
+| ode Filter Collapse Del
| ls
| big.txt
== 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
+|7: /tmp/pardes-snap/lookloaded/cwd/big.txt Save Tt GONE
== snap look-noFS grid=100x31 cursor=14,13
-|8: ty Collapse Del big.txt:10
+|8: y Collapse Del big.txt:10
|9: 6 line 6
|10: 7 line 7
|11: 8 line 8
diff --git a/test/snapshots/lookpanes.golden b/test/snapshots/lookpanes.golden
index 79cbb368..3c407401 100644
--- a/test/snapshots/lookpanes.golden
+++ b/test/snapshots/lookpanes.golden
@@ -1,46 +1,46 @@
== 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 Collap /tmp/pardes-snap/lookpanes/cwd Tty+bash Save Mode Filter Coll
-| se Del apse Del
+| /tmp/pardes-snap/lookpanes/cwd/beta/bmark.txt Save Tty Collaps /tmp/pardes-snap/lookpanes/cwd Tty+bash Save Mode Filter Colla
+| e Del pse 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 Filte
-| r Collapse Del
+| /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filter
+| Collapse Del
| ls
| amark.txt both.txt pick.txt twin.txt zqonly.txt
== 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
+|6: /tmp/pardes-snap/lookpanes/cwd/alpha/zqonly.txt Save Tty Colla
+|7: pse Del /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filter
+|8: 1 ONLY-IN-ALPHA Collapse Del
== snap clicked-wins grid=130x51 cursor=7,11
-|9: /tmp/pardes-snap/lookpanes/cwd/beta/both.txt Save Tty Collaps ls
-|10: e Del amark.txt both.txt pick.txt twin.txt zqonly.txt
+|9: /tmp/pardes-snap/lookpanes/cwd/beta/both.txt Save Tty Collapse ls
+|10: Del amark.txt both.txt pick.txt twin.txt zqonly.txt
|11: 1 BOTH-FROM-BETA
== snap mru-alpha grid=130x51 cursor=72,14
-|12: /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collap
-|13: se Del
+|12: /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collaps
+|13: e Del
|14: 1 PICK-FROM-ALPHA
== snap mru-beta grid=130x51 cursor=7,14
-|12: /tmp/pardes-snap/lookpanes/cwd/beta/twin.txt Save Tty Collaps /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collap
-|13: e Del se Del
+|12: /tmp/pardes-snap/lookpanes/cwd/beta/twin.txt Save Tty Collapse /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collaps
+|13: Del e Del
|14: 1 TWIN-FROM-BETA 1 PICK-FROM-ALPHA
== 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-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,
+|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-2 d,#1d2824, 3-7 #b2c3b8,#1d2824, 8-67 d,#1d2824, 68-74 #b2c3b8,#1d2824, 75-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-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,
+|6: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-39 #b2c3b8,#1d2824, 40-49 #d3b0d9,#1d2824, 50-64 #b2c3b8,#1d2824, 65-65 d,#608269, 66-129 d,#0d1410,
+|7: 0-2 d,#1d2824, 3-9 #b2c3b8,#1d2824, 10-64 d,#1d2824, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824,
+|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-64 d,#0d1410, 65-67 d,#1d2824, 68-80 #b2c3b8,#1d2824, 81-129 d,#1d2824,
+|9: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-46 #d3b0d9,#1d2824, 47-64 #b2c3b8,#1d2824, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
+|10: 0-2 d,#1d2824, 3-6 #b2c3b8,#1d2824, 7-64 d,#1d2824, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410,
|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-129 d,#0d1410,
-|12: 0-1 #000000,#add38a, 2-2 d,#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,
+|12: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-38 #becfc4,#26362d, 39-46 #dfbce5,#26362d, 47-64 #becfc4,#26362d, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824,
+|13: 0-2 d,#26362d, 3-6 #becfc4,#26362d, 7-64 d,#26362d, 65-67 d,#1d2824, 68-72 #b2c3b8,#1d2824, 73-129 d,#1d2824,
|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-86 #e5eee5,#0d1410, 87-129 d,#0d1410,
|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410,
|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410,
diff --git a/test/snapshots/lookrange.golden b/test/snapshots/lookrange.golden
index 7d802b0b..f070a953 100644
--- a/test/snapshots/lookrange.golden
+++ b/test/snapshots/lookrange.golden
@@ -62,7 +62,6 @@
|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
|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,
diff --git a/test/snapshots/lsprelpath.golden b/test/snapshots/lsprelpath.golden
index c8121b5a..4cc687ab 100644
--- a/test/snapshots/lsprelpath.golden
+++ b/test/snapshots/lsprelpath.golden
@@ -117,4 +117,3 @@
|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/modal.golden b/test/snapshots/modal.golden
index 2a329bf8..b452c564 100644
--- a/test/snapshots/modal.golden
+++ b/test/snapshots/modal.golden
@@ -1,8 +1,8 @@
== 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 /tmp/pardes-snap/modal/cwd Tty+bash Save Mode
-| Filter Collapse Del Filter Collapse Del
+| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F
+| ilter Collapse Del ilter Collapse Del
| ls ls
| words.txt words.txt
| printf 'one two three\nfour five six\nseven ei
@@ -16,8 +16,8 @@
|
|
|
-| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode
-| Filter Collapse Del
+| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F
+| ilter Collapse Del
| ls
| words.txt
== snap moved grid=100x31 cursor=8,6
@@ -25,8 +25,8 @@
== 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-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,
+|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-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-48 #f5f7ee,#3c614a, 49-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
@@ -40,8 +40,8 @@
|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-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,
+|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-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-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
diff --git a/test/snapshots/respawn.golden b/test/snapshots/respawn.golden
index e8f94978..d6d21a81 100644
--- a/test/snapshots/respawn.golden
+++ b/test/snapshots/respawn.golden
@@ -1,12 +1,10 @@
-== snap respawn grid=140x31 cursor=4,11
+== snap respawn grid=140x31 cursor=4,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/respawn/cwd Tty+bash Save Mode Filter Collapse De /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse De
-| l l
+| /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del
| ls ls
|
-|$ /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse De
-| l
+|$ /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del
| $ ls
| $ echo RESP''AWNOK
| RESPAWNOK
diff --git a/test/snapshots/rsearch.golden b/test/snapshots/rsearch.golden
index efbd81c9..89c4f7bd 100644
--- a/test/snapshots/rsearch.golden
+++ b/test/snapshots/rsearch.golden
@@ -18,13 +18,11 @@
|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
@@ -36,13 +34,8 @@
== 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
+== snap outside grid=210x31 cursor=20,10
+|25: /tmp/pardes-snap/rsearch/cwd/+Search Save Tty Collapse Del LocationsConfig
+|26: 1 code.txt:4:10-14
+|27: 2 pick two words
+|28: 3
diff --git a/test/snapshots/rsearch.snap b/test/snapshots/rsearch.snap
index 0ce257bd..8c81db1d 100644
--- a/test/snapshots/rsearch.snap
+++ b/test/snapshots/rsearch.snap
@@ -74,13 +74,8 @@ 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
+# acme sized code.txt to its four lines, so line 4 is on its last row: a
+# pane's last row is text like any other, never a resize handle.
# 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
diff --git a/test/snapshots/scroll.golden b/test/snapshots/scroll.golden
index 19db2e0a..5c85a5df 100644
--- a/test/snapshots/scroll.golden
+++ b/test/snapshots/scroll.golden
@@ -2,7 +2,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/scroll/cwd Tty+bash Save Mode /tmp/pardes-snap/scroll/cwd Tty+bash Save Mode
-| Filter Collapse Del Filter Collapse Del
+| Filter Collapse Del Filter Collapse Del
| 189 ls
| 190 s.txt
| 191
@@ -17,7 +17,7 @@
| 200
|
| /tmp/pardes-snap/scroll/cwd Tty+bash Save Mode
-| Filter Collapse Del
+| Filter Collapse Del
| ls
| s.txt
== snap wheel3 grid=100x31 cursor=4,16
@@ -79,8 +79,8 @@
== 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-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,
+|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-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,#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,
@@ -94,8 +94,8 @@
|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-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,
+|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-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-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,
diff --git a/test/snapshots/selarg.golden b/test/snapshots/selarg.golden
index ef240e31..f41c355b 100644
--- a/test/snapshots/selarg.golden
+++ b/test/snapshots/selarg.golden
@@ -2,7 +2,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/selarg/cwd Tty+bash Save Mode /tmp/pardes-snap/selarg/cwd Tty+bash Save Mode
-| Filter Collapse Del Filter Collapse Del
+| Filter Collapse Del Filter Collapse Del
| ZZARG ZZVEE ./c.sh
|
|
@@ -17,7 +17,7 @@
|
|
| /tmp/pardes-snap/selarg/cwd Tty+bash Save Mode
-| Filter Collapse Del
+| Filter Collapse Del
| ls
| s.txt
== snap crosspane grid=100x31 cursor=6,4
diff --git a/test/snapshots/shellset.golden b/test/snapshots/shellset.golden
index 718842a4..0af8f2e1 100644
--- a/test/snapshots/shellset.golden
+++ b/test/snapshots/shellset.golden
@@ -7,7 +7,7 @@
|3: 1 Shell zznosuchshell Shell
== 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 Sav /tmp/pardes-snap/shellset/cwd Tty+bash Sa
-|3: e Tty Collapse Del ve Mode Filter Collapse Del
+|2: /tmp/pardes-snap/shellset/cwd/cmd.txt Save /tmp/pardes-snap/shellset/cwd Tty+bash Sav
+|3: Tty Collapse Del e 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 70d36c6a..552722f9 100644
--- a/test/snapshots/shrinktag.golden
+++ b/test/snapshots/shrinktag.golden
@@ -1,8 +1,8 @@
== 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 M /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M
-| ode Filter Collapse Del ode Filter Collapse Del
+| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
+| de Filter Collapse Del de Filter Collapse Del
| ls ls
| w.txt w.txt
|
@@ -16,13 +16,13 @@
|
|
|
-| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M
-| ode Filter Collapse Del
+| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
+| de Filter Collapse Del
| ls
| w.txt
== snap shrunk grid=100x31 cursor=2,9
-|3: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M ode Filter Collapse Del
-|4: ode Filter Collapse Del ls
+|3: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo de Filter Collapse Del
+|4: de Filter Collapse Del ls
|5: ls w.txt
|6: w.txt
|8:
@@ -33,13 +33,13 @@
== snap move-preview grid=100x31 cursor=2,9
|8: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
== snap restored grid=100x31 cursor=2,9
-|3: ode Filter Collapse Del ode Filter Collapse Del
+|3: de Filter Collapse Del de 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 M
-|18: ode Filter Collapse Del
+|17: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
+|18: de Filter Collapse Del
|19: ls
|20: w.txt
== snap bottom-shrunk grid=100x31 cursor=2,9
@@ -47,4 +47,4 @@
|18:
|19:
|20:
-|30: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M
+|30: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo
diff --git a/test/snapshots/shrinktag.snap b/test/snapshots/shrinktag.snap
index f17c402f..31edad14 100644
--- a/test/snapshots/shrinktag.snap
+++ b/test/snapshots/shrinktag.snap
@@ -1,6 +1,8 @@
-# a pane can shrink to just its tagline: border-drag the handle to the tag row
-# (0 body rows), the gutter box still grabs a move, and dragging back down
-# restores the body intact (the hidden pane's pty/vt is never resized)
+# a pane can shrink to just its tagline: the pane below dragged by its grip up
+# to the tag row (0 body rows) -- a terminal has no rule between panes, so
+# the grip is the handle, as acme's button is -- the gutter box still grabs a
+# move, and dragging back down restores the body intact (the hidden pane's
+# pty/vt is never resized)
file w.txt x
start 31 100 -n 3
wait 8000 w.txt
@@ -16,16 +18,15 @@ key esc
settle 100
stable 400 5000
snap before
-# drag pane 0's bottom handle (its last row) up to its tag row: tag-only pane
-press left 20 17
-drag left 20 3
-release left 20 3
+# drag the lower pane's grip up under pane 0's tag row: tag-only pane
+press left 1 18
+drag left 1 3
+release left 1 3
motion 40 25
stable 500 8000
snap shrunk
-# the shrunk pane's single row is now the pair's resize handle, but the gutter
-# box must still win: grab it, preview a move into the right column, abort by
-# releasing over the pane itself
+# the shrunk pane's gutter box grabs a move: preview one into the right
+# column, abort by releasing over the pane itself
press left 1 3
drag left 70 9
stable 400 5000
@@ -33,17 +34,17 @@ snap move-preview
drag left 20 3
release left 20 3
stable 400 5000
-# re-enlarge: the tag row doubles as the drag handle, pull it back down
-press left 20 3
-drag left 20 17
-release left 20 17
+# re-enlarge: the lower pane's grip, now under pane 0's tag, pulled back down
+press left 1 4
+drag left 1 18
+release left 1 18
motion 40 25
stable 500 8000
snap restored
-# the bottom pane can shrink to ITS tag too: drag past the bottom, clamp holds
-press left 20 17
-drag left 20 31
-release left 20 31
+# the bottom pane can shrink to ITS tag too: its grip dragged past the bottom
+press left 1 18
+drag left 1 31
+release left 1 31
motion 40 25
stable 500 8000
snap bottom-shrunk
diff --git a/test/snapshots/splitparent.golden b/test/snapshots/splitparent.golden
index 5159c1d6..e64df572 100644
--- a/test/snapshots/splitparent.golden
+++ b/test/snapshots/splitparent.golden
@@ -2,13 +2,13 @@
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
| New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
| /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del
-| Mode Filter Collapse Del ls
+| /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,9
|5:
|6: /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-|7: Mode Filter Collapse Del
+|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
@@ -20,11 +20,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/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del
-| Mode Filter Collapse Del ls
+| /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
+| Mode Filter Collapse Del
| ls
|
|
@@ -35,5 +35,5 @@
|
|
| /tmp/pardes-snap/splitparent/cwd Tty+bash Save
-| Mode Filter Collapse Del
+| Mode Filter Collapse Del
| ls
diff --git a/test/snapshots/splitparent.snap b/test/snapshots/splitparent.snap
index 3ec42b14..b9038af2 100644
--- a/test/snapshots/splitparent.snap
+++ b/test/snapshots/splitparent.snap
@@ -5,10 +5,11 @@
config Placement pardes
start 31 100 -n 3
stable 700 20000
-# shrink pane 0 (top-left) to just its tagline
-press left 20 17
-drag left 20 3
-release left 20 3
+# shrink pane 0 (top-left) to just its tagline: the pane below's grip dragged
+# up under it (a terminal has no rule between panes; the grip is the handle)
+press left 1 18
+drag left 1 3
+release left 1 3
motion 40 25
stable 500 8000
snap shrunk
diff --git a/test/snapshots/tagalign.golden b/test/snapshots/tagalign.golden
index f6a8f9d5..8a522ced 100644
--- a/test/snapshots/tagalign.golden
+++ b/test/snapshots/tagalign.golden
@@ -11,8 +11,8 @@
== 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 C
-| ollapse Del
+| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty Co
+| llapse Del
| 1 alpha
| 2 beta
| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del
@@ -21,8 +21,8 @@
== 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
-| Collapse Del
+| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty C
+| ollapse Del
| 1 alpha
| 2 beta
| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del
@@ -38,20 +38,20 @@
| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del
| ls
| deep
-== snap revealed grid=70x31 cursor=31,3
+== snap revealed grid=70x31 cursor=30,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exi
| New Tty Find Grep Joincol Delcol
-| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/docume
-| nt.txt Save Tty Collapse Del
+| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/documen
+| t.txt Save Tty Collapse Del
| 1 alpha
| 2 beta
-| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse D
-| el
+| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse De
+| l
| ls
| deep
== snap alone grid=70x31 cursor=4,6
-|2: /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse D
-|3: el
+|2: /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse De
+|3: l
|4: ls
|5: deep
|6:
diff --git a/test/snapshots/tagalign2col.golden b/test/snapshots/tagalign2col.golden
index f5306f42..a35edccb 100644
--- a/test/snapshots/tagalign2col.golden
+++ b/test/snapshots/tagalign2col.golden
@@ -1,20 +1,20 @@
== 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 D /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del
-| el ls Left
+| /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
+| l ls Left
| 1 alpha deep
| 2 beta
| 3 gamma
== snap aligned grid=160x31 cursor=4,10
-|3: el ls
+|3: l 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 Fil
-|3: 1 alpha ╎ ter Collapse Del
+|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 ╎ er Collapse Del
|4: 2 beta ╎ ls
|5: 3 gamma ╎ deep
|6: ╎
diff --git a/test/snapshots/tagbottom.golden b/test/snapshots/tagbottom.golden
index a86bb8fb..d5f1f720 100644
--- a/test/snapshots/tagbottom.golden
+++ b/test/snapshots/tagbottom.golden
@@ -131,8 +131,8 @@
|27:
|30: /tmp/pardes-snap/tagbottom/cwd/bottom.txt Save Tty Collapse Del
== snap body-cursor grid=100x31 cursor=9,6
-== snap seam-noop grid=100x31 cursor=9,6
-== snap seam-dragged grid=100x31 cursor=9,6
+== snap seam-noop grid=100x31 cursor=12,16
+== snap seam-dragged grid=100x31 cursor=12,12
|11: /tmp/pardes-snap/tagbottom/cwd/top.txt Save Tty Collapse Del bottom.txt
|12: 1 ZZONE
|13: 2 ZZTWO
diff --git a/test/snapshots/tagbottom.snap b/test/snapshots/tagbottom.snap
index a70ace1f..68b79fc7 100644
--- a/test/snapshots/tagbottom.snap
+++ b/test/snapshots/tagbottom.snap
@@ -67,19 +67,21 @@ press left 10 7
release left 10 7
stable 400 8000
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.
+# No row of text is a resize handle: a click on the lower pane's FIRST row
+# (grid 16, click row 17), where the seam used to be, is a click in its text,
+# and both sizes stay.
press left 40 17
release left 40 17
motion 40 25
stable 500 8000
snap seam-noop
-# ...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 17
-drag left 40 13
-release left 40 13
+# A terminal has no rule between panes, so the grip resizes them, as acme's
+# button moves a window: the lower pane's, on its tag at the bottom, dragged up
+# to row 13. The upper pane keeps 10 rows, the lower grows to 19. Both
+# taglines follow their own pane's bottom.
+press left 1 31
+drag left 1 13
+release left 1 13
motion 40 25
stable 500 8000
snap seam-dragged
diff --git a/test/snapshots/tagnav.golden b/test/snapshots/tagnav.golden
index dd982322..3c718db0 100644
--- a/test/snapshots/tagnav.golden
+++ b/test/snapshots/tagnav.golden
@@ -1,13 +1,13 @@
== 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 Col /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
-| lapse Del Filter Collapse Del
+| /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
+| apse 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
+| Filter Collapse Del
| ls Up
| w.txt
== snap colon grid=100x31 cursor=37,2
@@ -21,13 +21,13 @@
== snap left-tag grid=100x31 cursor=43,2
== 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
+|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll$ /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,6
== snap column-tag grid=100x31 cursor=3,1
-|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Col /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode
+|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /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
diff --git a/test/snapshots/theme.golden b/test/snapshots/theme.golden
index a81c4232..919c7ea8 100644
--- a/test/snapshots/theme.golden
+++ b/test/snapshots/theme.golden
@@ -1,8 +1,8 @@
== 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 /tmp/pardes-snap/theme/cwd Tty+bash Save Mode
-| Filter Collapse Del Filter Collapse Del
+| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F
+| ilter Collapse Del ilter Collapse Del
| ls ls
| t.txt t.txt
|
@@ -16,15 +16,15 @@
|
|
|
-| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode
-| Filter Collapse Del
+| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F
+| ilter 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-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,
+|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-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-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
@@ -38,8 +38,8 @@
|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-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,
+|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-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-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
@@ -56,8 +56,8 @@
== 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-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,
+|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-2 d,#3b2f3b, 3-20 #d2c2d1,#3b2f3b, 21-49 d,#3b2f3b, 50-52 d,#2c2630, 53-70 #c6b6c5,#2c2630, 71-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-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,
@@ -71,8 +71,8 @@
|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-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,
+|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-2 d,#2c2630, 3-20 #c6b6c5,#2c2630, 21-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-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,
@@ -89,8 +89,8 @@
== 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-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,
+|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-2 d,#223342, 3-20 #e1eef9,#223342, 21-49 d,#223342, 50-52 d,#15212c, 53-70 #d5e4f1,#15212c, 71-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-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,
@@ -104,8 +104,8 @@
|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-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,
+|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-2 d,#15212c, 3-20 #d5e4f1,#15212c, 21-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-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,
@@ -122,8 +122,8 @@
== 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-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,
+|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-2 d,#dce5dc, 3-20 #4a5a52,#dce5dc, 21-49 d,#dce5dc, 50-52 d,#e8eae3, 53-70 #52625a,#e8eae3, 71-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-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,
@@ -137,8 +137,8 @@
|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-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,
+|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-2 d,#e8eae3, 3-20 #52625a,#e8eae3, 21-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-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,
@@ -155,8 +155,8 @@
== 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-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,
+|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-2 d,#dfeaf3, 3-20 #213e56,#dfeaf3, 21-49 d,#dfeaf3, 50-52 d,#edf2f7, 53-70 #29465e,#edf2f7, 71-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-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,
@@ -170,8 +170,8 @@
|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-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,
+|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-2 d,#edf2f7, 3-20 #29465e,#edf2f7, 21-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-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,
@@ -188,8 +188,8 @@
== 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-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,
+|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-2 d,#d3e7df, 3-20 #35584d,#d3e7df, 21-49 d,#d3e7df, 50-52 d,#e4f0ed, 53-70 #3d6055,#e4f0ed, 71-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-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,
@@ -203,8 +203,8 @@
|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-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,
+|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-2 d,#e4f0ed, 3-20 #3d6055,#e4f0ed, 21-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-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,
@@ -232,8 +232,8 @@
|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-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,
+|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-2 d,#e4f0ed, 3-20 #3d6055,#e4f0ed, 21-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,
@@ -251,8 +251,8 @@
== 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-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,
+|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-2 d,#2d3440, 3-20 #d1d5de,#2d3440, 21-49 d,#2d3440, 50-52 d,#22252b, 53-70 #c5c9d2,#22252b, 71-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-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,
@@ -266,8 +266,8 @@
|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-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,
+|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-2 d,#22252b, 3-20 #c5c9d2,#22252b, 21-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-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,
@@ -284,8 +284,8 @@
== 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-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,
+|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-2 d,#34574d, 3-20 #c7e3d7,#34574d, 21-49 d,#34574d, 50-52 d,#29443f, 53-70 #bbd7cb,#29443f, 71-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-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,
@@ -299,8 +299,8 @@
|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-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,
+|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-2 d,#29443f, 3-20 #bbd7cb,#29443f, 21-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-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,
@@ -317,8 +317,8 @@
== 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-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,
+|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-2 d,#214e50, 3-20 #d3e0c9,#214e50, 21-49 d,#214e50, 50-52 d,#123f43, 53-70 #c7d4bd,#123f43, 71-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-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,
@@ -332,8 +332,8 @@
|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-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,
+|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-2 d,#123f43, 3-20 #c7d4bd,#123f43, 21-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-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,
@@ -350,8 +350,8 @@
== 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-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,
+|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-2 d,#4d4736, 3-20 #e9e4c7,#4d4736, 21-49 d,#4d4736, 50-52 d,#3b3830, 53-70 #ddd8bb,#3b3830, 71-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-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,
@@ -365,8 +365,8 @@
|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-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,
+|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-2 d,#3b3830, 3-20 #ddd8bb,#3b3830, 21-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-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,
@@ -383,8 +383,8 @@
== 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-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,
+|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-2 d,#424d38, 3-20 #e1e4c4,#424d38, 21-49 d,#424d38, 50-52 d,#343b2e, 53-70 #d5d8b8,#343b2e, 71-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-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,
@@ -398,8 +398,8 @@
|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-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,
+|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-2 d,#343b2e, 3-20 #d5d8b8,#343b2e, 21-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-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,
@@ -416,8 +416,8 @@
== 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-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,
+|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-2 d,#424f40, 3-20 #e3e2c2,#424f40, 21-49 d,#424f40, 50-52 d,#343f36, 53-70 #d7d6b6,#343f36, 71-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-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,
@@ -431,8 +431,8 @@
|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-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,
+|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-2 d,#343f36, 3-20 #d7d6b6,#343f36, 21-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-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,
@@ -449,8 +449,8 @@
== 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-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,
+|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-2 d,#2d3440, 3-20 #d1d5de,#2d3440, 21-49 d,#2d3440, 50-52 d,#22252b, 53-70 #c5c9d2,#22252b, 71-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-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,
@@ -464,8 +464,8 @@
|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-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,
+|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-2 d,#22252b, 3-20 #c5c9d2,#22252b, 21-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-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,
@@ -482,8 +482,8 @@
== 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-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,
+|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-2 d,#373e49, 3-20 #d1d5de,#373e49, 21-49 d,#373e49, 50-52 d,#2a2d34, 53-70 #c5c9d2,#2a2d34, 71-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-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,
@@ -497,8 +497,8 @@
|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-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,
+|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-2 d,#2a2d34, 3-20 #c5c9d2,#2a2d34, 21-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-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,
@@ -515,8 +515,8 @@
== 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-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,
+|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-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,#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,
@@ -530,8 +530,8 @@
|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-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,
+|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-2 d,#1d2824, 3-20 #b2c3b8,#1d2824, 21-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-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,
@@ -548,8 +548,8 @@
== 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-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,
+|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-2 d,#121212, 3-20 #949494,#121212, 21-52 d,#121212, 53-70 #949494,#121212, 71-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-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,
@@ -563,8 +563,8 @@
|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-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,
+|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-2 d,#121212, 3-20 #949494,#121212, 21-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-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,
@@ -581,8 +581,8 @@
== 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-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,
+|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-2 d,#2c2a3e, 3-20 #969696,#2c2a3e, 21-52 d,#2c2a3e, 53-70 #969696,#2c2a3e, 71-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-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,
@@ -596,8 +596,8 @@
|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-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,
+|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-2 d,#2c2a3e, 3-20 #969696,#2c2a3e, 21-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-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,
@@ -614,8 +614,8 @@
== 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-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,
+|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-2 d,#eaffff, 3-20 #000000,#eaffff, 21-52 d,#eaffff, 53-70 #000000,#eaffff, 71-99 d,#eaffff,
|4: 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,
@@ -629,8 +629,8 @@
|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-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,
+|17: 0-1 #000000,#b9c3e5, 2-2 d,#eaffff, 3-49 #000000,#eaffff, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
+|18: 0-2 d,#eaffff, 3-20 #000000,#eaffff, 21-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-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
|21: 0-51 d,#ffffea, 52-99 #000000,#ffffea,
@@ -647,8 +647,8 @@
== 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-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,
+|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-2 d,#1e262e, 3-20 #e0e0e0,#1e262e, 21-52 d,#1e262e, 53-70 #e0e0e0,#1e262e, 71-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-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,
@@ -662,8 +662,8 @@
|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-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,
+|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-2 d,#1e262e, 3-20 #e0e0e0,#1e262e, 21-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-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,
diff --git a/test/snapshots/tinywin.golden b/test/snapshots/tinywin.golden
index 082e75c1..d24ab0c5 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 C
+|$ /tmp/pardes-snap/tinywin/cwd Tty+bash Save Mode Filter Co
| /+New Save Tty Collapse Del
== snap columns grid=100x31 cursor=23,3
|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit
diff --git a/test/snapshots/topbarexec.golden b/test/snapshots/topbarexec.golden
index a621dd18..8cb03894 100644
--- a/test/snapshots/topbarexec.golden
+++ b/test/snapshots/topbarexec.golden
@@ -1,8 +1,8 @@
== 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 /tmp/pardes-snap/topbarexec/cwd Tty+bash
-| Save Mode Filter Collapse Del Save Mode Filter Collapse Del
+| /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp/pardes-snap/topbarexec/cwd Tty+bash S
+| ave Mode Filter Collapse Del ave Mode Filter Collapse Del
| ls ls
| zmarkfile.txt zmarkfile.txt
|
@@ -11,14 +11,14 @@
|
|
|
-| /tmp/pardes-snap/topbarexec/cwd Tty+bash
-| Save Mode Filter Collapse Del
+| /tmp/pardes-snap/topbarexec/cwd Tty+bash S
+| ave Mode Filter Collapse Del
| ls
| zmarkfile.txt
== snap noarg-runs grid=90x21 cursor=14,5
|1: New Tty Find Grep Joincol Delcol New ╭────────────────────────────────────╮
-|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash /tmp│pardes resizes=1 │
-|3: Save Mode Filter Collapse Del Save│theme orchard │
+|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp│pardes resizes=1 │
+|3: ave Mode Filter Collapse Del ave │theme orchard │
|4: ls ls │font default │
|5: zmarkfile.txt zmark│terms 3 cols 2 │
|6: │active #0 43x8 │
@@ -28,12 +28,12 @@
|10: ╰────────────────────────────────────╯
== 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 /tmp/pardes-snap/topbarexec/cwd Tty+bash
-|3: Save Mode Filter Collapse Del Save Mode Filter Collapse Del
+|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp/pardes-snap/topbarexec/cwd Tty+bash S
+|3: ave Mode Filter Collapse Del ave Mode Filter Collapse Del
|4: ls ls
|5: zmarkfile.txt zmarkfile.txt
|6:
-|7: /tmp/pardes-snap/topbarexec/cwd/+Search S
-|8: ave Tty Collapse Del
+|7: /tmp/pardes-snap/topbarexec/cwd/+Search Sa
+|8: ve Tty Collapse Del
|9: 1 zmarkfile.txt
|10: 2
diff --git a/test/snapshots/tty.golden b/test/snapshots/tty.golden
index 4034be54..b876ace6 100644
--- a/test/snapshots/tty.golden
+++ b/test/snapshots/tty.golden
@@ -1,8 +1,8 @@
== 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 Fi /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi
-| lter Collapse Del lter Collapse Del
+|$ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil
+| ter Collapse Del ter Collapse Del
| $ ls ls
| m.txt m.txt
| $
@@ -16,8 +16,8 @@
|
|
|
-| /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi
-| lter Collapse Del
+| /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil
+| ter Collapse Del
| ls
| m.txt
== snap tty-echo grid=100x31 cursor=4,8
@@ -25,12 +25,12 @@
|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
+|2: /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil
|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
+|2: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil
|4: $ ls ls
|6: $ echo SNAP''OK
|8: $
@@ -38,6 +38,6 @@
|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
+|17: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil
|19: $ ls
|21: $
diff --git a/test/snapshots/ttyclick.golden b/test/snapshots/ttyclick.golden
index 75eb9b30..a69d951f 100644
--- a/test/snapshots/ttyclick.golden
+++ b/test/snapshots/ttyclick.golden
@@ -1,8 +1,8 @@
== 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 Mo /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo
-| de Filter Collapse Del de Filter Collapse Del
+|$ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod
+| e Filter Collapse Del e Filter Collapse Del
| $ ls ls
| probe.txt probe.txt
| $
@@ -16,13 +16,13 @@
|
|
|
-| /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo
-| de Filter Collapse Del
+| /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod
+| e Filter Collapse Del
| ls
| probe.txt
== snap tty-tagedit grid=100x31 cursor=33,2
== 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 Mo /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo
+|2: ^ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod
|4: ls ls
|6:
diff --git a/test/snapshots/ttyedit.golden b/test/snapshots/ttyedit.golden
index 5e25c810..4411962e 100644
--- a/test/snapshots/ttyedit.golden
+++ b/test/snapshots/ttyedit.golden
@@ -1,8 +1,8 @@
== 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 Mod /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod
-| e Filter Collapse Del e Filter Collapse Del
+| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/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
@@ -16,18 +16,18 @@
|
|
|
-| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod
-| e Filter Collapse Del
+| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode
+| Filter Collapse Del
| ls
| words.txt
== 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
+|2: ^ /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode
|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
+|2: /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode
|10: OPEN
|11: four five six
|12: seven eight nine
diff --git a/test/snapshots/ttyfollow.golden b/test/snapshots/ttyfollow.golden
index bb040593..1527ce86 100644
--- a/test/snapshots/ttyfollow.golden
+++ b/test/snapshots/ttyfollow.golden
@@ -1,8 +1,8 @@
== 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 M /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M
-| ode Filter Collapse Del ode Filter Collapse Del
+| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo
+| de Filter Collapse Del de Filter Collapse Del
| 186 ls
| 187 s.txt
| 188
@@ -16,19 +16,19 @@
| 196
| 197
| 198
-| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M
-| ode Filter Collapse Del
+| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo
+| de Filter Collapse Del
| ls
| s.txt
== 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
+|2: /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo$ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo
|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 M $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M
+|2: $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo$ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo
|4: 293 $ ls
|5: 294 s.txt
|6: 295 $ touch go; until test -f done1; do sleep 0.05;
diff --git a/test/snapshots/ttylook.golden b/test/snapshots/ttylook.golden
index 14c7c0c0..ceb32b50 100644
--- a/test/snapshots/ttylook.golden
+++ b/test/snapshots/ttylook.golden
@@ -1,8 +1,8 @@
== 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 C /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter C
-| ollapse Del ollapse Del
+| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co
+| llapse Del llapse Del
| ls ls
| big.txt plain.txt sub big.txt plain.txt sub
|
@@ -16,15 +16,15 @@
|
|
|
-| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter C
-| ollapse Del
+| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co
+| llapse 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-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,
+|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-2 d,#26362d, 3-12 #becfc4,#26362d, 13-59 d,#26362d, 60-62 d,#1d2824, 63-72 #b2c3b8,#1d2824, 73-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,
@@ -38,8 +38,8 @@
|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-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,
+|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-2 d,#1d2824, 3-12 #b2c3b8,#1d2824, 13-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-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,
@@ -59,13 +59,13 @@
== 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
+|7: /tmp/pardes-snap/ttylook/cwd/sub Tty+bash Save Mode Filte
+|8: r 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
+|22: /tmp/pardes-snap/ttylook/cwd/+Search Save Tty Collapse De
+|23: l 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 6945faec..40e0ae4c 100644
--- a/test/snapshots/ttypaste.golden
+++ b/test/snapshots/ttypaste.golden
@@ -1,8 +1,8 @@
== 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 Mo /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mo
-| de Filter Collapse Del de Filter Collapse Del
+|$ /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod
+| e Filter Collapse Del e Filter Collapse Del
| $ ls ls
| src.txt src.txt
| $ echo BRACKETED
@@ -16,8 +16,8 @@
|
|
|
-| /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mo
-| de Filter Collapse Del
+| /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod
+| e Filter Collapse Del
| ls
| src.txt
== 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 1bbaacf4..0033c78f 100644
--- a/test/snapshots/ttyprompt.golden
+++ b/test/snapshots/ttyprompt.golden
@@ -1,8 +1,8 @@
== 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 M /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M
-| ode Filter Collapse Del ode Filter Collapse Del
+| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo
+| de Filter Collapse Del de Filter Collapse Del
| ls ls
| probe.txt probe.txt
|
@@ -16,15 +16,15 @@
|
|
|
-| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M
-| ode Filter Collapse Del
+| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo
+| de Filter Collapse Del
| ls
| probe.txt
== 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
+|2: $ /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo
|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
+|2: /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo
|4: ls ls
|6: echo ABCDEFGH
diff --git a/test/snapshots/winch.golden b/test/snapshots/winch.golden
index f009aa5d..a1e8afc4 100644
--- a/test/snapshots/winch.golden
+++ b/test/snapshots/winch.golden
@@ -1,8 +1,8 @@
== 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 /tmp/pardes-snap/winch/cwd Tty+bash Save Mode
-| Filter Collapse Del Filter Collapse Del
+|$ /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F
+| ilter Collapse Del ilter Collapse Del
| $ ls ls
| $ for i in $(seq 600); do kill -WINCH $PPID; don
| e; echo SUR''VIVED
@@ -16,14 +16,14 @@
|
|
|
-| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode
-| Filter Collapse Del
+| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F
+| ilter Collapse Del
| ls
== 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 Fil /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Fil
-| ter Collapse Del ter Collapse Del
+|$ /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt
+| er Collapse Del er Collapse Del
| $ ls ls
| $ for i in $(seq 600); do kill -WINCH $PPID; done;
| echo SUR''VIVED
@@ -39,6 +39,6 @@
|
|
|
-| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Fil
-| ter Collapse Del
+| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt
+| er Collapse Del
| ls
diff --git a/test/snapshots/windownav.golden b/test/snapshots/windownav.golden
index 2a3319f9..13fc7ae7 100644
--- a/test/snapshots/windownav.golden
+++ b/test/snapshots/windownav.golden
@@ -1,8 +1,8 @@
== 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 /tmp/pardes-snap/windownav/cwd Tty+bash Save M
-| Collapse Del ode Filter Collapse Del
+| /tmp/pardes-snap/windownav/cwd/w.txt Save Tty C /tmp/pardes-snap/windownav/cwd Tty+bash Save Mo
+| ollapse Del de Filter Collapse Del
| 1 alpha ls Left
| 2 beta w.txt
| 3 gamma
@@ -16,8 +16,8 @@
|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
+|7: /tmp/pardes-snap/windownav/cwd Tty+bash Save Mo
+|8: de Filter Collapse Del
|9: ls
|10: w.txt
== snap up grid=100x31 cursor=7,4
diff --git a/test/snapshots/windowops.golden b/test/snapshots/windowops.golden
index 98454854..38450ac2 100644
--- a/test/snapshots/windowops.golden
+++ b/test/snapshots/windowops.golden
@@ -1,8 +1,8 @@
== 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 M
-| ode Filter Collapse Del ╎ ode Filter Collapse Del
+| /tmp/pardes-snap/windowops/cwd Tty+bash Save M╎ /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo
+| de Filter Collapse Del ╎ de Filter Collapse Del
| ls ╎ ls
| w.txt ╎ w.txt
| ╎
@@ -17,7 +17,7 @@
| ╎
| ╎
| /tmp/pardes-snap/windowops/cwd Tty+bash Save M╎
-| ode Filter Collapse Del ╎
+| de Filter Collapse Del ╎
| ls ╎
| w.txt ╎
| ╎
@@ -32,8 +32,8 @@
| ╎
== 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 M /tmp/pardes-snap/windowops/cwd Tty+bash Save M
-|3: ode Filter Collapse Del ode Filter Collapse Del
+|2: /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo
+|3: de Filter Collapse Del de Filter Collapse Del
|4: ls ls
|5: w.txt w.txt
|6:
@@ -47,8 +47,8 @@
|14:
|15:
|16:
-|17: /tmp/pardes-snap/windowops/cwd Tty+bash Save M
-|18: ode Filter Collapse Del
+|17: /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo
+|18: de Filter Collapse Del
|19: ls
|20: w.txt
|21:
@@ -63,8 +63,8 @@
|30:
== 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 M /tmp/pardes-snap/windowops/cwd Tty+bash Save M
-|3: ode Filter Collapse Del ╎ ode Filter Collapse Del
+|2: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save Mo /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo
+|3: de Filter Collapse Del ╎ de Filter Collapse Del
|4: ls ╎ ls
|5: w.txt ╎ w.txt
|6: ╎
@@ -78,8 +78,8 @@
|14: ╎
|15: ╎
|16: ╎
-|17: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save M
-|18: ode Filter Collapse Del ╎
+|17: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save Mo
+|18: de Filter Collapse Del ╎
|19: ls ╎
|20: w.txt ╎
|21: ╎
@@ -95,15 +95,15 @@
== 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: ash Save Mode Filter Collapse Del ╎ Collapse Del
+|3: sh Save Mode Filter Collapse Del ╎ Collapse Del
|4: ls ╎ ls
|5: w.txt ╎ w.txt
|17: /tmp/pardes-snap/windowops/cwd Tty+b╎
-|18: ash Save Mode Filter Collapse Del ╎
+|18: sh 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+b /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter
-|3: ash Save Mode Filter Collapse Del Collapse Del
+|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter
+|3: sh Save Mode Filter Collapse Del Collapse Del
|4: ls ls
|5: w.txt w.txt
|6:
@@ -112,9 +112,9 @@
|9:
|10:
|11:
-|12: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-|13: /tmp/pardes-snap/windowops/cwd Tty+b
-|14: ash Save Mode Filter Collapse Del
+|12:
+|13: /tmp/pardes-snap/windowops/cwd Tty+ba
+|14: sh Save Mode Filter Collapse Del
|15: ls
|16: w.txt
|17:
@@ -132,19 +132,18 @@
|29:
|30:
== snap move-preview grid=100x31 cursor=4,17
-|12:
== 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
+|18: /tmp/pardes-snap/windowops/cwd Tty+ba
+|19: sh 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+b /tmp/pardes-snap/wi│pardes resizes=1 │
-|3: ash Save Mode Filter Collapse Del Collapse Del │theme orchard │
+|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/wi│pardes resizes=1 │
+|3: sh 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 │
@@ -156,8 +155,8 @@
== 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-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,
+|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-2 d,#1d2824, 3-4 #d3b0d9,#1d2824, 5-34 #b2c3b8,#1d2824, 35-39 d,#1d2824, 40-42 d,#26362d, 43-54 #becfc4,#26362d, 55-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-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,
@@ -167,13 +166,13 @@
|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-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,
+|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-2 d,#1d2824, 3-4 #d3b0d9,#1d2824, 5-34 #b2c3b8,#1d2824, 35-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-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,
+|18: 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,
+|19: 0-2 d,#1d2824, 3-4 #d3b0d9,#1d2824, 5-34 #b2c3b8,#1d2824, 35-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-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
@@ -187,8 +186,8 @@
|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,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: ash Save Mode Filter Collapse Del Collapse Del
+|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter
+|3: sh Save Mode Filter Collapse Del Collapse Del
|4: ls ls
|5: w.txt w.txt
|6:
diff --git a/test/snapshots/windowops.snap b/test/snapshots/windowops.snap
index 32d8c83b..8166f663 100644
--- a/test/snapshots/windowops.snap
+++ b/test/snapshots/windowops.snap
@@ -1,4 +1,4 @@
-# hover resize hints, border drags, move drag, Alt-n, Ctrl-w focus (routed through the Right builtin), Debug box
+# hover resize hints, border drags, grip resize, move drag, Alt-n, Ctrl-w focus (routed through the Right builtin), Debug box
file w.txt x
start 31 100 -n 3
wait 8000 w.txt
@@ -7,7 +7,7 @@ stable 700 20000
motion 50 10
stable 400 5000
snap hover-v
-# hover over the top-left pane's last row: horizontal dash hint
+# hover over the top-left pane's last row: no hint, a row of text is no handle
motion 20 16
stable 400 5000
snap hover-h
@@ -19,10 +19,11 @@ snap border-drag
release left 40 11
stable 500 8000
snap border-done
-# drag the horizontal border up 4 rows (pane 0's new last row)
-press left 20 17
-drag left 20 13
-release left 20 13
+# resize the left pair by the lower pane's grip, 4 rows up (a terminal has no
+# rule between panes; pane 0's last row is now 13)
+press left 1 18
+drag left 1 14
+release left 1 14
stable 500 8000
snap hborder-done
# move pane 1 (its gutter box) into the right column
diff --git a/test/snapshots/wrap.golden b/test/snapshots/wrap.golden
index df41535b..cc834fc5 100644
--- a/test/snapshots/wrap.golden
+++ b/test/snapshots/wrap.golden
@@ -1,8 +1,8 @@
== 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
-| Collapse Del
+| /tmp/pardes-snap/wrap/cwd/long.txt Save Tty C
+| ollapse Del
| 1 0---------1---------2---------3---------↩
| 4---------5---------6---------7---------↩
| 8---------9---------A---------B---------↩