use super::super::layout::*; use insta::assert_snapshot; #[cfg(not(windows))] fn normalize_layout_debug(s: String) -> String { s } #[cfg(windows)] fn normalize_layout_debug(s: String) -> String { // On Windows, PathBuf's Debug output uses `\\` (escaped backslash). // Replace `\\\\` (two escaped backslashes in Debug repr) with `/` // so that snapshots match Unix-recorded baselines. s.replace("\\\\", "/") } #[test] fn empty_layout() { let kdl_layout = "layout"; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some((TiledPaneLayout::default(), vec![])), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_one_pane() { let kdl_layout = r#" layout { pane } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![TiledPaneLayout::default()], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_multiple_panes() { let kdl_layout = r#" layout { pane pane pane } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![ TiledPaneLayout::default(), TiledPaneLayout::default(), TiledPaneLayout::default(), ], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_nested_panes() { let kdl_layout = r#" layout { pane split_direction="Vertical" { pane pane } pane { pane pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![ TiledPaneLayout { children_split_direction: SplitDirection::Vertical, children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()], ..Default::default() }, TiledPaneLayout { children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()], ..Default::default() }, ], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_floating_panes() { let kdl_layout = r#" layout { floating_panes { pane pane { x 10 y "10%" width 10 height "10%" } pane x=10 y="10%" pane command="htop" } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout::default(), vec![ FloatingPaneLayout::default(), FloatingPaneLayout { x: Some(PercentOrFixed::Fixed(10)), y: Some(PercentOrFixed::Percent(10)), width: Some(PercentOrFixed::Fixed(10)), height: Some(PercentOrFixed::Percent(10)), ..Default::default() }, FloatingPaneLayout { x: Some(PercentOrFixed::Fixed(10)), y: Some(PercentOrFixed::Percent(10)), ..Default::default() }, FloatingPaneLayout { run: Some(Run::Command(RunCommand { command: PathBuf::from("htop"), hold_on_close: true, ..Default::default() })), ..Default::default() }, ], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_mixed_panes_and_floating_panes() { let kdl_layout = r#" layout { pane pane floating_panes { pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()], ..Default::default() }, vec![FloatingPaneLayout::default()], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_hidden_floating_panes() { let kdl_layout = r#" layout { tab hide_floating_panes=true { pane pane floating_panes { pane } } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { tabs: vec![( None, TiledPaneLayout { children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()], hide_floating_panes: true, ..Default::default() }, vec![FloatingPaneLayout::default()], )], template: Some((TiledPaneLayout::default(), vec![])), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_floating_panes_template() { let kdl_layout = r#" layout { pane_template name="my_cool_template" { x 10 y "10%" } pane floating_panes { pane my_cool_template } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![TiledPaneLayout::default()], ..Default::default() }, vec![ FloatingPaneLayout::default(), FloatingPaneLayout { x: Some(PercentOrFixed::Fixed(10)), y: Some(PercentOrFixed::Percent(10)), ..Default::default() }, ], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_shared_tiled_and_floating_panes_template() { let kdl_layout = r#" layout { pane_template name="htop" { command "htop" } htop floating_panes { pane htop } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![TiledPaneLayout { run: Some(Run::Command(RunCommand { command: PathBuf::from("htop"), hold_on_close: true, ..Default::default() })), ..Default::default() }], ..Default::default() }, vec![ FloatingPaneLayout::default(), FloatingPaneLayout { run: Some(Run::Command(RunCommand { command: PathBuf::from("htop"), hold_on_close: true, ..Default::default() })), ..Default::default() }, ], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_tabs_and_floating_panes() { let kdl_layout = r#" layout { tab { floating_panes { pane } } tab { floating_panes { pane pane } } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn layout_with_tabs() { let kdl_layout = r#" layout { tab } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { tabs: vec![(None, TiledPaneLayout::default(), vec![])], template: Some((TiledPaneLayout::default(), vec![])), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_nested_differing_tabs() { let kdl_layout = r#" layout { tab split_direction="Vertical" { pane pane pane } tab { pane pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { tabs: vec![ ( None, TiledPaneLayout { children_split_direction: SplitDirection::Vertical, children: vec![ TiledPaneLayout::default(), TiledPaneLayout::default(), TiledPaneLayout::default(), ], ..Default::default() }, vec![], // floating panes ), ( None, TiledPaneLayout { children_split_direction: SplitDirection::Horizontal, children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()], ..Default::default() }, vec![], // floating panes ), ], template: Some((TiledPaneLayout::default(), vec![])), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_panes_in_different_mixed_split_sizes() { let kdl_layout = r#" layout { pane size=1; pane size="10%"; pane; pane size=2; } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![ TiledPaneLayout { split_size: Some(SplitSize::Fixed(1)), ..Default::default() }, TiledPaneLayout { split_size: Some(SplitSize::Percent(10)), ..Default::default() }, TiledPaneLayout { split_size: None, ..Default::default() }, TiledPaneLayout { split_size: Some(SplitSize::Fixed(2)), ..Default::default() }, ], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_command_panes() { let kdl_layout = r#" layout { pane command="htop" } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![TiledPaneLayout { run: Some(Run::Command(RunCommand { command: PathBuf::from("htop"), hold_on_close: true, ..Default::default() })), ..Default::default() }], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_command_panes_and_cwd() { let kdl_layout = r#" layout { pane command="htop" cwd="/path/to/my/cwd" } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![TiledPaneLayout { run: Some(Run::Command(RunCommand { command: PathBuf::from("htop"), cwd: Some(PathBuf::from("/path/to/my/cwd")), hold_on_close: true, ..Default::default() })), ..Default::default() }], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_command_panes_and_cwd_and_args() { let kdl_layout = r#" layout { pane command="htop" cwd="/path/to/my/cwd" { args "-h" "-v" } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![TiledPaneLayout { run: Some(Run::Command(RunCommand { command: PathBuf::from("htop"), cwd: Some(PathBuf::from("/path/to/my/cwd")), args: vec![String::from("-h"), String::from("-v")], hold_on_close: true, ..Default::default() })), ..Default::default() }], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_command_panes_and_close_on_exit() { let kdl_layout = r#" layout { pane command="htop" { close_on_exit true } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn layout_with_command_panes_and_start_suspended() { let kdl_layout = r#" layout { pane command="htop" { start_suspended true } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn layout_with_plugin_panes() { let kdl_layout = r#" layout { pane { plugin location="zellij:tab-bar" } pane { plugin location="file:/path/to/my/plugin.wasm" } pane { plugin location="zellij:status-bar" { config_key_1 "config_value_1" "2" true } } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let mut expected_plugin_configuration = BTreeMap::new(); expected_plugin_configuration.insert("config_key_1".to_owned(), "config_value_1".to_owned()); expected_plugin_configuration.insert("2".to_owned(), "true".to_owned()); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![ TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin { location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), _allow_exec_host_cmd: false, configuration: Default::default(), ..Default::default() }))), ..Default::default() }, TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin { location: RunPluginLocation::File(PathBuf::from( "/path/to/my/plugin.wasm", )), _allow_exec_host_cmd: false, configuration: Default::default(), ..Default::default() }))), ..Default::default() }, TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin { location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), _allow_exec_host_cmd: false, configuration: PluginUserConfiguration(expected_plugin_configuration), ..Default::default() }))), ..Default::default() }, ], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_borderless_panes() { let kdl_layout = r#" layout { pane borderless=true } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![TiledPaneLayout { borderless: Some(true), ..Default::default() }], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_focused_panes() { let kdl_layout = r#" layout { pane focus=true } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![TiledPaneLayout { focus: Some(true), ..Default::default() }], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_pane_names() { let kdl_layout = r#" layout { pane name="my awesome pane" } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![TiledPaneLayout { name: Some("my awesome pane".into()), ..Default::default() }], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_tab_names() { let kdl_layout = r#" layout { tab name="my cool tab name 1" tab name="my cool tab name 2" } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { tabs: vec![ ( Some("my cool tab name 1".into()), TiledPaneLayout { children: vec![], ..Default::default() }, vec![], // floating panes ), ( Some("my cool tab name 2".into()), TiledPaneLayout { children: vec![], ..Default::default() }, vec![], // floating panes ), ], template: Some((TiledPaneLayout::default(), vec![])), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_focused_tab() { let kdl_layout = r#" layout { tab tab focus=true tab } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { tabs: vec![ (None, TiledPaneLayout::default(), vec![]), (None, TiledPaneLayout::default(), vec![]), (None, TiledPaneLayout::default(), vec![]), ], template: Some((TiledPaneLayout::default(), vec![])), focused_tab_index: Some(1), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_tab_templates() { let kdl_layout = r#" layout { tab_template name="one-above-one-below" { pane children pane } one-above-one-below name="my first tab" split_direction="Vertical" { pane pane } one-above-one-below name="my second tab" { pane pane } one-above-one-below } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { tabs: vec![ ( Some("my first tab".into()), TiledPaneLayout { children_split_direction: SplitDirection::Horizontal, children: vec![ TiledPaneLayout::default(), TiledPaneLayout { children_split_direction: SplitDirection::Vertical, children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()], ..Default::default() }, TiledPaneLayout::default(), ], ..Default::default() }, vec![], // floating panes ), ( Some("my second tab".into()), TiledPaneLayout { children_split_direction: SplitDirection::Horizontal, children: vec![ TiledPaneLayout::default(), TiledPaneLayout { children_split_direction: SplitDirection::Horizontal, children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()], ..Default::default() }, TiledPaneLayout::default(), ], ..Default::default() }, vec![], // floating panes ), ( None, TiledPaneLayout { children_split_direction: SplitDirection::Horizontal, children: vec![ TiledPaneLayout::default(), TiledPaneLayout::default(), TiledPaneLayout::default(), ], ..Default::default() }, vec![], // floating panes ), ], template: Some((TiledPaneLayout::default(), vec![])), ..Default::default() }; assert_eq!(layout, expected_layout); } #[test] fn layout_with_default_tab_template() { let kdl_layout = r#" layout { default_tab_template { pane children pane } tab name="my first tab" split_direction="Vertical" { pane pane } tab name="my second tab" { pane pane } tab } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn layout_with_new_tab_template() { let kdl_layout = r#" layout { new_tab_template { pane split_direction="vertical" { pane pane } } tab name="my first tab" split_direction="Vertical" { pane pane } tab name="my second tab" { pane pane } tab } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn layout_with_pane_templates() { let kdl_layout = r#" layout { pane_template name="left-and-right" split_direction="Vertical" { pane children pane } left-and-right { pane } left-and-right { pane pane } left-and-right split_direction="Vertical" { pane pane } left-and-right } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn layout_with_tab_and_pane_templates() { let kdl_layout = r#" layout { tab_template name="left-right-and-htop" { left-and-right { pane command="htop" } } pane_template name="left-and-right" split_direction="Vertical" { pane children pane } left-right-and-htop } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn layout_with_nested_pane_templates() { let kdl_layout = r#" layout { pane_template name="left-and-right" split_direction="Vertical" { pane children up-and-down pane } pane_template name="up-and-down" split_direction="Horizontal" { pane pane } left-and-right } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn layout_with_nested_branched_pane_templates() { let kdl_layout = r#" layout { pane_template name="left-and-right" split_direction="Vertical" { pane children up-and-down three-horizontal-panes } pane_template name="three-horizontal-panes" split_direction="Horizontal" { pane pane pane } pane_template name="up-and-down" split_direction="Horizontal" { pane pane } left-and-right } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn circular_dependency_pane_templates_error() { let kdl_layout = r#" layout { pane_template name="one" split_direction="Vertical" { pane children two } pane_template name="two" split_direction="Horizontal" { pane three pane } pane_template name="three" split_direction="Horizontal" { one } one } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "circular dependency detected"); } #[test] fn children_not_as_first_child_of_tab_template() { let kdl_layout = r#" layout { tab_template name="horizontal-with-vertical-top" { pane split_direction="Vertical" { pane children } pane } horizontal-with-vertical-top name="my tab" { pane pane } horizontal-with-vertical-top } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn error_on_more_than_one_children_block_in_tab_template() { let kdl_layout = r#" layout { tab_template name="horizontal-with-vertical-top" { pane split_direction="Vertical" { pane children } children pane } horizontal-with-vertical-top name="my tab" { pane pane } horizontal-with-vertical-top } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!( layout.is_err(), "error provided for more than one children block" ); } #[test] fn children_not_as_first_child_of_pane_template() { let kdl_layout = r#" layout { pane_template name="horizontal-with-vertical-top" { pane split_direction="Vertical" { pane children } pane } horizontal-with-vertical-top name="my pane" { pane pane } horizontal-with-vertical-top } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn error_on_more_than_one_children_block_in_pane_template() { let kdl_layout = r#" layout { pane_template name="horizontal-with-vertical-top" { pane split_direction="Vertical" { pane children } children pane } horizontal-with-vertical-top name="my tab" { pane pane } horizontal-with-vertical-top } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!( layout.is_err(), "error provided for more than one children block" ); } #[test] fn combined_tab_and_pane_template_both_with_children() { let kdl_layout = r#" layout { tab_template name="horizontal-with-vertical-top" { vertical-sandwich { pane name="middle" } children } pane_template name="vertical-sandwich" split_direction="Vertical" { pane children pane } horizontal-with-vertical-top name="my tab" { pane pane } horizontal-with-vertical-top } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn layout_with_pane_excluded_from_sync() { let kdl_layout = r#" layout { pane exclude_from_sync=true } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn cannot_define_tab_template_name_with_space() { let kdl_layout = r#" layout { tab_template name="with space" { pane children pane } pane } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided for tab name with space"); } #[test] fn cannot_define_pane_template_name_with_space() { let kdl_layout = r#" layout { pane_template name="with space" { pane children pane } pane } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided for tab name with space"); } #[test] fn cannot_define_panes_and_tabs_on_same_level() { let kdl_layout = r#" layout { pane tab { pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!( layout.is_err(), "error provided for tab and pane on the same level" ); } #[test] fn cannot_define_tab_template_names_as_keywords() { let keywords = vec![ "pane", "layout", "pane_template", "tab_template", "default_tab_template", "command", "plugin", "children", "tab", ]; for keyword in keywords { let kdl_layout = format!( " layout {{ tab_template name=\"{}\" {{ pane children pane }} pane }} ", keyword ); let layout = Layout::from_kdl(&kdl_layout, Some("layout_file_name".into()), None, None); assert!( layout.is_err(), "{}", format!( "error provided for tab template name with keyword: {}", keyword ) ); } } #[test] fn cannot_define_pane_template_names_as_keywords() { let keywords = vec![ "pane", "layout", "pane_template", "tab_template", "command", "plugin", "children", "tab", ]; for keyword in keywords { let kdl_layout = format!( " layout {{ pane_template name=\"{}\" {{ pane children pane }} pane }} ", keyword ); let layout = Layout::from_kdl(&kdl_layout, Some("layout_file_name".into()), None, None); assert!( layout.is_err(), "{}", format!( "error provided for pane template name with keyword: {}", keyword ) ); } } #[test] fn error_on_multiple_layout_nodes_in_file() { let kdl_layout = format!( " layout layout " ); let layout_error = Layout::from_kdl(&kdl_layout, Some("layout_file_name".into()), None, None).unwrap_err(); assert_snapshot!(normalize_layout_debug(format!("{:?}", layout_error))); } #[test] fn error_on_unknown_layout_node() { let kdl_layout = format!( " layout {{ pane i_am_not_a_proper_node pane }} " ); let layout_error = Layout::from_kdl(&kdl_layout, Some("layout_file_name".into()), None, None).unwrap_err(); assert_snapshot!(normalize_layout_debug(format!("{:?}", layout_error))); } #[test] fn error_on_unknown_layout_pane_property() { let kdl_layout = format!( " layout {{ pane spit_size=1 }} " ); let layout_error = Layout::from_kdl(&kdl_layout, Some("layout_file_name".into()), None, None).unwrap_err(); assert_snapshot!(normalize_layout_debug(format!("{:?}", layout_error))); } #[test] fn error_on_unknown_layout_pane_template_property() { let kdl_layout = format!( " layout {{ pane_template name=\"my_cool_template\" spit_size=1 }} " ); let layout_error = Layout::from_kdl(&kdl_layout, Some("layout_file_name".into()), None, None).unwrap_err(); assert_snapshot!(normalize_layout_debug(format!("{:?}", layout_error))); } #[test] fn error_on_unknown_layout_tab_property() { let kdl_layout = format!( " layout {{ tab spit_size=1 }} " ); let layout_error = Layout::from_kdl(&kdl_layout, Some("layout_file_name".into()), None, None).unwrap_err(); assert_snapshot!(normalize_layout_debug(format!("{:?}", layout_error))); } #[test] fn error_on_unknown_layout_tab_template_property() { let kdl_layout = format!( " layout {{ tab_template name=\"my_cool_template\" spit_size=1 }} " ); let layout_error = Layout::from_kdl(&kdl_layout, Some("layout_file_name".into()), None, None).unwrap_err(); assert_snapshot!(normalize_layout_debug(format!("{:?}", layout_error))); } #[test] fn error_on_pane_templates_without_a_name() { let kdl_layout = format!( " layout {{ pane_template {{ pane children pane }} }} " ); let layout_error = Layout::from_kdl(&kdl_layout, Some("layout_file_name".into()), None, None).unwrap_err(); assert_snapshot!(normalize_layout_debug(format!("{:?}", layout_error))); } #[test] fn error_on_tab_templates_without_a_name() { let kdl_layout = format!( " layout {{ tab_template {{ pane children pane }} }} " ); let layout_error = Layout::from_kdl(&kdl_layout, Some("layout_file_name".into()), None, None).unwrap_err(); assert_snapshot!(normalize_layout_debug(format!("{:?}", layout_error))); } #[test] fn error_on_more_than_one_focused_tab() { let kdl_layout = r#" layout { tab focus=true tab focus=true tab } "#; let layout_error = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap_err(); assert_snapshot!(normalize_layout_debug(format!("{:?}", layout_error))); } #[test] fn args_override_args_in_template() { let kdl_layout = r#" layout { pane_template name="tail" { command "tail" args "-f" "/tmp/foo" } tail tail { args "-f" "/tmp/bar" } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn close_on_exit_overrides_close_on_exit_in_template() { let kdl_layout = r#" layout { pane_template name="tail" { command "tail" close_on_exit false } tail tail { close_on_exit true } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn args_added_to_args_in_template() { let kdl_layout = r#" layout { pane_template name="tail" { command "tail" } tail tail { args "-f" "/tmp/bar" } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn close_on_exit_added_to_close_on_exit_in_template() { let kdl_layout = r#" layout { pane_template name="tail" { command "tail" } tail tail { close_on_exit true } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn cwd_override_cwd_in_template() { let kdl_layout = r#" layout { pane_template name="tail" { command "tail" cwd "/tmp" } tail tail { cwd "/" } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn cwd_added_to_cwd_in_template() { let kdl_layout = r#" layout { pane_template name="tail" { command "tail" } tail tail { cwd "/home" } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn error_on_mixed_command_and_child_panes() { let kdl_layout = r#" layout { pane command="tail" { pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided"); } #[test] fn error_on_mixed_cwd_and_child_panes() { let kdl_layout = r#" layout { pane cwd="/tmp" { pane pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided"); } #[test] fn error_on_bare_args_without_command() { let kdl_layout = r#" layout { pane { args "-f" } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided"); } #[test] fn error_on_bare_close_on_exit_without_command() { let kdl_layout = r#" layout { pane { close_on_exit true } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided"); } #[test] fn error_on_bare_args_in_template_without_command() { let kdl_layout = r#" layout { pane_template name="my_template" my_template { args "--help" } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided"); } #[test] fn error_on_bare_close_on_exit_in_template_without_command() { let kdl_layout = r#" layout { pane_template name="my_template" my_template { close_on_exit true } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided"); } #[test] fn pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="tail" { command "tail" cwd "bar" } tail command="pwd" { cwd "foo" } // pane should have /tmp/foo and not /tmp/bar as cwd } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="tail" { command "tail" cwd "bar" } tail command="pwd" // pane should have /tmp/bar as its cwd with the pwd command } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="tail" { command "tail" } tail command="pwd" { cwd "bar" } // pane should have /tmp/bar as its cwd with the pwd command } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="tail" { command "tail" cwd "foo" } tail { cwd "bar" } // pane should have /tmp/bar as its cwd with the tail command } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn pane_template_command_without_cwd_receives_its_consumers_bare_cwd() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="tail" { command "tail" } tail { cwd "bar" } // pane should have /tmp/bar as its cwd with the tail command } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn pane_template_with_bare_cwd_overriden_by_its_consumers_bare_cwd() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="tail" { cwd "foo" } tail { cwd "bar" } // pane should have /tmp/foo without a command } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn pane_template_with_bare_propagated_to_its_consumer_command_without_cwd() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="tail" { cwd "foo" } tail command="tail" // pane should have /tmp/foo with the tail command } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn pane_template_with_bare_propagated_to_its_consumer_command_with_cwd() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="tail" { cwd "foo" } tail command="tail" { cwd "bar" } // pane should have /tmp/bar with the tail command } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn pane_template_with_bare_propagated_to_its_consumer_edit() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="tail" { cwd "foo" } tail edit="bar" // pane should have /tmp/foo/bar with the edit file variant } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn pane_template_with_command_propagated_to_its_consumer_edit() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="tail" command="not-vim" { cwd "foo" } tail edit="bar" // pane should have /tmp/foo/bar with the edit file variant } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn global_cwd_given_to_panes_without_cwd() { let kdl_layout = r#" layout { cwd "/tmp" pane pane command="tail" // both should have the /tmp cwd } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn global_cwd_prepended_to_panes_with_cwd() { let kdl_layout = r#" layout { cwd "/tmp" pane cwd="foo" // should be /tmp/foo pane command="tail" cwd="/home/foo" // should be /home/foo because its an absolute path } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn global_cwd_passed_from_layout_constructor() { // this is used by the new-tab cli action with --cwd let kdl_layout = r#" layout { pane pane command="tail" // both should have the /tmp cwd } "#; let layout = Layout::from_kdl( kdl_layout, Some("layout_file_name".into()), None, Some(PathBuf::from("/tmp")), ) .unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file() { // this is used by the new-tab cli action with --cwd let kdl_layout = r#" layout { cwd "/home" pane pane command="tail" // both should have the /tmp cwd } "#; let layout = Layout::from_kdl( kdl_layout, Some("layout_file_name".into()), None, Some(PathBuf::from("/tmp")), ) .unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn global_cwd_with_tab_cwd_given_to_panes_without_cwd() { let kdl_layout = r#" layout { cwd "/tmp" tab cwd="./foo" { pane pane command="tail" } // both should have the /tmp/foo cwd } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn tab_cwd_given_to_panes_without_cwd() { let kdl_layout = r#" layout { tab cwd="/tmp" { pane pane command="tail" } // both should have the /tmp cwd } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn tab_cwd_prepended_to_panes_with_cwd() { let kdl_layout = r#" layout { tab cwd="/tmp" { pane cwd="./foo" pane command="tail" cwd="./foo" } // both should have the /tmp/foo cwd } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd() { let kdl_layout = r#" layout { cwd "/tmp" tab cwd="./foo" { pane // should have /tmp/foo pane command="tail" cwd="./bar" // should have /tmp/foo/bar } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_pane_templates() { let kdl_layout = r#" layout { cwd "/tmp" pane_template name="my_pane_template" { pane // should have /tmp/foo pane command="tail" cwd="./bar" // should have /tmp/foo/bar children } tab cwd="./foo" { my_pane_template { pane // should have /tmp/foo } } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_tab_templates() { let kdl_layout = r#" layout { cwd "/tmp" tab_template name="my_tab_template" { pane // should have /tmp/foo pane command="tail" cwd="./bar" // should have /tmp/foo/bar children } my_tab_template cwd="./foo" { pane // should have /tmp/foo } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn can_load_swap_layouts_from_a_different_file() { let kdl_layout = r#" layout { // here we define a tab_template in the main layout and later make sure we can sue it // in the swap layouts tab_template name="ui" { pane size=1 borderless=true { plugin location="zellij:tab-bar" } children pane size=2 borderless=true { plugin location="zellij:status-bar" } } pane } "#; let kdl_swap_layout = r#" swap_tiled_layout name="vertical" { ui max_panes=5 { pane split_direction="vertical" { pane pane { children; } } } ui max_panes=8 { pane split_direction="vertical" { pane { children; } pane { pane; pane; pane; pane; } } } ui max_panes=12 { pane split_direction="vertical" { pane { children; } pane { pane; pane; pane; pane; } pane { pane; pane; pane; pane; } } } } "#; let layout = Layout::from_kdl( kdl_layout, Some("layout_file_name".into()), Some(("swap_layout_file_name".into(), kdl_swap_layout)), None, ) .unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn can_define_stacked_children_for_pane_node() { let kdl_layout = r#" layout { pane stacked=true { pane pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn can_define_stacked_children_for_pane_template() { let kdl_layout = r#" layout { pane_template name="stack" stacked=true { children } stack { pane pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn can_define_a_stack_with_an_expanded_pane() { let kdl_layout = r#" layout { pane stacked=true { pane pane expanded=true pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_snapshot!(normalize_layout_debug(format!("{:#?}", layout))); } #[test] fn cannot_define_stacked_panes_for_bare_node() { let kdl_layout = r#" layout { pane stacked=true } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided for tab name with space"); } #[test] fn cannot_define_an_expanded_pane_outside_of_a_stack() { let kdl_layout = r#" layout { pane { pane pane expanded=true } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided for tab name with space"); } #[test] fn cannot_define_stacked_panes_with_vertical_split_direction() { let kdl_layout = r#" layout { pane stacked=true split_direction="vertical" { pane pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided for tab name with space"); } #[test] fn cannot_define_stacked_panes_with_grandchildren() { let kdl_layout = r#" layout { pane stacked=true { pane { pane pane } pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided for tab name with space"); } #[test] fn cannot_define_stacked_panes_with_grandchildren_in_pane_template() { let kdl_layout = r#" layout { pane_template name="stack" stacked=true { children } stack { pane pane { pane pane } } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "error provided for tab name with space"); } #[test] fn run_plugin_location_parsing() { let kdl_layout = r#" layout { pane { plugin location="zellij:tab-bar" } pane { plugin location="file:/path/to/my/plugin.wasm" } pane { plugin location="file:plugin.wasm" } pane { plugin location="file:relative/with space/plugin.wasm" } pane { plugin location="file:///absolute/with space/plugin.wasm" } pane { plugin location="file:c:/absolute/windows/plugin.wasm" } pane { plugin location="filepicker" } pane { plugin location="https://example.com/plugin.wasm" } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); let expected_layout = Layout { template: Some(( TiledPaneLayout { children: vec![ TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), configuration: Default::default(), ..Default::default() }))), ..Default::default() }, TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from( "/path/to/my/plugin.wasm", )), configuration: Default::default(), ..Default::default() }))), ..Default::default() }, TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from("plugin.wasm")), configuration: Default::default(), ..Default::default() }))), ..Default::default() }, TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from( "relative/with space/plugin.wasm", )), configuration: Default::default(), ..Default::default() }))), ..Default::default() }, TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from( "/absolute/with space/plugin.wasm", )), configuration: Default::default(), ..Default::default() }))), ..Default::default() }, TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from( "c:/absolute/windows/plugin.wasm", )), configuration: Default::default(), ..Default::default() }))), ..Default::default() }, TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::Alias(PluginAlias { name: "filepicker".to_owned(), configuration: Some(PluginUserConfiguration::default()), ..Default::default() }))), ..Default::default() }, TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::Remote(String::from( "https://example.com/plugin.wasm", )), configuration: Default::default(), ..Default::default() }))), ..Default::default() }, ], ..Default::default() }, vec![], )), ..Default::default() }; assert_eq!(layout, expected_layout); } #[cfg(unix)] #[test] fn env_var_expansion() { let raw_layout = r#" layout { // cwd tests + composition cwd "$TEST_GLOBAL_CWD" pane cwd="relative" // -> /abs/path/relative pane cwd="/another/abs" // -> /another/abs pane cwd="$TEST_LOCAL_CWD" // -> /another/abs pane cwd="$TEST_RELATIVE" // -> /abs/path/relative pane command="ls" cwd="$TEST_ABSOLUTE" // -> /somewhere pane edit="file.rs" cwd="$TEST_ABSOLUTE" // -> /somewhere/file.rs pane edit="file.rs" cwd="~/backup" // -> /home/aram/backup/file.rs // other paths pane command="~/backup/executable" // -> /home/aram/backup/executable pane edit="~/backup/foo.txt" // -> /home/aram/backup/foo.txt } "#; let env_vars = [ ("TEST_GLOBAL_CWD", "/abs/path"), ("TEST_LOCAL_CWD", "/another/abs"), ("TEST_RELATIVE", "relative"), ("TEST_ABSOLUTE", "/somewhere"), ("HOME", "/home/aram"), ]; let mut old_vars = Vec::new(); // set environment variables for test, keeping track of existing values. for (key, value) in env_vars { old_vars.push((key, std::env::var(key).ok())); std::env::set_var(key, value); } let layout = Layout::from_kdl(raw_layout, Some("layout_file_name".into()), None, None); // restore environment. for (key, opt) in old_vars { match opt { Some(value) => std::env::set_var(key, &value), None => std::env::remove_var(key), } } let layout = layout.unwrap(); assert_snapshot!(normalize_layout_debug(format!("{layout:#?}"))); } #[test] fn env_var_missing() { std::env::remove_var("SOME_UNIQUE_VALUE"); let kdl_layout = r#" layout { cwd "$SOME_UNIQUE_VALUE" pane cwd="relative" } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err(), "invalid env var lookup should fail"); } #[test] fn layout_node_with_tab_name() { let kdl_layout = r#" layout name="my-layout" { pane } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_eq!(layout.tabs.len(), 1); assert_eq!(layout.tabs[0].0, Some("my-layout".to_string())); } #[test] fn layout_node_with_split_direction() { let kdl_layout = r#" layout split_direction="vertical" { pane pane } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_eq!( layout.tabs[0].1.children_split_direction, SplitDirection::Vertical ); } #[test] fn layout_node_with_hide_floating_panes() { let kdl_layout = r#" layout hide_floating_panes=true { pane } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_eq!(layout.tabs[0].1.hide_floating_panes, true); } #[test] fn layout_node_with_cwd() { let kdl_layout = r#" layout cwd="/tmp" { pane } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); // Verify cwd was applied - check the first pane's run property assert!(layout.tabs.len() > 0); // The cwd should be propagated to children if let Some(Run::Cwd(path)) = &layout.tabs[0].1.children[0].run { assert_eq!(path.to_str(), Some("/tmp")); } else { panic!("Expected cwd to be set on pane"); } } #[test] fn layout_node_with_multiple_tab_properties() { let kdl_layout = r#" layout name="test" split_direction="vertical" hide_floating_panes=true { pane pane } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); assert_eq!(layout.tabs[0].0, Some("test".to_string())); assert_eq!( layout.tabs[0].1.children_split_direction, SplitDirection::Vertical ); assert_eq!(layout.tabs[0].1.hide_floating_panes, true); } #[test] fn layout_node_with_name_and_explicit_tabs_errors() { let kdl_layout = r#" layout name="test" { tab { pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err()); let error_message = format!("{:?}", layout.unwrap_err()); assert!(error_message.contains( "name' property on the layout node is treated as a tab name and cannot be used when there are explicit tab nodes" )); } #[test] fn layout_node_with_split_direction_and_explicit_tabs_errors() { let kdl_layout = r#" layout split_direction="vertical" { tab { pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err()); let error_message = format!("{:?}", layout.unwrap_err()); assert!(error_message.contains( "split_direction' property on the layout node is treated as a tab property and cannot be used when there are explicit tab nodes" )); } #[test] fn layout_node_with_hide_floating_panes_and_explicit_tabs_errors() { let kdl_layout = r#" layout hide_floating_panes=true { tab { pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None); assert!(layout.is_err()); let error_message = format!("{:?}", layout.unwrap_err()); assert!(error_message.contains( "hide_floating_panes' property on the layout node is treated as a tab property and cannot be used when there are explicit tab nodes" )); } #[test] fn layout_node_with_name_and_only_floating_panes() { let kdl_layout = r#" layout name="floating-only" { floating_panes { pane } } "#; let layout = Layout::from_kdl(kdl_layout, Some("layout_file_name".into()), None, None).unwrap(); // Verify the tab name was applied assert_eq!(layout.tabs.len(), 1); assert_eq!(layout.tabs[0].0, Some("floating-only".to_string())); } #[test] fn floating_pane_coordinates_allows_zero_percent_for_x_y() { use crate::data::FloatingPaneCoordinates; use crate::input::layout::PercentOrFixed; let coords = FloatingPaneCoordinates::new( Some("0%".to_string()), Some("0%".to_string()), Some("50%".to_string()), Some("50%".to_string()), None, Some(false), ); assert!(coords.is_some()); let coords = coords.unwrap(); assert_eq!(coords.x, Some(PercentOrFixed::Percent(0))); assert_eq!(coords.y, Some(PercentOrFixed::Percent(0))); assert_eq!(coords.width, Some(PercentOrFixed::Percent(50))); assert_eq!(coords.height, Some(PercentOrFixed::Percent(50))); } #[test] fn floating_pane_coordinates_rejects_zero_percent_for_width() { use crate::data::FloatingPaneCoordinates; use crate::input::layout::PercentOrFixed; let coords = FloatingPaneCoordinates::new( Some("10%".to_string()), Some("10%".to_string()), Some("0%".to_string()), Some("50%".to_string()), None, Some(false), ); // Should have coords but width will be None due to rejection assert!(coords.is_some()); let coords = coords.unwrap(); assert!(coords.width.is_none()); assert_eq!(coords.height, Some(PercentOrFixed::Percent(50))); } #[test] fn floating_pane_coordinates_rejects_zero_percent_for_height() { use crate::data::FloatingPaneCoordinates; use crate::input::layout::PercentOrFixed; let coords = FloatingPaneCoordinates::new( Some("10%".to_string()), Some("10%".to_string()), Some("50%".to_string()), Some("0%".to_string()), None, Some(false), ); // Should have coords but height will be None due to rejection assert!(coords.is_some()); let coords = coords.unwrap(); assert_eq!(coords.width, Some(PercentOrFixed::Percent(50))); assert!(coords.height.is_none()); } #[test] fn floating_pane_coordinates_static_zero_works() { use crate::data::FloatingPaneCoordinates; use crate::input::layout::PercentOrFixed; let coords = FloatingPaneCoordinates::new( Some("0".to_string()), Some("0".to_string()), Some("100".to_string()), Some("100".to_string()), None, Some(false), ); assert!(coords.is_some()); let coords = coords.unwrap(); assert_eq!(coords.x, Some(PercentOrFixed::Fixed(0))); assert_eq!(coords.y, Some(PercentOrFixed::Fixed(0))); assert_eq!(coords.width, Some(PercentOrFixed::Fixed(100))); assert_eq!(coords.height, Some(PercentOrFixed::Fixed(100))); } #[test] fn floating_pane_coordinates_one_percent_works() { use crate::data::FloatingPaneCoordinates; use crate::input::layout::PercentOrFixed; let coords = FloatingPaneCoordinates::new( Some("1%".to_string()), Some("1%".to_string()), Some("50%".to_string()), Some("50%".to_string()), None, Some(false), ); assert!(coords.is_some()); let coords = coords.unwrap(); assert_eq!(coords.x, Some(PercentOrFixed::Percent(1))); assert_eq!(coords.y, Some(PercentOrFixed::Percent(1))); assert_eq!(coords.width, Some(PercentOrFixed::Percent(50))); assert_eq!(coords.height, Some(PercentOrFixed::Percent(50))); } #[test] fn tiled_pane_still_rejects_zero_percent() { use crate::input::layout::SplitSize; use std::str::FromStr; // Verify SplitSize::from_str still rejects 0% let result = SplitSize::from_str("0%"); assert!(result.is_err()); // But 1% should work let result = SplitSize::from_str("1%"); assert!(result.is_ok()); } // --- sl0p: `arrangement` on a `children` placeholder --------------------- // // These assert the resulting GEOMETRY rather than the tree shape: the tree is // an implementation detail, "did I get a grid" is the actual contract. /// Panes as (x, y, cols, rows), sorted, for a layout applied to `space`. #[cfg(test)] fn arranged_geoms(kdl: &str, pane_count: usize, cols: usize, rows: usize) -> Vec<(usize, usize, usize, usize)> { let layout = Layout::from_kdl(kdl, Some("file.kdl".into()), None, None).unwrap(); // a layout written without an explicit `tab` node lands in `template` let tab_layout = layout.template.as_ref().unwrap().0.clone(); // percent-with-inner, exactly as a real display area is built // (PaneGeom::from(&Size)); a fixed-size space rejects implicitly sized children let space = PaneGeom::from(&crate::pane_size::Size { rows, cols }); let mut geoms: Vec<(usize, usize, usize, usize)> = tab_layout .position_panes_in_space(&space, Some(pane_count), false, false) .unwrap() .iter() .map(|(_, g)| (g.x, g.y, g.cols.as_usize(), g.rows.as_usize())) .collect(); geoms.sort(); geoms } #[test] fn sl0p_arrangement_grid_lays_four_panes_out_two_by_two() { let kdl = r#" layout { pane arrangement="grid" { children } } "#; let geoms = arranged_geoms(kdl, 4, 80, 20); assert_eq!( geoms, vec![ (0, 0, 40, 10), (0, 10, 40, 10), (40, 0, 40, 10), (40, 10, 40, 10), ], "four panes should form a 2x2 grid" ); } #[test] fn sl0p_arrangement_grid_wraps_an_uneven_pane_count() { // 5 panes -> ceil(sqrt(5)) = 3 columns, so rows of 3 and 2 let kdl = r#" layout { pane arrangement="grid" { children } } "#; let geoms = arranged_geoms(kdl, 5, 90, 20); let top: Vec<_> = geoms.iter().filter(|(_, y, _, _)| *y == 0).collect(); let bottom: Vec<_> = geoms.iter().filter(|(_, y, _, _)| *y != 0).collect(); assert_eq!(top.len(), 3, "first row should hold 3 panes, got {:?}", geoms); assert_eq!( bottom.len(), 2, "second row should hold the remaining 2, got {:?}", geoms ); assert!( bottom.iter().all(|(_, _, cols, _)| *cols > top[0].2), "the shorter row should share the width between fewer panes, got {:?}", geoms ); } #[test] fn sl0p_arrangement_master_stack_gives_the_master_the_larger_share() { let kdl = r#" layout { pane arrangement="master-stack" { children } } "#; let geoms = arranged_geoms(kdl, 3, 100, 20); // master: full height, 60% of the width assert!( geoms.contains(&(0, 0, 60, 20)), "expected a 60%-wide full-height master, got {:?}", geoms ); // the other two split the remaining 40% vertically assert_eq!( geoms .iter() .filter(|(x, _, cols, rows)| *x == 60 && *cols == 40 && *rows == 10) .count(), 2, "expected two stacked panes beside the master, got {:?}", geoms ); } #[test] fn sl0p_arrangement_absent_still_produces_flat_siblings() { // the historical behaviour must be untouched when no arrangement is given let kdl = r#" layout { pane split_direction="vertical" { children } } "#; let geoms = arranged_geoms(kdl, 4, 80, 20); assert!( geoms.iter().all(|(_, y, _, rows)| *y == 0 && *rows == 20), "without an arrangement the panes should stay in one flat row, got {:?}", geoms ); } #[test] fn sl0p_arrangement_rejects_an_unknown_value() { let kdl = r#" layout { pane arrangement="fibonacci" { children } } "#; let err = Layout::from_kdl(kdl, Some("file.kdl".into()), None, None) .expect_err("an unknown arrangement must be a config error, not silently ignored"); let msg = format!("{:?}", err); assert!( msg.contains("fibonacci") && msg.contains("grid"), "the error should name the bad value and list the valid ones, got {}", msg ); } #[test] fn sl0p_arrangement_without_a_children_node_is_rejected() { // an arrangement only acts on panes inserted at a `children` placeholder, // so without one it would quietly do nothing let kdl = r#" layout { pane arrangement="grid" { pane pane } } "#; assert!( Layout::from_kdl(kdl, Some("file.kdl".into()), None, None).is_err(), "an arrangement without a children node must be rejected" ); } // --- sl0p: preserving plugin panes when synthesizing a layout ------------ #[cfg(test)] fn plugin_pane_layout() -> TiledPaneLayout { TiledPaneLayout { run: Some(Run::Plugin(RunPluginOrAlias::Alias(PluginAlias::new( "status-bar", &None, None, )))), split_size: Some(SplitSize::Fixed(1)), ..Default::default() } } #[test] fn sl0p_with_content_replaced_by_keeps_plugin_panes_pinned() { // REGRESSION: a synthesized layout describes only where the user's panes go. // Applied bare to a tab that also has a status bar, the status bar became an // ordinary member and was stranded mid-screen. Real swap layouts avoid this // because their `tab` node is expanded with the default tab template, which // carries those slots -- a runtime-built layout has to do the same. // // Mirrors the default layout: `default_tab_template { children; pane size=1 // borderless { plugin } }`. let base = TiledPaneLayout { children: vec![TiledPaneLayout::default(), plugin_pane_layout()], ..Default::default() }; let replacement = TiledPaneLayout::single_stack(); let result = base.with_content_replaced_by(&replacement); assert_eq!(result.children.len(), 2, "both slots should survive"); assert!( result.children[0].children_are_stacked, "the content slot should have become the stack, got {:?}", result.children[0] ); assert!( result.children[1].is_plugin_pane(), "the plugin pane must be preserved, got {:?}", result.children[1] ); assert_eq!( result.children[1].split_size, Some(SplitSize::Fixed(1)), "and keep its pinned size" ); } #[test] fn sl0p_with_content_replaced_by_collapses_several_content_panes_into_one() { // The replacement accounts for every user pane by itself, so the other // content leaves must be dropped rather than left as empty slots that would // steal screen space. let base = TiledPaneLayout { children: vec![ TiledPaneLayout::default(), TiledPaneLayout::default(), plugin_pane_layout(), TiledPaneLayout::default(), ], ..Default::default() }; let result = base.with_content_replaced_by(&TiledPaneLayout::single_grid()); assert_eq!( result.children.len(), 2, "three content panes collapse to one, plugin kept: {:?}", result.children ); assert_eq!( result.children[0].children_arrangement, Some(PaneArrangement::Grid) ); assert!(result.children[1].is_plugin_pane()); } #[test] fn sl0p_with_content_replaced_by_falls_back_when_there_is_no_content_slot() { // A layout of nothing but plugins has nowhere to put the panes; returning it // unchanged would silently drop every user pane. let base = TiledPaneLayout { children: vec![plugin_pane_layout()], ..Default::default() }; let result = base.with_content_replaced_by(&TiledPaneLayout::single_stack()); assert!( result.children_are_stacked, "should fall back to the replacement itself, got {:?}", result ); } // --- sl0p: status bar layout ------------------------------------------------- #[test] fn sl0p_status_bar_button_spans_are_where_they_are_drawn() { // The renderer draws from this layout and the mouse hit-test resolves against // it. Computing that geometry twice is exactly how the stacked-pane picker // ended up selecting the wrong pane, so assert every drawn column resolves // back to the button drawn there. let buttons = vec![ StatusBarButton { id: String::from("approve"), label: String::from("Approve"), }, StatusBarButton { id: String::from("reject"), label: String::from("Reject"), }, ]; for inner_cols in 20..120usize { let layout = layout_status_bar(inner_cols, Some("waiting for review"), &buttons, Default::default()); for span in &layout.buttons { assert!( span.start + span.width <= inner_cols, "{} cols: span {:?} runs past the row", inner_cols, span ); for col in span.start..span.start + span.width { assert_eq!( layout.button_at(col), Some(span.id.as_str()), "{} cols: column {} draws {:?} but resolves elsewhere", inner_cols, col, span.id ); } // just outside must not resolve to it if span.start > 0 { assert_ne!(layout.button_at(span.start - 1), Some(span.id.as_str())); } } // buttons must never overlap each other for a in &layout.buttons { for b in &layout.buttons { if a.id >= b.id { continue; } assert!( a.start + a.width <= b.start || b.start + b.width <= a.start, "{} cols: {:?} overlaps {:?}", inner_cols, a, b ); } } } } #[test] fn sl0p_status_bar_drops_buttons_that_cannot_fit_rather_than_overflowing() { // A narrow pane must not draw a half button, and must never report a click on // one it did not draw. let buttons: Vec = (0..6) .map(|i| StatusBarButton { id: format!("id{}", i), label: String::from("LongishLabel"), }) .collect(); let layout = layout_status_bar(20, Some("status"), &buttons, Default::default()); assert!( layout.buttons.len() < buttons.len(), "expected some buttons to be dropped at 20 cols, got {:?}", layout.buttons ); for span in &layout.buttons { assert!(span.start + span.width <= 20); } } #[test] fn sl0p_status_bar_text_yields_space_to_buttons() { // Buttons are actionable, the text is informational -- so the text truncates // rather than pushing a button off the row. let buttons = vec![StatusBarButton { id: String::from("go"), label: String::from("Go"), }]; let long = "x".repeat(200); let layout = layout_status_bar(30, Some(&long), &buttons, Default::default()); assert_eq!(layout.buttons.len(), 1, "the button must survive"); assert!( layout.text.chars().count() < 30, "text should have been truncated, got {} chars", layout.text.chars().count() ); assert!(layout.text.ends_with('…'), "truncation should be visible"); } #[test] fn sl0p_status_bar_wire_format_round_trips() { // Serialised into the session layout and parsed back on resurrection. let buttons = vec![ StatusBarButton { id: String::from("a-1"), label: String::from("semi;colon"), }, StatusBarButton { id: String::from("b_2"), label: String::from("percent%and:colon"), }, ]; let wire = serialize_status_bar_buttons(&buttons); let parsed = parse_status_bar_buttons(wire.split(';')); assert_eq!(parsed, buttons, "wire form: {}", wire); } #[test] fn sl0p_status_bar_button_style_changes_rendering_and_spans_together() { // The style changes how wide a button is, and the hit-test resolves against // the same layout -- so an indexed button must be clickable across exactly // the columns it is drawn on, not the columns a boxed one would have used. use crate::input::theme::StatusBarButtonStyle; let buttons: Vec = ["Approve", "Reject", "Defer"] .iter() .enumerate() .map(|(i, label)| StatusBarButton { id: format!("id{}", i), label: label.to_string(), }) .collect(); let boxed = layout_status_bar(80, None, &buttons, StatusBarButtonStyle::Boxed); let indexed = layout_status_bar(80, None, &buttons, StatusBarButtonStyle::Indexed); assert_eq!(boxed.buttons[0].rendered, "[Approve]"); assert_eq!(indexed.buttons[0].rendered, "1:Approve"); assert_eq!(indexed.buttons[2].rendered, "3:Defer", "1-based index"); for layout in [&boxed, &indexed] { for span in &layout.buttons { assert_eq!( span.width, span.rendered.chars().count(), "span width must match what is drawn: {:?}", span ); for col in span.start..span.start + span.width { assert_eq!(layout.button_at(col), Some(span.id.as_str())); } } } } #[test] fn sl0p_pane_purpose_is_sanitised_to_a_bounded_token() { use crate::input::layout::sanitize_pane_purpose; // Kept: the vocabulary consumers actually use. assert_eq!( sanitize_pane_purpose("dev-server"), Some("dev-server".to_string()) ); assert_eq!( sanitize_pane_purpose("pi:agent/scratch_1.v2"), Some("pi:agent/scratch_1.v2".to_string()) ); // A purpose arrives from a layout file in someone else's repo, or from any // program in the pane via OSC. It is pasted into shell and JSON pipelines, so // quoting, whitespace, separators and control characters must not survive. assert_eq!( sanitize_pane_purpose("rm -rf /; echo \"pwned\"\n"), Some("rm-rf/echopwned".to_string()) ); assert_eq!( sanitize_pane_purpose("$(id)`id`"), Some("idid".to_string()), "shell metacharacters are dropped, the identifier bytes are not" ); // Absent and empty must not be able to diverge. assert_eq!(sanitize_pane_purpose(""), None); assert_eq!(sanitize_pane_purpose(" "), None); assert_eq!(sanitize_pane_purpose("!!!"), None); // Bounded. assert_eq!(sanitize_pane_purpose(&"a".repeat(200)).unwrap().len(), 64); } #[test] fn sl0p_pane_purpose_parses_from_kdl_for_tiled_and_floating_panes() { let kdl = r#" layout { tab name="work" { pane name="server" purpose="dev-server" pane name="plain" floating_panes { pane name="notes" purpose="human:notes" } } } "#; let layout = Layout::from_kdl(kdl, Some("w.kdl".into()), None, None) .expect("a layout with purposes should parse"); let (_, tiled, floating) = layout.tabs().into_iter().next().expect("one tab"); let purposes: Vec> = tiled.children.iter().map(|c| c.purpose.clone()).collect(); assert_eq!( purposes, vec![Some("dev-server".to_string()), None], "declared purposes land on the right panes, and absence stays absent" ); assert_eq!( floating.iter().map(|f| f.purpose.clone()).collect::>(), vec![Some("human:notes".to_string())], "floating panes carry a purpose too" ); } #[test] fn sl0p_unknown_pane_properties_are_still_rejected() { // Adding `purpose` must not turn the pane property allowlist into "accept // anything" -- a typo in a workspace file should still be a loud error. let kdl = r#" layout { pane name="server" porpoise="dev-server" } "#; assert!( Layout::from_kdl(kdl, Some("w.kdl".into()), None, None).is_err(), "an unknown pane property should be a parse error" ); } #[test] fn sl0p_tab_root_layout_is_never_itself_a_pane() { // Tab-level `purpose` is stored on the tab's root TiledPaneLayout, reusing // the pane field. That is only safe if the root is never applied AS a pane -- // otherwise a tab purpose would leak onto a pane. Checked for the tightest // case: a tab holding exactly one pane. let kdl = r#" layout { tab name="solo" purpose="agent-work" { pane name="only" } } "#; let layout = Layout::from_kdl(kdl, Some("w.kdl".into()), None, None).expect("parses"); let (_, tiled, _) = layout.tabs().into_iter().next().expect("one tab"); assert_eq!( tiled.purpose.as_deref(), Some("agent-work"), "the tab's purpose lands on the tab's root layout" ); assert_eq!(tiled.children.len(), 1, "the pane is a CHILD of the root"); assert_eq!( tiled.children[0].purpose, None, "the tab purpose must not leak onto the pane inside it" ); }