Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/qt-cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
build/qt-cpp/qt-unicode-bin/Release/runtime-manifest.json
build/qt-cpp/qt-modes-bin/Release/runtime-manifest.json
- name: Run Qt offscreen smoke in its owned Job
run: ctest --test-dir build/qt-cpp -C Release -R '^(qt_cpp_(shell|resize)|qt_unicode_input|qt_terminal_modes)$' --no-tests=error --verbose
run: ctest --test-dir build/qt-cpp -C Release -R '^(qt_cpp_(shell|resize)|qt_unicode_input|qt_terminal_modes(_retained)?)$' --no-tests=error --verbose
# Keep this configuration independent from the default/system-backend and
# Alacritty reference jobs. SDKs, runtime folders and raw logs are not
# publication artifacts; this job does not produce a release package.
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ add_executable(resize_reflow_addressing_test experiments/core/resize_reflow_addr
target_link_libraries(resize_reflow_addressing_test PRIVATE termforge_core)
termforge_warnings(resize_reflow_addressing_test)

add_executable(display_cursor_test experiments/core/display_cursor_test.cpp)
target_link_libraries(display_cursor_test PRIVATE termforge_core)
termforge_warnings(display_cursor_test)

add_executable(resize_edit_test experiments/core/resize_edit_test.cpp)
target_link_libraries(resize_edit_test PRIVATE termforge_core)
termforge_warnings(resize_edit_test)
Expand Down Expand Up @@ -451,6 +455,8 @@ add_test(NAME retained_columns_core COMMAND retained_columns_test)
add_test(NAME retained_resize_address_core COMMAND resize_address_test --retained-columns)
add_test(NAME reflow_addressing_core COMMAND resize_reflow_addressing_test)
set_tests_properties(reflow_addressing_core PROPERTIES TIMEOUT 10)
add_test(NAME display_cursor_core COMMAND display_cursor_test)
set_tests_properties(display_cursor_core PROPERTIES TIMEOUT 10)
add_test(NAME retained_resize_edit_core COMMAND resize_edit_test)
add_test(NAME retained_resize_pending_core COMMAND resize_pending_test)
add_test(NAME retained_resize_hidden_core COMMAND resize_hidden_test)
Expand Down Expand Up @@ -722,6 +728,10 @@ if(TERMFORGE_BUILD_QT_SHELL)
"${PROJECT_SOURCE_DIR}/src/qt/run_smoke.py" "$<TARGET_FILE:qt_terminal_modes_test>")
set_tests_properties(qt_terminal_modes PROPERTIES TIMEOUT 35
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
add_test(NAME qt_terminal_modes_retained COMMAND "${Python3_EXECUTABLE}"
"${PROJECT_SOURCE_DIR}/src/qt/run_smoke.py" "$<TARGET_FILE:qt_terminal_modes_test>" --retained-contract)
set_tests_properties(qt_terminal_modes_retained PROPERTIES TIMEOUT 35
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
if(TERMFORGE_ENABLE_RESIZE_REPRO)
add_test(NAME qt_terminal_modes_resize_repro COMMAND "${Python3_EXECUTABLE}"
"${PROJECT_SOURCE_DIR}/src/qt/run_smoke.py" "$<TARGET_FILE:qt_terminal_modes_test>" --resize)
Expand Down
1 change: 1 addition & 0 deletions docs/EVIDENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
| 固定 ConPTY resize 与输出机制 | [1.25.260930003 包身份及 pinned 源码核查](research/conpty-resize-output-2026-11-02.md) | 当前 0x18 是字宽,旧 resize quirk 已删除;signal 成功非完成屏障,内部 reflow 不保证重发整屏,DSR 光标同步不证明正文一致。静态解释不替代同次 Qt 动态证据或原版 oracle;本次未新增运行或改 flags |
| 原版 resize 后寻址与自主核心字面诊断 | [独立直接协议样本](../research/ui/results/2026-11-02-resize-address-reference/README.md)、[默认裁剪路径的失败诊断](../experiments/core/results/2026-11-02-resize-address/README.md) | 四例自然追加、CR、LF、CUP 保留动作前观测,文档、DSR、显示坐标分别核对;第五例 EL1 保留原审计失败。默认 `Screen::resize` 历史诊断仅 16/64 阶段通过、退出 1;当时 `EXCLUDE_FROM_ALL`,现参与默认构建但 CTest 只注册另行验证的显式保留入口。旧红不设 `WILL_FAIL`,常规 CI 或另一入口通过不能豁免默认路径差异 |
| 缩窄后跨前方显示重排的 CUP/DSR 寻址(#27/#29 显式入口) | [单例原版四阶段严格记录](../research/ui/results/2026-11-03-reflow-addressing-reference/README.md)、[核心字面回放与身份](../experiments/core/results/2026-11-03-reflow-addressing/README.md):`CSI 8;2HQ` 写入文档行 6(保留的缩放前源片段行)而非重排行 5,DSR `8;3`、caret 同口径;窄屏矩形 (155,144) 恢复 (155,126) 证实显示页独立 +1 行重排 | 单例固定 ASCII、`91×20→61×20→91×20`、一个动作、最大一次运行;只补既有样本无法区分的行映射,不含备用屏、高度、Unicode、历史、样式、Qt/固定 ConPTY 或性能;默认 `Screen::resize`/Qt 未切换,#27/#29 不关闭 |
| 正式 Qt / 固定 ConPTY 接入保留事务、显示投影与显示光标 | [契约、身份与差异](../experiments/shell/results/2026-11-03-qt-retained-display/README.md):可达真实 child→固定 ConPTY→core→widget;新 `qt_terminal_modes_retained` 12/12 检查通过,只缩宽后显示页第 8 行与原生重排逐字节一致,B/C/D 保留源位,命中经 layout revision 回到源格;Qt 正式路径 CTest 5/5,核心 36/36(含 `display_cursor_core`) | 旧六阶段 `--resize` 仍 exit 1 且失败信息不变,保留为原生重排网格与保留行合同的坐标差;同时缩宽+缩高的原版视口/滚动与后续 CUP 仍缺独立证据;不覆盖真实呈现/DPI/历史/Find/性能,#27/#29 不关闭 |
| 自主 C++ 同 owner 保留列宽事务与有限编辑(#27/#29 独立入口) | [源码、定向红绿与限制](../experiments/core/results/2026-11-02-retained-columns/README.md):`a35be70` 的四个寻址 case 为 64/64 阶段;`fdeaed6` 使打印/ICH/DCH 按协议宽更新目标行,内容长度与 `written` 分离,三例编辑为 64/64 阶段。该固定源码重新构建后核心 22/22、Qt/固定 ConPTY 4/4,各配置身份独立记录 | 默认 resize / Qt 未切换;Qt 回归不执行新保留入口。Q/R 与 ICH/DCH 红绿分别保留,owner 存储及空白长度工程检查没有独立先行 RED。后续光标与隐藏帧修正另列,不继承本轮结果;默认裁剪与 B/CD 失败不豁免 |
| 原版活动/隐藏帧与缩放后下一字符 | [6次原版、29阶段](../research/ui/results/2026-11-02-resize-frame-reference/README.md):5次原审计通过、1次隐藏主屏原审计失败后独立离线补充通过;无补跑。文档、DSR与显示分别记录 | 活动备用屏保留正文;隐藏主屏返回的正文和边界不同;4个ON/OFF缩放末列样本支持下一字符覆盖当前位置。原版Document索引差异、私有模式位及完整网格不据此判定 |
| 自主 C++ 缩放后的末列输入与隐藏主屏 | [末列输入红绿](../experiments/core/results/2026-11-02-resize-pending/README.md):`9b67886` 的4例44/72→72/72;[隐藏主屏红绿](../experiments/core/results/2026-11-02-resize-hidden/README.md):`afdcf59` 的12/32→32/32,精确比较正文空白长度、源位置、显示与DSR | 定向实现均只走显式保留入口;默认 resize / Qt 尚未接入。隐藏主屏样本不能推出活动帧 margin、单纯隐藏增宽或完整 Unicode 语义;各报告区分固定生产源码、执行后身份与缺失的历史二进制 |
Expand Down
2 changes: 1 addition & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

| 新路线迁移入口 | 当前状态与边界 | 目标差距 | 路标 |
|---|---|---|---|
| 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供默认 80×24、随可用区域变化的 cmd 标签、基本 ASCII 与[限定 Unicode 文本字节](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、16 色及各会话独立有界队列、[输入法组合/提交/取消(事件级 + 固定拼音验证)](../research/ui/results/2026-10-30-qt-ime-fixed-pinyin/README.md),并接入[无修饰导航键与基础鼠标正反拖选高亮](../experiments/shell/results/2026-11-01-r2-core-input/README.md);[基础动态尺寸验证](../experiments/shell/results/2026-10-29-qt-resize/README.md)分离窗口/核心/transport 与真实 child,覆盖极小、重复、隐藏页和关闭交叉,并复验原标签算术及事件循环回收。678 原生极窄失败保留;0ff 仅修订字面子区域/顶层实际区域夹具,另有[冻结包一次隔离原生成功](../experiments/shell/results/2026-10-29-qt-resize/native-validation.json):实际顶层区域、core/transport/child 一致,窗口仅 DPI 96。旧尺寸结果保留各自源码;[Unicode 联合源码的一次完整61项](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json)分开 Qt3项与其余58项既有/独立诊断,13条内部skip保留 | 分屏、其他 shell、工作区恢复、完整 resize/reflow、历史浏览/自动滚动、选词/选行与系统复制/粘贴、Find、完整 Unicode 字素/绘制、候选窗与真实 IME 交叉(已有固定拼音;[固定时间点对照](research/qt-ime-candidate-panel-reference-2026-11-01.md)中输出后两侧都保持旧位置、resize 后延后点都更新;字体阶段原版未触发,面板身份未证明,且对照 Qt 源为历史 5fab30b)、真实 DPI、应用兼容与性能仍需实现或迁移;物理裁剪不恢复丢弃正文,完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 |
| 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供默认 80×24、随可用区域变化的 cmd 标签、基本 ASCII 与[限定 Unicode 文本字节](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、16 色及各会话独立有界队列、[输入法组合/提交/取消(事件级 + 固定拼音验证)](../research/ui/results/2026-10-30-qt-ime-fixed-pinyin/README.md),并接入[无修饰导航键与基础鼠标正反拖选高亮](../experiments/shell/results/2026-11-01-r2-core-input/README.md);[基础动态尺寸验证](../experiments/shell/results/2026-10-29-qt-resize/README.md)分离窗口/核心/transport 与真实 child,覆盖极小、重复、隐藏页和关闭交叉,并复验原标签算术及事件循环回收。678 原生极窄失败保留;0ff 仅修订字面子区域/顶层实际区域夹具,另有[冻结包一次隔离原生成功](../experiments/shell/results/2026-10-29-qt-resize/native-validation.json):实际顶层区域、core/transport/child 一致,窗口仅 DPI 96。旧尺寸结果保留各自源码;[Unicode 联合源码的一次完整61项](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json)分开 Qt3项与其余58项既有/独立诊断,13条内部skip保留。正式尺寸事务已改走显式保留入口并绘制独立显示投影/显示光标:[只缩宽的保留行契约](../experiments/shell/results/2026-11-03-qt-retained-display/README.md)在真实 child/固定 ConPTY 核对 12/12 检查,Qt 正式路径 CTest 5/5;旧六阶段 native-sync 失败保留并归类为原生重排网格与保留行合同的坐标差 | 分屏、其他 shell、工作区恢复、同时缩宽+缩高的原版视口/滚动与后续 CUP 证据、完整 reflow、历史浏览/自动滚动、选词/选行与系统复制/粘贴、Find、完整 Unicode 字素/绘制、候选窗与真实 IME 交叉(已有固定拼音;[固定时间点对照](research/qt-ime-candidate-panel-reference-2026-11-01.md)中输出后两侧都保持旧位置、resize 后延后点都更新;字体阶段原版未触发,面板身份未证明,且对照 Qt 源为历史 5fab30b)、真实 DPI、应用兼容与性能仍需实现或迁移;完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 |

自主 C++ 核心已具备双缓冲与限定 `?1049`、DECAWM、DECOM、DECCKM/DECTCEM、每屏 DECSTBM 边距、字符/行插删、ED3/RIS、DSR 6n 和部分 SGR 状态。依据分别见 [1049](../experiments/core/results/2026-10-29-cpp-1049/README.md)、[DECAWM](../experiments/core/results/2026-10-29-cpp-decawm/README.md)及[支持表](../experiments/core/README.md)。[原版每屏 origin 对照](../research/ui/results/2026-11-02-mode-state-reference/README.md)确认备用屏进入时关闭 origin、主屏返回恢复自身状态及固定样本的 margin 下界;核心已改为[每屏保存 origin](../experiments/core/results/2026-11-02-origin-frame/README.md)。区域 LF、IL/DL 后的精确行序,以及[区域自动换行续接、RIS/ED3 历史结构发布](../experiments/core/results/2026-11-02-r27-mode-cross/README.md)已有修正。完整网格/样式、resize 和运输交叉仍由 #27/#29 验收,不能据此声明完整 TUI 通过。

Expand Down
4 changes: 3 additions & 1 deletion experiments/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,12 @@ build/bin/Release/receive.exe 512 edge stream 580000 512 input.bin

## 显示投影续接

显式 `HistoryViewport::begin_projection` / `step_projection` 按每步至多200个源片段生成指定宽度的页面;`ProjectionJob` 独占未完成半行和页面,完成前不发布。源被修改、导入或换屏、真实尺寸变化、新请求及取消会使任务失效;尺寸no-op不打断。它与默认物理`resize`/`snapshot`仍分开,当前Qt未消费这个入口。
显式 `HistoryViewport::begin_projection` / `step_projection` 按每步至多200个源片段生成指定宽度的页面;`ProjectionJob` 独占未完成半行和页面,完成前不发布。源被修改、导入或换屏、真实尺寸变化、新请求及取消会使任务失效;尺寸no-op不打断。它与默认物理`resize`/`snapshot`仍分开;正式 Qt 目前消费单次 `project` 绘制显示页,分步入口仍未被 Qt 使用。

返回cursor是绑定owner/行身份/version/epoch/order的源锚点,可以定义另一宽度下的局部起点,不代表全局显示行号。静止500片段的分步进展和选择/Find交叉见[固定验证](results/2026-11-02-projection-job/README.md);持续输出下的调度与进展、任意显示位置定位仍未交付。旧单次`project`保留有界窗口语义,部分软行在`source_limit`可能不推进,跨预算任务应使用新分步入口。

`display_cursor`(`display_cursor.hpp`)把协议光标这一保留源位置翻译为指定投影页上的显示单元,不移动源光标、不创造源文字;`ViewMapping` 暴露 `columns()`/`sources()`/`layout_revision()` 供消费者校验页面。宽度重排、软续接片段、空硬行、尾部 padding 与宽字后的光标位置由 `display_cursor_core` 锁定;Qt 侧光标/IME 与选择命中消费同一页布局 revision,见 [Qt 保留/投影结果](../shell/results/2026-11-03-qt-retained-display/README.md)。

## 文字投影与选择

`text_document.hpp` 提供同线程、短生命周期的只读适配;返回行文本与跨度均按值持有,以行身份/版本和物理半开列范围表达位置,不向消费方暴露可变行引用。`text_selection.hpp` 为每个会话独立持有选区,合并真实软折行、以 CRLF 复制硬行边界并保留显式空格;不拆开宽字和有限组合标记。
Expand Down
85 changes: 85 additions & 0 deletions experiments/core/display_cursor.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Source-to-display caret mapping for one projected page.
//
// The protocol cursor is a retained source position (fragment row and column).
// A display page reflows complete source atoms at another width, so the caret
// cell is a different coordinate. This header owns only that conversion; it
// never moves the protocol cursor and never invents source content.
#pragma once
#include "history_view.hpp"
#include <optional>

namespace termforge {

struct DisplayCursor {
// The page covered the cursor's source row and produced a display cell.
bool inside = false;
// Display cell: zero-based row within the page and column within its width.
size_t row = 0, column = 0;
};

inline std::optional<DisplayCursor> display_cursor(const Screen& screen, const ViewSnapshot& page) {
if (screen.row < 0 || screen.row >= screen.height()) return std::nullopt;
const size_t retained = screen.history_lines().size() + static_cast<size_t>(screen.row);
if (retained >= screen.retained_size()) return std::nullopt;
const auto& line = screen.retained_line(retained);
const auto& spans = page.mapping.sources();
const size_t columns = page.mapping.columns();
if (!columns) return std::nullopt;
size_t column = static_cast<size_t>((std::max)(screen.column, 0));
column = (std::min)(column, line.size());
const ViewSourceSpan* first = nullptr;
const ViewSourceSpan* last = nullptr;
for (const auto& span : spans) {
if (span.source.begin.line_id != line.id || span.source.begin.version != line.version) continue;
if (!first) first = &span;
last = &span;
}
if (!first) return std::nullopt;
for (const auto& span : spans) {
if (span.source.begin.line_id != line.id || span.source.begin.version != line.version) continue;
if (span.source.begin.column > column) continue;
if (column < span.source.end.column) {
DisplayCursor result;
result.inside = true;
result.row = span.row;
const auto source_width = span.source.end.column - span.source.begin.column;
const auto display_width = span.column_end - span.column_begin;
// A wide or otherwise indivisible display atom keeps its leading
// edge; one-to-one cells preserve the column offset.
result.column = display_width == source_width
? span.column_begin + (column - span.source.begin.column)
: span.column_begin;
return result;
}
if (column == span.source.end.column) {
// The caret sits immediately after this atom: inside the span when
// another span continues on the same display row, otherwise at its
// trailing edge. A trailing edge at the display row width continues
// on the next display row without claiming a source identity there.
DisplayCursor result;
result.inside = true;
result.row = span.row;
result.column = span.column_end;
if (result.column >= columns) {
result.row += result.column / columns;
result.column %= columns;
}
return result;
}
}
// Cursor in untouched trailing padding: the display keeps the same cell
// offsets after the last published atom on that fragment's final row.
DisplayCursor result;
result.inside = true;
result.row = last->row;
result.column = last->column_end + (column - last->source.end.column);
if (result.column >= columns) {
// Synthetic padding ends at the display row edge; the caret continues
// on the next display row without claiming a source identity there.
result.row += result.column / columns;
result.column %= columns;
}
return result;
}

} // namespace termforge
Loading