summaryrefslogtreecommitdiffstats
path: root/zellij-utils/src/input/layout.rs
diff options
context:
space:
mode:
Diffstat (limited to 'zellij-utils/src/input/layout.rs')
-rw-r--r--zellij-utils/src/input/layout.rs375
1 files changed, 327 insertions, 48 deletions
diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs
index eaa2e3950..a0d61b241 100644
--- a/zellij-utils/src/input/layout.rs
+++ b/zellij-utils/src/input/layout.rs
@@ -22,6 +22,7 @@ use std::str::FromStr;
use super::plugins::{PluginTag, PluginsConfigError};
use serde::{Deserialize, Serialize};
+use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::vec::Vec;
use std::{
@@ -169,6 +170,21 @@ impl Run {
}
}
}
+ pub fn is_same_category(first: &Option<Run>, second: &Option<Run>) -> bool {
+ match (first, second) {
+ (Some(Run::Plugin(..)), Some(Run::Plugin(..))) => true,
+ (Some(Run::Command(..)), Some(Run::Command(..))) => true,
+ (Some(Run::EditFile(..)), Some(Run::EditFile(..))) => true,
+ (Some(Run::Cwd(..)), Some(Run::Cwd(..))) => true,
+ _ => false,
+ }
+ }
+ pub fn is_terminal(run: &Option<Run>) -> bool {
+ match run {
+ Some(Run::Command(..)) | Some(Run::EditFile(..)) | Some(Run::Cwd(..)) | None => true,
+ _ => false,
+ }
+ }
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
@@ -211,12 +227,27 @@ impl fmt::Display for RunPluginLocation {
}
}
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
+pub enum LayoutConstraint {
+ MaxPanes(usize),
+ MinPanes(usize),
+ NoConstraint,
+}
+
+pub type SwapTiledLayout = (BTreeMap<LayoutConstraint, TiledPaneLayout>, Option<String>); // Option<String> is the swap layout name
+pub type SwapFloatingLayout = (
+ BTreeMap<LayoutConstraint, Vec<FloatingPaneLayout>>,
+ Option<String>,
+); // Option<String> is the swap layout name
+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
pub struct Layout {
- pub tabs: Vec<(Option<String>, PaneLayout, Vec<FloatingPanesLayout>)>,
+ pub tabs: Vec<(Option<String>, TiledPaneLayout, Vec<FloatingPaneLayout>)>,
pub focused_tab_index: Option<usize>,
- pub template: Option<PaneLayout>,
- pub floating_panes_template: Vec<FloatingPanesLayout>,
+ pub template: Option<(TiledPaneLayout, Vec<FloatingPaneLayout>)>,
+ pub swap_layouts: Vec<(TiledPaneLayout, Vec<FloatingPaneLayout>)>,
+ pub swap_tiled_layouts: Vec<SwapTiledLayout>,
+ pub swap_floating_layouts: Vec<SwapFloatingLayout>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
@@ -270,8 +301,7 @@ impl FromStr for PercentOrFixed {
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
-pub struct FloatingPanesLayout {
- // TODO: change name to singular
+pub struct FloatingPaneLayout {
pub name: Option<String>,
pub height: Option<PercentOrFixed>,
pub width: Option<PercentOrFixed>,
@@ -281,7 +311,7 @@ pub struct FloatingPanesLayout {
pub focus: Option<bool>,
}
-impl FloatingPanesLayout {
+impl FloatingPaneLayout {
pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) {
match self.run.as_mut() {
Some(run) => run.add_cwd(cwd),
@@ -292,9 +322,9 @@ impl FloatingPanesLayout {
}
}
-impl From<&PaneLayout> for FloatingPanesLayout {
- fn from(pane_layout: &PaneLayout) -> Self {
- FloatingPanesLayout {
+impl From<&TiledPaneLayout> for FloatingPaneLayout {
+ fn from(pane_layout: &TiledPaneLayout) -> Self {
+ FloatingPaneLayout {
name: pane_layout.name.clone(),
run: pane_layout.run.clone(),
focus: pane_layout.focus,
@@ -304,21 +334,22 @@ impl From<&PaneLayout> for FloatingPanesLayout {
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
-pub struct PaneLayout {
+pub struct TiledPaneLayout {
pub children_split_direction: SplitDirection,
pub name: Option<String>,
- pub children: Vec<PaneLayout>,
+ pub children: Vec<TiledPaneLayout>,
pub split_size: Option<SplitSize>,
pub run: Option<Run>,
pub borderless: bool,
pub focus: Option<bool>,
pub external_children_index: Option<usize>,
+ pub children_are_stacked: bool,
}
-impl PaneLayout {
+impl TiledPaneLayout {
pub fn insert_children_layout(
&mut self,
- children_layout: &mut PaneLayout,
+ children_layout: &mut TiledPaneLayout,
) -> Result<bool, ConfigError> {
// returns true if successfully inserted and false otherwise
match self.external_children_index {
@@ -338,6 +369,30 @@ impl PaneLayout {
},
}
}
+ pub fn insert_children_nodes(
+ &mut self,
+ children_nodes: &mut Vec<TiledPaneLayout>,
+ ) -> Result<bool, ConfigError> {
+ // returns true if successfully inserted and false otherwise
+ match self.external_children_index {
+ Some(external_children_index) => {
+ children_nodes.reverse();
+ for child_node in children_nodes.drain(..) {
+ self.children.insert(external_children_index, child_node);
+ }
+ self.external_children_index = None;
+ Ok(true)
+ },
+ None => {
+ for pane in self.children.iter_mut() {
+ if pane.insert_children_nodes(children_nodes)? {
+ return Ok(true);
+ }
+ }
+ Ok(false)
+ },
+ }
+ }
pub fn children_block_count(&self) -> usize {
let mut count = 0;
if self.external_children_index.is_some() {
@@ -348,11 +403,50 @@ impl PaneLayout {
}
count
}
+ pub fn pane_count(&self) -> usize {
+ if self.children.is_empty() {
+ 1 // self
+ } else {
+ let mut pane_count = 0;
+ for child in &self.children {
+ pane_count += child.pane_count();
+ }
+ pane_count
+ }
+ }
pub fn position_panes_in_space(
&self,
space: &PaneGeom,
- ) -> Result<Vec<(PaneLayout, PaneGeom)>, &'static str> {
- let layouts = split_space(space, self, space);
+ max_panes: Option<usize>,
+ ) -> Result<Vec<(TiledPaneLayout, PaneGeom)>, &'static str> {
+ let layouts = match max_panes {
+ Some(max_panes) => {
+ let mut layout_to_split = self.clone();
+ let pane_count_in_layout = layout_to_split.pane_count();
+ if max_panes > pane_count_in_layout {
+ // the + 1 here is because this was previously an "actual" pane and will now
+ // become just a container, so we need to account for it too
+ // TODO: make sure this works when the `children` node has sibling nodes,
+ // because we really should support that
+ let children_count = (max_panes - pane_count_in_layout) + 1;
+ let mut extra_children = vec![TiledPaneLayout::default(); children_count];
+ if !layout_to_split.has_focused_node() {
+ if let Some(last_child) = extra_children.last_mut() {
+ last_child.focus = Some(true);
+ }
+ }
+ let _ = layout_to_split.insert_children_nodes(&mut extra_children);
+ } else {
+ layout_to_split.truncate(max_panes);
+ }
+ if !layout_to_split.has_focused_node() {
+ layout_to_split.focus_deepest_pane();
+ }
+
+ split_space(space, &layout_to_split, space)?
+ },
+ None => split_space(space, self, space)?,
+ };
for (_pane_layout, pane_geom) in layouts.iter() {
if !pane_geom.is_at_least_minimum_size() {
return Err("No room on screen for this layout!");
@@ -374,8 +468,8 @@ impl PaneLayout {
run_instructions
}
pub fn with_one_pane() -> Self {
- let mut default_layout = PaneLayout::default();
- default_layout.children = vec![PaneLayout::default()];
+ let mut default_layout = TiledPaneLayout::default();
+ default_layout.children = vec![TiledPaneLayout::default()];
default_layout
}
pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) {
@@ -389,6 +483,79 @@ impl PaneLayout {
child.add_cwd_to_layout(cwd);
}
}
+ pub fn deepest_depth(&self) -> usize {
+ let mut deepest_child_depth = 0;
+ for child in self.children.iter() {
+ let child_deepest_depth = child.deepest_depth();
+ if child_deepest_depth > deepest_child_depth {
+ deepest_child_depth = child_deepest_depth;
+ }
+ }
+ deepest_child_depth + 1
+ }
+ pub fn focus_deepest_pane(&mut self) {
+ let mut deepest_child_index = None;
+ let mut deepest_path = 0;
+ for (i, child) in self.children.iter().enumerate() {
+ let child_deepest_path = child.deepest_depth();
+ if child_deepest_path >= deepest_path {
+ deepest_path = child_deepest_path;
+ deepest_child_index = Some(i)
+ }
+ }
+ match deepest_child_index {
+ Some(deepest_child_index) => {
+ if let Some(child) = self.children.get_mut(deepest_child_index) {
+ child.focus_deepest_pane();
+ }
+ },
+ None => {
+ self.focus = Some(true);
+ },
+ }
+ }
+ pub fn truncate(&mut self, max_panes: usize) -> usize {
+ // returns remaining children length
+ // if max_panes is 1, it means there's only enough panes for this node,
+ // if max_panes is 0, this is probably the root layout being called with 0 max panes
+ if max_panes <= 1 {
+ self.children.clear();
+ } else if max_panes <= self.children.len() {
+ self.children.truncate(max_panes);
+ self.children.iter_mut().for_each(|l| l.children.clear());
+ } else {
+ let mut remaining_panes = max_panes
+ - self
+ .children
+ .iter()
+ .filter(|c| c.children.is_empty())
+ .count();
+ for child in self.children.iter_mut() {
+ if remaining_panes > 1 && child.children.len() > 0 {
+ remaining_panes =
+ remaining_panes.saturating_sub(child.truncate(remaining_panes));
+ } else {
+ child.children.clear();
+ }
+ }
+ }
+ if self.children.len() > 0 {
+ self.children.len()
+ } else {
+ 1 // just me
+ }
+ }
+ pub fn has_focused_node(&self) -> bool {
+ if self.focus.map(|f| f).unwrap_or(false) {
+ return true;
+ };
+ for child in &self.children {
+ if child.has_focused_node() {
+ return true;
+ }
+ }
+ false
+ }
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
@@ -429,8 +596,8 @@ impl Layout {
pub fn stringified_from_path_or_default(
layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>,
- ) -> Result<(String, String), ConfigError> {
- // (path_to_layout as String, stringified_layout)
+ ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
+ // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
match layout_path {
Some(layout_path) => {
// The way we determine where to look for the layout is similar to
@@ -455,24 +622,32 @@ impl Layout {
layout_dir: Option<PathBuf>,
config: Config,
) -> Result<(Layout, Config), ConfigError> {
- let (path_to_raw_layout, raw_layout) =
+ let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
Layout::stringified_from_path_or_default(layout_path, layout_dir)?;
- let layout = Layout::from_kdl(&raw_layout, path_to_raw_layout, None)?;
+ let layout = Layout::from_kdl(
+ &raw_layout,
+ path_to_raw_layout,
+ raw_swap_layouts
+ .as_ref()
+ .map(|(r, f)| (r.as_str(), f.as_str())),
+ None,
+ )?;
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
Ok((layout, config))
}
pub fn from_str(
raw: &str,
path_to_raw_layout: String,
+ swap_layouts: Option<(&str, &str)>, // Option<path_to_swap_layout, stringified_swap_layout>
cwd: Option<PathBuf>,
) -> Result<Layout, ConfigError> {
- Layout::from_kdl(raw, path_to_raw_layout, cwd)
+ Layout::from_kdl(raw, path_to_raw_layout, swap_layouts, cwd)
}
pub fn stringified_from_dir(
layout: &PathBuf,
layout_dir: Option<&PathBuf>,
- ) -> Result<(String, String), ConfigError> {
- // (path_to_layout as String, stringified_layout)
+ ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
+ // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
match layout_dir {
Some(dir) => {
let layout_path = &dir.join(layout);
@@ -485,18 +660,28 @@ impl Layout {
None => Layout::stringified_from_default_assets(layout),
}
}
- pub fn stringified_from_path(layout_path: &Path) -> Result<(String, String), ConfigError> {
- // (path_to_layout as String, stringified_layout)
+ pub fn stringified_from_path(
+ layout_path: &Path,
+ ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
+ // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
let mut layout_file = File::open(&layout_path)
.or_else(|_| File::open(&layout_path.with_extension("kdl")))
.map_err(|e| ConfigError::IoPath(e, layout_path.into()))?;
+ let swap_layout_and_path = Layout::swap_layout_and_path(&layout_path);
+
let mut kdl_layout = String::new();
layout_file.read_to_string(&mut kdl_layout)?;
- Ok((layout_path.as_os_str().to_string_lossy().into(), kdl_layout))
- }
- pub fn stringified_from_default_assets(path: &Path) -> Result<(String, String), ConfigError> {
- // (path_to_layout as String, stringified_layout)
+ Ok((
+ layout_path.as_os_str().to_string_lossy().into(),
+ kdl_layout,
+ swap_layout_and_path,
+ ))
+ }
+ pub fn stringified_from_default_assets(
+ path: &Path,
+ ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
+ // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
// TODO: ideally these should not be hard-coded
// we should load layouts by name from the config
// and load them from a hashmap or some such
@@ -504,18 +689,31 @@ impl Layout {
Some("default") => Ok((
"Default layout".into(),
Self::stringified_default_from_assets()?,
+ Some((
+ "Default swap layout".into(),
+ Self::stringified_default_swap_from_assets()?,
+ )),
)),
Some("strider") => Ok((
"Strider layout".into(),
Self::stringified_strider_from_assets()?,
+ Some((
+ "Strider swap layout".into(),
+ Self::stringified_strider_swap_from_assets()?,
+ )),
)),
Some("disable-status-bar") => Ok((
"Disable Status Bar layout".into(),
Self::stringified_disable_status_from_assets()?,
+ None,
)),
Some("compact") => Ok((
"Compact layout".into(),
Self::stringified_compact_from_assets()?,
+ Some((
+ "Compact layout swap".into(),
+ Self::stringified_compact_swap_from_assets()?,
+ )),
)),
None | Some(_) => Err(ConfigError::IoPath(
std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"),
@@ -526,10 +724,15 @@ impl Layout {
pub fn stringified_default_from_assets() -> Result<String, ConfigError> {
Ok(String::from_utf8(setup::DEFAULT_LAYOUT.to_vec())?)
}
-
+ pub fn stringified_default_swap_from_assets() -> Result<String, ConfigError> {
+ Ok(String::from_utf8(setup::DEFAULT_SWAP_LAYOUT.to_vec())?)
+ }
pub fn stringified_strider_from_assets() -> Result<String, ConfigError> {
Ok(String::from_utf8(setup::STRIDER_LAYOUT.to_vec())?)
}
+ pub fn stringified_strider_swap_from_assets() -> Result<String, ConfigError> {
+ Ok(String::from_utf8(setup::STRIDER_SWAP_LAYOUT.to_vec())?)
+ }
pub fn stringified_disable_status_from_assets() -> Result<String, ConfigError> {
Ok(String::from_utf8(setup::NO_STATUS_LAYOUT.to_vec())?)
@@ -539,12 +742,12 @@ impl Layout {
Ok(String::from_utf8(setup::COMPACT_BAR_LAYOUT.to_vec())?)
}
- pub fn new_tab(&self) -> (PaneLayout, Vec<FloatingPanesLayout>) {
- let template = match &self.template {
- Some(template) => template.clone(),
- None => PaneLayout::default(),
- };
- (template, self.floating_panes_template.clone())
+ pub fn stringified_compact_swap_from_assets() -> Result<String, ConfigError> {
+ Ok(String::from_utf8(setup::COMPACT_BAR_SWAP_LAYOUT.to_vec())?)
+ }
+
+ pub fn new_tab(&self) -> (TiledPaneLayout, Vec<FloatingPaneLayout>) {
+ self.template.clone().unwrap_or_default()
}
pub fn is_empty(&self) -> bool {
@@ -555,7 +758,7 @@ impl Layout {
!self.tabs.is_empty()
}
- pub fn tabs(&self) -> Vec<(Option<String>, PaneLayout, Vec<FloatingPanesLayout>)> {
+ pub fn tabs(&self) -> Vec<(Option<String>, TiledPaneLayout, Vec<FloatingPaneLayout>)> {
// String is the tab name
self.tabs.clone()
}
@@ -563,16 +766,60 @@ impl Layout {
pub fn focused_tab_index(&self) -> Option<usize> {
self.focused_tab_index
}
+
+ fn swap_layout_and_path(path: &Path) -> Option<(String, String)> {
+ // Option<path, stringified_swap_layout>
+ let mut swap_layout_path = PathBuf::from(path);
+ swap_layout_path.set_extension("swap.kdl");
+ match File::open(&swap_layout_path) {
+ Ok(mut stringified_swap_layout_file) => {
+ let mut swap_kdl_layout = String::new();
+ match stringified_swap_layout_file.read_to_string(&mut swap_kdl_layout) {
+ Ok(..) => Some((
+ swap_layout_path.as_os_str().to_string_lossy().into(),
+ swap_kdl_layout,
+ )),
+ Err(e) => {
+ log::warn!(
+ "Failed to read swap layout file: {}. Error: {:?}",
+ swap_layout_path.as_os_str().to_string_lossy(),
+ e
+ );
+ None
+ },
+ }
+ },
+ Err(e) => {
+ log::warn!(
+ "Failed to read swap layout file: {}. Error: {:?}",
+ swap_layout_path.as_os_str().to_string_lossy(),
+ e
+ );
+ None
+ },
+ }
+ }
}
fn split_space(
space_to_split: &PaneGeom,
- layout: &PaneLayout,
+ layout: &TiledPaneLayout,
total_space_to_split: &PaneGeom,
-) -> Vec<(PaneLayout, PaneGeom)> {
+) -> Result<Vec<(TiledPaneLayout, PaneGeom)>, &'static str> {
let mut pane_positions = Vec::new();
- let sizes: Vec<Option<SplitSize>> =
- layout.children.iter().map(|part| part.split_size).collect();
+ let sizes: Vec<Option<SplitSize>> = if layout.children_are_stacked {
+ let mut sizes: Vec<Option<SplitSize>> = layout
+ .children
+ .iter()
+ .map(|_part| Some(SplitSize::Fixed(1)))
+ .collect();
+ if let Some(last_size) = sizes.last_mut() {
+ *last_size = None;
+ }
+ sizes
+ } else {
+ layout.children.iter().map(|part| part.split_size).collect()
+ };
let mut split_geom = Vec::new();
let (
@@ -595,7 +842,22 @@ fn split_space(
),
};
+ let min_size_for_panes = sizes.iter().fold(0, |acc, size| match size {
+ Some(SplitSize::Percent(_)) | None => acc + 1, // TODO: minimum height/width as relevant here
+ Some(SplitSize::Fixed(fixed)) => acc + fixed,
+ });
+ if min_size_for_panes > split_dimension_space.as_usize() {
+ return Err("Not enough room for panes"); // TODO: use error infra
+ }
+
let flex_parts = sizes.iter().filter(|s| s.is_none()).count();
+ let total_fixed_size = sizes.iter().fold(0, |acc, s| {
+ if let Some(SplitSize::Fixed(fixed)) = s {
+ acc + fixed
+ } else {
+ acc
+ }
+ });
let mut total_pane_size = 0;
for (&size, _part) in sizes.iter().zip(&*layout.children) {
@@ -617,7 +879,11 @@ fn split_space(
Dimension::percent(free_percent / flex_parts as f64)
},
};
- split_dimension.adjust_inner(total_split_dimension_space.as_usize());
+ split_dimension.adjust_inner(
+ total_split_dimension_space
+ .as_usize()
+ .saturating_sub(total_fixed_size),
+ );
total_pane_size += split_dimension.as_usize();
let geom = match layout.children_split_direction {
@@ -626,20 +892,22 @@ fn split_space(
y: space_to_split.y,
cols: split_dimension,
rows: inherited_dimension,
+ is_stacked: layout.children_are_stacked,
},
SplitDirection::Horizontal => PaneGeom {
x: space_to_split.x,
y: current_position,
cols: inherited_dimension,
rows: split_dimension,
+ is_stacked: layout.children_are_stacked,
},
};
split_geom.push(geom);
current_position += split_dimension.as_usize();
}
- // add extra space from rounding errors to the last pane
if total_pane_size < split_dimension_space.as_usize() {
+ // add extra space from rounding errors to the last pane
let increase_by = split_dimension_space.as_usize() - total_pane_size;
if let Some(last_geom) = split_geom.last_mut() {
match layout.children_split_direction {
@@ -647,21 +915,32 @@ fn split_space(
SplitDirection::Horizontal => last_geom.rows.increase_inner(increase_by),
}
}
+ } else if total_pane_size > split_dimension_space.as_usize() {
+ // remove extra space from rounding errors to the last pane
+ let decrease_by = total_pane_size - split_dimension_space.as_usize();
+ if let Some(last_geom) = split_geom.last_mut() {
+ match layout.children_split_direction {
+ SplitDirection::Vertical => last_geom.cols.decrease_inner(decrease_by),
+ SplitDirection::Horizontal => last_geom.rows.decrease_inner(decrease_by),
+ }
+ }
}
for (i, part) in layout.children.iter().enumerate() {
let part_position_and_size = split_geom.get(i).unwrap();
if !part.children.is_empty() {
let mut part_positions =
- split_space(part_position_and_size, part, total_space_to_split);
+ split_space(part_position_and_size, part, total_space_to_split)?;
pane_positions.append(&mut part_positions);
} else {
- pane_positions.push((part.clone(), *part_position_and_size));
+ let part = part.clone();
+ pane_positions.push((part, *part_position_and_size));
}
}
if pane_positions.is_empty() {
- pane_positions.push((layout.clone(), space_to_split.clone()));
+ let layout = layout.clone();
+ pane_positions.push((layout, space_to_split.clone()));
}
- pane_positions
+ Ok(pane_positions)
}
impl TryFrom<Url> for RunPluginLocation {