use std::collections::HashMap; use std::fs::File; use std::io::BufReader; use std::ops::Deref; use std::ops::DerefMut; use std::path::PathBuf; use color_eyre::eyre::anyhow; use color_eyre::Result; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use crate::Rect; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct CustomLayout(Vec); impl Deref for CustomLayout { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for CustomLayout { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl CustomLayout { pub fn from_path_buf(path: PathBuf) -> Result { let invalid_filetype = anyhow!("custom layouts must be json or yaml files"); let layout: Self = match path.extension() { Some(extension) => { if extension == "yaml" || extension == "yml" { serde_yaml::from_reader(BufReader::new(File::open(path)?))? } else if extension == "json" { serde_json::from_reader(BufReader::new(File::open(path)?))? } else { return Err(invalid_filetype); } } None => return Err(invalid_filetype), }; if !layout.is_valid() { return Err(anyhow!("the layout file provided was invalid")); } Ok(layout) } #[must_use] pub fn column_with_idx(&self, idx: usize) -> (usize, Option<&Column>) { let column_idx = self.column_for_container_idx(idx); let column = self.get(column_idx); (column_idx, column) } #[must_use] pub fn primary_idx(&self) -> Option { for (i, column) in self.iter().enumerate() { if let Column::Primary(_) = column { return Option::from(i); } } None } #[must_use] pub fn primary_width_percentage(&self) -> Option { for column in self.iter() { if let Column::Primary(Option::Some(ColumnWidth::WidthPercentage(percentage))) = column { return Option::from(*percentage); } } None } pub fn set_primary_width_percentage(&mut self, percentage: usize) { for column in self.iter_mut() { if let Column::Primary(Option::Some(ColumnWidth::WidthPercentage(current))) = column { *current = percentage; } } } #[must_use] pub fn is_valid(&self) -> bool { // A valid layout must have at least one column if self.is_empty() { return false; }; // Vertical column splits aren't supported at the moment for column in self.iter() { match column { Column::Tertiary(ColumnSplit::Vertical) | Column::Secondary(Some(ColumnSplitWithCapacity::Vertical(_))) => return false, _ => {} } } // The final column must not have a fixed capacity match self.last() { Some(Column::Tertiary(_)) => {} _ => return false, } let mut primaries = 0; let mut tertiaries = 0; for column in self.iter() { match column { Column::Primary(_) => primaries += 1, Column::Tertiary(_) => tertiaries += 1, Column::Secondary(_) => {} } } // There must only be one primary and one tertiary column matches!(primaries, 1) && matches!(tertiaries, 1) } pub(crate) fn column_container_counts(&self) -> HashMap { let mut count_map = HashMap::new(); for (idx, column) in self.iter().enumerate() { match column { Column::Primary(_) | Column::Secondary(None) => { count_map.insert(idx, 1); } Column::Secondary(Some(split)) => { count_map.insert( idx, match split { ColumnSplitWithCapacity::Vertical(n) | ColumnSplitWithCapacity::Horizontal(n) => *n, }, ); } Column::Tertiary(_) => {} } } count_map } #[must_use] pub fn first_container_idx(&self, col_idx: usize) -> usize { let count_map = self.column_container_counts(); let mut container_idx_accumulator = 0; for i in 0..col_idx { if let Some(n) = count_map.get(&i) { container_idx_accumulator += n; } } container_idx_accumulator } #[must_use] pub fn column_for_container_idx(&self, idx: usize) -> usize { let count_map = self.column_container_counts(); let mut container_idx_accumulator = 0; // always -1 because we don't insert the tertiary column in the count_map for i in 0..self.len() - 1 { if let Some(n) = count_map.get(&i) { container_idx_accumulator += n; // The accumulator becomes greater than the window container index // for the first time when we reach a column that contains that // window container index if container_idx_accumulator > idx { return i; } } } // If the accumulator never reaches a point where it is greater than the // window container index, then the only remaining possibility is the // final tertiary column self.len() - 1 } #[must_use] pub fn column_area(&self, work_area: &Rect, idx: usize, offset: Option) -> Rect { let divisor = offset.map_or_else(|| self.len(), |offset| self.len() - offset); #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] let equal_width = work_area.right / divisor as i32; let mut left = work_area.left; let right = equal_width; for _ in 0..idx { left += right; } Rect { left, top: work_area.top, right, bottom: work_area.bottom, } } #[must_use] pub fn column_area_with_last( len: usize, work_area: &Rect, primary_right: i32, last_column: Option, offset: Option, ) -> Rect { let divisor = offset.map_or_else(|| len - 1, |offset| len - offset - 1); #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] let equal_width = (work_area.right - primary_right) / divisor as i32; let left = last_column.map_or(work_area.left, |last| last.left + last.right); let right = equal_width; Rect { left, top: work_area.top, right, bottom: work_area.bottom, } } #[must_use] pub fn main_column_area( work_area: &Rect, primary_right: i32, last_column: Option, ) -> Rect { let left = last_column.map_or(work_area.left, |last| last.left + last.right); Rect { left, top: work_area.top, right: primary_right, bottom: work_area.bottom, } } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] #[serde(tag = "column", content = "configuration")] pub enum Column { Primary(Option), Secondary(Option), Tertiary(ColumnSplit), } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] pub enum ColumnWidth { WidthPercentage(usize), } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] pub enum ColumnSplit { Horizontal, Vertical, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] pub enum ColumnSplitWithCapacity { Horizontal(usize), Vertical(usize), }