summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/helix-keys.md5
-rw-r--r--src/pardes.zig190
-rw-r--r--src/tutor.txt26
-rw-r--r--test/snapshots/images.snap9
-rw-r--r--test/snapshots/tagnav.golden372
-rw-r--r--test/snapshots/tagnav.snap75
-rw-r--r--test/snapshots/tagpath.golden310
-rw-r--r--test/snapshots/tagpath.snap69
8 files changed, 987 insertions, 69 deletions
diff --git a/docs/helix-keys.md b/docs/helix-keys.md
index a479fefc..6a77a28d 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 |
-| `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; `Ctrl-w` while editing a tag (tags are always insert) is a no-op | 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 |
+| `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`) |
| `Space` (normal, body) | the pardes LEADER: a key path from here runs a BUILTIN with no arguments — the same builtins the topbar and the tags hold. `SPC k` Kill, `SPC d` Del, `SPC f s` Save, `SPC f f` Find (fd over the pane's directory, results into `+Search`), `SPC h t` Tutor, `SPC c n`/`SPC c d` Newcol/Delcol, `SPC t d/c/n/r` Debug/Colors/NextColor/Crt, `SPC t p/l/a` Petscii/Palette/Ascii (an image pane's renderer toggles — the words its tag used to spell out, so the bar stays a bar), `SPC s d`/`SPC s r` Dump/Restore, `SPC w h/j/k/l` Left/Down/Up/Right (directional pane focus — the `Ctrl-w` prefix's four moves as builtins), `SPC w t` Toggleterm (focus hops between the most recently focused document — file, image or `+Search`/`+Help` buffer — and the most recently focused terminal; the same key hops back). `?` at ANY depth opens the Help builtin listing what the prefix can still reach (`SPC ?` = all, `SPC h ?` = the docs group), into a `+Help` output buffer. The pending path shows at the right edge of the active pane's tag; Esc — or any unmapped key — abandons it | helix spends Space on pickers/LSP (section C); pardes has neither, and acme's builtins are what a leader is for. Enum-derived: the paths, the Help lines and the execute dispatch all fold out of one `Builtin` enum at comptime. Normal mode on a BODY only — tags are always insert, tty keys belong to the program | pardes-specific |
@@ -230,7 +231,7 @@ text.
| `s S Alt-s`, `Alt-minus Alt-_`, `& _`, `, Alt-,`, `C Alt-C`, `( ) Alt-( Alt-)`, `K Alt-K`, `Alt-:` | regex select/split, merge, align/trim, rotate, keep/remove, copy-to-line, ensure-forward | multicursor machinery — pardes has exactly one selection |
| `Alt-J` | join + select the inserted space | marginal over `J`; multicursor-flavored |
| `\|` `Alt-\|` `!` `Alt-!` `$` | shell pipe/insert/append/keep | shell — pardes executes via Tab / middle-click chords instead |
-| `:` | command mode | side-effects/file-ops — pardes builtins live in the tag |
+| `:` | command mode | side-effects/file-ops — pardes builtins live in the tag, and `:` is bound to focusing it (section A) |
| `gf` | goto file under selection | covered by pardes Enter-look |
| `Ctrl-c` | toggle comments | language-dependent |
| `Ctrl-z` | suspend | pardes IS the terminal multiplexer |
diff --git a/src/pardes.zig b/src/pardes.zig
index 31757732..6df520ce 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -704,6 +704,12 @@ pub const Pane = struct {
/// the body mode a tag edit hijacked (tags are always insert); terminals
/// restore it on exit so clicking the tag never changes the pane's mode
tag_mode: Mode = .normal,
+ /// THE tag coordinate space: byte columns into the WHOLE rendered tag,
+ /// prefix ++ tail (tagText). One space, no conversions — the renderer, the
+ /// mouse and every motion speak it directly. The prefix is live chrome, so
+ /// it is selectable/yankable/executable but READ-ONLY: every edit op
+ /// measures from `edit0` (= tagPrefix().len, the first editable column) and
+ /// does nothing left of it.
tag_col: u16 = 0,
tag_anchor: u16 = 0,
ed_undo: std.ArrayList(EditSnap) = .empty,
@@ -838,6 +844,10 @@ const Drag = union(enum) {
border_v: struct { left_col: usize, cur_x: u16 },
border_h: struct { col: usize, top_idx: usize, cur_y: u16 },
move: struct { id: usize, cur_x: u16, cur_y: u16 },
+ /// a left sweep along a pane's TAG row: it drives the tag's own cursor and
+ /// selection (rendered-tag columns) rather than the body's block selection,
+ /// which is what makes a one-line tag select like a line of text
+ tag: struct { id: usize },
/// chorded: a 1-2/1-3 cut/paste chord fired during this left drag —
/// the drag's own release is then inert
select: struct { id: usize, button: Mouse.Button, chorded: bool = false },
@@ -1209,10 +1219,20 @@ pub const Pardes = struct {
return pane_tail;
}
- /// focus the tag for editing, seeding the tail on first touch and placing
- /// the cursor under the click (a column within the text area)
- fn enterTagEdit(p: *Pardes, pane: *Pane, click_col: i32) void {
- const pfx = p.tagPrefix(pane) catch return;
+ /// the tag exactly as it is rendered: prefix ++ tail. THE text tag_col and
+ /// tag_anchor index, so the renderer, the mouse, the motions and the chord
+ /// all read the same bytes at the same columns.
+ fn tagText(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) ![]u8 {
+ return std.fmt.allocPrint(arena, "{s}{s}", .{ try p.tagPrefix(pane), curTail(pane) });
+ }
+
+ /// focus the tag for editing, seeding the tail on first touch and parking
+ /// the cursor at `col` — a column of the RENDERED tag (see tag_col).
+ /// NEGATIVE means the first EDITABLE column, which is where `:` and a
+ /// tagline hop land: a place no click can name, so it needs no sentinel of
+ /// its own and the callers need no prefix length.
+ fn enterTagEdit(p: *Pardes, pane: *Pane, col: i32) void {
+ const edit0: i32 = @intCast((p.tagPrefix(pane) catch return).len);
if (!pane.tag_init) {
pane.tag_tail.appendSlice(p.gpa, curTail(pane)) catch return;
pane.tag_init = true;
@@ -1223,8 +1243,8 @@ pub const Pardes = struct {
// a one-line tag has no use for normal mode: always insert
pane.mode = .insert;
pane.pending = 0;
- const cc = std.math.clamp(click_col - @as(i32, @intCast(pfx.len)), 0, @as(i32, @intCast(pane.tag_tail.items.len)));
- pane.tag_col = @intCast(cc);
+ const end = edit0 + @as(i32, @intCast(pane.tag_tail.items.len));
+ pane.tag_col = @intCast(if (col < 0) edit0 else std.math.clamp(col, 0, end));
}
fn exitTagEdit(pane: *Pane) void {
@@ -1245,28 +1265,27 @@ pub const Pardes = struct {
return .{ .lo = @min(a, c), .hi = @max(a, c) };
}
- /// the text a tag Enter/Tab chord acts on: the char selection, else the
- /// file-ish word under the cursor
+ /// the text a tag Enter/Tab chord (and `y`) acts on: the char selection,
+ /// else the file-ish word under the cursor. Over the WHOLE rendered tag, so
+ /// the path is a word like any other — Enter on it looks it.
fn tagChordText(p: *Pardes, pane: *Pane) ?[]const u8 {
- const arena = p.scratch.allocator();
- const tail = pane.tag_tail.items;
+ const text = p.tagText(p.scratch.allocator(), pane) catch return null;
if (pane.tag_sel) {
const b = tagSelBounds(pane);
- const hi = @min(b.hi + 1, tail.len);
- if (hi > b.lo) return arena.dupe(u8, tail[b.lo..hi]) catch null;
- return null;
+ const hi = @min(b.hi + 1, text.len);
+ return if (hi > b.lo) text[b.lo..hi] else null;
}
- const col: usize = @min(@as(usize, pane.tag_col), tail.len);
+ const col: usize = @min(@as(usize, pane.tag_col), text.len);
var lo = col;
- while (lo > 0 and look.isFileChar(tail[lo - 1])) lo -= 1;
+ while (lo > 0 and look.isFileChar(text[lo - 1])) lo -= 1;
var hi = col;
- while (hi < tail.len and look.isFileChar(tail[hi])) hi += 1;
- if (hi > lo) return arena.dupe(u8, tail[lo..hi]) catch null;
- return null;
+ while (hi < text.len and look.isFileChar(text[hi])) hi += 1;
+ return if (hi > lo) text[lo..hi] else null;
}
- /// tag-edit key dispatch: a modal one-line editor over the tail, sharing
- /// the pane's mode. Newlines are always dropped.
+ /// tag-edit key dispatch: a modal one-line editor over the rendered tag,
+ /// sharing the pane's mode — the cursor moves over all of it, edits reach
+ /// only the tail. Newlines are always dropped.
fn tagInsertKey(p: *Pardes, pane: *Pane, key: Key) void {
if (is(key, Key.escape)) {
exitTagEdit(pane); // the tag is ALWAYS insert; Esc leaves it
@@ -1281,35 +1300,40 @@ pub const Pardes = struct {
}
return;
}
+ // the prefix is live chrome, not text you own: the cursor may sit in it
+ // (that is how the path selects), but every edit below is measured from
+ // the first EDITABLE column and simply does nothing to the left of it.
+ const edit0: u16 = @intCast((p.tagPrefix(pane) catch return).len);
+ const end: u16 = edit0 + @as(u16, @intCast(pane.tag_tail.items.len));
if (key.text.len > 0) {
+ if (pane.tag_col < edit0) return; // typing inside the path: inert
for (key.text) |ch| {
if (ch == '\n' or ch == '\r') continue; // one-line tag
- pane.tag_tail.insert(p.gpa, pane.tag_col, ch) catch return;
+ pane.tag_tail.insert(p.gpa, pane.tag_col - edit0, ch) catch return;
pane.tag_col += 1;
}
return;
}
switch (key.cp) {
- Key.backspace => if (pane.tag_col > 0) {
- _ = pane.tag_tail.orderedRemove(pane.tag_col - 1);
+ Key.backspace => if (pane.tag_col > edit0) {
+ _ = pane.tag_tail.orderedRemove(pane.tag_col - edit0 - 1);
pane.tag_col -= 1;
},
Key.left => if (pane.tag_col > 0) {
pane.tag_col -= 1;
},
- Key.right => if (pane.tag_col < pane.tag_tail.items.len) {
+ Key.right => if (pane.tag_col < end) {
pane.tag_col += 1;
},
Key.home => pane.tag_col = 0,
- Key.end => pane.tag_col = @intCast(pane.tag_tail.items.len),
+ Key.end => pane.tag_col = end,
else => {},
}
}
- /// write a helix range back onto the tag cursor + selection: the tail's
- /// one-line mirror of setPaneRange.
- fn setTagRange(pane: *Pane, r: modal.HxRange) void {
- const text = pane.tag_tail.items;
+ /// write a helix range back onto the tag cursor + selection: the rendered
+ /// tag's one-line mirror of setPaneRange.
+ fn setTagRange(pane: *Pane, text: []const u8, r: modal.HxRange) void {
const lo = @min(r.anchor, r.head);
const hi = @max(r.anchor, r.head);
pane.tag_col = @intCast(modal.hxCursor(text, r));
@@ -1318,10 +1342,12 @@ pub const Pardes = struct {
}
/// normal mode ON the tag — where `:` lands. The body's own helix motions
- /// with the tail as a one-line document, plus insert entry and the acme
- /// chords; editing keys stay in insert (`i` then type, like the mouse path).
+ /// with the WHOLE rendered tag as a one-line document (so the path selects
+ /// like any other text, and Enter on it looks it), plus insert entry and
+ /// the acme chords; editing keys stay in insert (`i` then type, like the
+ /// mouse path) and never reach left of `edit0`.
fn tagNormalKey(p: *Pardes, pane: *Pane, key: Key) void {
- const text = pane.tag_tail.items;
+ const text = p.tagText(p.scratch.allocator(), pane) catch return;
const cur: usize = @min(@as(usize, pane.tag_col), text.len);
// Esc abandons the command line: back to the body, tail kept as text
if (is(key, Key.escape)) return exitTagEdit(pane);
@@ -1337,21 +1363,57 @@ pub const Pardes = struct {
p.actOnSelection(btn, id, txt, null);
return;
}
- // insert entry (one line, so I/A are the tail's ends)
+ // y — yank what the chord would run: the selection, else the word under
+ // the cursor. The path is selectable, so this is how you copy it out.
+ if (is(key, 'y')) {
+ if (p.tagChordText(pane)) |txt| p.setYank(txt);
+ return;
+ }
+ // insert entry (one line, so I/A are the tail's ends). The prefix is
+ // read-only, so entering insert inside it parks at the first editable
+ // column instead — you can never be typing into the path.
if (is(key, 'i') or is(key, 'a') or is(key, 'I') or is(key, 'A')) {
- pane.tag_col = @intCast(switch (key.cp) {
+ const edit0: usize = (p.tagPrefix(pane) catch return).len;
+ pane.tag_col = @intCast(@max(edit0, switch (key.cp) {
'a' => modal.nextGrapheme(text, cur),
'I' => 0,
'A' => text.len,
else => cur,
- });
+ }));
pane.tag_sel = false;
pane.mode = .insert;
return;
}
- const target: ?usize = if (is(key, 'h') or is(key, Key.left))
+ // h/j/k/l — a tagline is a place in the LAYOUT, so the four letters walk
+ // it: focus the neighbour and land on ITS tagline, still in normal mode,
+ // so tag-to-tag navigation never drops through a body. Runs the SAME
+ // 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.
+ const dir: ?Builtin = if (is(key, 'h'))
+ .Left
+ else if (is(key, 'j'))
+ .Down
+ else if (is(key, 'k'))
+ .Up
+ else if (is(key, 'l'))
+ .Right
+ else
+ null;
+ if (dir) |d| {
+ const from = p.active;
+ p.runBuiltin(d, from, "");
+ if (p.active == from) 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;
+ }
+ const target: ?usize = if (is(key, Key.left))
modal.prevGrapheme(text, cur)
- else if (is(key, 'l') or is(key, Key.right))
+ else if (is(key, Key.right))
modal.nextGrapheme(text, cur)
else if (is(key, '0') or is(key, Key.home))
0
@@ -1361,7 +1423,7 @@ pub const Pardes = struct {
modal.firstNonWs(text)
else
null;
- if (target) |t| return setTagRange(pane, .{ .anchor = t, .head = t });
+ if (target) |t| return setTagRange(pane, text, .{ .anchor = t, .head = t });
// word motions select their traversed span, exactly like the body's
const wt: ?modal.WordTarget = if (is(key, 'w'))
.next_word_start
@@ -1379,18 +1441,14 @@ pub const Pardes = struct {
null;
if (wt) |t| {
const range: modal.HxRange = .{ .anchor = cur, .head = modal.nextGrapheme(text, cur) };
- return setTagRange(pane, modal.hxWordMove(text, range, 1, t));
+ return setTagRange(pane, text, modal.hxWordMove(text, range, 1, t));
}
}
- /// d/c: delete the selection (or the char under the cursor), yanking it;
- /// c then enters insert
/// tag + '\n' + body: the full selectable pane text (row 0 = the tag)
fn paneText(p: *Pardes, pane: *Pane) ![]u8 {
const arena = p.scratch.allocator();
- const pfx = try p.tagPrefix(pane);
- const body = try p.bodyText(arena, pane);
- return std.fmt.allocPrint(arena, "{s}{s}\n{s}", .{ pfx, curTail(pane), body });
+ return std.fmt.allocPrint(arena, "{s}\n{s}", .{ try p.tagText(arena, pane), try p.bodyText(arena, pane) });
}
/// block-selected text, newline-joined per row; reads the rendered screen
@@ -2395,7 +2453,7 @@ pub const Pardes = struct {
// hands the body back (`:w<Tab>` = Save). Always the start, not the
// remembered tag column, so `:w` is the same keystrokes every time.
if (is(key, ':')) {
- p.enterTagEdit(pane, 0);
+ p.enterTagEdit(pane, -1);
if (pane.tag_edit) pane.mode = .normal;
return;
}
@@ -2435,7 +2493,9 @@ pub const Pardes = struct {
pane.tag_sel = false;
pane.mode = .insert;
pane.pending = 0;
- pane.tag_col = @intCast(pane.tag_tail.items.len);
+ // tag_col is a RENDERED-tag column, so the end of the tail is past the
+ // prefix (search_at stays a tail offset — it slices tag_tail)
+ pane.tag_col = @intCast((p.tagPrefix(pane) catch return).len + pane.tag_tail.items.len);
}
/// Enter on an armed input: the pattern is everything past the marker's
@@ -3970,9 +4030,14 @@ pub const Pardes = struct {
p.active = id;
pane.scrollBy(-(@as(i32, mrow) - @as(i32, r.y + BOX_H)));
} else if (mrow < r.y + BOX_H) {
- // left click on the tag row: edit the tail in place
+ // left press on the tag row: focus the tag AT that
+ // column (a rendered-tag column — the mouse and the tag
+ // agree, no clamp into the tail) and anchor a sweep, so
+ // dragging selects any span of it, path included
p.active = id;
p.enterTagEdit(pane, @as(i32, mcol) - @as(i32, r.x + GUTTER));
+ pane.tag_anchor = pane.tag_col;
+ p.drag = .{ .tag = .{ .id = id } };
} else {
p.active = id;
exitTagEdit(pane); // clicking the body leaves tag editing
@@ -4070,6 +4135,16 @@ pub const Pardes = struct {
pane.sel[b].r1 = @as(i32, mrow) - @as(i32, r.y);
}
},
+ .tag => |d| {
+ // the tag cursor follows the mouse across the rendered tag; the
+ // row is ignored (a tag is one line) and the anchor stays where
+ // the press put it, so this is the mouse's `v`
+ const pane = p.panes[d.id] orelse return;
+ const end: i32 = @intCast((p.tagPrefix(pane) catch return).len + pane.tag_tail.items.len);
+ const c = @as(i32, mcol) - @as(i32, p.rects[d.id].x + GUTTER);
+ pane.tag_col = @intCast(std.math.clamp(c, 0, end));
+ pane.tag_sel = pane.tag_col != pane.tag_anchor;
+ },
.none => {},
}
}
@@ -4159,6 +4234,7 @@ pub const Pardes = struct {
if (txt) |tx| p.actOnSelection(s.button, s.id, tx, arg);
}
},
+ .tag => {}, // dragUpdate already left the tag cursor + selection set
.none => {},
}
}
@@ -4987,8 +5063,7 @@ pub const Pardes = struct {
for (p.panes, 0..) |slot, id| {
const pane = slot orelse continue;
slot_to_pane[id] = panes.items.len;
- const pfx = try p.tagPrefix(pane);
- const tag = try std.fmt.allocPrint(arena, "{s}{s}", .{ pfx, curTail(pane) });
+ const tag = try p.tagText(arena, pane);
const body = try p.bodyText(arena, pane);
const scroll: usize = @intCast(@max(0, pane.scroll()));
const dp: dump.Pane = if (pane.file) |f| .{
@@ -5402,7 +5477,7 @@ pub const Pardes = struct {
s.set(d.cur_x, placement.row, "▌", .{ .fg = .{ .rgb = th.lineno } });
}
},
- .none, .select => {
+ .none, .select, .tag => {
// hover hints on a pane's own trailing edge
var c: usize = 0;
while (c + 1 < p.ncol) : (c += 1) {
@@ -5494,8 +5569,7 @@ pub const Pardes = struct {
// tag: live prefix (mode + cwd/path) + editable tail, on a dim bar
const tag_style: CellStyle = .{ .fg = .{ .rgb = th.tag_fg }, .bg = .{ .rgb = th.tag_bg } };
s.fill(tx, r.y, tw, BOX_H, .{ .bg = .{ .rgb = th.tag_bg } });
- const pfx = try p.tagPrefix(pane);
- const tag = try std.fmt.allocPrint(arena, "{s}{s}", .{ pfx, curTail(pane) });
+ const tag = try p.tagText(arena, pane);
_ = s.print(tx, r.y, tw, tag, tag_style);
// pending SPC leader path, on the ACTIVE pane's tag at its RIGHT edge:
// the same rule as the `/` input — state you can see, nothing that
@@ -5514,11 +5588,12 @@ pub const Pardes = struct {
const w: u16 = @intCast(iw);
if (w < tw) _ = s.print(tx + tw - w, r.y, w, ibuf[0..iw], tag_style);
}
- // tag-tail char selection highlight (helix v/x), inclusive [lo, hi]
+ // tag char selection highlight (helix v/x, or a tagline sweep),
+ // inclusive [lo, hi] — rendered-tag columns, so no conversion
if (pane.tag_edit and pane.tag_sel) {
const b = tagSelBounds(pane);
- var col: usize = pfx.len + b.lo;
- const end: usize = pfx.len + b.hi;
+ var col: usize = b.lo;
+ const end: usize = b.hi;
while (col <= end and col < tw) : (col += 1) {
const cell = s.at(tx + @as(u16, @intCast(col)), r.y);
cell.default = false;
@@ -5526,11 +5601,10 @@ pub const Pardes = struct {
cell.style.fg = .{ .rgb = msel_fg };
}
}
- // cursor while editing the tail: prefix length + column within it
+ // cursor while editing the tag: a rendered-tag column, verbatim
if (active and pane.tag_edit) {
- const cx: usize = pfx.len + pane.tag_col;
// bar while typing, block for `:` normal mode (same rule as a body)
- if (cx < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = r.y, .bar = pane.mode == .insert };
+ if (pane.tag_col < tw) s.cursor = .{ .x = tx + pane.tag_col, .y = r.y, .bar = pane.mode == .insert };
}
// image pane: the picture fills the body — petscii glyph art into the
diff --git a/src/tutor.txt b/src/tutor.txt
index 39a869e2..c263015b 100644
--- a/src/tutor.txt
+++ b/src/tutor.txt
@@ -113,11 +113,23 @@
writes the current file to disk, Del closes the window. Clicking a tag
edits it in insert mode: type straight in, Enter looks / Tab executes
the word at the cursor, Esc hands focus back to the body. `:` from the
- body focuses that same tag in NORMAL mode, parked at its start — vim's
- command line with acme's words in it: `:w<Tab>` is w onto "Save", Tab
- to execute it, and you land back in the body where you left off (the
- chord keys are the same here as anywhere else; `i` if you would rather
- type a command than walk to one). Terminal and image panes show "Del".
+ body focuses that same tag in NORMAL mode, parked at the first EDITABLE
+ column — vim's command line with acme's words in it: `:w<Tab>` is w
+ onto "Save", Tab to execute it, and you land back in the body where you
+ left off (the chord keys are the same here as anywhere else; `i` if you
+ would rather type a command than walk to one). Terminal and image panes
+ show "Del".
+ In that normal mode h/j/k/l do NOT move inside the tag: a tagline is a
+ place in the LAYOUT, so they focus the neighbouring window and land on
+ 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.
+ 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
+ with the cursor inside it does nothing; edits only ever reach the words
+ you own, to the right of the path.
"Delcol" still exists as a command: type it in
a tag or body and execute it to close the whole column. A SEPARATE bar
across the top of the screen holds the window-agnostic builtins:
@@ -523,7 +535,9 @@
the input is empty, mid-text otherwise)
KEYS (helix): h j k l w b e 0 $ ^ gg G v x d c y p i a I A o O
u undo U redo Enter=look Tab=execute
- : = the tag as a command line (motions, Enter, back)
+ : = 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)
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/images.snap b/test/snapshots/images.snap
index 9c510bed..e31d57ec 100644
--- a/test/snapshots/images.snap
+++ b/test/snapshots/images.snap
@@ -36,9 +36,12 @@ key a
stable 700 15000
snap no-ascii
# the tag takes an edit like any other pane's now — nothing live left in it to
-# protect, so the image early-return in enterTagEdit is gone
-press left 20 2
-release left 20 2
+# protect, so the image early-return in enterTagEdit is gone. Col 42 is the
+# FIRST EDITABLE column (right after "img <path>"): a tag column is a column of
+# the whole rendered tag, and the live prefix is read-only, so a click at col 20
+# would land in the path and the typing below would be inert.
+press left 42 2
+release left 42 2
stable 400 5000
text Zork
stable 400 5000
diff --git a/test/snapshots/tagnav.golden b/test/snapshots/tagnav.golden
new file mode 100644
index 00000000..2b00eba5
--- /dev/null
+++ b/test/snapshots/tagnav.golden
@@ -0,0 +1,372 @@
+== snap start grid=100x30 cursor=7,2
+|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 colon 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
+| 2 beta w.txt
+| 3 gamma
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap right-tag grid=100x30 cursor=83,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
+| 2 beta w.txt
+| 3 gamma
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap left-tag 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
+| 2 beta w.txt
+| 3 gamma
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap down-tag grid=100x30 cursor=33,16
+|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 up-tag 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
+| 2 beta w.txt
+| 3 gamma
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap up-edge 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
+| 2 beta w.txt
+| 3 gamma
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap arrows-still-motion grid=100x30 cursor=41,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
+| 2 beta w.txt
+| 3 gamma
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap back-to-body grid=100x30 cursor=7,2
+|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 tty-on grid=100x30 cursor=54,4
+|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 tty-tag grid=100x30 cursor=83,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
+| 2 beta w.txt
+| 3 gamma
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+| NOR /tmp/pardes-snap/tagnav/cwd Del
+|
+| w.txt
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap tty-restored grid=100x30 cursor=54,4
+|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
new file mode 100644
index 00000000..6ba86c08
--- /dev/null
+++ b/test/snapshots/tagnav.snap
@@ -0,0 +1,75 @@
+# h/j/k/l ON A TAGLINE move focus BETWEEN taglines. A tag is a place in the
+# LAYOUT, not just a line of text, so the four letters walk the layout and land
+# on the neighbour's TAG (top row of the window), still in normal mode — you
+# walk the taglines and never drop through a body. They run the SAME
+# Left/Down/Up/Right builtins `SPC w h/j/k/l` and `Ctrl-w h/j/k/l` run: one
+# focusDir, three routes to it. The ARROWS keep the in-tag grapheme motion, so
+# no capability was traded away.
+#
+# Evidence is the `cursor=` header (the ACTIVE pane's cursor, which while a tag
+# is focused sits on THAT pane's tag row) plus the tag text itself: a focused
+# tag renders its owner's mode, so NOR means the tag is in normal mode and the
+# pane we left is back in its body mode.
+file w.txt alpha\nbeta\ngamma
+start 30 100 w.txt
+wait 8000 Kill Newcol
+wait 8000 w.txt
+stable 700 20000
+# Alt-n: a shell below the file, so the left column has two rows and every
+# direction has somewhere to go
+key a-n
+stable 700 15000
+key space w k
+stable 400 5000
+snap start
+# `:` -> the file's OWN tagline, normal mode, parked at the tail's start
+key :
+stable 400 5000
+snap colon
+# l -> right, onto the launch shell's TAGLINE (not its body)
+key l
+stable 400 5000
+snap right-tag
+# h -> back left onto the file's tagline
+key h
+stable 400 5000
+snap left-tag
+# j -> down onto the tagline of the shell under the file
+key j
+stable 400 5000
+snap down-tag
+# k -> back up onto the file's
+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.
+key k
+stable 400 5000
+snap up-edge
+# the arrows are still in-tag motion: two rights walk two columns of the tail
+key right right
+stable 400 5000
+snap arrows-still-motion
+key esc
+settle 700
+stable 400 5000
+snap back-to-body
+# A TERMINAL MUST NOT LOSE ITS TTY MODE. Put the launch shell in raw tty mode
+# (Ctrl-b), come back, then hop onto its tagline and off again: exitTagEdit
+# restores the body mode the tag hijacked, so its prefix reads TTY again.
+key c-w l
+stable 400 5000
+key c-b
+stable 700 15000
+snap tty-on
+key c-w h
+stable 400 5000
+key :
+key l
+stable 400 5000
+snap tty-tag
+key esc
+settle 700
+stable 400 5000
+snap tty-restored
diff --git a/test/snapshots/tagpath.golden b/test/snapshots/tagpath.golden
new file mode 100644
index 00000000..0b073a8c
--- /dev/null
+++ b/test/snapshots/tagpath.golden
@@ -0,0 +1,310 @@
+== snap start grid=100x30 cursor=7,2
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del
+| 1 line 1
+| 2 line 2 c.txt
+| 3 line 3
+| 4 line 4
+| 5
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap colon grid=100x30 cursor=40,1
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del
+| 1 line 1
+| 2 line 2 c.txt
+| 3 line 3
+| 4 line 4
+| 5
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap col0 grid=100x30 cursor=2,1
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del
+| 1 line 1
+| 2 line 2 c.txt
+| 3 line 3
+| 4 line 4
+| 5
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap sel-nor grid=100x30 cursor=5,1
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del
+| 1 line 1
+| 2 line 2 c.txt
+| 3 line 3
+| 4 line 4
+| 5
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap sel-path grid=100x30 cursor=39,1
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del
+| 1 line 1
+| 2 line 2 c.txt
+| 3 line 3
+| 4 line 4
+| 5
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== style sel-path
+|0: 0-37 #949494,#121212, 38-99 d,#121212,
+|1: 0-1 d,#626262, 2-4 #949494,#121212, 5-39 #d8c8a8,#4a3a1e, 40-48 #949494,#121212, 49-49 d,#121212, 50-51 d,#262626, 52-87 #949494,#121212, 88-99 d,#121212,
+|2: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|3: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|4: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|5: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|6: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|7: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|8: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|9: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|10: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|11: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|12: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|13: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|14: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|15: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|16: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|17: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|18: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|19: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|20: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|21: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|22: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|23: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|24: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|25: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|26: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|27: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|28: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|29: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+== snap yanked-path grid=100x30 cursor=42,2
+|Kill Newcol Tutor Debug NextColor Dump
+| NOR /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del
+| 1 l /tmp/pardes-snap/tagpath/cwd/c.txtine 1
+| 2 line 2 c.txt
+| 3 line 3
+| 4 line 4
+| 5
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== snap drag-cross grid=100x30 cursor=45,1
+|Kill Newcol Tutor Debug NextColor Dump
+| INS /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del
+| 1 l /tmp/pardes-snap/tagpath/cwd/c.txtine 1
+| 2 line 2 c.txt
+| 3 line 3
+| 4 line 4
+| 5
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+== style drag-cross
+|0: 0-37 #949494,#121212, 38-99 d,#121212,
+|1: 0-1 d,#626262, 2-10 #949494,#121212, 11-45 #d8c8a8,#4a3a1e, 46-48 #949494,#121212, 49-49 d,#121212, 50-51 d,#262626, 52-87 #949494,#121212, 88-99 d,#121212,
+|2: 0-1 d,#444444, 2-6 #626262,#080808, 7-47 #bdbdbd,#080808, 48-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|3: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|4: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|5: 0-1 d,#444444, 2-6 #626262,#080808, 7-12 #bdbdbd,#080808, 13-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|6: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|7: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|8: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|9: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|10: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|11: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|12: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|13: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|14: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|15: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|16: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|17: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|18: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|19: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|20: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|21: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|22: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|23: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|24: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|25: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|26: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|27: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|28: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+|29: 0-1 d,#444444, 2-6 #626262,#080808, 7-49 d,#080808, 50-51 d,#444444, 52-99 #bdbdbd,#080808,
+== snap path-inert grid=100x30 cursor=11,1
+|Kill Newcol Tutor Debug NextColor Dump
+| INS /tmp/pardes-snap/tagpath/cwd/c.txt Save Del NOR /tmp/pardes-snap/tagpath/cwd Del
+| 1 l /tmp/pardes-snap/tagpath/cwd/c.txtine 1
+| 2 line 2 c.txt
+| 3 line 3
+| 4 line 4
+| 5
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
+|
diff --git a/test/snapshots/tagpath.snap b/test/snapshots/tagpath.snap
new file mode 100644
index 00000000..61181dd5
--- /dev/null
+++ b/test/snapshots/tagpath.snap
@@ -0,0 +1,69 @@
+# THE PATH IN A TAG IS SELECTABLE, NOT EDITABLE.
+# tag_col/tag_anchor index the WHOLE rendered tag (prefix ++ tail) — one
+# coordinate space, no conversions: the renderer prints the cursor at tag_col
+# verbatim and a mouse column IS a tag column. So motions and sweeps reach the
+# mode indicator and the path, a selection over them yanks and executes like
+# any other text, and the ONLY thing fenced off is EDITING: every edit op
+# measures from the first editable column (tagPrefix().len) and does nothing to
+# the left of it.
+lines c.txt 4
+start 30 100 c.txt
+wait 8000 Kill Newcol
+wait 8000 c.txt
+stable 700 20000
+snap start
+# --- keyboard: select part of the PATH ---
+# `:` parks at the tail's start (the first EDITABLE column) — unchanged, that
+# is what makes `:w<Tab>` the same two keystrokes every time
+key :
+stable 400 5000
+snap colon
+# 0 goes to column 0 of the RENDERED tag: the "N" of NOR, a column the keyboard
+# could not name at all before
+key 0
+stable 400 5000
+snap col0
+# W: the long word it traverses, landing the cursor on the space after "NOR"
+key W
+stable 400 5000
+snap sel-nor
+# E: extend to the end of the next long word — the style capture shows ONE
+# highlighted run from that space through the last char of the path
+key E
+stable 400 5000
+snap sel-path
+snapstyle sel-path
+# y yanks the selection. Esc hands the body back; p pastes the register into
+# the file, which is the only way a snapshot can see what was yanked.
+key y
+key esc
+settle 700
+stable 400 5000
+key p
+stable 400 5000
+snap yanked-path
+# --- mouse: a sweep across the prefix/tail boundary ---
+# a left press on a tagline anchors a TAG sweep (not the body's block
+# selection), so the drag paints one run of the tag row. Press at col 12
+# (inside the path), release at col 46 (just past "Save" in the tail): the
+# highlight crosses the boundary — screen col 40, where the tail starts — in
+# one piece, which is the whole point of there being one coordinate space.
+press left 12 2
+drag left 30 2
+drag left 46 2
+release left 46 2
+stable 400 5000
+snap drag-cross
+snapstyle drag-cross
+# --- the path is inert to editing ---
+# a plain click INSIDE the path leaves the cursor there (no clamp into the
+# tail), in insert mode like any tag click — and then typing and backspace do
+# NOTHING: the tag row below is byte-identical to the one in `yanked-path`.
+press left 12 2
+release left 12 2
+stable 400 5000
+text ZZNOPE
+key bs
+key bs
+stable 400 5000
+snap path-inert