feat(capslock): Phase 1 — doc 30 SET semantics (LED ON = Korean)#13
Open
hiking90 wants to merge 3 commits into
Open
feat(capslock): Phase 1 — doc 30 SET semantics (LED ON = Korean)#13hiking90 wants to merge 3 commits into
hiking90 wants to merge 3 commits into
Conversation
…capsLock When CapsLock is configured as the Korean/English toggle key, the IOKit alpha-lock state can briefly remain ON between the physical press and CapsLockSync.forceOff() taking effect. A keyDown arriving in that race window carries .maskAlphaShift, and in English passthrough mode the event reaches the focused app as-is — the first English character after a Korean→English toggle is uppercased, then later keys auto-correct. This is the symptom reported in #10. Strip .maskAlphaShift directly from the event in the CGEventTap callback right after forceOff(). The tap runs before HIToolbox/IME/app, so both the IMK path and the English passthrough path see the corrected flags regardless of forceOff()'s IOKit propagation latency. event.keyboardGetUnicodeString() also picks up the stripped flags, keeping the focus-steal recorded character consistent with what the app receives. Scope is unchanged: only active when toggleKey == .capsLock and maskAlphaShift is set. The flagsChanged CapsLock branch is intentionally left as passthrough so forceOff()'s OFF correction remains visible to downstream state tracking. This is defensive hardening, not a definitive fix for #10. The reported case is environment-dependent (not reproducible on a clean maintainer setup) and likely involves coexisting low-level keyboard monitors (e.g. SokIM) or macOS 26 Tahoe-specific timing. A planned HID-based CapsLock handler will need to gate this strip on a future realCapsLockActive flag. Verified: swiftc -typecheck of all sources passes with no warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ration)
User-facing capslock.md was telling users to "disable CapsLock delay in
System Settings → Keyboard" — but no such UI toggle exists in macOS
(Sequoia/Tahoe). The only legitimate path is the `hidutil` command
`hidutil property --set '{"CapsLockDelayOverride":0}'` from Terminal,
which doesn't persist across reboots (LaunchAgent needed for that).
Replace the misleading instruction with the actual `hidutil` command +
note on persistence. Also clarify the "Use Caps Lock to switch to and
from ABC" disable location (System Settings → Keyboard → Input Sources
→ Edit…) and reframe the LED behavior to match doc 30's planned SET
semantics (LED ON = Korean, LED OFF = English) — implemented in the
follow-up commit on this branch.
Adds design/32-hid-capslock-press-duration.md (design v2) — covers
HID-based CapsLock press-duration detection for macOS platform parity
("short tap = language switch, long press = uppercase", per the Apple
official UI text "Press and hold to enable typing in all uppercase").
This is a forward-looking design that gates on a Phase 1.5 spike
verifying that IOHIDSetModifierLockState(true) actually delivers
uppercase to apps on Tahoe (SokIM evidence suggests it may not, hence
the spike). The doc enumerates A/B/C scenario branches.
Other design/*.md files remain untracked per existing repo convention;
broader tracking decision deferred.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements design/30-capslock-mode-sync.md: CapsLock LED mirrors the current input mode via IOHIDSetModifierLockState. Replaces the previous "always force off" stance with bidirectional SET — LED ON = Korean, LED OFF = English. Mode changes from any path (CapsLock press, other toggle keys, app switch, ESC→English, focus-steal correction, English Lock) auto-sync the LED. Changes: CapsLockSync (rewrite) - Add `setState(_: Bool)` — IOKit SET for either direction - Add `shouldHandle(capsLockOn:)` — `expectedState` + 100ms timeout guard to filter our own setState() echoes from user input - `reset()` for settings-change LED off - `forceOff()` retained as thin alias for backward compat with existing callers (KeyEventTap PR #11 strip path, preferences-panel toggleKey-change defense) InputStateCoordinator - `setMode(_, syncCapsLock: Bool = true)` — LED sync built into every mode change. Default true; explicit `false` for the CapsLock-press path where hardware already toggled state - `toggleEngineMode` — same LED sync (this path skips setMode) - New public `setModeFromCapsLockPress(korean:for:)` — single setter-style entry for CapsLock-press SET semantics (calls setMode with syncCapsLock=false). Per-app mode store updated, Lock guarded. Korean→English transition flushes composing jamo. KeyEventTap flagsChanged CapsLock branch - Replaces "only handle capsLockOn=true + always force off + toggle" with SET semantics: `shouldHandle()` filters echoes, then `performCapsLockModeSet(korean: capsLockOn)`. Event still passed through (hardware already toggled — consuming would desync app's modifier tracking). OngeulInputController - New `performCapsLockModeSet(korean:)` for the CGEventTap callback entry point - `handleFlagsChanged` IMK-fallback CapsLock branch migrated to the same SET semantics (uses `shouldHandle` + `setModeFromCapsLockPress` instead of `toggleMode + forceOff`) Localizable strings (ko/en) - `prefs.capsLockDelay` tooltip corrected: macOS has no UI to disable the Caps Lock delay; hidutil is required (matches docs/.../capslock.md) Note on TICapsLockLanguageSwitchCapable: doc 30 prerequisite step "remove from Info.plist" — verified key is already absent. No change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 21, 2026
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 21, 2026
End-to-end test on user's environment (2026-05-21):
Korean mode
→ long press CapsLock
→ English mode + LED on + uppercase typing works ("HELLO" produces uppercase)
→ short press
→ Korean mode restored + LED off + Korean input works
All four checkpoints passed → doc 32's largest single assumption
("IOHIDSetModifierLockState(handle, kIOHIDCapsLockState, true) actually
delivers uppercase to apps") is confirmed.
Notable: the OS-level stomp behavior SokIM works around with their
0/20/40/.../180ms repeated setState(false) sequence (the "Sonoma 이후
커서 밑에 생기는 버블/HUD/Indicator/Accessory 방지" pattern) was NOT
observed in our environment — a single setState(true) call holds. We
may not need that mitigation, though it's worth re-testing in
constrained environments later.
Also confirmed: the modeBeforeRealLock save/restore logic correctly
gives single-action exit per macOS native parity. The full sequence
("Korean → long → English+caps → short → Korean") is one atomic
gesture from the user's perspective.
Status header updated, § 가장 큰 단일 가정 gains a result subsection.
PR #14 spike gate released — ready to merge in order (#11 → #13 → #14).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements
design/30-capslock-mode-sync.md(previously "2차 검토 완료 — 구현 대기"): the system Caps Lock LED becomes a
mirror of the current input mode — LED ON = Korean, LED OFF = English. Any
mode change (CapsLock press, other toggle keys, per-app restore on app switch,
English Lock, ESC→English, focus-steal correction) auto-syncs the LED via
IOHIDSetModifierLockState. AnexpectedState+ 100 ms guard filters thesoftware's own echo
flagsChangeds from real user input.This PR is the Phase 1 half of the broader CapsLock-toggle work.
Phase 2 (HID-based short/long-press parity) follows as a separate stacked PR.
What changes
OngeulApp/Sources/CapsLockSync.swiftsetState(_: Bool),shouldHandle(capsLockOn:),reset(); oldforceOff()retained as alias (removed in Phase 2 PR).expectedState+ 100 ms timeout guard filters software echoes.OngeulApp/Sources/InputStateCoordinator.swiftsetMode(_, syncCapsLock: Bool = true)andtoggleEngineModenow sync LED whentoggleKey == .capsLock. Default true;syncCapsLock: falsefor the CapsLock-press path where hardware already toggled. New publicsetModeFromCapsLockPress(korean:for:).OngeulApp/Sources/KeyEventTap.swiftshouldHandle()filtering andperformCapsLockModeSet(korean:)dispatch.OngeulApp/Sources/OngeulInputController.swiftperformCapsLockModeSet(korean:). IMK fallbackhandleFlagsChangedCapsLock branch migrated to same SET semantics.OngeulApp/Resources/*.lproj/Localizable.stringsprefs.capsLockDelaytooltip corrected — macOS has no UI to disable the Caps Lock delay;hidutilis required (see capslock.md).docs/src/user/features/capslock.mdhidutil property --set '{"CapsLockDelayOverride":0}'command (with persistence note).design/32-hid-capslock-press-duration.mdIncludes PR #11's keyDown strip
This branch was branched off PR #11, so it contains commit
3acfafc— the equivalent of #11's CapsLock keyDown.maskAlphaShiftstrip (#10 hardening).Recommended order: merge #11 first.
Verification
swiftc -typecheckof all sources (Generated +OngeulApp/Sources/*.swift) passes with zero warnings on this branch. Runtime verification is recommended on Sequoia (ongeul-sequoiarepro VM) and Tahoe (no VM yet — separate Phase 0.5 infrastructure work).Replaces PR #12
PR #12 bundled Phase 1 + Phase 2 in one ~990-line PR. Re-review identified:
IOHIDSetModifierLockState(true)actually delivers uppercase — a methodology violationexitRealCapsLockbroke macOS-native parity (two-action exit vs single-action)commitSettingshad a controller-injection raceexpectedStatetimeout OS-load sensitivityforceOff()alias was a code smellPhase 1 (this PR) is the foundation. Phase 2 (the next PR) is gated on the spike and includes fixes for items 2–5 above. #12 will be closed in favor of this split.
What's NOT in this PR
IOHIDSetModifierLockState(true)→ Phase 2 prerequisite🤖 Generated with Claude Code