fix: resize state and correctness (#5656)

* fix: resize state and correctness

* rustfmt

* add pr
This commit is contained in:
Aram Drevekenin
2026-09-25 11:39:39 +02:00
committed by GitHub
parent 2fce032cce
commit 53051a1a32
7 changed files with 155 additions and 41 deletions
+1
View File
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* feat: nested sessions input-mode + keybindings reporting (https://github.com/zellij-org/zellij/issues/5561)
* fix: control characters occasionally appearing in panes on startup (https://github.com/zellij-org/zellij/pull/5655)
* fix: group bar-plugins on viewport edge for layout purposes (https://github.com/zellij-org/zellij/pull/5653)
* fix: resize state and correctness (https://github.com/zellij-org/zellij/pull/5656)
* fix: compress kitty images if the host terminal supports it (https://github.com/zellij-org/zellij/pull/5608)
## [0.45.1] - 2026-08-28
+50 -29
View File
@@ -1367,18 +1367,21 @@ impl Grid {
self.active_charset = index;
}
fn cursor_canonical_line_index(&self) -> usize {
let mut cursor_canonical_line_index = 0;
self.canonical_line_index_of_row(self.cursor.y)
}
fn canonical_line_index_of_row(&self, y: usize) -> usize {
let mut canonical_line_index = 0;
let mut canonical_lines_traversed = 0;
for (i, line) in self.viewport.iter().enumerate() {
if line.is_canonical {
cursor_canonical_line_index = canonical_lines_traversed;
canonical_line_index = canonical_lines_traversed;
canonical_lines_traversed += 1;
}
if i == self.cursor.y {
if i == y {
break;
}
}
cursor_canonical_line_index
canonical_line_index
}
// TODO: merge these two functions
fn cursor_index_in_canonical_line(&self) -> usize {
@@ -1430,6 +1433,16 @@ impl Grid {
}
y_coordinates
}
fn last_row_of_line_starting_at(&self, first_y: usize) -> usize {
let mut last_y = first_y;
for (y, row) in self.viewport.iter().enumerate().skip(first_y + 1) {
if row.is_canonical {
break;
}
last_y = y;
}
last_y
}
fn kitty_canonical_line_starts(&self) -> Vec<usize> {
let mut starts = Vec::new();
let mut wrapped_row = 0usize;
@@ -1630,24 +1643,12 @@ impl Grid {
found_something
}
pub fn force_change_size(&mut self, new_rows: usize, new_columns: usize) {
// this is an ugly hack - it's here because sometimes we need to change_size to the
// existing size (eg. when resizing an alternative_grid to the current height/width) and
// the change_size method is a no-op in that case. Should be fixed by making the
// change_size method atomic
let intermediate_rows = if new_rows == self.height {
new_rows + 1
} else {
new_rows
};
let intermediate_columns = if new_columns == self.width {
new_columns + 1
} else {
new_columns
};
self.change_size(intermediate_rows, intermediate_columns);
self.change_size(new_rows, new_columns);
self.resize_and_reflow(new_rows, new_columns, true);
}
pub fn change_size(&mut self, new_rows: usize, new_columns: usize) {
self.resize_and_reflow(new_rows, new_columns, false);
}
fn resize_and_reflow(&mut self, new_rows: usize, new_columns: usize, force_rewrap: bool) {
// Do nothing if this pane hasn't been given a proper size yet
if new_columns == 0 || new_rows == 0 {
return;
@@ -1665,11 +1666,15 @@ impl Grid {
self.sixel_grid.character_cell_size_possibly_changed();
self.kitty_grid.character_cell_size_possibly_changed();
self.kitty_reanchor_all_from_pixels();
let cursors = if new_columns != self.width {
let cursors = if force_rewrap || new_columns != self.width {
self.horizontal_tabstops = create_horizontal_tabstops(new_columns);
let mut cursor_canonical_line_index = self.cursor_canonical_line_index();
let cursor_index_in_canonical_line = self.cursor_index_in_canonical_line();
let saved_cursor_index_in_canonical_line = self.saved_cursor_index_in_canonical_line();
let mut saved_cursor_canonical_line_index = self
.saved_cursor_position
.as_ref()
.map(|saved_cursor| self.canonical_line_index_of_row(saved_cursor.y));
let mut viewport_canonical_lines = vec![];
for mut row in self.viewport.drain(..) {
if !row.is_canonical
@@ -1680,6 +1685,9 @@ impl Grid {
first_line_above.append(&mut row);
viewport_canonical_lines.push(first_line_above);
cursor_canonical_line_index += 1;
if let Some(index) = saved_cursor_canonical_line_index.as_mut() {
*index += 1;
}
} else if row.is_canonical {
viewport_canonical_lines.push(row);
} else {
@@ -1736,13 +1744,20 @@ impl Grid {
self.viewport = VecDeque::from(new_viewport_rows);
let mut new_cursor_y = self.canonical_line_y_coordinates(cursor_canonical_line_index)
+ (cursor_index_in_canonical_line / new_columns);
let mut saved_cursor_y_coordinates =
self.saved_cursor_position.as_ref().map(|saved_cursor| {
self.canonical_line_y_coordinates(saved_cursor.y)
+ saved_cursor_index_in_canonical_line.as_ref().unwrap() / new_columns
});
let cursor_line_first_y =
self.canonical_line_y_coordinates(cursor_canonical_line_index);
let cursor_line_last_y = self.last_row_of_line_starting_at(cursor_line_first_y);
let mut new_cursor_y =
cursor_line_first_y + (cursor_index_in_canonical_line / new_columns);
let mut saved_cursor_y_coordinates = match (
saved_cursor_canonical_line_index,
saved_cursor_index_in_canonical_line,
) {
(Some(line_index), Some(index_in_line)) => Some(
self.canonical_line_y_coordinates(line_index) + index_in_line / new_columns,
),
_ => None,
};
// A cursor at EOL has two equivalent positions - end of this line or beginning of
// next. If not already at the beginning of line, bias to EOL so add character logic
@@ -1752,6 +1767,12 @@ impl Grid {
new_cursor_y = new_cursor_y.saturating_sub(1);
new_cursor_x = new_columns
}
if new_cursor_y > cursor_line_last_y {
let offset_in_last_row = cursor_index_in_canonical_line
.saturating_sub((cursor_line_last_y - cursor_line_first_y) * new_columns);
new_cursor_y = cursor_line_last_y;
new_cursor_x = offset_in_last_row.min(new_columns.saturating_sub(1));
}
let saved_cursor_x_coordinates = match (
saved_cursor_index_in_canonical_line.as_ref(),
self.saved_cursor_position.as_mut(),
@@ -1779,7 +1800,7 @@ impl Grid {
new_cursor_x,
saved_cursor_x_coordinates,
))
} else if new_rows != self.height {
} else if new_rows != self.height || self.viewport.len() != new_rows {
let saved_cursor_y_coordinates = self
.saved_cursor_position
.as_ref()
+1 -1
View File
@@ -1479,7 +1479,7 @@ impl TerminalPane {
fn reflow_lines(&mut self) {
let rows = self.get_content_rows();
let cols = self.get_content_columns();
self.grid.force_change_size(rows, cols);
self.grid.change_size(rows, cols);
if self.banner.is_some() {
self.grid.reset_terminal_state();
self.render_first_run_banner();
+101
View File
@@ -9399,3 +9399,104 @@ fn a_character_wider_than_two_columns_advances_the_cursor_by_its_full_width() {
assert_eq!(row.width(), 4);
assert_eq!(cursor_position(&grid), Some((4, 0)));
}
fn create_sized_grid_with_content(rows: usize, columns: usize, content: &str) -> Grid {
let mut vte_parser = vte::Parser::new();
let mut grid = Grid::new(
rows,
columns,
Rc::new(RefCell::new(Palette::default())),
Rc::new(RefCell::new(HashMap::new())),
Rc::new(RefCell::new(LinkHandler::new())),
Rc::new(RefCell::new(None)),
Rc::new(RefCell::new(SixelImageStore::default())),
Rc::new(RefCell::new(KittyImageStore::default())),
Style::default(),
false,
true,
true,
true,
false,
);
vte_parser.advance(&mut grid, content.as_bytes());
grid
}
fn feed(grid: &mut Grid, content: &str) {
let mut vte_parser = vte::Parser::new();
vte_parser.advance(grid, content.as_bytes());
}
#[test]
fn same_size_forced_reflow_keeps_cursor_at_start_of_wrapped_row() {
let mut grid = create_sized_grid_with_content(5, 10, "0123456789a\u{8}");
assert_eq!(cursor_position(&grid), Some((0, 1)));
grid.force_change_size(5, 10);
assert_eq!(cursor_position(&grid), Some((0, 1)));
feed(&mut grid, "\rX");
assert_eq!(rendered_row(&grid, 0), "0123456789");
assert_eq!(rendered_row(&grid, 1), "X");
}
#[test]
fn height_only_resize_keeps_cursor_at_start_of_wrapped_row() {
let mut grid = create_sized_grid_with_content(5, 10, "0123456789a\u{8}");
grid.force_change_size(6, 10);
assert_eq!(cursor_position(&grid), Some((0, 1)));
grid.change_size(7, 10);
assert_eq!(cursor_position(&grid), Some((0, 1)));
feed(&mut grid, "\rX");
assert_eq!(rendered_row(&grid, 0), "0123456789");
assert_eq!(rendered_row(&grid, 1), "X");
}
#[test]
fn prompt_redraw_after_same_size_reflow_does_not_duplicate_wrapped_prompt() {
let prompt = "> ~/c/zellij-code-2 on main";
let mut grid = create_sized_grid_with_content(5, 20, prompt);
for _ in 0..3 {
feed(&mut grid, "\r");
grid.force_change_size(5, 20);
feed(&mut grid, "\u{1b}[1A\u{1b}[J");
feed(&mut grid, prompt);
}
assert_eq!(rendered_row(&grid, 0), "> ~/c/zellij-code-2 ");
assert_eq!(rendered_row(&grid, 1).trim_end(), "on main");
assert_eq!(grid.viewport.len(), 2);
}
#[test]
fn saved_cursor_follows_its_own_line_when_wrapped_lines_are_above_it() {
let mut grid = create_sized_grid_with_content(
6,
10,
"0123456789abcdefghij\n\rXY\u{1b}7Z\n\rline3\n\rline4",
);
grid.change_size(6, 20);
feed(&mut grid, "\u{1b}8Q");
assert_eq!(rendered_row(&grid, 0), "0123456789abcdefghij");
assert_eq!(rendered_row(&grid, 1), "XYQ");
assert_eq!(rendered_row(&grid, 2), "line3");
assert_eq!(rendered_row(&grid, 3), "line4");
}
#[test]
fn shrinking_keeps_cursor_on_its_own_line_when_it_sits_past_the_content() {
let mut grid = create_sized_grid_with_content(6, 31, "first\n\r> prompt text here okay \x1b[J");
assert_eq!(cursor_position(&grid), Some((24, 1)));
grid.change_size(6, 23);
assert_eq!(cursor_position(&grid), Some((22, 1)));
feed(&mut grid, "\rX");
assert_eq!(rendered_row(&grid, 0), "first");
assert_eq!(rendered_row(&grid, 1), "X prompt text here okay");
assert_eq!(grid.viewport.len(), 2);
}
-9
View File
@@ -1957,9 +1957,6 @@ impl Tab {
.non_fatal();
}
self.set_force_render();
self.senders
.send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes)
.with_context(|| format!("failed to apply cached resizes"))?;
Ok(())
}
fn relayout_tiled_panes(&mut self, search_backwards: bool) -> Result<()> {
@@ -2014,9 +2011,6 @@ impl Tab {
// we do this so that the new swap layout has a chance to pass through the constraint system
self.tiled_panes.resize(display_area);
self.set_should_clear_display_before_rendering();
self.senders
.send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes)
.with_context(|| format!("failed to apply cached resizes"))?;
Ok(())
}
fn settled_tiled_pane_count(&self) -> usize {
@@ -5211,9 +5205,6 @@ impl Tab {
let _ = self.relayout_tiled_panes(false);
}
self.set_should_clear_display_before_rendering();
self.senders
.send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes)
.with_context(|| format!("failed to update plugins with mode info"))?;
LayoutApplier::offset_viewport(
self.viewport.clone(),
self.display_area.clone(),
@@ -2,4 +2,4 @@
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
---
[StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, ApplyCachedResizes, ResizePty(0, 119, 18, None, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([102, 111, 111], 0, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, Exit]
[StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([102, 111, 111], 0, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, Exit]
@@ -2,4 +2,4 @@
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
---
[StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, ApplyCachedResizes, ResizePty(0, 119, 18, None, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([105, 110, 112, 117, 116, 32, 102, 114, 111, 109, 32, 116, 104, 101, 32, 99, 108, 105], 0, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, Exit]
[StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([105, 110, 112, 117, 116, 32, 102, 114, 111, 109, 32, 116, 104, 101, 32, 99, 108, 105], 0, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, Exit]