diff --git a/experiments/core/edit_csi_test.cpp b/experiments/core/edit_csi_test.cpp index 38c26cd4..ff7ee037 100644 --- a/experiments/core/edit_csi_test.cpp +++ b/experiments/core/edit_csi_test.cpp @@ -42,6 +42,28 @@ int main() { parser.feed("\x1b[1;1H"); parser.feed("\x1b[99@"); require(row_text(parser.screen, 0).empty(), "over-wide ICH should clear the row"); + + // IL opens blank rows at the cursor and lets the bottom rows fall off. + Parser lines(4, 10, 10, termforge::ParseMode::recovery); + lines.feed("a\r\nb\r\nc\r\nd"); + lines.feed("\x1b[1;1H"); + lines.feed("\x1b[2L"); + require(row_text(lines.screen, 0).empty() && row_text(lines.screen, 1).empty() && + row_text(lines.screen, 2) == "a" && row_text(lines.screen, 3) == "b", + "IL did not open blank rows"); + require(lines.screen.row == 0 && lines.screen.column == 0, "IL moved the cursor"); + + // DL closes rows from the cursor and appends blanks at the bottom. + lines.feed("\x1b[M"); + require(row_text(lines.screen, 0).empty() && row_text(lines.screen, 1) == "a" && + row_text(lines.screen, 2) == "b" && row_text(lines.screen, 3).empty(), + "DL did not close a row"); + + // A count past the bottom only removes what remains. + lines.feed("\x1b[99L"); + require(row_text(lines.screen, 0).empty() && row_text(lines.screen, 1).empty() && + row_text(lines.screen, 2).empty() && row_text(lines.screen, 3).empty(), + "over-large IL should clear the screen"); std::puts("edit csi contracts passed"); return 0; } diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index e2c7ac8c..affaad39 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -438,6 +438,26 @@ class Screen { // Microsoft Terminal ECH contract explicitly cancels delayed wrapping. wrap_pending = false; invalidate_attachment(); } + void insert_lines(int count) { + if (count < 1) throw InputError("invalid insert line count"); + const int insert = (std::min)(count, height() - row); + if (insert <= 0) { wrap_pending = false; return; } + // Rows pushed past the bottom leave the screen; their identities stay + // spent. Inserted rows are new lines and get fresh identities. + for (int r = height() - 1; r >= row + insert; --r) lines[r] = std::move(lines[r - insert]); + for (int r = row; r < row + insert; ++r) lines[r] = make_line(width(), Cell{}, next_id_); + wrap_pending = false; + invalidate_attachment(); + } + void delete_lines(int count) { + if (count < 1) throw InputError("invalid delete line count"); + const int remove = (std::min)(count, height() - row); + if (remove <= 0) { wrap_pending = false; return; } + for (int r = row; r < height() - remove; ++r) lines[r] = std::move(lines[r + remove]); + for (int r = height() - remove; r < height(); ++r) lines[r] = make_line(width(), Cell{}, next_id_); + wrap_pending = false; + invalidate_attachment(); + } void insert_characters(int count) { if (count < 1) throw InputError("invalid insert character count"); const int shift = (std::min)(count, width() - column); diff --git a/experiments/core/terminal.hpp b/experiments/core/terminal.hpp index 4f6773fa..c0f523d0 100644 --- a/experiments/core/terminal.hpp +++ b/experiments/core/terminal.hpp @@ -72,6 +72,8 @@ class Parser { case 'H': case 'f': screen.move(n - 1, params.size() < 2 || params[1] == 0 ? 0 : params[1] - 1); break; case 'J': screen.erase_display(params[0]); break; case 'K': screen.erase_line(params[0]); break; + case 'L': screen.insert_lines(n); break; + case 'M': screen.delete_lines(n); break; case 'X': screen.erase_characters(n); break; case '@': screen.insert_characters(n); break; case 'P': screen.delete_characters(n); break;