mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(terminal): preserve Polish and Option-composed text in kitty panes (#21082)
* fix(terminal): preserve Option-composed text in kitty panes Adapt the composition fix from #20579 and the input-source correction from #20164. Extend coverage to every Polish letter, live setting changes, associated text, and Chromium-to-PTY word entry. Co-authored-by: yu.xia <yuxianice@163.com> Co-authored-by: Alexandre Blause <alexandre.blause@gmail.com> * test: guard native Korean IME against background launch --------- Co-authored-by: yu.xia <yuxianice@163.com> Co-authored-by: Alexandre Blause <alexandre.blause@gmail.com>
This commit is contained in:
co-authored by
yu.xia
Alexandre Blause
parent
fa4ea57871
commit
8dee68a8d1
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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<string, string> = {
|
||||
KeyQ: 'q',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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[<u\\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[<u\\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[<u\\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<string, string> = {
|
||||
ą: '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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user