refactor(terminal): log scroll region as tuple rather than Option<tuple> (#4082)

* initial go over

* do not pad scroll region by default

* some cleanups

* moar cleanups

* style(fmt): rustfmt

* docs(changelog): add PR
This commit is contained in:
Aram Drevekenin 2025-03-22 01:57:17 +01:00 committed by GitHub
parent 5081352b54
commit 503e20132a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 175 additions and 248 deletions

View file

@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## [Unreleased]
* refactor(terminal): track scroll_region as tuple rather than Option (https://github.com/zellij-org/zellij/pull/4082)
## [0.42.1] - 2025-03-21
* fix(mouse): fix mouse handling in windows terminal (https://github.com/zellij-org/zellij/pull/4076)

View file

@ -320,11 +320,7 @@ pub struct Grid {
cursor: Cursor,
cursor_is_hidden: bool,
saved_cursor_position: Option<Cursor>,
// FIXME: change scroll_region to be (usize, usize) - where the top line is always the first
// line of the viewport and the bottom line the last unless it's changed with CSI r and friends
// Having it as an Option causes lots of complexity and issues, and according to DECSTBM, this
// should be the behaviour anyway
scroll_region: Option<(usize, usize)>,
scroll_region: (usize, usize),
active_charset: CharsetIndex,
preceding_char: Option<TerminalCharacter>,
terminal_emulator_colors: Rc<RefCell<Palette>>,
@ -511,7 +507,7 @@ impl Grid {
cursor: Cursor::new(0, 0, styled_underlines),
cursor_is_hidden: false,
saved_cursor_position: None,
scroll_region: None,
scroll_region: (0, rows.saturating_sub(1)),
preceding_char: None,
width: columns,
height: rows,
@ -1033,9 +1029,7 @@ impl Grid {
}
self.height = new_rows;
self.width = new_columns;
if self.scroll_region.is_some() {
self.set_scroll_region_to_viewport_size();
}
self.set_scroll_region_to_viewport_size();
self.scrollback_buffer_lines = self.recalculate_scrollback_buffer_count();
self.search_results.selections.clear();
self.search_viewport();
@ -1254,44 +1248,36 @@ impl Grid {
}
}
pub fn rotate_scroll_region_up(&mut self, count: usize) {
if let Some((scroll_region_top, scroll_region_bottom)) = self
.scroll_region
.or_else(|| Some((0, self.height.saturating_sub(1))))
{
self.pad_lines_until(scroll_region_bottom, EMPTY_TERMINAL_CHARACTER);
for _ in 0..count {
if self.cursor.y >= scroll_region_top && self.cursor.y <= scroll_region_bottom {
if self.viewport.get(scroll_region_bottom).is_some() {
self.viewport.remove(scroll_region_bottom);
}
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
let columns = VecDeque::from(vec![pad_character; self.width]);
self.viewport
.insert(scroll_region_top, Row::from_columns(columns).canonical());
let (scroll_region_top, scroll_region_bottom) = self.scroll_region;
self.pad_lines_until(scroll_region_bottom, EMPTY_TERMINAL_CHARACTER);
for _ in 0..count {
if self.cursor.y >= scroll_region_top && self.cursor.y <= scroll_region_bottom {
if self.viewport.get(scroll_region_bottom).is_some() {
self.viewport.remove(scroll_region_bottom);
}
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
let columns = VecDeque::from(vec![pad_character; self.width]);
self.viewport
.insert(scroll_region_top, Row::from_columns(columns).canonical());
}
self.output_buffer.update_all_lines(); // TODO: only update scroll region lines
}
self.output_buffer.update_all_lines(); // TODO: only update scroll region lines
}
pub fn rotate_scroll_region_down(&mut self, count: usize) {
if let Some((scroll_region_top, scroll_region_bottom)) = self
.scroll_region
.or_else(|| Some((0, self.height.saturating_sub(1))))
{
self.pad_lines_until(scroll_region_bottom, EMPTY_TERMINAL_CHARACTER);
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
for _ in 0..count {
if scroll_region_top < self.viewport.len() {
self.viewport.remove(scroll_region_top);
}
let columns = VecDeque::from(vec![pad_character.clone(); self.width]);
self.viewport
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
let (scroll_region_top, scroll_region_bottom) = self.scroll_region;
self.pad_lines_until(scroll_region_bottom, EMPTY_TERMINAL_CHARACTER);
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
for _ in 0..count {
if scroll_region_top < self.viewport.len() {
self.viewport.remove(scroll_region_top);
}
self.output_buffer.update_all_lines(); // TODO: only update scroll region lines
let columns = VecDeque::from(vec![pad_character.clone(); self.width]);
self.viewport
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
}
self.output_buffer.update_all_lines(); // TODO: only update scroll region lines
}
pub fn fill_viewport(&mut self, character: TerminalCharacter) {
if self.alternate_screen_state.is_some() {
@ -1307,45 +1293,38 @@ impl Grid {
self.output_buffer.update_all_lines();
}
pub fn add_canonical_line(&mut self) {
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
if self.cursor.y == scroll_region_bottom {
// end of scroll region
// when we have a scroll region set and we're at its bottom
// we need to delete its first line, thus shifting all lines in it upwards
// then we add an empty line at its end which will be filled by the application
// controlling the scroll region (presumably filled by whatever comes next in the
// scroll buffer, but that's not something we control)
if scroll_region_top >= self.viewport.len() {
// the state is corrupted
return;
}
if scroll_region_bottom == self.height - 1 && scroll_region_top == 0 {
if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1);
} else {
self.viewport.remove(0);
}
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
let columns = VecDeque::from(vec![pad_character; self.width]);
self.viewport.push(Row::from_columns(columns).canonical());
self.selection.move_up(1);
} else {
self.viewport.remove(scroll_region_top);
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
let columns = VecDeque::from(vec![pad_character; self.width]);
if self.viewport.len() >= scroll_region_bottom {
self.viewport
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
} else {
self.viewport.push(Row::from_columns(columns).canonical());
}
}
self.output_buffer.update_all_lines(); // TODO: only update scroll region lines
let (scroll_region_top, scroll_region_bottom) = self.scroll_region;
if self.cursor.y == scroll_region_bottom {
// end of scroll region
// when we have a scroll region set and we're at its bottom
// we need to delete its first line, thus shifting all lines in it upwards
// then we add an empty line at its end which will be filled by the application
// controlling the scroll region (presumably filled by whatever comes next in the
// scroll buffer, but that's not something we control)
if scroll_region_top >= self.viewport.len() {
// the state is corrupted
return;
}
if scroll_region_bottom == self.height - 1 && scroll_region_top == 0 {
if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1);
} else {
self.viewport.remove(0);
}
self.viewport.push(Row::new().canonical());
self.selection.move_up(1);
} else {
self.viewport.remove(scroll_region_top);
if self.viewport.len() >= scroll_region_bottom {
self.viewport
.insert(scroll_region_bottom, Row::new().canonical());
} else {
self.viewport.push(Row::new().canonical());
}
}
self.output_buffer.update_all_lines(); // TODO: only update scroll region lines
return;
}
if self.viewport.len() <= self.cursor.y + 1 {
// FIXME: this should add an empty line with the pad_character
@ -1355,16 +1334,6 @@ impl Grid {
self.viewport.push(new_row);
}
if self.cursor.y == self.height.saturating_sub(1) {
if self.scroll_region.is_none() {
if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1);
} else {
self.sixel_grid.offset_grid_top();
self.viewport.remove(0);
}
self.selection.move_up(1);
}
self.output_buffer.update_all_lines();
} else {
self.cursor.y += 1;
@ -1550,37 +1519,26 @@ impl Grid {
}
}
pub fn move_cursor_to(&mut self, x: usize, y: usize, pad_character: TerminalCharacter) {
match self.scroll_region {
Some((scroll_region_top, scroll_region_bottom)) => {
self.cursor.x = std::cmp::min(self.width - 1, x);
let y_offset = if self.erasure_mode {
scroll_region_top
} else {
0
};
if y >= scroll_region_top && y <= scroll_region_bottom {
self.cursor.y = std::cmp::min(scroll_region_bottom, y + y_offset);
} else {
self.cursor.y = std::cmp::min(self.height - 1, y + y_offset);
}
self.pad_lines_until(self.cursor.y, pad_character.clone());
self.pad_current_line_until(self.cursor.x, pad_character);
},
None => {
self.cursor.x = std::cmp::min(self.width - 1, x);
self.cursor.y = std::cmp::min(self.height - 1, y);
self.pad_lines_until(self.cursor.y, pad_character.clone());
self.pad_current_line_until(self.cursor.x, pad_character);
},
let (scroll_region_top, scroll_region_bottom) = self.scroll_region;
self.cursor.x = std::cmp::min(self.width - 1, x);
let y_offset = if self.erasure_mode {
scroll_region_top
} else {
0
};
if y >= scroll_region_top && y <= scroll_region_bottom {
self.cursor.y = std::cmp::min(scroll_region_bottom, y + y_offset);
} else {
self.cursor.y = std::cmp::min(self.height - 1, y + y_offset);
}
self.pad_lines_until(self.cursor.y, pad_character.clone());
self.pad_current_line_until(self.cursor.x, pad_character);
}
pub fn move_cursor_up(&mut self, count: usize) {
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
if self.cursor.y >= scroll_region_top && self.cursor.y <= scroll_region_bottom {
self.cursor.y =
std::cmp::max(self.cursor.y.saturating_sub(count), scroll_region_top);
return;
}
let (scroll_region_top, scroll_region_bottom) = self.scroll_region;
if self.cursor.y >= scroll_region_top && self.cursor.y <= scroll_region_bottom {
self.cursor.y = std::cmp::max(self.cursor.y.saturating_sub(count), scroll_region_top);
return;
}
self.cursor.y = if self.cursor.y < count {
0
@ -1589,8 +1547,7 @@ impl Grid {
};
}
pub fn move_cursor_up_with_scrolling(&mut self, count: usize) {
let (scroll_region_top, scroll_region_bottom) =
self.scroll_region.unwrap_or((0, self.height - 1));
let (scroll_region_top, scroll_region_bottom) = self.scroll_region;
for _ in 0..count {
let current_line_index = self.cursor.y;
if current_line_index == scroll_region_top {
@ -1615,11 +1572,10 @@ impl Grid {
count: usize,
pad_character: TerminalCharacter,
) {
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
if self.cursor.y >= scroll_region_top && self.cursor.y <= scroll_region_bottom {
self.cursor.y = std::cmp::min(self.cursor.y + count, scroll_region_bottom);
return;
}
let (scroll_region_top, scroll_region_bottom) = self.scroll_region;
if self.cursor.y >= scroll_region_top && self.cursor.y <= scroll_region_bottom {
self.cursor.y = std::cmp::min(self.cursor.y + count, scroll_region_bottom);
return;
}
self.cursor.y = std::cmp::min(self.cursor.y + count, self.height - 1);
self.pad_lines_until(self.cursor.y, pad_character);
@ -1643,42 +1599,37 @@ impl Grid {
}
pub fn set_scroll_region(&mut self, top_line_index: usize, bottom_line_index: Option<usize>) {
let bottom_line_index = bottom_line_index.unwrap_or(self.height.saturating_sub(1));
self.scroll_region = Some((top_line_index, bottom_line_index));
self.scroll_region = (top_line_index, bottom_line_index);
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
self.move_cursor_to(0, 0, pad_character); // DECSTBM moves the cursor to column 1 line 1 of the page
}
pub fn clear_scroll_region(&mut self) {
self.scroll_region = None;
}
pub fn set_scroll_region_to_viewport_size(&mut self) {
self.scroll_region = Some((0, self.height.saturating_sub(1)));
self.scroll_region = (0, self.height.saturating_sub(1));
}
pub fn delete_lines_in_scroll_region(
&mut self,
count: usize,
pad_character: TerminalCharacter,
) {
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
let current_line_index = self.cursor.y;
if current_line_index >= scroll_region_top && current_line_index <= scroll_region_bottom
{
// when deleting lines inside the scroll region, we must make sure it stays the
// same size (and that other lines below it aren't shifted inside it)
// so we delete the current line(s) and add an empty line at the end of the scroll
// region
for _ in 0..count {
self.viewport.remove(current_line_index);
let columns = VecDeque::from(vec![pad_character.clone(); self.width]);
if self.viewport.len() > scroll_region_bottom {
self.viewport
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
} else {
self.viewport.push(Row::from_columns(columns).canonical());
}
let (scroll_region_top, scroll_region_bottom) = self.scroll_region;
let current_line_index = self.cursor.y;
if current_line_index >= scroll_region_top && current_line_index <= scroll_region_bottom {
// when deleting lines inside the scroll region, we must make sure it stays the
// same size (and that other lines below it aren't shifted inside it)
// so we delete the current line(s) and add an empty line at the end of the scroll
// region
for _ in 0..count {
self.viewport.remove(current_line_index);
let columns = VecDeque::from(vec![pad_character.clone(); self.width]);
if self.viewport.len() > scroll_region_bottom {
self.viewport
.insert(scroll_region_bottom, Row::from_columns(columns).canonical());
} else {
self.viewport.push(Row::from_columns(columns).canonical());
}
self.output_buffer.update_all_lines(); // TODO: move accurately
}
self.output_buffer.update_all_lines(); // TODO: move accurately
}
}
pub fn add_empty_lines_in_scroll_region(
@ -1686,24 +1637,22 @@ impl Grid {
count: usize,
pad_character: TerminalCharacter,
) {
if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region {
let current_line_index = self.cursor.y;
if current_line_index >= scroll_region_top && current_line_index <= scroll_region_bottom
{
// when adding empty lines inside the scroll region, we must make sure it stays the
// same size and that lines don't "leak" outside of it
// so we add an empty line where the cursor currently is, and delete the last line
// of the scroll region
for _ in 0..count {
if scroll_region_bottom < self.viewport.len() {
self.viewport.remove(scroll_region_bottom);
}
let columns = VecDeque::from(vec![pad_character.clone(); self.width]);
self.viewport
.insert(current_line_index, Row::from_columns(columns).canonical());
let (scroll_region_top, scroll_region_bottom) = self.scroll_region;
let current_line_index = self.cursor.y;
if current_line_index >= scroll_region_top && current_line_index <= scroll_region_bottom {
// when adding empty lines inside the scroll region, we must make sure it stays the
// same size and that lines don't "leak" outside of it
// so we add an empty line where the cursor currently is, and delete the last line
// of the scroll region
for _ in 0..count {
if scroll_region_bottom < self.viewport.len() {
self.viewport.remove(scroll_region_bottom);
}
self.output_buffer.update_all_lines(); // TODO: move accurately
let columns = VecDeque::from(vec![pad_character.clone(); self.width]);
self.viewport
.insert(current_line_index, Row::from_columns(columns).canonical());
}
self.output_buffer.update_all_lines(); // TODO: move accurately
}
}
pub fn move_cursor_to_column(&mut self, column: usize) {
@ -1767,7 +1716,6 @@ impl Grid {
self.viewport = vec![Row::new().canonical()];
self.alternate_screen_state = None;
self.cursor_key_mode = false;
self.scroll_region = None;
self.clear_viewport_before_rendering = true;
self.cursor = Cursor::new(0, 0, self.styled_underlines);
self.saved_cursor_position = None;
@ -1786,6 +1734,7 @@ impl Grid {
self.focus_event_tracking = false;
self.cursor_is_hidden = false;
self.supports_kitty_keyboard_protocol = false;
self.set_scroll_region_to_viewport_size();
if let Some(images_to_reap) = self.sixel_grid.clear() {
self.sixel_grid.reap_images(images_to_reap);
}
@ -2894,7 +2843,7 @@ impl Perform for Grid {
},
3 => {
// DECCOLM - only side effects
self.scroll_region = None;
self.set_scroll_region_to_viewport_size();
self.clear_all(EMPTY_TERMINAL_CHARACTER);
self.cursor.x = 0;
self.cursor.y = 0;
@ -2997,7 +2946,7 @@ impl Perform for Grid {
},
3 => {
// DECCOLM - only side effects
self.scroll_region = None;
self.set_scroll_region_to_viewport_size();
self.clear_all(EMPTY_TERMINAL_CHARACTER);
self.cursor.x = 0;
self.cursor.y = 0;
@ -3079,14 +3028,11 @@ impl Perform for Grid {
self.move_cursor_to_beginning_of_line();
}
} else {
self.clear_scroll_region();
self.set_scroll_region_to_viewport_size();
}
} else if c == 'M' {
// delete lines if currently inside scroll region, or otherwise
// delete lines in the entire viewport
if self.scroll_region.is_none() {
self.set_scroll_region_to_viewport_size();
}
let line_count_to_delete = next_param_or(1);
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
@ -3094,9 +3040,6 @@ impl Perform for Grid {
} else if c == 'L' {
// insert blank lines if inside scroll region, or otherwise insert
// blank lines in the entire viewport
if self.scroll_region.is_none() {
self.set_scroll_region_to_viewport_size();
}
let line_count_to_add = next_param_or(1);
let mut pad_character = EMPTY_TERMINAL_CHARACTER;
pad_character.styles = self.cursor.pending_styles.clone();
@ -3297,10 +3240,7 @@ impl Perform for Grid {
// CPR - cursor position report
// Note that this is relative to scrolling region.
let offset = match self.scroll_region {
Some((scroll_region_top, _scroll_region_bottom)) => scroll_region_top,
_ => 0,
};
let offset = self.scroll_region.0; // scroll_region_top
let position_report = format!(
"\u{1b}[{};{}R",
self.cursor.y + 1 - offset,

View file

@ -1,7 +1,6 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): line5
01 (C): line6
@ -57,4 +56,3 @@ expression: "format!(\"{:?}\", grid)"
51 (C): line56
52 (C): line57
53 (C): line58

View file

@ -1,7 +1,6 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): 7foo
01 (C): 8foo
@ -38,4 +37,3 @@ expression: "format!(\"{:?}\", grid)"
32 (C):
33 (C): 5zzr
34 (C):

View file

@ -1,7 +1,6 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): 22 line2
01 (C): 21 line3
@ -31,4 +30,3 @@ expression: "format!(\"{:?}\", grid)"
25 (C): 3 line27
26 (C): NORMAL testfile.rs unix | utf-8 | rust 57% 24:1
27 (C):

View file

@ -1,7 +1,6 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): Test of autowrap, mixing control and print characters.
01 (C): The left/right margins should have letters in order:
@ -44,4 +43,3 @@ expression: "format!(\"{:?}\", grid)"
38 (C):
39 (C):
40 (C):

View file

@ -1,6 +1,5 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 148
expression: "format!(\"{:?}\", grid)"
---
00 (C): Test of autowrap, mixing control and print characters.
@ -44,4 +43,3 @@ expression: "format!(\"{:?}\", grid)"
38 (C):
39 (C):
40 (C):

View file

@ -1,6 +1,5 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 305
expression: "format!(\"{:?}\", grid)"
---
00 (C): Push <RETURN>
@ -44,4 +43,3 @@ expression: "format!(\"{:?}\", grid)"
38 (C): Soft scroll up region [12..13] size 2 Line 15
39 (C): Soft scroll up region [12..13] size 2 Line 16
40 (C): Soft scroll up region [12..13] size 2 Line 17

View file

@ -1,6 +1,5 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
assertion_line: 349
expression: "format!(\"{:?}\", grid)"
---
00 (C): Push <RETURN>
@ -44,4 +43,3 @@ expression: "format!(\"{:?}\", grid)"
38 (C): Jump scroll up region [12..13] size 2 Line 15
39 (C): Jump scroll up region [12..13] size 2 Line 16
40 (C): Jump scroll up region [12..13] size 2 Line 17