aboutsummaryrefslogtreecommitdiffstats
path: root/src/client
diff options
context:
space:
mode:
authordenis <denismaximov98@gmail.com>2021-01-24 18:38:46 +0200
committerdenis <denismaximov98@gmail.com>2021-01-24 18:38:46 +0200
commitb23ef332c7531b8e9bb202bd25e7f37fd157c22c (patch)
tree2ad09a6173e8528a91c37609e00c59d9b8a0b04b /src/client
parentMerge branch 'main' into server-client (diff)
downloadsl0ppij-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.rs472
-rw-r--r--src/client/layout.rs220
-rw-r--r--src/client/mod.rs9
-rw-r--r--src/client/panes/grid.rs820
-rw-r--r--src/client/panes/mod.rs9
-rw-r--r--src/client/panes/plugin_pane.rs175
-rw-r--r--src/client/panes/terminal_character.rs572
-rw-r--r--src/client/panes/terminal_pane.rs664
-rw-r--r--src/client/tab.rs1798
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. "")
+ 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(&params, &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(&params[..], bell_terminated);
+ }
+ VteEvent::CsiDispatch(params, intermediates, ignore, c) => {
+ self.csi_dispatch(&params, &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();
+ }
+ }
+}