diff --git a/experiments/core/edit_csi_test.cpp b/experiments/core/edit_csi_test.cpp index 840ff58b..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"); @@ -75,6 +76,33 @@ 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; + } catch (const std::exception& error) { + std::fprintf(stderr, "edit csi contract failed: %s\n", error.what()); + return 1; + } } 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..98bbd5ec 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; } @@ -204,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"):