diff --git a/src/renderer/src/components/terminal-pane/terminal-option-shortcut-policy.ts b/src/renderer/src/components/terminal-pane/terminal-option-shortcut-policy.ts index 0754bfda1bd..2c5899941df 100644 --- a/src/renderer/src/components/terminal-pane/terminal-option-shortcut-policy.ts +++ b/src/renderer/src/components/terminal-pane/terminal-option-shortcut-policy.ts @@ -47,20 +47,20 @@ function createRelease(flags: number): TerminalOptionKittyRelease | undefined { return (flags & KITTY_REPORT_EVENT_TYPES) === 0 ? undefined : { flags } } -// Why ASCII-only: the protocol says a text-producing key sends its text, but #8031 needs Option -// hotkeys to still reach kitty TUIs. ASCII splits the two — layouts hide `@ $ # [ ] { } \ |` behind -// Option with no other way to type them, while the glyphs on TUI-bound keys (π, ƒ, ∫) never are. -function isLayoutComposedAsciiCharacter( +// Compose-side text must survive kitty negotiation; Alt-configured sides still send chords. +function isLayoutComposedCharacter( key: string, characterWithoutOption: string | undefined ): boolean { - if (key.length !== 1) { + const chars = Array.from(key) + if (chars.length !== 1) { return false } - const codePoint = key.codePointAt(0) as number + const codePoint = chars[0].codePointAt(0) return ( + codePoint !== undefined && codePoint > 0x20 && - codePoint <= 0x7e && + !(codePoint >= 0x7f && codePoint <= 0x9f) && (characterWithoutOption === undefined || key.toLowerCase() !== characterWithoutOption.toLowerCase()) ) @@ -131,7 +131,7 @@ export function resolveTerminalOptionShortcutAction( !kittyReportsAllKeysAsEscapeCodes(flags) && canSendComposedText && !isNumpad && - isLayoutComposedAsciiCharacter(event.key, characterWithoutOption) + isLayoutComposedCharacter(event.key, characterWithoutOption) ) { return { type: 'sendInput', data: event.key, optionKittyRelease: createRelease(flags) } } diff --git a/src/renderer/src/components/terminal-pane/terminal-shortcut-option-compose.test.ts b/src/renderer/src/components/terminal-pane/terminal-shortcut-option-compose.test.ts index 74c5763b56e..c369a616787 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shortcut-option-compose.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shortcut-option-compose.test.ts @@ -136,18 +136,73 @@ describe('Option-composed characters in kitty keyboard panes', () => { ).toBeNull() }) - it('still reports non-ASCII Option chords as kitty CSI-u hotkeys', () => { - // #8031: compose layouts must keep reaching TUI Option hotkeys, and every - // glyph those layouts compose on a bound key is non-ASCII. + it('types non-ASCII composed characters instead of reporting chords on a compose side (#20171)', () => { expect(resolveKitty(event({ key: 'ƒ', code: 'KeyF', altKey: true }))).toEqual({ type: 'sendInput', - data: '\x1b[102;3u' + data: 'ƒ' }) expect(resolveKitty(event({ key: '∫', code: 'KeyB', altKey: true }))).toEqual({ type: 'sendInput', - data: '\x1b[98;3u' + data: '∫' }) expect(resolveKitty(event({ key: 'å', code: 'KeyA', altKey: true }))).toEqual({ + type: 'sendInput', + data: 'å' + }) + }) + + it.each([1, 5])('types every Polish letter and uppercase form under kitty flags %s', (flags) => { + const letters = [ + ['a', 'ą'], + ['c', 'ć'], + ['e', 'ę'], + ['l', 'ł'], + ['n', 'ń'], + ['o', 'ó'], + ['s', 'ś'], + ['x', 'ź'], + ['z', 'ż'] + ] + for (const [base, composed] of letters) { + for (const shiftKey of [false, true]) { + const key = shiftKey ? composed.toUpperCase() : composed + const code = `Key${base.toUpperCase()}` + const layout = (candidate: string, shifted: boolean): string | undefined => + candidate === code ? (shifted ? base.toUpperCase() : base) : undefined + for (const [mode, side] of [ + ['false', 0], + ['left', 2], + ['right', 1] + ] as const) { + expect( + resolveKitty(event({ key, code, altKey: true, shiftKey }), mode, side, layout, flags) + ).toEqual({ type: 'sendInput', data: key }) + } + for (const [mode, side] of [ + ['true', 0], + ['left', 1], + ['right', 2] + ] as const) { + expect( + resolveKitty(event({ key, code, altKey: true, shiftKey }), mode, side, layout, flags) + ).toEqual({ + type: 'sendInput', + data: `\x1b[${base.codePointAt(0)}${flags === 5 && shiftKey ? `:${base.toUpperCase().codePointAt(0)}` : ''};${shiftKey ? 4 : 3}u` + }) + } + } + } + }) + + it('counts supplementary-plane compositions as one character, not a chord', () => { + expect(resolveKitty(event({ key: '𝕒', code: 'KeyA', altKey: true }))).toEqual({ + type: 'sendInput', + data: '𝕒' + }) + }) + + it('still reports non-ASCII chords when the user explicitly configures Option as Alt', () => { + expect(resolveKitty(event({ key: 'å', code: 'KeyA', altKey: true }), 'true', 0)).toEqual({ type: 'sendInput', data: '\x1b[97;3u' }) diff --git a/src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts b/src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts index e8168a8309b..2dccf9125c3 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts @@ -759,34 +759,33 @@ describe('kitty keyboard protocol panes', () => { active ) - it('encodes Option+letter as kitty CSI-u with the physical base key in compose mode', () => { - // macOS composition reports key='π' for Option+P on ABC/compose layouts; - // OMP binds alt+p (temporary model) and alt+m (model selector). + it('types Option-composed letters on a compose side instead of chords (#20171)', () => { + // Compose layouts need their letters; TUI hotkeys remain available on configured Alt sides. expect(resolveKitty(event({ key: 'π', code: 'KeyP', altKey: true }))).toEqual({ type: 'sendInput', - data: '\x1b[112;3u' + data: 'π' }) expect(resolveKitty(event({ key: 'µ', code: 'KeyM', altKey: true }))).toEqual({ type: 'sendInput', - data: '\x1b[109;3u' + data: 'µ' }) }) - it('includes shift in the kitty modifier field', () => { + it('types shifted compositions on a compose side instead of chords', () => { expect(resolveKitty(event({ key: '∏', code: 'KeyP', altKey: true, shiftKey: true }))).toEqual({ type: 'sendInput', - data: '\x1b[112;4u' + data: '∏' }) }) - it('encodes Option+digit, punctuation, and configured Alt', () => { + it('types composed digits and punctuation; configured Alt keeps chords', () => { expect(resolveKitty(event({ key: '¡', code: 'Digit1', altKey: true }))).toEqual({ type: 'sendInput', - data: '\x1b[49;3u' + data: '¡' }) expect(resolveKitty(event({ key: '≥', code: 'Period', altKey: true }))).toEqual({ type: 'sendInput', - data: '\x1b[46;3u' + data: '≥' }) expect(resolveKitty(event({ key: 'p', code: 'KeyP', altKey: true }), 'true')).toEqual({ type: 'sendInput', @@ -828,12 +827,11 @@ describe('kitty keyboard protocol panes', () => { ).toEqual({ type: 'sendInput', data: '\x1bb' }) }) - it('encodes the compose-side Option key as kitty CSI-u in left/right modes', () => { - // In 'left' mode the right Option normally composes; a kitty pane asked - // for modifier-accurate keys, so it gets alt-encoded too. + it('types on the compose-side Option; the Alt side keeps CSI-u in left/right modes', () => { + // In 'left' mode the right Option composes and now types its text (#20171). expect(resolveKitty(event({ key: '¬', code: 'KeyL', altKey: true }), 'left', 2)).toEqual({ type: 'sendInput', - data: '\x1b[108;3u' + data: '¬' }) // The designated meta side upgrades from legacy Esc+letter to CSI-u. expect(resolveKitty(event({ key: '¬', code: 'KeyL', altKey: true }), 'left', 1)).toEqual({ @@ -886,23 +884,23 @@ describe('kitty keyboard protocol panes', () => { layoutCharacterForCode ) - // AZERTY types M at the physical Semicolon position; the layout map must - // win over the US punctuation table so the chord reports alt+m, not alt+;. + // AZERTY types M at the physical Semicolon position; the layout map must win over the US + // punctuation table so an uncomposed press still reports alt+m, not alt+;. const azerty = (code: string): string | undefined => (code === 'Semicolon' ? 'm' : undefined) - expect(resolveWithLayout(event({ key: 'µ', code: 'Semicolon', altKey: true }), azerty)).toEqual( + expect(resolveWithLayout(event({ key: 'm', code: 'Semicolon', altKey: true }), azerty)).toEqual( { type: 'sendInput', data: '\x1b[109;3u' } ) // Colemak types P at the physical KeyR position. const colemak = (code: string): string | undefined => (code === 'KeyR' ? 'p' : undefined) - expect(resolveWithLayout(event({ key: 'π', code: 'KeyR', altKey: true }), colemak)).toEqual({ + expect(resolveWithLayout(event({ key: 'p', code: 'KeyR', altKey: true }), colemak)).toEqual({ type: 'sendInput', data: '\x1b[112;3u' }) // Falls back to the US table when the layout map has no entry. const empty = (): string | undefined => undefined - expect(resolveWithLayout(event({ key: 'π', code: 'KeyP', altKey: true }), empty)).toEqual({ + expect(resolveWithLayout(event({ key: 'p', code: 'KeyP', altKey: true }), empty)).toEqual({ type: 'sendInput', data: '\x1b[112;3u' }) diff --git a/src/renderer/src/lib/keyboard-layout/detect-option-as-alt.ts b/src/renderer/src/lib/keyboard-layout/detect-option-as-alt.ts index 3e731e5b9b1..da681a78954 100644 --- a/src/renderer/src/lib/keyboard-layout/detect-option-as-alt.ts +++ b/src/renderer/src/lib/keyboard-layout/detect-option-as-alt.ts @@ -10,13 +10,12 @@ * The only defensible default is the one that varies per layout. This * module fingerprints the active layout from Chromium's * navigator.keyboard.getLayoutMap() (ships in Chrome 69+, so every Electron - * we could run). We match Ghostty's taxonomy: US / US-International map to - * `true`; everything else — including Dvorak, Colemak, UK, every - * international layout — maps to `false`. + * we could run). The base layer cannot separate US from US-International or + * ABC, so every US-shaped layout maps to `true` here and `input-source-id.ts` + * narrows that to plain US whenever macOS gives us the real input source ID. + * Everything else — Dvorak, Colemak, UK, every international layout — maps + * to `false`. * - * Reference implementation in Ghostty: - * ~/projects/ghostty/src/input/keyboard.zig:25-57 (Layout enum + detectOptionAsAlt) - * ~/projects/ghostty/macos/Sources/Helpers/KeyboardLayout.swift (Carbon probe) */ /** Minimal shape of the `KeyboardLayoutMap` we consume, so callers can stub @@ -27,14 +26,13 @@ export type LayoutMapLike = { } export type DetectedLayoutCategory = - /** US Standard or US-International. Default → `'true'` (Option = Alt). */ + /** US-shaped base layer; a native input-source override may still require composition. */ | 'us' /** Any other recognized layout (UK, German, Turkish, French, Dvorak, etc.). * Default → `'false'` (Option composes layout characters). */ | 'non-us' /** API unavailable, empty map, or fingerprint incomplete. Default → - * `'false'` — the conservative safe choice, matching Ghostty's - * `.unknown => .false`. */ + * `'false'` so Option remains available for composition. */ | 'unknown' /** @@ -60,8 +58,8 @@ export type DetectedLayoutCategory = * Colemak passes KeyQ/W/A/Z/Quote/Backquote/BracketLeft/BracketRight but fails * Semicolon (`o` vs `;`). Dvorak fails KeyQ immediately. Both get classified * as `non-us` and default to `'false'`; users who want `'true'` flip the - * explicit override. Matches Ghostty (Ghostty only whitelists - * com.apple.keylayout.US and com.apple.keylayout.USInternational). + * explicit override. The native input-source classifier distinguishes + * plain US from US-shaped composition layouts. */ const US_FINGERPRINT: Record = { KeyQ: 'q', diff --git a/src/renderer/src/lib/keyboard-layout/input-source-id.test.ts b/src/renderer/src/lib/keyboard-layout/input-source-id.test.ts index 407aacdb98d..215d19666a7 100644 --- a/src/renderer/src/lib/keyboard-layout/input-source-id.test.ts +++ b/src/renderer/src/lib/keyboard-layout/input-source-id.test.ts @@ -12,8 +12,8 @@ describe('classifyInputSourceId', () => { expect(classifyInputSourceId('com.apple.keylayout.US')).toBe('meta') }) - it('allowlists US International PC as meta', () => { - expect(classifyInputSourceId('com.apple.keylayout.USInternational-PC')).toBe('meta') + it('classifies US International PC as compose (Option+C → ç repro)', () => { + expect(classifyInputSourceId('com.apple.keylayout.USInternational-PC')).toBe('compose') }) it('is case-insensitive on the allowlist (defaults differ between macOS versions)', () => { @@ -38,9 +38,9 @@ describe('classifyInputSourceId', () => { }) it('classifies every other Apple-shipped layout as compose (default-deny)', () => { - // Matches Ghostty: only US and USInternational-PC are allowlisted; - // everything else (Dvorak, Colemak, German, French, Turkish, Spanish, - // Swedish, every CJK Roman IME) falls back to compose. + // Only plain US is allowlisted; everything else (Dvorak, Colemak, + // German, French, Turkish, Spanish, Swedish, every CJK Roman IME) + // falls back to compose. expect(classifyInputSourceId('com.apple.keylayout.Dvorak')).toBe('compose') expect(classifyInputSourceId('com.apple.keylayout.Colemak')).toBe('compose') expect(classifyInputSourceId('com.apple.keylayout.German')).toBe('compose') diff --git a/src/renderer/src/lib/keyboard-layout/input-source-id.ts b/src/renderer/src/lib/keyboard-layout/input-source-id.ts index d9f873e76e5..4f5022fb739 100644 --- a/src/renderer/src/lib/keyboard-layout/input-source-id.ts +++ b/src/renderer/src/lib/keyboard-layout/input-source-id.ts @@ -1,47 +1,5 @@ -/** - * Classifier for macOS's `AppleCurrentKeyboardLayoutInputSourceID`. - * - * Why this exists alongside detect-option-as-alt: the layout-fingerprint - * probe (`detectOptionAsAltFromLayoutMap`) inspects `navigator.keyboard - * .getLayoutMap()`, which only surfaces the base (unshifted) layer. Many - * macOS layouts keep a US-identical base layer but repurpose the Option - * layer for dead-key composition — ABC (Option+A = å), Polish Pro - * (Option+A = ą), US Extended, ABC Extended, and the CJK Roman IMEs all - * share this trap. The fingerprint classifies them as `'us'`, the - * effective setting resolves to `'true'`, xterm's `macOptionIsMeta` - * turns on, and every Option+letter keystroke is silently translated to - * an Esc+letter readline chord — so typing å, ą, ï, etc., fails with no - * visible feedback (issue #1205). - * - * The only layouts where Option-as-Meta is the right default are plain - * US Standard and US-International-PC — matching Ghostty's - * `detectOptionAsAlt` (~/projects/ghostty/src/input/keyboard.zig:25-57 - * + ~/projects/ghostty/macos/Sources/Helpers/KeyboardLayout.swift, - * which whitelists only `com.apple.keylayout.US` and - * `com.apple.keylayout.USInternational-PC`). - * - * When the main-process IPC returns a non-null ID, this classifier is - * authoritative: `'meta'` → Option-as-Meta is safe; `'compose'` → - * Option must compose. The fingerprint probe is only consulted when no - * ID is available (non-Darwin, sandboxed defaults, IPC failure). - */ - -/** - * Input source IDs where Option-as-Meta is the correct default. The - * shipped US Standard and US-International-PC layouts are the only - * Apple layouts that don't use Option for composition. Everything else - * — including ABC, Polish Pro, US Extended, ABC Extended, every - * international layout, every CJK Roman IME — composes via Option and - * must stay `'false'`. - * - * Matching is case-insensitive full-ID. We deliberately do NOT prefix- - * match here: `com.apple.keylayout.US` must not silently allowlist - * `com.apple.keylayout.USExtended`. - */ -const META_INPUT_SOURCE_IDS: readonly string[] = [ - 'com.apple.keylayout.us', - 'com.apple.keylayout.usinternational-pc' -] +// The base-layer probe cannot distinguish US from layouts whose Option layer composes text. +const META_INPUT_SOURCE_IDS: readonly string[] = ['com.apple.keylayout.us'] export type InputSourceOverride = /** Option-as-Meta is safe on this input source. Resolves to `'us'` @@ -66,11 +24,6 @@ export function classifyInputSourceId(id: string | null | undefined): InputSourc return 'meta' } } - // Why: any other macOS input source ID composes via Option. This - // includes ABC (not to be confused with US), Polish Pro, US Extended, - // ABC Extended, every international layout, Dvorak, Colemak, and - // every CJK Roman IME. Forcing `'compose'` matches Ghostty's - // allowlist-only behavior and prevents the #1205-style silent-swallow - // bug from recurring for any future Apple-shipped layout. + // International layouts need their Option composition layer, including US-International-PC. return 'compose' } diff --git a/src/renderer/src/lib/keyboard-layout/option-as-alt-probe.ts b/src/renderer/src/lib/keyboard-layout/option-as-alt-probe.ts index 5dd2f216b8c..2ae80dff636 100644 --- a/src/renderer/src/lib/keyboard-layout/option-as-alt-probe.ts +++ b/src/renderer/src/lib/keyboard-layout/option-as-alt-probe.ts @@ -159,13 +159,7 @@ export function createOptionAsAltProbe( return } - // Why: when macOS returns a concrete input source ID, it's authoritative. - // The fingerprint can only see the base (unshifted) layer, which is - // US-identical on ABC, Polish Pro, US Extended, ABC Extended, and every - // CJK Roman IME — so trusting it flips macOptionIsMeta=true on all of - // them and silently swallows Option+letter compositions (#1205). The - // Only the two known Option-as-Meta layouts are allowed; every other - // concrete input source keeps Option available for composition. + // Native input-source identity distinguishes composition layouts with a US-shaped base layer. const override = classifyInputSourceId(inputSourceId) if (override === 'meta') { notify('us') diff --git a/tests/e2e/terminal-macos-2set-korean-native.spec.ts b/tests/e2e/terminal-macos-2set-korean-native.spec.ts index e29413f7c95..c4d8892cadb 100644 --- a/tests/e2e/terminal-macos-2set-korean-native.spec.ts +++ b/tests/e2e/terminal-macos-2set-korean-native.spec.ts @@ -119,8 +119,10 @@ async function runNativeScenario( test.describe('Native macOS 2-Set Korean terminal input @headful', () => { test.skip( - process.platform !== 'darwin' || process.env.ORCA_E2E_NATIVE_MACOS_KOREAN !== '1', - 'Requires macOS with 2-Set Korean selected and Accessibility access' + process.platform !== 'darwin' || + process.env.ORCA_E2E_NATIVE_MACOS_KOREAN !== '1' || + process.env.ORCA_E2E_FOREGROUND !== '1', + 'Requires macOS with 2-Set Korean, Accessibility access, and an isolated foreground run' ) test('forwards physical Hangul input as exact PTY bytes', async ({ diff --git a/tests/e2e/terminal-option-composed-ascii.spec.ts b/tests/e2e/terminal-option-composed-ascii.spec.ts index 17f22750c22..93fe9a74b00 100644 --- a/tests/e2e/terminal-option-composed-ascii.spec.ts +++ b/tests/e2e/terminal-option-composed-ascii.spec.ts @@ -1,19 +1,10 @@ -/** - * E2E for issue #14024: on a macOS layout that composes printable ASCII with - * Option (Turkish Q: Option+Q → `@`, Option+4 → `$`), a kitty-keyboard TUI must - * receive the composed character, not the physical Alt chord. - * - * The regression only exists once the pane's application has negotiated the - * kitty keyboard protocol, so the kitty flags are armed the way a real TUI arms - * them — by emitting `CSI > 1 u` from the PTY — rather than by poking renderer - * state. Bytes are asserted at the main-process `pty:write` boundary so the test - * proves what actually leaves the renderer. - */ +// Option composition must survive kitty negotiation (#14024, #20171, #20850). import { test, expect } from './helpers/orca-app' import type { ElectronApplication, Page } from '@stablyai/playwright-test' import { execInTerminal, + waitForTerminalOutput, waitForActiveTerminalManager, waitForActivePanePtyId } from './helpers/terminal' @@ -155,7 +146,7 @@ async function setUpPane( return { ptyId, joinedWrites: async () => (await getPtyWrites(app)).join('') } } -test.describe('Option-composed ASCII in a kitty-keyboard pane', () => { +test.describe('Option-composed text in a kitty-keyboard pane', () => { test.skip(process.platform !== 'darwin', 'Option composition is a macOS-only input path (#14024)') test('types the composed character instead of reporting the physical Alt chord', async ({ @@ -223,9 +214,12 @@ test.describe('Option-composed ASCII in a kitty-keyboard pane', () => { expect(await joinedWrites()).not.toContain('@') }) - test('keeps non-ASCII Option glyphs as TUI hotkeys', async ({ orcaPage, electronApp }) => { + test('keeps non-ASCII Option glyphs as TUI hotkeys when configured as Alt', async ({ + orcaPage, + electronApp + }) => { const { joinedWrites } = await setUpPane(orcaPage, electronApp) - await setMacOptionAsAlt(orcaPage, 'false') + await setMacOptionAsAlt(orcaPage, 'true') await clearPtyWriteLog(electronApp) // #8031: OMP-class TUIs bind Option+P, which composes the non-ASCII `π`. @@ -240,4 +234,101 @@ test.describe('Option-composed ASCII in a kitty-keyboard pane', () => { .toContain('\x1b[112;3u') expect(await joinedWrites()).not.toContain('π') }) + + test('types all Polish letters once under Claude flags and updates the mounted pane setting', async ({ + orcaPage, + electronApp + }) => { + const { ptyId, joinedWrites } = await setUpPane(orcaPage, electronApp) + await execInTerminal(orcaPage, ptyId, `printf '\\033[5u'`) + await expect.poll(() => getPaneKittyKeyboardFlags(orcaPage)).toBe(5) + await setMacOptionAsAlt(orcaPage, 'false') + await clearPtyWriteLog(electronApp) + const letters = [ + ['a', 'ą'], + ['c', 'ć'], + ['e', 'ę'], + ['l', 'ł'], + ['n', 'ń'], + ['o', 'ó'], + ['s', 'ś'], + ['x', 'ź'], + ['z', 'ż'] + ] + for (const [base, key] of letters) { + await pressOptionComposedKey(orcaPage, { key, code: `Key${base.toUpperCase()}` }) + await pressOptionComposedKey(orcaPage, { + key: key.toUpperCase(), + code: `Key${base.toUpperCase()}`, + shiftKey: true + }) + } + await expect.poll(joinedWrites).toBe('ąĄćĆęĘłŁńŃóÓśŚźŹżŻ') + await setMacOptionAsAlt(orcaPage, 'true') + await clearPtyWriteLog(electronApp) + await pressOptionComposedKey(orcaPage, { key: 'ą', code: 'KeyA' }) + await expect.poll(joinedWrites).toBe('\x1b[97;3u') + await setMacOptionAsAlt(orcaPage, 'false') + await clearPtyWriteLog(electronApp) + await pressOptionComposedKey(orcaPage, { key: 'ą', code: 'KeyA' }) + await expect.poll(joinedWrites).toBe('ą') + }) + + test('reports Polish associated text once under report-all flags', async ({ + orcaPage, + electronApp + }) => { + const { ptyId, joinedWrites } = await setUpPane(orcaPage, electronApp) + await execInTerminal(orcaPage, ptyId, `printf '\\033[29u'`) + await expect.poll(() => getPaneKittyKeyboardFlags(orcaPage)).toBe(29) + await setMacOptionAsAlt(orcaPage, 'false') + await clearPtyWriteLog(electronApp) + await pressOptionComposedKey(orcaPage, { key: 'ą', code: 'KeyA' }) + await expect.poll(joinedWrites).toBe('\x1b[97;3;261u') + }) + + test('renders Polish words entered through Chromium keyboard events', async ({ + orcaPage, + electronApp + }, testInfo) => { + const { ptyId, joinedWrites } = await setUpPane(orcaPage, electronApp) + await execInTerminal(orcaPage, ptyId, `printf '\\033[5u'; cat`) + await expect.poll(() => getPaneKittyKeyboardFlags(orcaPage)).toBe(5) + await setMacOptionAsAlt(orcaPage, 'false') + await clearPtyWriteLog(electronApp) + const cdp = await orcaPage.context().newCDPSession(orcaPage) + const bases: Record = { + ą: 'a', + ć: 'c', + ę: 'e', + ł: 'l', + ń: 'n', + ó: 'o', + ś: 's', + ź: 'x', + ż: 'z' + } + const phrase = 'zażółć wcześniej łącznie' + try { + for (const key of phrase) { + const base = bases[key] ?? key + const code = key === ' ' ? 'Space' : `Key${base.toUpperCase()}` + const modifiers = bases[key] ? 1 : 0 + await cdp.send('Input.dispatchKeyEvent', { + type: 'keyDown', + key, + code, + modifiers, + text: key, + unmodifiedText: base + }) + await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key, code, modifiers }) + } + await expect.poll(joinedWrites).toBe(phrase) + await waitForTerminalOutput(orcaPage, phrase) + await orcaPage.screenshot({ path: testInfo.outputPath('polish-words.png') }) + } finally { + await cdp.detach() + } + }) })