// This is a converter from the old yaml layout to the new KDL layout. // // It is supposed to be mostly self containing - please refrain from adding to it, importing // from it or changing it use super::old_config::{config_yaml_to_config_kdl, OldConfigFromYaml, OldRunCommand}; use serde::{Deserialize, Serialize}; use std::vec::Vec; use std::{fmt, path::PathBuf}; use url::Url; fn pane_line( pane_name: Option<&String>, split_size: Option, focus: Option, borderless: bool, ) -> String { let mut pane_line = format!("pane"); if let Some(pane_name) = pane_name { // we use debug print here so that quotes and backslashes will be escaped pane_line.push_str(&format!(" name={:?}", pane_name)); } if let Some(split_size) = split_size { pane_line.push_str(&format!(" size={}", split_size)); } if let Some(focus) = focus { pane_line.push_str(&format!(" focus={}", focus)); } if borderless { pane_line.push_str(" borderless=true"); } pane_line } fn tab_line( pane_name: Option<&String>, split_size: Option, focus: Option, borderless: bool, ) -> String { let mut pane_line = format!("tab"); if let Some(pane_name) = pane_name { // we use debug print here so that quotes and backslashes will be escaped pane_line.push_str(&format!(" name={:?}", pane_name)); } if let Some(split_size) = split_size { pane_line.push_str(&format!(" size={}", split_size)); } if let Some(focus) = focus { pane_line.push_str(&format!(" focus={}", focus)); } if borderless { pane_line.push_str(" borderless=true"); } pane_line } fn pane_line_with_children( pane_name: Option<&String>, split_size: Option, focus: Option, borderless: bool, split_direction: OldDirection, ) -> String { let mut pane_line = format!("pane"); if let Some(pane_name) = pane_name { // we use debug print here so that quotes and backslashes will be escaped pane_line.push_str(&format!(" name={:?}", pane_name)); } if let Some(split_size) = split_size { pane_line.push_str(&format!(" size={}", split_size)); } if let Some(focus) = focus { pane_line.push_str(&format!(" focus={}", focus)); } pane_line.push_str(&format!(" split_direction=\"{}\"", split_direction)); if borderless { pane_line.push_str(" borderless=true"); } pane_line } fn pane_command_line( pane_name: Option<&String>, split_size: Option, focus: Option, borderless: bool, command: &PathBuf, ) -> String { let mut pane_line = format!("pane command={:?}", command); if let Some(pane_name) = pane_name { // we use debug print here so that quotes and backslashes will be escaped pane_line.push_str(&format!(" name={:?}", pane_name)); } if let Some(split_size) = split_size { pane_line.push_str(&format!(" size={}", split_size)); } if let Some(focus) = focus { pane_line.push_str(&format!(" focus={}", focus)); } if borderless { pane_line.push_str(" borderless=true"); } pane_line } fn tab_line_with_children( pane_name: Option<&String>, split_size: Option, focus: Option, borderless: bool, split_direction: OldDirection, ) -> String { let mut pane_line = format!("tab"); if let Some(pane_name) = pane_name { // we use debug print here so that quotes and backslashes will be escaped pane_line.push_str(&format!(" name={:?}", pane_name)); } if let Some(split_size) = split_size { pane_line.push_str(&format!(" size={}", split_size)); } if let Some(focus) = focus { pane_line.push_str(&format!(" focus={}", focus)); } pane_line.push_str(&format!(" split_direction=\"{}\"", split_direction)); if borderless { pane_line.push_str(" borderless=true"); } pane_line } fn stringify_template( template: &OldLayoutTemplate, indentation: String, has_no_tabs: bool, is_base: bool, ) -> String { let mut stringified = if is_base { String::new() } else { String::from("\n") }; if is_base && !template.parts.is_empty() && template.direction == OldDirection::Vertical { // we don't support specifying the split direction in the layout node // eg. layout split_direction="Vertical" { .. } <== this is not supported!! // so we need to add a child wrapper with the split direction instead: // layout { // pane split_direction="Vertical" { .. } // } let child_indentation = format!("{} ", &indentation); stringified.push_str(&stringify_template( template, child_indentation, has_no_tabs, false, )); } else if !template.parts.is_empty() { if !is_base { stringified.push_str(&format!( "{}{} {{", indentation, pane_line_with_children( template.pane_name.as_ref(), template.split_size, template.focus, template.borderless, template.direction ) )); } for part in &template.parts { let child_indentation = format!("{} ", &indentation); stringified.push_str(&stringify_template( &part, child_indentation, has_no_tabs, false, )); } if !is_base { stringified.push_str(&format!("\n{}}}", indentation)); } } else if template.body && !has_no_tabs { stringified.push_str(&format!("{}children", indentation)); } else { match template.run.as_ref() { Some(OldRunFromYaml::Plugin(plugin_from_yaml)) => { stringified.push_str(&format!( "{}{} {{\n", &indentation, pane_line( template.pane_name.as_ref(), template.split_size, template.focus, template.borderless ) )); stringified.push_str(&format!( "{} plugin location=\"{}\"\n", &indentation, plugin_from_yaml.location )); stringified.push_str(&format!("{}}}", &indentation)); }, Some(OldRunFromYaml::Command(command_from_yaml)) => { stringified.push_str(&format!( "{}{}", &indentation, &pane_command_line( template.pane_name.as_ref(), template.split_size, template.focus, template.borderless, &command_from_yaml.command ) )); if let Some(cwd) = command_from_yaml.cwd.as_ref() { stringified.push_str(&format!(" cwd={:?}", cwd)); } if !command_from_yaml.args.is_empty() { stringified.push_str(" {\n"); stringified.push_str(&format!( "{} args {}\n", &indentation, command_from_yaml .args .iter() // we use debug print here so that quotes and backslashes will be // escaped .map(|s| format!("{:?}", s)) .collect::>() .join(" ") )); stringified.push_str(&format!("{}}}", &indentation)); } }, None => { stringified.push_str(&format!( "{}{}", &indentation, pane_line( template.pane_name.as_ref(), template.split_size, template.focus, template.borderless ) )); }, }; } stringified } fn stringify_tabs(tabs: Vec) -> String { let mut stringified = String::new(); for tab in tabs { let child_indentation = String::from(" "); if !tab.parts.is_empty() { stringified.push_str(&format!( "\n{}{} {{", child_indentation, tab_line_with_children( tab.pane_name.as_ref(), tab.split_size, tab.focus, tab.borderless, tab.direction ) )); let tab_template = OldLayoutTemplate::from(tab); stringified.push_str(&stringify_template( &tab_template, child_indentation.clone(), true, true, )); stringified.push_str(&format!("\n{}}}", child_indentation)); } else { stringified.push_str(&format!( "\n{}{}", child_indentation, tab_line( tab.pane_name.as_ref(), tab.split_size, tab.focus, tab.borderless ) )); } } stringified } pub fn layout_yaml_to_layout_kdl(raw_yaml_layout: &str) -> Result { // returns the raw kdl config let layout_from_yaml: OldLayoutFromYamlIntermediate = serde_yaml::from_str(raw_yaml_layout) .map_err(|e| format!("Failed to parse yaml: {:?}", e))?; let mut kdl_layout = String::new(); kdl_layout.push_str("layout {"); let template = layout_from_yaml.template; let tabs = layout_from_yaml.tabs; let has_no_tabs = tabs.is_empty() || tabs.len() == 1 && tabs.get(0).map(|t| t.parts.is_empty()).unwrap_or(false); if has_no_tabs { let indentation = String::from(""); kdl_layout.push_str(&stringify_template( &template, indentation, has_no_tabs, true, )); } else { kdl_layout.push_str("\n default_tab_template {"); let indentation = String::from(" "); kdl_layout.push_str(&stringify_template( &template, indentation, has_no_tabs, true, )); kdl_layout.push_str("\n }"); kdl_layout.push_str(&stringify_tabs(tabs)); } kdl_layout.push_str("\n}"); let layout_config = config_yaml_to_config_kdl(raw_yaml_layout, true)?; if let Some(session_name) = layout_from_yaml.session.name { // we use debug print here so that quotes and backslashes will be escaped kdl_layout.push_str(&format!("\nsession_name {:?}", session_name)); if let Some(attach_to_session) = layout_from_yaml.session.attach { kdl_layout.push_str(&format!("\nattach_to_session {}", attach_to_session)); } } if !layout_config.is_empty() { kdl_layout.push('\n'); } kdl_layout.push_str(&layout_config); Ok(kdl_layout) } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)] pub enum OldDirection { #[serde(alias = "horizontal")] Horizontal, #[serde(alias = "vertical")] Vertical, } impl fmt::Display for OldDirection { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { Self::Horizontal => write!(f, "Horizontal"), Self::Vertical => write!(f, "Vertical"), } } } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] pub enum OldSplitSize { #[serde(alias = "percent")] Percent(u64), // 1 to 100 #[serde(alias = "fixed")] Fixed(usize), // An absolute number of columns or rows } impl fmt::Display for OldSplitSize { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { Self::Percent(percent) => write!(f, "\"{}%\"", percent), Self::Fixed(fixed_size) => write!(f, "{}", fixed_size), } } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub enum OldRunFromYaml { #[serde(rename = "plugin")] Plugin(OldRunPluginFromYaml), #[serde(rename = "command")] Command(OldRunCommand), } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct OldRunPluginFromYaml { #[serde(default)] pub _allow_exec_host_cmd: bool, pub location: Url, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(default)] pub struct OldLayoutFromYamlIntermediate { #[serde(default)] pub template: OldLayoutTemplate, #[serde(default)] pub borderless: bool, #[serde(default)] pub tabs: Vec, #[serde(default)] pub session: OldSessionFromYaml, #[serde(flatten)] pub config: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(default)] pub struct OldLayoutFromYaml { #[serde(default)] pub session: OldSessionFromYaml, #[serde(default)] pub template: OldLayoutTemplate, #[serde(default)] pub borderless: bool, #[serde(default)] pub tabs: Vec, } #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] pub struct OldSessionFromYaml { pub name: Option, #[serde(default = "default_as_some_true")] pub attach: Option, } fn default_as_some_true() -> Option { Some(true) } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct OldLayoutTemplate { pub direction: OldDirection, #[serde(default)] pub pane_name: Option, #[serde(default)] pub borderless: bool, #[serde(default)] pub parts: Vec, #[serde(default)] pub body: bool, pub split_size: Option, pub focus: Option, pub run: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct OldTabLayout { #[serde(default)] pub direction: OldDirection, pub pane_name: Option, #[serde(default)] pub borderless: bool, #[serde(default)] pub parts: Vec, pub split_size: Option, #[serde(default)] pub name: String, pub focus: Option, pub run: Option, } impl From for OldLayoutTemplate { fn from(old_tab_layout: OldTabLayout) -> Self { OldLayoutTemplate { direction: old_tab_layout.direction, pane_name: old_tab_layout.pane_name.clone(), borderless: old_tab_layout.borderless, parts: old_tab_layout.parts.iter().map(|o| o.into()).collect(), split_size: old_tab_layout.split_size, focus: old_tab_layout.focus, run: old_tab_layout.run.clone(), body: false, } } } impl From<&OldTabLayout> for OldLayoutTemplate { fn from(old_tab_layout: &OldTabLayout) -> Self { OldLayoutTemplate { direction: old_tab_layout.direction, pane_name: old_tab_layout.pane_name.clone(), borderless: old_tab_layout.borderless, parts: old_tab_layout.parts.iter().map(|o| o.into()).collect(), split_size: old_tab_layout.split_size, focus: old_tab_layout.focus, run: old_tab_layout.run.clone(), body: false, } } } impl From<&mut OldTabLayout> for OldLayoutTemplate { fn from(old_tab_layout: &mut OldTabLayout) -> Self { OldLayoutTemplate { direction: old_tab_layout.direction, pane_name: old_tab_layout.pane_name.clone(), borderless: old_tab_layout.borderless, parts: old_tab_layout.parts.iter().map(|o| o.into()).collect(), split_size: old_tab_layout.split_size, focus: old_tab_layout.focus, run: old_tab_layout.run.clone(), body: false, } } } impl From for OldLayoutFromYaml { fn from(layout_from_yaml_intermediate: OldLayoutFromYamlIntermediate) -> Self { Self { template: layout_from_yaml_intermediate.template, borderless: layout_from_yaml_intermediate.borderless, tabs: layout_from_yaml_intermediate.tabs, session: layout_from_yaml_intermediate.session, } } } impl From for OldLayoutFromYamlIntermediate { fn from(layout_from_yaml: OldLayoutFromYaml) -> Self { Self { template: layout_from_yaml.template, borderless: layout_from_yaml.borderless, tabs: layout_from_yaml.tabs, config: None, session: layout_from_yaml.session, } } } impl Default for OldLayoutFromYamlIntermediate { fn default() -> Self { OldLayoutFromYaml::default().into() } } impl Default for OldLayoutTemplate { fn default() -> Self { Self { direction: OldDirection::Horizontal, pane_name: None, body: false, borderless: false, parts: vec![OldLayoutTemplate { direction: OldDirection::Horizontal, pane_name: None, body: true, borderless: false, split_size: None, focus: None, run: None, parts: vec![], }], split_size: None, focus: None, run: None, } } } impl Default for OldDirection { fn default() -> Self { OldDirection::Horizontal } } // The unit test location. #[path = "./unit/convert_layout_tests.rs"] #[cfg(test)] mod convert_layout_test;