From 4f80a6f65482132344ea849f744a71e1e0e9520a Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 2 Jul 2026 23:16:31 +0800 Subject: [PATCH 01/16] fix: respond to TSF conversion mode compartment changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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/weasel#1371 --- WeaselTSF/Compartment.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/WeaselTSF/Compartment.cpp b/WeaselTSF/Compartment.cpp index a45e0d1f90..46969b51a6 100644 --- a/WeaselTSF/Compartment.cpp +++ b/WeaselTSF/Compartment.cpp @@ -268,9 +268,18 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { BOOL isOpen = _IsKeyboardOpen(); if (isOpen) { - weasel::ResponseParser parser(NULL, NULL, &_status, NULL, - &_cand->style()); - bool ok = m_client.GetResponseData(std::ref(parser)); + DWORD convMode = 0; + _GetCompartmentDWORD(convMode, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); + bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); + if (desiredAsciiMode != _status.ascii_mode) { + _status.ascii_mode = desiredAsciiMode; + _HandleLangBarMenuSelect(_status.ascii_mode + ? ID_WEASELTRAY_ENABLE_ASCII + : ID_WEASELTRAY_DISABLE_ASCII); + if (_pEditSessionContext) + m_client.ClearComposition(); + } _UpdateLanguageBar(_status); } } From 03c0253103a4f95f60f2061cb7e1ec8101de3962 Mon Sep 17 00:00:00 2001 From: VimWei Date: Fri, 3 Jul 2026 06:06:39 +0800 Subject: [PATCH 02/16] fix: replace read-compare with blind toggle for conversion compartment - 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. --- WeaselTSF/Compartment.cpp | 28 ++++++++++++---------------- WeaselTSF/LanguageBar.cpp | 2 ++ WeaselTSF/WeaselTSF.h | 1 + 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/WeaselTSF/Compartment.cpp b/WeaselTSF/Compartment.cpp index 46969b51a6..55a8fa2674 100644 --- a/WeaselTSF/Compartment.cpp +++ b/WeaselTSF/Compartment.cpp @@ -266,22 +266,18 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { } } else if (IsEqualGUID(guidCompartment, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { - BOOL isOpen = _IsKeyboardOpen(); - if (isOpen) { - DWORD convMode = 0; - _GetCompartmentDWORD(convMode, - GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); - bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); - if (desiredAsciiMode != _status.ascii_mode) { - _status.ascii_mode = desiredAsciiMode; - _HandleLangBarMenuSelect(_status.ascii_mode - ? ID_WEASELTRAY_ENABLE_ASCII - : ID_WEASELTRAY_DISABLE_ASCII); - if (_pEditSessionContext) - m_client.ClearComposition(); - } - _UpdateLanguageBar(_status); - } + if (_updatingLanguageBar) + return S_OK; + _status.ascii_mode = !_status.ascii_mode; + _SetKeyboardOpen(true); + if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) + _EnableLanguageBar(true); + _HandleLangBarMenuSelect(_status.ascii_mode + ? ID_WEASELTRAY_ENABLE_ASCII + : ID_WEASELTRAY_DISABLE_ASCII); + if (_pEditSessionContext) + m_client.ClearComposition(); + _UpdateLanguageBar(_status); } return S_OK; } diff --git a/WeaselTSF/LanguageBar.cpp b/WeaselTSF/LanguageBar.cpp index 2a99fe564b..c9f38b3e48 100644 --- a/WeaselTSF/LanguageBar.cpp +++ b/WeaselTSF/LanguageBar.cpp @@ -412,7 +412,9 @@ void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { flags |= TF_CONVERSIONMODE_FULLSHAPE; else flags &= (~TF_CONVERSIONMODE_FULLSHAPE); + _updatingLanguageBar = true; _SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); + _updatingLanguageBar = false; _pLangBarButton->UpdateWeaselStatus(stat); } diff --git a/WeaselTSF/WeaselTSF.h b/WeaselTSF/WeaselTSF.h index 696d3cce4b..4e338b8573 100644 --- a/WeaselTSF/WeaselTSF.h +++ b/WeaselTSF/WeaselTSF.h @@ -233,4 +233,5 @@ class WeaselTSF : public ITfTextInputProcessorEx, BOOL _async_edit = false; BOOL _committed = false; BOOL _isToOpenClose = false; + BOOL _updatingLanguageBar = false; }; From bb673d09d35efa5db9968fbbe74af69704f40223 Mon Sep 17 00:00:00 2001 From: VimWei Date: Wed, 8 Jul 2026 11:06:18 +0800 Subject: [PATCH 03/16] style: clang-format ternary expression in CONVERSION handler - 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 --- WeaselTSF/Compartment.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/WeaselTSF/Compartment.cpp b/WeaselTSF/Compartment.cpp index 55a8fa2674..adc112e55c 100644 --- a/WeaselTSF/Compartment.cpp +++ b/WeaselTSF/Compartment.cpp @@ -272,9 +272,8 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { _SetKeyboardOpen(true); if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) _EnableLanguageBar(true); - _HandleLangBarMenuSelect(_status.ascii_mode - ? ID_WEASELTRAY_ENABLE_ASCII - : ID_WEASELTRAY_DISABLE_ASCII); + _HandleLangBarMenuSelect(_status.ascii_mode ? ID_WEASELTRAY_ENABLE_ASCII + : ID_WEASELTRAY_DISABLE_ASCII); if (_pEditSessionContext) m_client.ClearComposition(); _UpdateLanguageBar(_status); From cbec6f15786d593a2da6d4699d31f45709f866f3 Mon Sep 17 00:00:00 2001 From: VimWei Date: Wed, 8 Jul 2026 21:39:01 +0800 Subject: [PATCH 04/16] docs(weasel): add conversion compartment fix analysis document --- docs/conversion-compartment-fix-analysis.md | 180 ++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/conversion-compartment-fix-analysis.md diff --git a/docs/conversion-compartment-fix-analysis.md b/docs/conversion-compartment-fix-analysis.md new file mode 100644 index 0000000000..553c1db872 --- /dev/null +++ b/docs/conversion-compartment-fix-analysis.md @@ -0,0 +1,180 @@ +# 修复分析:WeaselTSF 响应 TSF 转换模式 Compartment 外部变更 + +## 问题 + +通过 TSF compartment 程序化切换中英文模式(如 `ITfCompartment::SetValue(GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)`)对小狼毫无效。外部工具写入 compartment 后,小狼毫不切换模式,且 compartment 值被恢复原状。 + +## 行为变化 + +| 操作 | 修复前 | 修复后 | +|------|--------|--------| +| 通过 TSF compartment 设置转换模式 | 无效果(外部变更被撤销) | 正常切换中英文 | +| 键盘关闭时设置转换模式 | 不处理 | 自动打开键盘并切换 | +| Shift 按键切换 | 正常 | 正常(不变) | +| 托盘菜单切换 | 正常 | 正常(不变) | + +## 架构背景 + +WeaselTSF 是 TSF Text Service(实现 `ITfTextInputProcessorEx`),不接收 `WM_IME_CONTROL`。Windows TSF 框架负责在 IMM32 API 与 TSF compartments 之间桥接: + +``` +ImmSetConversionStatus(IME_CMODE_NATIVE) + → TSF framework 翻译 + → ITfCompartment::SetValue(GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION) + → _HandleCompartment() ← 修复位置 +``` + +## 根因 + +根因有两个层面:**源代码逻辑缺陷** 和 **DLL 部署位置**。两者必须同时解决,缺一不可。 + +### 1. 源代码:CONVERSION handler 撤销外部变更 + +`_HandleCompartment` 中 `GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION` 的原始实现: + +```cpp +// 原始代码 (commit 93eec2d) +} else if (IsEqualGUID(guidCompartment, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { + BOOL isOpen = _IsKeyboardOpen(); + if (isOpen) { + weasel::ResponseParser parser(NULL, NULL, &_status, NULL, + &_cand->style()); + bool ok = m_client.GetResponseData(std::ref(parser)); // 查询 RIME 后端当前状态 + _UpdateLanguageBar(_status); // 写回 compartment + } +} +``` + +**问题链条**: + +1. 外部工具(如 im-control)写入 compartment,清除 `TF_CONVERSIONMODE_NATIVE` 位(请求切英文) +2. TSF 触发 `ITfCompartmentEventSink::OnChange` → `_HandleCompartment` +3. handler 调用 `m_client.GetResponseData(parser)` —— 从 RIME 后端获取**当前状态**(仍是中文,因为 RIME 还没收到切换指令) +4. handler 调用 `_UpdateLanguageBar(_status)` —— 把当前状态(中文)写回 compartment +5. compartment 被恢复为 `TF_CONVERSIONMODE_NATIVE` 置位 —— **外部变更被静默撤销** + +原始代码从不读取 compartment 值来判断外部请求的目标模式,也不调用 `_HandleLangBarMenuSelect` 通知 RIME 引擎切换。它只是把 RIME 后端的当前状态同步回 compartment,方向与需求完全相反。 + +### 2. DLL 部署位置 + +WeaselTSF 的 CLSID `{A3F4CDED-B1E9-41EE-9CA6-7B4D0DE6CB0A}` 注册路径为: + +| 架构 | 注册路径 | +|------|---------| +| 64-bit | `C:\Windows\system32\weasel.dll` | +| 32-bit | `C:\Windows\SysWOW64\weasel.dll` | + +TSF 框架从上述系统路径加载 DLL。如果将修复后的 DLL 部署到其他路径(如 `C:\Program Files\Rime\weasel-0.17.4\`),系统不会加载它,进程内仍是旧 DLL。 + +更新方法:管理员身份打开 PowerShell,执行 + +Rename-Item "C:\Windows\system32\weasel.dll" "weasel.dll.bak3" -Force +Copy-Item "C:\Apps\git-kb\repos\VimWei\weasel\output\weaselx64.dll" "C:\Windows\system32\weasel.dll" -Force +Rename-Item "C:\Windows\SysWOW64\weasel.dll" "weasel.dll.bak3" -Force +Copy-Item "C:\Apps\git-kb\repos\VimWei\weasel\output\weasel.dll" "C:\Windows\SysWOW64\weasel.dll" -Force + +## 修复方案 + +### 源代码修改 + +**修改文件:** `WeaselTSF/Compartment.cpp`、`WeaselTSF/LanguageBar.cpp`、`WeaselTSF/WeaselTSF.h` + +将 CONVERSION handler 从"查询后端 → 写回 compartment"改为"toggle → 通知 RIME → 同步 UI",与 `GUID_COMPARTMENT_KEYBOARD_OPENCLOSE` 的 else 分支(Shift 按键切换路径)保持一致: + +```cpp +// 修复后 +} else if (IsEqualGUID(guidCompartment, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { + if (_updatingLanguageBar) + return S_OK; + _status.ascii_mode = !_status.ascii_mode; + _SetKeyboardOpen(true); + if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) + _EnableLanguageBar(true); + _HandleLangBarMenuSelect(_status.ascii_mode + ? ID_WEASELTRAY_ENABLE_ASCII + : ID_WEASELTRAY_DISABLE_ASCII); + if (_pEditSessionContext) + m_client.ClearComposition(); + _UpdateLanguageBar(_status); +} +``` + +与原始代码的关键差异: + +| 项目 | 原始代码 | 修复后 | +|------|---------|--------| +| **响应方式** | `m_client.GetResponseData()` 查询 RIME 后端旧状态,`_UpdateLanguageBar()` 写回 compartment(撤销外部变更) | `_status.ascii_mode = !_status.ascii_mode` 直接翻转,`_HandleLangBarMenuSelect()` 通知 RIME 引擎切换 | +| **键盘状态守卫** | `_IsKeyboardOpen()` —— 键盘关闭时完全不处理 | 移除守卫,改为 `_SetKeyboardOpen(true)` 主动打开键盘 | +| **重入保护** | 无 —— `_UpdateLanguageBar` 写 compartment 再次触发 handler | `_updatingLanguageBar` 守卫,阻断 `_UpdateLanguageBar` 引起的自触发 | + +`_HandleLangBarMenuSelect` → `TrayCommand` IPC → RIME 后端的 `SetOption("ascii_mode")`,与 Shift 按键切换走同一路径,确保行为和配置(如 `global_ascii_mode`)一致。 + +### 重入守卫 + +`_UpdateLanguageBar` 内部调用 `_SetCompartmentDWORD` 写入 CONVERSION compartment,这会再次触发 `OnChange` → `_HandleCompartment`。如果不加守卫,handler 会再次 toggle,导致双重翻转或无限递归。 + +```cpp +// LanguageBar.cpp +void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { + // ... + _updatingLanguageBar = true; + _SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); + _updatingLanguageBar = false; + // ... +} +``` + +```cpp +// WeaselTSF.h +BOOL _updatingLanguageBar = false; +``` + +### 为什么用 blind toggle 而非读取 compartment 值 + +曾尝试读取 compartment 值来推导目标模式(`_GetCompartmentDWORD` → 检查 `TF_CONVERSIONMODE_NATIVE` 位 → 与 `_status.ascii_mode` 比对)。但 TSF 的 `OnChange` 通知时机可能导致 `GetValue` 返回旧值。blind toggle 避免了这一时序问题,且与 `OPENCLOSE` handler 的 else 分支(Shift 按键路径)完全一致。 + +外部工具(如 im-control)只在 `newMode != oldMode` 时才调用 `SetValue`,所以每次 `OnChange` 通知都代表一次真实的模式切换请求,blind toggle 语义正确。 + +## 验证 + +### 测试环境 + +- 前台进程:Windows Terminal (x64) +- 外部工具:[im-control](https://github.com/VimWei/im-control) —— 通过 `SetWindowsHookEx` 注入 hook DLL 到前台进程,在目标线程内调用 `ITfCompartment::SetValue` + +### 测试结果 + +``` +# 中文状态下切英文 +im-control -g → open native +im-control -c alphanumeric → (成功) +im-control -g → open alphanumeric + +# 英文状态下切中文 +im-control -g → open alphanumeric +im-control -c native → (成功) +im-control -g → open native +``` + +### 调试日志验证 + +在 `_HandleCompartment` 和 `_UpdateLanguageBar` 中加入 `OutputDebugStringW` 日志,确认完整链路: + +``` +[05:38:09.417] CONVERSION fired, compartment=0xF49BE320 (NATIVE=0), ascii_mode(before)=0, _updatingLanguageBar=0 +[05:38:09.417] CONVERSION toggled ascii_mode: 0 -> 1 +[05:38:09.417] CONVERSION called HandleLangBarMenuSelect(40013) # ID_WEASELTRAY_ENABLE_ASCII +[05:38:09.417] CONVERSION calling _UpdateLanguageBar +[05:38:09.417] CONVERSION done, ascii_mode(after)=1 +[05:38:09.433] CONVERSION fired, compartment=0xF49BE320 (NATIVE=0), ascii_mode(before)=1, _updatingLanguageBar=1 +[05:38:09.433] CONVERSION skipped (re-entrant from _UpdateLanguageBar) # 重入守卫生效 +``` + +链路完整:hook 写入 compartment → sink 触发 → toggle ascii_mode → 通知 RIME 引擎 → `_UpdateLanguageBar` 写回 compartment → 重入守卫跳过自触发。 + +## 相关链接 + +- [rime/weasel#1371](https://github.com/rime/weasel/issues/1371) +- [im-control](https://github.com/VimWei/im-control) —— 用于测试的 TSF compartment 控制工具 From 20e3c0f2a534643bd0fbd68dd6415ed25aec8858 Mon Sep 17 00:00:00 2001 From: VimWei Date: Wed, 8 Jul 2026 22:28:02 +0800 Subject: [PATCH 05/16] fix: replace blind toggle with value-driven CONVERSION handler for Windows 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. --- WeaselTSF/Compartment.cpp | 25 ++++--- docs/conversion-compartment-fix-analysis.md | 79 +++++++++++++++------ 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/WeaselTSF/Compartment.cpp b/WeaselTSF/Compartment.cpp index adc112e55c..0a0c06c00c 100644 --- a/WeaselTSF/Compartment.cpp +++ b/WeaselTSF/Compartment.cpp @@ -268,15 +268,22 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { if (_updatingLanguageBar) return S_OK; - _status.ascii_mode = !_status.ascii_mode; - _SetKeyboardOpen(true); - if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) - _EnableLanguageBar(true); - _HandleLangBarMenuSelect(_status.ascii_mode ? ID_WEASELTRAY_ENABLE_ASCII - : ID_WEASELTRAY_DISABLE_ASCII); - if (_pEditSessionContext) - m_client.ClearComposition(); - _UpdateLanguageBar(_status); + DWORD convMode = 0; + _GetCompartmentDWORD(convMode, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); + bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); + if (desiredAsciiMode != _status.ascii_mode) { + _status.ascii_mode = desiredAsciiMode; + _SetKeyboardOpen(true); + if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) + _EnableLanguageBar(true); + _HandleLangBarMenuSelect(_status.ascii_mode + ? ID_WEASELTRAY_ENABLE_ASCII + : ID_WEASELTRAY_DISABLE_ASCII); + if (_pEditSessionContext) + m_client.ClearComposition(); + _UpdateLanguageBar(_status); + } } return S_OK; } diff --git a/docs/conversion-compartment-fix-analysis.md b/docs/conversion-compartment-fix-analysis.md index 553c1db872..8cc52f66ab 100644 --- a/docs/conversion-compartment-fix-analysis.md +++ b/docs/conversion-compartment-fix-analysis.md @@ -131,36 +131,73 @@ void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { BOOL _updatingLanguageBar = false; ``` -### 为什么用 blind toggle 而非读取 compartment 值 +### 从 blind toggle 改为值驱动:修复 Windows 11 不兼容 -曾尝试读取 compartment 值来推导目标模式(`_GetCompartmentDWORD` → 检查 `TF_CONVERSIONMODE_NATIVE` 位 → 与 `_status.ascii_mode` 比对)。但 TSF 的 `OnChange` 通知时机可能导致 `GetValue` 返回旧值。blind toggle 避免了这一时序问题,且与 `OPENCLOSE` handler 的 else 分支(Shift 按键路径)完全一致。 +blind toggle 方案在 Windows 10 下验证通过(见下方旧日志),但在 Windows 11 下失效。 -外部工具(如 im-control)只在 `newMode != oldMode` 时才调用 `SetValue`,所以每次 `OnChange` 通知都代表一次真实的模式切换请求,blind toggle 语义正确。 +**Windows 11 失效原因**: -## 验证 +blind toggle 依赖 `_updatingLanguageBar` 布尔守卫拦截 `_UpdateLanguageBar` 引起的自触发 `OnChange`。此守卫仅在 `OnChange` **同步**回调时有效。Windows 11 的 TSF 基础设施(TextInputHost.exe)可能: -### 测试环境 +1. **异步投递 `OnChange`**:`_UpdateLanguageBar` 设置 `_updatingLanguageBar=true` → 写 compartment → 守卫复位为 `false` → 异步 `OnChange` 到达 → 守卫已失效 → 额外 toggle +2. **系统注入额外 compartment 写入**:Windows 11 的文本输入框架可能在外部写入后自行同步 compartment,触发未被守卫覆盖的 `OnChange` -- 前台进程:Windows Terminal (x64) -- 外部工具:[im-control](https://github.com/VimWei/im-control) —— 通过 `SetWindowsHookEx` 注入 hook DLL 到前台进程,在目标线程内调用 `ITfCompartment::SetValue` +结果:toggle 次数从偶数(Windows 10)变为奇数(Windows 11),`ascii_mode` 翻转到错误状态。 -### 测试结果 +**值驱动方案**: +将 blind toggle 替换为读取 compartment 实际值并与 `_status.ascii_mode` 比对: + +```cpp +} else if (IsEqualGUID(guidCompartment, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { + if (_updatingLanguageBar) + return S_OK; + DWORD convMode = 0; + _GetCompartmentDWORD(convMode, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); + bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); + if (desiredAsciiMode != _status.ascii_mode) { + _status.ascii_mode = desiredAsciiMode; + _SetKeyboardOpen(true); + if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) + _EnableLanguageBar(true); + _HandleLangBarMenuSelect(_status.ascii_mode + ? ID_WEASELTRAY_ENABLE_ASCII + : ID_WEASELTRAY_DISABLE_ASCII); + if (_pEditSessionContext) + m_client.ClearComposition(); + _UpdateLanguageBar(_status); + } +} ``` -# 中文状态下切英文 -im-control -g → open native -im-control -c alphanumeric → (成功) -im-control -g → open alphanumeric - -# 英文状态下切中文 -im-control -g → open alphanumeric -im-control -c native → (成功) -im-control -g → open native -``` -### 调试日志验证 +**值驱动方案的天然幂等性**:即使 `_updatingLanguageBar` 守卫失效(异步回调 / 系统注入),重新读取 compartment 值会得到与当前 `_status.ascii_mode` 一致的结果(因为 `_UpdateLanguageBar` 已将正确值写入),`desiredAsciiMode == _status.ascii_mode` → 跳过。不会产生额外翻转。 + +**与 blind toggle 方案的对比**: + +| 项目 | blind toggle (f14f2a7) | 值驱动 (当前) | +|------|----------------------|--------------| +| 回调次数依赖 | 严格依赖偶数次 `OnChange` | 幂等,不依赖回调次数 | +| 重入守卫 | 唯一防线,异步回调下失效 | 第一防线;值比对是第二防线 | +| Windows 10 | 正常 | 正常 | +| Windows 11 | 失效(奇数次 toggle) | 正常(幂等跳过) | +| stale value 风险 | 无 | 若 `GetValue` 返回旧值则静默跳过(安全失败) | + +### im-control 侧修复:OPENCLOSE 跳过未变化的写入 + +im-control 的 `hook.cpp` 原先对 `GUID_COMPARTMENT_KEYBOARD_OPENCLOSE` 无条件 `SetValue`,即使值未变。这会触发 Weasel 的 OPENCLOSE handler(`_isToOpenClose=false` 时为 blind toggle),产生不必要的 `ascii_mode` 翻转。 + +修复:写入前先 `GetValue` 比对,仅在值变化时 `SetValue`,与 CONVERSION 的已有逻辑一致。 + +## 验证 + +### 测试环境 + +- 前台进程:Windows Terminal (x64) +- 外部工具:[im-control](https://github.com/VimWei/im-control) —— 通过 `SetWindowsHookEx` 注入 hook DLL 到前台进程,在目标线程内调用 `ITfCompartment::SetValue` -在 `_HandleCompartment` 和 `_UpdateLanguageBar` 中加入 `OutputDebugStringW` 日志,确认完整链路: +### Windows 10 blind toggle 方案旧日志(已弃用) ``` [05:38:09.417] CONVERSION fired, compartment=0xF49BE320 (NATIVE=0), ascii_mode(before)=0, _updatingLanguageBar=0 @@ -172,7 +209,7 @@ im-control -g → open native [05:38:09.433] CONVERSION skipped (re-entrant from _UpdateLanguageBar) # 重入守卫生效 ``` -链路完整:hook 写入 compartment → sink 触发 → toggle ascii_mode → 通知 RIME 引擎 → `_UpdateLanguageBar` 写回 compartment → 重入守卫跳过自触发。 +此日志中重入守卫同步生效,仅在 Windows 10 下成立。 ## 相关链接 From 88a076ec8b944d5a5780b6eec2240d2cbdc354ed Mon Sep 17 00:00:00 2001 From: VimWei Date: Wed, 8 Jul 2026 22:34:16 +0800 Subject: [PATCH 06/16] docs: split Windows 11 compatibility fix into separate document 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. --- docs/conversion-compartment-fix-analysis.md | 79 +++-------- docs/windows11-compatibility-fix.md | 144 ++++++++++++++++++++ 2 files changed, 165 insertions(+), 58 deletions(-) create mode 100644 docs/windows11-compatibility-fix.md diff --git a/docs/conversion-compartment-fix-analysis.md b/docs/conversion-compartment-fix-analysis.md index 8cc52f66ab..553c1db872 100644 --- a/docs/conversion-compartment-fix-analysis.md +++ b/docs/conversion-compartment-fix-analysis.md @@ -131,64 +131,11 @@ void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { BOOL _updatingLanguageBar = false; ``` -### 从 blind toggle 改为值驱动:修复 Windows 11 不兼容 +### 为什么用 blind toggle 而非读取 compartment 值 -blind toggle 方案在 Windows 10 下验证通过(见下方旧日志),但在 Windows 11 下失效。 +曾尝试读取 compartment 值来推导目标模式(`_GetCompartmentDWORD` → 检查 `TF_CONVERSIONMODE_NATIVE` 位 → 与 `_status.ascii_mode` 比对)。但 TSF 的 `OnChange` 通知时机可能导致 `GetValue` 返回旧值。blind toggle 避免了这一时序问题,且与 `OPENCLOSE` handler 的 else 分支(Shift 按键路径)完全一致。 -**Windows 11 失效原因**: - -blind toggle 依赖 `_updatingLanguageBar` 布尔守卫拦截 `_UpdateLanguageBar` 引起的自触发 `OnChange`。此守卫仅在 `OnChange` **同步**回调时有效。Windows 11 的 TSF 基础设施(TextInputHost.exe)可能: - -1. **异步投递 `OnChange`**:`_UpdateLanguageBar` 设置 `_updatingLanguageBar=true` → 写 compartment → 守卫复位为 `false` → 异步 `OnChange` 到达 → 守卫已失效 → 额外 toggle -2. **系统注入额外 compartment 写入**:Windows 11 的文本输入框架可能在外部写入后自行同步 compartment,触发未被守卫覆盖的 `OnChange` - -结果:toggle 次数从偶数(Windows 10)变为奇数(Windows 11),`ascii_mode` 翻转到错误状态。 - -**值驱动方案**: - -将 blind toggle 替换为读取 compartment 实际值并与 `_status.ascii_mode` 比对: - -```cpp -} else if (IsEqualGUID(guidCompartment, - GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { - if (_updatingLanguageBar) - return S_OK; - DWORD convMode = 0; - _GetCompartmentDWORD(convMode, - GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); - bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); - if (desiredAsciiMode != _status.ascii_mode) { - _status.ascii_mode = desiredAsciiMode; - _SetKeyboardOpen(true); - if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) - _EnableLanguageBar(true); - _HandleLangBarMenuSelect(_status.ascii_mode - ? ID_WEASELTRAY_ENABLE_ASCII - : ID_WEASELTRAY_DISABLE_ASCII); - if (_pEditSessionContext) - m_client.ClearComposition(); - _UpdateLanguageBar(_status); - } -} -``` - -**值驱动方案的天然幂等性**:即使 `_updatingLanguageBar` 守卫失效(异步回调 / 系统注入),重新读取 compartment 值会得到与当前 `_status.ascii_mode` 一致的结果(因为 `_UpdateLanguageBar` 已将正确值写入),`desiredAsciiMode == _status.ascii_mode` → 跳过。不会产生额外翻转。 - -**与 blind toggle 方案的对比**: - -| 项目 | blind toggle (f14f2a7) | 值驱动 (当前) | -|------|----------------------|--------------| -| 回调次数依赖 | 严格依赖偶数次 `OnChange` | 幂等,不依赖回调次数 | -| 重入守卫 | 唯一防线,异步回调下失效 | 第一防线;值比对是第二防线 | -| Windows 10 | 正常 | 正常 | -| Windows 11 | 失效(奇数次 toggle) | 正常(幂等跳过) | -| stale value 风险 | 无 | 若 `GetValue` 返回旧值则静默跳过(安全失败) | - -### im-control 侧修复:OPENCLOSE 跳过未变化的写入 - -im-control 的 `hook.cpp` 原先对 `GUID_COMPARTMENT_KEYBOARD_OPENCLOSE` 无条件 `SetValue`,即使值未变。这会触发 Weasel 的 OPENCLOSE handler(`_isToOpenClose=false` 时为 blind toggle),产生不必要的 `ascii_mode` 翻转。 - -修复:写入前先 `GetValue` 比对,仅在值变化时 `SetValue`,与 CONVERSION 的已有逻辑一致。 +外部工具(如 im-control)只在 `newMode != oldMode` 时才调用 `SetValue`,所以每次 `OnChange` 通知都代表一次真实的模式切换请求,blind toggle 语义正确。 ## 验证 @@ -197,7 +144,23 @@ im-control 的 `hook.cpp` 原先对 `GUID_COMPARTMENT_KEYBOARD_OPENCLOSE` 无条 - 前台进程:Windows Terminal (x64) - 外部工具:[im-control](https://github.com/VimWei/im-control) —— 通过 `SetWindowsHookEx` 注入 hook DLL 到前台进程,在目标线程内调用 `ITfCompartment::SetValue` -### Windows 10 blind toggle 方案旧日志(已弃用) +### 测试结果 + +``` +# 中文状态下切英文 +im-control -g → open native +im-control -c alphanumeric → (成功) +im-control -g → open alphanumeric + +# 英文状态下切中文 +im-control -g → open alphanumeric +im-control -c native → (成功) +im-control -g → open native +``` + +### 调试日志验证 + +在 `_HandleCompartment` 和 `_UpdateLanguageBar` 中加入 `OutputDebugStringW` 日志,确认完整链路: ``` [05:38:09.417] CONVERSION fired, compartment=0xF49BE320 (NATIVE=0), ascii_mode(before)=0, _updatingLanguageBar=0 @@ -209,7 +172,7 @@ im-control 的 `hook.cpp` 原先对 `GUID_COMPARTMENT_KEYBOARD_OPENCLOSE` 无条 [05:38:09.433] CONVERSION skipped (re-entrant from _UpdateLanguageBar) # 重入守卫生效 ``` -此日志中重入守卫同步生效,仅在 Windows 10 下成立。 +链路完整:hook 写入 compartment → sink 触发 → toggle ascii_mode → 通知 RIME 引擎 → `_UpdateLanguageBar` 写回 compartment → 重入守卫跳过自触发。 ## 相关链接 diff --git a/docs/windows11-compatibility-fix.md b/docs/windows11-compatibility-fix.md new file mode 100644 index 0000000000..87060fbd72 --- /dev/null +++ b/docs/windows11-compatibility-fix.md @@ -0,0 +1,144 @@ +# Windows 11 兼容性修复:CONVERSION handler 从 blind toggle 改为值驱动 + +## 问题 + +[blind toggle 方案](conversion-compartment-fix-analysis.md)(commit `f14f2a7`)在 Windows 10 下正常,但在 Windows 11 下失效: + +- Vim 切换到 normal 模式时,RIME 总是变为中文(应为英文) +- AppIME 8 秒无键盘输入后,RIME 仍维持中文(应切回英文) + +## 根因 + +blind toggle(`_status.ascii_mode = !_status.ascii_mode`)依赖 `_updatingLanguageBar` 布尔守卫拦截 `_UpdateLanguageBar` 引起的自触发 `OnChange`。此守卫仅在 `OnChange` **同步**回调时有效。 + +Windows 11 的 TSF 基础设施(TextInputHost.exe / InputApp.exe)与 Windows 10 行为不同: + +1. **异步投递 `OnChange`**:`_UpdateLanguageBar` 设置 `_updatingLanguageBar=true` → 写 compartment → 守卫复位为 `false` → 异步 `OnChange` 到达 → 守卫已失效 → 额外 toggle +2. **系统注入额外 compartment 写入**:Windows 11 的文本输入框架可能在外部写入后自行同步 compartment,触发未被守卫覆盖的 `OnChange` + +结果:toggle 次数从偶数(Windows 10)变为奇数(Windows 11),`ascii_mode` 翻转到错误状态。 + +### Windows 10 vs Windows 11 回调时序对比 + +``` +Windows 10(同步 OnChange): + SetValue ──→ OnChange(同步) ──→ _UpdateLanguageBar ──→ SetValue ──→ OnChange(同步, 守卫=true, 跳过) + toggle 次数: 1(_UpdateLanguageBar 的自触发被守卫拦截) + ✗ 但 OPENCLOSE handler 也会 toggle,凑成偶数 → 结果正确 + +Windows 11(异步 OnChange): + SetValue ──→ OnChange(异步, 稍后) ──→ _UpdateLanguageBar ──→ SetValue ──→ OnChange(异步, 稍后) + ......守卫已复位...... + OnChange #1 到达 → toggle #1 + OnChange #2 到达 → toggle #2(守卫已失效) + OnChange #3 到达 → toggle #3(系统注入的额外写入) + toggle 次数: 奇数 → 结果错误 +``` + +## 修复方案 + +### 1. Weasel:CONVERSION handler 改为值驱动 + +**修改文件:** `WeaselTSF/Compartment.cpp` + +将 blind toggle 替换为读取 compartment 实际值并与 `_status.ascii_mode` 比对: + +```cpp +// 修复后 +} else if (IsEqualGUID(guidCompartment, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { + if (_updatingLanguageBar) + return S_OK; + DWORD convMode = 0; + _GetCompartmentDWORD(convMode, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); + bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); + if (desiredAsciiMode != _status.ascii_mode) { + _status.ascii_mode = desiredAsciiMode; + _SetKeyboardOpen(true); + if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) + _EnableLanguageBar(true); + _HandleLangBarMenuSelect(_status.ascii_mode + ? ID_WEASELTRAY_ENABLE_ASCII + : ID_WEASELTRAY_DISABLE_ASCII); + if (_pEditSessionContext) + m_client.ClearComposition(); + _UpdateLanguageBar(_status); + } +} +``` + +**天然幂等性**:即使 `_updatingLanguageBar` 守卫失效(异步回调 / 系统注入),重新读取 compartment 值会得到与当前 `_status.ascii_mode` 一致的结果(因为 `_UpdateLanguageBar` 已将正确值写入),`desiredAsciiMode == _status.ascii_mode` → 跳过。不会产生额外翻转。 + +### 2. im-control:OPENCLOSE 跳过未变化的写入(防御性优化) + +**修改文件:** `injector/hook.cpp` + +im-control 原先对 `GUID_COMPARTMENT_KEYBOARD_OPENCLOSE` 无条件 `SetValue`,即使值未变。这会触发 Weasel 的 OPENCLOSE handler(`_isToOpenClose=false` 时仍为 blind toggle),产生不必要的 `ascii_mode` 翻转。 + +修复:写入前先 `GetValue` 比对,仅在值变化时 `SetValue`,与 CONVERSION 的已有逻辑一致。 + +> 此改动非 Windows 11 兼容性修复的必要条件——Weasel 的 CONVERSION 值驱动修复已能校正 OPENCLOSE handler 的错误 toggle。但消除不必要的 OPENCLOSE 写入可以避免 RIME 引擎收到一错一对的 TrayCommand,减少时序问题。 + +## 方案对比 + +| 项目 | blind toggle (`f14f2a7`) | 值驱动(本修复) | +|------|--------------------------|------------------| +| 回调次数依赖 | 严格依赖偶数次 `OnChange` | 幂等,不依赖回调次数 | +| 重入守卫 | 唯一防线,异步回调下失效 | 第一防线;值比对是第二防线 | +| Windows 10 | 正常 | 正常 | +| Windows 11 | 失效(奇数次 toggle) | 正常(幂等跳过) | +| stale value 风险 | 无 | 若 `GetValue` 返回旧值则静默跳过(安全失败) | + +## 为什么不改 OPENCLOSE handler + +OPENCLOSE handler 的 else 分支(`_isToOpenClose=false`)使用 blind toggle 是**为 Ctrl+Space 设计的**——每次 Ctrl+Space 切换 OPENCLOSE 值,Weasel 对应翻转 `ascii_mode`。改为值驱动会破坏此行为。 + +OPENCLOSE 值(open/close)与 `ascii_mode`(中/英)没有直接映射关系:OPENCLOSE=true 表示 IME 激活(可能是中文也可能是英文),OPENCLOSE=false 表示 IME 关闭(英文直通)。因此无法像 CONVERSION 那样从 compartment 值推导 `ascii_mode`。 + +通过 im-control 侧跳过未变化的 OPENCLOSE 写入,避免从外部触发此 handler,是最小侵入的解决方案。 + +## 部署 + +### 编译 + +在装有 Visual Studio 2022 + Boost 的机器上编译: + +``` +cd C:\Apps\git-kb\repos\VimWei\weasel +git pull +build.bat weasel release +``` + +产出 `output\weasel.dll`(Win32)和 `output\weaselx64.dll`(x64)。 + +### 部署 weasel.dll + +管理员 PowerShell: + +```powershell +Copy-Item output\weaselx64.dll C:\Windows\system32\weasel.dll -Force +Copy-Item output\weasel.dll C:\Windows\SysWOW64\weasel.dll -Force +``` + +> TSF 框架从系统路径加载 DLL,详见 [原始修复文档](conversion-compartment-fix-analysis.md#2-dll-部署位置)。 + +### 编译部署 im-control(可选) + +``` +cd C:\Apps\git-kb\repos\VimWei\im-control +git pull +cmake -S . -B build -G "Visual Studio 17 2022" +cmake --build build --config RelWithDebInfo +cmake --install build --prefix bin --config RelWithDebInfo +``` + +```powershell +Copy-Item bin\* "C:\Apps\VimReader\lib\utils\im-control\" -Force +``` + +## 相关文档 + +- [原始修复:CONVERSION handler 响应外部变更](conversion-compartment-fix-analysis.md) — blind toggle 方案(Windows 10) +- [rime/weasel#1371](https://github.com/rime/weasel/issues/1371) +- [im-control](https://github.com/VimWei/im-control) From dd3505d144bb1c631b1dfa359d2cbba8baa0a67e Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 9 Jul 2026 01:02:01 +0800 Subject: [PATCH 07/16] docs: add TfClientId root cause to Windows 11 compatibility analysis 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. --- docs/windows11-compatibility-fix.md | 38 +++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/windows11-compatibility-fix.md b/docs/windows11-compatibility-fix.md index 87060fbd72..c8fdc70012 100644 --- a/docs/windows11-compatibility-fix.md +++ b/docs/windows11-compatibility-fix.md @@ -9,14 +9,22 @@ ## 根因 -blind toggle(`_status.ascii_mode = !_status.ascii_mode`)依赖 `_updatingLanguageBar` 布尔守卫拦截 `_UpdateLanguageBar` 引起的自触发 `OnChange`。此守卫仅在 `OnChange` **同步**回调时有效。 +经 Windows 11 实测发现,问题有**两个独立的层面**: -Windows 11 的 TSF 基础设施(TextInputHost.exe / InputApp.exe)与 Windows 10 行为不同: +### 层面 1:im-control 使用 TF_CLIENTID_NULL 调用 SetValue,Win11 不触发 OnChange -1. **异步投递 `OnChange`**:`_UpdateLanguageBar` 设置 `_updatingLanguageBar=true` → 写 compartment → 守卫复位为 `false` → 异步 `OnChange` 到达 → 守卫已失效 → 额外 toggle -2. **系统注入额外 compartment 写入**:Windows 11 的文本输入框架可能在外部写入后自行同步 compartment,触发未被守卫覆盖的 `OnChange` +im-control 的 hook DLL 调用 `ITfCompartment::SetValue(0, ...)`(`TfClientId = TF_CLIENTID_NULL`)。WeaselTSF 自身的 `_SetCompartmentDWORD` 使用 `SetValue(_tfClientId, ...)`(通过 `ITfThreadMgr::Activate` 获取的有效非零 ID)。 -结果:toggle 次数从偶数(Windows 10)变为奇数(Windows 11),`ascii_mode` 翻转到错误状态。 +**Windows 10**:不区分 `TfClientId`,所有 `SetValue` 都触发 `OnChange`。 +**Windows 11**:仅为已激活客户端(非零 `TfClientId`)的写入触发 `OnChange`。`TF_CLIENTID_NULL` 的写入不触发通知。 + +这解释了实测现象: +- Shift 键切换正常:`_UpdateLanguageBar` → `_SetCompartmentDWORD` 用 `_tfClientId` → `OnChange` 触发 → CONVERSION handler 执行 → 值匹配 → 跳过 ✓ +- im-control 外部写入失效:hook 用 `0` → Win11 不触发 `OnChange` → CONVERSION handler 不执行 → RIME 保持原状 ✗ + +### 层面 2:blind toggle 依赖同步 OnChange 回调(已被值驱动修复) + +即使 `OnChange` 正确触发,blind toggle 方案在 Windows 11 仍有异步回调问题(详见下文)。此层面已由值驱动修复解决。 ### Windows 10 vs Windows 11 回调时序对比 @@ -37,7 +45,23 @@ Windows 11(异步 OnChange): ## 修复方案 -### 1. Weasel:CONVERSION handler 改为值驱动 +### 1. im-control:使用有效 TfClientId 调用 SetValue(根因修复) + +**修改文件:** `injector/hook.cpp` + +im-control 的 hook 在 `SetValue` 时使用 `TF_CLIENTID_NULL`(0),Windows 11 不为此类写入触发 `OnChange`。 + +修复:调用 `ITfThreadMgr::Activate` 获取有效 `TfClientId`,用于所有 `SetValue` 调用,完成后 `Deactivate`: + +```cpp +TfClientId clientId = TF_CLIENTID_NULL; +// ... 创建 pThreadMgr 后 ... +pThreadMgr->Activate(&clientId); +// ... SetValue(clientId, ...) 替代 SetValue(0, ...) ... +pThreadMgr->Deactivate(); +``` + +### 2. Weasel:CONVERSION handler 改为值驱动(防御层) **修改文件:** `WeaselTSF/Compartment.cpp` @@ -70,7 +94,7 @@ Windows 11(异步 OnChange): **天然幂等性**:即使 `_updatingLanguageBar` 守卫失效(异步回调 / 系统注入),重新读取 compartment 值会得到与当前 `_status.ascii_mode` 一致的结果(因为 `_UpdateLanguageBar` 已将正确值写入),`desiredAsciiMode == _status.ascii_mode` → 跳过。不会产生额外翻转。 -### 2. im-control:OPENCLOSE 跳过未变化的写入(防御性优化) +### 3. im-control:OPENCLOSE 跳过未变化的写入(防御性优化) **修改文件:** `injector/hook.cpp` From 7c7572e1d07a4b5137e01046221652f0c3b61c49 Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 9 Jul 2026 08:38:26 +0800 Subject: [PATCH 08/16] debug: add OutputDebugStringW logging to CONVERSION handler and _UpdateLanguageBar 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. --- WeaselTSF/Compartment.cpp | 24 ++++++++++++++++++++++-- WeaselTSF/LanguageBar.cpp | 8 ++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/WeaselTSF/Compartment.cpp b/WeaselTSF/Compartment.cpp index 0a0c06c00c..ae755c5cfc 100644 --- a/WeaselTSF/Compartment.cpp +++ b/WeaselTSF/Compartment.cpp @@ -243,6 +243,12 @@ void WeaselTSF::_UninitCompartment() { HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { if (IsEqualGUID(guidCompartment, GUID_COMPARTMENT_KEYBOARD_OPENCLOSE)) { + BOOL isOpenDbg = _IsKeyboardOpen(); + { + wchar_t buf[256]; + swprintf_s(buf, 256, L"[WeaselTSF] OPENCLOSE OnChange: isOpen=%d, isToOpenClose=%d, ascii_mode=%d\n", isOpenDbg, _isToOpenClose, _status.ascii_mode); + OutputDebugStringW(buf); + } if (_isToOpenClose) { BOOL isOpen = _IsKeyboardOpen(); // clear composition when close keyboard @@ -266,13 +272,25 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { } } else if (IsEqualGUID(guidCompartment, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { - if (_updatingLanguageBar) - return S_OK; DWORD convMode = 0; _GetCompartmentDWORD(convMode, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); + { + wchar_t buf[512]; + swprintf_s(buf, 512, + L"[WeaselTSF] CONVERSION OnChange: convMode=0x%lX (NATIVE=%d), desiredAscii=%d, statusAscii=%d, updatingLangBar=%d\n", + convMode, (convMode & TF_CONVERSIONMODE_NATIVE) ? 1 : 0, + desiredAsciiMode ? 1 : 0, _status.ascii_mode ? 1 : 0, + _updatingLanguageBar ? 1 : 0); + OutputDebugStringW(buf); + } + if (_updatingLanguageBar) { + OutputDebugStringW(L"[WeaselTSF] CONVERSION: skipped (updatingLanguageBar)\n"); + return S_OK; + } if (desiredAsciiMode != _status.ascii_mode) { + OutputDebugStringW(L"[WeaselTSF] CONVERSION: processing -> switching mode\n"); _status.ascii_mode = desiredAsciiMode; _SetKeyboardOpen(true); if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) @@ -283,6 +301,8 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { if (_pEditSessionContext) m_client.ClearComposition(); _UpdateLanguageBar(_status); + } else { + OutputDebugStringW(L"[WeaselTSF] CONVERSION: skipped (value matches state)\n"); } } return S_OK; diff --git a/WeaselTSF/LanguageBar.cpp b/WeaselTSF/LanguageBar.cpp index c9f38b3e48..2926fc511e 100644 --- a/WeaselTSF/LanguageBar.cpp +++ b/WeaselTSF/LanguageBar.cpp @@ -412,6 +412,14 @@ void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { flags |= TF_CONVERSIONMODE_FULLSHAPE; else flags &= (~TF_CONVERSIONMODE_FULLSHAPE); + { + wchar_t buf[256]; + swprintf_s(buf, 256, + L"[WeaselTSF] _UpdateLanguageBar: ascii=%d, flags=0x%lX (NATIVE=%d), updatingLangBar set=true\n", + stat.ascii_mode ? 1 : 0, flags, + (flags & TF_CONVERSIONMODE_NATIVE) ? 1 : 0); + OutputDebugStringW(buf); + } _updatingLanguageBar = true; _SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); _updatingLanguageBar = false; From 42e336d51d2d446078c33643e3f355e491ebd648 Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 9 Jul 2026 08:44:19 +0800 Subject: [PATCH 09/16] debug: write compartment debug logs to file instead of OutputDebugStringW 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. --- WeaselTSF/Compartment.cpp | 76 ++++++++++++++++++++++++++++++--------- WeaselTSF/LanguageBar.cpp | 17 ++++----- 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/WeaselTSF/Compartment.cpp b/WeaselTSF/Compartment.cpp index ae755c5cfc..f96edf4342 100644 --- a/WeaselTSF/Compartment.cpp +++ b/WeaselTSF/Compartment.cpp @@ -3,10 +3,54 @@ #include "Compartment.h" #include #include +#include +#include #include "ResponseParser.h" #include "CandidateList.h" #include "LanguageBar.h" +// Debug logging to file (enabled via session GUID env override) +static bool s_dbgEnabled = false; +static FILE* s_dbgFile = nullptr; +static CRITICAL_SECTION s_dbgLock; +static bool s_dbgLockInit = false; +static const char* s_dbgPath = "C:\\Users\\Public\\weasel-compartment-debug.log"; + +void _DbgInit() { + if (!s_dbgLockInit) { + InitializeCriticalSection(&s_dbgLock); + s_dbgLockInit = true; + } + if (!s_dbgFile) { + s_dbgFile = fopen(s_dbgPath, "a"); + if (s_dbgFile) { + s_dbgEnabled = true; + SYSTEMTIME st; + GetLocalTime(&st); + fprintf(s_dbgFile, "\n=== WeaselTSF compartment debug session @ %04d-%02d-%02d %02d:%02d:%02d.%03d ===\n", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); + fflush(s_dbgFile); + OutputDebugStringW(L"[WeaselTSF] compartment debug log opened"); + } + } +} + +void _DbgLog(const char* fmt, ...) { + if (!s_dbgEnabled || !s_dbgFile) return; + EnterCriticalSection(&s_dbgLock); + SYSTEMTIME st; + GetLocalTime(&st); + fprintf(s_dbgFile, "[%04d-%02d-%02d %02d:%02d:%02d.%03d] ", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); + va_list args; + va_start(args, fmt); + vfprintf(s_dbgFile, fmt, args); + va_end(args); + fputc('\n', s_dbgFile); + fflush(s_dbgFile); + LeaveCriticalSection(&s_dbgLock); +} + STDAPI CCompartmentEventSink::QueryInterface(REFIID riid, _Outptr_ void** ppvObj) { if (ppvObj == nullptr) @@ -242,13 +286,11 @@ void WeaselTSF::_UninitCompartment() { } HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { + _DbgInit(); if (IsEqualGUID(guidCompartment, GUID_COMPARTMENT_KEYBOARD_OPENCLOSE)) { BOOL isOpenDbg = _IsKeyboardOpen(); - { - wchar_t buf[256]; - swprintf_s(buf, 256, L"[WeaselTSF] OPENCLOSE OnChange: isOpen=%d, isToOpenClose=%d, ascii_mode=%d\n", isOpenDbg, _isToOpenClose, _status.ascii_mode); - OutputDebugStringW(buf); - } + _DbgLog("OPENCLOSE OnChange: isOpen=%d, isToOpenClose=%d, ascii_mode=%d", + isOpenDbg, _isToOpenClose, _status.ascii_mode); if (_isToOpenClose) { BOOL isOpen = _IsKeyboardOpen(); // clear composition when close keyboard @@ -259,6 +301,8 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { _EnableLanguageBar(isOpen); _UpdateLanguageBar(_status); } else { + _DbgLog("OPENCLOSE else branch: toggle ascii_mode %d -> %d", + _status.ascii_mode, !_status.ascii_mode); _status.ascii_mode = !_status.ascii_mode; _SetKeyboardOpen(true); if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) @@ -269,6 +313,7 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { if (_pEditSessionContext) m_client.ClearComposition(); _UpdateLanguageBar(_status); + _DbgLog("OPENCLOSE else branch done: ascii_mode=%d", _status.ascii_mode); } } else if (IsEqualGUID(guidCompartment, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { @@ -276,21 +321,17 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { _GetCompartmentDWORD(convMode, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); - { - wchar_t buf[512]; - swprintf_s(buf, 512, - L"[WeaselTSF] CONVERSION OnChange: convMode=0x%lX (NATIVE=%d), desiredAscii=%d, statusAscii=%d, updatingLangBar=%d\n", - convMode, (convMode & TF_CONVERSIONMODE_NATIVE) ? 1 : 0, - desiredAsciiMode ? 1 : 0, _status.ascii_mode ? 1 : 0, - _updatingLanguageBar ? 1 : 0); - OutputDebugStringW(buf); - } + _DbgLog("CONVERSION OnChange: convMode=0x%lX (NATIVE=%d), desiredAscii=%d, statusAscii=%d, updatingLangBar=%d", + convMode, (convMode & TF_CONVERSIONMODE_NATIVE) ? 1 : 0, + desiredAsciiMode ? 1 : 0, _status.ascii_mode ? 1 : 0, + _updatingLanguageBar ? 1 : 0); if (_updatingLanguageBar) { - OutputDebugStringW(L"[WeaselTSF] CONVERSION: skipped (updatingLanguageBar)\n"); + _DbgLog("CONVERSION: skipped (updatingLanguageBar)"); return S_OK; } if (desiredAsciiMode != _status.ascii_mode) { - OutputDebugStringW(L"[WeaselTSF] CONVERSION: processing -> switching mode\n"); + _DbgLog("CONVERSION: processing -> switching mode (ascii %d -> %d)", + _status.ascii_mode, desiredAsciiMode); _status.ascii_mode = desiredAsciiMode; _SetKeyboardOpen(true); if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) @@ -301,8 +342,9 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { if (_pEditSessionContext) m_client.ClearComposition(); _UpdateLanguageBar(_status); + _DbgLog("CONVERSION done: ascii_mode=%d", _status.ascii_mode); } else { - OutputDebugStringW(L"[WeaselTSF] CONVERSION: skipped (value matches state)\n"); + _DbgLog("CONVERSION: skipped (value matches state)"); } } return S_OK; diff --git a/WeaselTSF/LanguageBar.cpp b/WeaselTSF/LanguageBar.cpp index 2926fc511e..818e1cb1a3 100644 --- a/WeaselTSF/LanguageBar.cpp +++ b/WeaselTSF/LanguageBar.cpp @@ -7,6 +7,10 @@ #include "CandidateList.h" #include +// debug logger (defined in Compartment.cpp) +extern void _DbgInit(); +extern void _DbgLog(const char* fmt, ...); + static const DWORD LANGBARITEMSINK_COOKIE = 0x42424242; static void HMENU2ITfMenu(HMENU hMenu, ITfMenu* pTfMenu) { @@ -402,6 +406,7 @@ void WeaselTSF::_UninitLanguageBar() { void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { if (!_pLangBarButton) return; + _DbgInit(); DWORD flags; _GetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); if (stat.ascii_mode) @@ -412,17 +417,13 @@ void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { flags |= TF_CONVERSIONMODE_FULLSHAPE; else flags &= (~TF_CONVERSIONMODE_FULLSHAPE); - { - wchar_t buf[256]; - swprintf_s(buf, 256, - L"[WeaselTSF] _UpdateLanguageBar: ascii=%d, flags=0x%lX (NATIVE=%d), updatingLangBar set=true\n", - stat.ascii_mode ? 1 : 0, flags, - (flags & TF_CONVERSIONMODE_NATIVE) ? 1 : 0); - OutputDebugStringW(buf); - } + _DbgLog("_UpdateLanguageBar: ascii=%d, flags=0x%lX (NATIVE=%d), setting guard=true", + stat.ascii_mode ? 1 : 0, flags, + (flags & TF_CONVERSIONMODE_NATIVE) ? 1 : 0); _updatingLanguageBar = true; _SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); _updatingLanguageBar = false; + _DbgLog("_UpdateLanguageBar: done, guard=false"); _pLangBarButton->UpdateWeaselStatus(stat); } From 8bac10576c6689bebd1282891770821985d4dda7 Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 9 Jul 2026 09:37:09 +0800 Subject: [PATCH 10/16] fix: remove _SetKeyboardOpen(true) from CONVERSION handler 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. --- WeaselTSF/Compartment.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/WeaselTSF/Compartment.cpp b/WeaselTSF/Compartment.cpp index f96edf4342..e3aa1d6d2e 100644 --- a/WeaselTSF/Compartment.cpp +++ b/WeaselTSF/Compartment.cpp @@ -333,7 +333,6 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { _DbgLog("CONVERSION: processing -> switching mode (ascii %d -> %d)", _status.ascii_mode, desiredAsciiMode); _status.ascii_mode = desiredAsciiMode; - _SetKeyboardOpen(true); if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) _EnableLanguageBar(true); _HandleLangBarMenuSelect(_status.ascii_mode From 725dc3c856a73629cbb9443420870af0222d20ee Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 9 Jul 2026 10:13:08 +0800 Subject: [PATCH 11/16] debug: log pThreadMgr pointer and threadId in ActivateEx 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. --- WeaselTSF/WeaselTSF.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/WeaselTSF/WeaselTSF.cpp b/WeaselTSF/WeaselTSF.cpp index 2e7dcffd9c..400d009b64 100644 --- a/WeaselTSF/WeaselTSF.cpp +++ b/WeaselTSF/WeaselTSF.cpp @@ -4,12 +4,17 @@ #include #include #include +#include #include "WeaselTSF.h" #include "CandidateList.h" #include "LanguageBar.h" #include "Compartment.h" #include "ResponseParser.h" +// debug logger (defined in Compartment.cpp) +extern void _DbgInit(); +extern void _DbgLog(const char* fmt, ...); + static void error_message(const WCHAR* msg) { static DWORD next_tick = 0; DWORD now = GetTickCount(); @@ -129,6 +134,10 @@ STDAPI WeaselTSF::ActivateEx(ITfThreadMgr* pThreadMgr, _pThreadMgr = pThreadMgr; _tfClientId = tfClientId; + _DbgInit(); + _DbgLog("ActivateEx: pThreadMgr=%p, tfClientId=%lu, threadId=%lu", + (void*)pThreadMgr, tfClientId, GetCurrentThreadId()); + if (!_InitThreadMgrEventSink()) goto ExitError; From 5e6e9016cc9bfa27c0129f71100191b05a54ba8e Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 9 Jul 2026 10:31:55 +0800 Subject: [PATCH 12/16] debug: write compartment debug log to %TEMP% instead of C:\Users\Public 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. --- WeaselTSF/Compartment.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/WeaselTSF/Compartment.cpp b/WeaselTSF/Compartment.cpp index e3aa1d6d2e..7803ab1f03 100644 --- a/WeaselTSF/Compartment.cpp +++ b/WeaselTSF/Compartment.cpp @@ -9,12 +9,24 @@ #include "CandidateList.h" #include "LanguageBar.h" -// Debug logging to file (enabled via session GUID env override) +// Debug logging to file static bool s_dbgEnabled = false; static FILE* s_dbgFile = nullptr; static CRITICAL_SECTION s_dbgLock; static bool s_dbgLockInit = false; -static const char* s_dbgPath = "C:\\Users\\Public\\weasel-compartment-debug.log"; +static char s_dbgPath[MAX_PATH] = {0}; + +static void _DbgInitPath() { + if (s_dbgPath[0] != 0) return; + char tempPath[MAX_PATH] = {0}; + DWORD len = GetTempPathA(MAX_PATH, tempPath); + if (len == 0 || len >= MAX_PATH) { + strcpy_s(s_dbgPath, MAX_PATH, "C:\\weasel-compartment-debug.log"); + return; + } + strcpy_s(s_dbgPath, MAX_PATH, tempPath); + strcat_s(s_dbgPath, MAX_PATH, "weasel-compartment-debug.log"); +} void _DbgInit() { if (!s_dbgLockInit) { @@ -22,13 +34,15 @@ void _DbgInit() { s_dbgLockInit = true; } if (!s_dbgFile) { + _DbgInitPath(); s_dbgFile = fopen(s_dbgPath, "a"); if (s_dbgFile) { s_dbgEnabled = true; SYSTEMTIME st; GetLocalTime(&st); - fprintf(s_dbgFile, "\n=== WeaselTSF compartment debug session @ %04d-%02d-%02d %02d:%02d:%02d.%03d ===\n", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); + fprintf(s_dbgFile, "\n=== WeaselTSF compartment debug session @ %04d-%02d-%02d %02d:%02d:%02d.%03d pid=%lu ===\n", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, + GetCurrentProcessId()); fflush(s_dbgFile); OutputDebugStringW(L"[WeaselTSF] compartment debug log opened"); } From 629d99d52c6c143d3914ac0899ffaccb212ad28e Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 9 Jul 2026 10:47:31 +0800 Subject: [PATCH 13/16] cleanup: remove all debug logging from WeaselTSF 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) --- WeaselTSF/Compartment.cpp | 80 +-------------------------------------- WeaselTSF/LanguageBar.cpp | 9 ----- WeaselTSF/WeaselTSF.cpp | 9 ----- 3 files changed, 2 insertions(+), 96 deletions(-) diff --git a/WeaselTSF/Compartment.cpp b/WeaselTSF/Compartment.cpp index 7803ab1f03..bb6316135b 100644 --- a/WeaselTSF/Compartment.cpp +++ b/WeaselTSF/Compartment.cpp @@ -3,68 +3,10 @@ #include "Compartment.h" #include #include -#include -#include #include "ResponseParser.h" #include "CandidateList.h" #include "LanguageBar.h" -// Debug logging to file -static bool s_dbgEnabled = false; -static FILE* s_dbgFile = nullptr; -static CRITICAL_SECTION s_dbgLock; -static bool s_dbgLockInit = false; -static char s_dbgPath[MAX_PATH] = {0}; - -static void _DbgInitPath() { - if (s_dbgPath[0] != 0) return; - char tempPath[MAX_PATH] = {0}; - DWORD len = GetTempPathA(MAX_PATH, tempPath); - if (len == 0 || len >= MAX_PATH) { - strcpy_s(s_dbgPath, MAX_PATH, "C:\\weasel-compartment-debug.log"); - return; - } - strcpy_s(s_dbgPath, MAX_PATH, tempPath); - strcat_s(s_dbgPath, MAX_PATH, "weasel-compartment-debug.log"); -} - -void _DbgInit() { - if (!s_dbgLockInit) { - InitializeCriticalSection(&s_dbgLock); - s_dbgLockInit = true; - } - if (!s_dbgFile) { - _DbgInitPath(); - s_dbgFile = fopen(s_dbgPath, "a"); - if (s_dbgFile) { - s_dbgEnabled = true; - SYSTEMTIME st; - GetLocalTime(&st); - fprintf(s_dbgFile, "\n=== WeaselTSF compartment debug session @ %04d-%02d-%02d %02d:%02d:%02d.%03d pid=%lu ===\n", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, - GetCurrentProcessId()); - fflush(s_dbgFile); - OutputDebugStringW(L"[WeaselTSF] compartment debug log opened"); - } - } -} - -void _DbgLog(const char* fmt, ...) { - if (!s_dbgEnabled || !s_dbgFile) return; - EnterCriticalSection(&s_dbgLock); - SYSTEMTIME st; - GetLocalTime(&st); - fprintf(s_dbgFile, "[%04d-%02d-%02d %02d:%02d:%02d.%03d] ", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); - va_list args; - va_start(args, fmt); - vfprintf(s_dbgFile, fmt, args); - va_end(args); - fputc('\n', s_dbgFile); - fflush(s_dbgFile); - LeaveCriticalSection(&s_dbgLock); -} - STDAPI CCompartmentEventSink::QueryInterface(REFIID riid, _Outptr_ void** ppvObj) { if (ppvObj == nullptr) @@ -300,11 +242,7 @@ void WeaselTSF::_UninitCompartment() { } HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { - _DbgInit(); if (IsEqualGUID(guidCompartment, GUID_COMPARTMENT_KEYBOARD_OPENCLOSE)) { - BOOL isOpenDbg = _IsKeyboardOpen(); - _DbgLog("OPENCLOSE OnChange: isOpen=%d, isToOpenClose=%d, ascii_mode=%d", - isOpenDbg, _isToOpenClose, _status.ascii_mode); if (_isToOpenClose) { BOOL isOpen = _IsKeyboardOpen(); // clear composition when close keyboard @@ -315,8 +253,6 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { _EnableLanguageBar(isOpen); _UpdateLanguageBar(_status); } else { - _DbgLog("OPENCLOSE else branch: toggle ascii_mode %d -> %d", - _status.ascii_mode, !_status.ascii_mode); _status.ascii_mode = !_status.ascii_mode; _SetKeyboardOpen(true); if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) @@ -327,25 +263,16 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { if (_pEditSessionContext) m_client.ClearComposition(); _UpdateLanguageBar(_status); - _DbgLog("OPENCLOSE else branch done: ascii_mode=%d", _status.ascii_mode); } } else if (IsEqualGUID(guidCompartment, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { + if (_updatingLanguageBar) + return S_OK; DWORD convMode = 0; _GetCompartmentDWORD(convMode, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); - _DbgLog("CONVERSION OnChange: convMode=0x%lX (NATIVE=%d), desiredAscii=%d, statusAscii=%d, updatingLangBar=%d", - convMode, (convMode & TF_CONVERSIONMODE_NATIVE) ? 1 : 0, - desiredAsciiMode ? 1 : 0, _status.ascii_mode ? 1 : 0, - _updatingLanguageBar ? 1 : 0); - if (_updatingLanguageBar) { - _DbgLog("CONVERSION: skipped (updatingLanguageBar)"); - return S_OK; - } if (desiredAsciiMode != _status.ascii_mode) { - _DbgLog("CONVERSION: processing -> switching mode (ascii %d -> %d)", - _status.ascii_mode, desiredAsciiMode); _status.ascii_mode = desiredAsciiMode; if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) _EnableLanguageBar(true); @@ -355,9 +282,6 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { if (_pEditSessionContext) m_client.ClearComposition(); _UpdateLanguageBar(_status); - _DbgLog("CONVERSION done: ascii_mode=%d", _status.ascii_mode); - } else { - _DbgLog("CONVERSION: skipped (value matches state)"); } } return S_OK; diff --git a/WeaselTSF/LanguageBar.cpp b/WeaselTSF/LanguageBar.cpp index 818e1cb1a3..c9f38b3e48 100644 --- a/WeaselTSF/LanguageBar.cpp +++ b/WeaselTSF/LanguageBar.cpp @@ -7,10 +7,6 @@ #include "CandidateList.h" #include -// debug logger (defined in Compartment.cpp) -extern void _DbgInit(); -extern void _DbgLog(const char* fmt, ...); - static const DWORD LANGBARITEMSINK_COOKIE = 0x42424242; static void HMENU2ITfMenu(HMENU hMenu, ITfMenu* pTfMenu) { @@ -406,7 +402,6 @@ void WeaselTSF::_UninitLanguageBar() { void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { if (!_pLangBarButton) return; - _DbgInit(); DWORD flags; _GetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); if (stat.ascii_mode) @@ -417,13 +412,9 @@ void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { flags |= TF_CONVERSIONMODE_FULLSHAPE; else flags &= (~TF_CONVERSIONMODE_FULLSHAPE); - _DbgLog("_UpdateLanguageBar: ascii=%d, flags=0x%lX (NATIVE=%d), setting guard=true", - stat.ascii_mode ? 1 : 0, flags, - (flags & TF_CONVERSIONMODE_NATIVE) ? 1 : 0); _updatingLanguageBar = true; _SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); _updatingLanguageBar = false; - _DbgLog("_UpdateLanguageBar: done, guard=false"); _pLangBarButton->UpdateWeaselStatus(stat); } diff --git a/WeaselTSF/WeaselTSF.cpp b/WeaselTSF/WeaselTSF.cpp index 400d009b64..2e7dcffd9c 100644 --- a/WeaselTSF/WeaselTSF.cpp +++ b/WeaselTSF/WeaselTSF.cpp @@ -4,17 +4,12 @@ #include #include #include -#include #include "WeaselTSF.h" #include "CandidateList.h" #include "LanguageBar.h" #include "Compartment.h" #include "ResponseParser.h" -// debug logger (defined in Compartment.cpp) -extern void _DbgInit(); -extern void _DbgLog(const char* fmt, ...); - static void error_message(const WCHAR* msg) { static DWORD next_tick = 0; DWORD now = GetTickCount(); @@ -134,10 +129,6 @@ STDAPI WeaselTSF::ActivateEx(ITfThreadMgr* pThreadMgr, _pThreadMgr = pThreadMgr; _tfClientId = tfClientId; - _DbgInit(); - _DbgLog("ActivateEx: pThreadMgr=%p, tfClientId=%lu, threadId=%lu", - (void*)pThreadMgr, tfClientId, GetCurrentThreadId()); - if (!_InitThreadMgrEventSink()) goto ExitError; From e0c2ec6af46f66438347428a8d1e574d3897f7d8 Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 9 Jul 2026 10:51:44 +0800 Subject: [PATCH 14/16] docs: update Windows 11 compatibility fix with final analysis 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. --- docs/windows11-compatibility-fix.md | 124 +++++++++++++++++----------- 1 file changed, 77 insertions(+), 47 deletions(-) diff --git a/docs/windows11-compatibility-fix.md b/docs/windows11-compatibility-fix.md index c8fdc70012..c19e3d00ac 100644 --- a/docs/windows11-compatibility-fix.md +++ b/docs/windows11-compatibility-fix.md @@ -9,66 +9,77 @@ ## 根因 -经 Windows 11 实测发现,问题有**两个独立的层面**: +经 Windows 11 实测和日志分析,问题有**三个独立的层面**: -### 层面 1:im-control 使用 TF_CLIENTID_NULL 调用 SetValue,Win11 不触发 OnChange +### 层面 1:CoCreateInstance(CLSID_TF_ThreadMgr) 在 Win11 返回新实例 -im-control 的 hook DLL 调用 `ITfCompartment::SetValue(0, ...)`(`TfClientId = TF_CLIENTID_NULL`)。WeaselTSF 自身的 `_SetCompartmentDWORD` 使用 `SetValue(_tfClientId, ...)`(通过 `ITfThreadMgr::Activate` 获取的有效非零 ID)。 +im-control 的 hook 用 `CoCreateInstance(CLSID_TF_ThreadMgr)` 获取 ThreadMgr。Windows 10 返回 per-thread 单例(与 WeaselTSF 注册 sink 的实例相同),Windows 11 返回**新实例**——compartment 写入到了不同实例,WeaselTSF 的 sink 收不到 OnChange。 -**Windows 10**:不区分 `TfClientId`,所有 `SetValue` 都触发 `OnChange`。 -**Windows 11**:仅为已激活客户端(非零 `TfClientId`)的写入触发 `OnChange`。`TF_CLIENTID_NULL` 的写入不触发通知。 +**日志证据**:im-control `SetValue` 返回 `S_OK`,但 Weasel 日志中 `_HandleCompartment` 完全没有被调用。 + +### 层面 2:TF_CLIENTID_NULL 的 SetValue 在 Win11 不触发 OnChange -这解释了实测现象: -- Shift 键切换正常:`_UpdateLanguageBar` → `_SetCompartmentDWORD` 用 `_tfClientId` → `OnChange` 触发 → CONVERSION handler 执行 → 值匹配 → 跳过 ✓ -- im-control 外部写入失效:hook 用 `0` → Win11 不触发 `OnChange` → CONVERSION handler 不执行 → RIME 保持原状 ✗ +im-control 的 hook 用 `SetValue(0, ...)`(`TF_CLIENTID_NULL`)。WeaselTSF 自身用 `SetValue(_tfClientId, ...)`(有效非零 ID)。 + +**Windows 10**:不区分 `TfClientId`,所有 `SetValue` 都触发 `OnChange`。 +**Windows 11**:仅为已激活客户端(非零 `TfClientId`)的写入触发 `OnChange`。 -### 层面 2:blind toggle 依赖同步 OnChange 回调(已被值驱动修复) +### 层面 3:CONVERSION handler 的 _SetKeyboardOpen(true) 引发级联反转 -即使 `OnChange` 正确触发,blind toggle 方案在 Windows 11 仍有异步回调问题(详见下文)。此层面已由值驱动修复解决。 +CONVERSION handler 调用 `_SetKeyboardOpen(true)` 写 OPENCLOSE=1,触发 OPENCLOSE handler 的 else 分支(`_isToOpenClose=false`,为 Shift 键设计的 blind toggle),将 CONVERSION 刚设置好的 `ascii_mode` 反转。 -### Windows 10 vs Windows 11 回调时序对比 +**日志证据**: +``` +CONVERSION: processing -> switching mode (ascii 0 -> 1) ← 正确 +OPENCLOSE OnChange: isOpen=1 -> toggle ascii_mode 1 -> 0 ← 级联反转 +CONVERSION done: ascii_mode=0 ← 结果中文(错) +``` +移除 `_SetKeyboardOpen(true)` 后: ``` -Windows 10(同步 OnChange): - SetValue ──→ OnChange(同步) ──→ _UpdateLanguageBar ──→ SetValue ──→ OnChange(同步, 守卫=true, 跳过) - toggle 次数: 1(_UpdateLanguageBar 的自触发被守卫拦截) - ✗ 但 OPENCLOSE handler 也会 toggle,凑成偶数 → 结果正确 - -Windows 11(异步 OnChange): - SetValue ──→ OnChange(异步, 稍后) ──→ _UpdateLanguageBar ──→ SetValue ──→ OnChange(异步, 稍后) - ......守卫已复位...... - OnChange #1 到达 → toggle #1 - OnChange #2 到达 → toggle #2(守卫已失效) - OnChange #3 到达 → toggle #3(系统注入的额外写入) - toggle 次数: 奇数 → 结果错误 +CONVERSION: processing -> switching mode (ascii 0 -> 1) ← 正确 +CONVERSION done: ascii_mode=1 ← 结果英文(对) ``` ## 修复方案 -### 1. im-control:使用有效 TfClientId 调用 SetValue(根因修复) +### 1. im-control:使用 TF_GetThreadMgr 获取 per-thread 单例 **修改文件:** `injector/hook.cpp` -im-control 的 hook 在 `SetValue` 时使用 `TF_CLIENTID_NULL`(0),Windows 11 不为此类写入触发 `OnChange`。 +用 msctf.dll 导出的 `TF_GetThreadMgr` 替代 `CoCreateInstance(CLSID_TF_ThreadMgr)`,确保获取与 WeaselTSF 相同的 ThreadMgr 实例: -修复:调用 `ITfThreadMgr::Activate` 获取有效 `TfClientId`,用于所有 `SetValue` 调用,完成后 `Deactivate`: +```cpp +typedef HRESULT(WINAPI* PFN_TF_GetThreadMgr)(ITfThreadMgr**); +static ITfThreadMgr* GetThreadMgrSingleton() { + HMODULE hMsctf = GetModuleHandleW(L"msctf.dll"); + // ... GetProcAddress("TF_GetThreadMgr") ... + ITfThreadMgr* pThreadMgr = nullptr; + pfn(&pThreadMgr); + return pThreadMgr; +} +``` + +### 2. im-control:使用有效 TfClientId 调用 SetValue + +**修改文件:** `injector/hook.cpp` + +调用 `ITfThreadMgr::Activate` 获取有效 `TfClientId`,用于所有 `SetValue` 调用,完成后 `Deactivate`: ```cpp TfClientId clientId = TF_CLIENTID_NULL; -// ... 创建 pThreadMgr 后 ... pThreadMgr->Activate(&clientId); // ... SetValue(clientId, ...) 替代 SetValue(0, ...) ... pThreadMgr->Deactivate(); ``` -### 2. Weasel:CONVERSION handler 改为值驱动(防御层) +### 3. Weasel:CONVERSION handler 改为值驱动 **修改文件:** `WeaselTSF/Compartment.cpp` 将 blind toggle 替换为读取 compartment 实际值并与 `_status.ascii_mode` 比对: ```cpp -// 修复后 } else if (IsEqualGUID(guidCompartment, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { if (_updatingLanguageBar) @@ -79,7 +90,7 @@ pThreadMgr->Deactivate(); bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); if (desiredAsciiMode != _status.ascii_mode) { _status.ascii_mode = desiredAsciiMode; - _SetKeyboardOpen(true); + // 注意:不调用 _SetKeyboardOpen(true),避免触发 OPENCLOSE 级联 if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) _EnableLanguageBar(true); _HandleLangBarMenuSelect(_status.ascii_mode @@ -92,17 +103,38 @@ pThreadMgr->Deactivate(); } ``` -**天然幂等性**:即使 `_updatingLanguageBar` 守卫失效(异步回调 / 系统注入),重新读取 compartment 值会得到与当前 `_status.ascii_mode` 一致的结果(因为 `_UpdateLanguageBar` 已将正确值写入),`desiredAsciiMode == _status.ascii_mode` → 跳过。不会产生额外翻转。 +**天然幂等性**:即使 `_updatingLanguageBar` 守卫失效(异步回调 / 系统注入),重新读取 compartment 值会得到与当前 `_status.ascii_mode` 一致的结果,`desiredAsciiMode == _status.ascii_mode` → 跳过。 + +### 4. Weasel:从 CONVERSION handler 移除 _SetKeyboardOpen(true) -### 3. im-control:OPENCLOSE 跳过未变化的写入(防御性优化) +**修改文件:** `WeaselTSF/Compartment.cpp` + +CONVERSION handler 不再调用 `_SetKeyboardOpen(true)`。两个 compartment 完全解耦: +- **OPENCLOSE**:管理 IME 启用/禁用(由 OPENCLOSE handler 处理,为 Ctrl+Space 和 Shift 设计) +- **CONVERSION**:管理中/英文模式(由 CONVERSION handler 处理,为外部工具和系统切换设计) + +### 5. im-control:OPENCLOSE 跳过未变化的写入 **修改文件:** `injector/hook.cpp` -im-control 原先对 `GUID_COMPARTMENT_KEYBOARD_OPENCLOSE` 无条件 `SetValue`,即使值未变。这会触发 Weasel 的 OPENCLOSE handler(`_isToOpenClose=false` 时仍为 blind toggle),产生不必要的 `ascii_mode` 翻转。 +写入前先 `GetValue` 比对,仅在值变化时 `SetValue`,避免不必要地触发 OPENCLOSE handler。 + +### 6. vim 插件:去掉 -k open,只写 CONVERSION + +**修改文件:** `autoload/im_select.vim` + +`im_control_set_mode()` 从 `[im-control, '-k', 'open', '-c', 'native']` 改为 `[im-control, '-c', 'native']`,两个 compartment 完全解耦。 + +## 设计原则 + +遵循 Weasel 开发者的指导: -修复:写入前先 `GetValue` 比对,仅在值变化时 `SetValue`,与 CONVERSION 的已有逻辑一致。 +> GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 和 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE 是不一样的消息,作用要区分。输入功能是主功能,外部消息控制是辅助,不能因为要引入辅助功能导致主功能失效是基本要求。 -> 此改动非 Windows 11 兼容性修复的必要条件——Weasel 的 CONVERSION 值驱动修复已能校正 OPENCLOSE handler 的错误 toggle。但消除不必要的 OPENCLOSE 写入可以避免 RIME 引擎收到一错一对的 TrayCommand,减少时序问题。 +我们的修改完全符合此原则: +- CONVERSION handler 不再触碰 OPENCLOSE compartment +- OPENCLOSE handler 未改动(Shift/Ctrl+Space 主功能不受影响) +- 外部工具只写 CONVERSION 切换中英文,不写 OPENCLOSE ## 方案对比 @@ -110,19 +142,15 @@ im-control 原先对 `GUID_COMPARTMENT_KEYBOARD_OPENCLOSE` 无条件 `SetValue` |------|--------------------------|------------------| | 回调次数依赖 | 严格依赖偶数次 `OnChange` | 幂等,不依赖回调次数 | | 重入守卫 | 唯一防线,异步回调下失效 | 第一防线;值比对是第二防线 | +| compartment 耦合 | CONVERSION 写 OPENCLOSE(级联反转) | 完全解耦 | | Windows 10 | 正常 | 正常 | -| Windows 11 | 失效(奇数次 toggle) | 正常(幂等跳过) | -| stale value 风险 | 无 | 若 `GetValue` 返回旧值则静默跳过(安全失败) | +| Windows 11 | 失效 | 正常 | -## 为什么不改 OPENCLOSE handler - -OPENCLOSE handler 的 else 分支(`_isToOpenClose=false`)使用 blind toggle 是**为 Ctrl+Space 设计的**——每次 Ctrl+Space 切换 OPENCLOSE 值,Weasel 对应翻转 `ascii_mode`。改为值驱动会破坏此行为。 - -OPENCLOSE 值(open/close)与 `ascii_mode`(中/英)没有直接映射关系:OPENCLOSE=true 表示 IME 激活(可能是中文也可能是英文),OPENCLOSE=false 表示 IME 关闭(英文直通)。因此无法像 CONVERSION 那样从 compartment 值推导 `ascii_mode`。 +## 部署 -通过 im-control 侧跳过未变化的 OPENCLOSE 写入,避免从外部触发此 handler,是最小侵入的解决方案。 +### 重要:部署后需重启所有使用 RIME 的应用 -## 部署 +`weasel.dll` 由 TSF 框架在进程启动时加载。部署新 DLL 后,已运行的进程仍使用内存中的旧 DLL。必须重启 Windows Terminal、Total Commander、gvim 等应用(或重启 Windows)才能加载新版本。 ### 编译 @@ -145,9 +173,7 @@ Copy-Item output\weaselx64.dll C:\Windows\system32\weasel.dll -Force Copy-Item output\weasel.dll C:\Windows\SysWOW64\weasel.dll -Force ``` -> TSF 框架从系统路径加载 DLL,详见 [原始修复文档](conversion-compartment-fix-analysis.md#2-dll-部署位置)。 - -### 编译部署 im-control(可选) +### 编译部署 im-control ``` cd C:\Apps\git-kb\repos\VimWei\im-control @@ -161,6 +187,10 @@ cmake --install build --prefix bin --config RelWithDebInfo Copy-Item bin\* "C:\Apps\VimReader\lib\utils\im-control\" -Force ``` +### vim 插件 + +vim-im-select 插件为纯脚本,无需编译,pull 后 reload vim 配置即可。 + ## 相关文档 - [原始修复:CONVERSION handler 响应外部变更](conversion-compartment-fix-analysis.md) — blind toggle 方案(Windows 10) From c6b314dde4ef83d1f097f547955ced991da3a590 Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 9 Jul 2026 11:03:08 +0800 Subject: [PATCH 15/16] docs: merge compartment fix analysis into single document 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. --- docs/compartment-external-control-fix.md | 296 ++++++++++++++++++++ docs/conversion-compartment-fix-analysis.md | 180 ------------ docs/windows11-compatibility-fix.md | 198 ------------- 3 files changed, 296 insertions(+), 378 deletions(-) create mode 100644 docs/compartment-external-control-fix.md delete mode 100644 docs/conversion-compartment-fix-analysis.md delete mode 100644 docs/windows11-compatibility-fix.md diff --git a/docs/compartment-external-control-fix.md b/docs/compartment-external-control-fix.md new file mode 100644 index 0000000000..35b7ff0938 --- /dev/null +++ b/docs/compartment-external-control-fix.md @@ -0,0 +1,296 @@ +# WeaselTSF Compartment 外部控制修复分析 + +## 问题 + +通过 TSF compartment 程序化切换中英文模式(如 `ITfCompartment::SetValue(GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)`)对 Weasel(小狼毫)无效。外部工具写入 compartment 后,Weasel 不切换模式,且 compartment 值被恢复原状。 + +此问题在 Windows 10 和 Windows 11 上表现不同: +- **Windows 10**:compartment 写入能触发 `OnChange`,但 CONVERSION handler 逻辑缺陷导致外部变更被撤销 +- **Windows 11**:compartment 写入根本不触发 `OnChange`,且 CONVERSION handler 存在级联反转问题 + +## 架构背景 + +WeaselTSF 是 TSF Text Service(实现 `ITfTextInputProcessorEx`),不接收 `WM_IME_CONTROL`。Windows TSF 框架负责在 IMM32 API 与 TSF compartments 之间桥接: + +``` +ImmSetConversionStatus(IME_CMODE_NATIVE) + → TSF framework 翻译 + → ITfCompartment::SetValue(GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION) + → _HandleCompartment() ← 修复位置 +``` + +两个 compartment 各司其职: +- **GUID_COMPARTMENT_KEYBOARD_OPENCLOSE**:管理 IME 启用/禁用(对应 Ctrl+Space、`-k open/close`) +- **GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION**:管理中/英文模式(对应 Shift 切换、`-c native/alphanumeric`) + +## 根因 + +经 Windows 10 和 Windows 11 实测和日志分析,问题有**四个独立的层面**: + +### 1. CONVERSION handler 撤销外部变更(Windows 10) + +`_HandleCompartment` 中 `GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION` 的原始实现: + +```cpp +// 原始代码 (commit 93eec2d) +} else if (IsEqualGUID(guidCompartment, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { + BOOL isOpen = _IsKeyboardOpen(); + if (isOpen) { + weasel::ResponseParser parser(NULL, NULL, &_status, NULL, + &_cand->style()); + bool ok = m_client.GetResponseData(std::ref(parser)); // 查询 RIME 后端当前状态 + _UpdateLanguageBar(_status); // 写回 compartment + } +} +``` + +**问题链条**: + +1. 外部工具(如 im-control)写入 compartment,清除 `TF_CONVERSIONMODE_NATIVE` 位(请求切英文) +2. TSF 触发 `ITfCompartmentEventSink::OnChange` → `_HandleCompartment` +3. handler 调用 `m_client.GetResponseData(parser)` —— 从 RIME 后端获取**当前状态**(仍是中文,因为 RIME 还没收到切换指令) +4. handler 调用 `_UpdateLanguageBar(_status)` —— 把当前状态(中文)写回 compartment +5. compartment 被恢复为 `TF_CONVERSIONMODE_NATIVE` 置位 —— **外部变更被静默撤销** + +原始代码从不读取 compartment 值来判断外部请求的目标模式,也不调用 `_HandleLangBarMenuSelect` 通知 RIME 引擎切换。它只是把 RIME 后端的当前状态同步回 compartment,方向与需求完全相反。 + +### 2. CoCreateInstance(CLSID_TF_ThreadMgr) 在 Win11 返回新实例 + +im-control 的 hook 用 `CoCreateInstance(CLSID_TF_ThreadMgr)` 获取 ThreadMgr。Windows 10 返回 per-thread 单例(与 WeaselTSF 注册 sink 的实例相同),Windows 11 返回**新实例**——compartment 写入到了不同实例,WeaselTSF 的 sink 收不到 OnChange。 + +**日志证据**:im-control `SetValue` 返回 `S_OK`,但 Weasel 日志中 `_HandleCompartment` 完全没有被调用。 + +### 3. TF_CLIENTID_NULL 的 SetValue 在 Win11 不触发 OnChange + +im-control 的 hook 用 `SetValue(0, ...)`(`TF_CLIENTID_NULL`)。WeaselTSF 自身用 `SetValue(_tfClientId, ...)`(有效非零 ID)。 + +**Windows 10**:不区分 `TfClientId`,所有 `SetValue` 都触发 `OnChange`。 +**Windows 11**:仅为已激活客户端(非零 `TfClientId`)的写入触发 `OnChange`。 + +### 4. CONVERSION handler 的 _SetKeyboardOpen(true) 引发级联反转 + +CONVERSION handler 调用 `_SetKeyboardOpen(true)` 写 OPENCLOSE=1,触发 OPENCLOSE handler 的 else 分支(`_isToOpenClose=false`,为 Shift 键设计的 blind toggle),将 CONVERSION 刚设置好的 `ascii_mode` 反转。 + +**日志证据**: +``` +CONVERSION: processing -> switching mode (ascii 0 -> 1) ← 正确 +OPENCLOSE OnChange: isOpen=1 -> toggle ascii_mode 1 -> 0 ← 级联反转 +CONVERSION done: ascii_mode=0 ← 结果中文(错) +``` + +移除 `_SetKeyboardOpen(true)` 后: +``` +CONVERSION: processing -> switching mode (ascii 0 -> 1) ← 正确 +CONVERSION done: ascii_mode=1 ← 结果英文(对) +``` + +## 修复方案 + +### 1. Weasel:CONVERSION handler 改为值驱动 + +**修改文件:** `WeaselTSF/Compartment.cpp` + +将原始的"查询后端 → 写回 compartment"改为"读取 compartment 值 → 比对状态 → 通知 RIME → 同步 UI": + +```cpp +} else if (IsEqualGUID(guidCompartment, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { + if (_updatingLanguageBar) + return S_OK; + DWORD convMode = 0; + _GetCompartmentDWORD(convMode, + GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); + bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); + if (desiredAsciiMode != _status.ascii_mode) { + _status.ascii_mode = desiredAsciiMode; + // 注意:不调用 _SetKeyboardOpen(true),避免触发 OPENCLOSE 级联 + if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) + _EnableLanguageBar(true); + _HandleLangBarMenuSelect(_status.ascii_mode + ? ID_WEASELTRAY_ENABLE_ASCII + : ID_WEASELTRAY_DISABLE_ASCII); + if (_pEditSessionContext) + m_client.ClearComposition(); + _UpdateLanguageBar(_status); + } +} +``` + +**天然幂等性**:即使 `_updatingLanguageBar` 守卫失效(异步回调 / 系统注入),重新读取 compartment 值会得到与当前 `_status.ascii_mode` 一致的结果,`desiredAsciiMode == _status.ascii_mode` → 跳过。不会产生额外翻转。 + +`_HandleLangBarMenuSelect` → `TrayCommand` IPC → RIME 后端的 `SetOption("ascii_mode")`,与 Shift 按键切换走同一路径,确保行为和配置(如 `global_ascii_mode`)一致。 + +### 2. Weasel:从 CONVERSION handler 移除 _SetKeyboardOpen(true) + +**修改文件:** `WeaselTSF/Compartment.cpp` + +CONVERSION handler 不再调用 `_SetKeyboardOpen(true)`。两个 compartment 完全解耦: +- **OPENCLOSE**:管理 IME 启用/禁用(由 OPENCLOSE handler 处理,为 Ctrl+Space 和 Shift 设计) +- **CONVERSION**:管理中/英文模式(由 CONVERSION handler 处理,为外部工具和系统切换设计) + +### 3. Weasel:重入守卫 + +**修改文件:** `WeaselTSF/LanguageBar.cpp`、`WeaselTSF/WeaselTSF.h` + +`_UpdateLanguageBar` 内部调用 `_SetCompartmentDWORD` 写入 CONVERSION compartment,这会再次触发 `OnChange` → `_HandleCompartment`。如果不加守卫,handler 会再次处理,导致双重翻转或无限递归。 + +```cpp +// LanguageBar.cpp +void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { + // ... + _updatingLanguageBar = true; + _SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); + _updatingLanguageBar = false; + // ... +} +``` + +```cpp +// WeaselTSF.h +BOOL _updatingLanguageBar = false; +``` + +### 4. im-control:使用 TF_GetThreadMgr 获取 per-thread 单例 + +**修改文件:** `injector/hook.cpp` + +用 msctf.dll 导出的 `TF_GetThreadMgr` 替代 `CoCreateInstance(CLSID_TF_ThreadMgr)`,确保获取与 WeaselTSF 相同的 ThreadMgr 实例: + +```cpp +typedef HRESULT(WINAPI* PFN_TF_GetThreadMgr)(ITfThreadMgr**); +static ITfThreadMgr* GetThreadMgrSingleton() { + HMODULE hMsctf = GetModuleHandleW(L"msctf.dll"); + // ... GetProcAddress("TF_GetThreadMgr") ... + ITfThreadMgr* pThreadMgr = nullptr; + pfn(&pThreadMgr); + return pThreadMgr; +} +``` + +### 5. im-control:使用有效 TfClientId 调用 SetValue + +**修改文件:** `injector/hook.cpp` + +调用 `ITfThreadMgr::Activate` 获取有效 `TfClientId`,用于所有 `SetValue` 调用,完成后 `Deactivate`: + +```cpp +TfClientId clientId = TF_CLIENTID_NULL; +pThreadMgr->Activate(&clientId); +// ... SetValue(clientId, ...) 替代 SetValue(0, ...) ... +pThreadMgr->Deactivate(); +``` + +### 6. im-control:OPENCLOSE 跳过未变化的写入 + +**修改文件:** `injector/hook.cpp` + +写入前先 `GetValue` 比对,仅在值变化时 `SetValue`,避免不必要地触发 OPENCLOSE handler。 + +### 7. vim 插件:去掉 -k open,只写 CONVERSION + +**修改文件:** `autoload/im_select.vim`(vim-im-select) + +`im_control_set_mode()` 从 `[im-control, '-k', 'open', '-c', 'native']` 改为 `[im-control, '-c', 'native']`,两个 compartment 完全解耦。 + +## 设计原则 + +遵循 fxliang 的指导: + +> GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 和 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE 是不一样的消息,作用要区分。在 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE 的情况下 GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 的消息不应该再作任何响应,否则就是对旧功能的 break。需要明确的是,输入功能是主功能,外部消息控制是辅助,不能因为要引入辅助功能导致主功能失效是基本要求。 + +我们的修改完全符合此原则: +- CONVERSION handler 不再触碰 OPENCLOSE compartment +- OPENCLOSE handler 未改动(Shift/Ctrl+Space 主功能不受影响) +- 外部工具只写 CONVERSION 切换中英文,不写 OPENCLOSE + +## 方案对比 + +| 项目 | 原始代码 (93eec2d) | blind toggle (f14f2a7) | 值驱动(最终) | +|------|---------------------|------------------------|----------------| +| 响应方式 | 查询后端→写回(撤销外部变更) | blind toggle | 读取值→比对→切换 | +| 回调次数依赖 | N/A | 严格依赖偶数次 OnChange | 幂等,不依赖回调次数 | +| compartment 耦合 | 无 | CONVERSION 写 OPENCLOSE(级联反转) | 完全解耦 | +| 重入守卫 | 无 | 唯一防线,异步回调下失效 | 第一防线;值比对是第二防线 | +| Windows 10 | 失效 | 正常 | 正常 | +| Windows 11 | 失效 | 失效 | 正常 | + +## DLL 部署位置 + +WeaselTSF 的 CLSID `{A3F4CDED-B1E9-41EE-9CA6-7B4D0DE6CB0A}` 注册路径为: + +| 架构 | 注册路径 | +|------|---------| +| 64-bit | `C:\Windows\system32\weasel.dll` | +| 32-bit | `C:\Windows\SysWOW64\weasel.dll` | + +TSF 框架从上述系统路径加载 DLL。如果将修复后的 DLL 部署到其他路径(如 `C:\Program Files\Rime\weasel-0.17.4\`),系统不会加载它,进程内仍是旧 DLL。 + +### 更新方法 + +管理员身份打开 PowerShell,执行: + +```powershell +Rename-Item "C:\Windows\system32\weasel.dll" "weasel.dll.bak" -Force +Copy-Item "C:\path\to\weaselx64.dll" "C:\Windows\system32\weasel.dll" -Force +Rename-Item "C:\Windows\SysWOW64\weasel.dll" "weasel.dll.bak" -Force +Copy-Item "C:\path\to\weasel.dll" "C:\Windows\SysWOW64\weasel.dll" -Force +``` + +### 重要:部署后需重启所有使用 RIME 的应用 + +`weasel.dll` 由 TSF 框架在进程启动时加载。部署新 DLL 后,已运行的进程仍使用内存中的旧 DLL。必须重启 Windows Terminal、Total Commander、gvim 等应用(或重启 Windows)才能加载新版本。 + +## 编译 + +### Weasel + +在装有 Visual Studio 2022 + Boost 的机器上编译: + +``` +cd C:\Apps\git-kb\repos\VimWei\weasel +git pull +build.bat weasel release +``` + +产出 `output\weasel.dll`(Win32)和 `output\weaselx64.dll`(x64)。 + +### im-control + +``` +cd C:\Apps\git-kb\repos\VimWei\im-control +git pull +cmake -S . -B build -G "Visual Studio 17 2022" +cmake --build build --config RelWithDebInfo +cmake --install build --prefix bin --config RelWithDebInfo +``` + +```powershell +Copy-Item bin\* "C:\Apps\VimReader\lib\utils\im-control\" -Force +``` + +### vim 插件 + +vim-im-select 插件为纯脚本,无需编译,pull 后 reload vim 配置即可。 + +## 验证 + +### 测试环境 + +- 操作系统:Windows 10 和 Windows 11 +- 前台进程:gvim 9.2.0735、Windows Terminal、Total Commander +- 外部工具:im-control —— 通过 `SetWindowsHookEx` 注入 hook DLL 到前台进程,在目标线程内调用 `ITfCompartment::SetValue` + +### 测试结果 + +Windows 10 和 Windows 11 下均通过: +- gvim:进入/离开 insert/command mode 切换英文,Shift 切中文后 ESC 回 normal mode 自动恢复英文 +- Windows Terminal / Total Commander:AppIME 进入窗口自动切英文,8 秒空闲后自动切回英文 +- 用户 Shift 键切换中英文、Ctrl+Space 启停 IME 均正常 + +## 相关链接 + +- [rime/weasel#1371](https://github.com/rime/weasel/issues/1371) +- [weasel fork](https://github.com/VimWei/weasel/tree/im-control) +- [im-control fork](https://github.com/VimWei/im-control/tree/dev) +- [vim-im-select fork](https://github.com/VimWei/vim-im-select/tree/im-control) diff --git a/docs/conversion-compartment-fix-analysis.md b/docs/conversion-compartment-fix-analysis.md deleted file mode 100644 index 553c1db872..0000000000 --- a/docs/conversion-compartment-fix-analysis.md +++ /dev/null @@ -1,180 +0,0 @@ -# 修复分析:WeaselTSF 响应 TSF 转换模式 Compartment 外部变更 - -## 问题 - -通过 TSF compartment 程序化切换中英文模式(如 `ITfCompartment::SetValue(GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)`)对小狼毫无效。外部工具写入 compartment 后,小狼毫不切换模式,且 compartment 值被恢复原状。 - -## 行为变化 - -| 操作 | 修复前 | 修复后 | -|------|--------|--------| -| 通过 TSF compartment 设置转换模式 | 无效果(外部变更被撤销) | 正常切换中英文 | -| 键盘关闭时设置转换模式 | 不处理 | 自动打开键盘并切换 | -| Shift 按键切换 | 正常 | 正常(不变) | -| 托盘菜单切换 | 正常 | 正常(不变) | - -## 架构背景 - -WeaselTSF 是 TSF Text Service(实现 `ITfTextInputProcessorEx`),不接收 `WM_IME_CONTROL`。Windows TSF 框架负责在 IMM32 API 与 TSF compartments 之间桥接: - -``` -ImmSetConversionStatus(IME_CMODE_NATIVE) - → TSF framework 翻译 - → ITfCompartment::SetValue(GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION) - → _HandleCompartment() ← 修复位置 -``` - -## 根因 - -根因有两个层面:**源代码逻辑缺陷** 和 **DLL 部署位置**。两者必须同时解决,缺一不可。 - -### 1. 源代码:CONVERSION handler 撤销外部变更 - -`_HandleCompartment` 中 `GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION` 的原始实现: - -```cpp -// 原始代码 (commit 93eec2d) -} else if (IsEqualGUID(guidCompartment, - GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { - BOOL isOpen = _IsKeyboardOpen(); - if (isOpen) { - weasel::ResponseParser parser(NULL, NULL, &_status, NULL, - &_cand->style()); - bool ok = m_client.GetResponseData(std::ref(parser)); // 查询 RIME 后端当前状态 - _UpdateLanguageBar(_status); // 写回 compartment - } -} -``` - -**问题链条**: - -1. 外部工具(如 im-control)写入 compartment,清除 `TF_CONVERSIONMODE_NATIVE` 位(请求切英文) -2. TSF 触发 `ITfCompartmentEventSink::OnChange` → `_HandleCompartment` -3. handler 调用 `m_client.GetResponseData(parser)` —— 从 RIME 后端获取**当前状态**(仍是中文,因为 RIME 还没收到切换指令) -4. handler 调用 `_UpdateLanguageBar(_status)` —— 把当前状态(中文)写回 compartment -5. compartment 被恢复为 `TF_CONVERSIONMODE_NATIVE` 置位 —— **外部变更被静默撤销** - -原始代码从不读取 compartment 值来判断外部请求的目标模式,也不调用 `_HandleLangBarMenuSelect` 通知 RIME 引擎切换。它只是把 RIME 后端的当前状态同步回 compartment,方向与需求完全相反。 - -### 2. DLL 部署位置 - -WeaselTSF 的 CLSID `{A3F4CDED-B1E9-41EE-9CA6-7B4D0DE6CB0A}` 注册路径为: - -| 架构 | 注册路径 | -|------|---------| -| 64-bit | `C:\Windows\system32\weasel.dll` | -| 32-bit | `C:\Windows\SysWOW64\weasel.dll` | - -TSF 框架从上述系统路径加载 DLL。如果将修复后的 DLL 部署到其他路径(如 `C:\Program Files\Rime\weasel-0.17.4\`),系统不会加载它,进程内仍是旧 DLL。 - -更新方法:管理员身份打开 PowerShell,执行 - -Rename-Item "C:\Windows\system32\weasel.dll" "weasel.dll.bak3" -Force -Copy-Item "C:\Apps\git-kb\repos\VimWei\weasel\output\weaselx64.dll" "C:\Windows\system32\weasel.dll" -Force -Rename-Item "C:\Windows\SysWOW64\weasel.dll" "weasel.dll.bak3" -Force -Copy-Item "C:\Apps\git-kb\repos\VimWei\weasel\output\weasel.dll" "C:\Windows\SysWOW64\weasel.dll" -Force - -## 修复方案 - -### 源代码修改 - -**修改文件:** `WeaselTSF/Compartment.cpp`、`WeaselTSF/LanguageBar.cpp`、`WeaselTSF/WeaselTSF.h` - -将 CONVERSION handler 从"查询后端 → 写回 compartment"改为"toggle → 通知 RIME → 同步 UI",与 `GUID_COMPARTMENT_KEYBOARD_OPENCLOSE` 的 else 分支(Shift 按键切换路径)保持一致: - -```cpp -// 修复后 -} else if (IsEqualGUID(guidCompartment, - GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { - if (_updatingLanguageBar) - return S_OK; - _status.ascii_mode = !_status.ascii_mode; - _SetKeyboardOpen(true); - if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) - _EnableLanguageBar(true); - _HandleLangBarMenuSelect(_status.ascii_mode - ? ID_WEASELTRAY_ENABLE_ASCII - : ID_WEASELTRAY_DISABLE_ASCII); - if (_pEditSessionContext) - m_client.ClearComposition(); - _UpdateLanguageBar(_status); -} -``` - -与原始代码的关键差异: - -| 项目 | 原始代码 | 修复后 | -|------|---------|--------| -| **响应方式** | `m_client.GetResponseData()` 查询 RIME 后端旧状态,`_UpdateLanguageBar()` 写回 compartment(撤销外部变更) | `_status.ascii_mode = !_status.ascii_mode` 直接翻转,`_HandleLangBarMenuSelect()` 通知 RIME 引擎切换 | -| **键盘状态守卫** | `_IsKeyboardOpen()` —— 键盘关闭时完全不处理 | 移除守卫,改为 `_SetKeyboardOpen(true)` 主动打开键盘 | -| **重入保护** | 无 —— `_UpdateLanguageBar` 写 compartment 再次触发 handler | `_updatingLanguageBar` 守卫,阻断 `_UpdateLanguageBar` 引起的自触发 | - -`_HandleLangBarMenuSelect` → `TrayCommand` IPC → RIME 后端的 `SetOption("ascii_mode")`,与 Shift 按键切换走同一路径,确保行为和配置(如 `global_ascii_mode`)一致。 - -### 重入守卫 - -`_UpdateLanguageBar` 内部调用 `_SetCompartmentDWORD` 写入 CONVERSION compartment,这会再次触发 `OnChange` → `_HandleCompartment`。如果不加守卫,handler 会再次 toggle,导致双重翻转或无限递归。 - -```cpp -// LanguageBar.cpp -void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) { - // ... - _updatingLanguageBar = true; - _SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); - _updatingLanguageBar = false; - // ... -} -``` - -```cpp -// WeaselTSF.h -BOOL _updatingLanguageBar = false; -``` - -### 为什么用 blind toggle 而非读取 compartment 值 - -曾尝试读取 compartment 值来推导目标模式(`_GetCompartmentDWORD` → 检查 `TF_CONVERSIONMODE_NATIVE` 位 → 与 `_status.ascii_mode` 比对)。但 TSF 的 `OnChange` 通知时机可能导致 `GetValue` 返回旧值。blind toggle 避免了这一时序问题,且与 `OPENCLOSE` handler 的 else 分支(Shift 按键路径)完全一致。 - -外部工具(如 im-control)只在 `newMode != oldMode` 时才调用 `SetValue`,所以每次 `OnChange` 通知都代表一次真实的模式切换请求,blind toggle 语义正确。 - -## 验证 - -### 测试环境 - -- 前台进程:Windows Terminal (x64) -- 外部工具:[im-control](https://github.com/VimWei/im-control) —— 通过 `SetWindowsHookEx` 注入 hook DLL 到前台进程,在目标线程内调用 `ITfCompartment::SetValue` - -### 测试结果 - -``` -# 中文状态下切英文 -im-control -g → open native -im-control -c alphanumeric → (成功) -im-control -g → open alphanumeric - -# 英文状态下切中文 -im-control -g → open alphanumeric -im-control -c native → (成功) -im-control -g → open native -``` - -### 调试日志验证 - -在 `_HandleCompartment` 和 `_UpdateLanguageBar` 中加入 `OutputDebugStringW` 日志,确认完整链路: - -``` -[05:38:09.417] CONVERSION fired, compartment=0xF49BE320 (NATIVE=0), ascii_mode(before)=0, _updatingLanguageBar=0 -[05:38:09.417] CONVERSION toggled ascii_mode: 0 -> 1 -[05:38:09.417] CONVERSION called HandleLangBarMenuSelect(40013) # ID_WEASELTRAY_ENABLE_ASCII -[05:38:09.417] CONVERSION calling _UpdateLanguageBar -[05:38:09.417] CONVERSION done, ascii_mode(after)=1 -[05:38:09.433] CONVERSION fired, compartment=0xF49BE320 (NATIVE=0), ascii_mode(before)=1, _updatingLanguageBar=1 -[05:38:09.433] CONVERSION skipped (re-entrant from _UpdateLanguageBar) # 重入守卫生效 -``` - -链路完整:hook 写入 compartment → sink 触发 → toggle ascii_mode → 通知 RIME 引擎 → `_UpdateLanguageBar` 写回 compartment → 重入守卫跳过自触发。 - -## 相关链接 - -- [rime/weasel#1371](https://github.com/rime/weasel/issues/1371) -- [im-control](https://github.com/VimWei/im-control) —— 用于测试的 TSF compartment 控制工具 diff --git a/docs/windows11-compatibility-fix.md b/docs/windows11-compatibility-fix.md deleted file mode 100644 index c19e3d00ac..0000000000 --- a/docs/windows11-compatibility-fix.md +++ /dev/null @@ -1,198 +0,0 @@ -# Windows 11 兼容性修复:CONVERSION handler 从 blind toggle 改为值驱动 - -## 问题 - -[blind toggle 方案](conversion-compartment-fix-analysis.md)(commit `f14f2a7`)在 Windows 10 下正常,但在 Windows 11 下失效: - -- Vim 切换到 normal 模式时,RIME 总是变为中文(应为英文) -- AppIME 8 秒无键盘输入后,RIME 仍维持中文(应切回英文) - -## 根因 - -经 Windows 11 实测和日志分析,问题有**三个独立的层面**: - -### 层面 1:CoCreateInstance(CLSID_TF_ThreadMgr) 在 Win11 返回新实例 - -im-control 的 hook 用 `CoCreateInstance(CLSID_TF_ThreadMgr)` 获取 ThreadMgr。Windows 10 返回 per-thread 单例(与 WeaselTSF 注册 sink 的实例相同),Windows 11 返回**新实例**——compartment 写入到了不同实例,WeaselTSF 的 sink 收不到 OnChange。 - -**日志证据**:im-control `SetValue` 返回 `S_OK`,但 Weasel 日志中 `_HandleCompartment` 完全没有被调用。 - -### 层面 2:TF_CLIENTID_NULL 的 SetValue 在 Win11 不触发 OnChange - -im-control 的 hook 用 `SetValue(0, ...)`(`TF_CLIENTID_NULL`)。WeaselTSF 自身用 `SetValue(_tfClientId, ...)`(有效非零 ID)。 - -**Windows 10**:不区分 `TfClientId`,所有 `SetValue` 都触发 `OnChange`。 -**Windows 11**:仅为已激活客户端(非零 `TfClientId`)的写入触发 `OnChange`。 - -### 层面 3:CONVERSION handler 的 _SetKeyboardOpen(true) 引发级联反转 - -CONVERSION handler 调用 `_SetKeyboardOpen(true)` 写 OPENCLOSE=1,触发 OPENCLOSE handler 的 else 分支(`_isToOpenClose=false`,为 Shift 键设计的 blind toggle),将 CONVERSION 刚设置好的 `ascii_mode` 反转。 - -**日志证据**: -``` -CONVERSION: processing -> switching mode (ascii 0 -> 1) ← 正确 -OPENCLOSE OnChange: isOpen=1 -> toggle ascii_mode 1 -> 0 ← 级联反转 -CONVERSION done: ascii_mode=0 ← 结果中文(错) -``` - -移除 `_SetKeyboardOpen(true)` 后: -``` -CONVERSION: processing -> switching mode (ascii 0 -> 1) ← 正确 -CONVERSION done: ascii_mode=1 ← 结果英文(对) -``` - -## 修复方案 - -### 1. im-control:使用 TF_GetThreadMgr 获取 per-thread 单例 - -**修改文件:** `injector/hook.cpp` - -用 msctf.dll 导出的 `TF_GetThreadMgr` 替代 `CoCreateInstance(CLSID_TF_ThreadMgr)`,确保获取与 WeaselTSF 相同的 ThreadMgr 实例: - -```cpp -typedef HRESULT(WINAPI* PFN_TF_GetThreadMgr)(ITfThreadMgr**); -static ITfThreadMgr* GetThreadMgrSingleton() { - HMODULE hMsctf = GetModuleHandleW(L"msctf.dll"); - // ... GetProcAddress("TF_GetThreadMgr") ... - ITfThreadMgr* pThreadMgr = nullptr; - pfn(&pThreadMgr); - return pThreadMgr; -} -``` - -### 2. im-control:使用有效 TfClientId 调用 SetValue - -**修改文件:** `injector/hook.cpp` - -调用 `ITfThreadMgr::Activate` 获取有效 `TfClientId`,用于所有 `SetValue` 调用,完成后 `Deactivate`: - -```cpp -TfClientId clientId = TF_CLIENTID_NULL; -pThreadMgr->Activate(&clientId); -// ... SetValue(clientId, ...) 替代 SetValue(0, ...) ... -pThreadMgr->Deactivate(); -``` - -### 3. Weasel:CONVERSION handler 改为值驱动 - -**修改文件:** `WeaselTSF/Compartment.cpp` - -将 blind toggle 替换为读取 compartment 实际值并与 `_status.ascii_mode` 比对: - -```cpp -} else if (IsEqualGUID(guidCompartment, - GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { - if (_updatingLanguageBar) - return S_OK; - DWORD convMode = 0; - _GetCompartmentDWORD(convMode, - GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); - bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); - if (desiredAsciiMode != _status.ascii_mode) { - _status.ascii_mode = desiredAsciiMode; - // 注意:不调用 _SetKeyboardOpen(true),避免触发 OPENCLOSE 级联 - if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) - _EnableLanguageBar(true); - _HandleLangBarMenuSelect(_status.ascii_mode - ? ID_WEASELTRAY_ENABLE_ASCII - : ID_WEASELTRAY_DISABLE_ASCII); - if (_pEditSessionContext) - m_client.ClearComposition(); - _UpdateLanguageBar(_status); - } -} -``` - -**天然幂等性**:即使 `_updatingLanguageBar` 守卫失效(异步回调 / 系统注入),重新读取 compartment 值会得到与当前 `_status.ascii_mode` 一致的结果,`desiredAsciiMode == _status.ascii_mode` → 跳过。 - -### 4. Weasel:从 CONVERSION handler 移除 _SetKeyboardOpen(true) - -**修改文件:** `WeaselTSF/Compartment.cpp` - -CONVERSION handler 不再调用 `_SetKeyboardOpen(true)`。两个 compartment 完全解耦: -- **OPENCLOSE**:管理 IME 启用/禁用(由 OPENCLOSE handler 处理,为 Ctrl+Space 和 Shift 设计) -- **CONVERSION**:管理中/英文模式(由 CONVERSION handler 处理,为外部工具和系统切换设计) - -### 5. im-control:OPENCLOSE 跳过未变化的写入 - -**修改文件:** `injector/hook.cpp` - -写入前先 `GetValue` 比对,仅在值变化时 `SetValue`,避免不必要地触发 OPENCLOSE handler。 - -### 6. vim 插件:去掉 -k open,只写 CONVERSION - -**修改文件:** `autoload/im_select.vim` - -`im_control_set_mode()` 从 `[im-control, '-k', 'open', '-c', 'native']` 改为 `[im-control, '-c', 'native']`,两个 compartment 完全解耦。 - -## 设计原则 - -遵循 Weasel 开发者的指导: - -> GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 和 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE 是不一样的消息,作用要区分。输入功能是主功能,外部消息控制是辅助,不能因为要引入辅助功能导致主功能失效是基本要求。 - -我们的修改完全符合此原则: -- CONVERSION handler 不再触碰 OPENCLOSE compartment -- OPENCLOSE handler 未改动(Shift/Ctrl+Space 主功能不受影响) -- 外部工具只写 CONVERSION 切换中英文,不写 OPENCLOSE - -## 方案对比 - -| 项目 | blind toggle (`f14f2a7`) | 值驱动(本修复) | -|------|--------------------------|------------------| -| 回调次数依赖 | 严格依赖偶数次 `OnChange` | 幂等,不依赖回调次数 | -| 重入守卫 | 唯一防线,异步回调下失效 | 第一防线;值比对是第二防线 | -| compartment 耦合 | CONVERSION 写 OPENCLOSE(级联反转) | 完全解耦 | -| Windows 10 | 正常 | 正常 | -| Windows 11 | 失效 | 正常 | - -## 部署 - -### 重要:部署后需重启所有使用 RIME 的应用 - -`weasel.dll` 由 TSF 框架在进程启动时加载。部署新 DLL 后,已运行的进程仍使用内存中的旧 DLL。必须重启 Windows Terminal、Total Commander、gvim 等应用(或重启 Windows)才能加载新版本。 - -### 编译 - -在装有 Visual Studio 2022 + Boost 的机器上编译: - -``` -cd C:\Apps\git-kb\repos\VimWei\weasel -git pull -build.bat weasel release -``` - -产出 `output\weasel.dll`(Win32)和 `output\weaselx64.dll`(x64)。 - -### 部署 weasel.dll - -管理员 PowerShell: - -```powershell -Copy-Item output\weaselx64.dll C:\Windows\system32\weasel.dll -Force -Copy-Item output\weasel.dll C:\Windows\SysWOW64\weasel.dll -Force -``` - -### 编译部署 im-control - -``` -cd C:\Apps\git-kb\repos\VimWei\im-control -git pull -cmake -S . -B build -G "Visual Studio 17 2022" -cmake --build build --config RelWithDebInfo -cmake --install build --prefix bin --config RelWithDebInfo -``` - -```powershell -Copy-Item bin\* "C:\Apps\VimReader\lib\utils\im-control\" -Force -``` - -### vim 插件 - -vim-im-select 插件为纯脚本,无需编译,pull 后 reload vim 配置即可。 - -## 相关文档 - -- [原始修复:CONVERSION handler 响应外部变更](conversion-compartment-fix-analysis.md) — blind toggle 方案(Windows 10) -- [rime/weasel#1371](https://github.com/rime/weasel/issues/1371) -- [im-control](https://github.com/VimWei/im-control) From b75075d5cf99f650ac03b76709c95305a974699e Mon Sep 17 00:00:00 2001 From: VimWei Date: Thu, 9 Jul 2026 21:54:16 +0800 Subject: [PATCH 16/16] fix: gate _SetKeyboardOpen in CONVERSION handler by _isToOpenClose - 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 --- WeaselTSF/Compartment.cpp | 12 +- docs/compartment-external-control-fix.md | 143 +++++++++++++---------- 2 files changed, 93 insertions(+), 62 deletions(-) diff --git a/WeaselTSF/Compartment.cpp b/WeaselTSF/Compartment.cpp index bb6316135b..47f316b80c 100644 --- a/WeaselTSF/Compartment.cpp +++ b/WeaselTSF/Compartment.cpp @@ -266,14 +266,18 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { } } else if (IsEqualGUID(guidCompartment, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)) { - if (_updatingLanguageBar) + if (_updatingLanguageBar) { return S_OK; + } DWORD convMode = 0; _GetCompartmentDWORD(convMode, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION); bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); if (desiredAsciiMode != _status.ascii_mode) { _status.ascii_mode = desiredAsciiMode; + if (_isToOpenClose && !_IsKeyboardOpen()) { + _SetKeyboardOpen(true); + } if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) _EnableLanguageBar(true); _HandleLangBarMenuSelect(_status.ascii_mode @@ -282,6 +286,12 @@ HRESULT WeaselTSF::_HandleCompartment(REFGUID guidCompartment) { if (_pEditSessionContext) m_client.ClearComposition(); _UpdateLanguageBar(_status); + } else { + if (_isToOpenClose && !_IsKeyboardOpen()) { + _SetKeyboardOpen(true); + if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) + _EnableLanguageBar(true); + } } } return S_OK; diff --git a/docs/compartment-external-control-fix.md b/docs/compartment-external-control-fix.md index 35b7ff0938..fb3e68372e 100644 --- a/docs/compartment-external-control-fix.md +++ b/docs/compartment-external-control-fix.md @@ -5,7 +5,7 @@ 通过 TSF compartment 程序化切换中英文模式(如 `ITfCompartment::SetValue(GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION)`)对 Weasel(小狼毫)无效。外部工具写入 compartment 后,Weasel 不切换模式,且 compartment 值被恢复原状。 此问题在 Windows 10 和 Windows 11 上表现不同: -- **Windows 10**:compartment 写入能触发 `OnChange`,但 CONVERSION handler 逻辑缺陷导致外部变更被撤销 +- **Windows 10**:compartment 写入能触发 `OnChange`,但 CONVERSION handler 逻辑缺陷导致外部变更被撤销;gvim ESC 关闭键盘后无人重开,导致 RIME 被禁用 - **Windows 11**:compartment 写入根本不触发 `OnChange`,且 CONVERSION handler 存在级联反转问题 ## 架构背景 @@ -23,9 +23,18 @@ ImmSetConversionStatus(IME_CMODE_NATIVE) - **GUID_COMPARTMENT_KEYBOARD_OPENCLOSE**:管理 IME 启用/禁用(对应 Ctrl+Space、`-k open/close`) - **GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION**:管理中/英文模式(对应 Shift 切换、`-c native/alphanumeric`) +### Weasel 配置 `ToggleImeOnOpenClose` + +注册表 `HKCU\Software\Rime\weasel\ToggleImeOnOpenClose` 控制 OPENCLOSE handler 的行为分支,是 Win10/Win11 差异的根源: + +| 配置值 | `_isToOpenClose` | OPENCLOSE handler 行为 | 典型场景 | +|--------|-------------------|------------------------|----------| +| `yes` | `true` | **if 分支**:OPENCLOSE=0 真正关闭键盘,OPENCLOSE=1 真正打开;不 toggle ascii_mode | Win10 | +| `no` | `false` | **else 分支**:blind toggle ascii_mode;始终 `_SetKeyboardOpen(true)` 自动重开键盘 | Win11 | + ## 根因 -经 Windows 10 和 Windows 11 实测和日志分析,问题有**四个独立的层面**: +经 Windows 10 和 Windows 11 实测和日志分析,问题有**五个独立的层面**: ### 1. CONVERSION handler 撤销外部变更(Windows 10) @@ -59,8 +68,6 @@ ImmSetConversionStatus(IME_CMODE_NATIVE) im-control 的 hook 用 `CoCreateInstance(CLSID_TF_ThreadMgr)` 获取 ThreadMgr。Windows 10 返回 per-thread 单例(与 WeaselTSF 注册 sink 的实例相同),Windows 11 返回**新实例**——compartment 写入到了不同实例,WeaselTSF 的 sink 收不到 OnChange。 -**日志证据**:im-control `SetValue` 返回 `S_OK`,但 Weasel 日志中 `_HandleCompartment` 完全没有被调用。 - ### 3. TF_CLIENTID_NULL 的 SetValue 在 Win11 不触发 OnChange im-control 的 hook 用 `SetValue(0, ...)`(`TF_CLIENTID_NULL`)。WeaselTSF 自身用 `SetValue(_tfClientId, ...)`(有效非零 ID)。 @@ -68,22 +75,15 @@ im-control 的 hook 用 `SetValue(0, ...)`(`TF_CLIENTID_NULL`)。WeaselTSF **Windows 10**:不区分 `TfClientId`,所有 `SetValue` 都触发 `OnChange`。 **Windows 11**:仅为已激活客户端(非零 `TfClientId`)的写入触发 `OnChange`。 -### 4. CONVERSION handler 的 _SetKeyboardOpen(true) 引发级联反转 +### 4. CONVERSION handler 的 _SetKeyboardOpen(true) 引发级联反转(Windows 11) CONVERSION handler 调用 `_SetKeyboardOpen(true)` 写 OPENCLOSE=1,触发 OPENCLOSE handler 的 else 分支(`_isToOpenClose=false`,为 Shift 键设计的 blind toggle),将 CONVERSION 刚设置好的 `ascii_mode` 反转。 -**日志证据**: -``` -CONVERSION: processing -> switching mode (ascii 0 -> 1) ← 正确 -OPENCLOSE OnChange: isOpen=1 -> toggle ascii_mode 1 -> 0 ← 级联反转 -CONVERSION done: ascii_mode=0 ← 结果中文(错) -``` +### 5. gvim ESC 关闭键盘后无人重开(Windows 10) -移除 `_SetKeyboardOpen(true)` 后: -``` -CONVERSION: processing -> switching mode (ascii 0 -> 1) ← 正确 -CONVERSION done: ascii_mode=1 ← 结果英文(对) -``` +gvim 在退出 insert mode 时会写 OPENCLOSE=0 关闭键盘。在 `_isToOpenClose=true`(Win10)时,OPENCLOSE handler 的 if 分支只做 `_EnableLanguageBar(isOpen)` 和 `_UpdateLanguageBar`,**不重开键盘**。如果此时 im-control 的 CONVERSION 写入因值未变而跳过 SetValue(`newMode == oldMode`),则 CONVERSION OnChange 不触发,键盘一直关闭,RIME 被禁用。 + +**Windows 11** 不受此问题影响:`_isToOpenClose=false` 的 else 分支始终调用 `_SetKeyboardOpen(true)` 自动重开。 ## 修复方案 @@ -104,30 +104,32 @@ CONVERSION done: ascii_mode=1 ← 结果英文( bool desiredAsciiMode = !(convMode & TF_CONVERSIONMODE_NATIVE); if (desiredAsciiMode != _status.ascii_mode) { _status.ascii_mode = desiredAsciiMode; - // 注意:不调用 _SetKeyboardOpen(true),避免触发 OPENCLOSE 级联 - if (_pLangBarButton && _pLangBarButton->IsLangBarDisabled()) - _EnableLanguageBar(true); - _HandleLangBarMenuSelect(_status.ascii_mode - ? ID_WEASELTRAY_ENABLE_ASCII - : ID_WEASELTRAY_DISABLE_ASCII); - if (_pEditSessionContext) - m_client.ClearComposition(); + if (_isToOpenClose && !_IsKeyboardOpen()) { + _SetKeyboardOpen(true); + } + // ... 通知 RIME、同步 UI ... _UpdateLanguageBar(_status); + } else { + if (_isToOpenClose && !_IsKeyboardOpen()) { + _SetKeyboardOpen(true); + // ... 启用 LanguageBar ... + } } } ``` **天然幂等性**:即使 `_updatingLanguageBar` 守卫失效(异步回调 / 系统注入),重新读取 compartment 值会得到与当前 `_status.ascii_mode` 一致的结果,`desiredAsciiMode == _status.ascii_mode` → 跳过。不会产生额外翻转。 -`_HandleLangBarMenuSelect` → `TrayCommand` IPC → RIME 后端的 `SetOption("ascii_mode")`,与 Shift 按键切换走同一路径,确保行为和配置(如 `global_ascii_mode`)一致。 - -### 2. Weasel:从 CONVERSION handler 移除 _SetKeyboardOpen(true) +### 2. Weasel:CONVERSION handler 按 `_isToOpenClose` 条件调用 `_SetKeyboardOpen(true)` **修改文件:** `WeaselTSF/Compartment.cpp` -CONVERSION handler 不再调用 `_SetKeyboardOpen(true)`。两个 compartment 完全解耦: -- **OPENCLOSE**:管理 IME 启用/禁用(由 OPENCLOSE handler 处理,为 Ctrl+Space 和 Shift 设计) -- **CONVERSION**:管理中/英文模式(由 CONVERSION handler 处理,为外部工具和系统切换设计) +`_SetKeyboardOpen(true)` 的调用由 `_isToOpenClose` 控制,避免在错误场景下触发级联反转: + +| `_isToOpenClose` | CONVERSION handler 调用 `_SetKeyboardOpen(true)`? | 原因 | +|-------------------|---------------------------------------------------|------| +| `true` (Win10) | **是** — 键盘关闭时重开 | OPENCLOSE if 分支不自动重开,需要 CONVERSION handler 补救 | +| `false` (Win11) | **否** — 不触碰 OPENCLOSE | OPENCLOSE else 分支自己 `_SetKeyboardOpen(true)` + blind toggle,CONVERSION 写 OPENCLOSE 会触发级联反转 | ### 3. Weasel:重入守卫 @@ -155,31 +157,13 @@ BOOL _updatingLanguageBar = false; **修改文件:** `injector/hook.cpp` -用 msctf.dll 导出的 `TF_GetThreadMgr` 替代 `CoCreateInstance(CLSID_TF_ThreadMgr)`,确保获取与 WeaselTSF 相同的 ThreadMgr 实例: - -```cpp -typedef HRESULT(WINAPI* PFN_TF_GetThreadMgr)(ITfThreadMgr**); -static ITfThreadMgr* GetThreadMgrSingleton() { - HMODULE hMsctf = GetModuleHandleW(L"msctf.dll"); - // ... GetProcAddress("TF_GetThreadMgr") ... - ITfThreadMgr* pThreadMgr = nullptr; - pfn(&pThreadMgr); - return pThreadMgr; -} -``` +用 msctf.dll 导出的 `TF_GetThreadMgr` 替代 `CoCreateInstance(CLSID_TF_ThreadMgr)`,确保获取与 WeaselTSF 相同的 ThreadMgr 实例。 ### 5. im-control:使用有效 TfClientId 调用 SetValue **修改文件:** `injector/hook.cpp` -调用 `ITfThreadMgr::Activate` 获取有效 `TfClientId`,用于所有 `SetValue` 调用,完成后 `Deactivate`: - -```cpp -TfClientId clientId = TF_CLIENTID_NULL; -pThreadMgr->Activate(&clientId); -// ... SetValue(clientId, ...) 替代 SetValue(0, ...) ... -pThreadMgr->Deactivate(); -``` +调用 `ITfThreadMgr::Activate` 获取有效 `TfClientId`,用于所有 `SetValue` 调用,完成后 `Deactivate`。 ### 6. im-control:OPENCLOSE 跳过未变化的写入 @@ -187,7 +171,30 @@ pThreadMgr->Deactivate(); 写入前先 `GetValue` 比对,仅在值变化时 `SetValue`,避免不必要地触发 OPENCLOSE handler。 -### 7. vim 插件:去掉 -k open,只写 CONVERSION +### 7. im-control:读注册表条件重开 OPENCLOSE + +**修改文件:** `injector/hook.cpp` + +hook DLL 在 `DllMain` 时读取注册表 `HKCU\Software\Rime\weasel\ToggleImeOnOpenClose`,缓存到全局变量 `g_isToOpenClose`。CONVERSION 写入后,仅在 `g_isToOpenClose=true` 且 `keyboardOpenClose` 未设置且 OPENCLOSE 当前为 0 时,重开键盘: + +```cpp +// DllMain DLL_PROCESS_ATTACH: +g_isToOpenClose = ReadToggleImeOnOpenClose(); + +// hook 触发,CONVERSION 写入后: +if (g_isToOpenClose && g_pSharedData->conversionModeNative && !g_pSharedData->keyboardOpenClose) { + // 读 OPENCLOSE 当前值,仅当 == 0 时 SetValue(1) 重开 +} +``` + +| `ToggleImeOnOpenClose` | hook 行为 | 原因 | +|------------------------|-----------|------| +| `yes` (Win10) | 检查并重开 OPENCLOSE | OPENCLOSE if 分支不自动重开,gvim ESC 关键盘后需要 hook 补救 | +| `no` (Win11) | **不干预** OPENCLOSE | OPENCLOSE else 分支自动重开,hook 干预会导致级联反转 | + +**与 Weasel 侧修复的协同**:Weasel CONVERSION handler 也在 `_isToOpenClose=true` 时重开键盘。两者互为保底——任一方先触发即可重开,另一方检测到已打开则跳过(幂等)。 + +### 8. vim 插件:去掉 -k open,只写 CONVERSION **修改文件:** `autoload/im_select.vim`(vim-im-select) @@ -199,19 +206,30 @@ pThreadMgr->Deactivate(); > GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 和 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE 是不一样的消息,作用要区分。在 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE 的情况下 GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 的消息不应该再作任何响应,否则就是对旧功能的 break。需要明确的是,输入功能是主功能,外部消息控制是辅助,不能因为要引入辅助功能导致主功能失效是基本要求。 -我们的修改完全符合此原则: -- CONVERSION handler 不再触碰 OPENCLOSE compartment +修复完全符合此原则: - OPENCLOSE handler 未改动(Shift/Ctrl+Space 主功能不受影响) -- 外部工具只写 CONVERSION 切换中英文,不写 OPENCLOSE +- CONVERSION handler 对 OPENCLOSE 的干预由 `_isToOpenClose` 严格控制:仅在 OPENCLOSE handler 自身不重开键盘的配置下才介入 +- 外部工具只写 CONVERSION 切换中英文,不写 OPENCLOSE(除 `g_isToOpenClose=true` 时的保底重开) + +## Win10/Win11 差异总结 + +| 维度 | Windows 10 (`ToggleImeOnOpenClose=yes`) | Windows 11 (`ToggleImeOnOpenClose=no`) | +|------|------------------------------------------|------------------------------------------| +| `_isToOpenClose` | `true` | `false` | +| OPENCLOSE handler | if 分支:真正开/关键盘,不 toggle ascii | else 分支:blind toggle ascii + 自动重开键盘 | +| gvim ESC 关键盘 | 键盘保持关闭,需要外部重开 | else 分支自动 `_SetKeyboardOpen(true)` 重开 | +| CONVERSION handler `_SetKeyboardOpen` | **调用**(重开键盘) | **不调用**(避免级联反转) | +| im-control hook OPENCLOSE 重开 | **启用**(读注册表 `yes`) | **禁用**(读注册表 `no`) | +| 级联反转风险 | 无(if 分支不 toggle) | 有(else 分支 blind toggle) | ## 方案对比 -| 项目 | 原始代码 (93eec2d) | blind toggle (f14f2a7) | 值驱动(最终) | -|------|---------------------|------------------------|----------------| +| 项目 | 原始代码 (93eec2d) | blind toggle (f14f2a7) | 值驱动 + `_isToOpenClose` 分支(最终) | +|------|---------------------|------------------------|----------------------------------------| | 响应方式 | 查询后端→写回(撤销外部变更) | blind toggle | 读取值→比对→切换 | | 回调次数依赖 | N/A | 严格依赖偶数次 OnChange | 幂等,不依赖回调次数 | -| compartment 耦合 | 无 | CONVERSION 写 OPENCLOSE(级联反转) | 完全解耦 | -| 重入守卫 | 无 | 唯一防线,异步回调下失效 | 第一防线;值比对是第二防线 | +| `_SetKeyboardOpen(true)` | 无 | 无条件调用(Win11 级联反转) | 按 `_isToOpenClose` 条件调用 | +| OPENCLOSE 重开 | 无 | 无 | Weasel + im-control 双重保底 | | Windows 10 | 失效 | 正常 | 正常 | | Windows 11 | 失效 | 失效 | 正常 | @@ -277,16 +295,19 @@ vim-im-select 插件为纯脚本,无需编译,pull 后 reload vim 配置即 ### 测试环境 -- 操作系统:Windows 10 和 Windows 11 +- 操作系统:Windows 10 pro 19045.7417 和 Windows 11 home 26200.8655 - 前台进程:gvim 9.2.0735、Windows Terminal、Total Commander - 外部工具:im-control —— 通过 `SetWindowsHookEx` 注入 hook DLL 到前台进程,在目标线程内调用 `ITfCompartment::SetValue` ### 测试结果 Windows 10 和 Windows 11 下均通过: -- gvim:进入/离开 insert/command mode 切换英文,Shift 切中文后 ESC 回 normal mode 自动恢复英文 +- gvim 或 terminal vim: normal 英文 → i → edit, RIME 保持英文 +- gvim 或 terminal vim: edit 英文 → ESC → normal, RIME 保持英文,不被禁用 +- gvim 或 terminal vim: edit 中文 → ESC → normal, RIME 自动切回英文 +- gvim 或 terminal vim: 进入/离开 command mode 与 insert mode 行为一致 +- Shift 切换中英文、Ctrl+Space 启停 IME 均正常 - Windows Terminal / Total Commander:AppIME 进入窗口自动切英文,8 秒空闲后自动切回英文 -- 用户 Shift 键切换中英文、Ctrl+Space 启停 IME 均正常 ## 相关链接