export * as TuiKeybind from "./keybind" import type { KeyEvent, Renderable } from "@opentui/core" import type { Binding } from "@opentui/keymap" import type { BindingCommandMap, BindingConfig, BindingDefaults, BindingValue } from "@opentui/keymap/extras" import z from "zod" const KeyStroke = z .object({ name: z.string(), ctrl: z.boolean().optional(), shift: z.boolean().optional(), meta: z.boolean().optional(), super: z.boolean().optional(), hyper: z.boolean().optional(), }) .strict() const BindingObject = z .object({ key: z.union([z.string(), KeyStroke]), event: z.enum(["press", "release"]).optional(), preventDefault: z.boolean().optional(), fallthrough: z.boolean().optional(), }) .passthrough() const BindingItem = z.union([z.string(), KeyStroke, BindingObject]) export const BindingValueSchema = z.union([z.literal(false), z.literal("none"), BindingItem, z.array(BindingItem)]) type Definition = { default: z.input description: string } const inputUndoDefault = process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z" export const LeaderDefault = "ctrl+x" const keybind = (value: Definition["default"], description: string): Definition => ({ default: value, description }) const Definitions = { leader: keybind(LeaderDefault, "Leader key for keybind combinations"), app_exit: keybind("ctrl+c,ctrl+d,q", "Exit the application"), app_debug: keybind("none", "Toggle debug panel"), app_console: keybind("none", "Toggle console"), app_heap_snapshot: keybind("none", "Write heap snapshot"), app_toggle_animations: keybind("none", "Toggle animations"), app_toggle_file_context: keybind("none", "Toggle file context"), app_toggle_diffwrap: keybind("none", "Toggle diff wrapping"), app_toggle_paste_summary: keybind("none", "Toggle paste summary"), app_toggle_session_directory_filter: keybind("none", "Toggle session directory filtering"), command_list: keybind("ctrl+p", "List available commands"), help_show: keybind("none", "Open help dialog"), docs_open: keybind("none", "Open documentation"), editor_open: keybind("e", "Open external editor"), theme_list: keybind("t", "List available themes"), theme_switch_mode: keybind("none", "Switch between light and dark theme mode"), theme_mode_lock: keybind("none", "Lock or unlock theme mode"), sidebar_toggle: keybind("b", "Toggle sidebar"), scrollbar_toggle: keybind("none", "Toggle session scrollbar"), status_view: keybind("s", "View status"), session_export: keybind("x", "Export session to editor"), session_copy: keybind("none", "Copy session transcript"), session_new: keybind("n", "Create a new session"), session_list: keybind("l", "List all sessions"), session_timeline: keybind("g", "Show session timeline"), session_fork: keybind("none", "Fork session from message"), session_rename: keybind("ctrl+r", "Rename session"), session_delete: keybind("ctrl+d", "Delete session"), session_share: keybind("none", "Share current session"), session_unshare: keybind("none", "Unshare current session"), session_interrupt: keybind("escape", "Interrupt current session"), session_compact: keybind("c", "Compact the session"), session_toggle_timestamps: keybind("none", "Toggle message timestamps"), session_toggle_generic_tool_output: keybind("none", "Toggle generic tool output"), session_child_first: keybind("down", "Go to first child session"), session_child_cycle: keybind("right", "Go to next child session"), session_child_cycle_reverse: keybind("left", "Go to previous child session"), session_parent: keybind("up", "Go to parent session"), stash_delete: keybind("ctrl+d", "Delete stash entry"), model_provider_list: keybind("ctrl+a", "Open provider list from model dialog"), model_favorite_toggle: keybind("ctrl+f", "Toggle model favorite status"), model_list: keybind("m", "List available models"), model_cycle_recent: keybind("f2", "Next recently used model"), model_cycle_recent_reverse: keybind("shift+f2", "Previous recently used model"), model_cycle_favorite: keybind("none", "Next favorite model"), model_cycle_favorite_reverse: keybind("none", "Previous favorite model"), mcp_list: keybind("none", "List MCP servers"), provider_connect: keybind("none", "Connect provider"), console_org_switch: keybind("none", "Switch console organization"), agent_list: keybind("a", "List agents"), agent_cycle: keybind("tab", "Next agent"), agent_cycle_reverse: keybind("shift+tab", "Previous agent"), variant_cycle: keybind("ctrl+t", "Cycle model variants"), variant_list: keybind("none", "List model variants"), messages_page_up: keybind("pageup,ctrl+alt+b", "Scroll messages up by one page"), messages_page_down: keybind("pagedown,ctrl+alt+f", "Scroll messages down by one page"), messages_line_up: keybind("ctrl+alt+y", "Scroll messages up by one line"), messages_line_down: keybind("ctrl+alt+e", "Scroll messages down by one line"), messages_half_page_up: keybind("ctrl+alt+u", "Scroll messages up by half page"), messages_half_page_down: keybind("ctrl+alt+d", "Scroll messages down by half page"), messages_first: keybind("ctrl+g,home", "Navigate to first message"), messages_last: keybind("ctrl+alt+g,end", "Navigate to last message"), messages_next: keybind("none", "Navigate to next message"), messages_previous: keybind("none", "Navigate to previous message"), messages_last_user: keybind("none", "Navigate to last user message"), messages_copy: keybind("y", "Copy message"), messages_undo: keybind("u", "Undo message"), messages_redo: keybind("r", "Redo message"), messages_toggle_conceal: keybind("h", "Toggle code block concealment in messages"), tool_details: keybind("none", "Toggle tool details visibility"), display_thinking: keybind("none", "Toggle thinking blocks visibility"), prompt_submit: keybind("none", "Submit prompt"), prompt_editor_context_clear: keybind("none", "Clear editor context"), prompt_skills: keybind("none", "Open skill selector"), prompt_stash: keybind("none", "Stash prompt"), prompt_stash_pop: keybind("none", "Pop stashed prompt"), prompt_stash_list: keybind("none", "List stashed prompts"), workspace_set: keybind("none", "Set workspace"), input_clear: keybind("ctrl+c", "Clear input field"), input_paste: keybind({ key: "ctrl+v", preventDefault: false }, "Paste from clipboard"), input_submit: keybind("return", "Submit input"), input_newline: keybind("shift+return,ctrl+return,alt+return,ctrl+j", "Insert newline in input"), input_move_left: keybind("left,ctrl+b", "Move cursor left in input"), input_move_right: keybind("right,ctrl+f", "Move cursor right in input"), input_move_up: keybind("up", "Move cursor up in input"), input_move_down: keybind("down", "Move cursor down in input"), input_select_left: keybind("shift+left", "Select left in input"), input_select_right: keybind("shift+right", "Select right in input"), input_select_up: keybind("shift+up", "Select up in input"), input_select_down: keybind("shift+down", "Select down in input"), input_line_home: keybind("ctrl+a", "Move to start of line in input"), input_line_end: keybind("ctrl+e", "Move to end of line in input"), input_select_line_home: keybind("ctrl+shift+a", "Select to start of line in input"), input_select_line_end: keybind("ctrl+shift+e", "Select to end of line in input"), input_visual_line_home: keybind("alt+a", "Move to start of visual line in input"), input_visual_line_end: keybind("alt+e", "Move to end of visual line in input"), input_select_visual_line_home: keybind("alt+shift+a", "Select to start of visual line in input"), input_select_visual_line_end: keybind("alt+shift+e", "Select to end of visual line in input"), input_buffer_home: keybind("home", "Move to start of buffer in input"), input_buffer_end: keybind("end", "Move to end of buffer in input"), input_select_buffer_home: keybind("shift+home", "Select to start of buffer in input"), input_select_buffer_end: keybind("shift+end", "Select to end of buffer in input"), input_delete_line: keybind("ctrl+shift+d", "Delete line in input"), input_delete_to_line_end: keybind("ctrl+k", "Delete to end of line in input"), input_delete_to_line_start: keybind("ctrl+u", "Delete to start of line in input"), input_backspace: keybind("backspace,shift+backspace", "Backspace in input"), input_delete: keybind("ctrl+d,delete,shift+delete", "Delete character in input"), input_undo: keybind(inputUndoDefault, "Undo in input"), input_redo: keybind("ctrl+.,super+shift+z", "Redo in input"), input_word_forward: keybind("alt+f,alt+right,ctrl+right", "Move word forward in input"), input_word_backward: keybind("alt+b,alt+left,ctrl+left", "Move word backward in input"), input_select_word_forward: keybind("alt+shift+f,alt+shift+right", "Select word forward in input"), input_select_word_backward: keybind("alt+shift+b,alt+shift+left", "Select word backward in input"), input_delete_word_forward: keybind("alt+d,alt+delete,ctrl+delete", "Delete word forward in input"), input_delete_word_backward: keybind("ctrl+w,ctrl+backspace,alt+backspace", "Delete word backward in input"), input_select_all: keybind("super+a", "Select all in input"), history_previous: keybind("up", "Previous history item"), history_next: keybind("down", "Next history item"), "dialog.select.prev": keybind("up,ctrl+p", "Move to previous dialog item"), "dialog.select.next": keybind("down,ctrl+n", "Move to next dialog item"), "dialog.select.page_up": keybind("pageup", "Move up one page in dialog"), "dialog.select.page_down": keybind("pagedown", "Move down one page in dialog"), "dialog.select.home": keybind("home", "Move to first dialog item"), "dialog.select.end": keybind("end", "Move to last dialog item"), "dialog.select.submit": keybind("return", "Submit selected dialog item"), "dialog.mcp.toggle": keybind("space", "Toggle MCP in MCP dialog"), "prompt.autocomplete.prev": keybind("up,ctrl+p", "Move to previous autocomplete item"), "prompt.autocomplete.next": keybind("down,ctrl+n", "Move to next autocomplete item"), "prompt.autocomplete.hide": keybind("escape", "Hide autocomplete"), "prompt.autocomplete.select": keybind("return", "Select autocomplete item"), "prompt.autocomplete.complete": keybind("tab", "Complete autocomplete item"), "permission.prompt.fullscreen": keybind("ctrl+f", "Toggle permission prompt fullscreen"), "plugins.toggle": keybind("space", "Toggle plugin"), "dialog.plugins.install": keybind("shift+i", "Install plugin from plugin dialog"), terminal_suspend: keybind("ctrl+z", "Suspend terminal"), terminal_title_toggle: keybind("none", "Toggle terminal title"), tips_toggle: keybind("h", "Toggle tips on home screen"), plugin_manager: keybind("none", "Open plugin manager dialog"), plugin_install: keybind("none", "Install plugin"), which_key_toggle: keybind("ctrl+alt+k", "Toggle which-key panel"), which_key_layout_toggle: keybind("ctrl+alt+shift+k", "Switch which-key layout"), which_key_pending_toggle: keybind("ctrl+alt+shift+p", "Toggle which-key pending preview"), which_key_group_previous: keybind("ctrl+alt+left,ctrl+alt+[", "Previous which-key group"), which_key_group_next: keybind("ctrl+alt+right,ctrl+alt+]", "Next which-key group"), which_key_scroll_up: keybind("ctrl+alt+up,ctrl+alt+p", "Scroll which-key up"), which_key_scroll_down: keybind("ctrl+alt+down,ctrl+alt+n", "Scroll which-key down"), which_key_page_up: keybind("ctrl+alt+pageup", "Page which-key up"), which_key_page_down: keybind("ctrl+alt+pagedown", "Page which-key down"), which_key_home: keybind("ctrl+alt+home", "Jump to first which-key binding"), which_key_end: keybind("ctrl+alt+end", "Jump to last which-key binding"), } satisfies Record type KeybindName = keyof typeof Definitions & string const KeybindShape = Object.fromEntries( Object.entries(Definitions).map(([name, item]) => [ name, BindingValueSchema.optional().default(item.default).describe(item.description), ]), ) as Record>> const KeybindOverrideShape = Object.fromEntries( Object.entries(Definitions).map(([name, item]) => [name, BindingValueSchema.optional().describe(item.description)]), ) as Record> export const Keybinds = z.strictObject(KeybindShape).describe("TUI keybinding configuration") export const KeybindOverrides = z.strictObject(KeybindOverrideShape).describe("TUI keybinding overrides") export const Descriptions = Object.fromEntries( Object.entries(Definitions).map(([name, item]) => [name, item.description]), ) as Record export const CommandMap = { app_exit: "app.exit", app_debug: "app.debug", app_console: "app.console", app_heap_snapshot: "app.heap_snapshot", app_toggle_animations: "app.toggle.animations", app_toggle_file_context: "app.toggle.file_context", app_toggle_diffwrap: "app.toggle.diffwrap", app_toggle_paste_summary: "app.toggle.paste_summary", app_toggle_session_directory_filter: "app.toggle.session_directory_filter", command_list: "command.palette.show", help_show: "help.show", docs_open: "docs.open", editor_open: "prompt.editor", theme_list: "theme.switch", theme_switch_mode: "theme.switch_mode", theme_mode_lock: "theme.mode.lock", sidebar_toggle: "session.sidebar.toggle", scrollbar_toggle: "session.toggle.scrollbar", status_view: "opencode.status", session_export: "session.export", session_copy: "session.copy", session_new: "session.new", session_list: "session.list", session_timeline: "session.timeline", session_fork: "session.fork", session_rename: "session.rename", session_delete: "session.delete", session_share: "session.share", session_unshare: "session.unshare", session_interrupt: "session.interrupt", session_compact: "session.compact", session_toggle_timestamps: "session.toggle.timestamps", session_toggle_generic_tool_output: "session.toggle.generic_tool_output", session_child_first: "session.child.first", session_child_cycle: "session.child.next", session_child_cycle_reverse: "session.child.previous", session_parent: "session.parent", stash_delete: "stash.delete", model_provider_list: "model.dialog.provider", model_favorite_toggle: "model.dialog.favorite", model_list: "model.list", model_cycle_recent: "model.cycle_recent", model_cycle_recent_reverse: "model.cycle_recent_reverse", model_cycle_favorite: "model.cycle_favorite", model_cycle_favorite_reverse: "model.cycle_favorite_reverse", mcp_list: "mcp.list", provider_connect: "provider.connect", console_org_switch: "console.org.switch", agent_list: "agent.list", agent_cycle: "agent.cycle", agent_cycle_reverse: "agent.cycle.reverse", variant_cycle: "variant.cycle", variant_list: "variant.list", messages_page_up: "session.page.up", messages_page_down: "session.page.down", messages_line_up: "session.line.up", messages_line_down: "session.line.down", messages_half_page_up: "session.half.page.up", messages_half_page_down: "session.half.page.down", messages_first: "session.first", messages_last: "session.last", messages_next: "session.message.next", messages_previous: "session.message.previous", messages_last_user: "session.messages_last_user", messages_copy: "messages.copy", messages_undo: "session.undo", messages_redo: "session.redo", messages_toggle_conceal: "session.toggle.conceal", tool_details: "session.toggle.actions", display_thinking: "session.toggle.thinking", prompt_submit: "prompt.submit", prompt_editor_context_clear: "prompt.editor_context.clear", prompt_skills: "prompt.skills", prompt_stash: "prompt.stash", prompt_stash_pop: "prompt.stash.pop", prompt_stash_list: "prompt.stash.list", workspace_set: "workspace.set", input_clear: "prompt.clear", input_paste: "prompt.paste", input_submit: "input.submit", input_newline: "input.newline", input_move_left: "input.move.left", input_move_right: "input.move.right", input_move_up: "input.move.up", input_move_down: "input.move.down", input_select_left: "input.select.left", input_select_right: "input.select.right", input_select_up: "input.select.up", input_select_down: "input.select.down", input_line_home: "input.line.home", input_line_end: "input.line.end", input_select_line_home: "input.select.line.home", input_select_line_end: "input.select.line.end", input_visual_line_home: "input.visual.line.home", input_visual_line_end: "input.visual.line.end", input_select_visual_line_home: "input.select.visual.line.home", input_select_visual_line_end: "input.select.visual.line.end", input_buffer_home: "input.buffer.home", input_buffer_end: "input.buffer.end", input_select_buffer_home: "input.select.buffer.home", input_select_buffer_end: "input.select.buffer.end", input_delete_line: "input.delete.line", input_delete_to_line_end: "input.delete.to.line.end", input_delete_to_line_start: "input.delete.to.line.start", input_backspace: "input.backspace", input_delete: "input.delete", input_undo: "input.undo", input_redo: "input.redo", input_word_forward: "input.word.forward", input_word_backward: "input.word.backward", input_select_word_forward: "input.select.word.forward", input_select_word_backward: "input.select.word.backward", input_delete_word_forward: "input.delete.word.forward", input_delete_word_backward: "input.delete.word.backward", input_select_all: "input.select.all", history_previous: "prompt.history.previous", history_next: "prompt.history.next", terminal_suspend: "terminal.suspend", terminal_title_toggle: "terminal.title.toggle", tips_toggle: "tips.toggle", plugin_manager: "plugins.list", plugin_install: "plugins.install", which_key_toggle: "which-key.toggle", which_key_layout_toggle: "which-key.layout.toggle", which_key_pending_toggle: "which-key.pending.toggle", which_key_group_previous: "which-key.group.previous", which_key_group_next: "which-key.group.next", which_key_scroll_up: "which-key.scroll.up", which_key_scroll_down: "which-key.scroll.down", which_key_page_up: "which-key.page.up", which_key_page_down: "which-key.page.down", which_key_home: "which-key.home", which_key_end: "which-key.end", } satisfies BindingCommandMap const CommandDescriptions = Object.fromEntries( Object.entries(Definitions).map(([name, item]) => [ CommandMap[name as keyof typeof CommandMap] ?? name, item.description, ]), ) as Record export type Keybinds = z.output export type KeybindOverrides = z.output export type BindingLookupView = { readonly bindings: readonly Binding[] get(command: string): readonly Binding[] has(command: string): boolean gather(name: string, commands: readonly string[]): readonly Binding[] pick(name: string, commands: readonly string[]): Binding[] omit(name: string, commands: readonly string[]): Binding[] } export function toBindingConfig(keybinds: Keybinds): BindingConfig { return Object.fromEntries(Object.entries(keybinds)) as BindingConfig } export function bindingDefaults(): BindingDefaults { return ({ command, binding }) => { if (binding.desc !== undefined) return return { desc: CommandDescriptions[command] } } }