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
37 changes: 35 additions & 2 deletions experiments/core/edit_csi_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,30 @@ int main() {
row_text(margin.screen, 2).empty() && row_text(margin.screen, 3) == "d",
"DL ignored the scroll margins");

// Margins belong to the buffer: a 1049 switch keeps each buffer's region.
{
Parser buffers(4, 10, 10, termforge::ParseMode::recovery);
buffers.feed("\x1b[2;3r");
require(buffers.screen.region_top() == 1 && buffers.screen.region_bottom() == 2,
"primary region was not set");
buffers.feed("\x1b[?1049h");
require(buffers.screen.region_top() == 0 && buffers.screen.region_bottom() == 3,
"alternate buffer inherited the primary region");
buffers.feed("\x1b[1;2r");
require(buffers.screen.region_top() == 0 && buffers.screen.region_bottom() == 1,
"alternate region was not set");
buffers.feed("\x1b[?1049l");
require(buffers.screen.region_top() == 1 && buffers.screen.region_bottom() == 2,
"primary region was lost across the switch");
}
// A resize drops the margins: scrolling afterwards reaches history again.
{
Parser resized(4, 10, 10, termforge::ParseMode::recovery);
resized.feed("a\r\nb\r\nc\r\nd");
resized.feed("\x1b[2;3r");
require(resized.screen.scroll_bottom() == 2, "test setup lost the region");
require(resized.screen.region_bottom() == 2, "test setup lost the region");
resized.screen.resize(4, 12);
require(resized.screen.scroll_bottom() == 3, "resize kept the stale region bottom");
require(resized.screen.region_bottom() == 3, "resize kept the stale region bottom");
resized.feed("\x1b[4;1H");
resized.feed("\n");
require(resized.screen.scrollback_lines() > 0,
Expand Down Expand Up @@ -148,6 +164,23 @@ int main() {
require(row_text(insert.screen, 0) == "abcdQ" && row_text(insert.screen, 1).empty(),
"text after a cancelled ICH wrap did not stay on the row");
}
// The 175-byte margin source from experiments/shell/margin_checks.py, with
// its lone line feeds translated to CRLF the way a console does on output.
// The core must land on the "required" screen, not the ConPTY "observed" one.
{
Parser margin(24, 80, 100, termforge::ParseMode::recovery);
margin.feed("\x1b[2J\x1b[HTOP_FIXED\r\nREGION_A\r\nREGION_B\r\nBOTTOM_FIXED\r\n\x1b[2;3r");
margin.feed("\x1b[?1049hALT_OUTSIDE\r\n\x1b[4;1HALT_A\r\nALT_B\x1b[4;5r\x1b[5;1H\r\nALT_C\r\nALT_D");
margin.feed("\x1b[?1049l");
margin.feed("\x1b[3;1H\r\nREGION_C\r\nREGION_D\x1b[6;1HTF_MARGIN_DONE\r\n");
const std::string expected[] = {"TOP_FIXED", "REGION_C", "REGION_D", "BOTTOM_FIXED", "",
"TF_MARGIN_DONE"};
for (int index = 0; index < 6; ++index)
require(row_text(margin.screen, index) == expected[index],
"margin source produced the ConPTY-observed screen");
require(margin.screen.row == 6 && margin.screen.column == 0,
"margin source cursor mismatch");
}
std::puts("edit csi contracts passed");
return 0;
} catch (const std::exception& error) {
Expand Down
48 changes: 29 additions & 19 deletions experiments/core/screen.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,13 @@ class Screen {
// True only while the retained sequence is known to be ordered by id.
// False is the safe fallback: lookups scan instead of bisecting.
bool ordered_ = true;
// DECSTBM margins belong to the frame: the primary and alternate buffers
// keep their own scrolling region across a 1049 switch.
int scroll_top = 0;
int scroll_bottom = -1;
template<class State> void swap(State& other) noexcept {
std::swap(row, other.row); std::swap(column, other.column);
std::swap(scroll_top, other.scroll_top); std::swap(scroll_bottom, other.scroll_bottom);
std::swap(last_row, other.last_row); std::swap(last_column, other.last_column);
std::swap(wrap_pending, other.wrap_pending); std::swap(discarded, other.discarded);
std::swap(style, other.style);
Expand All @@ -81,18 +86,19 @@ class Screen {
std::deque<Line> history;
int columns_;
uint64_t next_id_ = 1;
int scroll_top_ = 0;
// Negative means "the bottom row", which keeps full-screen scrolling on the
// history path after a resize changes the height.
int scroll_bottom_ = -1;
// DECSTBM margins travel with the buffer through active_state_copy/swap.
// Negative bottom means "the last row", which survives a height change.
int scroll_top = 0;
int scroll_bottom = -1;
uint64_t buffer_epoch_ = 0;
uint64_t order_revision_ = 0;
bool ordered_ = true;
int last_row = -1, last_column = -1;
ScreenBuffer active_buffer_ = ScreenBuffer::primary;
std::optional<FrameState> inactive_;
FrameState active_state_copy() const {
return {row, column, last_row, last_column, wrap_pending, discarded, style, lines, history, ordered_};
return {row, column, last_row, last_column, wrap_pending, discarded, style, lines, history, ordered_,
scroll_top, scroll_bottom};
}
void swap_active(FrameState& state) noexcept {
state.swap(*this);
Expand Down Expand Up @@ -382,16 +388,18 @@ class Screen {
if (hidden) inactive_->swap(*hidden);
columns_ = columns; next_id_ = identity;
// The reference terminal drops the scrolling margins on resize, so a
// stale bottom could point past the new height.
scroll_top_ = 0; scroll_bottom_ = -1;
// stale bottom could point past the new height. Both frames reset.
scroll_top = 0; scroll_bottom = -1;
if (inactive_) { inactive_->scroll_top = 0; inactive_->scroll_bottom = -1; }
advance_order(ordered_);
}
void invalidate_attachment() { last_row = last_column = -1; }
void move(int r, int c) {
row = std::clamp(r, 0, height() - 1); column = std::clamp(c, 0, width() - 1);
wrap_pending = false; invalidate_attachment();
}
int scroll_bottom() const { return scroll_bottom_ < 0 ? height() - 1 : scroll_bottom_; }
int region_bottom() const { return scroll_bottom < 0 ? height() - 1 : scroll_bottom; }
int region_top() const { return scroll_top; }
// DECSTBM: rows inside [top, bottom] scroll together. The list itself keeps
// whole rows only, so a partial region scroll shifts entries in place; rows
// that leave a full-screen region still reach history.
Expand All @@ -401,7 +409,7 @@ class Screen {
// An invalid region is ignored rather than rejected: the reference
// terminal logs and returns, leaving the previous margins in place.
if (bottom - top < 1) return;
scroll_top_ = top; scroll_bottom_ = bottom;
scroll_top = top; scroll_bottom = bottom;
// DECSTBM homes the cursor to the screen origin, matching the reference
// terminal (vendor/alacritty_terminal/src/term/mod.rs: goto(0, 0)).
move(0, 0);
Expand All @@ -414,17 +422,19 @@ class Screen {
// A newly allocated bottom row is known before moving its predecessor.
previous.next_id = automatic ? (row + 1 < height() ? lines[row + 1].id : next_id_) : 0;
wrap_pending = false; invalidate_attachment();
if (row + 1 < height() && row < scroll_bottom()) { ++row; return; }
if (row < scroll_bottom() || scroll_top_ > 0 || scroll_bottom() < height() - 1) {
// Scrolling inside a region (or inside a movement that stops early)
// moves rows up within the margins and blanks the last region row.
for (int r = scroll_top_; r < scroll_bottom(); ++r) lines[r] = std::move(lines[r + 1]);
lines[scroll_bottom()] = make_line(width(), Cell{}, next_id_);
row = scroll_bottom();
const int bottom = region_bottom();
// Only the bottom margin scrolls; rows above it move down normally and
// rows below it must not disturb the region at all.
if (row < bottom) { ++row; return; }
if (row > bottom) { if (row + 1 < height()) ++row; return; }
if (region_top() > 0 || bottom < height() - 1) {
// Scrolling inside the margins moves rows up and blanks the last one.
for (int r = region_top(); r < bottom; ++r) lines[r] = std::move(lines[r + 1]);
lines[bottom] = make_line(width(), Cell{}, next_id_);
row = bottom;
advance_order(ordered_);
return;
}
if (row < height() - 1) { ++row; return; }
const auto limit = active_history_limit();
if (limit) {
history.push_back(std::move(lines.front()));
Expand Down Expand Up @@ -472,7 +482,7 @@ class Screen {
}
void insert_lines(int count) {
if (count < 1) throw InputError("invalid insert line count");
const int bottom = scroll_bottom();
const int bottom = region_bottom();
const int insert = (std::min)(count, bottom + 1 - row);
if (insert <= 0) { wrap_pending = false; return; }
// Rows pushed past the bottom margin leave the screen; their identities
Expand All @@ -484,7 +494,7 @@ class Screen {
}
void delete_lines(int count) {
if (count < 1) throw InputError("invalid delete line count");
const int bottom = scroll_bottom();
const int bottom = region_bottom();
const int remove = (std::min)(count, bottom + 1 - row);
if (remove <= 0) { wrap_pending = false; return; }
for (int r = row; r <= bottom - remove; ++r) lines[r] = std::move(lines[r + remove]);
Expand Down