diff options
| author | denis <denismaximov98@gmail.com> | 2021-01-24 18:38:46 +0200 |
|---|---|---|
| committer | denis <denismaximov98@gmail.com> | 2021-01-24 18:38:46 +0200 |
| commit | b23ef332c7531b8e9bb202bd25e7f37fd157c22c (patch) | |
| tree | 2ad09a6173e8528a91c37609e00c59d9b8a0b04b /src/client | |
| parent | Merge branch 'main' into server-client (diff) | |
| download | sl0ppij-b23ef332c7531b8e9bb202bd25e7f37fd157c22c.tar.gz sl0ppij-b23ef332c7531b8e9bb202bd25e7f37fd157c22c.tar.xz sl0ppij-b23ef332c7531b8e9bb202bd25e7f37fd157c22c.zip | |
wip: restructing in progress, to prep for server-client
Diffstat (limited to 'src/client')
| -rw-r--r-- | src/client/boundaries.rs | 472 | ||||
| -rw-r--r-- | src/client/layout.rs | 220 | ||||
| -rw-r--r-- | src/client/mod.rs | 9 | ||||
| -rw-r--r-- | src/client/panes/grid.rs | 820 | ||||
| -rw-r--r-- | src/client/panes/mod.rs | 9 | ||||
| -rw-r--r-- | src/client/panes/plugin_pane.rs | 175 | ||||
| -rw-r--r-- | src/client/panes/terminal_character.rs | 572 | ||||
| -rw-r--r-- | src/client/panes/terminal_pane.rs | 664 | ||||
| -rw-r--r-- | src/client/tab.rs | 1798 |
9 files changed, 4739 insertions, 0 deletions
diff --git a/src/client/boundaries.rs b/src/client/boundaries.rs new file mode 100644 index 00000000..498a3731 --- /dev/null +++ b/src/client/boundaries.rs @@ -0,0 +1,472 @@ +use crate::tab::Pane; +use std::collections::HashMap; + +pub mod boundary_type { + pub const TOP_RIGHT: &str = "┐"; + pub const VERTICAL: &str = "│"; + pub const HORIZONTAL: &str = "─"; + pub const TOP_LEFT: &str = "┌"; + pub const BOTTOM_RIGHT: &str = "┘"; + pub const BOTTOM_LEFT: &str = "└"; + pub const VERTICAL_LEFT: &str = "┤"; + pub const VERTICAL_RIGHT: &str = "├"; + pub const HORIZONTAL_DOWN: &str = "┬"; + pub const HORIZONTAL_UP: &str = "┴"; + pub const CROSS: &str = "┼"; +} + +pub type BoundaryType = &'static str; // easy way to refer to boundary_type above + +fn combine_symbols(current_symbol: &str, next_symbol: &str) -> Option<&'static str> { + match (current_symbol, next_symbol) { + (boundary_type::TOP_RIGHT, boundary_type::TOP_RIGHT) => { + // (┐, ┐) => Some(┐) + Some(boundary_type::TOP_RIGHT) + } + (boundary_type::TOP_RIGHT, boundary_type::VERTICAL) => { + // (┐, │) => Some(┤) + Some(boundary_type::VERTICAL_LEFT) + } + (boundary_type::TOP_RIGHT, boundary_type::HORIZONTAL) => { + // (┐, ─) => Some(┬) + Some(boundary_type::HORIZONTAL_DOWN) + } + (boundary_type::TOP_RIGHT, boundary_type::TOP_LEFT) => { + // (┐, ┌) => Some(┬) + Some(boundary_type::HORIZONTAL_DOWN) + } + (boundary_type::TOP_RIGHT, boundary_type::BOTTOM_RIGHT) => { + // (┐, ┘) => Some(┤) + Some(boundary_type::VERTICAL_LEFT) + } + (boundary_type::TOP_RIGHT, boundary_type::BOTTOM_LEFT) => { + // (┐, └) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::TOP_RIGHT, boundary_type::VERTICAL_LEFT) => { + // (┐, ┤) => Some(┤) + Some(boundary_type::VERTICAL_LEFT) + } + (boundary_type::TOP_RIGHT, boundary_type::VERTICAL_RIGHT) => { + // (┐, ├) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::TOP_RIGHT, boundary_type::HORIZONTAL_DOWN) => { + // (┐, ┬) => Some(┬) + Some(boundary_type::HORIZONTAL_DOWN) + } + (boundary_type::TOP_RIGHT, boundary_type::HORIZONTAL_UP) => { + // (┐, ┴) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::TOP_RIGHT, boundary_type::CROSS) => { + // (┐, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::HORIZONTAL, boundary_type::HORIZONTAL) => { + // (─, ─) => Some(─) + Some(boundary_type::HORIZONTAL) + } + (boundary_type::HORIZONTAL, boundary_type::VERTICAL) => { + // (─, │) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::HORIZONTAL, boundary_type::TOP_LEFT) => { + // (─, ┌) => Some(┬) + Some(boundary_type::HORIZONTAL_DOWN) + } + (boundary_type::HORIZONTAL, boundary_type::BOTTOM_RIGHT) => { + // (─, ┘) => Some(┴) + Some(boundary_type::HORIZONTAL_UP) + } + (boundary_type::HORIZONTAL, boundary_type::BOTTOM_LEFT) => { + // (─, └) => Some(┴) + Some(boundary_type::HORIZONTAL_UP) + } + (boundary_type::HORIZONTAL, boundary_type::VERTICAL_LEFT) => { + // (─, ┤) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::HORIZONTAL, boundary_type::VERTICAL_RIGHT) => { + // (─, ├) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::HORIZONTAL, boundary_type::HORIZONTAL_DOWN) => { + // (─, ┬) => Some(┬) + Some(boundary_type::HORIZONTAL_DOWN) + } + (boundary_type::HORIZONTAL, boundary_type::HORIZONTAL_UP) => { + // (─, ┴) => Some(┴) + Some(boundary_type::HORIZONTAL_UP) + } + (boundary_type::HORIZONTAL, boundary_type::CROSS) => { + // (─, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::VERTICAL, boundary_type::VERTICAL) => { + // (│, │) => Some(│) + Some(boundary_type::VERTICAL) + } + (boundary_type::VERTICAL, boundary_type::TOP_LEFT) => { + // (│, ┌) => Some(├) + Some(boundary_type::VERTICAL_RIGHT) + } + (boundary_type::VERTICAL, boundary_type::BOTTOM_RIGHT) => { + // (│, ┘) => Some(┤) + Some(boundary_type::VERTICAL_LEFT) + } + (boundary_type::VERTICAL, boundary_type::BOTTOM_LEFT) => { + // (│, └) => Some(├) + Some(boundary_type::VERTICAL_RIGHT) + } + (boundary_type::VERTICAL, boundary_type::VERTICAL_LEFT) => { + // (│, ┤) => Some(┤) + Some(boundary_type::VERTICAL_LEFT) + } + (boundary_type::VERTICAL, boundary_type::VERTICAL_RIGHT) => { + // (│, ├) => Some(├) + Some(boundary_type::VERTICAL_RIGHT) + } + (boundary_type::VERTICAL, boundary_type::HORIZONTAL_DOWN) => { + // (│, ┬) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::VERTICAL, boundary_type::HORIZONTAL_UP) => { + // (│, ┴) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::VERTICAL, boundary_type::CROSS) => { + // (│, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::TOP_LEFT, boundary_type::TOP_LEFT) => { + // (┌, ┌) => Some(┌) + Some(boundary_type::TOP_LEFT) + } + (boundary_type::TOP_LEFT, boundary_type::BOTTOM_RIGHT) => { + // (┌, ┘) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::TOP_LEFT, boundary_type::BOTTOM_LEFT) => { + // (┌, └) => Some(├) + Some(boundary_type::VERTICAL_RIGHT) + } + (boundary_type::TOP_LEFT, boundary_type::VERTICAL_LEFT) => { + // (┌, ┤) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::TOP_LEFT, boundary_type::VERTICAL_RIGHT) => { + // (┌, ├) => Some(├) + Some(boundary_type::VERTICAL_RIGHT) + } + (boundary_type::TOP_LEFT, boundary_type::HORIZONTAL_DOWN) => { + // (┌, ┬) => Some(┬) + Some(boundary_type::HORIZONTAL_DOWN) + } + (boundary_type::TOP_LEFT, boundary_type::HORIZONTAL_UP) => { + // (┌, ┴) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::TOP_LEFT, boundary_type::CROSS) => { + // (┌, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::BOTTOM_RIGHT, boundary_type::BOTTOM_RIGHT) => { + // (┘, ┘) => Some(┘) + Some(boundary_type::BOTTOM_RIGHT) + } + (boundary_type::BOTTOM_RIGHT, boundary_type::BOTTOM_LEFT) => { + // (┘, └) => Some(┴) + Some(boundary_type::HORIZONTAL_UP) + } + (boundary_type::BOTTOM_RIGHT, boundary_type::VERTICAL_LEFT) => { + // (┘, ┤) => Some(┤) + Some(boundary_type::VERTICAL_LEFT) + } + (boundary_type::BOTTOM_RIGHT, boundary_type::VERTICAL_RIGHT) => { + // (┘, ├) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::BOTTOM_RIGHT, boundary_type::HORIZONTAL_DOWN) => { + // (┘, ┬) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::BOTTOM_RIGHT, boundary_type::HORIZONTAL_UP) => { + // (┘, ┴) => Some(┴) + Some(boundary_type::HORIZONTAL_UP) + } + (boundary_type::BOTTOM_RIGHT, boundary_type::CROSS) => { + // (┘, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::BOTTOM_LEFT, boundary_type::BOTTOM_LEFT) => { + // (└, └) => Some(└) + Some(boundary_type::BOTTOM_LEFT) + } + (boundary_type::BOTTOM_LEFT, boundary_type::VERTICAL_LEFT) => { + // (└, ┤) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::BOTTOM_LEFT, boundary_type::VERTICAL_RIGHT) => { + // (└, ├) => Some(├) + Some(boundary_type::VERTICAL_RIGHT) + } + (boundary_type::BOTTOM_LEFT, boundary_type::HORIZONTAL_DOWN) => { + // (└, ┬) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::BOTTOM_LEFT, boundary_type::HORIZONTAL_UP) => { + // (└, ┴) => Some(┴) + Some(boundary_type::HORIZONTAL_UP) + } + (boundary_type::BOTTOM_LEFT, boundary_type::CROSS) => { + // (└, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::VERTICAL_LEFT, boundary_type::VERTICAL_LEFT) => { + // (┤, ┤) => Some(┤) + Some(boundary_type::VERTICAL_LEFT) + } + (boundary_type::VERTICAL_LEFT, boundary_type::VERTICAL_RIGHT) => { + // (┤, ├) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::VERTICAL_LEFT, boundary_type::HORIZONTAL_DOWN) => { + // (┤, ┬) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::VERTICAL_LEFT, boundary_type::HORIZONTAL_UP) => { + // (┤, ┴) => Some(┼) + Some(boundary_type::HORIZONTAL_UP) + } + (boundary_type::VERTICAL_LEFT, boundary_type::CROSS) => { + // (┤, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::VERTICAL_RIGHT, boundary_type::VERTICAL_RIGHT) => { + // (├, ├) => Some(├) + Some(boundary_type::VERTICAL_RIGHT) + } + (boundary_type::VERTICAL_RIGHT, boundary_type::HORIZONTAL_DOWN) => { + // (├, ┬) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::VERTICAL_RIGHT, boundary_type::HORIZONTAL_UP) => { + // (├, ┴) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::VERTICAL_RIGHT, boundary_type::CROSS) => { + // (├, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::HORIZONTAL_DOWN, boundary_type::HORIZONTAL_DOWN) => { + // (┬, ┬) => Some(┬) + Some(boundary_type::HORIZONTAL_DOWN) + } + (boundary_type::HORIZONTAL_DOWN, boundary_type::HORIZONTAL_UP) => { + // (┬, ┴) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::HORIZONTAL_DOWN, boundary_type::CROSS) => { + // (┬, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::HORIZONTAL_UP, boundary_type::HORIZONTAL_UP) => { + // (┴, ┴) => Some(┴) + Some(boundary_type::HORIZONTAL_UP) + } + (boundary_type::HORIZONTAL_UP, boundary_type::CROSS) => { + // (┴, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (boundary_type::CROSS, boundary_type::CROSS) => { + // (┼, ┼) => Some(┼) + Some(boundary_type::CROSS) + } + (_, _) => None, + } +} + +fn find_next_symbol(first_symbol: &str, second_symbol: &str) -> Option<&'static str> { + if let Some(symbol) = combine_symbols(first_symbol, second_symbol) { + Some(symbol) + } else { + combine_symbols(second_symbol, first_symbol) + } +} + +#[derive(PartialEq, Eq, Hash, Debug)] +pub struct Coordinates { + x: usize, + y: usize, +} + +impl Coordinates { + pub fn new(x: usize, y: usize) -> Self { + Coordinates { x, y } + } +} + +pub trait Rect { + fn x(&self) -> usize; + fn y(&self) -> usize; + fn rows(&self) -> usize; + fn columns(&self) -> usize; + fn right_boundary_x_coords(&self) -> usize { + self.x() + self.columns() + } + fn bottom_boundary_y_coords(&self) -> usize { + self.y() + self.rows() + } + fn is_directly_right_of(&self, other: &Self) -> bool { + self.x() == other.x() + other.columns() + 1 + } + fn is_directly_left_of(&self, other: &Self) -> bool { + self.x() + self.columns() + 1 == other.x() + } + fn is_directly_below(&self, other: &Self) -> bool { + self.y() == other.y() + other.rows() + 1 + } + fn is_directly_above(&self, other: &Self) -> bool { + self.y() + self.rows() + 1 == other.y() + } + fn horizontally_overlaps_with(&self, other: &Self) -> bool { + (self.y() >= other.y() && self.y() <= (other.y() + other.rows())) + || ((self.y() + self.rows()) <= (other.y() + other.rows()) + && (self.y() + self.rows()) > other.y()) + || (self.y() <= other.y() && (self.y() + self.rows() >= (other.y() + other.rows()))) + || (other.y() <= self.y() && (other.y() + other.rows() >= (self.y() + self.rows()))) + } + fn get_horizontal_overlap_with(&self, other: &Self) -> usize { + std::cmp::min(self.y() + self.rows(), other.y() + other.rows()) + - std::cmp::max(self.y(), other.y()) + } + fn vertically_overlaps_with(&self, other: &Self) -> bool { + (self.x() >= other.x() && self.x() <= (other.x() + other.columns())) + || ((self.x() + self.columns()) <= (other.x() + other.columns()) + && (self.x() + self.columns()) > other.x()) + || (self.x() <= other.x() + && (self.x() + self.columns() >= (other.x() + other.columns()))) + || (other.x() <= self.x() + && (other.x() + other.columns() >= (self.x() + self.columns()))) + } + fn get_vertical_overlap_with(&self, other: &Self) -> usize { + std::cmp::min(self.x() + self.columns(), other.x() + other.columns()) + - std::cmp::max(self.x(), other.x()) + } +} + +pub struct Boundaries { + columns: usize, + rows: usize, + boundary_characters: HashMap<Coordinates, BoundaryType>, +} + +impl Boundaries { + pub fn new(columns: u16, rows: u16) -> Self { + let columns = columns as usize; + let rows = rows as usize; + Boundaries { + columns, + rows, + boundary_characters: HashMap::new(), + } + } + pub fn add_rect(&mut self, rect: &dyn Pane) { + if self.rect_right_boundary_is_before_screen_edge(rect) { + // let boundary_x_coords = self.rect_right_boundary_x_coords(rect); + let boundary_x_coords = rect.right_boundary_x_coords(); + let first_row_coordinates = self.rect_right_boundary_row_start(rect); + let last_row_coordinates = self.rect_right_boundary_row_end(rect); + for row in first_row_coordinates..last_row_coordinates { + let coordinates = Coordinates::new(boundary_x_coords, row); + let symbol_to_add = if row == first_row_coordinates && row != 0 { + boundary_type::TOP_RIGHT + } else if row == last_row_coordinates - 1 && row != self.rows - 1 { + boundary_type::BOTTOM_RIGHT + } else { + boundary_type::VERTICAL + }; + let next_symbol = self + .boundary_characters + .get(&coordinates) + .and_then(|current_symbol| find_next_symbol(current_symbol, symbol_to_add)) + .unwrap_or(symbol_to_add); + self.boundary_characters.insert(coordinates, next_symbol); + } + } + if self.rect_bottom_boundary_is_before_screen_edge(rect) { + let boundary_y_coords = rect.bottom_boundary_y_coords(); + let first_col_coordinates = self.rect_bottom_boundary_col_start(rect); + let last_col_coordinates = self.rect_bottom_boundary_col_end(rect); + for col in first_col_coordinates..last_col_coordinates { + let coordinates = Coordinates::new(col, boundary_y_coords); + let symbol_to_add = if col == first_col_coordinates && col != 0 { + boundary_type::BOTTOM_LEFT + } else if col == last_col_coordinates - 1 && col != self.columns - 1 { + boundary_type::BOTTOM_RIGHT + } else { + boundary_type::HORIZONTAL + }; + let next_symbol = self + .boundary_characters + .get(&coordinates) + .and_then(|current_symbol| find_next_symbol(current_symbol, symbol_to_add)) + .unwrap_or(symbol_to_add); + self.boundary_characters.insert(coordinates, next_symbol); + } + } + } + pub fn vte_output(&self) -> String { + let mut vte_output = String::new(); + for (coordinates, boundary_character) in &self.boundary_characters { + vte_output.push_str(&format!( + "\u{1b}[{};{}H\u{1b}[m{}", + coordinates.y + 1, + coordinates.x + 1, + boundary_character + )); // goto row/col + boundary character + } + vte_output + } + fn rect_right_boundary_is_before_screen_edge(&self, rect: &dyn Pane) -> bool { + rect.x() + rect.columns() < self.columns + } + fn rect_bottom_boundary_is_before_screen_edge(&self, rect: &dyn Pane) -> bool { + rect.y() + rect.rows() < self.rows + } + fn rect_right_boundary_row_start(&self, rect: &dyn Pane) -> usize { + if rect.y() == 0 { + 0 + } else { + rect.y() - 1 + } + } + fn rect_right_boundary_row_end(&self, rect: &dyn Pane) -> usize { + let rect_bottom_row = rect.y() + rect.rows(); + // we do this because unless we're on the screen edge, we'd like to go one extra row to + // connect to whatever boundary is beneath us + if rect_bottom_row == self.rows { + rect_bottom_row + } else { + rect_bottom_row + 1 + } + } + fn rect_bottom_boundary_col_start(&self, rect: &dyn Pane) -> usize { + if rect.x() == 0 { + 0 + } else { + rect.x() - 1 + } + } + fn rect_bottom_boundary_col_end(&self, rect: &dyn Pane) -> usize { + let rect_right_col = rect.x() + rect.columns(); + // we do this because unless we're on the screen edge, we'd like to go one extra column to + // connect to whatever boundary is right of us + if rect_right_col == self.columns { + rect_right_col + } else { + rect_right_col + 1 + } + } +} diff --git a/src/client/layout.rs b/src/client/layout.rs new file mode 100644 index 00000000..d09e7618 --- /dev/null +++ b/src/client/layout.rs @@ -0,0 +1,220 @@ +use crate::utils::consts::MOSAIC_ROOT_LAYOUT_DIR; +use directories_next::ProjectDirs; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::{fs::File, io::prelude::*}; + +use crate::panes::PositionAndSize; + +fn split_space_to_parts_vertically( + space_to_split: &PositionAndSize, + sizes: Vec<Option<SplitSize>>, +) -> Vec<PositionAndSize> { + let mut split_parts = Vec::new(); + let mut current_x_position = space_to_split.x; + let mut current_width = 0; + let max_width = space_to_split.columns - (sizes.len() - 1); // minus space for gaps + + let mut parts_to_grow = Vec::new(); + + // First fit in the parameterized sizes + for size in sizes { + let columns = match size { + Some(SplitSize::Percent(percent)) => { + (max_width as f32 * (percent as f32 / 100.0)) as usize + } // TODO: round properly + Some(SplitSize::Fixed(size)) => size as usize, + None => { + parts_to_grow.push(current_x_position); + 1 // This is grown later on + } + }; + split_parts.push(PositionAndSize { + x: current_x_position, + y: space_to_split.y, + columns, + rows: space_to_split.rows, + }); + current_width += columns; + current_x_position += columns + 1; // 1 for gap + } + + if current_width > max_width { + panic!("Layout contained too many columns to fit onto the screen!"); + } + + let mut last_flexible_index = split_parts.len() - 1; + if let Some(new_columns) = (max_width - current_width).checked_div(parts_to_grow.len()) { + current_width = 0; + current_x_position = 0; + for (idx, part) in split_parts.iter_mut().enumerate() { + part.x = current_x_position; + if parts_to_grow.contains(&part.x) { + part.columns = new_columns; + last_flexible_index = idx; + } + current_width += part.columns; + current_x_position += part.columns + 1; // 1 for gap + } + } + + if current_width < max_width { + // we have some extra space left, let's add it to the last flexible part + let extra = max_width - current_width; + let mut last_part = split_parts.get_mut(last_flexible_index).unwrap(); + last_part.columns += extra; + for part in (&mut split_parts[last_flexible_index + 1..]).iter_mut() { + part.x += extra; + } + } + split_parts +} + +fn split_space_to_parts_horizontally( + space_to_split: &PositionAndSize, + sizes: Vec<Option<SplitSize>>, +) -> Vec<PositionAndSize> { + let mut split_parts = Vec::new(); + let mut current_y_position = space_to_split.y; + let mut current_height = 0; + let max_height = space_to_split.rows - (sizes.len() - 1); // minus space for gaps + + let mut parts_to_grow = Vec::new(); + + for size in sizes { + let rows = match size { + Some(SplitSize::Percent(percent)) => { + (max_height as f32 * (percent as f32 / 100.0)) as usize + } // TODO: round properly + Some(SplitSize::Fixed(size)) => size as usize, + None => { + parts_to_grow.push(current_y_position); + 1 // This is grown later on + } + }; + split_parts.push(PositionAndSize { + x: space_to_split.x, + y: current_y_position, + columns: space_to_split.columns, + rows, + }); + current_height += rows; + current_y_position += rows + 1; // 1 for gap + } + + if current_height > max_height { + panic!("Layout contained too many rows to fit onto the screen!"); + } + + let mut last_flexible_index = split_parts.len() - 1; + if let Some(new_rows) = (max_height - current_height).checked_div(parts_to_grow.len()) { + current_height = 0; + current_y_position = 0; + + for (idx, part) in split_parts.iter_mut().enumerate() { + part.y = current_y_position; + if parts_to_grow.contains(&part.y) { + part.rows = new_rows; + last_flexible_index = idx; + } + current_height += part.rows; + current_y_position += part.rows + 1; // 1 for gap + } + } + + if current_height < max_height { + // we have some extra space left, let's add it to the last flexible part + let extra = max_height - current_height; + let mut last_part = split_parts.get_mut(last_flexible_index).unwrap(); + last_part.rows += extra; + for part in (&mut split_parts[last_flexible_index + 1..]).iter_mut() { + part.y += extra; + } + } + split_parts +} + +fn split_space( + space_to_split: &PositionAndSize, + layout: &Layout, +) -> Vec<(Layout, PositionAndSize)> { + let mut pane_positions = Vec::new(); + let sizes: Vec<Option<SplitSize>> = layout.parts.iter().map(|part| part.split_size).collect(); + + let split_parts = match layout.direction { + Direction::Vertical => split_space_to_parts_vertically(space_to_split, sizes), + Direction::Horizontal => split_space_to_parts_horizontally(space_to_split, sizes), + }; + for (i, part) in layout.parts.iter().enumerate() { + let part_position_and_size = split_parts.get(i).unwrap(); + if !part.parts.is_empty() { + let mut part_positions = split_space(&part_position_and_size, part); + pane_positions.append(&mut part_positions); + } else { + pane_positions.push((part.clone(), *part_position_and_size)); + } + } + pane_positions +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum Direction { + Horizontal, + Vertical, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub enum SplitSize { + Percent(u8), // 1 to 100 + Fixed(u16), // An absolute number of columns or rows +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Layout { + pub direction: Direction, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub parts: Vec<Layout>, + #[serde(skip_serializing_if = "Option::is_none")] + pub split_size: Option<SplitSize>, + #[serde(skip_serializing_if = "Option::is_none")] + pub plugin: Option<PathBuf>, +} + +impl Layout { + pub fn new(layout_path: PathBuf) -> Self { + let project_dirs = ProjectDirs::from("org", "Mosaic Contributors", "Mosaic").unwrap(); + let layout_dir = project_dirs.data_dir().join("layouts/"); + let root_layout_dir = Path::new(MOSAIC_ROOT_LAYOUT_DIR); + let mut layout_file = File::open(&layout_path) + .or_else(|_| File::open(&layout_path.with_extension("yaml"))) + .or_else(|_| File::open(&layout_dir.join(&layout_path).with_extension("yaml"))) + .or_else(|_| File::open(root_layout_dir.join(&layout_path).with_extension("yaml"))) + .unwrap_or_else(|_| panic!("cannot find layout {}", &layout_path.display())); + + let mut layout = String::new(); + layout_file + .read_to_string(&mut layout) + .unwrap_or_else(|_| panic!("could not read layout {}", &layout_path.display())); + let layout: Layout = serde_yaml::from_str(&layout) + .unwrap_or_else(|_| panic!("could not parse layout {}", &layout_path.display())); + layout + } + + pub fn total_terminal_panes(&self) -> usize { + let mut total_panes = 0; + total_panes += self.parts.len(); + for part in self.parts.iter() { + if part.plugin.is_none() { + total_panes += part.total_terminal_panes(); + } + } + total_panes + } + + pub fn position_panes_in_space( + &self, + space: &PositionAndSize, + ) -> Vec<(Layout, PositionAndSize)> { + split_space(space, &self) + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 00000000..619ba375 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,9 @@ +pub mod boundaries; +pub mod layout; +pub mod tab; +pub mod panes; + + +pub fn start_client() { + +}
\ No newline at end of file diff --git a/src/client/panes/grid.rs b/src/client/panes/grid.rs new file mode 100644 index 00000000..062b5c3a --- /dev/null +++ b/src/client/panes/grid.rs @@ -0,0 +1,820 @@ +use std::{ + cmp::Ordering, + fmt::{self, Debug, Formatter}, +}; + +use crate::panes::terminal_character::{ + CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, +}; + +fn get_top_non_canonical_rows(rows: &mut Vec<Row>) -> Vec<Row> { + 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 Vec<Row>) -> Vec<Row> { + let mut index_of_last_non_canonical_row = None; + for (i, row) in rows.iter().enumerate().rev() { + if row.is_canonical { + index_of_last_non_canonical_row = Some(i); + 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 transfer_rows_down( + source: &mut Vec<Row>, + destination: &mut Vec<Row>, + count: usize, + max_src_width: Option<usize>, + max_dst_width: Option<usize>, +) { + let mut next_lines: Vec<Row> = 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() { + 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)], + }; + } + 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 mut excess_rows = + Row::from_rows(next_lines).split_to_rows_of_length(max_row_width); + source.append(&mut excess_rows); + } + None => { + let excess_row = Row::from_rows(next_lines); + source.push(excess_row); + } + } + } +} + +fn transfer_rows_up( + source: &mut Vec<Row>, + destination: &mut Vec<Row>, + count: usize, + max_src_width: Option<usize>, + max_dst_width: Option<usize>, +) { + let mut next_lines: Vec<Row> = 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 + } + } + destination.push(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); + } + } + } +} + +#[derive(Clone)] +pub struct Grid { + lines_above: Vec<Row>, + viewport: Vec<Row>, + lines_below: Vec<Row>, + cursor: Cursor, + scroll_region: Option<(usize, usize)>, + 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: vec![], + viewport: vec![], + lines_below: vec![], + cursor: Cursor::new(0, 0), + scroll_region: None, + width: columns, + height: rows, + } + } + 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().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 { + self.lines_above.push(line_to_push_up); + } else { + let mut last_line_above = self.lines_above.pop().unwrap(); + last_line_above.append(&mut line_to_push_up.columns); + self.lines_above.push(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().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<Row> = 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<Vec<TerminalCharacter>> { + let mut lines: Vec<Vec<TerminalCharacter>> = self + .viewport + .iter() + .map(|r| { + let mut line: Vec<TerminalCharacter> = 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) { + // TBD + } + pub fn rotate_scroll_region_down(&mut self, _count: usize) { + // TBD + } + 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) + self.viewport.remove(scroll_region_top); + self.viewport + .insert(scroll_region_bottom, Row::new().canonical()); + return; + } + } + if self.viewport.len() <= self.cursor.y + 1 { + 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.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 { + self.insert_character_at_cursor_position(terminal_character); + } else { + // 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.insert_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) { + for _ in self.viewport.len()..position { + self.viewport.push(Row::new().canonical()); + } + } + pub fn move_cursor_to(&mut self, x: usize, y: usize) { + self.cursor.x = x; + self.cursor.y = y; + self.pad_lines_until(self.cursor.y + 1); + 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 + 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) { + 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); + } + 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) { + 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); + self.viewport + .insert(scroll_region_bottom, Row::new().canonical()); + } + } + } + } + pub fn add_empty_lines_in_scroll_region(&mut self, count: usize) { + 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 { + self.viewport.remove(scroll_region_bottom); + self.viewport + .insert(current_line_index, Row::new().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) { + self.cursor.y = line; + self.pad_lines_until(self.cursor.y + 1); + 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); + } +} + +#[derive(Clone)] +pub struct Row { + pub columns: Vec<TerminalCharacter>, + 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<TerminalCharacter>) -> Self { + Row { + columns, + is_canonical: false, + } + } + pub fn from_rows(mut rows: Vec<Row>) -> 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 replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { + // this is much more performant than remove/insert + self.columns.push(terminal_character); + self.columns.swap_remove(x); + } + pub fn replace_columns(&mut self, columns: Vec<TerminalCharacter>) { + 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<TerminalCharacter>) { + self.columns.append(to_append); + } + pub fn replace_beginning_with(&mut self, mut line_part: Vec<TerminalCharacter>) { + 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) { + self.columns.remove(x); + } + pub fn split_to_rows_of_length(&mut self, max_row_length: usize) -> Vec<Row> { + let mut parts: Vec<Row> = vec![]; + let mut current_part: Vec<TerminalCharacter> = 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, + } + } +} diff --git a/src/client/panes/mod.rs b/src/client/panes/mod.rs new file mode 100644 index 00000000..4e7d6e80 --- /dev/null +++ b/src/client/panes/mod.rs @@ -0,0 +1,9 @@ +mod grid; +mod plugin_pane; +mod terminal_character; +mod terminal_pane; + +pub use grid::*; +pub use plugin_pane::*; +pub use terminal_character::*; +pub use terminal_pane::*; diff --git a/src/client/panes/plugin_pane.rs b/src/client/panes/plugin_pane.rs new file mode 100644 index 00000000..a93ff8c0 --- /dev/null +++ b/src/client/panes/plugin_pane.rs @@ -0,0 +1,175 @@ +#![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 selectable: 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, + selectable: 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 selectable(&self) -> bool { + self.selectable + } + fn set_selectable(&mut self, selectable: bool) { + self.selectable = selectable; + } + 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/client/panes/terminal_character.rs b/src/client/panes/terminal_character.rs new file mode 100644 index 00000000..80c3162b --- /dev/null +++ b/src/client/panes/terminal_character.rs @@ -0,0 +1,572 @@ +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.is_empty() { + 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(AnsiCode::Reset) = self.bold { + // we only reset dim if both dim and bold should be 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/client/panes/terminal_pane.rs b/src/client/panes/terminal_pane.rs new file mode 100644 index 00000000..853ae064 --- /dev/null +++ b/src/client/panes/terminal_pane.rs @@ -0,0 +1,664 @@ +#![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 std::fmt::Debug; + +use crate::panes::grid::Grid; +use crate::panes::terminal_character::{ + CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, +}; +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? +} +#[derive(Clone, Copy, Debug, Default)] +pub struct PositionAndSize { + pub x: usize, + pub y: usize, + pub rows: usize, + pub columns: usize, +} + +impl From<Winsize> for PositionAndSize { + fn from(winsize: Winsize) -> PositionAndSize { + PositionAndSize { + columns: winsize.ws_col as usize, + rows: winsize.ws_row as usize, + ..Default::default() + } + } +} + +#[derive(Debug)] +pub struct TerminalPane { + pub grid: Grid, + pub alternative_grid: Option<Grid>, // for 1049h/l instructions which tell us to switch between these two + pub pid: RawFd, + pub should_render: bool, + pub selectable: 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, + clear_viewport_before_rendering: bool, +} + +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.grid.cursor_coordinates() + } + 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 selectable(&self) -> bool { + self.selectable + } + fn set_selectable(&mut self, selectable: bool) { + self.selectable = selectable; + } + 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(); + if self.clear_viewport_before_rendering { + for line_index in 0..self.grid.height { + let x = self.get_x(); + let y = self.get_y(); + vte_output = format!( + "{}\u{1b}[{};{}H\u{1b}[m", + vte_output, + y + line_index + 1, + x + 1 + ); // goto row/col and reset styles + for _col_index in 0..self.grid.width { + vte_output.push(EMPTY_TERMINAL_CHARACTER.character); + } + } + self.clear_viewport_before_rendering = false; + } + 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.grid.move_viewport_up(count); + self.mark_for_rerender(); + } + fn scroll_down(&mut self, count: usize) { + self.grid.move_viewport_down(count); + self.mark_for_rerender(); + } + fn clear_scroll(&mut self) { + self.grid.reset_viewport(); + self.mark_for_rerender(); + } +} + +impl TerminalPane { + pub fn new(pid: RawFd, position_and_size: PositionAndSize) -> TerminalPane { + let grid = Grid::new(position_and_size.rows, position_and_size.columns); + let pending_styles = CharacterStyles::new(); + + TerminalPane { + pid, + grid, + alternative_grid: None, + should_render: true, + selectable: true, + pending_styles, + position_and_size, + position_and_size_override: None, + cursor_key_mode: false, + clear_viewport_before_rendering: 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.grid.change_size(rows, columns); + if let Some(alternative_grid) = self.alternative_grid.as_mut() { + alternative_grid.change_size(rows, columns); + } + } + + pub fn read_buffer_as_lines(&self) -> Vec<Vec<TerminalCharacter>> { + self.grid.as_character_lines() + } + #[cfg(test)] + pub fn cursor_coordinates(&self) -> Option<(usize, usize)> { + // (x, y) + self.grid.cursor_coordinates() + } + pub fn rotate_scroll_region_up(&mut self, count: usize) { + self.grid.rotate_scroll_region_up(count); + self.mark_for_rerender(); + } + pub fn rotate_scroll_region_down(&mut self, count: usize) { + self.grid.rotate_scroll_region_down(count); + self.mark_for_rerender(); + } + fn add_newline(&mut self) { + self.grid.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.grid.move_cursor_to_beginning_of_line(); + } + fn move_cursor_backwards(&mut self, count: usize) { + self.grid.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.grid.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.grid.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.grid.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.grid + .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.grid + .replace_characters_in_line_before_cursor(char_to_replace); + } else if params[0] == 2 { + self.grid.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.grid.clear_all_after_cursor(char_to_replace); + } else if params[0] == 2 { + self.grid.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 - 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.grid.move_cursor_to(col, row); + } else if c == 'A' { + // move cursor up until edge of screen + let move_up_count = if params[0] == 0 { 1 } else { params[0] }; + self.grid.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.grid.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.grid.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_grid) = self.alternative_grid.as_mut() { + std::mem::swap(&mut self.grid, alternative_grid); + } + self.alternative_grid = None; + self.clear_viewport_before_rendering = true; + self.mark_for_rerender(); + } + Some(&25) => { + self.grid.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.grid.show_cursor(); + self.mark_for_rerender(); + } + Some(&1049) => { + let columns = self + .position_and_size_override + .map(|x| x.columns) + .unwrap_or(self.position_and_size.columns); + let rows = self + .position_and_size_override + .map(|x| x.rows) + .unwrap_or(self.position_and_size.rows); + let current_grid = + std::mem::replace(&mut self.grid, Grid::new(rows, columns)); + self.alternative_grid = Some(current_grid); + 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 - 1; + let bottom_line_index = params[1] as usize - 1; + self.grid + .set_scroll_region(top_line_index, bottom_line_index); + self.grid.show_cursor(); + } else { + self.grid.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.grid + .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.grid + .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 - 1 + }; + self.grid.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.grid.move_cursor_to_line(line); + } else if c == 'P' { + // erase characters + let count = if params[0] == 0 { + 1 + } else { + params[0] as usize + }; + self.grid.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.grid + .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.grid.delete_lines_in_scroll_region(count); + self.grid.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) { + if let (b'M', None) = (byte, intermediates.get(0)) { + self.grid.move_cursor_up_with_scrolling(1); + } + } +} diff --git a/src/client/tab.rs b/src/client/tab.rs new file mode 100644 index 00000000..6c65958f --- /dev/null +++ b/src/client/tab.rs @@ -0,0 +1,1798 @@ +use crate::panes::{PaneId, PositionAndSize, TerminalPane}; +use crate::pty_bus::{PtyInstruction, VteEvent}; +use crate::{boundaries::Boundaries, panes::PluginPane}; +use crate::{layout::Layout, wasm_vm::PluginInstruction}; +use crate::{os_input_output::OsApi, utils::shared::pad_to_size}; +use crate::{AppInstruction, SenderWithContext}; +use std::os::unix::io::RawFd; +use std::{ + cmp::Reverse, + collections::{BTreeMap, HashSet}, +}; +use std::{io::Write, sync::mpsc::channel}; + +/* + * Tab + * + * this holds multiple panes (currently terminal panes) which are currently displayed + * when this tab is active. + * it tracks their coordinates (x/y) and size, as well as how they should be resized + * + */ + +const CURSOR_HEIGHT_WIDTH_RATIO: usize = 4; // this is not accurate and kind of a magic number, TODO: look into this + +type BorderAndPaneIds = (usize, Vec<PaneId>); + +fn split_vertically_with_gap(rect: &PositionAndSize) -> (PositionAndSize, PositionAndSize) { + let width_of_each_half = (rect.columns - 1) / 2; + let mut first_rect = *rect; + let mut second_rect = *rect; + if rect.columns % 2 == 0 { + first_rect.columns = width_of_each_half + 1; + } else { + first_rect.columns = width_of_each_half; + } + second_rect.x = first_rect.x + first_rect.columns + 1; + second_rect.columns = width_of_each_half; + (first_rect, second_rect) +} + +fn split_horizontally_with_gap(rect: &PositionAndSize) -> (PositionAndSize, PositionAndSize) { + let height_of_each_half = (rect.rows - 1) / 2; + let mut first_rect = *rect; + let mut second_rect = *rect; + if rect.rows % 2 == 0 { + first_rect.rows = height_of_each_half + 1; + } else { + first_rect.rows = height_of_each_half; + } + second_rect.y = first_rect.y + first_rect.rows + 1; + second_rect.rows = height_of_each_half; + (first_rect, second_rect) +} + +pub struct Tab { + pub index: usize, + panes: BTreeMap<PaneId, Box<dyn Pane>>, + panes_to_hide: HashSet<PaneId>, + active_terminal: Option<PaneId>, + max_panes: Option<usize>, + full_screen_ws: PositionAndSize, + fullscreen_is_active: bool, + os_api: Box<dyn OsApi>, + pub send_pty_instructions: SenderWithContext<PtyInstruction>, + pub send_plugin_instructions: SenderWithContext<PluginInstruction>, + pub send_app_instructions: SenderWithContext<AppInstruction>, +} + +// FIXME: Use a struct that has a pane_type enum, to reduce all of the duplication +pub trait Pane { + fn x(&self) -> usize; + fn y(&self) -> usize; + fn rows(&self) -> usize; + fn columns(&self) -> usize; + fn reset_size_and_position_override(&mut self); + fn change_pos_and_size(&mut self, position_and_size: &PositionAndSize); + fn override_size_and_position(&mut self, x: usize, y: usize, size: &PositionAndSize); + fn handle_event(&mut self, event: VteEvent); + fn cursor_coordinates(&self) -> Option<(usize, usize)>; + fn adjust_input_to_terminal(&self, input_bytes: Vec<u8>) -> Vec<u8>; + + fn position_and_size_override(&self) -> Option<PositionAndSize>; + fn should_render(&self) -> bool; + fn set_should_render(&mut self, should_render: bool); + fn selectable(&self) -> bool; + fn set_selectable(&mut self, selectable: bool); + fn render(&mut self) -> Option<String>; + fn pid(&self) -> PaneId; + fn reduce_height_down(&mut self, count: usize); + fn increase_height_down(&mut self, count: usize); + fn increase_height_up(&mut self, count: usize); + fn reduce_height_up(&mut self, count: usize); + fn increase_width_right(&mut self, count: usize); + fn reduce_width_right(&mut self, count: usize); + fn reduce_width_left(&mut self, count: usize); + fn increase_width_left(&mut self, count: usize); + fn scroll_up(&mut self, count: usize); + fn scroll_down(&mut self, count: usize); + fn clear_scroll(&mut self); + + fn right_boundary_x_coords(&self) -> usize { + self.x() + self.columns() + } + fn bottom_boundary_y_coords(&self) -> usize { + self.y() + self.rows() + } + fn is_directly_right_of(&self, other: &dyn Pane) -> bool { + self.x() == other.x() + other.columns() + 1 + } + fn is_directly_left_of(&self, other: &dyn Pane) -> bool { + self.x() + self.columns() + 1 == other.x() + } + fn is_directly_below(&self, other: &dyn Pane) -> bool { + self.y() == other.y() + other.rows() + 1 + } + fn is_directly_above(&self, other: &dyn Pane) -> bool { + self.y() + self.rows() + 1 == other.y() + } + fn horizontally_overlaps_with(&self, other: &dyn Pane) -> bool { + (self.y() >= other.y() && self.y() <= (other.y() + other.rows())) + || ((self.y() + self.rows()) <= (other.y() + other.rows()) + && (self.y() + self.rows()) > other.y()) + || (self.y() <= other.y() && (self.y() + self.rows() >= (other.y() + other.rows()))) + || (other.y() <= self.y() && (other.y() + other.rows() >= (self.y() + self.rows()))) + } + fn get_horizontal_overlap_with(&self, other: &dyn Pane) -> usize { + std::cmp::min(self.y() + self.rows(), other.y() + other.rows()) + - std::cmp::max(self.y(), other.y()) + } + fn vertically_overlaps_with(&self, other: &dyn Pane) -> bool { + (self.x() >= other.x() && self.x() <= (other.x() + other.columns())) + || ((self.x() + self.columns()) <= (other.x() + other.columns()) + && (self.x() + self.columns()) > other.x()) + || (self.x() <= other.x() + && (self.x() + self.columns() >= (other.x() + other.columns()))) + || (other.x() <= self.x() + && (other.x() + other.columns() >= (self.x() + self.columns()))) + } + fn get_vertical_overlap_with(&self, other: &dyn Pane) -> usize { + std::cmp::min(self.x() + self.columns(), other.x() + other.columns()) + - std::cmp::max(self.x(), other.x()) + } +} + +impl Tab { + // FIXME: Too many arguments here! Maybe bundle all of the senders for the whole program in a struct? + pub fn new( + index: usize, + full_screen_ws: &PositionAndSize, + mut os_api: Box<dyn OsApi>, + send_pty_instructions: SenderWithContext<PtyInstruction>, + send_plugin_instructions: SenderWithContext<PluginInstruction>, + send_app_instructions: SenderWithContext<AppInstruction>, + max_panes: Option<usize>, + pane_id: Option<PaneId>, + ) -> Self { + let panes = if let Some(PaneId::Terminal(pid)) = pane_id { + let new_terminal = TerminalPane::new(pid, *full_screen_ws); + os_api.set_terminal_size_using_fd( + new_terminal.pid, + new_terminal.columns() as u16, + new_terminal.rows() as u16, + ); + let mut panes: BTreeMap<PaneId, Box<dyn Pane>> = BTreeMap::new(); + panes.insert(PaneId::Terminal(pid), Box::new(new_terminal)); + panes + } else { + BTreeMap::new() + }; + Tab { + index, + panes, + max_panes, + panes_to_hide: HashSet::new(), + active_terminal: pane_id, + full_screen_ws: *full_screen_ws, + fullscreen_is_active: false, + os_api, + send_app_instructions, + send_pty_instructions, + send_plugin_instructions, + } + } + + pub fn apply_layout(&mut self, layout: Layout, new_pids: Vec<RawFd>) { + // TODO: this should be an attribute on Screen instead of full_screen_ws + let free_space = PositionAndSize { + x: 0, + y: 0, + rows: self.full_screen_ws.rows, + columns: self.full_screen_ws.columns, + }; + self.panes_to_hide.clear(); + let positions_in_layout = layout.position_panes_in_space(&free_space); + let mut positions_and_size = positions_in_layout.iter(); + for (pane_kind, terminal_pane) in self.panes.iter_mut() { + // for now the layout only supports terminal panes + if let PaneId::Terminal(pid) = pane_kind { + match positions_and_size.next() { + Some((_, position_and_size)) => { + terminal_pane.reset_size_and_position_override(); + terminal_pane.change_pos_and_size(&position_and_size); + self.os_api.set_terminal_size_using_fd( + *pid, + position_and_size.columns as u16, + position_and_size.rows as u16, + ); + } + None => { + // we filled the entire layout, no room for this pane + // TODO: handle active terminal + self.panes_to_hide.insert(PaneId::Terminal(*pid)); + } + } + } + } + let mut new_pids = new_pids.iter(); + for (layout, position_and_size) in positions_and_size { + // Just a regular terminal + if let Some(plugin) = &layout.plugin { + let (pid_tx, pid_rx) = channel(); + self.send_plugin_instructions + .send(PluginInstruction::Load(pid_tx, plugin.clone())) + .unwrap(); + let pid = pid_rx.recv().unwrap(); + let new_plugin = PluginPane::new( + pid, + *position_and_size, + self.send_plugin_instructions.clone(), + ); + self.panes.insert(PaneId::Plugin(pid), Box::new(new_plugin)); + } else { + // there are still panes left to fill, use the pids we received in this method + let pid = new_pids.next().unwrap(); // if this crashes it means we got less pids than there are panes in this layout + let new_terminal = TerminalPane::new(*pid, *position_and_size); + self.os_api.set_terminal_size_using_fd( + new_terminal.pid, + new_terminal.columns() as u16, + new_terminal.rows() as u16, + ); + self.panes + .insert(PaneId::Terminal(*pid), Box::new(new_terminal)); + } + } + for unused_pid in new_pids { + // this is a bit of a hack and happens because we don't have any central location that + // can query the screen as to how many panes it needs to create a layout + // fixing this will require a bit of an architecture change + self.send_pty_instructions + .send(PtyInstruction::ClosePane(PaneId::Terminal(*unused_pid))) + .unwrap(); + } + self.active_terminal = self.panes.iter().map(|(id, _)| id.to_owned()).next(); + self.render(); + } + pub fn new_pane(&mut self, pid: PaneId) { + self.close_down_to_max_terminals(); + if self.fullscreen_is_active { + self.toggle_active_pane_fullscreen(); + } + if !self.has_panes() { + // FIXME: This could use a second look + if let PaneId::Terminal(term_pid) = pid { + let new_terminal = TerminalPane::new(term_pid, self.full_screen_ws); + self.os_api.set_terminal_size_using_fd( + new_terminal.pid, + new_terminal.columns() as u16, + new_terminal.rows() as u16, + ); + self.panes.insert(pid, Box::new(new_terminal)); + self.active_terminal = Some(pid); + } + } else { + // TODO: check minimum size of active terminal + + let (_longest_edge, terminal_id_to_split) = self.get_panes().fold( + (0, PaneId::Terminal(0)), // FIXME: This is a bit hacky, try to use a maximum method? + |(current_longest_edge, current_terminal_id_to_split), id_and_terminal_to_check| { + let (id_of_terminal_to_check, terminal_to_check) = id_and_terminal_to_check; + let terminal_size = (terminal_to_check.rows() * CURSOR_HEIGHT_WIDTH_RATIO) + * terminal_to_check.columns(); + if terminal_size > current_longest_edge { + (terminal_size, *id_of_terminal_to_check) + } else { + (current_longest_edge, current_terminal_id_to_split) + } + }, + ); + let terminal_to_split = self.panes.get_mut(&terminal_id_to_split).unwrap(); + let terminal_ws = PositionAndSize { + rows: terminal_to_split.rows(), + columns: terminal_to_split.columns(), + x: terminal_to_split.x(), + y: terminal_to_split.y(), + }; + if terminal_to_split.rows() * CURSOR_HEIGHT_WIDTH_RATIO > terminal_to_split.columns() { + // FIXME: This could use a second look + if let PaneId::Terminal(term_pid) = pid { + let (top_winsize, bottom_winsize) = split_horizontally_with_gap(&terminal_ws); + let new_terminal = TerminalPane::new(term_pid, bottom_winsize); + self.os_api.set_terminal_size_using_fd( + new_terminal.pid, + bottom_winsize.columns as u16, + bottom_winsize.rows as u16, + ); + terminal_to_split.change_pos_and_size(&top_winsize); + self.panes.insert(pid, Box::new(new_terminal)); + if let PaneId::Terminal(terminal_id_to_split) = terminal_id_to_split { + self.os_api.set_terminal_size_using_fd( + terminal_id_to_split, + top_winsize.columns as u16, + top_winsize.rows as u16, + ); + } + self.active_terminal = Some(pid); + } + } else { + // FIXME: This could use a second look + if let PaneId::Terminal(term_pid) = pid { + let (left_winszie, right_winsize) = split_vertically_with_gap(&terminal_ws); + let new_terminal = TerminalPane::new(term_pid, right_winsize); + self.os_api.set_terminal_size_using_fd( + new_terminal.pid, + right_winsize.columns as u16, + right_winsize.rows as u16, + ); + terminal_to_split.change_pos_and_size(&left_winszie); + self.panes.insert(pid, Box::new(new_terminal)); + if let PaneId::Terminal(terminal_id_to_split) = terminal_id_to_split { + self.os_api.set_terminal_size_using_fd( + terminal_id_to_split, + left_winszie.columns as u16, + left_winszie.rows as u16, + ); + } + } + } + self.active_terminal = Some(pid); + self.render(); + } + } + pub fn horizontal_split(&mut self, pid: PaneId) { + self.close_down_to_max_terminals(); + if self.fullscreen_is_active { + self.toggle_active_pane_fullscreen(); + } + if !self.has_panes() { + // FIXME: This could use a second look + if let PaneId::Terminal(term_pid) = pid { + let new_terminal = TerminalPane::new(term_pid, self.full_screen_ws); + self.os_api.set_terminal_size_using_fd( + new_terminal.pid, + new_terminal.columns() as u16, + new_terminal.rows() as u16, + ); + self.panes.insert(pid, Box::new(new_terminal)); + self.active_terminal = Some(pid); + } + } else { + // FIXME: This could use a second look + if let PaneId::Terminal(term_pid) = pid { + // TODO: check minimum size of active terminal + let active_pane_id = &self.get_active_pane_id().unwrap(); + let active_pane = self.panes.get_mut(active_pane_id).unwrap(); + let terminal_ws = PositionAndSize { + x: active_pane.x(), + y: active_pane.y(), + rows: active_pane.rows(), + columns: active_pane.columns(), + }; + let (top_winsize, bottom_winsize) = split_horizontally_with_gap(&terminal_ws); + + active_pane.change_pos_and_size(&top_winsize); + + let new_terminal = TerminalPane::new(term_pid, bottom_winsize); + self.os_api.set_terminal_size_using_fd( + new_terminal.pid, + bottom_winsize.columns as u16, + bottom_winsize.rows as u16, + ); + self.panes.insert(pid, Box::new(new_terminal)); + + if let PaneId::Terminal(active_terminal_pid) = active_pane_id { + self.os_api.set_terminal_size_using_fd( + *active_terminal_pid, + top_winsize.columns as u16, + top_winsize.rows as u16, + ); + } + + self.active_terminal = Some(pid); + self.render(); + } + } + } + pub fn vertical_split(&mut self, pid: PaneId) { + self.close_down_to_max_terminals(); + if self.fullscreen_is_active { + self.toggle_active_pane_fullscreen(); + } + if !self.has_panes() { + // FIXME: This could use a second look + if let PaneId::Terminal(term_pid) = pid { + let new_terminal = TerminalPane::new(term_pid, self.full_screen_ws); + self.os_api.set_terminal_size_using_fd( + new_terminal.pid, + new_terminal.columns() as u16, + new_terminal.rows() as u16, + ); + self.panes.insert(pid, Box::new(new_terminal)); + self.active_terminal = Some(pid); + } + } else { + // FIXME: This could use a second look + if let PaneId::Terminal(term_pid) = pid { + // TODO: check minimum size of active terminal + let active_pane_id = &self.get_active_pane_id().unwrap(); + let active_pane = self.panes.get_mut(active_pane_id).unwrap(); + let terminal_ws = PositionAndSize { + x: active_pane.x(), + y: active_pane.y(), + rows: active_pane.rows(), + columns: active_pane.columns(), + }; + let (left_winsize, right_winsize) = split_vertically_with_gap(&terminal_ws); + + active_pane.change_pos_and_size(&left_winsize); + + let new_terminal = TerminalPane::new(term_pid, right_winsize); + self.os_api.set_terminal_size_using_fd( + new_terminal.pid, + right_winsize.columns as u16, + right_winsize.rows as u16, + ); + self.panes.insert(pid, Box::new(new_terminal)); + + if let PaneId::Terminal(active_terminal_pid) = active_pane_id { + self.os_api.set_terminal_size_using_fd( + *active_terminal_pid, + left_winsize.columns as u16, + left_winsize.rows as u16, + ); + } + + self.active_terminal = Some(pid); + self.render(); + } + } + } + pub fn get_active_pane(&self) -> Option<&dyn Pane> { + // FIXME: Could use Option::map() here + match self.get_active_pane_id() { + Some(active_pane) => self.panes.get(&active_pane).map(Box::as_ref), + None => None, + } + } + fn get_active_pane_id(&self) -> Option<PaneId> { + self.active_terminal + } + fn get_active_terminal_id(&self) -> Option<RawFd> { + // FIXME: Is there a better way to do this? + if let Some(PaneId::Terminal(pid)) = self.active_terminal { + Some(pid) + } else { + None + } + } + pub fn handle_pty_event(&mut self, pid: RawFd, event: VteEvent) { + // if we don't have the terminal in self.terminals it's probably because + // of a race condition where the terminal was created in pty_bus but has not + // yet been created in Screen. These events are currently not buffered, so + // if you're debugging seemingly randomly missing stdout data, this is + // the reason + if let Some(terminal_output) = self.panes.get_mut(&PaneId::Terminal(pid)) { + terminal_output.handle_event(event); + } + } + pub fn write_to_active_terminal(&mut self, input_bytes: Vec<u8>) { + match self.get_active_pane_id() { + Some(PaneId::Terminal(active_terminal_id)) => { + let active_terminal = self.get_active_pane().unwrap(); + let mut adjusted_input = active_terminal.adjust_input_to_terminal(input_bytes); + self.os_api + .write_to_tty_stdin(active_terminal_id, &mut adjusted_input) + .expect("failed to write to terminal"); + self.os_api + .tcdrain(active_terminal_id) + .expect("failed to drain terminal"); + } + Some(PaneId::Plugin(pid)) => { + self.send_plugin_instructions + .send(PluginInstruction::Input(pid, input_bytes)) + .unwrap(); + } + _ => {} + } + } + pub fn get_active_terminal_cursor_position(&self) -> Option<(usize, usize)> { + // (x, y) + let active_terminal = &self.get_active_pane()?; + active_terminal + .cursor_coordinates() + .map(|(x_in_terminal, y_in_terminal)| { + let x = active_terminal.x() + x_in_terminal; + let y = active_terminal.y() + y_in_terminal; + (x, y) + }) + } + pub fn toggle_active_pane_fullscreen(&mut self) { + if let Some(active_pane_id) = self.get_active_pane_id() { + if self + .get_active_pane() + .unwrap() + .position_and_size_override() + .is_some() + { + for terminal_id in self.panes_to_hide.iter() { + self.panes + .get_mut(terminal_id) + .unwrap() + .set_should_render(true); + } + self.panes_to_hide.clear(); + let active_terminal = self.panes.get_mut(&active_pane_id).unwrap(); + active_terminal.reset_size_and_position_override(); + } else { + let terminals = self.get_panes(); + let all_ids_except_current = + terminals.filter_map( + |(&id, _)| { + if id != active_pane_id { + Some(id) + } else { + None + } + }, + ); + self.panes_to_hide = all_ids_except_current.collect(); + let active_terminal = self.panes.get_mut(&active_pane_id).unwrap(); + active_terminal.override_size_and_position(0, 0, &self.full_screen_ws); + } + let active_terminal = self.panes.get(&active_pane_id).unwrap(); + if let PaneId::Terminal(active_pid) = active_pane_id { + self.os_api.set_terminal_size_using_fd( + active_pid, + active_terminal.columns() as u16, + active_terminal.rows() as u16, + ); + } + self.render(); + self.toggle_fullscreen_is_active(); + } + } + pub fn toggle_fullscreen_is_active(&mut self) { + self.fullscreen_is_active = !self.fullscreen_is_active; + } + pub fn render(&mut self) { + if self.active_terminal.is_none() { + // we might not have an active terminal if we closed the last pane + // in that case, we should not render as the app is exiting + return; + } + let mut stdout = self.os_api.get_stdout_writer(); + let mut boundaries = Boundaries::new( + self.full_screen_ws.columns as u16, + self.full_screen_ws.rows as u16, + ); + for (kind, terminal) in self.panes.iter_mut() { + if !self.panes_to_hide.contains(&terminal.pid()) { + boundaries.add_rect(terminal.as_ref()); + if let Some(vte_output) = terminal.render() { + let vte_output = if let PaneId::Terminal(_) = kind { + vte_output + } else { + pad_to_size(&vte_output, terminal.rows(), terminal.columns()) + }; + // FIXME: Use Termion for cursor and style clearing? + write!( + stdout, + "\u{1b}[{};{}H\u{1b}[m{}", + terminal.y() + 1, + terminal.x() + 1, + vte_output + ) + .expect("cannot write to stdout"); + } + } + } + + // TODO: only render (and calculate) boundaries if there was a resize + let vte_output = boundaries.vte_output(); + stdout + .write_all(&vte_output.as_bytes()) + .expect("cannot write to stdout"); + + match self.get_active_terminal_cursor_position() { + Some((cursor_position_x, cursor_position_y)) => { + let show_cursor = "\u{1b}[?25h"; + let goto_cursor_position = format!( + "\u{1b}[{};{}H\u{1b}[m", + cursor_position_y + 1, + cursor_position_x + 1 + ); // goto row/col + stdout + .write_all(&show_cursor.as_bytes()) + .expect("cannot write to stdout"); + stdout + .write_all(&goto_cursor_position.as_bytes()) + .expect("cannot write to stdout"); + stdout.flush().expect("could not flush"); + } + None => { + let hide_cursor = "\u{1b}[?25l"; + stdout + .write_all(&hide_cursor.as_bytes()) + .expect("cannot write to stdout"); + stdout.flush().expect("could not flush"); + } + } + } + fn get_panes(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> { + self.panes.iter() + } + // FIXME: This is some shameful duplication... + fn get_selectable_panes(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> { + self.panes.iter().filter(|(_, p)| p.selectable()) + } + fn has_panes(&self) -> bool { + let mut all_terminals = self.get_panes(); + all_terminals.next().is_some() + } + fn has_selectable_panes(&self) -> bool { + let mut all_terminals = self.get_selectable_panes(); + all_terminals.next().is_some() + } + fn next_active_pane(&self, panes: Vec<PaneId>) -> Option<PaneId> { + panes + .into_iter() + .rev() + .find(|pid| self.panes.get(pid).unwrap().selectable()) + } + fn pane_ids_directly_left_of(&self, id: &PaneId) -> Option<Vec<PaneId>> { + let mut ids = vec![]; + let terminal_to_check = self.panes.get(id).unwrap(); + if terminal_to_check.x() == 0 { + return None; + } + for (&pid, terminal) in self.get_panes() { + if terminal.x() + terminal.columns() == terminal_to_check.x() - 1 { + ids.push(pid); + } + } + if ids.is_empty() { + None + } else { + Some(ids) + } + } + fn pane_ids_directly_right_of(&self, id: &PaneId) -> Option<Vec<PaneId>> { + let mut ids = vec![]; + let terminal_to_check = self.panes.get(id).unwrap(); + for (&pid, terminal) in self.get_panes() { + if terminal.x() == terminal_to_check.x() + terminal_to_check.columns() + 1 { + ids.push(pid); + } + } + if ids.is_empty() { + None + } else { + Some(ids) + } + } + fn pane_ids_directly_below(&self, id: &PaneId) -> Option<Vec<PaneId>> { + let mut ids = vec![]; + let terminal_to_check = self.panes.get(id).unwrap(); + for (&pid, terminal) in self.get_panes() { + if terminal.y() == terminal_to_check.y() + terminal_to_check.rows() + 1 { + ids.push(pid); + } + } + if ids.is_empty() { + None + } else { + Some(ids) + } + } + fn pane_ids_directly_above(&self, id: &PaneId) -> Option<Vec<PaneId>> { + let mut ids = vec![]; + let terminal_to_check = self.panes.get(id).unwrap(); + for (&pid, terminal) in self.get_panes() { + if terminal.y() + terminal.rows() + 1 == terminal_to_check.y() { + ids.push(pid); + } + } + if ids.is_empty() { + None + } else { + Some(ids) + } + } + fn panes_top_aligned_with_pane(&self, pane: &dyn Pane) -> Vec<&dyn Pane> { + self.panes + .keys() + .map(|t_id| self.panes.get(&t_id).unwrap().as_ref()) + .filter(|terminal| terminal.pid() != pane.pid() && terminal.y() == pane.y()) + .collect() + } + fn panes_bottom_aligned_with_pane(&self, pane: &dyn Pane) -> Vec<&dyn Pane> { + self.panes + .keys() + .map(|t_id| self.panes.get(&t_id).unwrap().as_ref()) + .filter(|terminal| { + terminal.pid() != pane.pid() + && terminal.y() + terminal.rows() == pane.y() + pane.rows() + }) + .collect() + } + fn panes_right_aligned_with_pane(&self, pane: &dyn Pane) -> Vec<&dyn Pane> { + self.panes + .keys() + .map(|t_id| self.panes.get(&t_id).unwrap().as_ref()) + .filter(|terminal| { + terminal.pid() != pane.pid() + && terminal.x() + terminal.columns() == pane.x() + pane.columns() + }) + .collect() + } + fn panes_left_aligned_with_pane(&self, pane: &dyn Pane) -> Vec<&dyn Pane> { + self.panes + .keys() + .map(|t_id| self.panes.get(&t_id).unwrap().as_ref()) + .filter(|terminal| terminal.pid() != pane.pid() && terminal.x() == pane.x()) + .collect() + } + fn right_aligned_contiguous_panes_above( + &self, + id: &PaneId, + terminal_borders_to_the_right: &HashSet<usize>, + ) -> BorderAndPaneIds { + let mut terminals = vec![]; + let terminal_to_check = self + .panes + .get(id) + .expect("terminal id does not exist") + .as_ref(); + let mut right_aligned_terminals = self.panes_right_aligned_with_pane(terminal_to_check); + // terminals that are next to each other up to current + right_aligned_terminals.sort_by_key(|a| Reverse(a.y())); + for terminal in right_aligned_terminals { + let terminal_to_check = terminals.last().unwrap_or(&terminal_to_check); + if terminal.y() + terminal.rows() + 1 == terminal_to_check.y() { + terminals.push(terminal); + } + } + // top-most border aligned with a pane border to the right + let mut top_resize_border = 0; + for terminal in &terminals { + let bottom_terminal_boundary = terminal.y() + terminal.rows(); + if terminal_borders_to_the_right + .get(&(bottom_terminal_boundary + 1)) + .is_some() + && top_resize_border < bottom_terminal_boundary + { + top_resize_border = bottom_terminal_boundary + 1; + } + } + terminals.retain(|terminal| terminal.y() >= top_resize_border); + // if there are no adjacent panes to resize, we use the border of the main pane we're + // resizing + let top_resize_border = if terminals.is_empty() { + terminal_to_check.y() + } else { + top_resize_border + }; + let terminal_ids: Vec<PaneId> = terminals.iter().map(|t| t.pid()).collect(); + (top_resize_border, terminal_ids) + } + fn right_aligned_contiguous_panes_below( + &self, + id: &PaneId, + terminal_borders_to_the_right: &HashSet<usize>, + ) -> BorderAndPaneIds { + let mut terminals = vec![]; + let terminal_to_check = self + .panes + .get(id) + .expect("terminal id does not exist") + .as_ref(); + let mut right_aligned_terminals = self.panes_right_aligned_with_pane(terminal_to_check); + // terminals that are next to each other up to current + right_aligned_terminals.sort_by_key(|a| a.y()); + for terminal in right_aligned_terminals { + let terminal_to_check = terminals.last().unwrap_or(&terminal_to_check); + if terminal.y() == terminal_to_check.y() + terminal_to_check.rows() + 1 { + terminals.push(terminal); + } + } + // bottom-most border aligned with a pane border to the right + let mut bottom_resize_border = self.full_screen_ws.rows; + for terminal in &terminals { + let top_terminal_boundary = terminal.y(); + if terminal_borders_to_the_right + .get(&(top_terminal_boundary)) + .is_some() + && top_terminal_boundary < bottom_resize_border + { + bottom_resize_border = top_terminal_boundary; + } + } + terminals.retain(|terminal| terminal.y() + terminal.rows() <= bottom_resize_border); + // if there are no adjacent panes to resize, we use the border of the main pane we're + // resizing + let bottom_resize_border = if terminals.is_empty() { + terminal_to_check.y() + terminal_to_check.rows() + } else { + bottom_resize_border + }; + let terminal_ids: Vec<PaneId> = terminals.iter().map(|t| t.pid()).collect(); + (bottom_resize_border, terminal_ids) + } + fn left_aligned_contiguous_panes_above( + &self, + id: &PaneId, + terminal_borders_to_the_left: &HashSet<usize>, + ) -> BorderAndPaneIds { + let mut terminals = vec![]; + let terminal_to_check = self + .panes + .get(id) + .expect("terminal id does not exist") + .as_ref(); + let mut left_aligned_terminals = self.panes_left_aligned_with_pane(terminal_to_check); + // terminals that are next to each other up to current + left_aligned_terminals.sort_by_key(|a| Reverse(a.y())); + for terminal in left_aligned_terminals { + let terminal_to_check = terminals.last().unwrap_or(&terminal_to_check); + if terminal.y() + terminal.rows() + 1 == terminal_to_check.y() { + terminals.push(terminal); + } + } + // top-most border aligned with a pane border to the right + let mut top_resize_border = 0; + for terminal in &terminals { + let bottom_terminal_boundary = terminal.y() + terminal.rows(); + if terminal_borders_to_the_left + .get(&(bottom_terminal_boundary + 1)) + .is_some() + && top_resize_border < bottom_terminal_boundary + { + top_resize_border = bottom_terminal_boundary + 1; + } + } + terminals.retain(|terminal| terminal.y() >= top_resize_border); + // if there are no adjacent panes to resize, we use the border of the main pane we're + // resizing + let top_resize_border = if terminals.is_empty() { + terminal_to_check.y() + } else { + top_resize_border + }; + let terminal_ids: Vec<PaneId> = terminals.iter().map(|t| t.pid()).collect(); + (top_resize_border, terminal_ids) + } + fn left_aligned_contiguous_panes_below( + &self, + id: &PaneId, + terminal_borders_to_the_left: &HashSet<usize>, + ) -> BorderAndPaneIds { + let mut terminals = vec![]; + let terminal_to_check = self + .panes + .get(id) + .expect("terminal id does not exist") + .as_ref(); + let mut left_aligned_terminals = self.panes_left_aligned_with_pane(terminal_to_check); + // terminals that are next to each other up to current + left_aligned_terminals.sort_by_key(|a| a.y()); + for terminal in left_aligned_terminals { + let terminal_to_check = terminals.last().unwrap_or(&terminal_to_check); + if terminal.y() == terminal_to_check.y() + terminal_to_check.rows() + 1 { + terminals.push(terminal); + } + } + // bottom-most border aligned with a pane border to the left + let mut bottom_resize_border = self.full_screen_ws.rows; + for terminal in &terminals { + let top_terminal_boundary = terminal.y(); + if terminal_borders_to_the_left + .get(&(top_terminal_boundary)) + .is_some() + && top_terminal_boundary < bottom_resize_border + { + bottom_resize_border = top_terminal_boundary; + } + } + terminals.retain(|terminal| { + // terminal.y() + terminal.rows() < bottom_resize_border + terminal.y() + terminal.rows() <= bottom_resize_border + }); + // if there are no adjacent panes to resize, we use the border of the main pane we're + // resizing + let bottom_resize_border = if terminals.is_empty() { + terminal_to_check.y() + terminal_to_check.rows() + } else { + bottom_resize_border + }; + let terminal_ids: Vec<PaneId> = terminals.iter().map(|t| t.pid()).collect(); + (bottom_resize_border, terminal_ids) + } + fn top_aligned_contiguous_panes_to_the_left( + &self, + id: &PaneId, + terminal_borders_above: &HashSet<usize>, + ) -> BorderAndPaneIds { + let mut terminals = vec![]; + let terminal_to_check = self + .panes + .get(id) + .expect("terminal id does not exist") + .as_ref(); + let mut top_aligned_terminals = self.panes_top_aligned_with_pane(terminal_to_check); + // terminals that are next to each other up to current + top_aligned_terminals.sort_by_key(|a| Reverse(a.x())); + for terminal in top_aligned_terminals { + let terminal_to_check = terminals.last().unwrap_or(&terminal_to_check); + if terminal.x() + terminal.columns() + 1 == terminal_to_check.x() { + terminals.push(terminal); + } + } + // leftmost border aligned with a pane border above + let mut left_resize_border = 0; + for terminal in &terminals { + let right_terminal_boundary = terminal.x() + terminal.columns(); + if terminal_borders_above + .get(&(right_terminal_boundary + 1)) + .is_some() + && left_resize_border < right_terminal_boundary + { + left_resize_border = right_terminal_boundary + 1; + } + } + terminals.retain(|terminal| terminal.x() >= left_resize_border); + // if there are no adjacent panes to resize, we use the border of the main pane we're + // resizing + let left_resize_border = if terminals.is_empty() { + terminal_to_check.x() + } else { + left_resize_border + }; + let terminal_ids: Vec<PaneId> = terminals.iter().map(|t| t.pid()).collect(); + (left_resize_border, terminal_ids) + } + fn top_aligned_contiguous_panes_to_the_right( + &self, + id: &PaneId, + terminal_borders_above: &HashSet<usize>, + ) -> BorderAndPaneIds { + let mut terminals = vec![]; + let terminal_to_check = self.panes.get(id).unwrap().as_ref(); + let mut top_aligned_terminals = self.panes_top_aligned_with_pane(terminal_to_check); + // terminals that are next to each other up to current + top_aligned_terminals.sort_by_key(|a| a.x()); + for terminal in top_aligned_terminals { + let terminal_to_check = terminals.last().unwrap_or(&terminal_to_check); + if terminal.x() == terminal_to_check.x() + terminal_to_check.columns() + 1 { + terminals.push(terminal); + } + } + // rightmost border aligned with a pane border above + let mut right_resize_border = self.full_screen_ws.columns; + for terminal in &terminals { + let left_terminal_boundary = terminal.x(); + if terminal_borders_above + .get(&left_terminal_boundary) + .is_some() + && right_resize_border > left_terminal_boundary + { + right_resize_border = left_terminal_boundary; + } + } + terminals.retain(|terminal| terminal.x() + terminal.columns() <= right_resize_border); + // if there are no adjacent panes to resize, we use the border of the main pane we're + // resizing + let right_resize_border = if terminals.is_empty() { + terminal_to_check.x() + terminal_to_check.columns() + } else { + right_resize_border + }; + let terminal_ids: Vec<PaneId> = terminals.iter().map(|t| t.pid()).collect(); + (right_resize_border, terminal_ids) + } + fn bottom_aligned_contiguous_panes_to_the_left( + &self, + id: &PaneId, + terminal_borders_below: &HashSet<usize>, + ) -> BorderAndPaneIds { + let mut terminals = vec![]; + let terminal_to_check = self.panes.get(id).unwrap().as_ref(); + let mut bottom_aligned_terminals = self.panes_bottom_aligned_with_pane(terminal_to_check); + bottom_aligned_terminals.sort_by_key(|a| Reverse(a.x())); + // terminals that are next to each other up to current + for terminal in bottom_aligned_terminals { + let terminal_to_check = terminals.last().unwrap_or(&terminal_to_check); + if terminal.x() + terminal.columns() + 1 == terminal_to_check.x() { + terminals.push(terminal); + } + } + // leftmost border aligned with a pane border above + let mut left_resize_border = 0; + for terminal in &terminals { + let right_terminal_boundary = terminal.x() + terminal.columns(); + if terminal_borders_below + .get(&(right_terminal_boundary + 1)) + .is_some() + && left_resize_border < right_terminal_boundary + { + left_resize_border = right_terminal_boundary + 1; + } + } + terminals.retain(|terminal| terminal.x() >= left_resize_border); + // if there are no adjacent panes to resize, we use the border of the main pane we're + // resizing + let left_resize_border = if terminals.is_empty() { + terminal_to_check.x() + } else { + left_resize_border + }; + let terminal_ids: Vec<PaneId> = terminals.iter().map(|t| t.pid()).collect(); + (left_resize_border, terminal_ids) + } + fn bottom_aligned_contiguous_panes_to_the_right( + &self, + id: &PaneId, + terminal_borders_below: &HashSet<usize>, + ) -> BorderAndPaneIds { + let mut terminals = vec![]; + let terminal_to_check = self.panes.get(id).unwrap().as_ref(); + let mut bottom_aligned_terminals = self.panes_bottom_aligned_with_pane(terminal_to_check); + bottom_aligned_terminals.sort_by_key(|a| a.x()); + // terminals that are next to each other up to current + for terminal in bottom_aligned_terminals { + let terminal_to_check = terminals.last().unwrap_or(&terminal_to_check); + if terminal.x() == terminal_to_check.x() + terminal_to_check.columns() + 1 { + terminals.push(terminal); + } + } + // leftmost border aligned with a pane border above + let mut right_resize_border = self.full_screen_ws.columns; + for terminal in &terminals { + let left_terminal_boundary = terminal.x(); + if terminal_borders_below + .get(&left_terminal_boundary) + .is_some() + && right_resize_border > left_terminal_boundary + { + right_resize_border = left_terminal_boundary; + } + } + terminals.retain(|terminal| terminal.x() + terminal.columns() <= right_resize_border); + let right_resize_border = if terminals.is_empty() { + terminal_to_check.x() + terminal_to_check.columns() + } else { + right_resize_border + }; + let terminal_ids: Vec<PaneId> = terminals.iter().map(|t| t.pid()).collect(); + (right_resize_border, terminal_ids) + } + fn reduce_pane_height_down(&mut self, id: &PaneId, count: usize) { + let terminal = self.panes.get_mut(id).unwrap(); + terminal.reduce_height_down(count); + if let PaneId::Terminal(pid) = id { + self.os_api.set_terminal_size_using_fd( + *pid, + terminal.columns() as u16, + terminal.rows() as u16, + ); + } + } + fn reduce_pane_height_up(&mut self, id: &PaneId, count: usize) { + let terminal = self.panes.get_mut(id).unwrap(); + terminal.reduce_height_up(count); + if let PaneId::Terminal(pid) = id { + self.os_api.set_terminal_size_using_fd( + *pid, + terminal.columns() as u16, + terminal.rows() as u16, + ); + } + } + fn increase_pane_height_down(&mut self, id: &PaneId, count: usize) { + let terminal = self.panes.get_mut(id).unwrap(); + terminal.increase_height_down(count); + if let PaneId::Terminal(pid) = terminal.pid() { + self.os_api.set_terminal_size_using_fd( + pid, + terminal.columns() as u16, + terminal.rows() as u16, + ); + } + } + fn increase_pane_height_up(&mut self, id: &PaneId, count: usize) { + let terminal = self.panes.get_mut(id).unwrap(); + terminal.increase_height_up(count); + if let PaneId::Terminal(pid) = terminal.pid() { + self.os_api.set_terminal_size_using_fd( + pid, + terminal.columns() as u16, + terminal.rows() as u16, + ); + } + } + fn increase_pane_width_right(&mut self, id: &PaneId, count: usize) { + let terminal = self.panes.get_mut(id).unwrap(); + terminal.increase_width_right(count); + if let PaneId::Terminal(pid) = terminal.pid() { + self.os_api.set_terminal_size_using_fd( + pid, + terminal.columns() as u16, + terminal.rows() as u16, + ); + } + } + fn increase_pane_width_left(&mut self, id: &PaneId, count: usize) { + let terminal = self.panes.get_mut(id).unwrap(); + terminal.increase_width_left(count); + if let PaneId::Terminal(pid) = terminal.pid() { + self.os_api.set_terminal_size_using_fd( + pid, + terminal.columns() as u16, + terminal.rows() as u16, + ); + } + } + fn reduce_pane_width_right(&mut self, id: &PaneId, count: usize) { + let terminal = self.panes.get_mut(id).unwrap(); + terminal.reduce_width_right(count); + if let PaneId::Terminal(pid) = terminal.pid() { + self.os_api.set_terminal_size_using_fd( + pid, + terminal.columns() as u16, + terminal.rows() as u16, + ); + } + } + fn reduce_pane_width_left(&mut self, id: &PaneId, count: usize) { + let terminal = self.panes.get_mut(id).unwrap(); + terminal.reduce_width_left(count); + if let PaneId::Terminal(pid) = terminal.pid() { + self.os_api.set_terminal_size_using_fd( + pid, + terminal.columns() as u16, + terminal.rows() as u16, + ); + } + } + fn pane_is_between_vertical_borders( + &self, + id: &PaneId, + left_border_x: usize, + right_border_x: usize, + ) -> bool { + let terminal = self + .panes + .get(id) + .expect("could not find terminal to check between borders"); + terminal.x() >= left_border_x && terminal.x() + terminal.columns() <= right_border_x + } + fn pane_is_between_horizontal_borders( + &self, + id: &PaneId, + top_border_y: usize, + bottom_border_y: usize, + ) -> bool { + let terminal = self + .panes + .get(id) + .expect("could not find terminal to check between borders"); + terminal.y() >= top_border_y && terminal.y() + terminal.rows() <= bottom_border_y + } + fn reduce_pane_and_surroundings_up(&mut self, id: &PaneId, count: usize) { + let mut terminals_below = self + .pane_ids_directly_below(&id) + .expect("can't reduce pane size up if there are no terminals below"); + let terminal_borders_below: HashSet<usize> = terminals_below + .iter() + .map(|t| self.panes.get(t).unwrap().x()) + .collect(); + let (left_resize_border, terminals_to_the_left) = + self.bottom_aligned_contiguous_panes_to_the_left(&id, &terminal_borders_below); + let (right_resize_border, terminals_to_the_right) = + self.bottom_aligned_contiguous_panes_to_the_right(&id, &terminal_borders_below); + terminals_below.retain(|t| { + self.pane_is_between_vertical_borders(t, left_resize_border, right_resize_border) + }); + self.reduce_pane_height_up(&id, count); + for terminal_id in terminals_below { + self.increase_pane_height_up(&terminal_id, count); + } + for terminal_id in terminals_to_the_left + .iter() + .chain(terminals_to_the_right.iter()) + { + self.reduce_pane_height_up(&terminal_id, count); + } + } + fn reduce_pane_and_surroundings_down(&mut self, id: &PaneId, count: usize) { + let mut terminals_above = self + .pane_ids_directly_above(&id) + .expect("can't reduce pane size down if there are no terminals above"); + let terminal_borders_above: HashSet<usize> = terminals_above + .iter() + .map(|t| self.panes.get(t).unwrap().x()) + .collect(); + let (left_resize_border, terminals_to_the_left) = + self.top_aligned_contiguous_panes_to_the_left(&id, &terminal_borders_above); + let (right_resize_border, terminals_to_the_right) = + self.top_aligned_contiguous_panes_to_the_right(&id, &terminal_borders_above); + terminals_above.retain(|t| { + self.pane_is_between_vertical_borders(t, left_resize_border, right_resize_border) + }); + self.reduce_pane_height_down(&id, count); + for terminal_id in terminals_above { + self.increase_pane_height_down(&terminal_id, count); + } + for terminal_id in terminals_to_the_left + .iter() + .chain(terminals_to_the_right.iter()) + { + self.reduce_pane_height_down(&terminal_id, count); + } + } + fn reduce_pane_and_surroundings_right(&mut self, id: &PaneId, count: usize) { + let mut terminals_to_the_left = self + .pane_ids_directly_left_of(&id) + .expect("can't reduce pane size right if there are no terminals to the left"); + let terminal_borders_to_the_left: HashSet<usize> = terminals_to_the_left + .iter() + .map(|t| self.panes.get(t).unwrap().y()) + .collect(); + let (top_resize_border, terminals_above) = + self.left_aligned_contiguous_panes_above(&id, &terminal_borders_to_the_left); + let (bottom_resize_border, terminals_below) = + self.left_aligned_contiguous_panes_below(&id, &terminal_borders_to_the_left); + terminals_to_the_left.retain(|t| { + self.pane_is_between_horizontal_borders(t, top_resize_border, bottom_resize_border) + }); + self.reduce_pane_width_right(&id, count); + for terminal_id in terminals_to_the_left { + self.increase_pane_width_right(&terminal_id, count); + } + for terminal_id in terminals_above.iter().chain(terminals_below.iter()) { + self.reduce_pane_width_right(&terminal_id, count); + } + } + fn reduce_pane_and_surroundings_left(&mut self, id: &PaneId, count: usize) { + let mut terminals_to_the_right = self + .pane_ids_directly_right_of(&id) + .expect("can't reduce pane size left if there are no terminals to the right"); + let terminal_borders_to_the_right: HashSet<usize> = terminals_to_the_right + .iter() + .map(|t| self.panes.get(t).unwrap().y()) + .collect(); + let (top_resize_border, terminals_above) = + self.right_aligned_contiguous_panes_above(&id, &terminal_borders_to_the_right); + let (bottom_resize_border, terminals_below) = + self.right_aligned_contiguous_panes_below(&id, &terminal_borders_to_the_right); + terminals_to_the_right.retain(|t| { + self.pane_is_between_horizontal_borders(t, top_resize_border, bottom_resize_border) + }); + self.reduce_pane_width_left(&id, count); + for terminal_id in terminals_to_the_right { + self.increase_pane_width_left(&terminal_id, count); + } + for terminal_id in terminals_above.iter().chain(terminals_below.iter()) { + self.reduce_pane_width_left(&terminal_id, count); + } + } + fn increase_pane_and_surroundings_up(&mut self, id: &PaneId, count: usize) { + let mut terminals_above = self + .pane_ids_directly_above(&id) + .expect("can't increase pane size up if there are no terminals above"); + let terminal_borders_above: HashSet<usize> = terminals_above + .iter() + .map(|t| self.panes.get(t).unwrap().x()) + .collect(); + let (left_resize_border, terminals_to_the_left) = + self.top_aligned_contiguous_panes_to_the_left(&id, &terminal_borders_above); + let (right_resize_border, terminals_to_the_right) = + self.top_aligned_contiguous_panes_to_the_right(&id, &terminal_borders_above); + terminals_above.retain(|t| { + self.pane_is_between_vertical_borders(t, left_resize_border, right_resize_border) + }); + self.increase_pane_height_up(&id, count); + for terminal_id in terminals_above { + self.reduce_pane_height_up(&terminal_id, count); + } + for terminal_id in terminals_to_the_left + .iter() + .chain(terminals_to_the_right.iter()) + { + self.increase_pane_height_up(&terminal_id, count); + } + } + fn increase_pane_and_surroundings_down(&mut self, id: &PaneId, count: usize) { + let mut terminals_below = self + .pane_ids_directly_below(&id) + .expect("can't increase pane size down if there are no terminals below"); + let terminal_borders_below: HashSet<usize> = terminals_below + .iter() + .map(|t| self.panes.get(t).unwrap().x()) + .collect(); + let (left_resize_border, terminals_to_the_left) = + self.bottom_aligned_contiguous_panes_to_the_left(&id, &terminal_borders_below); + let (right_resize_border, terminals_to_the_right) = + self.bottom_aligned_contiguous_panes_to_the_right(&id, &terminal_borders_below); + terminals_below.retain(|t| { + self.pane_is_between_vertical_borders(t, left_resize_border, right_resize_border) + }); + self.increase_pane_height_down(&id, count); + for terminal_id in terminals_below { + self.reduce_pane_height_down(&terminal_id, count); + } + for terminal_id in terminals_to_the_left + .iter() + .chain(terminals_to_the_right.iter()) + { + self.increase_pane_height_down(&terminal_id, count); + } + } + fn increase_pane_and_surroundings_right(&mut self, id: &PaneId, count: usize) { + let mut terminals_to_the_right = self + .pane_ids_directly_right_of(&id) + .expect("can't increase pane size right if there are no terminals to the right"); + let terminal_borders_to_the_right: HashSet<usize> = terminals_to_the_right + .iter() + .map(|t| self.panes.get(t).unwrap().y()) + .collect(); + let (top_resize_border, terminals_above) = + self.right_aligned_contiguous_panes_above(&id, &terminal_borders_to_the_right); + let (bottom_resize_border, terminals_below) = + self.right_aligned_contiguous_panes_below(&id, &terminal_borders_to_the_right); + terminals_to_the_right.retain(|t| { + self.pane_is_between_horizontal_borders(t, top_resize_border, bottom_resize_border) + }); + self.increase_pane_width_right(&id, count); + for terminal_id in terminals_to_the_right { + self.reduce_pane_width_right(&terminal_id, count); + } + for terminal_id in terminals_above.iter().chain(terminals_below.iter()) { + self.increase_pane_width_right(&terminal_id, count); + } + } + fn increase_pane_and_surroundings_left(&mut self, id: &PaneId, count: usize) { + let mut terminals_to_the_left = self + .pane_ids_directly_left_of(&id) + .expect("can't increase pane size right if there are no terminals to the right"); + let terminal_borders_to_the_left: HashSet<usize> = terminals_to_the_left + .iter() + .map(|t| self.panes.get(t).unwrap().y()) + .collect(); + let (top_resize_border, terminals_above) = + self.left_aligned_contiguous_panes_above(&id, &terminal_borders_to_the_left); + let (bottom_resize_border, terminals_below) = + self.left_aligned_contiguous_panes_below(&id, &terminal_borders_to_the_left); + terminals_to_the_left.retain(|t| { + self.pane_is_between_horizontal_borders(t, top_resize_border, bottom_resize_border) + }); + self.increase_pane_width_left(&id, count); + for terminal_id in terminals_to_the_left { + self.reduce_pane_width_left(&terminal_id, count); + } + for terminal_id in terminals_above.iter().chain(terminals_below.iter()) { + self.increase_pane_width_left(&terminal_id, count); + } + } + fn panes_exist_above(&self, pane_id: &PaneId) -> bool { + let pane = self.panes.get(pane_id).expect("pane does not exist"); + pane.y() > 0 + } + fn panes_exist_below(&self, pane_id: &PaneId) -> bool { + let pane = self.panes.get(pane_id).expect("pane does not exist"); + pane.y() + pane.rows() < self.full_screen_ws.rows + } + fn panes_exist_to_the_right(&self, pane_id: &PaneId) -> bool { + let pane = self.panes.get(pane_id).expect("pane does not exist"); + pane.x() + pane.columns() < self.full_screen_ws.columns + } + fn panes_exist_to_the_left(&self, pane_id: &PaneId) -> bool { + let pane = self.panes.get(pane_id).expect("pane does not exist"); + pane.x() > 0 + } + pub fn resize_right(&mut self) { + // TODO: find out by how much we actually reduced and only reduce by that much + let count = 10; + if let Some(active_pane_id) = self.get_active_pane_id() { + if self.panes_exist_to_the_right(&active_pane_id) { + self.increase_pane_and_surroundings_right(&active_pane_id, count); + } else if self.panes_exist_to_the_left(&active_pane_id) { + self.reduce_pane_and_surroundings_right(&active_pane_id, count); + } + self.render(); + } + } + pub fn resize_left(&mut self) { + // TODO: find out by how much we actually reduced and only reduce by that much + let count = 10; + if let Some(active_pane_id) = self.get_active_pane_id() { + if self.panes_exist_to_the_right(&active_pane_id) { + self.reduce_pane_and_surroundings_left(&active_pane_id, count); + } else if self.panes_exist_to_the_left(&active_pane_id) { + self.increase_pane_and_surroundings_left(&active_pane_id, count); + } + self.render(); + } + } + pub fn resize_down(&mut self) { + // TODO: find out by how much we actually reduced and only reduce by that much + let count = 2; + if let Some(active_pane_id) = self.get_active_pane_id() { + if self.panes_exist_above(&active_pane_id) { + self.reduce_pane_and_surroundings_down(&active_pane_id, count); + } else if self.panes_exist_below(&active_pane_id) { + self.increase_pane_and_surroundings_down(&active_pane_id, count); + } + self.render(); + } + } + pub fn resize_up(&mut self) { + // TODO: find out by how much we actually reduced and only reduce by that much + let count = 2; + if let Some(active_pane_id) = self.get_active_pane_id() { + if self.panes_exist_above(&active_pane_id) { + self.increase_pane_and_surroundings_up(&active_pane_id, count); + } else if self.panes_exist_below(&active_pane_id) { + self.reduce_pane_and_surroundings_up(&active_pane_id, count); + } + self.render(); + } + } + pub fn move_focus(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + let active_terminal_id = self.get_active_pane_id().unwrap(); + let terminal_ids: Vec<PaneId> = self.get_selectable_panes().map(|(&pid, _)| pid).collect(); // TODO: better, no allocations + let first_terminal = terminal_ids.get(0).unwrap(); + let active_terminal_id_position = terminal_ids + .iter() + .position(|id| id == &active_terminal_id) + .unwrap(); + if let Some(next_terminal) = terminal_ids.get(active_terminal_id_position + 1) { + self.active_terminal = Some(*next_terminal); + } else { + self.active_terminal = Some(*first_terminal); + } + self.render(); + } + pub fn move_focus_left(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + let active_terminal = self.get_active_pane(); + if let Some(active) = active_terminal { + let terminals = self.get_selectable_panes(); + let next_index = terminals + .enumerate() + .filter(|(_, (_, c))| { + c.is_directly_left_of(active) && c.horizontally_overlaps_with(active) + }) + .max_by_key(|(_, (_, c))| c.get_horizontal_overlap_with(active)) + .map(|(_, (pid, _))| pid); + match next_index { + Some(&p) => { + self.active_terminal = Some(p); + } + None => { + self.active_terminal = Some(active.pid()); + } + } + } else { + self.active_terminal = Some(active_terminal.unwrap().pid()); + } + self.render(); + } + pub fn move_focus_down(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + let active_terminal = self.get_active_pane(); + if let Some(active) = active_terminal { + let terminals = self.get_selectable_panes(); + let next_index = terminals + .enumerate() + .filter(|(_, (_, c))| { + c.is_directly_below(active) && c.vertically_overlaps_with(active) + }) + .max_by_key(|(_, (_, c))| c.get_vertical_overlap_with(active)) + .map(|(_, (pid, _))| pid); + match next_index { + Some(&p) => { + self.active_terminal = Some(p); + } + None => { + self.active_terminal = Some(active.pid()); + } + } + } else { + self.active_terminal = Some(active_terminal.unwrap().pid()); + } + self.render(); + } + pub fn move_focus_up(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + let active_terminal = self.get_active_pane(); + if let Some(active) = active_terminal { + let terminals = self.get_selectable_panes(); + let next_index = terminals + .enumerate() + .filter(|(_, (_, c))| { + c.is_directly_above(active) && c.vertically_overlaps_with(active) + }) + .max_by_key(|(_, (_, c))| c.get_vertical_overlap_with(active)) + .map(|(_, (pid, _))| pid); + match next_index { + Some(&p) => { + self.active_terminal = Some(p); + } + None => { + self.active_terminal = Some(active.pid()); + } + } + } else { + self.active_terminal = Some(active_terminal.unwrap().pid()); + } + self.render(); + } + pub fn move_focus_right(&mut self) { + if !self.has_selectable_panes() { + return; + } + if self.fullscreen_is_active { + return; + } + let active_terminal = self.get_active_pane(); + if let Some(active) = active_terminal { + let terminals = self.get_selectable_panes(); + let next_index = terminals + .enumerate() + .filter(|(_, (_, c))| { + c.is_directly_right_of(active) && c.horizontally_overlaps_with(active) + }) + .max_by_key(|(_, (_, c))| c.get_horizontal_overlap_with(active)) + .map(|(_, (pid, _))| pid); + match next_index { + Some(&p) => { + self.active_terminal = Some(p); + } + None => { + self.active_terminal = Some(active.pid()); + } + } + } else { + self.active_terminal = Some(active_terminal.unwrap().pid()); + } + self.render(); + } + fn horizontal_borders(&self, terminals: &[PaneId]) -> HashSet<usize> { + terminals.iter().fold(HashSet::new(), |mut borders, t| { + let terminal = self.panes.get(t).unwrap(); + borders.insert(terminal.y()); + borders.insert(terminal.y() + terminal.rows() + 1); // 1 for the border width + borders + }) + } + fn vertical_borders(&self, terminals: &[PaneId]) -> HashSet<usize> { + terminals.iter().fold(HashSet::new(), |mut borders, t| { + let terminal = self.panes.get(t).unwrap(); + borders.insert(terminal.x()); + borders.insert(terminal.x() + terminal.columns() + 1); // 1 for the border width + borders + }) + } + fn panes_to_the_left_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> { + if let Some(terminal) = self.panes.get(&id) { + let upper_close_border = terminal.y(); + let lower_close_border = terminal.y() + terminal.rows() + 1; + + if let Some(mut terminals_to_the_left) = self.pane_ids_directly_left_of(&id) { + let terminal_borders_to_the_left = self.horizontal_borders(&terminals_to_the_left); + if terminal_borders_to_the_left.contains(&upper_close_border) + && terminal_borders_to_the_left.contains(&lower_close_border) + { + terminals_to_the_left.retain(|t| { + self.pane_is_between_horizontal_borders( + t, + upper_close_border, + lower_close_border, + ) + }); + return Some(terminals_to_the_left); + } + } + } + None + } + fn panes_to_the_right_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> { + if let Some(terminal) = self.panes.get(&id) { + let upper_close_border = terminal.y(); + let lower_close_border = terminal.y() + terminal.rows() + 1; + + if let Some(mut terminals_to_the_right) = self.pane_ids_directly_right_of(&id) { + let terminal_borders_to_the_right = + self.horizontal_borders(&terminals_to_the_right); + if terminal_borders_to_the_right.contains(&upper_close_border) + && terminal_borders_to_the_right.contains(&lower_close_border) + { + terminals_to_the_right.retain(|t| { + self.pane_is_between_horizontal_borders( + t, + upper_close_border, + lower_close_border, + ) + }); + return Some(terminals_to_the_right); + } + } + } + None + } + fn panes_above_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> { + if let Some(terminal) = self.panes.get(&id) { + let left_close_border = terminal.x(); + let right_close_border = terminal.x() + terminal.columns() + 1; + + if let Some(mut terminals_above) = self.pane_ids_directly_above(&id) { + let terminal_borders_above = self.vertical_borders(&terminals_above); + if terminal_borders_above.contains(&left_close_border) + && terminal_borders_above.contains(&right_close_border) + { + terminals_above.retain(|t| { + self.pane_is_between_vertical_borders( + t, + left_close_border, + right_close_border, + ) + }); + return Some(terminals_above); + } + } + } + None + } + fn panes_below_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> { + if let Some(terminal) = self.panes.get(&id) { + let left_close_border = terminal.x(); + let right_close_border = terminal.x() + terminal.columns() + 1; + + if let Some(mut terminals_below) = self.pane_ids_directly_below(&id) { + let terminal_borders_below = self.vertical_borders(&terminals_below); + if terminal_borders_below.contains(&left_close_border) + && terminal_borders_below.contains(&right_close_border) + { + terminals_below.retain(|t| { + self.pane_is_between_vertical_borders( + t, + left_close_border, + right_close_border, + ) + }); + return Some(terminals_below); + } + } + } + None + } + fn close_down_to_max_terminals(&mut self) { + if let Some(max_panes) = self.max_panes { + let terminals = self.get_pane_ids(); + for &pid in terminals.iter().skip(max_panes - 1) { + self.send_pty_instructions + .send(PtyInstruction::ClosePane(pid)) + .unwrap(); + self.close_pane_without_rerender(pid); + } + } + } + pub fn get_pane_ids(&self) -> Vec<PaneId> { + self.get_panes().map(|(&pid, _)| pid).collect() + } + pub fn set_pane_selectable(&mut self, id: PaneId, selectable: bool) { + if let Some(pane) = self.panes.get_mut(&id) { + pane.set_selectable(selectable); + if self.get_active_pane_id() == Some(id) && !selectable { + self.active_terminal = self.next_active_pane(self.get_pane_ids()) + } + } + } + pub fn close_pane(&mut self, id: PaneId) { + if self.panes.get(&id).is_some() { + self.close_pane_without_rerender(id); + } + } + pub fn close_pane_without_rerender(&mut self, id: PaneId) { + if let Some(terminal_to_close) = self.panes.get(&id) { + let terminal_to_close_width = terminal_to_close.columns(); + let terminal_to_close_height = terminal_to_close.rows(); + if let Some(terminals) = self.panes_to_the_left_between_aligning_borders(id) { + for terminal_id in terminals.iter() { + self.increase_pane_width_right(&terminal_id, terminal_to_close_width + 1); + // 1 for the border + } + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(terminals); + } + } else if let Some(terminals) = self.panes_to_the_right_between_aligning_borders(id) { + for terminal_id in terminals.iter() { + self.increase_pane_width_left(&terminal_id, terminal_to_close_width + 1); + // 1 for the border + } + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(terminals); + } + } else if let Some(terminals) = self.panes_above_between_aligning_borders(id) { + for terminal_id in terminals.iter() { + self.increase_pane_height_down(&terminal_id, terminal_to_close_height + 1); + // 1 for the border + } + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(terminals); + } + } else if let Some(terminals) = self.panes_below_between_aligning_borders(id) { + for terminal_id in terminals.iter() { + self.increase_pane_height_up(&terminal_id, terminal_to_close_height + 1); + // 1 for the border + } + if self.active_terminal == Some(id) { + self.active_terminal = self.next_active_pane(terminals); + } + } else { + } + self.panes.remove(&id); + if self.active_terminal.is_none() { + self.active_terminal = self.next_active_pane(self.get_pane_ids()); + } + } + } + pub fn close_focused_pane(&mut self) { + if let Some(active_pane_id) = self.get_active_pane_id() { + self.close_pane(active_pane_id); + self.send_pty_instructions + .send(PtyInstruction::ClosePane(active_pane_id)) + .unwrap(); + } + } + pub fn scroll_active_terminal_up(&mut self) { + if let Some(active_terminal_id) = self.get_active_terminal_id() { + let active_terminal = self + .panes + .get_mut(&PaneId::Terminal(active_terminal_id)) + .unwrap(); + active_terminal.scroll_up(1); + self.render(); + } + } + pub fn scroll_active_terminal_down(&mut self) { + if let Some(active_terminal_id) = self.get_active_terminal_id() { + let active_terminal = self + .panes + .get_mut(&PaneId::Terminal(active_terminal_id)) + .unwrap(); + active_terminal.scroll_down(1); + self.render(); + } + } + pub fn clear_active_terminal_scroll(&mut self) { + if let Some(active_terminal_id) = self.get_active_terminal_id() { + let active_terminal = self + .panes + .get_mut(&PaneId::Terminal(active_terminal_id)) + .unwrap(); + active_terminal.clear_scroll(); + } + } +} |
