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
28 changes: 28 additions & 0 deletions experiments/core/edit_csi_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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;
}
}
18 changes: 18 additions & 0 deletions experiments/core/screen.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
15 changes: 15 additions & 0 deletions experiments/core/terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 == ']';
Expand Down
3 changes: 2 additions & 1 deletion experiments/core/test_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
Loading