diff --git a/internal/ui/app.go b/internal/ui/app.go index 492b89ae..e1a9d266 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 - // Plain Enter sends; modified Enter variants insert 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/Alt+Enter, backslash+Enter, and + // Ctrl+J insert a newline. keystroke := msg.Key().Keystroke() stringForm := msg.String() isModifiedEnter := stringForm == "shift+enter" || keystroke == "shift+enter" || stringForm == "shift+return" || keystroke == "shift+return" || stringForm == "alt+enter" || keystroke == "alt+enter" || stringForm == "alt+return" || keystroke == "alt+return" diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index f8f7b6b5..19fde521 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()) + } +} + func TestHandleInsertMode_ShiftReturnInsertsNewline(t *testing.T) { app := NewApp() app.activeChannelID = "C1" 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) |