diff --git a/AGENTS.md b/AGENTS.md index 0bfd51dd..49645a07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,13 +2,13 @@ - Read README.md, docs/PLAN.md, docs/FEATURES.md and the active milestone before making architecture decisions. - Follow docs/adr/0006-confirmed-project-direction.md: fully recreate WindTerm 2.7.0, with Windows 11 x64 as the first complete-version target and Windows/Linux/macOS as the final platform goal. Prioritize terminal foundations and performance before SSH/SFTP and the remaining features. -- Preserve Alacritty as a candidate, not an approved product-core selection. Keep investigating the original code and mechanisms; internal implementation need not be identical. -- Follow docs/adr/0007-bundled-microsoft-conpty.md: the confirmed Windows local-shell product route bundles a fixed Microsoft ConPTY. Consolidate existing integration into this route; system ConPTY and WinPTY are diagnostic references rather than parallel product routes. Core and GUI choices remain unconfirmed, and route confirmation is not implementation acceptance. +- Follow docs/adr/0008-cpp-core-qt-widgets.md: the confirmed product core is independently developed C++, and Qt Widgets is the current GUI route. Keep investigating the original code and mechanisms; internal implementation need not be identical. Preserve Alacritty code and evidence for reference/differential work rather than expanding it as the product core. +- Follow docs/adr/0007-bundled-microsoft-conpty.md: the confirmed Windows local-shell product route bundles a fixed Microsoft ConPTY. Consolidate existing integration into this route; system ConPTY and WinPTY are diagnostic references rather than parallel product routes. Route confirmation is not implementation acceptance. - Default to observed WindTerm 2.7.0 behavior when it conflicts with standards, third-party behavior or existing project policy. Unknown behavior requires investigation; important deviations and original-bug fixes require user confirmation. - The user confirms scope changes, major architecture/dependency choices, acceptance criteria and important behavior differences. Agents may autonomously research, experiment, implement, test and fix within confirmed scope. Record proposal/confirmed status and confirmation evidence in ADRs and issues; merged code or closed issues do not imply user approval. - Performance tolerances have no approved numeric values yet. Propose them from reliable baseline evidence and explicitly agree them with the user before using them for acceptance; keep existing diagnostic guards separate. - The project has a limited Windows local-shell prototype. Do not claim a daily-use release, Linux/macOS client, complete WindTerm parity or measured performance equivalence. -- Do not lock the stack or add a large application skeleton before benchmark evidence supports it. +- Implement the user-confirmed route in small, measurable slices. Do not add a large application skeleton or claim that the selected stack meets performance targets before benchmark evidence supports it. - Every performance claim needs workload identity, environment, correctness checks, raw samples and limitations. - Separate observed facts, hypotheses and design decisions. Static symbols do not prove runtime behavior. - Keep parser/screen state, history, transport and presentation independently testable in later implementation. @@ -20,8 +20,8 @@ - Follow docs/adr/0002-automation-first-investigation.md: prefer unattended CLI/test workflows; do not turn automation blockers into repeated user clicks, commands or per-run confirmations. Preserve unresolved evidence gaps and continue independent investigation/prototype work. - R1–R5 in docs/PLAN.md are the current product milestones. Legacy M0–M4 and G1–G3 names appear only in historical evidence. Investigation and independently testable implementation may proceed in parallel; a complete baseline is required for supported performance claims. - docs/FEATURES.md owns current capabilities and limits; docs/PLAN.md owns milestone scope; the active milestone owns acceptance contracts; GitHub issues own execution status and blockers. Do not append batch diaries to current planning documents. -- Keep build configurations separate in claims: default core/system backend, main candidate/system backend, and an explicit experimental backend need their own source and validation identity. Do not combine their independent successes into a joint acceptance result. +- Keep build configurations separate in claims: the C++/Qt/fixed-ConPTY product route, legacy default/system build, Alacritty reference build and earlier backend experiments need their own source and validation identity. Do not combine their independent successes into a joint acceptance result or transfer candidate capabilities to the C++ core. - Source comments explain responsibilities, invariants and policy. Put changing status, batch names and future work in the owning issue; retain fixed experiment/protocol identities when they define behavior or evidence. A TODO must point to a current task rather than duplicate the roadmap. - Start only an issue whose real blockers are resolved. Record source observations separately from TermForge policies, implement independently, and validate the complete user-visible behavior. An unknown target behavior is an investigation item, not a passed comparison. -- ADR-0003's historical external-core comparison remains evidence for continued candidate work, not user confirmation of a permanent route. Before final product core/backend/GUI selection, revisit ADR-0003/0004 under ADR-0006 and obtain the user's confirmation of a concrete evidence-backed proposal. Do not silently turn experimental choices into permanent architecture. +- ADR-0003/0004 preserve historical comparison and Win32 presentation evidence; ADR-0007/0008 record the current user-confirmed routes. Substantial replacements, new major dependencies or important behavior deviations need a concrete evidence-backed proposal and user confirmation. Existing experimental success does not establish product acceptance. - Documentation is primarily Chinese; technical identifiers and concise English summaries are welcome. diff --git a/CMakeLists.txt b/CMakeLists.txt index e5b3e38a..4b1e2cac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.20) project(termforge_experiments VERSION 0.1.0 LANGUAGES CXX) option(TERMFORGE_BUILD_CANDIDATE_ADAPTER "Build the Rust candidate C++ adapter checks" OFF) +option(TERMFORGE_BUILD_QT_SHELL "Build the C++/Qt local-shell migration entry" OFF) # Single-config generators use bin/, multi-config generators use bin//. set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") @@ -327,3 +328,60 @@ if(BUILD_TESTING) termforge_add_python_test(research_guards -m unittest research.tests.test_ui_probe research.tests.test_ui_typography research.tests.test_ui_r1 research.tests.test_ime_evidence -v) endif() endif() + +if(TERMFORGE_BUILD_QT_SHELL) + if(NOT WIN32) + message(FATAL_ERROR "The first Qt local-shell migration entry requires Windows x64") + endif() + find_package(Qt6 6.10.3 EXACT REQUIRED COMPONENTS Widgets) + include(cmake/ConptyBackend.cmake) + get_target_property(qt_core_dll Qt6::Core IMPORTED_LOCATION_RELEASE) + get_filename_component(qt_binary_directory "${qt_core_dll}" DIRECTORY) + find_program(qt_deploy_tool windeployqt HINTS "${qt_binary_directory}" NO_DEFAULT_PATH REQUIRED) + + function(termforge_qt_entry target) + target_link_libraries(${target} PRIVATE termforge_core Qt6::Widgets user32) + target_compile_definitions(${target} PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN + _WIN32_WINNT=0x0A00 QT_NO_KEYWORDS) + termforge_warnings(${target}) + termforge_use_bundled_conpty(${target}) + add_custom_target(${target}_font_bundle + COMMAND "${CMAKE_COMMAND}" -E make_directory "$/assets/fonts" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${PROJECT_SOURCE_DIR}/assets/fonts/RobotoMono-Regular.ttf" + "${PROJECT_SOURCE_DIR}/assets/fonts/OFL.txt" + "${PROJECT_SOURCE_DIR}/assets/fonts/README.md" + "$/assets/fonts" + VERBATIM) + add_dependencies(${target} ${target}_font_bundle) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND "${qt_deploy_tool}" --release --no-translations --no-compiler-runtime + "$" + VERBATIM) + endfunction() + + qt_add_executable(termforge_qt_shell WIN32 src/qt/main.cpp) + set_target_properties(termforge_qt_shell PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qt-bin/$") + termforge_qt_entry(termforge_qt_shell) + + if(BUILD_TESTING) + find_package(Qt6 6.10.3 EXACT REQUIRED COMPONENTS Test) + add_executable(qt_cpp_shell_smoke src/qt/shell_smoke.cpp) + set_target_properties(qt_cpp_shell_smoke PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qt-smoke-bin/$") + termforge_qt_entry(qt_cpp_shell_smoke) + target_link_libraries(qt_cpp_shell_smoke PRIVATE Qt6::Test) + # windeployqt deploys the Windows product plugin; this independent + # non-input test uses Qt's offscreen platform and needs its own plugin. + add_custom_command(TARGET qt_cpp_shell_smoke POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" + "$/platforms/" + VERBATIM) + add_test(NAME qt_cpp_shell COMMAND "${Python3_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/src/qt/run_smoke.py" "$") + set_tests_properties(qt_cpp_shell PROPERTIES TIMEOUT 35 + ENVIRONMENT "QT_QPA_PLATFORM=offscreen") + endif() +endif() diff --git a/README.md b/README.md index d8702972..85f6c2b8 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ An independent, fully open-source terminal project targeting WindTerm 2.7.0 feature, behavior, UI and performance parity. Windows 11 x64 first; Windows, Linux and macOS ultimately. Not affiliated with WindTerm or KingToolbox. -当前是有限 Windows 本地终端原型,**R1 基础文字交互已按限定合同验收,正在推进 R2 可靠本地终端与开发版**。主线已有 Alacritty 候选实验、首批流式字素、有限多会话和配置恢复;候选核心与实验 ConPTY 后端的联合配置、完整 TUI/输入、真实不同 DPI、性能预算和开发包仍需闭合。现状见[能力矩阵](docs/FEATURES.md),接续顺序见 [R2 交付路线与验收](docs/milestones/R2-local-terminal.md)。SSH/SFTP 属于 R3,完整 UI 与 WindTerm 性能等价均未验收。`TermForge` 是暂名。 +当前是有限 Windows 本地终端原型,**R1 基础文字交互已按限定合同验收,正在推进 R2 可靠本地终端与开发版**。已有 Win32 原型、有限多会话与配置恢复,以及独立 Alacritty 参考实验。正式路线已确定为自主 C++ 核心、当前采用 Qt Widgets GUI 与随包固定微软 ConPTY;新配置的最小整合、核心能力缺口、完整 TUI/输入、真实不同 DPI、性能预算和开发包仍需闭合。现状见[能力矩阵](docs/FEATURES.md),接续顺序见 [R2 交付路线与验收](docs/milestones/R2-local-terminal.md)。SSH/SFTP 属于 R3,完整 UI 与 WindTerm 性能等价均未验收。`TermForge` 是暂名。 -方向已经用户确认,依据见 [ADR-0006](docs/adr/0006-confirmed-project-direction.md):原版行为优先,内部结构不强求一致,但认真研究、借鉴原版;Alacritty 保留为候选,正式核心与 GUI/呈现选型待用户确认;Windows 本地 Shell 后端已确认采用[随包固定微软 ConPTY](docs/adr/0007-bundled-microsoft-conpty.md),整合与验收尚未完成。关键性能指标分别验收,具体允许波动范围后续与用户明确。 +方向已经用户确认,依据见 [ADR-0006](docs/adr/0006-confirmed-project-direction.md):原版行为优先,内部结构不强求一致,但认真研究、借鉴原版;用户已确认[自主 C++ 核心与当前 Qt Widgets GUI 路线](docs/adr/0008-cpp-core-qt-widgets.md),Windows 本地 Shell 后端采用[随包固定微软 ConPTY](docs/adr/0007-bundled-microsoft-conpty.md)。Alacritty 保留为参考,路线确认与实现验收分开。关键性能指标分别验收,具体允许波动范围后续与用户明确。 ## 开始使用 @@ -22,6 +22,8 @@ An independent, fully open-source terminal project targeting WindTerm 2.7.0 feat ## 项目地图 +新的[自主 C++ / Qt 会话入口](src/qt/README.md)以显式构建开关提供固定 80×24 的最小本地会话;当前默认入口和上面的操作说明仍属于既有 Win32 原型。Qt 工作区、完整文字交互及动态 resize 尚待迁移,不能据此声明完整开发版。 + | 要了解什么 | 唯一入口 | |---|---| | 已经做了什么、还缺什么 | [当前能力与目标矩阵](docs/FEATURES.md) | diff --git a/assets/fonts/README.md b/assets/fonts/README.md index 3f5f2164..a0cf8ae6 100644 --- a/assets/fonts/README.md +++ b/assets/fonts/README.md @@ -1,5 +1,7 @@ # 工作区 UI 字体来源 +新的 [Qt 迁移入口](../../src/qt/README.md)也私有加载该独立 OFL 资源,作为首批固定 ASCII 终端绘制的可复建字体;不把它视为完整 CJK/emoji fallback、Nerd Font 或原版终端字体等价。旧 Win32 的字号/度量结果与 Qt 分开验证。 + `RobotoMono-Regular.ttf` 从 [Google Fonts 的 RobotoMono 官方仓库](https://github.com/googlefonts/RobotoMono)独立取得,未使用 WindTerm 发行物中的字体。固定上游提交为 [`895ec691990d041dd727c7b5afa3ce56525d98e6`](https://github.com/googlefonts/RobotoMono/tree/895ec691990d041dd727c7b5afa3ce56525d98e6),取得日期为 2026-10-23。这里只随附未修改的静态 Regular 字体及同一提交的许可文件;完整可编辑字体源、构建脚本在固定上游树中。 | 文件 | 上游固定原始文件 | 字节数 | SHA-256 | diff --git a/cmake/ConptyBackend.cmake b/cmake/ConptyBackend.cmake new file mode 100644 index 00000000..303ebf3b --- /dev/null +++ b/cmake/ConptyBackend.cmake @@ -0,0 +1,35 @@ +# Fixed Microsoft ConPTY deployment for the selected Windows product target. +include_guard(GLOBAL) +include("${CMAKE_CURRENT_LIST_DIR}/CopyConptyBackend.cmake") + +function(termforge_use_bundled_conpty target) + if(NOT WIN32 OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8) + message(FATAL_ERROR "The fixed ConPTY package requires Windows x64") + endif() + if(NOT TARGET ${target}) + message(FATAL_ERROR "ConPTY target does not exist: ${target}") + endif() + if(NOT TERMFORGE_CONPTY_BACKEND_DIR OR NOT IS_ABSOLUTE "${TERMFORGE_CONPTY_BACKEND_DIR}") + message(FATAL_ERROR "TERMFORGE_CONPTY_BACKEND_DIR must name the absolute fixed-package directory") + endif() + get_filename_component(repository "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/.." ABSOLUTE) + set(manifest "${repository}/third_party/conpty/manifest.json") + set(license "${repository}/third_party/conpty/LICENSE") + termforge_validate_conpty_pair("${TERMFORGE_CONPTY_BACKEND_DIR}" "${manifest}" "${license}") + target_compile_definitions(${target} PRIVATE TERMFORGE_CONPTY_SIDEBYSIDE=1) + target_link_libraries(${target} PRIVATE bcrypt) + + # An always-run dependency also repairs a missing bundle when no source + # requires relinking. The script checks the inputs again before copying. + add_custom_target(${target}_conpty_bundle + COMMAND "${CMAKE_COMMAND}" + "-DCONPTY_BUNDLE_INPUT_DIRECTORY=${TERMFORGE_CONPTY_BACKEND_DIR}" + "-DCONPTY_BUNDLE_MANIFEST=${manifest}" + "-DCONPTY_BUNDLE_LICENSE=${license}" + "-DCONPTY_BUNDLE_OUTPUT_DIRECTORY=$/conpty-backend" + -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/CopyConptyBackend.cmake" + DEPENDS "${TERMFORGE_CONPTY_BACKEND_DIR}/conpty.dll" + "${TERMFORGE_CONPTY_BACKEND_DIR}/OpenConsole.exe" "${manifest}" "${license}" + VERBATIM) + add_dependencies(${target} ${target}_conpty_bundle) +endfunction() diff --git a/cmake/CopyConptyBackend.cmake b/cmake/CopyConptyBackend.cmake new file mode 100644 index 00000000..bf8d4e18 --- /dev/null +++ b/cmake/CopyConptyBackend.cmake @@ -0,0 +1,40 @@ +# Shared configure/build check for the immutable pair in the package manifest. +function(termforge_validate_conpty_pair directory manifest license) + if(NOT IS_ABSOLUTE "${directory}" OR NOT IS_DIRECTORY "${directory}") + message(FATAL_ERROR "Fixed ConPTY input must be an absolute directory") + endif() + file(READ "${manifest}" metadata) + foreach(name conpty.dll OpenConsole.exe) + if(NOT EXISTS "${directory}/${name}") + message(FATAL_ERROR "Fixed ConPTY package is incomplete: ${name}") + endif() + string(JSON expected GET "${metadata}" files "${name}") + file(SHA256 "${directory}/${name}" actual) + if(NOT actual STREQUAL expected) + message(FATAL_ERROR "Fixed ConPTY identity mismatch: ${name}") + endif() + endforeach() + string(JSON expected_license GET "${metadata}" bundled_license_sha256) + file(SHA256 "${license}" actual_license) + if(NOT actual_license STREQUAL expected_license) + message(FATAL_ERROR "Fixed ConPTY license identity mismatch") + endif() +endfunction() + +if(CMAKE_SCRIPT_MODE_FILE STREQUAL CMAKE_CURRENT_LIST_FILE) + termforge_validate_conpty_pair("${CONPTY_BUNDLE_INPUT_DIRECTORY}" + "${CONPTY_BUNDLE_MANIFEST}" "${CONPTY_BUNDLE_LICENSE}") + if(NOT IS_ABSOLUTE "${CONPTY_BUNDLE_OUTPUT_DIRECTORY}") + message(FATAL_ERROR "Fixed ConPTY output must be an absolute directory") + endif() + file(MAKE_DIRECTORY "${CONPTY_BUNDLE_OUTPUT_DIRECTORY}") + execute_process(COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${CONPTY_BUNDLE_INPUT_DIRECTORY}/conpty.dll" + "${CONPTY_BUNDLE_INPUT_DIRECTORY}/OpenConsole.exe" + "${CONPTY_BUNDLE_MANIFEST}" "${CONPTY_BUNDLE_LICENSE}" + "${CONPTY_BUNDLE_OUTPUT_DIRECTORY}" + RESULT_VARIABLE copy_status) + if(NOT copy_status EQUAL 0) + message(FATAL_ERROR "Fixed ConPTY bundle copy failed: ${copy_status}") + endif() +endif() diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index e9163b4d..885a4eb0 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -4,13 +4,14 @@ 每条证据仅支持其固定配置,不能把历史成功、局部模块、未合并分支拼成当前产品验收: -产品方向和确认依据见 [ADR-0006](adr/0006-confirmed-project-direction.md)。Windows PTY 路线已由用户确认采用[随包固定微软 ConPTY](adr/0007-bundled-microsoft-conpty.md),整合与验收仍未完成。历史候选比较及限定合同保留事实身份,不构成用户批准正式选型或重要差异;关键性能容差仍待可靠基线后与用户明确。 +产品方向和确认依据见 [ADR-0006](adr/0006-confirmed-project-direction.md)。Windows PTY 路线已由用户确认采用[随包固定微软 ConPTY](adr/0007-bundled-microsoft-conpty.md),正式核心继续自主 C++,当前 GUI 采用 [Qt Widgets](adr/0008-cpp-core-qt-widgets.md);新组合的整合与验收仍未完成。历史 Alacritty 与 Win32 比较及限定合同保留事实身份,不转记为新产品组合能力或重要差异的批准;关键性能容差仍待可靠基线后与用户明确。 | 配置 | 证据定位 | |---|---| -| main 默认:自研核心 + 系统 ConPTY | R1 限定总验收和有限 VT/Unicode 子集;默认构建不需要 Rust | -| main 显式候选:`termforge.4` + 系统 ConPTY | [固定来源/补丁台账](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)、[#39 字素合同](research/streaming-graphemes.md);仅按已有合同验收 | -| PR #50 实验:`termforge.4` + 固定官方 ConPTY | [Draft PR](https://github.com/JavaZeroo/termforge/pull/50) / [#47](https://github.com/JavaZeroo/termforge/issues/47);[联合基线](research/joint-conpty-baseline.md)已固定,产品回归未全绿;[resize #51](https://github.com/JavaZeroo/termforge/issues/51)、[GUI Vim #52](https://github.com/JavaZeroo/termforge/issues/52)、[工作区 #63](https://github.com/JavaZeroo/termforge/issues/63)仍开放。[#53 的未完成粘贴、负例及原八会话证据](https://github.com/JavaZeroo/termforge/blob/848cb0e621a291f732cc0e046d0b2fab6a77d962/docs/research/resize-replay-cost.md)已按限定合同整合到实验父分支;不包含同次真实 CF/IME 或完整官方矩阵验收 | +| 自主 C++ + Qt Widgets + 固定微软 ConPTY 正式目标 | [ADR-0007](adr/0007-bundled-microsoft-conpty.md) / [ADR-0008](adr/0008-cpp-core-qt-widgets.md) 记录用户确认;新组合的实现与验证另行留证,不引用旧默认或 Alacritty 通过结果代替 | +| 既有默认/Alpha:自主 C++ + 系统 ConPTY + Win32 | R1 限定总验收和有限 VT/Unicode 子集;与当前 Qt/固定后端目标组合分列 | +| Alacritty 历史参考:`termforge.4` + 系统 ConPTY | [固定来源/补丁台账](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)、[#39 字素合同](research/streaming-graphemes.md);结果仅证明其固定配置,不转为自主 C++ 能力 | +| PR #50 历史联合基线:`termforge.4` + 固定官方 ConPTY | [Draft PR](https://github.com/JavaZeroo/termforge/pull/50) / [#47](https://github.com/JavaZeroo/termforge/issues/47);[联合基线](research/joint-conpty-baseline.md)已固定,产品回归未全绿;该基线当时的 resize/工作区失败由 #51/#63 后续在原实验分支限定闭合,#52 GUI Vim 及新组合完整验收仍未完成。[#53 的未完成粘贴、负例及原八会话证据](https://github.com/JavaZeroo/termforge/blob/848cb0e621a291f732cc0e046d0b2fab6a77d962/docs/research/resize-replay-cost.md)已按限定合同整合到实验父分支;不包含同次真实 CF/IME 或完整官方矩阵验收 | R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分别只代表原记录。源 VT、原生控制台 CHAR_INFO、transport、末端状态、自有 DIB、绘制提交和真实呈现是不同检查点;未知端点保持未知。 @@ -20,6 +21,8 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 ## 产品能力的限定验收 +新路线的[首个 Qt/C++ 会话证据](../experiments/shell/results/2026-10-28-qt-cpp-entry/README.md)绑定 `0fd35e2`:完整 CTest 47/47,其中 Qt 新入口 1 项、旧 system 诊断 46 项,内部 12 条跳过记录分列。固定 cmd 的输入/750 正文墨迹、自然退出、启动错误与人工关闭有实证;这不迁移旧 Win32/Alacritty 能力,也不是完整 R2、原生窗口呈现或性能验收。 + | 范围 | 结果与复现入口 | 可以支持的结论与限制 | |---|---|---| | R1 完整文字工作流(限定总出口) | [同次系统组合及独立审计](../experiments/shell/results/2026-10-24-r1-system-workflow/validation.json)、[同源码既有回归](../experiments/shell/results/2026-10-24-r1-system-workflow/regression.json)、[协议与复现](../experiments/shell/WORKFLOW.md)、[主CI 40045678550](https://github.com/JavaZeroo/termforge/actions/runs/40045678550)、[显式verify 40045686776](https://github.com/JavaZeroo/termforge/actions/runs/40045686776) | 固定源码 ce9b24d 的同次八会话 Copy→Paste→真实拼音、后台负载、滚动状态/提交端点及回收,满足 R1 限定总出口。内部 skip 与无独立 EXE 重哈希缺口保留;presented_done=null。 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index b5b51a5c..3e1fea77 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,10 +1,10 @@ # 当前能力与目标矩阵 -本文件维护当前 main 的能力与已知差距。R1 已按限定合同验收;R2 仍在实现与整合,尚未达到日常开发版出口。路标见[总体计划](PLAN.md),验收合同见 [R2 出口检查表](milestones/R2-local-terminal.md),执行状态见 [GitHub 任务入口](TRACKING.md),结果与来源见[证据索引](EVIDENCE.md)。每份证据只证明其固定源码、构建配置和场景;局部测试、未合并分支及历史成功不自动代表当前产品整体验收。 +本文件维护所读源码与固定证据的能力及已知差距;未合并分支不代表 main 已包含相应实现。R1 已按限定合同验收;R2 仍在实现与整合,尚未达到日常开发版出口。路标见[总体计划](PLAN.md),验收合同见 [R2 出口检查表](milestones/R2-local-terminal.md),执行状态见 [GitHub 任务入口](TRACKING.md),结果与来源见[证据索引](EVIDENCE.md)。每份证据只证明其固定源码、构建配置和场景;局部测试、未合并分支及历史成功不自动代表当前产品整体验收。 比较目标固定为 **WindTerm 2.7.0**。功能范围依据[固定提交的官方 README](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/README.md)、[2.7.0 release](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0)及仓库内的发行物/运行时研究。官方声明说明目标包含什么,不证明已经在本机验证了其行为,更不证明 TermForge 已实现。各平台、版本限制和具体交互仍需逐项形成可重复场景。 -用户确认的目标和权限见 [ADR-0006](adr/0006-confirmed-project-direction.md):首个完整版本验收 Windows 11 x64,最终覆盖 Windows/Linux/macOS;原版实际行为优先。Alacritty 保留为候选,正式核心和 GUI/呈现选型待确认;Windows PTY 已确认采用[随包固定微软 ConPTY](adr/0007-bundled-microsoft-conpty.md),实现尚待整合验收;关键性能指标逐项达标,具体波动范围后续与用户明确。以下历史限定验收描述实现证据,不自动表示用户批准了最终策略或已达到原版一致性。 +用户确认的目标和权限见 [ADR-0006](adr/0006-confirmed-project-direction.md):首个完整版本验收 Windows 11 x64,最终覆盖 Windows/Linux/macOS;原版实际行为优先。正式核心已确认继续自主开发 C++,当前 GUI 采用 [Qt Widgets 路线](adr/0008-cpp-core-qt-widgets.md);Windows PTY 采用[随包固定微软 ConPTY](adr/0007-bundled-microsoft-conpty.md),新配置尚待整合验收;Alacritty 保留为参考,关键性能指标逐项达标,具体波动范围后续与用户明确。以下历史限定验收描述实现证据,不自动表示用户批准了最终策略或已达到原版一致性。 状态定义:**限定验收**指表中明确范围已有实现与独立回归;**实现中,未验收**指已有实现,但整项目标场景或整合验收未完成;**未实现**指尚无相应产品能力;**待取证**指目标行为或比较结论未知;**限定测量**仅说明已有指定范围的观测,不代表性能预算通过。每行的“路标”表示主要交付位置,不表示之前的路标已完成。 @@ -12,14 +12,19 @@ | 配置 | 当前定位 | 可以引用的证据与限制 | |---|---|---| -| 默认 main:自研核心 + 系统 ConPTY | 默认构建与已发布 Alpha 的基础路线,不需要 Rust | 已有 R1 限定验收;VT/Unicode 能力仍为有限子集,不能引用候选核心的字素或 TUI 结果 | -| 显式候选 main:`termforge.4` + 系统 ConPTY | `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON`;按 [ADR-0003](adr/0003-core-reference-and-reuse.md)保留为可评估实验,正式核心选型待用户确认 | [固定来源](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)与 [#39 字素合同](research/streaming-graphemes.md);候选正确性通过不关闭系统 ConPTY 或 R2 父项 | -| 已确认后端路线的整合分支:`termforge.5` + 固定官方 ConPTY | [PR #50(Draft)](https://github.com/JavaZeroo/termforge/pull/50),早期 `termforge.4` 联合源码 `3e5b96d` 的[原基线](research/joint-conpty-baseline.md)保留;后续限定修复已整合,尚未合入 main 或通过产品整体验收 | [#47](https://github.com/JavaZeroo/termforge/issues/47) 跟踪后端整合;终端 margin 与原生状态分列。[resize #51](https://github.com/JavaZeroo/termforge/issues/51) 与[工作区 #63](https://github.com/JavaZeroo/termforge/issues/63) 已在实验父分支限定闭合,完整 resize/历史由 [#29](https://github.com/JavaZeroo/termforge/issues/29) 接续;[GUI Vim #52](https://github.com/JavaZeroo/termforge/issues/52) 仍开放。[#53](https://github.com/JavaZeroo/termforge/issues/53) 的原 3MB/768B/16KiB 未完成粘贴取消/关闭、负例及八会话 data-only 两轮已限定验证并整合到实验父分支;同次真实 CF/IME 与完整官方矩阵未通过。fence 默认关闭;[ADR-0007](adr/0007-bundled-microsoft-conpty.md)已记录用户正式后端选择,代码合入与完整验收仍未完成 | +| 正式目标:自主 C++ + Qt Widgets + 固定微软 ConPTY | [ADR-0007](adr/0007-bundled-microsoft-conpty.md) / [ADR-0008](adr/0008-cpp-core-qt-widgets.md) 已确认路线;先建立最小整合入口 | 新组合尚无完整工作流验收;核心能力与 Qt 交互按实际实现逐项验证,旧默认或 Alacritty 成果不自动继承 | +| 既有默认/Alpha:自主 C++ 核心 + 系统 ConPTY + Win32 | 现有原型与已发布体验包的固定路线,不需要 Rust;不等于新的正式组合已完成 | 已有 R1 限定验收;VT/Unicode 仍为有限子集,不能引用 Alacritty 参考的字素或 TUI 结果 | +| Alacritty 参考构建:`termforge.4` + 系统 ConPTY | `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON`;保留[历史核心比较](adr/0003-core-reference-and-reuse.md)与差分,不继续作为正式产品核心扩展 | [固定来源](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)与 [#39 字素合同](research/streaming-graphemes.md)均只证明其固定源码/配置 | +| 历史 Alacritty/固定后端整合分支:`termforge.5` + 固定官方 ConPTY | [PR #50(Draft)](https://github.com/JavaZeroo/termforge/pull/50),早期 `termforge.4` 联合源码 `3e5b96d` 的[原基线](research/joint-conpty-baseline.md)保留;后续限定修复已整合,尚未合入 main 或通过产品整体验收 | [#47](https://github.com/JavaZeroo/termforge/issues/47) 跟踪后端整合;终端 margin 与原生状态分列。[resize #51](https://github.com/JavaZeroo/termforge/issues/51) 与[工作区 #63](https://github.com/JavaZeroo/termforge/issues/63) 已在实验父分支限定闭合,完整 resize/历史由 [#29](https://github.com/JavaZeroo/termforge/issues/29) 接续;[GUI Vim #52](https://github.com/JavaZeroo/termforge/issues/52) 仍开放。[#53](https://github.com/JavaZeroo/termforge/issues/53) 的原 3MB/768B/16KiB 未完成粘贴取消/关闭、负例及八会话 data-only 两轮已限定验证并整合到实验父分支;同次真实 CF/IME 与完整官方矩阵未通过。fence 默认关闭;[ADR-0007](adr/0007-bundled-microsoft-conpty.md)已记录用户正式后端选择,接入成果须在自主 C++/Qt 路线重新整合并验证 | -以下能力默认指已合并实现;某个配置的通过结果不能转用于另一个配置。R1 的真实系统 CF/拼音验收属于固定自研核心配置,不外推到候选 `.4` 或实验后端联合配置。 +正式目标组合为自主 C++ 核心 + Qt Widgets + 随包固定微软 ConPTY;Qt 客户端尚无完整产品能力或联合验收,当前先建立最小整合入口。以下保留既有实现与各固定证据;某个配置的通过结果不能转用于另一个配置。R1 的真实系统 CF/拼音验收属于固定自研核心配置,不外推到候选 `.4` 或实验后端联合配置。 ## 基础文字交互与本地终端 +| 新路线迁移入口 | 当前状态与边界 | 目标差距 | 路标 | +|---|---|---|---| +| 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供固定 80×24 的单个真实 cmd、有界输入/输出、基本 ASCII/16 色绘制及错误/关闭处理;与旧 Win32/Alacritty 配置分别验证 | 完整工作区、动态 resize、历史/选择/Find、Unicode/IME、真实 DPI、应用兼容与性能仍需实现或迁移;当前为本地构建,不是可发布开发包 | R2 | + 官方参考的基础文字行为已按 [R1-01 合同](research/windterm-r1-text-contract.md)完成两组各两轮的自动取证;这为独立实现提供行为依据,产品能力仍按下表分别验收。 | 能力 | 当前状态与边界 | 目标差距 | 路标 | @@ -43,7 +48,7 @@ |---|---|---|---| | 主区/底部多标签与分屏 | **限定验收**:两个区域独立选中标签,共计最多 8 个真实本地会话;主区可有一个真实左右 split,Alt+- 创建、Alt+Z 隐藏/恢复、Alt+Left/Right 切换、Alt+X 关闭,[两轮独立输出/后台/焦点/回收证据](../experiments/shell/results/2026-10-24-r2-main-split/validation.json) | 仅主区一个 split;任意嵌套、标签排序/拖放、复制会话、标签搜索/颜色未完成;分屏持久化范围见配置行 | R2 / R3 / R4 | | 基础窗口操作 | **限定验收**:新建/切换/关闭/重命名、五条分隔线、面板显隐、底部焦点模式 | 完整会话动作/快捷键、命令面板、目标 Focus/SyncInput 行为 | R1 / R4 | -| 工作区外观 | **实现中,未验收**:两种逻辑尺寸下部分外框/层级依据直接取证;UI 字体来自独立 OFL 来源,私有加载、实际 GDI face/em/行高与缺失资产失败有自动检查 | 不是像素等价;图标、主题/透明度、多语言及所有状态仍缺。Explorer/Filer/Outline 当前为空区域 | R2 / R3 / R4 | +| 工作区外观 | **实现中,未验收**:当前 Qt Widgets 路线待迁移,以下为既有 Win32 原型证据;两种逻辑尺寸下部分外框/层级依据直接取证;UI 字体来自独立 OFL 来源,私有加载、实际 GDI face/em/行高与缺失资产失败有自动检查 | 不是像素等价;图标、主题/透明度、多语言及所有状态仍缺。Explorer/Filer/Outline 当前为空区域 | R2 / R3 / R4 | | DPI 与文字缩放 | **限定验收**:每会话 50%–200%,受控 96→144→192→96;[字体/显示合同](milestones/R2-font-display.md)明确 12×26 基准网格。两台真实 96-DPI 显示器间往返两次保持窗口、rename 焦点、隐藏标签和网格,[证据](../experiments/shell/results/2026-10-24-r2-monitor-font/validation.json)明确 `distinct_dpi=false`;v2 会话恢复另覆盖 100%/150%/75% zoom | 不同 DPI 的真实跨屏、实际呈现和真实 IME 跨屏仍未验收;恢复证据为同一 96-DPI 私有 desktop,目标字形/逐字形 fallback 未等价 | R2 | | 配置与会话持久化 | **限定验收**:有界 v2、原子替换、v1 迁移、空会话与损坏/未知版本保护;[多会话恢复](../experiments/shell/results/2026-10-25-workspace-restore-multi/validation.json)覆盖 shell 类型/目录/标题/zoom/split/窗口,[迁移回退](../experiments/shell/results/2026-10-25-workspace-recovery-guards/validation.json)及[写失败与关闭策略](../experiments/shell/results/2026-10-25-workspace-write-failure/validation.json)均有真实验证。[#61 的合法编辑 BS 前缀](../experiments/shell/results/2026-10-26-workspace-restore-bs/validation.json)与 [#72 的空记录 CR 重绘](../experiments/shell/results/2026-10-27-workspace-empty-cr/validation.json)已限定修正;后者同源码三配置共 18 次保存/恢复/钳制通过;目录/版本完成按[独立输出与新提示符合同](research/workspace-restore-transport-oracle.md)核对;只重建白名单 shell,不恢复原进程/历史;保存失败保留窗口,显式放弃保存才跳过 | 仅当前单工作区、正常窗口和白名单本地会话;偏好、分组/搜索、最大化/多窗口与文字标记恢复归后续。真实 DPI、呈现和 #38 等独立回归不因此视为通过,R2 出口未宣告完成 | R2 / R3 / R4 | | 文本增强与辅助面板 | **未实现** | 高亮/时间戳/折叠/outline、补全与 Fish 增强、命令历史、Sender/QuickBar、Free Type Mode(输入模式,行为待取证)、vim 本地/远程键位、锁屏、Sysmon | R4 | diff --git a/docs/PLAN.md b/docs/PLAN.md index b397b26a..dc6f7c30 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -12,7 +12,7 @@ 官方 `2.7.0` tag 指向提交 `a8336c1d1e3dd1a981500dae8ef9eb42066de95f`。目标清单的公开来源是[固定提交 README](https://github.com/kingToolbox/WindTerm/blob/a8336c1d1e3dd1a981500dae8ef9eb42066de95f/README.md)与[2.7.0 发布说明](https://github.com/kingToolbox/WindTerm/releases/tag/2.7.0)。官方声明、发行物静态事实、实际运行观察和本项目实现结果分别记录,前一类不能替代后一类。 -Windows 本地 Shell 的正式 PTY 路线已确认采用[随包固定微软 ConPTY](adr/0007-bundled-microsoft-conpty.md),集中整合已有成果;核心与 GUI/呈现仍待用户确认,后端选型不代表实现或性能已验收。 +正式产品核心已确认继续自主开发 C++,当前 GUI 路线采用 Qt Widgets,见 [ADR-0008](adr/0008-cpp-core-qt-widgets.md);Windows 本地 Shell 采用[随包固定微软 ConPTY](adr/0007-bundled-microsoft-conpty.md)。集中整合这一条产品路线,确认不代表新配置的实现或性能已验收。 当前实现与证据以[功能矩阵](FEATURES.md)为准,执行任务以[任务跟踪](TRACKING.md)为准。本文件只维护路标、工作包、依赖和出口条件,不累计每批开发过程。 @@ -20,7 +20,7 @@ Windows 本地 Shell 的正式 PTY 路线已确认采用[随包固定微软 ConP **R1 基础文字交互已按限定合同验收,当前推进 R2,尚未达到开发版出口。** 下一次产品交付是在明确支持的 Windows 环境中可靠运行本地 shell 和固定 TUI、恢复工作配置,并提供可复建的开发包。当前各构建配置已做什么见 [FEATURES](FEATURES.md);R2 的接续顺序、并行工作和出口检查只在 [R2 交付路线与验收](milestones/R2-local-terminal.md)维护,实时进度由 [GitHub 执行项](TRACKING.md)承接。 -[平台范围](../research/windterm-2.7.0-platform-scope.md)与 [ADR-0003](adr/0003-core-reference-and-reuse.md)保存已有调查和候选实验。Alacritty 代码保留为候选,与自主实现路线继续比较;#20/#26 的历史完成不代表用户确认采用。最终核心/GUI/平台边界由 #34 提交实际工作流、WindTerm 对标、成本及维护取舍,用户确认后定案;原型或 Alpha 均不代表可日常替代 WindTerm。 +[平台范围](../research/windterm-2.7.0-platform-scope.md)与 [ADR-0003](adr/0003-core-reference-and-reuse.md)保存已有调查和核心比较。Alacritty 的代码与证据保留为参考,不继续作为正式产品核心扩展;其字素、TUI 与历史结果不能转记到自主 C++ 核心。#34 接续已确认路线的接口、迁移、支持配置与证据收敛,重要原版差异和数值性能容差仍须用户确认;原型或 Alpha 均不代表可日常替代 WindTerm。 “完整复刻”同时包含功能、行为、UI、性能和交付,不能用单一完成比例概括。全量目标台账必须提前建立;性能测量和定向逆向从现在持续进行,远期路标表示验收位置,而不是推迟开始的日期。 @@ -108,7 +108,7 @@ Windows 本地 Shell 的正式 PTY 路线已确认采用[随包固定微软 ConP | 工作包 | 具体事项 | 完成条件 | |---|---|---| -| 核心路线重审 | 保留 ADR-0003 的实际候选及历史比较;继续对照原版行为、关键性能与维护成本,并研究原版公开代码和机制;规范与其他参考协助解释差异,原版未知项待取证 | 实验可继续;正式选型由 #34 提供具体方案并经用户确认,不能由历史 issue 关闭或候选存在自动确定 | +| 自主 C++ 核心对齐 | 按 ADR-0008 扩展现有 C++ 核心,研究原版公开代码与机制,逐项闭合终端、文字、历史和回复缺口;Alacritty/pyte 继续作差分参考 | 只验收实际 C++ 实现及声明配置,不把历史候选成功转移为产品能力;未知原版行为继续取证 | | 备用屏与滚动区域 | 主/备用屏切换、光标/样式恢复、历史隔离;局部滚动、边界、原点/换行模式及保存恢复 | 分块输入、重复切换、区域外保持、resize 交叉用例及参考差分通过 | | TUI 与终端回复 | 按固定真实应用补查询回复、插删、输入/输出模式和必要样式;定义模式与 UI 选择/粘贴的关系 | 应用可进入、操作、退出并恢复主屏;维护明确应用/协议支持矩阵 | | resize 与历史语义 | 核对逻辑行、软硬换行、裁剪/重排、宽字符边界及历史关系;处理与备用屏/滚动区交叉 | 当前物理行 resize 的限制被明确保留或由经验证的新合同取代;状态与真实子进程尺寸一致 | @@ -116,10 +116,10 @@ Windows 本地 Shell 的正式 PTY 路线已确认采用[随包固定微软 ConP | 字体与真实 DPI | 明确终端字体来源、网格度量、缺字处理;完成真实显示设置和跨显示器切换 | 实际环境中布局、焦点、编辑框、输入命中和隐藏标签正确;受控消息测试不冒充跨屏验收 | | 本地会话与工作区 | shell 类型、目录/参数、启动失败、退出;真正分屏、标签操作和焦点;核对参考动作上下文 | 固定本地 shell 与多会话真实任务通过;布局存在不等于面板业务完成 | | 持久化 | 会话配置、工作区布局、标题/比例等状态的明确保存范围;原子替换、版本迁移、损坏回退 | 新进程恢复与异常配置场景通过;不会把当前仅保存布局说成完整会话恢复 | -| 产品架构决策 | 根据已实现工作流、原版对标和测量结果,提出核心、GUI/平台、历史接口、传输/呈现隔离、依赖和维护方案 | 在扩大跨平台/远程产品整合前提交具体 ADR,经用户确认后定案;原型投入不锁定架构,性能容差另经用户明确 | +| 已定路线的架构与迁移 | 按 ADR-0007/0008 整合自主 C++、Qt Widgets 与固定微软 ConPTY;明确历史/快照、传输/呈现、依赖和维护边界 | #34 保留实现与证据收敛责任;重大替换或重要行为差异另经用户确认,性能容差后续明确 | | 开发版构建与交付 | 清理实验与产品入口、可分发构建、字体/依赖来源、使用说明、诊断与长时回归 | 干净机器可以构建和使用;功能声明对应可下载产物与真实测试;重复开关会话资源稳定 | -**前置与架构门槛:** [平台切片 #23](https://github.com/JavaZeroo/termforge/issues/23)和[核心比较 #20](https://github.com/JavaZeroo/termforge/issues/20)的限定证据保留,候选实验可按 [ADR-0003](adr/0003-core-reference-and-reuse.md)继续。[全量目标台账 #19](https://github.com/JavaZeroo/termforge/issues/19)的调查并行推进。#34 按 [ADR-0006](adr/0006-confirmed-project-direction.md)重审核心与 GUI/后端,提交证据和取舍供用户确认;#20/#26 已关闭不替代该确认,也不要求重做已有实验。 +**前置与架构门槛:** [平台切片 #23](https://github.com/JavaZeroo/termforge/issues/23)和[核心比较 #20](https://github.com/JavaZeroo/termforge/issues/20)的限定证据保留为参考;[全量目标台账 #19](https://github.com/JavaZeroo/termforge/issues/19)继续并行调查。#34 按已确认的 [ADR-0007](adr/0007-bundled-microsoft-conpty.md) / [ADR-0008](adr/0008-cpp-core-qt-widgets.md)收敛接口、迁移与证据,不重复申请相同路线;功能验收、重要差异及性能数值门槛仍分别闭合。 **出口条件:** 固定本地真实任务、TUI、文字输入、配置恢复、真实 DPI 和干净环境发布全部通过;实际支持与已知差异进入功能矩阵。详细验收、证据边界与近期执行顺序见 [R2 出口检查表](milestones/R2-local-terminal.md)。仅普通命令可运行不等同于可靠本地终端。 diff --git a/docs/README.md b/docs/README.md index 0b814f9b..8fc072f1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,13 +6,13 @@ |---|---| | 已经能做什么,离目标差在哪里? | [功能与差异矩阵](FEATURES.md) | | 最终目标是什么,如何分阶段完成? | [五个路标,远到近](PLAN.md) | -| 用户确认了什么,哪些决策仍待确认? | [ADR-0006:方向与决策权限](adr/0006-confirmed-project-direction.md)、[ADR-0007:随包固定微软 ConPTY](adr/0007-bundled-microsoft-conpty.md) | +| 用户确认了什么,哪些决策仍待确认? | [ADR-0006:方向与决策权限](adr/0006-confirmed-project-direction.md)、[ADR-0007:随包固定微软 ConPTY](adr/0007-bundled-microsoft-conpty.md)、[ADR-0008:自主 C++ 与 Qt Widgets](adr/0008-cpp-core-qt-widgets.md) | | R2 分几步交付,当前先做什么、怎样验收? | [R2 交付路线与验收](milestones/R2-local-terminal.md) | | 已验收的基础文字交互合同是什么? | [R1 基础文字交互](milestones/R1-local-text.md) | | 字体、网格和真实显示如何验收? | [R2 字体与显示合同](milestones/R2-font-display.md) | | 哪项可开工,谁阻塞谁? | [GitHub 跟踪索引](TRACKING.md) | | 已完成结论由什么支持? | [证据索引](EVIDENCE.md) | -| 外部核心候选如何构建和验证? | [Alacritty 固定候选实验](../experiments/candidates/alacritty/README.md) | +| 历史 Alacritty 参考如何构建和复验? | [Alacritty 固定候选实验](../experiments/candidates/alacritty/README.md) | | 当前可下载什么版本? | [Alpha 1 发布说明](releases/v0.1.0-alpha.1.md)、[发行验证](releases/v0.1.0-alpha.1-validation.json) | | 如何构建、运行与贡献? | [贡献指南](../CONTRIBUTING.md)、[本地 shell](../experiments/shell/README.md) | | 逆向什么、如何测量? | [研究工具](../research/README.md)、[UI 探针](../research/ui/README.md)、[ConPTY 中断输入机制](research/conpty-ctrl-c-input.md)、[性能协议](../benchmarks/PROTOCOL.md) | diff --git a/docs/TRACKING.md b/docs/TRACKING.md index 0bfa0dad..c2bad468 100644 --- a/docs/TRACKING.md +++ b/docs/TRACKING.md @@ -2,7 +2,7 @@ 本页只导航到 GitHub 任务,不维护第二份进度表。能力与构建配置见 [FEATURES](FEATURES.md),R1–R5 范围见 [PLAN](PLAN.md),当前接续顺序与完成条件见 [R2 交付路线与验收](milestones/R2-local-terminal.md)。实时状态、原生父子/阻塞关系和验收记录以各 issue 为准。 -执行方向按 [ADR-0006](adr/0006-confirmed-project-direction.md):优先终端基础能力与性能,首个完整版本为 Windows 11 x64,随后完成 Linux/macOS;原版行为优先。[ADR-0007](adr/0007-bundled-microsoft-conpty.md)已确认 Windows PTY 采用随包固定微软 ConPTY,#47 接续整合;#34 负责提交其余核心与 GUI/呈现选型供用户确认,#4/#54 提供基线和预算提案,性能容差经用户明确后生效。候选实验和既有关闭任务均不自动构成用户确认。 +执行方向按 [ADR-0006](adr/0006-confirmed-project-direction.md):优先终端基础能力与性能,首个完整版本为 Windows 11 x64,随后完成 Linux/macOS;原版行为优先。[ADR-0007](adr/0007-bundled-microsoft-conpty.md)已确认 Windows PTY 采用随包固定微软 ConPTY,#47 接续整合;[ADR-0008](adr/0008-cpp-core-qt-widgets.md)已确认自主 C++ 核心与当前 Qt Widgets GUI,#34 接续接口、迁移和支持配置收敛,#4/#54 提供基线和预算提案,性能容差经用户明确后生效。候选实验和既有关闭任务均不自动构成用户确认。 ## 产品路标 @@ -20,7 +20,7 @@ | 工作 | 责任任务 | 硬前置或联合条件 | |---|---|---| -| 主线候选与官方后端的联合基线 | [联合配置基线 #55](https://github.com/JavaZeroo/termforge/issues/55) | 从明确的 main 与 Draft 身份整合;可带已知 red 如实结项,不等待所有后端问题先修好 | +| 历史候选/官方后端联合基线 | [联合配置基线 #55](https://github.com/JavaZeroo/termforge/issues/55) | 保留原固定源码与红/绿结果;可复用输入和接入经验,不替代自主 C++/Qt/固定 ConPTY 的新配置验收 | | 原生 API / VT 末端 margin 的处理路径 | [平台语义与方案 #56](https://github.com/JavaZeroo/termforge/issues/56) | 可用固定探针独立推进;不能以策略说明豁免 #41 原合同 | | 实验后端连续 resize | [残尾与 Unicode 重放 #51](https://github.com/JavaZeroo/termforge/issues/51) | 原生 viewport 与原 GUI/Unicode 合同已由 PR #89 在实验父分支限定闭合;完整 resize 由 #29、最终官方配置由 #47 接续 | | 实验后端 Vim | [Console-width Vim #52](https://github.com/JavaZeroo/termforge/issues/52) | GUI 兼容方案仍未闭合;native 捕获修正、运行库隔离实验与原固定运行库结果分列,早期 flags0 成功不替代最终配置 | @@ -45,7 +45,7 @@ | Windows 工作流可靠性 | [回归汇总 #38](https://github.com/JavaZeroo/termforge/issues/38) | #40/#44/#57/#73/#80/#81/[#85](https://github.com/JavaZeroo/termforge/issues/85);已闭合切片和旧失败保留在该总项,不再排入当前队列 | | 显式官方 ConPTY 整合 | [后端汇总 #47](https://github.com/JavaZeroo/termforge/issues/47) | #52 GUI Vim、完整官方矩阵和同次系统输入仍未闭合;#51/#53/#62/#63 与 #52 native 捕获已有各自限定修正,实验可选不等于产品默认或日常验收 | | 每缓冲区 margin 端到端合同 | [margin 汇总 #41](https://github.com/JavaZeroo/termforge/issues/41) | #47/#56;原生 API、transport 和末端分别验证 | -| 产品核心、GUI/平台与呈现边界 | [选型提案与用户确认 #34](https://github.com/JavaZeroo/termforge/issues/34) | #28–#31/#54,结合 #23/#26/#32/#33 和原版对标证据;重审 ADR-0003/0004,记录用户确认 | +| 正式路线接口、GUI 迁移与呈现边界 | [架构与整合 #34](https://github.com/JavaZeroo/termforge/issues/34) | ADR-0007/0008 已确认路线;独立切片推进最小整合,#34 汇总接口与支持配置,#28–#31/#54 的实际验收条件继续保留 | | 最终 Windows 开发版 | [产物与持续运行 #35](https://github.com/JavaZeroo/termforge/issues/35) | #27–#34/#36/#38;在最终产物上执行完整合同后核对 #7 出口 | ## 已交付基础与历史入口 @@ -54,7 +54,7 @@ |---|---| | R1 基础文字与同次八会话限定验收 | [R1 总项 #6](https://github.com/JavaZeroo/termforge/issues/6)、[组合验收 #18](https://github.com/JavaZeroo/termforge/issues/18)、[合同](milestones/R1-local-text.md) | | Alpha 1 Windows x64 体验包 | [发布切片 #22](https://github.com/JavaZeroo/termforge/issues/22)、[发布说明](releases/v0.1.0-alpha.1.md)、[下载与验证](releases/v0.1.0-alpha.1-validation.json) | -| R2 平台调查与候选比较证据 | [平台 #23](https://github.com/JavaZeroo/termforge/issues/23)归属 [全量台账 #19](https://github.com/JavaZeroo/termforge/issues/19);[候选 #24](https://github.com/JavaZeroo/termforge/issues/24)、[差分 #25](https://github.com/JavaZeroo/termforge/issues/25)、[成本/历史路线 #26](https://github.com/JavaZeroo/termforge/issues/26)归属 [历史比较 #20](https://github.com/JavaZeroo/termforge/issues/20);限定结项保留,正式选型待 #34 用户确认 | +| R2 平台调查与候选比较证据 | [平台 #23](https://github.com/JavaZeroo/termforge/issues/23)归属 [全量台账 #19](https://github.com/JavaZeroo/termforge/issues/19);[候选 #24](https://github.com/JavaZeroo/termforge/issues/24)、[差分 #25](https://github.com/JavaZeroo/termforge/issues/25)、[成本/历史路线 #26](https://github.com/JavaZeroo/termforge/issues/26)归属 [历史比较 #20](https://github.com/JavaZeroo/termforge/issues/20);限定结项保留为参考,当前正式路线见 ADR-0007/0008,#34 接续整合与证据 | | 有限本地会话与配置恢复 | [本地会话 #32](https://github.com/JavaZeroo/termforge/issues/32)、[持久化 #33](https://github.com/JavaZeroo/termforge/issues/33),能力边界见 [FEATURES](FEATURES.md) | | 首批流式字素、宽度与 glyph | [字素合同 #39](https://github.com/JavaZeroo/termforge/issues/39)、[来源与合同](research/streaming-graphemes.md),完整输入由 #30 继续验收 | | 已闭合的回归/夹具与参考捕获 | #38 的原生子项及 [EVIDENCE](EVIDENCE.md)保留 #37/#42/#45/#46 等固定原因和证据,不再次当作待做功能 | diff --git a/docs/adr/0003-core-reference-and-reuse.md b/docs/adr/0003-core-reference-and-reuse.md index e7e8afdc..c78db0a5 100644 --- a/docs/adr/0003-core-reference-and-reuse.md +++ b/docs/adr/0003-core-reference-and-reuse.md @@ -1,16 +1,16 @@ # ADR-0003:差分参考与可重审的核心路线 -> 本文保留 C1/G1 等旧代号和当时的比较结果。2026-10-25 由 [#20](https://github.com/JavaZeroo/termforge/issues/20)/[#26](https://github.com/JavaZeroo/termforge/issues/26) 完成的候选比较继续作为证据,正式选型按下方当前决定重新评估;能力现状见 [FEATURES](../FEATURES.md)。 +> 本文保留 C1/G1 等旧代号和当时的比较结果。2026-10-25 由 [#20](https://github.com/JavaZeroo/termforge/issues/20)/[#26](https://github.com/JavaZeroo/termforge/issues/26) 完成的候选比较继续作为历史证据,当前自主 C++ 决定见下方及 ADR-0008;能力现状见 [FEATURES](../FEATURES.md)。 - 日期:2026-10-22;历史重审:2026-10-25;用户方向确认:2026-10-26 -- 状态:保留 Alacritty 作为候选;正式核心选型待用户确认。2026-10-25 的路线决定已被 [ADR-0006](0006-confirmed-project-direction.md) 的候选定位替代 +- 状态:历史比较保留;2026-10-28 用户已按 [ADR-0008](0008-cpp-core-qt-widgets.md)确认继续自主开发 C++ 正式核心,Alacritty 转为参考/差分材料。2026-10-25 的复用决定不再具有当前选型效力。 - 对应任务:C1;R2-00 至 R2-03 ## 当前决定与确认边界 -用户已确认保留 Alacritty 的代码和实验材料,重新按 WindTerm 2.7.0 的行为、关键性能及长期维护成本评估;不强求原版内部结构一致,但应认真研究、借鉴原版公开代码和机制。现有自研 C++ 核心与外部候选的证据均保留,允许继续可替换的实验和兼容修复。 +用户已确认继续自主开发 C++ 终端核心,以 WindTerm 2.7.0 的实际行为为对照,并研究、借鉴原版公开代码和机制;确认依据与实施边界见 [ADR-0008](0008-cpp-core-qt-widgets.md)。Alacritty 的既有代码、补丁和结果保留为参考/差分材料,不继续作为正式产品核心扩展。 -正式核心选择由 [#34](https://github.com/JavaZeroo/termforge/issues/34) 提供具体方案、原版对标、性能及维护取舍,用户确认后生效。#20/#24–#26 已完成的是其限定研究和实验;关闭状态、过去的“Accepted”或已有适配器不能替代用户确认。性能允许波动范围同样待与用户明确,旧候选成本不等于 WindTerm 等价预算。 +正式路线已由用户确认,[#34](https://github.com/JavaZeroo/termforge/issues/34)继续承担接口、迁移、支持配置与证据收敛。#20/#24–#26 的限定研究和实验保留原身份;候选已有能力不能自动转为 C++ 核心的能力或验收结果。性能允许波动范围仍待与用户明确,旧候选成本不等于 WindTerm 等价预算。 行为冲突默认以 WindTerm 2.7.0 实际行为为准;pyte、Alacritty、规范及现有项目合同是比较材料,未知原版行为继续取证。下面保留历史比较和当时的工程选择,供复验与重审,不作为强制采用某核心的当前指令。 diff --git a/docs/adr/0004-presentation-backend.md b/docs/adr/0004-presentation-backend.md index 94e1b21c..66a450e6 100644 --- a/docs/adr/0004-presentation-backend.md +++ b/docs/adr/0004-presentation-backend.md @@ -3,10 +3,10 @@ > A1/G1/G2 为历史代号;首个本地窗口已进入当前原型。后端与平台边界的重审归入 [R2 产品架构决策](../PLAN.md),须在扩大远程和跨平台产品整合前完成,不能以本 ADR 将实验后端永久定案。 - 日期:2026-10-23 -- 状态:历史实验选择保留;仅覆盖 Windows 的 A1/G1 实验,产品 GUI/呈现选型待用户确认。Windows 本地 Shell 的 PTY 后端已另由 [ADR-0007](0007-bundled-microsoft-conpty.md)确认。 +- 状态:历史 Win32/DirectWrite/Direct2D 实验保留;2026-10-28 用户已按 [ADR-0008](0008-cpp-core-qt-widgets.md)确认当前采用 Qt Widgets GUI 路线,具体绘制实现与验收仍待完成。Windows PTY 后端由 [ADR-0007](0007-bundled-microsoft-conpty.md)确认。 - 前置:[V1 窗口探针](../../experiments/presentation/README.md)、[ADR-0001](0001-evidence-before-stack.md)、[ADR-0002](0002-automation-first-investigation.md)。 -当前按 [ADR-0006](0006-confirmed-project-direction.md) 优先实现 Windows 11 x64 的完整复刻,最终覆盖 Windows/Linux/macOS。#34 必须给出原版行为、性能、跨平台可行性和维护取舍的具体方案,经用户确认后定案;保留本实验不等于用户选定 Win32、DirectWrite 或 Direct2D。具体性能容差尚待用户明确。 +当前按 [ADR-0006](0006-confirmed-project-direction.md) 优先实现 Windows 11 x64 的完整复刻,最终覆盖 Windows/Linux/macOS。产品 GUI 当前采用 Qt Widgets,见 [ADR-0008](0008-cpp-core-qt-widgets.md);本实验结果用于已有实现和绘制对照,不要求产品永久沿用 Win32 控件或当前软件 DC target。#34 接续迁移、平台与维护边界,具体性能容差尚待用户明确。 ## 问题与候选 @@ -20,7 +20,7 @@ DirectWrite 可从实际绘制 layout 的 glyph run 直接取得字体身份与 资源预热后,保留每配置 48 个 draw / submission 原始样本和相邻计时器探针。分布仍波动,计时包含 software DC 绘制和 GDI 提交,没有完整观察器校准、用户呈现或持续交互的测量。这些数据不支持候选性能排名。 -## 决策 +## 决策(历史实验) A1 暂采用 **DirectWrite 文字布局 + 可替换的 Direct2D 绘制适配**,沿用小型 Win32 HWND 宿主。当前软件 DC target 作为正确性和集成起点;硬件加速、交换链或其他 UI 框架不在本次决策中。 @@ -36,4 +36,4 @@ A1 暂采用 **DirectWrite 文字布局 + 可替换的 Direct2D 绘制适配** - 软件 DC target、逐格 layout 查找以及全量快照复制可能成为成本来源;当前未将它们认定为实际瓶颈。缓存对固定输入有界,产品路径必须增加预算/淘汰。 - 当 A1 接入持续输出/输入后,若帧预算、延迟、多会话公平性、内存、字体正确性或跨平台维护无法满足需求,重新比较硬件呈现、数据布局与其他候选。任何后端替换应重跑同一状态/光标/字体/图像检查,并记录真实端点和观察扰动。 -在 G2 产品后端定案前重审本 ADR。性能是否接近 WindTerm 仍归正式共同负载与端点对照,不能由本 ADR 推出。 +后续产品实施遵循 ADR-0007/0008;本实验与新配置分别验证。性能是否接近 WindTerm 仍归正式共同负载与端点对照,不能由本 ADR 推出。 diff --git a/docs/adr/0006-confirmed-project-direction.md b/docs/adr/0006-confirmed-project-direction.md index 8e897cd2..ef38bf9a 100644 --- a/docs/adr/0006-confirmed-project-direction.md +++ b/docs/adr/0006-confirmed-project-direction.md @@ -15,16 +15,17 @@ | 固定版本 | 用户明确“固定windterm2.7.0”;版本升级另行确认,当前任务不追随最新发行版扩大目标。 | | 平台与首个完整版本 | 用户明确“最终目标是全平台,但是首要目标是我们的windows”,并确认以 Windows 11 x64 为首个完整版本验收环境。最终覆盖 Windows、Linux、macOS;其他系统版本、最低版本和架构仍需单独明确。 | | 推进顺序 | 先对齐终端基础能力与性能,再逐步接入 SSH、SFTP 和其余功能。平台可行性及独立调查可并行;Linux/macOS 的完整客户端验收不阻塞首个 Windows 完整版本。 | -| Alacritty | 用户确认“可以保留”。保留已有代码与证据,重新作为待评估候选;比较目标行为、性能及维护成本后,再确认正式核心选型。 | -| Windows PTY 后端 | 2026-10-28 用户明确“现在明确 随包固定微软 ConPTY”。正式路线采用随应用分发的固定微软 ConPTY,集中整合与优化;具体范围和未验收项见 [ADR-0007](0007-bundled-microsoft-conpty.md)。该确认不包含终端核心或 GUI 选型。 | +| 正式终端核心 | 2026-10-28 用户明确“继续自主开发 C++ 核心”,按原版行为和机制推进独立实现。Alacritty 已有代码与证据保留为参考/差分材料,不继续作为正式产品核心扩展;见 [ADR-0008](0008-cpp-core-qt-widgets.md)。 | +| 当前 GUI 路线 | 对 Qt Widgets 建议,2026-10-28 用户确认“这个可以先用你推荐的来”。当前采用 Qt Widgets;具体版本、终端绘制与资源策略由工程证据决定,见 [ADR-0008](0008-cpp-core-qt-widgets.md)。 | +| Windows PTY 后端 | 2026-10-28 用户明确“现在明确 随包固定微软 ConPTY”。正式路线采用随应用分发的固定微软 ConPTY,集中整合与优化;具体范围和未验收项见 [ADR-0007](0007-bundled-microsoft-conpty.md)。核心与 GUI 由后续 [ADR-0008](0008-cpp-core-qt-widgets.md)另行确认。 | | 性能标准 | 吞吐、交互/滚动/搜索延迟、CPU、内存/长期增长及多会话关键指标分别达到原版同等水平。用户明确“具体的波动范围需要后续与我明确”;当前没有获得任何数值容差的批准。 | | 行为裁决 | 用户明确“原版优先”。与通用规范、第三方核心或既有项目策略冲突时,默认以固定原版的实际行为为准;明确缺陷的修正方案另由用户确认。未知目标行为继续调查,不计为匹配。 | | 决策权限 | 目标范围、主要技术路线、验收门槛和重要差异由用户确认;已确认方向内的研究、逆向调查、候选实验、具体实现、测试和修复由代理自主推进。 | ## 对现有路线的处理 -- [ADR-0003](0003-core-reference-and-reuse.md) 的既有 Alacritty 实验及结果保留;2026-10-25 的路线选择不再作为当前正式选型依据。最终决定由 [#34](https://github.com/JavaZeroo/termforge/issues/34)提交证据和方案供用户确认。 -- [ADR-0004](0004-presentation-backend.md) 的 Win32/DirectWrite/Direct2D 路线继续作为实验实现,GUI 与呈现尚未正式选定;Windows 本地 Shell 的 PTY 后端已由 [ADR-0007](0007-bundled-microsoft-conpty.md)确认采用随包固定微软 ConPTY,整合与验收另行完成。 +- [ADR-0003](0003-core-reference-and-reuse.md) 的既有 Alacritty 实验及结果保留为参考;自主 C++ 核心已由 [ADR-0008](0008-cpp-core-qt-widgets.md)确认,#34 接续整合与架构证据,不再重复申请同一选型。 +- [ADR-0004](0004-presentation-backend.md) 的 Win32/DirectWrite/Direct2D 实验保留历史身份;当前 Qt Widgets GUI 由 [ADR-0008](0008-cpp-core-qt-widgets.md)确认,Windows PTY 由 [ADR-0007](0007-bundled-microsoft-conpty.md)确认。已有默认配置与 Alpha 不自动成为新路线的实现或验收。 - [ADR-0005](0005-physical-row-resize.md)、字体/网格、字素、选择等现有合同仍用于准确描述实现和防止回归。它们与原版的匹配情况须另验;重要差异不因测试通过或写成合同而自动获准。 - 已完成的构建、实验和限定验收保留原来源、结果和历史状态。当前问题由现有责任 issue 接续;不因方向重审重开全部已关闭任务,也不关闭真实缺口。 diff --git a/docs/adr/0007-bundled-microsoft-conpty.md b/docs/adr/0007-bundled-microsoft-conpty.md index b9c1953c..aab3a006 100644 --- a/docs/adr/0007-bundled-microsoft-conpty.md +++ b/docs/adr/0007-bundled-microsoft-conpty.md @@ -5,7 +5,7 @@ - 决策者:项目负责人(用户) - 确认依据:在比较系统 ConPTY、随包固定微软 ConPTY 和 WindTerm 2.7.0 使用的 WinPTY 后,用户明确“现在明确 随包固定微软 ConPTY”。 -Windows 11 x64 产品的本地 Shell 后端采用随应用分发、版本和来源固定的 Microsoft ConPTY。集中整合和优化这一条产品路线;系统自带 ConPTY 和 WinPTY 可保留为诊断对照,不继续作为并行产品路线扩展。该决定只选择 PTY 后端,不确认 Alacritty 核心或 Windows GUI/绘制方案,也不宣布现有实验实现已经可交付。 +Windows 11 x64 产品的本地 Shell 后端采用随应用分发、版本和来源固定的 Microsoft ConPTY。集中整合和优化这一条产品路线;系统自带 ConPTY 和 WinPTY 可保留为诊断对照,不继续作为并行产品路线扩展。该次决定只选择 PTY 后端;后续自主 C++ 核心与当前 Qt Widgets GUI 的确认见 [ADR-0008](0008-cpp-core-qt-widgets.md)。路线确认不宣布现有实验实现已经可交付。 ## 依据与取舍 @@ -19,4 +19,4 @@ WindTerm 2.7.0 官方说明为避免 vi 的非法字符而改用 WinPTY,[已 - 保持同包 `conpty.dll` / `OpenConsole.exe`、来源与许可、构建/运行身份检查及完整生命周期;组件缺失或身份错误明确失败,不静默切回另一条产品路径。后续升级固定来源并复验实际行为。 - 合并及切换产品默认前,验证声明配置的完整 shell/TUI、resize/历史、输入、工作区与回收合同;保留内部跳过、旧失败和环境缺口。正式路线与代码是否合入、是否通过验收分别记录。 - 后端选择不自动批准与原版不同的滚动区域或原生控制台行为。#34 / #41 / #56 中的具体原版差异继续留证;按原版优先调查,接受重要差异或修正原版缺陷前取得用户确认。 -- 正式终端核心仍按 [ADR-0003](0003-core-reference-and-reuse.md)待用户确认;Windows GUI/呈现仍按 [ADR-0004](0004-presentation-backend.md)待确认。性能数值容差在可靠基线后另行商定。 +- 正式终端核心和当前 GUI 路线已由后续 [ADR-0008](0008-cpp-core-qt-widgets.md)确认;具体实现、原版差异与整体验收继续分开。性能数值容差在可靠基线后另行商定。 diff --git a/docs/adr/0008-cpp-core-qt-widgets.md b/docs/adr/0008-cpp-core-qt-widgets.md new file mode 100644 index 00000000..32f56c8f --- /dev/null +++ b/docs/adr/0008-cpp-core-qt-widgets.md @@ -0,0 +1,24 @@ +# ADR-0008:自主 C++ 核心与当前 Qt Widgets GUI 路线 + +- 状态:已确认路线;实现与验收未完成 +- 日期:2026-10-28 +- 决策者:项目负责人(用户) +- 确认依据:在比较自主核心、Alacritty、其他开源核心及 WindTerm 原版方案后,用户明确“继续自主开发 C++ 核心”;对上一轮 Qt Widgets GUI 建议确认“这个可以先用你推荐的来”。后一句确认当前采用 Qt Widgets,未将具体版本或渲染策略永久锁定。 + +正式产品继续自主开发 C++ 终端核心,按 WindTerm 2.7.0 的实际行为独立实现,并研究、借鉴许可明确的公开代码与原版机制。当前 GUI 路线采用 Qt Widgets,终端绘制与核心、历史、传输分别可测;Windows 本地 Shell 后端沿用 [ADR-0007](0007-bundled-microsoft-conpty.md) 已确认的随包固定微软 ConPTY。 + +## 取舍与已有成果 + +自主核心由本项目承担完整终端语义、文字、历史、回复和兼容维护,以便围绕固定原版目标控制实现。当前 C++ 核心仍是有限子集,不因本次选型获得 Alacritty 已验的备用屏、字素或 TUI 能力;按实际缺口逐项实现和复验。 + +Qt Widgets 提供窗口、控件和布局基础,便于推进完整桌面 UI 与最终跨平台目标;迁移现有 Win32 UI 有实际成本。原版包及[运行观察](../research/windterm-ui-typography-2026-10-23.md)确认 Qt Widgets、定制文本控件和部分 QPainter 路径,但不证明我们的 Qt 实现自然达到原版正确性或性能。 + +Alacritty 的候选代码、补丁、差分轨迹及各次结果保留为参考,不再作为正式产品核心扩展。已有 Win32/DirectWrite/Direct2D、默认 C++/系统 ConPTY、候选/实验后端构建与 Alpha 保留各自来源和历史状态;它们的成功不能拼成 C++/Qt/固定 ConPTY 新配置的验收。 + +## 实施边界 + +- 先建立可构建、可观测的 C++/Qt 最小入口并复用固定 ConPTY 接入,再按真实缺口补终端基础、历史、输入和 TUI;不先搭建大型应用骨架。 +- Qt 的具体版本、终端控件绘制策略、线程调度及缓存由工程证据决定并固定身份;引入新的重大依赖、改变范围或替换当前路线仍提交用户确认。当前采用 Qt 不批准任何未测性能排名。 +- 原版行为优先继续生效。未知行为保持调查项;重要差异或原版缺陷修正单独确认。不得为了沿用旧测试而静默接受不符合原版的行为。 +- 性能保持正确性、交互延迟、吞吐、CPU、内存及公平性分别测量。数值容差仍在可靠基线后与用户明确;本决定没有批准百分比门槛。 +- [#34](https://github.com/JavaZeroo/termforge/issues/34) 接续接口、迁移、支持配置与证据收敛,[R2 出口](../milestones/R2-local-terminal.md)和各责任 issue 继续维护验收。本次路线确认不关闭 #34、R2 或任何真实功能缺口。 diff --git a/docs/milestones/R2-local-terminal.md b/docs/milestones/R2-local-terminal.md index 895b8031..d09ff319 100644 --- a/docs/milestones/R2-local-terminal.md +++ b/docs/milestones/R2-local-terminal.md @@ -4,13 +4,13 @@ 用户结果:在明确支持的 Windows 环境中,用固定本地 shell、分页器和编辑器完成连续工作;复杂文字、输入和会话互相一致;重启后恢复已声明的配置;最终下载包能从公开源码复建。 -方向约束见 [ADR-0006](../adr/0006-confirmed-project-direction.md):优先对齐终端基础能力与性能,首个完整版本环境为 Windows 11 x64,R2 是其中的阶段性交付。原版行为优先;Alacritty 保留为候选,Windows PTY 已确认采用[随包固定微软 ConPTY](../adr/0007-bundled-microsoft-conpty.md),其整合与完整验收仍由 #47 接续;核心与 GUI/呈现正式方案待用户确认,性能容差在可靠基线后另行明确。历史已验合同继续作为回归依据,原版未知和重要差异由相应任务取证及提交确认。 +方向约束见 [ADR-0006](../adr/0006-confirmed-project-direction.md):优先对齐终端基础能力与性能,首个完整版本环境为 Windows 11 x64,R2 是其中的阶段性交付。原版行为优先;正式核心已确认继续自主开发 C++,当前 GUI 路线采用 [Qt Widgets](../adr/0008-cpp-core-qt-widgets.md),Windows PTY 采用[随包固定微软 ConPTY](../adr/0007-bundled-microsoft-conpty.md)。独立切片推进新组合整合,#34/#47 汇总接口与后端验收;性能容差在可靠基线后另行明确。历史已验合同继续作为回归依据,原版未知和重要差异由相应任务取证及提交确认。 ## 当前站位 -R2 的前期平台调查/候选比较、有限会话与恢复、首批流式字素已有各自限定交付;这些研究不代表用户确认正式核心路线。剩余工作集中在**联合配置、原版对照、兼容与输入、环境、性能、用户选型确认及最终产物**。已有证据继续复用;已发布 Alpha、主线显式候选、Draft 官方后端是不同配置,证据不得互借。 +R2 的平台调查、有限会话与恢复及 Alacritty 参考实验已有各自限定交付。当前正式路线为**自主 C++ 核心、Qt Widgets 与随包固定微软 ConPTY**,下一步先建立最小可构建整合,再补 C++ 核心能力和完整用户工作流。现有 C++ 核心仍是有限子集;候选的首批字素、TUI 与重排成功不转记为 C++ 能力。已有 Alpha、Win32 原型、Alacritty 和 Draft 后端保留固定身份,新组合需要自己的证据。 -联合源码 `3e5b96d` 的三配置[真实基线](../research/joint-conpty-baseline.md)已在实验分支冻结,包含原场景及完整回归的明确失败;近期转向这些失败的因果修复与复验。基线交付不选择默认后端或宣布兼容性通过,Draft #50 仍需通过原有回归。 +历史联合源码 `3e5b96d` 的三配置[真实基线](../research/joint-conpty-baseline.md)及其后续限定修复继续用于复用输入、接入经验和复验原失败;它们不证明新的 C++/Qt/固定后端组合已通过。Draft #50 的后端成果接入当前路线后仍须验证原合同,路线确认不解除失败或跳过项。 ## R2 内部的四段交付 @@ -18,35 +18,34 @@ R2 的前期平台调查/候选比较、有限会话与恢复、首批流式字 | 阶段 | 用户可审查的交付 | 责任与完成条件 | |---|---|---| -| 1. 冻结联合基线 | 主线候选和显式官方后端来自同一份可重建源码;系统对照与联合配置各有实际结果 | [联合基线 #55](https://github.com/JavaZeroo/termforge/issues/55):固定 source/依赖/二进制/包/flags,验证 API 生命期、原 175B margin、Unicode/glyph、真实尺寸 ack、退出与回收;重放 #51–#53 原场景,分列 red、green 和未复现。维持当前默认构建配置;#34 的正式产品选型仍待用户确认 | -| 2. 闭合兼容性与输入 | 明确支持的 shell、分页器、编辑器、复杂文字与会话交互在联合配置中连续工作 | #47/#52 接续后端联合验收,#51/#53/#62/#63 的限定成果纳入最终复验;#41 处理原生 API/末端语义;#27/#29 核对模式与 resize,#28 验收固定 TUI/回复/输入字节,#30 验收完整 IME/选择/粘贴。实际不同 DPI 由 #31 并行提供 | -| 3. 提交并确认产品架构与支持配置 | 提供可比较的核心/后端/GUI 方案、证据和取舍,经用户确认后定案 | #34 使用 #23/#26、#28–#33 的真实结果与 #54 的受控成本,重审 ADR-0003/0004;明确 Windows 11 x64 优先、未来平台障碍、接口、并发/取消和维护成本;范围及重要差异依 ADR-0006 确认 | +| 1. 建立正式路线最小整合 | 自主 C++、Qt Widgets 与固定微软 ConPTY 形成可构建、可观测的最小入口 | 独立实施切片固定 source/依赖/二进制/包身份,复用 #47 接入并验证最小输出、尺寸、退出与回收;#34 汇总接口与迁移边界。先形成基线,不搭大型应用骨架,也不宣称现有候选能力已经移植 | +| 2. 闭合核心、兼容性与输入 | 声明的 shell、固定 TUI、复杂文字与会话交互在新组合中连续工作 | #27/#29 补 C++ 模式、软硬行、历史与 resize;#28 验收真实应用/回复/输入模式,#30 补复杂文字与 IME/选择/粘贴。#39 参考成果、#51/#53/#62/#63 限定后端修复分别重验,真实不同 DPI 由 #31 并行提供 | +| 3. 收敛架构、支持配置与性能证据 | 已定路线的接口、依赖、迁移和资源策略有可审查实现与证据 | #34 复用 #23/#26/#32/#33 及新组合工作流,收敛核心/历史、GUI/平台、传输/呈现、背压/取消与维护边界;#54 提供受控成本,原版重要差异和数值容差仍由用户确认 | | 4. 验收最终开发包 | 用户从公开源码复建或解压下载包,在声明环境中完成固定工作流 | #35 在最终产物上复验本页全部门槛、配置升级/恢复、错误/回收及预先冻结的持续运行与资源合同;#36/#38 的可靠性出口必须闭合。来源、许可、说明与产物身份一致后,才结项 #7 | 关键依赖如下;实线为硬前置,虚线为联合复验条件,省略已关闭前置和冗余边。完整原生关系在 GitHub。 ```mermaid flowchart LR - baseline["55 联合基线"] --> backend["47 后端整合"] - fixes["52 GUI Vim"] --> backend - baseline -. "最终修复复验" .-> fixes - native["56 原生语义路径"] --> margin["41 margin 合同"] - backend --> margin --> modes["27 终端模式"] --> tui["28 固定 TUI"] - resize["29 resize / 历史"] -. "交叉验收" .-> tui - tui --> arch["34 架构定案"] - resize --> arch - input["30 完整输入"] --> arch - dpi["31 真实 DPI"] --> arch - perf["54 受控性能"] --> arch + route["ADR-0007 / 0008 已确认路线"] --> integration["C++ / Qt / 固定 ConPTY 最小整合"] + integration --> core["27 / 29 核心与历史"] --> tui["28 固定 TUI"] + integration --> backend["47 后端联合验收"] --> tui + integration --> input["30 文字与完整输入"] + integration --> dpi["31 真实显示"] + perf["3 / 4 / 54 原版基线与成本"] -. "持续并行" .-> integration + tui --> arch["34 架构与支持配置收敛"] + input --> arch + dpi --> arch + perf --> arch arch --> package["35 最终开发包"] reliability["36 / 38 可靠性"] --> package ``` ## 下一批的任务边界 -1. **在已冻结基线上闭合剩余后端回归与联合验收。** #55 已交付含红的真实基线;#51 原生 viewport resize、#63 工作区原生 viewport,以及 #62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)和 #53 原 UI/3MB/八会话 data-only 合同均已在各自范围验证并整合到实验父分支。接续 #52 GUI Vim 与 #47 的完整官方矩阵、同次真实 CF/IME;限定切片关闭不等于联合验收通过。未完成项可以用原冻结输入独立定位,最终修复验收须在联合配置重建;期限、负载和原失败保留,一次未复现不自动构成因果修复。 +1. **先建立正式 C++/Qt/固定后端最小整合,再复验后端成果。** 新组合先验证可构建、实际输出、尺寸、退出与回收,保留尚未支持的 C++ 核心模式和 Qt 交互缺口。 #55 已交付含红的真实基线;#51 原生 viewport resize、#63 工作区原生 viewport,以及 #62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)和 #53 原 UI/3MB/八会话 data-only 合同均已在各自范围验证并整合到实验父分支。接续 #52 GUI Vim 与 #47 的完整官方矩阵、同次真实 CF/IME;限定切片关闭不等于联合验收通过。未完成项可以用原冻结输入独立定位,最终修复验收须在联合配置重建;期限、负载和原失败保留,一次未复现不自动构成因果修复。 2. **并行处理原版对照、平台语义与性能。** [原生语义处理路径 #56](https://github.com/JavaZeroo/termforge/issues/56)先明确 #41 原合同需要什么、哪些路径可支持;保留必要的上游或独立实现工作,不用“平台不同”豁免缺口。#3/#4 优先建立原版 Windows 本地终端基线;#54 现在冻结重复方法与完成端点并定位吞吐成本,数值预算作为提案交用户确认,不等到 #34 才补性能。 -3. **在主线推进独立行为合同。** #29 核对极窄/极短、软硬行、历史/淘汰、选择/Find、连续 resize 和 child ack;#30 核对 IME 组合/提交/取消与输出/resize/切换、应用键盘/粘贴接收、实际复制及同次八会话。已验收的 #39 只提供首批字素基础,最终配置的交叉验收仍需执行。 +3. **按原版合同补自主 C++ 能力。** #29 核对极窄/极短、软硬行、历史/淘汰、选择/Find、连续 resize 和 child ack;#30 核对 IME 组合/提交/取消与输出/resize/切换、应用键盘/粘贴接收、实际复制及同次八会话。已验收的 #39 提供的是参考实现与固定场景;自主 C++ 是否具备同等字素能力必须独立实现和验证。 4. **补终端模式和应用矩阵。** #27 明确 DECOM/DECAWM、pending-wrap、保存恢复、异常序列与 margin×resize;#28 先固定应用版本/入口/动作及协议字节预期,再逐应用完成进入、操作、resize、退出、主屏恢复。Vim 的一个修复不替代完整 TUI 验收。 5. **独立推进真实显示与可靠性。** #31 当前缺实际不同 DPI 环境;优先自动探测或准备隔离测试宿主,不改个人显示设置、不反复测试同两台 96-DPI 屏幕。#36/#40/#44 及[旧 normal-1 事件 #57](https://github.com/JavaZeroo/termforge/issues/57)按有停止条件的诊断批次推进,原失败和未知保留,不阻塞其他独立工作。 @@ -64,11 +63,11 @@ flowchart LR | 门槛与责任项 | 验收必须证明什么 | 不能代替验收的结果 | |---|---|---| | 回归可靠性:#36、#38(含 #40、#44、#57、#73、#80、#81、#85) | 持续输出中断、controller 子进程到 shell 的正常退出、原 10 秒纯夹具及保存/选择、四 shell 交接、hosted 准备和复制夹具失败路径均有明确阶段、字节、退出和自有 Job 回收证据;修复对应真实失败,最终声明配置的完整回归通过 | 多次重跑绿不能回填旧失败原因;close 后的退出码不能冒充自然退出;增加超时或跳过不能解除阻塞 | -| 终端模式:#27、#41 | 候选核心主/备用屏、各自滚动区域、origin/wrap、光标/样式、历史隔离及 resize 交叉符合固定合同;真实 ConPTY 路径保留实现这些语义所需的信息 | 独立核心 feed 正确不能补足宿主已经丢失的信息;不能猜测补发原运输中不存在的 VT,或用“已知平台差异”自行关闭功能缺口 | +| 终端模式:#27、#41 | 自主 C++ 核心主/备用屏、各自滚动区域、origin/wrap、光标/样式、历史隔离及 resize 交叉符合原版合同;实际固定 ConPTY 路径保留必要信息 | 历史候选 feed 正确不等于 C++ 已实现;末端不能猜测补回宿主已丢失的信息,重要原版差异仍待确认 | | resize 与真实 TUI:#29、#28 | 逻辑/物理行、宽字符、历史与淘汰有明确策略;窗口、核心和真实子进程尺寸一致;固定应用逐一完成进入、操作、resize、退出、主屏恢复;回复、输入模式和 bracketed paste 字节经独立预期验证 | 离线回放、单独 Vim 成功或默认核心通过,不能代表整个候选产品矩阵;未声明应用不计完成 | -| 复杂文字与输入:#30(复用已验 #39) | 保留首批字素合同,在最终配置核对原始标量、宽度/光标、实际 glyph、复制与子进程接收;完成固定 IME 与同次八会话交叉回归 | #39 的字素与单列 resize 成功不代表完整 IME、应用模式或 R1 系统输入组合在另一配置已通过;未知 WindTerm 行为仍保留 | +| 复杂文字与输入:#30(参考 #39) | 在自主 C++ 和 Qt 声明配置核对原始标量、字素边界、宽度/光标、实际 glyph、复制与子进程接收,完成固定 IME 与同次八会话交叉 | #39 的 Alacritty/DWrite 限定结果不代表新核心或 Qt 已具备对应能力;未知 WindTerm 行为继续调查 | | 真实显示:#31 | 按[字体/显示合同](R2-font-display.md)记录实际不同 DPI、真实跨屏、焦点、IME/Find/rename、命中、隐藏会话及子进程尺寸;环境身份和重复结果可核对 | 相同 96 DPI 的两台显示器、受控消息、DIB 或实际提交时刻不能代替不同 DPI 或实际上屏证据;不得修改个人显示设置制造验收 | -| 产品架构:#34 | 基于实际工作流、原版对标及 #23/#26/#32/#33/#54 的证据,重审 ADR-0003/0004;提出核心、GUI/平台、历史/快照、传输/呈现、背压/取消和维护方案,记录用户确认依据 | 原型已采用某技术、历史 issue 关闭或候选已有接口,不构成最终产品选择;未测性能和未确认容差保持未完成 | +| 产品架构:#34 | 按 ADR-0007/0008 已定路线,以实际工作流、原版对标及成本证据收敛核心/历史/GUI/传输接口、资源与维护边界;记录实际支持配置 | 路线已确认不等于功能、平台、长期资源或性能验收通过;重大替换、重要差异和数值门槛仍需用户确认 | | 开发版交付:#35 | 最终下载产物通过干净构建、依赖/字体许可和清单审计、解压运行、shell/TUI/文字/配置升级/真实显示工作流;按预先冻结的负载和资源合同完成重复会话与持续运行,逐项核对本表 | Alpha 1、源码构建成功、短驻留或某一 CI 通过,不代表 R2 日常开发包、长期稳定或 WindTerm 性能等价 | #32 本地 shell/分屏标签、#33 会话与配置恢复已有各自限定合同;最终集成仍须复验这些合同。R2 不扩大为 SSH/SFTP、任意嵌套分屏、完整 WindTerm UI 或跨平台客户端;这些任务继续按 PLAN 的对应路标推进。 diff --git a/experiments/conpty/backend.hpp b/experiments/conpty/backend.hpp new file mode 100644 index 00000000..3195dcea --- /dev/null +++ b/experiments/conpty/backend.hpp @@ -0,0 +1,132 @@ +// Explicit ConPTY API-family ownership; original implementation, MIT. +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tf_conpty { +class Backend { +public: + enum class GlyphWidth { Default, Console }; +private: + struct Api { + HMODULE module = nullptr; + decltype(&::CreatePseudoConsole) create = ::CreatePseudoConsole; + decltype(&::ResizePseudoConsole) resize = ::ResizePseudoConsole; + decltype(&::ClosePseudoConsole) close = ::ClosePseudoConsole; + ~Api() { if (module) FreeLibrary(module); } + }; + std::shared_ptr api_; + DWORD creation_flags_ = 0; + explicit Backend(std::shared_ptr api, DWORD flags = 0) + : api_(std::move(api)), creation_flags_(flags) {} + + // Keep both files open without write/delete sharing from verification + // through module load, so the checked pair cannot be replaced mid-load. + class File { + HANDLE handle_ = INVALID_HANDLE_VALUE; + public: + explicit File(const std::filesystem::path& path) { + handle_ = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle_ == INVALID_HANDLE_VALUE) throw std::runtime_error("ConPTY backend file unavailable"); + } + ~File() { if (handle_ != INVALID_HANDLE_VALUE) CloseHandle(handle_); } + File(const File&) = delete; + File& operator=(const File&) = delete; + HANDLE get() const { return handle_; } + }; + static std::string digest(HANDLE file) { + BCRYPT_ALG_HANDLE algorithm = nullptr; + BCRYPT_HASH_HANDLE hash = nullptr; + struct Cleanup { + BCRYPT_ALG_HANDLE& algorithm; + BCRYPT_HASH_HANDLE& hash; + ~Cleanup() { + if (hash) BCryptDestroyHash(hash); + if (algorithm) BCryptCloseAlgorithmProvider(algorithm, 0); + } + } cleanup{algorithm, hash}; + const auto check = [](NTSTATUS status) { + if (status < 0) throw std::runtime_error("ConPTY backend identity check failed"); + }; + check(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0)); + check(BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0)); + std::array buffer{}; + DWORD count = 0; + size_t total = 0; + for (;;) { + if (!ReadFile(file, buffer.data(), static_cast(buffer.size()), &count, nullptr)) + throw std::runtime_error("ConPTY backend identity read failed"); + if (!count) break; + total += count; + if (total > 8 * 1024 * 1024) throw std::runtime_error("ConPTY backend file too large"); + check(BCryptHashData(hash, buffer.data(), count, 0)); + } + std::array bytes{}; + check(BCryptFinishHash(hash, bytes.data(), static_cast(bytes.size()), 0)); + static constexpr char hex[] = "0123456789abcdef"; + std::string value; + for (const auto byte : bytes) { value += hex[byte >> 4]; value += hex[byte & 15]; } + return value; + } +public: + static Backend system() { + static const auto api = std::make_shared(); + return Backend(api); + } + static Backend reviewed_side_by_side(const std::filesystem::path& directory, + GlyphWidth width = GlyphWidth::Default) { + if (sizeof(void*) != 8 || !directory.is_absolute()) + throw std::runtime_error("ConPTY reviewed backend requires x64 and an absolute directory"); + const auto canonical = std::filesystem::canonical(directory); + const auto dll = canonical / L"conpty.dll"; + File dll_file(dll), host_file(canonical / L"OpenConsole.exe"); + if (digest(dll_file.get()) != "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a" || + digest(host_file.get()) != "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586") + throw std::runtime_error("ConPTY reviewed backend identity mismatch"); + auto api = std::make_shared(); + api->module = LoadLibraryExW(dll.c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); + if (!api->module) throw std::runtime_error("ConPTY reviewed backend load failed"); + api->create = reinterpret_castcreate)>(GetProcAddress(api->module, "ConptyCreatePseudoConsole")); + api->resize = reinterpret_castresize)>(GetProcAddress(api->module, "ConptyResizePseudoConsole")); + api->close = reinterpret_castclose)>(GetProcAddress(api->module, "ConptyClosePseudoConsole")); + if (!api->create || !api->resize || !api->close) + throw std::runtime_error("ConPTY reviewed backend API missing"); + wchar_t loaded[32768]{}; + const auto count = GetModuleFileNameW(api->module, loaded, 32768); + if (!count || count >= 32768 || !std::filesystem::equivalent(dll, loaded)) + throw std::runtime_error("ConPTY reviewed backend module identity differs"); + if (width != GlyphWidth::Default && width != GlyphWidth::Console) + throw std::runtime_error("ConPTY reviewed glyph width policy invalid"); + // Fixed official 1.25 conpty.h: GLYPH_WIDTH_CONSOLE=0x18. This is + // a console API compatibility policy, not the removed passthrough. + return Backend(std::move(api), width == GlyphWidth::Console ? 0x18 : 0); + } + static Backend configured() { +#ifdef TERMFORGE_CONPTY_SIDEBYSIDE + wchar_t image[32768]{}; + const auto count = GetModuleFileNameW(nullptr, image, 32768); + if (!count || count >= 32768) throw std::runtime_error("ConPTY backend executable location unavailable"); + return reviewed_side_by_side(std::filesystem::path(image).parent_path() / L"conpty-backend", + GlyphWidth::Console); +#else + return system(); +#endif + } + HRESULT create(COORD size, HANDLE input, HANDLE output, DWORD flags, HPCON* console) const { + if (creation_flags_ && (flags & 0x18) && (flags & 0x18) != creation_flags_) return E_INVALIDARG; + return api_->create(size, input, output, flags | creation_flags_, console); + } + HRESULT resize(HPCON console, COORD size) const { return api_->resize(console, size); } + void close(HPCON console) const { api_->close(console); } + bool side_by_side() const { return api_->module != nullptr; } + DWORD creation_flags() const { return creation_flags_; } +}; +} // namespace tf_conpty diff --git a/experiments/conpty/margin_probe.cpp b/experiments/conpty/margin_probe.cpp index 00bb91c6..cfdea50b 100644 --- a/experiments/conpty/margin_probe.cpp +++ b/experiments/conpty/margin_probe.cpp @@ -6,22 +6,8 @@ #include #include #include -static decltype(&CreatePseudoConsole) create_pc = CreatePseudoConsole; -static decltype(&ResizePseudoConsole) resize_pc = ResizePseudoConsole; -static decltype(&ClosePseudoConsole) close_pc = ClosePseudoConsole; static COORD selected_size{80,24}; -// Keep the selected API family together. HPCON is opaque: a DLL-created -// pseudoconsole must never be resized or closed by the operating system API. -static HRESULT WINAPI probe_create(COORD, HANDLE input, HANDLE output, DWORD flags, HPCON* console) { - return create_pc(selected_size, input, output, flags, console); -} -#define CreatePseudoConsole probe_create -#define ResizePseudoConsole resize_pc -#define ClosePseudoConsole close_pc #include "session.hpp" -#undef CreatePseudoConsole -#undef ResizePseudoConsole -#undef ClosePseudoConsole #include "../shell/workflow_fixture.hpp" static void require(bool ok, const char* msg) { if (!ok) throw std::runtime_error(msg); } static void save(const std::filesystem::path& p, const std::string& b) { @@ -78,37 +64,33 @@ int wmain(int argc, wchar_t** argv) { require(std::string(go,go_size) == "GO\n" || std::string(go,go_size) == "GO\r\n", "GO required"); BOOL in_job = FALSE; require(IsProcessInJob(GetCurrentProcess(), nullptr, &in_job) && in_job, "owned Job required"); - HMODULE backend = nullptr; + auto backend = tf_conpty::Backend::system(); if (std::wstring(argv[1]) != L"system") { const std::filesystem::path dll(argv[1]); require(dll.is_absolute() && dll.filename() == L"conpty.dll", "absolute conpty.dll path required"); - backend = LoadLibraryExW(argv[1], nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); - require(backend != nullptr, "backend load failed"); - create_pc = reinterpret_cast(GetProcAddress(backend, "ConptyCreatePseudoConsole")); - resize_pc = reinterpret_cast(GetProcAddress(backend, "ConptyResizePseudoConsole")); - close_pc = reinterpret_cast(GetProcAddress(backend, "ConptyClosePseudoConsole")); - require(create_pc && resize_pc && close_pc, "backend API absent"); + backend = tf_conpty::Backend::reviewed_side_by_side(dll.parent_path()); } wchar_t self[32768]{}; const DWORD length = GetModuleFileNameW(nullptr, self, 32768); require(length && length < 32768, "self path failed"); const auto dir = std::filesystem::absolute(argv[3]); std::filesystem::create_directories(dir); - tf_conpty::Session session; - session.start(self, L"--client " + std::wstring(argv[2]) + L" \"" + dir.wstring() + L"\""); + tf_conpty::Session session(backend); + session.start(self, L"--client " + std::wstring(argv[2]) + L" \"" + dir.wstring() + L"\"", {}, {}, selected_size); const bool waited = session.wait_exit(5000); + // Preserve available bytes even if closing an affected backend hangs + // and the outer owner subsequently has to terminate the process. + save(dir / "transport.prefix.bin", session.output()); session.close(); save(dir / "transport.bin", session.output()); - std::cout << std::boolalpha << "{\"waited\":" << waited + std::cout << std::boolalpha << "{\"waited\":" << waited << ",\"side_by_side\":" << backend.side_by_side() + << ",\"creation_flags\":" << session.backend_creation_flags() << ",\"exit_code\":" << session.exit_code << ",\"read_error\":" << session.read_error << ",\"capture_overflow\":" << session.capture_overflow << ",\"forced_reader_cancel\":" << session.forced_reader_cancel << ",\"forced_process_cancel\":" << session.forced_process_cancel << ",\"cleanup_complete\":" << session.cleanup_complete << ",\"active_children_after\":" << session.active_after << "}\n"; - // The Session destructor must run before unloading its selected ABI. - // The short-lived diagnostic process retains the module until exit. - (void)backend; return waited && session.exit_code == 37 && session.cleanup_complete ? 0 : 1; } catch (const std::exception& e) { std::cerr << e.what() << '\n'; return 2; } } diff --git a/experiments/conpty/session.hpp b/experiments/conpty/session.hpp index 6f607f17..a925c9f7 100644 --- a/experiments/conpty/session.hpp +++ b/experiments/conpty/session.hpp @@ -12,6 +12,7 @@ #include #include #include +#include "backend.hpp" namespace tf_conpty { inline void fail(const char* operation, DWORD error = GetLastError()) { @@ -38,6 +39,8 @@ class Handle { // The local shell supplies an external bounded streaming consumer and input // worker; the lifecycle probe uses bounded capture and its --child entry point. class Session { + // Keep the API module alive until the reader, HPCON and handles stop. + Backend backend_; Handle input_, output_, process_, job_; HPCON console_ = nullptr; std::thread reader_; @@ -53,17 +56,22 @@ class Session { unsigned long long output_bytes = 0; bool capture_overflow = false, forced_reader_cancel = false, forced_process_cancel = false; bool child_signaled = false, cleanup_complete = false; - Session() = default; + explicit Session(Backend backend = Backend::configured()) : backend_(std::move(backend)) {} Session(const Session&) = delete; Session& operator=(const Session&) = delete; ~Session() { close(); } + const char* backend_identity() const { + return backend_.side_by_side() ? "reviewed-side-by-side-1.25.260930003" : "system"; + } + DWORD backend_creation_flags() const { return backend_.creation_flags(); } void start(const std::wstring& executable, const std::wstring& arguments = L"--child", - std::function consume = {}, const std::wstring& working_directory = {}) { + std::function consume = {}, const std::wstring& working_directory = {}, + COORD initial_size = {80, 24}) { Handle child_input, child_output; if (!CreatePipe(child_input.put(), input_.put(), nullptr, 0)) fail("CreatePipe(input)"); if (!CreatePipe(output_.put(), child_output.put(), nullptr, 0)) fail("CreatePipe(output)"); - const HRESULT hr = CreatePseudoConsole({80, 24}, child_input.get(), child_output.get(), 0, &console_); + const HRESULT hr = backend_.create(initial_size, child_input.get(), child_output.get(), 0, &console_); if (FAILED(hr)) fail("CreatePseudoConsole", static_cast(hr)); child_input.reset(); child_output.reset(); reader_ = std::thread([this, consume = std::move(consume)] { @@ -155,7 +163,7 @@ class Session { } } void resize(short columns, short rows) { - const HRESULT hr = ResizePseudoConsole(console_, {columns, rows}); + const HRESULT hr = backend_.resize(console_, {columns, rows}); if (FAILED(hr)) fail("ResizePseudoConsole", static_cast(hr)); } bool wait_exit(unsigned timeout_ms, DWORD* observed_wait_result = nullptr, DWORD* observed_wait_error = nullptr) { @@ -173,7 +181,7 @@ class Session { void close() noexcept { if (cleanup_complete) return; input_.reset(); - if (console_) { ClosePseudoConsole(console_); console_ = nullptr; } + if (console_) { backend_.close(console_); console_ = nullptr; } if (process_.get()) { child_signaled = WaitForSingleObject(process_.get(), 5000) == WAIT_OBJECT_0; if (!child_signaled && job_.get()) { diff --git a/experiments/shell/results/2026-10-28-qt-cpp-entry/README.md b/experiments/shell/results/2026-10-28-qt-cpp-entry/README.md new file mode 100644 index 00000000..eacf9751 --- /dev/null +++ b/experiments/shell/results/2026-10-28-qt-cpp-entry/README.md @@ -0,0 +1,55 @@ +# 自主 C++ / Qt 最小本地会话入口 + +状态:**最小迁移入口限定检查通过;完整 R2 仍未验收**。本记录仅覆盖首个 C++/Qt 本地会话迁移入口,不宣布 R2、完整终端兼容、WindTerm 等价或可分发开发包通过。 + +入口按 [ADR-0008](../../../../docs/adr/0008-cpp-core-qt-widgets.md)使用自主 C++ 核心与 Qt Widgets,进程由[随包固定微软 ConPTY](../../../../third_party/conpty/README.md)承载。当前固定 80×24、单个本地 cmd、基本 ASCII 输入和 16 色绘制;原 Win32 工作区、历史浏览、选择/复制、Find、完整键盘/IME、动态 resize 与会话恢复尚未迁移。Alacritty 的历史、备用屏、字素及 TUI 结果不转记为本入口能力。 + +## 固定身份与当前终态 + +环境为 Windows 11 build 26200、x64 Release、Qt **6.10.3 / MSVC 2022 x64 qtbase**,动态链接 Core/Gui/Widgets,测试另用 Test;固定微软 ConPTY 为 **1.25.260930003**。候选适配器关闭。Qt 使用 **offscreen** 平台和本项目独立取得的 OFL Roboto Mono,`presented_done=null`。 + +| 检查点 | 结果 | 边界 | +|---|---|---| +| 初次整合提交 `9d9b506c0c268a304046a20d096018a821fd3d78` 的完整构建 | **失败** | 旧 margin 探针宏替换与显式 Backend 类型冲突,保留 `build-final.log`;不能写成初次整合全绿 | +| 后续提交 `0fd35e23095c611865c6970ba9a3b8b910ef1efa` 的完整构建 | **退出 0** | 复用已有显式 Backend 注入的探针实现,未修改原 175B 输入;保留 `build-final-fixed.log` | +| 最终源码完整 CTest | **47/47,退出 0,320.79 秒** | 新 Qt 检查 1 项与旧 system 诊断 46 项分列;内部 12 条跳过记录不能当通过 | + +[validation.json](validation.json)冻结所引用日志的哈希,并绑定最终构建本地 manifest。该 manifest 的 **340 项文件已重新核对全部匹配**,其中运行时目录文件 **91 项**,不把 `CMakeCache.txt` 算作运行时文件。公开 JSON 保留 34 项精选源码/依赖/运行时身份及完整 manifest 哈希,未冒充完整清单副本。 + +## 实际反例与分次验证 + +| 片段 | 观察与处理 | 结果的含义 | +|---|---|---| +| `interface-red` | 缺少 `terminal_widget.hpp`,构建退出 1 | **编译红**,不是已复现的运行故障 | +| `runtime-first` | 测试 35.03 秒超时,留存 CTest 输出没有子进程诊断文字 | 失败保留,单凭该记录不裁决根因 | +| `runtime-diagnostic` | 加日志后仍超时,报告缺少 offscreen 平台插件 | 暴露部署缺口,没有改变为通过 | +| `runtime-platform` | 补插件后 0.21 秒到达实际 cmd 路径,但正文墨迹断言失败,并报告缺字体 | 平台插件问题与字体/墨迹问题分别留证 | +| `runtime-font` | 部署独立 OFL 字体后局部 CTest **1/1**,测试 0.36 秒;外层 Job active=0、total=4 | 该早期版本只验证较小场景,不包含后来增加的启动错误和人工关闭检查 | +| `runtime-close-red` | 新增关闭窗口场景后,5.52 秒报告未回收正在运行的 Shell | 真正的运行红;外层后来 Job=0 不能把窗口关闭动作改判成功 | +| `runtime-close-green` | 增加 closeEvent 回收后,局部 CTest **1/1**,测试 0.47 秒;外层 Job active=0、total=7 | 实际覆盖退出、错误图像和关闭,但它早于最新监督器清理 P2 修正,不能替代最终源码完整矩阵 | + +局部 Qt 检查通过公开按键事件向真实 cmd 输入 `set /a 731+19`,独立期望为 **750**,并检查结果三格有新增墨迹;还检查正常退出 **37**、启动失败的错误文字墨迹,以及人工关闭后的回收。外层 Job 总数只是聚合计数,不据此推测每个进程的身份。这里列出的秒数是测试耗时,不是终端性能指标。 + +中间红记录的 HEAD 为 `4441248`,同时存在工作树实现变更;保留当时源码/二进制的文件哈希,但不把每个局部 green 都标为最终提交的结果。Standards/Spec 报告的 P2 修正目前来自根代理汇总,没有伪造独立审查日志;最终复核范围为 `4441248..0fd35e2`:Standards 无未解决硬问题/实质异味,Spec 无未解决新增问题;清理 P2 的最终源码已随完整 CTest 通过。 + +## 后端适配与部署负例 + +独立 Backend/Session 检查白名单摘要记录:缺失/篡改运行时明确拒绝、正常构建修复文件且不重新链接 EXE、篡改构建输入被拒绝。system 与 fixed Session 各自验证 ready/echo、resize、正常退出 37、EOF 109 与回收;它们是独立配置,不能合成 Qt 产品组合验收。原始 Session JSON 含进程标识,只保留本地索引、哈希和脱敏汇总。 + +最终源码还分别移除 offscreen 插件和 OFL 字体,均观察到明确拒绝及 Job active=0;测试二进制身份保持,文件已恢复。负例只证明这两个部署缺口可见,不证明完整运行库部署或干净机器使用已经通过。 + +[Qt 依赖 manifest](../../../../third_party/qt/manifest.json)区分原始官方 7z、aqtinstall 修补的 SDK 与实际部署文件身份。对应源码、第三方许可清单、替换条件及干净机器验证仍有待完成,当前不是可发布包。 + +## 复现与公开边界 + +配置与构建入口见 [src/qt/README.md](../../../../src/qt/README.md) 和 [Qt 构建依赖](../../../../third_party/qt/README.md)。局部命令为: + +```powershell +ctest --test-dir build/qt-cpp -C Release -R '^qt_cpp_shell$' --output-on-failure +``` + +完整 CTest 由根代理保留同一源码/配置的终态,本草稿不估计通过数。没有真实键盘、原生窗口上屏、不同 DPI、完整 IME/TUI、长期稳定或性能等价验收;受控图像结果不能替代用户实际看到画面的时间。 + +公开内容仅含独立代码对应的合成断言、汇总和哈希。本地 proof 路径是取证索引,不是公共附件;原始日志、截图、布局、PID、desktop 名、私人绝对路径与 WindTerm 私有材料均不随本记录发布。 + +最终完整运行另有 **12 条内部跳过记录**:4 个参考/可选依赖整类跳过、7 个实际系统剪贴板场景、1 个未启用的原版失败注入。新 Qt 入口的三类行为检查全部执行,外层 Job active=0、total=7、terminated=0;这些聚合计数不单独证明每个进程的身份。完整 47 项含独立旧配置,不能表述成新 Qt 产品的所有既有能力通过。 diff --git a/experiments/shell/results/2026-10-28-qt-cpp-entry/validation.json b/experiments/shell/results/2026-10-28-qt-cpp-entry/validation.json new file mode 100644 index 00000000..4a07c274 --- /dev/null +++ b/experiments/shell/results/2026-10-28-qt-cpp-entry/validation.json @@ -0,0 +1,916 @@ +{ + "schema": "termforge-qt-cpp-entry-validation-v1", + "status": "limited_migration_entry_verified_full_r2_incomplete", + "prepared_at_utc": "2026-10-28T11:41:35.216378+00:00", + "scope": "First C++/Qt local-shell migration entry only; not complete R2, full terminal compatibility, WindTerm equivalence or a distributable product package.", + "configuration": { + "host": "Windows-11-10.0.26200-SP0", + "architecture": "x64", + "build": "Release", + "core": "independent C++", + "gui": "Qt Widgets 6.10.3", + "qt_sdk": "windows desktop win64_msvc2022_64", + "backend": "bundled fixed Microsoft ConPTY 1.25.260930003", + "dimensions": { + "columns": 80, + "rows": 24 + }, + "qt_platform": "offscreen", + "font": "independently acquired OFL Roboto Mono", + "candidate_adapter": false, + "presented_done": null + }, + "source_identity": { + "initial_integration_commit": "9d9b506c0c268a304046a20d096018a821fd3d78", + "final_source_commit": "0fd35e23095c611865c6970ba9a3b8b910ef1efa", + "final_frozen_manifest": { + "path": "local/qt-cpp-first/final-build.json", + "sha256": "41530bb740924ae88a79b58a161a5082beda79c24acd3ac28ddc1d59eff74773", + "bytes": 53893 + }, + "frozen_file_count": 340, + "runtime_file_count": 91, + "files_rehashed_matching": 340, + "selected_file_identities": [ + { + "path": "assets/fonts/RobotoMono-Regular.ttf", + "sha256": "af0bff7599c3df3831755c16e39b3c496df74b8c8d8a1161b14dc8461be17cb4" + }, + { + "path": "cmake/ConptyBackend.cmake", + "sha256": "dcd670e4cec807c5f18e03b797e2abda623fe818dd9c0698c95186009af49300" + }, + { + "path": "cmake/CopyConptyBackend.cmake", + "sha256": "8bf50df01e302df91ee23a592b8b23d6a203606a35dadb86b63f4eed723dcb67" + }, + { + "path": "CMakeLists.txt", + "sha256": "8f1b8734f31b22fd1d4b94046843d93a732137208bd6500556b92e90fdfa7a81" + }, + { + "path": "experiments/conpty/backend.hpp", + "sha256": "5ee3ab95dc4b9aefce23ff7900cda2ecdd9cd09d1b1044074de2f5e572c31489" + }, + { + "path": "experiments/conpty/margin_probe.cpp", + "sha256": "9fff80b3eb0ace2cc6944ea8a99603c5e5a2b0150b82933d858ed667b0357002" + }, + { + "path": "experiments/conpty/session.hpp", + "sha256": "7b3934715948a05c1996bcccb02ef2eb58ea11baf26bf97c2e728a52d58becda" + }, + { + "path": "src/qt/main.cpp", + "sha256": "ad7d67383dedf3d4e237e89a4db48e1a84ec5140f372d04c33313196a7c36237" + }, + { + "path": "src/qt/run_smoke.py", + "sha256": "25f4d6adec4e9bfad77698300c44d9162511ac1ebb722993d5a52cdb8a863983" + }, + { + "path": "src/qt/shell_smoke.cpp", + "sha256": "23238dd99e81c3aec1128278c487da9699bdba79b75ab15175e4a1d5dfdacfcb" + }, + { + "path": "src/qt/terminal_session.hpp", + "sha256": "3cdb5bfa28019a5de9fdd92c9f976f0bf14899f2de324a8f783574a16492b2af" + }, + { + "path": "src/qt/terminal_widget.hpp", + "sha256": "e6bd2759601de942560dc367f5c25961c0ef08278a0e327c3321ebe142bf55b7" + }, + { + "path": "third_party/conpty/LICENSE", + "sha256": "ad0cf28f3381ca9bb0bf101d127402d44c17bfa0991e1a00bff7ae6679e9dada" + }, + { + "path": "third_party/conpty/manifest.json", + "sha256": "e75e522a3355c0a66c6d60cafa02413a83d8757262747f650b77c959c09ed439" + }, + { + "path": "third_party/qt/manifest.json", + "sha256": "6ed37661f6df751d1c29752a54fc88e4d5ddfa17a0a831bc52ab42d6ce87a1f4" + }, + { + "path": "build/qt-cpp/bin/Release/assets/fonts/RobotoMono-Regular.ttf", + "sha256": "af0bff7599c3df3831755c16e39b3c496df74b8c8d8a1161b14dc8461be17cb4" + }, + { + "path": "build/qt-cpp/qt-bin/Release/assets/fonts/RobotoMono-Regular.ttf", + "sha256": "af0bff7599c3df3831755c16e39b3c496df74b8c8d8a1161b14dc8461be17cb4" + }, + { + "path": "build/qt-cpp/qt-bin/Release/conpty-backend/conpty.dll", + "sha256": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a" + }, + { + "path": "build/qt-cpp/qt-bin/Release/conpty-backend/OpenConsole.exe", + "sha256": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + }, + { + "path": "build/qt-cpp/qt-bin/Release/platforms/qwindows.dll", + "sha256": "cafd5bfdef3d89d176ee26725c8d4759fd6cb007645b645a4cbf6386364a3f36" + }, + { + "path": "build/qt-cpp/qt-bin/Release/Qt6Core.dll", + "sha256": "b712b4754588e89f855dc0aa087d6304b2fd21b0e0639d1cdeb9cb969bd5fb72" + }, + { + "path": "build/qt-cpp/qt-bin/Release/Qt6Gui.dll", + "sha256": "03ba47a2025036a04f5548bce242788ac21edf551e78d5b42e1b87beb799904f" + }, + { + "path": "build/qt-cpp/qt-bin/Release/Qt6Widgets.dll", + "sha256": "61ddd0316a731b8dbabaf2924cd0d47b756461e37329b4f535902cf450416127" + }, + { + "path": "build/qt-cpp/qt-bin/Release/termforge_qt_shell.exe", + "sha256": "864494117b85c9cc4b1138fdfc9a989aede75ebbaa66e60739a94a8a0fe6a100" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/assets/fonts/RobotoMono-Regular.ttf", + "sha256": "af0bff7599c3df3831755c16e39b3c496df74b8c8d8a1161b14dc8461be17cb4" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/conpty-backend/conpty.dll", + "sha256": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/conpty-backend/OpenConsole.exe", + "sha256": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/platforms/qoffscreen.dll", + "sha256": "a35f87f597b91121cfc0ef39dcb0a5e7ad2876850fbfaf6377edf80eaa067bfd" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/platforms/qwindows.dll", + "sha256": "cafd5bfdef3d89d176ee26725c8d4759fd6cb007645b645a4cbf6386364a3f36" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/Qt6Core.dll", + "sha256": "b712b4754588e89f855dc0aa087d6304b2fd21b0e0639d1cdeb9cb969bd5fb72" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/Qt6Gui.dll", + "sha256": "03ba47a2025036a04f5548bce242788ac21edf551e78d5b42e1b87beb799904f" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/Qt6Test.dll", + "sha256": "9d0473552b332aa378c60da05af97d741fcce06b0fc4dd68e36d885015eb5187" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/Qt6Widgets.dll", + "sha256": "61ddd0316a731b8dbabaf2924cd0d47b756461e37329b4f535902cf450416127" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/qt_cpp_shell_smoke.exe", + "sha256": "6a793876899cb762f69592eda0f22bfe59058d7864ffabde275a7b489b37d727" + } + ], + "selection_note": "This is a selected identity list, not a full copy of the 340-file local manifest. The manifest itself is hashed and all listed files were rechecked. Runtime count excludes CMakeCache.txt.", + "intermediate_identity_note": "Earlier red records used HEAD 4441248 plus working-tree changes; recorded historical filesystem source/binary hashes do not identify each intermediate run as the final commit." + }, + "dependencies": { + "qt_manifest": { + "path": "third_party/qt/manifest.json", + "sha256": "6ed37661f6df751d1c29752a54fc88e4d5ddfa17a0a831bc52ab42d6ce87a1f4", + "bytes": 5378 + }, + "qt_application_modules": [ + "Core", + "Gui", + "Widgets" + ], + "qt_test_only_modules": [ + "Test" + ], + "qt_original_archive": { + "name": "qt.qt6.6103.win64_msvc2022_64", + "version": "6.10.3-0-202603310407", + "release_date": "2026-03-31", + "archive": "qtbase-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64.7z", + "url": "https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6103/qt6_6103/qt.qt6.6103.win64_msvc2022_64/6.10.3-0-202603310407qtbase-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64.7z", + "bytes": 42748757, + "sha1": "08bd6fea980b53485877f5048c725ca334c19130", + "sha1_matches_official_sidecar": true, + "sha256": "4db84dee7fe3c558f242bef0a88852613af76580dc6d2b24596479f47004dad7" + }, + "qt_installed_identity_note": "The archive SHA-256 identifies the untouched official 7z. It is not the hash of an aqt-patched SDK directory, qmake.exe, .prl file or deployed product.", + "qt_source_and_redistribution": { + "matching_source_archive_identified": true, + "matching_source_archive_downloaded_and_hash_verified": false, + "runtime_redistribution_review": "pending_before_product_distribution", + "clean_machine_validation": "pending" + }, + "conpty_manifest": { + "path": "third_party/conpty/manifest.json", + "sha256": "e75e522a3355c0a66c6d60cafa02413a83d8757262747f650b77c959c09ed439", + "bytes": 1467 + }, + "conpty_version": "1.25.260930003", + "conpty_package_sha256": "6d6f8b008c655d814c498d8205d4012dcd4d0b6dfc1096461d0a185d1e3caf03", + "conpty_file_sha256": { + "conpty.dll": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a", + "OpenConsole.exe": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + }, + "deployment_note": "Original Qt archive, aqtinstall-patched SDK files, deployed runtime files and product distribution completeness are distinct identities. No Qt or ConPTY binary is included here." + }, + "cases": [ + { + "id": "interface-red", + "kind": "compile_interface_missing", + "status": "failed_as_expected", + "exit_code": 1, + "message": "terminal_widget.hpp not implemented yet", + "record": { + "stage": "compile_interface_missing", + "exit_code": 1, + "message": "terminal_widget.hpp not implemented yet", + "source_head": "4441248fb9ad0b809f69aff070e5b6e5a28e0aa7", + "files": [ + { + "path": "CMakeLists.txt", + "sha256": "4ce50167d817df6da607f48e378b724e92e84eb6fab4bf7851baf49d121d94c6" + }, + { + "path": "src/qt/main.cpp", + "sha256": "274e0ca9f7db8c448ca2fb5769d3ad8dc7eb3d1aab8cf06c3789ebe9e7f345da" + }, + { + "path": "src/qt/shell_smoke.cpp", + "sha256": "72c02d81f53f7ca39206fc6e2ea54ef7642c69f40ac9e6d4af0941a0843148fe" + }, + { + "path": "experiments/conpty/session.hpp", + "sha256": "7b3934715948a05c1996bcccb02ef2eb58ea11baf26bf97c2e728a52d58becda" + }, + { + "path": "experiments/conpty/backend.hpp", + "sha256": "5ee3ab95dc4b9aefce23ff7900cda2ecdd9cd09d1b1044074de2f5e572c31489" + }, + { + "path": "local/qt-cpp-first/interface-red.log", + "sha256": "350278bb77147f35f3ea94d521bdafdbf7742b91e5669108d908cecfcdc38ac6" + } + ] + }, + "proofs": [ + "interface-red.json", + "interface-red.log" + ], + "meaning": "Compile red for a missing public widget header; not a reproduced runtime product bug." + }, + { + "id": "runtime-first", + "kind": "runtime_timeout", + "status": "failed", + "test_elapsed_seconds": 35.03, + "ctest_elapsed_seconds": 35.04, + "exit_code": null, + "child_diagnostic_text_in_ctest_log": false, + "proofs": [ + "runtime-first.log", + "first-timeout-last.log" + ], + "meaning": "Initial bounded run timed out without child diagnostic text in retained CTest output; this alone does not establish the cause." + }, + { + "id": "runtime-diagnostic", + "kind": "runtime_timeout_with_platform_diagnostic", + "status": "failed", + "test_elapsed_seconds": 35.02, + "ctest_elapsed_seconds": 35.04, + "exit_code": null, + "message": "Could not find the Qt platform plugin offscreen", + "proofs": [ + "runtime-diagnostic.log", + "build-diagnostic.log" + ], + "meaning": "Adding diagnostic output exposed the missing platform plugin; the original timeout remained." + }, + { + "id": "runtime-platform", + "kind": "runtime_assertion_failure", + "status": "failed", + "test_elapsed_seconds": 0.21, + "ctest_elapsed_seconds": 0.22, + "exit_code": null, + "message": "arithmetic answer cell has no new rendered ink", + "additional_diagnostic": "Qt font directory unavailable", + "proofs": [ + "runtime-platform.log", + "build-platform.log" + ], + "meaning": "After platform deployment the real cmd path reached the independent ink assertion, which failed without the font deployment." + }, + { + "id": "runtime-font", + "kind": "targeted_ctest", + "status": "passed", + "passed": 1, + "total": 1, + "test_elapsed_seconds": 0.36, + "ctest_elapsed_seconds": 0.37, + "supervisor": { + "schema": "termforge-qt-cpp-supervisor-v1", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "sample": { + "schema": "termforge-qt-cpp-smoke-v1", + "qt": "6.10.3", + "core": "cpp", + "backend": "fixed-microsoft-conpty", + "arithmetic": 750, + "child_exit": 37, + "cleanup_verified": true, + "image_changed": true, + "presented_done": null + } + }, + "proofs": [ + "runtime-font.log", + "first-pass-last.log", + "build-font.log" + ], + "meaning": "The independently acquired OFL font enabled offscreen output ink; this earlier test did not yet contain later startup-error/manual-close checks." + }, + { + "id": "runtime-close-red", + "kind": "runtime_window_lifecycle_counterexample", + "status": "failed_as_expected", + "test_elapsed_seconds": 5.52, + "ctest_elapsed_seconds": 5.53, + "ctest_exit_code": 8, + "message": "closing the Qt window did not reclaim its running shell", + "record": { + "stage": "runtime_window_close_did_not_reclaim_running_shell", + "exit_code": 8, + "source_head": "4441248fb9ad0b809f69aff070e5b6e5a28e0aa7", + "files": [ + { + "path": "src/qt/terminal_widget.hpp", + "sha256": "1eaa1ab1df298ee90741912d84225ce7f47163f8df61cd4eb54afa0d03161831" + }, + { + "path": "src/qt/terminal_session.hpp", + "sha256": "3cdb5bfa28019a5de9fdd92c9f976f0bf14899f2de324a8f783574a16492b2af" + }, + { + "path": "src/qt/shell_smoke.cpp", + "sha256": "23238dd99e81c3aec1128278c487da9699bdba79b75ab15175e4a1d5dfdacfcb" + }, + { + "path": "src/qt/run_smoke.py", + "sha256": "09643b83e82c094524304bffa20427c500a341bc121bd2955253d2ec2a49adf5" + }, + { + "path": "local/qt-cpp-first/runtime-close-red.log", + "sha256": "4e73115a00ec565540334ee0bd8f153c087f5f8e5b932fb8b5b007198f80e137" + }, + { + "path": "build/qt-cpp/qt-smoke-bin/Release/qt_cpp_shell_smoke.exe", + "sha256": "370d907e172422d8d887e43d78634b609bd9ce84f60598f22ac0e76809750d29" + } + ] + }, + "supervisor": { + "status": "failed", + "timed_out": false, + "exit_code": 1, + "job_after": { + "active": 0, + "total": 7, + "terminated": 0 + } + }, + "proofs": [ + "runtime-close-red.log", + "close-red.json", + "build-close-red.log" + ], + "meaning": "Public close behavior failed although later outer cleanup reclaimed processes. Job0 does not make the public action pass." + }, + { + "id": "runtime-close-green", + "kind": "targeted_ctest", + "status": "passed", + "passed": 1, + "total": 1, + "test_elapsed_seconds": 0.47, + "ctest_elapsed_seconds": 0.48, + "supervisor": { + "schema": "termforge-qt-cpp-supervisor-v1", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 7, + "terminated": 0 + }, + "sample": { + "schema": "termforge-qt-cpp-smoke-v1", + "qt": "6.10.3", + "core": "cpp", + "backend": "fixed-microsoft-conpty", + "arithmetic": 750, + "child_exit": 37, + "cleanup_verified": true, + "image_changed": true, + "startup_error_visible": true, + "manual_close_reclaimed": true, + "presented_done": null + } + }, + "proofs": [ + "runtime-close-green.log", + "close-green-last.log", + "build-close-green.log" + ], + "meaning": "closeEvent cleanup passed natural exit, visible startup-error ink and manual-close checks. Aggregate Job totals are not used to infer individual process identities." + } + ], + "whole_build_checkpoints": [ + { + "source_commit": "9d9b506c0c268a304046a20d096018a821fd3d78", + "status": "failed", + "stage": "complete_build", + "diagnostic": "Existing margin_probe macro substitution conflicted with explicit Backend API types (C2440/C2439/C2064).", + "proof": "build-final.log", + "meaning": "The initial frozen integration commit did not pass the complete build." + }, + { + "source_commit": "0fd35e23095c611865c6970ba9a3b8b910ef1efa", + "status": "passed", + "terminal_exit_code": 0, + "followup": "Reuse the existing explicit-Backend-injection margin_probe implementation; original 175-byte fixture unchanged.", + "proof": "build-final-fixed.log", + "terminal_result_source": "Parent message after build returned, corroborated by frozen build log and final source/runtime manifest." + } + ], + "package_negative_checks": { + "source_commit": "0fd35e23095c611865c6970ba9a3b8b910ef1efa", + "binary_sha256": "6a793876899cb762f69592eda0f22bfe59058d7864ffabde275a7b489b37d727", + "cases": [ + { + "case": "missing platforms/qoffscreen.dll", + "wrapper_exit": 1, + "native_exit": 2, + "job_after": { + "active": 0, + "total": 2, + "terminated": 0 + }, + "expected_error_seen": true + }, + { + "case": "missing assets/fonts/RobotoMono-Regular.ttf", + "wrapper_exit": 1, + "native_exit": 1, + "job_after": { + "active": 0, + "total": 2, + "terminated": 0 + }, + "expected_error_seen": true + } + ], + "all_files_restored": true + }, + "final_ctest": { + "status": "passed", + "source_commit": "0fd35e23095c611865c6970ba9a3b8b910ef1efa", + "terminal_exit_code": 0, + "passed": 47, + "failed": 0, + "skipped": 0, + "total": 47, + "elapsed_seconds": 320.79, + "command": "ctest --test-dir build/qt-cpp -C Release --output-on-failure", + "environment": { + "PYTHONUTF8": "1", + "vim": "existing Git Vim added to this test process PATH" + }, + "internal_skip_record_count": 12, + "internal_skipped": [ + "setUpClass (test_candidate_compare.CandidateComparisonTests) ... skipped 'optional Alacritty binary is not built; core-candidate workflow runs these checks'", + "setUpClass (test_capability_matrix.CapabilityMatrixTests) ... skipped 'optional Alacritty binary is not built; core-candidate workflow runs these checks'", + "setUpClass (test_fuzz_matrix.FuzzMatrixTests) ... skipped 'optional Alacritty binary is not built; core-candidate workflow runs these checks'", + "setUpClass (test_real_app_matrix.RealApplicationMatrixTests) ... skipped 'optional real application capture dependencies are not built'", + "test_actual_system_copy_paste_and_shortcuts_twice (experiments.shell.test_actions.ActionWindowTest.test_actual_system_copy_paste_and_shortcuts_twice) ... skipped 'actual system Copy/Paste requires a disposable hosted VM'", + "test_suspended_target_is_reclaimed_if_job_assignment_fails (test_ui_r1.EvidenceBoundaryTests.test_suspended_target_is_reclaimed_if_job_assignment_fails) ... skipped 'opt-in pinned reference failure injection'", + "test_mouse_selection_to_real_system_clipboard (experiments.shell.test_shell.ShellTests.test_mouse_selection_to_real_system_clipboard) ... skipped 'system copy UI gate requires a disposable hosted VM'", + "test_system_clipboard_product_read_errors_cancel_and_close (experiments.shell.test_shell.ShellTests.test_system_clipboard_product_read_errors_cancel_and_close) ... skipped 'product clipboard read gate requires a disposable hosted VM'", + "test_system_clipboard_shortcut_to_window_confirmation_and_receiver (experiments.shell.test_shell.ShellTests.test_system_clipboard_shortcut_to_window_confirmation_and_receiver) ... skipped 'real system clipboard UI gate requires a disposable hosted VM'", + "test_owned_clipboard_contention_preserves_write_and_read_failures (experiments.shell.test_selection_bytes.SelectionBytesWindowTest.test_owned_clipboard_contention_preserves_write_and_read_failures) ... skipped 'system CF requires a disposable hosted VM'", + "test_real_system_copy_shortcuts_for_styled_fixture_twice (experiments.shell.test_selection_bytes.SelectionBytesWindowTest.test_real_system_copy_shortcuts_for_styled_fixture_twice) ... skipped 'system CF requires a disposable hosted VM'", + "test_real_system_copy_and_edit_context_twice (experiments.shell.test_selection_context.SelectionContextWindowTest.test_real_system_copy_and_edit_context_twice) ... skipped 'real CF/EDIT Copy requires a disposable hosted VM'" + ], + "internal_skip_summary": "4 class-level optional/reference dependency skips, 7 actual system clipboard cases, 1 opt-in original-reference failure injection", + "log": { + "path": "local/qt-cpp-first/ctest-final.log", + "sha256": "8823c21b21cd885159f6736e0f3885ce5d2891c2e7c32d326a19a51aef2978f9", + "bytes": 5152 + }, + "detailed_log": { + "path": "local/qt-cpp-first/last-test-final.log", + "sha256": "2f9a583edb3d4da498b3eb09680bda96136903906ca3fc5094144d662e3abffb", + "bytes": 125992 + }, + "configuration_boundary": "47 CTest entries comprise 1 new Qt/C++/fixed-ConPTY check and 46 legacy system diagnostic entries. Their independent successes are not joint product acceptance. Elapsed time is test-suite duration, not terminal performance." + }, + "conpty_adapter_checks": { + "configuration": "Windows x64 Release; explicit fixed Microsoft ConPTY; standalone public Backend/Session checks", + "source_files": [ + { + "path": "experiments/conpty/backend.hpp", + "sha256": "5ee3ab95dc4b9aefce23ff7900cda2ecdd9cd09d1b1044074de2f5e572c31489" + }, + { + "path": "experiments/conpty/session.hpp", + "sha256": "7b3934715948a05c1996bcccb02ef2eb58ea11baf26bf97c2e728a52d58becda" + }, + { + "path": "cmake/ConptyBackend.cmake", + "sha256": "dcd670e4cec807c5f18e03b797e2abda623fe818dd9c0698c95186009af49300" + }, + { + "path": "cmake/CopyConptyBackend.cmake", + "sha256": "8bf50df01e302df91ee23a592b8b23d6a203606a35dadb86b63f4eed723dcb67" + }, + { + "path": "third_party/conpty/manifest.json", + "sha256": "e75e522a3355c0a66c6d60cafa02413a83d8757262747f650b77c959c09ed439" + }, + { + "path": "third_party/conpty/LICENSE", + "sha256": "ad0cf28f3381ca9bb0bf101d127402d44c17bfa0991e1a00bff7ae6679e9dada" + } + ], + "bundle_checks": [ + { + "case": "tampered-runtime", + "exit_code": 2 + }, + { + "case": "repair-tampered", + "exit_code": 0 + }, + { + "case": "repair-tampered-identity", + "exit_code": 0 + }, + { + "case": "missing-runtime", + "exit_code": 2 + }, + { + "case": "repair-missing", + "exit_code": 0 + }, + { + "case": "repair-missing-identity", + "exit_code": 0 + }, + { + "case": "tampered-build-input", + "exit_code": 1 + } + ], + "repair_did_not_relink_exe": true, + "limitations": [ + "These checks do not validate Qt, GUI, IME, Unicode, TUI or product performance.", + "System/fixed Session results are independent configurations, not joint acceptance." + ], + "host": "Windows-11-10.0.26200-SP0", + "session_checks": [ + { + "configuration": "system", + "binary_sha256": "b35a89caf42848b8f57fd4240225f7179e41f0c94c20b54da3b9020bbd37ce57", + "status": "passed", + "exit_code": 0, + "timed_out": false, + "outer_job_active": 0, + "outer_job_total": 6, + "outer_job_terminated": 0, + "sample": { + "valid": true, + "ready": 1, + "echo": 1, + "resized": 1, + "child_signaled": 1, + "exit_code": 37, + "read_error": 109, + "active_children_after": 0, + "forced_reader_cancel": 0, + "forced_process_cancel": 0, + "handles_before": 58, + "handles_after": 58, + "threads_before": 4, + "threads_after": 4 + } + }, + { + "configuration": "fixed", + "binary_sha256": "97b9d054c44e70bca3ae7f9fecba0575fea31dc82d3edff1f7ed728f38e6e366", + "status": "passed", + "exit_code": 0, + "timed_out": false, + "outer_job_active": 0, + "outer_job_total": 6, + "outer_job_terminated": 0, + "sample": { + "valid": true, + "ready": 1, + "echo": 1, + "resized": 1, + "child_signaled": 1, + "exit_code": 37, + "read_error": 109, + "active_children_after": 0, + "forced_reader_cancel": 0, + "forced_process_cancel": 0, + "handles_before": 70, + "handles_after": 70, + "threads_before": 4, + "threads_after": 4 + } + } + ], + "proof_files": [ + { + "path": "local/conpty-adapter-checks/session-system.json", + "sha256": "4e83ffddc98a5cca2329fdc8890a3a99cd99b9e1602a387da06a5f48768d7d06" + }, + { + "path": "local/conpty-adapter-checks/session-fixed.json", + "sha256": "d7b90016b8fb8f067cadc914e392e7b78e79e71799cb2aedf52e57b873ee76ef" + }, + { + "path": "local/conpty-adapter-checks/session-build.log", + "sha256": "d6620bc91e6b6026487eb39c7b79fc90e7d409355f317e712ee9fc52324efee3" + }, + { + "path": "local/conpty-adapter-checks/session-configure.log", + "sha256": "b10bcc916f31c1761986eba9dde6ec38ad1d43973a1608081e65b1ce3e015521" + }, + { + "path": "local/conpty-adapter-checks/identity-green.log", + "sha256": "c8dee4a7387f588240dae8019b6923f181c00683171028ca7b9685967857d5e1" + }, + { + "path": "local/conpty-adapter-checks/tampered-runtime.stderr.log", + "sha256": "1966dbc4d5e765941327bd11a96898ad1fcf0cb44f57a3e30a3f4dceb01b79c5" + }, + { + "path": "local/conpty-adapter-checks/missing-runtime.stderr.log", + "sha256": "307d9cf050e456216ed8cfba237f06318d8b010b56f34cf902bb31494d7aeba6" + }, + { + "path": "local/conpty-adapter-checks/tampered-build-input.stderr.log", + "sha256": "61d93fb323af97dc3abb11b30753581689ee36abdfb1981f3165f56f58173d58" + } + ] + }, + "conpty_adapter_proof_hash_checks": [ + { + "path": "local/conpty-adapter-checks/session-system.json", + "matches_recorded_sha256": true + }, + { + "path": "local/conpty-adapter-checks/session-fixed.json", + "matches_recorded_sha256": true + }, + { + "path": "local/conpty-adapter-checks/session-build.log", + "matches_recorded_sha256": true + }, + { + "path": "local/conpty-adapter-checks/session-configure.log", + "matches_recorded_sha256": true + }, + { + "path": "local/conpty-adapter-checks/identity-green.log", + "matches_recorded_sha256": true + }, + { + "path": "local/conpty-adapter-checks/tampered-runtime.stderr.log", + "matches_recorded_sha256": true + }, + { + "path": "local/conpty-adapter-checks/missing-runtime.stderr.log", + "matches_recorded_sha256": true + }, + { + "path": "local/conpty-adapter-checks/tampered-build-input.stderr.log", + "matches_recorded_sha256": true + } + ], + "review": { + "status": "completed_no_open_new_findings", + "base_commit": "4441248fb9ad0b809f69aff070e5b6e5a28e0aa7", + "head_commit": "0fd35e23095c611865c6970ba9a3b8b910ef1efa", + "standards": { + "open_hard_findings": 0, + "open_substantive_smells": 0, + "fixed_findings": [ + "GUI startup error visibility", + "supervisor cleanup must preserve original failure and close Job" + ] + }, + "spec": { + "open_findings": 0, + "fixed_findings": [ + "GUI startup error visibility", + "outer Job must contain Qt root and ConPTY process tree before GO" + ] + }, + "additional_delta": "Explicit-Backend margin-probe integration reviewed on both axes with no new findings. Window-close runtime red was fixed and passes in the final full run.", + "record_source": "Root summary of completed independent Standards/Spec agent messages; no standalone review-log artifact claimed." + }, + "proofs": [ + { + "path": "local/qt-cpp-first/interface-red.json", + "sha256": "2a9cc774a5ef30f28468eb8c56e9f7d885b067dd4dc727f376b8abd10801a2f6", + "bytes": 1058 + }, + { + "path": "local/qt-cpp-first/interface-red.log", + "sha256": "350278bb77147f35f3ea94d521bdafdbf7742b91e5669108d908cecfcdc38ac6", + "bytes": 458 + }, + { + "path": "local/qt-cpp-first/configure.log", + "sha256": "4b2a79164f0aa3748f1c6c14097d5df51359e23b0d849498092d5e7b0f452634", + "bytes": 1222 + }, + { + "path": "local/qt-cpp-first/build-green.log", + "sha256": "947385fb5e31ae377779e1fb891b6766b42810fd40c9b03c0ff3682ced5f413f", + "bytes": 5830 + }, + { + "path": "local/qt-cpp-first/runtime-first.log", + "sha256": "9a964712e78378947e90dccbc05a0fd5e02e90b5d7244544ac8af867918eed02", + "bytes": 411 + }, + { + "path": "local/qt-cpp-first/first-timeout-last.log", + "sha256": "7d2ad8f500e056daddea7abdff4816da179180a385687e3387290086deb0b135", + "bytes": 738 + }, + { + "path": "local/qt-cpp-first/build-diagnostic.log", + "sha256": "ff1b2570258e2e10cf261adc78b63b09a4c643968cc5d7123866b7047d047a80", + "bytes": 2521 + }, + { + "path": "local/qt-cpp-first/runtime-diagnostic.log", + "sha256": "152dd1b4163f6553c916743264466cd5608fc745e0a0a9c6f5aefcec4667ec97", + "bytes": 492 + }, + { + "path": "local/qt-cpp-first/build-platform.log", + "sha256": "c2da55476e1924d0a03c7ce7513ee6b90e9dc99bc58f7c699872a7792faa5599", + "bytes": 3550 + }, + { + "path": "local/qt-cpp-first/runtime-platform.log", + "sha256": "71d2450000160e2a3c4d3f7917aa2c2bf2bc8b8d1549ad8059b0694d54416fb4", + "bytes": 736 + }, + { + "path": "local/qt-cpp-first/build-font.log", + "sha256": "fea253252d73cdd8303653f30fc1344a125fdf44649d72bc4e996b7ca08e4c9c", + "bytes": 6091 + }, + { + "path": "local/qt-cpp-first/runtime-font.log", + "sha256": "cbb300a4dcf9ee2f7d4dcc2655380fe07eba252a9d2d79878d360570cd751924", + "bytes": 321 + }, + { + "path": "local/qt-cpp-first/first-pass-last.log", + "sha256": "9f4533026c8e034bc326bb4a118755a5517cf533d6c90bd3e55139e0ab33fc51", + "bytes": 1258 + }, + { + "path": "local/qt-cpp-first/close-red.json", + "sha256": "e4ecbd46ec0fc2d46f1d08da3f56221fafb1eb7f68b86905bd760a0981e2bdc8", + "bytes": 1069 + }, + { + "path": "local/qt-cpp-first/build-close-red.log", + "sha256": "66e2f550c9f39231210faec85ed588acd097fde93a43a88905349fd2099f555c", + "bytes": 2833 + }, + { + "path": "local/qt-cpp-first/runtime-close-red.log", + "sha256": "4e73115a00ec565540334ee0bd8f153c087f5f8e5b932fb8b5b007198f80e137", + "bytes": 722 + }, + { + "path": "local/qt-cpp-first/build-close-green.log", + "sha256": "7e11225ccedc7106015dbfb82d591ecc3994bac6c91317158285518e44c9d2ae", + "bytes": 5267 + }, + { + "path": "local/qt-cpp-first/runtime-close-green.log", + "sha256": "2c396d004054be5fa1821360cf7b2a7da329bfdfdf9361f4c3e753c0de4d9ede", + "bytes": 321 + }, + { + "path": "local/qt-cpp-first/close-green-last.log", + "sha256": "b6166a283242088cb66142a546c9fa716863a83f39e247b6478b892a3092a6ef", + "bytes": 1321 + }, + { + "path": "local/qt-cpp-first/build-final.log", + "sha256": "af2c5d3dabbb4771335de95de755cbebb73d7d6459801b19d3cecf80a054118c", + "bytes": 16041 + }, + { + "path": "local/qt-cpp-first/build-final-fixed.log", + "sha256": "39408facbff5fe39f756b61a8433f785ab8cff86e27be1d6cba184b3af6ac958", + "bytes": 9706 + }, + { + "path": "local/qt-cpp-first/package-negatives.json", + "sha256": "d0d664e2682b4737de84184d74629020c8d965154c7559ad8e40ba719677e2a8", + "bytes": 722 + }, + { + "path": "local/conpty-adapter-checks/adapter-summary.json", + "sha256": "188a3b3dc2f7ac6171204e25236b99e22c042cba000bc1c307b5c57cd579f405", + "bytes": 4734 + }, + { + "path": "local/qt-cpp-first/ctest-final.log", + "sha256": "8823c21b21cd885159f6736e0f3885ce5d2891c2e7c32d326a19a51aef2978f9", + "bytes": 5152 + }, + { + "path": "local/qt-cpp-first/last-test-final.log", + "sha256": "2f9a583edb3d4da498b3eb09680bda96136903906ca3fc5094144d662e3abffb", + "bytes": 125992 + } + ], + "historical_proof_hash_checks": [ + { + "record": "compile_interface_missing", + "path": "local/qt-cpp-first/interface-red.log", + "matches_recorded_sha256": true + }, + { + "record": "runtime_window_close_did_not_reclaim_running_shell", + "path": "local/qt-cpp-first/runtime-close-red.log", + "matches_recorded_sha256": true + } + ], + "limitations": [ + "Qt offscreen is an image/control-path check, not physical keyboard interaction, native onscreen display, compositor presentation or different-DPI validation.", + "This entry is fixed 80x24 local cmd with basic ASCII input and the declared 16-color rendering path; full R2 input, IME, dynamic resize, workspace, history browsing, selection/clipboard and Find are not accepted here.", + "Arithmetic 750, answer-cell ink, child exit 37, startup-error ink and manual-close reclamation are bounded synthetic assertions, not general TUI or Unicode acceptance.", + "C++ core limits remain; Alacritty history, alternate-screen, grapheme and TUI results are not inherited.", + "System/fixed ConPTY Session checks are independent configurations; standalone adapter checks do not validate Qt or the whole product combination.", + "Elapsed times are test-suite durations, not terminal performance benchmarks. No numeric tolerance or WindTerm performance equivalence is established.", + "Qt source/notice/replacement-condition review, full deployed third-party license inventory, clean-machine completeness and product packaging remain pending.", + "Earlier failures/timeouts/greens stay separate; intermediate greens do not replace the final-source complete matrix." + ], + "privacy": { + "included": "Whitelisted synthetic assertions, relative repository/local proof references, hashes, source/configuration identities and aggregate cleanup fields.", + "excluded": "Raw logs/screenshots, layouts, private absolute paths, PID values, desktop names, credentials, personal sessions and proprietary WindTerm material." + }, + "final_qt_check": { + "schema": "termforge-qt-cpp-supervisor-v1", + "status": "passed", + "timed_out": false, + "exit_code": 0, + "job_after": { + "active": 0, + "total": 7, + "terminated": 0 + }, + "sample": { + "schema": "termforge-qt-cpp-smoke-v1", + "qt": "6.10.3", + "core": "cpp", + "backend": "fixed-microsoft-conpty", + "arithmetic": 750, + "child_exit": 37, + "cleanup_verified": true, + "image_changed": true, + "startup_error_visible": true, + "manual_close_reclaimed": true, + "presented_done": null + } + } +} diff --git a/src/qt/README.md b/src/qt/README.md new file mode 100644 index 00000000..bb1150cf --- /dev/null +++ b/src/qt/README.md @@ -0,0 +1,30 @@ +# C++ / Qt 本地会话迁移入口 + +按 [ADR-0008](../../docs/adr/0008-cpp-core-qt-widgets.md)采用自主 C++ 核心和 Qt Widgets,Windows 进程由[固定微软 ConPTY](../../third_party/conpty/README.md)承载。当前入口仅打通固定 80×24 的单个本地 cmd、基本 ASCII 按键、16 色绘制、启动错误与关闭回收;不是完整 R2 或 WindTerm 界面。 + +现有 C++ 核心的[能力限制](../../experiments/core/README.md)继续有效。Qt 尚未迁移原 Win32 的工作区、历史浏览、选择/复制、Find、完整键盘/IME、动态 resize 或会话恢复,也不能继承 Alacritty 分支的通过结果。 + +## 构建与运行 + +按 [Qt 依赖说明](../../third_party/qt/README.md)准备 Qt 6.10.3 / MSVC 2022 x64 qtbase 和固定 ConPTY 文件对: + +```powershell +cmake -S . -B build/qt-cpp -G "Visual Studio 17 2022" -A x64 ` + -DTERMFORGE_BUILD_QT_SHELL=ON ` + -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF ` + -DQt6_DIR="/lib/cmake/Qt6" ` + -DTERMFORGE_CONPTY_BACKEND_DIR="" +cmake --build build/qt-cpp --config Release --target termforge_qt_shell qt_cpp_shell_smoke +build/qt-cpp/qt-bin/Release/termforge_qt_shell.exe +ctest --test-dir build/qt-cpp -C Release -R '^qt_cpp_shell$' --output-on-failure +``` + +产品入口缺少固定包时明确报错;启动失败保留可关闭的错误窗口。随目录保留 Qt 动态运行库、平台插件、`conpty-backend/` 与 `assets/fonts/`。当前只是本地构建输出,Qt/第三方与运行库的完整分发审计、干净机器验证及产品打包尚未完成。 + +## 独立验证面 + +Parser/Screen 仅在 Qt 线程修改;ConPTY reader 向有界输出队列写入,输入由独立有界 worker 发送。自然退出等待子进程、reader EOF、输出排空和 Parser.finish;人工关闭按取消处理。外层监督器在 GO 前把测试根进程纳入自有 Job,超时或残留不能通过。 + +`qt_cpp_shell` 通过 Qt 按键事件向真实 cmd 输入 `set /a 731+19`,检查独立字面量 `750` 与正文三格新增墨迹,再检查自然退出 37、启动失败的可见错误及人工关闭。该测试使用 Qt offscreen 平台和本项目独立取得的 OFL Roboto Mono 字体;没有物理键盘、实际不同 DPI、Windows 原生窗口上屏或用户呈现时间验收,`presented_done=null`。测试耗时不是终端性能指标。 + +构建开关默认关闭只是从旧原型向新入口迁移的状态;正式路线已确认,旧构建保留为回归对照。Qt 文字与图形资源均独立取得,不使用 WindTerm 的专有字体、图标或主题。 diff --git a/src/qt/main.cpp b/src/qt/main.cpp new file mode 100644 index 00000000..4e790177 --- /dev/null +++ b/src/qt/main.cpp @@ -0,0 +1,34 @@ +#include "terminal_widget.hpp" +#include +#include +#include +#include + +int main(int argc, char** argv) { + QApplication app(argc, argv); + QApplication::setApplicationName(QStringLiteral("TermForge")); + try { + termforge::qt::SessionModel model; + termforge::qt::TerminalWidget window(model); + bool launched = false; + window.setWindowTitle(QStringLiteral("TermForge — local shell")); + model.on_changed = [&] { + window.update(); + if (launched && model.finished()) app.exit(model.natural_exit() ? static_cast(model.exit_code()) : 1); + }; + wchar_t directory[MAX_PATH]{}; + const UINT length = GetSystemDirectoryW(directory, MAX_PATH); + if (!length || length >= MAX_PATH) throw std::runtime_error("GetSystemDirectory failed"); + window.show(); + launched = model.start(std::wstring(directory) + L"\\cmd.exe", L"/D", QDir::homePath().toStdWString()); + if (!launched) { + window.setWindowTitle(QStringLiteral("TermForge — shell startup failed")); + window.update(); // Keep the error visible until the user closes it. + } + return app.exec(); + } catch (const std::exception& error) { + qCritical("%s", error.what()); + QMessageBox::critical(nullptr, QStringLiteral("TermForge startup failed"), QString::fromUtf8(error.what())); + return 1; + } +} diff --git a/src/qt/run_smoke.py b/src/qt/run_smoke.py new file mode 100644 index 00000000..31264210 --- /dev/null +++ b/src/qt/run_smoke.py @@ -0,0 +1,102 @@ +"""Run the public Qt smoke under an owned Job before any ConPTY is created.""" +import json +import os +from pathlib import Path +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +from experiments.conpty.run import OwnedJob + + +def main(): + binary = Path(sys.argv[1]).resolve(strict=True) + job, process, assigned = OwnedJob(), None, False + result = {"schema": "termforge-qt-cpp-supervisor-v1", "status": "failed"} + cleanup_errors = [] + + def root_running(): + try: + return process is not None and process.poll() is None + except Exception as error: + cleanup_errors.append("query root: " + str(error)) + return process is not None + try: + environment = os.environ.copy() + environment["QT_QPA_PLATFORM"] = "offscreen" + environment["QT_QPA_FONTDIR"] = str(binary.parent / "assets/fonts") + process = subprocess.Popen([str(binary)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=environment, + creationflags=subprocess.CREATE_NO_WINDOW) + # The native smoke reads this bounded gate before constructing Qt. + job.assign(process.pid) + assigned = True + try: + stdout, stderr = process.communicate(b"GO\n", timeout=25) + result["timed_out"] = False + except subprocess.TimeoutExpired: + result["timed_out"] = True + result["job_at_timeout"] = job.accounting() + job.terminate() + stdout, stderr = process.communicate(timeout=5) + result.update(exit_code=process.returncode, job_after=job.drain(timeout=3)) + if stderr: + sys.stderr.write(stderr.decode("utf-8", "replace")) + if result["timed_out"] or process.returncode or result["job_after"]["active"]: + raise RuntimeError("Qt smoke failed or left owned processes active") + if result["job_after"]["total"] < 3: + raise RuntimeError("Qt smoke did not observe its root, cmd and ConPTY host") + result["sample"] = json.loads(stdout.decode("utf-8")) + result["status"] = "passed" + except Exception as error: + result["error"] = str(error) + finally: + # Preserve natural Job accounting above; forced cleanup cannot pass. + try: + if root_running(): + if assigned: + try: + job.terminate() + except Exception as error: + cleanup_errors.append("terminate Job: " + str(error)) + # Also reclaims the pre-GO root when assignment failed. + try: + if root_running(): + process.kill() + except Exception as error: + cleanup_errors.append("terminate root: " + str(error)) + try: + process.communicate(timeout=5) + except Exception as error: + cleanup_errors.append("drain root: " + str(error)) + try: + remaining = job.accounting() + except Exception as error: + remaining = None + cleanup_errors.append("query Job: " + str(error)) + if remaining and remaining["active"]: + try: + job.terminate() + except Exception as error: + cleanup_errors.append("terminate remaining Job: " + str(error)) + try: + if job.drain(timeout=3)["active"]: + cleanup_errors.append("owned processes remain active") + except Exception as error: + cleanup_errors.append("drain Job: " + str(error)) + finally: + try: + job.close() + except Exception as error: + cleanup_errors.append("close Job: " + str(error)) + if cleanup_errors: + result["cleanup_errors"] = cleanup_errors + result["status"] = "failed" + passed = result["status"] == "passed" + print(json.dumps(result), file=sys.stdout if passed else sys.stderr) + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/qt/shell_smoke.cpp b/src/qt/shell_smoke.cpp new file mode 100644 index 00000000..8f1694d7 --- /dev/null +++ b/src/qt/shell_smoke.cpp @@ -0,0 +1,166 @@ +// Public Qt input, Screen output, rendered image and real child lifecycle. +#include "terminal_widget.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} +template +static void await(Predicate predicate, const termforge::qt::SessionModel& model, const char* message) { + QElapsedTimer timer; + timer.start(); + while (!predicate() && timer.elapsed() < 5000) { + QApplication::processEvents(QEventLoop::AllEvents, 20); + QTest::qWait(5); + if (!model.error().isEmpty()) throw std::runtime_error(model.error().toStdString()); + } + require(predicate(), message); +} +static bool has_line(const termforge::Screen& screen, std::string_view expected) { + for (const auto& row : screen.lines) { + std::string text; + for (const auto& cell : row) if (cell.width != 0) text += cell.text; + while (!text.empty() && text.back() == ' ') text.pop_back(); + if (text == expected) return true; + } + return false; +} +static bool has_text(const termforge::Screen& screen, std::string_view expected) { + for (const auto& row : screen.lines) { + std::string text; + for (const auto& cell : row) if (cell.width != 0) text += cell.text; + if (text.find(expected) != std::string::npos) return true; + } + return false; +} +int main(int argc, char** argv) { + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + // The outer supervisor contains Qt and any ConPTY host before startup. + char gate[3]{}; + DWORD count = 0; + BOOL owned = FALSE; + if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), gate, sizeof(gate), &count, nullptr) || + count != sizeof(gate) || std::string_view(gate, count) != "GO\n" || + !IsProcessInJob(GetCurrentProcess(), nullptr, &owned) || !owned) { + std::cerr << "Qt smoke requires its owned Job supervisor\n"; + return 2; + } + qInstallMessageHandler([](QtMsgType, const QMessageLogContext&, const QString& message) { + std::cerr << message.toStdString() << '\n' << std::flush; + }); + // Avoid a native fatal-error dialog before QApplication is available. + wchar_t executable[32768]{}; + const DWORD path_length = GetModuleFileNameW(nullptr, executable, 32768); + const char* selected_platform = std::getenv("QT_QPA_PLATFORM"); + if (selected_platform && std::string_view(selected_platform) == "offscreen" && + (!path_length || path_length >= 32768 || + !std::filesystem::is_regular_file(std::filesystem::path(executable).parent_path() / "platforms/qoffscreen.dll"))) { + std::cerr << "Qt smoke requires its deployed offscreen platform plugin\n"; + return 2; + } + std::cerr << "Qt application startup\n" << std::flush; + QApplication app(argc, argv); + QApplication::setQuitOnLastWindowClosed(false); + try { + QTemporaryDir workspace; + require(workspace.isValid(), "owned shell workspace could not be created"); + termforge::qt::SessionModel model; + termforge::qt::TerminalWidget window(model); + model.on_changed = [&] { window.update(); }; + window.show(); + window.setFocus(); + QApplication::processEvents(); + const QImage blank = window.grab().toImage(); + require(!blank.isNull(), "Qt window produced no initial image"); + wchar_t directory[MAX_PATH]{}; + const UINT length = GetSystemDirectoryW(directory, MAX_PATH); + require(length > 0 && length < MAX_PATH, "system cmd directory unavailable"); + require(model.start(std::wstring(directory) + L"\\cmd.exe", + L"/D /Q /K \"prompt TF_QT_READY$G\"", workspace.path().toStdWString()), + "fixed ConPTY cmd startup failed"); + std::cerr << "cmd started\n" << std::flush; + await([&] { return has_text(model.screen(), "TF_QT_READY>"); }, model, "cmd prompt was not rendered into the C++ screen"); + const QImage prompt = window.grab().toImage(); + // 750 is an independent expected result and is absent from the command. + QTest::keyClicks(&window, QStringLiteral("set /a 731+19")); + QTest::keyClick(&window, Qt::Key_Return); + await([&] { return has_line(model.screen(), "750"); }, model, "Qt keyboard input did not execute in cmd"); + const QImage result = window.grab().toImage(); + require(!result.isNull() && result != blank, "Qt output image stayed unchanged after real shell output"); + // Check ink in the answer's three cells, independently of the cursor + // and status line; a blank terminal renderer must fail this check. + int answer_row = -1; + for (int row = 0; row < model.screen().height(); ++row) { + const auto& line = model.screen().lines[static_cast(row)]; + if (line[0].text == "7" && line[1].text == "5" && line[2].text == "0") answer_row = row; + } + require(answer_row >= 0, "arithmetic answer has no display row"); + const QFontMetricsF font(window.font()); + const int cell_width = qCeil(font.horizontalAdvance(QLatin1Char('M'))); + const int cell_height = qCeil(font.height()) + 2; + for (int column = 0; column < 3; ++column) { + const QRect cell(8 + column * cell_width, 8 + answer_row * cell_height, cell_width, cell_height); + require(result.copy(cell) != prompt.copy(cell), "arithmetic answer cell has no new rendered ink"); + } + QTest::keyClicks(&window, QStringLiteral("exit 37")); + QTest::keyClick(&window, Qt::Key_Return); + await([&] { return model.finished(); }, model, "cmd exit and output drain did not finish"); + require(model.natural_exit() && model.exit_code() == 37 && model.cleanup_verified(), + "cmd did not exit naturally with complete owned cleanup"); + require(model.configuration_identity().contains(QStringLiteral("side-by-side")), + "Qt entry did not identify its bundled backend"); + + termforge::qt::SessionModel failed_model; + termforge::qt::TerminalWidget failed_window(failed_model); + failed_window.show(); + require(!failed_model.start((workspace.path() + QStringLiteral("/missing-shell.exe")).toStdWString(), L""), + "invalid shell startup was accepted"); + // A startup error is an expected visible result, so do not use the + // normal await helper which treats model.error() as an unexpected fault. + QElapsedTimer failure_timer; failure_timer.start(); + while (!failed_model.finished() && failure_timer.elapsed() < 5000) QTest::qWait(5); + require(failed_model.finished() && !failed_model.error().isEmpty() && failed_model.cleanup_verified(), + "failed shell startup lost its error or owned cleanup"); + const QImage failed_image = failed_window.grab().toImage(); + bool error_ink = false; + for (int y = failed_image.height() - cell_height - 16; y < failed_image.height(); ++y) + for (int x = 0; x < failed_image.width(); ++x) { + const QColor pixel = failed_image.pixelColor(x, y); + if (pixel.red() > pixel.green() + 40 && pixel.red() > pixel.blue() + 40) error_ink = true; + } + require(error_ink, "shell startup error has no visible error text"); + failed_window.close(); + + termforge::qt::SessionModel closing_model; + termforge::qt::TerminalWidget closing_window(closing_model); + closing_window.show(); + require(closing_model.start(std::wstring(directory) + L"\\cmd.exe", + L"/D /Q /K \"prompt TF_QT_CLOSE$G\"", workspace.path().toStdWString()), + "close-case shell startup failed"); + await([&] { return has_text(closing_model.screen(), "TF_QT_CLOSE>"); }, closing_model, + "close-case shell did not reach its prompt"); + closing_window.close(); + await([&] { return closing_model.finished(); }, closing_model, + "closing the Qt window did not reclaim its running shell"); + require(!closing_model.natural_exit() && closing_model.cleanup_verified(), + "manual close was reported as natural exit or failed cleanup"); + std::cout << "{\"schema\":\"termforge-qt-cpp-smoke-v1\",\"qt\":\"" << qVersion() + << "\",\"core\":\"cpp\",\"backend\":\"fixed-microsoft-conpty\"," + "\"arithmetic\":750,\"child_exit\":37,\"cleanup_verified\":true," + "\"image_changed\":true,\"startup_error_visible\":true," + "\"manual_close_reclaimed\":true,\"presented_done\":null}\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/src/qt/terminal_session.hpp b/src/qt/terminal_session.hpp new file mode 100644 index 00000000..0f5e089f --- /dev/null +++ b/src/qt/terminal_session.hpp @@ -0,0 +1,220 @@ +// GUI-owned terminal state with bounded, worker-owned ConPTY transport. +#pragma once + +#include "../../experiments/core/terminal.hpp" +#include "../../experiments/shell/queues.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace termforge::qt { + +// One model owns one fixed-size session. Only its Qt thread reads or modifies +// Parser/Screen; the reader publishes bounded byte chunks, never screen state. +class SessionModel final : public QObject { +public: + static constexpr int fixed_rows = 24; + static constexpr int fixed_columns = 80; + std::function on_changed; + +private: + Parser parser_{fixed_rows, fixed_columns, 100, ParseMode::recovery}; + OutputQueue output_; + std::unique_ptr session_; + std::unique_ptr input_; + QTimer timer_; + std::thread lifecycle_; + std::atomic close_requested_{false}; + std::atomic cleanup_done_{false}; + struct Completion { + DWORD exit_code = STILL_ACTIVE; + DWORD active_processes = MAXDWORD; + unsigned long long output_bytes = 0; + bool cleanup_verified = false; + bool natural_exit = false; + bool output_eof = false; + std::string error; + } completion_; + bool started_ = false; + bool finished_ = false; + bool parser_failed_ = false; + bool natural_exit_ = false; + bool cleanup_verified_ = false; + DWORD exit_code_ = STILL_ACTIVE; + QString error_; + + void notify() { if (on_changed) on_changed(); } + void remember_error(const QString& error) { + if (error_.isEmpty()) error_ = error; + } + // Only this worker calls wait/cancel/close once startup has completed. + // During natural close the Qt timer keeps draining the queue, allowing the + // ConPTY reader to make progress while ClosePseudoConsole waits for EOF. + void supervise(bool startup_succeeded) noexcept { + bool child_exited = false; + try { + if (startup_succeeded) { + while (!close_requested_.load(std::memory_order_acquire)) { + if (session_->wait_exit(20)) { child_exited = true; break; } + } + } + } catch (const std::exception& error) { + completion_.error = error.what(); + } catch (...) { + completion_.error = "Unknown session supervision failure"; + } + + const bool cancelled = close_requested_.load(std::memory_order_acquire) || !child_exited; + if (cancelled) { + output_.stop(); // Release a reader waiting for room before closing. + if (session_ && session_->pid) { + try { session_->cancel(); } + catch (const std::exception& error) { + if (completion_.error.empty()) completion_.error = error.what(); + } catch (...) { + if (completion_.error.empty()) completion_.error = "Session cancellation failed"; + } + } + } + if (input_) input_->stop(); + if (session_) { + session_->close(); + completion_.exit_code = session_->exit_code; + completion_.active_processes = session_->active_after; + completion_.output_bytes = session_->output_bytes; + completion_.cleanup_verified = session_->cleanup_complete; + completion_.output_eof = session_->read_error == ERROR_BROKEN_PIPE || session_->read_error == ERROR_SUCCESS; + completion_.natural_exit = child_exited && !cancelled && !session_->forced_process_cancel && + !session_->forced_reader_cancel && completion_.output_eof; + try { session_->check_reader(); } + catch (const std::exception& error) { + if (completion_.error.empty()) completion_.error = error.what(); + } catch (...) { + if (completion_.error.empty()) completion_.error = "Session output reader failed"; + } + } else { + completion_.active_processes = 0; + completion_.cleanup_verified = true; + } + cleanup_done_.store(true, std::memory_order_release); + } + void poll() { + if (finished_) return; + bool changed = false; + const auto bytes = output_.take(32 * 1024); + if (!bytes.empty()) { + changed = true; + if (!parser_failed_) { + try { parser_.feed(bytes); } + catch (const std::exception& error) { + parser_failed_ = true; + remember_error(QString::fromUtf8(error.what())); + close(); + } + } + } + if (cleanup_done_.load(std::memory_order_acquire) && !output_.snapshot().queued_bytes) { + if (!parser_failed_) { + try { parser_.finish(); } + catch (const std::exception& error) { + parser_failed_ = true; + remember_error(QString::fromUtf8(error.what())); + } + } + if (!completion_.error.empty()) remember_error(QString::fromUtf8(completion_.error.c_str())); + exit_code_ = completion_.exit_code; + cleanup_verified_ = completion_.cleanup_verified; + natural_exit_ = completion_.natural_exit && !close_requested_.load(std::memory_order_acquire) && + !parser_failed_ && completion_.error.empty(); + if (!cleanup_verified_) remember_error(QStringLiteral("Session cleanup could not be verified")); + finished_ = true; + timer_.stop(); + if (lifecycle_.joinable()) lifecycle_.join(); + changed = true; + } + if (changed) notify(); + } + +public: + explicit SessionModel(QObject* parent = nullptr) : QObject(parent) { + timer_.setInterval(10); + QObject::connect(&timer_, &QTimer::timeout, this, [this] { poll(); }); + } + ~SessionModel() override { + on_changed = {}; + timer_.stop(); + close_requested_.store(true, std::memory_order_release); + // QApplication may already have stopped dispatching timers. Abandoning + // this view must still release queue backpressure before worker join. + output_.stop(); + if (lifecycle_.joinable()) lifecycle_.join(); + } + bool start(const std::wstring& executable, const std::wstring& arguments, + const std::wstring& working_directory = {}) { + if (started_) { + remember_error(QStringLiteral("This terminal session has already been started")); + notify(); return false; + } + started_ = true; + bool succeeded = false; + try { + session_ = std::make_unique(tf_conpty::Backend::configured()); + if (std::string_view(session_->backend_identity()) == "system") + throw std::runtime_error("The Qt product entry requires the fixed bundled ConPTY backend"); + input_ = std::make_unique(*session_); + session_->start(executable, arguments, [this](std::string_view bytes) { output_.push(bytes); }, + working_directory, {fixed_columns, fixed_rows}); + input_->start(); + succeeded = true; + } catch (const std::exception& error) { + remember_error(QString::fromUtf8(error.what())); + close_requested_.store(true, std::memory_order_release); + output_.stop(); + } + lifecycle_ = std::thread([this, succeeded] { supervise(succeeded); }); + timer_.start(); + notify(); + return succeeded; + } + bool send(const std::string& bytes) { + if (!running() || !input_) return false; + try { + if (input_->send(bytes)) return true; + remember_error(QStringLiteral("Terminal input queue is full or unavailable")); + } catch (const std::exception& error) { + remember_error(QString::fromUtf8(error.what())); + close(); + } + notify(); return false; + } + bool send(const QByteArray& bytes) { + return send(std::string(bytes.constData(), static_cast(bytes.size()))); + } + void close() noexcept { + close_requested_.store(true, std::memory_order_release); + } + const Screen& screen() const { return parser_.screen; } + bool running() const { return started_ && !finished_ && !close_requested_.load(std::memory_order_acquire); } + bool finished() const { return finished_; } + bool natural_exit() const { return finished_ && natural_exit_; } + bool cleanup_verified() const { return finished_ && cleanup_verified_; } + DWORD exit_code() const { return exit_code_; } + DWORD active_processes_after() const { return finished_ ? completion_.active_processes : MAXDWORD; } + unsigned long long received_bytes() const { return finished_ ? completion_.output_bytes : 0; } + bool output_eof() const { return finished_ && completion_.output_eof; } + const QString& error() const { return error_; } + QString configuration_identity() const { + return QStringLiteral("cpp-core / qt-widgets / ") + + QString::fromLatin1(session_ ? session_->backend_identity() : "bundled-conpty-not-loaded") + + QStringLiteral(" / 80x24"); + } +}; + +} // namespace termforge::qt diff --git a/src/qt/terminal_widget.hpp b/src/qt/terminal_widget.hpp new file mode 100644 index 00000000..fd3a209c --- /dev/null +++ b/src/qt/terminal_widget.hpp @@ -0,0 +1,133 @@ +// Fixed-grid Qt entry point; terminal semantics stay in the owned C++ model. +#pragma once + +#include "terminal_session.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace termforge::qt { + +class TerminalWidget final : public QWidget { + SessionModel& model_; + static constexpr int margin = 8; + int cell_width_ = 9; + int cell_height_ = 19; + static QColor colour(int index, bool foreground) { + static const std::array palette{ + QColor(20, 24, 28), QColor(205, 73, 74), QColor(145, 185, 89), QColor(221, 190, 90), + QColor(89, 155, 218), QColor(187, 123, 205), QColor(83, 179, 184), QColor(216, 222, 228), + QColor(100, 108, 117), QColor(239, 112, 112), QColor(179, 220, 122), QColor(246, 218, 121), + QColor(128, 188, 240), QColor(217, 158, 234), QColor(124, 214, 220), QColor(247, 249, 250) + }; + return palette[index >= 0 && index < 16 ? static_cast(index) : foreground ? 7 : 0]; + } + void closeEvent(QCloseEvent* event) override { + model_.close(); + QWidget::closeEvent(event); + } + bool event(QEvent* event) override { + // QWidget normally consumes Tab for focus traversal before keyPressEvent. + if (event->type() == QEvent::KeyPress && static_cast(event)->key() == Qt::Key_Tab) { + keyPressEvent(static_cast(event)); return event->isAccepted(); + } + return QWidget::event(event); + } + void keyPressEvent(QKeyEvent* event) override { + QByteArray bytes; + if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_C) bytes = QByteArray(1, '\x03'); + else if (event->modifiers().testFlag(Qt::ControlModifier) || event->modifiers().testFlag(Qt::AltModifier)) { + QWidget::keyPressEvent(event); return; + } else { + switch (event->key()) { + case Qt::Key_Return: case Qt::Key_Enter: bytes = "\r"; break; + case Qt::Key_Backspace: bytes = QByteArray(1, '\x7f'); break; + case Qt::Key_Tab: bytes = "\t"; break; + case Qt::Key_Up: bytes = "\x1b[A"; break; + case Qt::Key_Down: bytes = "\x1b[B"; break; + case Qt::Key_Right: bytes = "\x1b[C"; break; + case Qt::Key_Left: bytes = "\x1b[D"; break; + default: + for (const auto character : event->text()) { + if (character.unicode() < 0x20 || character.unicode() > 0x7e) { + QWidget::keyPressEvent(event); return; + } + } + bytes = event->text().toLatin1(); + } + } + if (bytes.isEmpty()) { QWidget::keyPressEvent(event); return; } + model_.send(bytes); + event->accept(); + } + void paintEvent(QPaintEvent*) override { + QPainter painter(this); + painter.fillRect(rect(), colour(-1, false)); + const auto& screen = model_.screen(); + const auto base_font = font(); + for (int row = 0; row < screen.height(); ++row) { + const auto& line = screen.lines[static_cast(row)]; + for (int column = 0; column < screen.width(); ++column) { + const auto& cell = line[static_cast(column)]; + const QRectF area(margin + column * cell_width_, margin + row * cell_height_, cell_width_, cell_height_); + painter.fillRect(area, colour(cell.style.bg, false)); + } + for (int column = 0; column < screen.width(); ++column) { + const auto& cell = line[static_cast(column)]; + if (!cell.width || cell.text.empty()) continue; + auto cell_font = base_font; cell_font.setBold(cell.style.bold); painter.setFont(cell_font); + painter.setPen(colour(cell.style.fg, true)); + const QRectF area(margin + column * cell_width_, margin + row * cell_height_, + cell_width_ * cell.width, cell_height_); + painter.save(); painter.setClipRect(area); + painter.drawText(area, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, + QString::fromUtf8(cell.text.data(), static_cast(cell.text.size()))); + painter.restore(); + } + } + if (model_.running()) { + painter.fillRect(QRect(margin + screen.column * cell_width_, margin + (screen.row + 1) * cell_height_ - 2, + cell_width_, 2), colour(-1, true)); + } + painter.setFont(base_font); + painter.setPen(model_.error().isEmpty() ? colour(8, true) : colour(9, true)); + const QString status = !model_.error().isEmpty() ? model_.error() : model_.finished() ? + QStringLiteral("Session exited: %1").arg(model_.exit_code()) : QStringLiteral("Fixed 80 x 24 terminal"); + painter.drawText(QRect(margin, margin + SessionModel::fixed_rows * cell_height_, + width() - margin * 2, cell_height_ + margin), + Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, status); + } +public: + explicit TerminalWidget(SessionModel& model, QWidget* parent = nullptr) : QWidget(parent), model_(model) { + // Qt owns this one process-local registration until application exit. + // The font comes from the project's independent OFL asset bundle. + static const int font_id = QFontDatabase::addApplicationFont( + QCoreApplication::applicationDirPath() + QStringLiteral("/assets/fonts/RobotoMono-Regular.ttf")); + if (font_id < 0) throw std::runtime_error("The bundled Roboto Mono font could not be loaded"); + const auto families = QFontDatabase::applicationFontFamilies(font_id); + if (families.isEmpty()) throw std::runtime_error("The bundled font exposes no font family"); + QFont fixed(families.first()); + fixed.setStyleHint(QFont::TypeWriter); fixed.setPointSize(11); setFont(fixed); + const QFontMetricsF metrics(fixed); + cell_width_ = qCeil(metrics.horizontalAdvance(QLatin1Char('M'))); + cell_height_ = qCeil(metrics.height()) + 2; + setFocusPolicy(Qt::StrongFocus); + setAttribute(Qt::WA_InputMethodEnabled, false); + setFixedSize(sizeHint()); + model_.on_changed = [this] { update(); }; + } + ~TerminalWidget() override { model_.on_changed = {}; } + QSize sizeHint() const override { + return {margin * 2 + SessionModel::fixed_columns * cell_width_, + margin * 3 + (SessionModel::fixed_rows + 1) * cell_height_}; + } +}; + +} // namespace termforge::qt diff --git a/third_party/conpty/LICENSE b/third_party/conpty/LICENSE new file mode 100644 index 00000000..8cb179cd --- /dev/null +++ b/third_party/conpty/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/conpty/README.md b/third_party/conpty/README.md new file mode 100644 index 00000000..282155ec --- /dev/null +++ b/third_party/conpty/README.md @@ -0,0 +1,13 @@ +# 随包固定微软 ConPTY + +Windows 本地 Shell 的产品路线已按 [ADR-0007](../../docs/adr/0007-bundled-microsoft-conpty.md)确认采用随包固定微软 ConPTY。本目录只保存来源、身份及完整 MIT 许可,DLL/EXE 保留在本地受控目录,不提交到源码仓库。 + +[manifest.json](manifest.json) 固定现有整合起点 `Microsoft.Windows.Console.ConPTY 1.25.260930003` 的官方来源、源码提交、包及同包 Windows x64 两个文件的 SHA-256。它不是跟踪最新版本的声明,也不代表完整产品验收。包版本与 flags 可依据明确身份和行为证据更新,不由该元数据永久锁定。 + +首次取得组件时,按 manifest 的官方 URL 获取包并核对包大小及 SHA-256,仅提取列出的 `package_members`,保持 `conpty.dll` / `OpenConsole.exe` 名称。不得混合其他包或系统目录的文件。现有同哈希组件可直接复用,构建不隐式下载或修改 Windows 组件。 + +根构建将 `TERMFORGE_CONPTY_BACKEND_DIR` 设为该文件对的绝对目录,对正式入口调用 `termforge_use_bundled_conpty(target)`。该函数不依赖 Rust 候选开关;配置和每次构建均核对双文件及本目录许可的身份,并把两文件、manifest 和 LICENSE 复制到目标旁的 `conpty-backend/`。缺包或身份错误直接失败,不退回系统版本。 + +运行时 `Backend::configured()` 再核对固定双文件,从可执行文件旁的绝对目录加载;create/resize/close 与模块生命周期由同一个 Backend 拥有。当前复用已验证接口的 Console 字符宽度策略 `0x18`,不是旧 passthrough flag。该策略与原版 Unicode、原生控制台和末端行为的完整匹配仍需分别验证。 + +`experimental`、`default_backend` 与 `supported_client_platforms_verified` 保留了原固定包清单的历史集成状态;当前路线确认见 ADR,新的构建/行为验收须引用实际源码和结果。系统 ConPTY 可用作诊断对照;本目录不会把不同源码或后端的历史成功拼成联合验收。 diff --git a/third_party/conpty/manifest.json b/third_party/conpty/manifest.json new file mode 100644 index 00000000..a3f7fb23 --- /dev/null +++ b/third_party/conpty/manifest.json @@ -0,0 +1,29 @@ +{ + "schema": "termforge-conpty-package-v1", + "name": "Microsoft.Windows.Console.ConPTY", + "version": "1.25.260930003", + "architecture": "win-x64", + "license": "MIT", + "source_commit": "dc8ae365847d41f62d1ebca93815bb00959bc12f", + "source_tag": "v1.25.2733.0", + "license_source_url": "https://raw.githubusercontent.com/microsoft/terminal/dc8ae365847d41f62d1ebca93815bb00959bc12f/LICENSE", + "license_source_sha256": "5d177f23ecfeb0ea8e050b6a5a16355e1ae9a0b286436ca8f83ed08b3795be6b", + "bundled_license_sha256": "ad0cf28f3381ca9bb0bf101d127402d44c17bfa0991e1a00bff7ae6679e9dada", + "license_normalization": "CRLF to LF only; complete text verified equal", + "package_url": "https://github.com/microsoft/terminal/releases/download/v1.25.2733.0/Microsoft.Windows.Console.ConPTY.1.25.260930003.nupkg", + "package_bytes": 1750533, + "package_sha256": "6d6f8b008c655d814c498d8205d4012dcd4d0b6dfc1096461d0a185d1e3caf03", + "files": { + "conpty.dll": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a", + "OpenConsole.exe": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + }, + "package_members": { + "conpty.dll": "runtimes/win-x64/native/conpty.dll", + "OpenConsole.exe": "build/native/runtimes/x64/OpenConsole.exe" + }, + "experimental": true, + "configured_creation_flags": 24, + "configured_glyph_width": "PSEUDOCONSOLE_GLYPH_WIDTH_CONSOLE", + "default_backend": false, + "supported_client_platforms_verified": false +} diff --git a/third_party/qt/README.md b/third_party/qt/README.md new file mode 100644 index 00000000..8ea5511e --- /dev/null +++ b/third_party/qt/README.md @@ -0,0 +1,32 @@ +# 本地 Qt 构建依赖 + +当前 C++/Qt 迁移入口固定使用 Qt **6.10.3**、Windows desktop **MSVC 2022 x64** 的 `qtbase` SDK,应用动态链接 Core、Gui、Widgets;测试入口另使用 Test。Qt Widgets 的当前路线见 [ADR-0008](../../docs/adr/0008-cpp-core-qt-widgets.md)。本目录只保存来源说明和 [manifest.json](manifest.json),不提交 Qt 归档、DLL、插件或本地 SDK。 + +## 来源与身份 + +manifest 固定了 Qt 官方 Updates.xml 和原始 qtbase 7z 的 URL、大小与哈希。官方包名为 `qt.qt6.6103.win64_msvc2022_64`,归档大小 **42,748,757 B**,SHA-256 为 `4db84dee7fe3c558f242bef0a88852613af76580dc6d2b24596479f47004dad7`;本次整理已重新核对本地原始归档及 Updates.xml。 + +对应源码为 [qtbase 6.10.3 源码归档](https://download.qt.io/archive/qt/6.10/6.10.3/submodules/qtbase-everywhere-src-6.10.3.tar.xz)及 Qt 官方仓库的 `v6.10.3`。这里只标识源码获取位置,没有把源码归档下载、哈希复核或完整对应源码交付算作已完成。 + +本地 aqtinstall 3.3.0 会修补 qmake 和 `.prl` 的安装前缀。**原始归档、修补后的 SDK 文件、最终部署目录是不同身份**,不能用归档哈希冒充 SDK 或产品目录的完整哈希;具体构建结果按各次证据绑定实际输入和输出。 + +## 本地构建 + +使用已核对的 Qt SDK 和[固定微软 ConPTY 文件对](../conpty/README.md),只设置本次构建参数,无须修改全局 PATH: + +```powershell +cmake -S . -B build/qt-cpp -G "Visual Studio 17 2022" -A x64 ` + -DTERMFORGE_BUILD_QT_SHELL=ON ` + -DTERMFORGE_BUILD_CANDIDATE_ADAPTER=OFF ` + -DQt6_DIR="/lib/cmake/Qt6" ` + -DTERMFORGE_CONPTY_BACKEND_DIR="" +cmake --build build/qt-cpp --config Release --target termforge_qt_shell +``` + +`Qt6_DIR` 应包含该版本的 `Qt6Config.cmake`;正式入口缺少或不匹配固定 ConPTY 时明确失败,不回退系统版本。最小 SDK 未安装翻译,当前构建使用 `windeployqt --no-translations --no-compiler-runtime`,只形成本地构建目录,尚不是可发布的独立产品包。 + +## 许可与分发边界 + +Qt 模块保留自己的许可,不由项目 MIT 许可覆盖。[Qt 6.10 许可说明](https://doc.qt.io/qt-6.10/licensing.html)与 SDK SBOM/头文件显示,当前 Core、Gui、Widgets、Test 的模块许可选项包含 LGPL-3.0-only,以及相应 GPL/商业许可选项;manifest 保留精确 SPDX 表达式及实际 SBOM 身份。当前应用计划采用**动态链接的 LGPLv3 路径**,这不是对完整产品分发条件已经满足的声明。 + +Qt 内还包含使用其他许可的第三方代码;不能仅凭模块级许可表达式代替实际部署文件清单。产品分发前仍须完成运行库/插件及第三方许可声明、对应源码与替换条件的核对,以及干净机器运行验证;当前记录只支持本地构建依赖的来源与身份。 diff --git a/third_party/qt/manifest.json b/third_party/qt/manifest.json new file mode 100644 index 00000000..6f2cd5ca --- /dev/null +++ b/third_party/qt/manifest.json @@ -0,0 +1,107 @@ +{ + "schema": "termforge-qt-build-dependency-v1", + "name": "Qt qtbase", + "version": "6.10.3", + "host": "windows", + "target": "desktop", + "architecture": "win64_msvc2022_64", + "scope": "Local build dependency for the first C++/Qt migration entry; no released product package or complete redistribution acceptance.", + "required_application_modules": [ + "Core", + "Gui", + "Widgets" + ], + "test_only_modules": [ + "Test" + ], + "official_supported_configuration_url": "https://doc.qt.io/qt-6.10/windows.html", + "official_metadata": { + "url": "https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6103/qt6_6103/Updates.xml", + "bytes": 312649, + "sha256": "91e0a64cc859c28eb547192a9813cc2ffaac524a98250bc39a0b79fe8da6eae1" + }, + "official_binary_archive": { + "name": "qt.qt6.6103.win64_msvc2022_64", + "version": "6.10.3-0-202603310407", + "release_date": "2026-03-31", + "archive": "qtbase-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64.7z", + "url": "https://download.qt.io/online/qtsdkrepository/windows_x86/desktop/qt6_6103/qt6_6103/qt.qt6.6103.win64_msvc2022_64/6.10.3-0-202603310407qtbase-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64.7z", + "bytes": 42748757, + "sha1": "08bd6fea980b53485877f5048c725ca334c19130", + "sha1_matches_official_sidecar": true, + "sha256": "4db84dee7fe3c558f242bef0a88852613af76580dc6d2b24596479f47004dad7" + }, + "source": { + "module": "qtbase", + "version_tag": "v6.10.3", + "repository": "https://code.qt.io/qt/qtbase.git", + "source_archive_url": "https://download.qt.io/archive/qt/6.10/6.10.3/submodules/qtbase-everywhere-src-6.10.3.tar.xz", + "source_archive_downloaded_for_this_record": false, + "source_archive_sha256": null, + "note": "The matching official source archive is identified, not included or locally hash-verified by this build-dependency record." + }, + "linkage": "dynamic", + "license": { + "intended_runtime_library_option": "LGPL-3.0-only", + "alternatives_observed_in_module_metadata": "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only", + "module_observations": [ + { + "name": "Core", + "versionInfo": "6.10.3", + "licenseDeclared": "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only", + "licenseConcluded": "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only", + "downloadLocation": "git://code.qt.io/qt/qtbase.git@7ddbc87d8e14ce51d2957ea72d0a6077593d5ff4" + }, + { + "name": "Gui", + "versionInfo": "6.10.3", + "licenseDeclared": "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only", + "licenseConcluded": "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only", + "downloadLocation": "git://code.qt.io/qt/qtbase.git@7ddbc87d8e14ce51d2957ea72d0a6077593d5ff4" + }, + { + "name": "Widgets", + "versionInfo": "6.10.3", + "licenseDeclared": "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only", + "licenseConcluded": "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only", + "downloadLocation": "git://code.qt.io/qt/qtbase.git@7ddbc87d8e14ce51d2957ea72d0a6077593d5ff4" + }, + { + "name": "Test", + "versionInfo": "6.10.3", + "licenseDeclared": "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only", + "licenseConcluded": "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only", + "downloadLocation": "git://code.qt.io/qt/qtbase.git@7ddbc87d8e14ce51d2957ea72d0a6077593d5ff4" + } + ], + "evidence": { + "sdk_sbom_relative_path": "sbom/qtbase-6.10.3.spdx.json", + "sdk_sbom_sha256": "51bdcf25ae10a6af0ce10030bf816ab10da0b2f70b35e0e9c55bd4b43241e659", + "public_source_example": "https://raw.githubusercontent.com/qt/qtbase/v6.10.3/src/widgets/kernel/qwidget.h" + }, + "official_documents": [ + "https://doc.qt.io/qt-6.10/licensing.html", + "https://doc.qt.io/qt-6.10/qtwidgets-index.html#licenses", + "https://doc.qt.io/qt-6.10/qttest-index.html#licenses", + "https://doc.qt.io/qt-6.10/licenses-used-in-qt.html" + ], + "third_party_code_scope": "Qt includes third-party components with their own licenses; the module-level expression is not a full deployed-file license inventory.", + "redistribution_review_status": "pending_before_product_distribution" + }, + "installer": { + "name": "aqtinstall", + "version": "3.3.0", + "selected_archives": [ + "qtbase" + ], + "transformations": "aqtinstall patches qmake.exe and .prl installation prefixes; archive hash and installed selected-file hashes are distinct identities.", + "hash_scope": "The archive SHA-256 identifies the untouched official 7z. It is not the hash of an aqt-patched SDK directory, qmake.exe, .prl file or deployed product." + }, + "deployment_status": { + "published_product_package": false, + "clean_machine_runtime_validation": "pending", + "license_notices_and_corresponding_source_review": "pending", + "sdk_translations_installed": false, + "note": "The current local build uses windeployqt without translations or compiler-runtime deployment; no standalone clean-machine runtime completeness is claimed." + } +}