Why Korean/English key labels can drift

A hardware keyboard has the same blind spot and never suffers from it. The difference is not the protocol — it is how many legends fit on one keycap.

Source baseline: app 1.0.0 (2) · 15 August 2026

Finding. A hardware Bluetooth keyboard does not know the PC's IME mode either. It sends a Korean/English key and never learns whether the mode actually changed. It gets away with this because its keycaps carry the Latin and the Hangul legend at the same time, so the printing never asserts which mode is active. A phone screen has room for one legend per key, so the app must pick one — and picking one turns the label from a map into a claim. The claim rests on an assumption the app has no way to verify.

This note is about the label, not about the typing. Text sent from the input field is compiled into a plan that carries its own mode transitions, and that path is covered in the Bluetooth HID and Korean input note. What follows is specifically about the on-screen keyboard, where each keycap has to decide what to print before the user presses it.

Two legends on one keycap claim nothing

Look at the printing on a Korean hardware keyboard. The Q key carries Q and together, and the shifted forms live there too. The legend is a map: this key is one of these. It stays true no matter which mode the PC is in, because it never committed to a mode in the first place.

That is why mode drift is not an experience on hardware. A user types, sees Latin letters where Hangul was expected, presses the Korean/English key, and types again. Nothing on the keyboard was wrong, so nothing has to be corrected. The keyboard was never the thing being trusted — the screen was.

The blind spot is shared, not unique to the app. Neither a hardware keyboard nor this app can read the host's IME mode. The hardware keyboard simply has no reason to care, because it never displays a mode. The moment a keyboard displays one, the same blind spot becomes visible.

A phone screen fits one legend per key

A letter key in the app has four possible legends: base, shifted, Korean, and shifted Korean — q, Q, , . A phone screen has no room to draw them all. Each keycap is a touch target that a fingertip covers, its label is drawn on a single line, and every label size on the board is derived from one shared measurement — so a keycap crowded with two legends would shrink the legends on every other key along with it.

So the app prints one: Hangul in Korean mode, Latin in English mode. It is not missing information. It holds all four legends and uses all four to compute a label size that will not jitter when the mode or Shift changes, then displays exactly one of them. Space is the entire reason.

And a single legend is no longer a map. alone on a keycap says press this and you get . That is a claim about the host's current state, and it is only as good as the app's assumption about that state.

Bluetooth HID has no channel to check the claim

The app keeps an assumed IME state and drives the keycap legends from it. Nothing sends the real state back. The one thing a host does report to a keyboard is the LED output byte, and the standard boot-keyboard LEDs are Num Lock, Caps Lock, Scroll Lock, Compose, and Kana. There is no Hangul bit. Korean mode is a property of the host's input method, not of the keyboard device the host thinks it is talking to.

The app carries a hook for the case where some host does expose one — a profile may declare which LED bit means Hangul, and a matching report would mark the assumption verified for that session. None of the four shipped profiles declares such a bit, because there is no standard bit to declare. The hook is a place to put a future finding, not a working feedback loop.

How the assumption drifts

Each of these is enough on its own, and none of them produces a visible error at the moment it happens.

CauseWhat happens
The mode-switch key never landsThe app updates its assumption when it sends the switch, not when the host acts on it. One lost switch inverts every label from then on. This is not hypothetical: on Windows the first Hangul consonant after a switch was measured being dropped (빠른 arriving as ㅏ른), which is why the profile now waits 400 ms after a switch before typing.
The profile does not match the hostHosts do not agree on the switch key — right Alt and LANG1 are both in use on Windows, and other platforms use other keys. The app sends the first candidate of the selected profile, falling back to LANG1 when the profile lists none. Send a key the host does not read as a switch and the label flips while the host does not.
A session boundary resets one sideCrossing into a new session — a reconnect, an app process restart, a profile change — discards the verified mark and returns the keycaps to the initial layout for the configured keyboard language. The host's IME keeps whatever it had, and the app no longer holds a verified assumption it could align against.
Toggling has no absolute formThere is no way to command be Korean now; there is only a key that flips whatever the host currently has. Once the two sides disagree, every further toggle preserves the disagreement rather than resolving it.

The user, meanwhile, is pressing the key that says and getting q.

Why Caps Lock was fixable and this is not

Caps Lock had the identical shape of problem, and the app removed it. It does not send Caps Lock to the host at all. It tracks caps as app state and adds Shift to the keys it sends, so what arrives matches what the keycap shows regardless of the host's own Caps Lock. That was a response to a measured failure: when a key release was lost, the host toggled Caps Lock more times than the app did, and the result was text where only the first letter came out capitalized.

The same trick cannot work for Korean. Capitalization is a property of the keystroke, so the app can own it. Hangul composition is not — jamo are assembled into syllables by the host's input method, and the app's only way to produce is to send the physical key that a Korean IME reads as . There is no bypass. Whether that key becomes or q is decided on the host, by state the app cannot own and cannot read.

So the app makes recovery cheap instead

Since drift cannot be prevented, the design goal shifts to making it a two-second correction rather than a puzzle.

One residue worth knowing. When text sent from the input field ends in Hangul, the app marks that send as needing host confirmation: the final syllable sits uncommitted in the host's composition buffer, and the app sends no commit key of its own. Whatever eventually commits it happens on the host, outside anything the app observes.

Limits

Nothing here is fixed by a better app. Closing the loop needs a read-back path that the transport does not define: a standard Hangul indicator in the LED report, or a host that exposes its IME mode to an input device. Until then the keycap legend is the app's best assumption rendered at full size, and the long-press realign exists precisely because a best assumption is not a measurement.