use std::{ cmp::Ordering, collections::VecDeque, fmt::{self, Debug, Formatter}, }; const TABSTOP_WIDTH: usize = 8; // TODO: is this always right? const SCROLL_BACK: usize = 10_000; use crate::utils::logging::debug_log_to_file; use crate::panes::terminal_character::{ CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, }; fn get_top_non_canonical_rows(rows: &mut Vec) -> Vec { let mut index_of_last_non_canonical_row = None; for (i, row) in rows.iter().enumerate() { if row.is_canonical { break; } else { index_of_last_non_canonical_row = Some(i); } } match index_of_last_non_canonical_row { Some(index_of_last_non_canonical_row) => { rows.drain(..=index_of_last_non_canonical_row).collect() } None => vec![], } } fn get_bottom_canonical_row_and_wraps(rows: &mut VecDeque) -> Vec { let mut index_of_last_non_canonical_row = None; for (i, row) in rows.iter().enumerate().rev() { index_of_last_non_canonical_row = Some(i); if row.is_canonical { break; } } match index_of_last_non_canonical_row { Some(index_of_last_non_canonical_row) => { rows.drain(index_of_last_non_canonical_row..).collect() } None => vec![], } } fn transfer_rows_down( source: &mut VecDeque, destination: &mut Vec, count: usize, max_src_width: Option, max_dst_width: Option, ) { let mut next_lines: Vec = vec![]; let mut lines_added_to_destination: isize = 0; loop { if lines_added_to_destination as usize == count { break; } if next_lines.is_empty() { match source.pop_back() { Some(next_line) => { let mut top_non_canonical_rows_in_dst = get_top_non_canonical_rows(destination); lines_added_to_destination -= top_non_canonical_rows_in_dst.len() as isize; next_lines.push(next_line); next_lines.append(&mut top_non_canonical_rows_in_dst); next_lines = match max_dst_width { Some(max_row_width) => { Row::from_rows(next_lines).split_to_rows_of_length(max_row_width) } None => vec![Row::from_rows(next_lines)], }; if next_lines.is_empty() { // no more lines at source, the line we popped was probably empty break; } } None => break, // no more rows } } destination.insert(0, next_lines.pop().unwrap()); lines_added_to_destination += 1; } if !next_lines.is_empty() { match max_src_width { Some(max_row_width) => { let excess_rows = Row::from_rows(next_lines).split_to_rows_of_length(max_row_width); source.extend(excess_rows); } None => { let excess_row = Row::from_rows(next_lines); bounded_push(source, excess_row); } } } } fn transfer_rows_up( source: &mut Vec, destination: &mut VecDeque, count: usize, max_src_width: Option, max_dst_width: Option, ) { let mut next_lines: Vec = vec![]; for _ in 0..count { if next_lines.is_empty() { if !source.is_empty() { let next_line = source.remove(0); if !next_line.is_canonical { let mut bottom_canonical_row_and_wraps_in_dst = get_bottom_canonical_row_and_wraps(destination); next_lines.append(&mut bottom_canonical_row_and_wraps_in_dst); } next_lines.push(next_line); next_lines = match max_dst_width { Some(max_row_width) => { Row::from_rows(next_lines).split_to_rows_of_length(max_row_width) } None => vec![Row::from_rows(next_lines)], }; } else { break; // no more rows } } bounded_push(destination, next_lines.remove(0)); } if !next_lines.is_empty() { match max_src_width { Some(max_row_width) => { let excess_rows = Row::from_rows(next_lines).split_to_rows_of_length(max_row_width); for row in excess_rows { source.insert(0, row); } } None => { let excess_row = Row::from_rows(next_lines); source.insert(0, excess_row); } } } } fn bounded_push(vec: &mut VecDeque, value: Row) { if vec.len() >= SCROLL_BACK { vec.pop_front(); } vec.push_back(value) } #[derive(Clone)] pub struct Grid { lines_above: VecDeque, viewport: Vec, lines_below: Vec, alternative_lines_above_viewport_and_cursor: Option<(VecDeque, Vec, Cursor)>, cursor: Cursor, scroll_region: Option<(usize, usize)>, pending_styles: CharacterStyles, pub should_render: bool, pub cursor_key_mode: bool, // DECCKM - when set, cursor keys should send ANSI direction codes (eg. "OD") instead of the arrow keys (eg. "") pub clear_viewport_before_rendering: bool, pub width: usize, pub height: usize, } impl Debug for Grid { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { for (i, row) in self.viewport.iter().enumerate() { if row.is_canonical { writeln!(f, "{:?} (C): {:?}", i, row)?; } else { writeln!(f, "{:?} (W): {:?}", i, row)?; } } Ok(()) } } impl Grid { pub fn new(rows: usize, columns: usize) -> Self { Grid { lines_above: VecDeque::with_capacity(SCROLL_BACK), viewport: vec![Row::new().canonical()], lines_below: vec![], cursor: Cursor::new(0, 0), scroll_region: None, width: columns, height: rows, pending_styles: CharacterStyles::new(), should_render: true, cursor_key_mode: false, alternative_lines_above_viewport_and_cursor: None, clear_viewport_before_rendering: false, } } pub fn advance_to_next_tabstop(&mut self, styles: CharacterStyles) { let columns_until_next_tabstop = TABSTOP_WIDTH - (self.cursor.x % TABSTOP_WIDTH); let columns_until_screen_end = self.width - self.cursor.x; let columns_to_advance = std::cmp::min(columns_until_next_tabstop, columns_until_screen_end); let mut empty_character = EMPTY_TERMINAL_CHARACTER; empty_character.styles = styles; for _ in 0..columns_to_advance { self.add_character(empty_character) } } fn cursor_canonical_line_index(&self) -> usize { let mut cursor_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_lines_traversed += 1; } if i == self.cursor.y { break; } } cursor_canonical_line_index } // TODO: merge these two funtions fn cursor_index_in_canonical_line(&self) -> usize { let mut cursor_canonical_line_index = 0; let mut cursor_index_in_canonical_line = 0; for (i, line) in self.viewport.iter().enumerate() { if line.is_canonical { cursor_canonical_line_index = i; } if i == self.cursor.y { let line_wrap_position_in_line = self.cursor.y - cursor_canonical_line_index; cursor_index_in_canonical_line = line_wrap_position_in_line + self.cursor.x; break; } } cursor_index_in_canonical_line } fn canonical_line_y_coordinates(&self, canonical_line_index: usize) -> usize { let mut canonical_lines_traversed = 0; let mut y_coordinates = 0; for (i, line) in self.viewport.iter().enumerate() { if line.is_canonical { canonical_lines_traversed += 1; if canonical_lines_traversed == canonical_line_index + 1 { y_coordinates = i; break; } } } y_coordinates } pub fn scroll_up_one_line(&mut self) { if !self.lines_above.is_empty() && self.viewport.len() == self.height { let line_to_push_down = self.viewport.pop().unwrap(); self.lines_below.insert(0, line_to_push_down); let line_to_insert_at_viewport_top = self.lines_above.pop_back().unwrap(); self.viewport.insert(0, line_to_insert_at_viewport_top); } } pub fn scroll_down_one_line(&mut self) { if !self.lines_below.is_empty() && self.viewport.len() == self.height { let mut line_to_push_up = self.viewport.remove(0); if line_to_push_up.is_canonical { bounded_push(&mut self.lines_above, line_to_push_up); } else { let mut last_line_above = self.lines_above.pop_back().unwrap(); last_line_above.append(&mut line_to_push_up.columns); bounded_push(&mut self.lines_above, last_line_above); } let line_to_insert_at_viewport_bottom = self.lines_below.remove(0); self.viewport.push(line_to_insert_at_viewport_bottom); } } pub fn change_size(&mut self, new_rows: usize, new_columns: usize) { if new_columns != self.width { let mut cursor_canonical_line_index = self.cursor_canonical_line_index(); let cursor_index_in_canonical_line = self.cursor_index_in_canonical_line(); let mut viewport_canonical_lines = vec![]; for mut row in self.viewport.drain(..) { if !row.is_canonical && viewport_canonical_lines.is_empty() && !self.lines_above.is_empty() { let mut first_line_above = self.lines_above.pop_back().unwrap(); first_line_above.append(&mut row.columns); viewport_canonical_lines.push(first_line_above); cursor_canonical_line_index += 1; } else if row.is_canonical { viewport_canonical_lines.push(row); } else { match viewport_canonical_lines.last_mut() { Some(last_line) => { last_line.append(&mut row.columns); } None => { // the state is corrupted somehow // this is a bug and I'm not yet sure why it happens // usually it fixes itself and is a result of some race // TODO: investigate why this happens and solve it return; } } } } let mut new_viewport_rows = vec![]; for mut canonical_line in viewport_canonical_lines { let mut canonical_line_parts: Vec = vec![]; if canonical_line.columns.is_empty() { canonical_line_parts.push(Row::new().canonical()); } while !canonical_line.columns.is_empty() { let next_wrap = if canonical_line.len() > new_columns { canonical_line.columns.drain(..new_columns) } else { canonical_line.columns.drain(..) }; let row = Row::from_columns(next_wrap.collect()); // if there are no more parts, this row is canonical as long as it originally // was canonical (it might not have been for example if it's the first row in // the viewport, and the actual canonical row is above it in the scrollback) let row = if canonical_line_parts.is_empty() && canonical_line.is_canonical { row.canonical() } else { row }; canonical_line_parts.push(row); } new_viewport_rows.append(&mut canonical_line_parts); } self.viewport = new_viewport_rows; let mut new_cursor_y = self.canonical_line_y_coordinates(cursor_canonical_line_index); let new_cursor_x = (cursor_index_in_canonical_line / new_columns) + (cursor_index_in_canonical_line % new_columns); let current_viewport_row_count = self.viewport.len(); match current_viewport_row_count.cmp(&self.height) { Ordering::Less => { let row_count_to_transfer = self.height - current_viewport_row_count; transfer_rows_down( &mut self.lines_above, &mut self.viewport, row_count_to_transfer, None, Some(new_columns), ); let rows_pulled = self.viewport.len() - current_viewport_row_count; new_cursor_y += rows_pulled; } Ordering::Greater => { let row_count_to_transfer = current_viewport_row_count - self.height; if row_count_to_transfer > new_cursor_y { new_cursor_y = 0; } else { new_cursor_y -= row_count_to_transfer; } transfer_rows_up( &mut self.viewport, &mut self.lines_above, row_count_to_transfer, Some(new_columns), None, ); } Ordering::Equal => {} } self.cursor.y = new_cursor_y; self.cursor.x = new_cursor_x; } if new_rows != self.height { let current_viewport_row_count = self.viewport.len(); match current_viewport_row_count.cmp(&new_rows) { Ordering::Less => { let row_count_to_transfer = new_rows - current_viewport_row_count; transfer_rows_down( &mut self.lines_above, &mut self.viewport, row_count_to_transfer, None, Some(new_columns), ); let rows_pulled = self.viewport.len() - current_viewport_row_count; self.cursor.y += rows_pulled; } Ordering::Greater => { let row_count_to_transfer = current_viewport_row_count - new_rows; if row_count_to_transfer > self.cursor.y { self.cursor.y = 0; } else { self.cursor.y -= row_count_to_transfer; } transfer_rows_up( &mut self.viewport, &mut self.lines_above, row_count_to_transfer, Some(new_columns), None, ); } Ordering::Equal => {} } } self.height = new_rows; self.width = new_columns; if self.scroll_region.is_some() { self.set_scroll_region_to_viewport_size(); } } pub fn as_character_lines(&self) -> Vec> { let mut lines: Vec> = self .viewport .iter() .map(|r| { let mut line: Vec = r.columns.iter().copied().collect(); // pad line line.resize(self.width, EMPTY_TERMINAL_CHARACTER); line }) .collect(); let empty_row = vec![EMPTY_TERMINAL_CHARACTER; self.width]; for _ in lines.len()..self.height { lines.push(empty_row.clone()); } lines } pub fn cursor_coordinates(&self) -> Option<(usize, usize)> { if self.cursor.is_hidden { None } else { Some((self.cursor.x, self.cursor.y)) } } pub fn move_viewport_up(&mut self, count: usize) { for _ in 0..count { self.scroll_up_one_line(); } } pub fn move_viewport_down(&mut self, count: usize) { for _ in 0..count { self.scroll_down_one_line(); } } pub fn reset_viewport(&mut self) { let row_count_below = self.lines_below.len(); for _ in 0..row_count_below { self.scroll_down_one_line(); } } pub fn rotate_scroll_region_up(&mut self, count: usize) { if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region { for _ in 0..count { let columns = vec![EMPTY_TERMINAL_CHARACTER; self.width]; if scroll_region_bottom < self.viewport.len() { self.viewport.remove(scroll_region_bottom); } if scroll_region_top < self.viewport.len() { self.viewport .insert(scroll_region_top, Row::from_columns(columns).canonical()); } } } } pub fn rotate_scroll_region_down(&mut self, count: usize) { if let Some((scroll_region_top, scroll_region_bottom)) = self.scroll_region { for _ in 0..count { let columns = vec![EMPTY_TERMINAL_CHARACTER; self.width]; self.viewport.remove(scroll_region_top); if self.viewport.len() > scroll_region_top { self.viewport .insert(scroll_region_bottom, Row::from_columns(columns).canonical()); } else { self.viewport.push(Row::from_columns(columns).canonical()); } } } } 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; } self.viewport.remove(scroll_region_top); let columns = vec![EMPTY_TERMINAL_CHARACTER; self.width]; self.viewport .insert(scroll_region_bottom, Row::from_columns(columns).canonical()); return; } } if self.viewport.len() <= self.cursor.y + 1 { // FIXME: this should add an empty line with the pad_character // but for some reason this breaks rendering in various situations // it needs to be investigated and fixed let new_row = Row::new().canonical(); self.viewport.push(new_row); } if self.cursor.y == self.height - 1 { let row_count_to_transfer = 1; transfer_rows_up( &mut self.viewport, &mut self.lines_above, row_count_to_transfer, Some(self.width), None, ); } else { self.cursor.y += 1; } } pub fn move_cursor_to_beginning_of_line(&mut self) { self.cursor.x = 0; } pub fn move_cursor_backwards(&mut self, count: usize) { if self.cursor.x > count { self.cursor.x -= count; } else { self.cursor.x = 0; } } pub fn insert_character_at_cursor_position(&mut self, terminal_character: TerminalCharacter) { match self.viewport.get_mut(self.cursor.y) { Some(row) => { row.insert_character_at(terminal_character, self.cursor.x); if row.len() > self.width { row.truncate(self.width); } } None => { // pad lines until cursor if they do not exist for _ in self.viewport.len()..self.cursor.y { self.viewport.push(Row::new().canonical()); } self.viewport .push(Row::new().with_character(terminal_character).canonical()); } } } pub fn add_character_at_cursor_position(&mut self, terminal_character: TerminalCharacter) { match self.viewport.get_mut(self.cursor.y) { Some(row) => row.add_character_at(terminal_character, self.cursor.x), None => { // pad lines until cursor if they do not exist for _ in self.viewport.len()..self.cursor.y { self.viewport.push(Row::new().canonical()); } self.viewport .push(Row::new().with_character(terminal_character).canonical()); } } } pub fn add_character(&mut self, terminal_character: TerminalCharacter) { // TODO: try to separate adding characters from moving the cursors in this function if self.cursor.x >= self.width { // line wrap self.cursor.x = 0; if self.cursor.y == self.height - 1 { let row_count_to_transfer = 1; transfer_rows_up( &mut self.viewport, &mut self.lines_above, row_count_to_transfer, Some(self.width), None, ); let wrapped_row = Row::new(); self.viewport.push(wrapped_row); } else { self.cursor.y += 1; if self.viewport.len() <= self.cursor.y { let line_wrapped_row = Row::new(); self.viewport.push(line_wrapped_row); } } } self.add_character_at_cursor_position(terminal_character); self.move_cursor_forward_until_edge(1); } pub fn move_cursor_forward_until_edge(&mut self, count: usize) { let count_to_move = std::cmp::min(count, self.width - (self.cursor.x)); self.cursor.x += count_to_move; } pub fn replace_characters_in_line_after_cursor(&mut self, replace_with: TerminalCharacter) { self.viewport .get_mut(self.cursor.y) .unwrap() .truncate(self.cursor.x); if self.cursor.x < self.width - 1 { let mut characters_to_append = vec![replace_with; self.width - self.cursor.x]; self.viewport .get_mut(self.cursor.y) .unwrap() .append(&mut characters_to_append); } } pub fn replace_characters_in_line_before_cursor(&mut self, replace_with: TerminalCharacter) { let line_part = vec![replace_with; self.cursor.x]; let row = self.viewport.get_mut(self.cursor.y).unwrap(); row.replace_beginning_with(line_part); } pub fn clear_all_after_cursor(&mut self, replace_with: TerminalCharacter) { let cursor_row = self.viewport.get_mut(self.cursor.y).unwrap(); cursor_row.truncate(self.cursor.x); let mut replace_with_columns_in_cursor_row = vec![replace_with; self.width - self.cursor.x]; cursor_row.append(&mut replace_with_columns_in_cursor_row); let replace_with_columns = vec![replace_with; self.width]; self.replace_characters_in_line_after_cursor(replace_with); for row in self.viewport.iter_mut().skip(self.cursor.y + 1) { row.replace_columns(replace_with_columns.clone()); } } pub fn clear_cursor_line(&mut self) { self.viewport.get_mut(self.cursor.y).unwrap().truncate(0); } pub fn clear_all(&mut self, replace_with: TerminalCharacter) { let replace_with_columns = vec![replace_with; self.width]; self.replace_characters_in_line_after_cursor(replace_with); for row in self.viewport.iter_mut() { row.replace_columns(replace_with_columns.clone()); } } fn pad_current_line_until(&mut self, position: usize) { let current_row = self.viewport.get_mut(self.cursor.y).unwrap(); for _ in current_row.len()..position { current_row.push(EMPTY_TERMINAL_CHARACTER); } } fn pad_lines_until(&mut self, position: usize, pad_character: TerminalCharacter) { for _ in self.viewport.len()..=position { let columns = vec![pad_character; self.width]; self.viewport.push(Row::from_columns(columns).canonical()); } } pub fn move_cursor_to(&mut self, x: usize, y: usize, pad_character: TerminalCharacter) { 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); self.pad_current_line_until(self.cursor.x); } pub fn move_cursor_up(&mut self, count: usize) { self.cursor.y = if self.cursor.y < count { 0 } else { self.cursor.y - count }; } 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)); for _ in 0..count { let current_line_index = self.cursor.y; if current_line_index == scroll_region_top { // if we're at the top line, we create a new line and remove the last line that // would otherwise overflow if scroll_region_bottom < self.viewport.len() { self.viewport.remove(scroll_region_bottom); } self.viewport.insert(current_line_index, Row::new()); // TODO: .canonical() ? } else if current_line_index > scroll_region_top && current_line_index <= scroll_region_bottom { self.move_cursor_up(count); } } } pub fn move_cursor_down(&mut self, count: usize, pad_character: TerminalCharacter) { let lines_to_add = if self.cursor.y + count > self.height - 1 { (self.cursor.y + count) - (self.height - 1) } else { 0 }; self.cursor.y = if self.cursor.y + count > self.height - 1 { self.height - 1 } else { self.cursor.y + count }; for _ in 0..lines_to_add { self.add_canonical_line(); } self.pad_lines_until(self.cursor.y, pad_character); } pub fn move_cursor_back(&mut self, count: usize) { if self.cursor.x < count { self.cursor.x = 0; } else { self.cursor.x -= count; } } pub fn hide_cursor(&mut self) { self.cursor.is_hidden = true; } pub fn show_cursor(&mut self) { self.cursor.is_hidden = false; } pub fn set_scroll_region(&mut self, top_line_index: usize, bottom_line_index: usize) { self.scroll_region = Some((top_line_index, bottom_line_index)); } 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 - 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 = 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()); } } } } } pub fn add_empty_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 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 = vec![pad_character; self.width]; self.viewport .insert(current_line_index, Row::from_columns(columns).canonical()); } } } } pub fn move_cursor_to_column(&mut self, column: usize) { self.cursor.x = column; self.pad_current_line_until(self.cursor.x); } pub fn move_cursor_to_line(&mut self, line: usize, pad_character: TerminalCharacter) { self.cursor.y = std::cmp::min(self.height - 1, line); self.pad_lines_until(self.cursor.y, pad_character); self.pad_current_line_until(self.cursor.x); } pub fn replace_with_empty_chars(&mut self, count: usize, empty_char_style: CharacterStyles) { let mut empty_character = EMPTY_TERMINAL_CHARACTER; empty_character.styles = empty_char_style; let pad_until = std::cmp::min(self.width, self.cursor.x + count); self.pad_current_line_until(pad_until); let current_row = self.viewport.get_mut(self.cursor.y).unwrap(); for i in 0..count { current_row.replace_character_at(empty_character, self.cursor.x + i); } } pub fn erase_characters(&mut self, count: usize, empty_char_style: CharacterStyles) { let mut empty_character = EMPTY_TERMINAL_CHARACTER; empty_character.styles = empty_char_style; let current_row = self.viewport.get_mut(self.cursor.y).unwrap(); for _ in 0..count { current_row.delete_character(self.cursor.x); } let mut empty_space_to_append = vec![empty_character; count]; self.viewport .get_mut(self.cursor.y) .unwrap() .append(&mut empty_space_to_append); } fn add_newline(&mut self) { self.add_canonical_line(); self.mark_for_rerender(); } pub fn mark_for_rerender(&mut self) { self.should_render = true; } fn reset_terminal_state(&mut self) { self.lines_above = VecDeque::with_capacity(SCROLL_BACK); self.lines_below = vec![]; self.viewport = vec![Row::new().canonical()]; self.alternative_lines_above_viewport_and_cursor = None; self.cursor_key_mode = false; self.scroll_region = None; self.clear_viewport_before_rendering = true; self.cursor = Cursor::new(0, 0); } } impl vte::Perform for Grid { fn print(&mut self, c: char) { // apparently, building TerminalCharacter like this without a "new" method // is a little faster let terminal_character = TerminalCharacter { character: c, styles: self.pending_styles, }; self.add_character(terminal_character); } fn execute(&mut self, byte: u8) { match byte { 8 => { // backspace self.move_cursor_backwards(1); } 9 => { // tab self.advance_to_next_tabstop(self.pending_styles); } 10 => { // 0a, newline self.add_newline(); } 13 => { // 0d, carriage return self.move_cursor_to_beginning_of_line(); } _ => {} } } fn hook(&mut self, _params: &[i64], _intermediates: &[u8], _ignore: bool, _c: char) { // TBD } fn put(&mut self, _byte: u8) { // TBD } fn unhook(&mut self) { // TBD } fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) { // TBD } fn csi_dispatch(&mut self, params: &[i64], _intermediates: &[u8], _ignore: bool, c: char) { if c == 'm' { self.pending_styles.add_style_from_ansi_params(params); } else if c == 'C' { // move cursor forward let move_by = if params[0] == 0 { 1 } else { params[0] as usize }; self.move_cursor_forward_until_edge(move_by); } else if c == 'K' { // clear line (0 => right, 1 => left, 2 => all) if params[0] == 0 { let mut char_to_replace = EMPTY_TERMINAL_CHARACTER; char_to_replace.styles = self.pending_styles; self.replace_characters_in_line_after_cursor(char_to_replace); } else if params[0] == 1 { let mut char_to_replace = EMPTY_TERMINAL_CHARACTER; char_to_replace.styles = self.pending_styles; self.replace_characters_in_line_before_cursor(char_to_replace); } else if params[0] == 2 { self.clear_cursor_line(); } } else if c == 'J' { // clear all (0 => below, 1 => above, 2 => all, 3 => saved) let mut char_to_replace = EMPTY_TERMINAL_CHARACTER; char_to_replace.styles = self.pending_styles; if params[0] == 0 { self.clear_all_after_cursor(char_to_replace); } else if params[0] == 2 { self.clear_all(char_to_replace); } // TODO: implement 1 } else if c == 'H' { // goto row/col // we subtract 1 from the row/column because these are 1 indexed // (except when they are 0, in which case they should be 1 // don't look at me, I don't make the rules) let (row, col) = if params.len() == 1 { if params[0] == 0 { (0, params[0] as usize) } else { ((params[0] as usize).saturating_sub(1), params[0] as usize) } } else if params[0] == 0 { (0, (params[1] as usize).saturating_sub(1)) } else { ( (params[0] as usize).saturating_sub(1), (params[1] as usize).saturating_sub(1), ) }; let pad_character = EMPTY_TERMINAL_CHARACTER; self.move_cursor_to(col, row, pad_character); } else if c == 'A' { // move cursor up until edge of screen let move_up_count = if params[0] == 0 { 1 } else { params[0] }; self.move_cursor_up(move_up_count as usize); } else if c == 'B' { // move cursor down until edge of screen let move_down_count = if params[0] == 0 { 1 } else { params[0] }; let pad_character = EMPTY_TERMINAL_CHARACTER; self.move_cursor_down(move_down_count as usize, pad_character); } else if c == 'D' { let move_back_count = if params[0] == 0 { 1 } else { params[0] as usize }; self.move_cursor_back(move_back_count); } else if c == 'l' { let first_intermediate_is_questionmark = match _intermediates.get(0) { Some(b'?') => true, None => false, _ => false, }; if first_intermediate_is_questionmark { match params.get(0) { Some(&1049) => { if let Some(( alternative_lines_above, alternative_viewport, alternative_cursor, )) = self.alternative_lines_above_viewport_and_cursor.as_mut() { std::mem::swap(&mut self.lines_above, alternative_lines_above); std::mem::swap(&mut self.viewport, alternative_viewport); std::mem::swap(&mut self.cursor, alternative_cursor); } self.alternative_lines_above_viewport_and_cursor = None; self.clear_viewport_before_rendering = true; self.change_size(self.height, self.width); // the alternative_viewport might have been of a different size... self.mark_for_rerender(); } Some(&25) => { self.hide_cursor(); self.mark_for_rerender(); } Some(&1) => { self.cursor_key_mode = false; } _ => {} }; } } else if c == 'h' { let first_intermediate_is_questionmark = match _intermediates.get(0) { Some(b'?') => true, None => false, _ => false, }; if first_intermediate_is_questionmark { match params.get(0) { Some(&25) => { self.show_cursor(); self.mark_for_rerender(); } Some(&1049) => { let current_lines_above = std::mem::replace( &mut self.lines_above, VecDeque::with_capacity(SCROLL_BACK), ); let current_viewport = std::mem::replace(&mut self.viewport, vec![Row::new().canonical()]); let current_cursor = std::mem::replace(&mut self.cursor, Cursor::new(0, 0)); self.alternative_lines_above_viewport_and_cursor = Some((current_lines_above, current_viewport, current_cursor)); self.clear_viewport_before_rendering = true; } Some(&1) => { self.cursor_key_mode = true; } _ => {} }; } } else if c == 'r' { if params.len() > 1 { // minus 1 because these are 1 indexed let top_line_index = (params[0] as usize).saturating_sub(1); let bottom_line_index = (params[1] as usize).saturating_sub(1); self.set_scroll_region(top_line_index, bottom_line_index); self.show_cursor(); } else { self.clear_scroll_region(); } } else if c == 'M' { // delete lines if currently inside scroll region let line_count_to_delete = if params[0] == 0 { 1 } else { params[0] as usize }; let pad_character = EMPTY_TERMINAL_CHARACTER; self.delete_lines_in_scroll_region(line_count_to_delete, pad_character); } else if c == 'L' { // insert blank lines if inside scroll region let line_count_to_add = if params[0] == 0 { 1 } else { params[0] as usize }; let pad_character = EMPTY_TERMINAL_CHARACTER; self.add_empty_lines_in_scroll_region(line_count_to_add, pad_character); } else if c == 'q' { // ignore for now to run on mac } else if c == 'G' { let column = if params[0] == 0 { 0 } else { params[0] as usize - 1 }; self.move_cursor_to_column(column); } else if c == 'd' { // goto line let line = if params[0] == 0 { 1 } else { // minus 1 because this is 1 indexed params[0] as usize - 1 }; let pad_character = EMPTY_TERMINAL_CHARACTER; self.move_cursor_to_line(line, pad_character); } else if c == 'P' { // erase characters let count = if params[0] == 0 { 1 } else { params[0] as usize }; self.erase_characters(count, self.pending_styles); } else if c == 'X' { // erase characters and replace with empty characters of current style let count = if params[0] == 0 { 1 } else { params[0] as usize }; self.replace_with_empty_chars(count, self.pending_styles); } else if c == 'T' { /* * 124 54 T SD * Scroll down, new lines inserted at top of screen * [4T = Scroll down 4, bring previous lines back into view */ let line_count: i64 = *params.get(0).expect("A number of lines was expected."); if line_count >= 0 { self.rotate_scroll_region_up(line_count as usize); } else { // TODO: can this actually happen? self.rotate_scroll_region_down(line_count.abs() as usize); } } else if c == 'S' { // move scroll up let count = if params[0] == 0 { 1 } else { params[0] as usize }; self.rotate_scroll_region_down(count); } else if c == '@' { let count = if params[0] == 0 { 1 } else { params[0] as usize }; for _ in 0..count { // TODO: should this be styled? self.insert_character_at_cursor_position(EMPTY_TERMINAL_CHARACTER); } } else { let _ = debug_log_to_file(format!("Unhandled csi: {}->{:?}", c, params)); } } fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) { match (byte, intermediates.get(0)) { (b'M', None) => { self.move_cursor_up_with_scrolling(1); } (b'c', None) => { self.reset_terminal_state(); } _ => {} } } } #[derive(Clone)] pub struct Row { pub columns: Vec, pub is_canonical: bool, } impl Debug for Row { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { for character in &self.columns { write!(f, "{:?}", character)?; } Ok(()) } } impl Row { pub fn new() -> Self { Row { columns: vec![], is_canonical: false, } } pub fn from_columns(columns: Vec) -> Self { Row { columns, is_canonical: false, } } pub fn from_rows(mut rows: Vec) -> Self { if rows.is_empty() { Row::new() } else { let mut first_row = rows.remove(0); for row in rows.iter_mut() { first_row.append(&mut row.columns); } first_row } } pub fn with_character(mut self, terminal_character: TerminalCharacter) -> Self { self.columns.push(terminal_character); self } pub fn canonical(mut self) -> Self { self.is_canonical = true; self } pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { match self.columns.len().cmp(&x) { Ordering::Equal => self.columns.push(terminal_character), Ordering::Less => { self.columns.resize(x, EMPTY_TERMINAL_CHARACTER); self.columns.push(terminal_character); } Ordering::Greater => { // this is much more performant than remove/insert self.columns.push(terminal_character); self.columns.swap_remove(x); } } } pub fn insert_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { match self.columns.len().cmp(&x) { Ordering::Equal => self.columns.push(terminal_character), Ordering::Less => { self.columns.resize(x, EMPTY_TERMINAL_CHARACTER); self.columns.push(terminal_character); } Ordering::Greater => { self.columns.insert(x, terminal_character); } } } pub fn replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { // this is much more performant than remove/insert if x < self.columns.len() { self.columns.push(terminal_character); self.columns.swap_remove(x); } } pub fn replace_columns(&mut self, columns: Vec) { self.columns = columns; } pub fn push(&mut self, terminal_character: TerminalCharacter) { self.columns.push(terminal_character); } pub fn truncate(&mut self, x: usize) { self.columns.truncate(x); } pub fn append(&mut self, to_append: &mut Vec) { self.columns.append(to_append); } pub fn replace_beginning_with(&mut self, mut line_part: Vec) { if line_part.len() > self.columns.len() { self.columns.clear(); } else { drop(self.columns.drain(0..line_part.len())); } line_part.append(&mut self.columns); self.columns = line_part; } pub fn len(&self) -> usize { self.columns.len() } pub fn delete_character(&mut self, x: usize) { if x < self.columns.len() { self.columns.remove(x); } } pub fn split_to_rows_of_length(&mut self, max_row_length: usize) -> Vec { let mut parts: Vec = vec![]; let mut current_part: Vec = vec![]; for character in self.columns.drain(..) { if current_part.len() == max_row_length { parts.push(Row::from_columns(current_part)); current_part = vec![]; } current_part.push(character); } if !current_part.is_empty() { parts.push(Row::from_columns(current_part)) }; if !parts.is_empty() && self.is_canonical { parts.get_mut(0).unwrap().is_canonical = true; } parts } } #[derive(Clone, Debug)] pub struct Cursor { x: usize, y: usize, is_hidden: bool, } impl Cursor { pub fn new(x: usize, y: usize) -> Self { Cursor { x, y, is_hidden: false, } } }