From 802de4f12f125abc261c58bdb5385f6be3ad469a Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 1 Oct 2026 09:43:37 -0300 Subject: Shift-Esc always backs out: in every mode and pane it goes to the previous pane, as Esc does from normal, and a raw program never sees it; Ctrl-b alone toggles raw Shift-Esc switched a terminal in normal or insert mode into raw, so the one key meant to get out of anything got a script or a person into a program instead. It is now handled before any mode: an open prompt is closed, insert mode left, a pending count dropped, and Last runs, the pane keeping its mode (a raw terminal stays raw). Ctrl-b is the only switch between raw and normal. The keymap's two chords become one, back_out; the cheatsheet row, the guide's mode table and the tutor say so, and the tty and builtins goldens are re-recorded. Co-Authored-By: Claude Opus 5.5 --- src/pardes.zig | 53 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 8 deletions(-) (limited to 'src/pardes.zig') diff --git a/src/pardes.zig b/src/pardes.zig index dbb37084..0690b7eb 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -6122,6 +6122,24 @@ pub const Pardes = struct { } const pane = p.panes[p.active] orelse return; p.noteTyped(); + // Shift-Esc backs out of whatever the pane is in -- a prompt, insert + // mode, a pending count, a raw program -- to the previous pane, as + // Esc does from normal mode. Nothing under it sees the key. + if (hit(key, config.back_out)) { + p.leader_on = false; + p.ctrl_w_pending = false; + if (pane.prompt != .none) { + pane.closePrompt(); + pane.focus = .body; + } + for ([_]*Text{ &pane.body, &pane.tag }) |t| { + if (t.mode == .insert) edit.exitInsert(p, t); + t.normal.clear(); + t.select = false; + } + tagline.exitHeader(p); + return exec.runBuiltin(p, .Last, p.active, "", null); + } // Alt-Down and Alt-Up in a pane's tag expand and collapse it, in // either mode: acme's Tagdown and Tagup (plan9port text.c:776). if (pane.focus == .tag and key.alt and !key.ctrl and (key.cp == Key.down or key.cp == Key.up) and tagline.focusedHeader(p) == null) { @@ -6135,10 +6153,8 @@ pub const Pardes = struct { p.ctrl_w_pending = false; if (key.ctrl and !key.alt and key.cp == p.opts.tty_toggle) return p.toggleTty(p.active); - // Shift-Escape is the unconditional way out, even mid-application; - // bare Escape only leaves at a shell prompt. - if (hit(key, config.tty_toggle_alt)) - return exec.runBuiltin(p, .Last, p.active, "", null); + // Bare Escape leaves only at a shell prompt (Shift-Esc, above, + // always). if (key.cp == Key.escape and !key.ctrl and !key.alt and !key.shift and exec.takesCommandLine(p, p.active)) return exec.runBuiltin(p, .Last, p.active, "", null); panes.terminal.followOutput(pane); // typing snaps back to live output @@ -6196,8 +6212,7 @@ pub const Pardes = struct { } return; } - const tty_alt = hit(key, config.tty_toggle_alt); - const tty_toggle = (key.ctrl and key.cp == p.opts.tty_toggle) or tty_alt; + const tty_toggle = key.ctrl and key.cp == p.opts.tty_toggle; if (pane.isTerminal() and tty_toggle) { return p.toggleTty(p.active); } @@ -7601,7 +7616,7 @@ test "Esc back into a tty leaves its view at the prompt" { p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = r.x + config.GUTTER + 1, .row = r.y + BOX_H } }); try std.testing.expectEqual(shell, p.active); - p.update(.{ .key = .{ .cp = Key.escape, .shift = true } }); + p.update(.{ .key = .{ .cp = 'b', .ctrl = true } }); try std.testing.expectEqual(Text.Mode.tty, sp.body.mode); // tty mode follows output to the bottom, so a screenful and a half of it @@ -8012,7 +8027,7 @@ test "tty output follows only from the bottom" { p.update(.{ .key = .{ .cp = 'n', .alt = true } }); // a shell under the doc const shell = p.active; const sp = p.panes[shell].?; - p.update(.{ .key = .{ .cp = Key.escape, .shift = true } }); + p.update(.{ .key = .{ .cp = 'b', .ctrl = true } }); try std.testing.expectEqual(Text.Mode.tty, sp.body.mode); for (0..60) |i| { @@ -8122,6 +8137,28 @@ test "raw tty Escape only leaves at a shell prompt" { try std.testing.expectEqual(Text.Mode.tty, pane.body.mode); } +test "Shift-Esc backs out to the previous pane in every mode, never into raw" { + if (platform == .web) return; + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24, .file = "mise.toml" }); + defer p.deinit(); + p.update(.{ .key = .{ .cp = 'n', .alt = true } }); // a shell under the doc + const shell = p.active; + const sp = p.panes[shell].?; + // A terminal in normal mode: out, still normal, never raw. + if (sp.body.mode == .tty) p.update(.{ .key = .{ .cp = 'b', .ctrl = true } }); + try std.testing.expectEqual(Text.Mode.normal, sp.body.mode); + p.update(.{ .key = .{ .cp = Key.escape, .shift = true } }); + try std.testing.expectEqual(@as(usize, 0), p.active); + try std.testing.expectEqual(Text.Mode.normal, sp.body.mode); + // A text pane in insert mode: out of insert, and out of the pane. + const doc = p.panes[0].?; + p.update(.{ .key = .{ .cp = 'i', .text = "i" } }); + try std.testing.expectEqual(Text.Mode.insert, doc.body.mode); + p.update(.{ .key = .{ .cp = Key.escape, .shift = true } }); + try std.testing.expectEqual(Text.Mode.normal, doc.body.mode); + try std.testing.expect(p.active != 0); +} + test "raw tty Ctrl-B switches to editor mode without sending child input" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .file = "mise.toml" }); -- cgit v1.3