summaryrefslogtreecommitdiffstats
path: root/zellij-utils/src/input/layout.rs
diff options
context:
space:
mode:
authora-kenji <aks.kenji@protonmail.com>2021-05-31 21:47:32 +0200
committera-kenji <aks.kenji@protonmail.com>2021-06-14 21:55:17 +0200
commitcafcca0c57696968b609c4f4a8c7a6fe7f2909a8 (patch)
tree46adb670807caa3c715962cf50022004de00dd65 /zellij-utils/src/input/layout.rs
parent9cbe4107403e94b3545257c7b13acd7fdd2cf04c (diff)
Move `layout.rs` from `server` to `utils`
* give feedback on most errors in the layout-file
Diffstat (limited to 'zellij-utils/src/input/layout.rs')
-rw-r--r--zellij-utils/src/input/layout.rs266
1 files changed, 266 insertions, 0 deletions
diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs
new file mode 100644
index 000000000..ce562d2b9
--- /dev/null
+++ b/zellij-utils/src/input/layout.rs
@@ -0,0 +1,266 @@
+//! The layout system.
+// Layouts have been moved from [`zellij-server`] to
+// [`zellij-utils`] in order to provide more helpful
+// error messages to the user until a more general
+// logging system is in place.
+// In case there is a logging system in place evaluate,
+// if [`zellij-utils`], or [`zellij-server`] is a proper
+// place.
+// If plugins should be able to depend on the layout system
+// then [`zellij-utils`] could be a proper place.
+use crate::{input::config::ConfigError, pane_size::PositionAndSize};
+use crate::{serde, serde_yaml};
+
+use serde::{Deserialize, Serialize};
+use std::path::{Path, PathBuf};
+use std::{fs::File, io::prelude::*};
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(crate = "self::serde")]
+pub enum Direction {
+ Horizontal,
+ Vertical,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
+#[serde(crate = "self::serde")]
+pub 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 struct Layout {
+ pub direction: Direction,
+ #[serde(default)]
+ pub parts: Vec<Layout>,
+ pub split_size: Option<SplitSize>,
+ pub plugin: Option<PathBuf>,
+}
+
+type LayoutResult = Result<Layout, ConfigError>;
+
+impl Layout {
+ pub fn new(layout_path: &Path) -> LayoutResult {
+ let mut layout_file = File::open(&layout_path)
+ .or_else(|_| File::open(&layout_path.with_extension("yaml")))
+ .map_err(|e| ConfigError::IoPath(e, layout_path.into()))?;
+
+ let mut layout = String::new();
+ layout_file.read_to_string(&mut layout)?;
+ let layout: Layout = serde_yaml::from_str(&layout)?;
+ Ok(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) -> LayoutResult {
+ Self::new(&data_dir.join("layouts/").join(layout))
+ }
+
+ pub fn from_path_or_default(
+ layout: Option<&PathBuf>,
+ layout_path: Option<&PathBuf>,
+ data_dir: &Path,
+ ) -> Option<Layout> {
+ let layout_result = layout
+ .map(|p| Layout::from_dir(&p, &data_dir))
+ .or_else(|| layout_path.map(|p| Layout::new(&p)))
+ .or_else(|| {
+ Some(Layout::from_dir(
+ &std::path::PathBuf::from("default"),
+ &data_dir,
+ ))
+ });
+
+ match layout_result {
+ None => None,
+ Some(Ok(layout)) => Some(layout),
+ Some(Err(e)) => {
+ eprintln!("There was an error in the layout file:\n{}", e);
+ std::process::exit(1);
+ }
+ }
+ }
+
+ 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)
+ }
+}
+
+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, 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<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, 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<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
+}