Skip to content

fix(ime): keep toggle alive when activeController is lost in IMK lifecycle races - #20

Merged
hiking90 merged 1 commit into
mainfrom
fix/eventtap-controller-resilience
Jun 23, 2026
Merged

hiking90 merged 1 commit into
mainfrom
fix/eventtap-controller-resilience

Conversation

@hiking90

Copy link
Copy Markdown
Owner

Problem

Occasionally Shift+Space (Korean/English toggle) does nothing — no toggle, no space — most visibly with a fullscreen app such as VSCode. Switching to another window and back restores it.

Root cause

The CGEventTap toggle path's authority is the global KeyEventTap.activeController, maintained via IMK's activateServer/deactivateServer. During fullscreen/Space transitions IMK can deliver these out of order (activate → deactivate on the same instance) or drop the activate, leaving activeController nil while that controller is still the focused IME. The IMK input path doesn't use activeController, so typing still works while only the tap-driven toggle dies. Worse, the Shift+Space handler consumed the event unconditionally, so the key was swallowed with no effect (a black hole) until the next activateServer.

Fix

  • Add lastController (preserved across deactivate) and resolvedController = activeController ?? lastController, gated so the fallback only applies when Ongeul is the active input source (TIS) — preventing a stale controller from eating another IME's Shift+Space or persisting a wrong per-app mode.
  • Shift+Space now consumes only when a live client can apply the toggle; otherwise it passes the event through to IMK's correctly-routed handle() (correct routing + commits in-flight Hangul).
  • Apply the same controller resolution to the Vim-escape (Control+[), CapsLock, modifier-tap, and English-lock entry points.

Notes / scope

  • Engine state is a global singleton, so the toggle's mode flip needs no client; only applying the visual effect does — this is what makes the live-client gate safe.
  • Remaining documented limitation: genuinely multiple live controller instances with a stale English-lock cache (unobserved "case C"); closing it would require frontmost-app-based lock judgment, deferred as the risk outweighs a theoretical edge.

Verification

  • Builds clean (./scripts/build.sh).
  • Hard to reproduce naturally (timing-dependent). Monitoring path:
    log stream --predicate 'subsystem == "io.github.hiking90.inputmethod.Ongeul"' | grep -E "lastController fallback|no live controller"
    
    toggling [lastController fallback] appearing means the bug condition occurred and was auto-recovered.

…cycle races

CGEventTap toggle entry points relied solely on the global activeController, which IMK can leave nil or stale during fullscreen/Space transitions. The Shift+Space handler then consumed the event without toggling — a black hole where the key did nothing until the user re-focused the app.

Resolve the controller via activeController, falling back to lastController (kept across deactivate) only when Ongeul is the active input source, so a stale controller can't eat another IME's Shift+Space or persist a wrong per-app mode. Consume only when a live client can apply the toggle; otherwise pass through to IMK's correctly-routed handle(). Apply the same resolution to the Vim-escape, CapsLock, modifier-tap, and English-lock entry points.
@hiking90
hiking90 merged commit 4f7ce82 into main Jun 23, 2026
2 checks passed
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