From 0c9418f1cbfc3d08cd74c65e9a71ac742dfa8214 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:58:34 +0800 Subject: [PATCH 1/2] wip(core): ED3 and RIS with tests (test crash to fix) refs #29 Screen gains reset/erase_scrollback/scrollback_lines and the parser handles ESC c; the new ED3/RIS assertions still crash on the RIS case. --- experiments/core/edit_csi_test.cpp | 23 +++++++++++++++++++++++ experiments/core/screen.hpp | 18 ++++++++++++++++++ experiments/core/terminal.hpp | 8 ++++++++ 3 files changed, 49 insertions(+) diff --git a/experiments/core/edit_csi_test.cpp b/experiments/core/edit_csi_test.cpp index 840ff58b..da651b59 100644 --- a/experiments/core/edit_csi_test.cpp +++ b/experiments/core/edit_csi_test.cpp @@ -75,6 +75,29 @@ int main() { row_text(region.screen, 2).empty() && row_text(region.screen, 3) == "d", "region scroll changed rows outside the margins"); require(region.screen.row == 2, "region scroll moved the cursor past the margin"); + // ED3 empties the scrollback without touching the visible rows. + { + Parser scrollback(2, 6, 10, termforge::ParseMode::recovery); + scrollback.feed("a\r\nb\r\nc"); + require(scrollback.screen.scrollback_lines() > 0, "no scrollback was produced"); + scrollback.feed("\x1b[3J"); + require(scrollback.screen.scrollback_lines() == 0, "ED3 left scrollback behind"); + require(row_text(scrollback.screen, 0) == "b" && row_text(scrollback.screen, 1) == "c", + "ED3 changed the visible screen"); + } + // RIS returns attributes, cursor and modes to their start state. + { + Parser reset(2, 6, 10, termforge::ParseMode::recovery); + reset.feed("\x1b[31;1mX\x1b[?25l\x1b[?1h"); + require(!reset.screen.visible_lines()[0][0].style.plain(), "test setup lost the style"); + reset.feed("\x1b" "c"); + require(reset.screen.row == 0 && reset.screen.column == 0, "RIS did not home the cursor"); + require(reset.cursor_visible() && !reset.application_cursor_keys(), "RIS did not reset the modes"); + require(row_text(reset.screen, 0).empty(), "RIS did not clear the screen"); + reset.feed("Y"); + require(reset.screen.visible_lines()[0][0].style.plain() && + reset.screen.visible_lines()[0][0].style.fg == -1, "RIS did not reset attributes"); + } std::puts("edit csi contracts passed"); return 0; } diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index 827a5cc8..b21472ce 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -515,7 +515,25 @@ class Screen { erase_range(line, width() - shift, width()); wrap_pending = false; invalidate_attachment(); } + void reset() { + // RIS: every mode, attribute, region and buffer returns to its start + // state; the identities already spent stay spent. + for (int r = 0; r < height(); ++r) erase_range(lines[r], 0, width()); + history.clear(); + style = Style{}; + row = column = 0; + wrap_pending = false; + autowrap_enabled_ = true; + activate_buffer(ScreenBuffer::primary); + invalidate_attachment(); + } + size_t scrollback_lines() const { return history.size(); } + void erase_scrollback() { + // ED3 drops scrollback only; the visible screen is untouched. + if (!history.empty() || discarded) { history.clear(); discarded = 0; } + } void erase_display(int mode) { + if (mode == 3) { erase_scrollback(); return; } if (mode > 2) throw InputError("unsupported erase mode"); for (int r = 0; r < height(); ++r) if (mode == 2 || (mode == 0 && r > row) || (mode == 1 && r < row)) erase_range(lines[r], 0, width()); diff --git a/experiments/core/terminal.hpp b/experiments/core/terminal.hpp index 137d5cdd..43f0d216 100644 --- a/experiments/core/terminal.hpp +++ b/experiments/core/terminal.hpp @@ -106,6 +106,14 @@ class Parser { continue; } if (state == State::escape) { + // RIS resets the parser-owned modes too, so it is handled here + // rather than inside Screen. + if (c == 'c') { + screen.reset(); + mode_bits_ = 1u << 1; + state = State::ground; + continue; + } if (c != '[') throw std::runtime_error("unsupported escape"); state = State::csi; params.assign(1, 0); sequence_bytes = 0; private_csi = false; continue; } From a363a4c5fec51b245e6bbc67da3d90d2f7a2ea96 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Fri, 30 Oct 2026 13:00:21 +0800 Subject: [PATCH 2/2] feat(core): implement ED3 scrollback erase and RIS refs #29 Screen gains reset, erase_scrollback and a scrollback accessor; RIS runs on both parser paths and the edit_csi_test main now reports failures instead of aborting. --- experiments/core/edit_csi_test.cpp | 5 +++++ experiments/core/terminal.hpp | 7 +++++++ experiments/core/test_recovery.py | 3 ++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/experiments/core/edit_csi_test.cpp b/experiments/core/edit_csi_test.cpp index da651b59..3a4ecdb6 100644 --- a/experiments/core/edit_csi_test.cpp +++ b/experiments/core/edit_csi_test.cpp @@ -16,6 +16,7 @@ std::string row_text(const termforge::Screen& screen, int row) { } // namespace int main() { + try { using termforge::Parser; Parser parser(3, 10, 10, termforge::ParseMode::recovery); parser.feed("abcdef"); @@ -100,4 +101,8 @@ int main() { } std::puts("edit csi contracts passed"); return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "edit csi contract failed: %s\n", error.what()); + return 1; + } } diff --git a/experiments/core/terminal.hpp b/experiments/core/terminal.hpp index 43f0d216..98bbd5ec 100644 --- a/experiments/core/terminal.hpp +++ b/experiments/core/terminal.hpp @@ -212,6 +212,13 @@ class Parser { state = State::csi; sequence_bytes = 0; params.assign(1, 0); stats.max_parameters = (std::max)(stats.max_parameters, params.size()); return; } + if (state == State::escape && c == 'c') { + // RIS also works on the recovery path the product uses. + screen.reset(); + mode_bits_ = 1u << 1; + reset_sequence(); + return; + } if (state == State::escape && (c == ']' || c == 'P' || c == 'X' || c == '^' || c == '_')) { count(stats.ignored_sequences); osc = c == ']'; diff --git a/experiments/core/test_recovery.py b/experiments/core/test_recovery.py index ba670591..2fcbc036 100644 --- a/experiments/core/test_recovery.py +++ b/experiments/core/test_recovery.py @@ -147,7 +147,8 @@ def test_known_alternate_mode_is_not_counted_as_ignored(self): stats={"ignored_sequences": 0}) def test_invalid_parameter_does_not_apply_prefix(self): - self.expect(b"A\x1b[2;3;4H\x1b[3J\x1b[3KZ", "AZ", stats={"ignored_sequences": 3}) + # ED3 (CSI 3J) is implemented now, so only the bad CUP and CSI 3K count. + self.expect(b"A\x1b[2;3;4H\x1b[3J\x1b[3KZ", "AZ", stats={"ignored_sequences": 2}) def test_invalid_sgr_is_atomic(self): for data in (b"\x1b[1;999mX", b"\x1b[31m\x1b[0;999mX"):