diff options
| author | Brooks J Rady <b.j.rady@gmail.com> | 2021-01-06 17:46:50 +0000 |
|---|---|---|
| committer | Brooks J Rady <b.j.rady@gmail.com> | 2021-01-06 17:46:50 +0000 |
| commit | 4a176669fa93f6ea37530acb667acfaf151fb7f1 (patch) | |
| tree | 050f89fba88986bd7db2f8fd193091c854342a89 /src/panes | |
| parent | Warnings purged and some FIXME's addressed (diff) | |
| download | sl0ppij-4a176669fa93f6ea37530acb667acfaf151fb7f1.tar.gz sl0ppij-4a176669fa93f6ea37530acb667acfaf151fb7f1.tar.xz sl0ppij-4a176669fa93f6ea37530acb667acfaf151fb7f1.zip | |
A module rename to please clippy
Diffstat (limited to 'src/panes')
| -rw-r--r-- | src/panes/mod.rs | 9 | ||||
| -rw-r--r-- | src/panes/plugin_pane.rs | 167 | ||||
| -rw-r--r-- | src/panes/scroll.rs | 833 | ||||
| -rw-r--r-- | src/panes/terminal_character.rs | 577 | ||||
| -rw-r--r-- | src/panes/terminal_pane.rs | 617 |
5 files changed, 2203 insertions, 0 deletions
diff --git a/src/panes/mod.rs b/src/panes/mod.rs new file mode 100644 index 00000000..7d478886 --- /dev/null +++ b/src/panes/mod.rs @@ -0,0 +1,9 @@ +mod plugin_pane; +mod scroll; +mod terminal_character; +mod terminal_pane; + +pub use plugin_pane::*; +pub use scroll::*; +pub use terminal_character::*; +pub use terminal_pane::*; diff --git a/src/panes/plugin_pane.rs b/src/panes/plugin_pane.rs new file mode 100644 index 00000000..1573d577 --- /dev/null +++ b/src/panes/plugin_pane.rs @@ -0,0 +1,167 @@ +#![allow(clippy::clippy::if_same_then_else)] + +use crate::{pty_bus::VteEvent, tab::Pane, wasm_vm::PluginInstruction, SenderWithContext}; + +use std::{sync::mpsc::channel, unimplemented}; + +use crate::panes::{PaneId, PositionAndSize}; + +pub struct PluginPane { + pub pid: u32, + pub should_render: bool, + pub position_and_size: PositionAndSize, + pub position_and_size_override: Option<PositionAndSize>, + pub send_plugin_instructions: SenderWithContext<PluginInstruction>, +} + +impl PluginPane { + pub fn new( + pid: u32, + position_and_size: PositionAndSize, + send_plugin_instructions: SenderWithContext<PluginInstruction>, + ) -> Self { + Self { + pid, + should_render: true, + position_and_size, + position_and_size_override: None, + send_plugin_instructions, + } + } +} + +impl Pane for PluginPane { + // FIXME: These position and size things should all be moved to default trait implementations, + // with something like a get_pos_and_sz() method underpinning all of them. Alternatively and + // preferably, just use an enum and not a trait object + fn x(&self) -> usize { + self.position_and_size_override + .unwrap_or(self.position_and_size) + .x + } + fn y(&self) -> usize { + self.position_and_size_override + .unwrap_or(self.position_and_size) + .y + } + fn rows(&self) -> usize { + self.position_and_size_override + .unwrap_or(self.position_and_size) + .rows + } + fn columns(&self) -> usize { + self.position_and_size_override + .unwrap_or(self.position_and_size) + .columns + } + fn reset_size_and_position_override(&mut self) { + self.position_and_size_override = None; + self.should_render = true; + } + fn change_pos_and_size(&mut self, position_and_size: &PositionAndSize) { + self.position_and_size = *position_and_size; + self.should_render = true; + } + // FIXME: This is obviously a bit outdated and needs the x and y moved into `size` + fn override_size_and_position(&mut self, x: usize, y: usize, size: &PositionAndSize) { + let position_and_size_override = PositionAndSize { + x, + y, + rows: size.rows, + columns: size.columns, + }; + self.position_and_size_override = Some(position_and_size_override); + self.should_render = true; + } + fn handle_event(&mut self, _event: VteEvent) { + unimplemented!() + } + fn cursor_coordinates(&self) -> Option<(usize, usize)> { + None + } + fn adjust_input_to_terminal(&self, _input_bytes: Vec<u8>) -> Vec<u8> { + unimplemented!() // FIXME: Shouldn't need this implmented? + } + + fn position_and_size_override(&self) -> Option<PositionAndSize> { + self.position_and_size_override + } + fn should_render(&self) -> bool { + self.should_render + } + fn set_should_render(&mut self, should_render: bool) { + self.should_render = should_render; + } + fn render(&mut self) -> Option<String> { + // if self.should_render { + if true { + // while checking should_render rather than rendering each pane every time + // is more performant, it causes some problems when the pane to the left should be + // rendered and has wide characters (eg. Chinese characters or emoji) + // as a (hopefully) temporary hack, we render all panes until we find a better solution + let (buf_tx, buf_rx) = channel(); + + self.send_plugin_instructions + .send(PluginInstruction::Draw( + buf_tx, + self.pid, + self.rows(), + self.columns(), + )) + .unwrap(); + + self.should_render = false; + Some(buf_rx.recv().unwrap()) + } else { + None + } + } + fn pid(&self) -> PaneId { + PaneId::Plugin(self.pid) + } + fn reduce_height_down(&mut self, count: usize) { + self.position_and_size.y += count; + self.position_and_size.rows -= count; + self.should_render = true; + } + fn increase_height_down(&mut self, count: usize) { + self.position_and_size.rows += count; + self.should_render = true; + } + fn increase_height_up(&mut self, count: usize) { + self.position_and_size.y -= count; + self.position_and_size.rows += count; + self.should_render = true; + } + fn reduce_height_up(&mut self, count: usize) { + self.position_and_size.rows -= count; + self.should_render = true; + } + fn reduce_width_right(&mut self, count: usize) { + self.position_and_size.x += count; + self.position_and_size.columns -= count; + self.should_render = true; + } + fn reduce_width_left(&mut self, count: usize) { + self.position_and_size.columns -= count; + self.should_render = true; + } + fn increase_width_left(&mut self, count: usize) { + self.position_and_size.x -= count; + self.position_and_size.columns += count; + self.should_render = true; + } + fn increase_width_right(&mut self, count: usize) { + self.position_and_size.columns += count; + self.should_render = true; + } + fn scroll_up(&mut self, _count: usize) { + unimplemented!() + } + fn scroll_down(&mut self, _count: usize) { + unimplemented!() + } + fn clear_scroll(&mut self) { + unimplemented!() + } +} diff --git a/src/panes/scroll.rs b/src/panes/scroll.rs new file mode 100644 index 00000000..05252b03 --- /dev/null +++ b/src/panes/scroll.rs @@ -0,0 +1,833 @@ +use std::collections::VecDeque; +use std::fmt::{self, Debug, Formatter}; + +use crate::panes::terminal_character::{ + CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, +}; + +/* + * Scroll + * + * holds the terminal buffer and controls the viewport (which part of it we see) + * its functions include line-wrapping and tracking the location of the cursor + * + */ + +/* + * CanonicalLine vs. WrappedFragment + * + * If the terminal had infinite width and we did not need to line wrap, the CanonicalLine would + * be our only unit of line separation. + * Each CanonicalLine has one or more WrappedFragments, which are re-calculated when the terminal is + * resized, or when characters are added to the line + * + */ + +#[derive(Clone)] +pub struct CanonicalLine { + pub wrapped_fragments: Vec<WrappedFragment>, +} + +impl CanonicalLine { + pub fn new() -> Self { + CanonicalLine { + wrapped_fragments: vec![WrappedFragment::new()], + } + } + pub fn add_new_wrap(&mut self, terminal_character: TerminalCharacter) { + let mut new_fragment = WrappedFragment::new(); + new_fragment.add_character(terminal_character, 0); + self.wrapped_fragments.push(new_fragment); + } + pub fn change_width(&mut self, new_width: usize) { + let characters = self.flattened_characters(); + let wrapped_fragments = + CanonicalLine::fill_fragments_up_to_width(characters, new_width, None); + self.wrapped_fragments = wrapped_fragments; + } + pub fn clear_after(&mut self, fragment_index: usize, column_index: usize) { + let fragment_to_clear = self + .wrapped_fragments + .get_mut(fragment_index) + .expect("fragment out of bounds"); + fragment_to_clear.clear_after_and_including(column_index); + self.wrapped_fragments.truncate(fragment_index + 1); + } + pub fn replace_with_empty_chars( + &mut self, + fragment_index: usize, + from_col: usize, + count: usize, + style_of_empty_space: CharacterStyles, + ) { + let mut characters_replaced = 0; + let mut column_position_in_fragment = from_col; + let mut current_fragment = fragment_index; + let mut empty_space_character = EMPTY_TERMINAL_CHARACTER; + empty_space_character.styles = style_of_empty_space; + loop { + if characters_replaced == count { + break; + } + match self.wrapped_fragments.get_mut(current_fragment) { + Some(fragment_to_clear) => { + let fragment_characters_count = fragment_to_clear.characters.len(); + if fragment_characters_count >= column_position_in_fragment { + fragment_to_clear + .add_character(empty_space_character, column_position_in_fragment); + column_position_in_fragment += 1; + characters_replaced += 1; + } else { + current_fragment += 1; + column_position_in_fragment = 0; + } + } + None => { + // end of line, nothing more to clear + break; + } + } + } + } + pub fn erase_chars( + &mut self, + fragment_index: usize, + from_col: usize, + count: usize, + style_of_empty_space: CharacterStyles, + ) { + let mut empty_character = EMPTY_TERMINAL_CHARACTER; + empty_character.styles = style_of_empty_space; + let current_width = self.wrapped_fragments.get(0).unwrap().characters.len(); + let mut characters = self.flattened_characters(); + let absolute_position_of_character = fragment_index * current_width + from_col; + for _ in 0..count { + characters.remove(absolute_position_of_character); + } + let wrapped_fragments = CanonicalLine::fill_fragments_up_to_width( + characters, + current_width, + Some(empty_character), + ); + self.wrapped_fragments = wrapped_fragments; + } + pub fn replace_with_empty_chars_after_cursor( + &mut self, + fragment_index: usize, + from_col: usize, + total_columns: usize, + style_of_empty_space: CharacterStyles, + ) { + let mut empty_char_character = EMPTY_TERMINAL_CHARACTER; + empty_char_character.styles = style_of_empty_space; + let current_fragment = self.wrapped_fragments.get_mut(fragment_index).unwrap(); + let fragment_characters_count = current_fragment.characters.len(); + + for i in from_col..fragment_characters_count { + current_fragment.add_character(empty_char_character, i); + } + + for i in fragment_characters_count..total_columns { + current_fragment.add_character(empty_char_character, i); + } + + self.wrapped_fragments.truncate(fragment_index + 1); + } + pub fn replace_with_empty_chars_before_cursor( + &mut self, + fragment_index: usize, + until_col: usize, + style_of_empty_space: CharacterStyles, + ) { + let mut empty_char_character = EMPTY_TERMINAL_CHARACTER; + empty_char_character.styles = style_of_empty_space; + + if fragment_index > 0 { + for i in 0..(fragment_index - 1) { + let fragment = self.wrapped_fragments.get_mut(i).unwrap(); + for i in 0..fragment.characters.len() { + fragment.add_character(empty_char_character, i); + } + } + } + let current_fragment = self.wrapped_fragments.get_mut(fragment_index).unwrap(); + for i in 0..until_col { + current_fragment.add_character(empty_char_character, i); + } + } + fn flattened_characters(&self) -> Vec<TerminalCharacter> { + self.wrapped_fragments + .iter() + .fold( + Vec::with_capacity(self.wrapped_fragments.len()), + |mut characters, wrapped_fragment| { + characters.push(wrapped_fragment.characters.iter().copied()); + characters + }, + ) + .into_iter() + .flatten() + .collect() + } + fn fill_fragments_up_to_width( + mut characters: Vec<TerminalCharacter>, + width: usize, + padding: Option<TerminalCharacter>, + ) -> Vec<WrappedFragment> { + let mut wrapped_fragments = vec![]; + while !characters.is_empty() { + if characters.len() > width { + wrapped_fragments.push(WrappedFragment::from_vec( + characters.drain(..width).collect(), + )); + } else { + let mut last_fragment = WrappedFragment::from_vec(characters.drain(..).collect()); + let last_fragment_len = last_fragment.characters.len(); + if let Some(empty_char_character) = padding { + if last_fragment_len < width { + for _ in last_fragment_len..width { + last_fragment.characters.push(empty_char_character); + } + } + } + wrapped_fragments.push(last_fragment); + } + } + if wrapped_fragments.is_empty() { + wrapped_fragments.push(WrappedFragment::new()); + } + wrapped_fragments + } +} + +impl Debug for CanonicalLine { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for wrapped_fragment in &self.wrapped_fragments { + writeln!(f, "{:?}", wrapped_fragment)?; + } + Ok(()) + } +} + +#[derive(Clone)] +pub struct WrappedFragment { + pub characters: Vec<TerminalCharacter>, +} + +impl WrappedFragment { + pub fn new() -> Self { + WrappedFragment { characters: vec![] } + } + pub fn add_character( + &mut self, + terminal_character: TerminalCharacter, + position_in_line: usize, + ) { + if position_in_line == self.characters.len() { + self.characters.push(terminal_character); + } else { + // this is much more performant than remove/insert + self.characters.push(terminal_character); + self.characters.swap_remove(position_in_line); + } + } + pub fn from_vec(characters: Vec<TerminalCharacter>) -> Self { + WrappedFragment { characters } + } + pub fn clear_after_and_including(&mut self, character_index: usize) { + self.characters.truncate(character_index); + } +} + +impl Debug for WrappedFragment { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for character in &self.characters { + write!(f, "{:?}", character)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct CursorPosition { + line_index: (usize, usize), // (canonical line index, fragment index in line) + column_index: usize, // 0 is the first character from the pane edge +} + +impl CursorPosition { + pub fn new() -> Self { + CursorPosition { + line_index: (0, 0), + column_index: 0, + } + } + pub fn move_forward(&mut self, count: usize) { + // TODO: panic if out of bounds? + self.column_index += count; + } + pub fn move_backwards(&mut self, count: usize) { + self.column_index -= count; + } + pub fn move_to_next_linewrap(&mut self) { + self.line_index.1 += 1; + } + pub fn move_to_next_canonical_line(&mut self) { + self.line_index.0 += 1; + } + pub fn _move_to_prev_canonical_line(&mut self) { + self.line_index.0 -= 1; + } + pub fn move_to_beginning_of_linewrap(&mut self) { + self.column_index = 0; + } + pub fn move_to_beginning_of_canonical_line(&mut self) { + self.column_index = 0; + self.line_index.1 = 0; + } + pub fn move_down_by_canonical_lines(&mut self, count: usize) { + // this method does not verify that we will not overflow + let current_canonical_line_position = self.line_index.0; + self.line_index = (current_canonical_line_position + count, 0); + } + pub fn move_to_canonical_line(&mut self, index: usize) { + self.line_index = (index, 0); + } + pub fn move_to_column(&mut self, col: usize) { + self.column_index = col; + } + pub fn reset(&mut self) { + self.column_index = 0; + self.line_index = (0, 0); + } +} + +pub struct Scroll { + canonical_lines: Vec<CanonicalLine>, + cursor_position: CursorPosition, + total_columns: usize, + lines_in_view: usize, + viewport_bottom_offset: Option<usize>, + scroll_region: Option<(usize, usize)>, // start line, end line (if set, this is the area the will scroll) + show_cursor: bool, + alternative_buffer: Option<Vec<CanonicalLine>>, + alternative_cursor_position: Option<CursorPosition>, +} + +impl Scroll { + pub fn new(total_columns: usize, lines_in_view: usize) -> Self { + let mut canonical_lines = vec![]; + canonical_lines.push(CanonicalLine::new()); + let cursor_position = CursorPosition::new(); + Scroll { + canonical_lines: vec![CanonicalLine::new()], // The rest will be created by newlines explicitly + total_columns, + lines_in_view, + cursor_position, + viewport_bottom_offset: None, + scroll_region: None, + show_cursor: true, + alternative_buffer: None, + alternative_cursor_position: None, + } + } + pub fn as_character_lines(&self) -> Vec<Vec<TerminalCharacter>> { + let mut lines: VecDeque<Vec<TerminalCharacter>> = VecDeque::new(); // TODO: with capacity lines_from_end? + let canonical_lines = self.canonical_lines.iter().rev(); + let mut lines_to_skip = self.viewport_bottom_offset.unwrap_or(0); + 'gather_lines: for current_canonical_line in canonical_lines { + for wrapped_fragment in current_canonical_line.wrapped_fragments.iter().rev() { + let mut line: Vec<TerminalCharacter> = + wrapped_fragment.characters.iter().copied().collect(); + if lines_to_skip > 0 { + lines_to_skip -= 1; + } else { + for _ in line.len()..self.total_columns { + // pad line if needed + line.push(EMPTY_TERMINAL_CHARACTER); + } + lines.push_front(line); + } + if lines.len() == self.lines_in_view { + break 'gather_lines; + } + } + } + if lines.len() < self.lines_in_view { + // pad lines in case we don't have enough scrollback to fill the view + let empty_line = vec![EMPTY_TERMINAL_CHARACTER; self.total_columns]; + + for _ in lines.len()..self.lines_in_view { + // pad lines in case we didn't have enough + lines.push_back(empty_line.clone()); + } + } + Vec::from(lines) + } + pub fn add_character(&mut self, terminal_character: TerminalCharacter) { + let (canonical_line_position, wrapped_fragment_index_in_line) = + self.cursor_position.line_index; + let cursor_position_in_line = self.cursor_position.column_index; + let current_line = self + .canonical_lines + .get_mut(canonical_line_position) + .expect("cursor out of bounds"); + let current_wrapped_fragment = current_line + .wrapped_fragments + .get_mut(wrapped_fragment_index_in_line) + .expect("cursor out of bounds"); + + if cursor_position_in_line < self.total_columns { + current_wrapped_fragment.add_character(terminal_character, cursor_position_in_line); + self.cursor_position.move_forward(1); + } else { + current_line.add_new_wrap(terminal_character); + self.cursor_position.move_to_next_linewrap(); + self.cursor_position.move_to_beginning_of_linewrap(); + self.cursor_position.move_forward(1); + } + } + pub fn show_cursor(&mut self) { + self.show_cursor = true; + } + pub fn hide_cursor(&mut self) { + self.show_cursor = false; + } + pub fn add_canonical_line(&mut self) { + let current_canonical_line_index = self.cursor_position.line_index.0; + if let Some((scroll_region_top, scroll_region_bottom)) = + self.scroll_region_absolute_indices() + { + if current_canonical_line_index == 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) + self.canonical_lines.remove(scroll_region_top); + self.canonical_lines + .insert(scroll_region_bottom, CanonicalLine::new()); + return; + } + } + + use std::cmp::Ordering; + match current_canonical_line_index.cmp(&(self.canonical_lines.len() - 1)) { + Ordering::Equal => { + self.canonical_lines.push(CanonicalLine::new()); + self.cursor_position.move_to_next_canonical_line(); + self.cursor_position.move_to_beginning_of_canonical_line(); + } + Ordering::Less => { + self.cursor_position.move_to_next_canonical_line(); + self.cursor_position.move_to_beginning_of_canonical_line(); + } + _ => panic!("cursor out of bounds, cannot add_canonical_line"), + } + } + pub fn cursor_coordinates_on_screen(&self) -> Option<(usize, usize)> { + // (x, y) + if !self.show_cursor { + return None; + } + let (canonical_line_cursor_position, line_wrap_cursor_position) = + self.cursor_position.line_index; + let x = self.cursor_position.column_index; + let mut y = 0; + let indices_and_canonical_lines = self.canonical_lines.iter().enumerate().rev(); + for (current_index, current_line) in indices_and_canonical_lines { + if current_index == canonical_line_cursor_position { + y += current_line.wrapped_fragments.len() - line_wrap_cursor_position; + break; + } else { + y += current_line.wrapped_fragments.len(); + } + } + let total_lines = self + .canonical_lines + .iter() + .fold(0, |total_lines, current_line| { + total_lines + current_line.wrapped_fragments.len() + }); // TODO: is this performant enough? should it be cached or kept track of? + let y = if total_lines < self.lines_in_view { + total_lines - y + } else if y > self.lines_in_view { + self.lines_in_view + } else { + self.lines_in_view - y + }; + Some((x, y)) + } + pub fn move_cursor_forward(&mut self, count: usize) { + let (current_canonical_line_index, current_line_wrap_position) = + self.cursor_position.line_index; + let current_cursor_column_position = self.cursor_position.column_index; + let current_canonical_line = self + .canonical_lines + .get_mut(current_canonical_line_index) + .expect("cursor out of bounds"); + let current_fragment = current_canonical_line + .wrapped_fragments + .get_mut(current_line_wrap_position) + .expect("cursor out of bounds"); + let move_count = if current_cursor_column_position + count > self.total_columns { + // move to last column in the current line wrap + self.total_columns - current_cursor_column_position + } else { + count + }; + + for _ in current_fragment.characters.len()..current_cursor_column_position + move_count { + current_fragment.characters.push(EMPTY_TERMINAL_CHARACTER); + } + self.cursor_position.move_forward(move_count); + } + pub fn move_cursor_back(&mut self, count: usize) { + let current_cursor_column_position = self.cursor_position.column_index; + if current_cursor_column_position < count { + self.cursor_position.move_to_beginning_of_linewrap(); + } else { + self.cursor_position.move_backwards(count); + } + } + pub fn move_cursor_to_beginning_of_linewrap(&mut self) { + self.cursor_position.move_to_beginning_of_linewrap(); + } + pub fn _move_cursor_to_beginning_of_canonical_line(&mut self) { + self.cursor_position.move_to_beginning_of_canonical_line(); + } + pub fn move_cursor_backwards(&mut self, count: usize) { + self.cursor_position.move_backwards(count); + } + pub fn move_cursor_up(&mut self, count: usize) { + for _ in 0..count { + let (current_canonical_line_index, current_line_wrap_index) = + self.cursor_position.line_index; + if current_line_wrap_index > 0 { + // go up one line-wrap + self.cursor_position.line_index.1 -= 1; + } else if current_canonical_line_index > 0 { + // go up to previous canonical line + self.cursor_position.line_index.0 -= 1; + let current_canonical_line = self + .canonical_lines + .get(self.cursor_position.line_index.0) + .unwrap(); + let wraps_in_current_line = current_canonical_line.wrapped_fragments.len(); + // make sure to only go up to the last wrap of the previous line + self.cursor_position.line_index.1 = wraps_in_current_line - 1; + } + } + } + pub fn move_cursor_down(&mut self, count: usize) { + let current_canonical_line = self.cursor_position.line_index.0; + let max_count = (self.canonical_lines.len() - 1) - current_canonical_line; + let count = std::cmp::min(count, max_count); + self.cursor_position.move_down_by_canonical_lines(count); + } + pub fn change_size(&mut self, columns: usize, lines: usize) { + for canonical_line in self.canonical_lines.iter_mut() { + canonical_line.change_width(columns); + } + let cursor_line = self + .canonical_lines + .get(self.cursor_position.line_index.0) + .expect("cursor out of bounds"); + if cursor_line.wrapped_fragments.len() <= self.cursor_position.line_index.1 { + self.cursor_position.line_index.1 = cursor_line.wrapped_fragments.len() - 1; + } + self.lines_in_view = lines; + self.total_columns = columns; + } + pub fn clear_canonical_line_right_of_cursor(&mut self, style_of_empty_space: CharacterStyles) { + let (current_canonical_line_index, current_line_wrap_position) = + self.cursor_position.line_index; + let current_cursor_column_position = self.cursor_position.column_index; + let current_canonical_line = self + .canonical_lines + .get_mut(current_canonical_line_index) + .expect("cursor out of bounds"); + current_canonical_line.replace_with_empty_chars_after_cursor( + current_line_wrap_position, + current_cursor_column_position, + self.total_columns, + style_of_empty_space, + ); + } + pub fn clear_canonical_line_left_of_cursor(&mut self, style_of_empty_space: CharacterStyles) { + let (current_canonical_line_index, current_line_wrap_position) = + self.cursor_position.line_index; + let current_cursor_column_position = self.cursor_position.column_index; + let current_canonical_line = self + .canonical_lines + .get_mut(current_canonical_line_index) + .expect("cursor out of bounds"); + current_canonical_line.replace_with_empty_chars_before_cursor( + current_line_wrap_position, + current_cursor_column_position, + style_of_empty_space, + ); + } + pub fn clear_all_after_cursor(&mut self) { + let (current_canonical_line_index, current_line_wrap_position) = + self.cursor_position.line_index; + let current_cursor_column_position = self.cursor_position.column_index; + let current_canonical_line = self + .canonical_lines + .get_mut(current_canonical_line_index) + .expect("cursor out of bounds"); + current_canonical_line + .clear_after(current_line_wrap_position, current_cursor_column_position); + self.canonical_lines + .truncate(current_canonical_line_index + 1); + } + pub fn replace_with_empty_chars( + &mut self, + count: usize, + style_of_empty_space: CharacterStyles, + ) { + let (current_canonical_line_index, current_line_wrap_position) = + self.cursor_position.line_index; + let current_cursor_column_position = self.cursor_position.column_index; + let current_canonical_line = self + .canonical_lines + .get_mut(current_canonical_line_index) + .expect("cursor out of bounds"); + current_canonical_line.replace_with_empty_chars( + current_line_wrap_position, + current_cursor_column_position, + count, + style_of_empty_space, + ); + } + pub fn erase_characters(&mut self, count: usize, style_of_empty_space: CharacterStyles) { + let (current_canonical_line_index, current_line_wrap_position) = + self.cursor_position.line_index; + let current_cursor_column_position = self.cursor_position.column_index; + let current_canonical_line = self + .canonical_lines + .get_mut(current_canonical_line_index) + .expect("cursor out of bounds"); + current_canonical_line.erase_chars( + current_line_wrap_position, + current_cursor_column_position, + count, + style_of_empty_space, + ); + } + pub fn clear_all(&mut self) { + self.canonical_lines.clear(); + self.canonical_lines.push(CanonicalLine::new()); + self.cursor_position.reset(); + } + pub fn move_cursor_to(&mut self, line: usize, col: usize) { + let line_on_screen = if self.canonical_lines.len() > self.lines_in_view { + line + (self.canonical_lines.len() - self.lines_in_view) + } else { + line + }; + if self.canonical_lines.len() > line_on_screen { + self.cursor_position.move_to_canonical_line(line_on_screen); + } else { + for _ in self.canonical_lines.len()..=line_on_screen { + self.canonical_lines.push(CanonicalLine::new()); + } + self.cursor_position.move_to_canonical_line(line_on_screen); + } + let (current_canonical_line_index, current_line_wrap_position) = + self.cursor_position.line_index; + let current_canonical_line = self + .canonical_lines + .get_mut(current_canonical_line_index) + .expect("cursor out of bounds"); + let current_fragment = current_canonical_line + .wrapped_fragments + .get_mut(current_line_wrap_position) + .expect("cursor out of bounds"); + + for _ in current_fragment.characters.len()..col { + current_fragment.characters.push(EMPTY_TERMINAL_CHARACTER); + } + self.cursor_position.move_to_column(col); + } + pub fn move_cursor_to_column(&mut self, col: usize) { + let current_canonical_line = self.cursor_position.line_index.0; + self.move_cursor_to(current_canonical_line, col); + } + pub fn move_cursor_to_line(&mut self, line: usize) { + let current_column = self.cursor_position.column_index; + self.move_cursor_to(line, current_column); + } + pub fn move_current_buffer_to_alternative_buffer(&mut self) { + self.alternative_buffer = Some(self.canonical_lines.drain(..).collect()); + self.alternative_cursor_position = Some(self.cursor_position); + self.clear_all(); + } + pub fn override_current_buffer_with_alternative_buffer(&mut self) { + if let Some(alternative_buffer) = self.alternative_buffer.as_mut() { + self.canonical_lines = alternative_buffer.drain(..).collect(); + } + if let Some(alternative_cursor_position) = self.alternative_cursor_position { + self.cursor_position = alternative_cursor_position; + } + self.alternative_buffer = None; + self.alternative_cursor_position = None; + } + pub fn set_scroll_region(&mut self, top_line: usize, bottom_line: usize) { + self.scroll_region = Some((top_line, bottom_line)); + } + pub fn clear_scroll_region(&mut self) { + self.scroll_region = None; + } + fn scroll_region_absolute_indices(&mut self) -> Option<(usize, usize)> { + if self.scroll_region.is_none() { + return None; + }; + if self.canonical_lines.len() > self.lines_in_view { + let absolute_top = self.canonical_lines.len() - 1 - self.lines_in_view; + let absolute_bottom = self.canonical_lines.len() - 1; + Some((absolute_top, absolute_bottom)) + } else { + Some((self.scroll_region.unwrap().0, self.scroll_region.unwrap().1)) + } + } + fn scroll_region_absolute_indices_or_screen_edges(&mut self) -> (usize, usize) { + match self.scroll_region { + Some(_scroll_region) => self.scroll_region_absolute_indices().unwrap(), + None => { + // indices of screen top and bottom edge + // TODO: what if we don't have enough lines? + // let absolute_top = self.canonical_lines.len() - 1 - self.lines_in_view; + let absolute_top = self.canonical_lines.len() - self.lines_in_view; + let absolute_bottom = self.canonical_lines.len() - 1; + (absolute_top, absolute_bottom) + } + } + } + pub fn delete_lines_in_scroll_region(&mut self, count: usize) { + if let Some((scroll_region_top, scroll_region_bottom)) = + self.scroll_region_absolute_indices() + { + let current_canonical_line_index = self.cursor_position.line_index.0; + if current_canonical_line_index >= scroll_region_top + && current_canonical_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.canonical_lines.remove(current_canonical_line_index); + self.canonical_lines + .insert(scroll_region_bottom, CanonicalLine::new()); + } + } + } + } + pub fn add_empty_lines_in_scroll_region(&mut self, count: usize) { + if let Some((scroll_region_top, scroll_region_bottom)) = + self.scroll_region_absolute_indices() + { + let current_canonical_line_index = self.cursor_position.line_index.0; + if current_canonical_line_index >= scroll_region_top + && current_canonical_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 { + self.canonical_lines.remove(scroll_region_bottom); + self.canonical_lines + .insert(current_canonical_line_index, CanonicalLine::new()); + } + } + } + } + pub fn move_cursor_up_in_scroll_region(&mut self, count: usize) { + let (scroll_region_top, scroll_region_bottom) = + self.scroll_region_absolute_indices_or_screen_edges(); + for _ in 0..count { + let current_canonical_line_index = self.cursor_position.line_index.0; + if current_canonical_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 + self.canonical_lines.remove(scroll_region_bottom); + self.canonical_lines + .insert(current_canonical_line_index, CanonicalLine::new()); + } else if current_canonical_line_index > scroll_region_top + && current_canonical_line_index <= scroll_region_bottom + { + self.move_cursor_up(count); + } + } + } + /// [scroll_up](https://github.com/alacritty/alacritty/blob/ec42b42ce601808070462111c0c28edb0e89babb/alacritty_terminal/src/grid/mod.rs#L261) + /// This function takes the first line of the scroll region and moves it to the bottom (count times) + pub fn rotate_scroll_region_up(&mut self, count: usize) { + if let Some((_, scroll_region_bottom)) = self.scroll_region_absolute_indices() { + if self.show_cursor { + let scroll_region_bottom_index = scroll_region_bottom - 1; + self.cursor_position + .move_to_canonical_line(scroll_region_bottom_index); + + let new_empty_lines = vec![CanonicalLine::new(); count]; + self.canonical_lines.splice( + scroll_region_bottom_index..scroll_region_bottom_index + 1, + new_empty_lines, + ); + + self.cursor_position + .move_to_canonical_line(scroll_region_bottom_index + count); + } + } + } + /// [scroll_down](https://github.com/alacritty/alacritty/blob/ec42b42ce601808070462111c0c28edb0e89babb/alacritty_terminal/src/grid/mod.rs#L221) + /// This function takes the last line of the scroll region and moves it to the top (count times) + pub fn rotate_scroll_region_down(&mut self, count: usize) { + if let Some((scroll_region_top, _)) = self.scroll_region_absolute_indices() { + if self.show_cursor { + let scroll_region_top_index = scroll_region_top - 1; + self.cursor_position + .move_to_canonical_line(scroll_region_top_index); + + let new_empty_lines = vec![CanonicalLine::new(); count]; + self.canonical_lines.splice( + scroll_region_top_index..scroll_region_top_index, + new_empty_lines, + ); + + self.cursor_position + .move_to_canonical_line(scroll_region_top_index + count); + } + } + } + pub fn move_viewport_up(&mut self, count: usize) { + if let Some(current_offset) = self.viewport_bottom_offset.as_mut() { + *current_offset += count; + } else { + self.viewport_bottom_offset = Some(count); + } + } + pub fn move_viewport_down(&mut self, count: usize) { + if let Some(current_offset) = self.viewport_bottom_offset.as_mut() { + if *current_offset > count { + *current_offset -= count; + } else { + self.viewport_bottom_offset = None; + } + } + } + pub fn reset_viewport(&mut self) { + self.viewport_bottom_offset = None; + } +} + +impl Debug for Scroll { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for line in &self.canonical_lines { + writeln!(f, "{:?}", line)?; + } + Ok(()) + } +} diff --git a/src/panes/terminal_character.rs b/src/panes/terminal_character.rs new file mode 100644 index 00000000..28d638f9 --- /dev/null +++ b/src/panes/terminal_character.rs @@ -0,0 +1,577 @@ +use crate::utils::logging::debug_log_to_file; +use ::std::fmt::{self, Debug, Display, Formatter}; + +pub const EMPTY_TERMINAL_CHARACTER: TerminalCharacter = TerminalCharacter { + character: ' ', + styles: CharacterStyles { + foreground: Some(AnsiCode::Reset), + background: Some(AnsiCode::Reset), + strike: Some(AnsiCode::Reset), + hidden: Some(AnsiCode::Reset), + reverse: Some(AnsiCode::Reset), + slow_blink: Some(AnsiCode::Reset), + fast_blink: Some(AnsiCode::Reset), + underline: Some(AnsiCode::Reset), + bold: Some(AnsiCode::Reset), + dim: Some(AnsiCode::Reset), + italic: Some(AnsiCode::Reset), + }, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AnsiCode { + On, + Reset, + NamedColor(NamedColor), + RGBCode((u8, u8, u8)), + ColorIndex(u8), +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum NamedColor { + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, +} + +impl NamedColor { + fn to_foreground_ansi_code(&self) -> String { + match self { + NamedColor::Black => format!("{}", 30), + NamedColor::Red => format!("{}", 31), + NamedColor::Green => format!("{}", 32), + NamedColor::Yellow => format!("{}", 33), + NamedColor::Blue => format!("{}", 34), + NamedColor::Magenta => format!("{}", 35), + NamedColor::Cyan => format!("{}", 36), + NamedColor::White => format!("{}", 37), + } + } + fn to_background_ansi_code(&self) -> String { + match self { + NamedColor::Black => format!("{}", 40), + NamedColor::Red => format!("{}", 41), + NamedColor::Green => format!("{}", 42), + NamedColor::Yellow => format!("{}", 43), + NamedColor::Blue => format!("{}", 44), + NamedColor::Magenta => format!("{}", 45), + NamedColor::Cyan => format!("{}", 46), + NamedColor::White => format!("{}", 47), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct CharacterStyles { + pub foreground: Option<AnsiCode>, + pub background: Option<AnsiCode>, + pub strike: Option<AnsiCode>, + pub hidden: Option<AnsiCode>, + pub reverse: Option<AnsiCode>, + pub slow_blink: Option<AnsiCode>, + pub fast_blink: Option<AnsiCode>, + pub underline: Option<AnsiCode>, + pub bold: Option<AnsiCode>, + pub dim: Option<AnsiCode>, + pub italic: Option<AnsiCode>, +} + +impl CharacterStyles { + pub fn new() -> Self { + CharacterStyles { + foreground: None, + background: None, + strike: None, + hidden: None, + reverse: None, + slow_blink: None, + fast_blink: None, + underline: None, + bold: None, + dim: None, + italic: None, + } + } + pub fn foreground(mut self, foreground_code: Option<AnsiCode>) -> Self { + self.foreground = foreground_code; + self + } + pub fn background(mut self, background_code: Option<AnsiCode>) -> Self { + self.background = background_code; + self + } + pub fn bold(mut self, bold_code: Option<AnsiCode>) -> Self { + self.bold = bold_code; + self + } + pub fn dim(mut self, dim_code: Option<AnsiCode>) -> Self { + self.dim = dim_code; + self + } + pub fn italic(mut self, italic_code: Option<AnsiCode>) -> Self { + self.italic = italic_code; + self + } + pub fn underline(mut self, underline_code: Option<AnsiCode>) -> Self { + self.underline = underline_code; + self + } + pub fn blink_slow(mut self, slow_blink_code: Option<AnsiCode>) -> Self { + self.slow_blink = slow_blink_code; + self + } + pub fn blink_fast(mut self, fast_blink_code: Option<AnsiCode>) -> Self { + self.fast_blink = fast_blink_code; + self + } + pub fn reverse(mut self, reverse_code: Option<AnsiCode>) -> Self { + self.reverse = reverse_code; + self + } + pub fn hidden(mut self, hidden_code: Option<AnsiCode>) -> Self { + self.hidden = hidden_code; + self + } + pub fn strike(mut self, strike_code: Option<AnsiCode>) -> Self { + self.strike = strike_code; + self + } + pub fn clear(&mut self) { + self.foreground = None; + self.background = None; + self.strike = None; + self.hidden = None; + self.reverse = None; + self.slow_blink = None; + self.fast_blink = None; + self.underline = None; + self.bold = None; + self.dim = None; + self.italic = None; + } + pub fn update_and_return_diff( + &mut self, + new_styles: &CharacterStyles, + ) -> Option<CharacterStyles> { + let mut diff: Option<CharacterStyles> = None; + + if new_styles.foreground == Some(AnsiCode::Reset) + && new_styles.background == Some(AnsiCode::Reset) + && new_styles.strike == Some(AnsiCode::Reset) + && new_styles.hidden == Some(AnsiCode::Reset) + && new_styles.reverse == Some(AnsiCode::Reset) + && new_styles.fast_blink == Some(AnsiCode::Reset) + && new_styles.slow_blink == Some(AnsiCode::Reset) + && new_styles.underline == Some(AnsiCode::Reset) + && new_styles.bold == Some(AnsiCode::Reset) + && new_styles.dim == Some(AnsiCode::Reset) + && new_styles.italic == Some(AnsiCode::Reset) + { + self.foreground = Some(AnsiCode::Reset); + self.background = Some(AnsiCode::Reset); + self.strike = Some(AnsiCode::Reset); + self.hidden = Some(AnsiCode::Reset); + self.reverse = Some(AnsiCode::Reset); + self.fast_blink = Some(AnsiCode::Reset); + self.slow_blink = Some(AnsiCode::Reset); + self.underline = Some(AnsiCode::Reset); + self.bold = Some(AnsiCode::Reset); + self.dim = Some(AnsiCode::Reset); + self.italic = Some(AnsiCode::Reset); + return Some(*new_styles); + }; + + if self.foreground != new_styles.foreground { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.foreground(new_styles.foreground)); + self.foreground = new_styles.foreground; + } else { + diff = Some(CharacterStyles::new().foreground(new_styles.foreground)); + self.foreground = new_styles.foreground; + } + } + if self.background != new_styles.background { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.background(new_styles.background)); + self.background = new_styles.background; + } else { + diff = Some(CharacterStyles::new().background(new_styles.background)); + self.background = new_styles.background; + } + } + if self.strike != new_styles.strike { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.strike(new_styles.strike)); + self.strike = new_styles.strike; + } else { + diff = Some(CharacterStyles::new().strike(new_styles.strike)); + self.strike = new_styles.strike; + } + } + if self.hidden != new_styles.hidden { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.hidden(new_styles.hidden)); + self.hidden = new_styles.hidden; + } else { + diff = Some(CharacterStyles::new().hidden(new_styles.hidden)); + self.hidden = new_styles.hidden; + } + } + if self.reverse != new_styles.reverse { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.reverse(new_styles.reverse)); + self.reverse = new_styles.reverse; + } else { + diff = Some(CharacterStyles::new().reverse(new_styles.reverse)); + self.reverse = new_styles.reverse; + } + } + if self.slow_blink != new_styles.slow_blink { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.blink_slow(new_styles.slow_blink)); + self.slow_blink = new_styles.slow_blink; + } else { + diff = Some(CharacterStyles::new().blink_slow(new_styles.slow_blink)); + self.slow_blink = new_styles.slow_blink; + } + } + if self.fast_blink != new_styles.fast_blink { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.blink_fast(new_styles.fast_blink)); + self.fast_blink = new_styles.fast_blink; + } else { + diff = Some(CharacterStyles::new().blink_fast(new_styles.fast_blink)); + self.fast_blink = new_styles.fast_blink; + } + } + if self.underline != new_styles.underline { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.underline(new_styles.underline)); + self.underline = new_styles.underline; + } else { + diff = Some(CharacterStyles::new().underline(new_styles.underline)); + self.underline = new_styles.underline; + } + } + if self.bold != new_styles.bold { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.bold(new_styles.bold)); + self.bold = new_styles.bold; + } else { + diff = Some(CharacterStyles::new().bold(new_styles.bold)); + self.bold = new_styles.bold; + } + } + if self.dim != new_styles.dim { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.dim(new_styles.dim)); + self.dim = new_styles.dim; + } else { + diff = Some(CharacterStyles::new().dim(new_styles.dim)); + self.dim = new_styles.dim; + } + } + if self.italic != new_styles.italic { + if let Some(new_diff) = diff.as_mut() { + diff = Some(new_diff.italic(new_styles.italic)); + self.italic = new_styles.italic; + } else { + diff = Some(CharacterStyles::new().italic(new_styles.italic)); + self.italic = new_styles.italic; + } + } + diff + } + pub fn reset_all(&mut self) { + self.foreground = Some(AnsiCode::Reset); + self.background = Some(AnsiCode::Reset); + self.bold = Some(AnsiCode::Reset); + self.dim = Some(AnsiCode::Reset); + self.italic = Some(AnsiCode::Reset); + self.underline = Some(AnsiCode::Reset); + self.slow_blink = Some(AnsiCode::Reset); + self.fast_blink = Some(AnsiCode::Reset); + self.reverse = Some(AnsiCode::Reset); + self.hidden = Some(AnsiCode::Reset); + self.strike = Some(AnsiCode::Reset); + } + pub fn add_style_from_ansi_params(&mut self, ansi_params: &[i64]) { + let mut params_used = 1; // if there's a parameter, it is always used + match ansi_params { + [] | [0, ..] => self.reset_all(), + [1, ..] => *self = self.bold(Some(AnsiCode::On)), + [2, ..] => *self = self.dim(Some(AnsiCode::On)), + [3, ..] => *self = self.italic(Some(AnsiCode::On)), + [4, ..] => *self = self.underline(Some(AnsiCode::On)), + [5, ..] => *self = self.blink_slow(Some(AnsiCode::On)), + [6, ..] => *self = self.blink_fast(Some(AnsiCode::On)), + [7, ..] => *self = self.reverse(Some(AnsiCode::On)), + [8, ..] => *self = self.hidden(Some(AnsiCode::On)), + [9, ..] => *self = self.strike(Some(AnsiCode::On)), + [21, ..] => *self = self.bold(Some(AnsiCode::Reset)), + [22, ..] => { + *self = self.bold(Some(AnsiCode::Reset)); + *self = self.dim(Some(AnsiCode::Reset)); + } + [23, ..] => *self = self.italic(Some(AnsiCode::Reset)), + [24, ..] => *self = self.underline(Some(AnsiCode::Reset)), + [25, ..] => { + *self = self.blink_slow(Some(AnsiCode::Reset)); + *self = self.blink_fast(Some(AnsiCode::Reset)); + } + [27, ..] => *self = self.reverse(Some(AnsiCode::Reset)), + [28, ..] => *self = self.hidden(Some(AnsiCode::Reset)), + [29, ..] => *self = self.strike(Some(AnsiCode::Reset)), + [30, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::Black))), + [31, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::Red))), + [32, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::Green))), + [33, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::Yellow))), + [34, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::Blue))), + [35, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::Magenta))), + [36, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::Cyan))), + [37, ..] => *self = self.foreground(Some(AnsiCode::NamedColor(NamedColor::White))), + [38, 2, ..] => { + let ansi_code = AnsiCode::RGBCode(( + *ansi_params.get(2).unwrap() as u8, + *ansi_params.get(3).unwrap() as u8, + *ansi_params.get(4).unwrap() as u8, + )); + *self = self.foreground(Some(ansi_code)); + params_used += 4 // one for the indicator (2 in this case) and three for the rgb code + } + [38, 5, ..] => { + let ansi_code = AnsiCode::ColorIndex(*ansi_params.get(2).unwrap() as u8); + *self = self.foreground(Some(ansi_code)); + params_used += 2 // one for the indicator (5 in this case) and one for the color index + } + [38, ..] => { + // this is a bug + // it means we got a color encoding we don't know how to handle (or is invalid) + params_used += 1; // even if it's a bug, let's not create an endless loop, eh? + } + [39, ..] => *self = self.foreground(Some(AnsiCode::Reset)), + [40, ..] => *self = self.background(Some(AnsiCode::NamedColor(NamedColor::Black))), + [41, ..] => *self = self.background(Some(AnsiCode::NamedColor(NamedColor::Red))), + [42, ..] => *self = self.background(Some(AnsiCode::NamedColor(NamedColor::Green))), + [43, ..] => *self = self.background(Some(AnsiCode::NamedColor(NamedColor::Yellow))), + [44, ..] => *self = self.background(Some(AnsiCode::NamedColor(NamedColor::Blue))), + [45, ..] => *self = self.background(Some(AnsiCode::NamedColor(NamedColor::Magenta))), + [46, ..] => *self = self.background(Some(AnsiCode::NamedColor(NamedColor::Cyan))), + [47, ..] => *self = self.background(Some(AnsiCode::NamedColor(NamedColor::White))), + [48, 2, ..] => { + let ansi_code = AnsiCode::RGBCode(( + *ansi_params.get(2).unwrap() as u8, + *ansi_params.get(3).unwrap() as u8, + *ansi_params.get(4).unwrap() as u8, + )); + *self = self.background(Some(ansi_code)); + params_used += 4 // one for the indicator (2 in this case) and three for the rgb code + } + [48, 5, ..] => { + let ansi_code = AnsiCode::ColorIndex(*ansi_params.get(2).unwrap() as u8); + *self = self.background(Some(ansi_code)); + params_used += 2 // one for the indicator (5 in this case) and one for the color index + } + [48, ..] => { + // this is a bug + // it means we got a color encoding we don't know how to handle (or is invalid) + params_used += 1; // even if it's a bug, let's not create an endless loop, eh? + } + [49, ..] => *self = self.background(Some(AnsiCode::Reset)), + _ => { + // if this happens, it's a bug + let _ = debug_log_to_file(format!("unhandled csi m code {:?}", ansi_params)); + return; + } + } + if let Some(next_params) = ansi_params.get(params_used..) { + if next_params.len() > 0 { + self.add_style_from_ansi_params(next_params); + } + } + } +} + +impl Display for CharacterStyles { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if self.foreground == Some(AnsiCode::Reset) + && self.background == Some(AnsiCode::Reset) + && self.strike == Some(AnsiCode::Reset) + && self.hidden == Some(AnsiCode::Reset) + && self.reverse == Some(AnsiCode::Reset) + && self.fast_blink == Some(AnsiCode::Reset) + && self.slow_blink == Some(AnsiCode::Reset) + && self.underline == Some(AnsiCode::Reset) + && self.bold == Some(AnsiCode::Reset) + && self.dim == Some(AnsiCode::Reset) + && self.italic == Some(AnsiCode::Reset) + { + write!(f, "\u{1b}[m")?; // reset all + return Ok(()); + } + if let Some(ansi_code) = self.foreground { + match ansi_code { + AnsiCode::RGBCode((r, g, b)) => { + write!(f, "\u{1b}[38;2;{};{};{}m", r, g, b)?; + } + AnsiCode::ColorIndex(color_index) => { + write!(f, "\u{1b}[38;5;{}m", color_index)?; + } + AnsiCode::Reset => { + write!(f, "\u{1b}[39m")?; + } + AnsiCode::NamedColor(named_color) => { + write!(f, "\u{1b}[{}m", named_color.to_foreground_ansi_code())?; + } + _ => {} + } + }; + if let Some(ansi_code) = self.background { + match ansi_code { + AnsiCode::RGBCode((r, g, b)) => { + write!(f, "\u{1b}[48;2;{};{};{}m", r, g, b)?; + } + AnsiCode::ColorIndex(color_index) => { + write!(f, "\u{1b}[48;5;{}m", color_index)?; + } + AnsiCode::Reset => { + write!(f, "\u{1b}[49m")?; + } + AnsiCode::NamedColor(named_color) => { + write!(f, "\u{1b}[{}m", named_color.to_background_ansi_code())?; + } + _ => {} + } + } + if let Some(ansi_code) = self.strike { + match ansi_code { + AnsiCode::On => { + write!(f, "\u{1b}[9m")?; + } + AnsiCode::Reset => { + write!(f, "\u{1b}[29m")?; + } + _ => {} + } + } + if let Some(ansi_code) = self.hidden { + match ansi_code { + AnsiCode::On => { + write!(f, "\u{1b}[8m")?; + } + AnsiCode::Reset => { + write!(f, "\u{1b}[28m")?; + } + _ => {} + } + } + if let Some(ansi_code) = self.reverse { + match ansi_code { + AnsiCode::On => { + write!(f, "\u{1b}[7m")?; + } + AnsiCode::Reset => { + write!(f, "\u{1b}[27m")?; + } + _ => {} + } + } + if let Some(ansi_code) = self.fast_blink { + match ansi_code { + AnsiCode::On => { + write!(f, "\u{1b}[6m")?; + } + AnsiCode::Reset => { + write!(f, "\u{1b}[25m")?; + } + _ => {} + } + } + if let Some(ansi_code) = self.slow_blink { + match ansi_code { + AnsiCode::On => { + write!(f, "\u{1b}[5m")?; + } + AnsiCode::Reset => { + write!(f, "\u{1b}[25m")?; + } + _ => {} + } + } + if let Some(ansi_code) = self.bold { + match ansi_code { + AnsiCode::On => { + write!(f, "\u{1b}[1m")?; + } + AnsiCode::Reset => { + write!(f, "\u{1b}[22m\u{1b}[24m")?; + // TODO: this cancels bold + underline, if this behaviour is indeed correct, we + // need to properly handle it in the struct methods etc like dim + } + _ => {} + } + } + // notice the order is important here, bold must be before underline + // because the bold reset also resets underline, and would override it + // otherwise + if let Some(ansi_code) = self.underline { + match ansi_code { + AnsiCode::On => { + write!(f, "\u{1b}[4m")?; + } + AnsiCode::Reset => { + write!(f, "\u{1b}[24m")?; + } + _ => {} + } + } + if let Some(ansi_code) = self.dim { + match ansi_code { + AnsiCode::On => { + write!(f, "\u{1b}[2m")?; + } + AnsiCode::Reset => { + if let Some(bold) = self.bold { + // we only reset dim if both dim and bold should be reset + match bold { + AnsiCode::Reset => { + write!(f, "\u{1b}[22m")?; + } + _ => {} + } + } + } + _ => {} + } + } + if let Some(ansi_code) = self.italic { + match ansi_code { + AnsiCode::On => { + write!(f, "\u{1b}[3m")?; + } + AnsiCode::Reset => { + write!(f, "\u{1b}[23m")?; + } + _ => {} + } + }; + Ok(()) + } +} + +#[derive(Clone, Copy)] +pub struct TerminalCharacter { + pub character: char, + pub styles: CharacterStyles, +} + +impl ::std::fmt::Debug for TerminalCharacter { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.character) + } +} diff --git a/src/panes/terminal_pane.rs b/src/panes/terminal_pane.rs new file mode 100644 index 00000000..70eb216a --- /dev/null +++ b/src/panes/terminal_pane.rs @@ -0,0 +1,617 @@ +#![allow(clippy::clippy::if_same_then_else)] + +use crate::tab::Pane; +use ::nix::pty::Winsize; +use ::std::os::unix::io::RawFd; +use ::vte::Perform; + +use crate::panes::terminal_character::{CharacterStyles, TerminalCharacter}; +use crate::panes::Scroll; +use crate::utils::logging::debug_log_to_file; +use crate::VteEvent; + +#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug)] +pub enum PaneId { + Terminal(RawFd), + Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct? + BuiltIn(u32), +} + +#[derive(Clone, Copy, Debug)] +pub struct PositionAndSize { + pub x: usize, + pub y: usize, + pub rows: usize, + pub columns: usize, +} + +impl PositionAndSize { + pub fn from(winsize: Winsize) -> PositionAndSize { + PositionAndSize { + columns: winsize.ws_col as usize, + rows: winsize.ws_row as usize, + x: winsize.ws_xpixel as usize, + y: winsize.ws_ypixel as usize, + } + } +} + +#[derive(Debug)] +pub struct TerminalPane { + pub pid: RawFd, + pub scroll: Scroll, + pub should_render: bool, + pub position_and_size: PositionAndSize, + pub position_and_size_override: Option<PositionAndSize>, + pub cursor_key_mode: bool, // DECCKM - when set, cursor keys should send ANSI direction codes (eg. "OD") instead of the arrow keys (eg. "[D") + pending_styles: CharacterStyles, +} + +impl Pane for TerminalPane { + fn x(&self) -> usize { + self.get_x() + } + fn y(&self) -> usize { + self.get_y() + } + fn rows(&self) -> usize { + self.get_rows() + } + fn columns(&self) -> usize { + self.get_columns() + } + fn reset_size_and_position_override(&mut self) { + self.position_and_size_override = None; + self.reflow_lines(); + self.mark_for_rerender(); + } + fn change_pos_and_size(&mut self, position_and_size: &PositionAndSize) { + self.position_and_size.columns = position_and_size.columns; + self.position_and_size.rows = position_and_size.rows; + self.reflow_lines(); + self.mark_for_rerender(); + } + fn override_size_and_position(&mut self, x: usize, y: usize, size: &PositionAndSize) { + let position_and_size_override = PositionAndSize { + x, + y, + rows: size.rows, + columns: size.columns, + }; + self.position_and_size_override = Some(position_and_size_override); + self.reflow_lines(); + self.mark_for_rerender(); + } + fn handle_event(&mut self, event: VteEvent) { + match event { + VteEvent::Print(c) => { + self.print(c); + self.mark_for_rerender(); + } + VteEvent::Execute(byte) => { + self.execute(byte); + } + VteEvent::Hook(params, intermediates, ignore, c) => { + self.hook(¶ms, &intermediates, ignore, c); + } + VteEvent::Put(byte) => { + self.put(byte); + } + VteEvent::Unhook => { + self.unhook(); + } + VteEvent::OscDispatch(params, bell_terminated) => { + let params: Vec<&[u8]> = params.iter().map(|p| &p[..]).collect(); + self.osc_dispatch(¶ms[..], bell_terminated); + } + VteEvent::CsiDispatch(params, intermediates, ignore, c) => { + self.csi_dispatch(¶ms, &intermediates, ignore, c); + } + VteEvent::EscDispatch(intermediates, ignore, byte) => { + self.esc_dispatch(&intermediates, ignore, byte); + } + } + } + fn cursor_coordinates(&self) -> Option<(usize, usize)> { + // (x, y) + self.scroll.cursor_coordinates_on_screen() + } + fn adjust_input_to_terminal(&self, input_bytes: Vec<u8>) -> Vec<u8> { + // there are some cases in which the terminal state means that input sent to it + // needs to be adjusted. + // here we match against those cases - if need be, we adjust the input and if not + // we send back the original input + match input_bytes.as_slice() { + [27, 91, 68] => { + // left arrow + if self.cursor_key_mode { + // please note that in the line below, there is an ANSI escape code (27) at the beginning of the string, + // some editors will not show this + return "OD".as_bytes().to_vec(); + } + } + [27, 91, 67] => { + // right arrow + if self.cursor_key_mode { + // please note that in the line below, there is an ANSI escape code (27) at the beginning of the string, + // some editors will not show this + return "OC".as_bytes().to_vec(); + } + } + [27, 91, 65] => { + // up arrow + if self.cursor_key_mode { + // please note that in the line below, there is an ANSI escape code (27) at the beginning of the string, + // some editors will not show this + return "OA".as_bytes().to_vec(); + } + } + [27, 91, 66] => { + // down arrow + if self.cursor_key_mode { + // please note that in the line below, there is an ANSI escape code (27) at the beginning of the string, + // some editors will not show this + return "OB".as_bytes().to_vec(); + } + } + _ => {} + }; + input_bytes + } + + fn position_and_size_override(&self) -> Option<PositionAndSize> { + self.position_and_size_override + } + fn should_render(&self) -> bool { + self.should_render + } + fn set_should_render(&mut self, should_render: bool) { + self.should_render = should_render; + } + fn render(&mut self) -> Option<String> { + // if self.should_render { + if true { + // while checking should_render rather than rendering each pane every time + // is more performant, it causes some problems when the pane to the left should be + // rendered and has wide characters (eg. Chinese characters or emoji) + // as a (hopefully) temporary hack, we render all panes until we find a better solution + let mut vte_output = String::new(); + let buffer_lines = &self.read_buffer_as_lines(); + let display_cols = self.get_columns(); + let mut character_styles = CharacterStyles::new(); + for (row, line) in buffer_lines.iter().enumerate() { + let x = self.get_x(); + let y = self.get_y(); + vte_output = format!("{}\u{1b}[{};{}H\u{1b}[m", vte_output, y + row + 1, x + 1); // goto row/col and reset styles + for (col, t_character) in line.iter().enumerate() { + if col < display_cols { + // in some cases (eg. while resizing) some characters will spill over + // before they are corrected by the shell (for the prompt) or by reflowing + // lines + if let Some(new_styles) = + character_styles.update_and_return_diff(&t_character.styles) + { + // the terminal keeps the previous styles as long as we're in the same + // line, so we only want to update the new styles here (this also + // includes resetting previous styles as needed) + vte_output = format!("{}{}", vte_output, new_styles); + } + vte_output.push(t_character.character); + } + } + character_styles.clear(); + } + self.mark_for_rerender(); + Some(vte_output) + } else { + None + } + } + fn pid(&self) -> PaneId { + PaneId::Terminal(self.pid) + } + fn reduce_height_down(&mut self, count: usize) { + self.position_and_size.y += count; + self.position_and_size.rows -= count; + self.reflow_lines(); + self.mark_for_rerender(); + } + fn increase_height_down(&mut self, count: usize) { + self.position_and_size.rows += count; + self.reflow_lines(); + self.mark_for_rerender(); + } + fn increase_height_up(&mut self, count: usize) { + self.position_and_size.y -= count; + self.position_and_size.rows += count; + self.reflow_lines(); + self.mark_for_rerender(); + } + fn reduce_height_up(&mut self, count: usize) { + self.position_and_size.rows -= count; + self.reflow_lines(); + self.mark_for_rerender(); + } + fn reduce_width_right(&mut self, count: usize) { + self.position_and_size.x += count; + self.position_and_size.columns -= count; + self.reflow_lines(); + self.mark_for_rerender(); + } + fn reduce_width_left(&mut self, count: usize) { + self.position_and_size.columns -= count; + self.reflow_lines(); + self.mark_for_rerender(); + } + fn increase_width_left(&mut self, count: usize) { + self.position_and_size.x -= count; + self.position_and_size.columns += count; + self.reflow_lines(); + self.mark_for_rerender(); + } + fn increase_width_right(&mut self, count: usize) { + self.position_and_size.columns += count; + self.reflow_lines(); + self.mark_for_rerender(); + } + fn scroll_up(&mut self, count: usize) { + self.scroll.move_viewport_up(count); + self.mark_for_rerender(); + } + fn scroll_down(&mut self, count: usize) { + self.scroll.move_viewport_down(count); + self.mark_for_rerender(); + } + fn clear_scroll(&mut self) { + self.scroll.reset_viewport(); + self.mark_for_rerender(); + } +} + +impl TerminalPane { + pub fn new(pid: RawFd, position_and_size: PositionAndSize) -> TerminalPane { + let scroll = Scroll::new(position_and_size.columns, position_and_size.rows); + let pending_styles = CharacterStyles::new(); + + TerminalPane { + pid, + scroll, + should_render: true, + pending_styles, + position_and_size, + position_and_size_override: None, + cursor_key_mode: false, + } + } + pub fn mark_for_rerender(&mut self) { + self.should_render = true; + } + pub fn get_x(&self) -> usize { + match self.position_and_size_override { + Some(position_and_size_override) => position_and_size_override.x, + None => self.position_and_size.x as usize, + } + } + pub fn get_y(&self) -> usize { + match self.position_and_size_override { + Some(position_and_size_override) => position_and_size_override.y, + None => self.position_and_size.y as usize, + } + } + pub fn get_columns(&self) -> usize { + match &self.position_and_size_override.as_ref() { + Some(position_and_size_override) => position_and_size_override.columns, + None => self.position_and_size.columns as usize, + } + } + pub fn get_rows(&self) -> usize { + match &self.position_and_size_override.as_ref() { + Some(position_and_size_override) => position_and_size_override.rows, + None => self.position_and_size.rows as usize, + } + } + fn reflow_lines(&mut self) { + let rows = self.get_rows(); + let columns = self.get_columns(); + self.scroll.change_size(columns, rows); + } + + pub fn read_buffer_as_lines(&self) -> Vec<Vec<TerminalCharacter>> { + self.scroll.as_character_lines() + } + #[cfg(test)] + pub fn cursor_coordinates(&self) -> Option<(usize, usize)> { + // (x, y) + self.scroll.cursor_coordinates_on_screen() + } + pub fn rotate_scroll_region_up(&mut self, count: usize) { + self.scroll.rotate_scroll_region_up(count); + self.mark_for_rerender(); + } + pub fn rotate_scroll_region_down(&mut self, count: usize) { + self.scroll.rotate_scroll_region_down(count); + self.mark_for_rerender(); + } + fn add_newline(&mut self) { + self.scroll.add_canonical_line(); + // self.reset_all_ansi_codes(); // TODO: find out if we should be resetting here or not + self.mark_for_rerender(); + } + fn move_to_beginning_of_line(&mut self) { + self.scroll.move_cursor_to_beginning_of_linewrap(); + } + fn move_cursor_backwards(&mut self, count: usize) { + self.scroll.move_cursor_backwards(count); + } + fn _reset_all_ansi_codes(&mut self) { + self.pending_styles.clear(); + } +} + +impl vte::Perform for TerminalPane { + 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.scroll.add_character(terminal_character); + } + + fn execute(&mut self, byte: u8) { + match byte { + 8 => { + // backspace + self.move_cursor_backwards(1); + } + 9 => { + // tab + let terminal_tab_character = TerminalCharacter { + character: '\t', + styles: self.pending_styles, + }; + // TODO: handle better with line wrapping + self.scroll.add_character(terminal_tab_character); + } + 10 => { + // 0a, newline + self.add_newline(); + } + 13 => { + // 0d, carriage return + self.move_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.scroll.move_cursor_forward(move_by); + } else if c == 'K' { + // clear line (0 => right, 1 => left, 2 => all) + if params[0] == 0 { + self.scroll + .clear_canonical_line_right_of_cursor(self.pending_styles); + } else if params[0] == 1 { + self.scroll + .clear_canonical_line_left_of_cursor(self.pending_styles); + } + // TODO: implement 2 + } else if c == 'J' { + // clear all (0 => below, 1 => above, 2 => all, 3 => saved) + if params[0] == 0 { + self.scroll.clear_all_after_cursor(); + } else if params[0] == 2 { + self.scroll.clear_all(); + } + // 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 - 1, params[0] as usize) + } + } else { + if params[0] == 0 { + (0, params[1] as usize - 1) + } else { + (params[0] as usize - 1, params[1] as usize - 1) + } + }; + self.scroll.move_cursor_to(row, col); + } else if c == 'A' { + // move cursor up until edge of screen + let move_up_count = if params[0] == 0 { 1 } else { params[0] }; + self.scroll.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] }; + self.scroll.move_cursor_down(move_down_count as usize); + } else if c == 'D' { + let move_back_count = if params[0] == 0 { + 1 + } else { + params[0] as usize + }; + self.scroll.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) => { + self.scroll + .override_current_buffer_with_alternative_buffer(); + } + Some(&25) => { + self.scroll.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.scroll.show_cursor(); + self.mark_for_rerender(); + } + Some(&1049) => { + self.scroll.move_current_buffer_to_alternative_buffer(); + } + 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 - 1; + let bottom_line_index = params[1] as usize - 1; + self.scroll + .set_scroll_region(top_line_index, bottom_line_index); + self.scroll.show_cursor(); + } else { + self.scroll.clear_scroll_region(); + } + } else if c == 't' { + // TBD - title? + } else if c == 'n' { + // TBD - device status report + } else if c == 'c' { + // TBD - identify terminal + } 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 + }; + self.scroll + .delete_lines_in_scroll_region(line_count_to_delete); + } 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 + }; + self.scroll + .add_empty_lines_in_scroll_region(line_count_to_add); + } 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 + params[0] as usize - 1 + }; + self.scroll.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 + }; + self.scroll.move_cursor_to_line(line); + } else if c == 'P' { + // erase characters + let count = if params[0] == 0 { + 1 + } else { + params[0] as usize + }; + self.scroll.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.scroll + .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 { + 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.scroll.delete_lines_in_scroll_region(count); + self.scroll.add_empty_lines_in_scroll_region(count); + } 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.scroll.move_cursor_up_in_scroll_region(1); + } + _ => {} + } + } +} |
