From 9ed971cc90c9d65def3c52178578e43f29b68f9f Mon Sep 17 00:00:00 2001 From: Massimo Mund Date: Fri, 5 Sep 2025 07:56:51 +0200 Subject: Add keybindings for CTRL, ALT, SHIFT + UP, DOWN, RIGHT, LEFT, HOME, END, BACKSPACE, DELETE & more (#3996) * Added tests for `LightRenderer` * Added common SHIFT, ALT and ALT+SHIFT key sequences * Added common CTRL key sequences * Added common CTRL+ALT, CTRL+SHIFT, CTRL+ALT+SHIFT key sequences * Added proper xterm META modifier handling according to https://github.com/joejulian/xterm/blob/defc6dd5684a12dc8e56cb6973ef973e7a32caa3/input.c#L357-L375 * Fix `ctrl-backspace` and `ctrl-alt-backspace` * Fix broken tcell tests on windows by swallowing Resize events * Added tests for FullscreenRenderer * Removed own fork of tcell and updated tcell to 2.9.0 tcell 2.9.0 is needed for `Ctrl-Alt-*` and `Ctrl-Alt-Shift-*` shortcuts in Windows * Replace conditional checks with switch statements to improve readability * Replace long conditionals with constant slices to improve readability * Bind `ctrl-bspace` (`ctrl-h`) to `backward-delete-char` by default Since we now distinguish between Backspace and Ctrl-Backspace, Ctrl-Backspace should trigger the same action as Backspace by default. In that way nothing changes for the user but you can bind other actions to Ctrl-Backspace when desired. --- src/tui/light.go | 282 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 255 insertions(+), 27 deletions(-) (limited to 'src/tui/light.go') diff --git a/src/tui/light.go b/src/tui/light.go index 54abbe5c..ec93e67e 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -335,6 +335,8 @@ func (r *LightRenderer) GetChar() Event { return Event{CtrlQ, 0, nil} case 127: return Event{Backspace, 0, nil} + case 8: + return Event{CtrlBackspace, 0, nil} case 0: return Event{CtrlSpace, 0, nil} case 28: @@ -381,6 +383,9 @@ func (r *LightRenderer) escSequence(sz *int) Event { } *sz = 2 + if r.buffer[1] == 8 { + return Event{CtrlAltBackspace, 0, nil} + } if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 { return CtrlAltKey(rune(r.buffer[1] + 'a' - 1)) } @@ -473,22 +478,136 @@ func (r *LightRenderer) escSequence(sz *int) Event { if r.buffer[3] == '~' { return Event{Delete, 0, nil} } + if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' { + switch r.buffer[5] { + case '0': + return Event{AltShiftDelete, 0, nil} + case '1': + return Event{AltDelete, 0, nil} + case '2': + return Event{AltShiftDelete, 0, nil} + case '3': + return Event{CtrlAltDelete, 0, nil} + case '4': + return Event{CtrlAltShiftDelete, 0, nil} + case '5': + return Event{CtrlAltDelete, 0, nil} + case '6': + return Event{CtrlAltShiftDelete, 0, nil} + } + } if len(r.buffer) == 6 && r.buffer[5] == '~' { *sz = 6 switch r.buffer[4] { - case '5': - return Event{CtrlDelete, 0, nil} case '2': return Event{ShiftDelete, 0, nil} + case '3': + return Event{AltDelete, 0, nil} + case '4': + return Event{AltShiftDelete, 0, nil} + case '5': + return Event{CtrlDelete, 0, nil} + case '6': + return Event{CtrlShiftDelete, 0, nil} + case '7': + return Event{CtrlAltDelete, 0, nil} + case '8': + return Event{CtrlAltShiftDelete, 0, nil} + case '9': + return Event{AltDelete, 0, nil} } } return Event{Invalid, 0, nil} case '4': return Event{End, 0, nil} case '5': - return Event{PageUp, 0, nil} + if r.buffer[3] == '~' { + return Event{PageUp, 0, nil} + } + if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' { + switch r.buffer[5] { + case '0': + return Event{AltShiftPageUp, 0, nil} + case '1': + return Event{AltPageUp, 0, nil} + case '2': + return Event{AltShiftPageUp, 0, nil} + case '3': + return Event{CtrlAltPageUp, 0, nil} + case '4': + return Event{CtrlAltShiftPageUp, 0, nil} + case '5': + return Event{CtrlAltPageUp, 0, nil} + case '6': + return Event{CtrlAltShiftPageUp, 0, nil} + } + } + if len(r.buffer) == 6 && r.buffer[5] == '~' { + *sz = 6 + switch r.buffer[4] { + case '2': + return Event{ShiftPageUp, 0, nil} + case '3': + return Event{AltPageUp, 0, nil} + case '4': + return Event{AltShiftPageUp, 0, nil} + case '5': + return Event{CtrlPageUp, 0, nil} + case '6': + return Event{CtrlShiftPageUp, 0, nil} + case '7': + return Event{CtrlAltPageUp, 0, nil} + case '8': + return Event{CtrlAltShiftPageUp, 0, nil} + case '9': + return Event{AltPageUp, 0, nil} + } + } + return Event{Invalid, 0, nil} case '6': - return Event{PageDown, 0, nil} + if r.buffer[3] == '~' { + return Event{PageDown, 0, nil} + } + if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' { + switch r.buffer[5] { + case '0': + return Event{AltShiftPageDown, 0, nil} + case '1': + return Event{AltPageDown, 0, nil} + case '2': + return Event{AltShiftPageDown, 0, nil} + case '3': + return Event{CtrlAltPageDown, 0, nil} + case '4': + return Event{CtrlAltShiftPageDown, 0, nil} + case '5': + return Event{CtrlAltPageDown, 0, nil} + case '6': + return Event{CtrlAltShiftPageDown, 0, nil} + } + } + if len(r.buffer) == 6 && r.buffer[5] == '~' { + *sz = 6 + switch r.buffer[4] { + case '2': + return Event{ShiftPageDown, 0, nil} + case '3': + return Event{AltPageDown, 0, nil} + case '4': + return Event{AltShiftPageDown, 0, nil} + case '5': + return Event{CtrlPageDown, 0, nil} + case '6': + return Event{CtrlShiftPageDown, 0, nil} + case '7': + return Event{CtrlAltPageDown, 0, nil} + case '8': + return Event{CtrlAltShiftPageDown, 0, nil} + case '9': + return Event{AltPageDown, 0, nil} + } + } + return Event{Invalid, 0, nil} case '7': return Event{Home, 0, nil} case '8': @@ -526,63 +645,172 @@ func (r *LightRenderer) escSequence(sz *int) Event { } *sz = 6 switch r.buffer[4] { - case '1', '2', '3', '4', '5': + case '1', '2', '3', '4', '5', '6', '7', '8', '9': // Kitty iTerm2 WezTerm // SHIFT-ARROW "\e[1;2D" // ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D" // CTRL-SHIFT-ARROW "\e[1;6D" N/A // CMD-SHIFT-ARROW "\e[1;10D" N/A N/A ("\e[1;2D") - alt := r.buffer[4] == '3' + ctrl := bytes.IndexByte([]byte{'5', '6', '7', '8'}, r.buffer[4]) >= 0 + alt := bytes.IndexByte([]byte{'3', '4', '7', '8'}, r.buffer[4]) >= 0 + shift := bytes.IndexByte([]byte{'2', '4', '6', '8'}, r.buffer[4]) >= 0 char := r.buffer[5] - altShift := false - if r.buffer[4] == '1' && r.buffer[5] == '0' { - altShift = true - if len(r.buffer) < 7 { - return Event{Invalid, 0, nil} - } - *sz = 7 - char = r.buffer[6] - } else if r.buffer[4] == '4' { - altShift = true + if r.buffer[4] == '9' { + ctrl = false + alt = true + shift = false if len(r.buffer) < 6 { return Event{Invalid, 0, nil} } *sz = 6 char = r.buffer[5] + } else if r.buffer[4] == '1' && bytes.IndexByte([]byte{'0', '1', '2', '3', '4', '5', '6'}, r.buffer[5]) >= 0 { + ctrl = bytes.IndexByte([]byte{'3', '4', '5', '6'}, r.buffer[5]) >= 0 + alt = true + shift = bytes.IndexByte([]byte{'0', '2', '4', '6'}, r.buffer[5]) >= 0 + if len(r.buffer) < 7 { + return Event{Invalid, 0, nil} + } + *sz = 7 + char = r.buffer[6] } + ctrlShift := ctrl && shift + ctrlAlt := ctrl && alt + altShift := alt && shift + ctrlAltShift := ctrl && alt && shift switch char { case 'A': - if alt { - return Event{AltUp, 0, nil} + if ctrlAltShift { + return Event{CtrlAltShiftUp, 0, nil} + } + if ctrlAlt { + return Event{CtrlAltUp, 0, nil} + } + if ctrlShift { + return Event{CtrlShiftUp, 0, nil} } if altShift { return Event{AltShiftUp, 0, nil} } - return Event{ShiftUp, 0, nil} - case 'B': + if ctrl { + return Event{CtrlUp, 0, nil} + } if alt { - return Event{AltDown, 0, nil} + return Event{AltUp, 0, nil} + } + if shift { + return Event{ShiftUp, 0, nil} + } + case 'B': + if ctrlAltShift { + return Event{CtrlAltShiftDown, 0, nil} + } + if ctrlAlt { + return Event{CtrlAltDown, 0, nil} + } + if ctrlShift { + return Event{CtrlShiftDown, 0, nil} } if altShift { return Event{AltShiftDown, 0, nil} } - return Event{ShiftDown, 0, nil} - case 'C': + if ctrl { + return Event{CtrlDown, 0, nil} + } if alt { - return Event{AltRight, 0, nil} + return Event{AltDown, 0, nil} + } + if shift { + return Event{ShiftDown, 0, nil} + } + case 'C': + if ctrlAltShift { + return Event{CtrlAltShiftRight, 0, nil} + } + if ctrlAlt { + return Event{CtrlAltRight, 0, nil} + } + if ctrlShift { + return Event{CtrlShiftRight, 0, nil} } if altShift { return Event{AltShiftRight, 0, nil} } - return Event{ShiftRight, 0, nil} + if ctrl { + return Event{CtrlRight, 0, nil} + } + if shift { + return Event{ShiftRight, 0, nil} + } + if alt { + return Event{AltRight, 0, nil} + } case 'D': + if ctrlAltShift { + return Event{CtrlAltShiftLeft, 0, nil} + } + if ctrlAlt { + return Event{CtrlAltLeft, 0, nil} + } + if ctrlShift { + return Event{CtrlShiftLeft, 0, nil} + } + if altShift { + return Event{AltShiftLeft, 0, nil} + } + if ctrl { + return Event{CtrlLeft, 0, nil} + } if alt { return Event{AltLeft, 0, nil} } + if shift { + return Event{ShiftLeft, 0, nil} + } + case 'H': + if ctrlAltShift { + return Event{CtrlAltShiftHome, 0, nil} + } + if ctrlAlt { + return Event{CtrlAltHome, 0, nil} + } + if ctrlShift { + return Event{CtrlShiftHome, 0, nil} + } if altShift { - return Event{AltShiftLeft, 0, nil} + return Event{AltShiftHome, 0, nil} + } + if ctrl { + return Event{CtrlHome, 0, nil} + } + if alt { + return Event{AltHome, 0, nil} + } + if shift { + return Event{ShiftHome, 0, nil} + } + case 'F': + if ctrlAltShift { + return Event{CtrlAltShiftEnd, 0, nil} + } + if ctrlAlt { + return Event{CtrlAltEnd, 0, nil} + } + if ctrlShift { + return Event{CtrlShiftEnd, 0, nil} + } + if altShift { + return Event{AltShiftEnd, 0, nil} + } + if ctrl { + return Event{CtrlEnd, 0, nil} + } + if alt { + return Event{AltEnd, 0, nil} + } + if shift { + return Event{ShiftEnd, 0, nil} } - return Event{ShiftLeft, 0, nil} } } // r.buffer[4] } // r.buffer[3] -- cgit v1.2.3