diff --git a/screen.go b/screen.go index 1975520..592c524 100644 --- a/screen.go +++ b/screen.go @@ -110,8 +110,20 @@ func tcellEventToEvent(tev tcell.Event) Event { } } - // Special case: space must be sent as KeySpace with Ch=0 - // to match the convention expected by doAcceptChar + // Ctrl+Space via CSI u: tcell delivers KeyRune with + // rune=' ' and ModCtrl. Convert to KeyCtrlSpace (0x00). + if r == ' ' && mod&keyseq.ModCtrl != 0 { + mod &^= keyseq.ModCtrl + return Event{ + Type: EventKey, + Key: keyseq.KeyCtrlSpace, + Ch: 0, + Mod: mod, + } + } + + // Plain space must be sent as KeySpace with Ch=0 + // to match the convention expected by doAcceptChar. if r == ' ' { return Event{ Type: EventKey, @@ -138,17 +150,17 @@ func tcellEventToEvent(tev tcell.Event) Event { } } - // Ctrl+letter keys: tcell.KeyCtrlA(65)..KeyCtrlZ(90). + // Ctrl keys: tcell.KeyCtrlSpace(64)..KeyCtrlZ(90). // On terminals with enhanced keyboard protocols (CSI u / // fixterms), tcell normalizes Ctrl+letter to these constants // with ModCtrl set. Peco's keyseq system encodes the ctrl - // nature in the key value (0x01-0x1A), not in the modifier, + // nature in the key value (0x00-0x1A), not in the modifier, // so strip the redundant ModCtrl. (issue #715) - if key >= tcell.KeyCtrlA && key <= tcell.KeyCtrlZ { + if key >= tcell.KeyCtrlSpace && key <= tcell.KeyCtrlZ { mod &^= keyseq.ModCtrl return Event{ Type: EventKey, - Key: keyseq.KeyType(key - tcell.KeyCtrlA + 1), + Key: keyseq.KeyType(key - tcell.KeyCtrlSpace), Ch: 0, Mod: mod, } diff --git a/screen_test.go b/screen_test.go index fca194b..8f36ae1 100644 --- a/screen_test.go +++ b/screen_test.go @@ -538,6 +538,62 @@ func TestTcellEventToEventCtrlKeysStripModCtrl(t *testing.T) { } } +// TestTcellEventToEventCtrlSpace verifies that Ctrl+Space is correctly +// converted to KeyCtrlSpace (0x00) regardless of how the terminal reports it. +// +// Traditional terminals send NUL (0x00); tcell's input handler +// delivers this as KeyCtrlSpace(64) with ModCtrl. +// The KeyCtrlSpace..KeyCtrlZ path handles this. +// +// CSI u / enhanced terminals report Ctrl+Space as KeyRune with rune=' ' +// and ModCtrl. This must also produce Key=KeyCtrlSpace, Mod=ModNone +// so that the ToggleSelectionAndSelectNext action fires correctly. +func TestTcellEventToEventCtrlSpace(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + tcellEv *tcell.EventKey + wantKey keyseq.KeyType + wantCh rune + wantMod keyseq.ModifierKey + }{ + { + // Traditional terminal: NUL byte (0x00). + // tcell's input handler calls + // NewEventKey(KeyCtrlSpace+Key(r), 0, ModCtrl) + // which yields key=KeyCtrlSpace(64), mod=ModCtrl. + // The KeyCtrlSpace..KeyCtrlZ path strips ModCtrl and + // maps to keyseq.KeyCtrlSpace(0x00). + name: "traditional terminal: NUL byte via KeyCtrlSpace+ModCtrl", + tcellEv: tcell.NewEventKey(tcell.KeyCtrlSpace, 0, tcell.ModCtrl), + wantKey: keyseq.KeyCtrlSpace, + wantCh: 0, + wantMod: keyseq.ModNone, + }, + { + // CSI u / enhanced terminal: Ctrl+Space reported as + // KeyRune with rune=' ' and ModCtrl. + name: "CSI u terminal: Ctrl+Space as rune with ModCtrl", + tcellEv: tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModCtrl), + wantKey: keyseq.KeyCtrlSpace, + wantCh: 0, + wantMod: keyseq.ModNone, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := tcellEventToEvent(tt.tcellEv) + require.Equal(t, EventKey, got.Type, "event type") + require.Equal(t, tt.wantKey, got.Key, "key") + require.Equal(t, tt.wantCh, got.Ch, "ch") + require.Equal(t, tt.wantMod, got.Mod, "modifier") + }) + } +} + // TestTcellEventToEventCtrlWithAltPreservesAlt verifies that Ctrl+Alt // combinations strip ModCtrl but preserve ModAlt. func TestTcellEventToEventCtrlWithAltPreservesAlt(t *testing.T) {