diff options
author | Aram Drevekenin <aram@poor.dev> | 2022-10-28 13:03:37 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-28 13:03:37 +0200 |
commit | c97b972383d50ae6db750d4d7f2441232e41ba4c (patch) | |
tree | 448c6f3d626e7c405ec1e54c2be2ccf5a6bd2be7 | |
parent | eed9541a74879e1ec683beda13ccfda7e63bfa88 (diff) |
feat(command-panes): optionally allow panes to be closed on exit (#1869)
* feat(cli): allow option to close command pane on exit
* feat(layouts): allow option to close command panes on exit
* style(fmt): rustfmt
10 files changed, 342 insertions, 22 deletions
diff --git a/src/main.rs b/src/main.rs index 3df61c42e..3ed592c53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ fn main() { cwd, floating, name, + close_on_exit, })) = opts.command { let command_cli_action = CliAction::NewPane { @@ -33,6 +34,7 @@ fn main() { cwd, floating, name, + close_on_exit, }; commands::send_action_to_session(command_cli_action, opts.session); std::process::exit(0); diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 287ffab4d..299bab6a3 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -1821,6 +1821,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() { cwd: None, floating: false, name: None, + close_on_exit: false, }; send_cli_action_to_server( &session_metadata, @@ -1859,6 +1860,7 @@ pub fn send_cli_new_pane_action_with_split_direction() { cwd: None, floating: false, name: None, + close_on_exit: false, }; send_cli_action_to_server( &session_metadata, @@ -1897,6 +1899,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() { cwd: Some("/some/folder".into()), floating: false, name: None, + close_on_exit: false, }; send_cli_action_to_server( &session_metadata, diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 0525c0380..6f0ac59a7 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -146,6 +146,10 @@ pub enum Sessions { /// Name of the new pane #[clap(short, long, value_parser)] name: Option<String>, + + /// Close the pane immediately when its command exits + #[clap(short, long, value_parser, default_value("false"), takes_value(false))] + close_on_exit: bool, }, /// Edit file with default $EDITOR / $VISUAL #[clap(visible_alias = "e")] @@ -246,6 +250,17 @@ pub enum CliAction { /// Name of the new pane #[clap(short, long, value_parser)] name: Option<String>, + + /// Close the pane immediately when its command exits + #[clap( + short, + long, + value_parser, + default_value("false"), + takes_value(false), + requires("command") + )] + close_on_exit: bool, }, /// Open the specified file in a new zellij pane with your default EDITOR Edit { diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index a9ab37053..51ead67b3 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -258,17 +258,19 @@ impl Action { cwd, floating, name, + close_on_exit, } => { if !command.is_empty() { let mut command = command.clone(); let (command, args) = (PathBuf::from(command.remove(0)), command); let cwd = cwd.or_else(|| std::env::current_dir().ok()); + let hold_on_close = !close_on_exit; let run_command_action = RunCommandAction { command, args, cwd, direction, - hold_on_close: true, + hold_on_close, }; if floating { Ok(vec![Action::NewFloatingPane( diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 8d0dc104d..fa50237a9 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -130,6 +130,26 @@ impl Run { _ => {}, // plugins aren't yet supported } } + pub fn add_args(&mut self, args: Option<Vec<String>>) { + // overrides the args of a Run::Command if they are Some + // and not empty + if let Some(args) = args { + if let Run::Command(run_command) = self { + if !args.is_empty() { + run_command.args = args.clone(); + } + } + } + } + pub fn add_close_on_exit(&mut self, close_on_exit: Option<bool>) { + // overrides the args of a Run::Command if they are Some + // and not empty + if let Some(close_on_exit) = close_on_exit { + if let Run::Command(run_command) = self { + run_command.hold_on_close = !close_on_exit; + } + } + } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] diff --git a/zellij-utils/src/input/unit/layout_test.rs b/zellij-utils/src/input/unit/layout_test.rs index 1addb9382..9614eef46 100644 --- a/zellij-utils/src/input/unit/layout_test.rs +++ b/zellij-utils/src/input/unit/layout_test.rs @@ -269,6 +269,19 @@ fn layout_with_command_panes_and_cwd_and_args() { } #[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, "layout_file_name".into(), None).unwrap(); + assert_snapshot!(format!("{:#?}", layout)); +} + +#[test] fn layout_with_plugin_panes() { let kdl_layout = r#" layout { @@ -1034,6 +1047,24 @@ fn args_override_args_in_template() { } #[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, "layout_file_name".into(), None).unwrap(); + assert_snapshot!(format!("{:#?}", layout)); +} + +#[test] fn args_added_to_args_in_template() { let kdl_layout = r#" layout { @@ -1051,6 +1082,23 @@ fn args_added_to_args_in_template() { } #[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, "layout_file_name".into(), None).unwrap(); + assert_snapshot!(format!("{:#?}", layout)); +} + +#[test] fn cwd_override_cwd_in_template() { let kdl_layout = r#" layout { @@ -1126,6 +1174,19 @@ fn error_on_bare_args_without_command() { } #[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, "layout_file_name".into(), None); + assert!(layout.is_err(), "error provided"); +} + +#[test] fn error_on_bare_args_in_template_without_command() { let kdl_layout = r#" layout { @@ -1140,6 +1201,20 @@ fn error_on_bare_args_in_template_without_command() { } #[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, "layout_file_name".into(), 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 { diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_added_to_close_on_exit_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_added_to_close_on_exit_in_template.snap new file mode 100644 index 000000000..da83cc62c --- /dev/null +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_added_to_close_on_exit_in_template.snap @@ -0,0 +1,60 @@ +--- +source: zellij-utils/src/input/./unit/layout_test.rs +assertion_line: 1098 +expression: "format!(\"{:#?}\", layout)" +--- +Layout { + tabs: [], + focused_tab_index: None, + template: Some( + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [ + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Command( + RunCommand { + command: "tail", + args: [], + cwd: None, + hold_on_close: true, + }, + ), + ), + borderless: false, + focus: None, + external_children_index: None, + }, + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Command( + RunCommand { + command: "tail", + args: [], + cwd: None, + hold_on_close: false, + }, + ), + ), + borderless: false, + focus: None, + external_children_index: None, + }, + ], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ), +} diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_overrides_close_on_exit_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_overrides_close_on_exit_in_template.snap new file mode 100644 index 000000000..3cb80cf74 --- /dev/null +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_overrides_close_on_exit_in_template.snap @@ -0,0 +1,60 @@ +--- +source: zellij-utils/src/input/./unit/layout_test.rs +assertion_line: 1064 +expression: "format!(\"{:#?}\", layout)" +--- +Layout { + tabs: [], + focused_tab_index: None, + template: Some( + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [ + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Command( + RunCommand { + command: "tail", + args: [], + cwd: None, + hold_on_close: true, + }, + ), + ), + borderless: false, + focus: None, + external_children_index: None, + }, + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Command( + RunCommand { + command: "tail", + args: [], + cwd: None, + hold_on_close: false, + }, + ), + ), + borderless: false, + focus: None, + external_children_index: None, + }, + ], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ), +} diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_close_on_exit.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_close_on_exit.snap new file mode 100644 index 000000000..69e3d7f1f --- /dev/null +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_close_on_exit.snap @@ -0,0 +1,41 @@ +--- +source: zellij-utils/src/input/./unit/layout_test.rs +assertion_line: 281 +expression: "format!(\"{:#?}\", layout)" +--- +Layout { + tabs: [], + focused_tab_index: None, + template: Some( + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [ + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: Some( + Command( + RunCommand { + command: "htop", + args: [], + cwd: None, + hold_on_close: false, + }, + ), + ), + borderless: false, + focus: None, + external_children_index: None, + }, + ], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ), +} diff --git a/zellij-utils/src/kdl/kdl_layout_parser.rs b/zellij-utils/src/kdl/kdl_layout_parser.rs index ba3e1f204..b92c6a1b2 100644 --- a/zellij-utils/src/kdl/kdl_layout_parser.rs +++ b/zellij-utils/src/kdl/kdl_layout_parser.rs @@ -53,6 +53,7 @@ impl<'a> KdlLayoutParser<'a> { || word == "children" || word == "tab" || word == "args" + || word == "close_on_exit" || word == "borderless" || word == "focus" || word == "name" @@ -70,6 +71,7 @@ impl<'a> KdlLayoutParser<'a> { || property_name == "edit" || property_name == "cwd" || property_name == "args" + || property_name == "close_on_exit" || property_name == "split_direction" || property_name == "pane" || property_name == "children" @@ -237,22 +239,28 @@ impl<'a> KdlLayoutParser<'a> { .map(|c| PathBuf::from(c)); let cwd = self.parse_cwd(pane_node)?; let args = self.parse_args(pane_node)?; - match (command, edit, cwd, args, is_template) { - (None, None, Some(cwd), _, _) => Ok(Some(Run::Cwd(cwd))), - (None, _, _, Some(_args), false) => 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(), - )), - (Some(command), None, cwd, args, _) => Ok(Some(Run::Command(RunCommand { + let close_on_exit = + kdl_get_bool_property_or_child_value_with_error!(pane_node, "close_on_exit"); + if !is_template { + self.assert_no_bare_attributes_in_pane_node( + &command, + &args, + &close_on_exit, + pane_node, + )?; + } + let hold_on_close = close_on_exit.map(|c| !c).unwrap_or(true); + 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: true, + hold_on_close, }))), - (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( + (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(), @@ -370,12 +378,15 @@ impl<'a> KdlLayoutParser<'a> { 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 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_args_in_pane_node_with_template( + self.assert_no_bare_attributes_in_pane_node_with_template( &run, &pane_template.run, &args, + &close_on_exit, kdl_node, )?; self.insert_children_to_pane_template( @@ -384,13 +395,12 @@ impl<'a> KdlLayoutParser<'a> { pane_template_kdl_node, )?; pane_template.run = Run::merge(&pane_template.run, &run); - if let (Some(Run::Command(pane_template_run_command)), Some(args)) = - (pane_template.run.as_mut(), args) - { - if !args.is_empty() { - pane_template_run_command.args = args.clone(); - } - } + if let Some(pane_template_run_command) = pane_template.run.as_mut() { + // we need to do this because panes consuming a pane_templates + // 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); + }; if let Some(borderless) = borderless { pane_template.borderless = borderless; } @@ -584,11 +594,12 @@ impl<'a> KdlLayoutParser<'a> { } false } - fn assert_no_bare_args_in_pane_node_with_template( + 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>, pane_node: &KdlNode, ) -> Result<(), ConfigError> { if let (None, None, true) = (pane_run, pane_template_run, args.is_some()) { @@ -597,6 +608,37 @@ impl<'a> KdlLayoutParser<'a> { 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 + )); + } + Ok(()) + } + fn assert_no_bare_attributes_in_pane_node( + &self, + command: &Option<PathBuf>, + args: &Option<Vec<String>>, + close_on_exit: &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 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( |