Skip to content

Commit 0990274

Browse files
mswiszczclaude
andcommitted
feat: implement VSCode-style -command unbinding for keybindings
Both unbinding mechanisms now work: - "-block:close" prefix removes all default keys for that command - { key: null, command: "block:close" } does the same thing Both append null-handler entries that shadow defaults via reverse iteration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c3c6df1 commit 0990274

3 files changed

Lines changed: 48 additions & 4 deletions

File tree

docs/docs/keybindings.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ Key combinations use colon-separated format:
132132

133133
**Disable a keybinding:** Remove <Kbd k="Cmd:w"/> close block:
134134
```json
135+
[
136+
{ "command": "-block:close" }
137+
]
138+
```
139+
140+
You can also set `key` to `null` to unbind:
141+
```json
135142
[
136143
{ "key": null, "command": "block:close" }
137144
]

frontend/app/store/keymodel.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type ChordActionDef = {
5151
handler: KeyHandler;
5252
};
5353

54-
type KeyMapEntry<T = KeyHandler> = { key: string; handler: T };
54+
type KeyMapEntry<T = KeyHandler> = { key: string; handler: T | null };
5555
type ChordEntry = { key: string; subKeys: KeyMapEntry[] };
5656

5757
const simpleControlShiftAtom = jotai.atom(false);
@@ -442,10 +442,14 @@ async function handleSplitVertical(position: "before" | "after") {
442442
let lastHandledEvent: KeyboardEvent | null = null;
443443

444444
// returns [keymatch, T] — iterates in reverse so later entries (user overrides) win
445+
// a null handler means the key was explicitly unbound (via -command)
445446
function checkKeyArray<T>(waveEvent: WaveKeyboardEvent, entries: KeyMapEntry<T>[]): [string, T] {
446447
for (let i = entries.length - 1; i >= 0; i--) {
447448
const entry = entries[i];
448449
if (keyutil.checkKeyPressed(waveEvent, entry.key)) {
450+
if (entry.handler == null) {
451+
return [null, null]; // unbound
452+
}
449453
return [entry.key, entry.handler];
450454
}
451455
}
@@ -910,9 +914,28 @@ function buildKeyMaps(userOverrides: KeybindingEntry[]): void {
910914
console.warn(`Skipping keybinding entry with invalid key type for command: ${override.command}`);
911915
continue;
912916
}
917+
// Handle -command unbinding (VSCode convention)
918+
if (override.command.startsWith("-")) {
919+
const commandId = override.command.substring(1);
920+
const action = defaultActions.find((a) => a.id === commandId);
921+
if (action) {
922+
// Append null-handler entries for all default keys to shadow them
923+
for (const key of action.defaultKeys) {
924+
bindings.push({ key, handler: null });
925+
}
926+
}
927+
continue;
928+
}
913929
const commandId = override.command;
914930
if (override.key == null) {
915-
continue; // null key = no binding, handled by reverse iteration skipping
931+
// null key = unbind all default keys for this command
932+
const action = defaultActions.find((a) => a.id === commandId);
933+
if (action) {
934+
for (const key of action.defaultKeys) {
935+
bindings.push({ key, handler: null });
936+
}
937+
}
938+
continue;
916939
}
917940
const handler = actionHandlers.get(commandId);
918941
if (handler) {

schema/keybindings.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"command": {
1616
"type": "string",
17-
"description": "Action ID to bind.",
17+
"description": "Action ID to bind. Prefix with '-' to unbind all default keys for that action.",
1818
"enum": [
1919
"tab:new", "tab:close", "tab:prev", "tab:next",
2020
"tab:switchto1", "tab:switchto2", "tab:switchto3", "tab:switchto4", "tab:switchto5",
@@ -29,7 +29,21 @@
2929
"block:splitchord", "block:splitchordup", "block:splitchorddown", "block:splitchordleft", "block:splitchordright",
3030
"app:search", "app:openconnection", "app:toggleaipanel", "app:togglewidgetssidebar",
3131
"term:togglemultiinput",
32-
"generic:cancel"
32+
"generic:cancel",
33+
"-tab:new", "-tab:close", "-tab:prev", "-tab:next",
34+
"-tab:switchto1", "-tab:switchto2", "-tab:switchto3", "-tab:switchto4", "-tab:switchto5",
35+
"-tab:switchto6", "-tab:switchto7", "-tab:switchto8", "-tab:switchto9",
36+
"-block:new", "-block:close", "-block:splitright", "-block:splitdown",
37+
"-block:magnify", "-block:refocus",
38+
"-block:navup", "-block:navdown", "-block:navleft", "-block:navright",
39+
"-block:focusnext", "-block:focusprev",
40+
"-block:switchto1", "-block:switchto2", "-block:switchto3", "-block:switchto4", "-block:switchto5",
41+
"-block:switchto6", "-block:switchto7", "-block:switchto8", "-block:switchto9", "-block:switchtoai",
42+
"-block:replacewithlauncher",
43+
"-block:splitchord", "-block:splitchordup", "-block:splitchorddown", "-block:splitchordleft", "-block:splitchordright",
44+
"-app:search", "-app:openconnection", "-app:toggleaipanel", "-app:togglewidgetssidebar",
45+
"-term:togglemultiinput",
46+
"-generic:cancel"
3347
]
3448
}
3549
},

0 commit comments

Comments
 (0)