summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/mode.zig31
-rw-r--r--test/panes.zig384
-rw-r--r--test/pdf.zig23
-rw-r--r--test/snapshots/cmdword.golden4
-rw-r--r--test/snapshots/cmdword.snap24
-rw-r--r--test/snapshots/collapse.golden4
-rw-r--r--test/snapshots/collapse.snap7
-rw-r--r--test/snapshots/tag.snap20
-rw-r--r--test/snapshots/tagalign.snap9
-rw-r--r--test/snapshots/tagcmd.snap20
-rw-r--r--test/snapshots/tagnav.golden43
-rw-r--r--test/snapshots/tagnav.snap136
-rw-r--r--test/snapshots/tagpath.golden23
-rw-r--r--test/snapshots/tagpath.snap28
-rw-r--r--test/snapshots/ttyclick.golden4
-rw-r--r--test/snapshots/ttyclick.snap14
16 files changed, 360 insertions, 414 deletions
diff --git a/test/mode.zig b/test/mode.zig
index 19d59699..c429ea72 100644
--- a/test/mode.zig
+++ b/test/mode.zig
@@ -6,7 +6,7 @@ const Pardes = pardes.Pardes;
fn mode(p: *Pardes, id: usize, expected: panes.Text.Mode) !void {
try std.testing.expect(p.executeBuiltinLine(id, "Mode"));
try std.testing.expectEqual(expected, p.panes[id].?.body.mode);
- try std.testing.expect(!p.panes[id].?.tag_edit);
+ try std.testing.expect(p.panes[id].?.focus == .body);
}
fn clickModeTag(p: *Pardes) !void {
@@ -17,13 +17,12 @@ fn clickModeTag(p: *Pardes) !void {
const row = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
for ([_]pardes.Mouse.Kind{ .press, .release }) |kind|
p.update(.{ .mouse = .{ .button = .left, .kind = kind, .col = rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(col)), .row = row } });
- try std.testing.expect(pane.tag_edit);
+ try std.testing.expect(pane.focus == .tag);
}
-fn customTag(pane: *pardes.Pane, text: []const u8) void {
- @memcpy(pane.tag_tail[0..text.len], text);
- pane.tag_tail_len = text.len;
- pane.tag_init = true;
+fn customTag(pane: *pardes.Pane, text: []const u8) !void {
+ if (pane.tag.own) |own| pane.gpa.free(own);
+ pane.tag.own = try pane.gpa.dupe(u8, text);
}
test "Mode cycles terminal modes and keeps legacy toggle and custom tags" {
@@ -37,14 +36,18 @@ test "Mode cycles terminal modes and keeps legacy toggle and custom tags" {
try std.testing.expect(std.mem.indexOf(u8, try p.tagText(p.scratch.allocator(), pane), " Mode") != null);
try mode(p, 0, .normal);
try mode(p, 0, .insert);
+ // A clicked tag is typed into; its execute key is a normal-mode key, as
+ // a body's is, and the body keeps its own mode meanwhile.
try clickModeTag(p);
- try std.testing.expectEqual(panes.Text.Mode.insert, pane.tag_mode);
+ try std.testing.expectEqual(panes.Text.Mode.insert, pane.tag.mode);
+ try std.testing.expectEqual(panes.Text.Mode.insert, pane.body.mode);
+ p.update(.{ .key = .{ .cp = pardes.Key.escape } });
p.update(.{ .key = .{ .cp = pardes.Key.tab } });
try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode);
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.focus == .body);
while (p.nextEffect()) |effect| try std.testing.expect(effect != .write);
- customTag(pane, " Togglettymode Keep");
+ try customTag(pane, " Togglettymode Keep");
try mode(p, 0, .normal);
try mode(p, 0, .insert);
try std.testing.expect(p.executeBuiltinLine(0, "Togglettymode"));
@@ -53,7 +56,7 @@ test "Mode cycles terminal modes and keeps legacy toggle and custom tags" {
try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode);
p.update(.{ .key = .{ .cp = 'b', .ctrl = true } });
try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode);
- try std.testing.expectEqualStrings(" Togglettymode Keep", pane.tag_tail[0..pane.tag_tail_len]);
+ try std.testing.expectEqualStrings(" Togglettymode Keep", pane.tag.own.?);
}
test "Mode from actual tag Exec preserves parked file and output insert transactions" {
@@ -65,17 +68,17 @@ test "Mode from actual tag Exec preserves parked file and output insert transact
const pane = try p.setTestFile("ab\n");
if (output) pane.file.?.output = .{ .from = .search };
try std.testing.expect(std.mem.indexOf(u8, try p.tagText(p.scratch.allocator(), pane), " Mode") == null);
- customTag(pane, " Mode");
+ try customTag(pane, " Mode");
try mode(p, 0, .insert);
p.update(.{ .key = .{ .cp = '界', .text = "界" } });
p.update(.{ .key = .{ .cp = 'x', .text = "x" } });
try std.testing.expectEqualStrings("界xab\n", pane.file.?.content);
try clickModeTag(p);
- try std.testing.expectEqual(panes.Text.Mode.insert, pane.tag_mode);
- if (normal_tag) pane.body.mode = .normal;
+ try std.testing.expectEqual(panes.Text.Mode.insert, pane.body.mode);
+ if (normal_tag) pane.tag.mode = .normal else p.update(.{ .key = .{ .cp = pardes.Key.escape } });
p.update(.{ .key = .{ .cp = pardes.Key.tab } });
try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode);
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.focus == .body);
try std.testing.expect(pane.body.append_at == null);
p.update(.{ .key = .{ .cp = 'u' } });
try std.testing.expectEqualStrings("ab\n", pane.file.?.content);
diff --git a/test/panes.zig b/test/panes.zig
index f1adafee..a391dba2 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -12,6 +12,17 @@ const image = pardes.image;
const syntax = pardes.syntax;
const filesystem = pardes.filesystem;
+/// The tag's caret as a byte offset into the tag as it is shown, prefix
+/// included.
+fn tagCaret(p: *Pardes, pane: *Pane) !usize {
+ return (try p.tagPrefix(pane)).len + @as(usize, @intCast(pane.tag.cur_col));
+}
+
+fn setTag(pane: *Pane, text: []const u8) !void {
+ if (pane.tag.own) |own| pane.gpa.free(own);
+ pane.tag.own = try pane.gpa.dupe(u8, text);
+}
+
const TagLayerTests = struct {
const Kind = @FieldType(pardes.TagLayer, "kind");
fn findLayer(p: *Pardes, kind: Kind, id: usize) !*pardes.TagLayer {
@@ -36,10 +47,7 @@ const TagLayerTests = struct {
p.row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 5, .tagline_h = 10 };
p.gpa.free(pane.file.?.path);
pane.file.?.path = try p.gpa.dupe(u8, "/x");
- const tail = " " ++ "a" ** 38 ++ "\t界 END";
- @memcpy(pane.tag_tail[0..tail.len], tail);
- pane.tag_tail_len = tail.len;
- pane.tag_init = true;
+ try setTag(pane, " " ++ "a" ** 38 ++ "\t界 END");
var frame = std.heap.ArenaAllocator.init(p.gpa);
defer frame.deinit();
_ = try p.render(frame.allocator());
@@ -55,11 +63,11 @@ const TagLayerTests = struct {
var stale = mouse(layer, x, .press);
stale.tag_hit.?.serial +%= 1;
p.update(.{ .mouse = stale });
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.focus == .body);
p.update(.{ .mouse = mouse(layer, x, .press) });
p.update(.{ .mouse = mouse(layer, x, .release) });
- try std.testing.expect(pane.tag_edit);
- try std.testing.expectEqual(@as(u16, @intCast(at)), pane.tag_col);
+ try std.testing.expect(pane.focus == .tag);
+ try std.testing.expectEqual(at, try tagCaret(p, pane));
p.update(.{ .key = .{ .cp = 'X', .text = "X" } });
_ = try p.render(frame.allocator());
const edited = try p.tagText(frame.allocator(), pane);
@@ -91,7 +99,7 @@ const TagLayerTests = struct {
event.row = @intFromFloat(painted_y);
p.update(.{ .mouse = event });
}
- try std.testing.expectEqual(@as(u16, @intCast(at)), pane.tag_col);
+ try std.testing.expectEqual(at, try tagCaret(p, pane));
}
}
@@ -119,19 +127,17 @@ const TagLayerTests = struct {
try std.testing.expectEqualStrings(" ", p.surface.at(layer.viewport.x - 1, layer.viewport.y).grapheme());
p.update(.{ .mouse = mouse(layer, edge - 0.1, .press) });
try std.testing.expect(p.drag != .move);
- try std.testing.expect(!p.panes[second].?.tag_edit);
+ try std.testing.expect(p.panes[second].?.focus == .body);
p.update(.{ .mouse = mouse(layer, edge - 0.1, .release) });
p.update(.{ .mouse = mouse(layer, edge + 0.1, .press) });
p.update(.{ .mouse = mouse(layer, edge + 0.1, .release) });
try std.testing.expectEqual(second, p.active);
- try std.testing.expect(p.panes[second].?.tag_edit);
- try std.testing.expectEqual(@as(u16, 0), p.panes[second].?.tag_col);
+ // The first cell of the tag is its path: a click there drafts a name.
+ try std.testing.expect(p.panes[second].?.prompt == .name);
+ try std.testing.expectEqual(@as(i32, 0), p.panes[second].?.input.cur_col);
p.update(.{ .key = .{ .cp = Key.escape } });
const left = p.panes[0].?;
- const tail = " " ++ "a" ** 110;
- @memcpy(left.tag_tail[0..tail.len], tail);
- left.tag_tail_len = tail.len;
- left.tag_init = true;
+ try setTag(left, " " ++ "a" ** 110);
_ = try p.render(frame.allocator());
const left_layer = try findLayer(p, .pane, 0);
const left_text = try p.tagText(frame.allocator(), left);
@@ -142,8 +148,8 @@ const TagLayerTests = struct {
p.update(.{ .mouse = mouse(layer, beyond_x, .release) });
try std.testing.expectEqual(@as(usize, 0), p.active);
const end_col = left_layer.capacity(p.row_metrics) - 1;
- try std.testing.expectEqual(@as(u16, @intCast(panes.File.rawAtDisplay(left_text, end_col))), left.tag_col);
- try std.testing.expect(left.tag_sel);
+ try std.testing.expectEqual(panes.File.rawAtDisplay(left_text, end_col), try tagCaret(p, left));
+ try std.testing.expect(left.sel[0].state == .done and left.sel[0].r0 == 0);
p.update(.{ .key = .{ .cp = Key.escape } });
const long = "a" ** 60 ++ "界END";
try std.testing.expect(p.global_tag.set(long));
@@ -167,12 +173,12 @@ const TagNavigationTests = struct {
p.update(.{ .key = .{ .cp = cp } });
}
- fn enterNormal(p: *Pardes) *Pane {
- key(p, ':');
- return p.panes[p.active].?;
+ fn window(p: *Pardes, letter: u21) void {
+ p.update(.{ .key = .{ .cp = 'w', .ctrl = true } });
+ key(p, letter);
}
- test "tag navigation retains each pane cursor and mouse placement overrides memory" {
+ test "a tag remembers its cursor, starts on Save, and a click moves it" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 });
defer p.deinit();
const first = try p.setTestFile("body\n");
@@ -183,65 +189,97 @@ const TagNavigationTests = struct {
try std.testing.expect(layout.splitColumn(p, second_id, second_id, false));
p.update(.tick);
p.active = 0;
+ // Nothing to remember yet: `:` lands on Save, in normal mode.
key(p, ':');
+ try std.testing.expect(first.focus == .tag and first.tag.mode == .normal);
+ const tail = Pardes.curTail(first);
+ try std.testing.expectEqualStrings("Save", tail[@intCast(first.tag.cur_col)..][0..4]);
key(p, Key.home);
for (0..3) |_| key(p, Key.right);
- const first_col = first.tag_col;
- key(p, Key.escape);
+ try std.testing.expectEqual(@as(i32, 3), first.tag.cur_col);
+ // `:` in the tag's normal mode is the way back to the body.
+ key(p, ':');
+ try std.testing.expect(first.focus == .body);
p.active = second_id;
key(p, ':');
key(p, Key.home);
for (0..5) |_| key(p, Key.right);
- const second_col = second.tag_col;
- key(p, Key.escape);
+ key(p, ':');
p.active = 0;
key(p, ':');
- try std.testing.expectEqual(first_col, first.tag_col);
- key(p, Key.escape);
+ try std.testing.expectEqual(@as(i32, 3), first.tag.cur_col);
+ key(p, ':');
p.active = second_id;
key(p, ':');
- try std.testing.expectEqual(second_col, second.tag_col);
- key(p, Key.escape);
+ try std.testing.expectEqual(@as(i32, 5), second.tag.cur_col);
+ key(p, ':');
var frame = std.heap.ArenaAllocator.init(p.gpa);
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;
- ClickCursorTests.click(p, .left, rect.x + pardes.TAG_TEXT_INSET + 6, y);
+ const prefix: u16 = @intCast(panes.File.displayWidth(try p.tagPrefix(first)));
+ ClickCursorTests.click(p, .left, rect.x + pardes.TAG_TEXT_INSET + prefix + 6, y);
try std.testing.expectEqual(@as(usize, 0), p.active);
- try std.testing.expectEqual(@as(u16, 6), first.tag_col);
+ try std.testing.expect(first.focus == .tag and first.tag.mode == .insert);
+ try std.testing.expectEqual(@as(i32, 6), first.tag.cur_col);
key(p, Key.escape);
+ key(p, ':');
p.startPrompt(first, .{ .search = config.search_marker });
key(p, Key.escape);
key(p, ':');
- try std.testing.expectEqual(@as(u16, 6), first.tag_col);
- key(p, Key.escape);
+ try std.testing.expectEqual(@as(i32, 6), first.tag.cur_col);
+ key(p, ':');
try std.testing.expectEqualStrings("body\n", first.file.?.content);
}
- test "tag navigation clamps remembered columns to renamed graphemes and shorter tags" {
+ test "executing a word from the tag runs it and gives the keyboard back to the body" {
+ for ([_]u21{ Key.tab, Key.enter }) |chord| {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 });
+ defer p.deinit();
+ const pane = try p.setTestFile("body\n");
+ p.update(.{ .key = .{ .cp = 'i' } });
+ p.update(.{ .key = .{ .cp = 'x', .text = "x" } });
+ p.update(.{ .key = .{ .cp = Key.escape } });
+ while (p.nextEffect()) |_| {}
+ // `:` lands on Save; the execute key runs it, the look key looks
+ // it up. Either way the tag lets go of the keyboard first.
+ key(p, ':');
+ try std.testing.expect(pane.focus == .tag);
+ key(p, chord);
+ try std.testing.expect(pane.focus == .body);
+ try std.testing.expectEqual(.normal, pane.body.mode);
+ var saved = false;
+ while (p.nextEffect()) |effect| {
+ if (effect == .save_file) saved = true;
+ }
+ try std.testing.expectEqual(chord == Key.tab, saved);
+ // ...and the tag still knows where it was.
+ key(p, ':');
+ try std.testing.expectEqualStrings("Save", Pardes.curTail(pane)[@intCast(pane.tag.cur_col)..][0..4]);
+ }
+ }
+
+ test "a renamed path never moves the tag's cursor and a shorter tag pulls it back" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 14 });
defer p.deinit();
const pane = try p.setTestFile("body\n");
key(p, ':');
key(p, Key.home);
for (0..3) |_| key(p, Key.right);
- key(p, Key.escape);
+ key(p, ':');
p.gpa.free(pane.file.?.path);
pane.file.?.path = try p.gpa.dupe(u8, "/界.zig");
key(p, ':');
- try std.testing.expectEqual(@as(u16, 1), pane.tag_col);
- key(p, Key.end);
- key(p, Key.escape);
- p.gpa.free(pane.file.?.path);
- pane.file.?.path = try p.gpa.dupe(u8, "/é");
- pane.tag_tail_len = 0;
+ try std.testing.expectEqual(@as(i32, 3), pane.tag.cur_col);
+ key(p, ':');
+ try setTag(pane, "é");
key(p, ':');
- try std.testing.expectEqual(@as(u16, 3), pane.tag_col);
+ try std.testing.expectEqual(@as(i32, 2), pane.tag.cur_col);
try std.testing.expectEqualStrings("body\n", pane.file.?.content);
}
- test "tag navigation J and K restore cursors in stacked panes" {
+ test "window keys move between panes from a tag and each tag keeps its cursor" {
for ([_]bool{ false, true }) |bottom| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
defer p.deinit();
@@ -253,88 +291,67 @@ const TagNavigationTests = struct {
const second = p.panes[second_id].?;
p.update(.tick);
p.active = 0;
- _ = enterNormal(p);
+ key(p, ':');
key(p, Key.home);
key(p, 'l');
key(p, 'l');
- key(p, 'J');
+ window(p, 'j');
try std.testing.expectEqual(second_id, p.active);
- try std.testing.expect(!first.tag_edit and second.tag_edit);
+ try std.testing.expect(first.focus == .body and second.focus == .body);
+ key(p, ':');
key(p, Key.home);
for (0..4) |_| key(p, 'l');
- key(p, 'K');
+ window(p, 'k');
try std.testing.expectEqual(@as(usize, 0), p.active);
- try std.testing.expectEqual(@as(u16, 2), first.tag_col);
- key(p, 'J');
- try std.testing.expectEqual(second_id, p.active);
- try std.testing.expectEqual(@as(u16, 4), second.tag_col);
+ try std.testing.expect(second.focus == .body);
+ key(p, ':');
+ try std.testing.expectEqual(@as(i32, 2), first.tag.cur_col);
+ window(p, 'j');
+ key(p, ':');
+ try std.testing.expectEqual(@as(i32, 4), second.tag.cur_col);
}
}
- test "tag navigation uses uppercase pane and header keys while lowercase stays local" {
+ test "a tag's normal mode is the body's: motions stay in the tag and insert types into it" {
for ([_]bool{ false, true }) |bottom| {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 });
defer p.deinit();
_ = try p.setTestFile("body\n");
p.presentation.enabled = false;
p.settings.tag_bottom = bottom;
- p.settings.column_tags = true;
try std.testing.expect(p.executeBuiltinLine(0, "New"));
const right = p.active;
try std.testing.expect(layout.splitColumn(p, right, right, false));
p.update(.tick);
p.active = 0;
- const pane = enterNormal(p);
+ key(p, ':');
+ const pane = p.panes[0].?;
key(p, Key.home);
key(p, 'l');
- try std.testing.expectEqual(@as(u16, 1), pane.tag_col);
+ try std.testing.expectEqual(@as(i32, 1), pane.tag.cur_col);
key(p, 'h');
key(p, 'k');
- try std.testing.expectEqual(@as(u16, 0), pane.tag_col);
+ key(p, 'j');
+ try std.testing.expectEqual(@as(i32, 0), pane.tag.cur_col);
try std.testing.expectEqual(@as(usize, 0), p.active);
- try std.testing.expect(pane.tag_edit and p.topbar_col == null);
- key(p, 'L');
+ try std.testing.expect(pane.focus == .tag and p.topbar_col == null);
+ window(p, 'l');
try std.testing.expectEqual(right, p.active);
- try std.testing.expect(!pane.tag_edit and p.panes[right].?.tag_edit);
- key(p, 'H');
+ try std.testing.expect(pane.focus == .body and p.panes[right].?.focus == .body);
+ window(p, 'h');
try std.testing.expectEqual(@as(usize, 0), p.active);
- key(p, 'K');
- try std.testing.expect(p.topbar_col != null and p.header_column != null);
- try std.testing.expect(!pane.tag_edit);
- key(p, 'l');
- const column_cursor = p.topbar_col.?;
- key(p, 'K');
- try std.testing.expect(p.topbar_col != null and p.header_column == null);
- key(p, 'l');
- key(p, 'l');
- const workspace_cursor = p.topbar_col.?;
- key(p, 'J');
- try std.testing.expect(p.topbar_col != null and p.header_column != null);
- try std.testing.expectEqual(column_cursor, p.topbar_col.?);
- key(p, 'K');
- try std.testing.expectEqual(workspace_cursor, p.topbar_col.?);
- key(p, 'J');
- key(p, 'J');
- try std.testing.expect(p.topbar_col == null and pane.tag_edit);
- key(p, 'j');
- try std.testing.expect(!pane.tag_edit and p.topbar_col == null);
- try std.testing.expectEqual(@as(usize, 0), p.active);
- _ = enterNormal(p);
- key(p, 'K');
- key(p, 'j');
- try std.testing.expect(p.topbar_col == null and !pane.tag_edit);
key(p, ':');
key(p, 'i');
key(p, Key.end);
- const before = pane.tag_tail_len;
- try std.testing.expect(pane.tag_edit and pane.body.mode == .insert);
- for ("hjklHJKL") |letter| {
+ const before = Pardes.curTail(pane).len;
+ try std.testing.expect(pane.focus == .tag and pane.tag.mode == .insert and pane.body.mode == .normal);
+ for ("hjklHJKL:") |letter| {
const text = [_]u8{letter};
p.update(.{ .key = .{ .cp = letter, .text = &text } });
- try std.testing.expect(pane.tag_edit and pane.body.mode == .insert);
+ try std.testing.expect(pane.focus == .tag and pane.tag.mode == .insert);
}
- try std.testing.expectEqualStrings("hjklHJKL", pane.tag_tail[before..pane.tag_tail_len]);
- try std.testing.expect(pane.tag_edit and pane.body.mode == .insert and p.topbar_col == null);
+ try std.testing.expectEqualStrings("hjklHJKL:", Pardes.curTail(pane)[before..]);
+ try std.testing.expectEqualStrings("body\n", pane.file.?.content);
try std.testing.expectEqual(@as(usize, 0), p.active);
}
}
@@ -1151,32 +1168,26 @@ const TagNameTintTests = struct {
}
}
- test "tag filename tint follows scrolled Unicode staged names and yields to selection" {
+ test "tag filename tint follows scrolled 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");
- const old = pane.file.?.path.len;
configure(p);
defer p.custom_theme = null;
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- pane.tag_anchor = 0;
- pane.tag_col = @intCast(old - 1);
- pane.tag_sel = true;
const name = "/a/very/long/directory/that/extends/past/the/tag/界e\u{301}name-with-a-long-tail.zig";
- p.update(.{ .paste = name });
- try std.testing.expectEqualStrings(name, pane.tag_name[0..pane.tag_name_len.?]);
- pane.tag_col = @intCast(std.mem.indexOf(u8, name, "界").?);
- pane.tag_sel = false;
+ p.draftName(pane, 0);
+ redraft(pane, name);
+ pane.input.cur_col = @intCast(std.mem.indexOf(u8, name, "界").?);
try std.testing.expect((try check(p, 0, name)) > 0);
try std.testing.expect(pane.tag_scroll > 0);
- pane.tag_anchor = pane.tag_col;
- pane.tag_sel = true;
+ // 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
var arena = std.heap.ArenaAllocator.init(p.gpa);
defer arena.deinit();
const surface = try p.render(arena.allocator());
- const x = p.rects[0].x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.rawDisplayCol(name, pane.tag_col) - pane.tag_scroll));
+ 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;
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);
@@ -1208,32 +1219,46 @@ const TagNameTintTests = struct {
}
};
-test "pane filename edits stage commit and cancel without changing body or command tail" {
+/// Replace a name draft's text, the caret after it, as clearing it and
+/// typing would.
+fn redraft(pane: *Pane, name: []const u8) void {
+ if (pane.input.own) |own| pane.gpa.free(own);
+ pane.input.own = pane.gpa.dupe(u8, name) catch unreachable;
+ pane.input.cur_col = @intCast(name.len);
+}
+
+/// 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 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);
+ pane.input.cur_col = @intCast(pane.input.own.?.len);
+}
+
+test "pane filename drafts commit and cancel without changing body or command tail" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 70, .rows = 12 });
defer p.deinit();
const pane = try p.setTestFile("original body\n");
const original = try p.gpa.dupe(u8, pane.file.?.path);
defer p.gpa.free(original);
- p.seedTail(pane);
- try std.testing.expect(pane.appendTag(" SyntaxBold"));
- const tail = try p.gpa.dupe(u8, pane.tagSlice());
+ try pane.appendTag(" SyntaxBold");
+ const tail = try p.gpa.dupe(u8, Pardes.curTail(pane));
defer p.gpa.free(tail);
for ([_]bool{ false, true }) |commit| {
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- pane.tag_anchor = 0;
- pane.tag_col = @intCast(original.len - 1);
- pane.tag_sel = true;
+ clickPathEnd(p, pane);
+ p.update(.{ .key = .{ .cp = 'u', .ctrl = true } });
p.update(.{ .paste = "/new name.zig" });
try std.testing.expectEqualStrings(original, pane.file.?.path);
- try std.testing.expectEqualStrings("/new name.zig", pane.tag_name[0..pane.tag_name_len.?]);
- try std.testing.expectEqualStrings(tail, pane.tagSlice());
+ try std.testing.expectEqualStrings("/new name.zig", pane.input.own.?);
+ try std.testing.expectEqualStrings(tail, Pardes.curTail(pane));
try std.testing.expectEqualStrings("original body\n", pane.file.?.content);
p.update(.{ .key = .{ .cp = if (commit) Key.enter else Key.escape } });
- try std.testing.expect(!pane.tag_edit);
- try std.testing.expect(pane.tag_name_len == null);
+ try std.testing.expect(pane.focus == .body);
+ try std.testing.expect(pane.prompt == .none);
try std.testing.expectEqualStrings(if (commit) "/new name.zig" else original, pane.file.?.path);
- try std.testing.expectEqualStrings(tail, pane.tagSlice());
+ try std.testing.expectEqualStrings(tail, Pardes.curTail(pane));
while (p.nextEffect()) |effect| try std.testing.expect(std.meta.activeTag(effect) != .save_file);
}
try std.testing.expect(pane.file.?.revision != pane.file.?.saved_revision);
@@ -1243,27 +1268,21 @@ test "pane filename grapheme edits and invalid empty commits preserve the buffer
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
const pane = try p.setTestFile("untouched\n");
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- pane.tag_anchor = 0;
- pane.tag_col = @intCast(pane.file.?.path.len - 1);
- pane.tag_sel = true;
+ clickPathEnd(p, pane);
+ p.update(.{ .key = .{ .cp = 'u', .ctrl = true } });
p.update(.{ .paste = "/café界" });
p.update(.{ .key = .{ .cp = Key.backspace } });
- try std.testing.expectEqualStrings("/café", pane.tag_name[0..pane.tag_name_len.?]);
+ try std.testing.expectEqualStrings("/café", pane.input.own.?);
p.update(.{ .key = .{ .cp = Key.backspace } });
- try std.testing.expectEqualStrings("/caf", pane.tag_name[0..pane.tag_name_len.?]);
- pane.tag_anchor = 0;
- pane.tag_col = 3;
- pane.tag_sel = true;
- p.update(.{ .key = .{ .cp = Key.delete } });
- try std.testing.expectEqual(@as(u16, 0), pane.tag_name_len.?);
+ try std.testing.expectEqualStrings("/caf", pane.input.own.?);
+ p.update(.{ .key = .{ .cp = 'u', .ctrl = true } });
+ try std.testing.expectEqualStrings("", pane.input.own.?);
p.update(.{ .key = .{ .cp = Key.enter } });
- try std.testing.expect(pane.tag_edit);
+ try std.testing.expect(pane.prompt == .name);
try std.testing.expectEqualStrings("/test.txt", pane.file.?.path);
try std.testing.expectEqualStrings("untouched\n", pane.file.?.content);
p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expect(pane.tag_name_len == null);
+ try std.testing.expect(pane.prompt == .none);
}
test "leaving a pane or entering a header cancels an uncommitted filename" {
@@ -1273,11 +1292,10 @@ test "leaving a pane or entering a header cancels an uncommitted filename" {
for ([_]bool{ false, true }) |header| {
p.active = 0;
p.topbar_col = null;
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- p.update(.{ .key = .{ .cp = Key.home } });
+ p.update(.tick);
+ clickPathEnd(p, pane);
p.update(.{ .paste = "/draft" });
- try std.testing.expect(pane.tag_name_len != null);
+ try std.testing.expect(pane.prompt == .name);
if (header) {
p.topbar_col = 0;
p.update(.tick);
@@ -1285,21 +1303,18 @@ test "leaving a pane or entering a header cancels an uncommitted filename" {
try std.testing.expect(p.executeBuiltinLine(0, "New"));
p.update(.tick);
}
- try std.testing.expect(pane.tag_name_len == null);
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.prompt == .none);
+ try std.testing.expect(pane.focus == .body);
try std.testing.expectEqualStrings("/test.txt", pane.file.?.path);
}
}
-test "middle clicking Save commits the staged filename before issuing a save" {
+test "middle clicking Save commits the drafted filename before issuing a save" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 70, .rows = 12 });
defer p.deinit();
const pane = try p.setTestFile("untouched\n");
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- pane.tag_anchor = 0;
- pane.tag_col = @intCast(pane.file.?.path.len - 1);
- pane.tag_sel = true;
+ clickPathEnd(p, pane);
+ p.update(.{ .key = .{ .cp = 'u', .ctrl = true } });
p.update(.{ .paste = "/renamed.txt" });
var arena = std.heap.ArenaAllocator.init(p.gpa);
defer arena.deinit();
@@ -1312,7 +1327,7 @@ test "middle clicking Save commits the staged filename before issuing a save" {
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.expectEqualStrings("/renamed.txt", pane.file.?.path);
- try std.testing.expect(pane.tag_name_len == null);
+ try std.testing.expect(pane.prompt == .none);
try std.testing.expectEqualStrings("untouched\n", pane.file.?.content);
var saves: usize = 0;
while (p.nextEffect()) |effect| if (effect == .save_file) {
@@ -1322,24 +1337,19 @@ test "middle clicking Save commits the staged filename before issuing a save" {
try std.testing.expectEqual(@as(usize, 1), saves);
}
-test "oversized filename paste refuses atomically across a selected tag boundary" {
+test "an oversized drafted name is refused at commit and the draft stays to correct" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
const pane = try p.setTestFile("untouched\n");
- p.update(.{ .key = .{ .cp = ':' } });
- p.update(.{ .key = .{ .cp = 'i' } });
- const before = try p.tagText(p.gpa, pane);
- defer p.gpa.free(before);
- pane.tag_anchor = 0;
- pane.tag_col = @intCast(before.len - 1);
- pane.tag_sel = true;
- const excess = try p.gpa.alloc(u8, pane.tag_name.len + 1);
+ clickPathEnd(p, pane);
+ const excess = try p.gpa.alloc(u8, 5000);
defer p.gpa.free(excess);
@memset(excess, 'x');
+ excess[0] = '/';
p.update(.{ .paste = excess });
- try std.testing.expect(pane.tag_name_len == null);
- try std.testing.expect(pane.tag_sel);
- try std.testing.expectEqualStrings(before, try p.tagText(p.scratch.allocator(), pane));
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ try std.testing.expect(pane.prompt == .name);
+ try std.testing.expectEqualStrings("/test.txt", pane.file.?.path);
try std.testing.expectEqualStrings("untouched\n", pane.file.?.content);
}
@@ -1349,16 +1359,17 @@ test "paste into pane commands and prompts never inserts into the body" {
const pane = try p.setTestFile("one two one\n");
p.update(.{ .key = .{ .cp = ':' } });
p.update(.{ .key = .{ .cp = 'A' } });
- p.update(.{ .paste = " Custom\r\nCommand" });
- try std.testing.expect(std.mem.endsWith(u8, pane.tagSlice(), " CustomCommand"));
+ // A tag takes a paste as a body does, newline and all.
+ p.update(.{ .paste = " Custom\nCommand" });
+ try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Custom\nCommand") != null);
p.update(.{ .key = .{ .cp = Key.escape } });
p.startPrompt(pane, .{ .search = config.search_marker });
p.update(.{ .paste = "t\r\nwo" });
try std.testing.expectEqualStrings(config.search_marker ++ "two", pane.promptText().?);
- try std.testing.expect(std.mem.endsWith(u8, pane.tagSlice(), " CustomCommand"));
+ try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Custom\nCommand") != null);
try std.testing.expectEqualStrings("one two one\n", pane.file.?.content);
p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expect(std.mem.endsWith(u8, pane.tagSlice(), " CustomCommand"));
+ try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Custom\nCommand") != null);
}
test "long pane tags reveal commands and map visible clicks to whole tag addresses" {
@@ -1367,8 +1378,7 @@ test "long pane tags reveal commands and map visible clicks to whole tag address
const pane = try p.setTestFile("body\n");
p.gpa.free(pane.file.?.path);
pane.file.?.path = try p.gpa.dupe(u8, "/an/especially/deep/project/path/with/a/long/component/filename.zig");
- p.seedTail(pane);
- try std.testing.expect(pane.appendTag(" SyntaxBold"));
+ try pane.appendTag(" SyntaxBold");
p.update(.{ .key = .{ .cp = ':' } });
p.update(.{ .key = .{ .cp = 'A' } });
var arena = std.heap.ArenaAllocator.init(p.gpa);
@@ -1549,7 +1559,13 @@ test "terminal edits and undo survive partial history clearing and whole-screen
pane.body.cur_pinned = true;
pane.body.vsel = .{ .active = true, .row = row, .col = 1 };
panes.Terminal.pushUndo(p, pane);
- pane.body.ed_redo[0] = panes.Terminal.snap(p, pane) orelse return error.SnapshotFailed;
+ const overlay = pane.ovl.?;
+ pane.body.ed_redo[0] = .{
+ .ovl = .{ .row = overlay.row, .rows = overlay.rows, .text = try gpa.dupe(u8, overlay.text) },
+ .cur_row = pane.body.cur_row,
+ .cur_col = pane.body.cur_col,
+ .vsel = pane.body.vsel,
+ };
pane.body.ed_redo_len = 1;
if (action == .hidden_undo) {
gpa.free(pane.ovl.?.text);
@@ -1663,9 +1679,7 @@ test "a prompt's entry and cancellation leave the tag, the body and its cursor a
try pane.setOwnedCwd("/dír space");
const tail = " keep";
for (cases) |case| {
- @memcpy(pane.tag_tail[0..tail.len], tail);
- pane.tag_tail_len = tail.len;
- pane.tag_init = true;
+ try setTag(pane, tail);
pane.body.cur_row = 1;
pane.body.cur_col = 2;
pane.body.cur_pinned = true;
@@ -1677,17 +1691,17 @@ test "a prompt's entry and cancellation leave the tag, the body and its cursor a
p.startPrompt(pane, .{ .search = config.search_marker });
}
// The answer is typed into the prompt's own line, never the tag.
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.focus == .body);
try std.testing.expectEqual(.insert, pane.input.mode);
try std.testing.expectEqualStrings(case.marker, pane.promptText().?);
try std.testing.expectEqual(@as(i32, @intCast(case.marker.len)), pane.input.cur_col);
- try std.testing.expectEqualStrings(tail, pane.tag_tail[0..pane.tag_tail_len]);
+ try std.testing.expectEqualStrings(tail, pane.tag.own.?);
try std.testing.expectEqualDeep(modal.Normal.State{}, pane.body.normal);
p.update(.{ .key = .{ .cp = Key.escape } });
try std.testing.expect(pane.prompt == .none);
try std.testing.expect(pane.input.own == null);
try std.testing.expectEqual(.normal, pane.body.mode);
- try std.testing.expectEqualStrings(tail, pane.tag_tail[0..pane.tag_tail_len]);
+ try std.testing.expectEqualStrings(tail, pane.tag.own.?);
try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row);
try std.testing.expectEqual(@as(i32, 2), pane.body.cur_col);
try std.testing.expectEqualStrings("one\ntwo\n", pane.file.?.content);
@@ -1698,9 +1712,10 @@ test "a prompt opens whatever the tag already holds" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
const pane = try p.setTestFile("one\n");
- @memset(&pane.tag_tail, 'x');
- pane.tag_tail_len = pane.tag_tail.len;
- pane.tag_init = true;
+ const full = try p.gpa.alloc(u8, pardes.memory.limits.max_tag_tail);
+ defer p.gpa.free(full);
+ @memset(full, 'x');
+ try setTag(pane, full);
for (0..3) |case| {
switch (case) {
0 => p.startPrompt(pane, .{ .search = config.search_marker }),
@@ -1708,8 +1723,7 @@ test "a prompt opens whatever the tag already holds" {
else => p.update(.{ .key = .{ .cp = '|' } }),
}
try std.testing.expect(pane.prompt != .none);
- try std.testing.expectEqual(pane.tag_tail.len, pane.tag_tail_len);
- for (pane.tag_tail) |byte| try std.testing.expectEqual(@as(u8, 'x'), byte);
+ try std.testing.expectEqualStrings(full, pane.tag.own.?);
p.update(.{ .key = .{ .cp = Key.escape } });
}
}
@@ -1723,8 +1737,8 @@ test "generated output refuses every pipe prompt before seeding the tag" {
for ([_]Key{ .{ .cp = '|' }, .{ .cp = '|', .alt = true }, .{ .cp = '!' }, .{ .cp = '!', .alt = true } }) |key| {
p.update(.{ .key = key });
try std.testing.expect(pane.prompt == .none);
- try std.testing.expect(!pane.tag_init);
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.tag.own == null);
+ try std.testing.expect(pane.focus == .body);
try std.testing.expectEqualStrings("generated\n", pane.file.?.content);
}
}
@@ -1798,7 +1812,7 @@ test "a prompt that cannot be opened leaves the open one as it was" {
try std.testing.expect(pane.prompt == .search);
try std.testing.expectEqualStrings(config.search_marker ++ "prior", pane.promptText().?);
try std.testing.expectEqualDeep(previous_normal, pane.body.normal);
- try std.testing.expect(!pane.tag_init and !pane.tag_edit);
+ try std.testing.expect(pane.tag.own == null and pane.focus == .body);
}
}
@@ -1830,7 +1844,7 @@ test "a prompt opens with one allocation" {
const before = failing.alloc_index;
p.startPrompt(pane, request);
try std.testing.expectEqual(before + 1, failing.alloc_index);
- try std.testing.expect(!pane.tag_edit and pane.input.mode == .insert);
+ try std.testing.expect(pane.focus == .body and pane.input.mode == .insert);
}
}
@@ -4080,14 +4094,18 @@ test "cancelled normal gestures leave no count or subprefix after mouse and focu
const rect = p.rects[0];
const tag_y = if (p.settings.tag_bottom) rect.y + rect.h - pardes.BOX_H else rect.y;
const body_y = p.bodyTop(rect);
- const x = rect.x + config.GUTTER + @as(u16, @intCast(panes.File.gutterWidth(pane))) + 3;
+ const x = if (gesture == .tag)
+ rect.x + pardes.TAG_TEXT_INSET + @as(u16, @intCast(panes.File.displayWidth(try p.tagPrefix(pane)))) + 3
+ else
+ rect.x + config.GUTTER + @as(u16, @intCast(panes.File.gutterWidth(pane))) + 3;
const y = if (gesture == .tag) tag_y else body_y;
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = x, .row = y } });
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = x, .row = y } });
if (gesture == .tag) {
- try std.testing.expect(pane.tag_edit);
+ try std.testing.expect(pane.focus == .tag);
p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expect(!pane.tag_edit);
+ p.update(.{ .key = .{ .cp = ':' } });
+ try std.testing.expect(pane.focus == .body);
}
},
.focus, .last => {
diff --git a/test/pdf.zig b/test/pdf.zig
index 5bc271d4..745ecb1a 100644
--- a/test/pdf.zig
+++ b/test/pdf.zig
@@ -309,8 +309,8 @@ test "PDF dump fallback remains a byte-preserving file" {
try std.testing.expect(!hasPdf(pane));
try std.testing.expectEqualStrings(path, pane.file.?.path);
try std.testing.expectEqualSlices(u8, source, pane.file.?.content);
- try std.testing.expect(pane.tag_init);
- try std.testing.expectEqualStrings(" Keep Del", pane.tag_tail[0..pane.tag_tail_len]);
+ try std.testing.expect(pane.tag.own != null);
+ try std.testing.expectEqualStrings(" Keep Del", pane.tag.own.?);
}
test "PdfSections Look follows the exact owning PDF, not an equal path" {
@@ -918,9 +918,8 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" {
try std.testing.expectEqual(PdfTintMode.full, pane.pdf.?.tint);
pane.pdf.?.fit = .height;
const custom_tail = " Keep Del";
- @memcpy(pane.tag_tail[0..custom_tail.len], custom_tail);
- pane.tag_tail_len = custom_tail.len;
- pane.tag_init = true;
+ if (pane.tag.own) |own| p.gpa.free(own);
+ pane.tag.own = try p.gpa.dupe(u8, custom_tail);
try pardes.dump.dumpState(p);
const restored = try pardes.dump.initFromDump(gpa, .{}, p.dump_out.?);
defer restored.deinit();
@@ -929,10 +928,10 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" {
try std.testing.expectEqual(PdfFitMode.width, restored.panes[0].?.pdf.?.fit);
try std.testing.expectEqual(PdfTintMode.filtered, restored.panes[0].?.pdf.?.tint);
const restored_pane = restored.panes[0].?;
- try std.testing.expect(restored_pane.tag_init);
+ try std.testing.expect(restored_pane.tag.own != null);
try std.testing.expectEqualStrings(
custom_tail,
- restored_pane.tag_tail[0..restored_pane.tag_tail_len],
+ restored_pane.tag.own.?,
);
}
@@ -1100,20 +1099,20 @@ test "PDF normal adapter consumes unsupported actions and navigates page fallbac
try std.testing.expectEqual(@as(u32, 0), pane.body.normal.count);
try std.testing.expectEqual(.none, pane.body.normal.prefix);
try std.testing.expect(!pane.body.vsel.active and !pane.body.msel.active and pane.body.nsel == 0);
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.focus == .body);
// Cross-pane BODY-NORMAL actions keep their established shared paths.
p.update(.{ .key = .{ .cp = ':' } });
- try std.testing.expect(pane.tag_edit);
+ try std.testing.expect(pane.focus == .tag);
try std.testing.expectEqual(Mode.normal, pane.body.mode);
- p.update(.{ .key = .{ .cp = Key.escape } });
- try std.testing.expect(!pane.tag_edit);
+ p.update(.{ .key = .{ .cp = ':' } });
+ try std.testing.expect(pane.focus == .body);
p.update(.{ .key = .{ .cp = ' ' } });
try std.testing.expect(p.leader_on);
p.update(.{ .key = .{ .cp = '!' } });
try std.testing.expect(!p.leader_on);
p.update(.{ .key = .{ .cp = '/' } });
- try std.testing.expect(pane.prompt == .search and !pane.tag_edit);
+ try std.testing.expect(pane.prompt == .search and pane.focus == .body);
p.update(.{ .key = .{ .cp = Key.escape } });
try std.testing.expect(pane.prompt == .none);
}
diff --git a/test/snapshots/cmdword.golden b/test/snapshots/cmdword.golden
index e6f470fa..f099464f 100644
--- a/test/snapshots/cmdword.golden
+++ b/test/snapshots/cmdword.golden
@@ -16,9 +16,9 @@
== snap nested grid=120x31 cursor=4,24
|22: ls
|23: cmds.txt p.txt zzmark.txt
-== snap execword grid=120x31 cursor=69,17
+== snap execword grid=120x31 cursor=4,24
|17: /tmp/pardes-snap/cmdword/cwd Tty Save Mode Filter Collapse Del Exec
-== snap tagcmd grid=120x31 cursor=83,17
+== snap tagcmd grid=120x31 cursor=4,24
|17: /tmp/pardes-snap/cmdword/cwd Tty Save Mode Filter Collapse Del Exec @`echo ZZ''T`
|24: echo ZZ''T
|25: ZZT
diff --git a/test/snapshots/cmdword.snap b/test/snapshots/cmdword.snap
index eb0e0ea6..361e37ba 100644
--- a/test/snapshots/cmdword.snap
+++ b/test/snapshots/cmdword.snap
@@ -57,25 +57,31 @@ wait 10000 zzmark.txt
stable 700 15000
snap nested
# the same syntax typed into a TAG — the other place the expansion has to know
-# about backticks (tagChordText). The look above left the shell focused, so `:`
-# opens ITS tag; `i` then End walks to the end of the tail, and the chord runs
-# the word under the cursor exactly as the body's does.
+# about backticks. The look above left the shell focused, so `:` opens ITS
+# tag; `A` appends at the end of it, Esc and `h` put the normal-mode cursor on
+# the last character typed, and the execute key runs the word under it
+# exactly as the body's does.
key :
-key i
-key end
+key A
key space
text Exec
-key left
+key esc
+settle 100
+key h
key tab
stable 700 8000
# ...except that word is `Exec` ITSELF, with nothing to execute. This is the
# case that must not hang a re-entrant dispatcher: it runs out of argument
-# immediately and does nothing at all.
+# immediately and does nothing at all. Executing gave the keyboard back to the
+# body, and `:` finds the tag's cursor where it was left.
snap execword
-key end
+key :
+key A
key space
text @`echo ZZ''T`
-key left
+key esc
+settle 100
+key h
key tab
wait 10000 ZZT
stable 700 15000
diff --git a/test/snapshots/collapse.golden b/test/snapshots/collapse.golden
index 18d24d6b..eaf9bbc3 100644
--- a/test/snapshots/collapse.golden
+++ b/test/snapshots/collapse.golden
@@ -68,8 +68,8 @@
|3:
|4:
|5:
-== 9p tab-collapsed-cursor grid=160x25 cursor=51,2,bar
-== snap tab-restored grid=160x25 cursor=51,2
+== 9p tab-collapsed-cursor grid=160x25 cursor=none
+== snap tab-restored grid=160x25 cursor=7,3
|3: 1 KEEP-FIRST Collapse
|4: 2 KEEP-SECOND
|5: 3 KEEP-THIRD
diff --git a/test/snapshots/collapse.snap b/test/snapshots/collapse.snap
index 64f6bdb8..83872855 100644
--- a/test/snapshots/collapse.snap
+++ b/test/snapshots/collapse.snap
@@ -1,5 +1,6 @@
# A default pane-tag command collapses to one row and restores the body.
-# Exercise real middle clicks twice, then tag editing's Tab execution twice.
+# Exercise real middle clicks twice, then the tag's own execute key twice: a
+# click types into the tag, Esc is its normal mode, and Tab executes there.
file kept.txt KEEP-FIRST\nKEEP-SECOND\nKEEP-THIRD
start 25 160 kept.txt
wait 8000 KEEP-THIRD
@@ -19,12 +20,16 @@ stable 500 8000
snap middle-restored
press left @Collapse 3
release left @Collapse 3
+key esc
+settle 100
key tab
stable 500 8000
snap tab-collapsed
snap9p tab-collapsed-cursor
press left @Collapse 3
release left @Collapse 3
+key esc
+settle 100
key tab
wait 8000 KEEP-THIRD
stable 500 8000
diff --git a/test/snapshots/tag.snap b/test/snapshots/tag.snap
index a545ff9f..4faf6062 100644
--- a/test/snapshots/tag.snap
+++ b/test/snapshots/tag.snap
@@ -1,7 +1,8 @@
-# a CLICKED tag is insert mode: type straight in (no 'i'; `:` is the normal-
-# mode door, see tagcmd); backspace edits; Esc leaves back to the body; Enter
-# looks the word at the cursor (Tab executes it). Multi-word execution from
-# the tag stays the mouse's job (middle-drag), like anywhere else.
+# a CLICKED tag is typed straight into (no 'i'; `:` is the normal-mode door,
+# see tagcmd); backspace edits; Esc drops to the tag's normal mode, which is
+# the body's normal mode, and `:` there hands the keyboard back to the body;
+# Enter looks the word at the cursor (Tab executes it). Multi-word execution
+# from the tag stays the mouse's job (middle-drag), like anywhere else.
file t.txt ZZLOOKED
start 31 150 -n 3
wait 8000 t.txt
@@ -29,16 +30,21 @@ snap tag-del
send .txt
stable 600 8000
stable 400 5000
-# Esc: out of the tag, body back in normal mode, typed tail stays as text
+# Esc: the tag's normal mode; `:` then gives the keyboard back to the body,
+# which is in normal mode, and the typed text stays
key esc
settle 100
+key :
stable 400 5000
snap tag-exit
-# re-enter the tag and look the typed word: Enter opens t.txt as a file pane.
-# Click the inserted word itself; filename, separator and commands are compact.
+# re-enter the tag and look the typed word: Enter in normal mode opens t.txt
+# as a file pane. Click the inserted word itself; filename, separator and
+# commands are compact.
press left @t.txt 3
release left @t.txt 3
stable 400 5000
+key esc
+settle 100
key enter
wait 10000 ZZLOOKED
stable 700 15000
diff --git a/test/snapshots/tagalign.snap b/test/snapshots/tagalign.snap
index 9df22f57..a77415d9 100644
--- a/test/snapshots/tagalign.snap
+++ b/test/snapshots/tagalign.snap
@@ -22,9 +22,14 @@ press left 12 3
release left 12 3
stable 400 5000
snap clicked
-# End reveals the complete command suffix; a scrolled Del still executes.
+# The click was on the path, so it drafted a name; Esc drops the draft. `:`
+# then `A` puts the caret after the tag's end, which reveals the complete
+# command suffix; a scrolled Del still executes.
resize 31 70
-key end
+key esc
+settle 100
+key :
+key A
stable 400 5000
snap revealed
press middle @Del 3
diff --git a/test/snapshots/tagcmd.snap b/test/snapshots/tagcmd.snap
index 305ed521..0e9047fb 100644
--- a/test/snapshots/tagcmd.snap
+++ b/test/snapshots/tagcmd.snap
@@ -1,11 +1,12 @@
# `:` — vim's command line with acme's vocabulary. It focuses the pane's OWN
# tag in NORMAL mode (block cursor; the layout box shows the pane's BODY mode,
-# which for this file pane is normal either way), parked at the tail's
-# START — always the start, so `:w` is the same two keystrokes every time.
-# Motions walk the tag's words (w selects the "Save" span, exactly like a body
-# word motion), the execute chord runs the one under the cursor and hands the
-# body back in normal mode — same keys as the body, so Tab by default (see
-# config.look_key/exec_key). Esc backs out instead, leaving the tail as it was.
+# which for this file pane is normal either way). The tag remembers where its
+# cursor was; the first time there is nothing to remember and it starts on
+# Save. The tag's normal mode is the body's: w selects the "Save" span exactly
+# as a body word motion does, and the execute key runs the word under the
+# cursor and hands the body back — same keys as the body, so Tab by default
+# (see config.look_key/exec_key). `:` again backs out instead, leaving the tag
+# as it was.
lines c.txt 6
start 31 100 c.txt
wait 8000 Newcol Joincol
@@ -18,7 +19,7 @@ key esc
settle 100
stable 400 5000
snap edited
-# `:` -> the tag, in normal mode, cursor on the tail's first cell
+# `:` -> the tag, in normal mode, cursor on Save
key :
stable 400 5000
snap colon
@@ -32,10 +33,9 @@ stable 600 8000
stable 400 5000
snap saved
snapfile written /tmp/pardes-snap/tagcmd/cwd/c.txt
-# `:` then Esc: back to the body with the tail untouched, nothing run
+# `:` then `:`: back to the body with the tag untouched, nothing run
key :
stable 400 5000
-key esc
-settle 100
+key :
stable 400 5000
snap escaped
diff --git a/test/snapshots/tagnav.golden b/test/snapshots/tagnav.golden
index 52915940..3fdba9f2 100644
--- a/test/snapshots/tagnav.golden
+++ b/test/snapshots/tagnav.golden
@@ -22,39 +22,20 @@
== snap colon grid=100x31 cursor=28,2
|2: es-snap/tagnav/cwd/w.txt Save Tty Collapse Del /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte
|18: ls
-== snap right-tag grid=100x31 cursor=29,2
-== snap left-tag grid=100x31 cursor=28,2
-== snap down-tag grid=100x31 cursor=7,3
+== 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 Save Mode Filte
-== snap up-tag grid=100x31 cursor=7,3
-== snap column-tag grid=100x31 cursor=7,3
-== snap up-edge grid=100x31 cursor=7,3
-== snap topbar-word grid=100x31 cursor=11,3
-== snap topbar-graphemes grid=100x31 cursor=7,4
-== snap column-down grid=100x31 cursor=7,5
-== snap back-down grid=100x31 cursor=7,5
-== snap arrows-still-motion grid=100x31 cursor=9,5
-== snap back-to-body grid=100x31 cursor=4,20
-|3: 1 alpha Last ls
+|3: 1 alpha Right ls
+== snap right-tag grid=100x31 cursor=70,2
+|2: /tmp/pardes-snap/tagnav/cwd/w.txt Save Tty Coll p/tagnav/cwd Tty Save Mode Filter Collapse Del
+|3: 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 Save Mode Filte
+== 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 Save Mode Filte
|3: 1 alpha $ ls
|5: 3 gamma $
-== snap tty-tag grid=100x31 cursor=57,5
-|5: 3 gamma $ h:l
-== snap tty-restored grid=100x31 cursor=4,20
-|3: 1 alpha $ ls Last
-== snap topbar-dollar grid=100x31 cursor=48,17
-|3: 1 alpha $ ls
-|17: p/tagnav/cwd Tty Save Mode Filter Collapse Del
-== snap topbar-dump-word grid=100x31 cursor=25,17
-== snap topbar-exec grid=100x31 cursor=4,20
-|17: /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte
-|29: /tmp/pardes-snap/tagnav/cwd/+Search Save Tty Co
-|30: 1
-== snap topbar-restore-word grid=100x31 cursor=49,17
-|17: p/pardes-snap/tagnav/cwd Tty Save Mode Filter C
-== snap topbar-restored grid=100x31 cursor=4,20
-|17: /tmp/pardes-snap/tagnav/cwd Tty Save Mode Filte
-|27: /tmp/pardes-snap/tagnav/cwd/+Search Save Tty Co
-|28: 1
+== snap tty-tag grid=100x31 cursor=90,2
+== snap tty-restored grid=100x31 cursor=54,5
diff --git a/test/snapshots/tagnav.snap b/test/snapshots/tagnav.snap
index c375e644..eed808de 100644
--- a/test/snapshots/tagnav.snap
+++ b/test/snapshots/tagnav.snap
@@ -1,25 +1,22 @@
-# 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.
+# A tag's normal mode is the body's: h/l/w move in the tag's own text, and
+# moving between panes is the window keys' job from a tag as from a body --
+# there are no tag-only hops. `:` is the one key a tag and a body do not
+# share: from the body it focuses the pane's own tag, from the tag it hands the
+# keyboard back to the body. Each tag remembers its cursor; the first time
+# there is nothing to remember and it starts on Save.
#
# Evidence is the `cursor=` header (the ACTIVE pane's cursor, which while a tag
# is focused sits on THAT pane's tag row) plus each pane's layout box, which
-# carries its BODY mode as one character: `•` normal, `^` insert, `$` tty. A
-# tag edit parks the body mode rather than overwriting it, so the box keeps
-# saying what the pane we left will be when we hop back off its tagline.
+# carries its BODY mode as one character: ` ` normal, `^` insert, `$` tty. The
+# tag has a mode of its own, so the box never changes while a tag is used.
file w.txt alpha\nbeta\ngamma
start 31 100 w.txt
wait 8000 Newcol Joincol
wait 8000 w.txt
stable 700 20000
-# A FILE boots alone now, so the shell this script calls "the launch shell" is
-# opened here: Alt-n opens it and focuses it, Alt-c puts it in a column of its
-# own on the right, SPC w h comes back to the file. (`New`/`Newcol` open an
-# empty +New scratch now, not a shell.)
+# A FILE boots alone, so the launch shell is opened here: Alt-n opens it and
+# focuses it, Alt-c puts it in a column of its own on the right, SPC w h comes
+# back to the file.
key a-n
key a-c
stable 700 15000
@@ -32,110 +29,51 @@ 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
+# `:` -> the file's OWN tagline, normal mode, on Save
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 visits the column tag first, then the workspace tag. Both stay in normal
-# navigation mode; neither hop drops into a pane body.
-key k
-stable 400 5000
-snap column-tag
-key k
-stable 400 5000
-snap up-edge
-# the bar speaks the tag's own one-line vocabulary: w to the next word start
+# w selects in the tag the way it selects in a body
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
+snap word
+# l is a motion in the tag, not a hop to the next pane
+key l l
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
+snap in-tag
+# the window keys move between panes from a tag as from a body: the tag lets
+# go and the neighbour's BODY takes the keyboard
+key c-w l
+stable 400 5000
+snap right
+# `:` there -> that pane's own tag, on its Save the first time
+key :
stable 400 5000
-snap column-down
-key j
+snap right-tag
+# back left, and `:` finds the file's tag cursor where l left it
+key c-w h
stable 400 5000
-snap back-down
-# the arrows are still in-tag motion: two rights walk two columns of the tail
-key right right
+key :
stable 400 5000
-snap arrows-still-motion
-key esc
-settle 100
+snap left-tag
+# `:` in the tag's normal mode: back to the body
+key :
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, and its box reads `$` throughout —
-# during the hop too, because the box shows the PARKED mode, not the tag's.
+# A TERMINAL KEEPS ITS TTY MODE. Put the launch shell in raw tty mode (Ctrl-b),
+# click its tagline and come back: its box reads `$` throughout, because the
+# tag's mode is the tag's own.
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
+press left @Mode 3
+release left @Mode 3
stable 400 5000
snap tty-tag
key esc
settle 100
-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`). `$` proves the destructive
-# word is exactly last; four long-word steps back land on `Dump`, the visible
-# non-destructive action used here. It 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 k
-stable 400 5000
-key $
-stable 400 5000
-snap topbar-dollar
-key B B B B
-stable 400 5000
-snap topbar-dump-word
-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 k
stable 400 5000
-key $ B B
-stable 400 5000
-snap topbar-restore-word
-key enter
-stable 900 15000
-snap topbar-restored
+snap tty-restored
diff --git a/test/snapshots/tagpath.golden b/test/snapshots/tagpath.golden
index 1b798445..2ba1a2e6 100644
--- a/test/snapshots/tagpath.golden
+++ b/test/snapshots/tagpath.golden
@@ -8,12 +8,12 @@
| 4 line 4
| 5
== snap colon grid=100x31 cursor=38,2
-== snap col0 grid=100x31 cursor=3,2
-== snap sel-path grid=100x31 cursor=36,2
-== style sel-path grid=100x31
+== snap col0 grid=100x31 cursor=37,2
+== snap drag-cross grid=100x31 cursor=41,2
+== style drag-cross 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-99 d,#26362d,
-|2: 0-1 #000000,#add38a, 2-2 d,#26362d, 3-36 #f5f7ee,#3c614a, 37-58 #becfc4,#26362d, 59-99 d,#26362d,
+|2: 0-1 #000000,#add38a, 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-12 #e5eee5,#0d1410, 13-99 d,#0d1410,
|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-99 d,#0d1410,
|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-12 #e5eee5,#0d1410, 13-99 d,#0d1410,
@@ -42,16 +42,9 @@
|28: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-99 d,#0d1410,
|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
-|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,
-|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-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
-== snap name-cancelled grid=100x31 cursor=41,3
-|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
+== snap name-cancelled grid=100x31 cursor=7,3
+|2: /tmp/pardes-snap/tagpath/cwd/c.txt Save Tty Collapse Del
+== snap name-committed grid=100x31 cursor=7,3
|2: /renamed/tmp/pardes-snap/tagpath/cwd/c.txt * Save Tty Collapse Del
diff --git a/test/snapshots/tagpath.snap b/test/snapshots/tagpath.snap
index cf431fa1..b9ef80e6 100644
--- a/test/snapshots/tagpath.snap
+++ b/test/snapshots/tagpath.snap
@@ -1,5 +1,8 @@
-# Filename drafts share the selectable tag coordinate space with commands.
-# Escape cancels; Enter changes buffer identity, never renames a disk file.
+# The path is part of the tag's text for the mouse -- a sweep selects across
+# the path and the commands after it -- but never for the keyboard: the tag's
+# cursor lives in its own text, so `0` goes to the start of that, not of the
+# path. A click on the path drafts a new name there instead. Escape cancels;
+# Enter changes buffer identity, never renames a disk file.
lines c.txt 4
start 31 100 c.txt
wait 8000 Newcol Joincol
@@ -12,16 +15,6 @@ snap colon
key 0
stable 400 5000
snap col0
-key E
-stable 400 5000
-snap sel-path
-snapstyle sel-path
-key y
-key esc
-settle 100
-key p
-stable 400 5000
-snap yanked-path
# Sweep across the path / command boundary without injected padding.
press left 12 3
drag left 30 3
@@ -33,17 +26,18 @@ snapstyle drag-cross
key esc
settle 100
key :
-key 0
-key i
+stable 400 5000
+# a click on the path is a name draft, the caret where it was clicked
+press left 4 3
+release left 4 3
text /renamed
stable 400 5000
snap name-draft
key esc
stable 400 5000
snap name-cancelled
-key :
-key 0
-key i
+press left 4 3
+release left 4 3
text /renamed
key enter
stable 400 5000
diff --git a/test/snapshots/ttyclick.golden b/test/snapshots/ttyclick.golden
index 719e3955..14344371 100644
--- a/test/snapshots/ttyclick.golden
+++ b/test/snapshots/ttyclick.golden
@@ -20,11 +20,7 @@
| ls
| probe.txt
== snap tty-tagedit grid=100x31 cursor=33,2
-|3: ls ls
-|5:
== snap tty-tagexit grid=100x31 cursor=4,5
-|3: $ ls ls
-|5: $
== snap ins-bodyclick grid=100x31 cursor=11,4
|2: ^ /tmp/pardes-snap/ttyclick/cwd Tty Save Mode Fil /tmp/pardes-snap/ttyclick/cwd Tty Save Mode Fil
|3: ls ls
diff --git a/test/snapshots/ttyclick.snap b/test/snapshots/ttyclick.snap
index 89ed5300..c125a231 100644
--- a/test/snapshots/ttyclick.snap
+++ b/test/snapshots/ttyclick.snap
@@ -1,7 +1,7 @@
# clicks in a shell pane never change its mode: a body click in raw tty
-# keeps tty (before: the press dropped it to normal), a tag-edit round trip
-# restores the mode it hijacked, and an insert-mode body click re-pins the
-# cursor but stays insert. File panes keep the old behavior (click -> normal).
+# keeps tty (before: the press dropped it to normal), a round trip through the
+# tag leaves it alone (the tag has a mode of its own), and an insert-mode body
+# click re-pins the cursor but stays insert. File panes keep the old behavior (click -> normal).
# The mode is the character in each pane's layout box: `•` normal, `^` insert,
# `$` tty.
file probe.txt marker
@@ -15,14 +15,16 @@ press left 10 5
release left 10 5
stable 400 5000
snap tty-bodyclick
-# tag edit from tty: the box still reads `$` while the tag is being edited (the
-# body mode is PARKED, which is the invariant), and Esc hands it back — a bug
-# that lost it would show `•` in one snap or the other
+# tag edit from tty: the box still reads `$` while the tag is typed into (the
+# tag's mode is its own), and Esc then `:` hands the keyboard back — a bug
+# that lost the body's mode would show `•` in one snap or the other
press left @Tty 3
release left @Tty 3
stable 400 5000
snap tty-tagedit
key esc
+settle 100
+key :
stable 400 5000
snap tty-tagexit
# out of tty, into insert: a body click moves the cursor but keeps INS