diff --git a/KEYMAP.md b/KEYMAP.md index ae7f4c7..72bf4e7 100644 --- a/KEYMAP.md +++ b/KEYMAP.md @@ -121,14 +121,18 @@ selection entry, leader rails, tool commands, save/quit, and recoverability. Text-entry mode for ordinary typing. `mim` starts in Normal mode; Insert mode is entered explicitly with `i`, `a`, `o`, or another visible command. In Insert mode, `Space` commits a literal space immediately and `Tab` inserts spaces by default -(expandtab, matching the dotfiles Neovim baseline). Physical keyboards may use -conventional direct keys such as Escape as aliases back to Normal, but those -aliases are optional accelerators, not required controls. +(expandtab, matching the dotfiles Neovim baseline). Escape returns to Normal; +Ctrl-[ is the same terminal byte and is documented as the Ctrl-capable SSH alias. +This preserves ordinary spaces and avoids reviving a Space-prefix escape chord. +If a client exposes neither Esc nor Ctrl-[, it is missing the mobile SSH +accelerator tier required for safe Insert-mode recovery; use the client's Esc +toolbar or an attached-keyboard path rather than overloading literal text input. ### Select mode -Selection-building mode. Movement extends the active selection by default. -Vertical movement preserves a preferred cursor column across ragged lines, +Selection-building mode. Escape or Ctrl-[ returns to Normal. Movement extends the +active selection by default. Vertical movement preserves a preferred cursor +column across ragged lines, clamping only to the current line length until horizontal/editing motion resets that preference. Semantic object selections live behind the match/object rail (`m w`, `m l`, `m i`, `m p`, etc.) so bare `h/j/k/l`, word motions, arrows, and diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 6e8ee79..5c55fb1 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -27,6 +27,7 @@ Rows are redgate TSV requirements: `ringidsummary [tag]`. 1 014 Normal-mode dot repeat SHALL replay the last deterministic local edit for the supported insert/change/delete/put subset without recording panel, search, or external side effects. [mobile] 1 015 The keymap SHALL classify bindings into core mobile, mobile SSH accelerator, and attached-keyboard tiers so Esc/Ctrl/arrow-capable clients can be used without making those keys mandatory for core commands. [mobile] 1 016 Every attached-keyboard or Ctrl/Esc accelerator that performs a core editing, navigation, recovery, or panel action SHALL have a documented visible mobile fallback or rail path. [mobile] +1 017 Insert and Select mode SHALL return to Normal via Escape and Ctrl-[ as equivalent terminal events while preserving literal Insert-mode Space. [mobile] ## ui diff --git a/src/input.zig b/src/input.zig index e72cd84..0cd154f 100644 --- a/src/input.zig +++ b/src/input.zig @@ -290,3 +290,15 @@ test "adversarial: replay names first event mismatch" { .ok => return error.ExpectedTraceDivergence, } } + +test "regular: Ctrl-[ normalizes as escape" { + try std.testing.expectEqual(Event{ .key = .escape }, normalize("\x1b")); +} + +test "regular: Ctrl-[ trace labels as escape" { + var recorder = TraceRecorder.init(std.testing.allocator); + defer recorder.deinit(); + + try recorder.capture("\x1b"); + try std.testing.expect(std.mem.indexOf(u8, recorder.text(), "escape") != null); +} diff --git a/tools/terminal_e2e.py b/tools/terminal_e2e.py index 18beb62..372ece6 100755 --- a/tools/terminal_e2e.py +++ b/tools/terminal_e2e.py @@ -542,6 +542,7 @@ SCENARIOS = [ Scenario("attached-existing-medium", 72, 18, "existing", "attached-keyboard", "insert-normal", "truecolor", "file", setup_existing, ATTACHED_SAVE_QUIT, "alpha!\nbeta", ("1│", "2│", "☻"), max_key_events=8), Scenario("new-file-mono-narrow", 40, 12, "new", "ios-default-qwertz-space-path", "normal-symbol-save", "mono", "file", setup_new, MOBILE_SYMBOL_SAVE_QUIT, "/", ("1│", "☻", "mode:normal")), Scenario("esc-attached-key-mode-switch", 56, 14, "empty", "attached-keyboard", "insert-normal", "truecolor", "file", setup_empty, (b"i", b"e", b"s", b"c", b"\x1b", b" ", b"w", b" ", b"q"), "esc", ("1│", "☻", "mode:normal")), + Scenario("ctrl-bracket-mode-switch", 56, 14, "empty", "attached-keyboard", "insert-normal", "truecolor", "file", setup_empty, (b"i", b"c", b"t", b"r", b"l", b"\x1b", b" ", b"w", b" ", b"q"), "ctrl", ("1│", "☻", "mode:normal"), forbidden_raw=("^[",)), Scenario("attached-cursor-left-insert", 56, 14, "empty", "attached-keyboard", "insert-cursor", "truecolor", "file", setup_empty, (b"i", b"a", b"b", b"\x1b[D", b"X", b"\x1b", b" ", b"w", b" ", b"q"), "aXb", ("1│", "☻", "aX", "mode:normal")), Scenario("normal-A-append", 56, 14, "existing", "attached-keyboard", "append-eol", "truecolor", "file", setup_existing, (b"A", b"!", b"\x1b", b" ", b"w", b" ", b"q"), "alpha!\nbeta", ("1│", "☻", "alpha")), Scenario("insert-enter-indent", 56, 14, "indented", "attached-keyboard", "newline-indent", "truecolor", "file", setup_indented, (b"A", b"\r", b"x", b"\x1b", b" ", b"w", b" ", b"q"), " item\n x", ("1│", "2│", "☻")),