Skip to content

Commit 913a0fb

Browse files
committed
feat: allow ctrl+d to exit the app
This adds support for ctrl+d to exit the app. It follows the unix convention of only requiring a single press of the command. Additionally, it updates the help menu to reflect the new keybind.
1 parent b533cfc commit 913a0fb

File tree

4 files changed

+20
-11
lines changed

4 files changed

+20
-11
lines changed

packages/opencode/src/config/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ export namespace Config {
402402
.default("ctrl+x")
403403
.describe("Leader key for keybind combinations"),
404404
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
405-
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
405+
app_exit: z.string().optional().default("ctrl+d,ctrl+c,<leader>q").describe("Exit the application"),
406406
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
407407
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
408408
project_init: z.string().optional().default("<leader>i").describe("Create/update AGENTS.md"),

packages/sdk/python/src/opencode_ai/models/keybinds_config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class KeybindsConfig:
1515
Attributes:
1616
leader (Union[Unset, str]): Leader key for keybind combinations Default: 'ctrl+x'.
1717
app_help (Union[Unset, str]): Show help dialog Default: '<leader>h'.
18-
app_exit (Union[Unset, str]): Exit the application Default: 'ctrl+c,<leader>q'.
18+
app_exit (Union[Unset, str]): Exit the application Default: 'ctrl+d,ctrl+c,<leader>q'.
1919
editor_open (Union[Unset, str]): Open external editor Default: '<leader>e'.
2020
theme_list (Union[Unset, str]): List available themes Default: '<leader>t'.
2121
project_init (Union[Unset, str]): Create/update AGENTS.md Default: '<leader>i'.
@@ -67,7 +67,7 @@ class KeybindsConfig:
6767

6868
leader: Union[Unset, str] = "ctrl+x"
6969
app_help: Union[Unset, str] = "<leader>h"
70-
app_exit: Union[Unset, str] = "ctrl+c,<leader>q"
70+
app_exit: Union[Unset, str] = "ctrl+d,ctrl+c,<leader>q"
7171
editor_open: Union[Unset, str] = "<leader>e"
7272
theme_list: Union[Unset, str] = "<leader>t"
7373
project_init: Union[Unset, str] = "<leader>i"

packages/tui/internal/commands/command.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,6 @@ func LoadFromConfig(config *opencode.Config, customCommands []opencode.Command)
367367
Description: "last message",
368368
Keybindings: parseBindings("ctrl+alt+g"),
369369
},
370-
371370
{
372371
Name: MessagesCopyCommand,
373372
Description: "copy message",
@@ -388,7 +387,8 @@ func LoadFromConfig(config *opencode.Config, customCommands []opencode.Command)
388387
{
389388
Name: AppExitCommand,
390389
Description: "exit the app",
391-
Keybindings: parseBindings("ctrl+c", "<leader>q"),
390+
// NOTE: ctrl+c requires a double press to exit while ctrl+d requires a single press
391+
Keybindings: parseBindings("ctrl+c", "ctrl+d", "<leader>q"),
392392
Trigger: []string{"exit", "quit", "q"},
393393
},
394394
}

packages/tui/internal/tui/tui.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,14 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
305305
}
306306
}
307307

308-
// 8. Handle exit key debounce for app exit when using non-leader command
308+
// 8. Handle immediate exit command both for empty and non-empty editor
309309
exitCommand := a.app.Commands[commands.AppExitCommand]
310-
if exitCommand.Matches(msg, a.app.IsLeaderSequence) {
310+
if exitCommand.Matches(msg, a.app.IsLeaderSequence) && keyString == "ctrl+d" && a.editor.Length() == 0 {
311+
return a, util.CmdHandler(commands.ExecuteCommandMsg(exitCommand))
312+
}
313+
314+
// 9. Handle exit key debounce for app exit when using non-leader command
315+
if exitCommand.Matches(msg, a.app.IsLeaderSequence) && keyString != "ctrl+d" {
311316
switch a.exitKeyState {
312317
case ExitKeyIdle:
313318
// First exit key press - start debounce timer
@@ -324,22 +329,26 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
324329
}
325330
}
326331

327-
// 9. Check again for commands that don't require leader (excluding interrupt when busy and exit when in debounce)
332+
// 10. Check again for commands that don't require leader (excluding interrupt when busy and exit when in debounce)
328333
matches := a.app.Commands.Matches(msg, a.app.IsLeaderSequence)
329-
if len(matches) > 0 {
334+
if len(matches) > 0 && keyString != "ctrl+d" {
330335
// Skip interrupt key if we're in debounce mode and app is busy
331336
if interruptCommand.Matches(msg, a.app.IsLeaderSequence) && a.app.IsBusy() && a.interruptKeyState != InterruptKeyIdle {
332337
return a, nil
333338
}
339+
// Skip ctrl+d immediate exit key if the editor has content. Allow editor update commands to handle it as forward delete.
340+
if exitCommand.Matches(msg, a.app.IsLeaderSequence) && keyString == "ctrl+d" && a.editor.Length() > 0 {
341+
return a, nil
342+
}
334343
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
335344
}
336345

337-
// Fallback: suspend if ctrl+z is pressed and no user keybind matched
346+
// 11. Fallback: suspend if ctrl+z is pressed and no user keybind matched
338347
if keyString == "ctrl+z" {
339348
return a, tea.Suspend
340349
}
341350

342-
// 10. Fallback to editor. This is for other characters like backspace, tab, etc.
351+
// 12. Fallback to editor. This is for other characters like backspace, tab, etc.
343352
updatedEditor, cmd := a.editor.Update(msg)
344353
a.editor = updatedEditor.(chat.EditorComponent)
345354
return a, cmd

0 commit comments

Comments
 (0)