Skip to content

Fix cursor langbar - #1895

Open
VimWei wants to merge 18 commits into
rime:masterfrom
VimWei:fix-cursor-langbar
Open

Fix cursor langbar#1895
VimWei wants to merge 18 commits into
rime:masterfrom
VimWei:fix-cursor-langbar

Conversation

@VimWei

@VimWei VimWei commented Jul 12, 2026

Copy link
Copy Markdown

基于 #1889 的进一步修订,增加了两个 commit

光标语言栏按钮不刷新修复

问题

使用 AppIME / im-control / vim-im-select 外部工具切换 RIME/weasel 输入法中英文状态时,光标位置的语言栏按钮(_pLangBarButton)图标不更新——右下角托盘图标和任务栏指示器始终正确,唯独光标附近的语言栏按钮停留在旧状态。

三个层级

层级 位置 更新方式 修复前状态
RIME 引擎 右下角通知区域托盘图标 WeaselServer IPC 始终正确
任务栏"中"/"A"指示器 任务栏系统托盘区 TSF compartment 渲染 始终正确
光标语言栏按钮 光标附近的系统语言栏按钮 _pLangBarButton->UpdateWeaselStatus() 不更新

根因

1. OPENCLOSE handler else 分支盲 toggle + _UpdateLanguageBar 写回 compartment(race)

Win11(_isToOpenClose=false)时 OPENCLOSE else 分支盲目翻转 _status.ascii_mode,然后调 _UpdateLanguageBar 写 CONVERSION compartment。当外部工具(im-control)同时写 compartment 时,两条 OnChange 的到达顺序不确定——若 OPENCLOSE 后到,会覆盖外部写入,产生死锁。

2. CONVERSION handler 在 OnChange 回调中写 compartment 失败

_HandleCompartment(CONVERSION)OnChange 回调内调 _UpdateLanguageBar 写 compartment 会触发 TSF E_UNEXPECTED,导致 LBB 不刷新。

3. _status 陈旧导致方向性 force 缺失

用户 gvim insert 中文模式停顿 2s+ 后 Esc,RIME server 端 ascii_composer 可能已自动切回 ascii=true,但客户端 _status 未通过 DoEditSession 更新仍为 false(中文)。此时 im-control 读 compartment 发现值未变跳过 SetValue,无 OnChange 触发,LBB 停留在"中"。

修复

WeaselTSF 端(本 PR)

WeaselTSF/Compartment.cpp

  • OPENCLOSE if 分支(Win10 _isToOpenClose=true):调 _UpdateLanguageBar 前先从 CONVERSION compartment 同步 _status,避免陈旧状态覆盖 compartment
  • OPENCLOSE else 分支(Win11 _isToOpenClose=false):
    • 删除盲 toggle _status.ascii_mode = !_status.ascii_mode
    • 删除 _HandleLangBarMenuSelect + _UpdateLanguageBar(避免覆盖外部写入)
    • 改为值驱动:读 CONVERSION compartment 同步 _status,仅在 mismatch 时刷 LBB
    • 方向性 force:当 OPENCLOSE 刚被关闭(keyboardJustClosed)且 _status.ascii_mode 仍是中文时,主动切英文(通知 RIME + 写 compartment + 刷 LBB)。覆盖 gvim Esc 场景
  • CONVERSION handler:用 _pLangBarButton->UpdateWeaselStatus(_status) 替代 _UpdateLanguageBar(避免在 OnChange 回调中写 compartment),并调度 CUpdateLangBarEditSession 异步刷新

WeaselTSF/LanguageBar.cpp

  • 新增 _ReconcileCompartment():纯本地比对 _status 与 compartment,mismatch 时同步并刷 LBB。零 IPC、零广播
  • _UpdateLanguageBar_pLangBarButton 空指针保护

WeaselTSF/WeaselTSF.cpp

  • 新增 _InitDeferredWindow / _UninitDeferredWindow / _DeferredWndProc:message-only 隐藏窗口 + 2s SetTimer 周期调 _ReconcileCompartment,作为 OnChange 漏检的安全网
  • OnSetThreadFocus:先 _ReconcileCompartment 再从 server 拉权威态 + _UpdateLanguageBar

WeaselTSF/WeaselTSF.h — 新方法/成员声明
WeaselTSF/stdafx.h — 加 <strsafe.h>StringCchPrintfW

im-control 端(配套 PR)

injector/hook.cpp:OPENCLOSE 重开条件 g_isToOpenClose && conversionModeNative.has_value() && !keyboardOpenClose——不再解引用 *conversionModeNative(只在 -c native 时重开会漏掉 -c alphanumeric 场景导致 Win10 gvim RIME 被禁用)。

VimReader 端(配套修改)

IME.ahk 新增 IME_SetEnglishViaF13()IME_SetAlphanumeric()(im-control 值驱动切状态)+ Send {LControl}(触发前台 WeaselTSF OnKeyDownDoEditSession 拉 server 权威态刷 LBB)。LCtrl 不生成 WM_CHAR(不在任何 app 中插入可见字符),不被 RIME ascii_composer 处理(无 blind toggle 副作用)。

AppIME.ahk:两处触发点改用 IME_SetEnglishViaF13()

场景验证

# 场景 修复点 结果
1 Win10 AppME 闲置 8s 切英文 VimReader LCtrl keystroke path
2 Win10 gvim insert 英文 → Esc im-control OPENCLOSE 重开
2b Win10 gvim insert 中文 → Esc OPENCLOSE if 分支值驱动
3 Win11 AppME 闲置 8s 切英文 VimReader LCtrl keystroke path
4 Win11 gvim Shift 切中 → 快速 Esc RIME server vim_mode 或方向性 force
4b Win11 gvim Shift 切中 → 2s+ 停顿 → Esc OPENCLOSE else 方向性 force(!_status.ascii_mode
5 Win10/Win11 手动 Shift、Ctrl+Space 主功能未改动

设计原则

  1. 值驱动 > 盲 toggle:读 compartment / _status 决定方向,不盲目翻转
  2. 不覆盖外部写入:OPENCLOSE else 分支不调 _UpdateLanguageBar 写 compartment
  3. _isToOpenClose 分支隔离:Win10(if)和 Win11(else)行为独立,互不干扰
  4. 方向性 force 仅在 keyboardJustClosed + Chinese:不破坏 English→Esc、Ctrl+Space 等场景

维护踩坑警示

陷阱 正确做法
OPENCLOSE else 分支盲 toggle _status 读 compartment 同步 _status;仅 keyboardJustClosed && !_status.ascii_mode 时方向性 force
在 OnChange 回调中 _UpdateLanguageBar 写 compartment UpdateWeaselStatus 替代;或 PostMessage 延迟到消息泵
im-control hook *conversionModeNative 解引用 只检查 has_value(),alphanumeric 也需要重开 OPENCLOSE
AppME 用 Send Shift 切英文 Shift 是 RIME blind toggle,改用 im-control + LCtrl keystroke
AppME 用 Send F13 切英文 F13 在 gvim insert mode 显示 <F13> 字面文本,改用 LCtrl
删 2s _ReconcileCompartment 定时器 该定时器是 Win11 gvim OnChange 漏检的安全网,不可删
进程内实例广播解决跨进程 跨进程需走 WeaselServer IPC 或 keystroke path,PostMessage 只在同进程有效

相关文档

  • docs/compartment-external-control-fix.md — 历史 compartment 外部控制修复
  • docs/archives/cursor-indicator-debug.md — 完整调试过程记录(v1-v10 演进)
  • docs/archives/status-icon-debug.md — 早期状态图标探索(术语混淆,保留原始状态)

VimWei added 18 commits July 9, 2026 23:12
- Read TF_CONVERSIONMODE_NATIVE flag from the compartment on
GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION change
- Compare with current _status.ascii_mode and toggle via
_HandleLangBarMenuSelect when they differ
- Use the same IPC path (TrayCommand → SetOption) as the Shift key for
consistent behavior
- Clear pending composition on mode switch to avoid stale preedit

Closes rime#1371
- Replace reading compartment value and comparing with
  _status.ascii_mode by a blind toggle, matching the OPENCLOSE
  handler's Shift-key path. The OnChange notification may deliver
  stale values, making the read-compare approach unreliable.
- Remove _IsKeyboardOpen() guard; call _SetKeyboardOpen(true) to
  ensure the keyboard is active when processing external changes.
- Add _updatingLanguageBar re-entrancy guard in _UpdateLanguageBar
  to prevent _SetCompartmentDWORD from re-triggering the handler.
- Add _SetKeyboardOpen/_EnableLanguageBar calls to match the
  OPENCLOSE handler's else branch.
- Realign _HandleLangBarMenuSelect ternary to one-line condition with
  colon continuation aligned under the question mark, per Chromium
  clang-format style; resolves CI lint violations at lines 275-276
…ndows 11 compatibility

blind toggle (_status.ascii_mode = !_status.ascii_mode) relies on the
_updatingLanguageBar boolean guard catching synchronous OnChange
re-entrancy from _UpdateLanguageBar. On Windows 11, TSF may deliver
OnChange asynchronously or inject extra compartment writes via
TextInputHost.exe, causing the guard to miss and producing an odd
number of toggles (wrong mode).

Replace with reading the actual compartment value and comparing
against _status.ascii_mode. This is naturally idempotent: re-entrant
or delayed OnChange reads the value written by _UpdateLanguageBar,
finds it matches current state, and skips. The _updatingLanguageBar
guard remains as a first layer.

Also update analysis doc with Windows 11 root cause and fix rationale.
Restore conversion-compartment-fix-analysis.md to its original state
(documenting the blind-toggle approach for Windows 10). Create new
windows11-compatibility-fix.md for the value-driven fix that addresses
Windows 11 TSF async callback behavior.
Update windows11-compatibility-fix.md with the finding that Windows 11
TSF only triggers OnChange for writes from activated clients. im-control
was using TF_CLIENTID_NULL, so OnChange was never fired.
…teLanguageBar

Log compartment value, desiredAsciiMode, _status.ascii_mode,
_updatingLanguageBar state, and decision (skip/process) in
_HandleCompartment. Log flags and _updatingLanguageBar state in
_UpdateLanguageBar. Use DebugView to diagnose Windows 11 behavior.
…ingW

Add _DbgInit/_DbgLog helpers that append to
C:\Users\Public\weasel-compartment-debug.log (always-on for diagnosis).
Thread-safe with CRITICAL_SECTION, with timestamps.
Log OPENCLOSE/CONVERSION OnChange entry, decision branches, and
_UpdateLanguageBar guard transitions.
The CONVERSION handler called _SetKeyboardOpen(true) which writes
OPENCLOSE=1 unconditionally. On Windows 11 this triggers the OPENCLOSE
OnChange sink (else branch for _isToOpenClose=false), which blind-
toggles ascii_mode, reversing the mode that CONVERSION just set.

Debug logs confirmed the cascade:
  1. CONVERSION sets ascii 0->1 (correct)
  2. _SetKeyboardOpen(true) writes OPENCLOSE
  3. OPENCLOSE handler toggles ascii 1->0 (wrong)
  4. Final: ascii=0 (Chinese instead of English)

Per Weasel maintainer guidance: OPENCLOSE and CONVERSION are separate
compartments with separate responsibilities. OPENCLOSE manages
enable/disable, CONVERSION manages Chinese/English. The CONVERSION
handler should not touch OPENCLOSE.

Callers (e.g. im-control/vim plugin) always send both -k open and
-c together, so OPENCLOSE is handled by its own handler. When only
CONVERSION is written (keyboard already open), OPENCLOSE handler
doesn't fire and there is no cascade.
Record the ThreadMgr pointer and thread ID when WeaselTSF is activated,
to compare with im-control's TF_GetThreadMgr pointer and determine if
they share the same ThreadMgr instance in Windows Terminal.
The C:\Users\Public path may not be writable from all processes
(e.g. UWP-packaged apps like Windows Terminal). Use GetTempPathA
to write to the per-user temp directory. Also log PID in session
header to distinguish multiple processes.
Remove _DbgInit/_DbgLog infrastructure, all debug log calls in
Compartment.cpp, LanguageBar.cpp, and WeaselTSF.cpp. The production
code is clean and contains only the functional fixes:
- CONVERSION handler is value-driven (reads compartment, compares
  with state, only acts if different)
- CONVERSION handler no longer calls _SetKeyboardOpen(true)
Add three-layer root cause analysis (TF_GetThreadMgr singleton,
TfClientId, OPENCLOSE cascade), document the compartment decoupling
principle, and add deployment note about restarting applications
after DLL replacement.
Merge conversion-compartment-fix-analysis.md (blind toggle, Win10)
and windows11-compatibility-fix.md (value-driven, Win11) into one
coherent document: compartment-external-control-fix.md.

The new document covers all four root causes, the complete fix
across Weasel/im-control/vim-plugin, design principles, DLL
deployment instructions (preserved from original), and verification
results for both Windows 10 and 11.
- Call _SetKeyboardOpen(true) in CONVERSION handler only when
  _isToOpenClose is true (Win10, ToggleImeOnOpenClose=yes), since the
  OPENCLOSE if-branch does not auto-reopen the keyboard
- Skip _SetKeyboardOpen when _isToOpenClose is false (Win11) to avoid
  triggering the OPENCLOSE else-branch blind toggle that cascades and
  reverts the ascii_mode just set by CONVERSION
- Add value-matches branch that also reopens keyboard on Win10 when
  CONVERSION value already matches status but keyboard is closed
- Remove all debug logging (_DbgLog function, s_dbg* state, calls)
- Update compartment-external-control-fix.md with the fifth root cause,
  the registry-based conditional reopen, and Win10/Win11 difference table
When external tools (AppIME, im-control, vim-im-select) switch RIME
ascii_mode via TSF compartment writes, the cursor LangBar button
(_pLangBarButton) stayed stale while tray icon and taskbar indicator
updated correctly.

Root causes and fixes in WeaselTSF compartment handling:

1. OPENCLOSE handler if-branch (Win10, _isToOpenClose=true): sync
   _status.ascii_mode from CONVERSION compartment before calling
   _UpdateLanguageBar, preventing stale _status from overwriting the
   compartment value that an external writer just set.

2. OPENCLOSE handler else-branch (Win11, _isToOpenClose=false):
   replace the historical blind toggle (_status.ascii_mode = !
   _status.ascii_mode) + _HandleLangBarMenuSelect + _UpdateLanguageBar
   with a value-driven approach that reads CONVERSION compartment and
   syncs _status without writing back (avoiding overwrite of external
   writes). Add a directional force: when OPENCLOSE was just closed
   (e.g. gvim Esc) and _status.ascii_mode is still Chinese, force
   _status to English, notify RIME via _HandleLangBarMenuSelect
   (ENABLE_ASCII), and refresh LBB. This covers the gvim insert-
   Chinese -> 2s+ idle -> Esc case where RIME server-side
   ascii_composer may have already auto-toggled but client _status
   is stale. The force is idempotent: im-control -c alphanumeric
   arriving later finds compartment already ~NATIVE and skips
   SetValue.

3. CONVERSION handler: replace _UpdateLanguageBar call (which writes
   compartment inside OnChange callback and triggers TSF
   E_UNEXPECTED) with _pLangBarButton->UpdateWeaselStatus(_status)
   for direct LBB refresh. Schedule CUpdateLangBarEditSession for
   async compartment write outside the callback.

4. Add _ReconcileCompartment() (LanguageBar.cpp): pure local compare
   of _status vs CONVERSION compartment, refreshes LBB on mismatch.
   Zero IPC, zero broadcast. Called by a 2s SetTimer on a
   message-only window (_InitDeferredWindow/_DeferredWndProc) as a
   safety net for missed OnChange events (Win11 gvim race). Also
   called from OnSetThreadFocus before pulling server authoritative
   status.

5. _UpdateLanguageBar: add _pLangBarButton null check.

6. OnSetThreadFocus: call _ReconcileCompartment before the server
   handshake (ProcessKeyEvent(0) + GetResponseData) so stale local
   state is corrected before _UpdateLanguageBar writes compartment.

Files changed:
  WeaselTSF/Compartment.cpp   OPENCLOSE/CONVERSION handler rewrite
  WeaselTSF/LanguageBar.cpp   _ReconcileCompartment + null guard
  WeaselTSF/WeaselTSF.cpp     deferred window + 2s timer + focus reorder
  WeaselTSF/WeaselTSF.h       new method/member declarations
  WeaselTSF/stdafx.h          add <strsafe.h>

Paired with:
  im-control: hook.cpp OPENCLOSE reopen condition (separate repo)
  VimReader:  IME_SetEnglishViaF13 im-control + LCtrl path (separate repo)

See docs/cursor-langbar-fix.md for the scenario verification table and
design principles. Debug process logs are in docs/archives/.
New docs/cursor-langbar-fix.md: concise PR-level summary with root
causes, fixes, scenario verification table, design principles, and
maintenance pitfall warnings.

Archived:
  docs/archives/cursor-indicator-debug.md  full v1-v10 debug process
  docs/archives/status-icon-debug.md       early status icon exploration
  docs/archives/compartment-external-control-fix.md  historical fix

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refines WeaselTSF’s handling of TSF compartment changes to ensure the cursor-adjacent Language Bar Button (LBB, _pLangBarButton) reliably refreshes when external tools (e.g., im-control/AppIME/vim-im-select) switch ASCII/native mode—especially across Win10/Win11 differences and TSF callback constraints.

Changes:

  • Adds deferred/periodic reconciliation via a message-only window to mitigate missed or unsafe compartment updates.
  • Refactors OPENCLOSE and CONVERSION compartment handlers to avoid blind toggles and avoid writing compartments directly inside OnChange paths.
  • Adds documentation describing the root cause and the fix strategy.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
WeaselTSF/WeaselTSF.h Declares new reconciliation/deferred-window helpers and state flags.
WeaselTSF/WeaselTSF.cpp Creates/destroys message-only window, adds timer + deferred update path, adjusts focus refresh flow.
WeaselTSF/stdafx.h Adds <strsafe.h> include.
WeaselTSF/LanguageBar.cpp Adds _ReconcileCompartment() and updates _UpdateLanguageBar() behavior/guards.
WeaselTSF/Compartment.cpp Updates OPENCLOSE/CONVERSION compartment handling; introduces edit-session-based deferred updates.
docs/cursor-langbar-fix.md New user-facing/maintainer documentation for the cursor LBB refresh issue and fix.
docs/compartment-external-control-fix.md Adds extended analysis and rationale around external compartment control behavior.
docs/archives/status-icon-debug.md Adds historical debugging record.
docs/archives/cursor-indicator-debug.md Adds comprehensive archived debugging timeline and experiments.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread WeaselTSF/Compartment.cpp
Comment on lines +327 to +336
com_ptr<CUpdateLangBarEditSession> pSession;
pSession.Attach(new CUpdateLangBarEditSession(this, pContext));
if (pSession) {
HRESULT hr;
pContext->RequestEditSession(
_tfClientId, pSession,
TF_ES_ASYNCDONTCARE | TF_ES_READWRITE, &hr);
pSession.Release();
editSessionScheduled = true;
}
Comment thread WeaselTSF/Compartment.cpp
Comment on lines +267 to +280
DWORD convFlags;
if (SUCCEEDED(_GetCompartmentDWORD(convFlags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)))
_status.ascii_mode = !(convFlags & TF_CONVERSIONMODE_NATIVE);
_UpdateLanguageBar(_status);
} else {
_status.ascii_mode = !_status.ascii_mode;
DWORD ocFlags = 0;
_GetCompartmentDWORD(ocFlags, GUID_COMPARTMENT_KEYBOARD_OPENCLOSE);
BOOL keyboardJustClosed = (ocFlags == 0);
DWORD convFlags = 0;
bool desiredAscii = false;
bool haveConv = SUCCEEDED(_GetCompartmentDWORD(convFlags,
GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION));
if (haveConv)
desiredAscii = !(convFlags & TF_CONVERSIONMODE_NATIVE);
Comment thread WeaselTSF/LanguageBar.cpp
Comment on lines +402 to +415
void WeaselTSF::_ReconcileCompartment() {
DWORD flags;
_GetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION);
bool compartmentAscii = !(flags & TF_CONVERSIONMODE_NATIVE);
if (compartmentAscii != _status.ascii_mode) {
_status.ascii_mode = compartmentAscii;
_HandleLangBarMenuSelect(compartmentAscii
? ID_WEASELTRAY_ENABLE_ASCII
: ID_WEASELTRAY_DISABLE_ASCII);
if (_pLangBarButton) {
_pLangBarButton->UpdateWeaselStatus(_status);
}
}
}
Comment thread WeaselTSF/LanguageBar.cpp
Comment on lines 417 to 423
void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) {
if (!_pLangBarButton)
return;
DWORD flags;
_GetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION);
if (stat.ascii_mode)
flags &= (~TF_CONVERSIONMODE_NATIVE);
else
flags |= TF_CONVERSIONMODE_NATIVE;
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.

2 participants