diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-03-31 20:49:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-03-31 20:49:05 +0300 |
| commit | 41e009f9b7b46d335d41b365e41f9ad57d571ecb (patch) | |
| tree | 42b3be90c336ad6d4487e9f86abc56ebb40ba210 /vendor/vt100/src/grid.rs | |
| parent | f826fb78dcac89b4ba23e399ac5d141459c14ae9 (diff) | |
fix alt-screen scrollback handling
Diffstat (limited to 'vendor/vt100/src/grid.rs')
| -rw-r--r-- | vendor/vt100/src/grid.rs | 743 |
1 files changed, 743 insertions, 0 deletions
diff --git a/vendor/vt100/src/grid.rs b/vendor/vt100/src/grid.rs new file mode 100644 index 00000000..b1c5d341 --- /dev/null +++ b/vendor/vt100/src/grid.rs @@ -0,0 +1,743 @@ +use crate::term::BufWrite as _; + +#[derive(Clone, Debug)] +pub struct Grid { + size: Size, + pos: Pos, + saved_pos: Pos, + rows: Vec<crate::row::Row>, + scroll_top: u16, + scroll_bottom: u16, + origin_mode: bool, + saved_origin_mode: bool, + scrollback: std::collections::VecDeque<crate::row::Row>, + scrollback_len: usize, + scrollback_offset: usize, +} + +impl Grid { + pub fn new(size: Size, scrollback_len: usize) -> Self { + Self { + size, + pos: Pos::default(), + saved_pos: Pos::default(), + rows: vec![], + scroll_top: 0, + scroll_bottom: size.rows - 1, + origin_mode: false, + saved_origin_mode: false, + scrollback: std::collections::VecDeque::new(), + scrollback_len, + scrollback_offset: 0, + } + } + + pub fn allocate_rows(&mut self) { + if self.rows.is_empty() { + self.rows.extend( + std::iter::repeat_with(|| { + crate::row::Row::new(self.size.cols) + }) + .take(usize::from(self.size.rows)), + ); + } + } + + fn new_row(&self) -> crate::row::Row { + crate::row::Row::new(self.size.cols) + } + + pub fn clear(&mut self) { + self.pos = Pos::default(); + self.saved_pos = Pos::default(); + for row in self.drawing_rows_mut() { + row.clear(crate::attrs::Attrs::default()); + } + self.scroll_top = 0; + self.scroll_bottom = self.size.rows - 1; + self.origin_mode = false; + self.saved_origin_mode = false; + } + + pub fn clear_scrollback(&mut self) { + self.scrollback.clear(); + self.scrollback_offset = 0; + } + + pub fn size(&self) -> Size { + self.size + } + + pub fn set_size(&mut self, size: Size) { + if size.cols != self.size.cols { + for row in &mut self.rows { + row.wrap(false); + } + } + + if self.scroll_bottom == self.size.rows - 1 { + self.scroll_bottom = size.rows - 1; + } + + self.size = size; + for row in &mut self.rows { + row.resize(size.cols, crate::Cell::new()); + } + self.rows.resize(usize::from(size.rows), self.new_row()); + + if self.scroll_bottom >= size.rows { + self.scroll_bottom = size.rows - 1; + } + if self.scroll_bottom < self.scroll_top { + self.scroll_top = 0; + } + + self.row_clamp_top(false); + self.row_clamp_bottom(false); + self.col_clamp(); + + if self.saved_pos.row > self.size.rows - 1 { + self.saved_pos.row = self.size.rows - 1; + } + if self.saved_pos.col > self.size.cols - 1 { + self.saved_pos.col = self.size.cols - 1; + } + } + + pub fn pos(&self) -> Pos { + self.pos + } + + pub fn set_pos(&mut self, mut pos: Pos) { + if self.origin_mode { + pos.row = pos.row.saturating_add(self.scroll_top); + } + self.pos = pos; + self.row_clamp_top(self.origin_mode); + self.row_clamp_bottom(self.origin_mode); + self.col_clamp(); + } + + pub fn save_cursor(&mut self) { + self.saved_pos = self.pos; + self.saved_origin_mode = self.origin_mode; + } + + pub fn restore_cursor(&mut self) { + self.pos = self.saved_pos; + self.origin_mode = self.saved_origin_mode; + } + + pub fn visible_rows(&self) -> impl Iterator<Item = &crate::row::Row> { + let scrollback_len = self.scrollback.len(); + let rows_len = self.rows.len(); + self.scrollback + .iter() + .skip(scrollback_len - self.scrollback_offset) + // when scrollback_offset > rows_len (e.g. rows = 3, + // scrollback_len = 10, offset = 9) the skip(10 - 9) + // will take 9 rows instead of 3. we need to set + // the upper bound to rows_len (e.g. 3) + .take(rows_len) + // same for rows_len - scrollback_offset (e.g. 3 - 9). + // it'll panic with overflow. we have to saturate the subtraction. + .chain( + self.rows + .iter() + .take(rows_len.saturating_sub(self.scrollback_offset)), + ) + } + + pub fn drawing_rows(&self) -> impl Iterator<Item = &crate::row::Row> { + self.rows.iter() + } + + pub fn drawing_rows_mut( + &mut self, + ) -> impl Iterator<Item = &mut crate::row::Row> { + self.rows.iter_mut() + } + + pub fn visible_row(&self, row: u16) -> Option<&crate::row::Row> { + self.visible_rows().nth(usize::from(row)) + } + + pub fn drawing_row(&self, row: u16) -> Option<&crate::row::Row> { + self.drawing_rows().nth(usize::from(row)) + } + + pub fn drawing_row_mut( + &mut self, + row: u16, + ) -> Option<&mut crate::row::Row> { + self.drawing_rows_mut().nth(usize::from(row)) + } + + pub fn current_row_mut(&mut self) -> &mut crate::row::Row { + self.drawing_row_mut(self.pos.row) + // we assume self.pos.row is always valid + .unwrap() + } + + pub fn visible_cell(&self, pos: Pos) -> Option<&crate::Cell> { + self.visible_row(pos.row).and_then(|r| r.get(pos.col)) + } + + pub fn drawing_cell(&self, pos: Pos) -> Option<&crate::Cell> { + self.drawing_row(pos.row).and_then(|r| r.get(pos.col)) + } + + pub fn drawing_cell_mut(&mut self, pos: Pos) -> Option<&mut crate::Cell> { + self.drawing_row_mut(pos.row) + .and_then(|r| r.get_mut(pos.col)) + } + + pub fn scrollback_len(&self) -> usize { + self.scrollback_len + } + + pub fn scrollback(&self) -> usize { + self.scrollback_offset + } + + pub fn set_scrollback(&mut self, rows: usize) { + self.scrollback_offset = rows.min(self.scrollback.len()); + } + + pub fn write_contents(&self, contents: &mut String) { + let mut wrapping = false; + for row in self.visible_rows() { + row.write_contents(contents, 0, self.size.cols, wrapping); + if !row.wrapped() { + contents.push('\n'); + } + wrapping = row.wrapped(); + } + + while contents.ends_with('\n') { + contents.truncate(contents.len() - 1); + } + } + + pub fn write_contents_formatted( + &self, + contents: &mut Vec<u8>, + ) -> crate::attrs::Attrs { + crate::term::ClearAttrs.write_buf(contents); + crate::term::ClearScreen.write_buf(contents); + + let mut prev_attrs = crate::attrs::Attrs::default(); + let mut prev_pos = Pos::default(); + let mut wrapping = false; + for (i, row) in self.visible_rows().enumerate() { + // we limit the number of cols to a u16 (see Size), so + // visible_rows() can never return more rows than will fit + let i = i.try_into().unwrap(); + let (new_pos, new_attrs) = row.write_contents_formatted( + contents, + 0, + self.size.cols, + i, + wrapping, + Some(prev_pos), + Some(prev_attrs), + ); + prev_pos = new_pos; + prev_attrs = new_attrs; + wrapping = row.wrapped(); + } + + self.write_cursor_position_formatted( + contents, + Some(prev_pos), + Some(prev_attrs), + ); + + prev_attrs + } + + pub fn write_contents_diff( + &self, + contents: &mut Vec<u8>, + prev: &Self, + mut prev_attrs: crate::attrs::Attrs, + ) -> crate::attrs::Attrs { + let mut prev_pos = prev.pos; + let mut wrapping = false; + let mut prev_wrapping = false; + for (i, (row, prev_row)) in + self.visible_rows().zip(prev.visible_rows()).enumerate() + { + // we limit the number of cols to a u16 (see Size), so + // visible_rows() can never return more rows than will fit + let i = i.try_into().unwrap(); + let (new_pos, new_attrs) = row.write_contents_diff( + contents, + prev_row, + 0, + self.size.cols, + i, + wrapping, + prev_wrapping, + prev_pos, + prev_attrs, + ); + prev_pos = new_pos; + prev_attrs = new_attrs; + wrapping = row.wrapped(); + prev_wrapping = prev_row.wrapped(); + } + + self.write_cursor_position_formatted( + contents, + Some(prev_pos), + Some(prev_attrs), + ); + + prev_attrs + } + + pub fn write_cursor_position_formatted( + &self, + contents: &mut Vec<u8>, + prev_pos: Option<Pos>, + prev_attrs: Option<crate::attrs::Attrs>, + ) { + let prev_attrs = prev_attrs.unwrap_or_default(); + // writing a character to the last column of a row doesn't wrap the + // cursor immediately - it waits until the next character is actually + // drawn. it is only possible for the cursor to have this kind of + // position after drawing a character though, so if we end in this + // position, we need to redraw the character at the end of the row. + if prev_pos != Some(self.pos) && self.pos.col >= self.size.cols { + let mut pos = Pos { + row: self.pos.row, + col: self.size.cols - 1, + }; + if self + .drawing_cell(pos) + // we assume self.pos.row is always valid, and self.size.cols + // - 1 is always a valid column + .unwrap() + .is_wide_continuation() + { + pos.col = self.size.cols - 2; + } + let cell = + // we assume self.pos.row is always valid, and self.size.cols + // - 2 must be a valid column because self.size.cols - 1 is + // always valid and we just checked that the cell at + // self.size.cols - 1 is a wide continuation character, which + // means that the first half of the wide character must be + // before it + self.drawing_cell(pos).unwrap(); + if cell.has_contents() { + if let Some(prev_pos) = prev_pos { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + } else { + crate::term::MoveTo::new(pos).write_buf(contents); + } + cell.attrs().write_escape_code_diff(contents, &prev_attrs); + contents.extend(cell.contents().as_bytes()); + prev_attrs.write_escape_code_diff(contents, cell.attrs()); + } else { + // if the cell doesn't have contents, we can't have gotten + // here by drawing a character in the last column. this means + // that as far as i'm aware, we have to have reached here from + // a newline when we were already after the end of an earlier + // row. in the case where we are already after the end of an + // earlier row, we can just write a few newlines, otherwise we + // also need to do the same as above to get ourselves to after + // the end of a row. + let mut found = false; + for i in (0..self.pos.row).rev() { + pos.row = i; + pos.col = self.size.cols - 1; + if self + .drawing_cell(pos) + // i is always less than self.pos.row, which we assume + // to be always valid, so it must also be valid. + // self.size.cols - 1 is always a valid col. + .unwrap() + .is_wide_continuation() + { + pos.col = self.size.cols - 2; + } + let cell = self + .drawing_cell(pos) + // i is always less than self.pos.row, which we assume + // to be always valid, so it must also be valid. + // self.size.cols - 2 is valid because self.size.cols + // - 1 is always valid, and col gets set to + // self.size.cols - 2 when the cell at self.size.cols + // - 1 is a wide continuation character, meaning that + // the first half of the wide character must be before + // it + .unwrap(); + if cell.has_contents() { + if let Some(prev_pos) = prev_pos { + if prev_pos.row != i + || prev_pos.col < self.size.cols + { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + cell.attrs().write_escape_code_diff( + contents, + &prev_attrs, + ); + contents.extend(cell.contents().as_bytes()); + prev_attrs.write_escape_code_diff( + contents, + cell.attrs(), + ); + } + } else { + crate::term::MoveTo::new(pos).write_buf(contents); + cell.attrs().write_escape_code_diff( + contents, + &prev_attrs, + ); + contents.extend(cell.contents().as_bytes()); + prev_attrs.write_escape_code_diff( + contents, + cell.attrs(), + ); + } + contents.extend( + "\n".repeat(usize::from(self.pos.row - i)) + .as_bytes(), + ); + found = true; + break; + } + } + + // this can happen if you get the cursor off the end of a row, + // and then do something to clear the end of the current row + // without moving the cursor (IL, DL, ED, EL, etc). we know + // there can't be something in the last column because we + // would have caught that above, so it should be safe to + // overwrite it. + if !found { + pos = Pos { + row: self.pos.row, + col: self.size.cols - 1, + }; + if let Some(prev_pos) = prev_pos { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + } else { + crate::term::MoveTo::new(pos).write_buf(contents); + } + contents.push(b' '); + // we know that the cell has no contents, but it still may + // have drawing attributes (background color, etc) + let end_cell = self + .drawing_cell(pos) + // we assume self.pos.row is always valid, and + // self.size.cols - 1 is always a valid column + .unwrap(); + end_cell + .attrs() + .write_escape_code_diff(contents, &prev_attrs); + crate::term::SaveCursor.write_buf(contents); + crate::term::Backspace.write_buf(contents); + crate::term::EraseChar::new(1).write_buf(contents); + crate::term::RestoreCursor.write_buf(contents); + prev_attrs + .write_escape_code_diff(contents, end_cell.attrs()); + } + } + } else if let Some(prev_pos) = prev_pos { + crate::term::MoveFromTo::new(prev_pos, self.pos) + .write_buf(contents); + } else { + crate::term::MoveTo::new(self.pos).write_buf(contents); + } + } + + pub fn erase_all(&mut self, attrs: crate::attrs::Attrs) { + for row in self.drawing_rows_mut() { + row.clear(attrs); + } + } + + pub fn erase_all_forward(&mut self, attrs: crate::attrs::Attrs) { + let pos = self.pos; + for row in self.drawing_rows_mut().skip(usize::from(pos.row) + 1) { + row.clear(attrs); + } + + self.erase_row_forward(attrs); + } + + pub fn erase_all_backward(&mut self, attrs: crate::attrs::Attrs) { + let pos = self.pos; + for row in self.drawing_rows_mut().take(usize::from(pos.row)) { + row.clear(attrs); + } + + self.erase_row_backward(attrs); + } + + pub fn erase_row(&mut self, attrs: crate::attrs::Attrs) { + self.current_row_mut().clear(attrs); + } + + pub fn erase_row_forward(&mut self, attrs: crate::attrs::Attrs) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for col in pos.col..size.cols { + row.erase(col, attrs); + } + } + + pub fn erase_row_backward(&mut self, attrs: crate::attrs::Attrs) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for col in 0..=pos.col.min(size.cols - 1) { + row.erase(col, attrs); + } + } + + pub fn insert_cells(&mut self, count: u16) { + let size = self.size; + let pos = self.pos; + let wide = pos.col < size.cols + && self + .drawing_cell(pos) + // we assume self.pos.row is always valid, and we know we are + // not off the end of a row because we just checked pos.col < + // size.cols + .unwrap() + .is_wide_continuation(); + let row = self.current_row_mut(); + for _ in 0..count { + if wide { + row.get_mut(pos.col).unwrap().set_wide_continuation(false); + } + row.insert(pos.col, crate::Cell::new()); + if wide { + row.get_mut(pos.col).unwrap().set_wide_continuation(true); + } + } + row.truncate(size.cols); + } + + pub fn delete_cells(&mut self, count: u16) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for _ in 0..(count.min(size.cols - pos.col)) { + row.remove(pos.col); + } + row.resize(size.cols, crate::Cell::new()); + } + + pub fn erase_cells(&mut self, count: u16, attrs: crate::attrs::Attrs) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for col in pos.col..((pos.col.saturating_add(count)).min(size.cols)) { + row.erase(col, attrs); + } + } + + pub fn insert_lines(&mut self, count: u16) { + for _ in 0..count { + self.rows.remove(usize::from(self.scroll_bottom)); + self.rows.insert(usize::from(self.pos.row), self.new_row()); + // self.scroll_bottom is maintained to always be a valid row + self.rows[usize::from(self.scroll_bottom)].wrap(false); + } + } + + pub fn delete_lines(&mut self, count: u16) { + for _ in 0..(count.min(self.size.rows - self.pos.row)) { + self.rows + .insert(usize::from(self.scroll_bottom) + 1, self.new_row()); + self.rows.remove(usize::from(self.pos.row)); + } + } + + pub fn scroll_up(&mut self, count: u16) { + for _ in 0..(count.min(self.size.rows - self.scroll_top)) { + self.rows + .insert(usize::from(self.scroll_bottom) + 1, self.new_row()); + let removed = self.rows.remove(usize::from(self.scroll_top)); + if self.scrollback_len > 0 && self.scroll_top == 0 { + self.scrollback.push_back(removed); + while self.scrollback.len() > self.scrollback_len { + self.scrollback.pop_front(); + } + if self.scrollback_offset > 0 { + self.scrollback_offset = + self.scrollback.len().min(self.scrollback_offset + 1); + } + } + } + } + + pub fn scroll_down(&mut self, count: u16) { + for _ in 0..count { + self.rows.remove(usize::from(self.scroll_bottom)); + self.rows + .insert(usize::from(self.scroll_top), self.new_row()); + // self.scroll_bottom is maintained to always be a valid row + self.rows[usize::from(self.scroll_bottom)].wrap(false); + } + } + + pub fn set_scroll_region(&mut self, top: u16, bottom: u16) { + let bottom = bottom.min(self.size().rows - 1); + if top < bottom { + self.scroll_top = top; + self.scroll_bottom = bottom; + } else { + self.scroll_top = 0; + self.scroll_bottom = self.size().rows - 1; + } + self.pos.row = self.scroll_top; + self.pos.col = 0; + } + + fn in_scroll_region(&self) -> bool { + self.pos.row >= self.scroll_top && self.pos.row <= self.scroll_bottom + } + + pub fn set_origin_mode(&mut self, mode: bool) { + self.origin_mode = mode; + self.set_pos(Pos { row: 0, col: 0 }); + } + + pub fn row_inc_clamp(&mut self, count: u16) { + let in_scroll_region = self.in_scroll_region(); + self.pos.row = self.pos.row.saturating_add(count); + self.row_clamp_bottom(in_scroll_region); + } + + pub fn row_inc_scroll(&mut self, count: u16) -> u16 { + let in_scroll_region = self.in_scroll_region(); + self.pos.row = self.pos.row.saturating_add(count); + let lines = self.row_clamp_bottom(in_scroll_region); + if in_scroll_region { + self.scroll_up(lines); + lines + } else { + 0 + } + } + + pub fn row_dec_clamp(&mut self, count: u16) { + let in_scroll_region = self.in_scroll_region(); + self.pos.row = self.pos.row.saturating_sub(count); + self.row_clamp_top(in_scroll_region); + } + + pub fn row_dec_scroll(&mut self, count: u16) { + let in_scroll_region = self.in_scroll_region(); + // need to account for clamping by both row_clamp_top and by + // saturating_sub + let extra_lines = count.saturating_sub(self.pos.row); + self.pos.row = self.pos.row.saturating_sub(count); + let lines = self.row_clamp_top(in_scroll_region); + self.scroll_down(lines + extra_lines); + } + + pub fn row_set(&mut self, i: u16) { + self.pos.row = i; + self.row_clamp(); + } + + pub fn col_inc(&mut self, count: u16) { + self.pos.col = self.pos.col.saturating_add(count); + } + + pub fn col_inc_clamp(&mut self, count: u16) { + self.pos.col = self.pos.col.saturating_add(count); + self.col_clamp(); + } + + pub fn col_dec(&mut self, count: u16) { + self.pos.col = self.pos.col.saturating_sub(count); + } + + pub fn col_tab(&mut self) { + self.pos.col -= self.pos.col % 8; + self.pos.col += 8; + self.col_clamp(); + } + + pub fn col_set(&mut self, i: u16) { + self.pos.col = i; + self.col_clamp(); + } + + pub fn col_wrap(&mut self, width: u16, wrap: bool) { + if self.pos.col > self.size.cols - width { + let mut prev_pos = self.pos; + self.pos.col = 0; + let scrolled = self.row_inc_scroll(1); + prev_pos.row -= scrolled; + let new_pos = self.pos; + self.drawing_row_mut(prev_pos.row) + // we assume self.pos.row is always valid, and so prev_pos.row + // must be valid because it is always less than or equal to + // self.pos.row + .unwrap() + .wrap(wrap && prev_pos.row + 1 == new_pos.row); + } + } + + fn row_clamp_top(&mut self, limit_to_scroll_region: bool) -> u16 { + if limit_to_scroll_region && self.pos.row < self.scroll_top { + let rows = self.scroll_top - self.pos.row; + self.pos.row = self.scroll_top; + rows + } else { + 0 + } + } + + fn row_clamp_bottom(&mut self, limit_to_scroll_region: bool) -> u16 { + let bottom = if limit_to_scroll_region { + self.scroll_bottom + } else { + self.size.rows - 1 + }; + if self.pos.row > bottom { + let rows = self.pos.row - bottom; + self.pos.row = bottom; + rows + } else { + 0 + } + } + + fn row_clamp(&mut self) { + if self.pos.row > self.size.rows - 1 { + self.pos.row = self.size.rows - 1; + } + } + + fn col_clamp(&mut self) { + if self.pos.col > self.size.cols - 1 { + self.pos.col = self.size.cols - 1; + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Size { + pub rows: u16, + pub cols: u16, +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Pos { + pub row: u16, + pub col: u16, +} |
