Skip to content

fix(router): pass numpad keys through in Hangul mode#1

Merged
hiking90 merged 1 commit into
mainfrom
fix/numpad-passthrough
May 12, 2026
Merged

fix(router): pass numpad keys through in Hangul mode#1
hiking90 merged 1 commit into
mainfrom
fix/numpad-passthrough

Conversation

@hiking90
Copy link
Copy Markdown
Owner

Summary

  • In 3-beolsik layouts (3-390, 3-final) digits 0-9 are mapped to Hangul jamo, so numpad input was being composed as Hangul instead of inserting digits.
  • Route all numpad keys (except Enter, which is intentionally unified with main Enter) to flushAndPassToSystem.
  • Guard with both NSEvent.ModifierFlags.numericPad and the kVK_ANSI_Keypad* keycode range. Arrow keys also carry .numericPad, but they're routed earlier in routeKeyDown, so this check is unambiguous by the time it runs.

Test plan

  • xcodebuild test -scheme OngeulTests -only-testing:OngeulTests/HandleKeyDownRouterTests — 18 tests pass (4 new: numpad digits, numpad operators, numpad Enter regression, main-row digit regression).
  • Manual: switch to 3-final layout, type numpad digits/operators — system inserts raw digits, no Hangul composition.
  • Manual: mid-Hangul composition + numpad key — composition commits, then digit is inserted.
  • Manual: numpad Enter still flushes composition and inserts newline.

In 3-beolsik layouts digits 0-9 map to Hangul jamo, causing numpad
input to be incorrectly composed as Hangul. Route all numpad keys
(except Enter, which remains unified with main Enter) to
flushAndPassToSystem by checking both the .numericPad flag and the
kVK_ANSI_Keypad* keycode range.
@hiking90 hiking90 merged commit 4fe0c29 into main May 12, 2026
2 checks passed
hiking90 added a commit that referenced this pull request May 21, 2026
…an mode)

User report: in 영문→long→short→short sequence, the first short press
correctly exits real Caps Lock (LED off), but the second short press —
intended to toggle to Korean — instead turns the LED back on, looking
to the user like Caps Lock re-activated.

Root cause: design conflict between doc 30 SET semantics (LED ON =
Korean) and doc 32 HID realLockOn (LED ON = real Caps Lock active). In
HID mode, the LED was carrying two meanings:
- LED on for Korean mode (doc 30 SET via setMode/toggleEngineMode)
- LED on for realLockOn (doc 32 explicit setState in enterRealCapsLock)

Visually identical, semantically distinct. After exiting realLockOn and
then toggling to Korean, the LED turned on for the second reason and
the user (correctly) interpreted it as Caps Lock — conflicting with
the macOS convention "Caps Lock LED on = uppercase locked".

Fix: in HID mode, LED only reflects realLockOn. Mode indicator moves to
the menu bar icon entirely.

- InputStateCoordinator.setMode and toggleEngineMode: LED sync gate
  changed from `mode != .hidRealLockOn` to `mode == .cgEventTapAuthority`.
  doc 30 SET semantics still apply when HID isn't active (fallback);
  in HID mode, mode changes don't touch LED.
- OngeulInputController.performExitRealCapsLock: was `setState(korean)`
  (LED on if restored to Korean per doc 30 SET); now unconditionally
  `setState(false)` (LED off — restored mode shows via menu bar icon).
- doc 32 updated: § Doc 30과의 관계 clarifies the LED scope split,
  § 단일 상태 enum module table updated, ASCII state diagram note
  changed, transitions table includes LED column showing OFF/ON
  explicitly, 동작 시나리오 #1 and #4 reworded.

End-user behavior after this fix:
- 영문 → long → short → short → Korean
- LED: off → on(realLockOn) → off(exit) → off(toggle)
- Mode: English → English+caps → English → Korean
- LED never turns on for "just being in Korean mode" — only when real
  Caps Lock is active. Matches macOS convention; resolves user confusion.

Phase 1 PR (#13) unaffected — doc 30 SET continues to apply in the
CGEventTap-only baseline (no HID).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hiking90 added a commit that referenced this pull request May 22, 2026
…ift)

Detailed review of the CapsLock on/off mechanism found one real design
gap: realLockOn was decided "global" (doc 32 §10) but our realLockOn is
coupled to forced-English mode, unlike true macOS Caps Lock which is
orthogonal to input source. Consequence:

  1. App A English → long press → realLockOn (English + caps, LED on)
  2. Cmd+Tab to App B (per-app mode = Korean)
  3. App B activateServer → activateApp → setMode(.korean) → engine.setMode
     runs ungated → engine becomes Korean WHILE realLockOn flag/LED stay on
  4. User types in App B → Korean, not the expected English uppercase
  5. (and a short-tap exit would restore App A's mode into App B)

Fix (option B — session-scoped realLockOn): on deactivateServer, if
realLockOn is active, force-exit it and restore the pre-caps mode before
coordinator.deactivate persists the per-app mode (so English isn't saved
over the user's real mode).

- CapsLockHIDMonitor.exitRealLockForSessionEnd(): resets monitor state
  (mode → .hidToggleAuthority, clears modeBeforeRealLock / timer /
  flags), sets LED off, returns the pre-caps mode. Runs on the IMK
  lifecycle thread (main), so no controller dispatch needed.
- InputStateCoordinator.restoreModeAfterRealLock(_:): sets engine mode
  back (LED untouched via HID gate). Called before deactivate's per-app
  save so the restored mode is what gets persisted.
- OngeulInputController.deactivateServer: invokes the above at the top.

Trade-off: unlike true macOS Caps Lock, ours no longer persists across
app switches. Accepted because of the English-coupling; realLockOn is a
transient "type uppercase now" intent and reverting the source app to
its pre-caps state on session end is cleaner than drift.

doc 32 updated:
- § 동작 시나리오 #10 rewritten (session-scoped exit)
- § 미해결 #1 decision reversed (global → session-scoped, with rationale)
- § 미해결 #8 added (alpha-lock external desync — known limitation)
- § 미해결 #9 added (on/off asymmetry — intentional, documented)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant