diff --git a/src/browser/services/KeyboardService.ts b/src/browser/services/KeyboardService.ts index a6150df37a..493f306537 100644 --- a/src/browser/services/KeyboardService.ts +++ b/src/browser/services/KeyboardService.ts @@ -40,7 +40,7 @@ export class KeyboardService implements IKeyboardService { } const kittyFlags = this._coreService.kittyKeyboard.flags; return this.useKitty - ? this._getKittyKeyboard().evaluate(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS) + ? this._getKittyKeyboard().evaluate(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS, isMac && this._optionsService.rawOptions.macOptionIsMeta) : evaluateKeyboardEvent(event, this._coreService.decPrivateModes.applicationCursorKeys, isMac, this._optionsService.rawOptions.macOptionIsMeta); } @@ -51,7 +51,7 @@ export class KeyboardService implements IKeyboardService { } const kittyFlags = this._coreService.kittyKeyboard.flags; if (this.useKitty && (kittyFlags & KittyKeyboardFlags.REPORT_EVENT_TYPES)) { - return this._getKittyKeyboard().evaluate(event, kittyFlags, KittyKeyboardEventType.RELEASE); + return this._getKittyKeyboard().evaluate(event, kittyFlags, KittyKeyboardEventType.RELEASE, isMac && this._optionsService.rawOptions.macOptionIsMeta); } return undefined; } diff --git a/src/common/input/KittyKeyboard.test.ts b/src/common/input/KittyKeyboard.test.ts index b0daf9a7c9..b5aa9d643c 100644 --- a/src/common/input/KittyKeyboard.test.ts +++ b/src/common/input/KittyKeyboard.test.ts @@ -729,5 +729,86 @@ describe('KittyKeyboard', () => { assert.strictEqual(result.key, '\x1b[57440u'); }); }); + + describe('macOS Option as Alt (macOptionIsMeta)', () => { + const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES; + const press = KittyKeyboardEventType.PRESS; + + it('Opt+f (key=ƒ) → CSI 102;3 u', () => { + const result = kitty.evaluate(createEvent({ key: 'ƒ', code: 'KeyF', altKey: true }), flags, press, true); + assert.strictEqual(result.key, '\x1b[102;3u'); + }); + + it('Opt+b (key=∫) → CSI 98;3 u', () => { + const result = kitty.evaluate(createEvent({ key: '∫', code: 'KeyB', altKey: true }), flags, press, true); + assert.strictEqual(result.key, '\x1b[98;3u'); + }); + + it('Opt+d (key=∂) → CSI 100;3 u', () => { + const result = kitty.evaluate(createEvent({ key: '∂', code: 'KeyD', altKey: true }), flags, press, true); + assert.strictEqual(result.key, '\x1b[100;3u'); + }); + + it('Opt+n dead key (key=Dead, code=KeyN) → CSI 110;3 u', () => { + const result = kitty.evaluate(createEvent({ key: 'Dead', code: 'KeyN', altKey: true }), flags, press, true); + assert.strictEqual(result.key, '\x1b[110;3u'); + }); + + it('Opt+e dead key (key=Dead, code=KeyE) → CSI 101;3 u', () => { + const result = kitty.evaluate(createEvent({ key: 'Dead', code: 'KeyE', altKey: true }), flags, press, true); + assert.strictEqual(result.key, '\x1b[101;3u'); + }); + + it('Opt+u dead key (key=Dead, code=KeyU) → CSI 117;3 u', () => { + const result = kitty.evaluate(createEvent({ key: 'Dead', code: 'KeyU', altKey: true }), flags, press, true); + assert.strictEqual(result.key, '\x1b[117;3u'); + }); + + it('Opt+5 (key=∞) → CSI 53;3 u', () => { + const result = kitty.evaluate(createEvent({ key: '∞', code: 'Digit5', altKey: true }), flags, press, true); + assert.strictEqual(result.key, '\x1b[53;3u'); + }); + + it('Opt+Shift+f (key=Ï) → CSI 102;4 u', () => { + const result = kitty.evaluate(createEvent({ key: 'Ï', code: 'KeyF', altKey: true, shiftKey: true }), flags, press, true); + assert.strictEqual(result.key, '\x1b[102;4u'); + }); + + it('Ctrl+Opt+f (key=ƒ) → CSI 102;7 u', () => { + const result = kitty.evaluate(createEvent({ key: 'ƒ', code: 'KeyF', altKey: true, ctrlKey: true }), flags, press, true); + assert.strictEqual(result.key, '\x1b[102;7u'); + }); + + it('does not unwind when macOptionAsAlt is false (Linux Alt is a chord)', () => { + const result = kitty.evaluate(createEvent({ key: 'a', code: 'KeyA', altKey: true }), flags, press, false); + assert.strictEqual(result.key, '\x1b[97;3u'); + }); + + it('does not unwind on Linux AZERTY (key=a, code=KeyQ) — uses ev.key not ev.code', () => { + const result = kitty.evaluate(createEvent({ key: 'a', code: 'KeyQ', altKey: true }), flags, press, false); + assert.strictEqual(result.key, '\x1b[97;3u'); + }); + + it('does not unwind when macOptionAsAlt is false even with composed key', () => { + const result = kitty.evaluate(createEvent({ key: 'ƒ', code: 'KeyF', altKey: true }), flags, press, false); + assert.strictEqual(result.key, '\x1b[402;3u'); + }); + + it('does not unwind when altKey is false', () => { + const result = kitty.evaluate(createEvent({ key: 'ƒ', code: 'KeyF' }), flags, press, true); + assert.strictEqual(result.key, 'ƒ'); + }); + + it('falls through when ev.code is not Key*/Digit* (Opt+;)', () => { + const result = kitty.evaluate(createEvent({ key: '…', code: 'Semicolon', altKey: true }), flags, press, true); + assert.strictEqual(result.key, '\x1b[8230;3u'); + }); + + it('Opt+f release with REPORT_EVENT_TYPES → CSI 102;3:3 u', () => { + const releaseFlags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES | KittyKeyboardFlags.REPORT_EVENT_TYPES; + const result = kitty.evaluate(createEvent({ key: 'ƒ', code: 'KeyF', altKey: true }), releaseFlags, KittyKeyboardEventType.RELEASE, true); + assert.strictEqual(result.key, '\x1b[102;3:3u'); + }); + }); }); }); diff --git a/src/common/input/KittyKeyboard.ts b/src/common/input/KittyKeyboard.ts index f4bfb4ba0e..e34f470705 100644 --- a/src/common/input/KittyKeyboard.ts +++ b/src/common/input/KittyKeyboard.ts @@ -218,7 +218,7 @@ export class KittyKeyboard { * Returns the lowercase codepoint for letters. * For shifted keys, uses the code property to get the base key. */ - private _getKeyCode(ev: IKeyboardEvent): number | undefined { + private _getKeyCode(ev: IKeyboardEvent, macOptionAsAlt: boolean): number | undefined { const numpadCode = this._getNumpadKeyCode(ev); if (numpadCode !== undefined) { return numpadCode; @@ -234,7 +234,7 @@ export class KittyKeyboard { return funcCode; } - if (ev.shiftKey && ev.code) { + if ((ev.shiftKey || (macOptionAsAlt && ev.altKey)) && ev.code) { if (ev.code.startsWith('Digit') && ev.code.length === 6) { const digit = ev.code.charAt(5); if (digit >= '0' && digit <= '9') { @@ -410,12 +410,14 @@ export class KittyKeyboard { * @param ev The keyboard event. * @param flags The active Kitty keyboard enhancement flags. * @param eventType The event type (press, repeat, release). + * @param macOptionAsAlt When true, macOS Option-composed ev.key values are unwound via ev.code. * @returns The keyboard result with the encoded key sequence. */ public evaluate( ev: IKeyboardEvent, flags: number, - eventType: KittyKeyboardEventType = KittyKeyboardEventType.PRESS + eventType: KittyKeyboardEventType = KittyKeyboardEventType.PRESS, + macOptionAsAlt: boolean = false ): IKeyboardResult { const result: IKeyboardResult = { type: KeyboardResultType.SEND_KEY, @@ -464,7 +466,7 @@ export class KittyKeyboard { return result; } - const keyCode = this._getKeyCode(ev); + const keyCode = this._getKeyCode(ev, macOptionAsAlt); if (keyCode === undefined) { return result; }