diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/pardes.zig | 114 | ||||
| -rw-r--r-- | src/tutor.txt | 19 |
2 files changed, 121 insertions, 12 deletions
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, |
