zellij/zellij-utils/src/kdl/kdl_layout_parser.rs
2022-12-24 16:06:16 +01:00

1799 lines
75 KiB
Rust

use crate::input::{
command::RunCommand,
config::ConfigError,
layout::{
FloatingPanesLayout, Layout, PaneLayout, PercentOrFixed, Run, RunPlugin, RunPluginLocation,
SplitDirection, SplitSize,
},
};
use kdl::*;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use crate::{
kdl_child_with_name, kdl_children_nodes, kdl_get_bool_property_or_child_value,
kdl_get_bool_property_or_child_value_with_error, kdl_get_child,
kdl_get_int_property_or_child_value, kdl_get_property_or_child,
kdl_get_string_property_or_child_value, kdl_get_string_property_or_child_value_with_error,
kdl_name, kdl_parsing_error, kdl_property_names, kdl_property_or_child_value_node,
kdl_string_arguments,
};
use std::convert::TryFrom;
use std::path::PathBuf;
use std::vec::Vec;
use url::Url;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaneOrFloatingPane {
Pane(PaneLayout),
FloatingPane(FloatingPanesLayout),
Either(PaneLayout),
}
pub struct KdlLayoutParser<'a> {
global_cwd: Option<PathBuf>,
raw_layout: &'a str,
tab_templates: HashMap<String, (PaneLayout, Vec<FloatingPanesLayout>, KdlNode)>,
pane_templates: HashMap<String, (PaneOrFloatingPane, KdlNode)>,
default_tab_template: Option<(PaneLayout, Vec<FloatingPanesLayout>, KdlNode)>,
}
impl<'a> KdlLayoutParser<'a> {
pub fn new(raw_layout: &'a str, global_cwd: Option<PathBuf>) -> Self {
KdlLayoutParser {
raw_layout,
tab_templates: HashMap::new(),
pane_templates: HashMap::new(),
default_tab_template: None,
global_cwd,
}
}
fn is_a_reserved_word(&self, word: &str) -> bool {
word == "pane"
|| word == "layout"
|| word == "pane_template"
|| word == "tab_template"
|| word == "default_tab_template"
|| word == "command"
|| word == "edit"
|| word == "plugin"
|| word == "children"
|| word == "tab"
|| word == "args"
|| word == "close_on_exit"
|| word == "start_suspended"
|| word == "borderless"
|| word == "focus"
|| word == "name"
|| word == "size"
|| word == "cwd"
|| word == "split_direction"
}
fn is_a_valid_pane_property(&self, property_name: &str) -> bool {
property_name == "borderless"
|| property_name == "focus"
|| property_name == "name"
|| property_name == "size"
|| property_name == "plugin"
|| property_name == "command"
|| property_name == "edit"
|| property_name == "cwd"
|| property_name == "args"
|| property_name == "close_on_exit"
|| property_name == "start_suspended"
|| property_name == "split_direction"
|| property_name == "pane"
|| property_name == "children"
}
fn is_a_valid_floating_pane_property(&self, property_name: &str) -> bool {
property_name == "borderless"
|| property_name == "focus"
|| property_name == "name"
|| property_name == "plugin"
|| property_name == "command"
|| property_name == "edit"
|| property_name == "cwd"
|| property_name == "args"
|| property_name == "close_on_exit"
|| property_name == "start_suspended"
|| property_name == "x"
|| property_name == "y"
|| property_name == "width"
|| property_name == "height"
}
fn is_a_valid_tab_property(&self, property_name: &str) -> bool {
property_name == "focus"
|| property_name == "name"
|| property_name == "split_direction"
|| property_name == "cwd"
|| property_name == "floating_panes"
}
fn assert_legal_node_name(&self, name: &str, kdl_node: &KdlNode) -> Result<(), ConfigError> {
if name.contains(char::is_whitespace) {
Err(ConfigError::new_layout_kdl_error(
format!("Node names ({}) cannot contain whitespace.", name),
kdl_node.span().offset(),
kdl_node.span().len(),
))
} else if self.is_a_reserved_word(&name) {
Err(ConfigError::new_layout_kdl_error(
format!("Node name '{}' is a reserved word.", name),
kdl_node.span().offset(),
kdl_node.span().len(),
))
} else {
Ok(())
}
}
fn assert_legal_template_name(
&self,
name: &str,
kdl_node: &KdlNode,
) -> Result<(), ConfigError> {
if name.is_empty() {
Err(ConfigError::new_layout_kdl_error(
format!("Template names cannot be empty"),
kdl_node.span().offset(),
kdl_node.span().len(),
))
} else if name.contains(')') || name.contains('(') {
Err(ConfigError::new_layout_kdl_error(
format!("Template names cannot contain parantheses"),
kdl_node.span().offset(),
kdl_node.span().len(),
))
} else if name
.chars()
.next()
.map(|first_char| first_char.is_numeric())
.unwrap_or(false)
{
Err(ConfigError::new_layout_kdl_error(
format!("Template names cannot start with numbers"),
kdl_node.span().offset(),
kdl_node.span().len(),
))
} else {
Ok(())
}
}
fn parse_split_size(&self, kdl_node: &KdlNode) -> Result<Option<SplitSize>, ConfigError> {
if let Some(size) = kdl_get_string_property_or_child_value!(kdl_node, "size") {
match SplitSize::from_str(size) {
Ok(size) => Ok(Some(size)),
Err(_e) => Err(kdl_parsing_error!(
format!(
"size should be a fixed number (eg. 1) or a quoted percent (eg. \"50%\")"
),
kdl_node
)),
}
} else if let Some(size) = kdl_get_int_property_or_child_value!(kdl_node, "size") {
if size == 0 {
return Err(kdl_parsing_error!(
format!("size should be greater than 0"),
kdl_node
));
}
Ok(Some(SplitSize::Fixed(size as usize)))
} else if let Some(node) = kdl_property_or_child_value_node!(kdl_node, "size") {
Err(kdl_parsing_error!(
format!("size should be a fixed number (eg. 1) or a quoted percent (eg. \"50%\")"),
node
))
} else if let Some(node) = kdl_child_with_name!(kdl_node, "size") {
Err(kdl_parsing_error!(
format!(
"size cannot be bare, it should have a value (eg. 'size 1', or 'size \"50%\"')"
),
node
))
} else {
Ok(None)
}
}
fn parse_percent_or_fixed(
&self,
kdl_node: &KdlNode,
value_name: &str,
can_be_zero: bool,
) -> Result<Option<PercentOrFixed>, ConfigError> {
if let Some(size) = kdl_get_string_property_or_child_value!(kdl_node, value_name) {
match PercentOrFixed::from_str(size) {
Ok(size) => {
if !can_be_zero && size.is_zero() {
Err(kdl_parsing_error!(
format!("{} should be greater than 0", value_name),
kdl_node
))
} else {
Ok(Some(size))
}
},
Err(_e) => Err(kdl_parsing_error!(
format!(
"{} should be a fixed number (eg. 1) or a quoted percent (eg. \"50%\")",
value_name
),
kdl_node
)),
}
} else if let Some(size) = kdl_get_int_property_or_child_value!(kdl_node, value_name) {
if size == 0 && !can_be_zero {
return Err(kdl_parsing_error!(
format!("{} should be greater than 0", value_name),
kdl_node
));
}
Ok(Some(PercentOrFixed::Fixed(size as usize)))
} else if let Some(node) = kdl_property_or_child_value_node!(kdl_node, "size") {
Err(kdl_parsing_error!(
format!(
"{} should be a fixed number (eg. 1) or a quoted percent (eg. \"50%\")",
value_name
),
node
))
} else if let Some(node) = kdl_child_with_name!(kdl_node, "size") {
Err(kdl_parsing_error!(
format!(
"{} cannot be bare, it should have a value (eg. 'size 1', or 'size \"50%\"')",
value_name
),
node
))
} else {
Ok(None)
}
}
fn parse_plugin_block(&self, plugin_block: &KdlNode) -> Result<Option<Run>, ConfigError> {
let _allow_exec_host_cmd =
kdl_get_bool_property_or_child_value_with_error!(plugin_block, "_allow_exec_host_cmd")
.unwrap_or(false);
let string_url =
kdl_get_string_property_or_child_value_with_error!(plugin_block, "location").ok_or(
ConfigError::new_layout_kdl_error(
"Plugins must have a location".into(),
plugin_block.span().offset(),
plugin_block.span().len(),
),
)?;
let url_node = kdl_get_property_or_child!(plugin_block, "location").ok_or(
ConfigError::new_layout_kdl_error(
"Plugins must have a location".into(),
plugin_block.span().offset(),
plugin_block.span().len(),
),
)?;
let url = Url::parse(string_url).map_err(|e| {
ConfigError::new_layout_kdl_error(
format!("Failed to parse url: {:?}", e),
url_node.span().offset(),
url_node.span().len(),
)
})?;
let location = RunPluginLocation::try_from(url)?;
Ok(Some(Run::Plugin(RunPlugin {
_allow_exec_host_cmd,
location,
})))
}
fn parse_args(&self, pane_node: &KdlNode) -> Result<Option<Vec<String>>, ConfigError> {
match kdl_get_child!(pane_node, "args") {
Some(kdl_args) => {
if kdl_args.entries().is_empty() {
return Err(kdl_parsing_error!(format!("args cannot be empty and should contain one or more command arguments (eg. args \"-h\" \"-v\")"), kdl_args));
}
Ok(Some(
kdl_string_arguments!(kdl_args)
.iter()
.map(|s| String::from(*s))
.collect(),
))
},
None => Ok(None),
}
}
fn cwd_prefix(&self, tab_cwd: Option<&PathBuf>) -> Result<Option<PathBuf>, ConfigError> {
Ok(match (&self.global_cwd, tab_cwd) {
(Some(global_cwd), Some(tab_cwd)) => Some(global_cwd.join(tab_cwd)),
(None, Some(tab_cwd)) => Some(tab_cwd.clone()),
(Some(global_cwd), None) => Some(global_cwd.clone()),
(None, None) => None,
})
}
fn parse_cwd(&self, kdl_node: &KdlNode) -> Result<Option<PathBuf>, ConfigError> {
Ok(
kdl_get_string_property_or_child_value_with_error!(kdl_node, "cwd")
.map(|cwd| PathBuf::from(cwd)),
)
}
fn parse_pane_command(
&self,
pane_node: &KdlNode,
is_template: bool,
) -> Result<Option<Run>, ConfigError> {
let command = kdl_get_string_property_or_child_value_with_error!(pane_node, "command")
.map(|c| PathBuf::from(c));
let edit = kdl_get_string_property_or_child_value_with_error!(pane_node, "edit")
.map(|c| PathBuf::from(c));
let cwd = self.parse_cwd(pane_node)?;
let args = self.parse_args(pane_node)?;
let close_on_exit =
kdl_get_bool_property_or_child_value_with_error!(pane_node, "close_on_exit");
let start_suspended =
kdl_get_bool_property_or_child_value_with_error!(pane_node, "start_suspended");
if !is_template {
self.assert_no_bare_attributes_in_pane_node(
&command,
&args,
&close_on_exit,
&start_suspended,
pane_node,
)?;
}
let hold_on_close = close_on_exit.map(|c| !c).unwrap_or(true);
let hold_on_start = start_suspended.map(|c| c).unwrap_or(false);
match (command, edit, cwd) {
(None, None, Some(cwd)) => Ok(Some(Run::Cwd(cwd))),
(Some(command), None, cwd) => Ok(Some(Run::Command(RunCommand {
command,
args: args.unwrap_or_else(|| vec![]),
cwd,
hold_on_close,
hold_on_start,
}))),
(None, Some(edit), Some(cwd)) => Ok(Some(Run::EditFile(cwd.join(edit), None))),
(None, Some(edit), None) => Ok(Some(Run::EditFile(edit, None))),
(Some(_command), Some(_edit), _) => Err(ConfigError::new_layout_kdl_error(
"cannot have both a command and an edit instruction for the same pane".into(),
pane_node.span().offset(),
pane_node.span().len(),
)),
_ => Ok(None),
}
}
fn parse_command_plugin_or_edit_block(
&self,
kdl_node: &KdlNode,
) -> Result<Option<Run>, ConfigError> {
let mut run = self.parse_pane_command(kdl_node, false)?;
if let Some(plugin_block) = kdl_get_child!(kdl_node, "plugin") {
let has_non_cwd_run_prop = run
.map(|r| match r {
Run::Cwd(_) => false,
_ => true,
})
.unwrap_or(false);
if has_non_cwd_run_prop {
return Err(ConfigError::new_layout_kdl_error(
"Cannot have both a command/edit and a plugin block for a single pane".into(),
plugin_block.span().offset(),
plugin_block.span().len(),
));
}
run = self.parse_plugin_block(plugin_block)?;
}
Ok(run)
}
fn parse_command_plugin_or_edit_block_for_template(
&self,
kdl_node: &KdlNode,
) -> Result<Option<Run>, ConfigError> {
let mut run = self.parse_pane_command(kdl_node, true)?;
if let Some(plugin_block) = kdl_get_child!(kdl_node, "plugin") {
let has_non_cwd_run_prop = run
.map(|r| match r {
Run::Cwd(_) => false,
_ => true,
})
.unwrap_or(false);
if has_non_cwd_run_prop {
return Err(ConfigError::new_layout_kdl_error(
"Cannot have both a command/edit and a plugin block for a single pane".into(),
plugin_block.span().offset(),
plugin_block.span().len(),
));
}
run = self.parse_plugin_block(plugin_block)?;
}
Ok(run)
}
fn parse_pane_node(&self, kdl_node: &KdlNode) -> Result<PaneLayout, ConfigError> {
self.assert_valid_pane_properties(kdl_node)?;
let borderless = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless");
let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus");
let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name")
.map(|name| name.to_string());
let split_size = self.parse_split_size(kdl_node)?;
let run = self.parse_command_plugin_or_edit_block(kdl_node)?;
let children_split_direction = self.parse_split_direction(kdl_node)?;
let (external_children_index, children) = match kdl_children_nodes!(kdl_node) {
Some(children) => self.parse_child_pane_nodes_for_pane(&children)?,
None => (None, vec![]),
};
self.assert_no_mixed_children_and_properties(kdl_node)?;
Ok(PaneLayout {
borderless: borderless.unwrap_or_default(),
focus,
name,
split_size,
run,
children_split_direction,
external_children_index,
children,
..Default::default()
})
}
fn parse_floating_pane_node(
&self,
kdl_node: &KdlNode,
) -> Result<FloatingPanesLayout, ConfigError> {
self.assert_valid_floating_pane_properties(kdl_node)?;
let height = self.parse_percent_or_fixed(kdl_node, "height", false)?;
let width = self.parse_percent_or_fixed(kdl_node, "width", false)?;
let x = self.parse_percent_or_fixed(kdl_node, "x", true)?;
let y = self.parse_percent_or_fixed(kdl_node, "y", true)?;
let run = self.parse_command_plugin_or_edit_block(kdl_node)?;
let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus");
let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name")
.map(|name| name.to_string());
self.assert_no_mixed_children_and_properties(kdl_node)?;
Ok(FloatingPanesLayout {
name,
height,
width,
x,
y,
run,
focus,
..Default::default()
})
}
fn insert_children_to_pane_template(
&self,
kdl_node: &KdlNode,
pane_template: &mut PaneLayout,
pane_template_kdl_node: &KdlNode,
) -> Result<(), ConfigError> {
let children_split_direction = self.parse_split_direction(kdl_node)?;
let (external_children_index, pane_parts) = match kdl_children_nodes!(kdl_node) {
Some(children) => self.parse_child_pane_nodes_for_pane(&children)?,
None => (None, vec![]),
};
if pane_parts.len() > 0 {
let child_panes_layout = PaneLayout {
children_split_direction,
children: pane_parts,
external_children_index,
..Default::default()
};
self.assert_one_children_block(&pane_template, pane_template_kdl_node)?;
self.insert_layout_children_or_error(
pane_template,
child_panes_layout,
pane_template_kdl_node,
)?;
}
Ok(())
}
fn parse_pane_node_with_template(
&self,
kdl_node: &KdlNode,
pane_template: PaneOrFloatingPane,
pane_template_kdl_node: &KdlNode,
) -> Result<PaneLayout, ConfigError> {
match pane_template {
PaneOrFloatingPane::Pane(mut pane_template)
| PaneOrFloatingPane::Either(mut pane_template) => {
let borderless =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless");
let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus");
let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name")
.map(|name| name.to_string());
let args = self.parse_args(kdl_node)?;
let close_on_exit =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "close_on_exit");
let start_suspended =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "start_suspended");
let split_size = self.parse_split_size(kdl_node)?;
let run = self.parse_command_plugin_or_edit_block_for_template(kdl_node)?;
self.assert_no_bare_attributes_in_pane_node_with_template(
&run,
&pane_template.run,
&args,
&close_on_exit,
&start_suspended,
kdl_node,
)?;
self.insert_children_to_pane_template(
kdl_node,
&mut pane_template,
pane_template_kdl_node,
)?;
pane_template.run = Run::merge(&pane_template.run, &run);
if let Some(pane_template_run_command) = pane_template.run.as_mut() {
// we need to do this because panes consuming a pane_template
// can have bare args without a command
pane_template_run_command.add_args(args);
pane_template_run_command.add_close_on_exit(close_on_exit);
pane_template_run_command.add_start_suspended(start_suspended);
};
if let Some(borderless) = borderless {
pane_template.borderless = borderless;
}
if let Some(focus) = focus {
pane_template.focus = Some(focus);
}
if let Some(name) = name {
pane_template.name = Some(name);
}
if let Some(split_size) = split_size {
pane_template.split_size = Some(split_size);
}
if let Some(index_of_children) = pane_template.external_children_index {
pane_template
.children
.insert(index_of_children, PaneLayout::default());
}
pane_template.external_children_index = None;
Ok(pane_template)
},
PaneOrFloatingPane::FloatingPane(_) => {
let pane_template_name = kdl_get_string_property_or_child_value_with_error!(
pane_template_kdl_node,
"name"
)
.map(|name| name.to_string());
Err(ConfigError::new_layout_kdl_error(
format!("pane_template {}, is a floating pane template (derived from its properties) and cannot be applied to a tiled pane", pane_template_name.unwrap_or("".into())),
kdl_node.span().offset(),
kdl_node.span().len(),
))
},
}
}
fn parse_floating_pane_node_with_template(
&self,
kdl_node: &KdlNode,
pane_template: PaneOrFloatingPane,
pane_template_kdl_node: &KdlNode,
) -> Result<FloatingPanesLayout, ConfigError> {
match pane_template {
PaneOrFloatingPane::Pane(_) => {
let pane_template_name = kdl_get_string_property_or_child_value_with_error!(
pane_template_kdl_node,
"name"
)
.map(|name| name.to_string());
Err(ConfigError::new_layout_kdl_error(
format!("pane_template {}, is a non-floating pane template (derived from its properties) and cannot be applied to a floating pane", pane_template_name.unwrap_or("".into())),
kdl_node.span().offset(),
kdl_node.span().len(),
))
},
PaneOrFloatingPane::FloatingPane(mut pane_template) => {
let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus");
let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name")
.map(|name| name.to_string());
let args = self.parse_args(kdl_node)?;
let close_on_exit =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "close_on_exit");
let start_suspended =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "start_suspended");
let run = self.parse_command_plugin_or_edit_block_for_template(kdl_node)?;
self.assert_no_bare_attributes_in_pane_node_with_template(
&run,
&pane_template.run,
&args,
&close_on_exit,
&start_suspended,
kdl_node,
)?;
pane_template.run = Run::merge(&pane_template.run, &run);
if let Some(pane_template_run_command) = pane_template.run.as_mut() {
// we need to do this because panes consuming a pane_template
// can have bare args without a command
pane_template_run_command.add_args(args);
pane_template_run_command.add_close_on_exit(close_on_exit);
pane_template_run_command.add_start_suspended(start_suspended);
};
if let Some(focus) = focus {
pane_template.focus = Some(focus);
}
if let Some(name) = name {
pane_template.name = Some(name);
}
let height = self.parse_percent_or_fixed(kdl_node, "height", false)?;
let width = self.parse_percent_or_fixed(kdl_node, "width", false)?;
let x = self.parse_percent_or_fixed(kdl_node, "x", true)?;
let y = self.parse_percent_or_fixed(kdl_node, "y", true)?;
// let mut floating_pane = FloatingPanesLayout::from(&pane_template);
if let Some(height) = height {
pane_template.height = Some(height);
}
if let Some(width) = width {
pane_template.width = Some(width);
}
if let Some(y) = y {
pane_template.y = Some(y);
}
if let Some(x) = x {
pane_template.x = Some(x);
}
Ok(pane_template)
},
PaneOrFloatingPane::Either(mut pane_template) => {
let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus");
let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name")
.map(|name| name.to_string());
let args = self.parse_args(kdl_node)?;
let close_on_exit =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "close_on_exit");
let start_suspended =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "start_suspended");
let run = self.parse_command_plugin_or_edit_block_for_template(kdl_node)?;
self.assert_no_bare_attributes_in_pane_node_with_template(
&run,
&pane_template.run,
&args,
&close_on_exit,
&start_suspended,
kdl_node,
)?;
pane_template.run = Run::merge(&pane_template.run, &run);
if let Some(pane_template_run_command) = pane_template.run.as_mut() {
// we need to do this because panes consuming a pane_template
// can have bare args without a command
pane_template_run_command.add_args(args);
pane_template_run_command.add_close_on_exit(close_on_exit);
pane_template_run_command.add_start_suspended(start_suspended);
};
if let Some(focus) = focus {
pane_template.focus = Some(focus);
}
if let Some(name) = name {
pane_template.name = Some(name);
}
let height = self.parse_percent_or_fixed(kdl_node, "height", false)?;
let width = self.parse_percent_or_fixed(kdl_node, "width", false)?;
let x = self.parse_percent_or_fixed(kdl_node, "x", true)?;
let y = self.parse_percent_or_fixed(kdl_node, "y", true)?;
let mut floating_pane = FloatingPanesLayout::from(&pane_template);
if let Some(height) = height {
floating_pane.height = Some(height);
}
if let Some(width) = width {
floating_pane.width = Some(width);
}
if let Some(y) = y {
floating_pane.y = Some(y);
}
if let Some(x) = x {
floating_pane.x = Some(x);
}
Ok(floating_pane)
},
}
}
fn parse_split_direction(&self, kdl_node: &KdlNode) -> Result<SplitDirection, ConfigError> {
match kdl_get_string_property_or_child_value_with_error!(kdl_node, "split_direction") {
Some(direction) => match SplitDirection::from_str(direction) {
Ok(split_direction) => Ok(split_direction),
Err(_e) => Err(kdl_parsing_error!(
format!(
"split_direction should be either \"horizontal\" or \"vertical\" found: {}",
direction
),
kdl_node
)),
},
None => Ok(SplitDirection::default()),
}
}
fn has_only_neutral_pane_template_properties(
&self,
kdl_node: &KdlNode,
) -> Result<bool, ConfigError> {
// pane properties
let borderless = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless");
let split_size = self.parse_split_size(kdl_node)?;
let split_direction =
kdl_get_string_property_or_child_value_with_error!(kdl_node, "split_direction");
let has_children_nodes = self.has_child_nodes(kdl_node);
// floating pane properties
let height = self.parse_percent_or_fixed(kdl_node, "height", false)?;
let width = self.parse_percent_or_fixed(kdl_node, "width", false)?;
let x = self.parse_percent_or_fixed(kdl_node, "x", true)?;
let y = self.parse_percent_or_fixed(kdl_node, "y", true)?;
let has_pane_properties = borderless.is_some()
|| split_size.is_some()
|| split_direction.is_some()
|| has_children_nodes;
let has_floating_pane_properties =
height.is_some() || width.is_some() || x.is_some() || y.is_some();
if has_pane_properties || has_floating_pane_properties {
Ok(false)
} else {
Ok(true)
}
}
fn differentiate_pane_and_floating_pane_template(
&self,
kdl_node: &KdlNode,
) -> Result<bool, ConfigError> {
// returns true if it's a floating_pane template, false if not
// pane properties
let borderless = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless");
let split_size = self.parse_split_size(kdl_node)?;
let split_direction =
kdl_get_string_property_or_child_value_with_error!(kdl_node, "split_direction");
let has_children_nodes = self.has_child_nodes(kdl_node);
// floating pane properties
let height = self.parse_percent_or_fixed(kdl_node, "height", false)?;
let width = self.parse_percent_or_fixed(kdl_node, "width", false)?;
let x = self.parse_percent_or_fixed(kdl_node, "x", true)?;
let y = self.parse_percent_or_fixed(kdl_node, "y", true)?;
let has_pane_properties = borderless.is_some()
|| split_size.is_some()
|| split_direction.is_some()
|| has_children_nodes;
let has_floating_pane_properties =
height.is_some() || width.is_some() || x.is_some() || y.is_some();
if has_pane_properties && has_floating_pane_properties {
let mut pane_properties = vec![];
if borderless.is_some() {
pane_properties.push("borderless");
}
if split_size.is_some() {
pane_properties.push("split_size");
}
if split_direction.is_some() {
pane_properties.push("split_direction");
}
if has_children_nodes {
pane_properties.push("child nodes");
}
let mut floating_pane_properties = vec![];
if height.is_some() {
floating_pane_properties.push("height");
}
if width.is_some() {
floating_pane_properties.push("width");
}
if x.is_some() {
floating_pane_properties.push("x");
}
if y.is_some() {
floating_pane_properties.push("y");
}
Err(ConfigError::new_layout_kdl_error(
format!(
"A pane_template cannot have both pane ({}) and floating pane ({}) properties",
pane_properties.join(", "),
floating_pane_properties.join(", ")
),
kdl_node.span().offset(),
kdl_node.span().len(),
))
} else if has_floating_pane_properties {
Ok(true)
} else {
Ok(false)
}
}
fn parse_pane_template_node(&mut self, kdl_node: &KdlNode) -> Result<(), ConfigError> {
let template_name = kdl_get_string_property_or_child_value!(kdl_node, "name")
.map(|s| s.to_string())
.ok_or(ConfigError::new_layout_kdl_error(
"Pane templates must have a name".into(),
kdl_node.span().offset(),
kdl_node.span().len(),
))?;
self.assert_legal_node_name(&template_name, kdl_node)?;
self.assert_legal_template_name(&template_name, kdl_node)?;
let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus");
let run = self.parse_command_plugin_or_edit_block(kdl_node)?;
let is_floating = self.differentiate_pane_and_floating_pane_template(&kdl_node)?;
let can_be_either_floating_or_tiled =
self.has_only_neutral_pane_template_properties(&kdl_node)?;
if can_be_either_floating_or_tiled {
self.assert_valid_pane_or_floating_pane_properties(kdl_node)?;
self.pane_templates.insert(
template_name,
(
PaneOrFloatingPane::Either(PaneLayout {
focus,
run,
..Default::default()
}),
kdl_node.clone(),
),
);
} else if is_floating {
self.assert_valid_floating_pane_properties(kdl_node)?;
// floating pane properties
let height = self.parse_percent_or_fixed(kdl_node, "height", false)?;
let width = self.parse_percent_or_fixed(kdl_node, "width", false)?;
let x = self.parse_percent_or_fixed(kdl_node, "x", true)?;
let y = self.parse_percent_or_fixed(kdl_node, "y", true)?;
self.pane_templates.insert(
template_name,
(
PaneOrFloatingPane::FloatingPane(FloatingPanesLayout {
focus,
run,
height,
width,
x,
y,
..Default::default()
}),
kdl_node.clone(),
),
);
} else {
self.assert_valid_pane_properties(kdl_node)?;
// pane properties
let borderless =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless");
let split_size = self.parse_split_size(kdl_node)?;
let children_split_direction = self.parse_split_direction(kdl_node)?;
let (external_children_index, pane_parts) = match kdl_children_nodes!(kdl_node) {
Some(children) => self.parse_child_pane_nodes_for_pane(&children)?,
None => (None, vec![]),
};
self.assert_no_mixed_children_and_properties(kdl_node)?;
self.pane_templates.insert(
template_name,
(
PaneOrFloatingPane::Pane(PaneLayout {
borderless: borderless.unwrap_or_default(),
focus,
split_size,
run,
children_split_direction,
external_children_index,
children: pane_parts,
..Default::default()
}),
kdl_node.clone(),
),
);
}
Ok(())
}
fn parse_tab_node(
&mut self,
kdl_node: &KdlNode,
) -> Result<(bool, Option<String>, PaneLayout, Vec<FloatingPanesLayout>), ConfigError> {
// (is_focused, Option<tab_name>, PaneLayout, Vec<FloatingPanesLayout>)
self.assert_valid_tab_properties(kdl_node)?;
let tab_name =
kdl_get_string_property_or_child_value!(kdl_node, "name").map(|s| s.to_string());
let tab_cwd =
kdl_get_string_property_or_child_value!(kdl_node, "cwd").map(|c| PathBuf::from(c));
let is_focused = kdl_get_bool_property_or_child_value!(kdl_node, "focus").unwrap_or(false);
let children_split_direction = self.parse_split_direction(kdl_node)?;
let mut child_floating_panes = vec![];
let children = match kdl_children_nodes!(kdl_node) {
Some(children) => {
self.parse_child_pane_nodes_for_tab(children, &mut child_floating_panes)?
},
None => vec![],
};
let mut pane_layout = PaneLayout {
children_split_direction,
children,
..Default::default()
};
if let Some(cwd_prefix) = &self.cwd_prefix(tab_cwd.as_ref())? {
pane_layout.add_cwd_to_layout(&cwd_prefix);
}
Ok((is_focused, tab_name, pane_layout, child_floating_panes))
}
fn parse_child_pane_nodes_for_tab(
&self,
children: &[KdlNode],
child_floating_panes: &mut Vec<FloatingPanesLayout>,
) -> Result<Vec<PaneLayout>, ConfigError> {
let mut nodes = vec![];
for child in children {
if kdl_name!(child) == "pane" {
nodes.push(self.parse_pane_node(child)?);
} else if let Some((pane_template, pane_template_kdl_node)) =
self.pane_templates.get(kdl_name!(child)).cloned()
{
nodes.push(self.parse_pane_node_with_template(
child,
pane_template,
&pane_template_kdl_node,
)?);
} else if kdl_name!(child) == "floating_panes" {
self.populate_floating_pane_children(child, child_floating_panes)?;
} else if self.is_a_valid_tab_property(kdl_name!(child)) {
return Err(ConfigError::new_layout_kdl_error(
format!("Tab property '{}' must be placed on the tab title line and not in the child braces", kdl_name!(child)),
child.span().offset(),
child.span().len()
));
} else {
return Err(ConfigError::new_layout_kdl_error(
format!("Invalid tab property: {}", kdl_name!(child)),
child.span().offset(),
child.span().len(),
));
}
}
if nodes.is_empty() {
nodes.push(PaneLayout::default());
}
Ok(nodes)
}
fn parse_child_pane_nodes_for_pane(
&self,
children: &[KdlNode],
) -> Result<(Option<usize>, Vec<PaneLayout>), ConfigError> {
// usize is external_children_index
let mut external_children_index = None;
let mut nodes = vec![];
for (i, child) in children.iter().enumerate() {
if kdl_name!(child) == "pane" {
nodes.push(self.parse_pane_node(child)?);
} else if kdl_name!(child) == "children" {
let node_has_child_nodes = child.children().map(|c| !c.is_empty()).unwrap_or(false);
let node_has_entries = !child.entries().is_empty();
if node_has_child_nodes || node_has_entries {
return Err(ConfigError::new_layout_kdl_error(
format!("The `children` node must be bare. All properties should be places on the node consuming this template."),
child.span().offset(),
child.span().len(),
));
}
external_children_index = Some(i);
} else if let Some((pane_template, pane_template_kdl_node)) =
self.pane_templates.get(kdl_name!(child)).cloned()
{
nodes.push(self.parse_pane_node_with_template(
child,
pane_template,
&pane_template_kdl_node,
)?);
} else if !self.is_a_valid_pane_property(kdl_name!(child)) {
return Err(ConfigError::new_layout_kdl_error(
format!("Unknown pane property: {}", kdl_name!(child)),
child.span().offset(),
child.span().len(),
));
}
}
Ok((external_children_index, nodes))
}
fn has_child_nodes(&self, kdl_node: &KdlNode) -> bool {
if let Some(children) = kdl_children_nodes!(kdl_node) {
for child in children {
if kdl_name!(child) == "pane" || kdl_name!(child) == "children" || self.pane_templates.get(kdl_name!(child)).is_some() {
return true;
}
}
}
return false;
}
fn has_child_panes_tabs_or_templates(&self, kdl_node: &KdlNode) -> bool {
if let Some(children) = kdl_children_nodes!(kdl_node) {
for child in children {
let child_node_name = kdl_name!(child);
if child_node_name == "pane"
|| child_node_name == "children"
|| child_node_name == "tab"
|| child_node_name == "children"
{
return true;
} else if let Some((_pane_template, _pane_template_kdl_node)) =
self.pane_templates.get(child_node_name).cloned()
{
return true;
}
}
}
false
}
fn assert_no_bare_attributes_in_pane_node_with_template(
&self,
pane_run: &Option<Run>,
pane_template_run: &Option<Run>,
args: &Option<Vec<String>>,
close_on_exit: &Option<bool>,
start_suspended: &Option<bool>,
pane_node: &KdlNode,
) -> Result<(), ConfigError> {
if let (None, None, true) = (pane_run, pane_template_run, args.is_some()) {
return Err(kdl_parsing_error!(
format!("args can only be specified if a command was specified either in the pane_template or in the pane"),
pane_node
));
}
if let (None, None, true) = (pane_run, pane_template_run, close_on_exit.is_some()) {
return Err(kdl_parsing_error!(
format!("close_on_exit can only be specified if a command was specified either in the pane_template or in the pane"),
pane_node
));
}
if let (None, None, true) = (pane_run, pane_template_run, start_suspended.is_some()) {
return Err(kdl_parsing_error!(
format!("start_suspended can only be specified if a command was specified either in the pane_template or in the pane"),
pane_node
));
}
Ok(())
}
fn assert_no_bare_attributes_in_pane_node(
&self,
command: &Option<PathBuf>,
args: &Option<Vec<String>>,
close_on_exit: &Option<bool>,
start_suspended: &Option<bool>,
pane_node: &KdlNode,
) -> Result<(), ConfigError> {
if command.is_none() {
if close_on_exit.is_some() {
return Err(ConfigError::new_layout_kdl_error(
"close_on_exit can only be set if a command was specified".into(),
pane_node.span().offset(),
pane_node.span().len(),
));
}
if start_suspended.is_some() {
return Err(ConfigError::new_layout_kdl_error(
"start_suspended can only be set if a command was specified".into(),
pane_node.span().offset(),
pane_node.span().len(),
));
}
if args.is_some() {
return Err(ConfigError::new_layout_kdl_error(
"args can only be set if a command was specified".into(),
pane_node.span().offset(),
pane_node.span().len(),
));
}
}
Ok(())
}
fn assert_one_children_block(
&self,
layout: &PaneLayout,
kdl_node: &KdlNode,
) -> Result<(), ConfigError> {
let children_block_count = layout.children_block_count();
if children_block_count != 1 {
return Err(ConfigError::new_layout_kdl_error(format!("This template has {} children blocks, only 1 is allowed when used to insert child panes", children_block_count), kdl_node.span().offset(), kdl_node.span().len()));
}
Ok(())
}
fn assert_valid_pane_properties(&self, pane_node: &KdlNode) -> Result<(), ConfigError> {
for entry in pane_node.entries() {
match entry
.name()
.map(|e| e.value())
.or_else(|| entry.value().as_string())
{
Some(string_name) => {
if !self.is_a_valid_pane_property(string_name) {
return Err(ConfigError::new_layout_kdl_error(
format!("Unknown pane property: {}", string_name),
entry.span().offset(),
entry.span().len(),
));
}
},
None => {
return Err(ConfigError::new_layout_kdl_error(
"Unknown pane property".into(),
entry.span().offset(),
entry.span().len(),
));
},
}
}
Ok(())
}
fn assert_valid_floating_pane_properties(
&self,
pane_node: &KdlNode,
) -> Result<(), ConfigError> {
for entry in pane_node.entries() {
match entry
.name()
.map(|e| e.value())
.or_else(|| entry.value().as_string())
{
Some(string_name) => {
if !self.is_a_valid_floating_pane_property(string_name) {
return Err(ConfigError::new_layout_kdl_error(
format!("Unknown floating pane property: {}", string_name),
entry.span().offset(),
entry.span().len(),
));
}
},
None => {
return Err(ConfigError::new_layout_kdl_error(
"Unknown floating pane property".into(),
entry.span().offset(),
entry.span().len(),
));
},
}
}
Ok(())
}
fn assert_valid_pane_or_floating_pane_properties(
&self,
pane_node: &KdlNode,
) -> Result<(), ConfigError> {
for entry in pane_node.entries() {
match entry
.name()
.map(|e| e.value())
.or_else(|| entry.value().as_string())
{
Some(string_name) => {
if !self.is_a_valid_floating_pane_property(string_name)
|| !self.is_a_valid_pane_property(string_name)
{
return Err(ConfigError::new_layout_kdl_error(
format!("Unknown pane property: {}", string_name),
entry.span().offset(),
entry.span().len(),
));
}
},
None => {
return Err(ConfigError::new_layout_kdl_error(
"Unknown pane property".into(),
entry.span().offset(),
entry.span().len(),
));
},
}
}
Ok(())
}
fn assert_valid_tab_properties(&self, pane_node: &KdlNode) -> Result<(), ConfigError> {
let all_property_names = kdl_property_names!(pane_node);
for name in all_property_names {
if !self.is_a_valid_tab_property(name) {
return Err(ConfigError::new_layout_kdl_error(
format!("Invalid tab property '{}'", name),
pane_node.span().offset(),
pane_node.span().len(),
));
}
}
Ok(())
}
fn assert_no_mixed_children_and_properties(
&self,
kdl_node: &KdlNode,
) -> Result<(), ConfigError> {
let has_borderless_prop =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless").is_some();
let has_focus_prop =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus").is_some();
let has_cwd_prop =
kdl_get_string_property_or_child_value_with_error!(kdl_node, "cwd").is_some();
let has_non_cwd_run_prop = self
.parse_command_plugin_or_edit_block(kdl_node)?
.map(|r| match r {
Run::Cwd(_) => false,
_ => true,
})
.unwrap_or(false);
let has_nested_nodes_or_children_block = self.has_child_panes_tabs_or_templates(kdl_node);
if has_nested_nodes_or_children_block
&& (has_borderless_prop || has_focus_prop || has_non_cwd_run_prop || has_cwd_prop)
{
let mut offending_nodes = vec![];
if has_borderless_prop {
offending_nodes.push("borderless");
}
if has_focus_prop {
offending_nodes.push("focus");
}
if has_non_cwd_run_prop {
offending_nodes.push("command/edit/plugin");
}
if has_cwd_prop {
offending_nodes.push("cwd");
}
Err(ConfigError::new_layout_kdl_error(
format!(
"Cannot have both properties ({}) and nested children",
offending_nodes.join(", ")
),
kdl_node.span().offset(),
kdl_node.span().len(),
))
} else {
Ok(())
}
}
fn insert_layout_children_or_error(
&self,
layout: &mut PaneLayout,
mut child_panes_layout: PaneLayout,
kdl_node: &KdlNode,
) -> Result<(), ConfigError> {
let successfully_inserted = layout.insert_children_layout(&mut child_panes_layout)?;
if !successfully_inserted {
Err(ConfigError::new_layout_kdl_error(
"This template does not have children".into(),
kdl_node.span().offset(),
kdl_node.span().len(),
))
} else {
Ok(())
}
}
fn parse_tab_node_with_template(
&self,
kdl_node: &KdlNode,
mut tab_layout: PaneLayout,
mut tab_template_floating_panes: Vec<FloatingPanesLayout>,
tab_layout_kdl_node: &KdlNode,
) -> Result<(bool, Option<String>, PaneLayout, Vec<FloatingPanesLayout>), ConfigError> {
// (is_focused, Option<tab_name>, PaneLayout, Vec<FloatingPanesLayout>)
let tab_name =
kdl_get_string_property_or_child_value!(kdl_node, "name").map(|s| s.to_string());
let tab_cwd =
kdl_get_string_property_or_child_value!(kdl_node, "cwd").map(|c| PathBuf::from(c));
let is_focused = kdl_get_bool_property_or_child_value!(kdl_node, "focus").unwrap_or(false);
let children_split_direction = self.parse_split_direction(kdl_node)?;
match kdl_children_nodes!(kdl_node) {
Some(children) => {
let child_panes = self
.parse_child_pane_nodes_for_tab(children, &mut tab_template_floating_panes)?;
let child_panes_layout = PaneLayout {
children_split_direction,
children: child_panes,
..Default::default()
};
self.assert_one_children_block(&tab_layout, &tab_layout_kdl_node)?;
self.insert_layout_children_or_error(
&mut tab_layout,
child_panes_layout,
&tab_layout_kdl_node,
)?;
},
None => {
if let Some(index_of_children) = tab_layout.external_children_index {
tab_layout
.children
.insert(index_of_children, PaneLayout::default());
}
},
}
if let Some(cwd_prefix) = self.cwd_prefix(tab_cwd.as_ref())? {
tab_layout.add_cwd_to_layout(&cwd_prefix);
}
tab_layout.external_children_index = None;
Ok((
is_focused,
tab_name,
tab_layout,
tab_template_floating_panes,
))
}
fn populate_one_tab_template(&mut self, kdl_node: &KdlNode) -> Result<(), ConfigError> {
let template_name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name")
.map(|s| s.to_string())
.ok_or(ConfigError::new_layout_kdl_error(
"Tab templates must have a name".into(),
kdl_node.span().offset(),
kdl_node.span().len(),
))?;
self.assert_legal_node_name(&template_name, kdl_node)?;
self.assert_legal_template_name(&template_name, kdl_node)?;
if self.tab_templates.contains_key(&template_name) {
return Err(ConfigError::new_layout_kdl_error(
format!(
"Duplicate definition of the \"{}\" tab_template",
template_name
),
kdl_node.span().offset(),
kdl_node.span().len(),
));
}
if self.pane_templates.contains_key(&template_name) {
return Err(ConfigError::new_layout_kdl_error(
format!("There is already a pane_template with the name \"{}\" - can't have a tab_template with the same name", template_name),
kdl_node.span().offset(),
kdl_node.span().len(),
));
}
let (tab_template, tab_template_floating_panes) = self.parse_tab_template_node(kdl_node)?;
self.tab_templates.insert(
template_name,
(tab_template, tab_template_floating_panes, kdl_node.clone()),
);
Ok(())
}
fn populate_default_tab_template(&mut self, kdl_node: &KdlNode) -> Result<(), ConfigError> {
let (tab_template, tab_template_floating_panes) = self.parse_tab_template_node(kdl_node)?;
self.default_tab_template =
Some((tab_template, tab_template_floating_panes, kdl_node.clone()));
Ok(())
}
fn parse_tab_template_node(
&self,
kdl_node: &KdlNode,
) -> Result<(PaneLayout, Vec<FloatingPanesLayout>), ConfigError> {
self.assert_valid_tab_properties(kdl_node)?;
let children_split_direction = self.parse_split_direction(kdl_node)?;
let mut tab_children = vec![];
let mut tab_floating_children = vec![];
let mut external_children_index = None;
let mut children_index_offset = 0;
if let Some(children) = kdl_children_nodes!(kdl_node) {
for (i, child) in children.iter().enumerate() {
if kdl_name!(child) == "pane" {
tab_children.push(self.parse_pane_node(child)?);
} else if kdl_name!(child) == "children" {
let node_has_child_nodes =
child.children().map(|c| !c.is_empty()).unwrap_or(false);
let node_has_entries = !child.entries().is_empty();
if node_has_child_nodes || node_has_entries {
return Err(ConfigError::new_layout_kdl_error(
format!("The `children` node must be bare. All properties should be places on the node consuming this template."),
child.span().offset(),
child.span().len(),
));
}
external_children_index = Some(i.saturating_sub(children_index_offset));
} else if let Some((pane_template, pane_template_kdl_node)) =
self.pane_templates.get(kdl_name!(child)).cloned()
{
tab_children.push(self.parse_pane_node_with_template(
child,
pane_template,
&pane_template_kdl_node,
)?);
} else if kdl_name!(child) == "floating_panes" {
children_index_offset += 1;
self.populate_floating_pane_children(child, &mut tab_floating_children)?;
} else if self.is_a_valid_tab_property(kdl_name!(child)) {
return Err(ConfigError::new_layout_kdl_error(
format!("Tab property '{}' must be placed on the tab_template title line and not in the child braces", kdl_name!(child)),
child.span().offset(),
child.span().len()
));
} else {
return Err(ConfigError::new_layout_kdl_error(
format!("Invalid tab_template property: {}", kdl_name!(child)),
child.span().offset(),
child.span().len(),
));
}
}
}
Ok((
PaneLayout {
children_split_direction,
children: tab_children,
external_children_index,
..Default::default()
},
tab_floating_children,
))
}
fn default_template(&self) -> Result<Option<PaneLayout>, ConfigError> {
match &self.default_tab_template {
Some((template, _template_floating_panes, _kdl_node)) => {
let mut template = template.clone();
if let Some(children_index) = template.external_children_index {
template
.children
.insert(children_index, PaneLayout::default())
}
template.external_children_index = None;
Ok(Some(template))
},
None => Ok(None),
}
}
pub fn get_pane_template_dependency_tree(
&self,
kdl_children: &'a [KdlNode],
) -> Result<HashMap<&'a str, HashSet<&'a str>>, ConfigError> {
let mut dependency_tree = HashMap::new();
for child in kdl_children {
if kdl_name!(child) == "pane_template" {
let template_name = kdl_get_string_property_or_child_value!(child, "name").ok_or(
ConfigError::new_layout_kdl_error(
"Pane templates must have a name".into(),
child.span().offset(),
child.span().len(),
),
)?;
let mut template_children = HashSet::new();
self.get_pane_template_dependencies(child, &mut template_children)?;
if dependency_tree.contains_key(template_name) {
return Err(ConfigError::new_layout_kdl_error(
format!(
"Duplicate definition of the \"{}\" pane_template",
template_name
),
child.span().offset(),
child.span().len(),
));
}
dependency_tree.insert(template_name, template_children);
}
}
let all_pane_template_names: HashSet<&str> = dependency_tree.keys().cloned().collect();
for (_pane_template_name, dependencies) in dependency_tree.iter_mut() {
dependencies.retain(|d| all_pane_template_names.contains(d));
}
Ok(dependency_tree)
}
fn get_pane_template_dependencies(
&self,
node: &'a KdlNode,
all_dependencies: &mut HashSet<&'a str>,
) -> Result<(), ConfigError> {
if let Some(children) = kdl_children_nodes!(node) {
for child in children {
let child_name = kdl_name!(child);
if child_name == "pane" {
self.get_pane_template_dependencies(child, all_dependencies)?;
} else if !self.is_a_reserved_word(child_name) {
all_dependencies.insert(child_name);
self.get_pane_template_dependencies(child, all_dependencies)?;
}
}
}
Ok(())
}
pub fn parse_pane_template_by_name(
&mut self,
pane_template_name: &str,
kdl_children: &[KdlNode],
) -> Result<(), ConfigError> {
for child in kdl_children.iter() {
let child_name = kdl_name!(child);
if child_name == "pane_template" {
let child_name = kdl_get_string_property_or_child_value!(child, "name");
if child_name == Some(pane_template_name) {
self.parse_pane_template_node(child)?;
}
}
}
Ok(())
}
fn populate_global_cwd(&mut self, layout_node: &KdlNode) -> Result<(), ConfigError> {
// we only populate global cwd from the layout file if another wasn't explicitly passed to us
if self.global_cwd.is_none() {
if let Some(global_cwd) =
kdl_get_string_property_or_child_value_with_error!(layout_node, "cwd")
{
self.global_cwd = Some(PathBuf::from(global_cwd));
}
}
Ok(())
}
fn populate_pane_templates(
&mut self,
layout_children: &[KdlNode],
kdl_layout: &KdlDocument,
) -> Result<(), ConfigError> {
let mut pane_template_dependency_tree =
self.get_pane_template_dependency_tree(layout_children)?;
let mut pane_template_names_to_parse: Vec<&str> = vec![];
// toposort the dependency tree so that we parse the pane_templates before their
// dependencies
while !pane_template_dependency_tree.is_empty() {
let mut candidates: Vec<&str> = vec![];
for (pane_tempalte, dependencies) in pane_template_dependency_tree.iter() {
if dependencies.is_empty() {
candidates.push(pane_tempalte);
}
}
if candidates.is_empty() {
return Err(ConfigError::new_layout_kdl_error(
"Circular dependency detected between pane templates.".into(),
kdl_layout.span().offset(),
kdl_layout.span().len(),
));
}
for candidate_to_remove in candidates {
pane_template_dependency_tree.remove(candidate_to_remove);
for (_pane_tempalte, dependencies) in pane_template_dependency_tree.iter_mut() {
dependencies.remove(candidate_to_remove);
}
pane_template_names_to_parse.push(candidate_to_remove);
}
}
// once we've toposorted, parse the sorted list in order
for pane_template_name in pane_template_names_to_parse {
self.parse_pane_template_by_name(pane_template_name, &layout_children)?;
}
Ok(())
}
fn populate_tab_templates(&mut self, layout_children: &[KdlNode]) -> Result<(), ConfigError> {
for child in layout_children.iter() {
let child_name = kdl_name!(child);
if child_name == "tab_template" {
self.populate_one_tab_template(child)?;
} else if child_name == "default_tab_template" {
self.populate_default_tab_template(child)?;
}
}
Ok(())
}
fn layout_with_tabs(
&self,
tabs: Vec<(Option<String>, PaneLayout, Vec<FloatingPanesLayout>)>,
focused_tab_index: Option<usize>,
) -> Result<Layout, ConfigError> {
let template = self
.default_template()?
.unwrap_or_else(|| PaneLayout::default());
Ok(Layout {
tabs: tabs,
template: Some(template),
focused_tab_index,
..Default::default()
})
}
fn layout_with_one_tab(
&self,
panes: Vec<PaneLayout>,
floating_panes: Vec<FloatingPanesLayout>,
) -> Result<Layout, ConfigError> {
let main_tab_layout = PaneLayout {
children: panes,
..Default::default()
};
let default_template = self.default_template()?;
let tabs = if default_template.is_none() {
// in this case, the layout will be created as the default template and we don't need
// to explicitly place it in the first tab
vec![]
} else {
vec![(None, main_tab_layout.clone(), floating_panes.clone())]
};
let template = default_template.unwrap_or_else(|| main_tab_layout.clone());
// create a layout with one tab that has these child panes
Ok(Layout {
tabs,
template: Some(template),
floating_panes_template: floating_panes,
..Default::default()
})
}
fn layout_with_one_pane(
&self,
child_floating_panes: Vec<FloatingPanesLayout>,
) -> Result<Layout, ConfigError> {
let template = self
.default_template()?
.unwrap_or_else(|| PaneLayout::default());
Ok(Layout {
template: Some(template),
floating_panes_template: child_floating_panes,
..Default::default()
})
}
fn populate_layout_child(
&mut self,
child: &KdlNode,
child_tabs: &mut Vec<(bool, Option<String>, PaneLayout, Vec<FloatingPanesLayout>)>,
child_panes: &mut Vec<PaneLayout>,
child_floating_panes: &mut Vec<FloatingPanesLayout>,
) -> Result<(), ConfigError> {
let child_name = kdl_name!(child);
if (child_name == "pane" || child_name == "floating_panes") && !child_tabs.is_empty() {
return Err(ConfigError::new_layout_kdl_error(
"Cannot have both tabs and panes in the same node".into(),
child.span().offset(),
child.span().len(),
));
}
if child_name == "pane" {
let mut pane_node = self.parse_pane_node(child)?;
if let Some(global_cwd) = &self.global_cwd {
pane_node.add_cwd_to_layout(&global_cwd);
}
child_panes.push(pane_node);
} else if child_name == "floating_panes" {
self.populate_floating_pane_children(child, child_floating_panes)?;
} else if child_name == "tab" {
if !child_panes.is_empty() || !child_floating_panes.is_empty() {
return Err(ConfigError::new_layout_kdl_error(
"Cannot have both tabs and panes in the same node".into(),
child.span().offset(),
child.span().len(),
));
}
match &self.default_tab_template {
Some((
default_tab_template,
default_tab_template_floating_panes,
default_tab_template_kdl_node,
)) => {
let default_tab_template = default_tab_template.clone();
child_tabs.push(self.parse_tab_node_with_template(
child,
default_tab_template,
default_tab_template_floating_panes.clone(),
default_tab_template_kdl_node,
)?);
},
None => {
child_tabs.push(self.parse_tab_node(child)?);
},
}
} else if let Some((tab_template, tab_template_floating_panes, tab_template_kdl_node)) =
self.tab_templates.get(child_name).cloned()
{
if !child_panes.is_empty() {
return Err(ConfigError::new_layout_kdl_error(
"Cannot have both tabs and panes in the same node".into(),
child.span().offset(),
child.span().len(),
));
}
child_tabs.push(self.parse_tab_node_with_template(
child,
tab_template,
tab_template_floating_panes,
&tab_template_kdl_node,
)?);
} else if let Some((pane_template, pane_template_kdl_node)) =
self.pane_templates.get(child_name).cloned()
{
if !child_tabs.is_empty() {
return Err(ConfigError::new_layout_kdl_error(
"Cannot have both tabs and panes in the same node".into(),
child.span().offset(),
child.span().len(),
));
}
let mut pane_template =
self.parse_pane_node_with_template(child, pane_template, &pane_template_kdl_node)?;
if let Some(cwd_prefix) = &self.cwd_prefix(None)? {
pane_template.add_cwd_to_layout(&cwd_prefix);
}
child_panes.push(pane_template);
} else if !self.is_a_reserved_word(child_name) {
return Err(ConfigError::new_layout_kdl_error(
format!("Unknown layout node: '{}'", child_name),
child.span().offset(),
child.span().len(),
));
}
Ok(())
}
fn populate_floating_pane_children(
&self,
child: &KdlNode,
child_floating_panes: &mut Vec<FloatingPanesLayout>,
) -> Result<(), ConfigError> {
if let Some(children) = kdl_children_nodes!(child) {
for child in children {
if kdl_name!(child) == "pane" {
let mut pane_node = self.parse_floating_pane_node(child)?;
if let Some(global_cwd) = &self.global_cwd {
pane_node.add_cwd_to_layout(&global_cwd);
}
child_floating_panes.push(pane_node);
} else if let Some((pane_template, pane_template_kdl_node)) =
self.pane_templates.get(kdl_name!(child)).cloned()
{
let pane_node = self.parse_floating_pane_node_with_template(
child,
pane_template,
&pane_template_kdl_node,
)?;
child_floating_panes.push(pane_node);
} else {
// TODO: invalid node name
return Err(ConfigError::new_layout_kdl_error(
format!(
"floating_panes can only contain pane nodes, found: {}",
kdl_name!(child)
),
child.span().offset(),
child.span().len(),
));
}
}
};
Ok(())
}
pub fn parse(&mut self) -> Result<Layout, ConfigError> {
let kdl_layout: KdlDocument = self.raw_layout.parse()?;
let layout_node = kdl_layout
.nodes()
.iter()
.find(|n| kdl_name!(n) == "layout")
.ok_or(ConfigError::new_layout_kdl_error(
"No layout found".into(),
kdl_layout.span().offset(),
kdl_layout.span().len(),
))?;
let has_multiple_layout_nodes = kdl_layout
.nodes()
.iter()
.filter(|n| kdl_name!(n) == "layout")
.count()
> 1;
if has_multiple_layout_nodes {
return Err(ConfigError::new_layout_kdl_error(
"Only one layout node per file allowed".into(),
kdl_layout.span().offset(),
kdl_layout.span().len(),
));
}
let mut child_tabs = vec![];
let mut child_panes = vec![];
let mut child_floating_panes = vec![];
if let Some(children) = kdl_children_nodes!(layout_node) {
self.populate_global_cwd(layout_node)?;
self.populate_pane_templates(children, &kdl_layout)?;
self.populate_tab_templates(children)?;
for child in children {
self.populate_layout_child(
child,
&mut child_tabs,
&mut child_panes,
&mut child_floating_panes,
)?;
}
}
if !child_tabs.is_empty() {
let has_more_than_one_focused_tab = child_tabs
.iter()
.filter(|(is_focused, _, _, _)| *is_focused)
.count()
> 1;
if has_more_than_one_focused_tab {
return Err(ConfigError::new_layout_kdl_error(
"Only one tab can be focused".into(),
kdl_layout.span().offset(),
kdl_layout.span().len(),
));
}
let focused_tab_index = child_tabs
.iter()
.position(|(is_focused, _, _, _)| *is_focused);
let child_tabs: Vec<(Option<String>, PaneLayout, Vec<FloatingPanesLayout>)> =
child_tabs
.drain(..)
.map(
|(_is_focused, tab_name, pane_layout, floating_panes_layout)| {
(tab_name, pane_layout, floating_panes_layout)
},
)
.collect();
self.layout_with_tabs(child_tabs, focused_tab_index)
} else if !child_panes.is_empty() {
self.layout_with_one_tab(child_panes, child_floating_panes)
} else {
self.layout_with_one_pane(child_floating_panes)
}
}
}