mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-12 07:01:09 -07:00
Fix alt key mappings for mac (#12231)
This commit is contained in:
committed by
GitHub
parent
2e003ad8cf
commit
82c10421a0
@@ -67,3 +67,12 @@ This document lists the available keyboard shortcuts in the Gemini CLI.
|
|||||||
| Shortcut | Description |
|
| Shortcut | Description |
|
||||||
| -------- | --------------------------------- |
|
| -------- | --------------------------------- |
|
||||||
| `Ctrl+G` | See context CLI received from IDE |
|
| `Ctrl+G` | See context CLI received from IDE |
|
||||||
|
|
||||||
|
## Meta+key combos on mac
|
||||||
|
|
||||||
|
On Mac, all Meta+char combos should work normally except for these three which
|
||||||
|
are mapped to special functionality.
|
||||||
|
|
||||||
|
- `meta+b`: "∫" back one word
|
||||||
|
- `meta+f`: "ƒ" forward one word
|
||||||
|
- `meta+m`: "µ" toggle markup view
|
||||||
|
|||||||
@@ -872,99 +872,118 @@ describe('Kitty Sequence Parsing', () => {
|
|||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Terminals to test
|
describe('Cross-terminal Alt key handling (simulating macOS)', () => {
|
||||||
const terminals = ['iTerm2', 'Ghostty', 'MacTerminal', 'VSCodeTerminal'];
|
let originalPlatform: NodeJS.Platform;
|
||||||
|
|
||||||
// Key mappings: letter -> [keycode, accented character]
|
beforeEach(() => {
|
||||||
const keys: Record<string, [number, string]> = {
|
originalPlatform = process.platform;
|
||||||
a: [97, 'å'],
|
Object.defineProperty(process, 'platform', {
|
||||||
o: [111, 'ø'],
|
value: 'darwin',
|
||||||
m: [109, 'µ'],
|
configurable: true,
|
||||||
};
|
|
||||||
|
|
||||||
it.each(
|
|
||||||
terminals.flatMap((terminal) =>
|
|
||||||
Object.entries(keys).map(([key, [keycode, accentedChar]]) => {
|
|
||||||
if (terminal === 'Ghostty') {
|
|
||||||
// Ghostty uses kitty protocol sequences
|
|
||||||
return {
|
|
||||||
terminal,
|
|
||||||
key,
|
|
||||||
chunk: `\x1b[${keycode};3u`,
|
|
||||||
expected: {
|
|
||||||
name: key,
|
|
||||||
ctrl: false,
|
|
||||||
meta: true,
|
|
||||||
shift: false,
|
|
||||||
paste: false,
|
|
||||||
kittyProtocol: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else if (terminal === 'MacTerminal') {
|
|
||||||
// Mac Terminal sends ESC + letter
|
|
||||||
return {
|
|
||||||
terminal,
|
|
||||||
key,
|
|
||||||
kitty: false,
|
|
||||||
chunk: `\x1b${key}`,
|
|
||||||
expected: {
|
|
||||||
sequence: `\x1b${key}`,
|
|
||||||
name: key,
|
|
||||||
ctrl: false,
|
|
||||||
meta: true,
|
|
||||||
shift: false,
|
|
||||||
paste: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// iTerm2 and VSCode send accented characters (å, ø, µ)
|
|
||||||
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode but
|
|
||||||
// gets converted to m with meta:true
|
|
||||||
return {
|
|
||||||
terminal,
|
|
||||||
key,
|
|
||||||
chunk: accentedChar,
|
|
||||||
expected: {
|
|
||||||
name: key,
|
|
||||||
ctrl: false,
|
|
||||||
meta: true, // Always expect meta:true after conversion
|
|
||||||
shift: false,
|
|
||||||
paste: false,
|
|
||||||
sequence: accentedChar,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)(
|
|
||||||
'should handle Alt+$key in $terminal',
|
|
||||||
({
|
|
||||||
chunk,
|
|
||||||
expected,
|
|
||||||
kitty = true,
|
|
||||||
}: {
|
|
||||||
chunk: string;
|
|
||||||
expected: Partial<Key>;
|
|
||||||
kitty?: boolean;
|
|
||||||
}) => {
|
|
||||||
const keyHandler = vi.fn();
|
|
||||||
const testWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<KeypressProvider kittyProtocolEnabled={kitty}>
|
|
||||||
{children}
|
|
||||||
</KeypressProvider>
|
|
||||||
);
|
|
||||||
const { result } = renderHook(() => useKeypressContext(), {
|
|
||||||
wrapper: testWrapper,
|
|
||||||
});
|
});
|
||||||
act(() => result.current.subscribe(keyHandler));
|
});
|
||||||
|
|
||||||
act(() => stdin.write(chunk));
|
afterEach(() => {
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: originalPlatform,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(keyHandler).toHaveBeenCalledWith(
|
// Terminals to test
|
||||||
expect.objectContaining(expected),
|
const terminals = ['iTerm2', 'Ghostty', 'MacTerminal', 'VSCodeTerminal'];
|
||||||
);
|
|
||||||
},
|
// Key mappings: letter -> [keycode, accented character]
|
||||||
);
|
const keys: Record<string, [number, string]> = {
|
||||||
|
b: [98, '\u222B'],
|
||||||
|
f: [102, '\u0192'],
|
||||||
|
m: [109, '\u00B5'],
|
||||||
|
};
|
||||||
|
|
||||||
|
it.each(
|
||||||
|
terminals.flatMap((terminal) =>
|
||||||
|
Object.entries(keys).map(([key, [keycode, accentedChar]]) => {
|
||||||
|
if (terminal === 'Ghostty') {
|
||||||
|
// Ghostty uses kitty protocol sequences
|
||||||
|
return {
|
||||||
|
terminal,
|
||||||
|
key,
|
||||||
|
chunk: `\x1b[${keycode};3u`,
|
||||||
|
expected: {
|
||||||
|
name: key,
|
||||||
|
ctrl: false,
|
||||||
|
meta: true,
|
||||||
|
shift: false,
|
||||||
|
paste: false,
|
||||||
|
kittyProtocol: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (terminal === 'MacTerminal') {
|
||||||
|
// Mac Terminal sends ESC + letter
|
||||||
|
return {
|
||||||
|
terminal,
|
||||||
|
key,
|
||||||
|
kitty: false,
|
||||||
|
chunk: `\x1b${key}`,
|
||||||
|
expected: {
|
||||||
|
sequence: `\x1b${key}`,
|
||||||
|
name: key,
|
||||||
|
ctrl: false,
|
||||||
|
meta: true,
|
||||||
|
shift: false,
|
||||||
|
paste: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// iTerm2 and VSCode send accented characters (å, ø, µ)
|
||||||
|
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode but
|
||||||
|
// gets converted to m with meta:true
|
||||||
|
return {
|
||||||
|
terminal,
|
||||||
|
key,
|
||||||
|
chunk: accentedChar,
|
||||||
|
expected: {
|
||||||
|
name: key,
|
||||||
|
ctrl: false,
|
||||||
|
meta: true, // Always expect meta:true after conversion
|
||||||
|
shift: false,
|
||||||
|
paste: false,
|
||||||
|
sequence: accentedChar,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)(
|
||||||
|
'should handle Alt+$key in $terminal',
|
||||||
|
({
|
||||||
|
chunk,
|
||||||
|
expected,
|
||||||
|
kitty = true,
|
||||||
|
}: {
|
||||||
|
chunk: string;
|
||||||
|
expected: Partial<Key>;
|
||||||
|
kitty?: boolean;
|
||||||
|
}) => {
|
||||||
|
const keyHandler = vi.fn();
|
||||||
|
const testWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<KeypressProvider kittyProtocolEnabled={kitty}>
|
||||||
|
{children}
|
||||||
|
</KeypressProvider>
|
||||||
|
);
|
||||||
|
const { result } = renderHook(() => useKeypressContext(), {
|
||||||
|
wrapper: testWrapper,
|
||||||
|
});
|
||||||
|
act(() => result.current.subscribe(keyHandler));
|
||||||
|
|
||||||
|
act(() => stdin.write(chunk));
|
||||||
|
|
||||||
|
expect(keyHandler).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining(expected),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('Backslash key handling', () => {
|
describe('Backslash key handling', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -48,33 +48,12 @@ export const PASTE_CODE_TIMEOUT_MS = 50; // Flush incomplete paste code after 50
|
|||||||
export const SINGLE_QUOTE = "'";
|
export const SINGLE_QUOTE = "'";
|
||||||
export const DOUBLE_QUOTE = '"';
|
export const DOUBLE_QUOTE = '"';
|
||||||
|
|
||||||
const ALT_KEY_CHARACTER_MAP: Record<string, string> = {
|
// On Mac, hitting alt+char will yield funny characters.
|
||||||
'\u00E5': 'a',
|
// Remap these three since we listen for them.
|
||||||
'\u222B': 'b',
|
const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
|
||||||
'\u00E7': 'c',
|
'\u222B': 'b', // "∫" back one word
|
||||||
'\u2202': 'd',
|
'\u0192': 'f', // "ƒ" forward one word
|
||||||
'\u00B4': 'e',
|
'\u00B5': 'm', // "µ" toggle markup view
|
||||||
'\u0192': 'f',
|
|
||||||
'\u00A9': 'g',
|
|
||||||
'\u02D9': 'h',
|
|
||||||
'\u02C6': 'i',
|
|
||||||
'\u2206': 'j',
|
|
||||||
'\u02DA': 'k',
|
|
||||||
'\u00AC': 'l',
|
|
||||||
'\u00B5': 'm',
|
|
||||||
'\u02DC': 'n',
|
|
||||||
'\u00F8': 'o',
|
|
||||||
'\u03C0': 'p',
|
|
||||||
'\u0153': 'q',
|
|
||||||
'\u00AE': 'r',
|
|
||||||
'\u00DF': 's',
|
|
||||||
'\u2020': 't',
|
|
||||||
'\u00A8': 'u',
|
|
||||||
'\u221A': 'v',
|
|
||||||
'\u2211': 'w',
|
|
||||||
'\u2248': 'x',
|
|
||||||
'\u00A5': 'y',
|
|
||||||
'\u03A9': 'z',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -615,8 +594,8 @@ export function KeypressProvider({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappedLetter = ALT_KEY_CHARACTER_MAP[key.sequence];
|
const mappedLetter = MAC_ALT_KEY_CHARACTER_MAP[key.sequence];
|
||||||
if (mappedLetter && !key.meta) {
|
if (process.platform === 'darwin' && mappedLetter && !key.meta) {
|
||||||
broadcast({
|
broadcast({
|
||||||
name: mappedLetter,
|
name: mappedLetter,
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user