fix: respond to TSF conversion mode compartment changes#1889
Conversation
|
fix: replace read-compare with blind toggle for conversion compartment
|
|
Update:这个修复分析已经过时,请跳过,后面的跟帖有更新 修复分析:WeaselTSF 响应 TSF 转换模式 Compartment 外部变更问题通过 TSF compartment 程序化切换中英文模式(如 行为变化
架构背景WeaselTSF 是 TSF Text Service(实现 根因根因有两个层面:源代码逻辑缺陷 和 DLL 部署位置。两者必须同时解决,缺一不可。 1. 源代码:CONVERSION handler 撤销外部变更
// 原始代码 (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
}
}问题链条:
原始代码从不读取 compartment 值来判断外部请求的目标模式,也不调用 2. DLL 部署位置WeaselTSF 的 CLSID
TSF 框架从上述系统路径加载 DLL。如果将修复后的 DLL 部署到其他路径(如 修复方案源代码修改修改文件: 将 CONVERSION handler 从"查询后端 → 写回 compartment"改为"toggle → 通知 RIME → 同步 UI",与 // 修复后
} 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);
}与原始代码的关键差异:
重入守卫
// LanguageBar.cpp
void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) {
// ...
_updatingLanguageBar = true;
_SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION);
_updatingLanguageBar = false;
// ...
}// WeaselTSF.h
BOOL _updatingLanguageBar = false;为什么用 blind toggle 而非读取 compartment 值曾尝试读取 compartment 值来推导目标模式( 外部工具(如 im-control)只在 验证测试环境
测试结果调试日志验证在 链路完整:hook 写入 compartment → sink 触发 → toggle ascii_mode → 通知 RIME 引擎 → 相关链接
|
|
不用管_IsKeyboardOpen? |
|
按照我之前测试结果,实际上微软拼音/微软日文输入法也不响应 |
|
因为目前有两种可能的状态,一个是开关键盘的状态,一个是切换ascii的状态。 你的修改上没有考虑输入法是不是开启,感觉有洞 |
|
期待~ |
感谢回复。我想确认一下你说的”输入法是不是开启“具体指哪种情况:
|
|
原来旧的weasel的响应 打开/关闭输入法 消息是执行的打开keyboard和关闭keyboard,对应影响的是_IsKeyboardOpen 的状态;后来改了默认切换ascii_mode,可选toggleime |
感谢澄清历史背景,现在理解了 OPENCLOSE 语义的演变。 基于你的说明,我梳理了当前 PR 的设计,确认在 _isToOpenClose=false(默认 Shift 切换模式)下是正确的: 当前PR的设计:CONVERSION handler 中 _SetKeyboardOpen(true) 的调用,在 im-control 的典型工作流里是 no-op——因为外部工具总是先写 OPENCLOSE=1 启用 RIME,再写 CONVERSION 切换中英。到 CONVERSION handler 执行时 _IsKeyboardOpen() 已为 true,SetValue(TRUE) 不改变值、不触发 OnChange,OPENCLOSE handler 不会被调用。经实测,gvim 和 terminal vim 下进入/离开 insert mode 均工作正常:进入时切英文、离开时恢复英文、全程不会误禁用 RIME。 一个可选的优化方向:从 CONVERSION handler 中移除 _SetKeyboardOpen(true),让两个 compartment 各司其职——OPENCLOSE 管启用/禁用,CONVERSION 管中英切换。这样在 _isToOpenClose=false 模式下不会依赖已被重新定义语义的 OPENCLOSE compartment。但这需要外部调用方(如 im-control)保证先 -k open 再 -c,否则单独写 CONVERSION 不会启用 RIME。 考虑到当前实现已经正确工作且对调用方更宽容(即使单独写 CONVERSION 也会自动启用 RIME),我倾向保持现状。您认为呢? PS:上述提到的正确工作,其环境包括: |
|
发现这个PR在 windows 10 下没有问题,但 windows 11 下有问题,修复中... |
|
GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 和 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE 是不一样的消息,作用要区分。在GUID_COMPARTMENT_KEYBOARD_OPENCLOSE的情况下GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION的消息不应该再作任何响应,否则就是对旧功能的break,毕竟已知是有人要这种open/close状态切换而不是ascii切换(这也是传统小狼毫的基本状态)。需要明确的是,输入功能是主功能,外部消息控制是辅助,不能因为要引入辅助功能导致主功能失效是基本要求。 另外调试可以不用那么复杂,debugview++来实时看就行,调用weaselutility.h中 |
感谢指导!我做了以下调整:
经过 windows 10 和 11 测试,目前均可以正常工作了。 |
|
更新总结之前的所有修订: WeaselTSF Compartment 外部控制修复分析问题通过 TSF compartment 程序化切换中英文模式(如 此问题在 Windows 10 和 Windows 11 上表现不同:
架构背景WeaselTSF 是 TSF Text Service(实现 两个 compartment 各司其职:
根因经 Windows 10 和 Windows 11 实测和日志分析,问题有四个独立的层面: 1. CONVERSION handler 撤销外部变更(Windows 10)
// 原始代码 (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
}
}问题链条:
原始代码从不读取 compartment 值来判断外部请求的目标模式,也不调用 2. CoCreateInstance(CLSID_TF_ThreadMgr) 在 Win11 返回新实例im-control 的 hook 用 日志证据:im-control 3. TF_CLIENTID_NULL 的 SetValue 在 Win11 不触发 OnChangeim-control 的 hook 用 Windows 10:不区分 4. CONVERSION handler 的 _SetKeyboardOpen(true) 引发级联反转CONVERSION handler 调用 日志证据: 移除 修复方案1. Weasel:CONVERSION handler 改为值驱动修改文件: 将原始的"查询后端 → 写回 compartment"改为"读取 compartment 值 → 比对状态 → 通知 RIME → 同步 UI": } 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);
}
}天然幂等性:即使
2. Weasel:从 CONVERSION handler 移除 _SetKeyboardOpen(true)修改文件: CONVERSION handler 不再调用
3. Weasel:重入守卫修改文件:
// LanguageBar.cpp
void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) {
// ...
_updatingLanguageBar = true;
_SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION);
_updatingLanguageBar = false;
// ...
}// WeaselTSF.h
BOOL _updatingLanguageBar = false;4. im-control:使用 TF_GetThreadMgr 获取 per-thread 单例修改文件: 用 msctf.dll 导出的 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修改文件: 调用 TfClientId clientId = TF_CLIENTID_NULL;
pThreadMgr->Activate(&clientId);
// ... SetValue(clientId, ...) 替代 SetValue(0, ...) ...
pThreadMgr->Deactivate();6. im-control:OPENCLOSE 跳过未变化的写入修改文件: 写入前先 7. vim 插件:去掉 -k open,只写 CONVERSION修改文件:
设计原则遵循 fxliang 的指导:
我们的修改完全符合此原则:
方案对比
DLL 部署位置WeaselTSF 的 CLSID
TSF 框架从上述系统路径加载 DLL。如果将修复后的 DLL 部署到其他路径(如 更新方法管理员身份打开 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在装有 Visual Studio 2022 + Boost 的机器上编译: 产出 im-controlCopy-Item bin\* "C:\Apps\VimReader\lib\utils\im-control\" -Forcevim 插件vim-im-select 插件为纯脚本,无需编译,pull 后 reload vim 配置即可。 验证测试环境
测试结果Windows 10 和 Windows 11 下均通过:
相关链接 |
|
New: 增强 windows10 和 windows11 的兼容性,并更新文档,rebase 到 master 最新提交: WeaselTSF Compartment 外部控制修复分析问题通过 TSF compartment 程序化切换中英文模式(如 此问题在 Windows 10 和 Windows 11 上表现不同:
架构背景WeaselTSF 是 TSF Text Service(实现 两个 compartment 各司其职:
Weasel 配置
|
| 配置值 | _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 实测和日志分析,问题有五个独立的层面:
1. CONVERSION handler 撤销外部变更(Windows 10)
_HandleCompartment 中 GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 的原始实现:
// 原始代码 (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
}
}问题链条:
- 外部工具(如 im-control)写入 compartment,清除
TF_CONVERSIONMODE_NATIVE位(请求切英文) - TSF 触发
ITfCompartmentEventSink::OnChange→_HandleCompartment - handler 调用
m_client.GetResponseData(parser)—— 从 RIME 后端获取当前状态(仍是中文,因为 RIME 还没收到切换指令) - handler 调用
_UpdateLanguageBar(_status)—— 把当前状态(中文)写回 compartment - 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。
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) 引发级联反转(Windows 11)
CONVERSION handler 调用 _SetKeyboardOpen(true) 写 OPENCLOSE=1,触发 OPENCLOSE handler 的 else 分支(_isToOpenClose=false,为 Shift 键设计的 blind toggle),将 CONVERSION 刚设置好的 ascii_mode 反转。
5. gvim ESC 关闭键盘后无人重开(Windows 10)
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) 自动重开。
修复方案
1. Weasel:CONVERSION handler 改为值驱动
修改文件: WeaselTSF/Compartment.cpp
将原始的"查询后端 → 写回 compartment"改为"读取 compartment 值 → 比对状态 → 通知 RIME → 同步 UI":
} 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;
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 → 跳过。不会产生额外翻转。
2. Weasel:CONVERSION handler 按 _isToOpenClose 条件调用 _SetKeyboardOpen(true)
修改文件: WeaselTSF/Compartment.cpp
_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:重入守卫
修改文件: WeaselTSF/LanguageBar.cpp、WeaselTSF/WeaselTSF.h
_UpdateLanguageBar 内部调用 _SetCompartmentDWORD 写入 CONVERSION compartment,这会再次触发 OnChange → _HandleCompartment。如果不加守卫,handler 会再次处理,导致双重翻转或无限递归。
// LanguageBar.cpp
void WeaselTSF::_UpdateLanguageBar(weasel::Status stat) {
// ...
_updatingLanguageBar = true;
_SetCompartmentDWORD(flags, GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION);
_updatingLanguageBar = false;
// ...
}// 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 实例。
5. im-control:使用有效 TfClientId 调用 SetValue
修改文件: injector/hook.cpp
调用 ITfThreadMgr::Activate 获取有效 TfClientId,用于所有 SetValue 调用,完成后 Deactivate。
6. im-control:OPENCLOSE 跳过未变化的写入
修改文件: injector/hook.cpp
写入前先 GetValue 比对,仅在值变化时 SetValue,避免不必要地触发 OPENCLOSE handler。
7. im-control:读注册表条件重开 OPENCLOSE
修改文件: injector/hook.cpp
hook DLL 在 DllMain 时读取注册表 HKCU\Software\Rime\weasel\ToggleImeOnOpenClose,缓存到全局变量 g_isToOpenClose。CONVERSION 写入后,仅在 g_isToOpenClose=true 且 keyboardOpenClose 未设置且 OPENCLOSE 当前为 0 时,重开键盘:
// 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)
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。需要明确的是,输入功能是主功能,外部消息控制是辅助,不能因为要引入辅助功能导致主功能失效是基本要求。
修复完全符合此原则:
- OPENCLOSE handler 未改动(Shift/Ctrl+Space 主功能不受影响)
- 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) | 值驱动 + _isToOpenClose 分支(最终) |
|---|---|---|---|
| 响应方式 | 查询后端→写回(撤销外部变更) | blind toggle | 读取值→比对→切换 |
| 回调次数依赖 | N/A | 严格依赖偶数次 OnChange | 幂等,不依赖回调次数 |
_SetKeyboardOpen(true) |
无 | 无条件调用(Win11 级联反转) | 按 _isToOpenClose 条件调用 |
| OPENCLOSE 重开 | 无 | 无 | Weasel + im-control 双重保底 |
| 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,执行:
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
Copy-Item bin\* "C:\Apps\VimReader\lib\utils\im-control\" -Forcevim 插件
vim-im-select 插件为纯脚本,无需编译,pull 后 reload vim 配置即可。
验证
测试环境
- 操作系统: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 或 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 秒空闲后自动切回英文
相关链接
- Read TF_CONVERSIONMODE_NATIVE flag from the compartment on GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION change - Compare with current _status.ascii_mode and toggle via _HandleLangBarMenuSelect when they differ - Use the same IPC path (TrayCommand → SetOption) as the Shift key for consistent behavior - Clear pending composition on mode switch to avoid stale preedit Closes rime#1371
- Replace reading compartment value and comparing with _status.ascii_mode by a blind toggle, matching the OPENCLOSE handler's Shift-key path. The OnChange notification may deliver stale values, making the read-compare approach unreliable. - Remove _IsKeyboardOpen() guard; call _SetKeyboardOpen(true) to ensure the keyboard is active when processing external changes. - Add _updatingLanguageBar re-entrancy guard in _UpdateLanguageBar to prevent _SetCompartmentDWORD from re-triggering the handler. - Add _SetKeyboardOpen/_EnableLanguageBar calls to match the OPENCLOSE handler's else branch.
- Realign _HandleLangBarMenuSelect ternary to one-line condition with colon continuation aligned under the question mark, per Chromium clang-format style; resolves CI lint violations at lines 275-276
…ndows 11 compatibility blind toggle (_status.ascii_mode = !_status.ascii_mode) relies on the _updatingLanguageBar boolean guard catching synchronous OnChange re-entrancy from _UpdateLanguageBar. On Windows 11, TSF may deliver OnChange asynchronously or inject extra compartment writes via TextInputHost.exe, causing the guard to miss and producing an odd number of toggles (wrong mode). Replace with reading the actual compartment value and comparing against _status.ascii_mode. This is naturally idempotent: re-entrant or delayed OnChange reads the value written by _UpdateLanguageBar, finds it matches current state, and skips. The _updatingLanguageBar guard remains as a first layer. Also update analysis doc with Windows 11 root cause and fix rationale.
Restore conversion-compartment-fix-analysis.md to its original state (documenting the blind-toggle approach for Windows 10). Create new windows11-compatibility-fix.md for the value-driven fix that addresses Windows 11 TSF async callback behavior.
Update windows11-compatibility-fix.md with the finding that Windows 11 TSF only triggers OnChange for writes from activated clients. im-control was using TF_CLIENTID_NULL, so OnChange was never fired.
…teLanguageBar Log compartment value, desiredAsciiMode, _status.ascii_mode, _updatingLanguageBar state, and decision (skip/process) in _HandleCompartment. Log flags and _updatingLanguageBar state in _UpdateLanguageBar. Use DebugView to diagnose Windows 11 behavior.
…ingW Add _DbgInit/_DbgLog helpers that append to C:\Users\Public\weasel-compartment-debug.log (always-on for diagnosis). Thread-safe with CRITICAL_SECTION, with timestamps. Log OPENCLOSE/CONVERSION OnChange entry, decision branches, and _UpdateLanguageBar guard transitions.
The CONVERSION handler called _SetKeyboardOpen(true) which writes OPENCLOSE=1 unconditionally. On Windows 11 this triggers the OPENCLOSE OnChange sink (else branch for _isToOpenClose=false), which blind- toggles ascii_mode, reversing the mode that CONVERSION just set. Debug logs confirmed the cascade: 1. CONVERSION sets ascii 0->1 (correct) 2. _SetKeyboardOpen(true) writes OPENCLOSE 3. OPENCLOSE handler toggles ascii 1->0 (wrong) 4. Final: ascii=0 (Chinese instead of English) Per Weasel maintainer guidance: OPENCLOSE and CONVERSION are separate compartments with separate responsibilities. OPENCLOSE manages enable/disable, CONVERSION manages Chinese/English. The CONVERSION handler should not touch OPENCLOSE. Callers (e.g. im-control/vim plugin) always send both -k open and -c together, so OPENCLOSE is handled by its own handler. When only CONVERSION is written (keyboard already open), OPENCLOSE handler doesn't fire and there is no cascade.
Record the ThreadMgr pointer and thread ID when WeaselTSF is activated, to compare with im-control's TF_GetThreadMgr pointer and determine if they share the same ThreadMgr instance in Windows Terminal.
The C:\Users\Public path may not be writable from all processes (e.g. UWP-packaged apps like Windows Terminal). Use GetTempPathA to write to the per-user temp directory. Also log PID in session header to distinguish multiple processes.
Remove _DbgInit/_DbgLog infrastructure, all debug log calls in Compartment.cpp, LanguageBar.cpp, and WeaselTSF.cpp. The production code is clean and contains only the functional fixes: - CONVERSION handler is value-driven (reads compartment, compares with state, only acts if different) - CONVERSION handler no longer calls _SetKeyboardOpen(true)
Add three-layer root cause analysis (TF_GetThreadMgr singleton, TfClientId, OPENCLOSE cascade), document the compartment decoupling principle, and add deployment note about restarting applications after DLL replacement.
Merge conversion-compartment-fix-analysis.md (blind toggle, Win10) and windows11-compatibility-fix.md (value-driven, Win11) into one coherent document: compartment-external-control-fix.md. The new document covers all four root causes, the complete fix across Weasel/im-control/vim-plugin, design principles, DLL deployment instructions (preserved from original), and verification results for both Windows 10 and 11.
- Call _SetKeyboardOpen(true) in CONVERSION handler only when _isToOpenClose is true (Win10, ToggleImeOnOpenClose=yes), since the OPENCLOSE if-branch does not auto-reopen the keyboard - Skip _SetKeyboardOpen when _isToOpenClose is false (Win11) to avoid triggering the OPENCLOSE else-branch blind toggle that cascades and reverts the ascii_mode just set by CONVERSION - Add value-matches branch that also reopens keyboard on Win10 when CONVERSION value already matches status but keyboard is closed - Remove all debug logging (_DbgLog function, s_dbg* state, calls) - Update compartment-external-control-fix.md with the fifth root cause, the registry-based conditional reopen, and Win10/Win11 difference table
Closes #1371