From efe8bd5ff7d22afe46aa4d736bcc0f4dd4de1e16 Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Tue, 2 Jun 2026 14:21:30 +0200 Subject: [PATCH] fix: detect Shift+Enter on macOS terminals without Kitty protocol support macOS Terminal.app (and other legacy terminals) don't support the Kitty keyboard protocol, so Shift+Enter sends the same byte as plain Enter. This makes it impossible to distinguish the two at the terminal protocol level. Use the macOS CoreGraphics API (CGEventSourceFlagsState) to query the system-level keyboard modifier state when a plain Enter key event is received. If Shift is physically held, treat it as a newline insertion instead of submitting the message. This approach works universally across all macOS terminal emulators without requiring any terminal configuration. For terminals that do support the Kitty protocol (VSCode, iTerm2 3.5+, Ghostty), the existing shift+enter detection continues to work via the protocol, and the CoreGraphics fallback is never reached. --- pkg/tui/components/editor/editor.go | 10 ++++++++ pkg/tui/internal/termfeatures/keyboard.go | 11 ++++++++- .../internal/termfeatures/keyboard_test.go | 9 +++++--- .../internal/termfeatures/modifiers_darwin.go | 23 +++++++++++++++++++ .../internal/termfeatures/modifiers_other.go | 9 ++++++++ 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 pkg/tui/internal/termfeatures/modifiers_darwin.go create mode 100644 pkg/tui/internal/termfeatures/modifiers_other.go diff --git a/pkg/tui/components/editor/editor.go b/pkg/tui/components/editor/editor.go index a4e1fbcf5..a1c306979 100644 --- a/pkg/tui/components/editor/editor.go +++ b/pkg/tui/components/editor/editor.go @@ -771,6 +771,16 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) { return e, nil } + // On macOS, terminals like Terminal.app don't support the Kitty keyboard + // protocol, so Shift+Enter sends the same byte as Enter. Use the macOS + // CoreGraphics API to check if Shift is physically held, and treat it + // as a newline insertion. + if msg.String() == "enter" && termfeatures.IsShiftPressed() { + e.textarea.InsertString("\n") + e.refreshSuggestion() + return e, nil + } + // Let textarea process the key - it handles newlines via InsertNewline binding prev := e.textarea.Value() e.textarea, _ = e.textarea.Update(msg) diff --git a/pkg/tui/internal/termfeatures/keyboard.go b/pkg/tui/internal/termfeatures/keyboard.go index dc0d64efb..f59a60e4f 100644 --- a/pkg/tui/internal/termfeatures/keyboard.go +++ b/pkg/tui/internal/termfeatures/keyboard.go @@ -1,10 +1,19 @@ package termfeatures -import "strings" +import ( + "runtime" + "strings" +) // SupportsModifiedEnter returns true for terminals that can distinguish // Shift+Enter from Enter even when they do not report Kitty keyboard flags. +// On macOS, we use the CoreGraphics API to detect modifier key state at the +// system level, so all terminals effectively support this. func SupportsModifiedEnter(getenv func(string) string) bool { + if runtime.GOOS == "darwin" { + return true + } + if getenv == nil { return false } diff --git a/pkg/tui/internal/termfeatures/keyboard_test.go b/pkg/tui/internal/termfeatures/keyboard_test.go index e2bed09e5..b82e9e426 100644 --- a/pkg/tui/internal/termfeatures/keyboard_test.go +++ b/pkg/tui/internal/termfeatures/keyboard_test.go @@ -1,6 +1,9 @@ package termfeatures -import "testing" +import ( + "runtime" + "testing" +) func TestSupportsModifiedEnter(t *testing.T) { t.Parallel() @@ -14,8 +17,8 @@ func TestSupportsModifiedEnter(t *testing.T) { {name: "wezterm pane", env: map[string]string{"WEZTERM_PANE": "1"}, want: true}, {name: "wezterm socket", env: map[string]string{"WEZTERM_UNIX_SOCKET": "/tmp/wezterm.sock"}, want: true}, {name: "wezterm term", env: map[string]string{"TERM": "wezterm"}, want: true}, - {name: "other terminal", env: map[string]string{"TERM_PROGRAM": "Apple_Terminal", "TERM": "xterm-256color"}, want: false}, - {name: "nil getenv", env: nil, want: false}, + {name: "other terminal", env: map[string]string{"TERM_PROGRAM": "Apple_Terminal", "TERM": "xterm-256color"}, want: runtime.GOOS == "darwin"}, + {name: "nil getenv", env: nil, want: runtime.GOOS == "darwin"}, } for _, tt := range tests { diff --git a/pkg/tui/internal/termfeatures/modifiers_darwin.go b/pkg/tui/internal/termfeatures/modifiers_darwin.go new file mode 100644 index 000000000..f8c23bcf1 --- /dev/null +++ b/pkg/tui/internal/termfeatures/modifiers_darwin.go @@ -0,0 +1,23 @@ +//go:build darwin + +package termfeatures + +/* +#cgo LDFLAGS: -framework CoreGraphics +#include + +static int isShiftPressed() { + CGEventFlags flags = CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState); + return (flags & kCGEventFlagMaskShift) != 0; +} +*/ +import "C" + +// IsShiftPressed queries the macOS CoreGraphics event system to determine +// whether the Shift key is currently held down. This allows us to distinguish +// Shift+Enter from Enter in terminals that don't support the Kitty keyboard +// protocol (like macOS Terminal.app), since those terminals send the same byte +// for both key combinations. +func IsShiftPressed() bool { + return C.isShiftPressed() != 0 +} diff --git a/pkg/tui/internal/termfeatures/modifiers_other.go b/pkg/tui/internal/termfeatures/modifiers_other.go new file mode 100644 index 000000000..fe8b029bd --- /dev/null +++ b/pkg/tui/internal/termfeatures/modifiers_other.go @@ -0,0 +1,9 @@ +//go:build !darwin + +package termfeatures + +// IsShiftPressed returns false on non-macOS platforms. The CoreGraphics-based +// modifier detection is only available on macOS. +func IsShiftPressed() bool { + return false +}