Skip to content

fix: 한→영 전환·ABC 입력 소스, 웹 편집기 목록 분리, 입력 소스 중복 수정 - #2

Open
calvinjkim wants to merge 13 commits into
mainfrom
fix/english-switch-abc-bugs
Open

calvinjkim wants to merge 13 commits into
mainfrom
fix/english-switch-abc-bugs

Conversation

@calvinjkim

Copy link
Copy Markdown
Owner

한→영 전환 직후 영어 입력이 깨지던 문제, 웹 편집기에서 한글 조합이 목록을 쪼개던 문제, 설정에 입력 소스가 중복으로 뜨던 문제를 고쳤습니다.

배경과 목적

전환 직후 첫 로마자가 대문자로 들어가고, 꺼 둔 ABC 입력 소스가 되살아나고, Confluence 같은 웹 편집기에서 한 글자가 목록 항목을 갈라놓는 문제가 있었습니다. 공식 PriType 배포와 섞이지 않게 개인 포크의 로컬 overlay 설치 방식으로 이어갑니다.

변경 요약

  • 한→영 전환 직후 첫 글자 대문자 오입력과, 꺼 둔 ABC 입력 소스가 되살아나는 경로를 막았습니다.
  • 웹 편집기에서 한글 조합이 목록 항목을 쪼개던 조합 전달 방식을 호스트별로 분리했습니다.
  • 설정의 입력 소스 중복 행을 없애고, 로컬 overlay 설치가 공식 배포 번들 id를 바꾸지 않도록 유지했습니다.

검증 상태

릴리즈 빌드 성공, 테스트 195개(28개 스위트) 전부 통과했습니다. 실제 IME 동작 확인은 미확인 — 설치 후 한/영 전환과 웹 편집기 입력을 직접 확인해야 합니다.

설치 주의: 서명 인증서가 없어 ad-hoc 서명으로 떨어지므로, 설치할 때마다 시스템 설정 > 개인정보 보호 및 보안 > 손쉬운 사용에서 권한을 껐다 켜야 우측 Command 전환키가 동작합니다.

참고 링크

🤖 Generated with Claude Code

calvinjkim and others added 13 commits September 2, 2026 13:26
Stop treating empty cursor context as a sentence start after a Korean-to-English switch, and stop requesting a disabled ABC layout on every focus change.

Co-authored-by: Cursor <cursoragent@cursor.com>
Web hosts were ending composition on the first choseong, and TIS minted extra PriType rows when the Korean mode id matched the bundle id and Hang+Latn were both advertised.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ive minted Korean.

A new ad-hoc id never appears in Settings, migrating …v2.v2.korean can steal Hangul from 2-Set, and first-mark {caret,0} plus activateServer adapter rebuilds split web compositions.

Co-authored-by: Cursor <cursoragent@cursor.com>
The marked-text path ends a composition on every Hangul syllable. In a
ProseMirror editor (Confluence) an empty block reconciles that composition
end into two blocks, so the first syllable of a line was orphaned and the
rest restarted in a new block: 라이브 arrived as 랑 + newline + 라이브.
On-device logs showed the host caret drifting +2 at that commit, and the
same input was clean in Chromium and under Apple's own Korean IME.

Exempt WebKit browsers from the direct-insertion denial. That denial's
evidence was gathered on Electron/Chromium, which report selection
asynchronously; WebKit does not. Direct insertion never ends a composition,
so ProseMirror has nothing to reconcile. Safari stays a web-content host,
Blink and Gecko stay denied, and the adapter's caret guard still degrades
to marked text when the host reports an unusable selection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Raw U+1100 jamo exists to keep a MARKED composition open in web editors past
the first choseong. A host composing by direct insertion has no composition:
the jamo is written into the document as real text and survives the rewrite.
Typing 사파이어메모 inside existing text in Safari left a bare ᄉ (U+1109)
at the caret.

Gate it on the delivery mode rather than on being a web host. Chromium,
Electron and Atlassian wrappers still compose via marked text and keep it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…write.

The adapter read only selectedRange().location and discarded the length.
Safari reports a whole-field range such as {0, 42} during composition, which
read as "caret at 0", so the in-place rewrite landed at the start of the
field: typing 글자밀어넣기 mid-text duplicated 기 at position 0.

A composition's selection is always collapsed, so a non-zero length means the
host handed us a range and not a caret. Bail to marked text there, the same
degradation the adapter already uses for an unreadable selection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cond relaunch.

ditto merges: a file the build no longer produces stays in the installed bundle
forever. PriType_PriTypeCore.bundle moved to a Contents/ layout and the old flat
copy survived, so codesign reported "a sealed resource is missing or invalid".
A broken seal voids the TCC match against the signing identity, which is what
makes an Accessibility grant survive a rebuild. Stage beside the destination and
rename, so it holds exactly the new bundle and is missing only for the rename
rather than for the whole copy.

Drop the IME from the second killall. It was already killed above; killing it
again makes macOS relaunch it twice, and each launch without a grant opens its
own Settings pane, so installing asked for Accessibility two times.

Add --debug, which builds and installs the debug configuration. DebugLogger is
compiled out of release builds, so a release install records nothing when an
input bug needs tracing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…teals one.

modifierMask maps the left and right key of a modifier to the same CGEventFlags
bit, so releasing the bound key while the other side is held never cleared
toggleModifierIsDown: hold Left Command, tap Right Command, release both, and
every later key event lost its Command — Cmd+C inserted a literal "c" until the
toggle key was pressed again. Read press and release from the device-dependent
NX_DEVICE*KEYMASK bits, which name one physical key, and strip both bits when
passing a key through. The hanja binding had the same defect.

The recorder also accepted a bare printable key. Since the tap consumes the
bound key system-wide, binding Space stopped the space bar everywhere including
in the settings window, so nothing short of `defaults write` could undo it.
Reject those at capture time with a visible reason, and repair an already-saved
one on read: a user who bound Space cannot type a space to fix it. A modifier
key alone, a regular key with a modifier, and a bare function key all still work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… type.

DirectInsertionAdapter can degrade to marked text mid-session and stays the same
object, so three decisions keyed off its type or its host's id were wrong after
that happened.

finalize skipped re-inserting the syllable because direct insertion writes it as
real text. After a fallback it had not: the syllable was only in the host's
marked range, the host dropped that on focus loss, and the typed character was
gone. Bailing also forgot the real text it had already written and drew the same
syllable again as marked text, so the host kept both; remove it first, using our
own record of where the write landed rather than the selection that just proved
unusable.

Raw conjoining jamo is a marked-text device — under direct insertion it is left
in the document as real text. It was gated on the bundle id, which gets both
sides wrong: a host configured for direct insertion still composes with marked
text when the activation probe failed or the adapter degraded, and needs the
jamo then. Take the mode from the adapter in use.

Also make a combo binding match its modifiers exactly. The subset test meant
Control+Space swallowed Control+Command+Space, so the Emoji picker never opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t rects.

Launch and the Settings accessibility check each wired the toggle handlers
separately, and only launch registered onTapFailed — a tap armed from Settings
therefore ran with no IOKit fallback and died silently when the system disabled
it. Move the wiring into KeyMonitoring and call it from both. Arming is
idempotent, so activateServer can re-arm: granting Accessibility more than two
minutes after launch used to leave the toggle key dead for the whole session
while Settings showed it as granted, because the startup poll had already given
up and nothing else started the tap.

capsLockInputSourceSwitchEnabled hit cfprefsd on every event-tap and HID
callback while the key bindings beside it were memoized for exactly that reason.
Hold the answer for a second: a slow lookup there pushes the tap past its
deadline, and three of those disable it for good.

The caret-rect filter required positive coordinates, so a display arranged left
of or below the primary — where screen coordinates are negative — had its valid
rects thrown away and the Hanja window opened on the wrong monitor. Compare
magnitude instead; the filter is for denormals like 1.6e-314, and the sign only
says which display.

Drop the recorder branch in the event tap. Nothing ever set isRecordingKey: the
settings recorder uses an NSEvent local monitor, which sits behind the tap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Typing into the middle of existing text lost the character right after the
caret, the moment the syllable completed — reproducibly in Slack, Claude and
Chrome, and never in Notes or in Safari. The one thing those three share is the
conjoining U+1100 preedit: Notes never used it, and Safari stopped when it moved
to direct insertion. A space after the caret survived, because a space cannot
combine with a jamo. That is what conjoining jamo are defined to do.

They were introduced so a web editor would not read the lone first consonant as
a finished letter and commit it into its own ProseMirror block, splitting a list
item. That split is real, but it needs an empty block and one editor; this loses
a character during ordinary mid-text typing in every Blink host. Route every
preedit through CompositionHelpers.preeditString, which hands the host the
standalone compatibility letter that combines with nothing.

The block split may return in Blink hosts as a result. Safari, where Confluence
is actually used here, composes by direct insertion and is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t does not carry.

main's toggle-key rework overlaps this branch. Where it is the better change it
wins outright: ToggleKeyMonitor replaces the KeyMonitoring helper added here — it
solves the same missing-onTapFailed defect and additionally stops the tap before
handing over to IOKit, which this branch did not — and removing the
TISRomanSwitchState gate entirely supersedes the one-second cache added here,
since the callers on the keystroke path are gone.

Two defects survived that refactor and are re-applied on top of it:

- KeyBinding.modifierFlagMask still maps the left and right key of a modifier to
  one CGEventFlags bit, so the classifier could not tell the bound key's release
  from the other side still being held. Added deviceModifierFlagMask
  (NX_DEVICE*KEYMASK) and detect presses through it; strip both bits, since
  leaving the device bit behind still reads as right Command. The upstream
  classifier tests represented a press with the shared bit alone — real events
  carry both, so the fixtures now do too.
- hasRequiredModifiers was a subset test, so Control+Space also matched
  Control+Command+Space and swallowed the Emoji picker. Compare the bindable
  modifier bits exactly.

Also kept from this branch: the two-minute cap on the launch permission poll,
which main's version dropped; the bare-key binding guard; and the removal of the
dead isRecordingKey branch, which the rewrite reintroduced.

Co-Authored-By: Claude Opus 5 <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