detached 1cc654e

Recognize Esc and other control keys in kitty keyboard mode

Enabling the kitty keyboard protocol for Shift+Enter made terminals
report Esc as CSI 27 u, which the CSI-u parser dropped as KeyUnknown,
so Esc stopped aborting the agent. Map kitty control codepoints
(Esc=27, Tab=9, Backspace=127/8) back to their dedicated keys.
commit
1cc654ebbfceb15f4f56e61011d1a02ea1825a10
author
patriceckhart · · 2026-06-16 05:46 UTC
parent
ab7fb37
signature
unsigned
refs
tag: v0.2.34
2 files · +32 -0
M packages/tui/input.go +13 -0
@@ -342,9 +342,22 @@ func keyFromModifiedCode(code, mod int) (Key, bool) {
342342 shift := bits&1 != 0
343343 alt := bits&2 != 0
344344 ctrl := bits&4 != 0
345+ // Kitty keyboard protocol (CSI ... u) reports control keys as their
346+ // codepoints: Esc=27, Enter=13, Tab=9, Backspace=127. Without the
347+ // enhanced-mode handling these arrive as raw bytes; with it enabled
348+ // they come through here, so map them back to their dedicated keys.
345349 switch code {
346350 case 13:
347351 return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl}, true
352+ case 27:
353+ return Key{Kind: KeyEsc, Shift: shift, Alt: alt, Ctrl: ctrl}, true
354+ case 9:
355+ if shift {
356+ return Key{Kind: KeyShiftTab, Alt: alt, Ctrl: ctrl}, true
357+ }
358+ return Key{Kind: KeyTab, Shift: shift, Alt: alt, Ctrl: ctrl}, true
359+ case 127, 8:
360+ return Key{Kind: KeyBackspace, Shift: shift, Alt: alt, Ctrl: ctrl}, true
348361 }
349362 if ctrl {
350363 switch code {
@@ -342,9 +342,22 @@ func keyFromModifiedCode(code, mod int) (Key, bool) {
342 shift := bits&1 != 0342 shift := bits&1 != 0
343 alt := bits&2 != 0343 alt := bits&2 != 0
344 ctrl := bits&4 != 0344 ctrl := bits&4 != 0
345+ // Kitty keyboard protocol (CSI ... u) reports control keys as their
346+ // codepoints: Esc=27, Enter=13, Tab=9, Backspace=127. Without the
347+ // enhanced-mode handling these arrive as raw bytes; with it enabled
348+ // they come through here, so map them back to their dedicated keys.
345 switch code {349 switch code {
346 case 13:350 case 13:
347 return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl}, true351 return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl}, true
352+ case 27:
353+ return Key{Kind: KeyEsc, Shift: shift, Alt: alt, Ctrl: ctrl}, true
354+ case 9:
355+ if shift {
356+ return Key{Kind: KeyShiftTab, Alt: alt, Ctrl: ctrl}, true
357+ }
358+ return Key{Kind: KeyTab, Shift: shift, Alt: alt, Ctrl: ctrl}, true
359+ case 127, 8:
360+ return Key{Kind: KeyBackspace, Shift: shift, Alt: alt, Ctrl: ctrl}, true
348 }361 }
349 if ctrl {362 if ctrl {
350 switch code {363 switch code {
M packages/tui/input_test.go +19 -0
@@ -30,6 +30,25 @@ func TestReaderParsesModifyOtherKeysCtrlC(t *testing.T) {
3030 }
3131 }
3232
33+func TestReaderParsesCSIUEsc(t *testing.T) {
34+ k := readKey(t, "\x1b[27u")
35+ if k.Kind != KeyEsc {
36+ t.Fatalf("Read kind=%v, want esc", k.Kind)
37+ }
38+}
39+
40+func TestReaderParsesCSIUTabAndBackspace(t *testing.T) {
41+ if k := readKey(t, "\x1b[9u"); k.Kind != KeyTab {
42+ t.Fatalf("Read kind=%v, want tab", k.Kind)
43+ }
44+ if k := readKey(t, "\x1b[9;2u"); k.Kind != KeyShiftTab {
45+ t.Fatalf("Read kind=%v, want shift-tab", k.Kind)
46+ }
47+ if k := readKey(t, "\x1b[127u"); k.Kind != KeyBackspace {
48+ t.Fatalf("Read kind=%v, want backspace", k.Kind)
49+ }
50+}
51+
3352 func TestReaderParsesSGRMouseWheel(t *testing.T) {
3453 cases := []struct {
3554 seq string
@@ -30,6 +30,25 @@ func TestReaderParsesModifyOtherKeysCtrlC(t *testing.T) {
30 }30 }
31 }31 }
32 32
33+func TestReaderParsesCSIUEsc(t *testing.T) {
34+ k := readKey(t, "\x1b[27u")
35+ if k.Kind != KeyEsc {
36+ t.Fatalf("Read kind=%v, want esc", k.Kind)
37+ }
38+}
39+
40+func TestReaderParsesCSIUTabAndBackspace(t *testing.T) {
41+ if k := readKey(t, "\x1b[9u"); k.Kind != KeyTab {
42+ t.Fatalf("Read kind=%v, want tab", k.Kind)
43+ }
44+ if k := readKey(t, "\x1b[9;2u"); k.Kind != KeyShiftTab {
45+ t.Fatalf("Read kind=%v, want shift-tab", k.Kind)
46+ }
47+ if k := readKey(t, "\x1b[127u"); k.Kind != KeyBackspace {
48+ t.Fatalf("Read kind=%v, want backspace", k.Kind)
49+ }
50+}
51+
33 func TestReaderParsesSGRMouseWheel(t *testing.T) {52 func TestReaderParsesSGRMouseWheel(t *testing.T) {
34 cases := []struct {53 cases := []struct {
35 seq string54 seq string

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close