diff options
| -rw-r--r-- | docs/config.md | 2 | ||||
| -rw-r--r-- | docs/helix-keys.md | 4 | ||||
| -rw-r--r-- | docs/tags.md | 14 | ||||
| -rw-r--r-- | docs/ui-review.md | 10 | ||||
| -rw-r--r-- | src/builtins.zig | 7 | ||||
| -rw-r--r-- | src/config.zig | 3 | ||||
| -rw-r--r-- | src/pardes.zig | 119 | ||||
| -rw-r--r-- | test/appearance_gallery.py | 4 | ||||
| -rw-r--r-- | test/mode.zig | 114 | ||||
| -rw-r--r-- | test/panes.zig | 1 | ||||
| -rw-r--r-- | test/tag_cleanup.py | 23 |
11 files changed, 238 insertions, 63 deletions
diff --git a/docs/config.md b/docs/config.md index 42163f2b..2474c6b0 100644 --- a/docs/config.md +++ b/docs/config.md @@ -320,7 +320,7 @@ can set `PARDES_V9FS_HELPER` to its absolute path. See [v9fs.md](v9fs.md). Ctrl-B switches between raw TTY and editor mode. Plain Escape at a detected shell prompt hops back to the previous pane. Other keys, including Ctrl-O, Ctrl-W, paste shortcuts, and modified Escape belong -to the child. Use `Togglettymode` in the pane tag to return to editor +to the child. Use `Mode` in the pane tag to return to editor mode in place. Desktop paste events still feed the terminal. `Font` and `FontSel` exist ONLY in the SDL GUI and native macOS builds — a diff --git a/docs/helix-keys.md b/docs/helix-keys.md index 7675e73b..9dc86ee2 100644 --- a/docs/helix-keys.md +++ b/docs/helix-keys.md @@ -426,9 +426,9 @@ pinned to Spaces(4), smart-tab off, and the buffer-setup transaction committed as its own undo revision. Build: `cargo build --release -p helix-term --features helix-term/integration --bin hx-harness`. -Raw TTY input keeps Ctrl-b for `Togglettymode` and unmodified Escape at a detected shell +Raw TTY input keeps Ctrl-b for the terminal/editor toggle and unmodified Escape at a detected shell prompt for `Last`. Other Ctrl/Alt chords, modified Escape, and clipboard shortcut -keys go to the child. Use the `Togglettymode` tag to leave raw input in place. +keys go to the child. Use `Mode` in the tag to leave raw input in place. Completed mouse rectangles keep their source text when jump navigation scrolls the pane. Wrapped pieces stay separate copied rows. Changing file content or diff --git a/docs/tags.md b/docs/tags.md index 0e0fb85d..20c8c67a 100644 --- a/docs/tags.md +++ b/docs/tags.md @@ -68,11 +68,15 @@ tail, without displaying the current tint state. `PdfFit` (`SPC t z`) remains available, as do the shortcuts for `PdfTint` (`SPC t i`) and `PdfSections` (`SPC t s`, or `f` on a PDF). -Terminal tags include `Togglettymode`, which toggles between normal editor mode and raw -terminal input. Ctrl-B enters terminal input from editor mode; raw TTY mode -forwards it to the child. The tag command also works while editing -the tag: the command leaves tag editing and toggles the parked body mode. -Executing it on a non-terminal pane does nothing. +Terminal tags include `Mode`, which cycles through raw terminal input, normal +editor mode, insert mode, and back to terminal input. On files and text output +panes, `Mode` cycles between normal and insert mode; it is available as a command +but does not appear in their default tags. Images and PDFs keep their normal mode. +Executing `Mode` from a tag leaves the tag and advances the parked body mode. + +Ctrl-B keeps its terminal/editor toggle. Existing custom tags can still use +`Togglettymode` for that two-way terminal toggle. Old default terminal tags +upgrade to `Mode`; customized command text is preserved. ## Saved workspaces diff --git a/docs/ui-review.md b/docs/ui-review.md index e43ef9b7..2438f2ef 100644 --- a/docs/ui-review.md +++ b/docs/ui-review.md @@ -41,8 +41,8 @@ sent to the user's session. ## Compact rails, classic palettes and workspace pets `New` now belongs to column tags only. Pane tags keep their local save, -terminal and close actions: `Tty` opens a terminal, and `Togglettymode` -switches editor/raw input using the same transition as Ctrl-B. Historical +terminal and close actions: `Tty` opens a terminal, and `Mode` +cycles raw input, normal editing and insert mode. Historical default pane tails upgrade during Restore; explicitly customized text remains owned by the user. @@ -85,10 +85,10 @@ the native contrast tests. PDFs now use `filename.pdf [page/total] Tty Del PdfSections PdfTint Collapse`, keeping sections and tint commands visible without the long generated -control/status prefix. Terminal tags add `Togglettymode`, sharing Ctrl-B's actual mode -transition, including when invoked from an edited tag. +control/status prefix. Terminal tags include `Mode`, which cycles the body mode +even when invoked from an edited tag. Ctrl-B keeps its two-way toggle. -`test/tag_cleanup.py` verifies the compact PDF tag, middle-click `Togglettymode`, Ctrl-B, +`test/tag_cleanup.py` verifies the compact PDF tag, middle-click `Mode`, Ctrl-B, and tag-edit/Tab execution through real host input and 9P observations on both SDL and TTY. The PDF source stays unchanged. The full unit suites pass 773 TTY tests and 797 SDL tests; SDL image/PDF and Kitty PDF rendering harnesses pass. diff --git a/src/builtins.zig b/src/builtins.zig index 765fa93c..a21a20c4 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -463,6 +463,13 @@ pub const Filter = struct { } }; +/// Cycle the pane's input modes: terminals add raw tty mode to normal/insert. +pub const Mode = struct { + pub fn run(c: Ctx) void { + c.p.cycleMode(c.id); + } +}; + /// Switch this terminal between raw program input and normal editor mode. pub const Togglettymode = struct { pub fn run(c: Ctx) void { diff --git a/src/config.zig b/src/config.zig index 7f0a7715..f91d95c6 100644 --- a/src/config.zig +++ b/src/config.zig @@ -43,6 +43,7 @@ pub const leader_path = paths: { .Subtypes = "lT", .Del = "d", .Filter = null, + .Mode = null, .Togglettymode = null, .Collapse = null, .Kill = null, @@ -225,7 +226,7 @@ pub const topbar_str = "Newcol Joincol Find Grep Help Changelog Tutor Dump NextC pub const pane_builtins_str = "Tty Del Collapse"; pub const pdf_pane_builtins_str = "Tty Del PdfSections PdfTint Collapse"; pub const file_pane_builtins_str = "Save Tty Del Collapse"; -pub const terminal_pane_builtins_str = "Tty Save Del Togglettymode Filter Collapse"; +pub const terminal_pane_builtins_str = "Tty Save Del Mode Filter Collapse"; // Each mode badge is one codepoint. pub const tag_normal = " "; diff --git a/src/pardes.zig b/src/pardes.zig index 90658589..91605d38 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -291,6 +291,7 @@ const pre_collapse_file_pane_tail = " Save Tty Del"; const pre_collapse_pdf_pane_tail = " Tty Del PdfSections PdfTint"; const pre_collapse_terminal_pane_tail = " Save Tty Del Togglettymode Filter"; const pre_tty_first_terminal_pane_tail = " Save Tty Del Togglettymode Filter Collapse"; +const pre_mode_terminal_pane_tail = " Tty Save Del Togglettymode Filter Collapse"; const Builtin = builtins.registry.Builtin(); @@ -1003,7 +1004,7 @@ test "pane-tag Exec prefers Tty and argument builtins before shell fallback" { p.acknowledgeShell(0, "/bin/sh", false); const pane = p.panes[0].?; - try std.testing.expectEqualStrings(" Save Tty Del Togglettymode Filter Collapse", Pardes.curTail(pane)); + try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(pane)); const tag_x = p.rects[0].x + config.GUTTER; const tag_y = p.rects[0].y; @@ -1201,7 +1202,9 @@ test "Togglettymode builtin and keyboard chord share mode transitions including const p = try Pardes.init(std.testing.allocator, .{ .cols = 120, .rows = 20, .tty_only = true }); defer p.deinit(); const pane = p.panes[0].?; - try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Togglettymode ") != null); + try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Mode ") != null); + p.seedTail(pane); + try std.testing.expect(pane.appendTag(" Togglettymode")); try std.testing.expectEqual(Pane.Mode.tty, pane.mode); try std.testing.expect(p.executeBuiltinLine(0, "Togglettymode")); try std.testing.expectEqual(Pane.Mode.normal, pane.mode); @@ -1288,7 +1291,7 @@ test "compact tag commands stay beside their own prefix across layout changes" { defer gpa.free(above_tag); const below_tag = try p.tagText(p.scratch.allocator(), below); try std.testing.expect(below_tag.len > above_tag.len); - const tail = " Save Tty Del Togglettymode Filter Collapse"; + const tail = terminal_pane_tail; try std.testing.expectEqualStrings(tail, above_tag[above_tag.len - tail.len ..]); try std.testing.expectEqual(above.cwdSlice().len + tail.len, above_tag.len); @@ -1324,14 +1327,14 @@ test "legacy default tag tails upgrade while custom tails remain owned" { }); p.restoreTail(terminal, terminal_old); try std.testing.expect(!terminal.tag_init); - try std.testing.expectEqualStrings(" Tty Save Del Togglettymode Filter Collapse", Pardes.curTail(terminal)); + try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal)); const terminal_previous = try std.fmt.allocPrint(p.scratch.allocator(), "{s} New Del", .{ try p.tagPrefix(terminal), }); p.restoreTail(terminal, terminal_previous); try std.testing.expect(!terminal.tag_init); - try std.testing.expectEqualStrings(" Tty Save Del Togglettymode Filter Collapse", Pardes.curTail(terminal)); + try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal)); const terminal_prefilter = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{ try p.tagPrefix(terminal), @@ -1339,8 +1342,21 @@ test "legacy default tag tails upgrade while custom tails remain owned" { }); p.restoreTail(terminal, terminal_prefilter); try std.testing.expect(!terminal.tag_init); - try std.testing.expectEqualStrings(" Tty Save Del Togglettymode Filter Collapse", Pardes.curTail(terminal)); + try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal)); + + for ([_][]const u8{ pre_mode_terminal_pane_tail, pre_tty_first_terminal_pane_tail, pre_collapse_terminal_pane_tail }) |old_tail| { + const old = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{ try p.tagPrefix(terminal), old_tail }); + p.restoreTail(terminal, old); + try std.testing.expect(!terminal.tag_init); + try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal)); + } + const custom_mode = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s} Keep", .{ try p.tagPrefix(terminal), pre_mode_terminal_pane_tail }); + p.restoreTail(terminal, custom_mode); + try std.testing.expect(terminal.tag_init); + try std.testing.expectEqualStrings(pre_mode_terminal_pane_tail ++ " Keep", Pardes.curTail(terminal)); + terminal.tag_tail_len = 0; + terminal.tag_init = false; const terminal_custom = try std.fmt.allocPrint(p.scratch.allocator(), "{s} Keep Del", .{ try p.tagPrefix(terminal), }); @@ -1368,7 +1384,7 @@ test "legacy default tag tails upgrade while custom tails remain owned" { .terminal = .{ .cwd = "/historical/cwd" }, }); try std.testing.expect(!terminal.tag_init); - try std.testing.expectEqualStrings(" Tty Save Del Togglettymode Filter Collapse", Pardes.curTail(terminal)); + try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal)); // This happens to be the historical FILE default, but on a terminal it is // user-owned text and must not be swallowed by migration. @@ -2211,7 +2227,7 @@ test "Save on an output buffer writes its rows out and leaves the buffer alone" defer gpa.free(rows); // Save leads its tagline now, and the path is REQUIRED: a bare Save asks - try std.testing.expectEqualStrings(" Save Tty Del Collapse", Pardes.curTail(out)); + try std.testing.expectEqualStrings(file_pane_tail, Pardes.curTail(out)); try std.testing.expect(p.executeBuiltinLine(id, "Save")); try std.testing.expect(out.prompt == .save); try std.testing.expect(out.appendTag("help.txt")); @@ -2399,7 +2415,7 @@ test "Save reaches every tagline with text behind it and no other" { const out = try p.setTestFile("build.zig:1:1 pub fn main\n"); out.file.?.output = .{ .from = .search }; - try std.testing.expectEqualStrings(" Save Tty Del Collapse", Pardes.curTail(out)); + try std.testing.expectEqualStrings(locations_pane_tail, Pardes.curTail(out)); // A dump written before Save reached output buffers carries the generic // tail. That is a DEFAULT and not something typed, so it upgrades. @@ -2409,7 +2425,7 @@ test "Save reaches every tagline with text behind it and no other" { }); p.restoreTail(out, dumped); try std.testing.expect(!out.tag_init); - try std.testing.expectEqualStrings(" Save Tty Del Collapse", Pardes.curTail(out)); + try std.testing.expectEqualStrings(locations_pane_tail, Pardes.curTail(out)); // A scratch is an output buffer that has worn the FILE tails all along, // so its own older default upgrades from that family too. @@ -7557,7 +7573,9 @@ pub const Pardes = struct { const cmd = if (hit(key, config.look_key)) config.look_cmd else config.exec_cmd; const txt = p.tagChordText(pane) orelse return; const id = p.active; + const body_mode = pane.tag_mode; exitTagEdit(pane); // leave the tag FIRST: Del frees the pane + pane.mode = body_mode; p.runBuiltin(cmd, id, "", txt); return; } @@ -8135,6 +8153,56 @@ pub const Pardes = struct { return std.fmt.allocPrint(p.scratch.allocator(), "{s} {s}", .{ t, a }) catch txt; } + /// Cycle only the body modes supported by this pane. + pub fn cycleMode(p: *Pardes, id: usize) void { + const pane = p.panes[id] orelse return; + if (pane.file == null and !pane.isTerminal()) return; + const previous = if (pane.tag_edit) pane.tag_mode else pane.mode; + p.active = id; + p.exitHeader(); + if (pane.tag_edit) exitTagEdit(pane); + pane.mode = previous; + switch (previous) { + .tty => { + pane.mode = .normal; + pane.normal.clear(); + }, + .normal => p.enterInsert(pane, .at, 1), + .insert => { + p.exitInsert(pane); + if (pane.isTerminal()) panes.Terminal.enterTty(p, id); + }, + } + } + + fn exitInsert(p: *Pardes, pane: *Pane) void { + pane.mode = .normal; + pane.msel.active = false; + pane.normal.clear(); + if (pane.append_at) |aa| { + pane.append_at = null; + const text = p.flatSurface(pane) catch return; + const gap = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.cur_row)), .col = @intCast(@max(0, pane.cur_col)) }); + const a_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, aa.row)), .col = @intCast(@max(0, aa.col)) }); + const back = @max(a_off, modal.prevGrapheme(text, gap)); + const bc = modal.positionAt(text, back); + pane.cur_row = @intCast(bc.row); + pane.cur_col = @intCast(bc.col); + pane.vsel = .{ .active = a_off != back, .row = aa.row, .col = aa.col, .explicit = false }; + pane.cur_pinned = true; + pane.ensureCursorVisible(); + for (pane.sels[0..pane.nsel]) |*s| { + const sgap = modal.offsetAt(text, .{ .row = @intCast(@max(0, s.row)), .col = @intCast(@max(0, s.col)) }); + const b2 = if (back == gap) sgap else modal.prevGrapheme(text, sgap); + const bp = modal.positionAt(text, b2); + s.row = @intCast(bp.row); + s.col = @intCast(bp.col); + s.arow = s.row; + s.acol = s.col; + } + } + } + /// One mode transition for the terminal tag command and the Ctrl-B chord. pub fn toggleTty(p: *Pardes, id: usize) void { const pane = p.panes[id] orelse return; @@ -8258,34 +8326,7 @@ pub const Pardes = struct { p.handleNormal(pane, key); }, .insert => { - if (hit(key, config.escape)) { - pane.mode = .normal; - pane.msel.active = false; - pane.normal.clear(); - if (pane.append_at) |aa| { - pane.append_at = null; - const text = p.flatSurface(pane) catch return; - const gap = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.cur_row)), .col = @intCast(@max(0, pane.cur_col)) }); - const a_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, aa.row)), .col = @intCast(@max(0, aa.col)) }); - const back = @max(a_off, modal.prevGrapheme(text, gap)); - const bc = modal.positionAt(text, back); - pane.cur_row = @intCast(bc.row); - pane.cur_col = @intCast(bc.col); - pane.vsel = .{ .active = a_off != back, .row = aa.row, .col = aa.col, .explicit = false }; - pane.cur_pinned = true; - pane.ensureCursorVisible(); - for (pane.sels[0..pane.nsel]) |*s| { - const sgap = modal.offsetAt(text, .{ .row = @intCast(@max(0, s.row)), .col = @intCast(@max(0, s.col)) }); - const b2 = if (back == gap) sgap else modal.prevGrapheme(text, sgap); - const bp = modal.positionAt(text, b2); - s.row = @intCast(bp.row); - s.col = @intCast(bp.col); - s.arow = s.row; - s.acol = s.col; - } - } - return; - } + if (hit(key, config.escape)) return p.exitInsert(pane); p.handleInsert(pane, key); }, .tty => panes.Terminal.forwardKey(p, p.active, key), @@ -12813,7 +12854,7 @@ pub const Pardes = struct { pre_collapse_file_pane_tail, pre_collapse_pane_tail, }, - .terminal => &.{ terminal_pane_tail, pre_tty_first_terminal_pane_tail, pre_collapse_terminal_pane_tail, pre_column_terminal_pane_tail, pre_tty_terminal_pane_tail, prev_terminal_pane_tail, legacy_terminal_pane_tail, pane_tail, pre_collapse_pane_tail, pre_column_pane_tail, prev_pane_tail, legacy_pane_tail }, + .terminal => &.{ terminal_pane_tail, pre_mode_terminal_pane_tail, pre_tty_first_terminal_pane_tail, pre_collapse_terminal_pane_tail, pre_column_terminal_pane_tail, pre_tty_terminal_pane_tail, prev_terminal_pane_tail, legacy_terminal_pane_tail, pane_tail, pre_collapse_pane_tail, pre_column_pane_tail, prev_pane_tail, legacy_pane_tail }, }; for (defaults) |default| if (std.mem.eql(u8, said, std.mem.trimStart(u8, default, " "))) return; diff --git a/test/appearance_gallery.py b/test/appearance_gallery.py index c80d1a33..7779efa9 100644 --- a/test/appearance_gallery.py +++ b/test/appearance_gallery.py @@ -17,7 +17,7 @@ NOTES = b'''Pardes / workspace review New belongs to the column. Tty opens a terminal. -Togglettymode switches its input mode. +Mode switches its input mode. Font MartianMono-NrRg:18 TaglineSize 80 @@ -106,7 +106,7 @@ def run(args): for serial, tag in panes(client).items(): require(' New ' not in tag, 'pane still contains New') require('Newtty' not in tag, 'old terminal name remains') - require('Togglettymode' in panes(client)[terminal], 'terminal mode action missing') + require('Mode' in panes(client)[terminal], 'terminal mode action missing') def owned_window(): result = subprocess.run(['xdotool', 'search', '--onlyvisible', '--pid', str(handles['pid'])], diff --git a/test/mode.zig b/test/mode.zig new file mode 100644 index 00000000..6d694b42 --- /dev/null +++ b/test/mode.zig @@ -0,0 +1,114 @@ +const std = @import("std"); +const pardes = @import("pardes"); +const panes = pardes.panes; +const Pardes = pardes.Pardes; + +fn mode(p: *Pardes, id: usize, expected: pardes.Pane.Mode) !void { + try std.testing.expect(p.executeBuiltinLine(id, "Mode")); + try std.testing.expectEqual(expected, p.panes[id].?.mode); + try std.testing.expect(!p.panes[id].?.tag_edit); +} + +fn clickModeTag(p: *Pardes) !void { + const pane = p.panes[0].?; + const text = try p.tagText(p.scratch.allocator(), pane); + const col = std.mem.indexOf(u8, text, "Mode") orelse return error.MissingModeTag; + const rect = p.rects[0]; + 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.config.GUTTER + @as(u16, @intCast(col)), .row = row } }); + try std.testing.expect(pane.tag_edit); +} + +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; +} + +test "Mode cycles terminal modes and keeps legacy toggle and custom tags" { + if (comptime !panes.Terminal.enabled) return error.SkipZigTest; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100 }); + defer p.deinit(); + p.presentation.enabled = false; + while (p.nextEffect()) |_| {} + const pane = p.panes[0].?; + try std.testing.expectEqual(pardes.Pane.Mode.tty, pane.mode); + 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); + try clickModeTag(p); + try std.testing.expectEqual(pardes.Pane.Mode.insert, pane.tag_mode); + p.update(.{ .key = .{ .cp = pardes.Key.tab } }); + try std.testing.expectEqual(pardes.Pane.Mode.tty, pane.mode); + try std.testing.expect(!pane.tag_edit); + while (p.nextEffect()) |effect| try std.testing.expect(effect != .write); + + customTag(pane, " Togglettymode Keep"); + try mode(p, 0, .normal); + try mode(p, 0, .insert); + try std.testing.expect(p.executeBuiltinLine(0, "Togglettymode")); + try std.testing.expectEqual(pardes.Pane.Mode.tty, pane.mode); + p.update(.{ .key = .{ .cp = 'b', .ctrl = true } }); + try std.testing.expectEqual(pardes.Pane.Mode.normal, pane.mode); + p.update(.{ .key = .{ .cp = 'b', .ctrl = true } }); + try std.testing.expectEqual(pardes.Pane.Mode.tty, pane.mode); + try std.testing.expectEqualStrings(" Togglettymode Keep", pane.tag_tail[0..pane.tag_tail_len]); +} + +test "Mode from actual tag Exec preserves parked file and output insert transactions" { + for ([_]bool{ false, true }) |output| { + for ([_]bool{ false, true }) |normal_tag| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100 }); + defer p.deinit(); + p.presentation.enabled = false; + 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 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(pardes.Pane.Mode.insert, pane.tag_mode); + if (normal_tag) pane.mode = .normal; + p.update(.{ .key = .{ .cp = pardes.Key.tab } }); + try std.testing.expectEqual(pardes.Pane.Mode.normal, pane.mode); + try std.testing.expect(!pane.tag_edit); + try std.testing.expect(pane.append_at == null); + p.update(.{ .key = .{ .cp = 'u' } }); + try std.testing.expectEqualStrings("ab\n", pane.file.?.content); + p.update(.{ .key = .{ .cp = 'a' } }); + p.update(.{ .key = .{ .cp = 'z', .text = "z" } }); + try std.testing.expect(pane.append_at != null); + try mode(p, 0, .normal); + try std.testing.expect(pane.append_at == null); + p.update(.{ .key = .{ .cp = 'u' } }); + try std.testing.expectEqualStrings("ab\n", pane.file.?.content); + } + } +} + +test "Mode leaves image and PDF document modes unchanged" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const picture = try panes.Image.create(p, 1, "/mode-test.png", &.{}); + try mode(p, 1, .normal); + try std.testing.expect(picture.image != null); + if (comptime panes.Pdf.enabled) { + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const bytes = try panes.Pdf.pdf.makeLinksTestPdf(std.testing.allocator); + defer std.testing.allocator.free(bytes); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "mode.pdf", .data = bytes }); + var buffer: [4096]u8 = undefined; + const len = try tmp.dir.realPath(std.testing.io, &buffer); + const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/mode.pdf", .{buffer[0..len]}); + defer std.testing.allocator.free(path); + const document = try panes.Pdf.openPane(p, 2, path, 1); + const page = document.pdf.?.page; + try mode(p, 2, .normal); + try std.testing.expectEqual(page, document.pdf.?.page); + } +} diff --git a/test/panes.zig b/test/panes.zig index ed48db82..f3e7034f 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -3929,6 +3929,7 @@ test { _ = JumpSelectionTests; _ = @import("output.zig"); _ = @import("column_drag.zig"); + _ = @import("mode.zig"); _ = @import("pdf.zig"); _ = @import("fs_namespace.zig"); _ = @import("hxdiff.zig"); diff --git a/test/tag_cleanup.py b/test/tag_cleanup.py index 81b15ad8..52ee8e9d 100644 --- a/test/tag_cleanup.py +++ b/test/tag_cleanup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Check compact PDF tags and Togglettymode/Ctrl-B parity in an isolated 9P session.""" +"""Check compact PDF tags and Mode cycling and Ctrl-B in an isolated 9P session.""" import argparse from pathlib import Path @@ -42,27 +42,34 @@ def run(args): frame = client.screen() for row in range(frame['rows']): text = ''.join(c[0] for c in frame['cells'][row * frame['cols']:(row + 1) * frame['cols']]) - if ' Togglettymode Filter' in text: - return text.index(' Togglettymode Filter') + 1, row, text[0] + if ' Mode Filter' in text: + return text.index(' Mode Filter') + 1, row, text[0] return None require(wait_for(lambda: (p := tag_position()) and p[2] == '$'), 'new terminal not in raw mode') x, y, _ = tag_position() keys.click(x, y, button=1) - require(wait_for(lambda: (p := tag_position()) and p[2] == ' '), 'Togglettymode click did not enter normal mode') + require(wait_for(lambda: (p := tag_position()) and p[2] == ' '), 'Mode click did not enter normal mode') + for badge in ('^', '$'): + x, y, _ = tag_position() + keys.click(x, y, button=1) + require(wait_for(lambda: (p := tag_position()) and p[2] == badge), 'Mode cycle did not reach ' + badge) + x, y, _ = tag_position() + keys.click(x, y, button=1) + require(wait_for(lambda: (p := tag_position()) and p[2] == ' '), 'Mode did not return to normal') keys.send(b'\x02') require(wait_for(lambda: (p := tag_position()) and p[2] == '$'), 'Ctrl-B did not restore raw mode') x, y, _ = tag_position() keys.click(x, y) keys.send(b'\t') - require(wait_for(lambda: (p := tag_position()) and p[2] == ' '), 'Togglettymode from edited tag ignored parked raw mode') + require(wait_for(lambda: (p := tag_position()) and p[2] == ' '), 'Mode from edited tag ignored parked raw mode') keys.send(b'\x02') - client.write(f'/self/pane/{tty}/pty/data', b"printf 'Togglettymode toggle ready\\n'\r") - require(wait_for(lambda: b'Togglettymode toggle ready' in client.read(f'/self/pane/{tty}/body')), 'terminal input failed') + client.write(f'/self/pane/{tty}/pty/data', b"printf 'Mode toggle ready\\n'\r") + require(wait_for(lambda: b'Mode toggle ready' in client.read(f'/self/pane/{tty}/body')), 'terminal input failed') capture(client, root, 'pdf-and-tty-tags', not args.tty) require(source.read_bytes() == original, 'PDF file changed') client.write(f'/self/pane/{tty}/pty/data', b'exit\r') - print('PDF tag, Togglettymode click, Ctrl-B, edited-tag execution and source preservation passed') + print('PDF tag, Mode click, Ctrl-B, edited-tag execution and source preservation passed') if __name__ == '__main__': |
