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
This commit is contained in:
Aram Drevekenin 2022-10-28 13:03:37 +02:00 committed by GitHub
parent eed9541a74
commit c97b972383
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 342 additions and 22 deletions

View file

@ -25,6 +25,7 @@ fn main() {
cwd, cwd,
floating, floating,
name, name,
close_on_exit,
})) = opts.command })) = opts.command
{ {
let command_cli_action = CliAction::NewPane { let command_cli_action = CliAction::NewPane {
@ -33,6 +34,7 @@ fn main() {
cwd, cwd,
floating, floating,
name, name,
close_on_exit,
}; };
commands::send_action_to_session(command_cli_action, opts.session); commands::send_action_to_session(command_cli_action, opts.session);
std::process::exit(0); std::process::exit(0);

View file

@ -1821,6 +1821,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() {
cwd: None, cwd: None,
floating: false, floating: false,
name: None, name: None,
close_on_exit: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,
@ -1859,6 +1860,7 @@ pub fn send_cli_new_pane_action_with_split_direction() {
cwd: None, cwd: None,
floating: false, floating: false,
name: None, name: None,
close_on_exit: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,
@ -1897,6 +1899,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() {
cwd: Some("/some/folder".into()), cwd: Some("/some/folder".into()),
floating: false, floating: false,
name: None, name: None,
close_on_exit: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,

View file

@ -146,6 +146,10 @@ pub enum Sessions {
/// Name of the new pane /// Name of the new pane
#[clap(short, long, value_parser)] #[clap(short, long, value_parser)]
name: Option<String>, 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 /// Edit file with default $EDITOR / $VISUAL
#[clap(visible_alias = "e")] #[clap(visible_alias = "e")]
@ -246,6 +250,17 @@ pub enum CliAction {
/// Name of the new pane /// Name of the new pane
#[clap(short, long, value_parser)] #[clap(short, long, value_parser)]
name: Option<String>, 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 /// Open the specified file in a new zellij pane with your default EDITOR
Edit { Edit {

View file

@ -258,17 +258,19 @@ impl Action {
cwd, cwd,
floating, floating,
name, name,
close_on_exit,
} => { } => {
if !command.is_empty() { if !command.is_empty() {
let mut command = command.clone(); let mut command = command.clone();
let (command, args) = (PathBuf::from(command.remove(0)), command); let (command, args) = (PathBuf::from(command.remove(0)), command);
let cwd = cwd.or_else(|| std::env::current_dir().ok()); let cwd = cwd.or_else(|| std::env::current_dir().ok());
let hold_on_close = !close_on_exit;
let run_command_action = RunCommandAction { let run_command_action = RunCommandAction {
command, command,
args, args,
cwd, cwd,
direction, direction,
hold_on_close: true, hold_on_close,
}; };
if floating { if floating {
Ok(vec![Action::NewFloatingPane( Ok(vec![Action::NewFloatingPane(

View file

@ -130,6 +130,26 @@ impl Run {
_ => {}, // plugins aren't yet supported _ => {}, // 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)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]

View file

@ -268,6 +268,19 @@ fn layout_with_command_panes_and_cwd_and_args() {
assert_eq!(layout, expected_layout); 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, "layout_file_name".into(), None).unwrap();
assert_snapshot!(format!("{:#?}", layout));
}
#[test] #[test]
fn layout_with_plugin_panes() { fn layout_with_plugin_panes() {
let kdl_layout = r#" let kdl_layout = r#"
@ -1033,6 +1046,24 @@ fn args_override_args_in_template() {
assert_snapshot!(format!("{:#?}", layout)); assert_snapshot!(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, "layout_file_name".into(), None).unwrap();
assert_snapshot!(format!("{:#?}", layout));
}
#[test] #[test]
fn args_added_to_args_in_template() { fn args_added_to_args_in_template() {
let kdl_layout = r#" let kdl_layout = r#"
@ -1050,6 +1081,23 @@ fn args_added_to_args_in_template() {
assert_snapshot!(format!("{:#?}", layout)); assert_snapshot!(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, "layout_file_name".into(), None).unwrap();
assert_snapshot!(format!("{:#?}", layout));
}
#[test] #[test]
fn cwd_override_cwd_in_template() { fn cwd_override_cwd_in_template() {
let kdl_layout = r#" let kdl_layout = r#"
@ -1125,6 +1173,19 @@ fn error_on_bare_args_without_command() {
assert!(layout.is_err(), "error provided"); 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, "layout_file_name".into(), None);
assert!(layout.is_err(), "error provided");
}
#[test] #[test]
fn error_on_bare_args_in_template_without_command() { fn error_on_bare_args_in_template_without_command() {
let kdl_layout = r#" let kdl_layout = r#"
@ -1139,6 +1200,20 @@ fn error_on_bare_args_in_template_without_command() {
assert!(layout.is_err(), "error provided"); 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, "layout_file_name".into(), None);
assert!(layout.is_err(), "error provided");
}
#[test] #[test]
fn pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd() { fn pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd() {
let kdl_layout = r#" let kdl_layout = r#"

View file

@ -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,
},
),
}

View file

@ -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,
},
),
}

View file

@ -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,
},
),
}

View file

@ -53,6 +53,7 @@ impl<'a> KdlLayoutParser<'a> {
|| word == "children" || word == "children"
|| word == "tab" || word == "tab"
|| word == "args" || word == "args"
|| word == "close_on_exit"
|| word == "borderless" || word == "borderless"
|| word == "focus" || word == "focus"
|| word == "name" || word == "name"
@ -70,6 +71,7 @@ impl<'a> KdlLayoutParser<'a> {
|| property_name == "edit" || property_name == "edit"
|| property_name == "cwd" || property_name == "cwd"
|| property_name == "args" || property_name == "args"
|| property_name == "close_on_exit"
|| property_name == "split_direction" || property_name == "split_direction"
|| property_name == "pane" || property_name == "pane"
|| property_name == "children" || property_name == "children"
@ -237,22 +239,28 @@ impl<'a> KdlLayoutParser<'a> {
.map(|c| PathBuf::from(c)); .map(|c| PathBuf::from(c));
let cwd = self.parse_cwd(pane_node)?; let cwd = self.parse_cwd(pane_node)?;
let args = self.parse_args(pane_node)?; let args = self.parse_args(pane_node)?;
match (command, edit, cwd, args, is_template) { let close_on_exit =
(None, None, Some(cwd), _, _) => Ok(Some(Run::Cwd(cwd))), kdl_get_bool_property_or_child_value_with_error!(pane_node, "close_on_exit");
(None, _, _, Some(_args), false) => Err(ConfigError::new_layout_kdl_error( if !is_template {
"args can only be set if a command was specified".into(), self.assert_no_bare_attributes_in_pane_node(
pane_node.span().offset(), &command,
pane_node.span().len(), &args,
)), &close_on_exit,
(Some(command), None, cwd, args, _) => Ok(Some(Run::Command(RunCommand { 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, command,
args: args.unwrap_or_else(|| vec![]), args: args.unwrap_or_else(|| vec![]),
cwd, cwd,
hold_on_close: true, hold_on_close,
}))), }))),
(None, Some(edit), Some(cwd), _, _) => Ok(Some(Run::EditFile(cwd.join(edit), None))), (None, Some(edit), Some(cwd)) => Ok(Some(Run::EditFile(cwd.join(edit), None))),
(None, Some(edit), None, _, _) => Ok(Some(Run::EditFile(edit, None))), (None, Some(edit), None) => Ok(Some(Run::EditFile(edit, None))),
(Some(_command), Some(_edit), _, _, _) => Err(ConfigError::new_layout_kdl_error( (Some(_command), Some(_edit), _) => Err(ConfigError::new_layout_kdl_error(
"cannot have both a command and an edit instruction for the same pane".into(), "cannot have both a command and an edit instruction for the same pane".into(),
pane_node.span().offset(), pane_node.span().offset(),
pane_node.span().len(), 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") let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name")
.map(|name| name.to_string()); .map(|name| name.to_string());
let args = self.parse_args(kdl_node)?; 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 split_size = self.parse_split_size(kdl_node)?;
let run = self.parse_command_plugin_or_edit_block_for_template(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, &run,
&pane_template.run, &pane_template.run,
&args, &args,
&close_on_exit,
kdl_node, kdl_node,
)?; )?;
self.insert_children_to_pane_template( self.insert_children_to_pane_template(
@ -384,13 +395,12 @@ impl<'a> KdlLayoutParser<'a> {
pane_template_kdl_node, pane_template_kdl_node,
)?; )?;
pane_template.run = Run::merge(&pane_template.run, &run); pane_template.run = Run::merge(&pane_template.run, &run);
if let (Some(Run::Command(pane_template_run_command)), Some(args)) = if let Some(pane_template_run_command) = pane_template.run.as_mut() {
(pane_template.run.as_mut(), args) // we need to do this because panes consuming a pane_templates
{ // can have bare args without a command
if !args.is_empty() { pane_template_run_command.add_args(args);
pane_template_run_command.args = args.clone(); pane_template_run_command.add_close_on_exit(close_on_exit);
} };
}
if let Some(borderless) = borderless { if let Some(borderless) = borderless {
pane_template.borderless = borderless; pane_template.borderless = borderless;
} }
@ -584,11 +594,12 @@ impl<'a> KdlLayoutParser<'a> {
} }
false false
} }
fn assert_no_bare_args_in_pane_node_with_template( fn assert_no_bare_attributes_in_pane_node_with_template(
&self, &self,
pane_run: &Option<Run>, pane_run: &Option<Run>,
pane_template_run: &Option<Run>, pane_template_run: &Option<Run>,
args: &Option<Vec<String>>, args: &Option<Vec<String>>,
close_on_exit: &Option<bool>,
pane_node: &KdlNode, pane_node: &KdlNode,
) -> Result<(), ConfigError> { ) -> Result<(), ConfigError> {
if let (None, None, true) = (pane_run, pane_template_run, args.is_some()) { if let (None, None, true) = (pane_run, pane_template_run, args.is_some()) {
@ -597,6 +608,37 @@ impl<'a> KdlLayoutParser<'a> {
pane_node 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(()) Ok(())
} }
fn assert_one_children_block( fn assert_one_children_block(