diff options
134 files changed, 5602 insertions, 1811 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index 9473974c..39a6b1ce 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -40,8 +40,9 @@ the `pid`, `version` and `panes` of a session new enough to serve it. machine. It is not the per-pane kernel mount the `Tty9p` builtin makes, which gives one pane's shell `$PARDES_MOUNT`; see [docs/v9fs.md](../../../docs/v9fs.md) for that. Either mountpoint serves the same tree. A `9ns --unix SOCKET -- cmd` -mount exists only inside `cmd`'s private namespace; to share one, use the -registry (`9ns --mntgen`). +mount exists only inside `cmd`'s private namespace, and there `$NINE_MOUNT` is +the session's own root (`$NINE_MOUNT/index`), not a registry; to share one, +use the registry (`9ns --mntgen`). A running session serves whatever binary started it. If a listing does not match this document, that session predates the change; restart it. @@ -53,7 +54,9 @@ $m/README the served guide, worth reading first $m/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name $m/status pid, version, panes $m/look write a line = a right click on it at the active pane (file:/re/, file:#n, :/re/ - select by address, as acme's look; a miss changes nothing) + select by address, as acme's look, from the file's dot: file:0/re/ for the + first match; file:N selects the line; a miss changes nothing, logs err, and + look reads back empty) $m/exec write a line = a middle click: an editor command word, or a shell line $m/pane/<n>/pty/run write one line, read `exit N` + its output, or `busy` / `error ...`, on the same open: exec 3<>$m/pane/$n/pty/run; echo make >&3; cat <&3 @@ -142,9 +145,13 @@ but never a 9P write: a script sends code by writing the REPL pane's `pty/data`, and runs a command from such a file with `Exec <text>` or the tag. A bound terminal's `ctl` line ends with its id (`python-a`); an event record written back from such a file's body goes to its REPL, as the click -would. `Repl -` unbinds; a bare `Repl` says the binding. Multi-line code +would. `Repl -` unbinds; a bare `Repl` says the binding. With several +REPLs bound for a language an exec asks which, logged `ask <serial> repl a b` +(Del's side from the keyboard is `ask <serial> del k j`): answer with +`echo 'answer a' > $m/pane/<serial>/ctl`, or `answer -` to send nothing. Multi-line code written to `pty/data` should be a bracketed paste, `\e[200~<code>\e[201~` -then `\r`: sent line by line, a blank line ends a Python block, and Python +then `\r`, and a second `\r` when the code's last line is indented (a `def` +or `for` body): one Enter leaves such a block open. Sent line by line, a blank line ends a Python block, and Python 3.14's REPL auto-indents each line it is typed. Every refused 9P write adds an `err <serial|-> <file>: <why>` record to `$m/log`; through a mount the write itself only says `Invalid argument`. @@ -158,7 +165,21 @@ Rename everywhere, or any sam edit, is one write: `echo 'Edit ,x/foo/c/bar/' v c a i d s p = m t u` and `{ }` (commands in braces one to a line, so from exec or a tag, not a one-line ctl write). Its changes are one undo step, and one that fails changes nothing and fails the write with acme's words (`Edit: -no substitution`), logged as `err`. `p` and `=` print to the directory's +no substitution`), logged as `err`. A block goes in one write to the pane's +`ctl`, the root `ctl` (the active pane) or `exec`: an `Edit` line takes the +lines after it until its `{` closes or its `a`/`c`/`i` text ends with `.`. +bash's builtin `printf` writes line by line, so use a heredoc or `env printf`: + +```sh +cat > $pane/ctl <<'END' +Edit ,x/area_of/{ +i/[/ +a/]/ +} +END +env printf 'Edit ,x/foo/{\ni/</\na/>/\n}\n' > $pane/ctl +``` + `p` and `=` print to the directory's `+Errors`. Not there: `b B D e r w f X Y`, `< | >`, and `\1`-`\9` in `s`. | Operation | Shell | Python client | @@ -175,7 +196,7 @@ inserts after the first: write `addr` again before each replacement. Truncating `data` is pardes's own (acme ignores OTRUNC and always inserts). | Read the selection | `cat $pane/dot` (offsets), `cat $pane/sel` (text) | the same two reads | | Select the addressed range | `cp $pane/addr $pane/dot` | `client.write(pane + '/dot', client.read(pane + '/addr'))` | -| Reload from disk | `echo get > $pane/ctl` | `client.write(pane + '/ctl', b'get\n')` | +| Reload from disk | `echo get > $pane/ctl` (refused once while there are unsaved edits; again discards) | `client.write(pane + '/ctl', b'get\n')` | | Close it | `rmdir $pane` | `client.remove(pane)` | `addr`, `dot` and `limit` each read the pair of offsets they also accept, which @@ -185,7 +206,10 @@ is why copying one onto another is all that acme's `addr=dot`, `dot=addr` and `12:5,14:1`), whose regexps are mvzr's searched as sam searches: `^`/`$` match at any line's start and end, `.` and `[^...]` never match a newline, the leftmost match wins (the first alternative there, not the -longest). An expression is evaluated from the current address (the last one +longest). In a pattern with `\n`, `^` works only first (`^def .*\n` finds +every def line) and `$` only just before a `\n`; anywhere else the pattern is +refused, not silently unmatched. `^` inside an alternation (`^def|^ `) holds +only where the search starts: fine in `x` over lines or `g`, not mid-line. An expression is evaluated from the current address (the last one written, or just past the last `data` write): `.` is that address, not the selection, `/re/` searches on from its end and wraps unless `limit` is set, `?re?` or `-/re/` searches back, `#100,#50` fails `addresses out of order`, @@ -194,8 +218,11 @@ past a step budget (about 300 ms) fails with `regular expression search gave up, ...`. A failed address says why (`no match for regexp`, `address out of range`) and leaves no address: `data` refuses until the next good one, so a missed target is never -written at the old one. Moving `dot` scrolls the pane to it. `limit` bounds a -search and reads empty until set; truncate it to lift it. +written at the old one. Moving `dot` scrolls the pane to it. `limit` bounds +only the end of a forward search, as in acme, and reads empty until set; +truncate it to lift it. In `12:5` the column counts bytes from 1 and clamps +past the end of the line; a line past the end is `address out of range`. +A refused write repeated the same way is one `err` line counted, `(x4)`. `dirty`, `mark` and `scroll` read `0` or `1` and take `0` or `1`: whether the buffer differs from its file, whether a write pushes an undo point, and whether @@ -293,7 +320,8 @@ Only terminal panes have `pty/`. Write keystroke bytes to `pty/data`, not command. For an interactive application, send its actual input bytes; `b'\x03'` is Ctrl-C, and Ctrl-U is `b'\x15'` where that application supports it. These go to the child terminal, not to Pardes key bindings. `pty/ctl` takes `winsize C R`, -`sig INT` and `exec`, one per line. +`sig INT` and `exec`, one per line. `pty/status` reads one line: the pty's cols, +rows and busy (1 while a command runs or text is typed at the prompt). `client.screen()` returns `cols`, `rows`, `cursor`, `styles` and row-major `cells` of `[grapheme, style_index]`. Reconstruct rows using `cols`; resolve @@ -124,6 +124,8 @@ zig build 9p-test freestanding protocol tests zig build quic-test -Dquic=true optional QUIC transport tests zig build snap scripted input traces against frozen golden grids zig build snap-driver-test retry evidence, strict failures and fixture isolation +zig build monkey random snapshot scripts hunting panics (not a gate) +zig build monkey-test the monkey's generator and panic detector zig build hxdiff differential suite against helix's own behaviour zig build hxdiff-test comparator, allocation and CLI regression checks zig build hxdiff-live compare against a freshly run hx-harness @@ -150,6 +152,19 @@ The benchmark steps — `perf`, `pdf-bench`, `pdf-scroll-bench`, `pdf-sections-b Snapshot scripts can use `snap9p` to capture core cells and styles through the default socket alongside the terminal emulator's independent captures. +`zig build monkey -Dplatform=tty -- <seeds> [steps] [--from=N] [--out=DIR] [--keep]` +writes one random script per seed (keys, leader bursts, typing, clicks, drags, +wheel, tiny resizes, prompts and long prompt text over bad-UTF-8 and wide-glyph +files), runs it through pardes-snap and looks for a panic's `.zig:N:N: 0x` +frame in the report and in the captured grid. A hit keeps the script, its +captures and its log in DIR (default `/tmp/pardes-monkey`); a seed always makes +the same script, so `--from=N` with one seed replays it. A trace longer than +the grid scrolls away: replay the kept script at a larger `start` size. Each +crash found gets a fix and a regression script in `test/snapshots/`. The tty's +tagline is the body's pitch, so what a narrower tagline does is fuzzed by a +unit test instead (`a monkey over notices…` in `src/draw.zig`, longer with +`PARDES_FUZZ_SEED`/`PARDES_FUZZ_STEPS`). + `-Dtest-filter=<text>` applies to every unit-test binary. Use `-Doptimize=ReleaseFast` for performance measurements. To record output and first/warm command runtimes across revisions, run: diff --git a/build.zig.zon b/build.zig.zon index 781571aa..877dfacb 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,8 +9,8 @@ // read-only HTTPS URL is what a manifest can carry. Re-pin with // `zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#<commit>`. .cloud9 = .{ - .url = "git+https://git.sr.ht/~gbrls/cloud9#09b77cf7c13c3384a53594e76f51161cab64e5dd", - .hash = "cloud9-0.1.0-yt86qmwFHgDi2ZU-ZRIyVZshMZ4Ce6A6Iaxu-FtJG2pI", + .url = "git+https://git.sr.ht/~gbrls/cloud9#f35b7ed09d0a170e4ffe55b5286c28f74c9a8760", + .hash = "cloud9-0.1.0-yt86quEKHgCwewf5XRlEZji1JCmdsfufsrZYGZqzHj5_", }, // ZLS as a LIBRARY, not a language server: src/lsp_zls.zig imports the // `zls` module its build.zig publishes and calls the analyser in diff --git a/build/snap.zig b/build/snap.zig index 6b3d2104..7aa06884 100644 --- a/build/snap.zig +++ b/build/snap.zig @@ -49,6 +49,25 @@ pub fn addTty(b: *std.Build, opts: struct { run_snap.has_side_effects = true; const snap_step = b.step("snap", "run the snapshot parity suite (-- --update regenerates goldens)"); snap_step.dependOn(&run_snap.step); + // Random scripts through pardes-snap, hunting panics (test/monkey.zig): + // not a gate, it is slow and random over seeds; a seed replays. + const monkey = b.addExecutable(.{ + .name = "pardes-monkey", + .root_module = b.createModule(.{ + .target = opts.target, + .optimize = opts.optimize, + .root_source_file = b.path("test/monkey.zig"), + .link_libc = true, + }), + }); + const run_monkey = b.addRunArtifact(monkey); + run_monkey.addArtifactArg(snap); + run_monkey.addArtifactArg(opts.exe); + if (b.args) |args| run_monkey.addArgs(args); + run_monkey.has_side_effects = true; + b.step("monkey", "random snapshot scripts hunting panics (-- <seeds> [steps] [--from=N] [--out=DIR])").dependOn(&run_monkey.step); + const monkey_tests = b.addTest(.{ .root_module = monkey.root_module }); + b.step("monkey-test", "test the monkey's generator and panic detector").dependOn(&b.addRunArtifact(monkey_tests).step); const run_driver_test = b.addRunArtifact(snap); run_driver_test.addArg("--self-test"); run_driver_test.has_side_effects = true; diff --git a/docs/config.md b/docs/config.md index 6905446a..0332d854 100644 --- a/docs/config.md +++ b/docs/config.md @@ -322,8 +322,10 @@ name, thirteen required RGB roles (`sel_bg`, `sel_fg`, `tag_bg`, `tag_fg`, `scroll_thumb`), nullable `bg`/`fg`, and either a 16-color RGB `palette` or `null`. Optional nullable RGB roles extend this format: `tag_active_bg`, `tag_active_fg`, `tag_name_fg`, `tag_active_name_fg`, `border`, `empty_col`, `lineno_active`, `search_bg`, `search_fg`, -`diagnostic_error`, `diagnostic_warning`, `diagnostic_info`, and -`diagnostic_hint`. Missing new roles use backward-compatible defaults, so +`diagnostic_error`, `diagnostic_warning`, `diagnostic_info`, +`diagnostic_hint`, `tag_sel_bg`, `tag_rule`, `box_border` and `box_dirty`; +`sweep_bg` and `sweep_fg` take three triples, and `rule_px` and `rail_px` a +pixel count. Missing new roles use backward-compatible defaults, so previously exported files remain valid. [Theme customization](themes.md) describes each role and its fallback. RGB values are three-byte arrays, and hex literals are accepted. The original fields remain required; there is no @@ -534,6 +536,23 @@ the chain empty the pass is bypassed. CRT works in linear light with restrained scanlines, mask, bloom and vignette, and no curvature, so clicks land where they are drawn. +The focused pane can stand off the page (SDL GUI, off by default): + +```text +Lift shadow a soft drop shadow, on the other panes' bodies only +Lift rim a hairline just above the focused tag (light, or shade on a light page) +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 +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) +``` + +`InactiveDim` works everywhere (a grid shows it at once) and never takes a +pair below its own contrast or 4.5. Under `Lift auto` on a dark page it is 30 +while unset. `Motion` sets how every such effect moves: see docs/effects.md. + `EffectCode PanelAscii` or `EffectCode Crt` lists the current backend's build-embedded source paths under `/virtual`. Look opens each full file; no checkout is needed, but the build must carry them (`-Dembed-sources=true`, @@ -614,7 +633,10 @@ writes a timestamped file in the default directory. A restored terminal comes back live: its recorded output (the last MiB of it) is replayed as history, a dim `── restored history ──` line marks where it -ends, and a new shell starts below it in the directory the old one was in. A +ends, and a new shell starts below it in the directory the old one was in, +the same shell it ran (a `Tty bash` pane comes back running bash). A view left scrolled back stays where it was. Only the shell is new; nothing the -old one was running is restarted. The web shell, which has no ptys, still +old one was running is restarted. A command pane comes back finished, with +what it printed and its `exit N`, and is not run again (a command still +running when dumped comes back as `exit ?`). The web shell, which has no ptys, still shows the history alone. diff --git a/docs/effects.md b/docs/effects.md new file mode 100644 index 00000000..3f375b7d --- /dev/null +++ b/docs/effects.md @@ -0,0 +1,80 @@ +# Effects: feel reviews + +**The rule above all the others:** no effect may alter or cover a focus +indicator (the focused pane's tag colour, its grip, the cursor, the +selection) or reduce its contrast. The effects are sugar; the indicators are +how a person knows where they are. Every G stage is reviewed against this. +Lift, for one, falls only on pane bodies and rails, never on a tag, a grip or +a header, and its strength is capped so text and the selection keep min(their +contrast, 4.5) (tests in src/gui/gui.zig). The rule binds effects, not a +theme's structure: the rules between panes (2px, `rule_px`) are the theme's +borders, and the one over a tag band stays in the band's slack, never over +its text. A dim touches only the unfocused +panes, with the same floor, and the focused pane's text is never at less +contrast than theirs (tests in src/draw.zig). + +Each effect of docs/render-pipeline.md §9 lands behind its own switch, off, +and is kept only after a feel review (§8.4): a frame series on the virtual +clock, a recording, and a verdict — keep, polish or drop — with one line of +why. The verdict is the user's. + +| effect | switch | state | review material | verdict | +|---|---|---|---|---| +| 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) | + +## Motion flavours + +`Motion off|crisp|smooth|bouncy|playful` (default smooth, tuned so arrivals +land inside §8.1's 220 ms; bouncy and playful run longer, opt-in) is one parameter set +(animation.Motion) every fx animation reads, so a flavour is data, not a +branch in each effect. Input is never blocked, and a new target always +retargets from the current position and velocity. + +| flavour | timing (ω, length) | follow-through (ζ) | anticipation | squash/stretch | exaggeration (gain) | secondary lag | arcs | +|---|---|---|---|---|---|---|---| +| off | instant | — | — | — | — | — | — | +| crisp | 60, 0.6× (leaves at full speed) | 1 (none) | — | — | 1 | in step | — | +| smooth | 34, 1.1× (slow in, slow out; ≤220 ms) | 1 (none) | — | — | 1 | 0.85 | 0.04 | +| bouncy | 24, 1.4× | 0.35 (overshoots ~30%, settles twice) | — | 0.15 | 1.25 | 0.8 | 0.08 | +| playful | 18, 1.7× | 0.28 (overshoots ~40%) | 0.15 of the move | 0.35 | 1.4 | 0.55 | 0.15 | + +The flavours are distinct by design, not tuning: crisp leaves at full speed +and never passes its mark; smooth takes twice as long and eases in and out; bouncy clearly passes its mark and settles back once or twice; +playful winds up the other way first, flies well past, and stretches along +its path and squashes as it lands, its text with it (the user's choice); a +pointer inverts the same stretched box, and landed, a pane is exactly its +target. A motion only a few pixels big (Lift) is +exaggerated by the flavour's gain so its character shows. The flavours drive +the Lift spring (time-based), the notice drop and the pane moves (PanelSlide, +PanelZoom, PanelVertical opening or moving, whose length scales with the +flavour); a closing pane keeps its effect's own exit. Chart and side-by-side +video: .scratch/render/motion/compare/. + +Which principles each motion uses: + +- **Lift** (G1): timing, slow in / slow out (the spring), follow-through + (bouncy and playful lift past full and settle back), anticipation (playful + dips before it rises; below zero nothing is drawn, so it reads as a beat + before the lift), staging (only the focused pane lifts; nothing else moves + with a focus change), exaggeration (the flavour's gain). Squash, stretch, + arcs and secondary lag have nothing to act on in a lift. +- **Notice drop and pane moves**: timing and follow-through from the flavour's + 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. + + +Notes on G1: the lift and the dim run on one focus spring per pane, at the +Motion flavour's pace, sampled at each frame's own time; while it moves the +GUI draws at the display's rate, and a grid snaps. A notice floats on a lift +of 1 while a shadow or rim is on. InactiveDim under `Lift auto` defaults to +30. The fade is in linear light, so on a dark page it is gentle: at 30 +forge's text goes from 15.5:1 to 11.2:1 and dusk's from 6.4:1 to 4.8:1, +plainly quieter and still easy reading; at 10 forge barely moves (14.1:1). +On a light page the same percent bites far harder (acme: 7.0:1 at 10, the +4.5 floor at 30), which is why `auto` shades there instead of dimming. @@ -61,6 +61,8 @@ private namespace: 9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"' ``` +(Under `9ns --unix`, `$NINE_MOUNT` is that session's root itself.) + A `9ns --unix` mount lives in the private namespace of the command it runs, and nothing outside that command sees it. The mount everyone on the machine shares is the registry one, `9ns --mntgen` (default `/mnt/9p`): every running @@ -127,7 +129,9 @@ Restore puts a new editor under every client, so the write of it is answered and then every connection is hung up, their fids naming the old editor's panes (a 9ns older than cloud9 2a7137c could fail the write with ECONNRESET all the same, when another request's send met the hang-up -before the answer was read; the log is the authority): dial again, and the new +before the answer was read; the log is the authority): dial again -- a 9ns +mount is one such connection, so after a Restore stop it and start 9ns +again, or every file under it fails -- and the new log names the restored panes and `restore <path>`. The answer has 200 ms to leave before the cut, so a slow client may see only the cut; the log's `restore <path>` is what says the @@ -137,16 +141,18 @@ never needed); and `Kill`, which does not quit but stops commands, as acme's does: bare, every command pardes started, and `Kill make ls`, those whose line begins with one of the words. A command pardes started is a command pane's, until its child exits -- Kill -sends its whole process group SIGTERM -- or a line it typed into a terminal (a +sends SIGTERM to its running job and to its shell, so the whole line stops +(of `sleep 30; echo done` the `echo` never runs) -- or a line it typed into +a terminal (a word written to `exec`, a middle click on one, a `pty/run`), from its shell's start mark (C) to its end mark (D), where Kill sends its foreground job SIGTERM -- acme posts the "kill" note, which ends a process -- and never signals the shell itself; in a shell running without job control (`set +m`) the job shares the shell's group, so there is none to signal: Kill says `Kill: no job to signal`, and a write of it to `ctl` fails with that; with nothing running -it says `Kill: nothing running`; and only the -foreground job, so of `sleep 30; echo done` the `echo` still runs once the -`sleep` is stopped -- +it says `Kill: nothing running`; and, of a typed line, only the +foreground job, so of `sleep 30; echo done` typed at a prompt the `echo` +still runs once the `sleep` is stopped -- and reads every setting there is, one a line, in the words a write of it takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir` bare for the default directory, `LocationsConfig ...`), so writing what it reads @@ -249,23 +255,32 @@ same tree without leaving the process. `src/builtins.zig` (`Save`, `Del`, `New`, `Newcol`, `Mount NAME DIAL`, `Unmount NAME`, `Dump`, `Restore`, `Msg TEXT`, `Find`, `Grep`, `Tty`, ...), or anything else, a command line. Written at a terminal at its prompt it - is typed into that shell. From anywhere else -- a file, a scratch, a tag, + is typed into that shell (and a terminal whose shell exits, `exit` typed + or run, closes its pane). From anywhere else -- a file, a scratch, a tag, a terminal whose tty a program holds -- it runs as a command pane: a terminal whose child is the root ctl's `Shell` (fish unless set) run - with `-c` and the line, in the pane's directory, + with `-c` and the line, in the pane's directory, with job control on + (bash, sh, dash, zsh, ksh `-m`; fish `status job-control full`), which shows its output and then `exit N` (its tag reads `<dir> (<line>) running`, then `exit N`), and stays. The command is over when its process exits, as in acme, not when its terminal closes: a job it left in the background prints on below `exit N` until it lets go of the pty, and a - command that lets go of its terminal early runs on to its own exit. The directory's next command runs in - that pane once it is done, below what it showed, after a `% <line>` line; + command that lets go of its terminal early runs on to its own exit. A + background job outlives the command: job control gives it a process + group of its own, so the hangup the kernel sends the terminal's + foreground group when the shell exits misses it. It survives the pane + closing too, but its writes to the terminal then fail, so start one that + must keep writing with `nohup` or its output redirected. The directory's + next command runs in that pane once it is done and nothing holds its pty, below what it showed, after a `% <line>` line; one still running gets a second pane. Before the next command the pane leaves any alternate screen and turns off the modes a program left on (mouse reports, bracketed paste, a hidden cursor); a command that clears the screen and its scrollback (`clear`, ED3) erases the history above it. A command pane's own exec starts the next command there too. The log says `run <serial> <line>` and `exit - <serial> <N|?>`; `exec` reads back the command pane's serial; Kill ends - its whole process group; a line is at most 1 KB. A misspelled word is a + <serial> <N|?>`; `exec` reads back the command pane's serial; Kill stops + its whole line; a line is at most 1 KB. To run a command again, execute + its line again from its directory: `echo 'make test' > pane/<n>/exec` on + the command pane runs it there, below the last run. A misspelled word is a command that says so and ends `exit 127`. `echo Tty > pane/<n>/ctl` makes an interactive terminal in that pane's directory. @@ -296,7 +311,9 @@ the tag is how to run `make` from that file), `Exec <text>` run by name the text, looked at or clicked (`# @`pytest -x`` in a script). A 9P `exec` is no gesture and is never sent: a script writes to the REPL pane's `pty/data`, multi-line code as a bracketed paste (`\e[200~<code>\e[201~` -then `\r`), since line by line a blank line ends a Python block and Python +then `\r`, and a second `\r` when its last line is indented: one Enter +leaves a block open, pasted or typed; a middle click sends that second one +itself), since line by line a blank line ends a Python block and Python 3.14's REPL auto-indents each line typed into it. The REPL gets the text wherever it is -- at a `pdb` or `input()` prompt too. Bindings are not dumped, so a Restore leaves none. @@ -312,12 +329,18 @@ not the hits buffer it opened. A look takes acme's addresses after a colon (editors/acme/look.c:450): a line written to `look` as `file:/re/`, `file:#n`, `file:$` or any address -opens (or finds) the file and selects what the address names, evaluated -from its dot; `:addr` does the same in the pane itself. `file:12` and -`file:12:5` stay line and column. A bare `/re/` is a path, as in acme, and -failing that a search for its text. A look that finds nothing, or an -address that does not evaluate, says so on the message row and changes -nothing: no buffer opens and the selection stays. +opens (or finds) the file and selects what the address names. **The +address is evaluated from the file's dot**, as acme's is: `file:/re/` +finds the next match after the current selection, not the first in the +file. For the first, start at the top: `file:0/re/` (or `file:#0/re/`). +`:addr` does the same in the pane itself, and a pattern may hold blanks +(`calc.py:/return a/`). `file:12` selects line 12, its newline included, +as acme's does; `file:12:5` puts the caret at line 12, column 5. A bare +`/re/` is a path, as in acme, and failing that a search for its text. A +look that finds nothing, an address that does not evaluate, or a line past +the file's end (`calc.py:99`) says so on the message row and in the log +(`err <serial> look: ...`), focuses and opens nothing, keeps the +selection, and leaves `look` reading back empty. `/pane/<n>/name` reads the pane's file name (a terminal's directory) and writing it renames the buffer; a relative name resolves against the pane's @@ -344,7 +367,13 @@ tab width — followed by rio's `current` or `notcurrent` (rio(4), `wctl`): whether the pane has the keyboard. It takes the pane's builtins (below), `get`, which reloads the buffer from the name it carries (unsaved edits are refused once, `<name>: Modified (get again to -discard)`, as acme's get asks winclean, exec.c:513), and acme's `lock` and `unlock` (editors/acme/xfid.c:603-611), for an +discard)`, as acme's get asks winclean, exec.c:513), `answer <choice>` +for the question the pane asks on its notice band, which the log names as +`ask <serial> <what> <choices>` -- `ask 4 del k j` for Del's side from the +keyboard (`k` the pane above takes the rows, `j` the one below), `ask 4 +repl a b` for which bound REPL takes an exec (a REPL's letter) -- `answer -` +taking it back as Esc does (with no question standing, `answer` is refused), +and acme's `lock` and `unlock` (editors/acme/xfid.c:603-611), for an edit of several writes to `addr` and `data` that another client must not land in the middle of. As in acme the lock binds only the clients that take it: a `lock` while another open holds it fails at once with `file in use` @@ -365,14 +394,19 @@ they also accept, so copying one onto another is all that acme's `addr=dot`, address expression (`#0,#5`, `/pattern/`, `2+1`, and pardes's own `12:5`, below); `addr` selects what `data` and `xdata` read or replace, `dot` is the editor's own selection and moving it -scrolls the pane into view, and `limit` bounds a search and reads empty until -it is set. Truncating `dot` empties it, truncating `limit` lifts it, and +scrolls the pane into view, and `limit` bounds only the end of a forward +search, as acme's does, and reads empty until it is set. Truncating `dot` empties it, truncating `limit` lifts it, and truncating `addr` leaves it as it is (below). A rename everywhere, or any other sam edit, is one write to the pane's `ctl`: `Edit ,x/foo/c/bar/` runs acme's Edit (docs/tags.md) on the body as one undo step; a failure fails the write with acme's words and changes -nothing. +nothing. A write is one message a line, except that an `Edit` line takes +the lines after it while its `{` group is open or its `a`, `c` or `i` text +block waits for its `.` line, on a pane's `ctl`, the root's (at the active +pane) and `exec` alike; an unclosed group is refused (``unmatched `{'``). +bash's builtin `printf` writes a line at a time, so write a block with a +heredoc or `env printf`. `line:col` is a pardes extension to sam's addresses, the spelling Look takes in `file:12:5`: `12:5` is the point at line 12, column 5, and it @@ -406,7 +440,15 @@ lazy `*?` included), searched the way sam searches (editors/acme/regx.c): as lines, so `^` and `$` match at the start and end of any line, `.` and a negated class never match a newline, and `$` also matches at the end of a text with no final newline. A pattern that names a newline (`\n`) runs over -the whole text instead, its `.` kept to one line. An expression is +the whole text instead, its `.` kept to one line; there a leading `^` still +matches at every line start, and `$` may stand just before a `\n` (where it +changes nothing). Any other `^` or `$` in such a pattern, `(^|\n)def` or +`a\nb$`, is refused with `in a pattern with \n, ^ can only come first and $ +only just before a \n`, since mvzr would read it as the start or end of the +whole text: never a search that silently finds nothing. Within a line, `^` +inside an alternation (`^def|^ `) matches only where the search starts, so it +works from a line's start (an `x` over lines, a `g` on one) and not from its +middle: an mvzr limit. An expression is evaluated from the current address, the range last written to `addr` (or left by the last `data` write, just past it), as acme evaluates it from `w->addr` (xfid.c:446): `.` is that address, not the selection (`dot` is @@ -448,13 +490,15 @@ The three flag files `dirty`, `mark` and `scroll` read `0` or `1` and take an undo point (writing `1` pushes one now), and whether a write scrolls the pane. -`tag` reads the whole tag as the pane shows it: the computed path, dirty -marker or PDF page, then the text you may edit. A write appends to that text, +`tag` reads the whole tag as the pane shows it: the computed path or PDF +page (no mark for unsaved text: the grip shows that, and `dirty` says it), +then the text you may edit. A write appends to that text, newlines included, and a tag with more than one line takes a row per line on screen; truncating `tag` clears it, as acme's `cleartag` does -- the default words (`Del`, `Put` and the rest) with it, since they are that text until you edit it, so `echo Make > tag` leaves only `Make`; append with `>>` to -keep them. The clearing is an edit of the tag like a typed one and its undo +keep them, with `printf ' Make' >> tag`: `echo` ends its word with a +newline, which starts a new line of the tag. The clearing is an edit of the tag like a typed one and its undo history is kept: `u` in the tag brings back the text it cleared, words included. @@ -472,9 +516,13 @@ an open renders its frame; stat the entry. `/log` is one ring (64 KiB) that records whether or not anyone reads it: `new`, `del`, `rename` (a terminal's too, as its shell changes directory, -since a terminal is named by its directory) and `save <serial> <name>`, +since a terminal is named by its directory), `exit <serial> <N>` before +the `del` of a terminal whose shell exited by itself, `ask <serial> <what> +<choices>` when a pane asks a question (answered by `answer` on its ctl), +and `save <serial> <name>`, `dump <path>` when a Dump is written and `restore <path>` in a Restore's -new log after its panes' `new`s, and `msg <serial|-> <text>` +new log after its panes' `new`s, then `restored <old> <new>` for each pane, +mapping the serial it had to the one it has now, and `msg <serial|-> <text>` for every line the editor says, repeats included (with `verbose` on, that includes each builtin announcing itself as it runs), and `err <serial|-> <file>: <why>` for every write or truncation the tree refused or that @@ -497,8 +545,11 @@ pane is being made can precede that pane's `new`; panes present at boot are recorded before anything else. Control characters in a record become spaces, so a record is one line. (An `event` record is not: acme's `<origin><action><q0> <q1> <flag> <n> -<text>\n`, whose text may hold newlines; read `n` bytes of it rather than -up to a newline -- bytes here, where acme counts runes. Every offset and +<text>\n`, whose text may hold newlines. The origin is `E` (a 9P write to body or tag), `F` (other +files, the editor's own lines), `K` (the keyboard) or `M` (the mouse); the +action's case says where: `x`/`l` a click executed or looked at in the tag, +`X`/`L` in the body, `I`/`D` text put in or taken out of the body, `i`/`d` +of the tag. Read `n` bytes of the text, not up to a newline -- bytes here, where acme counts runes. Every offset and count pardes serves is in bytes, `#n` and `q0`/`q1` too; the event count follows them rather than switch alone, so an acme library reads pardes correctly for ASCII text and not beyond it. A `#n` that falls inside a @@ -545,8 +596,14 @@ are read so that the answer costs the editor a bounded amount. `exit ?` is a command whose end mark carried no status, which is not a success; `error out of memory` is an answer that could not be made. The header is always the whole first line. It reads -`busy` at once when a command is running or text is typed at the prompt, -which is also when the third field of `pty/status` reads 1. A line written +`busy` at once when a command is running or text is typed at the prompt +-- a run is a line typed at the shell's prompt, so a program holding the +terminal (a REPL, `less`) takes none: write to `pty/data` for it -- +which is also when the third field of `pty/status` reads 1. `pty/status` is +one line, three right-aligned fields and a newline: the pty's columns and +rows, then busy (0 or 1). The size, and `pty/ctl`'s `winsize` read back, is +what `winsize C R` last set, until the pane itself resizes and gives the pty +its grid again. A line written before a new terminal's shell has drawn its first prompt is not busy: it waits for that prompt (a respawn meanwhile keeps it waiting for the new shell's) and is sent then, so the first command a script gives a fresh @@ -555,8 +612,13 @@ could not instrument, or a startup that hangs) leaves such a line waiting for ever: cancel the read (interrupt it, or close the open) to give up; `error not run` when the shell refused the line without running it (a fish syntax error; the line is taken back off -the prompt); `error shell gone` when the pane closed or its shell was -replaced; `error no prompt marks` for a shell pardes could not instrument; +the prompt): the shell's marks say only that it did not run, so the answer +carries no reason or code, and the shell's own complaint is in the pane's +body (`tail body`); `exit N` and what it printed when the line ended the shell +itself (`exit 3`, or `echo bye; exit 3`): its terminal closes, and a read +of the run's open still answers after the pane is gone; `error shell gone` +when the pane closed or its shell was replaced, or the shell went without +an exit status to tell; `error no prompt marks` for a shell pardes could not instrument; `error command done; not a shell` (or `error a command runs here, not a shell`) on a command pane, whose child is its command. It relies on the OSC 133 marks pardes injects into bash and fish, tagged diff --git a/docs/macos.md b/docs/macos.md index 5166e2d5..cf7091e6 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -355,8 +355,8 @@ The dirty half needed one core watermark. `File` counted `revision` but never recorded which edit was last *written*, so no shell could derive "unsaved" — `File.saved_revision` is that watermark, advanced by `Save` at the moment the write is asked for and by a successful external-file reload whose bytes -already came from disk. The core renders the same answer as ` *` after the -path in every tagline, while this host also puts it in the native close button. +already came from disk. The core shows the same answer on each pane's grip +button, while this host also puts it in the native close button. Save is marked at ask-time rather than on completion because `save_file` carries none back, which makes both indicators exactly as honest as the Save request. diff --git a/docs/selections.md b/docs/selections.md index 5cc1df1a..0817c215 100644 --- a/docs/selections.md +++ b/docs/selections.md @@ -121,3 +121,30 @@ the replay. line is its own haystack, so `^`/`$` hold at every line and a class like `\s` does not reach the newline unless the pattern names `\n`. - Undo restores only the primary (above). + +## The mouse + +A B1, B2 or B3 sweep selects a stream, as acme's does (libframe +frselect.c:115): from the press to the end of its line, the lines between +whole, the last from its start to the release, the character under the +pointer included. A plain B1 click puts the caret on its cell. A second +plain click at the same cell within half a second is acme's double-click +(plan9port acme text.c:1407, textdoubleclick): just after `{ [ ( < «` or +just before their closers it selects up to the match, nested pairs +counted; at a line's start or end the whole line; just inside `' " \`` +the quoted text; anywhere else the word (letters, digits, `_` and any +non-ASCII character). It works in bodies, tags and a terminal in normal +mode; a terminal whose program has the tty gets its own clicks. + +A B2 or B3 pressed while B1 holds a sweep is acme's chord: B1-B2 cuts, +B1-B3 pastes over it. Everything done while B1 stays down is one undo +step, as acme marks the file once per B1 hold (text.c:881, textselect), so +B1-B2 then B1-B3 is a copy: the text cut and pasted back, the cut text +left in the register. + +A B1 sweep held past a pane's top (on its tag) or bottom (past it, or on +its last row when the pane reaches the screen's bottom, which the pointer +cannot leave) scrolls the body a line a tick, as acme's frselect scrolls, +the sweep's end under the pointer and its start on its text. A sweep that +scrolled ends as the text's own selection, from where it began to where it +ended. B2 and B3 sweeps do not scroll. diff --git a/docs/tags.md b/docs/tags.md index b8299f22..aa680c48 100644 --- a/docs/tags.md +++ b/docs/tags.md @@ -72,21 +72,33 @@ are passed over: they only keep a weight for later. A tag is a text like a pane's body: pane tags, column tags and the workspace tag all edit with the body's own normal and insert modes, undo -included, and may hold more than one line. A tag with a newline in it takes -a row per line, up to eight: a pane's tag leaves its body at least one row -and a collapsed pane shows only the first line; the column and workspace -tags take no more than a third of the screen, and the panes below move down -to make room. The path, the dirty marker and a PDF's page at the start of a +included. A pane's tag may hold more than one line, and a line wider than +the pane wraps onto more rows, as acme's tag does: it takes a row per shown +line, up to eight, and leaves its body at least one row. A word the wrap +breaks across rows is still one word to a click. A collapsed pane shows +only the first row. + +A pane tag starts expanded, showing all its rows, and can be collapsed to +one row, as acme's Tagup does. In the tag's insert mode Up on its first row +collapses it and Down on its last row expands it; Alt-Up and Alt-Down do +the same in either mode. acme expands or collapses on any arrow key typed in +the tag (Up collapses, the others expand); here arrows keep moving the +cursor, and only these keys at the tag's edges change its height. The column and workspace tags +are one line, as acme's (cols.c:244 gives a column tag one font height): a +newline typed, pasted or written into one becomes a space, and a dump that +holds a column or workspace tag of several lines, from before, comes back +with its lines joined by spaces. The path and a PDF's page at the start of a pane tag are computed, never stored, and read-only. The keyboard reaches them as the mouse does: `0` goes to the line's start, the path's, as in acme, and motions select and yank across the path and the commands. An edit that would change them is refused and leaves the cursor where it was; typing into a file's path instead drafts a new name, as clicking it does. When they -grow or shrink (a rename, the dirty marker, a PDF's page) the cursor keeps +grow or shrink (a rename, a PDF's page) the cursor keeps its place in the text after them. -Left-click a tag or a header to type into it at the click, in insert mode; -tags reveal the caret horizontally when text is wider than their column. +Left-click a tag or a header to type into it at the click, in insert mode. +A pane tag wraps instead of scrolling sideways; a column or workspace tag +reveals the caret horizontally when its text is wider than its column. Esc is normal mode, where everything a body's normal mode does works. There Tab executes the word under the cursor (or an explicit selection), and Enter looks it up in a pane's tag but runs it in a column or workspace tag, whose @@ -170,6 +182,10 @@ ignored, with a message saying so. the shared bold, underline, and strikethrough attributes, including diagnostic underlines and the optional `SyntaxBold` keyword weight. +Unsaved text shows on the pane's grip button, as acme's modbutton does, +not in the tag: the tag names the file and nothing more. The `dirty` file +and `index`'s flag say the same to a script. + ## Empty columns A column can hold no pane, as acme's can: its tag stands over blank space in diff --git a/docs/themes.md b/docs/themes.md index 103a3f5a..e028e806 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -115,6 +115,12 @@ so existing exported themes remain valid. | `diagnostic_warning` | Warning text | Same foreground fallback | | `diagnostic_info` | Informational diagnostic text | Same foreground fallback | | `diagnostic_hint` | Hint text | Same foreground fallback | +| `tag_sel_bg` | A tag's selection ground | `sel_bg` | +| `sweep_bg`, `sweep_fg` | Three triples each: the select, exec and look sweeps' ground and ink | `sel_bg` tinted toward `kw`, `str` and `num`, in `sel_fg` | +| `tag_rule` | The one-pixel rule between a pane's tag and its body, quieter than the `rule_px` rules between panes and columns | Halfway between `tag_bg` and the page the shell draws | +| `rule_px` | Pixel shells: the width of the rules between columns, between panes stacked in a column, and under the workspace and column tags, in `border` (the rule between a tag and its body is always one pixel) | 2 | +| `box_border`, `box_dirty` | The grip is acme's button: `box` filled when focused, else a two-pixel ring of `box_border` round the tag's ground; `box_dirty` fills it inside either ring while its file is unsaved (a grid, which has no ring, shows a bold `*` on `box_dirty` in the grip's second cell) | `box_dim`; `diagnostic_warning`, then `num` | +| `rail_px` | Pixel shells: the scroll column's width, the grip's button over the scrollbar (thumb a pixel narrower), in pixels at a 17px tagline, scaled with it | 12, acme's Scrollwid | Native palettes give filenames a distinct hue at approximately the same brightness as the surrounding tag text. This also applies to output names diff --git a/shaders/decor.frag.glsl b/shaders/decor.frag.glsl index 4c2bd6d8..3e2c286f 100644 --- a/shaders/decor.frag.glsl +++ b/shaders/decor.frag.glsl @@ -1,24 +1,70 @@ #version 450 -// Decor: a flat, hard-edged fill (a rule, a rail, a grip mark, a bar cursor) -// that ui.vert.glsl places like a cell, its track's transition included. Its -// colour rides in the cell's ground (a_bg) and its coverage in a_fg.r. The -// pipeline blends it as the overlay pipeline does (backgroundLayerBlend), so -// a fading rule shows through, and the result is that pipeline's exactly. -// Ink (a cursor) is drawn with the opacity uniform at 1. +// Decor, drawn by ui.vert.glsl like a cell, its track's transition included, +// and blended as the overlay pipeline blends (backgroundLayerBlend). Ink (a +// cursor) is drawn with the opacity uniform at 1. +// +// Flat: a hard-edged fill (a rule, a rail, a grip mark, a bar cursor). Its +// colour rides in the cell's ground (a_bg), its coverage in a_fg.r, so the +// result is the overlay pipeline's exactly. +// +// Soft (flag 0x10000000): a rectangle's shadow, rim or glow blurred by a Gaussian, +// in closed form (the product of two erf edges), on one quad grown by 3 +// sigma: no blur pass. Everything is in the quad's own 0..1 space, so a +// track's transform carries it: sigma per axis in the uv, the caster's +// offset from its shadow in fg.gb, the strength in fg.r. The caster's own +// rectangle is left alone (it is drawn already), and the alpha is pre-warped +// so black darkens in linear light, then dithered so it never bands. layout(std140, set = 3, binding = 0) uniform UiUniforms { float background_opacity; } u_ui; +layout(location = 0) in vec2 v_uv; layout(location = 1) in vec3 v_fg; layout(location = 2) in vec3 v_bg; +layout(location = 3) in vec2 v_cell_uv; +layout(location = 4) flat in uvec4 v_effect; layout(location = 5) flat in vec4 v_clip; layout(location = 0) out vec4 o_col; +// erf to about 4e-4 (Winitzki's approximation): smooth, cheap, odd. +float erfApprox(float x) { + float x2 = x * x; + const float a = 0.147; + float e = sqrt(1.0 - exp(-x2 * (1.2732395 + a * x2) / (1.0 + a * x2))); + return x < 0.0 ? -e : e; +} + +// How much of [lo, hi] a Gaussian of sigma s centred at x covers. +float edge(float x, float lo, float hi, float s) { + float k = 0.70710678 / s; + return 0.5 * (erfApprox((x - lo) * k) - erfApprox((x - hi) * k)); +} + +float ign(vec2 p) { + return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); +} + void main() { if (v_clip.z >= v_clip.x && (gl_FragCoord.x < v_clip.x || gl_FragCoord.x >= v_clip.z || gl_FragCoord.y < v_clip.y || gl_FragCoord.y >= v_clip.w)) discard; - o_col = vec4(v_bg * clamp(u_ui.background_opacity, 0.0, 1.0), v_fg.r); + float opacity = clamp(u_ui.background_opacity, 0.0, 1.0); + if ((v_effect.x & 0x10000000u) == 0u) { + o_col = vec4(v_bg * opacity, v_fg.r); + return; + } + vec2 s = v_uv; + vec2 m = 3.0 * s; + vec2 p = v_cell_uv; + vec2 caster0 = m - v_fg.gb; + vec2 caster1 = 1.0 - m - v_fg.gb; + if (all(greaterThanEqual(p, caster0)) && all(lessThan(p, caster1))) discard; + float a = v_fg.r * edge(p.x, m.x, 1.0 - m.x, s.x) * edge(p.y, m.y, 1.0 - m.y, s.y); + // Black darkens in linear light; a light colour (a rim, a glow) is laid + // on as it is, within §8.2's 25%. + if (all(equal(v_bg, vec3(0.0)))) a = 1.0 - pow(max(1.0 - a, 0.0), 1.0 / 2.2); + a = clamp(a + (ign(gl_FragCoord.xy) - 0.5) / 255.0, 0.0, 1.0); + o_col = vec4(v_bg * opacity, a); } diff --git a/shaders/ui.frag.glsl b/shaders/ui.frag.glsl index 0b099db3..dca224ba 100644 --- a/shaders/ui.frag.glsl +++ b/shaders/ui.frag.glsl @@ -43,7 +43,7 @@ void main() { float bgA = (v_effect.x & 0x20000000u) != 0u ? 1.0 : (v_effect.x & 0x40000000u) != 0u ? 0.0 : clamp(u_ui.background_opacity, 0.0, 1.0); - uint effect = v_effect.x & 0x1fffffffu; + uint effect = v_effect.x & 0x0fffffffu; float progress = clamp(uintBitsToFloat(v_effect.y), 0.0, 1.0); uint cellCol = v_effect.w & 0xffffu; uint cellRow = v_effect.w >> 16; diff --git a/shaders/ui.vert.glsl b/shaders/ui.vert.glsl index c1779add..5551f3f1 100644 --- a/shaders/ui.vert.glsl +++ b/shaders/ui.vert.glsl @@ -14,9 +14,9 @@ layout(location = 3) in vec3 a_bg; layout(location = 4) in vec4 a_panel_rect; layout(location = 5) in vec4 a_present_rect; // effect id, eased-progress float bits, pane serial, local row/column packed -// into one uint. The THREE high effect bits are flags — the frozen-old cell -// layer, a see-through cell background and an opaque block-cursor fill — so -// the id is what is left below them; integer transport preserves the exact +// into one uint. The FOUR high effect bits are flags — the frozen-old cell +// layer, a see-through cell background, an opaque block-cursor fill and a +// soft shadow (decor.frag.glsl) — so the id is what is left below them; integer transport preserves the exact // cross-backend hash. layout(location = 6) in uvec4 a_effect; // A clip rectangle in target pixels (x0, y0, x1, y1): the fragment shader @@ -61,7 +61,7 @@ void main() { v_cell_uv = vec2(0.0, 1.0); } - uint effect = a_effect.x & 0x1fffffffu; + uint effect = a_effect.x & 0x0fffffffu; if (effect == 1u || effect == 2u || effect == 5u) { // NDC y points up, so a top-left/bottom-right box has a NEGATIVE y // delta. Preserve that sign or every local y is mirrored/outside. diff --git a/src/Messages.zig b/src/Messages.zig index cc4b3656..8b6c795d 100644 --- a/src/Messages.zig +++ b/src/Messages.zig @@ -356,11 +356,14 @@ pub fn messageMotion(p: *const Pardes, life: Pane.MessageLife) MessageMotion { // last frames only settle it. No overshoot: at a row's height // and a dozen frames, a follow-through is a pixel or two that // reads as a jitter, not a bounce. - const left = 1 - t; + // The drop follows the Motion flavour (its wind-up, overshoot + // and all) over the fall's length. + const left = 1 - animation.Motion.of(p.settings.motion).ease(t); // The colour leads the motion: solid by half way, so what // lands is already legible rather than still fading up. const faded = 1 - @min(1, 2 * t); - break :blk .{ .alpha = 1 - faded * faded, .slide = -(left * left * left) }; + // And never behind it, however fast the flavour leaves. + break :blk .{ .alpha = @max(1 - faded * faded, std.math.clamp(1 - left, 0, 1)), .slide = -left }; }, .leaving => blk: { // Counted to its last frame rather than past it, so that frame @@ -644,6 +647,9 @@ test "a message falls in, lingers past the input that dismisses it, and dissolve while (p.nextEffect()) |_| {} const pane = p.panes[p.active].?; try std.testing.expect(p.settings.message_animation); + // Crisp: the flavour whose drop leaves at full speed and lands without + // passing its row (the others are the Motion tests'). + p.settings.motion = .crisp; try std.testing.expect(p.executeBuiltinLine(p.active, "MessageLinger 160")); try std.testing.expect(p.executeBuiltinLine(p.active, "Msg hello")); try std.testing.expectEqual(Pane.MessagePhase.entering, pane.msg_life.phase); diff --git a/src/Presentation.zig b/src/Presentation.zig index 3b5bb3e0..15ed9ea6 100644 --- a/src/Presentation.zig +++ b/src/Presentation.zig @@ -183,7 +183,7 @@ pub fn sync(self: *Presentation, p: *Pardes) void { prior.?.from else old.box; - const next: Track = .{ + const next = stamp(p, .{ .serial = pane.serial, .pane = @intCast(id), .phase = if (!same_lifetime or @@ -191,19 +191,19 @@ pub fn sync(self: *Presentation, p: *Pardes) void { .effect = effect, .from = from, .to = target, - }; + }); self.tracks[id] = next; changed = true; animated_change = true; } else { - const next: Track = .{ + const next = stamp(p, .{ .serial = pane.serial, .pane = @intCast(id), .phase = .opening, .effect = effect, .from = openingBox(effect, target, p.screen_w), .to = target, - }; + }); self.tracks[id] = next; changed = true; animated_change = true; @@ -215,6 +215,21 @@ pub fn sync(self: *Presentation, p: *Pardes) void { if (changed and self.acknowledged) self.pending = true; } +/// A pane that moves (slides, zooms or rises) follows the Motion flavour: +/// its curve, and a length in proportion (animation.Motion.span). +fn stamp(p: *const Pardes, track: Track) Track { + var stamped = track; + switch (track.effect) { + .slide, .zoom, .vertical => { + const motion = animation.Motion.of(p.settings.motion); + stamped.motion = @intFromEnum(p.settings.motion) + 1; + stamped.frame_count = @intFromFloat(@max(2, @round(@as(f32, @floatFromInt(track.effect.frames())) * motion.span))); + }, + else => {}, + } + return stamped; +} + fn appendClosing(self: *Presentation, track: Track) bool { std.debug.assert(track.phase == .closing); const baseline = self.previous_layout[track.pane] orelse return false; @@ -388,6 +403,10 @@ pub fn pointerFractional(self: *const Presentation, cols: u16, rows: u16, x: f32 .slide, .zoom, .vertical => { const target = track.contentBox(); if (track.effect == .vertical and !boxContainsPoint(target, x, y)) continue; + // Past its mark (a flavour's overshoot or wind-up) a pane + // is drawn over a neighbour; a point there is the + // neighbour's, never the moving pane's. + if (pastMark(track) and !boxContainsPoint(target, x, y)) continue; const shown = track.presented(); if (!boxContainsPoint(shown, x, y)) { if (track.effect == .vertical) return null; @@ -410,6 +429,13 @@ pub fn pointerFractional(self: *const Presentation, cols: u16, rows: u16, x: f32 return .{ .x = x, .y = y }; } +/// A flavour's overshoot or wind-up: the pane is drawn beyond the path from +/// where it was to where it goes. +fn pastMark(track: Track) bool { + const t = track.amount(); + return t < 0 or t > 1; +} + fn boxContainsPoint(box: Box, x: f32, y: f32) bool { return box.w > 0 and box.h > 0 and x >= box.x and y >= box.y and x < box.x + box.w and y < box.y + box.h; } @@ -436,6 +462,8 @@ pub fn pointer(self: *const Presentation, cols: u16, rows: u16, col: u16, row: u switch (track.effect) { .slide, .zoom => { const shown = track.presented(); + // Past its mark, the neighbour's cells stay the neighbour's. + if (pastMark(track) and !boxContainsCell(track.to, col, row)) continue; if (shown.w <= 0 or shown.h <= 0 or !boxContainsCell(shown, col, row)) continue; const x = @as(f32, @floatFromInt(col)) + 0.5; const y = @as(f32, @floatFromInt(row)) + 0.5; @@ -688,6 +716,29 @@ test "presentation submission failure preserves shown geometry until retry is ac try std.testing.expectEqual(CellPosition{ .col = 5, .row = 3 }, p.presentation.pointer(p.screen_w, p.screen_h, 5, 3).?); } +test "a pane past its mark never takes a neighbour's click" { + // Bouncy, at its peak: pane 0 slides right to 0..40 and swings past it, + // over the neighbour at 40..80. + var presentation: Presentation = .{}; + var track: Track = .{ .serial = 7, .effect = .slide, .motion = @intFromEnum(animation.Motion.Flavour.bouncy) + 1, .frame_count = 20, .from = .{ .x = -40, .y = 0, .w = 40, .h = 20 }, .to = .{ .x = 0, .y = 0, .w = 40, .h = 20 } }; + var peak: u16 = 0; + var furthest: f32 = -1e9; + while (track.frame < track.frames()) : (track.frame += 1) { + if (track.presented().x > furthest) { + furthest = track.presented().x; + peak = track.frame; + } + } + track.frame = peak; + try std.testing.expect(track.presented().x > 2); + presentation.shown_tracks[0] = track; + // A cell of the neighbour under the overshoot is the neighbour's own. + const col: u16 = 41; + try std.testing.expectEqual(CellPosition{ .col = col, .row = 5 }, presentation.pointer(80, 20, col, 5).?); + const point = presentation.pointerFractional(80, 20, 41.5, 5.5).?; + try std.testing.expectApproxEqAbs(@as(f32, 41.5), point.x, 1e-4); +} + test "fractional panel inverse preserves compact header rows through slide and zoom" { const layer: pardes.Layer = .{ .serial = 7, .viewport = .{ .x = 4, .y = 3, .w = 30, .h = 10 }, .cols = 30, .rows = 15, .context_rows = 4 }; for ([_]Transition{ .slide, .zoom, .vertical }) |effect| { diff --git a/src/Terminal.zig b/src/Terminal.zig index 91c23305..76be29d0 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -659,6 +659,36 @@ test "replay restores terminal cells from the retained stream" { try std.testing.expectEqualSlices(u8, try replayBytes(pane, gpa), try replayBytes(restored, gpa)); } +test "a restored command pane comes back finished, its command not run again" { + if (comptime !enabled or pardes.isolated) return error.SkipZigTest; + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 60, .rows = 16 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const cmd = p.freeSlot().?; + _ = try p.newCommand(cmd, "/tmp", "make test"); + feedOutput(p, p.panes[cmd].?, "ok\r\n"); + p.update(.{ .exited = .{ .pane = @intCast(cmd), .status = 2 } }); + try dump.dumpState(p); + const restored = try dump.initFromDump(gpa, .{ .tty_only = true, .cols = 60, .rows = 16 }, p.dump_out.?); + defer restored.deinit(); + var spawns: usize = 0; + while (restored.nextEffect()) |effect| switch (effect) { + .spawn => spawns += 1, + else => {}, + }; + try std.testing.expectEqual(@as(usize, 1), spawns); // the shell, not the command + const again = for (restored.panes) |slot| { + if (slot) |pane| if (pane.command != null) break pane; + } else return error.CommandPaneLost; + try std.testing.expectEqualStrings("make test", again.command.?); + try std.testing.expect(again.command_done); + try std.testing.expectEqual(@as(?u8, 2), again.command_status); + const text = try screenTextAlloc(again, gpa); + defer gpa.free(text); + try std.testing.expect(std.mem.indexOf(u8, text, "ok") != null); +} + test "a restored terminal keeps its history above a marker and gets a shell where it was" { if (comptime !enabled or pardes.isolated) return error.SkipZigTest; const gpa = std.testing.allocator; @@ -668,6 +698,7 @@ test "a restored terminal keeps its history above a marker and gets a shell wher const pane = p.panes[0].?; feedOutput(p, pane, "old output\r\n$ ls\r\n"); try pane.setOwnedCwd("/tmp"); + pane.shell = try gpa.dupe(u8, "bash"); // `Tty bash`: it comes back as bash try dump.dumpState(p); const restored = try dump.initFromDump(gpa, .{ .tty_only = true, .cols = 40, .rows = 12 }, p.dump_out.?); defer restored.deinit(); @@ -678,6 +709,8 @@ test "a restored terminal keeps its history above a marker and gets a shell wher }; try std.testing.expect(spawned); const again = restored.panes[0].?; + try std.testing.expectEqualStrings("bash", again.shell.?); + try std.testing.expect(again.command == null); try std.testing.expectEqual(.spawn, again.pending_command.wait); const text = try screenTextAlloc(again, gpa); defer gpa.free(text); @@ -961,6 +994,15 @@ pub const CommandOutput = struct { /// What the newest finished command printed: the screen text between its /// C and D marks, read from at most `max_rows` rows above its end, so a /// command that printed the whole history costs no more than its tail. +/// The shell exited in the middle of a command (its `exit 3`): no end mark +/// will come, so the command's output ends where the cursor is. +pub fn endOutputHere(pane: *Pane) void { + if (comptime !enabled) return; + const state = pane.terminal orelse return; + const marks = &state.stream.handler; + if (marks.phase == .running and marks.out_end == null) marks.pinCursor(&marks.out_end, true); +} + pub fn commandOutput(pane: *Pane, gpa: std.mem.Allocator, max_rows: usize) !CommandOutput { // Unless there is text to read, whether the nothing it printed is the // whole of it. @@ -1427,6 +1469,9 @@ pub fn dumpPane( .collapsed = pane.collapsed, .terminal = .{ .cwd = try arena.dupe(u8, pane.cwdSlice()), + .shell = try arena.dupe(u8, pane.shell orelse ""), + .command = try arena.dupe(u8, pane.command orelse ""), + .status = pane.command_status, .stream = try arena.dupe(u8, text), .stream_b64 = &.{}, .cursor = .{ .col = 0, .row = 0 }, @@ -1476,6 +1521,9 @@ pub fn dumpPane( .collapsed = pane.collapsed, .terminal = .{ .cwd = try arena.dupe(u8, pane.cwdSlice()), + .shell = try arena.dupe(u8, pane.shell orelse ""), + .command = try arena.dupe(u8, pane.command orelse ""), + .status = pane.command_status, .stream = stream[0..len], .stream_b64 = try dump.encodeBytes(arena, try screenVt(pane, arena)), .stream_is_screen = true, @@ -2111,6 +2159,8 @@ test "terminal Filter keys indexed truecolor OSC and background-only cells throu defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); try testing.expect(pane.tty_filter); pane.tty_filter = false; diff --git a/src/animation.zig b/src/animation.zig index 948a6679..9b43644e 100644 --- a/src/animation.zig +++ b/src/animation.zig @@ -129,7 +129,9 @@ pub const Track = extern struct { pane: u8 = 0, phase: Phase = .moving, effect: Transition = .off, - _padding: u8 = 0, + /// The Motion flavour a pane's move follows, plus one; 0 is the + /// effect's own easing (Presentation.stamp). + motion: u8 = 0, frame: u16 = 0, frame_count: u16 = 0, from: Box = .{}, @@ -148,11 +150,47 @@ pub const Track = extern struct { // accelerates down out of the fixed clip. if (track.phase == .closing and track.effect == .vertical) return progressEased(.in_cubic, track.frames(), track.frame); + if (track.flavour()) |motion| + return motion.ease(track.progress()); return progressEased(track.effect.easing(), track.frames(), track.frame); } + /// The flavour an opening or moving pane follows; a closing one keeps + /// its effect's own exit. + fn flavour(track: Track) ?Motion { + if (track.motion == 0 or track.phase == .closing) return null; + const which = std.enums.fromInt(Motion.Flavour, track.motion - 1) orelse return null; + return Motion.of(which); + } + + fn progress(track: Track) f32 { + if (track.frames() <= 1 or track.frame >= track.frames() - 1) return 1; + return @as(f32, @floatFromInt(track.frame)) / @as(f32, @floatFromInt(track.frames() - 1)); + } + pub fn presented(track: Track) Box { - return lerpBox(track.from, track.to, track.amount()); + // A flavour's wind-up goes back past where the move began. + const t = track.amount(); + // Landed, it is exactly where it goes: no stretch or float residue. + if (track.flavour() != null and track.progress() >= 1) return track.to; + var box = if (t < 0) lerpBox(track.to, track.from, 1 - t) else lerpBox(track.from, track.to, t); + // Squash and stretch: along its path a moving pane lengthens with + // its speed and, landing past its mark and coming back, squashes + // (the user's choice: its text with it; a pointer inverts the same + // box, so a click lands on the glyph it is drawn as). + const motion = track.flavour() orelse return box; + if (motion.stretch == 0) return box; + const factor = 1 + motion.stretch * std.math.clamp(motion.speed(track.progress()) / 4, -1, 1); + if (@abs(track.to.x - track.from.x) >= @abs(track.to.y - track.from.y)) { + const w = box.w * factor; + box.x -= (w - box.w) / 2; + box.w = w; + } else { + const h = box.h * factor; + box.y -= (h - box.h) / 2; + box.h = h; + } + return box; } pub fn visualBox(track: Track) Box { @@ -586,6 +624,252 @@ pub const frame_ms: u32 = 16; pub const frame_ns: u64 = frame_ms * std.time.ns_per_ms; pub const transition_steps: u16 = 10; +/// How the fx track moves (`Motion <flavour>`, docs/effects.md): one set of +/// parameters a flavour picks, read by every animation that follows the +/// classic principles, so a flavour is data and no effect branches on it. +pub const Motion = struct { + pub const Flavour = enum(u8) { off, crisp, smooth, bouncy, playful }; + + /// Nothing moves: every change lands at once (reduced motion). + instant: bool = false, + /// Timing: the spring's natural frequency, rad/s; slow in / slow out + /// come from the spring itself. + omega: f32 = 36, + /// Follow-through: 1 is critically damped (no overshoot); below 1 it + /// settles past its target and back. + zeta: f32 = 1, + /// Anticipation: a move from rest first winds up the other way, by + /// this fraction of the distance. + anticipate: f32 = 0, + /// Squash and stretch: how much a moving thing lengthens along its + /// velocity and squashes as it lands (G3's cursor, G7's panes). + stretch: f32 = 0, + /// Secondary action: how far behind a follower trails its leader (the + /// shadow behind a pane, a notice's text behind its chip), as the + /// follower's frequency over the leader's; 1 is in step. + follow: f32 = 1, + /// Arcs: how far a two-dimensional path bows, as a fraction of its + /// length (the cursor's glide). + arc: f32 = 0, + /// Exaggeration: how far a small motion goes, as a multiple of its + /// plain distance (a lift of 1 rises `gain`), so a flavour's character + /// shows on a motion only a few pixels big. + gain: f32 = 1, + /// Timing: a move from rest leaves at this fraction of full speed (1: + /// out of the gate at once and easing in, 0: slow in and slow out). + launch: f32 = 0, + /// For a motion of fixed length (a pane's move, a notice's drop): its + /// length as a multiple of the effect's own, and the spring's frequency + /// over that length (`ease`). + span: f32 = 1, + norm: f32 = 10, + + pub fn of(flavour: Flavour) Motion { + return switch (flavour) { + .off => .{ .instant = true }, + // Productivity: quick and exact, nothing past its mark. + .crisp => .{ .omega = 60, .launch = 1, .span = 0.6, .norm = 10 }, + // Longer and softer, easing in and out, still without a bounce. + // The default: within §8.1's 220 ms for an arrival. + .smooth => .{ .omega = 34, .follow = 0.85, .arc = 0.04, .span = 1.1, .norm = 6 }, + // Clearly past its mark and back, once or twice, stretching + // with its speed. + .bouncy => .{ .omega = 24, .zeta = 0.35, .stretch = 0.15, .follow = 0.8, .arc = 0.08, .gain = 1.25, .span = 1.4, .norm = 13 }, + // A cartoon's: winds up, flies past, squashes as it lands, + // its followers trailing. + .playful => .{ .omega = 18, .zeta = 0.28, .anticipate = 0.15, .stretch = 0.35, .follow = 0.55, .arc = 0.15, .gain = 1.4, .span = 1.7, .norm = 18 }, + }; + } + + /// A move of fixed length from 0 to 1, `u` of the way through it: the + /// flavour's spring (wind-up, overshoot and all), landing exactly on 1 + /// at its end. `instant` is there at once. + pub fn ease(motion: Motion, u: f32) f32 { + if (motion.instant or u >= 1) return 1; + if (u <= 0) return 0; + const spring: Spring = .{ .omega = motion.norm, .zeta = motion.zeta, .target = 1, .from = 0, .velocity = (motion.launch - 2 * motion.anticipate) * motion.norm, .settled = false }; + return spring.value(@intFromFloat(u * std.time.ns_per_s)); + } + + /// The same move's speed at `u`, in lengths per unit of `u`. + pub fn speed(motion: Motion, u: f32) f32 { + const h: f32 = 1.0 / 256.0; + return (motion.ease(@min(1, u + h)) - motion.ease(@max(0, u - h))) / (@min(1, u + h) - @max(0, u - h)); + } +}; + +/// A value that follows its target on a damped spring, in closed form +/// (docs/render-pipeline.md §7.2), critically damped or under: a new target +/// taken partway keeps the value AND its velocity, so a quick run of changes +/// never snaps or starts over. A move from rest can wind up first +/// (Motion.anticipate). Settled, it is exactly its target and asks for no +/// frames. +pub const Spring = struct { + omega: f32 = 36, + zeta: f32 = 1, + target: f32 = 0, + /// The value and velocity (per second) at `from_ns`. + from: f32 = 0, + velocity: f32 = 0, + from_ns: u64 = 0, + settled: bool = true, + + const State = struct { value: f32, velocity: f32 }; + + fn at(spring: *const Spring, now_ns: u64) State { + if (spring.settled) return .{ .value = spring.target, .velocity = 0 }; + const t: f32 = @floatCast(@as(f64, @floatFromInt(now_ns -| spring.from_ns)) / std.time.ns_per_s); + const w = spring.omega; + const x0 = spring.from - spring.target; + const v0 = spring.velocity; + if (spring.zeta >= 1) { + const c = v0 + w * x0; + const decay = @exp(-w * t); + return .{ .value = spring.target + (x0 + c * t) * decay, .velocity = (v0 - w * t * c) * decay }; + } + // Underdamped: it rings about its target as it decays. + const z = spring.zeta; + const wd = w * @sqrt(1 - z * z); + const b = (v0 + z * w * x0) / wd; + const decay = @exp(-z * w * t); + const cos = @cos(wd * t); + const sin = @sin(wd * t); + return .{ + .value = spring.target + decay * (x0 * cos + b * sin), + .velocity = decay * ((b * wd - z * w * x0) * cos - (x0 * wd + z * w * b) * sin), + }; + } + + pub fn value(spring: *const Spring, now_ns: u64) f32 { + return spring.at(now_ns).value; + } + + /// Heads for `target` from wherever it is at `now_ns`, moving as it was, + /// at the pace and damping `motion` gives: at once when it is `off`. + pub fn retarget(spring: *Spring, target: f32, now_ns: u64, motion: Motion) void { + if (target == spring.target) return; + if (motion.instant) { + spring.* = .{ .omega = motion.omega, .zeta = motion.zeta, .target = target, .from = target }; + return; + } + const state = spring.at(now_ns); + // From rest, a launch, or a wind-up: set off the other way, a little. + const kick: f32 = if (spring.settled) (motion.launch - 2 * motion.anticipate) * (target - state.value) * motion.omega else 0; + spring.* = .{ .omega = motion.omega, .zeta = motion.zeta, .target = target, .from = state.value, .velocity = state.velocity + kick, .from_ns = now_ns, .settled = false }; + } + + /// Settles once it is within half a percent of its target and barely + /// moving (no pixel of a shadow tells the rest): it is its target from + /// then on. True while it still moves. + pub fn step(spring: *Spring, now_ns: u64) bool { + if (spring.settled) return false; + const state = spring.at(now_ns); + if (@abs(state.value - spring.target) < 5e-3 and @abs(state.velocity) < 5e-2) spring.settled = true; + return !spring.settled; + } +}; + +test "a spring settles without overshoot and keeps its velocity when retargeted" { + var spring: Spring = .{}; + const ms = std.time.ns_per_ms; + const crisp = Motion.of(.crisp); + spring.retarget(1, 0, crisp); + var last: f32 = 0; + var t: u64 = 0; + while (spring.step(t)) : (t += frame_ns) { + const v = spring.value(t); + try std.testing.expect(v >= last and v <= 1); + last = v; + } + try std.testing.expect(t >= 80 * ms and t <= 200 * ms); + try std.testing.expectEqual(@as(f32, 1), spring.value(t + 5 * ms)); + // Back the other way partway up: it carries on up for a moment, then + // turns, never jumping. + spring = .{}; + spring.retarget(1, 0, crisp); + const before = spring.at(60 * ms); + spring.retarget(0, 60 * ms, crisp); + const after = spring.at(60 * ms); + try std.testing.expectApproxEqAbs(before.value, after.value, 1e-6); + try std.testing.expectApproxEqAbs(before.velocity, after.velocity, 1e-4); + try std.testing.expect(spring.value(60 * ms + 500_000) > before.value); +} + +test "each flavour is its own motion: off lands at once, bouncy overshoots, playful winds up first" { + const ms = std.time.ns_per_ms; + var spring: Spring = .{}; + spring.retarget(1, 0, Motion.of(.off)); + try std.testing.expect(spring.settled); + try std.testing.expectEqual(@as(f32, 1), spring.value(0)); + var settle: [5]u64 = @splat(0); + for ([_]Motion.Flavour{ .crisp, .smooth, .bouncy, .playful }) |flavour| { + spring = .{}; + spring.retarget(1, 0, Motion.of(flavour)); + var peak: f32 = 0; + var low: f32 = 0; + var t: u64 = 0; + var last = spring.at(0); + while (spring.step(t)) : (t += ms) { + const now = spring.at(t); + peak = @max(peak, now.value); + low = @min(low, now.value); + // Continuous, a millisecond at a time: no jump anywhere. + try std.testing.expect(@abs(now.value - last.value) < 0.07); + last = now; + } + try std.testing.expect(t < 1600 * ms); + settle[@intFromEnum(flavour)] = t; + switch (flavour) { + .crisp, .smooth => try std.testing.expect(peak <= 1.0005 and low >= 0), + // Clearly past the mark, not a hint of it. + .bouncy => try std.testing.expect(peak > 1.25 and low >= 0), + .playful => try std.testing.expect(peak > 1.35 and low < -0.03), + .off => unreachable, + } + } + // Smooth takes twice crisp's time and more: a character, not a tuning. + // Yet as the default a pane's arrival (a slide's 12 frames, scaled) is + // inside §8.1's 220 ms, and a lift is still before 300 ms. + try std.testing.expect(settle[@intFromEnum(Motion.Flavour.smooth)] > 2 * settle[@intFromEnum(Motion.Flavour.crisp)]); + try std.testing.expect(settle[@intFromEnum(Motion.Flavour.smooth)] <= 300 * ms); + try std.testing.expect(@round(@as(f32, 12) * Motion.of(.smooth).span) * frame_ms <= 220); + // A fixed-length move lands exactly on its mark, whatever the flavour. + for (std.enums.values(Motion.Flavour)) |flavour| try std.testing.expectEqual(@as(f32, 1), Motion.of(flavour).ease(1)); +} + +test "a flavour's stretched pane lands exactly on its box, and a click finds the glyph drawn there" { + const from: Box = .{ .x = 0, .y = 2, .w = 30, .h = 10 }; + const to: Box = .{ .x = 37.3, .y = 2, .w = 30, .h = 10 }; + for (std.enums.values(Motion.Flavour)) |flavour| { + var track: Track = .{ .effect = .slide, .motion = @intFromEnum(flavour) + 1, .frame_count = 20, .from = from, .to = to }; + track.frame = track.frames() - 1; + const landed = track.presented(); + // Bit for bit: no stretch or float residue once it lands. + try std.testing.expectEqual(to, landed); + } + // Playful at its most stretched: the pane is drawn wider than its box, + // and the drawn place of a cell maps back to that cell (the pointer + // inverts the same box: target + (x - shown.x) / shown.w * target.w). + var track: Track = .{ .effect = .slide, .motion = @intFromEnum(Motion.Flavour.playful) + 1, .frame_count = 20, .from = from, .to = to }; + var most: u16 = 0; + var widest: f32 = 0; + while (track.frame < track.frames()) : (track.frame += 1) { + if (track.presented().w > widest) { + widest = track.presented().w; + most = track.frame; + } + } + track.frame = most; + const shown = track.presented(); + try std.testing.expect(shown.w > to.w * 1.05); + for (0..30) |k| { + const cell = to.x + @as(f32, @floatFromInt(k)) + 0.5; + const drawn = shown.x + (cell - to.x) / to.w * shown.w; + const back = to.x + (drawn - shown.x) / shown.w * to.w; + try std.testing.expectApproxEqAbs(cell, back, 1e-3); + } +} + /// 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/body_layer.zig b/src/body_layer.zig index a0195fb8..6e1fe8f1 100644 --- a/src/body_layer.zig +++ b/src/body_layer.zig @@ -72,7 +72,7 @@ fn paintTerminalSelection(p: *Pardes, s: *Surface, pane: *Pane, r: Rect, rows: [ hi = @min(hi, line.len); if (lo >= hi) continue; var col = panes.File.rawDisplayCol(line, lo); - const end = panes.File.rawDisplayCol(line, hi); + const end = if (row.to_edge and hi == line.len) width else panes.File.rawDisplayCol(line, hi); while (col < end and col < width) : (col += 1) { const cell = s.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(visible))); cell.default = false; @@ -103,7 +103,7 @@ fn paintSourceSelection(p: *Pardes, s: *Surface, pane: *Pane, r: Rect, rows: []c const hi = @min(end, row.hi); if (lo >= hi) continue; var col = prefix + @as(i32, @intCast(panes.File.rawDisplayCol(line[start..], lo - start))); - const last = prefix + @as(i32, @intCast(panes.File.rawDisplayCol(line[start..], hi - start))); + const last = if (row.to_edge and hi == end) @as(i32, width) else prefix + @as(i32, @intCast(panes.File.rawDisplayCol(line[start..], hi - start))); while (col < last and col < width) : (col += 1) { const cell = s.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(visible))); cell.default = false; @@ -130,13 +130,12 @@ pub fn paintPointerSelection( fg: ?[3]u8, ) void { if (pane.pointerSelection(sl)) |rows| return paintSourceSelection(p, s, pane, r, rows, bg, fg); - const r0 = @min(sl.r0, sl.r1); - const r1 = @max(sl.r0, sl.r1); - const c0 = @max(0, @min(sl.c0, sl.c1)); - const c1 = @max(0, @max(sl.c0, sl.c1)); var row: u16 = 0; while (row < r.h) : (row += 1) { - if (@as(i32, row) < r0 or @as(i32, row) > r1 or row < from_row) continue; + if (row < from_row) continue; + const cols = edit.sweepCols(sl, row) orelse continue; + const c0: i32 = if (cols.first) cols.lo else 0; + const c1: i32 = cols.hi orelse std.math.maxInt(i16); const sy = if (row < pane.tag_rows) tag_y + row else body_y + row - pane.tag_rows; // File line numbers occupy a gutter only in the body. The tag is // row zero in Sel space and starts at its real first text cell. @@ -156,6 +155,7 @@ pub fn paintPointerSelection( /// The selection colour of each pointer button's sweep: the select button's /// and the look and exec buttons', each tinted toward its own accent. pub fn buttonTints(th: *const colors.Theme) [3][3]u8 { + if (th.sweep_bg) |own| return own; const accent2 = if (std.mem.eql(u8, &th.num, &th.str)) th.comment else th.num; return .{ colors.mix(th.sel_bg, colors.mix(th.sel_bg, th.kw)), @@ -164,6 +164,11 @@ pub fn buttonTints(th: *const colors.Theme) [3][3]u8 { }; } +/// The ink over each button's sweep. +pub fn buttonInks(th: *const colors.Theme) [3][3]u8 { + return th.sweep_fg orelse @splat(th.sel_fg); +} + /// A pane's body, painted once. A file body with compact tree-sitter /// context rows is painted into its body layer -- more rows than the grid /// has, the context rows at the tagline's height -- and the grid's rows are @@ -312,7 +317,7 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane // A select sweep over the tag is the tag's selection, painted there // in the tag's own selection colours. if (b == pardes.sel_slot and @min(sl.r0, sl.r1) < pane.tag_rows) continue; - paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sl, pane.tag_rows, sel_btn[b], th.sel_fg); + paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sl, pane.tag_rows, sel_btn[b], buttonInks(th)[b]); } // modal line selection (helix `x`): whole body rows, normal mode only if ((pane.body.mode == .normal or modal_hover) and pane.body.msel.active) { diff --git a/src/builtins.zig b/src/builtins.zig index 7b34b376..657f49d0 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -28,6 +28,8 @@ pub const capabilities: config.Runtime.Capabilities = .{ // Only the SDL shell shapes with HarfBuzz and honours the setting. macOS // draws CoreText ligatures of its own, but nothing there reads it yet. .ligatures = pardes.platform == .gui, + // Soft shadows are the SDL shell's decor. + .lift = pardes.platform == .gui, }; /// Where a builtin acts: at the pane it runs from, or on the editor as a @@ -169,8 +171,8 @@ pub const registry = struct { inline for (comptime settingList(), manualBuiltinCount()..) |setting, i| if (@intFromEnum(b) == i) return switch (setting.action) { // a switch flips bare, and DumpDir bare is the default - .toggle, .transition, .scene, .dump_dir, .choice => false, - .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .shader_animation => true, + .toggle, .transition, .scene, .dump_dir, .choice, .lift => false, + .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .shader, .shader_animation, .motion, .inactive_dim, .grip_width => true, }; unreachable; } @@ -854,6 +856,8 @@ pub const Del = struct { { c.pane.prompt = .del_side; c.p.active = c.id; + var rec: [32]u8 = undefined; + pardes.ctlfs.events.notePath(c.p, "ask", std.fmt.bufPrint(&rec, "{d} del k j", .{c.pane.serial}) catch ""); // The question says Del already. if (c.pane.msg_announcement) c.pane.msg_len = 0; return; diff --git a/src/colors.zig b/src/colors.zig index 401b77f1..224591d3 100644 --- a/src/colors.zig +++ b/src/colors.zig @@ -65,6 +65,29 @@ pub const Theme = struct { // The current line number is a quiet foreground cue, not a tag-colored // background block. Older themes fall back to their normal gutter ink. lineno_active: ?[3]u8 = null, + /// A tag's selection, where it differs from the body's (acme's #9eeeee). + tag_sel_bg: ?[3]u8 = null, + /// Each pointer button's sweep, select, exec and look (Mouse.Button + /// order), and the ink over it. Unset, tints of `sel_bg` in `sel_fg`. + sweep_bg: ?[3][3]u8 = null, + sweep_fg: ?[3][3]u8 = null, + /// A pixel shell's rules, in pixels (2 unset); the rule between a tag + /// and its body is always one. + rule_px: ?u8 = null, + /// A grip is acme's button: `box` filled when focused, else a ring of + /// `box_border` (unset: `box_dim`) round the tag's ground; `box_dirty` + /// (unset: `diagnostic_warning`, then `num`) fills it, in either ring, + /// while its file is unsaved. + box_border: ?[3]u8 = null, + box_dirty: ?[3]u8 = null, + /// The rule between a pane's tag and its body, one pixel, quieter than + /// the rules between panes (acme's #8888cc under its 2px black ones). + /// Unset, halfway between the tag's ground and the page. + tag_rule: ?[3]u8 = null, + /// A pixel shell's scroll column, the grip's button over the scrollbar, + /// in pixels at a 17px tagline, the thumb one narrower (acme's 12px + /// Scrollwid, the default). + rail_px: ?u8 = null, }; const curated = struct { @@ -110,6 +133,8 @@ fn fold(comptime C: type) [@typeInfo(C).@"struct".decls.len]Theme { } pub const themes = fold(curated) ++ fold(@import("generated_themes")); +/// The first `native_count` of `themes` are pardes's own; the rest imported. +pub const native_count = @typeInfo(curated).@"struct".decls.len; /// Original and classic-inspired Pardes palettes precede retained imports. pub const native_theme_count = 15; diff --git a/src/config.zig b/src/config.zig index 01a5daf8..24c48770 100644 --- a/src/config.zig +++ b/src/config.zig @@ -119,6 +119,10 @@ 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(.Lift, null); + table.set(.Motion, null); + table.set(.InactiveDim, null); + if (builtins.capabilities.lift) table.set(.GripWidth, null); if (builtins.capabilities.panel_transitions) { table.set(.PanelSlide, "as"); table.set(.PanelZoom, "az"); @@ -621,6 +625,24 @@ pub const Runtime = struct { /// A font's programming ligatures (`->`, `!=`) draw across their cells. /// Only a shell that shapes text has the setting (`Capabilities.ligatures`). ligatures: bool = true, + /// How the focused pane and the notices stand off the page (G1, + /// docs/render-pipeline.md §9.1), or off. Opt-in until the focus lift's + /// default is decided. Only a pixel shell draws it (`Capabilities.lift`). + lift: LiftStyle = .off, + /// Blends the unfocused panes' text toward each cell's own ground, + /// percent (0 off): the focused pane wins by comparison, its tag tint + /// untouched. Kept above min(each cell's contrast, 4.5). + inactive_dim: u8 = 0, + /// The grip's button and the scrollbar under it, one width, as a percent + /// of the theme's `rail_px` (acme's 12px Scrollwid at a 17px tagline): + /// a pixel shell's (docs/themes.md). + grip_width: u16 = 150, + /// How the fx track's animations move (animation.Motion): off for + /// reduced motion. + /// Smooth by default, tuned so arrivals land inside §8.1's 220 ms; the + /// slower flavours are a choice. + motion: animation.Motion.Flavour = .smooth, + /// Builtins announce themselves on the message row. verbose: bool = true, /// A message falls into its row and dissolves out of it. @@ -678,6 +700,16 @@ pub const Runtime = struct { /// A setting that is one of a few words, each an enum field of Runtime. pub const Choice = enum { placement, boot_shell }; + /// shadow: a soft drop shadow. rim: a light hairline along the top of + /// the focused pane's tag rows. auto: a shadow on a light page; on a + /// dark one, where a shadow has nothing to darken, the other panes + /// recede instead (InactiveDim, at `auto_dim` unless it is set). + pub const LiftStyle = enum(u8) { off, shadow, rim, auto }; + /// What `Lift auto` dims the unfocused panes by on a dark page when + /// InactiveDim is 0: enough to read at a glance (the user's own init + /// sets 35), not so much that they stop being worth reading. + pub const auto_dim: u8 = 30; + /// Each pass the bundled Crt or a Shadertoy file. pub const Post = struct { pub const max = 8; @@ -822,6 +854,8 @@ pub const Runtime = struct { window_blur: bool = false, /// The shell shapes text, so it can draw ligatures and stop drawing them. ligatures: bool = false, + /// The shell draws soft shadows under what stands off the page. + lift: bool = false, }; pub const Capability = std.meta.FieldEnum(Capabilities); @@ -843,6 +877,13 @@ pub const Runtime = struct { window_blur, message_ms: MessageTime, dump_dir, + /// A lift style, `on` (auto) or `off`; bare flips auto and off. + lift, + /// A motion flavour. + motion, + /// A percentage, 0 through 100. + inactive_dim, + grip_width, transition: animation.Transition, scene: Scene, /// A Shadertoy file into the post chain or out of it; `off` empties @@ -877,6 +918,10 @@ pub const Runtime = struct { .{ .word = "Verbose", .action = .{ .toggle = .verbose } }, .{ .word = "MessageAnimation", .action = .{ .toggle = .message_animation } }, .{ .word = "Ligatures", .action = .{ .toggle = .ligatures }, .availability = .ligatures }, + .{ .word = "Lift", .action = .lift, .availability = .lift }, + .{ .word = "Motion", .action = .motion }, + .{ .word = "InactiveDim", .action = .inactive_dim }, + .{ .word = "GripWidth", .action = .grip_width, .availability = .lift }, .{ .word = "MessageLinger", .action = .{ .message_ms = .linger } }, .{ .word = "MessageFall", .action = .{ .message_ms = .fall } }, .{ .word = "MessageDissolve", .action = .{ .message_ms = .dissolve } }, @@ -940,6 +985,8 @@ pub const Runtime = struct { @compileError("scene effects must use the scene-shader capability"), // A toggle is plain state any shell can hold, except one for // what only some shells draw: that one exists where they do. + .lift, .grip_width => if (setting.availability != .lift) + @compileError("Lift and GripWidth must use the lift capability (a pixel shell's)"), .toggle => |field| if (field == .ligatures) { if (setting.availability != .ligatures) @compileError("the ligatures toggle must use the ligatures capability"); @@ -1073,6 +1120,31 @@ pub const Runtime = struct { std.meta.stringToEnum(T, text) orelse return false; }, }, + .lift => { + const text = std.mem.trim(u8, argument orelse "", " \t\r\n"); + state.lift = if (text.len == 0) + (if (state.lift == .off) .auto else .off) + else if (std.mem.eql(u8, text, "on")) + .auto + 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; + }, + .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; + if (percent > 100) return false; + state.inactive_dim = percent; + }, + .grip_width => { + const text = std.mem.trim(u8, argument orelse return false, " \t\r\n%"); + const percent = std.fmt.parseInt(u16, text, 10) catch return false; + if (percent < 50 or percent > 300) return false; + state.grip_width = percent; + }, .theme, .font => return false, } return true; @@ -1245,6 +1317,13 @@ pub const Runtime = struct { .choice => |which| switch (which) { inline else => |field| try out.print("{s} {s}\n", .{ setting.word, @tagName(@field(state, @tagName(field))) }), }, + .lift => if (setting.enabled(context.capabilities)) + try out.print("{s} {s}\n", .{ setting.word, @tagName(state.lift) }) + else + try out.print("{s} unsupported\n", .{setting.word}), + .motion => try out.print("{s} {s}\n", .{ setting.word, @tagName(state.motion) }), + .inactive_dim => try out.print("{s} {d}\n", .{ setting.word, state.inactive_dim }), + .grip_width => try out.print("{s} {d}\n", .{ setting.word, state.grip_width }), }; if (context.startup_config_path) |path| @@ -1376,6 +1455,21 @@ pub const Runtime = struct { try std.testing.expectEqual(@as(u8, 0), state.post.level(.crt)); } + test "Lift takes a style, on is auto, and bare flips it off and back to auto" { + var state: Runtime = .{}; + const lift = find("Lift").?; + try std.testing.expect(apply(&state, lift, null)); + try std.testing.expectEqual(LiftStyle.auto, state.lift); + try std.testing.expect(apply(&state, lift, "rim")); + try std.testing.expectEqual(LiftStyle.rim, state.lift); + try std.testing.expect(apply(&state, lift, null)); + try std.testing.expectEqual(LiftStyle.off, state.lift); + try std.testing.expect(apply(&state, lift, "on")); + try std.testing.expectEqual(LiftStyle.auto, state.lift); + try std.testing.expect(!apply(&state, lift, "sparkle")); + try std.testing.expectEqual(LiftStyle.auto, state.lift); + } + test "tagline size validates before mutating live state" { const setting = find("TaglineSize").?; var state: Runtime = .{}; @@ -1478,6 +1572,7 @@ pub const Runtime = struct { .window_opacity = true, .window_blur = true, .ligatures = true, + .lift = true, }, .state = &state, }; @@ -1500,6 +1595,10 @@ pub const Runtime = struct { .placement => "pardes", .boot_shell => "replace", }, + .lift => "rim", + .motion => "playful", + .inactive_dim => "35", + .grip_width => "200", else => null, }; try std.testing.expect(apply(&state, setting, argument)); @@ -1553,6 +1652,10 @@ pub const Runtime = struct { .placement => "Placement pardes\n", .boot_shell => "BootShell replace\n", }, + .lift => "Lift rim\n", + .motion => "Motion playful\n", + .inactive_dim => "InactiveDim 35\n", + .grip_width => "GripWidth 200\n", .theme, .font => unreachable, }; try std.testing.expect(std.mem.indexOf(u8, report, expected) != null); diff --git a/src/detached/server.zig b/src/detached/server.zig index 3921d8d9..857591d4 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -533,7 +533,7 @@ pub const Session = struct { const pt = s.ptys[pane]; // A command whose exit is recorded is reaped: its group may be another's. if (pt.fd < 0 or pt.cmd.exited) return; - host_io.signalTty(pt.pid, pt.fd, sig); + host_io.signalTty(pt.pid, pt.fd, sig, pt.cmd.watched); } fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool { @@ -706,16 +706,14 @@ pub const Session = struct { // it would hang up one that runs on without it. // Its exit, if it came first, is told now its output is in. if (pt.cmd.watched) return host_io.commandEof(s.core, &s.ptys, pane, s, closeWatched); - // Unwatched, a command's exit is read here, as its end. + // Unwatched, a command's exit is read here, as its end, and a + // shell's, for a run waiting on it and the log. const unwatched = if (s.core.panes[pane]) |pn| pn.command != null else false; - var status: ?u8 = null; - if (unwatched) { - status = host_io.exitStatus(pt.pid, 100); - if (status != null) pt.pid = 0; // reaped: no shell to retire - } + const status = host_io.exitStatus(pt.pid, 100); + if (status != null) pt.pid = 0; // reaped: no shell to retire s.closePty(pane); s.harvest(); - if (unwatched) s.core.update(.{ .exited = .{ .pane = pane, .status = status } }); + if (unwatched or status != null) s.core.update(.{ .exited = .{ .pane = pane, .status = status } }); s.core.update(.{ .eof = .{ .pane = pane } }); } diff --git a/src/detached/wire.zig b/src/detached/wire.zig index b1b81eda..c0e17054 100644 --- a/src/detached/wire.zig +++ b/src/detached/wire.zig @@ -11,6 +11,19 @@ const limits = @import("../memory.zig").limits; /// line), and a frame carries the core's placed regions after its layers. 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; +comptime { + @setEvalBranchQuota(10_000); + var hash = std.hash.Fnv1a_64.init(); + for (@typeInfo(pardes.Chrome).@"struct".fields) |field| { + hash.update(field.name); + hash.update(@typeName(field.type)); + } + if (hash.final() != chrome_layout) @compileError(std.fmt.comptimePrint("pardes.Chrome changed (layout 0x{x}): bump wire.version, then chrome_layout", .{hash.final()})); +} + pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace }; pub const max_cols: u16 = 512; @@ -248,11 +261,17 @@ pub const Frame = struct { region.owner = try r.getByte(); region.serial = try r.getU32(); region.rect = .{ .x = try r.getU16(), .y = try r.getU16(), .w = try r.getU16(), .h = try r.getU16() }; - region.active = try r.getBool(); + const flags = try r.getByte(); + if (flags > 3) return error.BadValue; + region.active = flags & 1 != 0; + region.dirty = flags & 2 != 0; region.thumb_y = try r.getU16(); region.thumb_h = try r.getU16(); region.notice = try r.getByte(); region.clip = try r.getU16(); + region.lift = @bitCast(try r.getU32()); + // A springy flavour overshoots, and a wind-up dips below. + if (!(region.lift >= -1 and region.lift <= 2)) return error.BadValue; if (region.rect.x > f.cols or region.rect.w > f.cols - region.rect.x or region.rect.y > f.rows or region.rect.h > f.rows - region.rect.y or region.clip > region.rect.w or region.thumb_y > region.rect.h or region.thumb_h > region.rect.h - region.thumb_y) return error.BadValue; @@ -265,6 +284,8 @@ pub const Frame = struct { rgb.* = .{ 0, 0, 0 }; for (&rgb.*.?) |*channel| channel.* = try r.getByte(); } + } else if (field.type == u8) { + rgb.* = try r.getByte(); } else for (rgb) |*channel| channel.* = try r.getByte(); } try r.end(); @@ -723,7 +744,7 @@ pub fn layersBound(layers: []const pardes.Layer, tags: []const pardes.Layer, reg return n + regions.len * region_len + chrome_len; } -const region_len = 1 + 1 + 4 + 8 + 1 + 2 + 2 + 1 + 2; +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; @@ -802,17 +823,20 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point try w.putU16(region.rect.y); try w.putU16(region.rect.w); try w.putU16(region.rect.h); - try w.putBool(region.active); + try w.putByte(@as(u8, @intFromBool(region.active)) | @as(u8, @intFromBool(region.dirty)) << 1); try w.putU16(region.thumb_y); try w.putU16(region.thumb_h); try w.putByte(region.notice); try w.putU16(region.clip); + try w.putU32(@bitCast(region.lift)); } inline for (@typeInfo(pardes.Chrome).@"struct".fields) |field| { const rgb = @field(chrome, field.name); if (field.type == ?[3]u8) { try w.putBool(rgb != null); if (rgb) |value| for (value) |channel| try w.putByte(channel); + } else if (field.type == u8) { + try w.putByte(rgb); } else for (rgb) |channel| try w.putByte(channel); } } @@ -1890,10 +1914,10 @@ test "detached wire carries compact body rows and exact pointer metrics" { var tag_cells: [3]pardes.Cell = @splat(.{}); tag_cells[2] = logical[10]; const tag: pardes.Layer = .{ .kind = .column, .id = 1, .viewport = .{ .x = 0, .y = 0, .w = 2, .h = 1 }, .cols = 3, .rows = 1, .bg = .{ 11, 22, 33 }, .cells = &tag_cells, .cursor = .{ .x = 2, .y = 0, .bar = true } }; - const placed = [_]pardes.Region{.{ .kind = .rail, .owner = 3, .serial = 42, .rect = .{ .x = 0, .y = 1, .w = 2, .h = 3 }, .active = true, .thumb_y = 1, .thumb_h = 2, .clip = 1 }}; + 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 } }; + 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 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 8f2f95f9..963d61c1 100644 --- a/src/draw.zig +++ b/src/draw.zig @@ -18,6 +18,7 @@ const mouse = @import("mouse.zig"); const tagline = @import("tagline.zig"); const body_layer = @import("body_layer.zig"); const Messages = @import("Messages.zig"); +const builtins = @import("builtins.zig"); const Color = @import("surface.zig").Color; const FontRole = @import("surface.zig").FontRole; const CellStyle = @import("surface.zig").CellStyle; @@ -57,7 +58,7 @@ pub fn place(p: *Pardes, s: *Surface) void { const active = id == p.active; const tag_y = p.tagTop(pane, r); const body_y = p.bodyTop(pane, r); - s.addRegion(.{ .kind = .grip, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x, .y = tag_y, .w = config.GUTTER, .h = pane.tag_rows } }); + s.addRegion(.{ .kind = .grip, .owner = owner, .serial = pane.serial, .active = active, .dirty = tagline.dirty(pane), .rect = .{ .x = r.x, .y = tag_y, .w = config.GUTTER, .h = pane.tag_rows } }); s.addRegion(.{ .kind = .tag, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x + config.GUTTER, .y = tag_y, .w = r.w - config.GUTTER, .h = pane.tag_rows } }); // A folded pane is its tag rows and nothing else. if (pane.collapsed) continue; @@ -119,6 +120,29 @@ pub fn place(p: *Pardes, s: *Surface) void { s.addRegion(.{ .kind = .workspace_tag, .rect = .{ .x = 0, .y = 0, .w = s.cols, .h = p.topBarHeight() } }); // A column carried to the window's right edge shows where it lands as a // rail in the last cell: the layers under it stop a cell short. + // Lift: the focused pane rises, the others settle back, each on its + // spring; a notice floats while Lift is on. + const lift = liftStyle(p); + for (&p.panes, &p.lifts, &p.lift_serials, 0..) |slot, *spring, *serial, id| { + const pane = slot orelse continue; + if (serial.* != pane.serial) { + serial.* = pane.serial; + spring.* = .{}; + } + // How focused it is, which both a lift and a dim follow: at this + // frame's own time, not the stepped animations' 16 ms grid. A grid + // snaps (it has no in-between), and so does nothing that shows it. + const shown = lift != .off or inactiveDim(p) != 0; + const motion = animation.Motion.of(if (shown and builtins.capabilities.lift) p.settings.motion else .off); + // A flavour exaggerates what is only a few pixels (Motion.gain). + spring.retarget(if (id == p.active) animation.Motion.of(p.settings.motion).gain else 0, p.now_ns, motion); + _ = spring.step(p.now_ns); + } + for (s.regions[0..s.nregions]) |*region| switch (region.kind) { + .grip, .tag, .body, .rail => region.lift = if (lift != .off) std.math.clamp(p.lifts[region.owner].value(p.now_ns), -1, 2) else 0, + .notice => region.lift = if (lift != .off) 1 else 0, + else => {}, + }; if (p.drag == .column_move) rail: { const d = p.drag.column_move; const source = layout.columnBySerial(p, d.serial) orelse break :rail; @@ -137,6 +161,185 @@ pub fn place(p: *Pardes, s: *Surface) void { } } +/// A pane's grip in the grid: `box` focused, else halfway between its ring +/// and `box_dim`, so it shows on its tag without the ring a pixel shell +/// draws (gui.zig draws its button from the region and Chrome instead). +fn gripBg(p: *const Pardes, active: bool) [3]u8 { + const chrome = p.chromeTheme(); + if (active) return chrome.box; + return colors.mix(boxBorder(p), chrome.box_dim); +} + +/// An unfocused grip's ring, and a dirty one's fill (colors.Theme). +pub fn boxBorder(p: *const Pardes) [3]u8 { + return p.theme().box_border orelse p.chromeTheme().box_dim; +} +pub fn boxDirty(p: *const Pardes) [3]u8 { + const th = p.theme(); + return th.box_dirty orelse th.diagnostic_warning orelse th.num; +} + +fn inkOn(bg: [3]u8) [3]u8 { + const lum = (@as(u16, bg[0]) * 3 + @as(u16, bg[1]) * 6 + @as(u16, bg[2])) / 10; + return if (lum > 140) .{ 0x00, 0x00, 0x00 } else .{ 0xff, 0xff, 0xff }; +} + +/// 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`). +pub fn liftStyle(p: *const Pardes) config.Runtime.LiftStyle { + return switch (p.settings.lift) { + .auto => if (darkPage(p)) .off else .shadow, + else => |style| style, + }; +} + +/// How far, in percent, the unfocused panes' text fades toward its ground: +/// InactiveDim, or under `Lift auto` on a dark page `auto_dim` while that +/// is unset. +pub fn inactiveDim(p: *const Pardes) u8 { + if (p.settings.inactive_dim != 0) return p.settings.inactive_dim; + return if (p.settings.lift == .auto and darkPage(p)) config.Runtime.auto_dim else 0; +} + +fn darkPage(p: *const Pardes) bool { + return if (p.theme().bg) |bg| luminance(bg) < 0.18 else true; +} + +/// sRGB channel to linear light. +const linear: [256]f32 = blk: { + @setEvalBranchQuota(100_000); + var table: [256]f32 = undefined; + for (&table, 0..) |*out, i| { + const c = @as(f64, @floatFromInt(i)) / 255; + out.* = @floatCast(if (c <= 0.04045) c / 12.92 else std.math.pow(f64, (c + 0.055) / 1.055, 2.4)); + } + break :blk table; +}; + +fn luminance(rgb: [3]u8) f32 { + return 0.2126 * linear[rgb[0]] + 0.7152 * linear[rgb[1]] + 0.0722 * linear[rgb[2]]; +} + +fn contrast(a: [3]u8, b: [3]u8) f32 { + const x = luminance(a); + const y = luminance(b); + return (@max(x, y) + 0.05) / (@min(x, y) + 0.05); +} + +/// Pane `id`'s text, `amount` (0 to 1) of the way to its ground, on the grid +/// and on every layer of it: its tag, its body, its notices. Its grip keeps +/// its colour, which already says it is not the focused one. +fn recede(p: *Pardes, s: *Surface, id: usize, amount: f32) void { + const th = p.theme(); + const chrome = p.chromeTheme(); + const r = p.rects[id]; + const palette = th.palette; + var memo: Memo = .{}; + const text_x = r.x + @min(config.GUTTER, r.w); + var row = r.y; + while (row < @min(@as(u32, r.y) + r.h, s.rows)) : (row += 1) { + var col = text_x; + while (col < @min(@as(u32, r.x) + r.w, s.cols)) : (col += 1) { + const cell = s.at(col, row); + if (cell.default) continue; + const tagline_role = cell.style.font_role == .tagline; + fadeStyle(&cell.style, if (tagline_role) chrome.tag_fg else th.fg, if (tagline_role) chrome.tag_bg else th.bg, palette, amount, &memo); + } + } + const body = &s.body_layers[id]; + if (body.rows != 0) for (body.cells[0..@as(usize, body.cols) * body.rows]) |*cell| fadeStyle(&cell.style, th.fg, th.bg, palette, amount, &memo); + const tag = &s.tag_layers[id]; + if (tag.rows != 0) for (tag.cells[0..@as(usize, tag.cols) * tag.rows]) |*cell| fadeStyle(&cell.style, chrome.tag_fg, tag.bg, palette, amount, &memo); + for (0..Pane.Notices.max) |i| { + const notice = &s.tag_layers[NOTICE_LAYER_BASE + id * Pane.Notices.max + i]; + if (notice.rows != 0) for (notice.cells[0..@as(usize, notice.cols) * notice.rows]) |*cell| fadeStyle(&cell.style, chrome.tag_fg, notice.bg, palette, amount, &memo); + } +} + +/// The few ink and paper pairs a pane has, each faded once a frame. +const Memo = struct { + keys: [32]?[6]u8 = @splat(null), + values: [32][3]u8 = undefined, + + fn faded(memo: *Memo, ink: [3]u8, paper: [3]u8, amount: f32) [3]u8 { + const key = ink ++ paper; + const slot = (@as(usize, ink[0]) *% 31 +% ink[1] *% 7 +% ink[2] +% @as(usize, paper[0]) *% 13 +% paper[1] *% 3 +% paper[2]) % memo.keys.len; + if (memo.keys[slot]) |held| if (std.mem.eql(u8, &held, &key)) return memo.values[slot]; + // Contrast falls as the ink nears its paper: the most of `amount` + // that keeps the pair at its own contrast or 4.5, whichever is lower. + const floor = @min(contrast(ink, paper), 4.5); + var t = amount; + if (contrast(blend(ink, paper, t), paper) < floor) { + var lo: f32 = 0; + var hi: f32 = t; + for (0..8) |_| { + const mid = (lo + hi) / 2; + if (contrast(blend(ink, paper, mid), paper) >= floor) lo = mid else hi = mid; + } + t = lo; + } + memo.keys[slot] = key; + memo.values[slot] = blend(ink, paper, t); + return memo.values[slot]; + } +}; + +/// A cell's ink blended `amount` of the way toward its paper, never below +/// the pair's own contrast or 4.5, whichever is lower (docs/effects.md): +/// faint resolved first, reverse honoured, as InactiveDim always did. +fn fadeStyle(style: *CellStyle, default_fg: ?[3]u8, default_bg: ?[3]u8, palette: ?[16][3]u8, amount: f32, memo: *Memo) void { + if (style.invisible) return; + var fg = resolved(style.fg, default_fg, palette) orelse return; + const bg = resolved(style.bg, default_bg, palette) orelse return; + if (style.dim) for (&fg) |*channel| { + channel.* = @intCast(@as(u16, channel.*) * 6 / 10); + }; + const ink = if (style.reverse) bg else fg; + const paper = if (style.reverse) fg else bg; + const rgb = memo.faded(ink, paper, amount); + if (style.reverse) { + style.bg = .{ .rgb = rgb }; + if (style.dim) style.fg = .{ .rgb = fg }; + } else style.fg = .{ .rgb = rgb }; + style.dim = false; +} + +/// `t` of the way from one colour to the other in linear light (§8.2), so a +/// coloured ink does not muddy on its way to the paper. +fn blend(from: [3]u8, to: [3]u8, t: f32) [3]u8 { + var out: [3]u8 = undefined; + for (&out, from, to) |*o, a, b| { + const l = linear[a] * (1 - t) + linear[b] * t; + o.* = encoded[@intFromFloat(@round(std.math.clamp(l, 0, 1) * (encoded.len - 1)))]; + } + return out; +} + +/// Linear light back to an sRGB channel, in 4096 steps. +const encoded: [4096]u8 = blk: { + @setEvalBranchQuota(1_000_000); + var table: [4096]u8 = undefined; + for (&table, 0..) |*out, i| { + const l = @as(f64, @floatFromInt(i)) / 4095; + const c = if (l <= 0.0031308) l * 12.92 else 1.055 * std.math.pow(f64, l, 1.0 / 2.4) - 0.055; + out.* = @intFromFloat(@round(c * 255)); + } + break :blk table; +}; + +fn resolved(color: Color, default: ?[3]u8, palette: ?[16][3]u8) ?[3]u8 { + return switch (color) { + .rgb => |rgb| rgb, + .default => default, + // A theme's own sixteen, as the painters give them (Terminal.palColor). + .index => |index| if (palette != null and index < 16) palette.?[index] else if (comptime pardes.terminal_panes) blk: { + const rgb = @import("ghostty-vt").color.default[index]; + break :blk .{ rgb.r, rgb.g, rgb.b }; + } else null, + }; +} + pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface { panes.File.refreshHighlights(p); const s = &p.surface; @@ -184,6 +387,16 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface { .tag_fg = chrome.tag_fg, .scroll_track = chrome.scroll_track, .scroll_thumb = chrome.scroll_thumb, + .accent = chrome.box, + .sel_fg = p.theme().sel_fg, + .sel_bg = p.theme().sel_bg, + .lift_style = @intFromEnum(liftStyle(p)), + .rule_px = p.theme().rule_px orelse 2, + .grip_border = boxBorder(p), + .grip_focus_ring = chrome.box, + .grip_dirty = boxDirty(p), + .tag_rule = p.theme().tag_rule, + .rail_px = @intCast(@min(255, @as(u32, p.theme().rail_px orelse 12) * p.settings.grip_width / 100)), }; place(p, s); // Separators have their own role; a scrollbar need not frame the page. @@ -204,6 +417,13 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface { if (region.kind == .notice) try joinNotice(p, s, arena, region); } + for (p.panes, p.lifts, 0..) |slot, spring, id| { + if (slot == null or id == p.active) continue; + const focus = spring.value(p.now_ns) / animation.Motion.of(p.settings.motion).gain; + const amount = @as(f32, @floatFromInt(inactiveDim(p))) / 100 * std.math.clamp(1 - focus, 0, 1); + if (amount > 0.001) recede(p, s, id, amount); + } + try paintHeaders(p, s, arena); switch (p.drag) { @@ -479,16 +699,30 @@ fn renderPane(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r: if (th.bg) |bg| s.fill(tx, r.y, tw, r.h, .{ .bg = .{ .rgb = bg } }); } - const box_bg = if (active) chrome.box else chrome.box_dim; - const box_lum = (@as(u16, box_bg[0]) * 3 + @as(u16, box_bg[1]) * 6 + @as(u16, box_bg[2])) / 10; - const box_ink: [3]u8 = if (box_lum > 140) .{ 0x00, 0x00, 0x00 } else .{ 0xff, 0xff, 0xff }; + // The grip is acme's button: the focused pane's filled, the others' + // a ring round the tag's ground, a dirty one's the dirty colour inside + // its ring. A pixel shell draws the ring (gui.zig gripDecor); a grid, + // which cannot, marks dirty in the grip's second cell with a bold `*` + // on the dirty colour, a cue that survives any palette. + const band = if (active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg; + const dirty = tagline.dirty(pane); + const box_bg = gripBg(p, active); const box_style: CellStyle = .{ - .fg = .{ .rgb = box_ink }, + .fg = .{ .rgb = inkOn(box_bg) }, .bg = .{ .rgb = box_bg }, // Grips keep physical grid dimensions, independently of tag text. .font_role = .tagline, }; - s.fill(r.x, tag_y, config.GUTTER, pane.tag_rows, box_style); + // On the first row alone, as acme's button is; the rows under it are + // the tag band's, and still grab the pane (the region spans them all, + // as acme's button column does). + s.fill(r.x, tag_y, config.GUTTER, @min(1, pane.tag_rows), box_style); + if (dirty and pane.tag_rows > 0 and config.GUTTER > 1) + s.set(r.x + config.GUTTER - 1, tag_y, "*", .{ .fg = .{ .rgb = inkOn(boxDirty(p)) }, .bg = .{ .rgb = boxDirty(p) }, .bold = true, .font_role = .tagline }); + s.fill(r.x, tag_y + 1, config.GUTTER, pane.tag_rows -| 1, .{ + .bg = .{ .rgb = band }, + .font_role = .tagline, + }); s.set(r.x, tag_y, switch (pane.body.mode) { .normal => config.tag_normal, .insert => config.tag_insert, @@ -552,7 +786,8 @@ pub fn paintHeaders(p: *Pardes, s: *Surface, arena: std.mem.Allocator) !void { const grip_bg = if (grabbed) chrome.column_box else chrome.column_box_dim; const lum = (@as(u16, grip_bg[0]) * 3 + @as(u16, grip_bg[1]) * 6 + @as(u16, grip_bg[2])) / 10; const ink: [3]u8 = if (lum > 140) .{ 0, 0, 0 } else .{ 255, 255, 255 }; - s.fill(p.col_x[c], region.rect.y, grip, column_rows, .{ .bg = .{ .rgb = grip_bg }, .fg = .{ .rgb = ink }, .font_role = .tagline }); + s.fill(p.col_x[c], region.rect.y, grip, @min(1, column_rows), .{ .bg = .{ .rgb = grip_bg }, .fg = .{ .rgb = ink }, .font_role = .tagline }); + s.fill(p.col_x[c], region.rect.y + 1, grip, column_rows -| 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); s.fill(p.col_x[c] + grip, region.rect.y, inset - grip, column_rows, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); } const top = if (p.header_focus and p.header_column == c) p.header_top else 0; @@ -685,7 +920,7 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer, var cell = panes.File.rawDisplayCol(shown, bounds.lo) -| scroll; const end = panes.File.rawDisplayCol(shown, bounds.hi) -| scroll; while (cell < end and cell < columns) : (cell += 1) { - surface.at(@intCast(cell), 0).style.bg = .{ .rgb = p.theme().sel_bg }; + surface.at(@intCast(cell), 0).style.bg = .{ .rgb = p.theme().tag_sel_bg orelse p.theme().sel_bg }; surface.at(@intCast(cell), 0).style.fg = .{ .rgb = p.theme().sel_fg }; } }; @@ -695,7 +930,7 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer, var col = panes.File.rawDisplayCol(shown, span.lo) -| scroll; const end = panes.File.rawDisplayCol(shown, span.hi) -| scroll; while (col < end and col < columns) : (col += 1) { - surface.at(@intCast(col), 0).style.bg = .{ .rgb = p.theme().sel_bg }; + surface.at(@intCast(col), 0).style.bg = .{ .rgb = p.theme().tag_sel_bg orelse p.theme().sel_bg }; surface.at(@intCast(col), 0).style.fg = .{ .rgb = p.theme().sel_fg }; } } @@ -714,9 +949,9 @@ fn joinPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r if (r.w <= TAG_TEXT_INSET) return paintPaneTag(p, s, arena, pane, r, id, active, capacity); const chrome = p.chromeTheme(); const region = s.region(.tag, id).?; - const tag = try tagline.tagText(p, arena, pane); + const tag = (try tagline.shownTag(p, arena, pane)).text; const rows = pane.tag_rows; - // As wide as the widest line it shows, and never narrower than its + // As wide as the widest row it shows, and never narrower than its // viewport. var widest: usize = 0; var split = std.mem.splitScalar(u8, tag, '\n'); @@ -860,8 +1095,10 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa // A row per line shown, from `tag_top`: the tag's first line carries // the prefix, the rest are the tag's own text alone. const top = pane.tag_top; + // Its lines wrapped at its width (tagline.shownTag): a row per row shown. + const shown = try tagline.shownTag(p, arena, pane); var lines: [MAX_TAG_ROWS][]const u8 = @splat(""); - var split = std.mem.splitScalar(u8, whole, '\n'); + var split = std.mem.splitScalar(u8, shown.text, '\n'); for (0..top) |_| _ = split.next(); for (lines[0..rows]) |*line| line.* = split.next() orelse ""; // The keyboard's caret, as a row and a byte offset in that row's @@ -870,42 +1107,45 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa const Caret = struct { row: usize, at: usize }; const caret: ?Caret = if (!active) null - else if (pane.prompt == .name) - .{ .row = 0, .at = @min(lines[0].len, @as(usize, @intCast(@max(0, pane.input.cur_col)))) } - else if (pane.focus == .tag and pane.prompt == .none and pane.tag.cur_row >= top and pane.tag.cur_row < top + rows) caret: { - const row: usize = @intCast(pane.tag.cur_row - top); - break :caret .{ .row = row, .at = @min(lines[row].len, @as(usize, @intCast(@max(0, pane.tag.cur_col)))) }; + else if (pane.prompt == .name) caret: { + // A drafted name is the prefix: its caret is on row 0 of the tag, + // the rows it wrapped onto included. + const at = modal.positionAt(shown.text, shown.fromReal(@intCast(@max(0, pane.input.cur_col)))); + if (at.row < top or at.row >= top + rows) break :caret null; + break :caret .{ .row = at.row - top, .at = @min(lines[at.row - top].len, at.col) }; + } + else if (pane.focus == .tag and pane.prompt == .none) caret: { + const real = modal.offsetAt(whole, .{ .row = @intCast(@max(0, pane.tag.cur_row)), .col = @intCast(@max(0, pane.tag.cur_col)) }); + const at = modal.positionAt(shown.text, shown.fromReal(real)); + if (at.row < top or at.row >= top + rows) break :caret null; + const row: usize = at.row - top; + break :caret .{ .row = row, .at = @min(lines[row].len, at.col) }; } else null; - if (caret) |at| { - const line = lines[at.row]; - const col = panes.File.rawDisplayCol(line, at.at); - var scroll: usize = pane.tag_scroll; - if (col < scroll) scroll = col; - if (col >= scroll + capacity) scroll = col - capacity + 1; - var raw = panes.File.rawAtDisplay(line, scroll); - scroll = panes.File.rawDisplayCol(line, raw); - if (col >= scroll + capacity) { - raw = modal.nextGrapheme(line, raw); - scroll = panes.File.rawDisplayCol(line, raw); - } - // A line past a u16 of columns keeps its caret off the band. - pane.tag_scroll = @intCast(@min(scroll, std.math.maxInt(u16))); - } else if (pane.focus != .tag) pane.tag_scroll = 0; + // Its rows are wrapped to fit: a pane tag never scrolls sideways. + _ = capacity; + pane.tag_scroll = 0; for (lines[0..rows], 0..) |line, row| { // The scroll is the caret's line's: a shorter row shows nothing. const start = @min(line.len, panes.File.rawAtDisplay(line, pane.tag_scroll)); _ = s.print(tx, tag_y + @as(u16, @intCast(row)), tw, line[start..], tag_style); } - const tag = lines[0]; - if (top == 0) if (tagTintRange(pane, tag, prefix_len)) |name| { - const lo = @max(name.lo, panes.File.rawAtDisplay(tag, pane.tag_scroll)); - const hi = @min(name.hi, tag.len); - if (lo < hi) { - var col = panes.File.rawDisplayCol(tag, lo) -| pane.tag_scroll; - const end = panes.File.rawDisplayCol(tag, hi) -| pane.tag_scroll; - const name_fg = if (active and p.settings.focus_tint) chrome.tag_active_name_fg else chrome.tag_name_fg; + // The name's tint, in the tag's first line, over whichever rows it + // wrapped onto. + const first_line = whole[0 .. std.mem.indexOfScalar(u8, whole, '\n') orelse whole.len]; + if (tagTintRange(pane, first_line, prefix_len)) |name| if (name.lo < name.hi) { + const name_fg = if (active and p.settings.focus_tint) chrome.tag_active_name_fg else chrome.tag_name_fg; + const a = modal.positionAt(shown.text, shown.fromReal(name.lo)); + const b = modal.positionAt(shown.text, shown.fromReal(@min(name.hi, first_line.len))); + var row = @max(a.row, top); + while (row <= b.row and row < top + rows) : (row += 1) { + const line = lines[row - top]; + const from = if (row == a.row) a.col else 0; + const to = if (row == b.row) b.col else line.len; + var col = panes.File.rawDisplayCol(line, @min(from, line.len)); + const end = panes.File.rawDisplayCol(line, @min(to, line.len)); + const y = tag_y + @as(u16, @intCast(row - top)); while (col < end and col < tw) : (col += 1) - s.at(tx + @as(u16, @intCast(col)), tag_y).style.fg = .{ .rgb = name_fg }; + s.at(tx + @as(u16, @intCast(col)), y).style.fg = .{ .rgb = name_fg }; } }; if (p.look_hover_preview) |preview| { @@ -923,8 +1163,8 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa const lo = @min(range.anchor, range.head); const hi = @max(range.anchor, range.head); if (modal.nextGrapheme(whole, lo) >= hi) continue; - const first = modal.positionAt(whole, lo); - const last = modal.positionAt(whole, hi); + const first = modal.positionAt(shown.text, shown.fromReal(lo)); + const last = modal.positionAt(shown.text, shown.fromReal(hi)); var row = @max(first.row, top); while (row <= last.row and row < top + rows) : (row += 1) { const line = lines[row - top]; @@ -936,7 +1176,7 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa while (col + pane.tag_scroll < end and col < tw) : (col += 1) { const cell = s.at(tx + @as(u16, @intCast(col)), y); cell.default = false; - cell.style.bg = .{ .rgb = th.sel_bg }; + cell.style.bg = .{ .rgb = th.tag_sel_bg orelse th.sel_bg }; cell.style.fg = .{ .rgb = th.sel_fg }; } } @@ -956,7 +1196,7 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa for (pane.sel, 0..) |selection, button| { if (selection.state == .none or (selection.state == .done and button != 0)) continue; if (button != pardes.sel_slot or @min(selection.r0, selection.r1) >= rows) continue; - body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, 0, th.sel_bg, th.sel_fg); + body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, 0, th.tag_sel_bg orelse th.sel_bg, th.sel_fg); } if (p.look_hover_preview) |preview| { if (preview.pane == id and preview.serial == pane.serial) if (preview.sel) |sel| if (@min(sel.r0, sel.r1) < rows) { @@ -965,10 +1205,100 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa }; } const sel_btn = body_layer.buttonTints(th); + const sel_ink = body_layer.buttonInks(th); for (pane.sel, 0..) |selection, button| { if (selection.state == .none or (selection.state == .done and button != 0)) continue; if (button == pardes.sel_slot or @min(selection.r0, selection.r1) >= rows) continue; - body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, 0, sel_btn[button], th.sel_fg); + body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, 0, sel_btn[button], sel_ink[button]); + } +} + +test "an unfocused grip shows on its tag in the grid, in every native theme" { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 }); + defer p.deinit(); + _ = try p.setTestFile("text\n"); + p.newScratchBelow(0); + p.animate_theme_changes = false; + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + for (pardes.themes[0..colors.native_count], 0..) |theme, index| { + colors.setThemeIndex(p, index); + p.active = 1; + _ = arena.reset(.retain_capacity); + const s = try p.render(arena.allocator()); + const r = p.rects[0]; + const grip = s.at(r.x, p.tagTop(p.panes[0].?, r)).style.bg.rgb; + const ratio = contrast(grip, p.chromeTheme().tag_bg); + // ponytail: dark and helix have always had a faint grip (1.2:1); flagged, not changed here. + const floor: f32 = if (std.mem.eql(u8, theme.name, "dark") or std.mem.eql(u8, theme.name, "helix")) 1.2 else 1.5; + if (ratio < floor) std.debug.print("{s}: grip {d:.2}\n", .{ theme.name, ratio }); + try std.testing.expect(ratio >= floor); + } +} + +test "a grip's four states, focused or not and clean or dirty, are each its own in every native theme" { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 }); + defer p.deinit(); + const pane = try p.setTestFile("text\n"); + p.newScratchBelow(0); + p.animate_theme_changes = false; + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + const State = struct { first: Cell, second: Cell, dirty: bool }; + for (0..colors.native_count) |index| { + colors.setThemeIndex(p, index); + var states: [4]State = undefined; + for (&states, 0..) |*state, i| { + p.active = if (i & 1 == 0) 0 else 1; + const saved = pane.file.?.saved_revision; + pane.file.?.revision = if (i & 2 != 0) saved + 1 else saved; + _ = arena.reset(.retain_capacity); + const s = try p.render(arena.allocator()); + const r = p.rects[0]; + const y = p.tagTop(pane, r); + state.* = .{ .first = s.at(r.x, y).*, .second = s.at(r.x + 1, y).*, .dirty = s.region(.grip, 0).?.dirty }; + try std.testing.expectEqual(i & 2 != 0, state.dirty); + if (i & 2 == 0) continue; + // Dirty is more than a shade: the grid prints a bold `*` (one + // grid for every shell; a pixel shell draws its button over it). + try std.testing.expectEqualStrings("*", state.second.grapheme()); + try std.testing.expect(state.second.style.bold); + } + for (states, 0..) |a, i| for (states[i + 1 ..]) |b| { + const same = std.meta.eql(a.first.style.bg, b.first.style.bg) and a.second.visuallyEqual(&b.second); + try std.testing.expect(!same); + }; + } + pane.file.?.revision = pane.file.?.saved_revision; +} + +test "a tag of three rows has its grip on the first, and band under it that still grabs the pane" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 }); + defer p.deinit(); + const pane = try p.setTestFile("text\n"); + pane.tag.own = try std.testing.allocator.dupe(u8, "Second\nthird\nfourth"); + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + for ([_]bool{ false, true }) |bottom| { + p.settings.tag_bottom = bottom; + p.sync(); + _ = arena.reset(.retain_capacity); + const s = try p.render(arena.allocator()); + const r = p.rects[0]; + const top = p.tagTop(pane, r); + try std.testing.expect(pane.tag_rows >= 3); + const chrome = p.chromeTheme(); + const band = if (p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg; + for (0..config.GUTTER) |dx| { + const x = r.x + @as(u16, @intCast(dx)); + try std.testing.expectEqual(Color{ .rgb = chrome.box }, s.at(x, top).style.bg); + for (1..pane.tag_rows) |line| { + const cell = s.at(x, top + @as(u16, @intCast(line))); + try std.testing.expectEqual(Color{ .rgb = band }, cell.style.bg); + try std.testing.expectEqualStrings(" ", cell.grapheme()); + } + } + try std.testing.expectEqual(pane.tag_rows, s.region(.grip, 0).?.rect.h); } } @@ -1108,6 +1438,221 @@ test "a notice wider than its band, as a shader's compile error is, draws its en } } +test "a lift is sampled at each frame's own time, and asks for frames only while it moves" { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + // A grid has no in-between: there a lift snaps. + if (!builtins.capabilities.lift) return error.SkipZigTest; + _ = try p.setTestFile("text\n"); + p.newScratchBelow(0); + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + const ms = std.time.ns_per_ms; + p.settings.lift = .shadow; + p.advance(1000 * ms); + _ = try p.render(arena.allocator()); + p.advance(3000 * ms); + _ = arena.reset(.retain_capacity); + _ = try p.render(arena.allocator()); + try std.testing.expect(!p.continuous()); + p.active = 0; + var lifts: [3]f32 = undefined; + // 144 Hz: 6.9 ms apart, none of them on a 16 ms step of its own. + for (&lifts, 0..) |*lift, i| { + p.advance(3000 * ms + @as(u64, @intCast(i)) * 6_944_444); + _ = arena.reset(.retain_capacity); + const s = try p.render(arena.allocator()); + lift.* = for (s.regionList()) |region| { + if (region.kind == .grip and region.owner == p.active) break region.lift; + } else return error.TestExpectedGrip; + try std.testing.expect(p.continuous()); + } + try std.testing.expect(lifts[0] < lifts[1] and lifts[1] < lifts[2]); + // Settled, it is exactly up, and the core is idle again. + p.advance(5000 * ms); + _ = arena.reset(.retain_capacity); + _ = try p.render(arena.allocator()); + try std.testing.expect(!p.continuous()); + try std.testing.expectEqual(@as(?u64, null), p.nextWake()); +} + +/// The contrast a pane's first body glyph `g` is drawn at, on the grid. +fn glyphContrast(p: *Pardes, s: *Surface, id: usize, g: u8) f32 { + const r = p.rects[id]; + const th = p.theme(); + var row = p.bodyTop(p.panes[id].?, r); + while (row < r.y + r.h) : (row += 1) { + var col = r.x + config.GUTTER; + while (col < r.x + r.w) : (col += 1) { + const cell = s.at(col, row); + if (cell.default or cell.len != 1 or cell.text[0] != g) continue; + var fg = resolved(cell.style.fg, th.fg orelse .{ 255, 255, 255 }, th.palette).?; + const bg = resolved(cell.style.bg, th.bg orelse .{ 0, 0, 0 }, th.palette).?; + if (cell.style.dim) for (&fg) |*channel| { + channel.* = @intCast(@as(u16, channel.*) * 6 / 10); + }; + return contrast(fg, bg); + } + } + unreachable; +} + +test "a focused pane's text is never at less contrast than an unfocused one's, in any Lift, dim or theme" { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + _ = try p.setTestFile("text\n"); + p.newScratchBelow(0); + p.animate_theme_changes = false; + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + const s_ns = std.time.ns_per_s; + var now: u64 = s_ns; + for (0..pardes.themes.len) |theme| { + colors.setThemeIndex(p, theme); + for (std.enums.values(config.Runtime.LiftStyle)) |style| for ([_]u8{ 0, 100 }) |dim| { + p.settings.lift = style; + p.settings.inactive_dim = dim; + var seen: [2]f32 = undefined; + for (&seen, [_]usize{ 0, 1 }) |*c, focus| { + p.active = focus; + _ = arena.reset(.retain_capacity); + _ = try p.render(arena.allocator()); + now += 2 * s_ns; + p.advance(now); + _ = arena.reset(.retain_capacity); + c.* = glyphContrast(p, try p.render(arena.allocator()), 0, 't'); + } + try std.testing.expect(seen[0] >= seen[1] - 1e-3); + // And the unfocused one is still legible. + try std.testing.expect(seen[1] >= @min(seen[0], 4.5) - 2e-2); + }; + } +} + +test "a dim leaves the focused pane's tag, grip and cells exactly as they were" { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + _ = try p.setTestFile("text\n"); + p.newScratchBelow(0); + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + p.active = 0; + p.advance(std.time.ns_per_s); + const plain = try p.render(arena.allocator()); + const r = p.rects[0]; + var before: std.ArrayList(Cell) = .empty; + defer before.deinit(std.testing.allocator); + for (r.y..r.y + r.h) |row| try before.appendSlice(std.testing.allocator, plain.cells[row * plain.cols + r.x ..][0..r.w]); + const tag_bg = plain.tag_layers[0].bg; + const tag_cells = try std.testing.allocator.dupe(Cell, plain.tag_layers[0].cells[0 .. @as(usize, plain.tag_layers[0].cols) * plain.tag_layers[0].rows]); + defer std.testing.allocator.free(tag_cells); + p.settings.inactive_dim = 100; + p.advance(3 * std.time.ns_per_s); + const dimmed = try p.render(arena.allocator()); + var i: usize = 0; + for (r.y..r.y + r.h) |row| for (dimmed.cells[row * dimmed.cols + r.x ..][0..r.w]) |cell| { + try std.testing.expect(cell.visuallyEqual(&before.items[i])); + i += 1; + }; + try std.testing.expectEqual(tag_bg, dimmed.tag_layers[0].bg); + for (tag_cells, dimmed.tag_layers[0].cells[0..tag_cells.len]) |a, b| try std.testing.expect(a.visuallyEqual(&b)); +} + +test "a dim never takes a pair below its own contrast or 4.5" { + for (pardes.themes) |theme| { + const fg = theme.fg orelse continue; + const bg = theme.bg orelse continue; + for ([_]bool{ false, true }) |reverse| for ([_]bool{ false, true }) |faint| { + var style: CellStyle = .{ .fg = .{ .rgb = fg }, .bg = .{ .rgb = bg }, .reverse = reverse, .dim = faint }; + var ink = fg; + if (faint) for (&ink) |*channel| { + channel.* = @intCast(@as(u16, channel.*) * 6 / 10); + }; + var memo: Memo = .{}; + fadeStyle(&style, null, null, null, 1, &memo); + const after = contrast(style.fg.rgb, style.bg.rgb); + try std.testing.expect(after >= @min(contrast(ink, bg), 4.5) - 2e-2); + }; + } +} + +test "a dim leaves a pair already under 4.5 alone, and reads a theme's own sixteen" { + var memo: Memo = .{}; + var low: CellStyle = .{ .fg = .{ .rgb = .{ 0x80, 0x80, 0x80 } }, .bg = .{ .rgb = .{ 0x60, 0x60, 0x60 } } }; + fadeStyle(&low, null, null, null, 1, &memo); + try std.testing.expectEqual([3]u8{ 0x80, 0x80, 0x80 }, low.fg.rgb); + // acme's bright white is black: faded, it stays a grey, never a white. + const acme = for (pardes.themes) |theme| { + if (std.mem.eql(u8, theme.name, "acme")) break theme; + } else unreachable; + var bright: CellStyle = .{ .fg = .{ .index = 15 } }; + memo = .{}; + fadeStyle(&bright, acme.fg, acme.bg, acme.palette, 0.3, &memo); + const rgb = bright.fg.rgb; + try std.testing.expect(rgb[0] == rgb[1] and rgb[0] < 0xc0); + // Reverse: the paper moves, the ink stays. + var reverse: CellStyle = .{ .fg = .{ .rgb = .{ 0, 0, 0 } }, .bg = .{ .rgb = .{ 255, 255, 255 } }, .reverse = true }; + memo = .{}; + fadeStyle(&reverse, null, null, null, 0.3, &memo); + try std.testing.expectEqual([3]u8{ 0, 0, 0 }, reverse.fg.rgb); + try std.testing.expect(reverse.bg.rgb[0] < 255); +} + +test "with no lift and no dim a focus switch asks for no frames" { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + _ = try p.setTestFile("text\n"); + p.newScratchBelow(0); + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + p.advance(std.time.ns_per_s); + _ = try p.render(arena.allocator()); + p.active = 0; + _ = arena.reset(.retain_capacity); + _ = try p.render(arena.allocator()); + try std.testing.expect(!p.continuous()); +} + +test "each Motion flavour reaches the lift as its own trajectory" { + // A grid snaps; the flavours are a pixel shell's. + if (!builtins.capabilities.lift) return error.SkipZigTest; + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + _ = try p.setTestFile("text\n"); + p.newScratchBelow(0); + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + p.settings.lift = .shadow; + var peaks: [5]f32 = undefined; + const ms = std.time.ns_per_ms; + var start: u64 = std.time.ns_per_s; + for (std.enums.values(animation.Motion.Flavour), &peaks) |flavour, *peak| { + p.settings.motion = flavour; + p.active = 1; + p.advance(start); + _ = arena.reset(.retain_capacity); + _ = try p.render(arena.allocator()); + start += 3 * std.time.ns_per_s; + p.advance(start); + _ = try p.render(arena.allocator()); + p.active = 0; + peak.* = 0; + var t: u64 = 0; + while (t <= 1500 * ms) : (t += 4 * ms) { + p.advance(start + t); + _ = arena.reset(.retain_capacity); + const s = try p.render(arena.allocator()); + const lift = for (s.regionList()) |region| { + if (region.kind == .grip and region.owner == 0) break region.lift; + } else unreachable; + peak.* = @max(peak.*, lift); + } + start += 3 * std.time.ns_per_s; + } + // Bouncy and playful pass their (exaggerated) mark; smooth never does. + try std.testing.expect(peaks[2] <= 1.001 and peaks[3] > 1.25 and peaks[4] > peaks[3]); +} + 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 8e669b77..ef442ddb 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -99,6 +99,13 @@ pub const PaneKind = enum { pub const Terminal = struct { cwd: []const u8 = "", + /// The shell it runs when not the configured one (`Tty bash`); empty, + /// the configured one, as in dumps from before. + shell: []const u8 = "", + /// A command pane's line and how it ended (null: unknown, or still + /// running when dumped); empty for an interactive terminal. + command: []const u8 = "", + status: ?u8 = null, /// The rendered scrollback and screen as plain text, one row per line. stream: []const u8 = "", stream_b64: []const u8 = "", @@ -161,6 +168,9 @@ pub const Pane = struct { rows: u16 = 0, vweight: f32 = 1, collapsed: bool = false, + /// Its serial when dumped (0 in older dumps): a Restore logs + /// `restored <old> <new>` for it. + serial: u32 = 0, /// Exact editable tail when the user touched it. Null retains the live /// default; a present empty slice deliberately restores an empty tail. tag_tail: ?[]const u8 = null, @@ -626,6 +636,7 @@ pub fn dumpState(p: *Pardes) !void { else try pardes.panes.Terminal.dumpPane(pane, arena, tag, body, scroll); dp.tag_tail = pane.tag.own; + dp.serial = pane.serial; dump_panes[panes_len] = dp; panes_len += 1; } @@ -676,16 +687,22 @@ pub fn restore(p: *Pardes, zon_bytes: []const u8, from: []const u8) !*Pardes { var opts = p.opts; opts.cols = p.screen_w; opts.rows = p.screen_h; - const replacement = try initDump(p.gpa, opts, zon_bytes, p); + var old: [MAX_PANES]u32 = @splat(0); + const replacement = try initDump(p.gpa, opts, zon_bytes, p, &old); pardes.ctlfs.events.notePath(replacement, "restore", from); + // Which serial each pane had, for a script that held the old ones. + for (replacement.panes, old) |slot, was| if (slot) |pane| if (was != 0) { + var rec: [32]u8 = undefined; + pardes.ctlfs.events.notePath(replacement, "restored", std.fmt.bufPrint(&rec, "{d} {d}", .{ was, pane.serial }) catch continue); + }; return replacement; } pub fn initFromDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8) !*Pardes { - return initDump(gpa, opts, zon_bytes, null); + return initDump(gpa, opts, zon_bytes, null, null); } -fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes) !*Pardes { +fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes, old_serials: ?*[MAX_PANES]u32) !*Pardes { const image_gpa = opts.image_allocator orelse gpa; const pdf_gpa = opts.pdf_allocator orelse gpa; const tree_sitter_gpa = opts.tree_sitter_allocator orelse gpa; @@ -735,17 +752,26 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo } for (st.panes, 0..) |src, i| { if (i >= MAX_PANES) break; + if (old_serials) |o| o[i] = src.serial; const pane: *pardes.panes.Pane = switch (src.kind) { .terminal => terminal: { const t = src.terminal.?; // A host that runs shells gets them back: the old history // above a new shell in the directory it was in. The web // shell and an isolated build only ever replay it. + // A command pane comes back finished, showing what it said + // and how it ended: its command is not run again. const live = comptime (terminal_panes and platform != .web and !isolated); - const revive = live and t.cwd.len <= effect_path_cap; + const revive = live and t.cwd.len <= effect_path_cap and t.command.len == 0; const restored = try pardes.panes.Terminal.restore(p, src, revive); p.installPane(i, restored); try restored.setOwnedCwd(t.cwd); + if (t.shell.len > 0) restored.shell = try gpa.dupe(u8, t.shell); + if (t.command.len > 0) { + restored.command = try gpa.dupe(u8, t.command); + restored.command_done = true; + restored.command_status = t.status; + } if (std.mem.startsWith(u8, src.tag, "TTY ")) restored.body.mode = .tty; if (revive) p.emit(.{ .spawn = .{ .pane = @intCast(i), .serial = restored.serial, .cwd = .from(t.cwd) } }); break :terminal restored; diff --git a/src/edit.zig b/src/edit.zig index 84bbb292..a194ef48 100644 --- a/src/edit.zig +++ b/src/edit.zig @@ -202,7 +202,7 @@ pub fn clipYank(p: *Pardes, pane: *Pane, main_only: bool) void { /// (the tag's lines first, the ones it shows) pub fn paneText(p: *Pardes, pane: *Pane) ![]u8 { const arena = p.scratch.allocator(); - const tag = try tagline.tagText(p, arena, pane); + const tag = (try tagline.shownTag(p, arena, pane)).text; const from = modal.lineStartOffset(tag, pane.tag_top); const cut = modal.lineStartOffset(tag, pane.tag_top + pane.tag_rows); const shown = if (cut < tag.len) tag[from .. cut - 1] else tag[from..]; @@ -234,10 +234,13 @@ pub fn capturePointerSelection(p: *Pardes, pane: *Pane, slot: usize) !void { errdefer p.gpa.free(rows); const raw_text = if (raw) try p.gpa.dupe(u8, try selectionText(p, pane, gesture)) else null; const prefix: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0; - const left = @max(0, @min(gesture.c0, gesture.c1)); - const right = @max(0, @max(gesture.c0, gesture.c1)); for (rows, 0..) |*row, index| { const visible = first + @as(i32, @intCast(index)); + // A stream: only the first row starts where the press was; the + // others start after the line numbers, and all but the last run on. + const cols = sweepCols(gesture, visible + @as(i32, pane.tag_rows)).?; + const left: i32 = if (cols.first) cols.lo else prefix; + const right: i32 = cols.hi orelse std.math.maxInt(i32) / 2; var source = pane.wrapAt(visible); if (raw) source = .{ .line = panes.Terminal.gridOffset(pane) + visible, .at = 0 }; const line = if (raw) modal.lineSlice(body, @intCast(visible)) else pointerSourceLine(pane, terminal_lines, source.line); @@ -250,7 +253,8 @@ pub fn capturePointerSelection(p: *Pardes, pane: *Pane, slot: usize) !void { row.* = .{ .row = source.line, .lo = lo, - .hi = if (right < prefix) lo else @min(end, modal.nextGrapheme(line, at)), + .hi = if (right < prefix) lo else if (cols.hi == null) end else @min(end, modal.nextGrapheme(line, at)), + .to_edge = cols.hi == null, .raw_terminal = raw, .prompt_bytes = if (raw) panes.Terminal.promptPrefixBytes(pane, source.line, line) else 0, }; @@ -294,40 +298,55 @@ pub fn selectionText(p: *Pardes, pane: *Pane, sl: Pane.Sel) ![]const u8 { } return result; } - const r0 = @min(sl.r0, sl.r1); - const r1 = @max(sl.r0, sl.r1); - const c0: usize = @intCast(@max(0, @min(sl.c0, sl.c1))); - const c1: usize = @intCast(@max(0, @max(sl.c0, sl.c1))); const text = try paneText(p, pane); - var total: usize = 0; - var selected: usize = 0; - var count_it = std.mem.splitAny(u8, text, "\n"); - var count_row: i32 = 0; - while (count_it.next()) |line| : (count_row += 1) { - if (count_row < r0 or count_row > r1) continue; - const b0 = @min(panes.File.renderedLineByteCol(pane, count_row, line, c0), line.len); - const b1 = modal.nextGrapheme(line, @min(panes.File.renderedLineByteCol(pane, count_row, line, c1), line.len)); - total += b1 - b0 + @intFromBool(selected > 0); - selected += 1; - } - const out = try arena.alloc(u8, total); - var at: usize = 0; + const shown = try tagline.shownTag(p, arena, pane); + var out: std.ArrayList(u8) = .empty; var it = std.mem.splitAny(u8, text, "\n"); var v: i32 = 0; var first = true; while (it.next()) |line| : (v += 1) { - if (v < r0 or v > r1) continue; - if (!first) { - out[at] = '\n'; - at += 1; - } + const cols = sweepCols(sl, v) orelse continue; + // A row the tag's wrapping began carries on its text: no newline. + const soft = v > 0 and v < pane.tag_rows and std.mem.indexOfScalar( + usize, + shown.soft, + modal.lineStartOffset(shown.text, @as(usize, @intCast(v)) + pane.tag_top) -| 1, + ) != null; + if (!first and !soft) try out.append(arena, '\n'); first = false; - const b0 = @min(panes.File.renderedLineByteCol(pane, v, line, c0), line.len); - const b1 = modal.nextGrapheme(line, @min(panes.File.renderedLineByteCol(pane, v, line, c1), line.len)); - @memcpy(out[at..][0 .. b1 - b0], line[b0..b1]); - at += b1 - b0; + // Past the first row a body row starts after its line numbers. + const gutter: i32 = if (pane.file != null and v >= pane.tag_rows) panes.File.gutterWidth(pane) else 0; + const lo: usize = @intCast(if (cols.first) cols.lo else gutter); + const b0 = @min(panes.File.renderedLineByteCol(pane, v, line, lo), line.len); + const b1 = if (cols.hi) |hi| + @max(b0, modal.nextGrapheme(line, @min(panes.File.renderedLineByteCol(pane, v, line, @intCast(hi)), line.len))) + else + line.len; + try out.appendSlice(arena, line[b0..b1]); } - return out; + return out.items; +} + +/// A sweep's columns on `row`, in its Sel space: acme's stream, not a +/// rectangle (libframe/frselect.c:115-140, frselectpaint): the first row +/// from the press to its end, the rows between whole, the last from its +/// start to the release. `hi` null runs to the end of the row. +pub const SweepCols = struct { lo: i32, hi: ?i32, first: bool }; + +pub fn sweepCols(sl: Pane.Sel, row: i32) ?SweepCols { + const start, const end = sweepEnds(sl); + if (row < start[0] or row > end[0]) return null; + return .{ + .lo = if (row == start[0]) @max(0, start[1]) else 0, + .hi = if (row == end[0]) @max(0, end[1]) else null, + .first = row == start[0], + }; +} + +/// The sweep's two ends in reading order, each (row, col). +pub fn sweepEnds(sl: Pane.Sel) struct { [2]i32, [2]i32 } { + const fwd = sl.r0 < sl.r1 or (sl.r0 == sl.r1 and sl.c0 <= sl.c1); + return if (fwd) .{ .{ sl.r0, sl.c0 }, .{ sl.r1, sl.c1 } } else .{ .{ sl.r1, sl.c1 }, .{ sl.r0, sl.c0 } }; } pub fn spanHas(r: i32, c: i32, ar: i32, ac: i32, br: i32, bc: i32) bool { @@ -738,6 +757,19 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void { Key.left, Key.right, Key.up, Key.down => { const lines = paneCursorLines(p, t) catch return; const cur0 = t.toModalCursor(); + // In a pane's tag, Up on its first line collapses it and Down + // on its last expands it, where they would move nothing; acme's + // arrows in a tag always do (plan9port text.c:776, Tagdown/Tagup). + if (t.what == .tag) if (t.pane()) |pane| { + if (key.cp == Key.up and cur0.row == 0) { + pane.tag_expanded = false; + return; + } + if (key.cp == Key.down and cur0.row + 1 >= lines.len) { + pane.tag_expanded = true; + return; + } + }; const nc = switch (key.cp) { Key.left => modal.charLeft(lines, cur0), Key.right => modal.charRight(lines, cur0), @@ -2000,7 +2032,8 @@ pub fn pushUndo(p: *Pardes, t: *Text) void { // one keystroke, one undo step — even when it edited at ten cursors. if (p.multi_on and !p.multi_first) return; switch (t.what) { - .body => {}, + // A B1 chord took its one undo point when it began (mouse.zig). + .body => if (p.chord_hold) return, .tag, .column, .workspace => return t.remember(p.gpa, if (t.own) |own| .{ .text = own } else null), // a prompt's answer is typed once and gone; it keeps no history .input => return, @@ -2215,3 +2248,120 @@ test "only the SPC clipboard commands cross to the system clipboard" { p.update(.{ .key = .{ .cp = 'l' } }); try std.testing.expect(p.clip_pending == null); } + +/// acme's double-click (plan9port acme text.c:1407, textdoubleclick): from +/// the point `q` (before the character clicked), the text up to the match +/// of a bracket just before it or just after it (text.c:1386, `{[(<«` and +/// their closers), the whole line at a line's start or end, the quoted text +/// next to `' " \``, else the word, acme's isalnum (util.c:328) run. +pub fn doubleClickRange(text: []const u8, q_in: usize) [2]usize { + const sets = [_][2][]const u21{ + .{ &.{ '{', '[', '(', '<', 0xab }, &.{ '}', ']', ')', '>', 0xbb } }, + .{ &.{'\n'}, &.{'\n'} }, + .{ &.{ '\'', '"', '`' }, &.{ '\'', '"', '`' } }, + }; + const q = @min(q_in, text.len); + for (sets) |set| { + const left, const right = set; + // the character to the left, looking right + const before = if (q == 0) .{ @as(u21, '\n'), @as(usize, 0) } else runeBefore(text, q); + if (std.mem.indexOfScalar(u21, left, before[0])) |k| { + var at = q; + if (clickMatch(text, before[0], right[k], true, &at)) { + const closer = if (before[0] == '\n') 0 else runeLen(right[k]); + return .{ q, at - closer }; + } + return .{ q, q }; + } + // the character to the right, looking left + const after = if (q == text.len) @as(u21, '\n') else runeAt(text, q); + if (std.mem.indexOfScalar(u21, right, after)) |k| { + var at = q; + if (clickMatch(text, after, left[k], false, &at)) { + const q1 = q + @as(usize, @intFromBool(q < text.len and after == '\n')); + var q0 = at; + if (after != '\n' or at != 0 or (text.len > 0 and text[0] == '\n')) q0 += runeLen(left[k]); + return .{ @min(q0, q1), q1 }; + } + return .{ q, q }; + } + } + var lo = q; + var hi = q; + while (hi < text.len and isAlnum(runeAt(text, hi))) hi += std.unicode.utf8ByteSequenceLength(text[hi]) catch 1; + while (lo > 0 and isAlnum(runeBefore(text, lo)[0])) lo = runeBefore(text, lo)[1]; + return .{ lo, hi }; +} + +/// acme's textclickmatch (text.c:1457): the nesting match of `cl` by `cr` +/// from `q`, leaving `q` just past it (forward) or on it (back). +fn clickMatch(text: []const u8, cl: u21, cr: u21, forward: bool, q: *usize) bool { + var nest: usize = 1; + while (true) { + var c: u21 = undefined; + if (forward) { + if (q.* >= text.len) break; + c = runeAt(text, q.*); + q.* += std.unicode.utf8ByteSequenceLength(text[q.*]) catch 1; + } else { + if (q.* == 0) break; + const b = runeBefore(text, q.*); + q.* = b[1]; + c = b[0]; + } + if (c == cr) { + nest -= 1; + if (nest == 0) return true; + } else if (c == cl) nest += 1; + } + return cl == '\n' and nest == 1; +} + +fn runeLen(c: u21) usize { + return std.unicode.utf8CodepointSequenceLength(c) catch 1; +} + +fn runeAt(text: []const u8, at: usize) u21 { + const n = std.unicode.utf8ByteSequenceLength(text[at]) catch return text[at]; + if (at + n > text.len) return text[at]; + return std.unicode.utf8Decode(text[at..][0..n]) catch text[at]; +} + +/// The rune ending at `at`, and where it starts. +fn runeBefore(text: []const u8, at: usize) struct { u21, usize } { + var s = at - 1; + while (s > 0 and text[s] & 0xC0 == 0x80 and at - s < 4) s -= 1; + return .{ runeAt(text, s), s }; +} + +fn isAlnum(c: u21) bool { + if (c <= ' ') return false; + if (c >= 0x7F and c <= 0xA0) return false; + if (c < 0x80 and std.mem.indexOfScalar(u8, "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", @intCast(c)) != null) return false; + return true; +} + +test "double-click selects as acme's does" { + const t = "fn foo_bar(a, b) {\n x\n}\n"; + const Case = struct { q: usize, want: []const u8 }; + for ([_]Case{ + .{ .q = 4, .want = "foo_bar" }, // in a word + .{ .q = 11, .want = "a, b" }, // just after ( + .{ .q = 15, .want = "a, b" }, // just before ) + .{ .q = 18, .want = "\n x\n" }, // just after { + .{ .q = 0, .want = "fn foo_bar(a, b) {\n" }, // a line's start + }) |c| { + const r = doubleClickRange(t, c.q); + try std.testing.expectEqualStrings(c.want, t[r[0]..r[1]]); + } + const quoted = "say \"hello there\" now"; + const r = doubleClickRange(quoted, 5); + try std.testing.expectEqualStrings("hello there", quoted[r[0]..r[1]]); + const french = "«oui»"; + const f = doubleClickRange(french, 2); + try std.testing.expectEqualStrings("oui", french[f[0]..f[1]]); + // a line's end: the whole line, its newline included + const lines = "one\ntwo\n"; + const e = doubleClickRange(lines, 7); + try std.testing.expectEqualStrings("two\n", lines[e[0]..e[1]]); +} diff --git a/src/exec.zig b/src/exec.zig index 22aae10a..56e6a2a9 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -435,9 +435,21 @@ fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed { src.question_len = @intCast(w.buffered().len); src.prompt = .repl_choice; p.active = from; + var rec: [96]u8 = undefined; + var rw = std.Io.Writer.fixed(&rec); + rw.print("{d} repl", .{src.serial}) catch {}; + for (bound[0..n]) |i| rw.print(" {c}", .{p.panes[i].?.repl.?.letter}) catch {}; + pardes.ctlfs.events.notePath(p, "ask", rw.buffered()); return .asked; } +fn pythonBlockOpen(text: []const u8) bool { + const trimmed = std.mem.trimEnd(u8, text, " \t\n"); + const start = if (std.mem.lastIndexOfScalar(u8, trimmed, '\n')) |nl| nl + 1 else 0; + const last = trimmed[start..]; + return last.len > 0 and (last[0] == ' ' or last[0] == '\t' or last[last.len - 1] == ':'); +} + /// Types `text` into the REPL in `to`, bracketed if its program asked for /// that, then Enter; says so on `from`'s message row and in the log. pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void { @@ -465,6 +477,11 @@ pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void { } edit.typeToTty(p, to, dst, clean[0..n]); p.emitWrite(to, "\r"); + // A Python block whose last line is indented, or a bare header, is not + // over at one Enter, pasted into 3.13's REPL or typed into the old one: + // the blank line a second Enter gives ends it. + if ((if (dst.repl) |r| r.lang == python_lang else false) and pythonBlockOpen(clean[0..n])) + p.emitWrite(to, "\r"); // Line by line, a blank line inside a Python block ends the block. if (!panes.Terminal.bracketedPaste(dst) and !dst.repl_warned and (if (dst.repl) |r| r.lang == python_lang else false) and std.mem.indexOfScalar(u8, text, '\n') != null) @@ -602,7 +619,7 @@ fn echoCommand(p: *Pardes, pane: *Pane, line: []const u8) void { /// The whole line, one record as `msg` carries its text: its control /// characters, a newline too, become spaces (events.pushLog). A new pane is /// announced first, so `new` comes before the `run` in it. -fn noteRun(p: *Pardes, pane: *Pane, what: []const u8, said: []const u8) void { +pub fn noteRun(p: *Pardes, pane: *Pane, what: []const u8, said: []const u8) void { pardes.ctlfs.events.announce(p); var buf: [command_max + 16]u8 = undefined; pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d} {s}", .{ pane.serial, std.mem.trim(u8, said, " \t\r\n") }) catch return); @@ -598,7 +598,8 @@ test "virtual body writes can read their input from the same pane" { const path = try std.fmt.bufPrint(&path_buffer, "/virtual/pane/{d}/body", .{pane.serial}); try write(p, path, pane.file.?.content); try std.testing.expectEqualStrings("same pane contents\n", pane.file.?.content); - try std.testing.expectEqual(@as(usize, 2), pane.file.?.history.undo_len); + // The truncation and the write are one undo step. + try std.testing.expectEqual(@as(usize, 1), pane.file.?.history.undo_len); const events = &p.panes[p.active].?.fs.events; try std.testing.expect(std.mem.startsWith(u8, events.peek().?, "ED")); events.pop(); diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 26e9e24d..5dfacee9 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -505,6 +505,8 @@ const initial_image_capacity: u32 = pardes.MAX_PANES; const old_layer_bit: u32 = 0x8000_0000; const clear_bg_bit: u32 = 0x4000_0000; const opaque_bg_bit: u32 = 0x2000_0000; +/// Decor that is a soft shadow, not a flat fill (decor.frag.glsl). +const soft_shadow_bit: u32 = 0x1000_0000; const SavedImagePlace = struct { key: pardes.ImageCacheKey, @@ -1874,6 +1876,10 @@ const Gui = struct { /// Pixels the capture holds past the grid (PARDES_TEST_PAD). capture_pad: u32 = 0, capture_dir: []const u8 = "", + /// PARDES_TEST_SERIES: every captured frame is kept too, numbered, for a + /// feel review's frame series (docs/render-pipeline.md §8.4). + capture_series: bool = false, + captured: u32 = 0, capture_tex: ?*c.SDL_GPUTexture = null, capture_tex_w: u32 = 0, capture_tex_h: u32 = 0, @@ -2124,7 +2130,13 @@ const Cover = packed struct(u16) { focus: bool = false, /// At a column's left edge, where a tag's anchor is inset. anchor: bool = false, - _: u7 = 0, + /// In a dirty pane's grip. + dirty: bool = false, + /// In a pane's grip (not a column's). + pane_grip: bool = false, + /// In a grip's first row, where its button is. + grip_top: bool = false, + _: u4 = 0, }; /// Marks the grid cells a body or tag layer covers, which the layers paint @@ -2156,10 +2168,13 @@ fn coverFrame(cover: []Cover, cols: u16, rows: u16, body_layers: []const pardes. if (region.kind == .column_grip) bar = region.rect; } for (regions) |region| switch (region.kind) { - .grip => { + .grip, .column_grip => { mark.rect(cover, cols, rows, region.rect, "grip", true); for (0..@min(region.rect.w, 16)) |offset| mark.rect(cover, cols, rows, .{ .x = region.rect.x +| @as(u16, @intCast(offset)), .y = region.rect.y, .w = 1, .h = region.rect.h }, "grip_offset", @as(u4, @intCast(offset))); - if (region.active) mark.rect(cover, cols, rows, region.rect, "focus", true); + if (region.kind == .grip and region.active) mark.rect(cover, cols, rows, region.rect, "focus", true); + if (region.dirty) mark.rect(cover, cols, rows, region.rect, "dirty", true); + if (region.kind == .grip) mark.rect(cover, cols, rows, region.rect, "pane_grip", true); + mark.rect(cover, cols, rows, .{ .x = region.rect.x, .y = region.rect.y, .w = region.rect.w, .h = @min(1, region.rect.h) }, "grip_top", true); }, .tag => if (region.active) mark.rect(cover, cols, rows, region.rect, "focus", true), .column => { @@ -2480,6 +2495,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u .glyphs = std.AutoHashMap(GlyphKey, Slot).init(gpa), .capture = test_mode, .capture_pad = if (test_mode) envU16(env, "PARDES_TEST_PAD") orelse 0 else 0, + .capture_series = test_mode and env.get("PARDES_TEST_SERIES") != null, .capture_dir = capture_dir orelse "", .soft_present = soft_present, .transparent = config.gui_transparent, @@ -2629,6 +2645,7 @@ fn localSession( .fs = fs, .test_mode = test_mode, .test_clock = if (test_mode) host_io.testClock() else null, + .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(); const host = shell.host(); @@ -3764,9 +3781,10 @@ test "tagline pointer mapping retains physical grips and carries logical text co try std.testing.expect(grip.tag_hit == null); try std.testing.expectEqual(r.x + config.GUTTER - 1, grip.col); - const body = gridCellAtDimensions(core, x, (@as(f32, @floatFromInt(tag_y + 1)) + 0.5) * body_h, body_w, body_h, tag_w); + const body_y = tag_y + core.panes[core.active].?.tag_rows; // a long tag wraps + const body = gridCellAtDimensions(core, x, (@as(f32, @floatFromInt(body_y)) + 0.5) * body_h, body_w, body_h, tag_w); try std.testing.expectEqual(pixelCell(x, 10), body.col); - try std.testing.expectEqual(tag_y + 1, body.row); + try std.testing.expectEqual(body_y, body.row); try std.testing.expect(body.tag_hit == null); const topbar = gridCellAtDimensions(core, 3.5 * tag_w, 0.5 * body_h, body_w, body_h, tag_w); @@ -3914,6 +3932,8 @@ const Shell = struct { retired_shells: [pardes.MAX_PANES]RetiredShell = @splat(.{}), /// PARDES_TEST_CLOCK's virtual time (host_io.testClock), in test mode. test_clock: ?u64 = null, + /// The display rate a virtual clock stands in for (PARDES_TEST_HZ). + test_hz: u64 = 60, fn reap(s: *Shell) void { const now = shellClock(); @@ -4079,15 +4099,13 @@ const Shell = struct { host_io.commandEof(s.core, s.ptys, e.pane, s, closeWatched); break :eof; } - // Unwatched, a command's exit is read here, as its end. + // Unwatched, a command's exit is read here, as its end, + // and a shell's, for a run waiting on it and the log. const unwatched = if (s.core.panes[e.pane]) |pane| pane.command != null else false; - var status: ?u8 = null; - if (unwatched) { - status = host_io.exitStatus(pt.pid, 100); - if (status != null) pt.pid = 0; // reaped: no shell to retire - } + const status = host_io.exitStatus(pt.pid, 100); + if (status != null) pt.pid = 0; // reaped: no shell to retire s.closePty(e.pane); - if (unwatched) s.core.update(.{ .exited = .{ .pane = e.pane, .status = status } }); + if (unwatched or status != null) s.core.update(.{ .exited = .{ .pane = e.pane, .status = status } }); s.core.update(.{ .eof = .{ .pane = e.pane } }); } s.saw_event = true; @@ -4166,10 +4184,17 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void { pardes.turn.rest(); const got = c.SDL_WaitEventTimeout(&sev, ms); pardes.turn.wake(); - // The core's wake ran out: a virtual clock moves exactly there. - if (!got and timeout_ms != 0) if (s.test_clock) |*virtual| { - virtual.* = @max(virtual.*, core.nextWake() orelse virtual.*); - }; + // A virtual clock: while a render-time animation moves, each loop + // is a presented frame and moves it by one display frame + // (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. + if (s.test_clock) |*virtual| { + if (core.continuous()) { + const display = virtual.* + std.time.ns_per_s / s.test_hz; + virtual.* = if (core.nextWake()) |due| @max(virtual.*, @min(due, display)) else display; + } else if (!got and timeout_ms != 0) virtual.* = @max(virtual.*, core.nextWake() orelse virtual.*); + } if (got) { // A run of queued motions is one move to its last position (the // handler reads only absolute x/y and held buttons): acting on @@ -4425,7 +4450,7 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void { fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void { const s = shellOf(ctx); // A command whose exit is recorded is reaped: its group may be another's. - if (s.ptys[pane]) |pt| if (pt.fd >= 0 and !pt.cmd.exited) host_io.signalTty(pt.pid, pt.fd, sig); + if (s.ptys[pane]) |pt| if (pt.fd >= 0 and !pt.cmd.exited) host_io.signalTty(pt.pid, pt.fd, sig, pt.cmd.watched); } fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool { @@ -5526,7 +5551,13 @@ fn renderFrame( const track_index = groupAt(&groups, layer.viewport.x, layer.viewport.y); // A notice floats over the panes, moving with its own. const group_index = if (layer.kind == .notice) noticeGroup(&groups, layer) else track_index; + const start = cell_next[group_index]; emitTagLayer(g, instances, &cell_next[group_index], layer, win_w, win_h, groups.items[track_index].track, page, cursors_shown, false); + // A notice dropping past its row (a flavour's overshoot) stays + // inside its pane's body. + if (layer.kind == .notice and layer.slide > 0) if (bodyBottom(surface, layer.id, layout)) |bottom| { + for (instances[start..cell_next[group_index]]) |*instance| clipInstance(instance, .{ -1e9, -1e9, 1e9, bottom }, win_w, win_h); + }; } for (groups.items[1..][0..groups.tracks], 1..) |group, index| { const track = group.track.?; @@ -5894,6 +5925,14 @@ test "the layer cover is the per-cell layer scan, over a core's layouts and rand } } +/// The bottom of pane `owner`'s body, in window pixels. +fn bodyBottom(surface: *const pardes.Surface, owner: u16, layout: CellLayout) ?f32 { + for (surface.regionList()) |region| { + if (region.kind == .body and region.owner == owner) return @as(f32, @floatFromInt(region.rect.y + region.rect.h)) * layout.h; + } + return null; +} + fn tagLayerCellCount(g: *const Gui, layer: *const pardes.Layer) u32 { const pixels = @as(u32, layer.viewport.w) * g.cell_w; return @min(layer.cols, (pixels + g.tagline_width - 1) / g.tagline_width); @@ -6031,6 +6070,67 @@ fn anchorInset(g: *const Gui, row: u16, win_h: f32) u32 { return taglineBandOffset(row, win_h, g.cell_h, g.tagline_height); } +/// acme's Scrollwid column at a pane's or column's left edge, in window +/// pixels: where its button sits over its scrollbar, one width for both +/// (`rail_px` at acme's 17px font, scaled with the tagline, and always +/// short of the cell so the text keeps a gap), right of the column's rule. +fn scrollColumn(g: *const Gui, chrome: *const pardes.Chrome, col: u16, layout: CellLayout) [2]f32 { + const rule: f32 = if (col == 0) 0 else rulePx(chrome); + const x0 = @round(@as(f32, @floatFromInt(col)) * layout.w + rule); + const scaled: f32 = @round(@as(f32, @floatFromInt(@max(1, chrome.rail_px))) * @as(f32, @floatFromInt(g.tagline_height)) / 17); + // The rail and the grip are GUTTER cells wide; the text starts after. + const w = std.math.clamp(scaled, 4, @max(4, @floor(layout.w * @as(f32, @floatFromInt(config.GUTTER))) - rule - 3)); + return .{ x0, x0 + w }; +} + +/// acme's button for the grip whose first cell is (col, row), in window +/// pixels: the scroll column wide, its tag row tall, from the rule above to +/// the rule below, so button and scrollbar read as one strip. +fn gripButton(g: *const Gui, chrome: *const pardes.Chrome, col: u16, row: u16, layout: CellLayout, win_h: f32) [4]f32 { + _ = win_h; + const x = scrollColumn(g, chrome, col, layout); + const y0 = @round(@as(f32, @floatFromInt(row)) * layout.h); + return .{ x[0], y0, x[1], @round(@as(f32, @floatFromInt(row + 1)) * layout.h) }; +} + +/// A pane's button fill, from its region's state, not the grid's cells +/// (which are drawn for cell shells): dirty, focused, or the tag's ground. +fn gripFill(chrome: *const pardes.Chrome, focus: bool, dirty: bool) [3]u8 { + if (dirty) return chrome.grip_dirty; + return if (focus) chrome.grip_focus_ring else chrome.tag_bg; +} + +fn inkOn(bg: [3]u8) [3]u8 { + const lum = (@as(u16, bg[0]) * 3 + @as(u16, bg[1]) * 6 + @as(u16, bg[2])) / 10; + return if (lum > 140) .{ 0x00, 0x00, 0x00 } else .{ 0xff, 0xff, 0xff }; +} + +/// Cuts an instance's quad to a pixel rectangle, its atlas window with it so +/// the glyph neither moves nor stretches; nothing left, an empty quad. +fn clipInstance(instance: *CellInstance, r: [4]f32, win_w: f32, win_h: f32) void { + const x0 = (instance.x0 + 1) / 2 * win_w; + const x1 = (instance.x1 + 1) / 2 * win_w; + const y0 = (1 - instance.y0) / 2 * win_h; + const y1 = (1 - instance.y1) / 2 * win_h; + const left = @max(0, r[0] - x0); + const right = @max(0, x1 - r[2]); + const top = @max(0, r[1] - y0); + const bottom = @max(0, y1 - r[3]); + if (x1 - right <= x0 + left or y1 - bottom <= y0 + top) { + instance.x1 = instance.x0; + instance.y1 = instance.y0; + return; + } + instance.x0 += left / win_w * 2; + instance.u0 += left / atlas_w; + instance.x1 -= right / win_w * 2; + instance.u1 -= right / atlas_w; + instance.y0 -= top / win_h * 2; + instance.v0 += top / atlas_h; + instance.y1 += bottom / win_h * 2; + instance.v1 -= bottom / atlas_h; +} + fn emitSurfaceCell( g: *Gui, chrome: *const pardes.Chrome, @@ -6063,33 +6163,82 @@ fn emitSurfaceCell( emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .body, (&band_base)[0..1], 0, null, old_layer, false, page); next.* += 1; // Tag glyphs keep their font size inside a full physical-width grip. - emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .tagline, line, at, cursor, old_layer, is_cursor, page); + // A pane's grip cells are the cell shells' form (a dirty `*`): the + // button draws its own glyph over a blank of them. + var blank = line[at]; + blank.text[0] = ' '; + blank.len = 1; + if (here.pane_grip) + emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .tagline, (&blank)[0..1], 0, null, old_layer, false, page) + else + emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .tagline, line, at, cursor, old_layer, is_cursor, page); // A grip's glyph is the cell at its left edge, and a blank one is // no glyph: its marks are decor. const glyph = &line[at - @min(here.grip_offset, at)]; - const grip = here.grip and !glyph.default and glyph.printableAscii() != ' ' and !glyph.style.invisible; + const grip = here.grip and here.grip_top and !glyph.default and glyph.printableAscii() != ' ' and !glyph.style.invisible; const offset: u16 = if (grip) @min(here.grip_offset, at) else 0; // An anchor starts at its column's rule: its left edge is inset by // its top margin so the band frames it on the left as above and // below, and its mark moves right by half that to stay centered. const inset: u32 = if (line_cover[at - offset].anchor) anchorInset(g, row, win_h) else 0; + // A grip is acme's button: its fill is this cell's ground, cut to + // the button (and a dirty one's inset a pixel inside its ring, so + // the ring reads round it), its glyph centered in it. + const button = if (here.grip and here.grip_top) gripButton(g, chrome, col - @min(here.grip_offset, @as(u16, @intCast(at))), row, body_layout, win_h) else null; if (grip) { // A grip is one glyph centered across the gutter, not text: it is // looked up by its codepoint's glyph, never shaped. const drawn = glyphForCodepoint(g, cellCodepoint(glyph)); const cached = cachedGlyph(g, .{ .face = drawn.face, .glyph = drawn.glyph, .role = .tagline, .decoration = GlyphDecoration.fromCell(glyph), .centered = true }); const slot = cached orelse g.space_slot; - const shift: f32 = if (cached != null) @as(f32, @floatFromInt(@as(u32, offset) * g.cell_w)) - @as(f32, @floatFromInt(inset / 2)) else 0; + const b = button.?; + const center = (b[0] + b[2]) / 2 - @as(f32, @floatFromInt(col - offset)) * body_layout.w; + const shift: f32 = if (cached != null) @as(f32, @floatFromInt(@as(u32, offset) * g.cell_w)) + @as(f32, @floatFromInt(config.GUTTER * g.cell_w)) / 2 - center else 0; const resolved = resolveCell(g, glyph[0..1], 0, null, .tagline, false, page); - instances[next.*].fr = @as(f32, @floatFromInt(resolved.fg[0])) / 255; - instances[next.*].fg = @as(f32, @floatFromInt(resolved.fg[1])) / 255; - instances[next.*].fb = @as(f32, @floatFromInt(resolved.fg[2])) / 255; + const ink = if (here.pane_grip) inkOn(gripFill(chrome, here.focus, here.dirty)) else resolved.fg; + instances[next.*].fr = @as(f32, @floatFromInt(ink[0])) / 255; + instances[next.*].fg = @as(f32, @floatFromInt(ink[1])) / 255; + instances[next.*].fb = @as(f32, @floatFromInt(ink[2])) / 255; instances[next.*].u0 = (@as(f32, @floatFromInt(slot.u)) + shift) / atlas_w; instances[next.*].v0 = @as(f32, @floatFromInt(slot.v)) / atlas_h; instances[next.*].v1 = @as(f32, @floatFromInt(slot.v + g.tagline_height)) / atlas_h; } instances[next.*].u1 = instances[next.*].u0 + @as(f32, @floatFromInt(g.cell_w)) / atlas_w; - if (inset > 0 and (!grip or offset == 0)) { + if (button) |b| { + // acme's button fills its tag row from the rule above to the rule + // below: the underlay is its fill, cut to its columns; a pane's + // from its region's state, not the grid's cell form (whose dirty + // `*` is the cell shells'), a column's its cell's own ground. + const base = &instances[next.* - 1]; + const own: [3]f32 = .{ instances[next.*].br, instances[next.*].bg, instances[next.*].bb }; + if (here.pane_grip) { + const fill = gripFill(chrome, here.focus, here.dirty); + base.br = @as(f32, @floatFromInt(fill[0])) / 255; + base.bg = @as(f32, @floatFromInt(fill[1])) / 255; + base.bb = @as(f32, @floatFromInt(fill[2])) / 255; + } else { + base.br = own[0]; + base.bg = own[1]; + base.bb = own[2]; + } + base.effect &= ~clear_bg_bit; + clipInstance(base, .{ b[0], -1e9, b[2], 1e9 }, win_w, win_h); + // Over it, only its own glyph; the band round it and the ring + // are decor (gripDecor). + if (here.pane_grip and !grip) { + instances[next.*].u0 = @as(f32, @floatFromInt(g.space_slot.u)) / atlas_w; + instances[next.*].u1 = instances[next.*].u0 + @as(f32, @floatFromInt(g.cell_w)) / atlas_w; + instances[next.*].v0 = @as(f32, @floatFromInt(g.space_slot.v)) / atlas_h; + instances[next.*].v1 = @as(f32, @floatFromInt(g.space_slot.v + g.tagline_height)) / atlas_h; + } + // Its ground is the fill again (a see-through ground would be + // written, not blended, in an opaque window). + instances[next.*].br = base.br; + instances[next.*].bg = base.bg; + instances[next.*].bb = base.bb; + instances[next.*].effect &= ~clear_bg_bit; + clipInstance(&instances[next.*], b, win_w, win_h); + } else if (inset > 0) { instances[next.*].x0 += @as(f32, @floatFromInt(inset)) / win_w * 2; instances[next.*].u0 += @as(f32, @floatFromInt(inset)) / atlas_w; } @@ -6100,6 +6249,125 @@ fn emitSurfaceCell( next.* += 1; } +test "every pixel of a grip column is band, button or ring, and the button sits over the scrollbar" { + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .cols = 60, .rows = 20 }); + defer core.deinit(); + const pane = try core.setTestFile("text\n" ** 60); + pane.tag.own = try gpa.dupe(u8, "second\nthird"); + core.newScratchBelow(0); + for ([_]struct { w: u32, h: u32, tag: u32 }{ .{ .w = 10, .h = 20, .tag = 12 }, .{ .w = 17, .h = 35, .tag = 30 }, .{ .w = 9, .h = 19, .tag = 15 }, .{ .w = 12, .h = 24, .tag = 24 } }) |m| for ([_]bool{ false, true }) |bottom| for ([_]usize{ 0, 1 }) |focus| for ([_]bool{ false, true }) |dirty| { + core.settings.tag_bottom = bottom; + core.active = focus; + pane.file.?.revision = pane.file.?.saved_revision + @intFromBool(dirty); + pardes.test_api.sync(core); + const s = try core.render(core.scratch.allocator()); + var g: Gui = undefined; + g.words = .{}; + g.decor = .empty; + defer g.decor.deinit(gpa); + g.cell_w = m.w; + g.cell_h = m.h; + g.tagline_width = m.w; + g.tagline_height = m.tag; + g.space_slot = .{ .u = 0, .v = 0 }; + const layout = fixedCellLayout(&g); + const win_w: f32 = @floatFromInt(60 * m.w); + const win_h: f32 = @floatFromInt(20 * m.h); + const cover = try gpa.alloc(Cover, @as(usize, s.cols) * s.rows); + defer gpa.free(cover); + coverFrame(cover, s.cols, s.rows, s.bodyLayers(), s.tagLayers(), s.regionList()); + var groups = makeGroups(&.{}, false); + try buildDecor(&g, gpa, s, &groups, layout, win_w, win_h, true); + const grip = s.region(.grip, 0).?; + // The grip column's pixels, band first, then its cells, then decor. + const x_px: u32 = grip.rect.x * m.w; + const w_px: u32 = config.GUTTER * m.w; + const y_px: u32 = grip.rect.y * m.h; + const h_px: u32 = grip.rect.h * m.h; + const pixels = try gpa.alloc(?[3]f32, w_px * h_px); + defer gpa.free(pixels); + @memset(pixels, null); + const paint = struct { + fn quad(into: []?[3]f32, x0n: f32, y0n: f32, x1n: f32, y1n: f32, rgb: [3]f32, ww: f32, wh: f32, ox: u32, oy: u32, pw: u32, ph: u32) void { + const px0: i64 = @intFromFloat(@round((x0n + 1) / 2 * ww)); + const px1: i64 = @intFromFloat(@round((x1n + 1) / 2 * ww)); + const py0: i64 = @intFromFloat(@round((1 - y0n) / 2 * wh)); + const py1: i64 = @intFromFloat(@round((1 - y1n) / 2 * wh)); + var y = @max(py0, oy); + while (y < @min(py1, oy + ph)) : (y += 1) { + var x = @max(px0, ox); + while (x < @min(px1, ox + pw)) : (x += 1) into[@intCast((y - oy) * pw + (x - ox))] = rgb; + } + } + }; + var instances: [4]CellInstance = undefined; + for (0..grip.rect.h) |line| for (0..config.GUTTER) |dx| { + const col: u16 = grip.rect.x + @as(u16, @intCast(dx)); + const row: u16 = grip.rect.y + @as(u16, @intCast(line)); + const at = @as(usize, row) * s.cols; + var next: u32 = 0; + emitSurfaceCell(&g, &s.chrome, &instances, &next, col, row, layout, win_w, win_h, null, s.cells[at..][0..s.cols], cover[at..][0..s.cols], col, null, false, false, Ground.opaqueRgb(bg_default)); + for (instances[0..next]) |i| paint.quad(pixels, i.x0, i.y0, i.x1, i.y1, .{ i.br, i.bg, i.bb }, win_w, win_h, x_px, y_px, w_px, h_px); + }; + for (g.decor.items) |i| if (i.fr >= 1 and i.clip_x1 < i.clip_x0) paint.quad(pixels, i.x0, i.y0, i.x1, i.y1, .{ i.br, i.bg, i.bb }, win_w, win_h, x_px, y_px, w_px, h_px); + const rgb = struct { + fn of(v: [3]u8) [3]f32 { + return .{ @as(f32, @floatFromInt(v[0])) / 255, @as(f32, @floatFromInt(v[1])) / 255, @as(f32, @floatFromInt(v[2])) / 255 }; + } + }.of; + const chrome = &s.chrome; + const band = if (focus == 0) chrome.tag_focus_bg else chrome.tag_bg; + // Band, fills, rings, the rules between stacked panes and under the + // tag, and the mode marks' ink (the grip's first cell's). + const ink = inkOn(gripFill(chrome, grip.active, grip.dirty)); + const allowed = [_][3]f32{ rgb(band), rgb(core.chromeTheme().box), rgb(pardes.draw.boxDirty(core)), rgb(chrome.grip_border), rgb(chrome.grip_focus_ring), rgb(chrome.border), rgb(ink), rgb(chrome.tag_rule orelse mixRgb(chrome.tag_bg, chrome.page orelse bg_default)) }; + for (pixels, 0..) |pixel, n| { + const got = pixel orelse return error.TestUnpaintedGripPixel; + const known = for (allowed) |a| { + if (std.meta.eql(a, got)) break true; + } else false; + // Under the first row there is no button: band and rules only. + if (n / w_px >= m.h and n / w_px < h_px - 1 and !std.meta.eql(got, rgb(band)) and !std.meta.eql(got, rgb(chrome.border))) + return error.TestButtonUnderFirstRow; + if (!known) std.debug.print("sliver {any} at {d},{d} of {d}x{d} (metrics {d}x{d}/{d}, bottom {}, focus {d}, dirty {}) page {any}\n", .{ got, n % w_px, n / w_px, w_px, h_px, m.w, m.h, m.tag, bottom, focus, dirty, s.chrome.page }); + try std.testing.expect(known); + } + // The rule under the tag runs unbroken across the grip column. + if (!bottom) for (pixels[(h_px - 1) * w_px ..][0..w_px]) |pixel| + try std.testing.expectEqual(rgb(chrome.tag_rule orelse mixRgb(chrome.tag_bg, chrome.page orelse bg_default)), pixel.?); + // The button's edges are the scrollbar's. + const button = gripButton(&g, chrome, grip.rect.x, grip.rect.y, layout, win_h); + const column = scrollColumn(&g, chrome, s.region(.rail, 0).?.rect.x, layout); + try std.testing.expectEqual(column[0], button[0]); + try std.testing.expectEqual(column[1], button[2]); + }; + pane.file.?.revision = pane.file.?.saved_revision; +} + +test "a rising pane past its mark stays inside its box" { + // Bouncy, a vertical rise at its highest overshoot: drawn past its box, + // clipped to it. + var g: Gui = undefined; + g.cell_w = 10; + g.cell_h = 20; + var track: pardes.animation.Track = .{ .serial = 3, .phase = .opening, .effect = .vertical, .motion = @intFromEnum(pardes.animation.Motion.Flavour.bouncy) + 1, .frame_count = 20, .from = .{ .x = 0, .y = 10, .w = 40, .h = 10 }, .to = .{ .x = 0, .y = 0, .w = 40, .h = 10 } }; + var peak: u16 = 0; + var highest: f32 = 1e9; + while (track.frame < track.frames()) : (track.frame += 1) { + if (track.presented().y < highest) { + highest = track.presented().y; + peak = track.frame; + } + } + track.frame = peak; + try std.testing.expect(track.presented().y < 0); + var instance: CellInstance = std.mem.zeroes(CellInstance); + setTransitionFields(&instance, track, fixedCellLayout(&g), 800, 480, 0); + try std.testing.expectEqual(@as(f32, 0), instance.clip_y0); + try std.testing.expectEqual(@as(f32, 200), instance.clip_y1); +} + test "small tagline fonts leave no background gutters between adjacent tags" { const chrome: pardes.Chrome = .{}; var g: Gui = undefined; @@ -6787,9 +7055,9 @@ test "pane indicator halves share dissolve changes and safe exhausted atlas" { try std.testing.expect(gripCellChanged(&surface, &cover, &previous_cover, 1, 0)); try std.testing.expect(!gripCellChanged(&surface, &cover, &previous_cover, 2, 0)); try std.testing.expect(!cover[2].grip); - // A column's grip has no glyph of a pane's. + // A column's grip is a button too, cut the same way (it has no glyph). coverFrame(&cover, 6, 2, &.{}, &.{}, &.{.{ .kind = .column_grip, .rect = .{ .x = 0, .y = 0, .w = 2, .h = 1 } }}); - try std.testing.expect(!cover[0].grip); + try std.testing.expect(cover[0].grip); coverFrame(&cover, 6, 2, &.{}, &.{}, &grip); const chrome: pardes.Chrome = .{}; @@ -7327,6 +7595,8 @@ fn captureFrame(g: *Gui, gpa: std.mem.Allocator, cmd: *c.SDL_GPUCommandBuffer, t try writeCapturePpm(g, gpa, pixels[0..size], sw, sh, "latest"); var name: [24]u8 = undefined; if (transition) |frame| try writeCapturePpm(g, gpa, pixels[0..size], sw, sh, std.fmt.bufPrint(&name, "transition-{d}", .{frame}) catch unreachable); + if (g.capture_series) try writeCapturePpm(g, gpa, pixels[0..size], sw, sh, std.fmt.bufPrint(&name, "frame-{d:0>5}", .{g.captured}) catch unreachable); + g.captured +%= 1; } fn writeCapturePpm(g: *Gui, gpa: std.mem.Allocator, pixels: []const u8, width: u32, height: u32, name: []const u8) !void { @@ -7401,6 +7671,303 @@ fn addDecor(g: *Gui, gpa: std.mem.Allocator, x0_in: f32, y0_in: f32, x1_in: f32, try g.decor.append(gpa, instance); } +/// How a lift of 0 to 1 stands off the page: the shadow's blur and offset +/// in cell heights, and its darkest, where it leaves the caster's edge. +/// Within §8.2's budget: sigma 0.4-0.8 cell, at most 30% darker at the edge. +const Shadow = struct { + // A key light above: the drop is most of the blur, so the shadow hangs + // below and to the right and barely reaches over the caster's top. + const sigma: f32 = 0.42; + const drop: f32 = 0.34; + const side: f32 = 0.1; + const strength: f32 = 0.42; +}; + +/// A soft rectangle around `caster` (window pixels), blurred by `sigma` +/// and moved by (dx, dy): black is a shadow, a light colour a rim or a +/// glow. The caster's own rectangle is left alone, and `track` moves it all +/// as its caster moves. +fn addSoft(g: *Gui, gpa: std.mem.Allocator, caster: [4]f32, sigma_in: f32, dx: f32, dy: f32, strength: f32, rgb: [3]u8, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void { + if (strength <= 0.002) return; + const sigma = @max(0.5, sigma_in); + const x0 = caster[0] + dx - 3 * sigma; + const y0 = caster[1] + dy - 3 * sigma; + const x1 = caster[2] + dx + 3 * sigma; + const y1 = caster[3] + dy + 3 * sigma; + const w = x1 - x0; + const ht = y1 - y0; + if (w <= 0 or ht <= 0) return; + // In the quad's own 0..1 space (decor.frag.glsl): sigma per axis, the + // caster's offset from its shadow, the strength. + var instance: CellInstance = .{ + .x0 = (x0 / win_w) * 2.0 - 1.0, + .y0 = 1.0 - (y0 / win_h) * 2.0, + .x1 = (x1 / win_w) * 2.0 - 1.0, + .y1 = 1.0 - (y1 / win_h) * 2.0, + .u0 = sigma / w, + .v0 = sigma / ht, + .u1 = sigma / w, + .v1 = sigma / ht, + .fr = strength, + .fg = dx / w, + .fb = dy / ht, + .br = @as(f32, @floatFromInt(rgb[0])) / 255, + .bg = @as(f32, @floatFromInt(rgb[1])) / 255, + .bb = @as(f32, @floatFromInt(rgb[2])) / 255, + }; + setTransitionFields(&instance, track, fixedCellLayout(g), win_w, win_h, 0); + instance.effect |= soft_shadow_bit; + try g.decor.append(gpa, instance); +} + +/// How far each lift style may go on this theme without its text falling +/// below min(its contrast, 4.5): a shadow darkens, the others lighten. +const LiftCeilings = struct { + shadow: f32, + white: f32, + /// A rim is light on a dark page and shade on a light one, where light + /// would not show. + rim: [3]u8, + + /// Over the text on the page, and over a selection's text on its own + /// ground: the lower of the two. + fn of(chrome: *const pardes.Chrome) LiftCeilings { + const fg = chrome.fg orelse fg_default; + const bg = chrome.page orelse bg_default; + return .{ + .shadow = @min(shadowCeiling(fg, bg), shadowCeiling(chrome.sel_fg, chrome.sel_bg)), + .white = @min(lightenCeiling(fg, bg, .{ 255, 255, 255 }), lightenCeiling(chrome.sel_fg, chrome.sel_bg, .{ 255, 255, 255 })), + .rim = if (luminance(bg) < 0.18) .{ 255, 255, 255 } else .{ 0, 0, 0 }, + }; + } +}; + +/// What stands a lifted part off the page, `lift` 0 to 1 (a pane's spring, +/// a notice's 1), in the style chosen: a soft shadow, or a rim along the +/// top of the focused pane's tag rows (`rim_edge`, a pane's only: +/// never a notice's). +fn addLift(g: *Gui, gpa: std.mem.Allocator, style: pardes.config.Runtime.LiftStyle, ceilings: LiftCeilings, caster: [4]f32, rim_edge: ?[3]f32, bodies: []const [4]f32, lift: f32, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void { + if (lift <= 0.001) return; + const h: f32 = @floatFromInt(g.cell_h); + // The core sends `auto` already resolved (draw.liftStyle). + const shadow = switch (style) { + .off => return, + .shadow, .auto => true, + .rim => false, + }; + if (shadow) { + // Nothing of a shadow may touch a focus indicator (docs/effects.md): + // it falls on pane bodies and rails, never a tag, a grip or a header. + const first = g.decor.items.len; + try addSoft(g, gpa, caster, Shadow.sigma * h * lift, Shadow.side * h * lift, Shadow.drop * h * lift, @min(Shadow.strength * lift, ceilings.shadow), .{ 0, 0, 0 }, track, win_w, win_h); + try clipToBodies(g, gpa, first, bodies); + } + // A hairline just above the focused tag rows, never on them: it adds to + // the indicator without changing its colour. + if (style == .rim) if (rim_edge) |edge| { + const light = ceilings.rim[0] == 255; + const alpha = @min(Lift.rim * lift, if (light) ceilings.white else ceilings.shadow); + try addDecor(g, gpa, edge[0], edge[2] - 1, edge[1], edge[2], ceilings.rim, alpha, track, win_w, win_h); + }; +} + +/// A rim's strength, in the §8.2 budget for coloured light. +const Lift = struct { + const rim: f32 = 0.35; +}; + +/// A body region in window pixels. +fn bodyPixels(region: pardes.Region, layout: CellLayout) [4]f32 { + return .{ + @as(f32, @floatFromInt(region.rect.x)) * layout.w, + @as(f32, @floatFromInt(region.rect.y)) * layout.h, + @as(f32, @floatFromInt(region.rect.x + region.rect.w)) * layout.w, + @as(f32, @floatFromInt(region.rect.y + region.rect.h)) * layout.h, + }; +} + +/// Replaces the decor from `first` on with a copy per pane body it reaches, +/// each clipped to that body (and to what a vertical transition already +/// clipped it to): the lift then never reaches a tag, a grip or a header. +fn clipToBodies(g: *Gui, gpa: std.mem.Allocator, first: usize, bodies: []const [4]f32) !void { + const added = g.decor.items.len - first; + if (added == 0) return; + var own: [8]CellInstance = undefined; + const n = @min(added, own.len); + @memcpy(own[0..n], g.decor.items[first..][0..n]); + g.decor.shrinkRetainingCapacity(first); + for (own[0..n]) |instance| for (bodies) |body| { + var copy = instance; + const vertical = copy.clip_x1 >= copy.clip_x0; + copy.clip_x0 = if (vertical) @max(copy.clip_x0, body[0]) else body[0]; + copy.clip_y0 = if (vertical) @max(copy.clip_y0, body[1]) else body[1]; + copy.clip_x1 = if (vertical) @min(copy.clip_x1, body[2]) else body[2]; + copy.clip_y1 = if (vertical) @min(copy.clip_y1, body[3]) else body[3]; + if (copy.clip_x1 <= copy.clip_x0 or copy.clip_y1 <= copy.clip_y0) continue; + try g.decor.append(gpa, copy); + }; +} + + +/// Like shadowCeiling for a light colour laid over at `a` (sRGB blend, as +/// the decor pipeline blends a colour that is not black): the largest `a` +/// that keeps text and page apart by min(their contrast, 4.5). +fn lightenCeiling(fg: [3]u8, bg: [3]u8, rgb: [3]u8) f32 { + const contrast = struct { + fn of(a: [3]u8, b: [3]u8) f32 { + const la = luminance(a); + const lb = luminance(b); + return (@max(la, lb) + 0.05) / (@min(la, lb) + 0.05); + } + fn blend(under: [3]u8, over: [3]u8, a: f32) [3]u8 { + var out: [3]u8 = undefined; + for (&out, under, over) |*o, x, y| o.* = @intFromFloat(@round(@as(f32, @floatFromInt(x)) * (1 - a) + @as(f32, @floatFromInt(y)) * a)); + return out; + } + }; + const target = @min(contrast.of(fg, bg), 4.5); + var lo: f32 = 0; + var hi: f32 = 1; + for (0..24) |_| { + const mid = (lo + hi) / 2; + if (contrast.of(contrast.blend(fg, rgb, mid), contrast.blend(bg, rgb, mid)) >= target - 1e-3) lo = mid else hi = mid; + } + return lo; +} + +/// sRGB's relative luminance (WCAG). +fn luminance(rgb: [3]u8) f32 { + var sum: f32 = 0; + for (rgb, [3]f32{ 0.2126, 0.7152, 0.0722 }) |channel, weight| { + const v = @as(f32, @floatFromInt(channel)) / 255; + sum += weight * (if (v <= 0.04045) v / 12.92 else std.math.pow(f32, (v + 0.055) / 1.055, 2.4)); + } + return sum; +} + +/// How dark a shadow may make the page and its text, both together, before +/// the pair drops below its own contrast or 4.5, whichever is lower (§8.2): +/// darkening in linear light by k leaves (k L1 + .05) / (k L2 + .05). +fn shadowCeiling(fg: [3]u8, bg: [3]u8) f32 { + const a = luminance(fg); + const b = luminance(bg); + const light = @max(a, b); + const dark = @min(a, b); + const target = @min((light + 0.05) / (dark + 0.05), 4.5); + const room = light - target * dark; + if (room <= 0) return 0; + return std.math.clamp(1 - 0.05 * (target - 1) / room, 0, 1); +} + +test "a rim never lightens a native theme's text below its contrast or 4.5" { + for (pardes.themes) |theme| { + const fg = theme.fg orelse fg_default; + const bg = theme.bg orelse bg_default; + for ([_][3]u8{.{ 255, 255, 255 }}) |light| { + const a = @min(Lift.rim, lightenCeiling(fg, bg, light)); + const blend = struct { + fn of(under: [3]u8, over: [3]u8, t: f32) [3]u8 { + var out: [3]u8 = undefined; + for (&out, under, over) |*o, x, y| o.* = @intFromFloat(@round(@as(f32, @floatFromInt(x)) * (1 - t) + @as(f32, @floatFromInt(y)) * t)); + return out; + } + }.of; + const ratio = struct { + fn of(x: [3]u8, y: [3]u8) f32 { + return (@max(luminance(x), luminance(y)) + 0.05) / (@min(luminance(x), luminance(y)) + 0.05); + } + }.of; + try std.testing.expect(ratio(blend(fg, light, a), blend(bg, light, a)) >= @min(ratio(fg, bg), 4.5) - 2e-3); + } + } +} + +test "a lift never touches a tag, a grip or a header, on a light theme or a dark one" { + const gpa = std.testing.allocator; + for ([_][]const u8{ "forge", "daybreak" }) |name| { + const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 }); + defer core.deinit(); + core.update(.{ .resize = .{ .cols = 80, .rows = 24, .row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 6, .tagline_h = 12 } } }); + _ = try core.newShell(1, ""); + try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false)); + var line: [64]u8 = undefined; + try std.testing.expect(core.executeBuiltinLine(0, try std.fmt.bufPrint(&line, "Theme {s}", .{name}))); + for (0..40) |_| core.update(.tick); + for (std.enums.values(pardes.config.Runtime.LiftStyle)) |style| { + core.settings.lift = style; + try std.testing.expect(core.executeBuiltinLine(0, "Msg a floating notice")); + core.advance(core.now_ns + 2 * std.time.ns_per_s); + pardes.test_api.sync(core); + const s = try core.render(core.scratch.allocator()); + core.advance(core.now_ns + 2 * std.time.ns_per_s); + const settled = try core.render(core.scratch.allocator()); + _ = s; + // The focus indicator itself: the focused pane's tag is not the + // others' colour, whatever the lift. + var active: ?[3]u8 = null; + var other: ?[3]u8 = null; + for (settled.tagLayers()) |layer| if (layer.rows != 0 and layer.kind == .pane) { + if (layer.id == core.active) active = layer.bg else other = layer.bg; + }; + try std.testing.expect(!std.mem.eql(u8, &active.?, &other.?)); + var g: Gui = undefined; + g.cell_w = 10; + g.cell_h = 20; + g.tagline_width = 6; + g.tagline_height = 12; + g.decor = .empty; + defer g.decor.deinit(gpa); + var groups = makeGroups(&.{}, false); + try buildDecor(&g, gpa, settled, &groups, fixedCellLayout(&g), 800, 480, true); + var lifted: usize = 0; + for (g.decor.items) |item| { + if (item.clip_x1 < item.clip_x0) continue; + lifted += 1; + // Inside a body or a rail, all of it. + const inside = for (settled.regionList()) |region| { + if (region.kind != .body and region.kind != .rail) continue; + const r = bodyPixels(region, fixedCellLayout(&g)); + if (item.clip_x0 >= r[0] and item.clip_y0 >= r[1] and item.clip_x1 <= r[2] and item.clip_y1 <= r[3]) break true; + } else false; + try std.testing.expect(inside); + } + const light_page = std.mem.eql(u8, name, "daybreak"); + try std.testing.expectEqual(style == .shadow or (style == .auto and light_page), lifted != 0); + // A rim is one hairline, just above the focused pane's tag row. + var rims: usize = 0; + for (g.decor.items) |item| { + if (item.clip_x1 >= item.clip_x0) continue; + const top = (1 - item.y0) / 2 * 480; + const height = (item.y0 - item.y1) / 2 * 480; + const on_active_tag = for (settled.regionList()) |region| { + if (region.kind == .tag and region.owner == core.active and @abs(top + 1 - @as(f32, @floatFromInt(region.rect.y)) * 20) < 0.01 and @abs(height - 1) < 0.01) break true; + } else false; + // Not the rule under a column bar, which may run there too. + rims += @intFromBool(on_active_tag and item.fr < 1); + } + try std.testing.expectEqual(@as(usize, @intFromBool(style == .rim)), rims); + } + } +} + +test "a shadow never darkens a native theme's text below its contrast or 4.5" { + var shaded: usize = 0; + for (pardes.themes) |theme| { + const fg = theme.fg orelse fg_default; + const bg = theme.bg orelse bg_default; + const ceiling = shadowCeiling(fg, bg); + const k = 1 - @min(Shadow.strength, ceiling); + const light = @max(luminance(fg), luminance(bg)); + const dark = @min(luminance(fg), luminance(bg)); + const before = (light + 0.05) / (dark + 0.05); + const after = (k * light + 0.05) / (k * dark + 0.05); + try std.testing.expect(after >= @min(before, 4.5) - 1e-3); + shaded += @intFromBool(ceiling > 0.05); + } + // And most themes have room for one. + try std.testing.expect(shaded * 2 > pardes.themes.len); +} + /// The frame's decor, group by group (docs/render-pipeline.md §3.4), all of it /// read from the regions and the palette: the page's rules and the still /// panes' rails, rules and grip marks in tier 0, a moving or closing pane's @@ -7409,9 +7976,27 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g g.decor.clearRetainingCapacity(); const chrome = &surface.chrome; const regions = surface.regionList(); + const ceilings = LiftCeilings.of(chrome); + var current_buf: [pardes.MAX_PANES][4]f32 = undefined; + var ncurrent: usize = 0; + for (regions) |part| if ((part.kind == .body or part.kind == .rail) and ncurrent < current_buf.len) { + current_buf[ncurrent] = bodyPixels(part, layout); + ncurrent += 1; + }; + const current_bodies = current_buf[0..ncurrent]; + const lift_style = std.enums.fromInt(pardes.config.Runtime.LiftStyle, chrome.lift_style) orelse .off; + const notice_lift = for (regions) |region| { + if (region.kind == .notice) break region.lift; + } else 0; for (groups.items[0..groups.len], 0..) |*group, index| { group.decor_start = @intCast(g.decor.items.len); defer group.decor_count = @as(u32, @intCast(g.decor.items.len)) - group.decor_start; + // The grips' buttons before any rule: the rules over their rows cut + // them, and the band round them never covers a rule. + if (index <= groups.tracks) { + const closing = if (group.track) |active| active.phase == .closing else false; + try gripDecor(g, gpa, if (closing) surface.previous_regions else regions, if (closing) surface.previous_cells else surface.cells, groups, index, chrome, layout, surface.cols, surface.rows, win_w, win_h, group.track); + } if (index == 0) { const top = for (regions) |region| { if (region.kind == .workspace_tag) break region.rect.h; @@ -7421,7 +8006,8 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g .grip, .tag, .column_grip, .column_tag => if (region.rect.y == top) break true, else => {}, } else false; - const height = topbarPaneBorderPixels(g.cell_h, g.tagline_height); + // As wide as the other rules, where the tagline leaves the room. + const height: u32 = @max(1, @min(chrome.rule_px, (g.cell_h -| g.tagline_height) * 2)); if (top != 0 and top < surface.rows and tag_under and height != 0) { const y0: f32 = @floatFromInt(top * g.cell_h - height / 2); try addDecor(g, gpa, 0, y0, win_w, y0 + @as(f32, @floatFromInt(height)), chrome.topbar_rule, 1, null, win_w, win_h); @@ -7430,7 +8016,7 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g for (regions) |bar| { if (bar.kind != .column_grip) continue; const y = @as(f32, @floatFromInt(bar.rect.y + bar.rect.h)) * layout.h; - try addDecor(g, gpa, 0, y - 1, win_w, y, chrome.border, 1, null, win_w, win_h); + try addDecor(g, gpa, 0, y - rulePx(chrome), win_w, y, chrome.border, 1, null, win_w, win_h); break; } // A tag on the last row runs on through the pixels under the grid. @@ -7446,7 +8032,14 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g // A closing pane is gone: its chrome is the last frame's. const closing = if (track) |active| active.phase == .closing else false; const placed = if (closing) surface.previous_regions else regions; - const grid = if (closing) surface.previous_cells else surface.cells; + // Where a lift may fall: pane bodies, never tags (addLift). + var body_buf: [pardes.MAX_PANES][4]f32 = undefined; + var nbodies: usize = 0; + for (placed) |part| if ((part.kind == .body or part.kind == .rail) and nbodies < body_buf.len) { + body_buf[nbodies] = bodyPixels(part, layout); + nbodies += 1; + }; + const bodies = body_buf[0..nbodies]; try paneDecor(g, gpa, placed, groups, index, chrome, layout, surface.cols, surface.rows, win_w, win_h, track); // A single spine runs down each column, through every tag and body. if (index == 0) for (regions) |column| { @@ -7455,14 +8048,42 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g const y0 = @as(f32, @floatFromInt(column.rect.y)) * layout.h; const grid_y1 = @as(f32, @floatFromInt(surface.rows)) * layout.h; const y1 = if (win_h - grid_y1 < layout.h) @max(grid_y1, win_h) else grid_y1; - if (y1 > y0) try addDecor(g, gpa, x, y0, x + 1, y1, chrome.border, 1, null, win_w, win_h); + // None at the window's edge: nothing to rule apart there. + if (column.rect.x == 0) continue; + if (y1 > y0) try addDecor(g, gpa, x, y0, x + rulePx(chrome), y1, chrome.border, 1, null, win_w, win_h); }; - try gripDecor(g, gpa, placed, grid, groups, index, layout, surface.cols, surface.rows, win_w, win_h, track); + // Cast last in the group, over its cells, images and chrome: a + // lifted pane's shadow darkens its neighbours' rails and rules + // as it darkens their text, and never the pane itself. + for (placed) |grip| { + if (grip.kind != .grip or grip.lift <= 0.001 or groupOf(groups, grip.serial) != index) continue; + var box: [4]f32 = .{ @as(f32, @floatFromInt(grip.rect.x)) * layout.w, @as(f32, @floatFromInt(grip.rect.y)) * layout.h, 0, @as(f32, @floatFromInt(grip.rect.y + grip.rect.h)) * layout.h }; + box[2] = box[0]; + for (placed) |part| { + if (part.owner != grip.owner or (part.kind != .tag and part.kind != .rail and part.kind != .body)) continue; + box[1] = @min(box[1], @as(f32, @floatFromInt(part.rect.y)) * layout.h); + box[2] = @max(box[2], @as(f32, @floatFromInt(part.rect.x + part.rect.w)) * layout.w); + box[3] = @max(box[3], @as(f32, @floatFromInt(part.rect.y + part.rect.h)) * layout.h); + } + // The top edge of its tag rows, for a rim: x0, x1, y. + const rim_edge: ?[3]f32 = for (placed) |part| { + if (part.owner == grip.owner and part.kind == .tag) break .{ @as(f32, @floatFromInt(grip.rect.x)) * layout.w, @as(f32, @floatFromInt(part.rect.x + part.rect.w)) * layout.w, @as(f32, @floatFromInt(part.rect.y)) * layout.h }; + } else null; + try addLift(g, gpa, lift_style, ceilings, box, rim_edge, bodies, grip.lift, track, win_w, win_h); + } } // A notice chip is tagline hung over the body's top rows, so it is // ruled off from the text beneath it the way a tag is: the same // colour and thickness along its bottom, running to the window's // edge when it does, fading up and dissolving with its band. + // A notice floats: its shadow first, then every rule. + for (surface.tagLayers()) |*layer| { + if (layer.rows == 0 or layer.kind != .notice or noticeGroup(groups, layer) != index) continue; + const x = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w; + const top = (@as(f32, @floatFromInt(layer.viewport.y)) + layer.slide) * layout.h; + const track = groups.items[groupAt(groups, layer.viewport.x, layer.viewport.y)].track; + try addLift(g, gpa, lift_style, ceilings, .{ x, top, x + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w, top + layout.h }, null, current_bodies, notice_lift * layer.fade, track, win_w, win_h); + } for (surface.tagLayers()) |*layer| { if (layer.rows == 0 or layer.kind != .notice or noticeGroup(groups, layer) != index) continue; const x0 = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w; @@ -7500,47 +8121,245 @@ fn paneDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, gr const rule_x1 = if (win_w - x1 < layout.w) @max(x1, win_w) else x1; const tag_bottom = tag.rect.y > rail.rect.y; const rule_y = @as(f32, @floatFromInt(if (tag_bottom) tag.rect.y else tag.rect.y + tag.rect.h)) * layout.h; - try addDecor(g, gpa, x0, if (tag_bottom) rule_y else rule_y - 1, rule_x1, if (tag_bottom) rule_y + 1 else rule_y, chrome.border, 1, track, win_w, win_h); + try addDecor(g, gpa, x0, if (tag_bottom) rule_y else rule_y - 1, rule_x1, if (tag_bottom) rule_y + 1 else rule_y, chrome.tag_rule orelse mixRgb(chrome.tag_bg, page), 1, track, win_w, win_h); + // Stacked panes are ruled apart as columns are, where one pane's + // body meets the next one's tag (acme's Border). + { + const pane_top = @min(tag.rect.y, rail.rect.y); + const stacked = for (regions) |other| { + if ((other.kind == .body or other.kind == .tag) and other.owner != rail.owner and other.rect.y + other.rect.h == pane_top and + other.rect.x < tag.rect.x + tag.rect.w and tag.rect.x < other.rect.x + other.rect.w) break true; + } else false; + // Inside the tag band's slack, never over its glyphs: the top + // slack of a tag on top, the bottom slack of the one above when + // tags are at the bottom; a pixel at least. + const y = @as(f32, @floatFromInt(pane_top)) * layout.h; + const spare = g.cell_h -| g.tagline_height; + if (stacked and !tag_bottom) { + const room = taglineBandOffset(pane_top, win_h, g.cell_h, g.tagline_height); + try addDecor(g, gpa, x0, y, rule_x1, y + @as(f32, @floatFromInt(@max(1, @min(chrome.rule_px, room)))), chrome.border, 1, track, win_w, win_h); + } else if (stacked and pane_top > 0) { + const room = spare - taglineBandOffset(pane_top - 1, win_h, g.cell_h, g.tagline_height); + try addDecor(g, gpa, x0, y - @as(f32, @floatFromInt(@max(1, @min(chrome.rule_px, room)))), rule_x1, y, chrome.border, 1, track, win_w, win_h); + } + } const y0 = @as(f32, @floatFromInt(rail.rect.y)) * layout.h; const body_end = rail.rect.y + rail.rect.h; const body_y1 = @as(f32, @floatFromInt(body_end)) * layout.h; const y1 = if (body_end == rows and win_h - body_y1 < layout.h) @max(body_y1, win_h) else body_y1; + // A theme's own track fills its width, the thumb one pixel short + // of it so the track edges it (acme's scrollbar); else a thin rail. + // acme's scrollbar: the track fills the scroll column, under the + // grip's button; the thumb is a pixel short of its right edge, so + // the track edges it. + const column = scrollColumn(g, chrome, rail.rect.x, layout); try addDecor(g, gpa, x0, y0, x0 + layout.w, y1, page, 1, track, win_w, win_h); - try addDecor(g, gpa, x0, y0, x0 + @min(layout.w, 2), y1, chrome.scroll_track, 1, track, win_w, win_h); + try addDecor(g, gpa, column[0], y0, column[1], y1, chrome.scroll_track, 1, track, win_w, win_h); if (rail.thumb_h != 0) { const thumb_y0 = @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y)) * layout.h; const thumb_y1 = if (rail.thumb_y + rail.thumb_h == rail.rect.h) y1 else @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y + rail.thumb_h)) * layout.h; - try addDecor(g, gpa, x0, thumb_y0, x0 + @min(layout.w, @max(2, @floor(layout.w * 0.5))), thumb_y1, chrome.scroll_thumb, 1, track, win_w, win_h); + try addDecor(g, gpa, column[0], thumb_y0, column[1] - 1, thumb_y1, chrome.scroll_thumb, 1, track, win_w, win_h); } } } +/// Halfway between two colours. +fn mixRgb(a: [3]u8, b: [3]u8) [3]u8 { + var out: [3]u8 = undefined; + for (&out, a, b) |*o, x, y| o.* = @intCast((@as(u16, x) + y) / 2); + return out; +} + +/// A rule's width: the theme's own, or one pixel. +fn rulePx(chrome: *const pardes.Chrome) f32 { + return @floatFromInt(@max(1, chrome.rule_px)); +} + /// The two short bars on each row of a grip with no mode glyph there, /// centered in its anchor. Only the panes of group `index`. -fn gripDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, cells: []const pardes.Cell, groups: *const Groups, index: usize, layout: CellLayout, cols: u16, rows: u16, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void { +fn gripDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, cells: []const pardes.Cell, groups: *const Groups, index: usize, chrome: *const pardes.Chrome, layout: CellLayout, cols: u16, rows: u16, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void { if (g.tagline_height < 6 or g.tagline_width < 3) return; for (regions) |grip| { - if (grip.kind != .grip or groupOf(groups, grip.serial) != index) continue; + if ((grip.kind != .grip and grip.kind != .column_grip) or groupOf(groups, grip.serial) != index) continue; + if (grip.rect.h == 0 or grip.rect.y >= rows) continue; + const button = gripButton(g, chrome, grip.rect.x, grip.rect.y, layout, win_h); + // Round the button, its first row is the tag's band: nothing of the + // cells under it shows there. + const band = if (grip.active) chrome.tag_focus_bg else chrome.tag_bg; + const left = @as(f32, @floatFromInt(grip.rect.x)) * layout.w; + const right = @as(f32, @floatFromInt(grip.rect.x + grip.rect.w)) * layout.w; + try addDecor(g, gpa, left, button[1], button[0], button[3], band, 1, track, win_w, win_h); + try addDecor(g, gpa, button[2], button[1], right, button[3], band, 1, track, win_w, win_h); + if (grip.kind != .grip) continue; + // acme's button ring, two pixels (ButtonBorder) inside the button: + // the focused one's in its own fill. An unfocused dirty one keeps a + // pixel of band inside its ring, so the four states differ in shape + // whatever a theme's colours (a focused dirty one is acme's + // modbutton, filled up to its ring). + { + // The rules drawn over the row (paneDecor, after this) cut into + // it: the ring runs inside them, from rule to rule. + const tag = for (regions) |part| { + if (part.kind == .tag and part.owner == grip.owner) break part; + } else grip; + const tag_bottom = for (regions) |part| { + if (part.kind == .rail and part.owner == grip.owner) break part.rect.y < tag.rect.y; + } else false; + const stacked = !tag_bottom and for (regions) |other| { + if ((other.kind == .body or other.kind == .tag) and other.owner != grip.owner and other.rect.y + other.rect.h == grip.rect.y and + other.rect.x < tag.rect.x + tag.rect.w and tag.rect.x < other.rect.x + other.rect.w) break true; + } else false; + const top_rule: f32 = if (tag_bottom) 1 else if (stacked) @floatFromInt(@max(1, @min(chrome.rule_px, taglineBandOffset(grip.rect.y, win_h, g.cell_h, g.tagline_height)))) else 0; + const bottom_rule: f32 = if (!tag_bottom and grip.rect.h == 1) 1 else 0; + const x0 = button[0]; + const x1 = button[2]; + const y0 = button[1] + top_rule; + const y1 = button[3] - bottom_rule; + const ring = if (grip.active) chrome.grip_focus_ring else chrome.grip_border; + try addDecor(g, gpa, x0, y0, x1, y0 + 2, ring, 1, track, win_w, win_h); + try addDecor(g, gpa, x0, y1 - 2, x1, y1, ring, 1, track, win_w, win_h); + try addDecor(g, gpa, x0, y0 + 2, x0 + 2, y1 - 2, ring, 1, track, win_w, win_h); + try addDecor(g, gpa, x1 - 2, y0 + 2, x1, y1 - 2, ring, 1, track, win_w, win_h); + if (grip.dirty and !grip.active) { + try addDecor(g, gpa, x0 + 2, y0 + 2, x1 - 2, y0 + 3, band, 1, track, win_w, win_h); + try addDecor(g, gpa, x0 + 2, y1 - 3, x1 - 2, y1 - 2, band, 1, track, win_w, win_h); + try addDecor(g, gpa, x0 + 2, y0 + 3, x0 + 3, y1 - 3, band, 1, track, win_w, win_h); + try addDecor(g, gpa, x1 - 3, y0 + 3, x1 - 2, y1 - 3, band, 1, track, win_w, win_h); + } + } const x = grip.rect.x; for (0..grip.rect.h) |line| { const y = grip.rect.y + @as(u16, @intCast(line)); if (x >= cols or y >= rows) continue; const cell = &cells[@as(usize, y) * cols + x]; if (cell.default or cell.printableAscii() != ' ') continue; - const rgb = switch (cell.style.fg) { - .rgb => |rgb| rgb, - else => continue, - }; - const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w + @as(f32, @floatFromInt(anchorInset(g, y, win_h) / 2)); + // The marks are in the button's ink, from the region's state. + const rgb = inkOn(gripFill(chrome, grip.active, grip.dirty)); + // The marks are the first row's alone, centered in the button. + if (line != 0) continue; + const center_x = @floor((button[0] + button[2]) / 2); const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2; - const half_w = @max(1, @min(3, layout.w - 2)); + const half_w = @max(1, @min(3, @floor((button[2] - button[0] - 6) / 2))); try addDecor(g, gpa, @floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), rgb, 1, track, win_w, win_h); try addDecor(g, gpa, @floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), rgb, 1, track, win_w, win_h); } } } +test "a theme's own rules and scrollbar are drawn at its widths: acme's 2px black rules and 12px track" { + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 }); + defer core.deinit(); + _ = try core.newShell(1, ""); + try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false)); + try std.testing.expect(core.executeBuiltinLine(0, "Theme acme")); + for (0..40) |_| core.update(.tick); + pardes.test_api.sync(core); + const s = try core.render(core.scratch.allocator()); + try std.testing.expectEqual(@as(u8, 2), s.chrome.rule_px); + // acme's 12px, at GripWidth's 150% default. + try std.testing.expectEqual(@as(u8, 18), s.chrome.rail_px); + var g: Gui = undefined; + g.decor = .empty; + defer g.decor.deinit(gpa); + g.cell_w = 16; + g.cell_h = 20; + g.tagline_width = 10; + g.tagline_height = 12; + var groups = makeGroups(&.{}, false); + try buildDecor(&g, gpa, s, &groups, fixedCellLayout(&g), 1280, 480, true); + const px = struct { + fn w(item: CellInstance) f32 { + return (item.x1 - item.x0) / 2 * 1280; + } + }; + var spines: usize = 0; + var tracks: usize = 0; + for (g.decor.items) |item| { + const black = item.br == 0 and item.bg == 0 and item.bb == 0; + // No rule at the window's left edge; one 2px spine where columns meet. + if (black and @abs(px.w(item) - 2) < 0.01 and item.y1 < -0.9) { + try std.testing.expect(item.x0 > -1); + spines += 1; + } + const column = scrollColumn(&g, &s.chrome, 40, fixedCellLayout(&g)); + if (@abs(item.br - @as(f32, 0x99) / 255) < 1e-3 and @abs(item.bb - @as(f32, 0x4c) / 255) < 1e-3 and @abs(px.w(item) - (column[1] - column[0])) < 0.01) tracks += 1; + } + try std.testing.expectEqual(@as(usize, 1), spines); + try std.testing.expect(tracks >= 2); +} + +test "the rule between stacked panes stays in the tag band's slack, at any tagline size" { + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 }); + defer core.deinit(); + _ = try core.newShell(1, ""); + core.newScratchBelow(0); + pardes.test_api.sync(core); + const s = try core.render(core.scratch.allocator()); + const lower = for (s.regionList()) |region| { + if (region.kind == .tag and region.rect.y > 2) break region; + } else return error.TestExpectedStackedTag; + for ([_]u32{ 12, 20 }) |tagline_h| { + var g: Gui = undefined; + g.decor = .empty; + defer g.decor.deinit(gpa); + g.cell_w = 10; + g.cell_h = 20; + g.tagline_width = 6; + g.tagline_height = tagline_h; + var groups = makeGroups(&.{}, false); + try buildDecor(&g, gpa, s, &groups, fixedCellLayout(&g), 800, 480, true); + const top = @as(f32, @floatFromInt(lower.rect.y)) * 20; + const slack: f32 = @floatFromInt(@max(1, pardes.taglineBandOffset(lower.rect.y, 480, 20, tagline_h))); + var found = false; + for (g.decor.items) |item| { + const y0 = (1 - item.y0) / 2 * 480; + const y1 = (1 - item.y1) / 2 * 480; + if (@abs(y0 - top) > 0.01 or @abs(item.br - @as(f32, @floatFromInt(s.chrome.border[0])) / 255) > 1e-3) continue; + found = true; + try std.testing.expect(y1 - y0 <= slack + 0.01); + } + try std.testing.expect(found); + } +} + +test "an empty column is ruled on both sides, as between two filled ones" { + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 }); + defer core.deinit(); + _ = try core.newShell(1, ""); + try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false)); + // Newcol from the left pane: an empty column between the two filled ones. + core.newColumn(0); + pardes.test_api.sync(core); + try std.testing.expectEqual(@as(usize, 3), core.ncol); + try std.testing.expectEqual(@as(usize, 0), core.col_n[1]); + const s = try core.render(core.scratch.allocator()); + var g: Gui = undefined; + g.decor = .empty; + defer g.decor.deinit(gpa); + g.cell_w = 10; + g.cell_h = 20; + g.tagline_width = 6; + g.tagline_height = 12; + const layout = fixedCellLayout(&g); + var groups = makeGroups(&.{}, false); + try buildDecor(&g, gpa, s, &groups, layout, 800, 480, true); + const border = [3]f32{ @as(f32, @floatFromInt(s.chrome.border[0])) / 255, @as(f32, @floatFromInt(s.chrome.border[1])) / 255, @as(f32, @floatFromInt(s.chrome.border[2])) / 255 }; + // Its left edge and its right edge (the next column's left): each a + // rule `rule_px` wide in the border colour, down to the window's foot. + for ([_]usize{ 1, 2 }) |column| { + const px: f32 = @floatFromInt(@as(u32, core.col_x[column]) * 10); + const rule = for (g.decor.items) |item| { + if (@abs(item.x0 - (px / 800 * 2 - 1)) < 1e-4 and @abs(item.x1 - ((px + @as(f32, @floatFromInt(s.chrome.rule_px))) / 800 * 2 - 1)) < 1e-4 and + std.mem.eql(f32, &.{ item.br, item.bg, item.bb }, &border)) break item; + } else return error.TestExpectedColumnRule; + try std.testing.expectEqual(@as(f32, -1), rule.y1); + } +} + test "decor is read from the regions, each pane's in its own group" { const gpa = std.testing.allocator; const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 }); @@ -7568,15 +8387,23 @@ test "decor is read from the regions, each pane's in its own group" { // workspace tag; a rail two pixels wide at each pane's. for (core.col_x[0..core.ncol]) |x| { const left = ndc.x(@as(f32, @floatFromInt(x)) * 10); + // Two pixels (rule_px), and none at the window's own edge. const spine = for (g.decor.items) |item| { - if (item.x0 == left and @abs(item.x1 - ndc.x(@as(f32, @floatFromInt(x)) * 10 + 1)) < 0.0001) break item; - } else return error.TestExpectedSpine; - try std.testing.expectApproxEqAbs(@as(f32, 1 - 20.0 / 480.0 * 2), spine.y0, 0.0001); - try std.testing.expectEqual(@as(f32, -1), spine.y1); + if (item.x0 == left and @abs(item.x1 - ndc.x(@as(f32, @floatFromInt(x)) * 10 + 2)) < 0.0001 and + @abs(item.y0 - (1 - 20.0 / 480.0 * 2)) < 0.0001 and std.mem.eql(f32, &.{ item.br, item.bg, item.bb }, &.{ @as(f32, @floatFromInt(s.chrome.border[0])) / 255, @as(f32, @floatFromInt(s.chrome.border[1])) / 255, @as(f32, @floatFromInt(s.chrome.border[2])) / 255 })) break item; + } else null; + try std.testing.expectEqual(x == 0, spine == null); + if (spine) |rule| { + try std.testing.expectApproxEqAbs(@as(f32, 1 - 20.0 / 480.0 * 2), rule.y0, 0.0001); + try std.testing.expectEqual(@as(f32, -1), rule.y1); + } + // The scrollbar fills the scroll column right of the rule. + const column = scrollColumn(&g, &s.chrome, x, layout); const rail = for (g.decor.items) |item| { - if (item.x0 == left and @abs(item.x1 - ndc.x(@as(f32, @floatFromInt(x)) * 10 + 2)) < 0.0001) break item; + if (@abs(item.x0 - ndc.x(column[0])) < 0.0001 and @abs(item.x1 - ndc.x(column[1])) < 0.0001 and + @abs(item.br - @as(f32, @floatFromInt(s.chrome.scroll_track[0])) / 255) < 0.0001) break item; } else return error.TestExpectedRail; - try std.testing.expectApproxEqAbs(@as(f32, s.chrome.scroll_track[0]) / 255, rail.br, 0.0001); + _ = rail; } try std.testing.expectEqual(@as(u32, 0), groups.items[groups.notices].decor_count); const still = groups.items[0].decor_count; diff --git a/src/host_io.zig b/src/host_io.zig index 25bd61e1..df78f73c 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -792,7 +792,7 @@ pub const Shell = struct { pub const Spawn = struct { path: [*:0]const u8, - argv: [4:null]?[*:0]const u8, + argv: [6:null]?[*:0]const u8, }; // Resolve in the parent. The path buffer and prompt files must survive through exec. @@ -803,7 +803,24 @@ pub const Shell = struct { .fish => if (prompt_rcs.fishCommand()) |command| .{ "-C", command.ptr } else .{ null, null }, .none => .{ null, null }, }; - return .{ .path = path, .argv = .{ path, marks[0], marks[1], null } }; + return .{ .path = path, .argv = .{ path, marks[0], marks[1], null, null, null } }; + } + + /// A command pane's shell runs its one line with job control on, so a + /// job the line puts in the background has a process group of its own. + /// The shell leads the terminal's session, and when it exits the kernel + /// hangs up the terminal's foreground group: with job control off that + /// is the shell's own, the job's too, and the job died with the command + /// (nohup'd or not; only setsid got out, and not always in time). A + /// shell pardes does not know runs the line as it is. + pub fn commandArgv(path: [*:0]const u8, line: [*:0]const u8) [6:null]?[*:0]const u8 { + const bin = std.mem.span(path); + const base = if (std.mem.lastIndexOfScalar(u8, bin, '/')) |s| bin[s + 1 ..] else bin; + const posix_shells = [_][]const u8{ "bash", "sh", "dash", "zsh", "ksh", "mksh", "yash" }; + for (posix_shells) |name| if (std.mem.eql(u8, base, name)) + return .{ path, "-m", "-c", line, null, null }; + if (family(bin) == .fish) return .{ path, "-C", "status job-control full", "-c", line, null }; + return .{ path, "-c", line, null, null, null }; } /// A shell by its path or its name in the usual directories, or null. @@ -840,6 +857,20 @@ pub const Shell = struct { return @ptrCast(buf); } + test "a command pane's line runs with job control on in the shells that have it" { + const want = struct { + fn check(path: [*:0]const u8, words: []const []const u8) !void { + const argv = commandArgv(path, "sleep 9 &"); + for (words, 0..) |w, i| try std.testing.expectEqualStrings(w, std.mem.span(argv[i].?)); + try std.testing.expect(argv[words.len] == null); + } + }; + try want.check("/usr/bin/bash", &.{ "/usr/bin/bash", "-m", "-c", "sleep 9 &" }); + try want.check("/bin/sh", &.{ "/bin/sh", "-m", "-c", "sleep 9 &" }); + try want.check("/usr/bin/fish", &.{ "/usr/bin/fish", "-C", "status job-control full", "-c", "sleep 9 &" }); + try want.check("/usr/bin/nu", &.{ "/usr/bin/nu", "-c", "sleep 9 &" }); + } + test "a path is taken at its word, a name is looked up, and both pick their own marks" { if (builtin.os.tag == .windows) return; var buf: [std.fs.max_path_bytes]u8 = undefined; @@ -1070,7 +1101,7 @@ pub fn forkShell( if (line.len > exec_command_max or std.mem.indexOfScalar(u8, line, 0) != null) return error.InvalidCommand; @memcpy(command_buf[0..line.len], line); command_buf[line.len] = 0; - spawn.argv = .{ spawn.path, "-c", command_buf[0..line.len :0].ptr, null }; + spawn.argv = Shell.commandArgv(spawn.path, command_buf[0..line.len :0].ptr); } var helper_buf: [4096]u8 = undefined; const mounted = one_line == null and if (core) |c| if (c.panes[pane]) |pn| pn.v9fs_on_spawn else false else false; @@ -1639,7 +1670,11 @@ fn reapShell(pid: libc.pid_t, grace_ms: u32) void { while (libc.waitpid(pid, null, 0) < 0 and libc.errno(-1) == .INTR) {} } -pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySignal) void { +/// The tty's foreground job is signalled. A command pane's shell (`command`) +/// is its command too: its line runs with job control on, so the job +/// running is in a group of its own, and the shell's group goes as well, or +/// the rest of the line would run on after the job it was on. +pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySignal, command: bool) void { const sig = switch (which) { .int => libc.SIG.INT, .term => libc.SIG.TERM, @@ -1648,11 +1683,9 @@ pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySigna .kill => libc.SIG.KILL, }; const fg = tcgetpgrp(master_fd); - if (fg > 0) { - _ = libc.kill(-fg, sig); - return; - } - if (shell_pid > 0) _ = libc.kill(shell_pid, sig); + if (fg > 0) _ = libc.kill(-fg, sig); + if (command and shell_pid > 0 and fg != shell_pid) _ = libc.kill(-shell_pid, sig); + if (fg <= 0 and !command and shell_pid > 0) _ = libc.kill(shell_pid, sig); } /// Kill: SIGTERM to the job in the tty's foreground, never to the shell. @@ -2043,6 +2076,43 @@ test "a command's exit is its process's, told by its watcher, whether or not its } } +test "a command's background job outlives it and its pty, and Kill stops its whole line" { + if (comptime !tty_probe_platform) return error.SkipZigTest; + if (!haveFile("/bin/sh")) return error.SkipZigTest; + const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer core.deinit(); + while (core.nextEffect()) |_| {} + const rcs: Shell.PromptFiles = .{}; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)]; + const Case = struct { line: []const u8, kill: bool, made: bool }; + for ([_]Case{ + // The shell leads the pty's session; its exit hangs up only the + // foreground group, which the job, in a group of its own, is not. + .{ .line = "(sleep 0.4; touch left) & exit 0", .kill = false, .made = true }, + // The job running is in its own group; the shell goes too. + .{ .line = "sleep 0.4; touch killed", .kill = true, .made = false }, + }) |c| { + const id = core.freeSlot().?; + _ = try core.newCommand(id, dir, c.line); + while (core.nextEffect()) |_| {} + const child = try forkShell(core, id, &rcs, "/bin/sh", dir, 24, 80, null); + if (c.kill) { + sleepMs(150); + signalTty(child.pid, child.file.handle, .term, true); + } + _ = exitStatus(child.pid, 2000); + _ = libc.close(child.file.handle); // the pane closes: the pty hangs up + sleepMs(800); + const name = if (c.kill) "killed" else "left"; + const made = if (tmp.dir.statFile(std.testing.io, name, .{})) |_| true else |_| false; + try std.testing.expectEqual(c.made, made); + try core.removePane(id, null); + } +} + test "a retired shell that ignores the hangup is killed and reaped, not left a zombie" { const pid = libc.fork(); if (pid == 0) { diff --git a/src/look.zig b/src/look.zig index a413bf50..d90d27b8 100644 --- a/src/look.zig +++ b/src/look.zig @@ -10,6 +10,7 @@ const platform_has_fs = fs.platform_has_fs; const layout = @import("layout.zig"); const animation = @import("animation.zig"); const modal = @import("modal.zig"); +const tagline = @import("tagline.zig"); const filesystem = @import("fs.zig"); const panes = @import("panes.zig"); const mouse = @import("mouse.zig"); @@ -466,6 +467,7 @@ pub fn expandedWord(line: []const u8, col: usize) ?ExpandedWord { pub fn expandedSel(p: *Pardes, pane: *Pane, at: Pane.Sel) ?Pane.Sel { var sl = at; if (sl.c0 != sl.c1 or sl.r0 != sl.r1) return sl; + if (sl.r0 >= 0 and sl.r0 < pane.tag_rows) return tagWordSel(p, pane, sl); const text = edit.paneText(p, pane) catch return null; var it = std.mem.splitAny(u8, text, "\n"); var v: i32 = 0; @@ -481,6 +483,34 @@ pub fn expandedSel(p: *Pardes, pane: *Pane, at: Pane.Sel) ?Pane.Sel { return null; } +/// A tag word is expanded in the tag's own text, so a word the wrapping +/// broke across rows is still one word, as acme's frame wraps a tag +/// without breaking its text. +fn tagWordSel(p: *Pardes, pane: *Pane, at: Pane.Sel) ?Pane.Sel { + const arena = p.scratch.allocator(); + const shown = tagline.shownTag(p, arena, pane) catch return null; + const whole = tagline.tagText(p, arena, pane) catch return null; + const row: usize = @as(usize, @intCast(at.r0)) + pane.tag_top; + const shown_line = modal.lineSlice(shown.text, row); + const real = shown.toReal(modal.lineStartOffset(shown.text, row) + panes.File.rawAtDisplay(shown_line, @intCast(@max(0, at.c0)))); + const start = if (std.mem.lastIndexOfScalar(u8, whole[0..@min(real, whole.len)], '\n')) |nl| nl + 1 else 0; + const end = std.mem.indexOfScalarPos(u8, whole, start, '\n') orelse whole.len; + const b = expandedWord(whole[start..end], real - start) orelse return null; + const lo = shownCell(shown.text, shown.fromReal(start + b.lo), pane.tag_top); + const hi = shownCell(shown.text, shown.fromReal(start + b.hi - 1), pane.tag_top); + return .{ .state = at.state, .r0 = lo.row, .c0 = lo.col, .r1 = hi.row, .c1 = hi.col }; +} + +fn shownCell(text: []const u8, off: usize, top: usize) struct { row: i32, col: i32 } { + const before = text[0..@min(off, text.len)]; + const row = std.mem.count(u8, before, "\n"); + const line_start = if (std.mem.lastIndexOfScalar(u8, before, '\n')) |nl| nl + 1 else 0; + return .{ + .row = @as(i32, @intCast(row)) - @as(i32, @intCast(top)), + .col = @intCast(panes.File.rawDisplayCol(text[line_start..], off - line_start)), + }; +} + /// the word under the modal cursor as a pane-local selection (paneText /// coords: row 0 is the tag; file panes carry the line-number prefix) pub fn cursorWordSel(p: *Pardes, pane: *Pane) Pane.Sel { @@ -1143,14 +1173,49 @@ pub fn followPdfLink(p: *Pardes, id: usize, probe: *const panes.Pdf.WordProbe) v } } +/// A look that found nothing: said on the message row, logged as an `err` +/// record, and marked so that /look reads back nothing (ninep/ctl.zig run). +fn missed(p: *Pardes, id: usize, comptime fmt: []const u8, args: anytype) void { + p.look_missed = true; + var said: [160]u8 = undefined; + const why = std.fmt.bufPrint(&said, fmt, args) catch "no match"; + var msg: [176]u8 = undefined; + p.setMessage(id, std.fmt.bufPrint(&msg, "Look: {s}", .{why}) catch "Look: no match"); + const serial = if (p.panes[id]) |pane| pane.serial else 0; + var rec: [200]u8 = undefined; + pardes.ctlfs.events.notePath(p, "err", std.fmt.bufPrint(&rec, "{d} look: {s}", .{ serial, why }) catch return); +} + +/// After a look at `path:N` focused its pane: a line past the end focuses +/// nothing, leaves no pane it opened, and is a miss; a bare line number +/// selects the line, its newline included, as acme's does. +fn settleLine(p: *Pardes, id: usize, path: []const u8, at: Spot, before: usize, opened: ?usize) void { + const shown = p.panes[p.active] orelse return; + const f = if (shown.file) |*file| file else return; + if (at.line > panes.File.nlines(p.gpa, f)) { + if (opened) |free| p.removePane(free, null) catch {}; + if (p.panes[before] != null) p.active = before; + return missed(p, id, "{s} has no line {d}", .{ path, at.line }); + } + if (at.line > 0 and at.col == 0 and at.end_line == 0) { + const start = modal.lineStartOffset(f.content, at.line - 1); + const next = std.mem.indexOfScalarPos(u8, f.content, start, '\n'); + const end = if (next) |i| i + 1 else f.content.len; + @import("ninep/pane.zig").setDot(shown, .{ .q0 = @intCast(start), .q1 = @intCast(end) }); + } +} + /// `file:addr` or `:addr`, acme's look.c:450-543: a colon followed by an /// address character (addr.c:28, isaddrc) that is not a line spot this /// parser already reads (`file:12`, `file:12:5`). Not a URL. fn addressed(tok: []const u8) ?struct { path: []const u8, expr: []const u8 } { if (std.mem.indexOf(u8, tok, "://") != null) return null; - if (std.mem.indexOfAny(u8, tok, " \t") != null) return null; const colon = std.mem.indexOfScalar(u8, tok, ':') orelse return null; const expr = tok[colon + 1 ..]; + // A written look is a whole line: the path has no blanks, but a + // pattern in the address may (`calc.py:/return a/`). + if (std.mem.indexOfAny(u8, tok[0..colon], " \t") != null) return null; + if (std.mem.indexOfAny(u8, expr, " \t") != null and std.mem.indexOfAny(u8, expr, "/?") == null) return null; if (expr.len == 0 or std.mem.indexOfScalar(u8, "0123456789+-/$.#,;", expr[0]) == null) return null; if (std.ascii.isDigit(expr[0])) { const pl = parsePathLine(tok); @@ -1178,12 +1243,10 @@ fn lookAddress(p: *Pardes, id: usize, path: []const u8, expr: []const u8) void { if (path.len > 0 and (f.output != null or !std.mem.endsWith(u8, f.path, std.fs.path.basename(path)))) return; var a: Addr = .{ .text = f.content, .lim = null, .expr = expr }; const r = a.address(ninep_pane.dotOf(pane)) orelse { - var said: [128]u8 = undefined; - return p.setMessage(id, std.fmt.bufPrint(&said, "Look: {s}", .{a.err}) catch "Look: bad address"); + return missed(p, id, "{s}", .{a.err}); }; if (a.i < expr.len or r.q0 > r.q1) { - var said: [128]u8 = undefined; - return p.setMessage(id, std.fmt.bufPrint(&said, "Look: {s}", .{if (r.q0 > r.q1) "addresses out of order" else "bad address syntax"}) catch "Look: bad address"); + return missed(p, id, "{s}", .{if (r.q0 > r.q1) "addresses out of order" else "bad address syntax"}); } clearNavigationSelection(p, pane); ninep_pane.setDot(pane, r); @@ -1210,6 +1273,10 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { if (comptime pdf_enabled) if (panes.Pdf.lookSection(p, id, pl.path, pl.at)) return; var realbuf: [4096]u8 = undefined; const found = resolveLookTarget(p, id, txt, &realbuf); + // A line past the end of an open pane is a miss (the target's line is + // already clamped, so the one written is asked). + if (found == .pane and found.pane.id < MAX_PANES) if (p.panes[found.pane.id]) |shown| if (shown.file) |*f| if (pl.at.line > panes.File.nlines(p.gpa, f)) + return missed(p, id, "{s} has no line {d}", .{ trimmed, pl.at.line }); // A miss changes nothing, the selection included; anything else starts // from a clean one. if (found != .none) clearNavigationSelection(p, pane); @@ -1220,10 +1287,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { p.reportError(id, "search", err); return; }; - if (rows == 0) { - var said: [96]u8 = undefined; - return p.setMessage(id, std.fmt.bufPrint(&said, "Look: no match for \"{s}\"", .{@import("Messages.zig").clip(trimmed, 48)}) catch "Look: no match"); - } + if (rows == 0) return missed(p, id, "no match for \"{s}\"", .{@import("Messages.zig").clip(trimmed, 48)}); clearNavigationSelection(p, pane); const at = pane.search_row; _ = searchStep(p, id, 1); @@ -1236,8 +1300,10 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { .pane => |t| { if (t.id >= MAX_PANES) return; const target = p.panes[t.id] orelse return; + const before = p.active; clearNavigationSelection(p, target); focusPaneLine(p, t.id, t.at, .center); + settleLine(p, id, trimmed, t.at, before, null); }, .url => |u| if (u.len <= 256) p.emit(.{ .open_link = .from(u) }), .dir => |dir| { @@ -1263,20 +1329,30 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { exec.placeNew(p, id, id, free, .doc); return; }; + // An open pane on this path too short for the line: a miss that + // touches nothing. + if (openPaneTarget(p, target.path, target.at)) |open| if (p.panes[open.pane.id].?.file) |*f| + if (target.at.line > panes.File.nlines(p.gpa, f)) + return missed(p, id, "{s} has no line {d}", .{ target.path, target.at.line }); // focus an existing pane on this path (rescrolled), else open - if (focusPaneByPath(p, target.path, target.at)) return; - const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots); - const nt = panes.File.open(p, free, target.path, target.at.line) catch |err| - return p.reportError(id, "look", err); - if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1); - exec.placeNew(p, id, id, free, .doc); - layout.compute(p); - nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2; - if (target.at.end_line != 0) { - const centered = nt.file.?.scroll; - selectSpan(p, nt, target.at); - nt.file.?.scroll = centered; + const before = p.active; + var opened: ?usize = null; + if (!focusPaneByPath(p, target.path, target.at)) { + const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots); + const nt = panes.File.open(p, free, target.path, target.at.line) catch |err| + return p.reportError(id, "look", err); + if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1); + exec.placeNew(p, id, id, free, .doc); + layout.compute(p); + nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2; + if (target.at.end_line != 0) { + const centered = nt.file.?.scroll; + selectSpan(p, nt, target.at); + nt.file.?.scroll = centered; + } + opened = free; } + settleLine(p, id, target.path, target.at, before, opened); }, .image => |target| { if (focusPaneByPath(p, target.path, .{})) return; @@ -1297,6 +1373,8 @@ test "Look hover waits without mutating the pane and input cancels it" { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = try p.setTestFile("alpha beta gamma\n"); + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); while (p.nextEffect()) |_| {} const other_id = p.freeSlot() orelse return error.NoSparePaneForHoverTest; _ = try p.newShell(other_id, ""); diff --git a/src/macos.zig b/src/macos.zig index b180d7ba..678b155d 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -1313,15 +1313,15 @@ fn drainInbox(st: *State) bool { host_io.commandEof(st.core, &st.ptys, e.pane, st, closeWatched); continue; } - // Unwatched, a command's exit is read here, as its end. - if (st.core.panes[e.pane]) |pane| if (pane.command != null) { - const status = host_io.exitStatus(pt.pid, 100); - if (status != null) pt.pid = 0; // reaped: no shell to retire - reap(st, e.pane); - st.core.update(.{ .exited = .{ .pane = e.pane, .status = status } }); - st.core.update(.{ .eof = .{ .pane = e.pane } }); - continue; - }; + // Unwatched, a command's exit is read here, as its end, + // and a shell's, for a run waiting on it and the log. + const command = if (st.core.panes[e.pane]) |pane| pane.command != null else false; + const status = host_io.exitStatus(pt.pid, 100); + if (status != null) pt.pid = 0; // reaped: no shell to retire + reap(st, e.pane); + if (command or status != null) st.core.update(.{ .exited = .{ .pane = e.pane, .status = status } }); + st.core.update(.{ .eof = .{ .pane = e.pane } }); + continue; } reap(st, e.pane); st.core.update(.{ .eof = .{ .pane = e.pane } }); @@ -2469,7 +2469,7 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void { fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void { const st = hostState(ctx); // A command whose exit is recorded is reaped: its group may be another's. - if (st.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig); + if (st.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig, pt.cmd.watched); } fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool { @@ -2866,7 +2866,7 @@ test "pardes.h matches the Zig boundary" { try expectEqual(@offsetOf(c.pardes_panel_box_s, field.name), @offsetOf(PanelBox, field.name)); try expectEqual(@sizeOf(c.pardes_panel_track_s), @sizeOf(PanelTrack)); inline for (@typeInfo(PanelTrack).@"struct".fields) |field| { - const c_name = comptime if (std.mem.eql(u8, field.name, "_padding")) + const c_name = comptime if (std.mem.eql(u8, field.name, "motion")) "reserved0" else field.name; @@ -3000,7 +3000,7 @@ test "mac panel ABI has the documented compact byte layout" { try std.testing.expectEqual(@as(usize, 4), @offsetOf(PanelTrack, "pane")); try std.testing.expectEqual(@as(usize, 5), @offsetOf(PanelTrack, "phase")); try std.testing.expectEqual(@as(usize, 6), @offsetOf(PanelTrack, "effect")); - try std.testing.expectEqual(@as(usize, 7), @offsetOf(PanelTrack, "_padding")); + try std.testing.expectEqual(@as(usize, 7), @offsetOf(PanelTrack, "motion")); try std.testing.expectEqual(@as(usize, 8), @offsetOf(PanelTrack, "frame")); try std.testing.expectEqual(@as(usize, 10), @offsetOf(PanelTrack, "frame_count")); try std.testing.expectEqual(@as(usize, 12), @offsetOf(PanelTrack, "from")); diff --git a/src/mouse.zig b/src/mouse.zig index ac72481a..36786612 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -48,6 +48,14 @@ pub const Drag = union(enum) { button: Mouse.Button, chorded: bool = false, ctrl: bool = false, + /// A B1 body sweep held past the pane's top (-1) or bottom (1): the + /// body scrolls a line a tick, as acme's frselect scrolls + /// (libframe frselect.c, the `scroll` callback while the button is + /// held outside the frame). + edge: i2 = 0, + /// Where the sweep began in the text, once it has scrolled: its + /// press row may be off the screen. + anchor: ?modal.Cursor = null, /// This gesture began over a usable native PDF raster. The payload is /// zero-bit when PDF support is absent. pdf: panes.Pdf.PointerDrag = .{}, @@ -625,6 +633,17 @@ pub fn dragUpdate(p: *Pardes, mcol: u16, mrow: u16, body_hit: ?Mouse.BodyHit) vo // from the pointer. const body_y = p.bodyTop(pane, r); const body_h = r.h -| pane.tag_rows; + s.edge = if (s.button != config.select_button or pane.sel[b].r0 < tag_rows or pane.file == null or pane.hasPdf() or body_h == 0) + 0 + else if (mrow < body_y) + -1 + else if (mrow >= body_y + body_h or + // The pointer cannot leave the screen: at a bottom pane, + // its last row is the edge. + (body_y + body_h >= p.screen_h and mrow + 1 >= body_y + body_h)) + 1 + else + 0; pane.sel[b].r1 = if (pane.sel[b].r0 < tag_rows) tag_line else if (body_h == 0) @@ -670,11 +689,12 @@ fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand: const pane = p.panes[id] orelse return .{}; if (on_tag) { const tag = tagline.tagText(p, p.scratch.allocator(), pane) catch return .{}; + const shown = tagline.shownTag(p, p.scratch.allocator(), pane) catch return .{}; const sel = operand.expanded orelse operand.preview orelse return .{}; const lead = if (sel.r0 < sel.r1 or (sel.r0 == sel.r1 and sel.c0 <= sel.c1)) sel else Pane.Sel{ .r0 = sel.r1, .c0 = sel.c1 }; const row: usize = @as(usize, @intCast(@max(0, lead.r0))) + pane.tag_top; - const start = modal.lineStartOffset(tag, row); - const at = start + panes.File.rawAtDisplay(modal.lineSlice(tag, row), @intCast(@max(0, lead.c0))); + const start = modal.lineStartOffset(shown.text, row); + const at = shown.toReal(start + panes.File.rawAtDisplay(modal.lineSlice(shown.text, row), @intCast(@max(0, lead.c0)))); const q0: u32 = @intCast(@min(at, tag.len)); return .{ .q0 = q0, .q1 = @intCast(@min(q0 + text.len, tag.len)) }; } @@ -695,12 +715,13 @@ fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand: const start: ?modal.Cursor = if (operand.file_word) |w| .{ .row = @intCast(@max(0, w.row)), .col = @intCast(@max(0, w.lo)) } else if (operand.expanded orelse operand.preview) |sel| lead: { - // A selection's leading corner in reading order, converted the way + // A selection's start in reading order, converted the way // `pointerOperand` converts the click itself. - const top = @min(sel.r0, sel.r1) - @as(i32, pane.tag_rows); + const lead = edit.sweepEnds(sel)[0]; + const top = lead[0] - @as(i32, pane.tag_rows); if (top < 0) break :lead null; const w = pane.wrapAt(top); - const col = edit.paneByteAtDisplay(p, &pane.body, w.line, w.at, @min(sel.c0, sel.c1) - panes.File.gutterWidth(pane)); + const col = edit.paneByteAtDisplay(p, &pane.body, w.line, w.at, lead[1] - panes.File.gutterWidth(pane)); break :lead .{ .row = @intCast(@max(0, w.line)), .col = @intCast(@max(0, col)), @@ -734,10 +755,11 @@ fn mirrorTtySelection(p: *Pardes, pane: *Pane) void { const rows = pane.pointerSelection(gesture) orelse return; const text = pane.rawPointerText(gesture) orelse return; if (rows.len == 0) return; - const endpoint = rows[if (gesture.r1 >= gesture.r0) rows.len - 1 else 0]; + const forward = gesture.r1 > gesture.r0 or (gesture.r1 == gesture.r0 and gesture.c1 >= gesture.c0); + const endpoint = rows[if (forward) rows.len - 1 else 0]; const body = body_layer.bodyText(p, p.scratch.allocator(), pane) catch return; const line = modal.lineSlice(body, @intCast(@max(0, endpoint.row - panes.Terminal.gridOffset(pane)))); - const byte = if (gesture.c1 >= gesture.c0) modal.prevGrapheme(line, @min(endpoint.hi, line.len)) else endpoint.lo; + const byte = if (forward) modal.prevGrapheme(line, @min(endpoint.hi, line.len)) else endpoint.lo; pane.body.cur_row = pane.surfRow(endpoint.row); var shown = std.mem.trimEnd(u8, line[@min(endpoint.prompt_bytes, line.len)..], " \t"); if (pane.ovl) |overlay| { @@ -757,11 +779,70 @@ fn mirrorTtySelection(p: *Pardes, pane: *Pane) void { edit.setYank(p, text); } +/// acme's double-click (text.c:881 textselect, plan9port text.c:1407 +/// textdoubleclick): a second plain B1 click at the same cell within half a +/// second selects what edit.doubleClickRange says, from the caret the click +/// left. A plain click still puts the caret on its cell. +fn doubleClick(p: *Pardes, id: usize, t: *panes.Text, on_tag: bool, row: i32, col: i32) void { + const now = clockMs(); + const prev = p.last_click; + p.last_click = .{ .id = id, .on_tag = on_tag, .row = row, .col = col, .ms = now }; + const c = prev orelse return; + if (c.id != id or c.on_tag != on_tag or c.row != row or c.col != col or now - c.ms > 500) return; + p.last_click = null; + const text = edit.flatSurface(p, t) catch return; + const q = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) }); + const r = edit.doubleClickRange(text, q); + if (r[1] <= r[0]) return; + const a = modal.positionAt(text, r[0]); + const h = modal.positionAt(text, modal.prevGrapheme(text, r[1])); + t.vsel = .{ .active = true, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true }; + t.cur_row = @intCast(h.row); + t.cur_col = @intCast(h.col); + t.cur_pinned = true; +} + +/// On each tick while a B1 body sweep is held past an edge: the body +/// scrolls a line toward it, the sweep's end stays under the pointer and +/// its start stays on its text (Drag.select.anchor). +pub fn edgeScroll(p: *Pardes) void { + if (p.drag != .select) return; + const s = &p.drag.select; + if (s.edge == 0) return; + const pane = p.panes[s.id] orelse return; + const sl = &pane.sel[sel_slot]; + if (s.anchor == null) s.anchor = sweepPoint(p, pane, sl.r0, sl.c0) orelse return; + const before = pane.scroll(); + pane.scrollBy(s.edge); + // The start's row follows its text while it is on the screen, and + // stays on the body's edge row once it is off (the anchor keeps it). + const top: i32 = pane.tag_rows; + sl.r0 = std.math.clamp(sl.r0 - (pane.scroll() - before), top, top + @as(i32, pane.rows) - 1); +} + +/// The text position under a body cell of the sweep's space. +fn sweepPoint(p: *Pardes, pane: *panes.Pane, r: i32, c: i32) ?modal.Cursor { + const visible = r - @as(i32, pane.tag_rows); + if (visible < 0) return null; + const w = pane.wrapAt(visible); + if (w.line < 0) return null; + const col = edit.paneByteAtDisplay(p, &pane.body, w.line, w.at, c - (if (pane.file != null) @as(i32, panes.File.gutterWidth(pane)) else 0)); + return .{ .row = @intCast(w.line), .col = @intCast(@max(0, col)) }; +} + +fn clockMs() i64 { + if (comptime !pardes.hosted) return 0; + var ts: std.c.timespec = undefined; + if (std.c.clock_gettime(.MONOTONIC, &ts) != 0) return 0; + return @as(i64, ts.sec) * 1000 + @divTrunc(@as(i64, ts.nsec), 1_000_000); +} + fn dragRelease(p: *Pardes, button: Mouse.Button) void { // A different button's release belongs to a chord or another host // stream; only the button which began this gesture may end it. if (p.drag.button() != button) return; defer p.drag = .none; + defer p.chord_hold = false; switch (p.drag) { .border_v => |d| { const c = d.left_col; @@ -844,10 +925,11 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { } else if (tagline.tagOffsetAt(p, pane, row, col)) |cursor| { pane.tag.cur_row = @intCast(cursor.row); pane.tag.cur_col = @intCast(cursor.col); + if (!dragged) doubleClick(p, s.id, &pane.tag, true, sl.r1, sl.c1); } return; } - if (dragged) edit.capturePointerSelection(p, pane, sel_slot) catch {}; + if (dragged and s.anchor == null) edit.capturePointerSelection(p, pane, sel_slot) catch {}; if (dragged and pane.body.mode == .tty) mirrorTtySelection(p, pane); const body_vis = sl.r1 - @as(i32, pane.tag_rows); if (body_vis >= 0 and pane.body.mode != .tty) { @@ -869,6 +951,15 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { pane.body.select = false; pane.body.restore_cursor = false; pane.body.sticky_col = -1; + // A ctrl-click is a definition jump, not half a double-click. + if (!dragged and !s.ctrl) doubleClick(p, s.id, &pane.body, false, sl.r1, sl.c1); + if (s.ctrl) p.last_click = null; + // A sweep that scrolled has its start off the screen: it + // is kept as the text's own selection, start to end. + if (dragged) if (s.anchor) |a| { + pane.sel[sel_slot].state = .none; + pane.body.vsel = .{ .active = true, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true }; + }; if (s.ctrl) p.lspRequest(s.id, .definition, ""); } } else { @@ -1001,6 +1092,11 @@ fn chordCutPaste(p: *Pardes, cut: bool) void { } if (!s.chorded) { s.chorded = true; + // Everything done while B1 stays down is one undo step, as acme + // marks the file once per B1 hold (text.c:881 textselect, `state` + // and its one filemark): B1-B2 then B1-B3 is a copy. + edit.pushUndo(p, &pane.body); + p.chord_hold = true; const sl = pane.sel[sel_slot]; pane.sel[sel_slot].state = .none; const pfx: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0; diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig index f57c62bd..1e47183b 100644 --- a/src/ninep/addr.zig +++ b/src/ninep/addr.zig @@ -222,8 +222,8 @@ pub const Addr = struct { a.err = "no previous regular expression"; return null; } - var rx = regexp_.Regex.compile(pat) catch { - a.err = e_regexp; + var rx = regexp_.Regex.compile(pat) catch |err| { + a.err = if (err == error.Anchor) regexp_.Regex.e_anchor else e_regexp; return null; }; const found = if (back) found: { diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 23d966e8..b4144fd2 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -69,10 +69,13 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { const before = Serials.snapshot(p); // An exec answers the pane it went to: a command pane it reused, or // the terminal it typed into. + p.look_missed = false; const went: ?usize = if (exec) pardes.exec.execute(p, id, text) else blk: { look.lookAt(p, id, text); break :blk null; }; + // A look that found nothing answers nothing, not the pane before it. + if (!exec and p.look_missed) return; const after = Serials.snapshot(p); var created = false; for (after.list[0..after.len]) |serial| if (std.mem.indexOfScalar(u32, before.list[0..before.len], serial) == null) { @@ -87,6 +90,37 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { noteResult(p, if (exec) context else (p.panes[p.active] orelse return).serial); } +/// A write's messages, a line each, except that an `Edit` line takes the +/// lines after it while its `{` group or `a`/`c`/`i` text block is open +/// (sam_edit.needsMore): a block written whole reaches Edit whole, never +/// as lines of their own. +const Messages = struct { + p: *Pardes, + data: []const u8, + at: usize = 0, + + fn init(p: *Pardes, data: []const u8) Messages { + return .{ .p = p, .data = data }; + } + + fn next(m: *Messages) ?[]const u8 { + if (m.at >= m.data.len) return null; + const start = m.at; + var end = std.mem.indexOfScalarPos(u8, m.data, start, '\n') orelse m.data.len; + m.at = end + 1; + const first = std.mem.trim(u8, m.data[start..end], " \t\r"); + const edit = "Edit"; + if (!std.mem.startsWith(u8, first, edit) or (first.len > edit.len and first[edit.len] != ' ' and first[edit.len] != '\t')) + return first; + const sam = @import("../sam_edit.zig"); + while (m.at < m.data.len and sam.needsMore(m.p.scratch.allocator(), std.mem.trim(u8, m.data[start..end], " \t\r")[edit.len..])) { + end = std.mem.indexOfScalarPos(u8, m.data, m.at, '\n') orelse m.data.len; + m.at = end + 1; + } + return std.mem.trim(u8, m.data[start..end], " \t\r"); + } +}; + // ---- look and exec ---- /// One command per line, checked whole before any line runs: a line is the @@ -96,11 +130,11 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply { for ([2]bool{ false, true }) |apply| { if (apply) p.fs.results_len = 0; - var it = std.mem.splitScalar(u8, req.data, '\n'); - while (it.next()) |raw| { - const text = std.mem.trim(u8, raw, " \t\r"); + var it = Messages.init(p, req.data); + while (it.next()) |text| { if (text.len == 0) continue; - for (text) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL); + // Only an Edit block holds newlines (Messages). + for (text) |c| if (c < ' ' and c != '\t' and c != '\n') return Reply.fail(req.tag, E.INVAL); if (!apply) continue; const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active; if (p.panes[id] == null) return Reply.fail(req.tag, E.NOENT); @@ -226,7 +260,8 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ? const word = line[0 .. sp orelse line.len]; const arg = if (sp) |s| std.mem.trim(u8, line[s + 1 ..], " \t") else ""; const b = std.meta.stringToEnum(Builtin, word) orelse return refuse(p, req, "unknown control message", line); - if (builtins.registry.scope(b) != scope) + // Edit is the one pane word the root takes too: at the active pane. + if (builtins.registry.scope(b) != scope and b != .Edit) return refuseTo(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line, if (scope == .pane) "/ctl" else "pane/<n>/ctl"); if (arg.len > 0 and !builtins.registry.takesArg(b)) return refuse(p, req, "wrong #args in control message", line); if (arg.len == 0 and builtins.registry.requiresArg(b)) return refuse(p, req, "wrong #args in control message", line); @@ -318,6 +353,10 @@ fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void { .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() }), .shader_animation => try w.print("{s} {s}\n", .{ word, @tagName(s.shader_animation) }), + .lift => try w.print("{s} {s}\n", .{ word, @tagName(s.lift) }), + .motion => try w.print("{s} {s}\n", .{ word, @tagName(s.motion) }), + .inactive_dim => try w.print("{s} {d}\n", .{ word, s.inactive_dim }), + .grip_width => try w.print("{s} {d}\n", .{ word, s.grip_width }), } } try w.writeAll("LocationsConfig "); @@ -345,9 +384,8 @@ pub fn readRoot(p: *Pardes, req: Req) Reply { pub fn writeRoot(p: *Pardes, req: Req) Reply { for ([2]bool{ false, true }) |apply| { if (apply) p.fs.results_len = 0; - var it = std.mem.splitScalar(u8, req.data, '\n'); - while (it.next()) |raw| { - const line = std.mem.trim(u8, raw, " \t\r"); + var it = Messages.init(p, req.data); + while (it.next()) |line| { if (line.len == 0) continue; if (!apply) { if (checkBuiltin(p, req, line, .session)) |refusal| return refusal; @@ -456,9 +494,8 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { for ([2]bool{ false, true }) |apply| { if (apply) p.fs.results_len = 0; var held = !other and pane.fs.lock != null; - var it = std.mem.splitScalar(u8, req.data, '\n'); - while (it.next()) |raw| { - const line = std.mem.trim(u8, raw, " \t\r"); + var it = Messages.init(p, req.data); + while (it.next()) |line| { if (line.len == 0) continue; // A line before this one may have closed the pane (`Del`). if (apply and p.paneBySerial(serial) == null) return Reply.fail(req.tag, E.NOENT); @@ -476,6 +513,14 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { if (!held) return tree.failText(req.tag, E.INVAL, e_not_locked); held = false; if (apply) pane.fs.lock = null; + } else if (std.mem.startsWith(u8, line, "answer ")) { + // The pane's question (its `ask` in the log), answered. + const choice = std.mem.trim(u8, line["answer ".len..], " "); + if (choice.len != 1) return refuse(p, req, "answer takes one choice, or -", line); + const id = p.paneBySerial(serial).?; + if (p.panes[id].?.prompt != .del_side and p.panes[id].?.prompt != .repl_choice) + return refuse(p, req, "no question asked", line); + if (apply) _ = p.answerQuestion(id, if (choice[0] == '-') null else choice[0]); } else if (!apply) { if (checkBuiltin(p, req, line, .pane)) |refusal| return refusal; } else if (runBuiltin(p, req, p.paneBySerial(serial).?, line)) |refusal| { @@ -790,6 +835,23 @@ test "Edit through a pane's ctl renames every foo in one undo step, and a failed try testing.expectEqualStrings("foo x foo y foo\n", rd(p, body, 0, 64).bytes); } +test "a write of an Edit block carries it whole, to a pane's ctl, the root's ctl and exec" { + const p = try withFile(testing.allocator, "foo x\nfoo y\n"); + defer p.deinit(); + const serial = serialOf(p); + const body = Node.of(serial, .body); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Edit ,x/foo/{\ni/[/\na/]/\n}\n").reply.status); + try testing.expectEqualStrings("[foo] x\n[foo] y\n", rd(p, body, 0, 64).bytes); + p.active = p.paneBySerial(serial).?; + try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Edit ,x/\\[/{\nc/</\n}\n").reply.status); + try testing.expectEqualStrings("<foo] x\n<foo] y\n", rd(p, body, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "Edit $a\nend\n.\n").reply.status); + try testing.expectEqualStrings("<foo] x\n<foo] y\nend\n", rd(p, body, 0, 64).bytes); + // An unclosed group is refused, and nothing after it runs as a line. + try testing.expectEqual(E.IO, wr(p, Node.of(serial, .ctl), "Edit ,x/foo/{\nd\n").errno()); + try testing.expectEqualStrings("<foo] x\n<foo] y\nend\n", rd(p, body, 0, 64).bytes); +} + test "dirty goes back to 0 when the text is the saved text again" { const p = try withFile(testing.allocator, "one\n"); defer p.deinit(); @@ -848,6 +910,18 @@ test "a look takes acme's :addr and file:addr, and a miss changes nothing" { try testing.expectEqual(Status.ok, wr(p, look_node, "foo\n").reply.status); var own: [16]u8 = undefined; try testing.expectEqualStrings(try std.fmt.bufPrint(&own, "{d}\n", .{serial}), rd(p, look_node, 0, 64).bytes); + // A pattern with a blank in it; 0/re/ searches from the top, whatever dot is. + try testing.expectEqual(Status.ok, wr(p, look_node, ":/foo y/\n").reply.status); + try testing.expectEqualStrings(" 6 11 ", rd(p, dot, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:0/foo/\n", .{path})).reply.status); + try testing.expectEqualStrings(" 0 3 ", rd(p, dot, 0, 64).bytes); + // file:N selects the line; a line past the end moves nothing and reads back empty. + try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:2\n", .{path})).reply.status); + try testing.expectEqualStrings(" 12 16 ", rd(p, dot, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&line, "{s}:99\n", .{path})).reply.status); + try testing.expectEqualStrings(" 12 16 ", rd(p, dot, 0, 64).bytes); + try testing.expectEqualStrings("", rd(p, look_node, 0, 64).bytes); + try testing.expectStringStartsWith(p.panes[0].?.msg[0..p.panes[0].?.msg_len], "Look: "); } test "exec runs a builtin at the pane and records the pane it acted on" { diff --git a/src/ninep/events.zig b/src/ninep/events.zig index 1c66c18f..0e489be7 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -934,8 +934,13 @@ test "a Dump written and a Restore made are in the log, with their files" { const log = try freezeLog(core); defer core.gpa.free(log.bytes); var want: [64]u8 = undefined; - try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "{s} /tmp/pardes.dump.zon\n", .{what}))); + try testing.expect(std.mem.indexOf(u8, log.bytes, try std.fmt.bufPrint(&want, "{s} /tmp/pardes.dump.zon\n", .{what})) != null); } + // The replacement's last says which serial each pane had. + const log = try freezeLog(restored); + defer restored.gpa.free(log.bytes); + var want: [64]u8 = undefined; + try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "restore /tmp/pardes.dump.zon\nrestored {d} {d}\n", .{ p.panes[0].?.serial, restored.panes[0].?.serial }))); } test "opens of the log share the open records, and a closed one frees its record" { diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 99acf24b..13381066 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -47,6 +47,10 @@ pub const State = struct { event_reader: bool = false, events: events.Queue = .{}, nomark: bool = false, + /// The revision a truncation of `data`, `xdata` or `body` left: the + /// write that follows it at once joins its undo step, so a shell's + /// `printf x > data` is one step, as acme's replace is. + joined: ?u32 = null, noscroll: bool = false, tag_snap: std.ArrayList(u8) = .empty, /// An open reads pty/data, so output queues for it; a second is refused. @@ -59,6 +63,9 @@ pub const State = struct { lock: ?u32 = null, /// The host started a shell it could not teach to mark its prompts. unmarked: bool = false, + /// The pty's size as pty/ctl's winsize last set it, until the pane's own + /// grid resizes and gives the pty its size again. + winsize: ?[2]u16 = null, /// Installed during this update; /log hears about it once the update ends /// and the pane has its file, name and place. unannounced: bool = false, @@ -190,7 +197,9 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const @memcpy(new[0..lo], f.content[0..lo]); @memcpy(new[lo..][0..take], bytes[0..take]); @memcpy(new[lo + take ..], f.content[hi..]); - if (!pane.fs.nomark) panes.File.pushUndo(p, pane); + const join = pane.fs.joined == f.revision; + pane.fs.joined = null; + if (!pane.fs.nomark and !join) panes.File.pushUndo(p, pane); panes.File.setContent(p, f, new); return take; } @@ -516,9 +525,12 @@ fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply { const f = fileOf(pane) orelse return Reply.fail(req.tag, E.INVAL); f.saved_revision = if (on) f.revision -% 1 else f.revision; }, + // acme's nomark joins the writes after it into one undo step. The + // step starts here: the text as it is now is what Undo brings back, + // and turning mark on again only ends it. .mark => { + if (!on and !pf.nomark) panes.File.pushUndo(p, pane); pf.nomark = !on; - if (on) panes.File.pushUndo(p, pane); }, .scroll => pf.noscroll = !on, else => unreachable, @@ -611,6 +623,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { switch (file) { .body => if (fileOf(pane) != null) { _ = spliceBody(p, pane, 0, bodyOf(pane).len, "") orelse return .err; + pf.joined = fileOf(pane).?.revision; pf.addr = .{}; setDot(pane, .{}); }, @@ -620,6 +633,7 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status { const q1 = @max(q0, pf.addr.q1); const before = dotOf(pane); _ = spliceBody(p, pane, q0, q1, "") orelse return .err; + pf.joined = fileOf(pane).?.revision; setDot(pane, .{ .q0 = shiftOne(before.q0, q0, q1 - q0, 0), .q1 = shiftOne(before.q1, q0, q1 - q0, 0) }); pf.addr = .{ .q0 = q0, .q1 = q0 }; }, @@ -906,6 +920,34 @@ test "the flag files read and write the state acme kept behind ctl verbs" { try testing.expectEqual(E.INVAL, wr(term, Node.of(serialOf(term), .dirty), "1").errno()); } +test "a replace through data, truncate then write, is one undo step" { + const p = try withFile(testing.allocator, "one two\n"); + defer p.deinit(); + const serial = serialOf(p); + _ = wr(p, Node.of(serial, .addr), "/two/"); + _ = call(p, .{ .tag = 1, .op = .setattr, .node = Node.of(serial, .data), .truncate = true }); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .data), "2").reply.status); + try testing.expectEqualStrings("one 2\n", rd(p, Node.of(serial, .body), 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status); + try testing.expectEqualStrings("one two\n", rd(p, Node.of(serial, .body), 0, 64).bytes); +} + +test "writes under mark 0 are one undo step, even with no history before them" { + const p = try withFile(testing.allocator, "one\n"); + defer p.deinit(); + const serial = serialOf(p); + const body = Node.of(serial, .body); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .mark), "0").reply.status); + _ = wr(p, body, "two\n"); + _ = wr(p, body, "three\n"); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .mark), "1").reply.status); + try testing.expectEqualStrings("one\ntwo\nthree\n", rd(p, body, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Undo").reply.status); + try testing.expectEqualStrings("one\n", rd(p, body, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Redo").reply.status); + try testing.expectEqualStrings("one\ntwo\nthree\n", rd(p, body, 0, 64).bytes); +} + test "truncating the tag clears its editable tail" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index 3599b612..393516ce 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -60,6 +60,7 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool { if (words.next() != null) return false; if (!apply) return true; p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } }); + pane.fs.winsize = .{ cols, rows }; }, .sig => { const which = signalNamed(words.next() orelse return false) orelse return false; @@ -79,13 +80,20 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool { /// ctl reads back its state in the words it takes, as a Plan 9 ctl does. pub fn readCtl(p: *Pardes, req: Req, pane: *Pane) Reply { - p.fs.stage(p.gpa).print(p.gpa, "winsize {d} {d}\n", .{ pane.cols, pane.rows }) catch + const size = winsize(pane); + p.fs.stage(p.gpa).print(p.gpa, "winsize {d} {d}\n", .{ size[0], size[1] }) catch return Reply.fail(req.tag, E.NOMEM); return tree.stagedReply(p, req); } -/// Three right-aligned fields: cols, rows and busy: whether a run written now -/// would be told `busy` (a command runs, or text is typed at the prompt). +/// The pty's size: what ctl's winsize set, else the pane's grid. +fn winsize(pane: *const Pane) [2]u16 { + return pane.fs.winsize orelse .{ pane.cols, pane.rows }; +} + +/// Three right-aligned fields: the pty's cols and rows, and busy: whether a +/// run written now would be told `busy` (a command runs, or text is typed at +/// the prompt). A newline ends the line, as a Plan 9 status file's does. pub const status_len: u64 = 36; pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { @@ -97,7 +105,8 @@ pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { const busy = p.hostTtyTaken(id) or (!pane.fs.unmarked and (waitingRun(p, pane) != null or (if (marks) |m| m.prompts > 0 and (m.phase != .input or !pardes.panes.Terminal.promptInputEmpty(pane)) else false))); const out = p.fs.stage(p.gpa); - out.print(p.gpa, "{d:>11} {d:>11} {d:>11} ", .{ pane.cols, pane.rows, @intFromBool(busy) }) catch {}; + const size = winsize(pane); + out.print(p.gpa, "{d:>11} {d:>11} {d:>11}\n", .{ size[0], size[1], @intFromBool(busy) }) catch {}; return tree.stagedReply(p, req); } @@ -287,6 +296,12 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void { const done = marks.finished orelse return; if (done.seq != slot.want) return; pf.run = null; + finish(p, slot, pane, done.status); +} + +/// Answers a run whose command ended with `status`: the header and the +/// output it printed. +fn finish(p: *Pardes, slot: *Run, pane: *Pane, status_code: ?i32) void { const printed = pardes.panes.Terminal.commandOutput(pane, p.gpa, output_rows) catch return answer(p, slot, "error out of memory", .{}); defer p.gpa.free(printed.text); @@ -305,7 +320,7 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void { } // A D that carries no status says nothing of how the command went. var code: [16]u8 = undefined; - const status = if (done.status) |s| std.fmt.bufPrint(&code, "{d}", .{s}) catch unreachable else "?"; + const status = if (status_code) |s| std.fmt.bufPrint(&code, "{d}", .{s}) catch unreachable else "?"; // The header is the whole first line, so a count there can never be // mistaken for output; `cut` with no count: its start is not there to // count from. @@ -317,6 +332,22 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void { answer(p, slot, "exit {s}", .{status}); } +/// The shell itself exited (a run's `exit 3`, or one typed): a run waiting +/// on the command it ran answers with the shell's status and what it +/// printed, and the log says `exit` before the pane's `del`. Unknown, the +/// status is not guessed: the run hears `error shell gone` when the pane +/// goes. +pub fn shellExited(p: *Pardes, pane: *Pane, status: ?u8) void { + const code = status orelse return; + if (waitingRun(p, pane)) |slot| if (slot.phase == .sent) { + pane.fs.run = null; + pardes.panes.Terminal.endOutputHere(pane); + finish(p, slot, pane, code); + }; + var buf: [4]u8 = undefined; + pardes.exec.noteRun(p, pane, "exit", std.fmt.bufPrint(&buf, "{d}", .{code}) catch unreachable); +} + /// The pane closed or its shell was replaced: the command's end will never /// be reported, so the run says so instead of waiting forever. A line not /// sent yet waits on for the replacement's first prompt. @@ -466,6 +497,12 @@ test "every pty/ctl verb, and every refusal" { try testing.expectEqual(@as(u16, 44), ws.winsize.?.rows); try testing.expectEqual(cols, pane.cols); try testing.expectEqual(rows, pane.rows); + // ctl and status read back the pty's size, until the pane resizes. + try testing.expectEqualStrings("winsize 132 44\n", rd(p, ctl, 0, 64).bytes); + try testing.expect(std.mem.startsWith(u8, rd(p, Node.of(serialOf(p), .pty_status), 0, 64).bytes, " 132 44 ")); + p.update(.{ .resize = .{ .cols = 76, .rows = 24 } }); + try testing.expect(pane.cols != 132); + try testing.expectEqualStrings(try std.fmt.bufPrint(&said, "winsize {d} {d}\n", .{ pane.cols, pane.rows }), rd(p, ctl, 0, 64).bytes); for ([_]struct { line: []const u8, want: pardes.PtySignal }{ .{ .line = "sig INT", .want = .int }, @@ -527,6 +564,8 @@ test "owned cwd pty exec rejects long paths before applying its batch" { const p = try withTerm(testing.allocator); defer p.deinit(); const pane = p.panes[0].?; + // A one-row tag: the long path would wrap it onto rows and resize the pty. + pane.tag_expanded = false; const ctl = Node.of(pane.serial, .pty_ctl); var path: [1025]u8 = @splat('d'); path[0] = '/'; @@ -554,7 +593,7 @@ test "pty/status reports the grid and who holds the tty" { const a = rd(p, status, 0, 256); try testing.expectEqual(Status.ok, a.reply.status); var want: [64]u8 = undefined; - const whole = try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11} ", .{ pane.cols, pane.rows, 0 }); + const whole = try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11}\n", .{ pane.cols, pane.rows, 0 }); try testing.expectEqualStrings(whole, a.bytes); try testing.expectEqual(@as(usize, 3 * 12), a.bytes.len); try testing.expectEqualStrings(whole[12..], rd(p, status, 12, 256).bytes); @@ -563,7 +602,7 @@ test "pty/status reports the grid and who holds the tty" { p.host = .{ .ctx = &probe, .vtable = &th.FakeTty.vtable }; const held = rd(p, status, 0, 256); try testing.expectEqualStrings( - try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11} ", .{ pane.cols, pane.rows, 1 }), + try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11}\n", .{ pane.cols, pane.rows, 1 }), held.bytes, ); } @@ -645,12 +684,12 @@ test "run answers how its command ended once the shell is back at a prompt" { // Before its first prompt the shell cannot take a line yet: the line // waits for it, and pty/status does not call the shell busy meanwhile. const status = Node.of(serialOf(p), .pty_status); - try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 0 ")); + try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 0\n")); const early = call(p, .{ .tag = 1, .op = .open, .node = node }); const waiting = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = early.reply.handle, .data = "true\n" }); try testing.expectEqualStrings("", waiting.pty()); try testing.expectEqual(Status.again, call(p, .{ .tag = 3, .op = .read, .node = node, .handle = early.reply.handle, .size = 64 }).reply.status); - try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 1 ")); + try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 1\n")); // A respawn before that prompt keeps it waiting for the new shell's. p.acknowledgeShell(0, "/bin/sh", true); try testing.expectEqual(Status.again, call(p, .{ .tag = 3, .op = .read, .node = node, .handle = early.reply.handle, .size = 64 }).reply.status); @@ -746,8 +785,52 @@ test "run answers how its command ended once the shell is back at a prompt" { _ = call(p, .{ .tag = 18, .op = .write, .node = node, .handle = gone.reply.handle, .data = "sleep 9\n" }); try p.removePane(0, null); try testing.expectEqualStrings("error shell gone\n", call(p, .{ .tag = 19, .op = .read, .node = node, .handle = gone.reply.handle, .size = 64 }).bytes); + // It still stats while the run is open (a reader's fstat), on the + // open or on the file's name, and not once it is released. + try testing.expectEqual(Status.ok, call(p, .{ .tag = 19, .op = .getattr, .node = node, .handle = gone.reply.handle }).reply.status); + try testing.expectEqual(Status.ok, call(p, .{ .tag = 19, .op = .getattr, .node = node }).reply.status); _ = call(p, .{ .tag = 20, .op = .release, .node = node, .handle = gone.reply.handle }); for (p.fs.opens) |o| try testing.expect(o.node == 0); + try testing.expectEqual(E.NOENT, call(p, .{ .tag = 21, .op = .getattr, .node = node }).errno()); +} + +test "a shell that exits under a run answers its status, and the log says exit before del" { + const gpa = testing.allocator; + const p = try withTerm(gpa); + defer p.deinit(); + const serial = serialOf(p); + const node = Node.of(serial, .pty_run); + const prompt = "\x1b]133;A;cl=line;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07"; + p.update(.{ .output = .{ .pane = 0, .bytes = prompt } }); + while (p.nextEffect()) |_| {} + const log = @intFromEnum(tree.TopFile.log); + const f = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + const frozen = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = f, .size = 1 << 16 }).bytes.len; + _ = call(p, .{ .tag = 3, .op = .write, .node = log, .handle = f, .data = "follow" }); + + const r = call(p, .{ .tag = 4, .op = .open, .node = node }); + _ = call(p, .{ .tag = 5, .op = .write, .node = node, .handle = r.reply.handle, .data = "echo bye; exit 3\n" }); + p.update(.{ .output = .{ .pane = 0, .bytes = "echo bye; exit 3\r\n\x1b]133;C;aid=pardes\x07bye\r\n" } }); + p.update(.{ .exited = .{ .pane = 0, .status = 3 } }); + p.update(.{ .eof = .{ .pane = 0 } }); + while (p.nextEffect()) |_| {} + try testing.expect(p.paneBySerial(serial) == null); + try testing.expectEqualStrings("exit 3\nbye\n", call(p, .{ .tag = 6, .op = .read, .node = node, .handle = r.reply.handle, .size = 64 }).bytes); + var want: [128]u8 = undefined; + // A followed log reads a record at a time: the exit, then the del. + var records: [2][]const u8 = undefined; + var at = frozen; + for (&records) |*rec| { + rec.* = try gpa.dupe(u8, call(p, .{ .tag = 7, .op = .read, .node = log, .handle = f, .off = at, .size = 4096 }).bytes); + at += rec.len; + } + defer for (records) |rec| gpa.free(rec); + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "exit {d} 3\n", .{serial}), records[0]); + try testing.expect(std.mem.startsWith(u8, records[1], try std.fmt.bufPrint(&want, "del {d} ", .{serial}))); + // A write to its pty/data now finds no pane. + try testing.expectEqual(E.NOENT, wr(p, Node.of(serial, .pty_data), "x").errno()); + _ = call(p, .{ .tag = 8, .op = .release, .node = node, .handle = r.reply.handle }); + _ = call(p, .{ .tag = 9, .op = .release, .node = log, .handle = f }); } test "run on a command pane says no shell is there" { diff --git a/src/ninep/screen.zig b/src/ninep/screen.zig index 88947d0d..462ea40f 100644 --- a/src/ninep/screen.zig +++ b/src/ninep/screen.zig @@ -254,6 +254,8 @@ test "screen inspection preserves acknowledged presentation and the next real fr var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); for ([_]*Pardes{ control, inspected }) |p| { + // The mechanism, at a fixed flavour, not the default's tuning. + p.settings.motion = .crisp; p.settings.panel_transition = .off; p.update(.tick); p.acknowledgePanelPresentation((try p.render(arena.allocator())).panelTracks()); diff --git a/src/ninep/testing.zig b/src/ninep/testing.zig index fce77f10..927552e2 100644 --- a/src/ninep/testing.zig +++ b/src/ninep/testing.zig @@ -56,6 +56,15 @@ pub fn rd(p: *Pardes, node: u64, off: u64, size: u32) Answer { return call(p, .{ .tag = 1, .op = .read, .node = node, .off = off, .size = size }); } +/// Whether the log, read whole from a fresh open, holds `needle`. +pub fn logHas(p: *Pardes, needle: []const u8) bool { + const log = @intFromEnum(tree.TopFile.log); + const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + defer _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); + const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes; + return std.mem.indexOf(u8, text, needle) != null; +} + pub fn wr(p: *Pardes, node: u64, data: []const u8) Answer { return call(p, .{ .tag = 2, .op = .write, .node = node, .data = data }); } diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 5ef69bb4..ad95f6bf 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -385,7 +385,7 @@ pub fn handle(p: *Pardes, req: Req) Reply { }; return switch (req.op) { .lookup => lookup(p, req, target), - .getattr => .{ .tag = req.tag, .attr = attrOf(p, target) orelse return Reply.fail(req.tag, E.NOENT) }, + .getattr => .{ .tag = req.tag, .attr = attrOf(p, target) orelse runAttr(p, req, target) orelse return Reply.fail(req.tag, E.NOENT) }, .setattr => setattr(p, req, target), .open => open(p, req, target), .release => release(p, req), @@ -395,6 +395,24 @@ pub fn handle(p: *Pardes, req: Req) Reply { }; } +/// A run's answer outlives its pane, so its file still stats while a run +/// is open on it (a reader's fstat, cat's), as acme's fid keeps its window. +/// The stat names the open when it came on one; a mount's comes on the +/// file's walked name, and serials are never reused, so any run open on +/// this node is the reader's. +fn runAttr(p: *Pardes, req: Req, target: Target) ?Reply.Attr { + const t = switch (target) { + .pane => |t| t, + .top => return null, + }; + if (t.file != .pty_run) return null; + const run = if (req.handle != 0) openOf(p, req) else for (&p.fs.opens) |*o| { + if (o.node == req.node and o.what == .run) break o; + } else null; + if ((run orelse return null).what != .run) return null; + return .{ .name = t.file.fileName(), .node = Node.of(t.serial, t.file), .mode = t.file.mode(), .mtime = p.fs.started }; +} + fn attrOf(p: *Pardes, target: Target) ?Reply.Attr { switch (target) { .top => |f| return .{ diff --git a/src/panes.zig b/src/panes.zig index 47582692..8ca2bb4d 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -27,8 +27,10 @@ pub const Pane = struct { /// The words that throw unsaved text away (builtins.zig warnModified). pub const Discarding = enum { Exit, Restore, get }; - /// One mouse selection (block-shaped), per button. c/r are text-area relative; - /// r counts from the tag's first row (the body starts at Pane.tag_rows). + /// One mouse sweep, per button: the press (c0, r0) and where it is now + /// (c1, r1), a stream between them as acme's (edit.sweepCols). c/r are + /// text-area relative; r counts from the tag's first row (the body + /// starts at Pane.tag_rows). pub const Sel = struct { state: enum { none, dragging, done } = .none, source_id: u64 = 0, @@ -38,12 +40,15 @@ pub const Pane = struct { r1: i32 = 0, }; - /// A rectangle keeps one source span per rendered row. Wrapped segments - /// remain separate rows when copied, even when they share a source line. + /// A captured sweep keeps one source span per rendered row. Wrapped + /// segments remain separate rows when copied, even when they share a + /// source line. pub const PointerRow = struct { row: i32, lo: usize, hi: usize, + /// Not the sweep's last row: its highlight runs to the right edge. + to_edge: bool = false, prefix: [32]u8 = @splat(0), prefix_len: u8 = 0, raw_terminal: bool = false, @@ -181,6 +186,11 @@ pub const Pane = struct { /// scrolls to keep its caret in view while it has the keyboard; pointer /// rows count from this line, not the tag's first. tag_top: u16 = 0, + /// The tag shows all its rows (acme's `tagexpand`, on by default); off, + /// one row, as acme's Tagup leaves it. Down on the + /// tag's last line and Up on its first, typing, or Alt-Down and Alt-Up + /// in the tag, as acme's arrows in a tag (plan9port text.c:776). + tag_expanded: bool = true, /// How long the tag's computed prefix was when its positions were last /// placed. They count from the start of the prefix, so when it grows or /// shrinks (a rename, the dirty marker, a PDF's page) what follows it diff --git a/src/pardes.zig b/src/pardes.zig index 18eedccb..6294b6cb 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1415,37 +1415,19 @@ test "pane defaults keep creation of scratch panes in column tags" { try std.testing.expectEqualStrings(tagline.file_pane_tail, tagline.curTail(file)); } -test "an unsaved file marker sits between its path and builtins until Save" { +test "an unsaved file's tag is its name alone: the grip, not a mark, says it is unsaved" { const gpa = std.testing.allocator; const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 100, .rows = 30 }); defer p.deinit(); while (p.nextEffect()) |_| {} const pane = try p.setTestFile("before\n"); - const clean = try tagline.tagText(p, p.scratch.allocator(), pane); - try std.testing.expect(std.mem.indexOf(u8, clean, "/test.txt *") == null); - panes.File.setContent(p, &pane.file.?, try gpa.dupe(u8, "after\n")); const dirty = try tagline.tagText(p, p.scratch.allocator(), pane); - const marker_at = std.mem.indexOf(u8, dirty, "/test.txt *") orelse return error.MissingDirtyMarker; - const save_at = std.mem.indexOf(u8, dirty, "Save") orelse return error.MissingSaveBuiltin; - try std.testing.expect(marker_at < save_at); - - try std.testing.expect(p.executeBuiltinLine(0, "Save")); - while (p.nextEffect()) |effect| p.perform(effect); - const saved = try tagline.tagText(p, p.scratch.allocator(), pane); - try std.testing.expect(std.mem.indexOf(u8, saved, "/test.txt *") == null); - - panes.File.changed(p, 0, "external\n"); - const reloaded = try tagline.tagText(p, p.scratch.allocator(), pane); - try std.testing.expect(std.mem.indexOf(u8, reloaded, "/test.txt *") == null); - - // A generated output is file-shaped and Save can write it to a path, but - // there is no file of its own for it to be dirty against. - pane.file.?.output = .{ .from = .search }; - panes.File.setContent(p, &pane.file.?, try gpa.dupe(u8, "result\n")); - const output = try tagline.tagText(p, p.scratch.allocator(), pane); - try std.testing.expect(std.mem.indexOf(u8, output, "/test.txt *") == null); + try std.testing.expectEqualStrings(clean, dirty); + try std.testing.expect(std.mem.indexOf(u8, dirty, " *") == null); + // The dirty file and index's flag still say it. + try std.testing.expect(ctlfs.pane.dirtyOf(pane)); } test "unknown Exec from a document runs as a command pane in its directory, typed into no terminal" { @@ -1656,6 +1638,11 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe _ = exec.executeFrom(p, 0, "a = 1\r\nb = \x1b[201~2", true); Sent.drain(p); try std.testing.expectEqualStrings("a = 1\rb = [201~2\r", Sent.said(1)); + // A block left open by its indented last line gets the blank line that + // ends it: a second Enter. + _ = exec.executeFrom(p, 0, "def f():\n return 1", true); + Sent.drain(p); + try std.testing.expectEqualStrings("def f():\r return 1\r\r", Sent.said(1)); // Several selections executed at once: each is sent, not the first. { const text = try edit.flatSurface(p, &file.body); @@ -1691,6 +1678,16 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe Sent.drain(p); try std.testing.expectEqualStrings("print(2)\r", Sent.said(2)); try std.testing.expect(Sent.len[1] == 0 and file.prompt == .none); + // The question is in the log, and a script answers it on the ctl. + { + const th = @import("ninep/testing.zig"); + _ = exec.executeFrom(p, 0, "print(5)", true); + var want: [32]u8 = undefined; + try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "ask {d} repl a b\n", .{file.serial}))); + const answered = th.wr(p, ctlfs.Node.of(file.serial, .ctl), "answer a\n"); + try std.testing.expectEqual(ctlfs.Status.ok, answered.reply.status); + try std.testing.expectEqualStrings("print(5)\r", answered.pty()); + } _ = exec.executeFrom(p, 0, "print(3)", true); p.update(.{ .key = .{ .cp = Key.escape } }); Sent.drain(p); @@ -3314,7 +3311,7 @@ test "header clicks beyond text append and long UTF-8 tags reveal their caret" { try std.testing.expectEqual(@as(usize, 0), p.header_scroll); } -test "a column tag with a newline in it takes a row per line and pushes its panes down" { +test "a newline typed into a column tag is a space: the tag stays one row" { const p = try Pardes.init(std.testing.allocator, .{ .cols = 50, .rows = 12, .tty_only = true }); defer p.deinit(); _ = try p.setTestFile("body\n"); @@ -3324,26 +3321,9 @@ test "a column tag with a newline in it takes a row per line and pushes its pane p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = 40, .row = TOPBAR_H } }); p.update(.{ .key = .{ .cp = Key.enter } }); p.update(.{ .key = .{ .cp = 'S', .text = "Snarf" } }); - try std.testing.expectEqualStrings("New\nSnarf", tagline.columnTag(p, 0)); - try std.testing.expectEqual(@as(u16, 2), p.columnBarHeight()); - const frame = try p.render(p.scratch.allocator()); - try std.testing.expectEqual(TOPBAR_H + 2, p.rects[0].y); - try std.testing.expectEqual(TOPBAR_H + 1, frame.cursor.?.y); - try std.testing.expectEqual(@as(u8, 'S'), frame.at(TAG_TEXT_INSET, TOPBAR_H + 1).text[0]); - - // A click on the second row lands on the second line. - p.update(.{ .key = .{ .cp = Key.escape } }); - p.acknowledgePanelPresentation(&.{}); - p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = TAG_TEXT_INSET + 1, .row = TOPBAR_H + 1 } }); - p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = TAG_TEXT_INSET + 1, .row = TOPBAR_H + 1 } }); - try std.testing.expectEqual(@as(i32, 1), p.col_tags[0].cur_row); - try std.testing.expectEqual(@as(i32, 1), p.col_tags[0].cur_col); - - // Taking the newline back out gives the panes their row again. - p.update(.{ .key = .{ .cp = Key.home } }); - p.update(.{ .key = .{ .cp = Key.backspace } }); - try std.testing.expectEqualStrings("NewSnarf", tagline.columnTag(p, 0)); _ = try p.render(p.scratch.allocator()); + try std.testing.expectEqualStrings("New Snarf", tagline.columnTag(p, 0)); + try std.testing.expectEqual(COLBAR_H, p.columnBarHeight()); try std.testing.expectEqual(TOPBAR_H + COLBAR_H, p.rects[0].y); } @@ -3733,6 +3713,8 @@ test "Newcol refuses an unsplittable restored weight before spawning" { test "layout commits publish finite tracks only for changed panes" { const p = try Pardes.init(std.testing.allocator, .{ .cols = 120, .rows = 20 }); defer p.deinit(); + // The mechanism, at a fixed flavour, not the default\'s tuning. + p.settings.motion = .crisp; _ = try p.newShell(1, ""); try std.testing.expect(layout.splitColumn(p, 0, 1, false)); @@ -3796,6 +3778,8 @@ test "vertical close samples only a canonical baseline containing that pane" { { const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 16, .tty_only = true }); defer p.deinit(); + // The mechanism, at a fixed flavour, not the default\'s tuning. + p.settings.motion = .crisp; var frame = std.heap.ArenaAllocator.init(std.testing.allocator); defer frame.deinit(); p.settings.panel_transition = .vertical; @@ -3819,6 +3803,8 @@ test "vertical close samples only a canonical baseline containing that pane" { { const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 16, .tty_only = true }); defer p.deinit(); + // The mechanism, at a fixed flavour, not the default\'s tuning. + p.settings.motion = .crisp; var frame = std.heap.ArenaAllocator.init(std.testing.allocator); defer frame.deinit(); p.settings.panel_transition = .vertical; @@ -4270,6 +4256,10 @@ pub const Pardes = struct { theme_file_generation: u32 = 0, theme_file_pane: u8 = 0, chrome_animation: colors.ChromeAnimation = colors.ChromeAnimation.init(colors.initial_chrome), + /// Each pane's lift off the page (Lift): 1 while it has the focus, on a + /// 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), 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 @@ -4290,6 +4280,13 @@ pub const Pardes = struct { pointer_tag_hit: ?TagHit = null, raw_hover_intent: bool = false, pointer_inside: bool = false, + /// The last plain B1 click, for acme's double-click (text.c:881, + /// textselect: a second click at the same point within 500 ms). + last_click: ?struct { id: usize, on_tag: bool, row: i32, col: i32, ms: i64 } = null, + /// A B1 chord's edits share the undo point it took (mouse.zig). + chord_hold: bool = false, + /// The look just run found nothing (look.zig missed). + look_missed: bool = false, look_hover_wait: ?look.LookHoverWait = null, look_hover_preview: ?look.LookHoverPreview = null, pdf_hover_preview: if (pdf_enabled) ?look.PdfWordPreview else void = if (pdf_enabled) null else {}, @@ -4617,24 +4614,21 @@ pub const Pardes = struct { return a; } - /// The workspace tag's height: a row per line of it while the setting - /// keeps it, zero once a native shell takes its commands over. Layout and - /// hit testing read this rather than the constant, the way they read - /// `columnBarHeight`. + /// The workspace tag's height: one row while the setting keeps it, as + /// acme's row tag is, zero once a native shell takes its commands over. + /// Layout and hit testing read this rather than the constant, the way + /// they read `columnBarHeight`. pub fn topBarHeight(p: *const Pardes) u16 { if (!p.settings.workspace_tag) return 0; - const lines = if (p.global_tag.own) |own| std.mem.count(u8, own, "\n") + 1 else TOPBAR_H; - return @intCast(@min(lines, MAX_TAG_ROWS, @max(TOPBAR_H, p.screen_h / 3))); + return TOPBAR_H; } - /// The column tags' height: a row per line of the tallest one. + /// The column tags' height: one row, as acme's (cols.c:244, colresize, + /// gives a column tag one font height); a newline in one is a space + /// (tagline.flattenHeaders). pub fn columnBarHeight(p: *const Pardes) u16 { if (p.screen_h < 3) return 0; - var lines: usize = COLBAR_H; - for (p.col_tags[0..p.ncol]) |t| if (t.own) |own| { - lines = @max(lines, std.mem.count(u8, own, "\n") + 1); - }; - return @intCast(@min(lines, MAX_TAG_ROWS, @max(COLBAR_H, p.screen_h / 3))); + return COLBAR_H; } fn detachCwds(p: *Pardes, parents: []const *Pane) !void { @@ -5227,7 +5221,9 @@ pub const Pardes = struct { if (v.wait_input) |f| { // Asleep until input or until core animation next needs a // frame; 0 is no timeout at all. - const timeout: u32 = if (p.nextWake()) |due| wake: { + // A render-time animation draws every loop: the wait only takes + // what input is there. + const timeout: u32 = if (p.continuous()) 1 else if (p.nextWake()) |due| wake: { const now = if (v.now) |clock| clock(h.ctx) else p.now_ns; const ms = ((due -| now) + std.time.ns_per_ms - 1) / std.time.ns_per_ms; break :wake @intCast(std.math.clamp(ms, 1, std.math.maxInt(u32))); @@ -5246,7 +5242,7 @@ pub const Pardes = struct { // rendering and nothing else. // A shell with a clock draws what `advance` stepped; one without // steps its own `.tick`s and draws while anything animates. - if (!p.needs_frame and (v.now != null or !p.animationActive())) return; + if (!p.needs_frame and !p.continuous() and (v.now != null or !p.animationActive())) return; _ = p.frame_arena.reset(.retain_capacity); const surface = try p.render(p.frame_arena.allocator()); if (v.present) |f| f(h.ctx, surface); @@ -5365,8 +5361,11 @@ pub const Pardes = struct { panes.Terminal.feedOutput(p, pane, o.bytes); ctlfs.pty.noteMarks(p, o.pane, pane); }, - .exited => |e| if (p.panes[e.pane]) |pane| if (pane.command != null and !pane.command_done) { - exec.commandDone(p, e.pane, e.status); + .exited => |e| if (p.panes[e.pane]) |pane| { + if (pane.command == null) + ctlfs.pty.shellExited(p, pane, e.status) + else if (!pane.command_done) + exec.commandDone(p, e.pane, e.status); }, .eof => |e| if (p.panes[e.pane]) |pane| if (pane.command != null) { // Output is over; the command is over when its child exits. @@ -5410,6 +5409,7 @@ pub const Pardes = struct { p.pointer_tag_hit = null; }, .tick => { + mouse.edgeScroll(p); p.chrome_animation.advance(); p.presentation.advance(); look.advanceLookHover(p); @@ -5494,37 +5494,66 @@ pub const Pardes = struct { } } - fn handleKey(p: *Pardes, key: Key) void { - if (p.macro.recording != 0 and p.macro.replaying == 0) Macro.append(&p.macro.recorded, p.gpa, key); - // A question waiting on a pane takes the next key, whatever mode the - // pane or a header edit is in: that key is its answer. - for (p.panes, 0..) |slot, id| { - const asked = slot orelse continue; - if (asked.prompt == .repl_choice) { + /// Answers the question pane `id` asks on its notice band, logged as + /// `ask <serial> <what> <choices>`: Del's side (`k` above, `j` below) or + /// the REPL to send to (its letter). Null, or a choice not offered, + /// takes the question back. A key answers it, or `answer` on the pane's + /// ctl; false when the pane asks nothing. + pub fn answerQuestion(p: *Pardes, id: usize, choice: ?u8) bool { + const asked = p.panes[id] orelse return false; + switch (asked.prompt) { + .repl_choice => { asked.prompt = .none; - const ask = p.repl_ask orelse return; + const ask = p.repl_ask orelse return true; p.repl_ask = null; defer p.gpa.free(ask.text); - // Its letter sends it there; anything else takes it back. - if (key.ctrl or key.alt or key.cp > 'z' or key.cp < 'a') return; - const serial = ask.offered[key.cp - 'a']; - if (serial == 0) return; + const letter = choice orelse return true; + if (letter < 'a' or letter > 'z') return true; + const serial = ask.offered[letter - 'a']; + if (serial == 0) return true; // The pane offered, still that REPL and still running: it // may have closed or been rebound while the question stood. if (p.paneBySerial(serial)) |to| if (p.panes[to].?.repl) |r| - if (r.lang == ask.lang and r.letter == key.cp and exec.replLive(p, to)) - return exec.sendToRepl(p, id, to, ask.text); + if (r.lang == ask.lang and r.letter == letter and exec.replLive(p, to)) { + exec.sendToRepl(p, id, to, ask.text); + return true; + }; p.setMessage(id, "Repl: the REPL chosen is gone; nothing sent"); - return; + }, + .del_side => { + asked.prompt = .none; + if (choice == 'k') exec.runBuiltin(p, .Del, id, "", "k"); + if (choice == 'j') exec.runBuiltin(p, .Del, id, "", "j"); + }, + else => return false, + } + return true; + } + + fn handleKey(p: *Pardes, key: Key) void { + // A double-click is two clicks with nothing between them. + p.last_click = null; + if (p.macro.recording != 0 and p.macro.replaying == 0) Macro.append(&p.macro.recorded, p.gpa, key); + // A question waiting on a pane takes the next key, whatever mode the + // pane or a header edit is in: that key is its answer. + for (p.panes, 0..) |slot, id| { + const asked = slot orelse continue; + switch (asked.prompt) { + // Its letter sends it there; anything else takes it back. + .repl_choice => _ = p.answerQuestion(id, if (key.ctrl or key.alt or key.cp > 'z' or key.cp < 'a') null else @intCast(key.cp)), + .del_side => _ = p.answerQuestion(id, if (hit(key, config.del_above)) 'k' else if (hit(key, config.del_below)) 'j' else null), + else => continue, } - if (asked.prompt != .del_side) continue; - asked.prompt = .none; - if (hit(key, config.del_above)) exec.runBuiltin(p, .Del, id, "", "k"); - if (hit(key, config.del_below)) exec.runBuiltin(p, .Del, id, "", "j"); return; } const pane = p.panes[p.active] orelse return; p.noteTyped(); + // Alt-Down and Alt-Up in a pane's tag expand and collapse it, in + // either mode: acme's Tagdown and Tagup (plan9port text.c:776). + if (pane.focus == .tag and key.alt and !key.ctrl and (key.cp == Key.down or key.cp == Key.up) and tagline.focusedHeader(p) == null) { + pane.tag_expanded = key.cp == Key.down; + return; + } // An open prompt takes the keys even when it answers for a header. const header = if (pane.prompt != .none) null else tagline.focusedHeader(p); if (header == null and pane.isTerminal() and pane.body.mode == .tty and pane.focus == .body and pane.prompt == .none) { @@ -6276,6 +6305,7 @@ pub const Pardes = struct { /// Recompute geometry, push grid-size changes to each emulator + pty, fire /// deferred greetings. The mirror of the prototype's loop epilogue. pub fn sync(p: *Pardes) void { + tagline.flattenHeaders(p); if (p.header_column != null and p.columnBarHeight() == 0) { tagline.exitHeader(p); } @@ -6326,11 +6356,30 @@ pub const Pardes = struct { } pane.tag_lead = lead; } - const lines = std.mem.count(u8, tagline.curTail(pane), "\n") + 1; - pane.tag_rows = @intCast(@min(lines, MAX_TAG_ROWS, @max(BOX_H, r.h -| 1))); + // A row per shown row: the tag's lines wrapped at its width. + var wrap_room: [32 * 1024]u8 = undefined; + var wrap_fba = std.heap.FixedBufferAllocator.init(&wrap_room); + const wa = wrap_fba.allocator(); + var lines = std.mem.count(u8, tagline.curTail(pane), "\n") + 1; + var wanted = lines; + var caret_row: usize = @intCast(@max(0, pane.tag.cur_row)); + if (tagline.prefixIn(wa, pane)) |prefix| { + if (std.mem.concat(wa, u8, &.{ prefix, tagline.curTail(pane) })) |whole| { + if (tagline.wrapTag(wa, whole, tagline.tagWidth(p, id))) |shown| { + lines = shown.rows(); + // Collapsed, one row, as acme's Tagup sets taglines to 1 + // (plan9port text.c:786). + wanted = if (pane.tag_expanded) lines else 1; + const cur = modal.offsetAt(whole, .{ .row = caret_row, .col = @intCast(@max(0, pane.tag.cur_col)) }); + caret_row = std.mem.count(u8, shown.text[0..@min(shown.fromReal(cur), shown.text.len)], "\n"); + } else |_| {} + } else |_| {} + } else |_| {} + if (!pane.tag_expanded) wanted = 1; + pane.tag_rows = @intCast(@min(wanted, MAX_TAG_ROWS, @max(BOX_H, r.h -| 1))); if (pane.collapsed) pane.tag_rows = BOX_H; if (pane.focus == .tag and pane.prompt != .name) { - const cur: usize = @intCast(@max(0, pane.tag.cur_row)); + const cur: usize = caret_row; var top: usize = pane.tag_top; if (cur < top) top = cur; if (cur >= top + pane.tag_rows) top = cur + 1 - pane.tag_rows; @@ -6345,6 +6394,7 @@ pub const Pardes = struct { panes.Terminal.resizeGrid(pane, p.gpa, cols, rows); p.shell_rows.markStale(pane); // reflow moved every row p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } }); + pane.fs.winsize = null; } pane.cols = cols; pane.rows = rows; @@ -6391,10 +6441,19 @@ pub const Pardes = struct { 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; + } + /// For shells that only need a yes or no: macOS and the board, which /// keep their own frame banks. pub fn animationActive(p: *const Pardes) bool { - return p.nextWake() != null; + return p.nextWake() != null or p.continuous(); } /// Bring core animation up to `now_ns`, the shell's monotonic clock: one @@ -6928,6 +6987,28 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" { try std.testing.expectEqual(top, p.active); try std.testing.expectEqual(h[2], p.rects[low].h); + // A script answers the question on the pane's ctl, as the log's `ask` + // names it: `answer -` takes it back, a side closes the pane toward it. + { + const th = @import("ninep/testing.zig"); + p.newScratchBelow(top); + const asked = p.active; + p.sync(); + const serial = p.panes[asked].?.serial; + const ctl = ctlfs.Node.of(serial, .ctl); + try std.testing.expectEqual(ctlfs.E.INVAL, th.wr(p, ctl, "answer k\n").errno()); + p.update(.{ .key = .{ .cp = ' ' } }); + p.update(.{ .key = .{ .cp = 'd' } }); + var want: [32]u8 = undefined; + try std.testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "ask {d} del k j\n", .{serial}))); + try std.testing.expectEqual(ctlfs.Status.ok, th.wr(p, ctl, "answer -\n").reply.status); + try std.testing.expect(p.panes[asked] != null and p.panes[asked].?.prompt == .none); + p.update(.{ .key = .{ .cp = ' ' } }); + p.update(.{ .key = .{ .cp = 'd' } }); + try std.testing.expectEqual(ctlfs.Status.ok, th.wr(p, ctl, "answer k\n").reply.status); + try std.testing.expect(p.paneBySerial(serial) == null); + } + // Bare Del from anything but a key closes at once and gives the rows up, // as it always has. p.newScratchBelow(top); diff --git a/src/regexp.zig b/src/regexp.zig index f6d540c4..249763c2 100644 --- a/src/regexp.zig +++ b/src/regexp.zig @@ -41,7 +41,12 @@ pub const Regex = struct { /// patterns take 45-80 ms, and a Debug build is ten times slower. pub const budget: u64 = if (builtin.mode == .Debug) 4_000_000 else 32_000_000; - pub fn compile(pat: []const u8) error{Bad}!Regex { + pub const e_anchor = "in a pattern with \\n, ^ can only come first and $ only just before a \\n"; + + /// `Anchor`: a pattern that names a newline has `^` other than first, + /// or `$` other than just before a `\n`, which mvzr would read as the + /// ends of the whole text and so never match where sam would. + pub fn compile(pat: []const u8) error{ Bad, Anchor }!Regex { if (pat.len == 0) return error.Bad; var buf: [256]u8 = undefined; var len: usize = 0; @@ -72,6 +77,12 @@ pub const Regex = struct { in_class = true; } else if (c == '.' and spans) { piece = "[^\\n]"; + } else if (spans and c == '^' and i != 0) { + return error.Anchor; + } else if (spans and c == '$') { + // `x$\n` is `x\n`; any other `$` would be the text's end. + if (std.mem.startsWith(u8, pat[i + 1 ..], "\\n")) continue; + return error.Anchor; } if (!emit) continue; if (len + piece.len > buf.len) return error.Bad; @@ -95,6 +106,20 @@ pub const Regex = struct { mvzr.steps_left = rx.steps; mvzr.exhausted = false; defer rx.steps = mvzr.steps_left; + // A `^` pattern that spans lines: mvzr's `^` is its haystack's start, + // so each line start from `from` on is tried as one. + // ponytail: a search per line start, each to `hi`; the step budget + // bounds it. + if (rx.spans and rx.bol) { + var s = if (from == 0 or text[from - 1] == '\n') from else (std.mem.indexOfScalarPos(u8, text[0..hi], from, '\n') orelse return null) + 1; + while (s <= last and s <= hi) { + const hit = rx.re.match(text[s..hi]); + if (mvzr.exhausted) return error.TooSlow; + if (hit) |m| if (m.start == 0) return .{ .start = s, .end = s + m.end }; + s = (std.mem.indexOfScalarPos(u8, text[0..hi], s, '\n') orelse return null) + 1; + } + return null; + } var start: usize = if (rx.spans) 0 else if (std.mem.lastIndexOfScalar(u8, text[0..from], '\n')) |nl| nl + 1 else 0; var at = from - start; // `^` cannot match in the middle of a line. @@ -140,6 +165,20 @@ test "lines are haystacks: ^ and $ at each line, . never a newline, \\n spans li try std.testing.expectEqual(c.end, m.end); } _ = try Regex.compile("a.*a\\nq"); + // `^` at every line start and `$` before a newline, when a pattern + // spans lines; anywhere else they are refused, never silently wrong. + const defs = "x = 1\ndef a\n\ndef b\n"; + var def = try Regex.compile("^def .*\\n"); + const d = (try def.find(defs, 0, defs.len, defs.len)).?; + try std.testing.expectEqual(@as(usize, 6), d.start); + try std.testing.expectEqual(@as(usize, 12), d.end); + try std.testing.expectEqual(@as(usize, 13), (try def.find(defs, 7, defs.len, defs.len)).?.start); + var blank = try Regex.compile("^\\n"); + try std.testing.expectEqual(@as(usize, 12), (try blank.find(defs, 0, defs.len, defs.len)).?.start); + var dollar = try Regex.compile("1$\\n"); + try std.testing.expectEqual(@as(usize, 4), (try dollar.find(defs, 0, defs.len, defs.len)).?.start); + try std.testing.expectError(error.Anchor, Regex.compile("(^|\\n)def")); + try std.testing.expectError(error.Anchor, Regex.compile("a$\\nb$")); var none = try Regex.compile("zzz"); try std.testing.expect(try none.find(text, 0, text.len, text.len) == null); try std.testing.expectError(error.Bad, Regex.compile("")); diff --git a/src/sam_edit.zig b/src/sam_edit.zig index c08c6cdd..679823da 100644 --- a/src/sam_edit.zig +++ b/src/sam_edit.zig @@ -82,6 +82,19 @@ pub fn run(arena: std.mem.Allocator, text: []const u8, dot: Range, name: []const return .{ .ops = ex.ops.items, .dot = d, .printed = ex.out.items }; } +/// Whether `command` stops inside a `{` group or an `a`, `c` or `i` text +/// block still waiting for its `.` line: a writer of lines (a ctl, exec) +/// hands an Edit the lines after it until it does not. +pub fn needsMore(arena: std.mem.Allocator, command: []const u8) bool { + var why: Why = .{}; + const src = std.fmt.allocPrint(arena, "{s}\n", .{command}) catch return false; + var ps: Parser = .{ .arena = arena, .src = src, .why = &why }; + while (true) { + const c = ps.parse(0) catch return std.mem.eql(u8, why.text(), "unmatched `{'") or ps.open_text; + if (c == null) return ps.open_text; + } +} + fn fail(why: *Why, comptime fmt: []const u8, args: anytype) Failure { why.len = if (std.fmt.bufPrint(&why.buf, fmt, args)) |s| s.len else |_| 0; return error.Edit; @@ -95,6 +108,10 @@ const Parser = struct { i: usize = 0, /// The last pattern given, for an empty one (edit.c:562, getregexp). lastpat: []const u8 = "", + /// The last `parse` that answered null met a `}`, not the end. + braced: bool = false, + /// A text block of `a`, `c` or `i` ran out before its `.` line. + open_text: bool = false, why: *Why, fn getch(ps: *Parser) ?u8 { @@ -248,6 +265,7 @@ const Parser = struct { try s.appendSlice(ps.arena, line); try s.append(ps.arena, '\n'); } + ps.open_text = true; return s.items; } const delim = ps.getch() orelse return ""; @@ -290,7 +308,11 @@ const Parser = struct { if (ps.nextc() == 'g') { ps.i += 1; cmd.global = true; - } + } else if (ps.nextc()) |d| if (d >= '1' and d <= '9') { + // `s/a/b/2`, the count after, as some write it for + // sam's `s2/a/b/`. + cmd.num = ps.getnum(1); + }; } try ps.atnl(); }, @@ -326,12 +348,18 @@ const Parser = struct { var last: ?*Cmd = null; while (true) { if (ps.skipbl() == '\n') ps.i += 1; - const next = (try ps.parse(nest + 1)) orelse break; + ps.braced = false; + const next = (try ps.parse(nest + 1)) orelse { + // sam reads on for the `}`; an Edit has no more to read. + if (!ps.braced) return fail(ps.why, "unmatched `{{'", .{}); + break; + }; if (last) |l| l.next = next else cmd.sub = next; last = next; } }, '}' => { + ps.braced = true; try ps.atnl(); if (nest == 0) return fail(ps.why, "right brace with no left brace", .{}); return null; @@ -380,7 +408,10 @@ const Exec = struct { } fn compile(ex: *Exec, pat: []const u8, c: u8) Failure!regexp.Regex { - return regexp.Regex.compile(pat) catch fail(ex.why, "bad regexp in {c} command", .{c}); + return regexp.Regex.compile(pat) catch |err| switch (err) { + error.Anchor => fail(ex.why, "{s}", .{regexp.Regex.e_anchor}), + error.Bad => fail(ex.why, "bad regexp in {c} command", .{c}), + }; } fn find(ex: *Exec, rx: *regexp.Regex, from: usize, hi: usize) Failure!?regexp.Regex.Match { @@ -625,6 +656,7 @@ test "sam's classic commands" { try expectEdit("aaa\n", ",s/a/b/g", "bbb\n"); try expectEdit("aaa\n", ",s/a/b/", "baa\n"); try expectEdit("aaa\n", ",s2/a/b/", "aba\n"); + try expectEdit("aaa\n", ",s/a/b/2", "aba\n"); try expectEdit("cat dog\n", ",s/[a-z]+/<&>/g", "<cat> <dog>\n"); // x within x, g within x. try expectEdit("int a;\nfloat b;\nint c;\n", ",x/.*\\n/g/^int/x/[a-z];/c/X;/", "int X;\nfloat b;\nint X;\n"); @@ -637,6 +669,8 @@ test "sam's classic commands" { try expectEdit("one\ntwo\n", "1 m $", "two\none\n"); try expectEdit("one\ntwo\n", "1 t $", "one\ntwo\none\n"); try expectEdit("one\ntwo\nthree\n", "2 d", "one\nthree\n"); + // ^ at every line start in a pattern that spans lines (dogfood round 5). + try expectEdit("x = 1\ndef a\ndef b\n", ",x/^def .*\\n/a/X\\n/", "x = 1\ndef a\nX\ndef b\nX\n"); // pardes's line:col. try expectEdit("one\ntwo\n", "2:2 i/-/", "one\nt-wo\n"); } @@ -650,9 +684,12 @@ test "an Edit that fails halfway changes nothing, and says why in acme's words" .{ "k", "unknown command k" }, .{ ",x/a/{\na/1/\ni/2/\n}", "changes out of sequence" }, .{ "}", "right brace with no left brace" }, + .{ ",x/foo/{", "unmatched `{'" }, + .{ ",x/foo/{\nd", "unmatched `{'" }, .{ "w /tmp/x", "w is not supported in pardes" }, .{ ",s/(a)/\\1/", "no \\1: mvzr keeps no submatches" }, .{ "1 m 1,2", "move overlaps itself" }, + .{ ",x/(^|\\n)foo/d", "in a pattern with \\n, ^ can only come first and $ only just before a \\n" }, }) |c| { var why: Why = .{}; try std.testing.expectError(error.Edit, run(arena_state.allocator(), "foo a\nfoo\n", .{}, "t", c[0], &why)); @@ -660,6 +697,19 @@ test "an Edit that fails halfway changes nothing, and says why in acme's words" } } +test "an Edit wants the lines after it until its braces and text blocks close" { + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const a = arena_state.allocator(); + try std.testing.expect(needsMore(a, ",x/foo/{")); + try std.testing.expect(needsMore(a, ",x/foo/{\ni/[/")); + try std.testing.expect(!needsMore(a, ",x/foo/{\ni/[/\n}")); + try std.testing.expect(needsMore(a, "$a\nline one")); + try std.testing.expect(!needsMore(a, "$a\nline one\n.")); + try std.testing.expect(!needsMore(a, ",s/{/x/")); + try std.testing.expect(!needsMore(a, ",x/foo/c/bar/")); +} + test "the dot an Edit leaves selects what a change put where it stood" { const ops = [_]Op{.{ .q0 = 2, .q1 = 4, .text = "XYZ" }}; try std.testing.expectEqual(Range{ .q0 = 2, .q1 = 5 }, moveDot(.{ .q0 = 2, .q1 = 2 }, &ops)); diff --git a/src/surface.zig b/src/surface.zig index a0471e51..15de128f 100644 --- a/src/surface.zig +++ b/src/surface.zig @@ -356,6 +356,8 @@ pub const Region = struct { rect: layout.Rect, /// The pane or column has the keyboard's focus. active: bool = false, + /// A grip's pane has changes it has not saved. + dirty: bool = false, /// A rail's thumb: rows from the rail's top, and how many. Zero rows is /// a rail with no thumb (an image). A native PDF's thumb is measured in /// document pixels and filled in by Pdf.draw while it paints, so read @@ -364,6 +366,10 @@ pub const Region = struct { thumb_h: u16 = 0, /// A notice's index in its pane's `notices`. notice: u8 = 0, + /// How far it stands off the page, 0 to 1: a pane's while it has the + /// focus (Lift, on a spring), a notice's while Lift is on. A pixel shell + /// casts a soft shadow from it; the tier it is drawn in never changes. + lift: f32 = 0, /// Cells the column-move rail takes off the right edge while it runs /// there: layers and pixel shells stop short of them, the grid does not. clip: u16 = 0, @@ -392,6 +398,26 @@ pub const Chrome = struct { tag_fg: [3]u8 = .{ 0, 0, 0 }, scroll_track: [3]u8 = .{ 0, 0, 0 }, scroll_thumb: [3]u8 = .{ 0, 0, 0 }, + /// The theme's accent (a glow's tint). + accent: [3]u8 = .{ 0, 0, 0 }, + /// A selection's text and ground: a lift keeps their contrast too. + sel_fg: [3]u8 = .{ 0, 0, 0 }, + sel_bg: [3]u8 = .{ 0, 0, 0 }, + /// How a lifted part stands off the page (config.Runtime.LiftStyle). + lift_style: u8 = 0, + /// Rule width in pixels (colors.Theme.rule_px). + rule_px: u8 = 2, + /// The ring round a grip, acme's button: unfocused, and focused. + grip_border: [3]u8 = .{ 0, 0, 0 }, + grip_focus_ring: [3]u8 = .{ 0, 0, 0 }, + /// A dirty pane's button fill. + grip_dirty: [3]u8 = .{ 0, 0, 0 }, + /// 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, + /// The scroll column's width, button and scrollbar alike, in pixels at + /// a 17px tagline (acme's 12 at its font); a pixel shell scales it. + rail_px: u8 = 12, }; /// The canonical interface: what a frame of pardes IS. The tty shell writes diff --git a/src/tagline.zig b/src/tagline.zig index c04cbc66..68fa2645 100644 --- a/src/tagline.zig +++ b/src/tagline.zig @@ -23,7 +23,15 @@ pub const context_pane_tail = file_pane_tail ++ " TreeContext"; pub const locations_pane_tail = file_pane_tail ++ " LocationsConfig"; pub const terminal_pane_tail = " " ++ config.terminal_pane_builtins_str; pub const command_pane_tail = " " ++ config.command_pane_builtins_str; +/// The mark a tag carried after an unsaved file's name before the grip +/// showed it; a dump from then still has it in its saved tags. const dirty_marker = " *"; + +/// A file with changes it has not saved. +pub fn dirty(pane: *const panes.Pane) bool { + const f = pane.file orelse return false; + return panes.Output.fileTraits(f.output).saves and f.revision != f.saved_revision; +} pub const legacy_pane_tail = " Del"; pub const legacy_file_pane_tail = " Save Del"; // The defaults from the release before Newtty joined every tagline. Recognized @@ -134,12 +142,9 @@ pub fn pathPrefix(arena: std.mem.Allocator, pane: *Pane) ![]const u8 { var idbuf: [48]u8 = undefined; return std.fmt.allocPrint(arena, "{s} {s}", .{ pane.cwdSlice(), exec.replId(&idbuf, pane) }); } - if (pane.file) |f| { - const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path; - if (panes.Output.fileTraits(f.output).saves and f.revision != f.saved_revision) - return std.fmt.allocPrint(arena, "{s}{s}", .{ name, dirty_marker }); - return name; - } + // Unsaved text shows on the grip, as acme's modbutton does, not as a + // mark in the text: the prefix is the name alone. + if (pane.file) |f| return if (pane.prompt == .name) pane.input.own orelse f.path else f.path; return pane.cwdSlice(); } @@ -207,13 +212,6 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void { t.cur_row = @intCast(cursor.row); t.cur_col = @intCast(cursor.col); t.cur_pinned = true; - // Reveal the suffix together with its commands, not merely the - // first command letter at the viewport's far right edge. - if (p.paneIdOf(pane)) |id| { - const line = whole[0 .. std.mem.indexOfScalar(u8, whole, '\n') orelse whole.len]; - const width = p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET); - pane.tag_scroll = @intCast(panes.File.displayWidth(line) + 1 -| width); - } } // The text may have changed since (a 9P write, a rename), so the // remembered cursor is pulled back inside it. It is the cursor that @@ -233,12 +231,83 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void { pane.focus = .tag; } -/// Where display column `col` of the tag's shown row `row` (tag scroll -/// included) puts its cursor: the tag as shown, prefix and all. +/// A pane's tag as it is shown: its lines, each wrapped onto more rows at +/// the tag's width as acme's frame wraps a tag at its right edge (plan9port +/// acme wind.c:140, wintaglines, counts the frame's lines). `soft` are the +/// offsets in `text` of the newlines the wrapping put in. +pub const Shown = struct { + text: []u8, + soft: []const usize, + + /// The tag's own offset of a shown one. + pub fn toReal(s: Shown, off: usize) usize { + var n: usize = 0; + for (s.soft) |b| { + if (b < off) n += 1 else break; + } + return off - n; + } + + /// The shown offset of the tag's own one. + pub fn fromReal(s: Shown, real: usize) usize { + var off = real; + for (s.soft) |b| { + if (b <= off) off += 1 else break; + } + return off; + } + + pub fn rows(s: Shown) usize { + return modal.cursorLineCount(s.text); + } +}; + +/// `whole` wrapped at `width` display columns. +pub fn wrapTag(arena: std.mem.Allocator, whole: []const u8, width: usize) !Shown { + var out: std.ArrayList(u8) = .empty; + var soft: std.ArrayList(usize) = .empty; + const w = @max(1, width); + var lines = std.mem.splitScalar(u8, whole, '\n'); + var first = true; + while (lines.next()) |line| { + if (!first) try out.append(arena, '\n'); + first = false; + var rest = line; + while (panes.File.displayWidth(rest) > w) { + var cut = modal.graphemeStart(rest, panes.File.rawAtDisplay(rest, w)); + if (cut == 0) cut = modal.nextGrapheme(rest, 0); + try out.appendSlice(arena, rest[0..cut]); + try soft.append(arena, out.items.len); + try out.append(arena, '\n'); + rest = rest[cut..]; + } + try out.appendSlice(arena, rest); + } + return .{ .text = out.items, .soft = soft.items }; +} + +/// The tag's width for wrapping: its columns, one kept for the caret past +/// a full row's end. +pub fn tagWidth(p: *const Pardes, id: usize) usize { + return @max(1, @as(usize, p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET)) -| 1); +} + +pub fn shownTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) !Shown { + const whole = try tagText(p, arena, pane); + const id = p.paneIdOf(pane) orelse return .{ .text = whole, .soft = &.{} }; + return wrapTag(arena, whole, tagWidth(p, id)); +} + +/// Where display column `col` of the tag's shown row `row` puts its +/// cursor: the tag as shown, prefix and all, wrapped rows included. pub fn tagOffsetAt(p: *Pardes, pane: *Pane, row: usize, col: usize) ?modal.Cursor { + const arena = p.scratch.allocator(); + const shown = shownTag(p, arena, pane) catch return null; + const whole = tagText(p, arena, pane) catch return null; const at = row + pane.tag_top; - const line = modal.lineSlice(tagText(p, p.scratch.allocator(), pane) catch return null, at); - return .{ .row = at, .col = modal.graphemeStart(line, panes.File.rawAtDisplay(line, col)) }; + const line = modal.lineSlice(shown.text, at); + const off = modal.lineStartOffset(shown.text, at) + modal.graphemeStart(line, panes.File.rawAtDisplay(line, col)); + return modal.positionAt(whole, @min(shown.toReal(off), whole.len)); } /// Whether display column `col` of the tag's shown row `row` is on a name @@ -454,3 +523,51 @@ pub fn headerSelection(t: *const Text, text: []const u8, row: usize) ?struct { l if (hi <= start or lo >= end) return null; return .{ .lo = @max(lo, start) - start, .hi = @min(hi, end) - start }; } + +/// Column and workspace tags are one line, as acme's: a newline typed, +/// pasted or restored from a dump into one becomes a space, the caret and +/// the selection kept on the same characters. +pub fn flattenHeaders(p: *Pardes) void { + for (p.col_tags[0..p.ncol]) |*t| flatten(t); + flatten(&p.global_tag); +} + +fn flatten(t: *Text) void { + const own = t.own orelse return; + if (std.mem.indexOfScalar(u8, own, '\n') == null) return; + const cur = modal.offsetAt(own, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) }); + const anchor = modal.offsetAt(own, .{ .row = @intCast(@max(0, t.vsel.row)), .col = @intCast(@max(0, t.vsel.col)) }); + std.mem.replaceScalar(u8, own, '\n', ' '); + t.cur_row = 0; + t.cur_col = @intCast(cur); + t.vsel.row = 0; + t.vsel.col = @intCast(anchor); + t.nsel = 0; +} + +test "a column or workspace tag is one line: a newline in it is a space" { + const p = try Pardes.init(std.testing.allocator, .{ .cols = 50, .rows = 12, .tty_only = true }); + defer p.deinit(); + p.col_tags[0].own = try p.gpa.dupe(u8, "New\nSnarf"); + p.col_tags[0].cur_row = 1; + p.col_tags[0].cur_col = 2; + p.global_tag.own = try p.gpa.dupe(u8, "Newcol\nExit"); + p.sync(); + try std.testing.expectEqualStrings("New Snarf", columnTag(p, 0)); + try std.testing.expectEqualStrings("Newcol Exit", p.global_tag.own.?); + try std.testing.expectEqual(@as(i32, 0), p.col_tags[0].cur_row); + try std.testing.expectEqual(@as(i32, 6), p.col_tags[0].cur_col); + try std.testing.expectEqual(pardes.COLBAR_H, p.columnBarHeight()); + try std.testing.expectEqual(pardes.TOPBAR_H, p.topBarHeight()); +} + +test "a pane tag wraps a long line onto rows, and offsets map both ways" { + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const shown = try wrapTag(arena_state.allocator(), "/a/b.txt Del Snarf Undo Put\nMore", 10); + try std.testing.expectEqualStrings("/a/b.txt D\nel Snarf U\nndo Put\nMore", shown.text); + try std.testing.expectEqual(@as(usize, 4), shown.rows()); + try std.testing.expectEqual(@as(usize, 10), shown.toReal(11)); // the 'e' of Del + try std.testing.expectEqual(@as(usize, 11), shown.fromReal(10)); +} + diff --git a/src/themes/acme.zig b/src/themes/acme.zig index 693055d6..621ffa23 100644 --- a/src/themes/acme.zig +++ b/src/themes/acme.zig @@ -6,20 +6,28 @@ //! in) is a mid grey, and 15 (bright white) is BLACK, because a program asking //! for the brightest color on a light page means "make this stand out". //! -//! The selection is acme's own: plan9's DDarkyellow #eeee9e, the colour acme -//! sweeps text with, and black ink over it — acme changes the paper under a -//! selection and never the letters. Out of the same draw.h palette the -//! scrollbar's #99994c (DYellowgreen) is already taken from. +//! Every colour below that acme has is acme's own, from plan9port's acme.c +//! iconinit and draw.h: the tag #eaffff and body #ffffea (each 25% of +//! DPalebluegreen or DPaleyellow over white), their selections #9eeeee and +//! #eeee9e with black ink (acme changes the paper under a selection, never +//! the letters), the exec and look sweeps #aa0000 and #006600 with white ink, +//! the scrollbar's #99994c track under a #ffffea thumb, 12px wide, the +//! #8888cc (DPurpleblue) line between a tag and its body and the column +//! button, and 2px black rules between columns, windows and tags. +//! +//! acme has no focus colour; pardes needs one, so the focused pane's button +//! is filled #8888cc (the column button's look) and every other pane's is +//! acme's own: #eaffff in a 2px #8888cc ring, #000099 inside it when dirty. pub const theme = .{ .name = "acme", .bg = .{ 0xff, 0xff, 0xea }, .fg = .{ 0x00, 0x00, 0x00 }, - .sel_bg = .{ 0xfa, 0xfa, 0x92 }, + .sel_bg = .{ 0xee, 0xee, 0x9e }, .sel_fg = .{ 0x00, 0x00, 0x00 }, .tag_bg = .{ 0xea, 0xff, 0xff }, .tag_fg = .{ 0x00, 0x00, 0x00 }, - .box = .{ 0x66, 0x3f, 0xe0 }, - .box_dim = .{ 0x35, 0x3a, 0x58 }, + .box = .{ 0x88, 0x88, 0xcc }, + .box_dim = .{ 0xea, 0xff, 0xff }, .kw = .{ 0x97, 0x0d, 0xae }, .str = .{ 0x00, 0x77, 0x33 }, .num = .{ 0xa0, 0x52, 0x00 }, @@ -29,6 +37,17 @@ pub const theme = .{ .scroll_thumb = .{ 0xff, 0xff, 0xea }, // acme paints a column with no window white (cols.c:187). .empty_col = .{ 0xff, 0xff, 0xff }, + .border = .{ 0x00, 0x00, 0x00 }, + .tag_rule = .{ 0x88, 0x88, 0xcc }, + .rule_px = 2, + .box_border = .{ 0x88, 0x88, 0xcc }, + .box_dirty = .{ 0x00, 0x00, 0x99 }, + .rail_px = 12, + .tag_sel_bg = .{ 0x9e, 0xee, 0xee }, + .sweep_bg = .{ .{ 0xee, 0xee, 0x9e }, .{ 0xaa, 0x00, 0x00 }, .{ 0x00, 0x66, 0x00 } }, + .sweep_fg = .{ .{ 0x00, 0x00, 0x00 }, .{ 0xff, 0xff, 0xff }, .{ 0xff, 0xff, 0xff } }, + .column_box = .{ 0x88, 0x88, 0xcc }, + .column_box_dim = .{ 0x88, 0x88, 0xcc }, .palette = .{ .{ 0x00, 0x00, 0x00 }, .{ 0xaa, 0x22, 0x22 }, .{ 0x22, 0x80, 0x22 }, .{ 0x88, 0x66, 0x00 }, .{ 0x22, 0x44, 0xbb }, .{ 0x99, 0x33, 0x99 }, .{ 0x11, 0x88, 0x88 }, .{ 0x55, 0x55, 0x55 }, diff --git a/src/tty/tty.zig b/src/tty/tty.zig index d17316e8..96aec9a9 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -1050,12 +1050,13 @@ const Shell = struct { return true; } _ = libc.close(pt.file.handle); - // Unwatched, a command's exit is read here, as its end. + // Unwatched, a command's exit is read here, as its end, + // and a shell's, for a run waiting on it and the log. const command = if (core.panes[e.id]) |pn| pn.command != null else false; - const status = if (command) host_io.exitStatus(pt.pid, 100) else null; + const status = host_io.exitStatus(pt.pid, 100); if (status == null) host_io.retireShell(pt.pid); s.ptys[e.id] = null; - if (command) core.update(.{ .exited = .{ .pane = @intCast(e.id), .status = status } }); + if (command or status != null) core.update(.{ .exited = .{ .pane = @intCast(e.id), .status = status } }); } core.update(.{ .eof = .{ .pane = @intCast(e.id) } }); }, @@ -1272,7 +1273,7 @@ const Shell = struct { const s = of(ctx); // A command whose exit is recorded is reaped: its group may be // another's by now. - if (s.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig); + if (s.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig, pt.cmd.watched); } /// The pane is gone: hang its pty up, which the kernel passes on to the @@ -662,7 +662,10 @@ def test(binary, quic=False): raise AssertionError('Restore left an old body fid usable') with Client(address) as restored: # The new log names its panes, then where they came from. - assert restored.read('/log').endswith(b'\nrestore ' + str(saved).encode() + b'\n'), restored.read('/log') + log = restored.read('/log') + assert b'\nrestore ' + str(saved).encode() + b'\n' in log, log + # then which serial each pane had: restored <old> <new> + assert log.endswith(b'\n'.join(b'restored %d %d' % (old, new) for old, new in zip(sorted(old_ids), sorted(int(r.split()[0]) for r in restored.read('/index').splitlines()))) + b'\n'), log restored_ids = [int(row.split()[0]) for row in restored.read('/index').splitlines()] assert len(restored_ids) == 2 and old_ids.isdisjoint(restored_ids), restored_ids file_id, control_id = restored_ids diff --git a/test/gui-goldens.txt b/test/gui-goldens.txt index 9a8baf9c..586c8985 100644 --- a/test/gui-goldens.txt +++ b/test/gui-goldens.txt @@ -1,17 +1,17 @@ -01-boot d4e3eb9dba35133df10d46b1d27ab7a90fa9bdefb64bcc304add221326ffdfc9 -02-second-document 235cdf75f3ce1d5800dbbdd7dcfde066af50cde01231a35d00c66c4310213a05 -03-notice 4d59205fe6b89a4589e94c68521257f21b40917c0198ea20248925687663d9f5 -04-context-rows-mid-thumb 7129ee318975f858c430b1e610b26ca756d5bb37553338c0dc6caab2290de2a4 -05-body-selection 39103f3747aae07a12b5151e7e377430b88b96e504670ac2f2d0272197eb827b -06-tag-focus-selection 6a7f50e292f4ede92df24a817218818b93a01fd15fcf39467cf06c3772077834 -07-prompt-notice 993a1c002ca8c59764566e0c27d3fee9106e6e54922ab08f6e68f6af8f0fc865 -08-wide-glyph-edges f5e12e1cdefcac0270f763a2a11a12479c6b7e30f3463828c695220bd8f60b78 -09-collapsed-focus-tint ae66076fdfb0cab9a906b052df257deee5ba91a6274712d52a76d8a7adc5b415 -10-tagbottom dd8ba0d3ecabf3420d000b0c9591a57278179cbd2a9060f849d283f3881d1847 -12-multiline-tag 0edffadbabef48e5a7422ef8433ccf2552b9c812a352e30b32bb000aacad607d -13-image e637c14afcbfe4e42ab19e7c7ea5ff2ce754e8404c12e8b1332fcc1f08fd1a09 -14-theme-ink 4785b07b95f27ae5180f6ec7665866edd0c4083d786ab6a9eaa70029dad23572 -15-theme-acme-light cdd8f24e67ed9940c4af3eaa5d54eb71c4fb0efb8fe48ce12368b72bbca90003 -16-debug f6b3b93bc6ca178f1762b7f77857586861e105c51eb6202d519f7605fed523d8 -17-terminal ea0d877ba228515f0ab7c10097486bf09318762c6e475c481fdbf478939d3a1b -18-mid-transition 9c84c5e27a7ecb063919aaef7a8d8a477e6b5b41909938f233b6cba859f8affc +01-boot 39c940960ccd264a0ed2e9dd99a2c352a47ca64c67d42e3313ae344713c712ae +02-second-document 9a810fe372fa37bc3d338d7f0bd04b5224957cb57ef63952c8fc02d1e72e705a +03-notice b52ebd3860694d35a43e56d149d180249b2609d4a3fc4374347d8cd3b5ebc05c +04-context-rows-mid-thumb e7b9f5d6da06143fff79b9f2686c29050556173f97c759a4119a201542f993ef +05-body-selection 06b7cbcf368bb4371873b26f70286fb2b5ada4293b758fa73458157a248c7aa6 +06-tag-focus-selection a781c95cebb8c47a5c5168e1b7f9747f44a0aad043cc4eec1338bb1f08d0a340 +07-prompt-notice 03bdefcd99ccbf4849d2c2f6b561cee6c24263a1be70d55ecb9550a815506e0e +08-wide-glyph-edges 9c7449be42c46ebbdbdcf156fbfbbe23d6e451720afde966659b1bd96b6886bc +09-collapsed-focus-tint 807bd7621d54948b84cf2965ef463d3928d050259e8ee2d27472b1d3d378f91e +10-tagbottom 03086852528d9c8a76f5926a7e7ee371200da617db279f42cbbb6825b5f41fce +12-multiline-tag cd12edf85d9f472fc79aac91669f5d37619e0cc0ceedd1fe22d100faa127b685 +13-image f9412d7bbe0ebdb53bc62145c51ad3028d2f07cc3e9d2e52a3aeb89de7db6da3 +14-theme-ink da6369bd141a2d71aabb91854e82da88026926e0c931d89947645c090896b5d3 +15-theme-acme-light ca5252f5117ac8d6c679cef917c9f316c9ca311fc7a8dcbac2796b4fc04d3b64 +16-debug 591bb7c45b4b9c46cd2458330b4fa3962afc6dac827fa6f5e20e927a72c586e6 +17-terminal 38299427891e31de0b5597159501c161474620eb26fd7708a6396f0fd685209e +18-mid-transition c914231ea8c32446a0975cfbe338cb3b248a86f3d58c6bc56af6c3e800906302 diff --git a/test/monkey.zig b/test/monkey.zig new file mode 100644 index 00000000..83869ce4 --- /dev/null +++ b/test/monkey.zig @@ -0,0 +1,244 @@ +// pardes-monkey: random snapshot scripts against the tty, hunting panics. +// +// zig build monkey -Dplatform=tty -- <seeds> [steps] [--from=N] [--out=DIR] [--keep] +// +// Each seed makes one script (a fixed seed makes the same script, so a hit +// replays): keys and leader bursts, typing, mouse presses, drags and wheel, +// tiny resizes, prompts and long prompt text, over files of bad UTF-8 and +// wide glyphs. pardes-snap runs it with --update into DIR (default +// /tmp/pardes-monkey), and a panic is a record in the app's crash file +// (src/crash.zig, in the script's fake home: found even when the crash cut +// the script short) or a `.zig:N:N: 0x` stack frame in the report or the +// grid it captured (a trace renders INTO the screen). A hit +// keeps DIR/seed-N.snap, its captures and its report; the rest are deleted. +// A panic's trace can scroll off a small grid: replay the kept script, or +// run the app in a larger pty. Every hit gets a fix and a pinned regression +// script in test/snapshots/. +// +// Not a test gate: it is slow, random over seeds and aimed at finding, not +// holding. Shell text typed into a pane only ever runs in the throwaway +// script directory, and its alphabet has no path characters. +const std = @import("std"); +const libc = std.c; + +const Script = struct { + out: std.ArrayListUnmanaged(u8) = .empty, + gpa: std.mem.Allocator, + rows: u16 = 24, + cols: u16 = 80, + + fn line(s: *Script, comptime fmt: []const u8, args: anytype) !void { + try s.out.print(s.gpa, fmt ++ "\n", args); + } +}; + +// Workspace-tag words: clicked with the middle button they run, and the +// harness finds their column on row 1 while the window is wide enough. +const commands = [_][]const u8{ "Newcol", "Find", "Grep", "Joincol", "Help", "Dump", "NextColor", "Debug" }; +const leader_keys = "abcdefghijklmnopqrstuvwxyz0123456789$^"; +const typed = "abcdefghijklmnopqrstuvwxyz0123456789 -_=+,:;()"; + +fn generate(gpa: std.mem.Allocator, seed: u64, steps: usize) ![]u8 { + var prng = std.Random.DefaultPrng.init(seed); + const random = prng.random(); + var s: Script = .{ .gpa = gpa }; + try s.line("# pardes-monkey seed {d}, {d} steps", .{ seed, steps }); + try s.line("file bad.txt hello \\xff\\xfe world\\nlatin-1 caf\\xe9 tail\\ntruncated \\xe6\\x97 here\\nend\\n", .{}); + try s.line("file wide.txt {s}tail\\nsecond line\\n", .{"\\xe6\\x97\\xa5\\xe6\\x9c\\xac\\xe8\\xaa\\x9e " ** 8}); + try s.line("lines long.txt 400 some words after the number, long enough to wrap in a narrow pane", .{}); + try s.line("start 24 80 bad.txt", .{}); + try s.line("wait 8000 Newcol Joincol", .{}); + try s.line("stable 300 5000", .{}); + for (0..steps) |step| { + const wide = s.cols >= 70 and s.rows >= 6; + switch (random.uintLessThan(u8, 12)) { + // A workspace word, run with a middle click: new columns make + // narrow panes, Find and Grep open prompts. + 0, 1 => if (wide) { + const word = commands[random.uintLessThan(usize, commands.len)]; + try s.line("press middle @{s} 1", .{word}); + try s.line("release middle @{s} 1", .{word}); + }, + // Typing: into a prompt when one is up, far past its band. + 2, 3 => { + var text: [200]u8 = undefined; + const n = random.intRangeAtMost(usize, 1, if (random.uintLessThan(u8, 4) == 0) text.len else 12); + for (text[0..n]) |*c| c.* = typed[random.uintLessThan(usize, typed.len)]; + try s.line("text {s}", .{text[0..n]}); + }, + 4 => try s.line("key {s}", .{switch (random.uintLessThan(u8, 10)) { + 0 => "enter", + 1 => "tab", + 2 => "bs", + 3 => "up", + 4 => "down", + 5 => "left", + 6 => "right", + 7 => "pgdn", + 8 => "home", + else => "end", + }}), + 5 => { + try s.line("key esc", .{}); + try s.line("settle 120", .{}); + }, + // A leader burst: space and a chord's keys. + 6 => { + var chord: [3][2]u8 = undefined; + const n = random.intRangeAtMost(usize, 1, 3); + for (chord[0..n]) |*c| c.* = .{ leader_keys[random.uintLessThan(usize, leader_keys.len)], 0 }; + try s.out.appendSlice(gpa, "key space"); + for (chord[0..n]) |c| try s.out.print(gpa, " {c}", .{c[0]}); + try s.out.append(gpa, '\n'); + }, + 7, 8 => { + const buttons = [_][]const u8{ "left", "middle", "right" }; + const button = buttons[random.uintLessThan(usize, buttons.len)]; + const col = random.intRangeAtMost(u16, 1, s.cols); + const row = random.intRangeAtMost(u16, 1, s.rows); + try s.line("press {s} {d} {d}", .{ button, col, row }); + if (random.boolean()) try s.line("drag {s} {d} {d}", .{ button, random.intRangeAtMost(u16, 1, s.cols), random.intRangeAtMost(u16, 1, s.rows) }); + try s.line("release {s} {d} {d}", .{ button, random.intRangeAtMost(u16, 1, s.cols), random.intRangeAtMost(u16, 1, s.rows) }); + }, + 9 => try s.line("wheel {s} {d} {d}", .{ if (random.boolean()) "up" else "down", random.intRangeAtMost(u16, 1, s.cols), random.intRangeAtMost(u16, 1, s.rows) }), + 10 => try s.line("motion {d} {d}", .{ random.intRangeAtMost(u16, 1, s.cols), random.intRangeAtMost(u16, 1, s.rows) }), + // Tiny windows as often as ordinary ones. + else => { + s.rows = if (random.boolean()) random.intRangeAtMost(u16, 1, 6) else random.intRangeAtMost(u16, 7, 40); + s.cols = if (random.boolean()) random.intRangeAtMost(u16, 1, 20) else random.intRangeAtMost(u16, 21, 140); + try s.line("resize {d} {d}", .{ s.rows, s.cols }); + }, + } + try s.line("settle 30", .{}); + if (step % 10 == 9) try s.line("snap s{d}", .{step}); + } + try s.line("stable 300 5000", .{}); + try s.line("snap end", .{}); + return s.out.toOwnedSlice(gpa); +} + +/// A stack frame of a Zig panic trace, `.zig:12:34: 0x`, of the app's: the +/// harness's own error trace (a script it could not finish) is not a hit. +fn hasTrace(bytes: []const u8) bool { + var lines = std.mem.splitScalar(u8, bytes, '\n'); + while (lines.next()) |line| { + if (std.mem.indexOf(u8, line, "test/snapshot.zig") != null or std.mem.indexOf(u8, line, "test/e2e_harness.zig") != null) continue; + if (frameIn(line)) return true; + } + return false; +} + +fn frameIn(bytes: []const u8) bool { + var from: usize = 0; + while (std.mem.indexOfPos(u8, bytes, from, ".zig:")) |at| : (from = at + 1) { + var i = at + ".zig:".len; + var digits: usize = 0; + while (i < bytes.len and std.ascii.isDigit(bytes[i])) : (i += 1) digits += 1; + if (digits == 0 or i >= bytes.len or bytes[i] != ':') continue; + i += 1; + digits = 0; + while (i < bytes.len and std.ascii.isDigit(bytes[i])) : (i += 1) digits += 1; + if (digits > 0 and std.mem.startsWith(u8, bytes[i..], ": 0x")) return true; + } + return false; +} + +test "a panic trace is found, and ordinary colons are not" { + try std.testing.expect(hasTrace(" /src/draw.zig:814:31: 0x4212821 in joinNotice")); + try std.testing.expect(!hasTrace("draw.zig:814 is where it was; 12:30: 0x")); + try std.testing.expect(!hasTrace("/x/test/snapshot.zig:197:28: 0x1279761 in main (snapshot.zig)")); +} + +test "a seed makes the same script every time" { + const a = try generate(std.testing.allocator, 42, 50); + defer std.testing.allocator.free(a); + const b = try generate(std.testing.allocator, 42, 50); + defer std.testing.allocator.free(b); + try std.testing.expectEqualStrings(a, b); +} + +fn readAll(gpa: std.mem.Allocator, io: std.Io, path: []const u8) []u8 { + return std.Io.Dir.cwd().readFileAlloc(io, path, gpa, .limited(64 << 20)) catch &.{}; +} + +pub fn main(init: std.process.Init) !void { + const gpa = std.heap.page_allocator; + const io = init.io; + const args = try init.minimal.args.toSlice(init.arena.allocator()); + if (args.len < 4) { + std.debug.print("usage: pardes-monkey <pardes-snap> <pardes> <seeds> [steps] [--from=N] [--out=DIR]\n", .{}); + std.process.exit(2); + } + const snap = args[1]; + const app = args[2]; + var seeds: u64 = 20; + var steps: usize = 70; + var from: u64 = 1; + var out: []const u8 = "/tmp/pardes-monkey"; + // Keep every script and capture, not only a hit's: to see what it did. + var keep = false; + var positional: usize = 0; + for (args[3..]) |a| { + if (std.mem.startsWith(u8, a, "--from=")) { + from = try std.fmt.parseInt(u64, a["--from=".len..], 10); + } else if (std.mem.eql(u8, a, "--keep")) { + keep = true; + } else if (std.mem.startsWith(u8, a, "--out=")) { + out = a["--out=".len..]; + } else { + switch (positional) { + 0 => seeds = try std.fmt.parseInt(u64, a, 10), + 1 => steps = try std.fmt.parseInt(usize, a, 10), + else => {}, + } + positional += 1; + } + } + std.Io.Dir.cwd().createDirPath(io, out) catch {}; + var hits: usize = 0; + var seed = from; + while (seed < from + seeds) : (seed += 1) { + const script = try generate(gpa, seed, steps); + defer gpa.free(script); + var path_buf: [4096]u8 = undefined; + const path = try std.fmt.bufPrint(&path_buf, "{s}/seed-{d}.snap", .{ out, seed }); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = path, .data = script }); + const run = std.process.run(gpa, io, .{ + .argv = &.{ snap, app, "--update", "--no-retry", "--jobs=1", path }, + .stdout_limit = .limited(64 << 20), + .stderr_limit = .limited(64 << 20), + }) catch |err| { + std.debug.print("seed {d}: pardes-snap did not run ({t})\n", .{ seed, err }); + continue; + }; + defer gpa.free(run.stdout); + defer gpa.free(run.stderr); + var golden_buf: [4096]u8 = undefined; + const golden_path = try std.fmt.bufPrint(&golden_buf, "{s}/seed-{d}.golden", .{ out, seed }); + const golden = readAll(gpa, io, golden_path); + defer gpa.free(golden); + // The app's own crash file (src/crash.zig) in the script's fake home: + // found even when the crash cut the script short of a capture. + var crash_buf: [4096]u8 = undefined; + const crash_path = try std.fmt.bufPrint(&crash_buf, "/tmp/pardes-snap/seed-{d}/home/.config/pardes/crashes", .{seed}); + const crashes = readAll(gpa, io, crash_path); + defer gpa.free(crashes); + if (crashes.len != 0 or hasTrace(run.stdout) or hasTrace(run.stderr) or hasTrace(golden)) { + hits += 1; + var log_buf: [4096]u8 = undefined; + const log_path = try std.fmt.bufPrint(&log_buf, "{s}/seed-{d}.log", .{ out, seed }); + const both = try std.mem.concat(gpa, u8, &.{ crashes, run.stdout, run.stderr }); + defer gpa.free(both); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = log_path, .data = both }); + std.debug.print("PANIC seed {d}: {s}\n", .{ seed, path }); + } else if (!keep) { + std.Io.Dir.cwd().deleteFile(io, path) catch {}; + std.Io.Dir.cwd().deleteFile(io, golden_path) catch {}; + std.debug.print("ok seed {d}\n", .{seed}); + } else { + std.debug.print("ok seed {d} (kept)\n", .{seed}); + } + } + std.debug.print("{d} of {d} seeds panicked; kept in {s}\n", .{ hits, seeds, out }); + if (hits != 0) std.process.exit(1); +} diff --git a/test/panes.zig b/test/panes.zig index 9802432e..f00b35c4 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -86,6 +86,8 @@ const TagLayerTests = struct { const source_y = @as(f32, @floatFromInt(layer.viewport.y)) + 0.5; const painted_x = shown.x + (x / 10 - track.to.x) / track.to.w * shown.w; const painted_y = shown.y + (source_y - track.to.y) / track.to.h * shown.h; + // Each is a first click, whatever cell the one before hit. + p.last_click = null; for ([_]pardes.Mouse.Kind{ .press, .release }) |kind| { var event = mouse(layer, x, kind); // Hosts inverse the exact painted pixel into the tag envelope; @@ -162,6 +164,23 @@ const TagLayerTests = struct { } }; + +// Rows relative to where the pane's body and tag are, whatever height its +// tag has (a tag wraps onto more rows when it is long). +fn paneAt(p: *Pardes, r: anytype) *pardes.Pane { + for (p.panes, 0..) |slot, id| if (slot) |pane| if (std.meta.eql(p.rects[id], r)) return pane; + unreachable; +} +fn bodyY(p: *Pardes, r: anytype) u16 { + return p.bodyTop(paneAt(p, r), r); +} +fn bodyH(p: *Pardes, r: anytype) u16 { + return r.h -| paneAt(p, r).tag_rows; +} +fn tagY(p: *Pardes, r: anytype) u16 { + return p.tagTop(paneAt(p, r), r); +} + test "a tag with a newline in it is as tall as its lines, and each line is typed into and pointed at" { for ([_]bool{ false, true }) |bottom| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 14 }); @@ -266,22 +285,14 @@ test "a tag with more lines than rows scrolls to keep its caret in view" { try std.testing.expectEqual(tag_y, top.cursor.?.y); try std.testing.expectEqualStrings("/", top.at(x, tag_y).grapheme()); - // A column tag does the same in its rows. + // A column tag is one row, as acme's: its lines are joined. many.clearRetainingCapacity(); try many.appendSlice(std.testing.allocator, "New"); for (1..21) |n| try many.print(std.testing.allocator, "\nC{d}", .{n}); p.col_tags[0].own = try p.gpa.dupe(u8, many.items); p.update(.tick); - const rows = p.columnBarHeight(); - try std.testing.expectEqual(@as(u16, pardes.MAX_TAG_ROWS), rows); - pardes.tagline.enterHeader(p, 0); - p.update(.{ .key = .{ .cp = 'g' } }); - p.update(.{ .key = .{ .cp = 'e' } }); - const header = try p.render(frame.allocator()); - const header_row = p.topBarHeight() + rows - 1; - try std.testing.expectEqual(header_row, header.cursor.?.y); - try std.testing.expectEqualStrings("C", header.at(p.col_x[0] + pardes.TAG_TEXT_INSET, header_row).grapheme()); - try std.testing.expectEqualStrings("2", header.at(p.col_x[0] + pardes.TAG_TEXT_INSET + 1, header_row).grapheme()); + try std.testing.expectEqual(pardes.COLBAR_H, p.columnBarHeight()); + try std.testing.expect(std.mem.indexOfScalar(u8, p.col_tags[0].own.?, '\n') == null); } const TagNavigationTests = struct { @@ -412,7 +423,7 @@ const TagNavigationTests = struct { defer frame.deinit(); _ = try p.render(frame.allocator()); const rect = p.rects[0]; - const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y; + const y = tagY(p, rect); const prefix: u16 = @intCast(panes.File.displayWidth(try pardes.tagline.tagPrefix(p, first))); ClickCursorTests.click(p, .left, rect.x + pardes.TAG_TEXT_INSET + prefix + 6, y); try std.testing.expectEqual(@as(usize, 0), p.active); @@ -594,7 +605,8 @@ const BodyHitTests = struct { p.update(.{ .mouse = mouse(layer, last - 1, prefix + 11, .press) }); // Release at a different source row without an intermediate drag event. p.update(.{ .mouse = mouse(layer, last, prefix + 14, .release) }); - try std.testing.expectEqualStrings("body\nbody", pardes.test_api.heldSelection(p, 0).?); + // A stream: the last row from its start, indentation and all. + try std.testing.expect(std.mem.endsWith(u8, pardes.test_api.heldSelection(p, 0).?, "// body") and std.mem.startsWith(u8, pardes.test_api.heldSelection(p, 0).?, "body\n")); p.update(.{ .mouse = mouse(layer, last, 10, .press) }); p.update(.{ .mouse = mouse(layer, last, 10, .release) }); try std.testing.expectEqual(expected, pane.body.cur_row); @@ -778,6 +790,8 @@ const TreeContextTests = struct { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 48, .rows = 14 }); defer p.deinit(); const pane = try setZigFile(p, "pub fn @\"界name\"(\n\tvalue: usize,\n) void {\n" ++ " _ = value;\n" ** 12 ++ "}\n"); + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); p.settings.wrap = false; p.settings.colors = false; try std.testing.expect(p.executeBuiltinLine(0, "TreeContext")); @@ -815,6 +829,8 @@ const TreeContextTests = struct { defer p.deinit(); const body = " // 012345678901234567890123456789界tail"; const pane = try setZigFile(p, "pub fn aLongDeclarationNameThatCannotFit() void {\n // before\n" ++ body ++ "\n}\n"); + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); p.settings.wrap = true; try std.testing.expect(p.executeBuiltinLine(0, "TreeContext")); pane.scrollBy(2); @@ -872,7 +888,7 @@ const TtySelectionTests = struct { try noChildInput(p); } - test "tty selection preserves exact rectangle through yank clipboard and mode changes" { + test "tty selection preserves its exact stream through yank clipboard and mode changes" { if (comptime !panes.Terminal.enabled) return error.SkipZigTest; for ([_]bool{ false, true }) |prompt| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 }); @@ -893,7 +909,7 @@ const TtySelectionTests = struct { const x = rect.x + config.GUTTER; const y = p.bodyTop(pane, rect); JumpSelectionTests.drag(p, x + @as(u16, if (prompt) 0 else 1), y, x + 3, y + @as(u16, if (prompt) 0 else 1)); - const expected = if (prompt) "$ ab" else "lph\nrav"; + const expected = if (prompt) "$ ab" else "lpha\nbrav"; try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); try std.testing.expectEqualStrings(expected, p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank); @@ -906,9 +922,13 @@ const TtySelectionTests = struct { try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode); const normal = try p.render(frame.allocator()); if (!prompt) { - const selection_bg: pardes.Color = .{ .rgb = p.theme().sel_bg }; - try std.testing.expect(!std.meta.eql(selection_bg, normal.at(x + 4, y).style.bg)); - try std.testing.expect(!std.meta.eql(selection_bg, normal.at(x, y + 1).style.bg)); + // A stream: the first row to its edge, the last from its + // start; (x + 1, y) is surely in it, whatever its tint. + const selected = normal.at(x + 1, y).style.bg; + try std.testing.expect(std.meta.eql(selected, normal.at(x + 4, y).style.bg)); + try std.testing.expect(std.meta.eql(selected, normal.at(x, y + 1).style.bg)); + try std.testing.expect(!std.meta.eql(selected, normal.at(x + 4, y + 1).style.bg)); + try std.testing.expect(!std.meta.eql(selected, normal.at(x, y).style.bg)); } try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); p.update(.{ .key = .{ .cp = 'y' } }); @@ -953,14 +973,14 @@ const TtySelectionTests = struct { _ = try p.render(frame.allocator()); const source = p.rects[0]; const sx = source.x + config.GUTTER; - const sy = if (p.settings.tag_bottom) source.y else source.y + pardes.BOX_H; + const sy = bodyY(p, source); JumpSelectionTests.drag(p, sx + 1, sy, sx + 3, sy + 1); - try std.testing.expectEqualStrings("lph\nrav", p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank); + try std.testing.expectEqualStrings("lpha\nbrav", p.registers.text(p.gpa, '"') orelse return error.MissingAutomaticTtyYank); try noChildInput(p); const target = p.rects[1]; const tx = target.x + config.GUTTER + 2; - const ty = if (p.settings.tag_bottom) target.y else target.y + pardes.BOX_H; + const ty = bodyY(p, target); p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = tx, .row = ty } }); p.update(.{ .mouse = .{ .button = .right, .kind = .press, .col = tx, .row = ty } }); p.update(.{ .mouse = .{ .button = .right, .kind = .release, .col = tx, .row = ty } }); @@ -975,13 +995,13 @@ const TtySelectionTests = struct { } const expected = try std.fmt.allocPrint(p.gpa, "{s}{s}", .{ if (reports_mouse) "\x1b[<0;3;1M\x1b[<0;3;1m" else "", - if (bracketed) "\x1b[200~lph\nrav\x1b[201~" else "lph\rrav", + if (bracketed) "\x1b[200~lpha\nbrav\x1b[201~" else "lpha\rbrav", }); defer p.gpa.free(expected); try std.testing.expectEqualStrings(expected, written.written()); try std.testing.expectEqual(panes.Text.Mode.tty, p.panes[0].?.body.mode); try std.testing.expectEqual(panes.Text.Mode.tty, p.panes[1].?.body.mode); - try std.testing.expectEqualStrings("lph\nrav", pardes.test_api.heldSelection(p, 0).?); + try std.testing.expectEqualStrings("lpha\nbrav", pardes.test_api.heldSelection(p, 0).?); } } } @@ -1059,6 +1079,107 @@ const ClickCursorTests = struct { p.update(.{ .mouse = .{ .button = button, .kind = .release, .col = x, .row = y } }); } + test "a B1 sweep held past a pane's bottom scrolls the body and keeps its start" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 }); + defer p.deinit(); + var text: std.ArrayList(u8) = .empty; + defer text.deinit(std.testing.allocator); + for (0..60) |i| try text.print(std.testing.allocator, "line{d}\n", .{i}); + const pane = try p.setTestFile(text.items); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const rect = p.rects[0]; + const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); + const y = p.bodyTop(pane, rect); + // The pane reaches the screen's bottom: its last row is the edge. + const below = rect.y + rect.h - 1; + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = x, .row = y + 1 } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = x + 2, .row = below } }); + for (0..20) |_| p.update(.tick); + try std.testing.expect(pane.scroll() >= 20); + _ = try p.render(frame.allocator()); // a frame between the ticks and the release + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = x + 2, .row = below } }); + const got = pardes.edit.currentSelText(p, pane).?; + try std.testing.expect(std.mem.startsWith(u8, got, "line1\nline2\n")); + try std.testing.expect(std.mem.count(u8, got, "\n") > 20); + } + + test "everything a B1 chord does while B1 is down is one undo step" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 }); + defer p.deinit(); + const pane = try p.setTestFile("one two three\n"); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const rect = p.rects[0]; + const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); + const y = p.bodyTop(pane, rect); + const M = struct { + fn ev(pp: *Pardes, b: pardes.Mouse.Button, k: pardes.Mouse.Kind, cx: u16, cy: u16) void { + pp.update(.{ .mouse = .{ .button = b, .kind = k, .col = cx, .row = cy } }); + } + }; + // B1-B2 then B1-B3: cut, then paste back: a copy, one step. + const before = pane.file.?.history.undo_len; + M.ev(p, .left, .press, x + 4, y); + M.ev(p, .left, .drag, x + 6, y); + M.ev(p, .middle, .press, x + 6, y); + M.ev(p, .middle, .release, x + 6, y); + M.ev(p, .right, .press, x + 6, y); + M.ev(p, .right, .release, x + 6, y); + M.ev(p, .left, .release, x + 6, y); + try std.testing.expectEqualStrings("one two three\n", pane.file.?.content); + try std.testing.expectEqualStrings("two", p.registers.text(p.gpa, '"').?); + try std.testing.expect(pane.file.?.history.undo_len <= before + 1); + // A cut alone comes back with one Undo. + M.ev(p, .left, .press, x + 4, y); + M.ev(p, .left, .drag, x + 6, y); + M.ev(p, .middle, .press, x + 6, y); + M.ev(p, .middle, .release, x + 6, y); + M.ev(p, .left, .release, x + 6, y); + try std.testing.expectEqualStrings("one three\n", pane.file.?.content); + try std.testing.expect(p.executeBuiltinLine(0, "Undo")); + try std.testing.expectEqualStrings("one two three\n", pane.file.?.content); + } + + test "a double-click selects a word, a line, a bracketed or quoted text, as acme's" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 }); + defer p.deinit(); + const pane = try p.setTestFile("call foo_bar(a, b)\nsay \"hi there\"\n"); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const rect = p.rects[0]; + const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); + const y = p.bodyTop(pane, rect); + const Want = struct { col: u16, row: u16, text: []const u8 }; + for ([_]Want{ + .{ .col = 6, .row = 0, .text = "foo_bar" }, + .{ .col = 13, .row = 0, .text = "a, b" }, + .{ .col = 0, .row = 0, .text = "call foo_bar(a, b)\n" }, + .{ .col = 5, .row = 1, .text = "hi there" }, + }) |w| { + click(p, .left, x + w.col, y + w.row); + click(p, .left, x + w.col, y + w.row); + try std.testing.expectEqualStrings(w.text, pardes.edit.currentSelText(p, pane).?); + // A third click is a plain click again: the caret, no selection. + click(p, .left, x + w.col, y + w.row); + try std.testing.expect(!pane.body.vsel.active); + } + // In the tag too. + const tag_text = try pardes.tagline.tagText(p, frame.allocator(), pane); + const del: u16 = @intCast(std.mem.indexOf(u8, tag_text, "Del").? + 1); + const tag_x = rect.x + pardes.TAG_TEXT_INSET + del; + click(p, .left, tag_x, p.tagTop(pane, rect)); + click(p, .left, tag_x, p.tagTop(pane, rect)); + try std.testing.expect(pane.tag.vsel.active); + const tag_now = try pardes.tagline.tagText(p, frame.allocator(), pane); + const a = modal.offsetAt(tag_now, .{ .row = @intCast(pane.tag.vsel.row), .col = @intCast(pane.tag.vsel.col) }); + const h = modal.offsetAt(tag_now, .{ .row = @intCast(pane.tag.cur_row), .col = @intCast(pane.tag.cur_col) }); + try std.testing.expectEqualStrings("Del", tag_now[a .. h + 1]); + } + test "click cursor and insertion agree beyond line and file ends" { for ([_]bool{ false, true }) |already_insert| { for ([_]bool{ false, true }) |bottom_tag| { @@ -1079,7 +1200,7 @@ const ClickCursorTests = struct { _ = try p.render(frame.allocator()); const rect = p.rects[0]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); - const y = if (bottom_tag) rect.y else rect.y + pardes.BOX_H; + const y = bodyY(p, rect); click(p, .left, x + 15, y + case.click_row); try std.testing.expectEqual(case.row, pane.body.cur_row); try std.testing.expectEqual(case.col, pane.body.cur_col); @@ -1148,7 +1269,7 @@ const ClickCursorTests = struct { _ = try p.render(frame.allocator()); const rect = p.rects[0]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); - const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const y = bodyY(p, rect); JumpSelectionTests.drag(p, x + 1, y + 4, x + 3, y + 4); try std.testing.expectEqualStrings("lph", pardes.test_api.heldSelection(p, 0).?); for (0..2) |_| p.update(.{ .mouse = .{ .button = .wheel_down, .kind = .press, .col = x + 2, .row = y + 4 } }); @@ -1252,9 +1373,42 @@ const JumpSelectionTests = struct { try std.testing.expectEqual(@as(i32, 0), pane.body.cur_col); } - test "jump selection clears copied rectangles in both history directions" { + test "a B1 sweep over several lines is a stream, as acme's, in a body and a tag" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 }); + defer p.deinit(); + const pane = try p.setTestFile("alpha\nbravo\ncharlie\n"); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const rect = p.rects[0]; + const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); + const y = p.bodyTop(pane, rect); + // From the press to the end of its line, the line between whole, the + // last from its start: no line numbers, no rectangle. + drag(p, x + 2, y, x + 1, y + 2); + try std.testing.expectEqualStrings("pha\nbravo\nch", pardes.test_api.heldSelection(p, 0).?); + // Upward, the same text. + drag(p, x + 1, y + 2, x + 2, y); + try std.testing.expectEqualStrings("pha\nbravo\nch", pardes.test_api.heldSelection(p, 0).?); + // A tag of several rows sweeps the same way. + if (pane.tag.own) |own| p.gpa.free(own); + pane.tag.own = try p.gpa.dupe(u8, "Save Del\nLook Edit"); + p.sync(); + _ = try p.render(frame.allocator()); + p.sync(); + try std.testing.expectEqual(@as(u16, 2), pane.tag_rows); + const sl: panes.Pane.Sel = .{ .state = .done, .r0 = 0, .c0 = 3, .r1 = 1, .c1 = 1 }; + const text = try pardes.edit.paneText(p, pane); + const swept = try pardes.edit.selectionText(p, pane, sl); + const first_line = text[0 .. std.mem.indexOfScalar(u8, text, '\n') orelse text.len]; + const second = std.mem.sliceTo(text[first_line.len + 1 ..], '\n'); + const want = try std.fmt.allocPrint(frame.allocator(), "{s}\n{s}", .{ first_line[3..], second[0..@min(2, second.len)] }); + try std.testing.expectEqualStrings(want, swept); + } + + test "jump selection clears copied sweeps in both history directions" { for ([_]bool{ false, true }) |reverse| - try exercise("alpha\nbravo\n", false, reverse, 1, 3, "lph\nrav"); + try exercise("alpha\nbravo\n", false, reverse, 1, 3, "lpha\nbrav"); } test "jump selection clears wrapped Unicode and tab rectangles" { @@ -1324,22 +1478,29 @@ const TagNameTintTests = struct { var arena = std.heap.ArenaAllocator.init(p.gpa); defer arena.deinit(); const surface = try p.render(arena.allocator()); - const text = try pardes.tagline.tagText(p, arena.allocator(), pane); + // The tag as shown: its rows wrapped, each offset mapped back to the + // tag's own to say whether it is in the name. + const shown = try pardes.tagline.shownTag(p, arena.allocator(), pane); const base = std.mem.lastIndexOfScalar(u8, name, '/') orelse return error.MissingDirectory; const rect = p.rects[id]; - const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y; const left = rect.x + pardes.TAG_TEXT_INSET; var visible_name: usize = 0; - var at: usize = 0; - while (at < text.len) : (at = modal.nextGrapheme(text, at)) { - const start: i32 = @as(i32, @intCast(panes.File.rawDisplayCol(text, at))) - pane.tag_scroll; - const end = start + @as(i32, @intCast(panes.File.displayWidth(text[at..modal.nextGrapheme(text, at)]))); - var col = @max(0, start); - while (col < end and col < rect.w - pardes.TAG_TEXT_INSET) : (col += 1) { - const in_name = at > base and at < name.len; - const fg = if (in_name) name_fg else tag_fg; - try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(left + @as(u16, @intCast(col)), y).style.fg); - if (in_name) visible_name += 1; + for (0..pane.tag_rows) |row| { + const line_start = modal.lineStartOffset(shown.text, row + pane.tag_top); + const line = modal.lineSlice(shown.text, row + pane.tag_top); + const y = tagY(p, rect) + @as(u16, @intCast(row)); + var at: usize = 0; + while (at < line.len) : (at = modal.nextGrapheme(line, at)) { + const real = shown.toReal(line_start + at); + const start: i32 = @intCast(panes.File.rawDisplayCol(line, at)); + const end = start + @as(i32, @intCast(panes.File.displayWidth(line[at..modal.nextGrapheme(line, at)]))); + var col = start; + while (col < end and col < rect.w - pardes.TAG_TEXT_INSET) : (col += 1) { + const in_name = real > base and real < name.len; + const fg = if (in_name) name_fg else tag_fg; + try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(left + @as(u16, @intCast(col)), y).style.fg); + if (in_name) visible_name += 1; + } } } return visible_name; @@ -1365,7 +1526,7 @@ const TagNameTintTests = struct { } } - test "tag filename tint follows scrolled Unicode drafted names and yields to selection" { + test "tag filename tint follows wrapped Unicode drafted names and yields to selection" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 38, .rows = 14 }); defer p.deinit(); const pane = try p.setTestFile("body\n"); @@ -1375,17 +1536,20 @@ const TagNameTintTests = struct { pardes.tagline.draftName(p, pane, 0); redraft(pane, name); pane.input.cur_col = @intCast(std.mem.indexOf(u8, name, "界").?); + p.sync(); // the drafted name's rows try std.testing.expect((try check(p, 0, name)) > 0); - try std.testing.expect(pane.tag_scroll > 0); - // A sweep over the wide glyph wears the selection over the tint. - const col: i32 = @intCast(panes.File.rawDisplayCol(name, @intCast(pane.input.cur_col))); - pane.sel[0] = .{ .state = .done, .c0 = col, .c1 = col + 1, .r0 = 0, .r1 = 0 }; - pane.tag_scroll += 4; // bring both cells of the selected wide glyph into view + try std.testing.expect(pane.tag_rows > 1); // the long name wraps + // A sweep over the wide glyph, on the row it wrapped to, wears the + // selection over the tint. var arena = std.heap.ArenaAllocator.init(p.gpa); defer arena.deinit(); + const shown = try pardes.tagline.shownTag(p, arena.allocator(), pane); + const pos = modal.positionAt(shown.text, shown.fromReal(@intCast(pane.input.cur_col))); + const col: i32 = @intCast(panes.File.rawDisplayCol(modal.lineSlice(shown.text, pos.row), pos.col)); + pane.sel[0] = .{ .state = .done, .c0 = col, .c1 = col + 1, .r0 = @intCast(pos.row), .r1 = @intCast(pos.row) }; const surface = try p.render(arena.allocator()); - const x = p.rects[0].x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(col - pane.tag_scroll)); - const y = if (p.settings.tag_bottom) p.rects[0].y + p.rects[0].h - pardes.BOX_H else p.rects[0].y; + const x = p.rects[0].x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(col)); + const y = tagY(p, p.rects[0]) + @as(u16, @intCast(pos.row)); try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().sel_fg }, surface.at(x, y).style.fg); try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().sel_fg }, surface.at(x + 1, y).style.fg); } @@ -1408,7 +1572,7 @@ const TagNameTintTests = struct { p.settings.focus_tint = active; const surface = try p.render(arena.allocator()); const x = p.rects[0].x + pardes.TAG_TEXT_INSET; - const y = if (p.settings.tag_bottom) p.rects[0].y + p.rects[0].h - pardes.BOX_H else p.rects[0].y; + const y = tagY(p, p.rects[0]); const fg = if (active) active_fg else tag_fg; try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(x + 1, y).style.fg); try std.testing.expectEqual(pardes.Color{ .rgb = fg }, surface.at(x + 5, y).style.fg); @@ -1427,7 +1591,7 @@ fn redraft(pane: *Pane, name: []const u8) void { /// Click the tagline at the end of the path: a name draft, caret there. fn clickPathEnd(p: *Pardes, pane: *Pane) void { const rect = p.rects[p.active]; - const y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y; + const y = tagY(p, rect); const x = rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(pane.file.?.path) - 1)); ClickCursorTests.click(p, .left, x, y); std.debug.assert(pane.prompt == .name); @@ -1569,7 +1733,7 @@ test "paste into pane commands and prompts never inserts into the body" { try std.testing.expect(std.mem.indexOf(u8, pardes.tagline.curTail(pane), " Custom\nCommand") != null); } -test "long pane tags reveal commands and map visible clicks to whole tag addresses" { +test "a long pane tag wraps onto rows, and a click on a wrapped row maps to the whole tag's address" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 50, .rows = 12 }); defer p.deinit(); const pane = try p.setTestFile("body\n"); @@ -1581,13 +1745,15 @@ test "long pane tags reveal commands and map visible clicks to whole tag address var arena = std.heap.ArenaAllocator.init(p.gpa); defer arena.deinit(); const surface = try p.render(arena.allocator()); - try std.testing.expect(pane.tag_scroll > 0); + try std.testing.expect(pane.tag_rows > 1); try std.testing.expect(surface.cursor.?.x < p.rects[0].x + p.rects[0].w); const text = try pardes.tagline.tagText(p, p.gpa, pane); defer p.gpa.free(text); - const offset = std.mem.indexOf(u8, text, "SyntaxBold").?; - const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + panes.File.rawDisplayCol(text, offset) - pane.tag_scroll); - const y = p.rects[0].y; + const shown = try pardes.tagline.shownTag(p, arena.allocator(), pane); + const at = shown.fromReal(std.mem.indexOf(u8, text, "SyntaxBold").?); + const pos = modal.positionAt(shown.text, at); + const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + panes.File.rawDisplayCol(modal.lineSlice(shown.text, pos.row), pos.col)); + const y = p.tagTop(pane, p.rects[0]) + @as(u16, @intCast(pos.row)); const before = p.settings.syntax_bold; p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .press } }); p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .release } }); @@ -1595,6 +1761,32 @@ test "long pane tags reveal commands and map visible clicks to whole tag address try std.testing.expectEqualStrings(text, try pardes.tagline.tagText(p, p.scratch.allocator(), pane)); } +test "a word the tag's wrapping breaks across rows is still one word to a click" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 50, .rows = 12 }); + defer p.deinit(); + const pane = try p.setTestFile("body\n"); + try pane.appendTag(" SyntaxBold"); + var arena = std.heap.ArenaAllocator.init(p.gpa); + defer arena.deinit(); + // Lengthen the path until the wrap splits the word after "Synt". + var pad: usize = 1; + const shown = while (pad < 80) : (pad += 1) { + p.gpa.free(pane.file.?.path); + const path = try std.fmt.allocPrint(arena.allocator(), "/{s}/f.zig", .{("a" ** 80)[0..pad]}); + pane.file.?.path = try p.gpa.dupe(u8, path); + p.sync(); + const s = try pardes.tagline.shownTag(p, arena.allocator(), pane); + if (std.mem.indexOf(u8, s.text, "Synt\n") != null) break s; + } else return error.NoSplit; + const pos = modal.positionAt(shown.text, std.mem.indexOf(u8, shown.text, "Synt").?); + const x: u16 = @intCast(p.rects[0].x + pardes.TAG_TEXT_INSET + pos.col + 1); + const y = p.tagTop(pane, p.rects[0]) + @as(u16, @intCast(pos.row)); + const before = p.settings.syntax_bold; + p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .press } }); + p.update(.{ .mouse = .{ .col = x, .row = y, .button = config.exec_button, .kind = .release } }); + try std.testing.expectEqual(!before, p.settings.syntax_bold); +} + test "terminal overlay recoloring never materializes unrelated history" { const testing = std.testing; const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 32, .rows = 8 }); @@ -1617,7 +1809,7 @@ test "terminal overlay recoloring never materializes unrelated history" { const surface = try p.render(frame.allocator()); try testing.expect(p.shell_rows.pane == null); const rect = p.rects[0]; - const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const body_y = bodyY(p, rect); const cell = surface.at(rect.x + config.GUTTER, body_y); try testing.expectEqualStrings("c", cell.grapheme()); try testing.expectEqual(pardes.Color{ .rgb = p.theme().palette.?[2] }, cell.style.fg); @@ -1665,7 +1857,7 @@ test "terminal output keeps following new rows after scrollback eviction" { const surface = try p.render(frame.allocator()); const rect = p.rects[0]; const x = rect.x + config.GUTTER; - const y = (if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H) + pane.rows - 2; + const y = (bodyY(p, rect)) + pane.rows - 2; for (last, 0..) |byte, col| { const cell = surface.at(x + @as(u16, @intCast(col)), y); try std.testing.expectEqualStrings(&.{byte}, cell.grapheme()); @@ -2318,7 +2510,7 @@ test "unplaced terminal panes keep their allocated grid until placement" { p.update(.tick); try std.testing.expect(p.rects[id].w > 0 and p.rects[id].h > pardes.BOX_H); try std.testing.expectEqual(p.rects[id].w - config.GUTTER, pane.cols); - try std.testing.expectEqual(p.rects[id].h - pardes.BOX_H, pane.rows); + try std.testing.expectEqual(bodyH(p, p.rects[id]), pane.rows); } test "document panes ignore terminal output without allocating terminal state" { @@ -2671,7 +2863,7 @@ const OutputTests = struct { layout.compute(p); const surface = try p.render(frame.allocator()); const rect = p.rects[pane.search_pane.?]; - const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const body_y = bodyY(p, rect); for (file.location_rows, 0..) |row, index| { if (!row.declaration) continue; const line = modal.lineSlice(file.content, index); @@ -2719,7 +2911,7 @@ const OutputTests = struct { _ = try p.render(frame.allocator()); const rect = p.rects[id]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(help) + @as(u16, @intCast(at - line_start + 2)); - const y = (if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H) + @as(u16, @intCast(row)); + const y = (bodyY(p, rect)) + @as(u16, @intCast(row)); p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .press, .col = x, .row = y } }); p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .release, .col = x, .row = y } }); try std.testing.expect(p.active != id); @@ -2789,7 +2981,7 @@ const OutputTests = struct { p.acknowledgePanelPresentation(&.{}); const rect = p.rects[id]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(result); - const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const y = bodyY(p, rect); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = x, .row = y } }); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .drag, .col = x + 2, .row = y + 1 } }); try std.testing.expect(p.drag == .select); @@ -3104,8 +3296,8 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; - const rows: usize = r.h - pardes.BOX_H; + const body_y = bodyY(p, r); + const rows: usize = bodyH(p, r); const cols: usize = r.w -| config.GUTTER; const Snap = struct { text: [][7]u8, len: []u8, style: []pardes.CellStyle }; @@ -3345,7 +3537,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const red: pardes.Color = .{ .index = 1 }; const green: pardes.Color = .{ .index = 2 }; const blue: pardes.Color = .{ .index = 4 }; @@ -3400,7 +3592,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); p.shell_rows.stale = true; const s = try p.render(frame.allocator()); @@ -3448,8 +3640,8 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; - const body_h = r.h - pardes.BOX_H; + const body_y = bodyY(p, r); + const body_h = bodyH(p, r); p.shell_rows.stale = true; const s = try p.render(frame.allocator()); @@ -3487,7 +3679,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const red: pardes.Color = .{ .index = 1 }; const green: pardes.Color = .{ .index = 2 }; const blue: pardes.Color = .{ .index = 4 }; @@ -3524,7 +3716,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const red: pardes.Color = .{ .index = 1 }; // Covers far more rows than the grid was ever dumped for, so the anchor @@ -3606,7 +3798,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); p.shell_rows.stale = true; const s = try p.render(frame.allocator()); @@ -3684,8 +3876,8 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; - const rows: usize = r.h - pardes.BOX_H; + const body_y = bodyY(p, r); + const rows: usize = bodyH(p, r); const cols: usize = r.w -| config.GUTTER; // What tty mode shows with nothing left behind: the reference. @@ -3736,6 +3928,8 @@ const TerminalTests = struct { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); pane.tty_filter = filter; pane.body.mode = mode; p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[32m\u{754C}\x1b[41m\x1b[K\x1b[7G\x1b[44m\x1b[K\r\n" } }); @@ -3744,7 +3938,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); p.shell_rows.stale = true; const s = try p.render(frame.allocator()); @@ -3780,7 +3974,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); p.shell_rows.stale = true; const s = try p.render(frame.allocator()); @@ -3813,10 +4007,10 @@ const TerminalTests = struct { const s = try p.render(frame.allocator()); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); var bad: usize = 0; - for (0..r.h -| pardes.BOX_H) |vr| { + for (0..bodyH(p, r)) |vr| { var buf: [16]u8 = undefined; var n: usize = 0; for (0..10) |c| { @@ -3856,7 +4050,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); p.update(.{ .output = .{ .pane = 0, .bytes = "plain text\r\n" } }); p.shell_rows.stale = true; @@ -3894,7 +4088,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); // The state two ordinary edits reach: one at the bottom, one that split a // line further up. The buffer now spans rows 2..6 and diverges at BOTH @@ -3932,7 +4126,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); // The state three keystrokes reach on any blank shell row: type a character // and delete it, and the buffer holds NO text while still standing in for @@ -3957,6 +4151,8 @@ const TerminalTests = struct { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); pane.tty_filter = false; pane.body.mode = .tty; @@ -3966,7 +4162,7 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const blue: pardes.Color = .{ .index = 4 }; p.shell_rows.stale = true; @@ -4009,12 +4205,12 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const s = try p.render(frame.allocator()); const want = pardes.Color{ .index = 33 }; var seen = false; - for (0..@as(usize, r.h -| pardes.BOX_H)) |vr| { + for (0..@as(usize, bodyH(p, r))) |vr| { var buf: [15]u8 = undefined; for (0..15) |c| { const g = s.at(tx + @as(u16, @intCast(c)), body_y + @as(u16, @intCast(vr))).grapheme(); @@ -4062,11 +4258,11 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const s = try p.render(frame.allocator()); var seen = false; - for (0..@as(usize, r.h -| pardes.BOX_H)) |vr| { + for (0..@as(usize, bodyH(p, r))) |vr| { var buf: [12]u8 = undefined; for (0..12) |c| { const g = s.at(tx + @as(u16, @intCast(c)), body_y + @as(u16, @intCast(vr))).grapheme(); @@ -4116,8 +4312,8 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; - const body_h = r.h -| pardes.BOX_H; + const body_y = bodyY(p, r); + const body_h = bodyH(p, r); var step: usize = 0; while (step < 12) : (step += 1) { @@ -4211,11 +4407,11 @@ const TerminalTests = struct { defer frame.deinit(); const r = p.rects[0]; const tx = r.x + config.GUTTER; - const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = bodyY(p, r); const s = try p.render(frame.allocator()); var seen: usize = 0; - for (0..@as(usize, r.h -| pardes.BOX_H)) |vr| { + for (0..@as(usize, bodyH(p, r))) |vr| { var buf: [12]u8 = undefined; for (0..12) |c| { const g = s.at(tx + @as(u16, @intCast(c)), body_y + @as(u16, @intCast(vr))).grapheme(); @@ -4293,7 +4489,7 @@ test "cancelled normal gestures leave no count or subprefix after mouse and focu switch (gesture) { .body, .tag => { const rect = p.rects[0]; - const tag_y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y; + const tag_y = tagY(p, rect); const body_y = p.bodyTop(pane, rect); const x = if (gesture == .tag) rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(try pardes.tagline.tagPrefix(p, pane)))) + 3 @@ -4423,9 +4619,9 @@ test "growing a file pane colors every newly visible Zig keyword" { _ = frame.reset(.retain_capacity); const surface = try p.render(frame.allocator()); const rect = p.rects[0]; - const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const body_y = bodyY(p, rect); var seen: usize = 0; - for (body_y..body_y + rect.h - pardes.BOX_H) |y| { + for (body_y..body_y + bodyH(p, rect)) |y| { for (rect.x + config.GUTTER..rect.x + rect.w - 1) |x| { const first = surface.at(@intCast(x), @intCast(y)); const second = surface.at(@intCast(x + 1), @intCast(y)); @@ -4466,6 +4662,8 @@ test "Zig keywords keep every rendered byte colored across line-number widths" { const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = view.cols, .rows = 10 }); defer p.deinit(); const pane = try p.setTestFile(source); + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); const file = &pane.file.?; gpa.free(file.path); file.path = try gpa.dupe(u8, "/highlight.zig"); @@ -4482,8 +4680,8 @@ test "Zig keywords keep every rendered byte colored across line-number widths" { var seen: usize = 0; var if_position: ?struct { x: u16, y: u16 } = null; const rect = p.rects[0]; - const body_y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; - for (body_y..body_y + rect.h - pardes.BOX_H) |y| { + const body_y = bodyY(p, rect); + for (body_y..body_y + bodyH(p, rect)) |y| { for (rect.x + config.GUTTER..rect.x + rect.w - 1) |x| { const first = surface.at(@intCast(x), @intCast(y)); const second = surface.at(@intCast(x + 1), @intCast(y)); @@ -4532,7 +4730,7 @@ test "terminal overlays preserve trailing blank styles without coloring inserted defer frame.deinit(); const rect = p.rects[0]; const x = rect.x + config.GUTTER; - const y = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; + const y = bodyY(p, rect); const palette = p.theme().palette.?; const colors = [_]pardes.Color{ .{ .rgb = palette[1] }, .{ .rgb = palette[2] }, .{ .rgb = palette[4] } }; const original = try p.render(frame.allocator()); diff --git a/test/snapshots/badutf.golden b/test/snapshots/badutf.golden index 0980612a..4490531a 100644 --- a/test/snapshots/badutf.golden +++ b/test/snapshots/badutf.golden @@ -7,10 +7,11 @@ | 3 truncated �� here | 4 end | 5 -== snap hscroll-midglyph grid=50x31 cursor=46,3 +== snap hscroll-midglyph grid=50x31 cursor=46,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/badutf/cwd/wide.txt Save Tty C +| /tmp/pardes-snap/badutf/cwd/wide.txt Save Tty +| Collapse Del | 1 日本語 日本語 日本語 日本語 日本語 tail | 2 | 3 diff --git a/test/snapshots/chordcut.golden b/test/snapshots/chordcut.golden index 6e143872..d6ea6879 100644 --- a/test/snapshots/chordcut.golden +++ b/test/snapshots/chordcut.golden @@ -1,11 +1,13 @@ -== snap opened grid=120x31 cursor=7,7 +== snap opened grid=120x31 cursor=7,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C +| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter +| Collapse Del Collapse Del | ls ls | f.txt g.txt f.txt g.txt | -| /tmp/pardes-snap/chordcut/cwd/f.txt Save Tty Collapse Del +| /tmp/pardes-snap/chordcut/cwd/f.txt Save Tty Collapse De +| l | 1 line 1 | 2 line 2 | 3 line 3 @@ -14,77 +16,79 @@ | 6 | | -| -| -| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter C +| /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter +| Collapse Del | ls | f.txt g.txt -== snap cut grid=120x31 cursor=7,8 -|6: /tmp/pardes-snap/chordcut/cwd/f.txt * Save Tty Collapse D -|8: 2 2 -== snap copied grid=120x31 cursor=10,9 -== snap pointpaste grid=120x31 cursor=16,10 -|10: 4 line 4line -== snap replaced grid=120x31 cursor=11,11 -|11: 5 lline 5 -== snap cancelexec grid=120x31 cursor=25,8 -|8: 2 2echo B''OO g.txt -== snap execafter grid=120x31 cursor=25,8 -|7: 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: ls % echo B''OO -|19: f.txt g.txt BOO -|20: exit 0 -== snap cancellook grid=120x31 cursor=25,8 -|7: 1 line 1 -== snap lookafter grid=120x31 cursor=7,22 -|21: /tmp/pardes-snap/chordcut/cwd/g.txt Save Tty Collapse Del -|22: 1 line 1 -|23: 2 line 2 -|24: 3 +== snap cut grid=120x31 cursor=7,10 +|7: * /tmp/pardes-snap/chordcut/cwd/f.txt Save Tty Collapse De +|10: 2 2 +== snap copied grid=120x31 cursor=10,11 +== snap pointpaste grid=120x31 cursor=16,12 +|12: 4 line 4line +== snap replaced grid=120x31 cursor=11,13 +|13: 5 lline 5 +== snap cancelexec grid=120x31 cursor=25,10 +|10: 2 2echo B''OO g.txt +== snap execafter grid=120x31 cursor=25,10 +|9: 1 line 1 Exec +|17: /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/chordcut/cwd (echo B''OO) exit 0 Kill S +|18: Collapse Del ave Collapse Del +|19: ls % echo B''OO +|20: f.txt g.txt BOO +|21: exit 0 +== snap cancellook grid=120x31 cursor=25,10 +|9: 1 line 1 +== snap lookafter grid=120x31 cursor=7,24 +|22: /tmp/pardes-snap/chordcut/cwd/g.txt Save Tty Collapse De +|23: l +|24: 1 line 1 +|25: 2 line 2 +|26: 3 == style chordsel grid=120x31 |0: 0-70 #b2c3b8,#1d2824, 71-119 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-59 d,#26362d, 60-61 #ffffff,#887455, 62-62 d,#1d2824, 63-94 #b2c3b8,#1d2824, 95-119 d,#1d2824, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-40 #d3b0d9,#1d2824, 41-59 #b2c3b8,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-92 #b2c3b8,#1d2824, 93-100 #d3b0d9,#1d2824, 101-119 #b2c3b8,#1d2824, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-40 #d3b0d9,#1d2824, 41-58 #b2c3b8,#1d2824, 59-59 d,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-92 #b2c3b8,#1d2824, 93-100 #d3b0d9,#1d2824, 101-118 #b2c3b8,#1d2824, 119-119 d,#1d2824, +|3: 0-2 d,#1d2824, 3-14 #b2c3b8,#1d2824, 15-62 d,#1d2824, 63-74 #b2c3b8,#1d2824, 75-119 d,#1d2824, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|5: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410, -|6: 0-1 #ffffff,#496656, 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, -|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-24 #e5eee5,#0d1410, 25-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|6: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410, +|7: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-37 #d3b0d9,#1d2824, 38-58 #b2c3b8,#1d2824, 59-59 d,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|8: 0-2 d,#1d2824, 3-3 #b2c3b8,#1d2824, 4-59 d,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|11: 0-0 d,#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, -|12: 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, -|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, +|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-24 #e5eee5,#0d1410, 25-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-16 #e5eee5,#0d1410, 17-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-40 #d3b0d9,#1d2824, 41-59 #b2c3b8,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-119 #b2c3b8,#1d2824, -|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-32 #b2c3b8,#1d2824, 33-40 #d3b0d9,#1d2824, 41-58 #b2c3b8,#1d2824, 59-59 d,#1d2824, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-118 #b2c3b8,#1d2824, 119-119 d,#1d2824, +|18: 0-2 d,#1d2824, 3-14 #b2c3b8,#1d2824, 15-62 d,#1d2824, 63-78 #b2c3b8,#1d2824, 79-119 d,#1d2824, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|20: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|21: 0-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, -|22: 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, -|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 #59675c,#0d1410, 7-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, +|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|21: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|22: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-32 #becfc4,#26362d, 33-37 #dfbce5,#26362d, 38-58 #becfc4,#26362d, 59-59 d,#26362d, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|23: 0-2 d,#26362d, 3-3 #becfc4,#26362d, 4-59 d,#26362d, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-10 #f5f7ee,#3c614a, 11-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |26: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |27: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, == style clickdismiss grid=120x31 -|22: 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, -|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -== snap ttyprobe grid=120x31 cursor=42,22 -|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: $ ls % echo B''OO -|20: $ stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[? exit 0 -|21: 1006h'; cat -v -|22: ^[[<0;38;6M^[[<0;38;6m^[[200~line^[[201~ -|23: +|24: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|25: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-12 #e5eee5,#0d1410, 13-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +== snap ttyprobe grid=120x31 cursor=42,23 +|17: $ /tmp/pardes-snap/chordcut/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/chordcut/cwd (echo B''OO) exit 0 Kill S +|19: $ ls % echo B''OO +|21: $ stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[? exit 0 +|22: 1006h'; cat -v +|23: ^[[<0;38;6M^[[<0;38;6m^[[200~line^[[201~ |24: -== snap ttyprompt grid=120x31 cursor=8,24 -|23: $ -|24: $ line +|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 c1db10fa..36aad675 100644 --- a/test/snapshots/chordcut.snap +++ b/test/snapshots/chordcut.snap @@ -29,42 +29,42 @@ wait 10000 line 3 stable 700 15000 snap opened # 1-2 Cut: left-drag over "line" on line 2, tap middle while holding -press left 8 9 -drag left 11 9 -press middle 11 9 -release middle 11 9 -release left 11 9 +press left 8 11 +drag left 11 11 +press middle 11 11 +release middle 11 11 +release left 11 11 stable 400 5000 snap cut # 1-2 then 1-3 in one hold = copy: line 3 keeps its text, register loaded -press left 8 10 -drag left 11 10 -press middle 11 10 -release middle 11 10 -press right 11 10 -release right 11 10 -release left 11 10 +press left 8 12 +drag left 11 12 +press middle 11 12 +release middle 11 12 +press right 11 12 +release right 11 12 +release left 11 12 stable 400 5000 snap copied # 1-3 at a bare click point: hold left past the end of line 4, tap right — # the register splices in at the click -press left 14 11 -press right 14 11 -release right 14 11 -release left 14 11 +press left 14 13 +press right 14 13 +release right 14 13 +release left 14 13 stable 400 5000 snap pointpaste # 1-3 over a fresh selection replaces it with the register -press left 9 12 -drag left 11 12 -press right 11 12 -release right 11 12 -release left 11 12 +press left 9 14 +drag left 11 14 +press right 11 14 +release right 11 14 +release left 11 14 stable 400 5000 snap replaced # arm the cancel tests: append `echo B''OO g.txt` to line 2 -press left 10 9 -release left 10 9 +press left 10 11 +release left 10 11 stable 400 5000 key i text echo B''OO g.txt @@ -73,72 +73,72 @@ settle 100 stable 400 5000 # 2-3 cancel (acme textselect2): tap right during the middle execute-drag # over `echo B''OO` — the command never runs (no BOO anywhere) -press middle 10 9 -drag middle 19 9 -press right 19 9 -release right 19 9 -release middle 19 9 +press middle 10 11 +drag middle 19 11 +press right 19 11 +release right 19 11 +release middle 19 11 stable 700 8000 snap cancelexec # the same drag released clean proves the word was live: BOO appears -press middle 10 9 -drag middle 19 9 -release middle 19 9 +press middle 10 11 +drag middle 19 11 +release middle 19 11 wait 10000 BOO stable 700 10000 snap execafter # 3-1 cancel (acme textselect3): tap left during the right look-drag over # g.txt — no pane opens -press right 21 9 -drag right 25 9 -press left 25 9 -release left 25 9 -release right 25 9 +press right 21 11 +drag right 25 11 +press left 25 11 +release left 25 11 +release right 25 11 stable 700 8000 snap cancellook # the same look released clean opens g.txt -press right 21 9 -drag right 25 9 -release right 25 9 +press right 21 11 +drag right 25 11 +release right 25 11 wait 10000 line 2 stable 700 10000 snap lookafter # a fresh left click dismisses the chord's leftover selection instead of # dragging the anchored span to the click: copy `line` on g.txt line 1 # (styled snap shows the selection), then click the next row (it's gone) -press left 8 23 -drag left 11 23 -press middle 11 23 -release middle 11 23 -press right 11 23 -release right 11 23 -release left 11 23 +press left 8 25 +drag left 11 25 +press middle 11 25 +release middle 11 25 +press right 11 25 +release right 11 25 +release left 11 25 stable 400 5000 snapstyle chordsel -press left 8 24 -release left 8 24 +press left 8 26 +release left 8 26 stable 400 5000 snapstyle clickdismiss # g.txt opened under the lower shell's text and left it three rows: close it, # and the shell grows back into its rows, for what follows to run in -press middle @Del 22 -release middle @Del 22 +press middle 59 23 +release middle 59 23 stable 700 15000 # tty paste chord: a mouse-listening app gets the click forwarded (SGR, # body-relative) then the register between bracketed-paste markers — # cat -v prints the received bytes visibly -press left 35 22 -release left 35 22 +press left 35 23 +release left 35 23 stable 400 5000 key c-b stable 600 8000 text stty -icanon -echo; printf '\x1b[?2004h\x1b[?1000h\x1b[?1006h'; cat -v key enter stable 700 8000 -press left 40 24 -press right 40 24 -release right 40 24 -release left 40 24 +press left 40 25 +press right 40 25 +release right 40 25 +release left 40 25 stable 700 8000 snap ttyprobe # a plain prompt (mouse modes off) gets no click bytes — just the paste; @@ -148,9 +148,9 @@ stable 600 8000 text stty sane; printf '\x1b[?1000l\x1b[?1006l' key enter stable 600 8000 -press left 40 24 -press right 40 24 -release right 40 24 -release left 40 24 +press left 40 25 +press right 40 25 +release right 40 25 +release left 40 25 stable 700 8000 snap ttyprompt diff --git a/test/snapshots/cmdexit.golden b/test/snapshots/cmdexit.golden index de95c383..c5c314b9 100644 --- a/test/snapshots/cmdexit.golden +++ b/test/snapshots/cmdexit.golden @@ -10,6 +10,6 @@ |19: BGSTARTED |20: exit 0 == snap letgo grid=120x31 cursor=7,3 -|17: /tmp/pardes-snap/cmdexit/cwd (exec </dev/null >/dev/null 2>&1; sleep 1; false) exit 1 Kill Save Collapse Del -|21: % exec </dev/null >/dev/null 2>&1; sleep 1; false -|22: exit 1 +|24: /tmp/pardes-snap/cmdexit/cwd (exec </dev/null >/dev/null 2>&1; sleep 1; false) exit 1 Kill Save Collapse Del +|25: % exec </dev/null >/dev/null 2>&1; sleep 1; false +|26: exit 1 diff --git a/test/snapshots/cmdexit.snap b/test/snapshots/cmdexit.snap index ff60a881..e9f39bfd 100644 --- a/test/snapshots/cmdexit.snap +++ b/test/snapshots/cmdexit.snap @@ -16,8 +16,10 @@ release middle 34 4 wait 10000 exit 0 stable 700 15000 snap background -# line 2, in the same pane now its command is done: its end of file comes at -# once, and its exit a second later, from `false` +# line 2, in a second pane: the first's command is done but its background +# `sleep 30` lives on (job control gives it its own group) and still holds +# that pane's pty; here the end of file comes at once, and the exit a second +# later, from `false` press middle 7 5 drag middle 54 5 release middle 54 5 diff --git a/test/snapshots/colgrip.golden b/test/snapshots/colgrip.golden index dd16074d..95a2390f 100644 --- a/test/snapshots/colgrip.golden +++ b/test/snapshots/colgrip.golden @@ -1,11 +1,11 @@ == style rest grid=100x24 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-49 d,#1d2824, 50-51 #ffffff,#887455, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-89 #dfbce5,#26362d, 90-99 #becfc4,#26362d, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-89 #dfbce5,#26362d, 90-98 #becfc4,#26362d, 99-99 d,#26362d, +|3: 0-2 d,#1d2824, 3-12 #b2c3b8,#1d2824, 13-49 d,#1d2824, 50-52 d,#26362d, 53-73 #becfc4,#26362d, 74-99 d,#26362d, +|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, |7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, @@ -25,10 +25,11 @@ |23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, == style held grid=100x24 |1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-49 d,#1d2824, 50-51 #000000,#f4c086, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d, -== snap resize-drag grid=100x24 cursor=54,5 +== snap resize-drag grid=100x24 cursor=54,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/colgrip/cwd/w.txt S╎ve Tty Col /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode +| /tmp/pardes-snap/colgrip/cwd/w.txt S╎ve Tty Co /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mod +| llapse Del ╎ e Filter Collapse Del | 1 x ╎ ls | ╎ w.txt | ╎ @@ -49,14 +50,13 @@ | ╎ | ╎ | ╎ -| ╎ == style resize-drag grid=100x24 |1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-38 d,#1d2824, 39-39 #f4c086,#1d2824, 40-49 d,#1d2824, 50-51 #000000,#f4c086, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-38 #b2c3b8,#1d2824, 39-39 #f4c086,#1d2824, 40-49 #b2c3b8,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-89 #dfbce5,#26362d, 90-99 #becfc4,#26362d, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-38 #b2c3b8,#1d2824, 39-39 #f4c086,#1d2824, 40-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-89 #dfbce5,#26362d, 90-98 #becfc4,#26362d, 99-99 d,#26362d, +|3: 0-2 d,#1d2824, 3-12 #b2c3b8,#1d2824, 13-38 d,#1d2824, 39-39 #f4c086,#1d2824, 40-49 d,#1d2824, 50-52 d,#26362d, 53-73 #becfc4,#26362d, 74-99 d,#26362d, +|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, |7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, @@ -74,12 +74,12 @@ |21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -== snap resize-done grid=100x24 cursor=44,5 +== snap resize-done grid=100x24 cursor=44,6 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/colgrip/cwd/w.txt Sa /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter Co -|3: 1 x ls -|4: w.txt -|5: +|2: /tmp/pardes-snap/colgrip/cwd/w.txt S /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter C +|3: ave Tty Collapse Del ollapse Del +|4: 1 x ls +|5: w.txt |6: |7: |8: @@ -98,10 +98,9 @@ |21: |22: |23: -== snap reorder-drag grid=100x24 cursor=44,5 -|3: ╎ 1 x ls -|4: ╎ w.txt -|5: ╎ +== snap reorder-drag grid=100x24 cursor=44,6 +|4: ╎ 1 x ls +|5: ╎ w.txt |6: ╎ |7: ╎ |8: ╎ @@ -122,11 +121,11 @@ |23: ╎ == style reorder-drag grid=100x24 |1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-39 d,#1d2824, 40-41 #000000,#f4c086, 42-42 d,#26362d, 43-74 #becfc4,#26362d, 75-99 d,#26362d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-39 #b2c3b8,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-71 #becfc4,#26362d, 72-79 #dfbce5,#26362d, 80-99 #becfc4,#26362d, -|3: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|4: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|5: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-99 d,#0d1410, -|6: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-38 #b2c3b8,#1d2824, 39-39 d,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-71 #becfc4,#26362d, 72-79 #dfbce5,#26362d, 80-98 #becfc4,#26362d, 99-99 d,#26362d, +|3: 0-2 d,#1d2824, 3-22 #b2c3b8,#1d2824, 23-39 d,#1d2824, 40-42 d,#26362d, 43-53 #becfc4,#26362d, 54-99 d,#26362d, +|4: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|5: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|6: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-99 d,#0d1410, |7: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |8: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |9: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, @@ -144,12 +143,12 @@ |21: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |22: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |23: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -== snap reorder-done grid=100x24 cursor=4,5 +== snap reorder-done grid=100x24 cursor=4,6 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter Co /tmp/pardes-snap/colgrip/cwd/w.txt Sa -|3: ls 1 x -|4: w.txt -|5: +|2: /tmp/pardes-snap/colgrip/cwd Tty+bash Save Mode Filter C /tmp/pardes-snap/colgrip/cwd/w.txt S +|3: ollapse Del ave Tty Collapse Del +|4: ls 1 x +|5: w.txt |6: |7: |8: diff --git a/test/snapshots/cornerdrag-left.golden b/test/snapshots/cornerdrag-left.golden index c9ccf859..e0d2ab51 100644 --- a/test/snapshots/cornerdrag-left.golden +++ b/test/snapshots/cornerdrag-left.golden @@ -1,11 +1,13 @@ -== snap right-split grid=100x31 cursor=54,9 +== snap right-split grid=100x31 cursor=54,11 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S +| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash +| Save Mode Filter Collapse Del Save Mode Filter Collapse Del | ls ls | w.txt w.txt | -| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S +| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash +| Save Mode Filter Collapse Del | ls | w.txt | @@ -14,22 +16,21 @@ | | | -| -| -| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S +| /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash +| Save Mode Filter Collapse Del | ls | w.txt -== snap hover-corner grid=100x31 cursor=54,9 +== snap hover-corner grid=100x31 cursor=54,11 |1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S -|3: ls ╎ ls -|4: w.txt ╎ w.txt -|5: ╎ -|6: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S -|7: ╎ ls -|8: ╎ w.txt -|9: ╎ -|10: ╎ +|2: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash +|3: Save Mode Filter Collapse Del ╎ Save Mode Filter Collapse Del +|4: ls ╎ ls +|5: w.txt ╎ w.txt +|6: ╎ +|7: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash +|8: ╎ Save Mode Filter Collapse Del +|9: ╎ ls +|10: ╎ w.txt |11: ╎ |12: ╎ |13: ╎ @@ -37,9 +38,9 @@ |15: ╎ |16: ╎ |17: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash ╎ -|18: ls ╎ -|19: w.txt ╎ -|20: ╎ +|18: Save Mode Filter Collapse Del ╎ +|19: ls ╎ +|20: w.txt ╎ |21: ╎ |22: ╎ |23: ╎ @@ -50,28 +51,28 @@ |28: ╎ |29: ╎ |30: ╎ -== snap hover-left-corner grid=100x31 cursor=54,9 -== snap corner-drag grid=100x31 cursor=54,9 +== snap hover-left-corner grid=100x31 cursor=54,11 +== snap corner-drag grid=100x31 cursor=54,11 |1: New Tty Find Grep Joincol ╎elcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash S /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S -|3: ls ╎ ls -|4: w.txt ╎ w.txt -|5: ╎ -|6: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash S -|7: ╎ ls -|8: ╎ w.txt -|9: ╎ -|10: ╎ +|2: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash +|3: Save Mode Filter Collapse ╎el Save Mode Filter Collapse Del +|4: ls ╎ ls +|5: w.txt ╎ w.txt +|6: ╎ +|7: ╎ /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash +|8: ╎ Save Mode Filter Collapse Del +|9: ╎ ls +|10: ╎ w.txt |11: ╎ -|12: ╎╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ +|12: ╎ |13: ╎ |14: ╎ |15: ╎ |16: ╎ -|17: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash S -|18: ls ╎ -|19: w.txt ╎ -|20: ╎ +|17: /tmp/pardes-snap/cornerdra╎-left/cwd Tty+bash +|18: Save Mode Filter Collapse ╎el +|19: ls ╎ +|20: w.txt ╎ |21: ╎ |22: ╎ |23: ╎ @@ -82,28 +83,28 @@ |28: ╎ |29: ╎ |30: ╎ -== snap corner-done grid=100x31 cursor=34,16 +== snap corner-done grid=100x31 cursor=34,11 |1: New Tty Find Grep Joincol D New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Coll -|3: ls ls -|4: w.txt w.txt -|5: -|6: -|7: -|8: -|9: -|10: +|2: /tmp/pardes-snap/cornerdra /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Col +|3: g-left/cwd Tty+bash Save M lapse Del +|4: ode Filter Collapse Del ls +|5: ls w.txt +|6: w.txt +|7: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Col +|8: lapse Del +|9: ls +|10: w.txt |11: |12: -|13: /tmp/pardes-snap/cornerdrag-left/cwd Tty+bash Save Mode Filter Coll -|14: ls -|15: w.txt +|13: +|14: +|15: |16: -|17: /tmp/pardes-snap/cornerdrag -|18: ls -|19: w.txt -|20: -|21: +|17: /tmp/pardes-snap/cornerdra +|18: g-left/cwd Tty+bash Save M +|19: ode Filter Collapse Del +|20: ls +|21: w.txt |22: |23: |24: diff --git a/test/snapshots/cornerdrag.golden b/test/snapshots/cornerdrag.golden index fc212219..d70e41e4 100644 --- a/test/snapshots/cornerdrag.golden +++ b/test/snapshots/cornerdrag.golden @@ -1,7 +1,8 @@ -== snap hover-corner grid=100x31 cursor=4,5 +== snap hover-corner grid=100x31 cursor=4,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save ╎ /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save M +| /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save ╎ /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save +| Mode Filter Collapse Del ╎ Mode Filter Collapse Del | ls ╎ ls | w.txt ╎ w.txt | ╎ @@ -15,8 +16,8 @@ | ╎ | ╎ | ╎ -| ╎ | /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save ╎ +| Mode Filter Collapse Del ╎ | ls ╎ | w.txt ╎ | ╎ @@ -29,13 +30,12 @@ | ╎ | ╎ | ╎ -| ╎ -== snap edge-drag grid=100x31 cursor=4,5 +== snap edge-drag grid=100x31 cursor=4,6 |1: New Tty Find Grep Joincol ╎elcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save M /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save M -|3: ls ╎ ls -|4: w.txt ╎ w.txt -|5: ╎ +|2: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save +|3: Mode Filter Collapse Del ╎ Mode Filter Collapse Del +|4: ls ╎ ls +|5: w.txt ╎ w.txt |6: ╎ |7: ╎ |8: ╎ @@ -47,10 +47,10 @@ |14: ╎ |15: ╎ |16: ╎ -|17: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save M -|18: ls ╎ -|19: w.txt ╎ -|20: ╎ +|17: /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save +|18: Mode Filter Collapse Del ╎ +|19: ls ╎ +|20: w.txt ╎ |21: ╎ |22: ╎ |23: ╎ @@ -61,13 +61,13 @@ |28: ╎ |29: ╎ |30: ╎ -== snap edge-done grid=100x31 cursor=4,5 +== snap edge-done grid=100x31 cursor=4,7 |1: New Tty Find Grep Joincol D New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter Collapse -|3: ls ls -|4: w.txt w.txt -|5: -|6: +|2: /tmp/pardes-snap/cornerdra /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter Collapse +|3: g/cwd Tty+bash Save Mode F Del +|4: ilter Collapse Del ls +|5: ls w.txt +|6: w.txt |7: |8: |9: @@ -78,11 +78,11 @@ |14: |15: |16: -|17: /tmp/pardes-snap/cornerdrag -|18: ls -|19: w.txt -|20: -|21: +|17: /tmp/pardes-snap/cornerdra +|18: g/cwd Tty+bash Save Mode F +|19: ilter Collapse Del +|20: ls +|21: w.txt |22: |23: |24: @@ -92,13 +92,13 @@ |28: |29: |30: -== snap corner-drag grid=100x31 cursor=4,5 +== snap corner-drag grid=100x31 cursor=4,7 |1: New Tty Find Grep Joincol D New Tty Find Grep Joincol ╎elcol -|2: /tmp/pardes-snap/cornerdrag /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save Mode Filter Collapse -|3: ls ls ╎ -|4: w.txt w.txt ╎ -|5: ╎ -|6: ╎ +|2: /tmp/pardes-snap/cornerdra /tmp/pardes-snap/cornerdra╎/cwd Tty+bash Save Mode Filter Collapse +|3: g/cwd Tty+bash Save Mode F Del ╎ +|4: ilter Collapse Del ls ╎ +|5: ls w.txt ╎ +|6: w.txt ╎ |7: ╎ |8: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ |9: ╎ @@ -109,11 +109,11 @@ |14: ╎ |15: ╎ |16: ╎ -|17: /tmp/pardes-snap/cornerdrag ╎ -|18: ls ╎ -|19: w.txt ╎ -|20: ╎ -|21: ╎ +|17: /tmp/pardes-snap/cornerdra ╎ +|18: g/cwd Tty+bash Save Mode F ╎ +|19: ilter Collapse Del ╎ +|20: ls ╎ +|21: w.txt ╎ |22: ╎ |23: ╎ |24: ╎ @@ -123,19 +123,19 @@ |28: ╎ |29: ╎ |30: ╎ -== snap corner-done grid=100x31 cursor=4,5 +== snap corner-done grid=100x31 cursor=4,6 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag/cwd Tty+b -|3: ls ls -|4: w.txt w.txt -|5: +|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte /tmp/pardes-snap/cornerdrag/cwd Tty+ +|3: r Collapse Del bash Save Mode Filter Collapse Del +|4: ls ls +|5: w.txt w.txt |6: |7: |8: -|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter -|10: ls -|11: w.txt -|12: +|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte +|10: r Collapse Del +|11: ls +|12: w.txt |13: |14: |15: @@ -154,19 +154,19 @@ |28: |29: |30: -== snap clamp-x grid=100x31 cursor=4,5 +== snap clamp-x grid=100x31 cursor=4,6 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol D╎lcol -|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag╎cwd Tty+b -|3: ls ls ╎ -|4: w.txt w.txt ╎ -|5: ╎ +|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte /tmp/pardes-snap/cornerdrag╎cwd Tty+ +|3: r Collapse Del bash Save Mode Filter Colla╎se Del +|4: ls ls ╎ +|5: w.txt w.txt ╎ |6: ╎ |7: ╎ |8: ╎ -|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter ╎ -|10: ls ╎ -|11: w.txt ╎ -|12: ╎ +|9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte ╎ +|10: r Collapse Del ╎ +|11: ls ╎ +|12: w.txt ╎ |13: ╎ |14: ╎ |15: ╎ @@ -185,19 +185,19 @@ |28: ╎ |29: ╎ |30: ╎ -== snap clamp-y grid=100x31 cursor=4,5 +== snap clamp-y grid=100x31 cursor=4,6 |1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte╎ /tmp/pardes-snap/cornerdrag/cwd Tty+b -|3: ls ╎ ls -|4: w.txt ╎ w.txt -|5: ╎ +|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte╎ /tmp/pardes-snap/cornerdrag/cwd Tty+ +|3: r Collapse Del ╎ bash Save Mode Filter Collapse Del +|4: ls ╎ ls +|5: w.txt ╎ w.txt |6: ╎ |7: ╎ |8: ╎ |9: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte╎ -|10: ls ╎ -|11: w.txt ╎ -|12: ╎ +|10: r Collapse Del ╎ +|11: ls ╎ +|12: w.txt ╎ |13: ╎ |14: ╎ |15: ╎ @@ -216,12 +216,12 @@ |28: ╎ |29: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ |30: ╎ -== snap clamp-done grid=100x31 cursor=4,5 +== snap clamp-done grid=100x31 cursor=4,6 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter /tmp/pardes-snap/cornerdrag/cwd Tty+b -|3: ls ls -|4: w.txt w.txt -|5: +|2: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte /tmp/pardes-snap/cornerdrag/cwd Tty+ +|3: r Collapse Del bash Save Mode Filter Collapse Del +|4: ls ls +|5: w.txt w.txt |6: |7: |8: @@ -246,4 +246,4 @@ |27: |28: |29: -|30: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter +|30: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filte diff --git a/test/snapshots/delask.golden b/test/snapshots/delask.golden index 1a350abf..ed5dd66a 100644 --- a/test/snapshots/delask.golden +++ b/test/snapshots/delask.golden @@ -18,7 +18,7 @@ | | | -| /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del +| * /tmp/pardes-snap/delask/cwd/+New Save Tty Collapse Del | 1 middle-pane | | @@ -27,7 +27,7 @@ | | | -| /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del +| * /tmp/pardes-snap/delask/cwd/+New Save Tty Collapse Del | 1 bottom-pane Up == snap asked grid=100x36 cursor=18,20 |20: 1 middle-pane Del: give space to k above j below @@ -40,7 +40,7 @@ |29: == snap typed grid=100x36 cursor=27,20 |20: 1 bottom-pane DelBelow -|28: /tmp/pardes-snap/delask/cwd/+New * Save Tty Collapse Del +|28: * /tmp/pardes-snap/delask/cwd/+New Save Tty Collapse Del |29: 1 third-pane == snap below-took-click grid=100x36 cursor=17,20 |20: 1 third-pane diff --git a/test/snapshots/doccol.golden b/test/snapshots/doccol.golden index fe21d23a..078f1c04 100644 --- a/test/snapshots/doccol.golden +++ b/test/snapshots/doccol.golden @@ -1,21 +1,25 @@ -== snap boot grid=100x31 cursor=7,3 +== snap boot grid=100x31 cursor=7,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/doccol/cwd/a.txt Save Tty Coll /tmp/pardes-snap/doccol/cwd Tty+bash Save Mode +| /tmp/pardes-snap/doccol/cwd/a.txt Save Tty Col /tmp/pardes-snap/doccol/cwd Tty+bash Save Mode +| lapse Del Filter Collapse Del | 1 AAA ls | a.txt b.txt c.txt -== snap two-docs grid=100x31 cursor=7,18 -|17: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Coll -|18: 1 BBB -|19: 2 B-TWO -== snap moved grid=100x31 cursor=57,21 +== snap two-docs grid=100x31 cursor=7,19 +|17: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Col +|18: lapse Del +|19: 1 BBB +|20: 2 B-TWO +== snap moved grid=100x31 cursor=57,22 |17: |18: |19: -|20: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Coll -|21: 1 BBB -|22: 2 B-TWO -== snap worked grid=100x31 cursor=59,22 -== snap third grid=100x31 cursor=57,27 -|26: /tmp/pardes-snap/doccol/cwd/c.txt Save Tty Coll -|27: 1 CCC +|20: /tmp/pardes-snap/doccol/cwd/b.txt Save Tty Col +|21: lapse Del +|22: 1 BBB +|23: 2 B-TWO +== snap worked grid=100x31 cursor=59,23 +== snap third grid=100x31 cursor=57,28 +|26: /tmp/pardes-snap/doccol/cwd/c.txt Save Tty Col +|27: lapse Del +|28: 1 CCC diff --git a/test/snapshots/doccol.snap b/test/snapshots/doccol.snap index 550aef1f..3a9befa3 100644 --- a/test/snapshots/doccol.snap +++ b/test/snapshots/doccol.snap @@ -31,8 +31,8 @@ stable 400 5000 snap boot # look b.txt in the shell's ls output: the second doc splits the first, so # both docs are in the left column and a.txt is the lower-numbered pane -press right 60 5 -release right 60 5 +press right 60 6 +release right 60 6 wait 10000 BBB stable 700 20000 snap two-docs @@ -45,17 +45,17 @@ stable 700 15000 snap moved # use a.txt, then b.txt: what counts is the doc last USED, so a.txt being the # most recent for a moment must not survive the click into b.txt -press left 5 4 -release left 5 4 +press left 5 5 +release left 5 5 stable 400 5000 -press left 60 22 -release left 60 22 +press left 60 23 +release left 60 23 key j stable 400 5000 snap worked # look c.txt from the shell: below b.txt, in the right column -press right 67 5 -release right 67 5 +press right 67 6 +release right 67 6 wait 10000 CCC stable 700 20000 snap third diff --git a/test/snapshots/dump.golden b/test/snapshots/dump.golden index f061e7be..deed689c 100644 --- a/test/snapshots/dump.golden +++ b/test/snapshots/dump.golden @@ -27,15 +27,19 @@ .{ .kind = .terminal, .tag = "/tmp/pardes-snap/dump/cwd Tty+bash Save Mode Filter Collapse Del", - .body = "ls\nd.txt\n\n\n\n typed-marker\n\n\n\n\n\n\n\n", + .body = "ls\nd.txt\n\n\n\n typed-marker\n\n\n\n\n\n\n", .scroll = 0, .cols = 48, - .rows = 14, + .rows = 13, .vweight = 1, .collapsed = false, + .serial = 1, .tag_tail = null, .terminal = .{ .cwd = "/tmp/pardes-snap/dump/cwd", + .shell = "", + .command = "", + .status = null, .stream = "ls\nd.txt\n", .stream_b64 = <elided> .stream_is_screen = true, @@ -47,15 +51,19 @@ .{ .kind = .terminal, .tag = "/tmp/pardes-snap/dump/cwd Tty+bash Save Mode Filter Collapse Del", - .body = "ls\nd.txt\n\n\n\n\n\n\n\n\n\n\n", + .body = "ls\nd.txt\n\n\n\n\n\n\n\n\n\n", .scroll = 0, .cols = 48, - .rows = 13, + .rows = 12, .vweight = 1, .collapsed = false, + .serial = 2, .tag_tail = null, .terminal = .{ .cwd = "/tmp/pardes-snap/dump/cwd", + .shell = "", + .command = "", + .status = null, .stream = "ls\nd.txt\n", .stream_b64 = <elided> .stream_is_screen = true, @@ -67,15 +75,19 @@ .{ .kind = .terminal, .tag = "/tmp/pardes-snap/dump/cwd Tty+bash Save Mode Filter Collapse Del", - .body = "ls\nd.txt\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + .body = "ls\nd.txt\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", .scroll = 0, .cols = 48, - .rows = 28, + .rows = 27, .vweight = 1, .collapsed = false, + .serial = 3, .tag_tail = null, .terminal = .{ .cwd = "/tmp/pardes-snap/dump/cwd", + .shell = "", + .command = "", + .status = null, .stream = "ls\nd.txt\n", .stream_b64 = <elided> .stream_is_screen = true, diff --git a/test/snapshots/dump.snap b/test/snapshots/dump.snap index 485c4932..29e7b09e 100644 --- a/test/snapshots/dump.snap +++ b/test/snapshots/dump.snap @@ -3,8 +3,8 @@ file d.txt x start 31 100 -n 3 wait 8000 d.txt stable 700 20000 -press left 5 9 -release left 5 9 +press left 5 10 +release left 5 10 stable 400 5000 key i text typed-marker diff --git a/test/snapshots/edit.golden b/test/snapshots/edit.golden index 63388124..d4ec9581 100644 --- a/test/snapshots/edit.golden +++ b/test/snapshots/edit.golden @@ -1,14 +1,14 @@ == snap inserted grid=100x31 cursor=16,10 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi +| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode F /tmp/pardes-snap/edit/cwd Tty+bash Save Mode F +| ilter Collapse Del ilter Collapse Del | ls ls | e.txt e.txt | | | | -| | hello-insert | | @@ -16,17 +16,18 @@ | | | -| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode Fi +| /tmp/pardes-snap/edit/cwd Tty+bash Save Mode F +| ilter Collapse Del | ls | e.txt == style inserted grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-86 #d3b0d9,#1d2824, 87-99 #b2c3b8,#1d2824, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-86 #d3b0d9,#1d2824, 87-98 #b2c3b8,#1d2824, 99-99 d,#1d2824, +|3: 0-2 d,#26362d, 3-20 #becfc4,#26362d, 21-49 d,#26362d, 50-52 d,#1d2824, 53-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|5: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|6: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, |7: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |9: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, @@ -37,11 +38,11 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|18: 0-2 d,#1d2824, 3-20 #b2c3b8,#1d2824, 21-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|21: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |22: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, diff --git a/test/snapshots/emptycol.golden b/test/snapshots/emptycol.golden index bb41c028..37415cd7 100644 --- a/test/snapshots/emptycol.golden +++ b/test/snapshots/emptycol.golden @@ -1,14 +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 Tt +| /tmp/pardes-snap/emptycol/cwd/notes.txt Save T +| ty Collapse Del | 1 one | 2 two -== snap filled grid=100x24 cursor=54,5 -|2: /tmp/pardes-snap/emptycol/cwd/notes.txt Save Tt /tmp/pardes-snap/emptycol/cwd Tty+bash Save Mod -|3: 1 one ls -|4: 2 two notes.txt +== snap filled grid=100x24 cursor=54,6 +|2: /tmp/pardes-snap/emptycol/cwd/notes.txt Save T /tmp/pardes-snap/emptycol/cwd Tty+bash Save Mo +|3: ty Collapse Del de Filter Collapse Del +|4: 1 one ls +|5: 2 two notes.txt == snap emptied grid=100x24 cursor=53,1 -|2: /tmp/pardes-snap/emptycol/cwd/notes.txt Save Tt -|3: 1 one -|4: 2 two +|2: /tmp/pardes-snap/emptycol/cwd/notes.txt Save T +|3: ty Collapse Del +|4: 1 one +|5: 2 two diff --git a/test/snapshots/exec-bigdrag.golden b/test/snapshots/exec-bigdrag.golden index 87d3b9bb..c74fd8f9 100644 --- a/test/snapshots/exec-bigdrag.golden +++ b/test/snapshots/exec-bigdrag.golden @@ -2,60 +2,60 @@ |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del -| ls bash: line: command not found -| big.txt 4line 2705line 2706line 2707line 2708line 2709line 2710line 2711line 2712line 2713line 2714lin -| bash: 4line: command not found -| 15line 2716line 2717line 2718line 2719line 2720line 2721line 2722line 2723line 2724line 2725li -| bash: 15line: command not found -| 726line 2727line 2728line 2729line 2730line 2731line 2732line 2733line 2734line 2735line 2736l -| bash: 726line: command not found -| 2737line 2738line 2739line 2740line 2741line 2742line 2743line 2744line 2745line 2746line 2747 -| bash: 2737line: command not found -| 2748line 2749line 2750line 2751line 2752line 2753line 2754line 2755line 2756line 2757line 275 -| bash: 2748line: command not found -| e 2759line 2760line 2761line 2762line 2763line 2764line 2765line 2766line 2767line 2768line 27 +| ls 2 +| big.txt bash: 91line: command not found +| 802line 2803line 2804line 2805line 2806line 2807line 2808line 2809line 2810line 2811line 2812lin +| e +| bash: 802line: command not found +| 2813line 2814line 2815line 2816line 2817line 2818line 2819line 2820line 2821line 2822line 2823li +| ne +| bash: 2813line: command not found +| 2824line 2825line 2826line 2827line 2828line 2829line 2830line 2831line 2832line 2833line 2834l +| in +| bash: 2824line: command not found +| e 2835line 2836line 2837line 2838line 2839line 2840line 2841line 2842line 2843line 2844line 2845 +| li | bash: e: command not found -| ne 2770line 2771line 2772line 2773line 2774line 2775line 2776line 2777line 2778line 2779line 2 +| ne 2846line 2847line 2848line 2849line 2850line 2851line 2852line 2853line 2854line 2855line 285 +| 6l | bash: ne: command not found -| ine 2781line 2782line 2783line 2784line 2785line 2786line 2787line 2788line 2789line 2790line +| ine 2857line 2858line 2859line 2860line 2861line 2862line 2863line 2864line 2865line 2866line 28 +| 67 | bash: ine: command not found -| line 2792line 2793line 2794line 2795line 2796line 2797line 2798line 2799line 2800line 2801line +| line 2868line 2869line 2870line 2871line 2872line 2873line 2874line 2875line 2876line 2877line 2 +| 87 | bash: line: command not found -| 2line 2803line 2804line 2805line 2806line 2807line 2808line 2809line 2810line 2811line 2812lin -| bash: 2line: command not found -| 13line 2814line 2815line 2816line 2817line 2818line 2819line 2820line 2821line 2822line 2823li -| bash: 13line: command not found -| 824line 2825line 2826line 2827line 2828line 2829line 2830line 2831line 2832line 2833line 2834l -| bash: 824line: command not found -| 2835line 2836line 2837line 2838line 2839line 2840line 2841line 2842line 2843line 2844line 2845 -| bash: 2835line: command not found -| 2846line 2847line 2848line 2849line 2850line 2851line 2852line 2853line 2854line 2855line 285 -| bash: 2846line: command not found -| /tmp/pardes-snap/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del e 2857line 2858line 2859line 2860line 2861line 2862line 2863line 2864line 2865line 2866line 28 -| ls bash: e: command not found -| big.txt ne 2868line 2869line 2870line 2871line 2872line 2873line 2874line 2875line 2876line 2877line 2 -| bash: ne: command not found -| ine 2879line 2880line 2881line 2882line 2883line 2884line 2885line 2886line 2887line 2888line -| bash: ine: command not found -| liecho alive-o''kne 2890line 2891line 2892line 2893line 2894line 2895line 2896line 2897line 2898li -| bash: line: command not found -| 0line 2901line 2902line 2903line 2904line 2905line 2906line 2907line 2908line 2909line 2910lin -| bash: 0line: command not found -| 11line 2912line 2913line 2914line 2915line 2916line 2917line 2918line 2919line 2920line 2921li -| bash: 11line: command not found -| 922line 2923line 2924line 2925line 2926line 2927line 2928line 2929line 2930line 2931line 2932l -| bash: 922line: command not found -| 2933line 2934line 2935line 2936line 2937line 2938line 2939line 2940line 2941line 2942line 2943 -| bash: 2933line: command not found -| 2944line 2945line 2946line 2947line 2948line 2949line 2950line 2951line 2952line 2953line 295 -| bash: 2944line: command not found -| e 2955line 2956line 2957line 2958line 2959line 2960line 2961line 2962line 2963line 2964line 29 +| 8line 2879line 2880line 2881line 2882line 2883line 2884line 2885line 2886line 2887line 2888line +| 28 +| bash: 8line: command not found +| 89line 2890line 2891line 2892line 2893line 2894line 2895line 2896line 2897line 2898line 2899line +| 2 +| bash: 89line: command not found +| /tmp/pardes-snap/exec-bigdrag/cwd Tty+bash Save Mode Filter Collapse Del 900line 2901line 2902line 2903line 2904line 2905line 2906line 2907line 2908line 2909line 2910lin +| ls e +| big.txt bash: 900line: command not found +| 2911line 2912line 2913line 2914line 2915line 2916line 2917line 2918line 2919line 2920line 2921li +| ne +| bash: 2911line: command not found +| 2echo alive-o''k922line 2923line 2924line 2925line 2926line 2927line 2928line 2929line 2930line 2 +| in +| bash: 2922line: command not found +| e 2933line 2934line 2935line 2936line 2937line 2938line 2939line 2940line 2941line 2942line 2943 +| li | bash: e: command not found -| ne 2966line 2967line 2968line 2969line 2970line 2971line 2972line 2973line 2974line 2975line 2 +| ne 2944line 2945line 2946line 2947line 2948line 2949line 2950line 2951line 2952line 2953line 295 +| 4l | bash: ne: command not found -| ine 2977line 2978line 2979line 2980line 2981line 2982line 2983line 2984line 2985line 2986line +| ine 2955line 2956line 2957line 2958line 2959line 2960line 2961line 2962line 2963line 2964line 29 +| 65 | bash: ine: command not found -| line 2988line 2989line 2990line 2991line 2992line 2993line 2994line 2995line 2996line 2997line +| line 2966line 2967line 2968line 2969line 2970line 2971line 2972line 2973line 2974line 2975line 2 +| 97 | bash: line: command not found +| 6line 2977line 2978line 2979line 2980line 2981line 2982line 2983line 2984line 2985line 2986line +| 29 +| bash: 6line: command not found +| 87line 2988line 2989line 2990line 2991line 2992line 2993line 2994line 2995line 2996line 2997line +| bash: 87line: command not found | echo alive-o''k | alive-ok diff --git a/test/snapshots/fileedit.golden b/test/snapshots/fileedit.golden index 4d367d72..4c651d3c 100644 --- a/test/snapshots/fileedit.golden +++ b/test/snapshots/fileedit.golden @@ -5,7 +5,7 @@ | ls ls | f.txt f.txt | -| /tmp/pardes-snap/fileedit/cwd/f.txt * Save Tty Collapse Del +| * /tmp/pardes-snap/fileedit/cwd/f.txt Save Tty Collapse Del | 1 liEDIT''MEne 1 | 2 line 2 | 3 line 3 diff --git a/test/snapshots/filewatch.golden b/test/snapshots/filewatch.golden index 479b8a1a..f19168b1 100644 --- a/test/snapshots/filewatch.golden +++ b/test/snapshots/filewatch.golden @@ -5,7 +5,7 @@ | ls | sub w.txt | -| /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del +| * /tmp/pardes-snap/filewatch/cwd/w.txt Save Tty Collapse Del | 1 liMYEDITne 1 | 2 line 2 | 3 /tmp/pardes-snap/filewatch/cwd/sub/s.txt @@ -53,7 +53,7 @@ |27: |28: == snap undone grid=210x31 cursor=15,7 -|6: /tmp/pardes-snap/filewatch/cwd/w.txt * Save Tty Collapse Del +|6: * /tmp/pardes-snap/filewatch/cwd/w.txt Save Tty Collapse Del |7: 1 liMYEDITne 1 |8: 2 line 2 |9: 3 /tmp/pardes-snap/filewatch/cwd/sub/s.txt diff --git a/test/snapshots/ghostty-mid.golden b/test/snapshots/ghostty-mid.golden index 965f6896..05d63bc8 100644 --- a/test/snapshots/ghostty-mid.golden +++ b/test/snapshots/ghostty-mid.golden @@ -1,7 +1,8 @@ -== snap ghostty-mid grid=100x31 cursor=4,7 +== snap ghostty-mid grid=100x31 cursor=4,8 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/ghostty-mid/cwd Tty+bash Save /tmp/pardes-snap/ghostty-mid/cwd Tty+bash Save +| Mode Filter Collapse Del Mode Filter Collapse Del | ZZPAD ls | ./g.sh g.txt | ./g.sh @@ -15,7 +16,7 @@ | | | -| | /tmp/pardes-snap/ghostty-mid/cwd Tty+bash Save +| Mode Filter Collapse Del | ls | g.txt diff --git a/test/snapshots/ghostty-mid.snap b/test/snapshots/ghostty-mid.snap index cb3088b4..c20db2f7 100644 --- a/test/snapshots/ghostty-mid.snap +++ b/test/snapshots/ghostty-mid.snap @@ -23,10 +23,10 @@ stable 700 10000 key c-b stable 600 8000 send \x1b[I -send \x1b[<35;10;9M\x1b[<35;7;7M\x1b[<35;4;5M -send \x1b[<1;4;5M -send \x1b[<1;4;5m -send \x1b[<35;6;5M +send \x1b[<35;10;10M\x1b[<35;7;8M\x1b[<35;4;6M +send \x1b[<1;4;6M +send \x1b[<1;4;6m +send \x1b[<35;6;6M wait 10000 GHOSTOK stable 700 10000 snap ghostty-mid diff --git a/test/snapshots/hscroll.golden b/test/snapshots/hscroll.golden index 4e9fd9af..8ce8b968 100644 --- a/test/snapshots/hscroll.golden +++ b/test/snapshots/hscroll.golden @@ -1,11 +1,13 @@ -== snap opened grid=90x31 cursor=7,7 +== snap opened grid=90x31 cursor=7,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/hscroll/cwd Tty+bash Save /tmp/pardes-snap/hscroll/cwd Tty+bash Save +| /tmp/pardes-snap/hscroll/cwd Tty+bash Sav /tmp/pardes-snap/hscroll/cwd Tty+bash Sav +| e Mode Filter Collapse Del e Mode Filter Collapse Del | ls ls | wide.txt wide.txt | -| /tmp/pardes-snap/hscroll/cwd/wide.txt Save +| /tmp/pardes-snap/hscroll/cwd/wide.txt Sav +| e Tty Collapse Del | 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp | | @@ -14,16 +16,15 @@ | | | -| -| -| /tmp/pardes-snap/hscroll/cwd Tty+bash Save +| /tmp/pardes-snap/hscroll/cwd Tty+bash Sav +| e Mode Filter Collapse Del | ls | wide.txt -== snap wheeled grid=90x31 cursor=7,7 -|7: 1 ND_A_MARKER_WAY_OUT_RIGHT_padpadpadpad -== snap clamped grid=90x31 cursor=7,7 -|7: 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp -== snap dollar grid=90x31 cursor=41,7 -|7: 1 padpadpadpadpadpadpadpadpadpad_TAIL -== snap zero grid=90x31 cursor=7,7 -|7: 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp +== snap wheeled grid=90x31 cursor=7,9 +|9: 1 ND_A_MARKER_WAY_OUT_RIGHT_padpadpadpad +== snap clamped grid=90x31 cursor=7,9 +|9: 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp +== snap dollar grid=90x31 cursor=41,9 +|9: 1 padpadpadpadpadpadpadpadpadpad_TAIL +== snap zero grid=90x31 cursor=7,9 +|9: 1 short1 END_A_MARKER_WAY_OUT_RIGHT_padp diff --git a/test/snapshots/images.golden b/test/snapshots/images.golden index 11d4ae3b..46caaff7 100644 --- a/test/snapshots/images.golden +++ b/test/snapshots/images.golden @@ -5,17 +5,17 @@ | ls ls | pic.ppm pic.ppm | -| img petscii:off palette:commodore ascii:on /tmp/pardes-snap/images/cwd/p -| 92@7%5Gm$D4▘🬵%🬨3'🬥🬆🬻 -| 🬥3▖╱{=CY,▌*)^M<▀🬎🬻 -| X▄jN.&🬺j*w3┼"🬥🬆🬻 -| ╱╳PxGXPV*q"(🬆~ -| HF[🬗█▀&t0^🬆🬻 -| 🬴X7─27/*🬆🬻 -| .g🬬🬎ks▛🬻 -| ▚i,V🬆🬹 -| mu🬆🬻 -| 🬆🬻 +| img petscii:off palette:commodore ascii:on /tmp/pardes-snap/images/cwd/ +| pic.ppm Tty Collapse Del +| x🬱\╲🬌\~▄🬥4▟g*🬺🬶_🬆🬻 +| >A8🬌Cq&.\H&🬹🬜C▛🬻 +| ><e'🬷y🬙▄Fso🬴🬆🬹 +| k[🬻🬟🬜7f🬜$>🬎🬻 +| 4▜>gi▟9🬙🬆🬻 +| ╱i🬹─B▜🬆🬻 +| @🬥🬺v🬆🬻 +| g🬷🬆🬻 +| 🬆🬻 | /tmp/pardes-snap/images/cwd Tty+bash Save Mode Filter Collapse Del | ls | pic.ppm @@ -26,17 +26,17 @@ |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-74 d,#0d1410, 75-75 d,#608269, 76-149 d,#0d1410, -|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-73 #becfc4,#26362d, 74-74 #dfbce5,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|7: 0-0 d,#152019, 1-27 d,#0d1410, 28-29 #959595,#6c6c6c, 30-30 #6c6c6c,#959595, 31-34 #959595,#6c6c6c, 35-35 #6c6c6c,#959595, 36-37 #959595,#6c6c6c, 38-38 #6c6c6c,#959595, 39-39 #959595,#6c6c6c, 40-40 #6c6c6c,#959595, 41-41 #959595,#6c6c6c, 42-42 #6c6c6c,#959595, 43-44 #959595,#6c6c6c, 45-45 #6c6c6c,#959595, 46-46 #959595,#6c5eb5, 47-47 #352879,#6c6c6c, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|8: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #6c6c6c,#959595, 29-29 #959595,#6c6c6c, 30-30 #6c6c6c,#959595, 31-34 #959595,#6c6c6c, 35-38 #6c6c6c,#959595, 39-42 #959595,#6c6c6c, 43-43 #6c6c6c,#959595, 44-44 #6c6c6c,#352879, 45-45 #352879,#959595, 46-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|9: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #959595,#6c6c6c, 29-29 #6c6c6c,#959595, 30-33 #959595,#6c6c6c, 34-34 #6c6c6c,#959595, 35-38 #959595,#6c6c6c, 39-39 #6c6c6c,#959595, 40-40 #959595,#6c6c6c, 41-41 #6c6c6c,#959595, 42-42 #959595,#6c5eb5, 43-43 #352879,#6c6c6c, 44-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|10: 0-0 d,#152019, 1-27 d,#0d1410, 28-29 #959595,#6c6c6c, 30-30 #6c6c6c,#959595, 31-33 #959595,#6c6c6c, 34-34 #6c6c6c,#959595, 35-39 #959595,#6c6c6c, 40-40 #959595,#6c5eb5, 41-41 #6c6c6c,#352879, 42-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|11: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #6c6c6c,#959595, 29-29 #959595,#6c6c6c, 30-34 #6c6c6c,#959595, 35-36 #959595,#6c6c6c, 37-37 #6c6c6c,#959595, 38-38 #6c6c6c,#6c5eb5, 39-39 #352879,#6c6c6c, 40-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|12: 0-0 d,#152019, 1-27 d,#0d1410, 28-29 #6c6c6c,#959595, 30-31 #959595,#6c6c6c, 32-32 #6c6c6c,#959595, 33-34 #959595,#6c6c6c, 35-35 #6c6c6c,#959595, 36-36 #6c6c6c,#6c5eb5, 37-37 #352879,#6c6c6c, 38-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|13: 0-0 d,#152019, 1-27 d,#0d1410, 28-29 #959595,#6c6c6c, 30-32 #6c6c6c,#959595, 33-33 #959595,#6c6c6c, 34-34 #6c6c6c,#352879, 35-35 #352879,#6c6c6c, 36-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|14: 0-0 d,#152019, 1-27 d,#0d1410, 28-31 #959595,#6c6c6c, 32-32 #959595,#6c5eb5, 33-33 #352879,#6c5eb5, 34-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|15: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #6c6c6c,#959595, 29-29 #959595,#6c6c6c, 30-30 #6c6c6c,#6c5eb5, 31-31 #352879,#6c6c6c, 32-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|16: 0-0 d,#152019, 1-27 d,#0d1410, 28-28 #6c6c6c,#6c5eb5, 29-29 #352879,#6c6c6c, 30-47 #352879,#352879, 48-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-73 #becfc4,#26362d, 74-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|7: 0-2 d,#26362d, 3-9 #dfbce5,#26362d, 10-26 #becfc4,#26362d, 27-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|8: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 #959595,#6c6c6c, 31-31 #6c6c6c,#959595, 32-32 #959595,#6c6c6c, 33-33 #6c6c6c,#959595, 34-35 #959595,#6c6c6c, 36-36 #6c6c6c,#959595, 37-37 #959595,#6c6c6c, 38-40 #6c6c6c,#959595, 41-41 #959595,#6c6c6c, 42-43 #6c6c6c,#959595, 44-44 #959595,#6c6c6c, 45-45 #6c6c6c,#6c5eb5, 46-46 #352879,#959595, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|9: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 #959595,#6c6c6c, 30-31 #6c6c6c,#959595, 32-35 #959595,#6c6c6c, 36-36 #6c6c6c,#959595, 37-38 #959595,#6c6c6c, 39-42 #6c6c6c,#959595, 43-43 #6c6c6c,#352879, 44-44 #352879,#6c6c6c, 45-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|10: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 #959595,#6c6c6c, 31-31 #6c6c6c,#959595, 32-32 #959595,#6c6c6c, 33-33 #6c6c6c,#959595, 34-34 #959595,#6c6c6c, 35-36 #6c6c6c,#959595, 37-39 #959595,#6c6c6c, 40-40 #6c6c6c,#959595, 41-41 #6c6c6c,#6c5eb5, 42-42 #352879,#6c5eb5, 43-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|11: 0-0 d,#152019, 1-28 d,#0d1410, 29-33 #6c6c6c,#959595, 34-35 #959595,#6c6c6c, 36-36 #6c6c6c,#959595, 37-38 #959595,#6c6c6c, 39-39 #6c6c6c,#352879, 40-40 #352879,#959595, 41-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|12: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 #6c6c6c,#959595, 31-33 #959595,#6c6c6c, 34-34 #6c6c6c,#959595, 35-35 #959595,#6c6c6c, 36-36 #6c6c6c,#959595, 37-37 #959595,#6c5eb5, 38-38 #352879,#6c6c6c, 39-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|13: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 #959595,#6c6c6c, 31-31 #6c6c6c,#959595, 32-33 #959595,#6c6c6c, 34-34 #6c6c6c,#959595, 35-35 #6c6c6c,#6c5eb5, 36-36 #352879,#6c6c6c, 37-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|14: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 #959595,#6c6c6c, 30-32 #6c6c6c,#959595, 33-33 #959595,#6c5eb5, 34-34 #352879,#6c6c6c, 35-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|15: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 #6c6c6c,#959595, 31-31 #6c6c6c,#6c5eb5, 32-32 #352879,#6c6c6c, 33-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|16: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 #6c6c6c,#6c5eb5, 30-30 #352879,#6c6c6c, 31-46 #352879,#352879, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-68 #b2c3b8,#1d2824, 69-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |18: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, @@ -52,29 +52,28 @@ |29: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |30: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, == style term-palette grid=150x31 -|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-72 #becfc4,#26362d, 73-74 #dfbce5,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|7: 0-0 d,#152019, 1-65 d,#0d1410, 66-66 d,#1d2824, 67-73 #b2c3b8,#1d2824, 74-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|8: 0-0 d,#152019, 1-28 d,#0d1410, 29-31 p8,p4, 32-33 p4,p8, 34-37 p8,p4, 38-38 p4,p8, 39-44 p8,p4, 45-45 p8,p12, 46-46 p12,p8, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|9: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 p8,p4, 30-30 p4,p8, 31-35 p8,p4, 36-36 p5,p8, 37-37 p8,p4, 38-38 p4,p8, 39-41 p8,p4, 42-42 p4,p8, 43-43 p8,p12, 44-45 p12,p8, 46-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|10: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 p8,p4, 31-32 p4,p8, 33-40 p8,p4, 41-41 p8,p12, 42-43 p12,p8, 44-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|11: 0-0 d,#152019, 1-28 d,#0d1410, 29-31 p8,p4, 32-32 p4,p8, 33-34 p8,p4, 35-37 p4,p8, 38-38 p8,p4, 39-39 p8,p12, 40-41 p12,p8, 42-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|12: 0-0 d,#152019, 1-28 d,#0d1410, 29-36 p8,p4, 37-37 p8,p12, 38-39 p12,p8, 40-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|13: 0-0 d,#152019, 1-28 d,#0d1410, 29-30 p4,p8, 31-34 p8,p4, 35-35 p8,p12, 36-36 p12,p8, 37-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|14: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 p4,p8, 30-31 p8,p4, 32-32 p4,p8, 33-33 p8,p12, 34-35 p12,p8, 36-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|15: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 p4,p8, 30-30 p8,p4, 31-31 p8,p12, 32-32 p12,p8, 33-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|16: 0-0 d,#152019, 1-28 d,#0d1410, 29-29 p8,p12, 30-30 p12,p8, 31-46 p12,p12, 47-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-72 #becfc4,#26362d, 73-73 #dfbce5,#26362d, 74-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|7: 0-2 d,#26362d, 3-8 #dfbce5,#26362d, 9-25 #becfc4,#26362d, 26-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|8: 0-0 d,#152019, 1-65 d,#0d1410, 66-66 d,#1d2824, 67-73 #b2c3b8,#1d2824, 74-74 d,#1d2824, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|9: 0-0 d,#152019, 1-29 d,#0d1410, 30-33 p8,p4, 34-34 p4,p8, 35-35 p8,p4, 36-36 p4,p8, 37-43 p8,p4, 44-44 p8,p12, 45-45 p12,p8, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|10: 0-0 d,#152019, 1-29 d,#0d1410, 30-30 p4,p8, 31-31 p8,p4, 32-32 p4,p8, 33-41 p8,p4, 42-43 p12,p8, 44-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|11: 0-0 d,#152019, 1-29 d,#0d1410, 30-34 p8,p4, 35-36 p4,p8, 37-37 p8,p4, 38-39 p4,p8, 40-40 p8,p12, 41-41 p12,p8, 42-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|12: 0-0 d,#152019, 1-29 d,#0d1410, 30-31 p8,p4, 32-32 p4,p8, 33-34 p8,p4, 35-35 p4,p8, 36-37 p8,p4, 38-38 p8,p12, 39-39 p12,p8, 40-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|13: 0-0 d,#152019, 1-29 d,#0d1410, 30-31 p8,p4, 32-32 p4,p8, 33-35 p8,p4, 36-36 p8,p12, 37-37 p12,p8, 38-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|14: 0-0 d,#152019, 1-29 d,#0d1410, 30-32 p8,p4, 33-33 p4,p8, 34-34 p8,p12, 35-35 p12,p8, 36-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|15: 0-0 d,#152019, 1-29 d,#0d1410, 30-30 p4,p8, 31-31 p8,p4, 32-32 p8,p12, 33-33 p12,p8, 34-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|16: 0-0 d,#152019, 1-29 d,#0d1410, 30-30 p8,p12, 31-31 p12,p8, 32-45 p12,p12, 46-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, == snap no-ascii grid=150x31 cursor=11,8 -|6: img petscii:off palette:terminal ascii:off /tmp/pardes-snap/images/cwd/p -|7: Ascii -|8: █🬝█╲███▙🬸🬬🬻█████🬝🬵 -|9: ███████🬻██🬸█🬝█🬝▄█ -|10: ████████████🬝▄█ -|11: ███🬸██████🬝▄█ -|12: ████████🬝🬵█ -|13: ╱█████🬝▄ -|14: █🬬█╲🬝🬵█ -|15: 🬺█🬝🬵 -|16: 🬝▄ +|6: img petscii:off palette:terminal ascii:off /tmp/pardes-snap/images/cwd/ +|8: Ascii +|9: ██████████████🬝🬹 +|10: ██████🬛█████🬝▄ +|11: ██████████🬝▄ +|12: ██╱█████🬝▄ +|13: █🬸╲███🬝🬹 +|14: ████🬝▄ +|15: │█🬝▄ +|16: 🬝🬹 == snap tag-edit grid=400x31 cursor=87,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol diff --git a/test/snapshots/layout-close.golden b/test/snapshots/layout-close.golden index 7e5574ff..e080d13e 100644 --- a/test/snapshots/layout-close.golden +++ b/test/snapshots/layout-close.golden @@ -1,15 +1,18 @@ -== snap built grid=140x41 cursor=4,13 +== snap built grid=140x41 cursor=4,16 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps +| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap +| se Del se Del | ls ls | marker.txt marker.txt | -| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps +| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap +| se Del | ls | marker.txt | -| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps +| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap +| se Del | ls | marker.txt | @@ -18,36 +21,50 @@ | | | -| -| -| -| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps +| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap +| se Del | ls | marker.txt | -| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps +| /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap +| se Del | ls | marker.txt -== snap altc grid=140x41 cursor=39,5 +== snap altc grid=140x41 cursor=39,7 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/layout-close/cw /tmp/pardes-snap/layout-close/cw /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps -|3: ls ls ls -|4: marker.txt marker.txt marker.txt -|6: /tmp/pardes-snap/layout-close/cw -|10: -|11: +|2: /tmp/pardes-snap/layout-close/c /tmp/pardes-snap/layout-close/c /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap +|3: wd Tty+bash Save Mode Filter Co wd Tty+bash Save Mode Filter Co se Del +|4: llapse Del llapse Del ls +|5: marker.txt ls marker.txt +|6: marker.txt +|7: /tmp/pardes-snap/layout-close/c +|8: wd Tty+bash Save Mode Filter Co +|9: llapse Del +|10: ls +|11: marker.txt |12: -|22: /tmp/pardes-snap/layout-close/cw -|26: /tmp/pardes-snap/layout-close/cw -== snap del grid=140x41 cursor=4,8 -|6: -|7: -|8: -== snap exit grid=140x41 cursor=4,8 +|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 +|30: ls +|31: marker.txt +== snap del grid=140x41 cursor=6,8 +|8: wd kd Tty+bash Save Mode Filter +|9: Collapse Del +== snap exit grid=140x41 cursor=4,12 |22: |23: |24: +|25: == snap colclose grid=140x41 cursor=38,1 -|2: /tmp/pardes-snap/layout-close/cw /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collaps -|3: ls ls -|4: marker.txt marker.txt +|2: /tmp/pardes-snap/layout-close/c /tmp/pardes-snap/layout-close/cwd Tty+bash Save Mode Filter Collap +|3: wd Tty+bash Save Mode Filter Co se Del +|4: llapse Del ls +|5: marker.txt marker.txt +|6: diff --git a/test/snapshots/layout-open.golden b/test/snapshots/layout-open.golden index 9f677d1c..c48f476e 100644 --- a/test/snapshots/layout-open.golden +++ b/test/snapshots/layout-open.golden @@ -2,11 +2,11 @@ |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/layout-open/cwd Tty+bash Save /tmp/pardes-snap/layout-open/cwd Tty+bash Save +| Mode Filter Collapse Del Mode Filter Collapse Del | ls ls | sub sub | | -| | echo one; echo two | | @@ -17,21 +17,26 @@ | | | /tmp/pardes-snap/layout-open/cwd Tty+bash Save +| Mode Filter Collapse Del | ls | sub -== snap altn-left grid=100x31 cursor=4,9 -|6: /tmp/pardes-snap/layout-open/cwd Tty+bash Save -|7: ls -|8: sub -== snap altn-right grid=100x31 cursor=54,9 -|6: /tmp/pardes-snap/layout-open/cwd Tty+bash Save /tmp/pardes-snap/layout-open/cwd Tty+bash Save -|7: ls ls -|8: sub sub -== snap file-open grid=100x31 cursor=7,22 -|21: /tmp/pardes-snap/layout-open/cwd/sub/f.txt Save -|22: 1 echo 'TTY'OK -== snap ttydir grid=100x31 cursor=7,22 -|19: sub /tmp/pardes-snap/layout-open/cwd/sub (echo 'TTY -|20: % echo 'TTY'OK -|21: /tmp/pardes-snap/layout-open/cwd/sub/f.txt Save TTYOK -|22: 1 echo 'TTY'OK exit 0 +== snap altn-left grid=100x31 cursor=4,11 +|7: /tmp/pardes-snap/layout-open/cwd Tty+bash Save +|8: Mode Filter Collapse Del +|9: ls +|10: sub +== snap altn-right grid=100x31 cursor=54,11 +|7: /tmp/pardes-snap/layout-open/cwd Tty+bash Save /tmp/pardes-snap/layout-open/cwd Tty+bash Save +|8: Mode Filter Collapse Del Mode Filter Collapse Del +|9: ls ls +|10: sub sub +== snap file-open grid=100x31 cursor=7,24 +|22: /tmp/pardes-snap/layout-open/cwd/sub/f.txt Sav +|23: e Tty Collapse Del +|24: 1 echo 'TTY'OK +== snap ttydir grid=100x31 cursor=7,24 +|20: sub /tmp/pardes-snap/layout-open/cwd/sub (echo 'TT +|21: Y'OK) exit 0 Kill Save Collapse Del +|22: /tmp/pardes-snap/layout-open/cwd/sub/f.txt Sav % echo 'TTY'OK +|23: e Tty Collapse Del TTYOK +|24: 1 echo 'TTY'OK exit 0 diff --git a/test/snapshots/layout-open.snap b/test/snapshots/layout-open.snap index 04770f27..43cf56dd 100644 --- a/test/snapshots/layout-open.snap +++ b/test/snapshots/layout-open.snap @@ -47,10 +47,10 @@ snap file-open # execute the file's command (middle drag over line 1): it runs as a command # pane for sub/, forked at the bottom of the rightmost column, splitting only # the pane above it -press middle 8 23 -drag middle 19 23 -release middle 19 23 +press middle 8 25 +drag middle 19 25 +release middle 19 25 wait 10000 TTYOK -motion 40 25 +motion 40 27 stable 700 10000 snap ttydir diff --git a/test/snapshots/load.golden b/test/snapshots/load.golden index 3a3c596a..9dfbcecd 100644 --- a/test/snapshots/load.golden +++ b/test/snapshots/load.golden @@ -1,7 +1,8 @@ == snap loaded grid=100x31 cursor=4,16 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi +| /tmp/pardes-snap/load/cwd Tty+bash Save Mode F /tmp/pardes-snap/load/cwd Tty+bash Save Mode F +| ilter Collapse Del ilter Collapse Del | $ echo LOAD''MARK; seq 1 40; echo E''ND $ ls | LOADMARK d.txt | 1 $ @@ -15,8 +16,8 @@ | 9 | 10 | 11 -| 12 -| /tmp/pardes-snap/load/cwd Tty+bash Save Mode Fi +| /tmp/pardes-snap/load/cwd Tty+bash Save Mode F +| ilter Collapse Del | $ ls | d.txt | $ @@ -24,13 +25,13 @@ == style loaded grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-86 #d3b0d9,#1d2824, 87-99 #b2c3b8,#1d2824, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-28 #becfc4,#26362d, 29-36 #dfbce5,#26362d, 37-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-78 #b2c3b8,#1d2824, 79-86 #d3b0d9,#1d2824, 87-98 #b2c3b8,#1d2824, 99-99 d,#1d2824, +|3: 0-2 d,#26362d, 3-20 #becfc4,#26362d, 21-49 d,#26362d, 50-52 d,#1d2824, 53-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-73 #e5eee5,#0d1410,f 74-99 #e5eee5,#0d1410, -|7: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, -|8: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|7: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-73 #e5eee5,#0d1410,f 74-99 #e5eee5,#0d1410, +|8: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, |9: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |10: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |11: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, @@ -39,13 +40,13 @@ |14: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |15: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |16: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-28 #b2c3b8,#1d2824, 29-36 #d3b0d9,#1d2824, 37-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|18: 0-2 d,#1d2824, 3-20 #b2c3b8,#1d2824, 21-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-23 #e5eee5,#0d1410,f 24-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|22: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-23 #e5eee5,#0d1410,f 24-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|23: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |25: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |26: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, diff --git a/test/snapshots/look-center.golden b/test/snapshots/look-center.golden index 3eaa819b..e5db101e 100644 --- a/test/snapshots/look-center.golden +++ b/test/snapshots/look-center.golden @@ -1,4 +1,4 @@ -== snap center-open grid=150x31 cursor=7,12 +== snap center-open grid=150x31 cursor=15,12 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del @@ -19,7 +19,7 @@ | /tmp/pardes-snap/look-center/cwd Tty+bash Save Mode Filter Collapse Del | ls | big.txt -== snap center-dedup grid=150x31 cursor=7,12 +== snap center-dedup grid=150x31 cursor=15,12 |7: 145 line 145 |8: 146 line 146 big.txt:150 big.txt:2 |9: 147 line 147 @@ -30,7 +30,7 @@ |14: 152 line 152 |15: 153 line 153 |16: 154 line 154 -== snap center-clamp grid=150x31 cursor=7,8 +== snap center-clamp grid=150x31 cursor=13,8 |7: 1 line 1 |8: 2 line 2 big.txt:150 big.txt:2 |9: 3 line 3 diff --git a/test/snapshots/look-dir.golden b/test/snapshots/look-dir.golden index 42627413..a946d119 100644 --- a/test/snapshots/look-dir.golden +++ b/test/snapshots/look-dir.golden @@ -1,11 +1,13 @@ -== snap dir-open grid=100x31 cursor=4,9 +== snap dir-open grid=100x31 cursor=4,11 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod +| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mo /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mo +| de Filter Collapse Del de Filter Collapse Del | ls ls | sub top.txt sub top.txt | -| /tmp/pardes-snap/look-dir/cwd/sub Tty+bash Save +| /tmp/pardes-snap/look-dir/cwd/sub Tty+bash Sav +| e Mode Filter Collapse Del | ls | inner.txt | @@ -14,8 +16,7 @@ | | | -| -| -| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mod +| /tmp/pardes-snap/look-dir/cwd Tty+bash Save Mo +| de Filter Collapse Del | ls | sub top.txt diff --git a/test/snapshots/look-file-1col.golden b/test/snapshots/look-file-1col.golden index f70f787b..45f9f20f 100644 --- a/test/snapshots/look-file-1col.golden +++ b/test/snapshots/look-file-1col.golden @@ -1,4 +1,4 @@ -== snap file-open-1col grid=100x31 cursor=7,19 +== snap file-open-1col grid=100x31 cursor=14,19 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/look-file-1col/cwd Tty+bash Save Mode Filter Collapse Del diff --git a/test/snapshots/look-file.golden b/test/snapshots/look-file.golden index 19b7899d..f0690264 100644 --- a/test/snapshots/look-file.golden +++ b/test/snapshots/look-file.golden @@ -1,4 +1,4 @@ -== snap file-open grid=150x31 cursor=7,12 +== snap file-open grid=150x31 cursor=14,12 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/look-file/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/look-file/cwd Tty+bash Save Mode Filter Collapse Del @@ -32,7 +32,7 @@ |9: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |10: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |11: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, -|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-14 #f5f7ee,#3c614a, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |13: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |14: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |15: 0-0 d,#152019, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-13 #e5eee5,#0d1410, 14-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, diff --git a/test/snapshots/lookloaded.golden b/test/snapshots/lookloaded.golden index 8883609a..462affe8 100644 --- a/test/snapshots/lookloaded.golden +++ b/test/snapshots/lookloaded.golden @@ -1,12 +1,13 @@ -== snap file-open grid=100x31 cursor=7,12 +== snap file-open grid=100x31 cursor=14,13 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M +| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save /tmp/pardes-snap/lookloaded/cwd Tty+bash Save +| Mode Filter Collapse Del Mode Filter Collapse Del | ls ls | big.txt big.txt | -| /tmp/pardes-snap/lookloaded/cwd/big.txt Save Tt -| 35 line 35 +| /tmp/pardes-snap/lookloaded/cwd/big.txt Save T +| ty Collapse Del | 36 line 36 | 37 line 37 | 38 line 38 @@ -15,21 +16,20 @@ | 41 line 41 | 42 line 42 | 43 line 43 -| 44 line 44 -| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save M +| /tmp/pardes-snap/lookloaded/cwd Tty+bash Save +| Mode Filter Collapse Del | ls | big.txt -== snap removed grid=100x31 cursor=54,7 -|5: rm big.txt; echo GO''NE -|6: /tmp/pardes-snap/lookloaded/cwd/big.txt Save Tt GONE -== snap look-noFS grid=100x31 cursor=7,12 -|7: 5 line 5 -|8: 6 line 6 big.txt:10 -|9: 7 line 7 -|10: 8 line 8 -|11: 9 line 9 -|12: 10 line 10 -|13: 11 line 11 -|14: 12 line 12 -|15: 13 line 13 -|16: 14 line 14 +== snap removed grid=100x31 cursor=54,8 +|6: rm big.txt; echo GO''NE +|7: /tmp/pardes-snap/lookloaded/cwd/big.txt Save T GONE +== snap look-noFS grid=100x31 cursor=14,13 +|8: ty Collapse Del big.txt:10 +|9: 6 line 6 +|10: 7 line 7 +|11: 8 line 8 +|12: 9 line 9 +|13: 10 line 10 +|14: 11 line 11 +|15: 12 line 12 +|16: 13 line 13 diff --git a/test/snapshots/lookpanes.golden b/test/snapshots/lookpanes.golden index 61b658c0..79cbb368 100644 --- a/test/snapshots/lookpanes.golden +++ b/test/snapshots/lookpanes.golden @@ -1,41 +1,47 @@ == snap shells grid=130x51 cursor=122,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/lookpanes/cwd/beta/bmark.txt Save Tty Collaps /tmp/pardes-snap/lookpanes/cwd Tty+bash Save Mode Filter Colla -| 1 both.txt ls -| 2 b-marker alpha beta zzboot.txt zqonly.txt pick.txt twin.txt +| /tmp/pardes-snap/lookpanes/cwd/beta/bmark.txt Save Tty Collap /tmp/pardes-snap/lookpanes/cwd Tty+bash Save Mode Filter Coll +| se Del apse Del +| 1 both.txt ls zqonly.txt pick.txt twin.txt +| 2 b-marker alpha beta zzboot.txt | -| /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filter +| /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filte +| r Collapse Del | ls | amark.txt both.txt pick.txt twin.txt zqonly.txt -== snap other-dir grid=130x51 cursor=7,6 -|5: /tmp/pardes-snap/lookpanes/cwd/alpha/zqonly.txt Save Tty Colla -|6: 1 ONLY-IN-ALPHA /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filter -== snap clicked-wins grid=130x51 cursor=7,9 -|8: /tmp/pardes-snap/lookpanes/cwd/beta/both.txt Save Tty Collapse amark.txt both.txt pick.txt twin.txt zqonly.txt -|9: 1 BOTH-FROM-BETA -== snap mru-alpha grid=130x51 cursor=72,11 -|10: /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collaps -|11: 1 PICK-FROM-ALPHA -== snap mru-beta grid=130x51 cursor=7,12 -|11: /tmp/pardes-snap/lookpanes/cwd/beta/twin.txt Save Tty Collapse 1 PICK-FROM-ALPHA -|12: 1 TWIN-FROM-BETA +== snap other-dir grid=130x51 cursor=7,8 +|6: /tmp/pardes-snap/lookpanes/cwd/alpha/zqonly.txt Save Tty Coll +|7: apse Del /tmp/pardes-snap/lookpanes/cwd/alpha Tty+bash Save Mode Filte +|8: 1 ONLY-IN-ALPHA r Collapse Del +== snap clicked-wins grid=130x51 cursor=7,11 +|9: /tmp/pardes-snap/lookpanes/cwd/beta/both.txt Save Tty Collaps ls +|10: e Del amark.txt both.txt pick.txt twin.txt zqonly.txt +|11: 1 BOTH-FROM-BETA +== snap mru-alpha grid=130x51 cursor=72,14 +|12: /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collap +|13: se Del +|14: 1 PICK-FROM-ALPHA +== snap mru-beta grid=130x51 cursor=7,14 +|12: /tmp/pardes-snap/lookpanes/cwd/beta/twin.txt Save Tty Collaps /tmp/pardes-snap/lookpanes/cwd/alpha/pick.txt Save Tty Collap +|13: e Del se Del +|14: 1 TWIN-FROM-BETA 1 PICK-FROM-ALPHA == style mru-beta grid=130x51 |0: 0-70 #b2c3b8,#1d2824, 71-129 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-64 d,#26362d, 65-66 #ffffff,#887455, 67-67 d,#1d2824, 68-99 #b2c3b8,#1d2824, 100-129 d,#1d2824, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-47 #d3b0d9,#1d2824, 48-64 #b2c3b8,#1d2824, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-98 #b2c3b8,#1d2824, 99-106 #d3b0d9,#1d2824, 107-129 #b2c3b8,#1d2824, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-47 #d3b0d9,#1d2824, 48-63 #b2c3b8,#1d2824, 64-64 d,#1d2824, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-98 #b2c3b8,#1d2824, 99-106 #d3b0d9,#1d2824, 107-128 #b2c3b8,#1d2824, 129-129 d,#1d2824, +|3: 0-2 d,#1d2824, 3-8 #b2c3b8,#1d2824, 9-67 d,#1d2824, 68-75 #b2c3b8,#1d2824, 76-129 d,#1d2824, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-14 #e5eee5,#0d1410, 15-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-121 #e5eee5,#0d1410, 122-129 d,#0d1410, -|5: 0-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, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-64 d,#0d1410, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824, -|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-129 #e5eee5,#0d1410, -|8: 0-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, -|9: 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, -|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-129 #b2c3b8,#1d2824, -|11: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-38 #becfc4,#26362d, 39-46 #dfbce5,#26362d, 47-64 #becfc4,#26362d, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-86 #e5eee5,#0d1410, 87-129 d,#0d1410, -|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, -|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, -|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-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, +|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-129 d,#0d1410, +|12: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-38 #becfc4,#26362d, 39-46 #dfbce5,#26362d, 47-63 #becfc4,#26362d, 64-64 d,#26362d, 65-66 #ffffff,#496656, 67-67 d,#1d2824, 68-104 #b2c3b8,#1d2824, 105-112 #d3b0d9,#1d2824, 113-128 #b2c3b8,#1d2824, 129-129 d,#1d2824, +|13: 0-2 d,#26362d, 3-7 #becfc4,#26362d, 8-64 d,#26362d, 65-67 d,#1d2824, 68-73 #b2c3b8,#1d2824, 74-129 d,#1d2824, +|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-20 #e5eee5,#0d1410, 21-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-86 #e5eee5,#0d1410, 87-129 d,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, |17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-64 d,#0d1410, 65-65 d,#608269, 66-66 d,#0d1410, 67-71 #59675c,#0d1410, 72-129 d,#0d1410, diff --git a/test/snapshots/lookpanes.snap b/test/snapshots/lookpanes.snap index 8ada3f1b..e1db907a 100644 --- a/test/snapshots/lookpanes.snap +++ b/test/snapshots/lookpanes.snap @@ -35,8 +35,8 @@ key space w h stable 400 5000 # an ordinary dir look on the shell's own ls output: a shell in ./alpha, # split below it -press right 70 5 -release right 70 5 +press right 70 6 +release right 70 6 wait 10000 amark.txt stable 700 20000 # type the words to look into what is left of the workdir shell (past its own @@ -51,8 +51,8 @@ settle 100 stable 400 5000 snap shells # make the beta pane the most recently focused, so the walk has to pass it -press left 10 4 -release left 10 4 +press left 10 5 +release left 10 5 stable 700 10000 # zqonly.txt is only under alpha: the clicked shell's cwd fails, beta fails, # the alpha shell answers. Before, this word became a search. @@ -64,15 +64,15 @@ snap other-dir # both.txt is in BOTH dirs and alpha is now the most recent (its file pane # just opened), but the CLICK still wins: looked inside the beta pane it # opens beta's copy -press right 10 4 -release right 10 4 +press right 10 5 +release right 10 5 wait 10000 BOTH-FROM-BETA stable 700 20000 snap clicked-wins # from a pane whose own dir holds neither copy the access order decides: # focus the alpha shell, look pick.txt (in both dirs) -> alpha's -press left 70 13 -release left 70 13 +press left 70 14 +release left 70 14 stable 700 10000 press right 109 5 release right 109 5 @@ -81,8 +81,8 @@ stable 700 20000 snap mru-alpha # focus the beta pane instead and the same look flips: twin.txt is in both # dirs, beta is most recent, beta's copy opens -press left 10 4 -release left 10 4 +press left 10 5 +release left 10 5 stable 700 10000 press right 118 5 release right 118 5 diff --git a/test/snapshots/lsp-client-edit.golden b/test/snapshots/lsp-client-edit.golden index 81079615..29ec05af 100644 --- a/test/snapshots/lsp-client-edit.golden +++ b/test/snapshots/lsp-client-edit.golden @@ -1,7 +1,7 @@ == snap formatted grid=100x25 cursor=7,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/lsp-client-edit/cwd/main.rs * Save Tty Collapse Del TreeContext +| * /tmp/pardes-snap/lsp-client-edit/cwd/main.rs Save Tty Collapse Del TreeContext | 1 // formatted by mock formatted 1 range(s) | 2 mod lib; | 3 @@ -23,7 +23,7 @@ |10: 8 |11: == snap renamed grid=100x25 cursor=12,5 -|2: /tmp/pardes-snap/lsp-client-edit/cwd/main.rs * Save Tty Collapse Del TreeContext +|2: * /tmp/pardes-snap/lsp-client-edit/cwd/main.rs Save Tty Collapse Del TreeContext |3: 1 mod lib; renamed 2 range(s) |6: 4 let x = sum(1, 2); |7: 5 let y = sum(3, 4); diff --git a/test/snapshots/lsp-rename.golden b/test/snapshots/lsp-rename.golden index 26571b26..03fb2311 100644 --- a/test/snapshots/lsp-rename.golden +++ b/test/snapshots/lsp-rename.golden @@ -1,7 +1,7 @@ == snap renamed grid=110x25 cursor=17,10 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/lsp-rename/cwd/rename.zig * Save Tty Collapse Del TreeContext +| * /tmp/pardes-snap/lsp-rename/cwd/rename.zig Save Tty Collapse Del TreeContext | 1 const std = @import("std"); renamed 2 range(s) | 2 | 3 fn renamed_helper(x: u32) u32 { @@ -23,6 +23,6 @@ |5: 3 fn helper(x: u32) u32 { |10: 8 _ = helper(41); == snap redone grid=110x25 cursor=17,10 -|2: /tmp/pardes-snap/lsp-rename/cwd/rename.zig * Save Tty Collapse Del TreeContext +|2: * /tmp/pardes-snap/lsp-rename/cwd/rename.zig Save Tty Collapse Del TreeContext |5: 3 fn renamed_helper(x: u32) u32 { |10: 8 _ = renamed_helper(41); diff --git a/test/snapshots/lspcomplete.golden b/test/snapshots/lspcomplete.golden index 8a1c916c..0b29034f 100644 --- a/test/snapshots/lspcomplete.golden +++ b/test/snapshots/lspcomplete.golden @@ -16,7 +16,7 @@ | 12 // note. see below | 13 == snap indent grid=120x41 cursor=11,12 -|2: ^ /tmp/pardes-snap/lspcomplete/cwd/a.zig * Save Tty Collapse Del TreeContext +|2: ^* /tmp/pardes-snap/lspcomplete/cwd/a.zig Save Tty Collapse Del TreeContext |12: 10 } == snap commentindent grid=120x41 cursor=19,14 |14: 12 // note. see below @@ -29,17 +29,17 @@ |18: 3 == snap refill grid=120x41 cursor=16,10 == snap step grid=120x41 cursor=19,4 -|2: /tmp/pardes-snap/lspcomplete/cwd/a.zig * Save Tty Collapse Del TreeContext +|2: * /tmp/pardes-snap/lspcomplete/cwd/a.zig Save Tty Collapse Del TreeContext == snap txtindent grid=120x41 cursor=15,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|^ /tmp/pardes-snap/lspcomplete/cwd/notes.txt * Save Tty Collapse Del +|^* /tmp/pardes-snap/lspcomplete/cwd/notes.txt Save Tty Collapse Del | 1 abc. def ghi | 2 == snap nosemi grid=100x19 cursor=29,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|^ /tmp/pardes-snap/lspcomplete/cwd/c.zig * Save Tty Collapse Del TreeContext +|^* /tmp/pardes-snap/lspcomplete/cwd/c.zig Save Tty Collapse Del TreeContext | 1 const Shade = enum { | 2 verdigris, | 3 cinnabar, @@ -55,7 +55,7 @@ == snap nobrace grid=100x19 cursor=16,10 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|^ /tmp/pardes-snap/lspcomplete/cwd/d.zig * Save Tty Collapse Del TreeContext +|^* /tmp/pardes-snap/lspcomplete/cwd/d.zig Save Tty Collapse Del TreeContext | 1 const Shade = enum { | 2 verdigris, | 3 cinnabar, diff --git a/test/snapshots/lspcompletemoved.golden b/test/snapshots/lspcompletemoved.golden index f12b3f0c..a033ed56 100644 --- a/test/snapshots/lspcompletemoved.golden +++ b/test/snapshots/lspcompletemoved.golden @@ -1,7 +1,7 @@ == snap typedthrough grid=120x21 cursor=16,14 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|^ /tmp/pardes-snap/lspcompletemoved/cwd/a.zig * Save Tty Collapse Del TreeContext +|^* /tmp/pardes-snap/lspcompletemoved/cwd/a.zig Save Tty Collapse Del TreeContext | 1 const Shade = enum { | 2 verdigris, | 3 cinnabar, diff --git a/test/snapshots/lsprelpath.golden b/test/snapshots/lsprelpath.golden index 9f08b7b4..c8121b5a 100644 --- a/test/snapshots/lsprelpath.golden +++ b/test/snapshots/lsprelpath.golden @@ -1,7 +1,7 @@ == snap absrow grid=100x35 cursor=16,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|^ /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig * Save Tty Collapse Del TreeContext +|^* /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig Save Tty Collapse Del TreeContext | 1 const shade = @import("../shade.zig"); | 2 | 3 pub fn pick(s: shade.Shade) u8 { @@ -14,7 +14,7 @@ | 2 /tmp/pardes-snap/lsprelpath/cwd/shade.zig:3:5-12 cinnabar cinnabar, | 3 == snap abslook grid=100x35 cursor=19,15 -|2: /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig * Save Tty Collapse Del TreeContext +|2: * /tmp/pardes-snap/lsprelpath/cwd/sub/parent.zig Save Tty Collapse Del TreeContext |13: /tmp/pardes-snap/lsprelpath/cwd/shade.zig Save Tty Collapse Del TreeContext |14: 1 pub const Shade = enum { |15: 2 verdigris, @@ -24,7 +24,7 @@ == style abslook grid=100x35 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-99 d,#26362d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, +|2: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-82 #b2c3b8,#1d2824, 83-99 d,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-20 #e5eee5,#0d1410, 21-27 #c9a7ed,#0d1410, 28-28 #e5eee5,#0d1410, 29-42 #b8d88a,#0d1410, 43-44 #e5eee5,#0d1410, 45-99 d,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-12 #c9a7ed,#0d1410, 13-38 #e5eee5,#0d1410, 39-99 d,#0d1410, @@ -60,7 +60,7 @@ == snap subrow grid=100x35 cursor=16,7 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -|^ /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig * Save Tty Collapse Del TreeContext +|^* /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig Save Tty Collapse Del TreeContext | 1 const tint = @import("inner/tint.zig"); | 2 | 3 pub fn hue(t: tint.Tint) u8 { @@ -73,7 +73,7 @@ | 2 inner/tint.zig:3:5-10 indigo indigo, | 3 == snap sublook grid=100x35 cursor=15,15 -|2: /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig * Save Tty Collapse Del TreeContext +|2: * /tmp/pardes-snap/lsprelpath/cwd/sub/subdir.zig Save Tty Collapse Del TreeContext |13: /tmp/pardes-snap/lsprelpath/cwd/sub/inner/tint.zig Save Tty Collapse Del TreeContext |14: 1 pub const Tint = enum { |15: 2 ochre, @@ -83,7 +83,7 @@ == style sublook grid=100x35 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-99 d,#26362d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, +|2: 0-0 #ffffff,#496656, 1-1 #000000,#f9cb70,b 2-2 d,#1d2824, 3-38 #b2c3b8,#1d2824, 39-48 #d3b0d9,#1d2824, 49-82 #b2c3b8,#1d2824, 83-99 d,#1d2824, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-19 #e5eee5,#0d1410, 20-26 #c9a7ed,#0d1410, 27-27 #e5eee5,#0d1410, 28-43 #b8d88a,#0d1410, 44-45 #e5eee5,#0d1410, 46-99 d,#0d1410, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #e5eee5,#0d1410, 11-12 #c9a7ed,#0d1410, 13-35 #e5eee5,#0d1410, 36-99 d,#0d1410, diff --git a/test/snapshots/modal.golden b/test/snapshots/modal.golden index 0147d5c8..2a329bf8 100644 --- a/test/snapshots/modal.golden +++ b/test/snapshots/modal.golden @@ -1,7 +1,8 @@ -== snap top grid=100x31 cursor=2,3 +== snap top grid=100x31 cursor=2,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F +| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode /tmp/pardes-snap/modal/cwd Tty+bash Save Mode +| Filter Collapse Del Filter Collapse Del | ls ls | words.txt words.txt | printf 'one two three\nfour five six\nseven ei @@ -15,35 +16,35 @@ | | | -| -| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode F +| /tmp/pardes-snap/modal/cwd Tty+bash Save Mode +| Filter Collapse Del | ls | words.txt -== snap moved grid=100x31 cursor=8,5 -== snap xsel grid=100x31 cursor=48,5 +== snap moved grid=100x31 cursor=8,6 +== snap xsel grid=100x31 cursor=48,6 == style xsel grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-99 #b2c3b8,#1d2824, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-98 #b2c3b8,#1d2824, 99-99 d,#1d2824, +|3: 0-2 d,#26362d, 3-21 #becfc4,#26362d, 22-49 d,#26362d, 50-52 d,#1d2824, 53-71 #b2c3b8,#1d2824, 72-99 d,#1d2824, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-48 #f5f7ee,#3c614a, 49-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-48 #f5f7ee,#3c614a, 49-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, |7: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |9: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|10: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|11: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |12: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |13: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|18: 0-2 d,#1d2824, 3-21 #b2c3b8,#1d2824, 22-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|21: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |22: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, @@ -53,17 +54,17 @@ |28: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |29: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |30: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -== snap xsel2 grid=100x31 cursor=13,6 -== snap yanked grid=100x31 cursor=13,6 -== snap vsel grid=100x31 cursor=14,7 +== snap xsel2 grid=100x31 cursor=13,7 +== snap yanked grid=100x31 cursor=13,7 +== snap vsel grid=100x31 cursor=14,8 == style vsel grid=100x31 -|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #f5f7ee,#3c614a, 50-50 d,#608269, 51-99 d,#0d1410, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #f5f7ee,#3c614a, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-14 #f5f7ee,#3c614a, 15-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -== snap typed grid=100x31 cursor=9,20 -|20: TYPED -== snap undone grid=100x31 cursor=4,20 -|20: -== snap redone grid=100x31 cursor=9,20 -|20: TYPED -== snap bottom grid=100x31 cursor=2,20 +|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #f5f7ee,#3c614a, 50-50 d,#608269, 51-99 d,#0d1410, +|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #f5f7ee,#3c614a, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-14 #f5f7ee,#3c614a, 15-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +== snap typed grid=100x31 cursor=9,21 +|21: TYPED +== snap undone grid=100x31 cursor=4,21 +|21: +== snap redone grid=100x31 cursor=9,21 +|21: TYPED +== snap bottom grid=100x31 cursor=2,21 diff --git a/test/snapshots/msg.golden b/test/snapshots/msg.golden index d9a4a079..ff7b39a0 100644 --- a/test/snapshots/msg.golden +++ b/test/snapshots/msg.golden @@ -45,7 +45,7 @@ |3: 1 AGAIN |4: 2 == snap msgbuiltin grid=70x17 cursor=29,5 -|2: /tmp/pardes-snap/msg/cwd/a.txt * Save Tty Collapse Del +|2: * /tmp/pardes-snap/msg/cwd/a.txt Save Tty Collapse Del |3: 1 AGAIN hand-written report |5: 3 Msg hand-written report == snap msgtail grid=70x17 cursor=32,7 diff --git a/test/snapshots/multicursor.golden b/test/snapshots/multicursor.golden index 96762bd5..a32a7fc3 100644 --- a/test/snapshots/multicursor.golden +++ b/test/snapshots/multicursor.golden @@ -52,13 +52,13 @@ |29: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |30: 0-0 d,#608269, 1-1 d,#0d1410, 2-74 #e5eee5,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, == snap edited-at-three grid=150x31 cursor=9,9 -|6: /tmp/pardes-snap/multicursor/cwd/f.txt * Save Tty Collapse Del +|6: * /tmp/pardes-snap/multicursor/cwd/f.txt Save Tty Collapse Del |7: 1 >>line 1 |8: 2 >>line 2 |9: 3 >>line 3 == snap three-selections grid=150x31 cursor=8,9 == style three-selections grid=150x31 -|6: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-35 #becfc4,#26362d, 36-40 #dfbce5,#26362d, 41-64 #becfc4,#26362d, 65-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, +|6: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-35 #becfc4,#26362d, 36-40 #dfbce5,#26362d, 41-62 #becfc4,#26362d, 63-74 d,#26362d, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #f5f7ee,#243a2d, 8-8 #3c614a,#f5f7ee, 9-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #f5f7ee,#243a2d, 8-8 #3c614a,#f5f7ee, 9-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, |9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-8 #f5f7ee,#3c614a, 9-14 #e5eee5,#0d1410, 15-74 d,#0d1410, 75-75 d,#608269, 76-76 d,#0d1410, 77-149 #e5eee5,#0d1410, diff --git a/test/snapshots/ninep.golden b/test/snapshots/ninep.golden index 65fb38ec..82bdb39c 100644 --- a/test/snapshots/ninep.golden +++ b/test/snapshots/ninep.golden @@ -73,7 +73,7 @@ |7: 5 return 7; @7: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-19 #e5eee5,#0d1410,,body 20-89 d,#0d1410,,body == snap edited-with-colors-off grid=90x15 cursor=7,4 -|2: /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext +|2: * /tmp/pardes-snap/ninep/cwd/demo.zig Save Tty Collapse Del TreeContext |3: 1 // café λ 界 |4: 2 const greeting = "café λ 界 é"; |5: 3 @@ -83,8 +83,8 @@ |9: 7 } |10: 8 == 9p edited-with-colors-off grid=90x15 cursor=7,4,block -|2: /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext -@2: 0-1 #000000,#add38a,,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-73 #becfc4,#26362d,,tagline 74-89 d,#26362d,,tagline +|2: * /tmp/pardes-snap/ninep/cwd/demo.zig Save Tty Collapse Del TreeContext +@2: 0-0 #000000,#add38a,,tagline 1-1 #000000,#f9cb70,b,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-71 #becfc4,#26362d,,tagline 72-89 d,#26362d,,tagline |3: 1 // café λ 界 +3: 13="é" 15="λ" 17="界" 18="" @3: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-18 #e5eee5,#0d1410,,body 19-89 d,#0d1410,,body @@ -104,7 +104,7 @@ |10: 8 @10: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-89 d,#0d1410,,body == style colors-on grid=90x15 -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-73 #becfc4,#26362d, 74-89 d,#26362d, +|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-71 #becfc4,#26362d, 72-89 d,#26362d, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #b9f480,#0d1410, 18-18 d,d, 19-81 d,#0d1410, 82-82 d,#1d2824, 83-88 #b2c3b8,#1d2824, 89-89 d,#1d2824, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-23 #e5eee5,#0d1410, 24-32 #b8d88a,#0d1410, 33-33 d,d, 34-36 #b8d88a,#0d1410, 37-37 #e5eee5,#0d1410, 38-89 d,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-89 d,#0d1410, @@ -128,7 +128,7 @@ == style resized grid=105x20 |0: 0-70 #b2c3b8,#1d2824, 71-104 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-104 d,#26362d, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-73 #becfc4,#26362d, 74-104 d,#26362d, +|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-71 #becfc4,#26362d, 72-104 d,#26362d, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-17 #b9f480,#0d1410, 18-18 d,d, 19-96 d,#0d1410, 97-97 d,#1d2824, 98-103 #b2c3b8,#1d2824, 104-104 d,#1d2824, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #616f64,#0d1410,b 7-23 #e5eee5,#0d1410, 24-32 #b8d88a,#0d1410, 33-33 d,d, 34-36 #b8d88a,#0d1410, 37-37 #e5eee5,#0d1410, 38-104 d,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-104 d,#0d1410, @@ -151,8 +151,8 @@ @0: 0-70 #b2c3b8,#1d2824,,tagline 71-104 d,#1d2824,,tagline |1: New Tty Find Grep Joincol Delcol @1: 0-1 #ffffff,#887455,,tagline 2-2 d,#26362d,,tagline 3-34 #becfc4,#26362d,,tagline 35-104 d,#26362d,,tagline -|2: /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext -@2: 0-1 #000000,#add38a,,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-73 #becfc4,#26362d,,tagline 74-104 d,#26362d,,tagline +|2: * /tmp/pardes-snap/ninep/cwd/demo.zig Save Tty Collapse Del TreeContext +@2: 0-0 #000000,#add38a,,tagline 1-1 #000000,#f9cb70,b,tagline 2-2 d,#26362d,,tagline 3-29 #becfc4,#26362d,,tagline 30-37 #dfbce5,#26362d,,tagline 38-71 #becfc4,#26362d,,tagline 72-104 d,#26362d,,tagline |3: 1 // café λ 界 Colors +3: 13="é" 15="λ" 17="界" 18="" @3: 0-0 d,#608269,,body 1-1 d,#0d1410,,body 2-6 #59675c,#0d1410,,body 7-18 #b9f480,#0d1410,,body 19-96 d,#0d1410,,body 97-97 d,#1d2824,,tagline 98-103 #b2c3b8,#1d2824,,tagline 104-104 d,#1d2824,,tagline @@ -192,7 +192,7 @@ == snap resized grid=105x20 cursor=7,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ninep/cwd/demo.zig * Save Tty Collapse Del TreeContext +| * /tmp/pardes-snap/ninep/cwd/demo.zig Save Tty Collapse Del TreeContext | 1 // café λ 界 Colors | 2 const greeting = "café λ 界 é"; | 3 diff --git a/test/snapshots/pipe.golden b/test/snapshots/pipe.golden index 6b7347bf..b7a8da86 100644 --- a/test/snapshots/pipe.golden +++ b/test/snapshots/pipe.golden @@ -5,7 +5,7 @@ | 1 alpha beta |tr a-z A-Z | 2 gamma delta == snap filtered grid=180x13 cursor=17,3 -|2: /tmp/pardes-snap/pipe/cwd/pipe.txt * Save Tty Collapse Del +|2: * /tmp/pardes-snap/pipe/cwd/pipe.txt Save Tty Collapse Del |3: 1 ALPHA BETA == snap undone grid=180x13 cursor=17,3 |2: /tmp/pardes-snap/pipe/cwd/pipe.txt Save Tty Collapse Del diff --git a/test/snapshots/respawn.golden b/test/snapshots/respawn.golden index d6d21a81..e8f94978 100644 --- a/test/snapshots/respawn.golden +++ b/test/snapshots/respawn.golden @@ -1,10 +1,12 @@ -== snap respawn grid=140x31 cursor=4,9 +== snap respawn grid=140x31 cursor=4,11 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del +| /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse De /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse De +| l l | ls ls | -|$ /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse Del +|$ /tmp/pardes-snap/respawn/cwd Tty+bash Save Mode Filter Collapse De +| l | $ ls | $ echo RESP''AWNOK | RESPAWNOK diff --git a/test/snapshots/respawn.snap b/test/snapshots/respawn.snap index eac74267..e0ebea13 100644 --- a/test/snapshots/respawn.snap +++ b/test/snapshots/respawn.snap @@ -6,8 +6,8 @@ wait 8000 Newcol Joincol stable 700 20000 # middle-click Del on the lower-left pane's tag: the pane id is freed while # its shell process stays alive in the slot -press middle @Del 18 -release middle @Del 18 +press middle @Collapse+9 18 +release middle @Collapse+9 18 stable 700 10000 # Alt-n reuses the freed id -> spawn effect lands on the occupied slot key a-n diff --git a/test/snapshots/rsearch.golden b/test/snapshots/rsearch.golden index 872693bb..efbd81c9 100644 --- a/test/snapshots/rsearch.golden +++ b/test/snapshots/rsearch.golden @@ -23,7 +23,7 @@ |17: 1 code.txt:4:1-8 |18: 2 pick two words |19: 3 -== snap path-open grid=210x31 cursor=7,22 +== 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 diff --git a/test/snapshots/savepane.golden b/test/snapshots/savepane.golden index 06e69790..8660a437 100644 --- a/test/snapshots/savepane.golden +++ b/test/snapshots/savepane.golden @@ -16,7 +16,7 @@ | | | -| /tmp/pardes-snap/savepane/cwd/+New * Save Tty Collapse Del +| * /tmp/pardes-snap/savepane/cwd/+New Save Tty Collapse Del | 1 scratch bytes == snap prompt grid=100x31 cursor=99,19 |18: 1 scratch bytes Save diff --git a/test/snapshots/scroll.golden b/test/snapshots/scroll.golden index 5960bdc5..19db2e0a 100644 --- a/test/snapshots/scroll.golden +++ b/test/snapshots/scroll.golden @@ -2,9 +2,9 @@ |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/scroll/cwd Tty+bash Save Mode /tmp/pardes-snap/scroll/cwd Tty+bash Save Mode -| 188 ls -| 189 s.txt -| 190 +| Filter Collapse Del Filter Collapse Del +| 189 ls +| 190 s.txt | 191 | 192 | 193 @@ -17,12 +17,12 @@ | 200 | | /tmp/pardes-snap/scroll/cwd Tty+bash Save Mode +| Filter Collapse Del | ls | s.txt == snap wheel3 grid=100x31 cursor=4,16 -|3: 185 ls -|4: 186 s.txt -|5: 187 +|4: 186 ls +|5: 187 s.txt |6: 188 |7: 189 |8: 190 @@ -35,9 +35,8 @@ |15: 197 |16: 198 == snap gutter-up grid=100x31 cursor=4,16 -|3: 178 ls -|4: 179 s.txt -|5: 180 +|4: 179 ls +|5: 180 s.txt |6: 181 |7: 182 |8: 183 @@ -50,9 +49,8 @@ |15: 190 |16: 191 == snap gutter-down grid=100x31 cursor=4,16 -|3: 180 ls -|4: 181 s.txt -|5: 182 +|4: 181 ls +|5: 182 s.txt |6: 183 |7: 184 |8: 185 @@ -65,9 +63,8 @@ |15: 192 |16: 193 == snap wheel-down grid=100x31 cursor=4,16 -|3: 181 ls -|4: 182 s.txt -|5: 183 +|4: 182 ls +|5: 183 s.txt |6: 184 |7: 185 |8: 186 @@ -82,11 +79,11 @@ == style wheel-down grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-30 #becfc4,#26362d, 31-38 #dfbce5,#26362d, 39-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-80 #b2c3b8,#1d2824, 81-88 #d3b0d9,#1d2824, 89-99 #b2c3b8,#1d2824, -|3: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-30 #becfc4,#26362d, 31-38 #dfbce5,#26362d, 39-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-80 #b2c3b8,#1d2824, 81-88 #d3b0d9,#1d2824, 89-98 #b2c3b8,#1d2824, 99-99 d,#1d2824, +|3: 0-2 d,#26362d, 3-22 #becfc4,#26362d, 23-49 d,#26362d, 50-52 d,#1d2824, 53-72 #b2c3b8,#1d2824, 73-99 d,#1d2824, |4: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|5: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, -|6: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|5: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|6: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, |7: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |8: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |9: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, @@ -97,11 +94,11 @@ |14: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |16: 0-0 d,#152019, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-30 #b2c3b8,#1d2824, 31-38 #d3b0d9,#1d2824, 39-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|18: 0-2 d,#1d2824, 3-22 #b2c3b8,#1d2824, 23-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|21: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |22: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, diff --git a/test/snapshots/scroll.snap b/test/snapshots/scroll.snap index d8d3e665..4b723756 100644 --- a/test/snapshots/scroll.snap +++ b/test/snapshots/scroll.snap @@ -17,12 +17,12 @@ wheel up 10 8 wheel up 10 8 stable 500 8000 snap wheel3 -press left 1 11 -release left 1 11 +press left 1 12 +release left 1 12 stable 500 8000 snap gutter-up -press right 1 6 -release right 1 6 +press right 1 7 +release right 1 7 stable 500 8000 snap gutter-down wheel down 10 8 diff --git a/test/snapshots/selarg.golden b/test/snapshots/selarg.golden index cc4cdec1..ef240e31 100644 --- a/test/snapshots/selarg.golden +++ b/test/snapshots/selarg.golden @@ -1,7 +1,8 @@ -== snap kept grid=100x31 cursor=6,3 +== snap kept grid=100x31 cursor=6,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/selarg/cwd Tty+bash Save Mode /tmp/pardes-snap/selarg/cwd Tty+bash Save Mode +| Filter Collapse Del Filter Collapse Del | ZZARG ZZVEE ./c.sh | | @@ -15,13 +16,13 @@ | | | -| | /tmp/pardes-snap/selarg/cwd Tty+bash Save Mode +| Filter Collapse Del | ls | s.txt -== snap crosspane grid=100x31 cursor=6,3 -|4: ./c.sh ZZARG -|5: CHORDARG:ZZARG -== snap vselarg grid=100x31 cursor=12,3 -|6: ./c.sh ZZVEE -|7: CHORDARG:ZZVEE +== snap crosspane grid=100x31 cursor=6,4 +|5: ./c.sh ZZARG +|6: CHORDARG:ZZARG +== snap vselarg grid=100x31 cursor=12,4 +|7: ./c.sh ZZVEE +|8: CHORDARG:ZZVEE diff --git a/test/snapshots/selarg.snap b/test/snapshots/selarg.snap index 1df7ee12..8aa59bbd 100644 --- a/test/snapshots/selarg.snap +++ b/test/snapshots/selarg.snap @@ -16,8 +16,8 @@ stable 700 15000 key c-b stable 600 8000 # pane B (top right): create the arg-printing script and show its path -press left 60 6 -release left 60 6 +press left 60 7 +release left 60 7 stable 400 5000 key c-b stable 600 8000 @@ -28,31 +28,31 @@ stable 700 15000 key c-b stable 600 8000 # keep a left selection over ZZARG in pane A (this also focuses A) -press left 3 4 -drag left 7 4 -release left 7 4 +press left 3 5 +drag left 7 5 +release left 7 5 stable 400 5000 snap kept # chord in pane B: middle on ./c.sh + left tap (B has NO selection: the # argument comes from pane A's kept left selection) + middle release runs -press middle 55 4 -press left 55 4 -release middle 55 4 -release left 55 4 +press middle 55 5 +press left 55 5 +release middle 55 5 +release left 55 5 wait 10000 CHORDARG:ZZARG stable 700 10000 snap crosspane # now an explicit modal selection: plain-click ZZVEE in A (drops A's kept # left selection), select it with v e, chord in B again -press left 9 4 -release left 9 4 +press left 9 5 +release left 9 5 stable 400 5000 text ve stable 400 5000 -press middle 55 4 -press left 55 4 -release middle 55 4 -release left 55 4 +press middle 55 5 +press left 55 5 +release middle 55 5 +release left 55 5 wait 10000 CHORDARG:ZZVEE stable 700 10000 snap vselarg diff --git a/test/snapshots/selregex.golden b/test/snapshots/selregex.golden index 7362a85a..9715a5ec 100644 --- a/test/snapshots/selregex.golden +++ b/test/snapshots/selregex.golden @@ -51,7 +51,7 @@ |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f5f7ee,#243a2d, 11-11 #3c614a,#f5f7ee, 12-18 #e5eee5,#0d1410, 19-19 #f8c082,#0d1410, 20-20 #e5eee5,#0d1410, 21-159 d,#0d1410, |5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-10 #f5f7ee,#243a2d, 11-11 #3c614a,#f5f7ee, 12-20 #e5eee5,#0d1410, 21-21 #f8c082,#0d1410, 22-22 #e5eee5,#0d1410, 23-159 d,#0d1410, == snap commented grid=160x25 cursor=14,3 -|2: /tmp/pardes-snap/selregex/cwd/m.zig * Save Tty Collapse Del TreeContext +|2: * /tmp/pardes-snap/selregex/cwd/m.zig Save Tty Collapse Del TreeContext |3: 1 // const one = 1; |4: 2 // const two = 2; |5: 3 // const three = 3; diff --git a/test/snapshots/shellset.golden b/test/snapshots/shellset.golden index 85fcb474..718842a4 100644 --- a/test/snapshots/shellset.golden +++ b/test/snapshots/shellset.golden @@ -5,8 +5,9 @@ | 1 Shell zznosuchshell == snap builtin-ran grid=90x21 cursor=25,3 |3: 1 Shell zznosuchshell Shell -== snap fallback-shell grid=90x21 cursor=49,5 +== snap fallback-shell grid=90x21 cursor=49,6 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/shellset/cwd/cmd.txt Save /tmp/pardes-snap/shellset/cwd Tty+bash Sav -|3: 1 Shell zznosuchshell ls -|4: cmd.txt zmarkfile.txt +|2: /tmp/pardes-snap/shellset/cwd/cmd.txt Sav /tmp/pardes-snap/shellset/cwd Tty+bash Sa +|3: e Tty Collapse Del ve Mode Filter Collapse Del +|4: 1 Shell zznosuchshell ls +|5: cmd.txt zmarkfile.txt diff --git a/test/snapshots/shrinktag.golden b/test/snapshots/shrinktag.golden index 8ed65105..70d36c6a 100644 --- a/test/snapshots/shrinktag.golden +++ b/test/snapshots/shrinktag.golden @@ -1,12 +1,12 @@ == snap before grid=100x31 cursor=2,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo +| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M +| ode Filter Collapse Del ode Filter Collapse Del | ls ls | w.txt w.txt | | -| | echo one; echo two | | @@ -16,29 +16,35 @@ | | | -| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo +| /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M +| ode Filter Collapse Del | ls | w.txt == snap shrunk grid=100x31 cursor=2,9 -|3: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo ls -|4: ls w.txt -|5: w.txt +|3: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M ode Filter Collapse Del +|4: ode Filter Collapse Del ls +|5: ls w.txt +|6: w.txt |8: |17: |18: |19: +|20: == snap move-preview grid=100x31 cursor=2,9 |8: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ == snap restored grid=100x31 cursor=2,9 -|3: ls ls -|4: w.txt w.txt -|5: +|3: ode Filter Collapse Del ode Filter Collapse Del +|4: ls ls +|5: w.txt w.txt +|6: |8: echo one; echo two -|17: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo -|18: ls -|19: w.txt +|17: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M +|18: ode Filter Collapse Del +|19: ls +|20: w.txt == snap bottom-shrunk grid=100x31 cursor=2,9 |17: |18: |19: -|30: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo +|20: +|30: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save M diff --git a/test/snapshots/splitparent.golden b/test/snapshots/splitparent.golden index 55488973..5159c1d6 100644 --- a/test/snapshots/splitparent.golden +++ b/test/snapshots/splitparent.golden @@ -1,26 +1,30 @@ -== snap shrunk grid=100x31 cursor=4,4 +== snap shrunk grid=100x31 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save -| /tmp/pardes-snap/splitparent/cwd Tty+bash Save ls +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del +| Mode Filter Collapse Del ls | ls -== snap swap-parent grid=100x31 cursor=4,8 +== snap swap-parent grid=100x31 cursor=4,9 +|5: |6: /tmp/pardes-snap/splitparent/cwd Tty+bash Save -|7: ls +|7: Mode Filter Collapse Del +|8: ls == snap tallest grid=100x6 cursor=54,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save | /tmp/pardes-snap/splitparent/cwd Tty+bash Save | /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save -== snap regrown grid=100x31 cursor=54,19 +== snap regrown grid=100x31 cursor=54,20 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save -| /tmp/pardes-snap/splitparent/cwd Tty+bash Save ls -| ls +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del +| Mode Filter Collapse Del ls | | /tmp/pardes-snap/splitparent/cwd Tty+bash Save +| Mode Filter Collapse Del | ls | | @@ -30,6 +34,6 @@ | | | -| | /tmp/pardes-snap/splitparent/cwd Tty+bash Save +| Mode Filter Collapse Del | ls diff --git a/test/snapshots/tagalign.golden b/test/snapshots/tagalign.golden index f4eae157..f6a8f9d5 100644 --- a/test/snapshots/tagalign.golden +++ b/test/snapshots/tagalign.golden @@ -11,20 +11,20 @@ == snap fits grid=87x31 cursor=4,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty Co +| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty C +| ollapse Del | 1 alpha | 2 beta -| 3 gamma | /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del | ls | deep == snap overby1 grid=86x31 cursor=4,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty C +| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/document.txt Save Tty +| Collapse Del | 1 alpha | 2 beta -| 3 gamma | /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del | ls | deep @@ -38,21 +38,23 @@ | /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse Del | ls | deep -== snap revealed grid=70x31 cursor=69,2 +== snap revealed grid=70x31 cursor=31,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exi | New Tty Find Grep Joincol Delcol -| d/deep/nested-project-directory/document.txt Save Tty Collapse Del +| /tmp/pardes-snap/tagalign/cwd/deep/nested-project-directory/docume +| nt.txt Save Tty Collapse Del | 1 alpha | 2 beta -| 3 gamma -| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse De +| /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse D +| el | ls | deep -== snap alone grid=70x31 cursor=4,5 -|2: /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse De -|3: ls -|4: deep -|5: +== snap alone grid=70x31 cursor=4,6 +|2: /tmp/pardes-snap/tagalign/cwd Tty+bash Save Mode Filter Collapse D +|3: el +|4: ls +|5: deep |6: |7: |8: +|9: diff --git a/test/snapshots/tagalign.snap b/test/snapshots/tagalign.snap index a77415d9..3bb3922e 100644 --- a/test/snapshots/tagalign.snap +++ b/test/snapshots/tagalign.snap @@ -32,7 +32,7 @@ key : key A stable 400 5000 snap revealed -press middle @Del 3 -release middle @Del 3 +press middle @Del 4 +release middle @Del 4 stable 700 15000 snap alone diff --git a/test/snapshots/tagalign2col.golden b/test/snapshots/tagalign2col.golden index 3ba64c2b..f5306f42 100644 --- a/test/snapshots/tagalign2col.golden +++ b/test/snapshots/tagalign2col.golden @@ -1,25 +1,26 @@ -== snap onecol grid=160x31 cursor=7,3 +== snap onecol grid=160x31 cursor=7,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse De /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del -| 1 alpha ls Left -| 2 beta deep +| /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse D /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del +| el ls Left +| 1 alpha deep +| 2 beta | 3 gamma -== snap aligned grid=160x31 cursor=4,9 -|3: 1 alpha ls -|6: /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del -|7: ls -|8: deep -== snap widened grid=160x31 cursor=4,9 +== snap aligned grid=160x31 cursor=4,10 +|3: el ls +|7: /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del +|8: ls +|9: deep +== snap widened grid=160x31 cursor=4,10 |1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse Del ╎ /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filt -|3: 1 alpha ╎ ls -|4: 2 beta ╎ deep -|5: 3 gamma ╎ -|6: /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del ╎ -|7: ls ╎ -|8: deep ╎ -|9: ╎ +|2: /tmp/pardes-snap/tagalign2col/cwd/deep/proj/document.txt Save Tty Collapse Del ╎ /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Fil +|3: 1 alpha ╎ ter Collapse Del +|4: 2 beta ╎ ls +|5: 3 gamma ╎ deep +|6: ╎ +|7: /tmp/pardes-snap/tagalign2col/cwd Tty+bash Save Mode Filter Collapse Del ╎ +|8: ls ╎ +|9: deep ╎ |10: ╎ |11: ╎ |12: ╎ diff --git a/test/snapshots/tagbottom.golden b/test/snapshots/tagbottom.golden index 851a8c06..a86bb8fb 100644 --- a/test/snapshots/tagbottom.golden +++ b/test/snapshots/tagbottom.golden @@ -164,7 +164,7 @@ |8: 7 mmm |9: 8 nnn |10: 9 ooo -|11: /tmp/pardes-snap/tagbottom/cwd/top.txt * Save Tty Collapse Del bottom.txt +|11: * /tmp/pardes-snap/tagbottom/cwd/top.txt Save Tty Collapse Del bottom.txt |12: 1 ZZONE == file cut-buffer aaa diff --git a/test/snapshots/tagcmd.golden b/test/snapshots/tagcmd.golden index 7853de32..8f1dc8d2 100644 --- a/test/snapshots/tagcmd.golden +++ b/test/snapshots/tagcmd.golden @@ -1,7 +1,7 @@ == snap edited grid=100x31 cursor=13,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/tagcmd/cwd/c.txt * Save Tty Collapse Del +| * /tmp/pardes-snap/tagcmd/cwd/c.txt Save Tty Collapse Del | 1 ZZEDITline 1 | 2 line 2 | 3 line 3 @@ -9,8 +9,8 @@ | 5 line 5 | 6 line 6 | 7 -== snap colon grid=100x31 cursor=39,2 -== snap word grid=100x31 cursor=43,2 +== snap colon grid=100x31 cursor=37,2 +== snap word grid=100x31 cursor=41,2 == snap saved grid=100x31 cursor=13,3 |2: /tmp/pardes-snap/tagcmd/cwd/c.txt Save Tty Collapse Del |3: 1 ZZEDITline 1 --:--:-- saved /tmp/pardes-snap/tagcmd/cwd/c.txt diff --git a/test/snapshots/tagnav.golden b/test/snapshots/tagnav.golden index 998a7a72..dd982322 100644 --- a/test/snapshots/tagnav.golden +++ b/test/snapshots/tagnav.golden @@ -1,54 +1,52 @@ -== snap start grid=100x31 cursor=7,3 +== snap start grid=100x31 cursor=7,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode +| /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Col /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode +| lapse Del Filter Collapse Del | 1 alpha ls | 2 beta w.txt | 3 gamma | /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode +| Filter Collapse Del | ls Up | w.txt -== snap colon grid=100x31 cursor=28,2 -|2: es-snap/tagnav/cwd/w.txt Save Tty Collapse Del /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode -|7: ls -== snap word grid=100x31 cursor=32,2 -== snap in-tag grid=100x31 cursor=34,2 -== snap right grid=100x31 cursor=54,5 -|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode -|3: 1 alpha Right ls -== snap right-tag grid=100x31 cursor=70,2 -|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll nav/cwd Tty+bash Save Mode Filter Collapse Del -|3: 1 alpha ls +== snap colon grid=100x31 cursor=37,2 +|9: ls +== snap word grid=100x31 cursor=41,2 +== snap in-tag grid=100x31 cursor=43,2 +== snap right grid=100x31 cursor=54,6 +|4: 1 alpha Right ls +== snap right-tag grid=100x31 cursor=90,2 +|4: 1 alpha ls == snap left-tag grid=100x31 cursor=43,2 -|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode -== snap back-to-body grid=100x31 cursor=7,3 -== snap tty-on grid=100x31 cursor=54,5 -|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll$ /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode -|3: 1 alpha $ ls -|5: 3 gamma $ +== snap back-to-body grid=100x31 cursor=7,4 +== snap tty-on grid=100x31 cursor=54,6 +|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Col $ /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode +|4: 1 alpha $ ls +|6: 3 gamma $ == snap tty-tag grid=100x31 cursor=95,2 -== snap tty-restored grid=100x31 cursor=54,5 +== snap tty-restored grid=100x31 cursor=54,6 == snap column-tag grid=100x31 cursor=3,1 -|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode -|3: 1 alpha Up ls -|5: 3 gamma +|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Col /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode +|4: 1 alpha Up ls +|6: 3 gamma == snap column-word grid=100x31 cursor=6,1 -|3: 1 alpha ls +|4: 1 alpha ls == snap workspace-tag grid=100x31 cursor=0,0 -|3: 1 alpha Up ls +|4: 1 alpha Up ls == snap topbar-dollar grid=100x31 cursor=70,0 -|3: 1 alpha ls +|4: 1 alpha ls == snap topbar-dump-word grid=100x31 cursor=46,0 -== snap topbar-exec grid=100x31 cursor=7,3 +== snap topbar-exec grid=100x31 cursor=7,4 |0: Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit Restore /tmp/pardes-snap/tag -|3: 1 alpha Dump ls +|4: 1 alpha Dump ls == snap topbar-restore-word grid=100x31 cursor=72,0 -|3: 1 alpha ls -== snap topbar-restored grid=100x31 cursor=7,3 +|4: 1 alpha ls +== snap topbar-restored grid=100x31 cursor=7,4 |0: Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit -|3: 1 alpha $ ls -|5: 3 gamma $ -|6: /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode ── restored history ── -|7: $ ls -|9: $ -|10: ── restored history ── +|4: 1 alpha $ ls +|6: 3 gamma $ +|7: /tmp/pardes-snap/tagnav/cwd Tty+bash Save Mode ── restored history ── +|9: $ ls +|11: $ +|12: ── restored history ── diff --git a/test/snapshots/tagpath.golden b/test/snapshots/tagpath.golden index ba22a859..30699276 100644 --- a/test/snapshots/tagpath.golden +++ b/test/snapshots/tagpath.golden @@ -43,15 +43,15 @@ |29: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410, |30: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410, == snap yanked-path grid=100x31 cursor=41,3 -|2: /tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del +|2: * /tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del |3: 1 l/tmp/pardes-snap/tagpath/cwd/c.txtine 1 == snap drag-cross grid=100x31 cursor=41,2 == style drag-cross grid=100x31 -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-10 #becfc4,#26362d, 11-41 #f5f7ee,#3c614a, 42-60 #becfc4,#26362d, 61-99 d,#26362d, +|2: 0-0 #000000,#add38a, 1-1 #000000,#f9cb70,b 2-2 d,#26362d, 3-10 #becfc4,#26362d, 11-41 #f5f7ee,#3c614a, 42-58 #becfc4,#26362d, 59-99 d,#26362d, |3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-41 #f5f7ee,#3c614a, 42-46 #e5eee5,#0d1410, 47-99 d,#0d1410, == snap name-draft grid=100x31 cursor=11,2 -|2: /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del +|2: * /renamed/tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del == snap name-cancelled grid=100x31 cursor=41,3 -|2: /tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del +|2: * /tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del == snap name-committed grid=100x31 cursor=41,3 -|2: /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del +|2: * /renamed/tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del diff --git a/test/snapshots/theme.golden b/test/snapshots/theme.golden index 4986eaae..a81c4232 100644 --- a/test/snapshots/theme.golden +++ b/test/snapshots/theme.golden @@ -1,7 +1,8 @@ -== snap orchard grid=100x31 cursor=4,5 +== snap orchard grid=100x31 cursor=4,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F +| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode /tmp/pardes-snap/theme/cwd Tty+bash Save Mode +| Filter Collapse Del Filter Collapse Del | ls ls | t.txt t.txt | @@ -15,18 +16,18 @@ | | | -| -| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode F +| /tmp/pardes-snap/theme/cwd Tty+bash Save Mode +| Filter Collapse Del | ls | t.txt == style orchard grid=100x31 |0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-99 #b2c3b8,#1d2824, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-98 #b2c3b8,#1d2824, 99-99 d,#1d2824, +|3: 0-2 d,#26362d, 3-21 #becfc4,#26362d, 22-49 d,#26362d, 50-52 d,#1d2824, 53-71 #b2c3b8,#1d2824, 72-99 d,#1d2824, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|5: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|6: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410, |7: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |9: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, @@ -37,11 +38,11 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|18: 0-2 d,#1d2824, 3-21 #b2c3b8,#1d2824, 22-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|20: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, +|21: 0-0 d,#608269, 1-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |22: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |23: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |24: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, @@ -51,15 +52,15 @@ |28: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |29: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, |30: 0-0 d,#608269, 1-1 d,#0d1410, 2-49 #e5eee5,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410, -== snap dusk grid=100x31 cursor=4,5 +== snap dusk grid=100x31 cursor=4,6 == style dusk grid=100x31 |0: 0-50 #c6b6c5,#2c2630, 51-59 #fff0eb,#654053, 60-70 #c6b6c5,#2c2630, 71-99 d,#2c2630, |1: 0-1 #ffffff,#757a5f, 2-2 d,#3b2f3b, 3-34 #d2c2d1,#3b2f3b, 35-49 d,#3b2f3b, 50-51 #ffffff,#757a5f, 52-52 d,#2c2630, 53-84 #c6b6c5,#2c2630, 85-99 d,#2c2630, -|2: 0-1 #000000,#edb092, 2-2 d,#3b2f3b, 3-29 #d2c2d1,#3b2f3b, 30-37 #a1d5c4,#3b2f3b, 38-49 #d2c2d1,#3b2f3b, 50-51 #ffffff,#745469, 52-52 d,#2c2630, 53-79 #c6b6c5,#2c2630, 80-87 #96c9b8,#2c2630, 88-99 #c6b6c5,#2c2630, -|3: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, +|2: 0-1 #000000,#edb092, 2-2 d,#3b2f3b, 3-29 #d2c2d1,#3b2f3b, 30-37 #a1d5c4,#3b2f3b, 38-48 #d2c2d1,#3b2f3b, 49-49 d,#3b2f3b, 50-51 #ffffff,#745469, 52-52 d,#2c2630, 53-79 #c6b6c5,#2c2630, 80-87 #96c9b8,#2c2630, 88-98 #c6b6c5,#2c2630, 99-99 d,#2c2630, +|3: 0-2 d,#3b2f3b, 3-21 #d2c2d1,#3b2f3b, 22-49 d,#3b2f3b, 50-52 d,#2c2630, 53-71 #c6b6c5,#2c2630, 72-99 d,#2c2630, |4: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, -|5: 0-0 d,#8e667c, 1-49 d,#39323b, 50-50 d,#8e667c, 51-99 d,#39323b, -|6: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, +|5: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, +|6: 0-0 d,#8e667c, 1-49 d,#39323b, 50-50 d,#8e667c, 51-99 d,#39323b, |7: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |8: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |9: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, @@ -70,11 +71,11 @@ |14: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |15: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |16: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, -|17: 0-1 #ffffff,#745469, 2-2 d,#2c2630, 3-29 #c6b6c5,#2c2630, 30-37 #96c9b8,#2c2630, 38-49 #c6b6c5,#2c2630, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, -|18: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, +|17: 0-1 #ffffff,#745469, 2-2 d,#2c2630, 3-29 #c6b6c5,#2c2630, 30-37 #96c9b8,#2c2630, 38-48 #c6b6c5,#2c2630, 49-49 d,#2c2630, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, +|18: 0-2 d,#2c2630, 3-21 #c6b6c5,#2c2630, 22-49 d,#2c2630, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |19: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, -|20: 0-0 d,#8e667c, 1-49 d,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, -|21: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, +|20: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, +|21: 0-0 d,#8e667c, 1-49 d,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |22: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |23: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |24: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, @@ -84,15 +85,15 @@ |28: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |29: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, |30: 0-0 d,#8e667c, 1-1 d,#39323b, 2-49 #c3b6c1,#39323b, 50-50 d,#8e667c, 51-51 d,#39323b, 52-99 #c3b6c1,#39323b, -== snap ink grid=100x31 cursor=4,5 +== snap ink grid=100x31 cursor=4,6 == style ink grid=100x31 |0: 0-50 #d5e4f1,#15212c, 51-59 #ffffff,#1b547c, 60-70 #d5e4f1,#15212c, 71-99 d,#15212c, |1: 0-1 #ffffff,#8f7d5b, 2-2 d,#223342, 3-34 #e1eef9,#223342, 35-49 d,#223342, 50-51 #ffffff,#8f7d5b, 52-52 d,#15212c, 53-84 #d5e4f1,#15212c, 85-99 d,#15212c, -|2: 0-1 #000000,#86d8ff, 2-2 d,#223342, 3-29 #e1eef9,#223342, 30-37 #ffeab8,#223342, 38-49 #e1eef9,#223342, 50-51 #ffffff,#5f7990, 52-52 d,#15212c, 53-79 #d5e4f1,#15212c, 80-87 #f5e0ac,#15212c, 88-99 #d5e4f1,#15212c, -|3: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, +|2: 0-1 #000000,#86d8ff, 2-2 d,#223342, 3-29 #e1eef9,#223342, 30-37 #ffeab8,#223342, 38-48 #e1eef9,#223342, 49-49 d,#223342, 50-51 #ffffff,#5f7990, 52-52 d,#15212c, 53-79 #d5e4f1,#15212c, 80-87 #f5e0ac,#15212c, 88-98 #d5e4f1,#15212c, 99-99 d,#15212c, +|3: 0-2 d,#223342, 3-21 #e1eef9,#223342, 22-49 d,#223342, 50-52 d,#15212c, 53-71 #d5e4f1,#15212c, 72-99 d,#15212c, |4: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, -|5: 0-0 d,#86a8c2, 1-49 d,#000000, 50-50 d,#86a8c2, 51-99 d,#000000, -|6: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, +|5: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, +|6: 0-0 d,#86a8c2, 1-49 d,#000000, 50-50 d,#86a8c2, 51-99 d,#000000, |7: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |8: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |9: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, @@ -103,11 +104,11 @@ |14: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |15: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |16: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, -|17: 0-1 #ffffff,#5f7990, 2-2 d,#15212c, 3-29 #d5e4f1,#15212c, 30-37 #f5e0ac,#15212c, 38-49 #d5e4f1,#15212c, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, -|18: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, +|17: 0-1 #ffffff,#5f7990, 2-2 d,#15212c, 3-29 #d5e4f1,#15212c, 30-37 #f5e0ac,#15212c, 38-48 #d5e4f1,#15212c, 49-49 d,#15212c, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, +|18: 0-2 d,#15212c, 3-21 #d5e4f1,#15212c, 22-49 d,#15212c, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |19: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, -|20: 0-0 d,#86a8c2, 1-49 d,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, -|21: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, +|20: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, +|21: 0-0 d,#86a8c2, 1-49 d,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |22: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |23: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |24: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, @@ -117,15 +118,15 @@ |28: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |29: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, |30: 0-0 d,#86a8c2, 1-1 d,#000000, 2-49 #f3f7fc,#000000, 50-50 d,#86a8c2, 51-51 d,#000000, 52-99 #f3f7fc,#000000, -== snap paper grid=100x31 cursor=4,5 +== snap paper grid=100x31 cursor=4,6 == style paper grid=100x31 |0: 0-50 #52625a,#e8eae3, 51-59 #203e31,#c5e2d0, 60-70 #52625a,#e8eae3, 71-99 d,#e8eae3, |1: 0-1 #000000,#bda27e, 2-2 d,#dce5dc, 3-34 #4a5a52,#dce5dc, 35-49 d,#dce5dc, 50-51 #000000,#bda27e, 52-52 d,#e8eae3, 53-84 #52625a,#e8eae3, 85-99 d,#e8eae3, -|2: 0-1 #ffffff,#367157, 2-2 d,#dce5dc, 3-29 #4a5a52,#dce5dc, 30-37 #69447a,#dce5dc, 38-49 #4a5a52,#dce5dc, 50-51 #000000,#8fa594, 52-52 d,#e8eae3, 53-79 #52625a,#e8eae3, 80-87 #724c83,#e8eae3, 88-99 #52625a,#e8eae3, -|3: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, +|2: 0-1 #ffffff,#367157, 2-2 d,#dce5dc, 3-29 #4a5a52,#dce5dc, 30-37 #69447a,#dce5dc, 38-48 #4a5a52,#dce5dc, 49-49 d,#dce5dc, 50-51 #000000,#8fa594, 52-52 d,#e8eae3, 53-79 #52625a,#e8eae3, 80-87 #724c83,#e8eae3, 88-98 #52625a,#e8eae3, 99-99 d,#e8eae3, +|3: 0-2 d,#dce5dc, 3-21 #4a5a52,#dce5dc, 22-49 d,#dce5dc, 50-52 d,#e8eae3, 53-71 #52625a,#e8eae3, 72-99 d,#e8eae3, |4: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, -|5: 0-0 d,#869d8a, 1-49 d,#f5f3ed, 50-50 d,#869d8a, 51-99 d,#f5f3ed, -|6: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, +|5: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, +|6: 0-0 d,#869d8a, 1-49 d,#f5f3ed, 50-50 d,#869d8a, 51-99 d,#f5f3ed, |7: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |8: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |9: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, @@ -136,11 +137,11 @@ |14: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |15: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |16: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, -|17: 0-1 #000000,#8fa594, 2-2 d,#e8eae3, 3-29 #52625a,#e8eae3, 30-37 #724c83,#e8eae3, 38-49 #52625a,#e8eae3, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, -|18: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, +|17: 0-1 #000000,#8fa594, 2-2 d,#e8eae3, 3-29 #52625a,#e8eae3, 30-37 #724c83,#e8eae3, 38-48 #52625a,#e8eae3, 49-49 d,#e8eae3, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, +|18: 0-2 d,#e8eae3, 3-21 #52625a,#e8eae3, 22-49 d,#e8eae3, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |19: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, -|20: 0-0 d,#869d8a, 1-49 d,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, -|21: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, +|20: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, +|21: 0-0 d,#869d8a, 1-49 d,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |22: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |23: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |24: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, @@ -150,15 +151,15 @@ |28: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |29: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, |30: 0-0 d,#869d8a, 1-1 d,#f5f3ed, 2-49 #303b3b,#f5f3ed, 50-50 d,#869d8a, 51-51 d,#f5f3ed, 52-99 #303b3b,#f5f3ed, -== snap daybreak grid=100x31 cursor=4,5 +== snap daybreak grid=100x31 cursor=4,6 == style daybreak grid=100x31 |0: 0-50 #29465e,#edf2f7, 51-59 #10364f,#b9dffb, 60-70 #29465e,#edf2f7, 71-99 d,#edf2f7, |1: 0-1 #000000,#b79a7a, 2-2 d,#dfeaf3, 3-34 #213e56,#dfeaf3, 35-49 d,#dfeaf3, 50-51 #000000,#b79a7a, 52-52 d,#edf2f7, 53-84 #29465e,#edf2f7, 85-99 d,#edf2f7, -|2: 0-1 #ffffff,#006198, 2-2 d,#dfeaf3, 3-29 #213e56,#dfeaf3, 30-37 #52255f,#dfeaf3, 38-49 #213e56,#dfeaf3, 50-51 #000000,#7b9eb7, 52-52 d,#edf2f7, 53-79 #29465e,#edf2f7, 80-87 #5a2d67,#edf2f7, 88-99 #29465e,#edf2f7, -|3: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, +|2: 0-1 #ffffff,#006198, 2-2 d,#dfeaf3, 3-29 #213e56,#dfeaf3, 30-37 #52255f,#dfeaf3, 38-48 #213e56,#dfeaf3, 49-49 d,#dfeaf3, 50-51 #000000,#7b9eb7, 52-52 d,#edf2f7, 53-79 #29465e,#edf2f7, 80-87 #5a2d67,#edf2f7, 88-98 #29465e,#edf2f7, 99-99 d,#edf2f7, +|3: 0-2 d,#dfeaf3, 3-21 #213e56,#dfeaf3, 22-49 d,#dfeaf3, 50-52 d,#edf2f7, 53-71 #29465e,#edf2f7, 72-99 d,#edf2f7, |4: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, -|5: 0-0 d,#6685a0, 1-49 d,#ffffff, 50-50 d,#6685a0, 51-99 d,#ffffff, -|6: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, +|5: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, +|6: 0-0 d,#6685a0, 1-49 d,#ffffff, 50-50 d,#6685a0, 51-99 d,#ffffff, |7: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |8: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |9: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, @@ -169,11 +170,11 @@ |14: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |15: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |16: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, -|17: 0-1 #000000,#7b9eb7, 2-2 d,#edf2f7, 3-29 #29465e,#edf2f7, 30-37 #5a2d67,#edf2f7, 38-49 #29465e,#edf2f7, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, -|18: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, +|17: 0-1 #000000,#7b9eb7, 2-2 d,#edf2f7, 3-29 #29465e,#edf2f7, 30-37 #5a2d67,#edf2f7, 38-48 #29465e,#edf2f7, 49-49 d,#edf2f7, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, +|18: 0-2 d,#edf2f7, 3-21 #29465e,#edf2f7, 22-49 d,#edf2f7, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |19: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, -|20: 0-0 d,#6685a0, 1-49 d,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, -|21: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, +|20: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, +|21: 0-0 d,#6685a0, 1-49 d,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |22: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |23: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |24: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, @@ -183,15 +184,15 @@ |28: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |29: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, |30: 0-0 d,#6685a0, 1-1 d,#ffffff, 2-49 #142d40,#ffffff, 50-50 d,#6685a0, 51-51 d,#ffffff, 52-99 #142d40,#ffffff, -== snap atelier grid=100x31 cursor=4,5 +== snap atelier grid=100x31 cursor=4,6 == style atelier grid=100x31 |0: 0-50 #3d6055,#e4f0ed, 51-59 #35492d,#edf2a5, 60-70 #3d6055,#e4f0ed, 71-99 d,#e4f0ed, |1: 0-1 #000000,#bca481, 2-2 d,#d3e7df, 3-34 #35584d,#d3e7df, 35-49 d,#d3e7df, 50-51 #000000,#bca481, 52-52 d,#e4f0ed, 53-84 #3d6055,#e4f0ed, 85-99 d,#e4f0ed, -|2: 0-1 #ffffff,#36826b, 2-2 d,#d3e7df, 3-29 #35584d,#d3e7df, 30-37 #653f6b,#d3e7df, 38-49 #35584d,#d3e7df, 50-51 #000000,#95baa2, 52-52 d,#e4f0ed, 53-79 #3d6055,#e4f0ed, 80-87 #6e4774,#e4f0ed, 88-99 #3d6055,#e4f0ed, -|3: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, +|2: 0-1 #ffffff,#36826b, 2-2 d,#d3e7df, 3-29 #35584d,#d3e7df, 30-37 #653f6b,#d3e7df, 38-48 #35584d,#d3e7df, 49-49 d,#d3e7df, 50-51 #000000,#95baa2, 52-52 d,#e4f0ed, 53-79 #3d6055,#e4f0ed, 80-87 #6e4774,#e4f0ed, 88-98 #3d6055,#e4f0ed, 99-99 d,#e4f0ed, +|3: 0-2 d,#d3e7df, 3-21 #35584d,#d3e7df, 22-49 d,#d3e7df, 50-52 d,#e4f0ed, 53-71 #3d6055,#e4f0ed, 72-99 d,#e4f0ed, |4: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, -|5: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, -|6: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, +|5: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, +|6: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |7: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |8: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |9: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, @@ -202,11 +203,11 @@ |14: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |15: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |16: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, -|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-37 #6e4774,#e4f0ed, 38-49 #3d6055,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, -|18: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, +|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-37 #6e4774,#e4f0ed, 38-48 #3d6055,#e4f0ed, 49-49 d,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, +|18: 0-2 d,#e4f0ed, 3-21 #3d6055,#e4f0ed, 22-49 d,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |19: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, -|20: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, -|21: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, +|20: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, +|21: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |22: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |23: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |24: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, @@ -216,12 +217,11 @@ |28: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |29: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, |30: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-49 #293f35,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-99 #293f35,#fffdeb, -== snap colors-off grid=100x31 cursor=4,5 -|3: ls Colors ls +== snap colors-off grid=100x31 cursor=4,6 +|4: ls Colors ls == style colors-off grid=100x31 -|3: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-3 #293f35,#fffdeb, 4-41 d,#fffdeb, 42-42 d,#e4f0ed, 43-48 #3d6055,#e4f0ed, 49-49 d,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-53 #293f35,#fffdeb, 54-99 d,#fffdeb, -|4: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #293f35,#fffdeb, 7-49 d,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-56 #293f35,#fffdeb, 57-99 d,#fffdeb, -|6: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, +|4: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-3 #293f35,#fffdeb, 4-41 d,#fffdeb, 42-42 d,#e4f0ed, 43-48 #3d6055,#e4f0ed, 49-49 d,#e4f0ed, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-53 #293f35,#fffdeb, 54-99 d,#fffdeb, +|5: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #293f35,#fffdeb, 7-49 d,#fffdeb, 50-50 d,#9fae76, 51-51 d,#fffdeb, 52-56 #293f35,#fffdeb, 57-99 d,#fffdeb, |7: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |8: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |9: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, @@ -232,10 +232,10 @@ |14: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |15: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |16: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, -|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-37 #6e4774,#e4f0ed, 38-49 #3d6055,#e4f0ed, 50-50 d,#9fae76, 51-99 d,#fffdeb, -|18: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-3 #293f35,#fffdeb, 4-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, -|19: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #293f35,#fffdeb, 7-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, -|20: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, +|17: 0-1 #000000,#95baa2, 2-2 d,#e4f0ed, 3-29 #3d6055,#e4f0ed, 30-37 #6e4774,#e4f0ed, 38-48 #3d6055,#e4f0ed, 49-49 d,#e4f0ed, 50-50 d,#9fae76, 51-99 d,#fffdeb, +|18: 0-2 d,#e4f0ed, 3-21 #3d6055,#e4f0ed, 22-49 d,#e4f0ed, 50-50 d,#9fae76, 51-99 d,#fffdeb, +|19: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-3 #293f35,#fffdeb, 4-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, +|20: 0-0 d,#9fae76, 1-1 d,#fffdeb, 2-6 #293f35,#fffdeb, 7-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |21: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |22: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |23: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, @@ -246,16 +246,16 @@ |28: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |29: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, |30: 0-0 d,#9fae76, 1-49 d,#fffdeb, 50-50 d,#9fae76, 51-99 d,#fffdeb, -== snap forge grid=100x31 cursor=4,5 -|3: ls ls +== snap forge grid=100x31 cursor=4,6 +|4: ls ls == style forge grid=100x31 |0: 0-50 #c5c9d2,#22252b, 51-59 #e2eef4,#1c5172, 60-70 #c5c9d2,#22252b, 71-99 d,#22252b, |1: 0-1 #ffffff,#6a7b67, 2-2 d,#2d3440, 3-34 #d1d5de,#2d3440, 35-49 d,#2d3440, 50-51 #ffffff,#6a7b67, 52-52 d,#22252b, 53-84 #c5c9d2,#22252b, 85-99 d,#22252b, -|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-37 #f8cba7,#2d3440, 38-49 #d1d5de,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-87 #ecbf9b,#22252b, 88-99 #c5c9d2,#22252b, -|3: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, +|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-37 #f8cba7,#2d3440, 38-48 #d1d5de,#2d3440, 49-49 d,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-87 #ecbf9b,#22252b, 88-98 #c5c9d2,#22252b, 99-99 d,#22252b, +|3: 0-2 d,#2d3440, 3-21 #d1d5de,#2d3440, 22-49 d,#2d3440, 50-52 d,#22252b, 53-71 #c5c9d2,#22252b, 72-99 d,#22252b, |4: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, -|5: 0-0 d,#708095, 1-49 d,#0c0c0e, 50-50 d,#708095, 51-99 d,#0c0c0e, -|6: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, +|5: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, +|6: 0-0 d,#708095, 1-49 d,#0c0c0e, 50-50 d,#708095, 51-99 d,#0c0c0e, |7: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |8: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |9: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, @@ -266,11 +266,11 @@ |14: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |15: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |16: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, -|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-37 #ecbf9b,#22252b, 38-49 #c5c9d2,#22252b, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, -|18: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, +|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-37 #ecbf9b,#22252b, 38-48 #c5c9d2,#22252b, 49-49 d,#22252b, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, +|18: 0-2 d,#22252b, 3-21 #c5c9d2,#22252b, 22-49 d,#22252b, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |19: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, -|20: 0-0 d,#708095, 1-49 d,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, -|21: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, +|20: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, +|21: 0-0 d,#708095, 1-49 d,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |22: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |23: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |24: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, @@ -280,15 +280,15 @@ |28: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |29: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, |30: 0-0 d,#708095, 1-1 d,#0c0c0e, 2-49 #e5e5e5,#0c0c0e, 50-50 d,#708095, 51-51 d,#0c0c0e, 52-99 #e5e5e5,#0c0c0e, -== snap lagoon grid=100x31 cursor=4,5 +== snap lagoon grid=100x31 cursor=4,6 == style lagoon grid=100x31 |0: 0-50 #bbd7cb,#29443f, 51-59 #e6f2f3,#325f6c, 60-70 #bbd7cb,#29443f, 71-99 d,#29443f, |1: 0-1 #ffffff,#9a7662, 2-2 d,#34574d, 3-34 #c7e3d7,#34574d, 35-49 d,#34574d, 50-51 #ffffff,#9a7662, 52-52 d,#29443f, 53-84 #bbd7cb,#29443f, 85-99 d,#29443f, -|2: 0-1 #000000,#81cfb7, 2-2 d,#34574d, 3-29 #c7e3d7,#34574d, 30-37 #f0cef8,#34574d, 38-49 #c7e3d7,#34574d, 50-51 #ffffff,#548372, 52-52 d,#29443f, 53-79 #bbd7cb,#29443f, 80-87 #e4c2ec,#29443f, 88-99 #bbd7cb,#29443f, -|3: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, +|2: 0-1 #000000,#81cfb7, 2-2 d,#34574d, 3-29 #c7e3d7,#34574d, 30-37 #f0cef8,#34574d, 38-48 #c7e3d7,#34574d, 49-49 d,#34574d, 50-51 #ffffff,#548372, 52-52 d,#29443f, 53-79 #bbd7cb,#29443f, 80-87 #e4c2ec,#29443f, 88-98 #bbd7cb,#29443f, 99-99 d,#29443f, +|3: 0-2 d,#34574d, 3-21 #c7e3d7,#34574d, 22-49 d,#34574d, 50-52 d,#29443f, 53-71 #bbd7cb,#29443f, 72-99 d,#29443f, |4: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, -|5: 0-0 d,#6e9b8b, 1-49 d,#34464c, 50-50 d,#6e9b8b, 51-99 d,#34464c, -|6: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, +|5: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, +|6: 0-0 d,#6e9b8b, 1-49 d,#34464c, 50-50 d,#6e9b8b, 51-99 d,#34464c, |7: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |8: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |9: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, @@ -299,11 +299,11 @@ |14: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |15: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |16: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, -|17: 0-1 #ffffff,#548372, 2-2 d,#29443f, 3-29 #bbd7cb,#29443f, 30-37 #e4c2ec,#29443f, 38-49 #bbd7cb,#29443f, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, -|18: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, +|17: 0-1 #ffffff,#548372, 2-2 d,#29443f, 3-29 #bbd7cb,#29443f, 30-37 #e4c2ec,#29443f, 38-48 #bbd7cb,#29443f, 49-49 d,#29443f, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, +|18: 0-2 d,#29443f, 3-21 #bbd7cb,#29443f, 22-49 d,#29443f, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |19: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, -|20: 0-0 d,#6e9b8b, 1-49 d,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, -|21: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, +|20: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, +|21: 0-0 d,#6e9b8b, 1-49 d,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |22: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |23: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |24: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, @@ -313,15 +313,15 @@ |28: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |29: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, |30: 0-0 d,#6e9b8b, 1-1 d,#34464c, 2-49 #bac9ce,#34464c, 50-50 d,#6e9b8b, 51-51 d,#34464c, 52-99 #bac9ce,#34464c, -== snap solarium grid=100x31 cursor=4,5 +== snap solarium grid=100x31 cursor=4,6 == style solarium grid=100x31 |0: 0-50 #c7d4bd,#123f43, 51-59 #eee8d5,#1f5c64, 60-70 #c7d4bd,#123f43, 71-99 d,#123f43, |1: 0-1 #ffffff,#347a9a, 2-2 d,#214e50, 3-34 #d3e0c9,#214e50, 35-49 d,#214e50, 50-51 #ffffff,#347a9a, 52-52 d,#123f43, 53-84 #c7d4bd,#123f43, 85-99 d,#123f43, -|2: 0-1 #000000,#e1c266, 2-2 d,#214e50, 3-29 #d3e0c9,#214e50, 30-37 #f5cdec,#214e50, 38-49 #d3e0c9,#214e50, 50-51 #ffffff,#467f76, 52-52 d,#123f43, 53-79 #c7d4bd,#123f43, 80-87 #e9c1e0,#123f43, 88-99 #c7d4bd,#123f43, -|3: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, +|2: 0-1 #000000,#e1c266, 2-2 d,#214e50, 3-29 #d3e0c9,#214e50, 30-37 #f5cdec,#214e50, 38-48 #d3e0c9,#214e50, 49-49 d,#214e50, 50-51 #ffffff,#467f76, 52-52 d,#123f43, 53-79 #c7d4bd,#123f43, 80-87 #e9c1e0,#123f43, 88-98 #c7d4bd,#123f43, 99-99 d,#123f43, +|3: 0-2 d,#214e50, 3-21 #d3e0c9,#214e50, 22-49 d,#214e50, 50-52 d,#123f43, 53-71 #c7d4bd,#123f43, 72-99 d,#123f43, |4: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, -|5: 0-0 d,#65978c, 1-49 d,#173e45, 50-50 d,#65978c, 51-99 d,#173e45, -|6: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, +|5: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, +|6: 0-0 d,#65978c, 1-49 d,#173e45, 50-50 d,#65978c, 51-99 d,#173e45, |7: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |8: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |9: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, @@ -332,11 +332,11 @@ |14: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |15: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |16: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, -|17: 0-1 #ffffff,#467f76, 2-2 d,#123f43, 3-29 #c7d4bd,#123f43, 30-37 #e9c1e0,#123f43, 38-49 #c7d4bd,#123f43, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, -|18: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, +|17: 0-1 #ffffff,#467f76, 2-2 d,#123f43, 3-29 #c7d4bd,#123f43, 30-37 #e9c1e0,#123f43, 38-48 #c7d4bd,#123f43, 49-49 d,#123f43, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, +|18: 0-2 d,#123f43, 3-21 #c7d4bd,#123f43, 22-49 d,#123f43, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |19: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, -|20: 0-0 d,#65978c, 1-49 d,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, -|21: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, +|20: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, +|21: 0-0 d,#65978c, 1-49 d,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |22: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |23: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |24: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, @@ -346,15 +346,15 @@ |28: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |29: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, |30: 0-0 d,#65978c, 1-1 d,#173e45, 2-49 #a7b7ae,#173e45, 50-50 d,#65978c, 51-51 d,#173e45, 52-99 #a7b7ae,#173e45, -== snap spectrum grid=100x31 cursor=4,5 +== snap spectrum grid=100x31 cursor=4,6 == style spectrum grid=100x31 |0: 0-50 #ddd8bb,#3b3830, 51-59 #fcfcfa,#513f60, 60-70 #ddd8bb,#3b3830, 71-99 d,#3b3830, |1: 0-1 #ffffff,#706596, 2-2 d,#4d4736, 3-34 #e9e4c7,#4d4736, 35-49 d,#4d4736, 50-51 #ffffff,#706596, 52-52 d,#3b3830, 53-84 #ddd8bb,#3b3830, 85-99 d,#3b3830, -|2: 0-1 #000000,#ffd866, 2-2 d,#4d4736, 3-29 #e9e4c7,#4d4736, 30-37 #d7e3ff,#4d4736, 38-49 #e9e4c7,#4d4736, 50-51 #ffffff,#92844f, 52-52 d,#3b3830, 53-79 #ddd8bb,#3b3830, 80-87 #c5d7ff,#3b3830, 88-99 #ddd8bb,#3b3830, -|3: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, +|2: 0-1 #000000,#ffd866, 2-2 d,#4d4736, 3-29 #e9e4c7,#4d4736, 30-37 #d7e3ff,#4d4736, 38-48 #e9e4c7,#4d4736, 49-49 d,#4d4736, 50-51 #ffffff,#92844f, 52-52 d,#3b3830, 53-79 #ddd8bb,#3b3830, 80-87 #c5d7ff,#3b3830, 88-98 #ddd8bb,#3b3830, 99-99 d,#3b3830, +|3: 0-2 d,#4d4736, 3-21 #e9e4c7,#4d4736, 22-49 d,#4d4736, 50-52 d,#3b3830, 53-71 #ddd8bb,#3b3830, 72-99 d,#3b3830, |4: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, -|5: 0-0 d,#948d6f, 1-49 d,#100e11, 50-50 d,#948d6f, 51-99 d,#100e11, -|6: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, +|5: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, +|6: 0-0 d,#948d6f, 1-49 d,#100e11, 50-50 d,#948d6f, 51-99 d,#100e11, |7: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |8: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |9: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, @@ -365,11 +365,11 @@ |14: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |15: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |16: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, -|17: 0-1 #ffffff,#92844f, 2-2 d,#3b3830, 3-29 #ddd8bb,#3b3830, 30-37 #c5d7ff,#3b3830, 38-49 #ddd8bb,#3b3830, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, -|18: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, +|17: 0-1 #ffffff,#92844f, 2-2 d,#3b3830, 3-29 #ddd8bb,#3b3830, 30-37 #c5d7ff,#3b3830, 38-48 #ddd8bb,#3b3830, 49-49 d,#3b3830, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, +|18: 0-2 d,#3b3830, 3-21 #ddd8bb,#3b3830, 22-49 d,#3b3830, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |19: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, -|20: 0-0 d,#948d6f, 1-49 d,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, -|21: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, +|20: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, +|21: 0-0 d,#948d6f, 1-49 d,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |22: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |23: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |24: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, @@ -379,15 +379,15 @@ |28: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |29: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, |30: 0-0 d,#948d6f, 1-1 d,#100e11, 2-49 #fcfcfa,#100e11, 50-50 d,#948d6f, 51-51 d,#100e11, 52-99 #fcfcfa,#100e11, -== snap harvest grid=100x31 cursor=4,5 +== snap harvest grid=100x31 cursor=4,6 == style harvest grid=100x31 |0: 0-50 #d5d8b8,#343b2e, 51-59 #f3f2cc,#4f5834, 60-70 #d5d8b8,#343b2e, 71-99 d,#343b2e, |1: 0-1 #ffffff,#9b583f, 2-2 d,#424d38, 3-34 #e1e4c4,#424d38, 35-49 d,#424d38, 50-51 #ffffff,#9b583f, 52-52 d,#343b2e, 53-84 #d5d8b8,#343b2e, 85-99 d,#343b2e, -|2: 0-1 #000000,#d8bd83, 2-2 d,#424d38, 3-29 #e1e4c4,#424d38, 30-37 #c5e7f7,#424d38, 38-49 #e1e4c4,#424d38, 50-51 #ffffff,#7b7e4e, 52-52 d,#343b2e, 53-79 #d5d8b8,#343b2e, 80-87 #badbeb,#343b2e, 88-99 #d5d8b8,#343b2e, -|3: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, +|2: 0-1 #000000,#d8bd83, 2-2 d,#424d38, 3-29 #e1e4c4,#424d38, 30-37 #c5e7f7,#424d38, 38-48 #e1e4c4,#424d38, 49-49 d,#424d38, 50-51 #ffffff,#7b7e4e, 52-52 d,#343b2e, 53-79 #d5d8b8,#343b2e, 80-87 #badbeb,#343b2e, 88-98 #d5d8b8,#343b2e, 99-99 d,#343b2e, +|3: 0-2 d,#424d38, 3-21 #e1e4c4,#424d38, 22-49 d,#424d38, 50-52 d,#343b2e, 53-71 #d5d8b8,#343b2e, 72-99 d,#343b2e, |4: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, -|5: 0-0 d,#888e5e, 1-49 d,#100e0b, 50-50 d,#888e5e, 51-99 d,#100e0b, -|6: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, +|5: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, +|6: 0-0 d,#888e5e, 1-49 d,#100e0b, 50-50 d,#888e5e, 51-99 d,#100e0b, |7: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |8: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |9: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, @@ -398,11 +398,11 @@ |14: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |15: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |16: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, -|17: 0-1 #ffffff,#7b7e4e, 2-2 d,#343b2e, 3-29 #d5d8b8,#343b2e, 30-37 #badbeb,#343b2e, 38-49 #d5d8b8,#343b2e, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, -|18: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, +|17: 0-1 #ffffff,#7b7e4e, 2-2 d,#343b2e, 3-29 #d5d8b8,#343b2e, 30-37 #badbeb,#343b2e, 38-48 #d5d8b8,#343b2e, 49-49 d,#343b2e, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, +|18: 0-2 d,#343b2e, 3-21 #d5d8b8,#343b2e, 22-49 d,#343b2e, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |19: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, -|20: 0-0 d,#888e5e, 1-49 d,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, -|21: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, +|20: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, +|21: 0-0 d,#888e5e, 1-49 d,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |22: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |23: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |24: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, @@ -412,15 +412,15 @@ |28: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |29: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, |30: 0-0 d,#888e5e, 1-1 d,#100e0b, 2-49 #f3f2cc,#100e0b, 50-50 d,#888e5e, 51-51 d,#100e0b, 52-99 #f3f2cc,#100e0b, -== snap clay grid=100x31 cursor=4,5 +== snap clay grid=100x31 cursor=4,6 == style clay grid=100x31 |0: 0-50 #d7d6b6,#343f36, 51-59 #fbf1c7,#505c38, 60-70 #d7d6b6,#343f36, 71-99 d,#343f36, |1: 0-1 #ffffff,#875e65, 2-2 d,#424f40, 3-34 #e3e2c2,#424f40, 35-49 d,#424f40, 50-51 #ffffff,#875e65, 52-52 d,#343f36, 53-84 #d7d6b6,#343f36, 85-99 d,#343f36, -|2: 0-1 #000000,#8ac874, 2-2 d,#424f40, 3-29 #e3e2c2,#424f40, 30-37 #b5ecec,#424f40, 38-49 #e3e2c2,#424f40, 50-51 #ffffff,#687c4d, 52-52 d,#343f36, 53-79 #d7d6b6,#343f36, 80-87 #aae0e0,#343f36, 88-99 #d7d6b6,#343f36, -|3: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, +|2: 0-1 #000000,#8ac874, 2-2 d,#424f40, 3-29 #e3e2c2,#424f40, 30-37 #b5ecec,#424f40, 38-48 #e3e2c2,#424f40, 49-49 d,#424f40, 50-51 #ffffff,#687c4d, 52-52 d,#343f36, 53-79 #d7d6b6,#343f36, 80-87 #aae0e0,#343f36, 88-98 #d7d6b6,#343f36, 99-99 d,#343f36, +|3: 0-2 d,#424f40, 3-21 #e3e2c2,#424f40, 22-49 d,#424f40, 50-52 d,#343f36, 53-71 #d7d6b6,#343f36, 72-99 d,#343f36, |4: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, -|5: 0-0 d,#8c9467, 1-49 d,#10100e, 50-50 d,#8c9467, 51-99 d,#10100e, -|6: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, +|5: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, +|6: 0-0 d,#8c9467, 1-49 d,#10100e, 50-50 d,#8c9467, 51-99 d,#10100e, |7: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |8: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |9: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, @@ -431,11 +431,11 @@ |14: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |15: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |16: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, -|17: 0-1 #ffffff,#687c4d, 2-2 d,#343f36, 3-29 #d7d6b6,#343f36, 30-37 #aae0e0,#343f36, 38-49 #d7d6b6,#343f36, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, -|18: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, +|17: 0-1 #ffffff,#687c4d, 2-2 d,#343f36, 3-29 #d7d6b6,#343f36, 30-37 #aae0e0,#343f36, 38-48 #d7d6b6,#343f36, 49-49 d,#343f36, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, +|18: 0-2 d,#343f36, 3-21 #d7d6b6,#343f36, 22-49 d,#343f36, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |19: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, -|20: 0-0 d,#8c9467, 1-49 d,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, -|21: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, +|20: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, +|21: 0-0 d,#8c9467, 1-49 d,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |22: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |23: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |24: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, @@ -445,15 +445,15 @@ |28: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |29: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, |30: 0-0 d,#8c9467, 1-1 d,#10100e, 2-49 #f3e5bc,#10100e, 50-50 d,#8c9467, 51-51 d,#10100e, 52-99 #f3e5bc,#10100e, -== snap forge-black grid=100x31 cursor=4,5 +== snap forge-black grid=100x31 cursor=4,6 == style forge-black grid=100x31 |0: 0-50 #c5c9d2,#22252b, 51-59 #e2eef4,#1c5172, 60-70 #c5c9d2,#22252b, 71-99 d,#22252b, |1: 0-1 #ffffff,#6a7b67, 2-2 d,#2d3440, 3-34 #d1d5de,#2d3440, 35-49 d,#2d3440, 50-51 #ffffff,#6a7b67, 52-52 d,#22252b, 53-84 #c5c9d2,#22252b, 85-99 d,#22252b, -|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-37 #f8cba7,#2d3440, 38-49 #d1d5de,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-87 #ecbf9b,#22252b, 88-99 #c5c9d2,#22252b, -|3: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, +|2: 0-1 #000000,#9cc3f7, 2-2 d,#2d3440, 3-29 #d1d5de,#2d3440, 30-37 #f8cba7,#2d3440, 38-48 #d1d5de,#2d3440, 49-49 d,#2d3440, 50-51 #ffffff,#55647b, 52-52 d,#22252b, 53-79 #c5c9d2,#22252b, 80-87 #ecbf9b,#22252b, 88-98 #c5c9d2,#22252b, 99-99 d,#22252b, +|3: 0-2 d,#2d3440, 3-21 #d1d5de,#2d3440, 22-49 d,#2d3440, 50-52 d,#22252b, 53-71 #c5c9d2,#22252b, 72-99 d,#22252b, |4: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, -|5: 0-0 d,#708095, 1-49 d,#000000, 50-50 d,#708095, 51-99 d,#000000, -|6: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, +|5: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, +|6: 0-0 d,#708095, 1-49 d,#000000, 50-50 d,#708095, 51-99 d,#000000, |7: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |8: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |9: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, @@ -464,11 +464,11 @@ |14: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |15: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |16: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, -|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-37 #ecbf9b,#22252b, 38-49 #c5c9d2,#22252b, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, -|18: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, +|17: 0-1 #ffffff,#55647b, 2-2 d,#22252b, 3-29 #c5c9d2,#22252b, 30-37 #ecbf9b,#22252b, 38-48 #c5c9d2,#22252b, 49-49 d,#22252b, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, +|18: 0-2 d,#22252b, 3-21 #c5c9d2,#22252b, 22-49 d,#22252b, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |19: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, -|20: 0-0 d,#708095, 1-49 d,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, -|21: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, +|20: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, +|21: 0-0 d,#708095, 1-49 d,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |22: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |23: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |24: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, @@ -478,15 +478,15 @@ |28: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |29: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, |30: 0-0 d,#708095, 1-1 d,#000000, 2-49 #f6f6f6,#000000, 50-50 d,#708095, 51-51 d,#000000, 52-99 #f6f6f6,#000000, -== snap forge-soft grid=100x31 cursor=4,5 +== snap forge-soft grid=100x31 cursor=4,6 == style forge-soft grid=100x31 |0: 0-50 #c5c9d2,#2a2d34, 51-59 #e2eef4,#1c5172, 60-70 #c5c9d2,#2a2d34, 71-99 d,#2a2d34, |1: 0-1 #ffffff,#6d7f6c, 2-2 d,#373e49, 3-34 #d1d5de,#373e49, 35-49 d,#373e49, 50-51 #ffffff,#6d7f6c, 52-52 d,#2a2d34, 53-84 #c5c9d2,#2a2d34, 85-99 d,#2a2d34, -|2: 0-1 #000000,#9cc3f7, 2-2 d,#373e49, 3-29 #d1d5de,#373e49, 30-37 #f8cba7,#373e49, 38-49 #d1d5de,#373e49, 50-51 #ffffff,#55647b, 52-52 d,#2a2d34, 53-79 #c5c9d2,#2a2d34, 80-87 #ecbf9b,#2a2d34, 88-99 #c5c9d2,#2a2d34, -|3: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, +|2: 0-1 #000000,#9cc3f7, 2-2 d,#373e49, 3-29 #d1d5de,#373e49, 30-37 #f8cba7,#373e49, 38-48 #d1d5de,#373e49, 49-49 d,#373e49, 50-51 #ffffff,#55647b, 52-52 d,#2a2d34, 53-79 #c5c9d2,#2a2d34, 80-87 #ecbf9b,#2a2d34, 88-98 #c5c9d2,#2a2d34, 99-99 d,#2a2d34, +|3: 0-2 d,#373e49, 3-21 #d1d5de,#373e49, 22-49 d,#373e49, 50-52 d,#2a2d34, 53-71 #c5c9d2,#2a2d34, 72-99 d,#2a2d34, |4: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, -|5: 0-0 d,#708095, 1-49 d,#35383e, 50-50 d,#708095, 51-99 d,#35383e, -|6: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, +|5: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, +|6: 0-0 d,#708095, 1-49 d,#35383e, 50-50 d,#708095, 51-99 d,#35383e, |7: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |8: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |9: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, @@ -497,11 +497,11 @@ |14: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |15: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |16: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, -|17: 0-1 #ffffff,#55647b, 2-2 d,#2a2d34, 3-29 #c5c9d2,#2a2d34, 30-37 #ecbf9b,#2a2d34, 38-49 #c5c9d2,#2a2d34, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, -|18: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, +|17: 0-1 #ffffff,#55647b, 2-2 d,#2a2d34, 3-29 #c5c9d2,#2a2d34, 30-37 #ecbf9b,#2a2d34, 38-48 #c5c9d2,#2a2d34, 49-49 d,#2a2d34, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, +|18: 0-2 d,#2a2d34, 3-21 #c5c9d2,#2a2d34, 22-49 d,#2a2d34, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |19: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, -|20: 0-0 d,#708095, 1-49 d,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, -|21: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, +|20: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, +|21: 0-0 d,#708095, 1-49 d,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |22: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |23: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |24: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, @@ -511,15 +511,15 @@ |28: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |29: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, |30: 0-0 d,#708095, 1-1 d,#35383e, 2-49 #bfc3cc,#35383e, 50-50 d,#708095, 51-51 d,#35383e, 52-99 #bfc3cc,#35383e, -== snap orchard-black grid=100x31 cursor=4,5 +== snap orchard-black grid=100x31 cursor=4,6 == style orchard-black grid=100x31 |0: 0-50 #b2c3b8,#1d2824, 51-59 #f5f7ee,#3c614a, 60-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-49 d,#26362d, 50-51 #ffffff,#887455, 52-52 d,#1d2824, 53-84 #b2c3b8,#1d2824, 85-99 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-49 #becfc4,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-99 #b2c3b8,#1d2824, -|3: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-29 #becfc4,#26362d, 30-37 #dfbce5,#26362d, 38-48 #becfc4,#26362d, 49-49 d,#26362d, 50-51 #ffffff,#496656, 52-52 d,#1d2824, 53-79 #b2c3b8,#1d2824, 80-87 #d3b0d9,#1d2824, 88-98 #b2c3b8,#1d2824, 99-99 d,#1d2824, +|3: 0-2 d,#26362d, 3-21 #becfc4,#26362d, 22-49 d,#26362d, 50-52 d,#1d2824, 53-71 #b2c3b8,#1d2824, 72-99 d,#1d2824, |4: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, -|5: 0-0 d,#608269, 1-49 d,#000000, 50-50 d,#608269, 51-99 d,#000000, -|6: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, +|5: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, +|6: 0-0 d,#608269, 1-49 d,#000000, 50-50 d,#608269, 51-99 d,#000000, |7: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |8: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |9: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, @@ -530,11 +530,11 @@ |14: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |15: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |16: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-49 #b2c3b8,#1d2824, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, -|18: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-29 #b2c3b8,#1d2824, 30-37 #d3b0d9,#1d2824, 38-48 #b2c3b8,#1d2824, 49-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, +|18: 0-2 d,#1d2824, 3-21 #b2c3b8,#1d2824, 22-49 d,#1d2824, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |19: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, -|20: 0-0 d,#608269, 1-49 d,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, -|21: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, +|20: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, +|21: 0-0 d,#608269, 1-49 d,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |22: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |23: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |24: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, @@ -544,15 +544,15 @@ |28: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |29: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, |30: 0-0 d,#608269, 1-1 d,#000000, 2-49 #f2f8ef,#000000, 50-50 d,#608269, 51-51 d,#000000, 52-99 #f2f8ef,#000000, -== snap helix grid=100x31 cursor=4,5 +== snap helix grid=100x31 cursor=4,6 == style helix grid=100x31 |0: 0-50 #949494,#121212, 51-59 #080808,#80a0ff, 60-70 #949494,#121212, 71-99 d,#121212, |1: 0-1 #ffffff,#826f46, 2-2 d,#121212, 3-34 #949494,#121212, 35-49 d,#121212, 50-51 #ffffff,#826f46, 52-52 d,#121212, 53-84 #949494,#121212, 85-99 d,#121212, -|2: 0-1 #ffffff,#626262, 2-2 d,#121212, 3-49 #949494,#121212, 50-51 #ffffff,#262626, 52-52 d,#121212, 53-99 #949494,#121212, -|3: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, +|2: 0-1 #ffffff,#626262, 2-2 d,#121212, 3-48 #949494,#121212, 49-49 d,#121212, 50-51 #ffffff,#262626, 52-52 d,#121212, 53-98 #949494,#121212, 99-99 d,#121212, +|3: 0-2 d,#121212, 3-21 #949494,#121212, 22-52 d,#121212, 53-71 #949494,#121212, 72-99 d,#121212, |4: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, -|5: 0-0 d,#444444, 1-49 d,#080808, 50-50 d,#444444, 51-99 d,#080808, -|6: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, +|5: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, +|6: 0-0 d,#444444, 1-49 d,#080808, 50-50 d,#444444, 51-99 d,#080808, |7: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, |8: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, |9: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, @@ -563,11 +563,11 @@ |14: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, |15: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, |16: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, -|17: 0-1 #ffffff,#262626, 2-2 d,#121212, 3-49 #949494,#121212, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, -|18: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, +|17: 0-1 #ffffff,#262626, 2-2 d,#121212, 3-48 #949494,#121212, 49-49 d,#121212, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, +|18: 0-2 d,#121212, 3-21 #949494,#121212, 22-49 d,#121212, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, |19: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, -|20: 0-0 d,#444444, 1-49 d,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, -|21: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, +|20: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, +|21: 0-0 d,#444444, 1-49 d,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, |22: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, |23: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, |24: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, @@ -577,15 +577,15 @@ |28: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, |29: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, |30: 0-0 d,#444444, 1-1 d,#080808, 2-49 #bdbdbd,#080808, 50-50 d,#444444, 51-51 d,#080808, 52-99 #bdbdbd,#080808, -== snap dark grid=100x31 cursor=4,5 +== snap dark grid=100x31 cursor=4,6 == style dark grid=100x31 |0: 0-50 #969696,#2c2a3e, 51-59 #d8c8a8,#513c17, 60-70 #969696,#2c2a3e, 71-99 d,#2c2a3e, |1: 0-1 #ffffff,#867a4e, 2-2 d,#2c2a3e, 3-34 #969696,#2c2a3e, 35-49 d,#2c2a3e, 50-51 #ffffff,#867a4e, 52-52 d,#2c2a3e, 53-84 #969696,#2c2a3e, 85-99 d,#2c2a3e, -|2: 0-1 #ffffff,#663fe0, 2-2 d,#2c2a3e, 3-49 #969696,#2c2a3e, 50-51 #ffffff,#353a58, 52-52 d,#2c2a3e, 53-99 #969696,#2c2a3e, -|3: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, +|2: 0-1 #ffffff,#663fe0, 2-2 d,#2c2a3e, 3-48 #969696,#2c2a3e, 49-49 d,#2c2a3e, 50-51 #ffffff,#353a58, 52-52 d,#2c2a3e, 53-98 #969696,#2c2a3e, 99-99 d,#2c2a3e, +|3: 0-2 d,#2c2a3e, 3-21 #969696,#2c2a3e, 22-52 d,#2c2a3e, 53-71 #969696,#2c2a3e, 72-99 d,#2c2a3e, |4: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, -|5: 0-0 d,#525252, 1-49 d,d, 50-50 d,#525252, 51-99 d,d, -|6: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, +|5: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, +|6: 0-0 d,#525252, 1-49 d,d, 50-50 d,#525252, 51-99 d,d, |7: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, |8: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, |9: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, @@ -596,11 +596,11 @@ |14: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, |15: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, |16: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, -|17: 0-1 #ffffff,#353a58, 2-2 d,#2c2a3e, 3-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, -|18: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, +|17: 0-1 #ffffff,#353a58, 2-2 d,#2c2a3e, 3-48 #969696,#2c2a3e, 49-49 d,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, +|18: 0-2 d,#2c2a3e, 3-21 #969696,#2c2a3e, 22-49 d,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, |19: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, -|20: 0-0 d,#525252, 1-49 d,d, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, -|21: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, +|20: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, +|21: 0-0 d,#525252, 1-49 d,d, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, |22: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, |23: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, |24: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, @@ -610,15 +610,15 @@ |28: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, |29: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, |30: 0-0 d,#525252, 1-1 d,d, 2-49 #969696,#2c2a3e, 50-50 d,#525252, 51-51 d,d, 52-99 #969696,#2c2a3e, -== snap acme grid=100x31 cursor=4,5 +== snap acme grid=100x31 cursor=4,6 == style acme grid=100x31 -|0: 0-50 #000000,#eaffff, 51-59 #000000,#fafa92, 60-70 #000000,#eaffff, 71-99 d,#eaffff, -|1: 0-1 #000000,#c5a87f, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-49 d,#eaffff, 50-51 #000000,#c5a87f, 52-52 d,#eaffff, 53-84 #000000,#eaffff, 85-99 d,#eaffff, -|2: 0-1 #ffffff,#663fe0, 2-2 d,#eaffff, 3-49 #000000,#eaffff, 50-51 #ffffff,#353a58, 52-52 d,#eaffff, 53-99 #000000,#eaffff, -|3: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, +|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, |4: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, -|5: 0-99 d,#ffffea, -|6: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, +|5: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, +|6: 0-99 d,#ffffea, |7: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, |8: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, |9: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, @@ -629,11 +629,11 @@ |14: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, |15: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, |16: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, -|17: 0-1 #ffffff,#353a58, 2-2 d,#eaffff, 3-49 #000000,#eaffff, 50-51 d,#ffffea, 52-99 #000000,#ffffea, -|18: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, +|17: 0-1 #000000,#b9c3e5, 2-2 d,#eaffff, 3-48 #000000,#eaffff, 49-49 d,#eaffff, 50-51 d,#ffffea, 52-99 #000000,#ffffea, +|18: 0-2 d,#eaffff, 3-21 #000000,#eaffff, 22-49 d,#eaffff, 50-51 d,#ffffea, 52-99 #000000,#ffffea, |19: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, -|20: 0-51 d,#ffffea, 52-99 #000000,#ffffea, -|21: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, +|20: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, +|21: 0-51 d,#ffffea, 52-99 #000000,#ffffea, |22: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, |23: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, |24: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, @@ -643,15 +643,15 @@ |28: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, |29: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, |30: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea, -== snap generated grid=100x31 cursor=4,5 +== snap generated grid=100x31 cursor=4,6 == style generated grid=100x31 |0: 0-50 #e0e0e0,#1e262e, 51-59 #e0e0e0,#303840, 60-70 #e0e0e0,#1e262e, 71-99 d,#1e262e, |1: 0-1 #ffffff,#4f7b96, 2-2 d,#1e262e, 3-34 #e0e0e0,#1e262e, 35-49 d,#1e262e, 50-51 #ffffff,#4f7b96, 52-52 d,#1e262e, 53-84 #e0e0e0,#1e262e, 85-99 d,#1e262e, -|2: 0-1 #000000,#ffa060, 2-2 d,#1e262e, 3-49 #e0e0e0,#1e262e, 50-51 #ffffff,#303840, 52-52 d,#1e262e, 53-99 #e0e0e0,#1e262e, -|3: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, +|2: 0-1 #000000,#ffa060, 2-2 d,#1e262e, 3-48 #e0e0e0,#1e262e, 49-49 d,#1e262e, 50-51 #ffffff,#303840, 52-52 d,#1e262e, 53-98 #e0e0e0,#1e262e, 99-99 d,#1e262e, +|3: 0-2 d,#1e262e, 3-21 #e0e0e0,#1e262e, 22-52 d,#1e262e, 53-71 #e0e0e0,#1e262e, 72-99 d,#1e262e, |4: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, -|5: 0-0 d,#303840, 1-49 d,#161e26, 50-50 d,#303840, 51-99 d,#161e26, -|6: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, +|5: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, +|6: 0-0 d,#303840, 1-49 d,#161e26, 50-50 d,#303840, 51-99 d,#161e26, |7: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, |8: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, |9: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, @@ -662,11 +662,11 @@ |14: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, |15: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, |16: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, -|17: 0-1 #ffffff,#303840, 2-2 d,#1e262e, 3-49 #e0e0e0,#1e262e, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, -|18: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, +|17: 0-1 #ffffff,#303840, 2-2 d,#1e262e, 3-48 #e0e0e0,#1e262e, 49-49 d,#1e262e, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, +|18: 0-2 d,#1e262e, 3-21 #e0e0e0,#1e262e, 22-49 d,#1e262e, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, |19: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, -|20: 0-0 d,#303840, 1-49 d,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, -|21: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, +|20: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, +|21: 0-0 d,#303840, 1-49 d,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, |22: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, |23: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, |24: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26, diff --git a/test/snapshots/themesel.golden b/test/snapshots/themesel.golden index 03aa4429..978baeb1 100644 --- a/test/snapshots/themesel.golden +++ b/test/snapshots/themesel.golden @@ -626,8 +626,8 @@ |30: 24 Theme adwaita_dark == style step-acme grid=100x31 |0: 0-70 #000000,#eaffff, 71-99 d,#eaffff, -|1: 0-1 #000000,#c5a87f, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-99 d,#eaffff, -|2: 0-1 #ffffff,#353a58, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff, +|1: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-99 d,#eaffff, +|2: 0-1 #000000,#b9c3e5, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff, |3: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea, |4: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea, |5: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea, @@ -641,7 +641,7 @@ |13: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea, |14: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea, |15: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea, -|16: 0-1 #ffffff,#663fe0, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff, +|16: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff, |17: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-20 #000000,#ffffea, 21-92 d,#ffffea, 93-93 d,#eaffff, 94-98 #000000,#eaffff, 99-99 d,#eaffff, |18: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-19 #000000,#ffffea, 20-99 d,#ffffea, |19: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea, @@ -706,8 +706,8 @@ == snap step-back grid=100x31 cursor=7,26 == style step-back grid=100x31 |0: 0-70 #000000,#eaffff, 71-99 d,#eaffff, -|1: 0-1 #000000,#c5a87f, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-99 d,#eaffff, -|2: 0-1 #ffffff,#353a58, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff, +|1: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-99 d,#eaffff, +|2: 0-1 #000000,#b9c3e5, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff, |3: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea, |4: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea, |5: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea, @@ -721,7 +721,7 @@ |13: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea, |14: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea, |15: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-13 #000000,#ffffea, 14-99 d,#ffffea, -|16: 0-1 #ffffff,#663fe0, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff, +|16: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-61 #000000,#eaffff, 62-99 d,#eaffff, |17: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-19 #000000,#ffffea, 20-92 d,#ffffea, 93-93 d,#eaffff, 94-98 #000000,#eaffff, 99-99 d,#eaffff, |18: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea, |19: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-23 #000000,#ffffea, 24-99 d,#ffffea, @@ -768,7 +768,7 @@ |29: 27 line 27 |30: 28 line 28 == style picked grid=100x31 -|2: 0-1 #ffffff,#663fe0, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff, +|2: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-59 #000000,#eaffff, 60-99 d,#eaffff, |3: 0-1 d,#ffffea, 2-6 #999980,#ffffea,b 7-12 #000000,#ffffea, 13-99 d,#ffffea, |7: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea, |8: 0-1 d,#ffffea, 2-6 #999980,#ffffea, 7-12 #000000,#ffffea, 13-99 d,#ffffea, diff --git a/test/snapshots/tinywin.golden b/test/snapshots/tinywin.golden index 9b5d06d4..082e75c1 100644 --- a/test/snapshots/tinywin.golden +++ b/test/snapshots/tinywin.golden @@ -25,7 +25,7 @@ == snap shrunk grid=60x4 cursor=59,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor | New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/tinywin/cwd Tty+bash Save Mode Filter Co +|$ /tmp/pardes-snap/tinywin/cwd Tty+bash Save Mode Filter C | /+New Save Tty Collapse Del == snap columns grid=100x31 cursor=23,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit @@ -55,13 +55,13 @@ |Newc | N |$ / -| ^[ -| n^ -| [n -| c -| n -| cn -| c +| t +| m +| p +| / +| p +| a +| r | $ | ^[ | n^ @@ -74,3 +74,10 @@ | cn | | / +| + +| N +| e +| w +| +| S +| a diff --git a/test/snapshots/topbarexec.golden b/test/snapshots/topbarexec.golden index 4b21e8eb..a621dd18 100644 --- a/test/snapshots/topbarexec.golden +++ b/test/snapshots/topbarexec.golden @@ -1,7 +1,8 @@ -== snap selected grid=90x21 cursor=14,4 +== snap selected grid=90x21 cursor=14,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp/pardes-snap/topbarexec/cwd Tty+bash S +| /tmp/pardes-snap/topbarexec/cwd Tty+bash /tmp/pardes-snap/topbarexec/cwd Tty+bash +| Save Mode Filter Collapse Del Save Mode Filter Collapse Del | ls ls | zmarkfile.txt zmarkfile.txt | @@ -10,29 +11,29 @@ | | | -| -| /tmp/pardes-snap/topbarexec/cwd Tty+bash S +| /tmp/pardes-snap/topbarexec/cwd Tty+bash +| Save Mode Filter Collapse Del | ls | zmarkfile.txt -== snap noarg-runs grid=90x21 cursor=14,4 +== snap noarg-runs grid=90x21 cursor=14,5 |1: New Tty Find Grep Joincol Delcol New ╭────────────────────────────────────╮ -|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp│pardes resizes=1 │ -|3: ls ls │theme orchard │ -|4: zmarkfile.txt zmark│font default │ -|5: │terms 3 cols 2 │ -|6: │active #0 43x9 │ -|7: │scroll 0/9 │ +|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash /tmp│pardes resizes=1 │ +|3: Save Mode Filter Collapse Del Save│theme orchard │ +|4: ls ls │font default │ +|5: zmarkfile.txt zmark│terms 3 cols 2 │ +|6: │active #0 43x8 │ +|7: │scroll 0/8 │ |8: │pinch 1.0000 │ |9: │tscroll 0.0000 │ |10: ╰────────────────────────────────────╯ -== snap arg-still-spliced grid=90x21 cursor=14,4 +== snap arg-still-spliced grid=90x21 cursor=14,5 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash S /tmp/pardes-snap/topbarexec/cwd Tty+bash S -|3: ls ls -|4: zmarkfile.txt zmarkfile.txt -|5: -|6: /tmp/pardes-snap/topbarexec/cwd/+Search Sa -|7: 1 zmarkfile.txt -|8: 2 -|9: -|10: +|2: /tmp/pardes-snap/topbarexec/cwd Tty+bash /tmp/pardes-snap/topbarexec/cwd Tty+bash +|3: Save Mode Filter Collapse Del Save Mode Filter Collapse Del +|4: ls ls +|5: zmarkfile.txt zmarkfile.txt +|6: +|7: /tmp/pardes-snap/topbarexec/cwd/+Search S +|8: ave Tty Collapse Del +|9: 1 zmarkfile.txt +|10: 2 diff --git a/test/snapshots/topbarexec.snap b/test/snapshots/topbarexec.snap index 516a23b3..81581b9d 100644 --- a/test/snapshots/topbarexec.snap +++ b/test/snapshots/topbarexec.snap @@ -19,9 +19,9 @@ wait 8000 zmarkfile.txt stable 700 20000 # sweep `zmarkfile.txt` out of the shell's own greeting: an ordinary left drag, # and the kind of stale highlight that is simply lying around in real use -press left 3 5 -drag left 15 5 -release left 15 5 +press left 3 6 +drag left 15 6 +release left 15 6 stable 700 15000 snap selected # Debug takes NO argument. Middle-clicked with that sweep live it must open the diff --git a/test/snapshots/tty.golden b/test/snapshots/tty.golden index 6c17c625..4034be54 100644 --- a/test/snapshots/tty.golden +++ b/test/snapshots/tty.golden @@ -1,7 +1,8 @@ -== snap tty-on grid=100x31 cursor=4,5 +== snap tty-on grid=100x31 cursor=4,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil +|$ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi +| lter Collapse Del lter Collapse Del | $ ls ls | m.txt m.txt | $ @@ -15,28 +16,28 @@ | | | -| -| /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil +| /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi +| lter Collapse Del | ls | m.txt -== snap tty-echo grid=100x31 cursor=4,7 -|5: $ echo SNAP''OK -|6: SNAPOK -|7: $ -== snap tty-off grid=100x31 cursor=4,7 -|2: /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil -|3: ls ls -|5: echo SNAP''OK -|7: -== snap shift-esc-on grid=100x31 cursor=4,7 -|2: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil -|3: $ ls ls -|5: $ echo SNAP''OK -|7: $ -== snap shift-esc-nodoc grid=100x31 cursor=4,20 -|3: $ ls Last ls -== snap ctrl-b-off grid=100x31 cursor=4,20 -|3: $ ls ls -|17: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fil -|18: $ ls -|20: $ +== snap tty-echo grid=100x31 cursor=4,8 +|6: $ echo SNAP''OK +|7: SNAPOK +|8: $ +== snap tty-off grid=100x31 cursor=4,8 +|2: /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi +|4: ls ls +|6: echo SNAP''OK +|8: +== snap shift-esc-on grid=100x31 cursor=4,8 +|2: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi +|4: $ ls ls +|6: $ echo SNAP''OK +|8: $ +== snap shift-esc-nodoc grid=100x31 cursor=4,21 +|4: $ ls Last ls +== snap ctrl-b-off grid=100x31 cursor=4,21 +|4: $ ls ls +|17: $ /tmp/pardes-snap/tty/cwd Tty+bash Save Mode Fi +|19: $ ls +|21: $ diff --git a/test/snapshots/ttyclick.golden b/test/snapshots/ttyclick.golden index 5233b8a9..75eb9b30 100644 --- a/test/snapshots/ttyclick.golden +++ b/test/snapshots/ttyclick.golden @@ -1,7 +1,8 @@ -== snap tty-bodyclick grid=100x31 cursor=4,5 +== snap tty-bodyclick grid=100x31 cursor=4,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod +|$ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo +| de Filter Collapse Del de Filter Collapse Del | $ ls ls | probe.txt probe.txt | $ @@ -15,13 +16,13 @@ | | | -| -| /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod +| /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo +| de Filter Collapse Del | ls | probe.txt == snap tty-tagedit grid=100x31 cursor=33,2 -== snap tty-tagexit grid=100x31 cursor=4,5 +== snap tty-tagexit grid=100x31 cursor=4,6 == snap ins-bodyclick grid=100x31 cursor=11,4 -|2: ^ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mod -|3: ls ls -|5: +|2: ^ /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyclick/cwd Tty+bash Save Mo +|4: ls ls +|6: diff --git a/test/snapshots/ttyedit.golden b/test/snapshots/ttyedit.golden index 144c97a0..5e25c810 100644 --- a/test/snapshots/ttyedit.golden +++ b/test/snapshots/ttyedit.golden @@ -1,7 +1,8 @@ -== snap before grid=100x31 cursor=7,7 +== snap before grid=100x31 cursor=7,8 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode +| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod +| e Filter Collapse Del e Filter Collapse Del | ls ls | words.txt words.txt | printf 'one two three\nfour five six\nseven ei @@ -15,28 +16,28 @@ | | | -| -| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode +| /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod +| e Filter Collapse Del | ls | words.txt -== snap split grid=100x31 cursor=2,8 -|2: ^ /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode -|7: one t -|8: wo three -|9: four five six -|10: seven eight nine -== snap opened grid=100x31 cursor=6,9 -|2: /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mode -|9: OPEN +== snap split grid=100x31 cursor=2,9 +|2: ^ /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod +|8: one t +|9: wo three +|10: four five six +|11: seven eight nine +== snap opened grid=100x31 cursor=6,10 +|2: /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod /tmp/pardes-snap/ttyedit/cwd Tty+bash Save Mod +|10: OPEN +|11: four five six +|12: seven eight nine +== snap joined grid=100x31 cursor=10,9 +|9: wo threeOPEN |10: four five six |11: seven eight nine -== snap joined grid=100x31 cursor=10,8 -|8: wo threeOPEN +|12: +== snap undone grid=100x31 cursor=7,8 +|8: one two three |9: four five six |10: seven eight nine |11: -== snap undone grid=100x31 cursor=7,7 -|7: one two three -|8: four five six -|9: seven eight nine -|10: diff --git a/test/snapshots/ttyedit.snap b/test/snapshots/ttyedit.snap index c453d230..6ee3ca03 100644 --- a/test/snapshots/ttyedit.snap +++ b/test/snapshots/ttyedit.snap @@ -17,8 +17,8 @@ stable 700 10000 key c-b stable 600 8000 # click into the middle of the first output row -press left 8 8 -release left 8 8 +press left 8 9 +release left 8 9 stable 400 5000 snap before # Enter splits the row there: "one t" stays, "wo three" becomes a real line diff --git a/test/snapshots/ttyfollow.golden b/test/snapshots/ttyfollow.golden index 692ecf8d..bb040593 100644 --- a/test/snapshots/ttyfollow.golden +++ b/test/snapshots/ttyfollow.golden @@ -1,10 +1,10 @@ == snap held-pre grid=100x31 cursor=4,16 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo -| 185 ls -| 186 s.txt -| 187 +| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M +| ode Filter Collapse Del ode Filter Collapse Del +| 186 ls +| 187 s.txt | 188 | 189 | 190 @@ -16,25 +16,25 @@ | 196 | 197 | 198 -| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo +| /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M +| ode Filter Collapse Del | ls | s.txt -== snap held-post grid=100x31 cursor=54,8 -|2: /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo$ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo -|3: 185 $ ls -|5: 187 $ touch go; until test -f done1; do sleep 0.05; -|6: 188 done; echo SYN''CED -|7: 189 SYNCED -|8: 190 $ +== snap held-post grid=100x31 cursor=54,9 +|2: /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M +|4: 186 $ ls +|6: 188 $ touch go; until test -f done1; do sleep 0.05; +|7: 189 done; echo SYN''CED +|8: 190 SYNCED +|9: 191 $ == snap tty-follows grid=100x31 cursor=4,16 -|2: $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo$ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save Mo -|3: 292 $ ls -|4: 293 s.txt -|5: 294 $ touch go; until test -f done1; do sleep 0.05; -|6: 295 done; echo SYN''CED -|7: 296 SYNCED -|8: 297 $ -|9: 298 +|2: $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M $ /tmp/pardes-snap/ttyfollow/cwd Tty+bash Save M +|4: 293 $ ls +|5: 294 s.txt +|6: 295 $ touch go; until test -f done1; do sleep 0.05; +|7: 296 done; echo SYN''CED +|8: 297 SYNCED +|9: 298 $ |10: 299 |11: 300 |12: [1]+ Done ( until test -f diff --git a/test/snapshots/ttylook.golden b/test/snapshots/ttylook.golden index 1d25d378..14c7c0c0 100644 --- a/test/snapshots/ttylook.golden +++ b/test/snapshots/ttylook.golden @@ -1,7 +1,8 @@ -== snap tok1 grid=120x31 cursor=2,4 +== snap tok1 grid=120x31 cursor=2,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co +| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter C /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter C +| ollapse Del ollapse Del | ls ls | big.txt plain.txt sub big.txt plain.txt sub | @@ -15,18 +16,18 @@ | | | -| -| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter Co +| /tmp/pardes-snap/ttylook/cwd Tty+bash Save Mode Filter C +| ollapse Del | ls | big.txt plain.txt sub == style tok1 grid=120x31 |0: 0-70 #b2c3b8,#1d2824, 71-119 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#26362d, 3-34 #becfc4,#26362d, 35-59 d,#26362d, 60-61 #ffffff,#887455, 62-62 d,#1d2824, 63-94 #b2c3b8,#1d2824, 95-119 d,#1d2824, -|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-39 #dfbce5,#26362d, 40-59 #becfc4,#26362d, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-91 #b2c3b8,#1d2824, 92-99 #d3b0d9,#1d2824, 100-119 #b2c3b8,#1d2824, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-8 #f5f7ee,#3c614a, 9-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|5: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-31 #becfc4,#26362d, 32-39 #dfbce5,#26362d, 40-58 #becfc4,#26362d, 59-59 d,#26362d, 60-61 #ffffff,#496656, 62-62 d,#1d2824, 63-91 #b2c3b8,#1d2824, 92-99 #d3b0d9,#1d2824, 100-118 #b2c3b8,#1d2824, 119-119 d,#1d2824, +|3: 0-2 d,#26362d, 3-13 #becfc4,#26362d, 14-59 d,#26362d, 60-62 d,#1d2824, 63-73 #b2c3b8,#1d2824, 74-119 d,#1d2824, +|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-8 #f5f7ee,#3c614a, 9-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|6: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-119 d,#0d1410, |7: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |9: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, @@ -37,11 +38,11 @@ |14: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |16: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-39 #d3b0d9,#1d2824, 40-59 #b2c3b8,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-39 #d3b0d9,#1d2824, 40-58 #b2c3b8,#1d2824, 59-59 d,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|18: 0-2 d,#1d2824, 3-13 #b2c3b8,#1d2824, 14-59 d,#1d2824, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |19: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|20: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, +|21: 0-0 d,#608269, 1-59 d,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |22: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |23: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |24: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, @@ -51,24 +52,20 @@ |28: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |29: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, |30: 0-0 d,#608269, 1-1 d,#0d1410, 2-59 #e5eee5,#0d1410, 60-60 d,#608269, 61-61 d,#0d1410, 62-119 #e5eee5,#0d1410, -== snap tok2 grid=120x31 cursor=11,4 -== snap tokback grid=120x31 cursor=2,4 -== snap insert grid=120x31 cursor=4,4 -|4: nnbig.txt plain.txt sub big.txt plain.txt sub -== snap tok3 grid=120x31 cursor=22,4 -|4: big.txt plain.txt sub big.txt plain.txt sub -== snap diropen grid=120x31 cursor=4,8 -|6: /tmp/pardes-snap/ttylook/cwd/sub Tty+bash Save Mode Filte -|7: ls -== snap noop grid=120x31 cursor=2,4 -== snap fileopen grid=120x31 cursor=7,22 -|21: /tmp/pardes-snap/ttylook/cwd/big.txt Save Tty Collapse De -|22: 1 line 1 -|23: 2 line 2 -|24: 3 line 3 -|25: 4 line 4 -|26: 5 line 5 -|27: 6 line 6 -|28: 7 line 7 -|29: 8 line 8 -|30: 9 line 9 +== snap tok2 grid=120x31 cursor=11,5 +== snap tokback grid=120x31 cursor=2,5 +== snap insert grid=120x31 cursor=4,5 +|5: nnbig.txt plain.txt sub big.txt plain.txt sub +== snap tok3 grid=120x31 cursor=22,5 +|5: big.txt plain.txt sub big.txt plain.txt sub +== snap diropen grid=120x31 cursor=4,10 +|7: /tmp/pardes-snap/ttylook/cwd/sub Tty+bash Save Mode Filt +|8: er Collapse Del +|9: ls +== snap noop grid=120x31 cursor=2,5 +== snap fileopen grid=120x31 cursor=3,4 +|22: /tmp/pardes-snap/ttylook/cwd/+Search Save Tty Collapse D +|23: el LocationsConfig +|24: 1 @p0:1:1-2 +|25: 2 ls +|26: 3 diff --git a/test/snapshots/ttypaste.golden b/test/snapshots/ttypaste.golden index 448cbce3..6945faec 100644 --- a/test/snapshots/ttypaste.golden +++ b/test/snapshots/ttypaste.golden @@ -1,7 +1,8 @@ -== snap bracketed-paste-reached-the-shell grid=100x31 cursor=18,5 +== snap bracketed-paste-reached-the-shell grid=100x31 cursor=18,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod +|$ /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mo /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mo +| de Filter Collapse Del de Filter Collapse Del | $ ls ls | src.txt src.txt | $ echo BRACKETED @@ -15,8 +16,8 @@ | | | -| -| /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mod +| /tmp/pardes-snap/ttypaste/cwd Tty+bash Save Mo +| de Filter Collapse Del | ls | src.txt -== snap ctrl-v-with-empty-register-is-inert grid=100x31 cursor=18,5 +== snap ctrl-v-with-empty-register-is-inert grid=100x31 cursor=18,6 diff --git a/test/snapshots/ttyprompt.golden b/test/snapshots/ttyprompt.golden index bfdfcd4b..1bbaacf4 100644 --- a/test/snapshots/ttyprompt.golden +++ b/test/snapshots/ttyprompt.golden @@ -1,7 +1,8 @@ -== snap greeted grid=100x31 cursor=4,5 +== snap greeted grid=100x31 cursor=4,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo +| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M +| ode Filter Collapse Del ode Filter Collapse Del | ls ls | probe.txt probe.txt | @@ -15,15 +16,15 @@ | | | -| -| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo +| /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M +| ode Filter Collapse Del | ls | probe.txt -== snap in-tty grid=100x31 cursor=17,5 -|2: $ /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo -|3: $ ls ls -|5: $ echo ABCDEFGH -== snap out-of-tty grid=100x31 cursor=17,5 -|2: /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save Mo -|3: ls ls -|5: echo ABCDEFGH +== snap in-tty grid=100x31 cursor=17,6 +|2: $ /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M +|4: $ ls ls +|6: $ echo ABCDEFGH +== snap out-of-tty grid=100x31 cursor=17,6 +|2: /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M /tmp/pardes-snap/ttyprompt/cwd Tty+bash Save M +|4: ls ls +|6: echo ABCDEFGH diff --git a/test/snapshots/winch.golden b/test/snapshots/winch.golden index 2e960cc6..f009aa5d 100644 --- a/test/snapshots/winch.golden +++ b/test/snapshots/winch.golden @@ -1,7 +1,8 @@ -== snap storm grid=100x31 cursor=4,7 +== snap storm grid=100x31 cursor=4,8 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F +|$ /tmp/pardes-snap/winch/cwd Tty+bash Save Mode /tmp/pardes-snap/winch/cwd Tty+bash Save Mode +| Filter Collapse Del Filter Collapse Del | $ ls ls | $ for i in $(seq 600); do kill -WINCH $PPID; don | e; echo SUR''VIVED @@ -15,13 +16,14 @@ | | | -| -| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode F +| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode +| Filter Collapse Del | ls -== snap resized grid=106x35 cursor=4,7 +== snap resized grid=106x35 cursor=4,8 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|$ /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt +|$ /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Fil /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Fil +| ter Collapse Del ter Collapse Del | $ ls ls | $ for i in $(seq 600); do kill -WINCH $PPID; done; | echo SUR''VIVED @@ -37,6 +39,6 @@ | | | -| -| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Filt +| /tmp/pardes-snap/winch/cwd Tty+bash Save Mode Fil +| ter Collapse Del | ls diff --git a/test/snapshots/windownav.golden b/test/snapshots/windownav.golden index 4422a953..2a3319f9 100644 --- a/test/snapshots/windownav.golden +++ b/test/snapshots/windownav.golden @@ -1,35 +1,37 @@ -== snap start grid=100x31 cursor=7,3 +== snap start grid=100x31 cursor=7,4 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/windownav/cwd/w.txt Save Tty C /tmp/pardes-snap/windownav/cwd Tty+bash Save Mo +| /tmp/pardes-snap/windownav/cwd/w.txt Save Tty /tmp/pardes-snap/windownav/cwd Tty+bash Save M +| Collapse Del ode Filter Collapse Del | 1 alpha ls Left | 2 beta w.txt | 3 gamma -== snap to-term grid=100x31 cursor=54,5 -|3: 1 alpha Last ls -== snap back-to-file grid=100x31 cursor=7,3 -|3: 1 alpha ls Last -== snap esc-to-term grid=100x31 cursor=54,5 -|3: 1 alpha Last ls -== snap esc-back-to-file grid=100x31 cursor=7,3 -|3: 1 alpha ls Last -== snap altn grid=100x31 cursor=4,9 -|3: 1 alpha ls -|6: /tmp/pardes-snap/windownav/cwd Tty+bash Save Mo -|7: ls -|8: w.txt -== snap up grid=100x31 cursor=7,3 -|7: ls Up -== snap right grid=100x31 cursor=54,5 -|3: 1 alpha Right ls -|7: ls -== snap left grid=100x31 cursor=7,3 -|3: 1 alpha ls Left -== snap down grid=100x31 cursor=4,9 -|3: 1 alpha Down ls -== snap toggle-doc grid=100x31 cursor=7,3 -|3: 1 alpha ls -|7: ls Last -== snap toggle-back grid=100x31 cursor=4,9 -|3: 1 alpha Last ls -|7: ls +== snap to-term grid=100x31 cursor=54,6 +|4: 1 alpha Last ls +== snap back-to-file grid=100x31 cursor=7,4 +|4: 1 alpha ls Last +== snap esc-to-term grid=100x31 cursor=54,6 +|4: 1 alpha Last ls +== snap esc-back-to-file grid=100x31 cursor=7,4 +|4: 1 alpha ls Last +== snap altn grid=100x31 cursor=4,11 +|4: 1 alpha ls +|7: /tmp/pardes-snap/windownav/cwd Tty+bash Save M +|8: ode Filter Collapse Del +|9: ls +|10: w.txt +== snap up grid=100x31 cursor=7,4 +|9: ls Up +== snap right grid=100x31 cursor=54,6 +|4: 1 alpha Right ls +|9: ls +== snap left grid=100x31 cursor=7,4 +|4: 1 alpha ls Left +== snap down grid=100x31 cursor=4,11 +|4: 1 alpha Down ls +== snap toggle-doc grid=100x31 cursor=7,4 +|4: 1 alpha ls +|9: ls Last +== snap toggle-back grid=100x31 cursor=4,11 +|4: 1 alpha Last ls +|9: ls diff --git a/test/snapshots/windowops.golden b/test/snapshots/windowops.golden index 63a7574a..98454854 100644 --- a/test/snapshots/windowops.golden +++ b/test/snapshots/windowops.golden @@ -1,7 +1,8 @@ -== snap hover-v grid=100x31 cursor=4,5 +== snap hover-v grid=100x31 cursor=4,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/windowops/cwd Tty+bash Save M╎ /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo +| /tmp/pardes-snap/windowops/cwd Tty+bash Save M╎ /tmp/pardes-snap/windowops/cwd Tty+bash Save M +| ode Filter Collapse Del ╎ ode Filter Collapse Del | ls ╎ ls | w.txt ╎ w.txt | ╎ @@ -15,8 +16,8 @@ | ╎ | ╎ | ╎ -| ╎ | /tmp/pardes-snap/windowops/cwd Tty+bash Save M╎ +| ode Filter Collapse Del ╎ | ls ╎ | w.txt ╎ | ╎ @@ -29,13 +30,12 @@ | ╎ | ╎ | ╎ -| ╎ -== snap hover-h grid=100x31 cursor=4,5 +== snap hover-h grid=100x31 cursor=4,6 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo -|3: ls ls -|4: w.txt w.txt -|5: +|2: /tmp/pardes-snap/windowops/cwd Tty+bash Save M /tmp/pardes-snap/windowops/cwd Tty+bash Save M +|3: ode Filter Collapse Del ode Filter Collapse Del +|4: ls ls +|5: w.txt w.txt |6: |7: |8: @@ -47,10 +47,10 @@ |14: |15: |16: -|17: /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo -|18: ls -|19: w.txt -|20: +|17: /tmp/pardes-snap/windowops/cwd Tty+bash Save M +|18: ode Filter Collapse Del +|19: ls +|20: w.txt |21: |22: |23: @@ -61,12 +61,12 @@ |28: |29: |30: -== snap border-drag grid=100x31 cursor=4,5 +== snap border-drag grid=100x31 cursor=4,6 |1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save Mo /tmp/pardes-snap/windowops/cwd Tty+bash Save Mo -|3: ls ╎ ls -|4: w.txt ╎ w.txt -|5: ╎ +|2: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save M /tmp/pardes-snap/windowops/cwd Tty+bash Save M +|3: ode Filter Collapse Del ╎ ode Filter Collapse Del +|4: ls ╎ ls +|5: w.txt ╎ w.txt |6: ╎ |7: ╎ |8: ╎ @@ -78,10 +78,10 @@ |14: ╎ |15: ╎ |16: ╎ -|17: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save Mo -|18: ls ╎ -|19: w.txt ╎ -|20: ╎ +|17: /tmp/pardes-snap/windowops/cwd Tty+b╎sh Save M +|18: ode Filter Collapse Del ╎ +|19: ls ╎ +|20: w.txt ╎ |21: ╎ |22: ╎ |23: ╎ @@ -92,18 +92,20 @@ |28: ╎ |29: ╎ |30: ╎ -== snap border-done grid=100x31 cursor=4,5 +== snap border-done grid=100x31 cursor=4,6 |1: New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol |2: /tmp/pardes-snap/windowops/cwd Tty+b╎ /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter -|3: ls ╎ ls -|4: w.txt ╎ w.txt +|3: ash Save Mode Filter Collapse Del ╎ Collapse Del +|4: ls ╎ ls +|5: w.txt ╎ w.txt |17: /tmp/pardes-snap/windowops/cwd Tty+b╎ -== snap hborder-done grid=100x31 cursor=4,5 +|18: ash Save Mode Filter Collapse Del ╎ +== snap hborder-done grid=100x31 cursor=4,6 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter -|3: ls ls -|4: w.txt w.txt -|5: +|2: /tmp/pardes-snap/windowops/cwd Tty+b /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter +|3: ash Save Mode Filter Collapse Del Collapse Del +|4: ls ls +|5: w.txt w.txt |6: |7: |8: @@ -111,10 +113,10 @@ |10: |11: |12: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ -|13: /tmp/pardes-snap/windowops/cwd Tty+ba -|14: ls -|15: w.txt -|16: +|13: /tmp/pardes-snap/windowops/cwd Tty+b +|14: ash Save Mode Filter Collapse Del +|15: ls +|16: w.txt |17: |18: |19: @@ -129,51 +131,52 @@ |28: |29: |30: -== snap move-preview grid=100x31 cursor=4,16 +== snap move-preview grid=100x31 cursor=4,17 |12: -== snap moved grid=100x31 cursor=4,16 -== snap altn grid=100x31 cursor=4,20 -|17: /tmp/pardes-snap/windowops/cwd Tty+ba -|18: ls -|19: w.txt -== snap focus-right grid=100x31 cursor=44,5 -|18: ls Right -== snap debug-on grid=100x31 cursor=44,5 +== snap moved grid=100x31 cursor=4,17 +== snap altn grid=100x31 cursor=4,22 +|18: /tmp/pardes-snap/windowops/cwd Tty+b +|19: ash Save Mode Filter Collapse Del +|20: ls +|21: w.txt +== snap focus-right grid=100x31 cursor=44,6 +|20: ls Right +== snap debug-on grid=100x31 cursor=44,6 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep J╭────────────────────────────────────╮ -|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/wi│pardes resizes=1 │ -|3: ls ls │theme orchard │ -|4: w.txt w.txt │font default │ -|5: │terms 4 cols 2 │ -|6: │active #2 58x28 │ -|7: │scroll 0/28 │ +|2: /tmp/pardes-snap/windowops/cwd Tty+b /tmp/pardes-snap/wi│pardes resizes=1 │ +|3: ash Save Mode Filter Collapse Del Collapse Del │theme orchard │ +|4: ls ls │font default │ +|5: w.txt w.txt │terms 4 cols 2 │ +|6: │active #2 58x27 │ +|7: │scroll 0/27 │ |8: │pinch 1.0000 │ |9: │tscroll 0.0000 │ |10: ╰────────────────────────────────────╯ -|18: ls +|20: ls == style debug-on grid=100x31 |0: 0-60 #b2c3b8,#1d2824, 61-65 #f5f7ee,#3c614a, 66-70 #b2c3b8,#1d2824, 71-99 d,#1d2824, |1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-39 d,#1d2824, 40-41 #ffffff,#887455, 42-42 d,#26362d, 43-61 #becfc4,#26362d, 62-99 d,d, -|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-39 #d3b0d9,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-61 #becfc4,#26362d, 62-62 d,d, 63-79 #b2c3b8,d, 80-98 d,#152019, 99-99 d,d, -|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-76 #b2c3b8,d, 77-98 d,#152019, 99-99 d,d, +|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-38 #d3b0d9,#1d2824, 39-39 d,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-61 #becfc4,#26362d, 62-62 d,d, 63-79 #b2c3b8,d, 80-98 d,#152019, 99-99 d,d, +|3: 0-2 d,#1d2824, 3-5 #d3b0d9,#1d2824, 6-35 #b2c3b8,#1d2824, 36-39 d,#1d2824, 40-42 d,#26362d, 43-55 #becfc4,#26362d, 56-61 d,#26362d, 62-62 d,d, 63-76 #b2c3b8,d, 77-98 d,#152019, 99-99 d,d, |4: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-76 #b2c3b8,d, 77-98 d,#152019, 99-99 d,d, -|5: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-61 d,#0d1410, 62-62 d,d, 63-77 #b2c3b8,d, 78-98 d,#152019, 99-99 d,d, -|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-78 #b2c3b8,d, 79-98 d,#152019, 99-99 d,d, +|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-77 #b2c3b8,d, 78-98 d,#152019, 99-99 d,d, +|6: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-61 d,#0d1410, 62-62 d,d, 63-78 #b2c3b8,d, 79-98 d,#152019, 99-99 d,d, |7: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-73 #b2c3b8,d, 74-98 d,#152019, 99-99 d,d, |8: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-75 #b2c3b8,d, 76-98 d,#152019, 99-99 d,d, |9: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-62 d,d, 63-77 #b2c3b8,d, 78-98 d,#152019, 99-99 d,d, |10: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-61 #e5eee5,#0d1410, 62-99 d,d, |11: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |12: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|13: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-39 #d3b0d9,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|13: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-38 #d3b0d9,#1d2824, 39-39 d,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|14: 0-2 d,#1d2824, 3-5 #d3b0d9,#1d2824, 6-35 #b2c3b8,#1d2824, 36-39 d,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |15: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|16: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|17: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-39 #d3b0d9,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|20: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|17: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|18: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-33 #b2c3b8,#1d2824, 34-38 #d3b0d9,#1d2824, 39-39 d,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|19: 0-2 d,#1d2824, 3-5 #d3b0d9,#1d2824, 6-35 #b2c3b8,#1d2824, 36-39 d,#1d2824, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |21: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, +|22: 0-0 d,#608269, 1-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |23: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |24: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |25: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, @@ -182,12 +185,12 @@ |28: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |29: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, |30: 0-0 d,#608269, 1-1 d,#0d1410, 2-39 #e5eee5,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410, -== snap debug-off grid=100x31 cursor=44,5 +== snap debug-off grid=100x31 cursor=44,6 |1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol -|2: /tmp/pardes-snap/windowops/cwd Tty+ba /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter -|3: ls ls -|4: w.txt w.txt -|5: +|2: /tmp/pardes-snap/windowops/cwd Tty+b /tmp/pardes-snap/windowops/cwd Tty+bash Save Mode Filter +|3: ash Save Mode Filter Collapse Del Collapse Del +|4: ls ls +|5: w.txt w.txt |6: |7: |8: diff --git a/test/snapshots/wrap.golden b/test/snapshots/wrap.golden index 8ef0ca9f..df41535b 100644 --- a/test/snapshots/wrap.golden +++ b/test/snapshots/wrap.golden @@ -1,7 +1,8 @@ -== snap on-narrow grid=48x15 cursor=7,3 +== snap on-narrow grid=48x15 cursor=7,4 |Newcol Joincol Find Grep Help Changelog Tutor Du | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/wrap/cwd/long.txt Save Tty C +| /tmp/pardes-snap/wrap/cwd/long.txt Save Tty +| Collapse Del | 1 0---------1---------2---------3---------↩ | 4---------5---------6---------7---------↩ | 8---------9---------A---------B---------↩ @@ -11,28 +12,28 @@ | 3 third line | 4 == snap clicked grid=48x15 cursor=17,4 -== snap logical-j grid=48x15 cursor=18,8 -== snap visual-gj grid=48x15 cursor=10,4 -== snap visual-gj-again grid=48x15 cursor=10,5 -== snap visual-gk grid=48x15 cursor=10,4 -== snap visual-count-gj grid=48x15 cursor=10,6 -== snap off grid=48x15 cursor=10,6 -|3: 1 0---------1---------2---------3---- Wrap -|4: 2 second line -|5: 3 third line -|6: 4 -|7: +== snap logical-j grid=48x15 cursor=17,9 +== snap visual-gj grid=48x15 cursor=10,5 +== snap visual-gj-again grid=48x15 cursor=10,6 +== snap visual-gk grid=48x15 cursor=10,5 +== snap visual-count-gj grid=48x15 cursor=10,7 +== snap off grid=48x15 cursor=10,7 +|4: 1 0---------1---------2---------3---- Wrap +|5: 2 second line +|6: 3 third line +|7: 4 |8: |9: |10: -== snap on-again grid=48x15 cursor=10,6 -|4: 4---------5---------6---------7---------↩ -|5: 8---------9---------A---------B---------↩ -|6: C---------D---------E---------F---------↩ -|7: G---------H---------I---------J--------- -|8: 2 second line -|9: 3 third line -|10: 4 +|11: +== snap on-again grid=48x15 cursor=10,7 +|5: 4---------5---------6---------7---------↩ +|6: 8---------9---------A---------B---------↩ +|7: C---------D---------E---------F---------↩ +|8: G---------H---------I---------J--------- +|9: 2 second line +|10: 3 third line +|11: 4 == snap on-wide grid=128x15 cursor=7,3 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol diff --git a/test/snapshots/yankpaste.golden b/test/snapshots/yankpaste.golden index 2efcd66d..22cc787f 100644 --- a/test/snapshots/yankpaste.golden +++ b/test/snapshots/yankpaste.golden @@ -1,7 +1,7 @@ == snap linewise-below grid=100x31 cursor=13,6 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol -| /tmp/pardes-snap/yankpaste/cwd/f.txt * Save Tty Collapse Del +| * /tmp/pardes-snap/yankpaste/cwd/f.txt Save Tty Collapse Del | 1 line 1 | 2 line 2 | 3 line 3 |
