authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 11:30:27-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-15 15:51:05-08:00
logf1cd76c17af2fa9496729ef08056ae69e1f27742
tree07667147cacd92bd25766c607d58750e3238ad96
parent31aa32b83bb234ef7f2bdad6a898d06ea1d0552e

Resolve terminal keybindings on server before sending config

- 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

6 files changed, 108 insertions(+), 134 deletions(-)

KEYBINDINGS.md+11-2
...@@ -4,6 +4,15 @@ T3 Code reads keybindings from:...@@ -4,6 +4,15 @@ T3 Code reads keybindings from:
44
5- `~/.t3/keybindings.json`5- `~/.t3/keybindings.json`
66
7Schema source of truth:
8
9- `packages/contracts/src/keybindings.ts`
10- `packages/contracts/src/server.ts`
11
12Server-side default resolution/merging:
13
14- `apps/server/src/wsServer.ts`
15
7The file must be a JSON array of rules:16The file must be a JSON array of rules:
817
9```json18```json
...@@ -71,13 +80,13 @@ Unknown condition keys evaluate to `false`....@@ -71,13 +80,13 @@ Unknown condition keys evaluate to `false`.
7180
72## Defaults81## Defaults
7382
74Built-in defaults:83Built-in defaults are resolved on the server and sent to the web client via `server.getConfig`:
7584
76- `terminal.toggle`: `mod+j`85- `terminal.toggle`: `mod+j`
77- `terminal.split`: `mod+d` when `terminalFocus`86- `terminal.split`: `mod+d` when `terminalFocus`
78- `terminal.new`: `mod+shift+d` when `terminalFocus`87- `terminal.new`: `mod+shift+d` when `terminalFocus`
7988
80If you define any rules for a command, the default rule for that command is removed.89If you define any rules for a command, the default rule for that command is removed on the server.
8190
82## Precedence91## Precedence
8392
apps/server/src/wsServer.test.ts+20-4
...@@ -309,7 +309,11 @@ describe("WebSocket Server", () => {...@@ -309,7 +309,11 @@ describe("WebSocket Server", () => {
309 expect(response.error).toBeUndefined();309 expect(response.error).toBeUndefined();
310 expect(response.result).toEqual({310 expect(response.result).toEqual({
311 cwd: "/my/workspace",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 });
315319
...@@ -379,7 +383,11 @@ describe("WebSocket Server", () => {...@@ -379,7 +383,11 @@ describe("WebSocket Server", () => {
379 expect(response.error).toBeUndefined();383 expect(response.error).toBeUndefined();
380 expect(response.result).toEqual({384 expect(response.result).toEqual({
381 cwd: "/my/workspace",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 expect(392 expect(
385 warnSpy.mock.calls.some(([message]) =>393 warnSpy.mock.calls.some(([message]) =>
...@@ -414,7 +422,11 @@ describe("WebSocket Server", () => {...@@ -414,7 +422,11 @@ describe("WebSocket Server", () => {
414 expect(response.error).toBeUndefined();422 expect(response.error).toBeUndefined();
415 expect(response.result).toEqual({423 expect(response.result).toEqual({
416 cwd: "/my/workspace",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 expect(431 expect(
420 warnSpy.mock.calls.some(([message]) =>432 warnSpy.mock.calls.some(([message]) =>
...@@ -445,7 +457,11 @@ describe("WebSocket Server", () => {...@@ -445,7 +457,11 @@ describe("WebSocket Server", () => {
445 expect(response.error).toBeUndefined();457 expect(response.error).toBeUndefined();
446 expect(response.result).toEqual({458 expect(response.result).toEqual({
447 cwd: "/my/workspace",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 expect(466 expect(
451 warnSpy.mock.calls.some(([message]) =>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,6 +70,31 @@ function parseBooleanEnv(value: string | undefined): boolean | undefined {
70 return undefined;70 return undefined;
71}71}
7272
73const 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
79function 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
73function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): KeybindingsConfig {98function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): KeybindingsConfig {
74 const configPath = path.join(os.homedir(), ".t3", "keybindings.json");99 const configPath = path.join(os.homedir(), ".t3", "keybindings.json");
75 try {100 try {
...@@ -79,7 +104,7 @@ function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): Keybind...@@ -79,7 +104,7 @@ function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): Keybind
79 logger.warn("ignoring keybindings config with unsupported format; expected array", {104 logger.warn("ignoring keybindings config with unsupported format; expected array", {
80 path: configPath,105 path: configPath,
81 });106 });
82 return [];107 return [...DEFAULT_KEYBINDINGS];
83 }108 }
84109
85 const sanitized: KeybindingsConfig = [];110 const sanitized: KeybindingsConfig = [];
...@@ -88,13 +113,6 @@ function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): Keybind...@@ -88,13 +113,6 @@ function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): Keybind
88 const result = keybindingRuleSchema.safeParse(entry);113 const result = keybindingRuleSchema.safeParse(entry);
89 if (result.success) {114 if (result.success) {
90 sanitized.push(result.data);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 continue;116 continue;
99 }117 }
100 invalidEntries += 1;118 invalidEntries += 1;
...@@ -106,17 +124,30 @@ function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): Keybind...@@ -106,17 +124,30 @@ function readKeybindingsConfig(logger: ReturnType<typeof createLogger>): Keybind
106 totalEntries: parsed.length,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 } catch (error) {141 } catch (error) {
111 if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {142 if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
112 return [];143 return [...DEFAULT_KEYBINDINGS];
113 }144 }
114 logger.warn("ignoring malformed keybindings config", {145 logger.warn("ignoring malformed keybindings config", {
115 path: configPath,146 path: configPath,
116 error: error instanceof Error ? error.message : String(error),147 error: error instanceof Error ? error.message : String(error),
117 });148 });
118 }149 }
119 return [];150 return [...DEFAULT_KEYBINDINGS];
120}151}
121152
122export function createServer(options: ServerOptions) {153export function createServer(options: ServerOptions) {
apps/web/src/components/ChatView.tsx+5-6
...@@ -52,11 +52,10 @@ import type { ChatImageAttachment } from "../types";...@@ -52,11 +52,10 @@ import type { ChatImageAttachment } from "../types";
52import BranchToolbar from "./BranchToolbar";52import BranchToolbar from "./BranchToolbar";
53import GitActionsControl from "./GitActionsControl";53import GitActionsControl from "./GitActionsControl";
54import {54import {
55 DEFAULT_TERMINAL_KEYBINDINGS,
56 isTerminalNewShortcut,55 isTerminalNewShortcut,
57 isTerminalSplitShortcut,56 isTerminalSplitShortcut,
58 isTerminalToggleShortcut,57 isTerminalToggleShortcut,
59 resolveTerminalKeybindings,58 type ResolvedTerminalKeybindings,
60 shortcutLabelForCommand,59 shortcutLabelForCommand,
61} from "../keybindings";60} from "../keybindings";
62import ChatMarkdown from "./ChatMarkdown";61import ChatMarkdown from "./ChatMarkdown";
...@@ -152,7 +151,7 @@ export default function ChatView() {...@@ -152,7 +151,7 @@ export default function ChatView() {
152 const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({});151 const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({});
153 const [nowTick, setNowTick] = useState(() => Date.now());152 const [nowTick, setNowTick] = useState(() => Date.now());
154 const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0);153 const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0);
155 const [terminalKeybindings, setTerminalKeybindings] = useState(DEFAULT_TERMINAL_KEYBINDINGS);154 const [terminalKeybindings, setTerminalKeybindings] = useState<ResolvedTerminalKeybindings>([]);
156 const messagesScrollRef = useRef<HTMLDivElement>(null);155 const messagesScrollRef = useRef<HTMLDivElement>(null);
157 const messagesEndRef = useRef<HTMLDivElement>(null);156 const messagesEndRef = useRef<HTMLDivElement>(null);
158 const shouldAutoScrollRef = useRef(true);157 const shouldAutoScrollRef = useRef(true);
...@@ -282,7 +281,7 @@ export default function ChatView() {...@@ -282,7 +281,7 @@ export default function ChatView() {
282281
283 useEffect(() => {282 useEffect(() => {
284 if (!api || !api.server || typeof api.server.getConfig !== "function") {283 if (!api || !api.server || typeof api.server.getConfig !== "function") {
285 setTerminalKeybindings(DEFAULT_TERMINAL_KEYBINDINGS);284 setTerminalKeybindings([]);
286 return;285 return;
287 }286 }
288287
...@@ -292,11 +291,11 @@ export default function ChatView() {...@@ -292,11 +291,11 @@ export default function ChatView() {
292 .getConfig()291 .getConfig()
293 .then((config) => {292 .then((config) => {
294 if (disposed) return;293 if (disposed) return;
295 setTerminalKeybindings(resolveTerminalKeybindings(config.keybindings));294 setTerminalKeybindings(config.keybindings);
296 })295 })
297 .catch(() => {296 .catch(() => {
298 if (disposed) return;297 if (disposed) return;
299 setTerminalKeybindings(DEFAULT_TERMINAL_KEYBINDINGS);298 setTerminalKeybindings([]);
300 });299 });
301300
302 return () => {301 return () => {
apps/web/src/keybindings.test.ts+26-64
...@@ -1,13 +1,12 @@...@@ -1,13 +1,12 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import type { KeybindingsConfig } from "@t3tools/contracts";
3import {4import {
4 DEFAULT_TERMINAL_KEYBINDINGS,
5 formatShortcutLabel,5 formatShortcutLabel,
6 isTerminalClearShortcut,6 isTerminalClearShortcut,
7 isTerminalNewShortcut,7 isTerminalNewShortcut,
8 isTerminalSplitShortcut,8 isTerminalSplitShortcut,
9 isTerminalToggleShortcut,9 isTerminalToggleShortcut,
10 resolveTerminalKeybindings,
11 shortcutLabelForCommand,10 shortcutLabelForCommand,
12 type ShortcutEventLike,11 type ShortcutEventLike,
13} from "./keybindings";12} from "./keybindings";
...@@ -23,65 +22,55 @@ function event(overrides: Partial<ShortcutEventLike> = {}): ShortcutEventLike {...@@ -23,65 +22,55 @@ function event(overrides: Partial<ShortcutEventLike> = {}): ShortcutEventLike {
23 };22 };
24}23}
2524
25const 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
26describe("isTerminalToggleShortcut", () => {31describe("isTerminalToggleShortcut", () => {
27 it("matches Cmd+J on macOS", () => {32 it("matches Cmd+J on macOS", () => {
28 expect(33 expect(isTerminalToggleShortcut(event({ metaKey: true }), DEFAULT_BINDINGS, { platform: "MacIntel" })).toBe(true);
29 isTerminalToggleShortcut(event({ metaKey: true }), DEFAULT_TERMINAL_KEYBINDINGS, {
30 platform: "MacIntel",
31 }),
32 ).toBe(true);
33 });34 });
3435
35 it("matches Ctrl+J on non-macOS", () => {36 it("matches Ctrl+J on non-macOS", () => {
36 expect(37 expect(isTerminalToggleShortcut(event({ ctrlKey: true }), DEFAULT_BINDINGS, { platform: "Win32" })).toBe(true);
37 isTerminalToggleShortcut(event({ ctrlKey: true }), DEFAULT_TERMINAL_KEYBINDINGS, {
38 platform: "Win32",
39 }),
40 ).toBe(true);
41 });38 });
42});39});
4340
44describe("split/new terminal shortcuts", () => {41describe("split/new terminal shortcuts", () => {
45 it("requires terminalFocus for default split/new bindings", () => {42 it("requires terminalFocus for default split/new bindings", () => {
46 expect(43 expect(
47 isTerminalSplitShortcut(event({ key: "d", metaKey: true }), DEFAULT_TERMINAL_KEYBINDINGS, {44 isTerminalSplitShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, {
48 platform: "MacIntel",45 platform: "MacIntel",
49 context: { terminalFocus: false },46 context: { terminalFocus: false },
50 }),47 }),
51 ).toBe(false);48 ).toBe(false);
52 expect(49 expect(
53 isTerminalNewShortcut(50 isTerminalNewShortcut(event({ key: "d", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
54 event({ key: "d", ctrlKey: true, shiftKey: true }),51 platform: "Linux",
55 DEFAULT_TERMINAL_KEYBINDINGS,52 context: { terminalFocus: false },
56 {53 }),
57 platform: "Linux",
58 context: { terminalFocus: false },
59 },
60 ),
61 ).toBe(false);54 ).toBe(false);
62 });55 });
6356
64 it("matches split/new when terminalFocus is true", () => {57 it("matches split/new when terminalFocus is true", () => {
65 expect(58 expect(
66 isTerminalSplitShortcut(event({ key: "d", metaKey: true }), DEFAULT_TERMINAL_KEYBINDINGS, {59 isTerminalSplitShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, {
67 platform: "MacIntel",60 platform: "MacIntel",
68 context: { terminalFocus: true },61 context: { terminalFocus: true },
69 }),62 }),
70 ).toBe(true);63 ).toBe(true);
71 expect(64 expect(
72 isTerminalNewShortcut(65 isTerminalNewShortcut(event({ key: "d", ctrlKey: true, shiftKey: true }), DEFAULT_BINDINGS, {
73 event({ key: "d", ctrlKey: true, shiftKey: true }),66 platform: "Linux",
74 DEFAULT_TERMINAL_KEYBINDINGS,67 context: { terminalFocus: true },
75 {68 }),
76 platform: "Linux",
77 context: { terminalFocus: true },
78 },
79 ),
80 ).toBe(true);69 ).toBe(true);
81 });70 });
8271
83 it("supports configured overrides with when expressions", () => {72 it("supports when expressions", () => {
84 const keybindings = resolveTerminalKeybindings([73 const keybindings: KeybindingsConfig = [
85 {74 {
86 key: "mod+\\",75 key: "mod+\\",
87 command: "terminal.split",76 command: "terminal.split",
...@@ -92,7 +81,8 @@ describe("split/new terminal shortcuts", () => {...@@ -92,7 +81,8 @@ describe("split/new terminal shortcuts", () => {
92 command: "terminal.new",81 command: "terminal.new",
93 when: "terminalOpen && !terminalFocus",82 when: "terminalOpen && !terminalFocus",
94 },83 },
95 ]);84 { key: "mod+j", command: "terminal.toggle" },
85 ];
96 expect(86 expect(
97 isTerminalSplitShortcut(event({ key: "\\", ctrlKey: true }), keybindings, {87 isTerminalSplitShortcut(event({ key: "\\", ctrlKey: true }), keybindings, {
98 platform: "Win32",88 platform: "Win32",
...@@ -114,41 +104,13 @@ describe("split/new terminal shortcuts", () => {...@@ -114,41 +104,13 @@ describe("split/new terminal shortcuts", () => {
114 });104 });
115});105});
116106
117describe("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
144describe("shortcutLabelForCommand", () => {107describe("shortcutLabelForCommand", () => {
145 it("returns the most recent binding label", () => {108 it("returns the most recent binding label", () => {
146 const resolved = resolveTerminalKeybindings([109 const bindings: KeybindingsConfig = [
147 { key: "mod+\\", command: "terminal.split", when: "terminalFocus" },110 { key: "mod+\\", command: "terminal.split", when: "terminalFocus" },
148 { key: "mod+shift+\\", command: "terminal.split", when: "!terminalFocus" },111 { key: "mod+shift+\\", command: "terminal.split", when: "!terminalFocus" },
149 ]);112 ];
150113 expect(shortcutLabelForCommand(bindings, "terminal.split", "Linux")).toBe("Ctrl+Shift+\\");
151 expect(shortcutLabelForCommand(resolved, "terminal.split", "Linux")).toBe("Ctrl+Shift+\\");
152 });114 });
153});115});
154116
apps/web/src/keybindings.ts+4-47
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import type { KeybindingCommand, KeybindingRule, KeybindingsConfig } from "@t3tools/contracts";1import type { KeybindingCommand, KeybindingsConfig } from "@t3tools/contracts";
22
3export interface ShortcutEventLike {3export interface ShortcutEventLike {
4 key: string;4 key: string;
...@@ -14,15 +14,8 @@ export interface ShortcutMatchContext {...@@ -14,15 +14,8 @@ export interface ShortcutMatchContext {
14 [key: string]: boolean;14 [key: string]: boolean;
15}15}
1616
17export type ResolvedTerminalKeybinding = KeybindingRule;
18export type ResolvedTerminalKeybindings = KeybindingsConfig;17export type ResolvedTerminalKeybindings = KeybindingsConfig;
1918
20export 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
26interface ParsedShortcut {19interface ParsedShortcut {
27 key: string;20 key: string;
28 metaKey: boolean;21 metaKey: boolean;
...@@ -306,21 +299,6 @@ function resolveContext(options: ShortcutMatchOptions | undefined): ShortcutMatc...@@ -306,21 +299,6 @@ function resolveContext(options: ShortcutMatchOptions | undefined): ShortcutMatc
306 };299 };
307}300}
308301
309function 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
324function matchesCommandShortcut(302function matchesCommandShortcut(
325 event: ShortcutEventLike,303 event: ShortcutEventLike,
326 keybindings: ResolvedTerminalKeybindings,304 keybindings: ResolvedTerminalKeybindings,
...@@ -340,27 +318,6 @@ function matchesCommandShortcut(...@@ -340,27 +318,6 @@ function matchesCommandShortcut(
340 return false;318 return false;
341}319}
342320
343export 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
364function formatShortcutKeyLabel(key: string): string {321function formatShortcutKeyLabel(key: string): string {
365 if (key === " ") return "Space";322 if (key === " ") return "Space";
366 if (key.length === 1) return key.toUpperCase();323 if (key.length === 1) return key.toUpperCase();
...@@ -413,7 +370,7 @@ export function shortcutLabelForCommand(...@@ -413,7 +370,7 @@ export function shortcutLabelForCommand(
413370
414export function isTerminalToggleShortcut(371export function isTerminalToggleShortcut(
415 event: ShortcutEventLike,372 event: ShortcutEventLike,
416 keybindings: ResolvedTerminalKeybindings = DEFAULT_TERMINAL_KEYBINDINGS,373 keybindings: ResolvedTerminalKeybindings,
417 options?: ShortcutMatchOptions,374 options?: ShortcutMatchOptions,
418): boolean {375): boolean {
419 return matchesCommandShortcut(event, keybindings, "terminal.toggle", options);376 return matchesCommandShortcut(event, keybindings, "terminal.toggle", options);
...@@ -421,7 +378,7 @@ export function isTerminalToggleShortcut(...@@ -421,7 +378,7 @@ export function isTerminalToggleShortcut(
421378
422export function isTerminalSplitShortcut(379export function isTerminalSplitShortcut(
423 event: ShortcutEventLike,380 event: ShortcutEventLike,
424 keybindings: ResolvedTerminalKeybindings = DEFAULT_TERMINAL_KEYBINDINGS,381 keybindings: ResolvedTerminalKeybindings,
425 options?: ShortcutMatchOptions,382 options?: ShortcutMatchOptions,
426): boolean {383): boolean {
427 return matchesCommandShortcut(event, keybindings, "terminal.split", options);384 return matchesCommandShortcut(event, keybindings, "terminal.split", options);
...@@ -429,7 +386,7 @@ export function isTerminalSplitShortcut(...@@ -429,7 +386,7 @@ export function isTerminalSplitShortcut(
429386
430export function isTerminalNewShortcut(387export function isTerminalNewShortcut(
431 event: ShortcutEventLike,388 event: ShortcutEventLike,
432 keybindings: ResolvedTerminalKeybindings = DEFAULT_TERMINAL_KEYBINDINGS,389 keybindings: ResolvedTerminalKeybindings,
433 options?: ShortcutMatchOptions,390 options?: ShortcutMatchOptions,
434): boolean {391): boolean {
435 return matchesCommandShortcut(event, keybindings, "terminal.new", options);392 return matchesCommandShortcut(event, keybindings, "terminal.new", options);