Skip to content

Commit 50527a3

Browse files
ThomasK33ammario
andauthored
fix: add mac ctrl behavior overrides for keybinds (#89)
Introduce optional macCtrlBehavior on keybind definitions to control when Control and Command modifiers are accepted on macOS shortcuts. This is so that a cmd+C copies text instead of interrupting the agent. Co-authored-by: Ammar Bandukwala <ammar@ammar.io>
1 parent 7b5291b commit 50527a3

File tree

1 file changed

+68
-12
lines changed

1 file changed

+68
-12
lines changed

src/utils/ui/keybinds.ts

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ export interface Keybind {
1515
shift?: boolean;
1616
alt?: boolean;
1717
meta?: boolean;
18+
/**
19+
* On macOS, Ctrl-based shortcuts traditionally use Cmd instead.
20+
* Use this field to control that behavior:
21+
* - "either" (default): accept Ctrl or Cmd
22+
* - "command": require Cmd specifically
23+
* - "control": require the Control key specifically
24+
*/
25+
macCtrlBehavior?: "either" | "command" | "control";
1826
}
1927

2028
/**
@@ -35,7 +43,7 @@ export function isMac(): boolean {
3543

3644
/**
3745
* Check if a keyboard event matches a keybind definition.
38-
* On macOS, ctrl in the definition matches either ctrl OR meta (Cmd) in the event.
46+
* On macOS, ctrl in the definition defaults to matching Ctrl or Cmd unless overridden.
3947
*/
4048
export function matchesKeybind(
4149
event: React.KeyboardEvent | KeyboardEvent,
@@ -46,23 +54,66 @@ export function matchesKeybind(
4654
return false;
4755
}
4856

49-
// On Mac, treat ctrl and meta as equivalent
50-
const ctrlOrMeta = isMac() ? event.ctrlKey || event.metaKey : event.ctrlKey;
57+
const onMac = isMac();
58+
const macCtrlBehavior = keybind.macCtrlBehavior ?? "either";
59+
const ctrlPressed = event.ctrlKey;
60+
const metaPressed = event.metaKey;
61+
62+
let ctrlRequired = false;
63+
let ctrlAllowed = false;
64+
let metaRequired = keybind.meta ?? false;
65+
let metaAllowed = metaRequired;
66+
67+
if (keybind.ctrl) {
68+
if (onMac) {
69+
switch (macCtrlBehavior) {
70+
case "control": {
71+
ctrlRequired = true;
72+
ctrlAllowed = true;
73+
// Only allow Cmd if explicitly requested via meta flag
74+
break;
75+
}
76+
case "command": {
77+
metaRequired = true;
78+
metaAllowed = true;
79+
ctrlAllowed = true;
80+
break;
81+
}
82+
case "either": {
83+
ctrlAllowed = true;
84+
metaAllowed = true;
85+
if (!ctrlPressed && !metaPressed) return false;
86+
break;
87+
}
88+
}
89+
} else {
90+
ctrlRequired = true;
91+
ctrlAllowed = true;
92+
}
93+
} else {
94+
ctrlAllowed = false;
95+
}
5196

52-
// Check modifiers
53-
if (keybind.ctrl && !ctrlOrMeta) return false;
54-
if (!keybind.ctrl && ctrlOrMeta) return false;
97+
if (ctrlRequired && !ctrlPressed) return false;
98+
if (!ctrlAllowed && ctrlPressed) return false;
5599

56100
if (keybind.shift && !event.shiftKey) return false;
57101
if (!keybind.shift && event.shiftKey) return false;
58102

59103
if (keybind.alt && !event.altKey) return false;
60104
if (!keybind.alt && event.altKey) return false;
61105

62-
// meta is explicit (only check when not handled by ctrl equivalence)
63-
if (!isMac()) {
64-
if (keybind.meta && !event.metaKey) return false;
65-
if (!keybind.meta && event.metaKey) return false;
106+
if (metaRequired && !metaPressed) return false;
107+
108+
if (!metaAllowed) {
109+
// If Cmd is allowed implicitly via ctrl behavior, mark it now
110+
if (onMac && keybind.ctrl && macCtrlBehavior !== "control") {
111+
metaAllowed = true;
112+
}
113+
}
114+
115+
if (!metaAllowed && metaPressed) {
116+
return false;
66117
}
67118

68119
return true;
@@ -92,7 +143,12 @@ export function formatKeybind(keybind: Keybind): string {
92143
// Mac-style formatting with symbols (using Unicode escapes for safety)
93144
// For ctrl on Mac, we actually mean Cmd in most cases since matcher treats them as equivalent
94145
if (keybind.ctrl && !keybind.meta) {
95-
parts.push("\u2318"); // ⌘ Command
146+
const macCtrlBehavior = keybind.macCtrlBehavior ?? "either";
147+
if (macCtrlBehavior === "control") {
148+
parts.push("\u2303"); // ⌃ Control
149+
} else {
150+
parts.push("\u2318"); // ⌘ Command
151+
}
96152
} else if (keybind.ctrl) {
97153
parts.push("\u2303"); // ⌃ Control
98154
}
@@ -134,7 +190,7 @@ export const KEYBINDS = {
134190
CANCEL: { key: "Escape" },
135191

136192
/** Interrupt active stream (destructive - stops AI generation) */
137-
INTERRUPT_STREAM: { key: "c", ctrl: true },
193+
INTERRUPT_STREAM: { key: "c", ctrl: true, macCtrlBehavior: "control" },
138194

139195
/** Focus chat input */
140196
FOCUS_INPUT_I: { key: "i" },

0 commit comments

Comments
 (0)