| author | |
| committer | |
| log | f1cd76c17af2fa9496729ef08056ae69e1f27742 |
| tree | 07667147cacd92bd25766c607d58750e3238ad96 |
| parent | 31aa32b83bb234ef7f2bdad6a898d06ea1d0552e |
- Move default keybinding resolution/override merging into `server.getConfig`
- Return server-resolved keybindings to the web client instead of resolving in UI
- Update docs and tests to reflect server-side defaults, overrides, and truncation behavior6 files changed, 108 insertions(+), 134 deletions(-)
KEYBINDINGS.md+11-2| ... | ... | @@ -4,6 +4,15 @@ T3 Code reads keybindings from: |
| 4 | 4 | |
| 5 | 5 | - `~/.t3/keybindings.json` |
| 6 | 6 | |
| 7 | Schema source of truth: | |
| 8 | ||
| 9 | - `packages/contracts/src/keybindings.ts` | |
| 10 | - `packages/contracts/src/server.ts` | |
| 11 | ||
| 12 | Server-side default resolution/merging: | |
| 13 | ||
| 14 | - `apps/server/src/wsServer.ts` | |
| 15 | ||
| 7 | 16 | The file must be a JSON array of rules: |
| 8 | 17 | |
| 9 | 18 | ```json |
| ... | ... | @@ -71,13 +80,13 @@ Unknown condition keys evaluate to `false`. |
| 71 | 80 | |
| 72 | 81 | ## Defaults |
| 73 | 82 | |
| 74 | Built-in defaults: | |
| 83 | Built-in defaults are resolved on the server and sent to the web client via `server.getConfig`: | |
| 75 | 84 | |
| 76 | 85 | - `terminal.toggle`: `mod+j` |
| 77 | 86 | - `terminal.split`: `mod+d` when `terminalFocus` |
| 78 | 87 | - `terminal.new`: `mod+shift+d` when `terminalFocus` |
| 79 | 88 | |
| 80 | If you define any rules for a command, the default rule for that command is removed. | |
| 89 | If you define any rules for a command, the default rule for that command is removed on the server. | |
| 81 | 90 | |
| 82 | 91 | ## Precedence |
| 83 | 92 |
apps/server/src/wsServer.test.ts+20-4| ... | ... | @@ -309,7 +309,11 @@ describe("WebSocket Server", () => { |
| 309 | 309 | expect(response.error).toBeUndefined(); |
| 310 | 310 | expect(response.result).toEqual({ |
| 311 | 311 | cwd: "/my/workspace", |
| 312 | keybindings: [], | |
| 312 | keybindings: [ | |
| 313 | { key: "mod+j", command: "terminal.toggle" }, | |
| 314 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, | |
| 315 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, | |
| 316 | ], | |
| 313 | 317 | }); |
| 314 | 318 | }); |
| 315 | 319 | |
| ... | ... | @@ -379,7 +383,11 @@ describe("WebSocket Server", () => { |
| 379 | 383 | expect(response.error).toBeUndefined(); |
| 380 | 384 | expect(response.result).toEqual({ |
| 381 | 385 | cwd: "/my/workspace", |
| 382 | keybindings: [{ key: "mod+j", command: "terminal.toggle" }], | |
| 386 | keybindings: [ | |
| 387 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, | |
| 388 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, | |
| 389 | { key: "mod+j", command: "terminal.toggle" }, | |
| 390 | ], | |
| 383 | 391 | }); |
| 384 | 392 | expect( |
| 385 | 393 | warnSpy.mock.calls.some(([message]) => |
| ... | ... | @@ -414,7 +422,11 @@ describe("WebSocket Server", () => { |
| 414 | 422 | expect(response.error).toBeUndefined(); |
| 415 | 423 | expect(response.result).toEqual({ |
| 416 | 424 | cwd: "/my/workspace", |
| 417 | keybindings: [], | |
| 425 | keybindings: [ | |
| 426 | { key: "mod+j", command: "terminal.toggle" }, | |
| 427 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, | |
| 428 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, | |
| 429 | ], | |
| 418 | 430 | }); |
| 419 | 431 | expect( |
| 420 | 432 | warnSpy.mock.calls.some(([message]) => |
| ... | ... | @@ -445,7 +457,11 @@ describe("WebSocket Server", () => { |
| 445 | 457 | expect(response.error).toBeUndefined(); |
| 446 | 458 | expect(response.result).toEqual({ |
| 447 | 459 | cwd: "/my/workspace", |
| 448 | keybindings: [], | |
| 460 | keybindings: [ | |
| 461 | { key: "mod+j", command: "terminal.toggle" }, | |
| 462 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, | |
| 463 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, | |
| 464 | ], | |
| 449 | 465 | }); |
| 450 | 466 | expect( |
| 451 | 467 | warnSpy.mock.calls.some(([message]) => |
apps/server/src/wsServer.ts+42-11| ... | ... | @@ -70,6 +70,31 @@ function parseBooleanEnv(value: string | undefined): boolean | undefined { |
| 70 | 70 | return undefined; |
| 71 | 71 | } |
| 72 | 72 | |
| 73 | const DEFAULT_KEYBINDINGS: KeybindingsConfig = [ | |
| 74 | { key: "mod+j", command: "terminal.toggle" }, | |
| 75 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, | |
| 76 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, | |
| 77 | ]; | |
| 78 | ||
| 79 | function mergeWithDefaultKeybindings(custom: KeybindingsConfig): KeybindingsConfig { | |
| 80 | if (custom.length === 0) { | |
| 81 | return [...DEFAULT_KEYBINDINGS]; | |
| 82 | } | |
| 83 | ||
| 84 | const overriddenCommands = new Set(custom.map((binding) => binding.command)); | |
| 85 | const retainedDefaults = DEFAULT_KEYBINDINGS.filter( | |
| 86 | (binding) => !overriddenCommands.has(binding.command), | |
| 87 | ); | |
| 88 | const merged = [...retainedDefaults, ...custom]; | |
| 89 | ||
| 90 | if (merged.length <= 256) { | |
| 91 | return merged; | |
| 92 | } | |
| 93 | ||
| 94 | // Keep the latest rules when the config exceeds max size; later rules have higher precedence. | |
| 95 | return merged.slice(-256); | |
| 96 | } | |
| 97 | ||
| 73 | 98 | function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): KeybindingsConfig { |
| 74 | 99 | const configPath = path.join(os.homedir(), ".t3", "keybindings.json"); |
| 75 | 100 | try { |
| ... | ... | @@ -79,7 +104,7 @@ function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): Keybind |
| 79 | 104 | logger.warn("ignoring keybindings config with unsupported format; expected array", { |
| 80 | 105 | path: configPath, |
| 81 | 106 | }); |
| 82 | return []; | |
| 107 | return [...DEFAULT_KEYBINDINGS]; | |
| 83 | 108 | } |
| 84 | 109 | |
| 85 | 110 | const sanitized: KeybindingsConfig = []; |
| ... | ... | @@ -88,13 +113,6 @@ function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): Keybind |
| 88 | 113 | const result = keybindingRuleSchema.safeParse(entry); |
| 89 | 114 | if (result.success) { |
| 90 | 115 | sanitized.push(result.data); |
| 91 | if (sanitized.length >= 256) { | |
| 92 | logger.warn("truncating keybindings config to max entries", { | |
| 93 | path: configPath, | |
| 94 | maxEntries: 256, | |
| 95 | }); | |
| 96 | break; | |
| 97 | } | |
| 98 | 116 | continue; |
| 99 | 117 | } |
| 100 | 118 | invalidEntries += 1; |
| ... | ... | @@ -106,17 +124,30 @@ function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): Keybind |
| 106 | 124 | totalEntries: parsed.length, |
| 107 | 125 | }); |
| 108 | 126 | } |
| 109 | return sanitized; | |
| 127 | const mergedBeforeCap = [ | |
| 128 | ...DEFAULT_KEYBINDINGS.filter( | |
| 129 | (binding) => !new Set(sanitized.map((entry) => entry.command)).has(binding.command), | |
| 130 | ), | |
| 131 | ...sanitized, | |
| 132 | ]; | |
| 133 | const merged = mergeWithDefaultKeybindings(sanitized); | |
| 134 | if (mergedBeforeCap.length > 256) { | |
| 135 | logger.warn("truncating merged keybindings config to max entries", { | |
| 136 | path: configPath, | |
| 137 | maxEntries: 256, | |
| 138 | }); | |
| 139 | } | |
| 140 | return merged; | |
| 110 | 141 | } catch (error) { |
| 111 | 142 | if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { |
| 112 | return []; | |
| 143 | return [...DEFAULT_KEYBINDINGS]; | |
| 113 | 144 | } |
| 114 | 145 | logger.warn("ignoring malformed keybindings config", { |
| 115 | 146 | path: configPath, |
| 116 | 147 | error: error instanceof Error ? error.message : String(error), |
| 117 | 148 | }); |
| 118 | 149 | } |
| 119 | return []; | |
| 150 | return [...DEFAULT_KEYBINDINGS]; | |
| 120 | 151 | } |
| 121 | 152 | |
| 122 | 153 | export function createServer(options: ServerOptions) { |
apps/web/src/components/ChatView.tsx+5-6| ... | ... | @@ -52,11 +52,10 @@ import type { ChatImageAttachment } from "../types"; |
| 52 | 52 | import BranchToolbar from "./BranchToolbar"; |
| 53 | 53 | import GitActionsControl from "./GitActionsControl"; |
| 54 | 54 | import { |
| 55 | DEFAULT_TERMINAL_KEYBINDINGS, | |
| 56 | 55 | isTerminalNewShortcut, |
| 57 | 56 | isTerminalSplitShortcut, |
| 58 | 57 | isTerminalToggleShortcut, |
| 59 | resolveTerminalKeybindings, | |
| 58 | type ResolvedTerminalKeybindings, | |
| 60 | 59 | shortcutLabelForCommand, |
| 61 | 60 | } from "../keybindings"; |
| 62 | 61 | import ChatMarkdown from "./ChatMarkdown"; |
| ... | ... | @@ -152,7 +151,7 @@ export default function ChatView() { |
| 152 | 151 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); |
| 153 | 152 | const [nowTick, setNowTick] = useState(() => Date.now()); |
| 154 | 153 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); |
| 155 | const [terminalKeybindings, setTerminalKeybindings] = useState(DEFAULT_TERMINAL_KEYBINDINGS); | |
| 154 | const [terminalKeybindings, setTerminalKeybindings] = useState<ResolvedTerminalKeybindings>([]); | |
| 156 | 155 | const messagesScrollRef = useRef<HTMLDivElement>(null); |
| 157 | 156 | const messagesEndRef = useRef<HTMLDivElement>(null); |
| 158 | 157 | const shouldAutoScrollRef = useRef(true); |
| ... | ... | @@ -282,7 +281,7 @@ export default function ChatView() { |
| 282 | 281 | |
| 283 | 282 | useEffect(() => { |
| 284 | 283 | if (!api || !api.server || typeof api.server.getConfig !== "function") { |
| 285 | setTerminalKeybindings(DEFAULT_TERMINAL_KEYBINDINGS); | |
| 284 | setTerminalKeybindings([]); | |
| 286 | 285 | return; |
| 287 | 286 | } |
| 288 | 287 | |
| ... | ... | @@ -292,11 +291,11 @@ export default function ChatView() { |
| 292 | 291 | .getConfig() |
| 293 | 292 | .then((config) => { |
| 294 | 293 | if (disposed) return; |
| 295 | setTerminalKeybindings(resolveTerminalKeybindings(config.keybindings)); | |
| 294 | setTerminalKeybindings(config.keybindings); | |
| 296 | 295 | }) |
| 297 | 296 | .catch(() => { |
| 298 | 297 | if (disposed) return; |
| 299 | setTerminalKeybindings(DEFAULT_TERMINAL_KEYBINDINGS); | |
| 298 | setTerminalKeybindings([]); | |
| 300 | 299 | }); |
| 301 | 300 | |
| 302 | 301 | return () => { |
apps/web/src/keybindings.test.ts+26-64| ... | ... | @@ -1,13 +1,12 @@ |
| 1 | 1 | import { describe, expect, it } from "vitest"; |
| 2 | 2 | |
| 3 | import type { KeybindingsConfig } from "@t3tools/contracts"; | |
| 3 | 4 | import { |
| 4 | DEFAULT_TERMINAL_KEYBINDINGS, | |
| 5 | 5 | formatShortcutLabel, |
| 6 | 6 | isTerminalClearShortcut, |
| 7 | 7 | isTerminalNewShortcut, |
| 8 | 8 | isTerminalSplitShortcut, |
| 9 | 9 | isTerminalToggleShortcut, |
| 10 | resolveTerminalKeybindings, | |
| 11 | 10 | shortcutLabelForCommand, |
| 12 | 11 | type ShortcutEventLike, |
| 13 | 12 | } from "./keybindings"; |
| ... | ... | @@ -23,65 +22,55 @@ function event(overrides: Partial<ShortcutEventLike> = {}): ShortcutEventLike { |
| 23 | 22 | }; |
| 24 | 23 | } |
| 25 | 24 | |
| 25 | const DEFAULT_BINDINGS: KeybindingsConfig = [ | |
| 26 | { key: "mod+j", command: "terminal.toggle" }, | |
| 27 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, | |
| 28 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, | |
| 29 | ]; | |
| 30 | ||
| 26 | 31 | describe("isTerminalToggleShortcut", () => { |
| 27 | 32 | it("matches Cmd+J on macOS", () => { |
| 28 | expect( | |
| 29 | isTerminalToggleShortcut(event({ metaKey: true }), DEFAULT_TERMINAL_KEYBINDINGS, { | |
| 30 | platform: "MacIntel", | |
| 31 | }), | |
| 32 | ).toBe(true); | |
| 33 | expect(isTerminalToggleShortcut(event({ metaKey: true }), DEFAULT_BINDINGS, { platform: "MacIntel" })).toBe(true); | |
| 33 | 34 | }); |
| 34 | 35 | |
| 35 | 36 | it("matches Ctrl+J on non-macOS", () => { |
| 36 | expect( | |
| 37 | isTerminalToggleShortcut(event({ ctrlKey: true }), DEFAULT_TERMINAL_KEYBINDINGS, { | |
| 38 | platform: "Win32", | |
| 39 | }), | |
| 40 | ).toBe(true); | |
| 37 | expect(isTerminalToggleShortcut(event({ ctrlKey: true }), DEFAULT_BINDINGS, { platform: "Win32" })).toBe(true); | |
| 41 | 38 | }); |
| 42 | 39 | }); |
| 43 | 40 | |
| 44 | 41 | describe("split/new terminal shortcuts", () => { |
| 45 | 42 | it("requires terminalFocus for default split/new bindings", () => { |
| 46 | 43 | expect( |
| 47 | isTerminalSplitShortcut(event({ key: "d", metaKey: true }), DEFAULT_TERMINAL_KEYBINDINGS, { | |
| 44 | isTerminalSplitShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, { | |
| 48 | 45 | platform: "MacIntel", |
| 49 | 46 | context: { terminalFocus: false }, |
| 50 | 47 | }), |
| 51 | 48 | ).toBe(false); |
| 52 | 49 | expect( |
| 53 | isTerminalNewShortcut( | |
| 54 | event({ key: "d", ctrlKey: true, shiftKey: true }), | |
| 55 | DEFAULT_TERMINAL_KEYBINDINGS, | |
| 56 | { | |
| 57 | platform: "Linux", | |
| 58 | context: { terminalFocus: false }, | |
| 59 | }, | |
| 60 | ), | |
| 50 | isTerminalNewShortcut(event({ key: "d", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, { | |
| 51 | platform: "Linux", | |
| 52 | context: { terminalFocus: false }, | |
| 53 | }), | |
| 61 | 54 | ).toBe(false); |
| 62 | 55 | }); |
| 63 | 56 | |
| 64 | 57 | it("matches split/new when terminalFocus is true", () => { |
| 65 | 58 | expect( |
| 66 | isTerminalSplitShortcut(event({ key: "d", metaKey: true }), DEFAULT_TERMINAL_KEYBINDINGS, { | |
| 59 | isTerminalSplitShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, { | |
| 67 | 60 | platform: "MacIntel", |
| 68 | 61 | context: { terminalFocus: true }, |
| 69 | 62 | }), |
| 70 | 63 | ).toBe(true); |
| 71 | 64 | expect( |
| 72 | isTerminalNewShortcut( | |
| 73 | event({ key: "d", ctrlKey: true, shiftKey: true }), | |
| 74 | DEFAULT_TERMINAL_KEYBINDINGS, | |
| 75 | { | |
| 76 | platform: "Linux", | |
| 77 | context: { terminalFocus: true }, | |
| 78 | }, | |
| 79 | ), | |
| 65 | isTerminalNewShortcut(event({ key: "d", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, { | |
| 66 | platform: "Linux", | |
| 67 | context: { terminalFocus: true }, | |
| 68 | }), | |
| 80 | 69 | ).toBe(true); |
| 81 | 70 | }); |
| 82 | 71 | |
| 83 | it("supports configured overrides with when expressions", () => { | |
| 84 | const keybindings = resolveTerminalKeybindings([ | |
| 72 | it("supports when expressions", () => { | |
| 73 | const keybindings: KeybindingsConfig = [ | |
| 85 | 74 | { |
| 86 | 75 | key: "mod+\\", |
| 87 | 76 | command: "terminal.split", |
| ... | ... | @@ -92,7 +81,8 @@ describe("split/new terminal shortcuts", () => { |
| 92 | 81 | command: "terminal.new", |
| 93 | 82 | when: "terminalOpen && !terminalFocus", |
| 94 | 83 | }, |
| 95 | ]); | |
| 84 | { key: "mod+j", command: "terminal.toggle" }, | |
| 85 | ]; | |
| 96 | 86 | expect( |
| 97 | 87 | isTerminalSplitShortcut(event({ key: "\\", ctrlKey: true }), keybindings, { |
| 98 | 88 | platform: "Win32", |
| ... | ... | @@ -114,41 +104,13 @@ describe("split/new terminal shortcuts", () => { |
| 114 | 104 | }); |
| 115 | 105 | }); |
| 116 | 106 | |
| 117 | describe("resolveTerminalKeybindings", () => { | |
| 118 | it("uses defaults when config is empty", () => { | |
| 119 | expect(resolveTerminalKeybindings(undefined)).toEqual(DEFAULT_TERMINAL_KEYBINDINGS); | |
| 120 | }); | |
| 121 | ||
| 122 | it("replaces defaults for commands configured by user", () => { | |
| 123 | const resolved = resolveTerminalKeybindings([ | |
| 124 | { key: "mod+\\", command: "terminal.split", when: "terminalFocus" }, | |
| 125 | ]); | |
| 126 | ||
| 127 | expect(resolved).toEqual([ | |
| 128 | { key: "mod+j", command: "terminal.toggle" }, | |
| 129 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, | |
| 130 | { key: "mod+\\", command: "terminal.split", when: "terminalFocus" }, | |
| 131 | ]); | |
| 132 | }); | |
| 133 | ||
| 134 | it("ignores invalid configured values", () => { | |
| 135 | expect( | |
| 136 | resolveTerminalKeybindings([ | |
| 137 | { key: " ", command: "terminal.toggle" }, | |
| 138 | { key: "mod+d+f", command: "terminal.split" }, | |
| 139 | ]), | |
| 140 | ).toEqual(DEFAULT_TERMINAL_KEYBINDINGS); | |
| 141 | }); | |
| 142 | }); | |
| 143 | ||
| 144 | 107 | describe("shortcutLabelForCommand", () => { |
| 145 | 108 | it("returns the most recent binding label", () => { |
| 146 | const resolved = resolveTerminalKeybindings([ | |
| 109 | const bindings: KeybindingsConfig = [ | |
| 147 | 110 | { key: "mod+\\", command: "terminal.split", when: "terminalFocus" }, |
| 148 | 111 | { key: "mod+shift+\\", command: "terminal.split", when: "!terminalFocus" }, |
| 149 | ]); | |
| 150 | ||
| 151 | expect(shortcutLabelForCommand(resolved, "terminal.split", "Linux")).toBe("Ctrl+Shift+\\"); | |
| 112 | ]; | |
| 113 | expect(shortcutLabelForCommand(bindings, "terminal.split", "Linux")).toBe("Ctrl+Shift+\\"); | |
| 152 | 114 | }); |
| 153 | 115 | }); |
| 154 | 116 |
apps/web/src/keybindings.ts+4-47| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | import type { KeybindingCommand, KeybindingRule, KeybindingsConfig } from "@t3tools/contracts"; | |
| 1 | import type { KeybindingCommand, KeybindingsConfig } from "@t3tools/contracts"; | |
| 2 | 2 | |
| 3 | 3 | export interface ShortcutEventLike { |
| 4 | 4 | key: string; |
| ... | ... | @@ -14,15 +14,8 @@ export interface ShortcutMatchContext { |
| 14 | 14 | [key: string]: boolean; |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | export type ResolvedTerminalKeybinding = KeybindingRule; | |
| 18 | 17 | export type ResolvedTerminalKeybindings = KeybindingsConfig; |
| 19 | 18 | |
| 20 | export const DEFAULT_TERMINAL_KEYBINDINGS: ResolvedTerminalKeybindings = [ | |
| 21 | { key: "mod+j", command: "terminal.toggle" }, | |
| 22 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, | |
| 23 | { key: "mod+shift+d", command: "terminal.new", when: "terminalFocus" }, | |
| 24 | ]; | |
| 25 | ||
| 26 | 19 | interface ParsedShortcut { |
| 27 | 20 | key: string; |
| 28 | 21 | metaKey: boolean; |
| ... | ... | @@ -306,21 +299,6 @@ function resolveContext(options: ShortcutMatchOptions | undefined): ShortcutMatc |
| 306 | 299 | }; |
| 307 | 300 | } |
| 308 | 301 | |
| 309 | function normalizeConfiguredKeybinding( | |
| 310 | keybinding: KeybindingRule, | |
| 311 | ): ResolvedTerminalKeybinding | null { | |
| 312 | const key = keybinding.key.trim(); | |
| 313 | if (key.length === 0) return null; | |
| 314 | if (!parseShortcutValue(key)) return null; | |
| 315 | ||
| 316 | const when = keybinding.when?.trim(); | |
| 317 | return { | |
| 318 | key, | |
| 319 | command: keybinding.command, | |
| 320 | ...(when ? { when } : {}), | |
| 321 | }; | |
| 322 | } | |
| 323 | ||
| 324 | 302 | function matchesCommandShortcut( |
| 325 | 303 | event: ShortcutEventLike, |
| 326 | 304 | keybindings: ResolvedTerminalKeybindings, |
| ... | ... | @@ -340,27 +318,6 @@ function matchesCommandShortcut( |
| 340 | 318 | return false; |
| 341 | 319 | } |
| 342 | 320 | |
| 343 | export function resolveTerminalKeybindings( | |
| 344 | input: KeybindingsConfig | null | undefined, | |
| 345 | ): ResolvedTerminalKeybindings { | |
| 346 | if (!input || input.length === 0) { | |
| 347 | return [...DEFAULT_TERMINAL_KEYBINDINGS]; | |
| 348 | } | |
| 349 | ||
| 350 | const normalizedInput = input | |
| 351 | .map((binding) => normalizeConfiguredKeybinding(binding)) | |
| 352 | .filter((binding): binding is ResolvedTerminalKeybinding => binding !== null); | |
| 353 | if (normalizedInput.length === 0) { | |
| 354 | return [...DEFAULT_TERMINAL_KEYBINDINGS]; | |
| 355 | } | |
| 356 | ||
| 357 | const overriddenCommands = new Set(normalizedInput.map((binding) => binding.command)); | |
| 358 | const retainedDefaults = DEFAULT_TERMINAL_KEYBINDINGS.filter( | |
| 359 | (binding) => !overriddenCommands.has(binding.command), | |
| 360 | ); | |
| 361 | return [...retainedDefaults, ...normalizedInput]; | |
| 362 | } | |
| 363 | ||
| 364 | 321 | function formatShortcutKeyLabel(key: string): string { |
| 365 | 322 | if (key === " ") return "Space"; |
| 366 | 323 | if (key.length === 1) return key.toUpperCase(); |
| ... | ... | @@ -413,7 +370,7 @@ export function shortcutLabelForCommand( |
| 413 | 370 | |
| 414 | 371 | export function isTerminalToggleShortcut( |
| 415 | 372 | event: ShortcutEventLike, |
| 416 | keybindings: ResolvedTerminalKeybindings = DEFAULT_TERMINAL_KEYBINDINGS, | |
| 373 | keybindings: ResolvedTerminalKeybindings, | |
| 417 | 374 | options?: ShortcutMatchOptions, |
| 418 | 375 | ): boolean { |
| 419 | 376 | return matchesCommandShortcut(event, keybindings, "terminal.toggle", options); |
| ... | ... | @@ -421,7 +378,7 @@ export function isTerminalToggleShortcut( |
| 421 | 378 | |
| 422 | 379 | export function isTerminalSplitShortcut( |
| 423 | 380 | event: ShortcutEventLike, |
| 424 | keybindings: ResolvedTerminalKeybindings = DEFAULT_TERMINAL_KEYBINDINGS, | |
| 381 | keybindings: ResolvedTerminalKeybindings, | |
| 425 | 382 | options?: ShortcutMatchOptions, |
| 426 | 383 | ): boolean { |
| 427 | 384 | return matchesCommandShortcut(event, keybindings, "terminal.split", options); |
| ... | ... | @@ -429,7 +386,7 @@ export function isTerminalSplitShortcut( |
| 429 | 386 | |
| 430 | 387 | export function isTerminalNewShortcut( |
| 431 | 388 | event: ShortcutEventLike, |
| 432 | keybindings: ResolvedTerminalKeybindings = DEFAULT_TERMINAL_KEYBINDINGS, | |
| 389 | keybindings: ResolvedTerminalKeybindings, | |
| 433 | 390 | options?: ShortcutMatchOptions, |
| 434 | 391 | ): boolean { |
| 435 | 392 | return matchesCommandShortcut(event, keybindings, "terminal.new", options); |