term: add scrollback

This commit is contained in:
Mark Poliakov 2025-02-13 13:10:06 +02:00
parent 250d70a958
commit de16799908
2 changed files with 147 additions and 23 deletions

View File

@ -85,12 +85,15 @@ impl DrawState {
dt.fill(default_bg); dt.fill(default_bg);
} }
if cursor_dirty { if cursor_dirty {
state.buffer.set_row_dirty(self.old_cursor.row); state.buffer.set_row_dirty(self.old_cursor.row);
} }
let bytes_per_line = (self.font.width() as usize + 7) / 8; let bytes_per_line = (self.font.width() as usize + 7) / 8;
for (i, row) in state.buffer.dirty_rows() { let scroll = state.adjust_scroll();
let cursor_visible = scroll == 0;
state.buffer.visible_rows_mut(scroll, |i, row| {
let cy = i * fh; let cy = i * fh;
for (j, cell) in row.cells().enumerate() { for (j, cell) in row.cells().enumerate() {
@ -132,27 +135,32 @@ impl DrawState {
y += 1; y += 1;
} }
} }
} });
// for (i, row) in state.buffer.dirty_rows() {
// }
// TODO check if there's a character under cursor // TODO check if there's a character under cursor
let cx = state.cursor.col * fw; if cursor_visible {
let cy = state.cursor.row * fh; let cx = state.cursor.col * fw;
let cy = state.cursor.row * fh;
// Fill block cursor // Fill block cursor
for y in 0..fh { for y in 0..fh {
let off = (cy + y) * self.width + cx; let off = (cy + y) * self.width + cx;
dt[off..off + fw].fill(default_fg); dt[off..off + fw].fill(default_fg);
}
if !self.focused {
// Remove cursor center
for y in 1..fh - 1 {
let off = (cy + y) * self.width + cx + 1;
dt[off..off + fw - 2].fill(default_bg);
} }
if !self.focused {
// Remove cursor center
for y in 1..fh - 1 {
let off = (cy + y) * self.width + cx + 1;
dt[off..off + fw - 2].fill(default_bg);
}
}
self.old_cursor = state.cursor;
} }
self.old_cursor = state.cursor;
self.force_redraw = false; self.force_redraw = false;
self.focus_changed = false; self.focus_changed = false;
} }
@ -211,32 +219,56 @@ impl Terminal<'_> {
EventOutcome::Redraw EventOutcome::Redraw
}); });
let state_c = state.clone();
let pty_master_c = pty_master.clone(); let pty_master_c = pty_master.clone();
window.set_on_key_input(move |ev| { window.set_on_key_input(move |ev| {
let mut s = state_c.lock().unwrap();
let mut pty_master = pty_master_c.lock().unwrap(); let mut pty_master = pty_master_c.lock().unwrap();
let mut need_redraw = false;
// TODO error reporting from handlers // TODO error reporting from handlers
if let Some(input) = ev.input { if let Some(input) = ev.input {
pty_master.write_all(&[input as u8]).unwrap(); pty_master.write_all(&[input as u8]).unwrap();
need_redraw = s.scroll_end();
} else { } else {
match (ev.modifiers, ev.key) { match (ev.modifiers, ev.key) {
(KeyModifiers::NONE, KeyboardKey::Escape) => { (KeyModifiers::NONE, KeyboardKey::Escape) => {
pty_master.write_all(b"\x1B").unwrap(); pty_master.write_all(b"\x1B").unwrap();
need_redraw = s.scroll_end();
} }
(KeyModifiers::NONE, KeyboardKey::Backspace) => { (KeyModifiers::NONE, KeyboardKey::Backspace) => {
pty_master.write_all(&[termios.chars.erase]).unwrap(); pty_master.write_all(&[termios.chars.erase]).unwrap();
need_redraw = s.scroll_end();
} }
(KeyModifiers::CTRL, KeyboardKey::Char(b'c')) => { (KeyModifiers::CTRL, KeyboardKey::Char(b'c')) => {
pty_master.write_all(&[termios.chars.interrupt]).unwrap(); pty_master.write_all(&[termios.chars.interrupt]).unwrap();
need_redraw = s.scroll_end();
} }
(KeyModifiers::CTRL, KeyboardKey::Char(b'd')) => { (KeyModifiers::CTRL, KeyboardKey::Char(b'd')) => {
pty_master.write_all(&[termios.chars.eof]).unwrap(); pty_master.write_all(&[termios.chars.eof]).unwrap();
need_redraw = s.scroll_end();
}
(KeyModifiers::SHIFT, KeyboardKey::PageUp) => {
need_redraw = s.scroll_up();
}
(KeyModifiers::SHIFT, KeyboardKey::PageDown) => {
need_redraw = s.scroll_down();
}
(KeyModifiers::SHIFT, KeyboardKey::Home) => {
need_redraw = s.scroll_home();
}
(KeyModifiers::SHIFT, KeyboardKey::End) => {
need_redraw = s.scroll_end();
} }
_ => (), _ => (),
} }
} }
EventOutcome::None if need_redraw {
EventOutcome::Redraw
} else {
EventOutcome::None
}
}); });
let draw_state_c = draw_state.clone(); let draw_state_c = draw_state.clone();

View File

@ -1,3 +1,5 @@
use std::collections::VecDeque;
use crate::attr::{CellAttributes, Color}; use crate::attr::{CellAttributes, Color};
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -13,9 +15,11 @@ pub struct GridRow {
} }
pub struct Buffer { pub struct Buffer {
scrollback: VecDeque<GridRow>,
rows: Vec<GridRow>, rows: Vec<GridRow>,
width: usize, width: usize,
height: usize, height: usize,
scrollback_limit: usize,
} }
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
@ -36,6 +40,7 @@ pub struct State {
esc_state: EscapeState, esc_state: EscapeState,
esc_args: Vec<u32>, esc_args: Vec<u32>,
scroll: usize,
pub cursor: Cursor, pub cursor: Cursor,
#[allow(unused)] #[allow(unused)]
saved_cursor: Option<Cursor>, saved_cursor: Option<Cursor>,
@ -106,6 +111,8 @@ impl Buffer {
pub fn new(width: usize, height: usize, bg: Color) -> Self { pub fn new(width: usize, height: usize, bg: Color) -> Self {
Self { Self {
rows: vec![GridRow::new(width, bg); height], rows: vec![GridRow::new(width, bg); height],
scrollback: VecDeque::new(),
scrollback_limit: 1024,
width, width,
height, height,
} }
@ -115,15 +122,30 @@ impl Buffer {
self.rows.fill(GridRow::new(self.width, bg)); self.rows.fill(GridRow::new(self.width, bg));
} }
pub fn dirty_rows(&mut self) -> impl Iterator<Item = (usize, &mut GridRow)> { pub fn iter_rows_mut<F: FnMut(usize, &mut GridRow)>(&mut self, scroll: usize, mut handler: F) {
self.rows.iter_mut().enumerate().filter_map(|(i, row)| { let scroll = scroll.min(self.scrollback.len());
let non_scroll = self.height.saturating_sub(scroll);
let scroll_end = self.height - non_scroll;
for y in 0..scroll_end {
let i = scroll - 1 - y;
let row = &mut self.scrollback[i];
handler(y, row);
}
for i in 0..non_scroll {
let y = scroll + i;
let row = &mut self.rows[i];
handler(y, row);
}
}
pub fn visible_rows_mut<F: FnMut(usize, &mut GridRow)>(&mut self, scroll: usize, mut handler: F) {
self.iter_rows_mut(scroll, |y, row| {
if row.dirty { if row.dirty {
row.dirty = false; row.dirty = false;
Some((i, row)) handler(y, row);
} else {
None
} }
}) });
} }
pub fn resize(&mut self, width: usize, height: usize, bg: Color) { pub fn resize(&mut self, width: usize, height: usize, bg: Color) {
@ -142,6 +164,11 @@ impl Buffer {
} }
pub fn scroll_once(&mut self, bg: Color) { pub fn scroll_once(&mut self, bg: Color) {
self.scrollback.push_front(self.rows[0].clone());
if self.scrollback.len() >= self.scrollback_limit {
self.scrollback.pop_back();
}
for i in 1..self.height { for i in 1..self.height {
self.rows[i - 1] = self.rows[i].clone(); self.rows[i - 1] = self.rows[i].clone();
self.rows[i - 1].dirty = true; self.rows[i - 1].dirty = true;
@ -156,6 +183,10 @@ impl Buffer {
pub fn set_row_dirty(&mut self, row: usize) { pub fn set_row_dirty(&mut self, row: usize) {
self.rows[row].dirty = true; self.rows[row].dirty = true;
} }
pub fn invalidate_rows(&mut self, scroll: usize) {
self.iter_rows_mut(scroll, |_, row| row.dirty = true);
}
} }
impl State { impl State {
@ -173,6 +204,7 @@ impl State {
esc_state: EscapeState::Normal, esc_state: EscapeState::Normal,
cursor: Cursor { row: 0, col: 0 }, cursor: Cursor { row: 0, col: 0 },
scroll: 0,
saved_cursor: None, saved_cursor: None,
default_attributes, default_attributes,
@ -183,6 +215,7 @@ impl State {
pub fn resize(&mut self, width: usize, height: usize) { pub fn resize(&mut self, width: usize, height: usize) {
self.buffer self.buffer
.resize(width, height, self.default_attributes.bg); .resize(width, height, self.default_attributes.bg);
self.scroll = 0;
if self.cursor.row >= height { if self.cursor.row >= height {
self.cursor.row = height - 1; self.cursor.row = height - 1;
@ -370,4 +403,63 @@ impl State {
EscapeState::Csi => self.handle_ctlseq_byte(ch), EscapeState::Csi => self.handle_ctlseq_byte(ch),
} }
} }
pub fn invalidate_current_viewport(&mut self) {
self.buffer.invalidate_rows(self.scroll);
}
pub fn adjust_scroll(&mut self) -> usize {
if self.scroll > self.buffer.scrollback.len() {
self.scroll = self.buffer.scrollback.len();
}
self.scroll
}
pub fn scroll_up(&mut self) -> bool {
let max = self.buffer.scrollback.len();
if max == 0 {
self.scroll = 0;
return true;
}
let amount = max.min(8);
if amount != 0 {
self.scroll += amount;
self.invalidate_current_viewport();
true
} else {
false
}
}
pub fn scroll_down(&mut self) -> bool {
let amount = self.scroll.min(8);
if amount != 0 {
self.scroll -= amount;
self.invalidate_current_viewport();
true
} else {
false
}
}
pub fn scroll_home(&mut self) -> bool {
if self.scroll != self.buffer.scrollback.len() {
self.scroll = self.buffer.scrollback.len();
self.invalidate_current_viewport();
true
} else {
false
}
}
pub fn scroll_end(&mut self) -> bool {
if self.scroll != 0 {
self.scroll = 0;
self.invalidate_current_viewport();
true
} else {
false
}
}
} }