From 6560da36cf8111575cd3fec443a42de6eff82f5f Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 15 Sep 2026 17:06:27 -0300 Subject: Cycle pane input modes with Mode and retain terminal tag access --- src/pardes.zig | 119 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 39 deletions(-) (limited to 'src/pardes.zig') 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; -- cgit v1.3