Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
58ad294
test(qt): prepare IME literal-commit receiver red case (refs #155)
JavaZeroo Oct 28, 2026
761bb2c
docs(qt): fix IME event, literal commit and native validation boundar…
JavaZeroo Oct 28, 2026
0bd74c7
Merge commit '7e896cb75283fd0d984c936500ba8d1de4ceaf46' into feat/qt-…
JavaZeroo Oct 28, 2026
905d4b1
feat(qt): send IME commits as bounded literal UTF-8 (refs #155)
JavaZeroo Oct 28, 2026
245eca6
test(qt): require observable IME preedit rendering (refs #155)
JavaZeroo Oct 28, 2026
4c11276
feat(qt): render bounded preedit and expose current IME geometry (ref…
JavaZeroo Oct 28, 2026
b6b704a
test(qt): verify IME attributes, cancellation and literal boundaries …
JavaZeroo Oct 28, 2026
13c6abc
fix(qt): preserve explicit IME cursor colour on raster devices (refs …
JavaZeroo Oct 28, 2026
07c5b05
test(qt): keep IME composition isolated through focus and lifecycle (…
JavaZeroo Oct 28, 2026
5c5ec66
test(qt): follow real console output and font changes during preedit …
JavaZeroo Oct 28, 2026
933f365
fix(test): initialize owned IME control handles without assignment (r…
JavaZeroo Oct 28, 2026
2ac8f8f
test(qt): prepare native IME event observer without synthetic input (…
JavaZeroo Oct 28, 2026
d4387ab
test(qt): expose no-op IME attribute overflow and composing interrupt…
JavaZeroo Oct 28, 2026
9d9246a
fix(qt): bound no-op IME attribute work before accepting commits (ref…
JavaZeroo Oct 28, 2026
4667d12
fix(qt): preserve the supported interrupt during composition (refs #155)
JavaZeroo Oct 28, 2026
16878ab
test(qt): bind native readiness, coalescing and verified exit states …
JavaZeroo Oct 28, 2026
1a9612f
test(qt): expose native driver geometry and interrupt event lineage (…
JavaZeroo Oct 28, 2026
c64208f
test(qt): separate the declared native IME budget from offscreen test…
JavaZeroo Oct 28, 2026
04f946d
docs(qt): declare native Pinyin N1 origin, target, byte and cleanup c…
JavaZeroo Oct 28, 2026
0db417b
docs(qt): separate initial IME proposal from current focused and nati…
JavaZeroo Oct 28, 2026
9db13ce
test(qt): reproduce actual observer normal and cancellation lifecycle…
JavaZeroo Oct 28, 2026
cfed906
fix(qt): exit observer after session retirement and retain absolute d…
JavaZeroo Oct 28, 2026
3beba23
fix(qt): check observer deadline before fallible evidence operations …
JavaZeroo Oct 28, 2026
05bc52f
test(qt): observe actual GUI-thread profile and separate requested ca…
JavaZeroo Oct 28, 2026
627094b
fix(qt): serialize Win32 diagnostic integer types explicitly (refs #155)
JavaZeroo Oct 28, 2026
194101a
docs(qt): bind native profile scope and bounded observer retirement c…
JavaZeroo Oct 28, 2026
9c8fe93
Merge remote-tracking branch 'origin/main' into feat/qt-ime-r2
JavaZeroo Oct 28, 2026
9cac5b4
Merge remote-tracking branch 'origin/main' into feat/qt-ime-r2
JavaZeroo Oct 28, 2026
0deb19b
test(qt): expose terminal geometry in native observer window coordina…
JavaZeroo Oct 28, 2026
c6d1033
fix(qt): preserve first IME observer evidence write failure
JavaZeroo Oct 29, 2026
3abd315
Merge remote-tracking branch 'origin/main' into feat/qt-ime-r2
JavaZeroo Oct 29, 2026
47e6a9b
ci(qt): run IME evidence write diagnostics
JavaZeroo Oct 29, 2026
5bbf86d
Merge remote-tracking branch 'origin/main' into feat/qt-ime-r2
JavaZeroo Oct 29, 2026
27ec32a
fix(qt): publish immutable native IME observation snapshots
JavaZeroo Oct 29, 2026
59d0662
docs(qt): clarify same-volume immutable snapshot publication
JavaZeroo Oct 29, 2026
5fab30b
docs: clarify IME protocol status and index row-order evidence
JavaZeroo Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/qt-cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
"-DTERMFORGE_CONPTY_BACKEND_DIR=$dependencyRoot/conpty" `
"-DTERMFORGE_MSVC_RUNTIME_DIR=$dependencyRoot/crt"
- name: Build Qt product and independent smoke
run: cmake --build build/qt-cpp --config Release --target termforge_qt_shell qt_cpp_shell_smoke qt_cpp_resize_smoke qt_unicode_input_test --parallel 2
run: cmake --build build/qt-cpp --config Release --target termforge_qt_shell qt_cpp_shell_smoke qt_cpp_resize_smoke qt_unicode_input_test qt_ime_input_test qt_ime_evidence_write_test --parallel 2
- name: Record complete path-free runtime manifests
run: |
python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-bin/Release --executable termforge_qt_shell.exe
Expand All @@ -53,6 +53,8 @@ jobs:
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-unicode-bin/Release --executable qt_unicode_input_test.exe
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-ime-bin/Release --executable qt_ime_input_test.exe
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Preserve runtime file identities without binaries
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
Expand All @@ -65,8 +67,9 @@ jobs:
build/qt-cpp/qt-smoke-bin/Release/runtime-manifest.json
build/qt-cpp/qt-resize-bin/Release/runtime-manifest.json
build/qt-cpp/qt-unicode-bin/Release/runtime-manifest.json
build/qt-cpp/qt-ime-bin/Release/runtime-manifest.json
- name: Run Qt offscreen smoke in its owned Job
run: ctest --test-dir build/qt-cpp -C Release -R '^(qt_cpp_(shell|resize)|qt_unicode_input)$' --no-tests=error --verbose
run: ctest --test-dir build/qt-cpp -C Release -R '^(qt_cpp_(shell|resize)|qt_(unicode|ime)_input|qt_ime_observer_lifecycle_(normal|cancel)|qt_ime_evidence_write)$' --no-tests=error --verbose
# Keep this configuration independent from the default/system-backend and
# Alacritty reference jobs. SDKs, runtime folders and raw logs are not
# publication artifacts; this job does not produce a release package.
33 changes: 33 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,39 @@ if(TERMFORGE_BUILD_QT_SHELL)
"${PROJECT_SOURCE_DIR}/src/qt/run_smoke.py" "$<TARGET_FILE:qt_unicode_input_test>")
set_tests_properties(qt_unicode_input PROPERTIES TIMEOUT 35
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;TERMFORGE_TEST_ARTIFACTS=${PROJECT_BINARY_DIR}/test-artifacts")
add_executable(qt_ime_input_test src/qt/ime_input_test.cpp)
set_target_properties(qt_ime_input_test PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qt-ime-bin/$<CONFIG>")
termforge_qt_entry(qt_ime_input_test)
# Read-only native diagnostics use Windows SDK COM/TSF/IMM APIs only in
# this test executable; the product entry keeps its existing dependencies.
target_link_libraries(qt_ime_input_test PRIVATE Qt6::Test imm32 ole32 uuid)
add_dependencies(qt_ime_input_test shell_input_client)
add_custom_command(TARGET qt_ime_input_test POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"$<TARGET_FILE:Qt6::QOffscreenIntegrationPlugin>"
"$<TARGET_FILE_DIR:qt_ime_input_test>/platforms/"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"$<TARGET_FILE:shell_input_client>" "$<TARGET_FILE_DIR:qt_ime_input_test>/"
VERBATIM)
add_test(NAME qt_ime_input COMMAND "${Python3_EXECUTABLE}"
"${PROJECT_SOURCE_DIR}/src/qt/run_smoke.py" "$<TARGET_FILE:qt_ime_input_test>")
set_tests_properties(qt_ime_input PROPERTIES TIMEOUT 35
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;TERMFORGE_TEST_ARTIFACTS=${PROJECT_BINARY_DIR}/test-artifacts")
add_executable(qt_ime_evidence_write_test src/qt/ime_evidence_write_test.cpp)
set_target_properties(qt_ime_evidence_write_test PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qt-ime-evidence-bin/$<CONFIG>")
termforge_qt_entry(qt_ime_evidence_write_test)
target_link_libraries(qt_ime_evidence_write_test PRIVATE imm32 ole32 uuid)
add_test(NAME qt_ime_evidence_write COMMAND qt_ime_evidence_write_test)
set_tests_properties(qt_ime_evidence_write PROPERTIES TIMEOUT 60)
foreach(scenario normal cancel)
add_test(NAME qt_ime_observer_lifecycle_${scenario} COMMAND "${Python3_EXECUTABLE}"
"${PROJECT_SOURCE_DIR}/src/qt/run_ime_observer_lifecycle.py" "$<TARGET_FILE:qt_ime_input_test>" "${scenario}")
set_tests_properties(qt_ime_observer_lifecycle_${scenario} PROPERTIES TIMEOUT 35
ENVIRONMENT "QT_QPA_PLATFORM=offscreen;TERMFORGE_TEST_ARTIFACTS=${PROJECT_BINARY_DIR}/test-artifacts")
endforeach()

add_executable(qt_cpp_resize_smoke src/qt/resize_smoke.cpp)
set_target_properties(qt_cpp_resize_smoke PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qt-resize-bin/$<CONFIG>")
Expand Down
1 change: 1 addition & 0 deletions docs/EVIDENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

| 配置 | 证据定位 |
|---|---|
| 稳定行身份与文档顺序(隔离实验) | [精确定位/顺序与18项定位成本](../experiments/core/results/2026-10-29-line-order-r164/README.md)、[独立静态Search成本](../experiments/core/results/2026-10-29-static-search-r164/README.md):原caller反例、两Adapter正确性、共享宿主样本及原失败分列;生产核心未因实验改变,完整增量Search/结构通知/UI与RSS仍待验,不构成永久容器或WindTerm性能验收 |
| 自主 C++ 主屏 ASCII DECAWM | [有限合同与红绿](../experiments/core/results/2026-10-29-cpp-decawm/README.md)、[固定验证身份](../experiments/core/results/2026-10-29-cpp-decawm/validation.json):抽取原版14–18阶段79B,strict/recovery各235个字面前缀分块用例;初次定向8/9的缺构建依赖失败、补建后的core单项31测试通过分列。跨1049/宽字/resize、origin/margin、GUI/固定ConPTY运输和完整TUI均不由本项验收 |
| 自主 C++ 限定 1049 协议 | [固定结果](../experiments/core/results/2026-10-29-cpp-1049/README.md)、[验证身份](../experiments/core/results/2026-10-29-cpp-1049/validation.json):文字/光标/颜色及 pending-wrap、重复进入与读者失效;55项中Qt1项分列,13条内部skip及旧负例判据失败保留。未验收非颜色属性/全网格背景、其他模式、真实TUI/IME/DPI、呈现或性能 |
| 自主 C++ 双缓冲存储与文档 epoch 基础 | [固定结果](../experiments/core/results/2026-10-29-buffer-state-r2/README.md)、[验证身份](../experiments/core/results/2026-10-29-buffer-state-r2/validation.json):拥有状态、共享 ID、隐藏帧 resize 回滚与跨缓冲句柄失效;Qt/candidate 关闭配置的完整 50 项及 12 条内部 skip 分列。未接 private CSI,不是原版模式、Qt 产品路线或完整 #27 验收 |
Expand Down
50 changes: 50 additions & 0 deletions docs/research/qt-ime-integration-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Qt IME 基础接线计划(#155)

状态:**初始计划历史记录**,描述源761bb2c执行前的test-only准备;下文“当前”仅指当时基线,不是最新实现状态。实施/实测由 [#155](https://github.com/JavaZeroo/termforge/issues/155)维护,后续原生计划见 [N1](qt-ime-native-observation-plan.md)。依据 [ADR-0008](../adr/0008-cpp-core-qt-widgets.md) 和 [R2 合同](../milestones/R2-local-terminal.md)。初始基点 main a0aed8a643a0a99b09e16c7836e5a4e3ad26daed 已包含 #147 的 Qt 键盘文本;旧原件保持不变。本页不宣告最新能力或验收通过。

## 已知事实与首个反例

当前 TerminalWidget 明确关闭 WA_InputMethodEnabled,没有重写 inputMethodEvent / inputMethodQuery。Qt 6.10 的 [QWidget 合同](https://doc.qt.io/qt-6.10/qwidget.html#inputMethodEvent)说明默认事件处理拒绝输入法事件;这只是源码和接口依据,**尚不是本切片的运行观察**。

test-only 源 58ad294aea7d01388e83720b70e467b4b55d82f5 注册 qt_ime_input,使用现有固定 ConPTY、run_smoke.py OwnedJob/GO 和独立 shell_input_client:

1. 等接收端独立输出 TF_KEY_READY,以公开 Qt 按键发送 L。
2. 发送带 TextFormat / Cursor 属性的公开 preedit zhongwen,保存前后实际自有 QImage、哈希、差异像素数和 query 值;这些诊断尚不作为首个失败前置。
3. 发送一次 IME commit:中文、NFD、U+1F600、字面 U+0008 和 U+0000,再用公开按键发送 R。
4. 接收端通过自己的 ReadConsoleW / Windows UTF-8 转换导出文件。完整字面预期为 **4ce4b8ade6968765cc81f09f9880080052(17B)**;preedit 不属于该字节流。只由 Session 发送该夹具的 EOT,待接收端自然结束及回收后,先保存观察再断言全文。
5. 当前实现会丢失 commit 是**待验证假设**;不提前把 WA_InputMethodEnabled 或图像检查失败写成“已取得独立接收红例”。

未延长原合同:阶段 gate5秒,既有监督器 communicate25秒、超时后5秒等待,CTest35秒;后续 drain 和 finally 清理各有原实现边界。它们不组成已证明的整程硬时限,更不是性能门槛。原件写入独立自有目录;图像仅属本项目控件离屏绘制,不是原版资源或物理呈现。

## 最小实现形状

只修改 Qt widget 的输入法与绘制接线,必要时复用 Model 的错误呈现通道增加单一 reject_input(reason) 领域入口。Parser/Screen 不保存 preedit,也不把终端屏幕当成可编辑输入行;ConPTY 和有界 InputQueue 的职责不变,不引入新组件、字体或测试钩子。

- **字面提交。** preedit 和 commit 各沿旧 ImeContext 的256个UTF-16单位工程上限,先严格校验完整 UTF-16,再按字面 UTF-8 编码。复用旧 ime.hpp 的 WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS) 语义,不调用会将 Backspace 转成 DEL 的 KeyboardEncoder.character。整个结果只调用一次有界 send,拒绝不发送前缀。该上限和非法输入策略属于当前 TermForge 工程政策,尚无 WindTerm 对照。
- **独立状态。** IME 不把自己的代理项交给 #147 的键盘编码器;无效或被拒绝的 IME 事件不改变已经持有的键盘半个标量。焦点/隐藏/关闭/不可用目标继续清理两种状态。旧 Win32 接线也是将 ImeMessage.committed 独立发送,不能因共享入口而套键盘控制码政策。
- **组合显示。** widget 只持有有界 preedit、格式范围和组合光标。按 [QInputMethodEvent 合同](https://doc.qt.io/qt-6.10/qinputmethodevent.html)至少实现 TextFormat 的前景/背景/下划线及 Cursor 位置/可见性/颜色,用 Qt 自有文字布局绘制,裁剪于终端可用区域。错误 UTF-16、越界/冲突属性明确拒绝并取消,不静默截断。
- **周边编辑请求。** 当前没有应用输入行/重转换合同,replacementStart或replacementLength非零明确拒绝,不向 child 注入猜测的退格/光标序列。周边文本和选择 query 不暴露屏幕正文作可编辑文本;计划返回空内容/索引或不支持值,并在验证中保留实际返回。
- **候选锚点。** ImEnabled 按实际目标可用性、焦点和可见性回答。ImCursorRectangle / ImAnchorRectangle、ImFont 使用当前 widget 逻辑坐标、模型光标和实际文字布局;output、resize、字体/DPR及焦点变化调用 [QInputMethod.update](https://doc.qt.io/qt-6.10/qinputmethod.html#update) 通知。Qt 逻辑矩形与 native 物理矩形分列,不将原 Win32 的12×26网格认定为 Qt 已等价。
- **取消与重入。** empty preedit 更新可取消组合但不发送文本。焦点转移先清理本地状态并依赖 Qt 自动重置输入上下文;若因本目标错误主动 reset,只在它仍是当前焦点时调用并先清理本地状态,不取消新目标或操作共享 OS 输入服务。
- **一次性发送。** 每次 commit 事件只在本 handler 发送一次,不再交给默认编辑处理或同时从 key text 重发。相同内容的另一次输入事件仍是新输入,不能用“内容去重”吞掉用户有意重复;真实 Windows 提交/字符事件是否重复由 native 证据核对。组合期间普通按键文本不能绕过 IME 进入 child。

Qt 接口要求是实现依据,未知 WindTerm 行为仍未知;没有因为文档或测试通过而批准重要行为差异。

## 后续红绿与停止条件

按 tracer bullet 接续,不先写完整假想测试矩阵:

| 顺序 | 公开动作和独立预期 |
|---|---|
| 首片 | 先编译/冻结/执行上面的17B接收红例一次,再实现字面提交使其通过;源、EXE、SDK/ConPTY/CRT、stdin GO、stdout/stderr、receiver和Job独立保留 |
| 组合 | 实际 QImage 的 preedit/格式/光标与取消后恢复,原始接收端证明 preedit 未发送;普通文本属性和 Cursor 显隐用可判读颜色/区域反例 |
| 拒绝 | 256/257单位、非法 UTF-16、replacement、无目标和实际发送失败;不通过改变等待或加生产队列钩子取绿。现容量 guard 的独立测试与 Qt 真实拒绝现场分列 |
| 上下文 | focus/hide/两标签/关闭/自然结束/失败目标;前后接收文件、查询、组合图像和回收均可核对,不根据空文件声称自然退出 |
| 几何 | output、resize、字体变化后的公开 query 和自有渲染;记录逻辑矩形/网格关系,不冒充跨DPI或实际上屏 |
| 完整配置 | 合入最新 main,复验原Qt键盘/cmd/resize,冻结源码/依赖/运行目录,运行一次必要完整矩阵并分开Qt和其他诊断、实际skip与旧失败 |
| 原生 | 源码和包/计划审查后,仅root在隔离Win11固定Microsoft拼音执行真实输入→Qt组合/结果→child字节,记录候选/焦点/清理;未满足则#155不能按完整原生条件结项 |

本次准备阶段只提交源码和计划,**编译/Qt执行/full/native均为0**。先等 root 的互斥负载租约;不改个人输入布局/桌面/剪贴板,不直接接管 Qt HWND 的 IMM,不终止 ctfmon 等共享服务,不执行未审核的 guest 写入。后续实际失败保留,不自动重试取绿或扩大原期限。

完整 Find/rename、同次八会话、CF、真实跨DPI、所有字素/glyph/fallback、物理呈现及性能仍由 #30/#31/R2 接续;只观察到合成 Qt 事件不能关闭真实固定拼音条件。

Loading