summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/helix-keys.md3
-rw-r--r--src/pardes.zig114
-rw-r--r--src/tutor.txt19
-rw-r--r--test/snapshots/tagnav.golden219
-rw-r--r--test/snapshots/tagnav.snap52
5 files changed, 391 insertions, 16 deletions
diff --git a/docs/helix-keys.md b/docs/helix-keys.md
index 6a77a28d..6f5fe0e7 100644
--- a/docs/helix-keys.md
+++ b/docs/helix-keys.md
@@ -87,7 +87,8 @@ with a reason in `test/hxcases/waivers.jsonl`.
| `Esc` (insert) | back to normal mode, cursor right after the insertion (no vim left-step) | | helix-verified |
| `Enter` (normal) | acme **look** chord: EXPLICIT selection, else file-ish word under cursor | pardes-specific, keep (helix normal-mode Enter unbound). Covers helix `gf`. Implicit motion residue falls back to the cursor word | pardes-specific |
| `Tab` (normal) | acme **execute** chord | pardes-specific, keep; explicit-selection rule as Enter | pardes-specific |
-| `:` (normal, body) | focuses the pane's OWN tag as a one-line editor in **normal** mode, parked at the first EDITABLE column: motions (`w` `b` `e` `W` `B` `E`, `0` `$` `^`, arrows, `Home`/`End`) walk the whole rendered tag, `y` yanks the selection, `Enter`/`Tab` look/execute it (else the file-ish word under the cursor), `i`/`a`/`I`/`A` enter insert, `Esc` hands the body back. `h`/`j`/`k`/`l` are NOT motion here — a tagline is a place in the LAYOUT, so they run the same `Left`/`Down`/`Up`/`Right` builtins and land on the neighbouring pane's TAGLINE, still in normal mode (nothing that way = stay put); the arrows keep the in-tag motion | helix `:` is command mode (section C); pardes' commands are acme words that live in the tag. `tag_col`/`tag_anchor` are columns of the RENDERED tag (prefix ++ tail) — one coordinate space, so the live mode+path prefix is selectable, yankable and executable, while every edit op (typing, `Backspace`, `i`/`a`/`I`/`A`) measures from the first editable column and is inert inside it | pardes-specific |
+| `:` (normal, body) | focuses the pane's OWN tag as a one-line editor in **normal** mode, parked at the first EDITABLE column: motions (`w` `b` `e` `W` `B` `E`, `0` `$` `^`, arrows, `Home`/`End`) walk the whole rendered tag, `y` yanks the selection, `Enter`/`Tab` look/execute it (else the file-ish word under the cursor), `i`/`a`/`I`/`A` enter insert, `Esc` hands the body back. `h`/`j`/`k`/`l` are NOT motion here — a tagline is a place in the LAYOUT, so they run the same `Left`/`Down`/`Up`/`Right` builtins and land on the neighbouring pane's TAGLINE, still in normal mode (nothing that way = stay put, EXCEPT `k` off the topmost tagline — see the next row); the arrows keep the in-tag motion | helix `:` is command mode (section C); pardes' commands are acme words that live in the tag. `tag_col`/`tag_anchor` are columns of the RENDERED tag (prefix ++ tail) — one coordinate space, so the live mode+path prefix is selectable, yankable and executable, while every edit op (typing, `Backspace`, `i`/`a`/`I`/`A`) measures from the first editable column and is inert inside it | pardes-specific |
+| `k` (tag normal, topmost tagline) | focuses the TOPBAR — row 0, the global tagline (`Kill Newcol Tutor Debug NextColor Dump`, plus `Restore <path>` once a dump exists). It is its own one-line normal mode: `h`/`l` and the arrows by grapheme (row 0 has no window left or right to walk to), `w`/`b`/`e`/`W`/`B`/`E` and `0`/`$`/`^` by word, `Enter`/`Tab` runs the word under the cursor through the same dispatch a MIDDLE click on it uses, `j` drops back onto the topmost pane's tagline, `Esc` leaves. No insert mode and no selection — the bar is chrome with no tail to own | pardes-specific. The topbar is not a pane, so `focusDir` can never reach it: this is a fallback on the `.Up` branch of the tagline hop, from a TAGLINE only (a body's `SPC w k`/`Ctrl-w k` keep their pane-to-pane meaning). Its whole state is one global `topbar_col: ?u16` (row 0 has no pane to hang it on), cleared by any mouse press and BEFORE the chord dispatch, because `Kill` up here frees the session the way `Del` frees a pane. The motion vocabulary is literally the tag's — both call `lineMotion` | pardes-specific |
| `Ctrl-w` + `h/j/k/l`/arrows | directional pane focus prefix — normal/tty modes only | pardes' own window handling (helix window mode skipped, section C). Runs the SAME `Left`/`Down`/`Up`/`Right` builtins `SPC w h/j/k/l` runs; kept alongside the leader because a pane in raw **tty** mode never sees `SPC` (the shell owns it), so this is the only keyboard way out of one. Insert mode owns `Ctrl-w` = delete-word-back, so a tag being TYPED into swallows it; from a tag in normal mode (`:`) it moves focus to the neighbour's BODY, while the bare letters `h/j/k/l` there move to its TAGLINE (next row) | pardes-specific |
| `Alt-n` | new terminal below (any mode) | shadows helix `Alt-n` TS sibling-select — skipped anyway (tree-sitter) | pardes-specific |
| `Alt-c` | move active terminal to a fresh column (any mode) | helix `Alt-c` is change-noyank; the pardes window op wins (do-not-touch contract). `Alt-d` + `i` covers the behavior | waived (`alt-c-window-op`) |
diff --git a/src/pardes.zig b/src/pardes.zig
index 6df520ce..8a947065 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -920,6 +920,11 @@ pub const Pardes = struct {
leader_on: bool = false,
leader_keys: [4]u8 = undefined,
leader_n: u8 = 0,
+ /// the TOPBAR holds the keyboard, parked at this column of the rendered
+ /// row-0 line. Global like leader_on for the same reason: row 0 is not a
+ /// pane and never will be, so its one piece of focus state cannot live on
+ /// one. `null` = the panes have the keyboard, which is every other frame.
+ topbar_col: ?u16 = null,
ov_pinch_scale: f32 = 1.0,
ov_touch_scroll_delta: f32 = 0.0,
/// middle+left chord argument: a kept left selection captured by a left
@@ -1390,7 +1395,8 @@ pub const Pardes = struct {
// Left/Down/Up/Right builtins `SPC w h/j/k/l` and `Ctrl-w` run — one
// focusDir. The ARROWS below keep the in-tag grapheme motion, so the
// letters cost nothing. Nothing in that direction = stay put, tag and
- // all: focusDir left `active` alone, so there is nothing to undo.
+ // all: focusDir left `active` alone, so there is nothing to undo —
+ // EXCEPT upwards, where "nothing" is still something (see below).
const dir: ?Builtin = if (is(key, 'h'))
.Left
else if (is(key, 'j'))
@@ -1404,13 +1410,35 @@ pub const Pardes = struct {
if (dir) |d| {
const from = p.active;
p.runBuiltin(d, from, "");
- if (p.active == from) return;
+ if (p.active == from) {
+ // above the topmost tagline is the TOPBAR — row 0, the global
+ // one. It is not a pane, so focusDir can never reach it; this
+ // one fallback is what makes `k` walk off the top of the
+ // layout instead of dying there. Only from a tagline: a body's
+ // `SPC w k`/`Ctrl-w k` keeps its pane-to-pane meaning.
+ if (d == .Up) {
+ exitTagEdit(pane);
+ p.topbar_col = 0;
+ }
+ return;
+ }
const dest = p.panes[p.active] orelse return;
exitTagEdit(pane); // the pane we left is fully out of its tag...
p.enterTagEdit(dest, -1); // ...and the tail's start is the new park
if (dest.tag_edit) dest.mode = .normal;
return;
}
+ if (lineMotion(text, cur, key)) |r| setTagRange(pane, text, r);
+ }
+
+ /// the one-line normal-mode motion vocabulary as a helix range over `text`,
+ /// or null when `key` is not one of them: arrows by grapheme, `0`/`$`/`^`
+ /// (Home/End) to the ends, and the word motions, which select the span they
+ /// traverse exactly like the body's. A pane's tag and the TOPBAR are the
+ /// same one-line normal mode over different bytes, so the keys are read in
+ /// one place; only `h`/`l` differ — a tagline spends them on the layout,
+ /// the topbar has no layout — and each caller answers those itself.
+ fn lineMotion(text: []const u8, cur: usize, key: Key) ?modal.HxRange {
const target: ?usize = if (is(key, Key.left))
modal.prevGrapheme(text, cur)
else if (is(key, Key.right))
@@ -1423,8 +1451,7 @@ pub const Pardes = struct {
modal.firstNonWs(text)
else
null;
- if (target) |t| return setTagRange(pane, text, .{ .anchor = t, .head = t });
- // word motions select their traversed span, exactly like the body's
+ if (target) |t| return .{ .anchor = t, .head = t };
const wt: ?modal.WordTarget = if (is(key, 'w'))
.next_word_start
else if (is(key, 'b'))
@@ -1439,10 +1466,69 @@ pub const Pardes = struct {
.next_long_word_end
else
null;
- if (wt) |t| {
- const range: modal.HxRange = .{ .anchor = cur, .head = modal.nextGrapheme(text, cur) };
- return setTagRange(pane, text, modal.hxWordMove(text, range, 1, t));
+ const t = wt orelse return null;
+ return modal.hxWordMove(text, .{ .anchor = cur, .head = modal.nextGrapheme(text, cur) }, 1, t);
+ }
+
+ /// normal mode ON the topbar — row 0, where `k` off a top-row tagline
+ /// lands. The same one-line vocabulary a tag has (lineMotion), plus `h`/`l`
+ /// as plain grapheme motion: up here they have no neighbouring window to
+ /// walk to, so they cost nothing. Enter/Tab runs the word under the cursor
+ /// through the very dispatch a middle click on it uses, and `j` drops back
+ /// onto a tagline. The bar is chrome with no tail of its own, so there is
+ /// deliberately no insert mode and no selection: focus, move, run, leave.
+ fn topbarKey(p: *Pardes, key: Key) void {
+ var tb_buf: [1200]u8 = undefined;
+ const bar = p.topbar(&tb_buf);
+ // the line GROWS a `Restore <path>` word the moment a dump lands, so
+ // the column is clamped against the live line on every use
+ const cur: usize = @min(@as(usize, p.topbar_col orelse return), bar.len);
+ if (is(key, Key.escape)) {
+ p.topbar_col = null; // the active pane still has its body focus
+ return;
}
+ // the chord: run the word under the cursor, exactly as a middle click
+ // on it does. Leave the bar FIRST — `Kill` lives up here and tears the
+ // session down, the same hazard the pane-tag chord has with `Del`.
+ if (is(key, Key.enter) or is(key, Key.tab)) {
+ const word = wordAtCol(bar, @intCast(cur));
+ p.topbar_col = null;
+ if (word.len > 0) p.actOnSelection(.middle, p.active, word, null);
+ return;
+ }
+ // j — back down onto a tagline, the mirror of the k that got you here:
+ // the pane you came from if it still holds the top row, else the
+ // leftmost pane that does. Recomputed, never remembered, so a pane
+ // deleted while the bar had focus strands nobody.
+ if (is(key, 'j')) {
+ var dest: ?usize = null;
+ for (p.panes, 0..) |slot, i| {
+ if (slot == null or p.rects[i].y != TOPBAR_H) continue;
+ if (i == p.active) {
+ dest = i;
+ break;
+ }
+ if (dest == null or p.rects[i].x < p.rects[dest.?].x) dest = i;
+ }
+ const d = dest orelse return;
+ const pane = p.panes[d].?;
+ p.topbar_col = null;
+ p.active = d;
+ p.enterTagEdit(pane, -1);
+ if (pane.tag_edit) pane.mode = .normal;
+ return;
+ }
+ const col: ?usize = if (is(key, 'h'))
+ modal.prevGrapheme(bar, cur)
+ else if (is(key, 'l'))
+ modal.nextGrapheme(bar, cur)
+ else if (lineMotion(bar, cur, key)) |r|
+ modal.hxCursor(bar, r)
+ else
+ null;
+ // never past the last cell: there is nothing to append up here, so the
+ // block cursor stays ON a character (which is where `$` already lands)
+ if (col) |c| p.topbar_col = @intCast(@min(c, modal.prevGrapheme(bar, bar.len)));
}
/// tag + '\n' + body: the full selectable pane text (row 0 = the tag)
@@ -1513,6 +1599,10 @@ pub const Pardes = struct {
}
fn handleKey(p: *Pardes, key: Key) void {
+ // the topbar holds the keyboard (`k` off the topmost tagline): row 0 is
+ // its own one-line normal mode and owns every key until Esc or a chord.
+ // Before the pane lookup because it needs no pane — that is the point.
+ if (p.topbar_col != null) return p.topbarKey(key);
const pane = p.panes[p.active] orelse return;
// a SPC leader in flight swallows the next key, whatever it is —
// before Ctrl-w, so a modified key abandons the sequence instead of
@@ -3935,6 +4025,10 @@ pub const Pardes = struct {
p.chord_arg = null;
}
}
+ // ...and any press at all takes the keyboard back off the topbar: the
+ // mouse names where focus goes, so leaving a cursor parked on row 0
+ // while you click into a pane would just be a lie
+ if (m.kind == .press) p.topbar_col = null;
switch (m.button) {
.none => {}, // hover tracked above; motion has no other meaning
@@ -5456,6 +5550,12 @@ pub const Pardes = struct {
s.fill(0, 0, s.cols, TOPBAR_H, .{ .bg = .{ .rgb = th.tag_bg } });
var tb_buf: [1200]u8 = undefined;
_ = s.print(0, 0, s.cols, p.topbar(&tb_buf), .{ .fg = .{ .rgb = th.tag_fg }, .bg = .{ .rgb = th.tag_bg } });
+ // the topbar's cursor, if it has the keyboard. AFTER the pane loop on
+ // purpose: there is exactly one Surface cursor and the bar's must beat
+ // the active pane's. Always a block — the bar has no insert mode.
+ if (p.topbar_col) |c| if (c < s.cols) {
+ s.cursor = .{ .x = c, .y = 0, .bar = false };
+ };
// resize-handle hint / drag previews: a dash overlay that keeps the
// underlying colors (border drags + hover), or the move indicator
diff --git a/src/tutor.txt b/src/tutor.txt
index c263015b..2d0e951a 100644
--- a/src/tutor.txt
+++ b/src/tutor.txt
@@ -124,7 +124,9 @@
ITS tagline, still in normal mode — you walk the taglines of the screen
and never drop through a body. (Same moves as SPC w h/j/k/l and Ctrl-w
h/j/k/l; nothing that way = you stay put.) The ARROWS are the in-tag
- motion, and w/b/e/0/$/^ still walk the tag's own words.
+ motion, and w/b/e/0/$/^ still walk the tag's own words. Off the TOPMOST
+ tagline k keeps going: above it is the top bar, which is a tagline too
+ — the screen's own — and j comes back down.
The MODE + path part of a tag is live chrome, so it cannot be edited —
but it CAN be selected, by motion or by dragging across it: 0 then W/E
selects the path, `y` yanks it, Enter looks it. Typing or backspacing
@@ -135,9 +137,14 @@
across the top of the screen holds the window-agnostic builtins:
Kill Newcol Tutor Debug NextColor Dump
Middle-click "Newcol" for a new column, "Tutor" to spawn this tutor,
- "Kill" to quit; Debug toggles the stats overlay and NextColor cycles
- the theme: "helix" (the default — a near-black page and chrome that
- barely lifts off it, copied from the helix editor's own theme), then
+ "Kill" to quit. From the KEYBOARD the bar is `k` off the topmost
+ tagline: it takes the cursor, h/l and the arrows move by character,
+ w/b/e and 0/$ by word, Enter or Tab runs the word under the cursor
+ exactly as a middle-click on it would, j drops back onto the tagline
+ below and Esc leaves. There is nothing to type up here — the bar is
+ chrome, so it has no insert mode. Debug toggles the stats overlay
+ and NextColor cycles the theme: "helix" (the default — a near-black
+ page and chrome that barely lifts off it, copied from helix's own), then
"dark", then the acme-light yellow. The bar holds only what you reach
for often — the rest of the builtins are words you execute and keys
you press. Every one of them has a KEY PATH under SPC (part 3.7):
@@ -526,6 +533,7 @@
2-1 = middle-exec with the left selection as argument
file tag = mode + path + "Save Del"; other tags show Del
top bar = Kill Newcol Tutor Debug NextColor Dump
+ (keyboard: k off the topmost tagline)
WEB TOUCH: one-finger tap = LOOK; drag = natural scroll
LOOK opens URLs or embedded tracked .zig source (read-only)
published TTY dump lists every source; tree-sitter colors it
@@ -537,7 +545,8 @@
u undo U redo Enter=look Tab=execute
: = the tag as a command line (arrows/words move in it,
hjkl walk to the neighbouring TAGLINE, y/Enter act on
- the selection; the mode+path selects but never edits)
+ the selection; the mode+path selects but never edits;
+ k off the TOPMOST tagline = the top bar, j back down)
zt zz zb Ctrl-d/u Ctrl-f Ctrl-w hjkl Alt-n/c
SPC = the leader: a key path runs a builtin (SPC ? lists
them; SPC k Kill, SPC d Del, SPC f s Save, SPC f f Find,
diff --git a/test/snapshots/tagnav.golden b/test/snapshots/tagnav.golden
index 2b00eba5..77d5a846 100644
--- a/test/snapshots/tagnav.golden
+++ b/test/snapshots/tagnav.golden
@@ -184,7 +184,100 @@
|
|
|
-== snap up-edge grid=100x30 cursor=39,1
+== snap up-edge grid=100x30 cursor=0,0
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del
+| 1 alpha
+| 2 beta w.txt
+| 3 gamma
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap topbar-word grid=100x30 cursor=4,0
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del
+| 1 alpha
+| 2 beta w.txt
+| 3 gamma
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap topbar-graphemes grid=100x30 cursor=6,0
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del
+| 1 alpha
+| 2 beta w.txt
+| 3 gamma
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap back-down grid=100x30 cursor=39,1
|Kill Newcol Tutor Debug NextColor Dump
| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del NOR /tmp/pardes-snap/tagnav/cwd Del
| 1 alpha
@@ -370,3 +463,127 @@
|
|
|
+== snap topbar-dollar grid=100x30 cursor=37,0
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del TTY /tmp/pardes-snap/tagnav/cwd Del
+| 1 alpha $ ls
+| 2 beta w.txt
+| 3 gamma $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap topbar-exec grid=100x30 cursor=7,2
+|Kill Newcol Tutor Debug NextColor Dump Restore /tmp/pardes-dump.zon
+| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del TTY /tmp/pardes-snap/tagnav/cwd Del
+| 1 alpha $ ls
+| 2 beta w.txt
+| 3 gamma $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap topbar-restore-word grid=100x30 cursor=39,0
+|Kill Newcol Tutor Debug NextColor Dump Restore /tmp/pardes-dump.zon
+| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del TTY /tmp/pardes-snap/tagnav/cwd Del
+| 1 alpha $ ls
+| 2 beta w.txt
+| 3 gamma $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap topbar-restored grid=100x30 cursor=7,2
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del TTY /tmp/pardes-snap/tagnav/cwd Del
+| 1 alpha $ ls
+| 2 beta w.txt
+| 3 gamma $
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
diff --git a/test/snapshots/tagnav.snap b/test/snapshots/tagnav.snap
index 6ba86c08..fbb9c917 100644
--- a/test/snapshots/tagnav.snap
+++ b/test/snapshots/tagnav.snap
@@ -42,11 +42,28 @@ snap down-tag
key k
stable 400 5000
snap up-tag
-# k again: nothing above the file. focusDir leaves `active` alone, so the move
-# is a plain no-op and the tag KEEPS focus — you stay exactly where you were.
+# k again: nothing above the file — but "nothing" is not nowhere. Above the
+# topmost tagline is the TOPBAR, row 0, the global one. It is not a pane, so
+# focusDir can never find it; this fallback is the one place k walks off the
+# layout entirely. Evidence is the cursor=0,0 header: the bar has the keyboard
+# and parks on `Kill`.
key k
stable 400 5000
snap up-edge
+# the bar speaks the tag's own one-line vocabulary: w to the next word start
+key w
+stable 400 5000
+snap topbar-word
+# h/l are plain graphemes up here — row 0 has no window to its left or right to
+# walk to — and the arrows still mean what they always did
+key l right
+stable 400 5000
+snap topbar-graphemes
+# j is the mirror of the k that got you here: back DOWN onto the topmost pane's
+# tagline, still in normal mode, so the hop is a round trip
+key j
+stable 400 5000
+snap back-down
# the arrows are still in-tag motion: two rights walk two columns of the tail
key right right
stable 400 5000
@@ -73,3 +90,34 @@ key esc
settle 700
stable 400 5000
snap tty-restored
+# Enter/Tab on the BAR runs the word under the cursor through the very dispatch
+# a middle click on it uses, and leaves the bar FIRST — `Kill` lives up here, so
+# a builtin that frees the session must never return into a focused topbar (the
+# same hazard the pane-tag chord has with `Del`). `Dump` is the visible one: the
+# bar GROWS a `Restore <path>` word, which is also the case where the line gets
+# longer than the column you were parked on.
+key c-w h
+stable 400 5000
+key :
+key k
+stable 400 5000
+key $
+stable 400 5000
+snap topbar-dollar
+key enter
+stable 700 15000
+snap topbar-exec
+# THE DELETION HAZARD, from the bar itself: `Restore` frees every pane and
+# swaps the core out from under the caller. Walk back onto the word the Dump
+# just grew — B twice off the tail, because the LONG word motions are the ones
+# that step over a path in one go — and run it. The bar drops focus BEFORE the
+# builtin, so nothing reaches for state the swap already freed.
+key :
+key k
+stable 400 5000
+key $ B B
+stable 400 5000
+snap topbar-restore-word
+key enter
+stable 900 15000
+snap topbar-restored