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
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ add_executable(alternate_mode_test experiments/core/alternate_mode_test.cpp)
target_link_libraries(alternate_mode_test PRIVATE termforge_core)
termforge_warnings(alternate_mode_test)

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

add_executable(paste_test experiments/shell/paste_test.cpp)
termforge_warnings(paste_test)
add_executable(clipboard_policy_test experiments/shell/clipboard_policy_test.cpp)
Expand Down Expand Up @@ -296,6 +300,8 @@ if(BUILD_TESTING)
add_test(NAME search_core COMMAND search_test)
add_test(NAME buffer_state_core COMMAND buffer_state_test)
add_test(NAME alternate_mode_core COMMAND alternate_mode_test)
add_test(NAME decawm_mode_core COMMAND decawm_mode_test)
set_tests_properties(decawm_mode_core PROPERTIES TIMEOUT 10)
add_test(NAME paste COMMAND paste_test)
add_test(NAME clipboard_policy COMMAND clipboard_policy_test)
add_test(NAME keyboard COMMAND keyboard_test)
Expand Down
1 change: 1 addition & 0 deletions docs/EVIDENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

| 配置 | 证据定位 |
|---|---|
| 自主 C++ 主屏 ASCII DECAWM | [有限合同与红绿](../experiments/core/results/2026-10-29-cpp-decawm/README.md)、[固定验证身份](../experiments/core/results/2026-10-29-cpp-decawm/validation.json):抽取原版14–18阶段79B,strict/recovery各235个字面前缀分块用例;初次定向8/9的缺构建依赖失败、补建后的core单项31测试通过分列。跨1049/宽字/resize、origin/margin、GUI/固定ConPTY运输和完整TUI均不由本项验收 |
| 自主 C++ 限定 1049 协议 | [固定结果](../experiments/core/results/2026-10-29-cpp-1049/README.md)、[验证身份](../experiments/core/results/2026-10-29-cpp-1049/validation.json):文字/光标/颜色及 pending-wrap、重复进入与读者失效;55项中Qt1项分列,13条内部skip及旧负例判据失败保留。未验收非颜色属性/全网格背景、其他模式、真实TUI/IME/DPI、呈现或性能 |
| 自主 C++ 双缓冲存储与文档 epoch 基础 | [固定结果](../experiments/core/results/2026-10-29-buffer-state-r2/README.md)、[验证身份](../experiments/core/results/2026-10-29-buffer-state-r2/validation.json):拥有状态、共享 ID、隐藏帧 resize 回滚与跨缓冲句柄失效;Qt/candidate 关闭配置的完整 50 项及 12 条内部 skip 分列。未接 private CSI,不是原版模式、Qt 产品路线或完整 #27 验收 |
| Qt 基础 Unicode 文本字节 | [红绿与联合验证](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、[固定联合身份](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json):公开Qt事件到独立ReadConsoleW接收端,整/分事件代理项、焦点/隐藏/关闭、1024B整包与NUL、两目标隔离。2b6d源码一次61项分列Qt3/其余58,13内部skip、旧红例与27原件保留;不是实际IME、字素/字形、原生Unicode窗口、DPI或性能验收 |
Expand Down
2 changes: 1 addition & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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 色及各会话独立有界队列;[基础动态尺寸验证](../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、真实 DPI、应用兼容与性能仍需实现或迁移;物理裁剪不恢复丢弃正文,完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 |

自主 C++ 核心已有 [#130 双缓冲存储基础](../experiments/core/results/2026-10-29-buffer-state-r2/README.md),并接入 [#135 限定 1049 协议](../experiments/core/results/2026-10-29-cpp-1049/README.md):strict/recovery 识别 `?1049h/l`,重复进入归首格、清旧备用文本和复位写入颜色,返回恢复主屏文字/光标/颜色及 pending-wrap。共享行 ID、双帧物理 resize 与逻辑 epoch 防止旧位置、选择、搜索及 viewport 复活,包括清屏后重写相同文字。原版完整网格/背景、非颜色属性及其他模式的完整行为仍未知;[origin/wrap/margin有限直接观察](research/windterm-origin-wrap-reference-2026-10-29.md)只提供后续实现依据,尚未接入产品。本地55项中 Qt1项与其余54项诊断分列,13条内部跳过保留,不表示完整 [#27](https://github.com/JavaZeroo/termforge/issues/27)、真实 TUI/Qt 交互或系统剪贴板已验收。
自主 C++ 核心已有 [#130 双缓冲存储基础](../experiments/core/results/2026-10-29-buffer-state-r2/README.md),并接入 [#135 限定 1049 协议](../experiments/core/results/2026-10-29-cpp-1049/README.md):strict/recovery 识别 `?1049h/l`,重复进入归首格、清旧备用文本和复位写入颜色,返回恢复主屏文字/光标/颜色及 pending-wrap。共享行 ID、双帧物理 resize 与逻辑 epoch 防止旧位置、选择、搜索及 viewport 复活,包括清屏后重写相同文字。[主屏 ASCII DECAWM 切片 #161](../experiments/core/results/2026-10-29-cpp-decawm/README.md)另按[有限直接观察](research/windterm-origin-wrap-reference-2026-10-29.md)实现 `?7h/l` 与末列开关,验证五阶段79B的字面文字/位置及分块;原版 origin/margin、wrap跨屏/宽字/resize和完整网格/样式仍未知或未实现。#135本地55项中Qt1项与其余54项诊断分列,13条内部跳过保留;#161仅有独立core CLI定向回归,不表示完整 [#27](https://github.com/JavaZeroo/termforge/issues/27)、真实 TUI/Qt 交互或系统剪贴板已验收。

Qt 构建显式准备并部署[固定微软 CRT 14.51.36247.0](../third_party/msvc_runtime/README.md),不安装到 System32。隔离 Windows 的[限定加载与 cmd 验证](../experiments/shell/results/2026-10-29-qt-crt-deploy/README.md)保留原包失败、原 52 文件不变且仅追加 CRT/原许可后的成功,以及新源码包的独立验证;它不解除完整产品打包、Qt/第三方分发审计、实际 DPI/IME 或物理呈现的验收缺口。

Expand Down
5 changes: 3 additions & 2 deletions experiments/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

| 输入 | 当前行为 |
|---|---|
| ASCII `0x20..0x7e` | 单列写入和覆盖;写到最右列时置待换行标志,下一个可打印字符才换行 |
| ASCII `0x20..0x7e` | 单列写入和覆盖;默认自动换行开启,写到最右列时记录末列状态,下一个可打印字符才换行 |
| CR / LF | CR 回到本行第 0 列;LF 下移并保持列号,底部时全屏向上滚动;清除待换行 |
| BS / TAB | BS 左移并限制到第 0 列;TAB 前进到下个 8 列制表位,限制到最右列;清除待换行 |
| `CSI A/B/C/D` | 相对移动,省略或 0 表示 1,限制到屏幕边界 |
Expand All @@ -22,12 +22,13 @@
| UTF-8 | 跨块解码 1–4 字节;严格拒绝过长编码、代理区、越界、孤立续字节及截断输入 |
| `CSI m` | 0 重置、1/22 粗体开关;30–37/40–47 和 90–97/100–107 前景/背景色;39/49 恢复默认色 |
| `CSI ?1049h/l` | [限定文字/光标/颜色合同](results/2026-10-29-cpp-1049/README.md):进入/重复进入归首格、清旧备用文本及复位前景/背景,退出恢复主屏与 pending-wrap;重复进入的逻辑世代使旧读者失效。原版完整网格背景、非颜色属性及其他模式未知 |
| `CSI ?7h/l` | [主屏 ASCII DECAWM 切片](results/2026-10-29-cpp-decawm/README.md):精确单参数开启/关闭;关闭后末列覆盖,关闭时写满末列后重新开启仍可换行。跨1049、宽/结合字及resize组合未完成原版对照 |

状态 JSON 使用 0 起始 `row/column`,每行保留尾部空格,记录 `wrap_pending`、完整可见行、保留的历史行和已淘汰行数。滚动按物理行保存历史,默认 24×80、最多 100 行历史;没有逻辑行重排。搜索由独立只读投影模块执行,解析器不持有查询。清除显示不是清除历史。

解析器在 `feed` 调用之间保留 ESC/CSI 和 UTF-8 状态。SGR 参数最多 16 个,其他命令按语义限制为 1 或 2 个;数值最多 99,999、CSI 参数到终止字符最多 32 字节。尺寸限制为 1–200 行、1–500 列,历史容量 0–10,000;容量为零时只累计淘汰数。

**明确未支持:** 完整字素簇和文字整形、ZWJ/变体选择符/旗帜/肤色修饰序列、256 色/真彩色及其他 SGR 属性、OSC/DCS 的功能、完整备用屏关联模式、滚动区域、插入删除、逻辑行 reflow、1049 以外的模式切换、回复查询及其他控制字节。严格模式下,未支持输入、非法参数或 EOF 时截断的序列使运行失败;不静默忽略,也不输出成功快照。严格模式不支持控制序列内部的 C0 中断/取消;错误后不得继续复用该解析器对象。Recovery 可消费这些未实现功能,但不会执行它们。
**明确未支持:** 完整字素簇和文字整形、ZWJ/变体选择符/旗帜/肤色修饰序列、256 色/真彩色及其他 SGR 属性、OSC/DCS 的功能、完整备用屏关联模式、滚动区域、插入删除、逻辑行 reflow、7/1049 以外的模式切换、回复查询及其他控制字节。严格模式下,未支持输入、非法参数或 EOF 时截断的序列使运行失败;不静默忽略,也不输出成功快照。严格模式不支持控制序列内部的 C0 中断/取消;错误后不得继续复用该解析器对象。Recovery 可消费这些未实现功能,但不会执行它们。

## 字符宽度与单元格契约

Expand Down
175 changes: 175 additions & 0 deletions experiments/core/decawm_mode_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#include "terminal.hpp"
#include "text_projection.hpp"
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

using namespace termforge;

static void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}

// WindTerm phase 15: the two source letters have no intervening DSR.
static void disabled_edge_overwrites(ParseMode mode) {
Parser parser(25, 91, 2, mode);
parser.feed("\x1b[?7l\x1b[10;91HCD");
parser.finish();
const auto& screen = parser.screen;
check(screen.row == 9 && screen.column == 90,
"disabled edge CD must end at (10,91)");
check(project_line(screen.lines[9]).text == std::string(90, ' ') + "D" &&
project_line(screen.lines[10]).text.empty() && screen.history.empty(),
"disabled edge CD must retain only D without a wrapped successor");
}

struct ObservedPhase {
const char* source;
int cursor_row, cursor_column, first_row;
const char* first_text;
const char* second_text;
bool wrapped;
};

// Extracted phases 14..18, excluding DSR queries and the unsupported phase-13
// margin reset. Coordinates and logical text are independent WindTerm facts;
// physical rows/continuation links below are this Screen's engineering model.
static const std::vector<ObservedPhase> phases{
{"\x1b[?7h\x1b[8;91HAB", 9, 2, 7, "A", "B", true},
{"\x1b[?7l\x1b[10;91HCD", 10, 91, 9, "D", "", false},
{"\x1b[?7h\x1b[12;91HEF", 13, 2, 11, "E", "F", true},
{"\x1b[14;91HG\x1b[?7lH", 14, 91, 13, "H", "", false},
{"\x1b[?7l\x1b[16;91HI\x1b[?7hJ", 17, 2, 15, "I", "J", true},
};

static void observed_prefix(ParseMode mode, size_t count, size_t split, bool bytewise) {
std::string source;
for (size_t index = 0; index < count; ++index) source += phases[index].source;
Parser parser(25, 91, 2, mode);
if (bytewise) for (const auto byte : source) parser.feed(std::string_view(&byte, 1));
else {
parser.feed(std::string_view(source).substr(0, split));
parser.feed(std::string_view(source).substr(split));
}
parser.finish();
const auto& screen = parser.screen;
const auto& last = phases[count - 1];
check(screen.row + 1 == last.cursor_row && screen.column + 1 == last.cursor_column,
"observed prefix cursor differs from its literal DSR coordinates");
check(screen.active_buffer() == ScreenBuffer::primary && screen.history.empty() &&
screen.discarded == 0 && parser.diagnostics().ignored_sequences == 0,
"a supported main-screen source changed buffers/history or was ignored");
for (size_t index = 0; index < count; ++index) {
const auto& phase = phases[index];
const auto& first = screen.lines[phase.first_row];
const auto& second = screen.lines[phase.first_row + 1];
const auto first_projection = project_line(first).text;
const auto second_projection = project_line(second).text;
check(first_projection == std::string(90, ' ') + phase.first_text &&
second_projection == phase.second_text,
"observed prefix text differs from independent literal text");
check(continues_into(first, second) == phase.wrapped &&
first.ending == (phase.wrapped ? LineEnding::soft_wrap : LineEnding::none) &&
first.wrap_columns == (phase.wrapped ? 91u : 0u),
"the model recorded a wrap without an actual continuation, or lost a real wrap");
}
}

static void observed_sources_across_chunks(ParseMode mode) {
size_t prefix_bytes = 0, cases = 0;
for (size_t count = 1; count <= phases.size(); ++count) {
prefix_bytes += std::string_view(phases[count - 1].source).size();
for (size_t split = 0; split <= prefix_bytes; ++split) {
observed_prefix(mode, count, split, false); ++cases;
}
observed_prefix(mode, count, 0, true); ++cases;
}
std::cout << "observed extracted source: " << prefix_bytes << " bytes, " << cases
<< " literal-prefix chunk cases\n";
}

static void private_commands_are_atomic(ParseMode mode) {
const std::vector<std::string> unsupported{
"\x1b[?7;1049h", "\x1b[?1049;7h", "\x1b[?7;7l", "\x1b[?7;l",
"\x1b[?7;9999l", "\x1b[?9999h", "\x1b[?7m", "\x1b[?7 h",
"\x1b[?7:1h", "\x1b[??7l", "\x1b[0?7l",
std::string("\x1b[?") + std::string(33, '0') + "7l"};
for (const auto& sequence : unsupported) {
Parser parser(25, 91, 2, mode);
parser.feed("\x1b[10;91H");
bool rejected = false;
try { parser.feed(sequence); }
catch (const std::exception&) { rejected = true; }
if (mode == ParseMode::strict) {
check(rejected && parser.screen.active_buffer() == ScreenBuffer::primary &&
parser.screen.row == 9 && parser.screen.column == 90 &&
project_line(parser.screen.lines[9]).text.empty(),
"strict private input was accepted or partially changed visible state");
} else {
check(!rejected, "recovery failed instead of consuming unsupported private input");
parser.feed("CD"); parser.finish();
check(parser.screen.active_buffer() == ScreenBuffer::primary &&
parser.screen.row == 10 && parser.screen.column == 1 &&
project_line(parser.screen.lines[9]).text == std::string(90, ' ') + "C" &&
project_line(parser.screen.lines[10]).text == "D" &&
parser.diagnostics().ignored_sequences == 1,
"recovery partially applied private modes or contaminated the next printable bytes");
}
}
}

static void recovery_drops_incomplete_mode_commands() {
for (const auto cancellation : {char(0x18), char(0x1a), char(0x1b)}) {
Parser parser(25, 91, 2, ParseMode::recovery);
parser.feed(std::string("\x1b[?7") + cancellation + "\x1b[10;91HCD");
parser.finish();
check(parser.screen.row == 10 && parser.screen.column == 1 &&
project_line(parser.screen.lines[10]).text == "D",
"cancellation or ESC resynchronization applied an incomplete private mode");
}
Parser parser(25, 91, 2, ParseMode::recovery);
parser.feed("\x1b[?7"); parser.finish();
parser.feed("\x1b[10;91HCD"); parser.finish();
check(parser.screen.row == 10 && parser.screen.column == 1 &&
parser.diagnostics().truncated_sequences == 1 &&
project_line(parser.screen.lines[10]).text == "D",
"EOF truncation applied a private mode or contaminated the next CSI");
}

// Safety/compatibility only: off x wide text was not observed in WindTerm.
// Preserve the pre-existing insufficient-space wrap without replacing content.
static void wide_fit_policy_is_preserved(ParseMode mode) {
Parser parser(3, 3, 2, mode);
parser.feed("\x1b[?7l\x1b[1;3H\xe4\xb8\xad");
parser.finish();
check(parser.screen.row == 1 && parser.screen.column == 2 &&
parser.screen.lines[1].size() == 3 &&
parser.screen.lines[1][0].text == "\xe4\xb8\xad" &&
parser.screen.lines[1][0].width == 2 && parser.screen.lines[1][1].width == 0 &&
parser.screen.lines[1][2].text == " " &&
project_line(parser.screen.lines[1]).text == "\xe4\xb8\xad" &&
parser.diagnostics().replaced_scalars == 0,
"wide scalar fit fallback replaced text, overflowed a row, or lost its trailing cell");
}

int main() {
int failures = 0, checks = 0;
const auto run = [&](const std::string& name, const auto& action) {
++checks;
try { action(); std::cout << name << " passed\n"; }
catch (const std::exception& error) {
++failures; std::cerr << name << " failed: " << error.what() << '\n';
}
};
for (const auto mode : {ParseMode::strict, ParseMode::recovery}) {
const std::string name = mode == ParseMode::strict ? "strict" : "recovery";
run(name + " disabled edge", [&] { disabled_edge_overwrites(mode); });
run(name + " observed chunks", [&] { observed_sources_across_chunks(mode); });
run(name + " private guards", [&] { private_commands_are_atomic(mode); });
run(name + " wide fit safety", [&] { wide_fit_policy_is_preserved(mode); });
}
run("recovery cancellation/truncation", recovery_drops_incomplete_mode_commands);
std::cout << "DECAWM checks: " << (checks - failures) << '/' << checks << " passed\n";
return failures ? 1 : 0;
}
Loading