summaryrefslogtreecommitdiffstats
path: root/zellij-utils/src/input
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2021-08-24 10:58:36 +0200
committerAram Drevekenin <aram@poor.dev>2021-08-24 10:58:36 +0200
commit618c2c376bb4edca8693ea4b871279bc04501559 (patch)
treeb4d4426b353955e8af1ed8ad6531f95860eda5f6 /zellij-utils/src/input
parent7a2f86db1b34b027ca03be73ec7bc4681f33fbdf (diff)
parent88b4063879845cf53397f60201473ce386280cfe (diff)
Merge branch 'tab-layout' of https://github.com/a-kenji/zellij into a-kenji-tab-layout
Diffstat (limited to 'zellij-utils/src/input')
-rw-r--r--zellij-utils/src/input/actions.rs7
-rw-r--r--zellij-utils/src/input/config.rs92
-rw-r--r--zellij-utils/src/input/layout.rs300
-rw-r--r--zellij-utils/src/input/unit/fixtures/layouts/deeply-nested-tab-layout.yaml43
-rw-r--r--zellij-utils/src/input/unit/fixtures/layouts/multiple-tabs-should-not-error.yaml18
-rw-r--r--zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-command.yaml35
-rw-r--r--zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-default-plugins.yaml31
-rw-r--r--zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab.yaml21
-rw-r--r--zellij-utils/src/input/unit/fixtures/layouts/three-tabs-merged-correctly.yaml29
-rw-r--r--zellij-utils/src/input/unit/keybinds_test.rs6
-rw-r--r--zellij-utils/src/input/unit/layout_test.rs685
11 files changed, 1241 insertions, 26 deletions
diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs
index 23c01eefe..cd4ca4641 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -1,6 +1,7 @@
//! Definition of the actions that can be bound to keys.
use super::command::RunCommandAction;
+use super::layout::TabLayout;
use crate::input::options::OnForceClose;
use serde::{Deserialize, Serialize};
use zellij_tile::data::InputMode;
@@ -19,7 +20,7 @@ pub enum Direction {
// As these actions are bound to the default config, please
// do take care when refactoring - or renaming.
// They might need to be adjusted in the default config
-// as well `../../../assets/config/default.yaml`
+// as well `../../assets/config/default.yaml`
/// Actions that can be bound to keys.
#[derive(Eq, Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum Action {
@@ -65,8 +66,8 @@ pub enum Action {
NewPane(Option<Direction>),
/// Close the focus pane.
CloseFocus,
- /// Create a new tab.
- NewTab,
+ /// Create a new tab, optionally with a specified tab layout.
+ NewTab(Option<TabLayout>),
/// Do nothing.
NoOp,
/// Go to the next tab.
diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs
index 6a65109a7..930fe07af 100644
--- a/zellij-utils/src/input/config.rs
+++ b/zellij-utils/src/input/config.rs
@@ -45,6 +45,9 @@ pub enum ConfigError {
IoPath(io::Error, PathBuf),
// Internal Deserialization Error
FromUtf8(std::string::FromUtf8Error),
+ // Missing the tab section in the layout.
+ Layout(LayoutMissingTabSectionError),
+ LayoutPartAndTab(LayoutPartAndTabError),
}
impl Default for Config {
@@ -129,6 +132,75 @@ impl Config {
}
}
+// TODO: Split errors up into separate modules
+#[derive(Debug, Clone)]
+pub struct LayoutMissingTabSectionError;
+#[derive(Debug, Clone)]
+pub struct LayoutPartAndTabError;
+
+impl fmt::Display for LayoutMissingTabSectionError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "MissingTabSectionError:
+There needs to be exactly one `tabs` section specified in the layout file, for example:
+---
+direction: Horizontal
+parts:
+ - direction: Vertical
+ - direction: Vertical
+ tabs:
+ - direction: Vertical
+ - direction: Vertical
+ - direction: Vertical
+"
+ )
+ }
+}
+
+impl std::error::Error for LayoutMissingTabSectionError {
+ fn description(&self) -> &str {
+ "One tab must be specified per Layout."
+ }
+}
+
+impl fmt::Display for LayoutPartAndTabError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "LayoutPartAndTabError:
+The `tabs` and `parts` section should not be specified on the same level in the layout file, for example:
+---
+direction: Horizontal
+parts:
+ - direction: Vertical
+ - direction: Vertical
+tabs:
+ - direction: Vertical
+ - direction: Vertical
+ - direction: Vertical
+
+should rather be specified as:
+---
+direction: Horizontal
+parts:
+ - direction: Vertical
+ - direction: Vertical
+ tabs:
+ - direction: Vertical
+ - direction: Vertical
+ - direction: Vertical
+"
+ )
+ }
+}
+
+impl std::error::Error for LayoutPartAndTabError {
+ fn description(&self) -> &str {
+ "The `tabs` and parts section should not be specified on the same level."
+ }
+}
+
impl Display for ConfigError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match self {
@@ -138,6 +210,12 @@ impl Display for ConfigError {
}
ConfigError::Serde(ref err) => write!(formatter, "Deserialization error: {}", err),
ConfigError::FromUtf8(ref err) => write!(formatter, "FromUtf8Error: {}", err),
+ ConfigError::Layout(ref err) => {
+ write!(formatter, "There was an error in the layout file, {}", err)
+ }
+ ConfigError::LayoutPartAndTab(ref err) => {
+ write!(formatter, "There was an error in the layout file, {}", err)
+ }
}
}
}
@@ -149,6 +227,8 @@ impl std::error::Error for ConfigError {
ConfigError::IoPath(ref err, _) => Some(err),
ConfigError::Serde(ref err) => Some(err),
ConfigError::FromUtf8(ref err) => Some(err),
+ ConfigError::Layout(ref err) => Some(err),
+ ConfigError::LayoutPartAndTab(ref err) => Some(err),
}
}
}
@@ -171,6 +251,18 @@ impl From<std::string::FromUtf8Error> for ConfigError {
}
}
+impl From<LayoutMissingTabSectionError> for ConfigError {
+ fn from(err: LayoutMissingTabSectionError) -> ConfigError {
+ ConfigError::Layout(err)
+ }
+}
+
+impl From<LayoutPartAndTabError> for ConfigError {
+ fn from(err: LayoutPartAndTabError) -> ConfigError {
+ ConfigError::LayoutPartAndTab(err)
+ }
+}
+
// The unit test location.
#[cfg(test)]
mod config_test {
diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs
index 33e08bfdb..96f6189c9 100644
--- a/zellij-utils/src/input/layout.rs
+++ b/zellij-utils/src/input/layout.rs
@@ -17,23 +17,26 @@ use crate::{serde, serde_yaml};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
+use std::vec::Vec;
use std::{fs::File, io::prelude::*};
-#[derive(Debug, Serialize, Deserialize, Clone)]
+use super::config::{LayoutMissingTabSectionError, LayoutPartAndTabError};
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(crate = "self::serde")]
pub enum Direction {
Horizontal,
Vertical,
}
-#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
#[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)]
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(crate = "self::serde")]
pub enum Run {
#[serde(rename = "plugin")]
@@ -42,35 +45,49 @@ pub enum Run {
Command(RunCommand),
}
-#[derive(Debug, Serialize, Deserialize, Clone)]
+// The layout struct ultimately used to build the layouts.
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(crate = "self::serde")]
pub struct Layout {
pub direction: Direction,
#[serde(default)]
pub parts: Vec<Layout>,
+ #[serde(default)]
+ pub tabs: Vec<TabLayout>,
pub split_size: Option<SplitSize>,
pub run: Option<Run>,
#[serde(default)]
pub borderless: bool,
}
-type LayoutResult = Result<Layout, ConfigError>;
+// The struct that is used to deserialize the layout from
+// a yaml configuration file
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
+#[serde(crate = "self::serde")]
+pub struct LayoutFromYaml {
+ //#[serde(default)]
+ pub template: LayoutTemplateFromYaml,
+ #[serde(default)]
+ pub tabs: Vec<TabLayout>,
+}
-impl Layout {
- pub fn new(layout_path: &Path) -> LayoutResult {
+type LayoutFromYamlResult = Result<LayoutFromYaml, ConfigError>;
+
+impl LayoutFromYaml {
+ pub fn new(layout_path: &Path) -> LayoutFromYamlResult {
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)?;
+ let layout: LayoutFromYaml = 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, layout_dir: Option<&PathBuf>) -> LayoutResult {
+ pub fn from_dir(layout: &PathBuf, layout_dir: Option<&PathBuf>) -> LayoutFromYamlResult {
match layout_dir {
Some(dir) => Self::new(&dir.join(layout))
.or_else(|_| Self::from_default_assets(layout.as_path())),
@@ -82,22 +99,31 @@ impl Layout {
layout: Option<&PathBuf>,
layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>,
- ) -> Option<Result<Layout, ConfigError>> {
+ ) -> Option<LayoutFromYamlResult> {
layout
- .map(|p| Layout::from_dir(p, layout_dir.as_ref()))
- .or_else(|| layout_path.map(|p| Layout::new(p)))
+ .map(|p| LayoutFromYaml::from_dir(p, layout_dir.as_ref()))
+ .or_else(|| layout_path.map(|p| LayoutFromYaml::new(p)))
.or_else(|| {
- Some(Layout::from_dir(
+ Some(LayoutFromYaml::from_dir(
&std::path::PathBuf::from("default"),
layout_dir.as_ref(),
))
})
}
+ pub fn construct_layout_template(&self) -> LayoutTemplate {
+ let (pre_tab, post_tab) = self.template.split_template().unwrap();
+ LayoutTemplate {
+ pre_tab: pre_tab.into(),
+ post_tab: Layout::from_vec_template_layout(post_tab),
+ tabs: self.tabs.clone(),
+ }
+ }
+
// Currently still needed but on nightly
// this is already possible:
// HashMap<&'static str, Vec<u8>>
- pub fn from_default_assets(path: &Path) -> LayoutResult {
+ pub fn from_default_assets(path: &Path) -> LayoutFromYamlResult {
match path.to_str() {
Some("default") => Self::default_from_assets(),
Some("strider") => Self::strider_from_assets(),
@@ -111,24 +137,123 @@ impl Layout {
// TODO Deserialize the assets from bytes &[u8],
// once serde-yaml supports zero-copy
- pub fn default_from_assets() -> LayoutResult {
- let layout: Layout =
+ pub fn default_from_assets() -> LayoutFromYamlResult {
+ let layout: LayoutFromYaml =
serde_yaml::from_str(String::from_utf8(setup::DEFAULT_LAYOUT.to_vec())?.as_str())?;
Ok(layout)
}
- pub fn strider_from_assets() -> LayoutResult {
- let layout: Layout =
+ pub fn strider_from_assets() -> LayoutFromYamlResult {
+ let layout: LayoutFromYaml =
serde_yaml::from_str(String::from_utf8(setup::STRIDER_LAYOUT.to_vec())?.as_str())?;
Ok(layout)
}
- pub fn disable_status_from_assets() -> LayoutResult {
- let layout: Layout =
+ pub fn disable_status_from_assets() -> LayoutFromYamlResult {
+ let layout: LayoutFromYaml =
serde_yaml::from_str(String::from_utf8(setup::NO_STATUS_LAYOUT.to_vec())?.as_str())?;
Ok(layout)
}
+}
+
+// The struct that carries the information template that is used to
+// construct the layout
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
+#[serde(crate = "self::serde")]
+pub struct LayoutTemplateFromYaml {
+ pub direction: Direction,
+ #[serde(default)]
+ pub parts: Vec<LayoutTemplateFromYaml>,
+ #[serde(default)]
+ pub body: bool,
+ pub split_size: Option<SplitSize>,
+ pub run: Option<Run>,
+}
+
+impl LayoutTemplateFromYaml {
+ // Split the layout into parts that can be reassebled per tab
+ // returns the layout pre tab and the parts post tab
+ pub fn split_template(
+ &self,
+ ) -> Result<(LayoutTemplateFromYaml, Vec<LayoutTemplateFromYaml>), LayoutPartAndTabError> {
+ let mut main_layout = self.clone();
+ let mut pre_tab_layout = self.clone();
+ let mut post_tab_layout = vec![];
+ let mut post_tab = false;
+
+ pre_tab_layout.parts.clear();
+
+ if main_layout.body {
+ post_tab = true;
+ }
+
+ for part in main_layout.parts.drain(..) {
+ let (curr_pre_layout, mut curr_post_layout) = part.split_template()?;
+
+ // Leaf
+ if !post_tab && !part.body {
+ pre_tab_layout.parts.push(curr_pre_layout);
+ }
+
+ // Node
+ if part.body {
+ post_tab = true;
+ // Leaf
+ } else if post_tab {
+ if curr_post_layout.is_empty() {
+ let mut part_no_tab = part.clone();
+ part_no_tab.parts.clear();
+ post_tab_layout.push(part_no_tab);
+ } else {
+ post_tab_layout.append(&mut curr_post_layout);
+ }
+ }
+ }
+ Ok((pre_tab_layout, post_tab_layout))
+ }
+}
+
+// The tab-layout struct used to specify each individual tab.
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
+#[serde(crate = "self::serde")]
+pub struct TabLayout {
+ pub direction: Direction,
+ #[serde(default)]
+ pub parts: Vec<TabLayout>,
+ pub split_size: Option<SplitSize>,
+ pub run: Option<Run>,
+}
+
+// Main template layout struct, that carries information based on position of
+// tabs in relation to the whole layout.
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
+#[serde(crate = "self::serde")]
+pub struct LayoutTemplate {
+ pub pre_tab: Layout,
+ pub post_tab: Vec<Layout>,
+ pub tabs: Vec<TabLayout>,
+}
+
+impl LayoutTemplate {
+ pub fn construct_tab_layout(&self, tab_layout: Option<TabLayout>) -> Layout {
+ if let Some(tab_layout) = tab_layout {
+ let mut pre_tab_layout = self.pre_tab.clone();
+ let post_tab_layout = &self.post_tab;
+ pre_tab_layout.merge_tab_layout(tab_layout);
+ pre_tab_layout.merge_layout_parts(post_tab_layout.to_owned());
+ pre_tab_layout
+ } else {
+ let mut pre_tab_layout = self.pre_tab.clone();
+ let post_tab_layout = &self.post_tab;
+ let default_tab_layout = TabLayout::default();
+ pre_tab_layout.merge_tab_layout(default_tab_layout);
+ pre_tab_layout.merge_layout_parts(post_tab_layout.to_owned());
+ pre_tab_layout
+ }
+ }
+}
+impl Layout {
pub fn total_terminal_panes(&self) -> usize {
let mut total_panes = 0;
total_panes += self.parts.len();
@@ -169,6 +294,101 @@ impl Layout {
) -> Vec<(Layout, PositionAndSize)> {
split_space(space, self)
}
+
+ // Split the layout into parts that can be reassebled per tab
+ // returns the layout pre tab, the parts post tab and the tab layouts
+ pub fn split_main_and_tab_layout(
+ &self,
+ ) -> Result<(Layout, Vec<Layout>, Vec<TabLayout>), LayoutPartAndTabError> {
+ let mut main_layout = self.clone();
+ let mut pre_tab_layout = self.clone();
+ let mut post_tab_layout = vec![];
+ let mut tabs = vec![];
+ let mut post_tab = false;
+
+ pre_tab_layout.parts.clear();
+ pre_tab_layout.tabs.clear();
+
+ if !main_layout.tabs.is_empty() {
+ tabs.append(&mut main_layout.tabs);
+ post_tab = true;
+ }
+
+ if !main_layout.tabs.is_empty() && !main_layout.parts.is_empty() {
+ return Err(LayoutPartAndTabError);
+ }
+
+ for part in main_layout.parts.drain(..) {
+ let (curr_pre_layout, mut curr_post_layout, mut curr_tabs) =
+ part.split_main_and_tab_layout()?;
+
+ // Leaf
+ if !post_tab && part.tabs.is_empty() {
+ pre_tab_layout.parts.push(curr_pre_layout);
+ }
+
+ if !part.tabs.is_empty() && !part.parts.is_empty() {
+ return Err(LayoutPartAndTabError);
+ }
+
+ // Node
+ if !part.tabs.is_empty() {
+ tabs.append(&mut part.tabs.clone());
+ post_tab = true;
+ // Node
+ } else if !curr_tabs.is_empty() {
+ tabs.append(&mut curr_tabs);
+ post_tab = true;
+ // Leaf
+ } else if post_tab {
+ if curr_post_layout.is_empty() {
+ let mut part_no_tab = part.clone();
+ part_no_tab.tabs.clear();
+ part_no_tab.parts.clear();
+ post_tab_layout.push(part_no_tab);
+ } else {
+ post_tab_layout.append(&mut curr_post_layout);
+ }
+ }
+ }
+ Ok((pre_tab_layout, post_tab_layout, tabs))
+ }
+
+ pub fn merge_tab_layout(&mut self, tab: TabLayout) {
+ self.parts.push(tab.into());
+ }
+
+ pub fn merge_layout_parts(&mut self, mut parts: Vec<Layout>) {
+ self.parts.append(&mut parts);
+ }
+
+ pub fn construct_layout_template(&self) -> Result<LayoutTemplate, ConfigError> {
+ let (pre_tab, post_tab, tabs) = self.split_main_and_tab_layout()?;
+
+ if tabs.is_empty() {
+ return Err(ConfigError::Layout(LayoutMissingTabSectionError));
+ }
+
+ Ok(LayoutTemplate {
+ pre_tab,
+ post_tab,
+ tabs,
+ })
+ }
+
+ fn from_vec_tab_layout(tab_layout: Vec<TabLayout>) -> Vec<Self> {
+ tab_layout
+ .iter()
+ .map(|tab_layout| Layout::from(tab_layout.to_owned()))
+ .collect()
+ }
+
+ fn from_vec_template_layout(layout_template: Vec<LayoutTemplateFromYaml>) -> Vec<Self> {
+ layout_template
+ .iter()
+ .map(|layout_template| Layout::from(layout_template.to_owned()))
+ .collect()
+ }
}
fn split_space_to_parts_vertically(
@@ -323,3 +543,43 @@ fn split_space(
}
pane_positions
}
+
+impl From<TabLayout> for Layout {
+ fn from(tab: TabLayout) -> Self {
+ Layout {
+ direction: tab.direction,
+ parts: Layout::from_vec_tab_layout(tab.parts),
+ tabs: vec![],
+ split_size: tab.split_size,
+ run: tab.run,
+ }
+ }
+}
+
+impl From<LayoutTemplateFromYaml> for Layout {
+ fn from(template: LayoutTemplateFromYaml) -> Self {
+ Layout {
+ direction: template.direction,
+ parts: Layout::from_vec_template_layout(template.parts),
+ tabs: vec![],
+ split_size: template.split_size,
+ run: template.run,
+ }
+ }
+}
+
+impl Default for TabLayout {
+ fn default() -> Self {
+ Self {
+ direction: Direction::Horizontal,
+ parts: vec![],
+ split_size: None,
+ run: None,
+ }
+ }
+}
+
+// The unit test location.
+#[cfg(test)]
+#[path = "./unit/layout_test.rs"]
+mod layout_test;
diff --git a/zellij-utils/src/input/unit/fixtures/layouts/deeply-nested-tab-layout.yaml b/zellij-utils/src/input/unit/fixtures/layouts/deeply-nested-tab-layout.yaml
new file mode 100644
index 000000000..633a1fae2
--- /dev/null
+++ b/zellij-utils/src/input/unit/fixtures/layouts/deeply-nested-tab-layout.yaml
@@ -0,0 +1,43 @@
+---
+template:
+ direction: Horizontal
+ parts:
+ - direction: Vertical
+ parts:
+ - direction: Horizontal
+ split_size:
+ Percent: 21
+ - direction: Vertical
+ split_size:
+ Percent: 79
+ parts:
+ - direction: Horizontal
+ split_size:
+ Percent: 22
+ - direction: Horizontal
+ split_size:
+ Percent: 78
+ parts:
+ - direction: Horizontal
+ split_size:
+ Percent: 23
+ - direction: Vertical
+ split_size:
+ Percent: 77
+ body: true
+ split_size:
+ Percent: 90
+ - direction: Vertical
+ split_size:
+ Percent: 15
+ - direction: Vertical
+ split_size:
+ Percent: 15
+ - direction: Vertical
+ split_size:
+ Percent: 15
+
+tabs:
+ - direction: Horizontal
+ split_size:
+ Percent: 24
diff --git a/zellij-utils/src/input/unit/fixtures/layouts/multiple-tabs-should-not-error.yaml b/zellij-utils/src/input/unit/fixtures/layouts/multiple-tabs-should-not-error.yaml
new file mode 100644
index 000000000..653787591
--- /dev/null
+++ b/zellij-utils/src/input/unit/fixtures/layouts/multiple-tabs-should-not-error.yaml
@@ -0,0 +1,18 @@
+---
+direction: Horizontal
+parts:
+ - direction: Horizontal
+ parts:
+ - direction: Vertical
+ parts:
+ - direction: Horizontal
+ split_size:
+ Percent: 50
+ - direction: Horizontal
+ tabs:
+ - direction: Vertical
+ split_size:
+ Percent: 50
+ - direction: Vertical
+ split_size:
+ Percent: 50
diff --git a/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-command.yaml b/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-command.yaml
new file mode 100644
index 000000000..9ad1034d4
--- /dev/null
+++ b/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-command.yaml
@@ -0,0 +1,35 @@
+---
+template:
+ direction: Horizontal
+ parts:
+ - direction: Vertical
+ split_size:
+ Fixed: 1
+ run:
+ plugin: tab-bar
+ - direction: Horizontal
+ body: true
+ - direction: Vertical
+ split_size:
+ Fixed: 2
+ run:
+ plugin: status-bar
+
+tabs:
+ - direction: Vertical
+ parts:
+ - direction: Horizontal
+ split_size: