From e3bf513b1e2d99838fdc18de7d386a3e1aaaa6c3 Mon Sep 17 00:00:00 2001 From: Jin Ku Date: Fri, 22 May 2026 09:56:55 -0700 Subject: [PATCH] Support Ctrl+Enter for sending messages Treat terminal-reported Ctrl+Enter the same as plain Enter in insert mode so IME users have a native send chord that is less likely to be consumed as composition commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/ui/app.go | 6 ++++-- internal/ui/app_test.go | 38 ++++++++++++++++++++++++++++++++++++++ wiki/Keybindings.md | 2 +- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/internal/ui/app.go b/internal/ui/app.go index e65bb953..f5ba85bb 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -3311,8 +3311,10 @@ func (a *App) handleInsertMode(msg tea.KeyMsg) tea.Cmd { return nil } } - // Plain Enter sends; Shift+Enter (and Ctrl+J as a fallback for terminals - // that don't disambiguate modifiers) inserts a newline. + // Plain Enter sends. Ctrl+Enter also sends when the terminal reports it + // distinctly, which gives IME users a send key that is less likely to be + // consumed as composition commit. Shift+Enter (and Ctrl+J as a fallback for + // terminals that don't disambiguate modifiers) inserts a newline. isSend := code == tea.KeyEnter && !mod.Contains(tea.ModShift) isNewline := (code == tea.KeyEnter && mod.Contains(tea.ModShift)) || (code == 'j' && mod == tea.ModCtrl) diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index d70b825e..dcc8901d 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -303,6 +303,44 @@ func TestHandleInsertMode_PlainEnterSends(t *testing.T) { } } +func TestHandleInsertMode_CtrlEnterSends(t *testing.T) { + app := NewApp() + app.activeChannelID = "C1" + app.focusedPanel = PanelMessages + app.SetMode(ModeInsert) + app.compose.SetValue("hello") + + cmd := app.handleInsertMode(tea.KeyPressMsg{Code: tea.KeyEnter, Mod: tea.ModCtrl}) + if cmd == nil { + t.Fatalf("Ctrl+Enter with text should return a send cmd") + } + msg := cmd() + if _, ok := msg.(SendMessageMsg); !ok { + t.Fatalf("expected SendMessageMsg, got %T", msg) + } + if app.compose.Value() != "" { + t.Fatalf("expected compose to be reset after Ctrl+Enter send, got %q", app.compose.Value()) + } +} + +func TestHandleInsertMode_ThreadReplyCtrlEnterSends(t *testing.T) { + app := NewApp() + app.activeChannelID = "C1" + app.threadPanel.SetThread(messages.MessageItem{TS: "P1"}, nil, "C1", "P1") + app.threadVisible = true + app.focusedPanel = PanelThread + app.SetMode(ModeInsert) + app.threadCompose.SetValue("reply") + + cmd := app.handleInsertMode(tea.KeyPressMsg{Code: tea.KeyEnter, Mod: tea.ModCtrl}) + if cmd == nil { + t.Fatalf("Ctrl+Enter with thread text should return a send cmd") + } + if _, ok := cmd().(SendThreadReplyMsg); !ok { + t.Fatalf("expected SendThreadReplyMsg, got %T", cmd()) + } +} + // TestHandleInsertMode_PlainEnterReturnsToNormalMode locks in the // vim-style UX: hitting Enter to submit a channel message drops the // user back to ModeNormal instead of leaving them in insert mode. diff --git a/wiki/Keybindings.md b/wiki/Keybindings.md index 4ebc111e..df062144 100644 --- a/wiki/Keybindings.md +++ b/wiki/Keybindings.md @@ -10,7 +10,7 @@ | `Enter` | Normal (message) | Open thread | | `i` / `F2` | Normal | Enter insert mode (`F2` is useful when a CJK IME consumes alphabetic keys for composition) | | `Esc` | Insert / Command | Return to normal mode | -| `Enter` | Insert | Send message | +| `Enter` / `Ctrl+Enter` | Insert | Send message (`Ctrl+Enter` is useful when IME composition consumes plain Enter) | | `Shift+Enter` | Insert | Newline | | `Ctrl+V` | Insert | Smart paste — image / file path / text (use `Ctrl+V`, not the terminal's `Ctrl+Shift+V`) | | `Ctrl+U` | Insert | Clear compose (text + pending attachments) |