code-yeongyu / code-yeongyu/web-terminal

iPhone Edge: Korean never composes — the keyboard delivers bare jamo keydowns that ghostty forwards as-is

Open
#6 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
22
Forks
5
PR merge metrics
No merged PRs in 30d

Description

### Summary

On iPhone Edge, Korean input never composes: typing Hangul produces disconnected compatibility jamo instead of syllables. Every jamo is delivered straight to the PTY as a standalone character and no syllable is ever formed.

Intended vs broken output (literal Hangul):

```
intended: 한글
broken: ㅎㅏㄴㄱㅡㄹ
```

### Environment

- web-terminal `3a73f62`, Bun `1.4.0-canary.1`, served over a cloudflared quick tunnel
- iPhone, iOS 26.5.2, Edge (EdgiOS/151.0), system Korean keyboard (2-beolsik)

### Evidence (captured on the device)

An in-page event recorder shipped to the affected device captured the full input pipeline while typing Hangul:

```
keydown key='ㅎ' keyCode=0 isComposing=false prevented=true textarea ''->'' sel 0->0
keydown key='ㅏ' keyCode=0 isComposing=false prevented=true textarea ''->''
keydown key='ㄴ' keyCode=0 isComposing=false prevented=true textarea ''->''
keydown key='ㄱ' ...
keydown key='ㅡ' ...
keydown key='ㄹ' ...
```

No `compositionstart/update/end`. No `beforeinput`. No `input`. Each jamo arrives as a bare `keydown` with the jamo itself in `event.key` and `keyCode 0`.

### Root cause

ghostty-web's keydown handler treats those keydowns as ordinary printable keys: it encodes `event.key` and sends the bare jamo to the PTY, then calls `preventDefault()` (visible as `prevented=true` above). Because the default is prevented, the character never enters the textarea, the field stays empty, and the OS keyboard never gets the field feedback it would need to compose in-field.

Neither of the repo's existing IME paths covers this delivery mode:

- the composition-event path (desktop IMEs) never fires — there are no composition events;
- the `beforeinput` delete+reinsert path in `ime-input.ts` (iOS Safari-style) never fires — there is no `beforeinput`.

### Fix

Compose client-side. A small standard 2-beolsik automaton intercepts bare compatibility-jamo keydowns on the container capture phase (ahead of ghostty's bubble-phase listener), assembles syllables — compound vowels, compound finals, final-consonant migration (dokkaebi-bul), mid-composition Backspace — and replays the result as synthetic composition events. That is exactly the stream a desktop IME produces, so ghostty's `compositionend` sender and the preedit rendering handle everything downstream without modification.

Activation is self-gating per keystroke: a compatibility-jamo `event.key` with `isComposing === false` never occurs with desktop or Android IMEs, which deliver `Process`/229 keydowns and composition events instead.

Verified by replaying the captured device trace in Playwright WebKit (iPhone profile) and confirmed on the physical device: syllables, compound finals, dokkaebi-bul, backspace and word boundaries all compose correctly, and desktop composition/typing is unaffected.

A PR with the implementation follows.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the keydown handler and the existing beforeinput path in ime-input.ts, then review the captured device trace and replay it in Playwright WebKit with the iPhone profile. Done means Korean syllables, compound vowels and finals, final-consonant migration, mid-composition Backspace, and word boundaries work on iPhone Edge without affecting desktop composition or ordinary typing.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, typescript
Domain
frontend, mobile, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.