summaryrefslogtreecommitdiff
path: root/packages/cli/src/config
diff options
context:
space:
mode:
authorLee Won Jun <[email protected]>2025-08-09 16:03:17 +0900
committerGitHub <[email protected]>2025-08-09 07:03:17 +0000
commitb8084ba8158b89facd49fd78a51abb80b1db54da (patch)
tree5fd41e255b5118d53798c29d9fad95478a1ed582 /packages/cli/src/config
parent6487cc16895976ef6c983f8beca08a64addb6688 (diff)
Centralize Key Binding Logic and Refactor (Reopen) (#5356)
Co-authored-by: Lee-WonJun <[email protected]>
Diffstat (limited to 'packages/cli/src/config')
-rw-r--r--packages/cli/src/config/keyBindings.test.ts62
-rw-r--r--packages/cli/src/config/keyBindings.ts180
2 files changed, 242 insertions, 0 deletions
diff --git a/packages/cli/src/config/keyBindings.test.ts b/packages/cli/src/config/keyBindings.test.ts
new file mode 100644
index 00000000..2e89e421
--- /dev/null
+++ b/packages/cli/src/config/keyBindings.test.ts
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ Command,
+ KeyBindingConfig,
+ defaultKeyBindings,
+} from './keyBindings.js';
+
+describe('keyBindings config', () => {
+ describe('defaultKeyBindings', () => {
+ it('should have bindings for all commands', () => {
+ const commands = Object.values(Command);
+
+ for (const command of commands) {
+ expect(defaultKeyBindings[command]).toBeDefined();
+ expect(Array.isArray(defaultKeyBindings[command])).toBe(true);
+ }
+ });
+
+ it('should have valid key binding structures', () => {
+ for (const [_, bindings] of Object.entries(defaultKeyBindings)) {
+ for (const binding of bindings) {
+ // Each binding should have either key or sequence, but not both
+ const hasKey = binding.key !== undefined;
+ const hasSequence = binding.sequence !== undefined;
+
+ expect(hasKey || hasSequence).toBe(true);
+ expect(hasKey && hasSequence).toBe(false);
+
+ // Modifier properties should be boolean or undefined
+ if (binding.ctrl !== undefined) {
+ expect(typeof binding.ctrl).toBe('boolean');
+ }
+ if (binding.shift !== undefined) {
+ expect(typeof binding.shift).toBe('boolean');
+ }
+ if (binding.command !== undefined) {
+ expect(typeof binding.command).toBe('boolean');
+ }
+ if (binding.paste !== undefined) {
+ expect(typeof binding.paste).toBe('boolean');
+ }
+ }
+ }
+ });
+
+ it('should export all required types', () => {
+ // Basic type checks
+ expect(typeof Command.HOME).toBe('string');
+ expect(typeof Command.END).toBe('string');
+
+ // Config should be readonly
+ const config: KeyBindingConfig = defaultKeyBindings;
+ expect(config[Command.HOME]).toBeDefined();
+ });
+ });
+});
diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts
new file mode 100644
index 00000000..f6ba52e2
--- /dev/null
+++ b/packages/cli/src/config/keyBindings.ts
@@ -0,0 +1,180 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Command enum for all available keyboard shortcuts
+ */
+export enum Command {
+ // Basic bindings
+ RETURN = 'return',
+ ESCAPE = 'escape',
+
+ // Cursor movement
+ HOME = 'home',
+ END = 'end',
+
+ // Text deletion
+ KILL_LINE_RIGHT = 'killLineRight',
+ KILL_LINE_LEFT = 'killLineLeft',
+ CLEAR_INPUT = 'clearInput',
+
+ // Screen control
+ CLEAR_SCREEN = 'clearScreen',
+
+ // History navigation
+ HISTORY_UP = 'historyUp',
+ HISTORY_DOWN = 'historyDown',
+ NAVIGATION_UP = 'navigationUp',
+ NAVIGATION_DOWN = 'navigationDown',
+
+ // Auto-completion
+ ACCEPT_SUGGESTION = 'acceptSuggestion',
+
+ // Text input
+ SUBMIT = 'submit',
+ NEWLINE = 'newline',
+
+ // External tools
+ OPEN_EXTERNAL_EDITOR = 'openExternalEditor',
+ PASTE_CLIPBOARD_IMAGE = 'pasteClipboardImage',
+
+ // App level bindings
+ SHOW_ERROR_DETAILS = 'showErrorDetails',
+ TOGGLE_TOOL_DESCRIPTIONS = 'toggleToolDescriptions',
+ TOGGLE_IDE_CONTEXT_DETAIL = 'toggleIDEContextDetail',
+ QUIT = 'quit',
+ EXIT = 'exit',
+ SHOW_MORE_LINES = 'showMoreLines',
+
+ // Shell commands
+ REVERSE_SEARCH = 'reverseSearch',
+ SUBMIT_REVERSE_SEARCH = 'submitReverseSearch',
+ ACCEPT_SUGGESTION_REVERSE_SEARCH = 'acceptSuggestionReverseSearch',
+}
+
+/**
+ * Data-driven key binding structure for user configuration
+ */
+export interface KeyBinding {
+ /** The key name (e.g., 'a', 'return', 'tab', 'escape') */
+ key?: string;
+ /** The key sequence (e.g., '\x18' for Ctrl+X) - alternative to key name */
+ sequence?: string;
+ /** Control key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
+ ctrl?: boolean;
+ /** Shift key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
+ shift?: boolean;
+ /** Command/meta key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
+ command?: boolean;
+ /** Paste operation requirement: true=must be paste, false=must not be paste, undefined=ignore */
+ paste?: boolean;
+}
+
+/**
+ * Configuration type mapping commands to their key bindings
+ */
+export type KeyBindingConfig = {
+ readonly [C in Command]: readonly KeyBinding[];
+};
+
+/**
+ * Default key binding configuration
+ * Matches the original hard-coded logic exactly
+ */
+export const defaultKeyBindings: KeyBindingConfig = {
+ // Basic bindings
+ [Command.RETURN]: [{ key: 'return' }],
+ // Original: key.name === 'escape'
+ [Command.ESCAPE]: [{ key: 'escape' }],
+
+ // Cursor movement
+ // Original: key.ctrl && key.name === 'a'
+ [Command.HOME]: [{ key: 'a', ctrl: true }],
+ // Original: key.ctrl && key.name === 'e'
+ [Command.END]: [{ key: 'e', ctrl: true }],
+
+ // Text deletion
+ // Original: key.ctrl && key.name === 'k'
+ [Command.KILL_LINE_RIGHT]: [{ key: 'k', ctrl: true }],
+ // Original: key.ctrl && key.name === 'u'
+ [Command.KILL_LINE_LEFT]: [{ key: 'u', ctrl: true }],
+ // Original: key.ctrl && key.name === 'c'
+ [Command.CLEAR_INPUT]: [{ key: 'c', ctrl: true }],
+
+ // Screen control
+ // Original: key.ctrl && key.name === 'l'
+ [Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }],
+
+ // History navigation
+ // Original: key.ctrl && key.name === 'p'
+ [Command.HISTORY_UP]: [{ key: 'p', ctrl: true }],
+ // Original: key.ctrl && key.name === 'n'
+ [Command.HISTORY_DOWN]: [{ key: 'n', ctrl: true }],
+ // Original: key.name === 'up'
+ [Command.NAVIGATION_UP]: [{ key: 'up' }],
+ // Original: key.name === 'down'
+ [Command.NAVIGATION_DOWN]: [{ key: 'down' }],
+
+ // Auto-completion
+ // Original: key.name === 'tab' || (key.name === 'return' && !key.ctrl)
+ [Command.ACCEPT_SUGGESTION]: [{ key: 'tab' }, { key: 'return', ctrl: false }],
+
+ // Text input
+ // Original: key.name === 'return' && !key.ctrl && !key.meta && !key.paste
+ [Command.SUBMIT]: [
+ {
+ key: 'return',
+ ctrl: false,
+ command: false,
+ paste: false,
+ },
+ ],
+ // Original: key.name === 'return' && (key.ctrl || key.meta || key.paste)
+ // Split into multiple data-driven bindings
+ [Command.NEWLINE]: [
+ { key: 'return', ctrl: true },
+ { key: 'return', command: true },
+ { key: 'return', paste: true },
+ ],
+
+ // External tools
+ // Original: key.ctrl && (key.name === 'x' || key.sequence === '\x18')
+ [Command.OPEN_EXTERNAL_EDITOR]: [
+ { key: 'x', ctrl: true },
+ { sequence: '\x18', ctrl: true },
+ ],
+ // Original: key.ctrl && key.name === 'v'
+ [Command.PASTE_CLIPBOARD_IMAGE]: [{ key: 'v', ctrl: true }],
+
+ // App level bindings
+ // Original: key.ctrl && key.name === 'o'
+ [Command.SHOW_ERROR_DETAILS]: [{ key: 'o', ctrl: true }],
+ // Original: key.ctrl && key.name === 't'
+ [Command.TOGGLE_TOOL_DESCRIPTIONS]: [{ key: 't', ctrl: true }],
+ // Original: key.ctrl && key.name === 'e'
+ [Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'e', ctrl: true }],
+ // Original: key.ctrl && (key.name === 'c' || key.name === 'C')
+ [Command.QUIT]: [
+ { key: 'c', ctrl: true },
+ { key: 'C', ctrl: true },
+ ],
+ // Original: key.ctrl && (key.name === 'd' || key.name === 'D')
+ [Command.EXIT]: [
+ { key: 'd', ctrl: true },
+ { key: 'D', ctrl: true },
+ ],
+ // Original: key.ctrl && key.name === 's'
+ [Command.SHOW_MORE_LINES]: [{ key: 's', ctrl: true }],
+
+ // Shell commands
+ // Original: key.ctrl && key.name === 'r'
+ [Command.REVERSE_SEARCH]: [{ key: 'r', ctrl: true }],
+ // Original: key.name === 'return' && !key.ctrl
+ // Note: original logic ONLY checked ctrl=false, ignored meta/shift/paste
+ [Command.SUBMIT_REVERSE_SEARCH]: [{ key: 'return', ctrl: false }],
+ // Original: key.name === 'tab'
+ [Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab' }],
+};