- 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 behavior
Unified · Split
| @@ -4,6 +4,15 @@ T3 Code reads keybindings from: · 3 unmodified lines | ||
| 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`. · 61 unmodified lines | ||
| 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 | |
| @@ -309,7 +309,11 @@ describe("WebSocket Server", () => { · 308 unmodified lines | ||
| 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", () => { · 63 unmodified lines | ||
| 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", () => { · 28 unmodified lines | ||
| 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", () => { · 24 unmodified lines | ||
| 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]) => |
| @@ -70,6 +70,31 @@ function parseBooleanEnv(value: string | undefined): boolean | undefined { · 69 unmodified lines | ||
| 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 · 3 unmodified lines | ||
| 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 · 2 unmodified lines | ||
| 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 · 5 unmodified lines | ||
| 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) { |
| @@ -52,11 +52,10 @@ import type { ChatImageAttachment } from "../types"; · 51 unmodified lines | ||
| 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() { · 89 unmodified lines | ||
| 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() { · 123 unmodified lines | ||
| 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() { · 3 unmodified lines | ||
| 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 () => { |
| @@ -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 { · 9 unmodified lines | ||
| 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", () => { · 4 unmodified lines | ||
| 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", () => { · 15 unmodified lines | ||
| 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 | |
| @@ -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 { · 9 unmodified lines | ||
| 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 · 277 unmodified lines | ||
| 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( · 13 unmodified lines | ||
| 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( · 46 unmodified lines | ||
| 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( · 1 unmodified line | ||
| 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( · 1 unmodified line | ||
| 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); |