use zellij_utils::{serde, serde_yaml}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use std::{fs::File, io::prelude::*}; use zellij_utils::pane_size::PositionAndSize; fn split_space_to_parts_vertically( space_to_split: &PositionAndSize, sizes: Vec>, ) -> Vec { 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, max_columns) = match size { Some(SplitSize::Percent(percent)) => { ((max_width as f32 * (percent as f32 / 100.0)) as usize, None) } // TODO: round properly Some(SplitSize::Fixed(size)) => (size as usize, Some(size as usize)), None => { parts_to_grow.push(current_x_position); ( 1, // This is grown later on None, ) } }; split_parts.push(PositionAndSize { x: current_x_position, y: space_to_split.y, columns, rows: space_to_split.rows, max_columns, ..Default::default() }); 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>, ) -> Vec { 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, max_rows) = match size { Some(SplitSize::Percent(percent)) => ( (max_height as f32 * (percent as f32 / 100.0)) as usize, None, ), // TODO: round properly Some(SplitSize::Fixed(size)) => (size as usize, Some(size as usize)), None => { parts_to_grow.push(current_y_position); ( 1, // This is grown later on None, ) } }; split_parts.push(PositionAndSize { x: space_to_split.x, y: current_y_position, columns: space_to_split.columns, rows, max_rows, ..Default::default() }); 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> = 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)] #[serde(crate = "self::serde")] pub(crate) enum Direction { Horizontal, Vertical, } #[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[serde(crate = "self::serde")] pub(crate) enum SplitSize { Percent(u8), // 1 to 100 Fixed(u16), // An absolute number of columns or rows } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(crate = "self::serde")] pub(crate) struct Layout { pub direction: Direction, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub parts: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub split_size: Option, #[serde(skip_serializing_if = "Option::is_none")] pub plugin: Option, } impl Layout { pub fn new(layout_path: &Path) -> Self { let mut layout_file = File::open(&layout_path) .or_else(|_| File::open(&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 } // It wants to use Path here, but that doesn't compile. #[allow(clippy::ptr_arg)] pub fn from_dir(layout: &PathBuf, data_dir: &Path) -> Self { Self::new(&data_dir.join("layouts/").join(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) } }