From abc700fc4d10d61c969ad94fa520d7d9336dcf14 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 1 Nov 2022 09:07:25 +0100 Subject: [PATCH] feat(command-panes): allow to start suspended (#1887) * feat(command-panes): allow panes to start suspended * style(fmt): remove unused code * style(fmt): rustfmt --- src/main.rs | 2 + src/tests/e2e/cases.rs | 19 ++ src/tests/e2e/remote_runner.rs | 3 +- ...__cases__send_command_through_the_cli.snap | 8 +- zellij-server/src/os_input_output.rs | 34 ++++ zellij-server/src/panes/floating_panes/mod.rs | 3 +- zellij-server/src/panes/grid.rs | 1 + zellij-server/src/panes/terminal_character.rs | 117 +++++++++++- zellij-server/src/panes/terminal_pane.rs | 52 ++++- zellij-server/src/panes/tiled_panes/mod.rs | 3 +- zellij-server/src/pty.rs | 179 +++++++++++++----- zellij-server/src/screen.rs | 84 +++++++- zellij-server/src/tab/mod.rs | 29 ++- .../src/tab/unit/tab_integration_tests.rs | 16 +- zellij-server/src/tab/unit/tab_tests.rs | 4 +- zellij-server/src/ui/pane_boundaries_frame.rs | 33 ++-- zellij-server/src/unit/screen_tests.rs | 9 +- ...send_cli_edit_action_with_line_number.snap | 2 +- ..._new_pane_action_with_command_and_cwd.snap | 4 +- zellij-utils/src/cli.rs | 14 ++ zellij-utils/src/input/actions.rs | 3 + zellij-utils/src/input/command.rs | 5 + zellij-utils/src/input/layout.rs | 11 +- zellij-utils/src/input/unit/layout_test.rs | 13 ++ ..._test__args_added_to_args_in_template.snap | 4 +- ..._test__args_override_args_in_template.snap | 4 +- ...it_added_to_close_on_exit_in_template.snap | 2 + ...t_overrides_close_on_exit_in_template.snap | 2 + ...ut_test__cwd_added_to_cwd_in_template.snap | 4 +- ...ut_test__cwd_override_cwd_in_template.snap | 4 +- ...epended_to_panes_with_and_without_cwd.snap | 3 +- ...ith_and_without_cwd_in_pane_templates.snap | 3 +- ...with_and_without_cwd_in_tab_templates.snap | 3 +- ...global_cwd_given_to_panes_without_cwd.snap | 3 +- ...al_cwd_passed_from_layout_constructor.snap | 3 +- ...r_overrides_global_cwd_in_layout_file.snap | 3 +- ...lobal_cwd_prepended_to_panes_with_cwd.snap | 3 +- ...th_tab_cwd_given_to_panes_without_cwd.snap | 3 +- ..._with_command_panes_and_close_on_exit.snap | 1 + ...ith_command_panes_and_start_suspended.snap | 42 ++++ ...t__layout_with_tab_and_pane_templates.snap | 3 +- ...s_overriden_by_its_consumers_bare_cwd.snap | 3 +- ...verriden_by_its_consumers_command_cwd.snap | 3 +- ..._consumer_command_does_not_have_a_cwd.snap | 3 +- ...cwd_is_overriden_by_its_consumers_cwd.snap | 3 +- ...t_cwd_receives_its_consumers_bare_cwd.snap | 3 +- ...ated_to_its_consumer_command_with_cwd.snap | 3 +- ...d_to_its_consumer_command_without_cwd.snap | 3 +- ...t__tab_cwd_given_to_panes_without_cwd.snap | 3 +- ...__tab_cwd_prepended_to_panes_with_cwd.snap | 3 +- zellij-utils/src/kdl/kdl_layout_parser.rs | 28 ++- zellij-utils/src/kdl/mod.rs | 47 ++--- 52 files changed, 668 insertions(+), 172 deletions(-) create mode 100644 zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_start_suspended.snap diff --git a/src/main.rs b/src/main.rs index 3ed592c53..6203f0f16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ fn main() { floating, name, close_on_exit, + start_suspended, })) = opts.command { let command_cli_action = CliAction::NewPane { @@ -35,6 +36,7 @@ fn main() { floating, name, close_on_exit, + start_suspended, }; commands::send_action_to_session(command_cli_action, opts.session); std::process::exit(0); diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 44551d997..359d74162 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -1914,6 +1914,25 @@ pub fn send_command_through_the_cli() { step_is_complete }, }) + .add_step(Step { + name: "Initial run of suspended command", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.snapshot_contains("") + && remote_terminal.cursor_position_is(0, 0) + // cursor does not appear in + // suspend_start panes + { + remote_terminal.send_key(&SPACE); // run script - here we use SPACE + // instead of the default ENTER because + // sending ENTER over SSH can be a little + // problematic (read: I couldn't get it + // to pass consistently) + step_is_complete = true + } + step_is_complete + }, + }) .add_step(Step { name: "Wait for command to run", instruction: |mut remote_terminal: RemoteTerminal| -> bool { diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index dd2c800da..9b007989c 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -346,7 +346,8 @@ impl RemoteTerminal { let mut channel = self.channel.lock().unwrap(); channel .write_all( - format!("{} run -- \"{}\"\n", ZELLIJ_EXECUTABLE_LOCATION, command).as_bytes(), + // note that this is run with the -s flag that suspends the command on startup + format!("{} run -s -- \"{}\"\n", ZELLIJ_EXECUTABLE_LOCATION, command).as_bytes(), ) .unwrap(); channel.flush().unwrap(); diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__send_command_through_the_cli.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__send_command_through_the_cli.snap index 84d33a037..0603ac26e 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__send_command_through_the_cli.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__send_command_through_the_cli.snap @@ -1,14 +1,14 @@ --- source: src/tests/e2e/cases.rs -assertion_line: 1968 +assertion_line: 1998 expression: last_snapshot --- Zellij (e2e-test)  Tab #1  ┌ Pane #1 ─────────────────────────────────────────────────┐┌ /usr/src/zellij/fixtures/append-echo-script.sh ──────────┐ │$ /usr/src/zellij/x86_64-unknown-linux-musl/release/zellij││foo │ -│ run -- "/usr/src/zellij/fixtures/append-echo-script.sh" ││foo │ -│$ ││█ │ -│ ││ │ +│ run -s -- "/usr/src/zellij/fixtures/append-echo-script.sh││foo │ +│" ││█ │ +│$ ││ │ │ ││ │ │ ││ │ │ ││ │ diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index 9a8ce0088..02f53dbe5 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -280,6 +280,7 @@ fn spawn_terminal( args, cwd: None, hold_on_close: false, + hold_on_start: false, } }, TerminalAction::RunCommand(command) => command, @@ -381,6 +382,10 @@ pub trait ServerOsApi: Send + Sync { quit_cb: Box, RunCommand) + Send>, // u32 is the exit status default_editor: Option, ) -> Result<(u32, RawFd, RawFd), SpawnTerminalError>; + // reserves a terminal id without actually opening a terminal + fn reserve_terminal_id(&self) -> Result { + unimplemented!() + } /// Read bytes from the standard output of the virtual terminal referred to by `fd`. fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result; /// Creates an `AsyncReader` that can be used to read from `fd` in an async context @@ -484,6 +489,35 @@ impl ServerOsApi for ServerOsInputOutput { None => Err(SpawnTerminalError::NoMoreTerminalIds), } } + fn reserve_terminal_id(&self) -> Result { + let mut terminal_id = None; + { + let current_ids: HashSet = self + .terminal_id_to_raw_fd + .lock() + .unwrap() + .keys() + .copied() + .collect(); + for i in 0..u32::MAX { + let i = i as u32; + if !current_ids.contains(&i) { + terminal_id = Some(i); + break; + } + } + } + match terminal_id { + Some(terminal_id) => { + self.terminal_id_to_raw_fd + .lock() + .unwrap() + .insert(terminal_id, None); + Ok(terminal_id) + }, + None => Err(SpawnTerminalError::NoMoreTerminalIds), + } + } fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result { unistd::read(fd, buf) } diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index f686f40bb..d17be7557 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -156,11 +156,12 @@ impl FloatingPanes { &mut self, pane_id: PaneId, exit_status: Option, + is_first_run: bool, run_command: RunCommand, ) { self.panes .get_mut(&pane_id) - .map(|p| p.hold(exit_status, run_command)); + .map(|p| p.hold(exit_status, is_first_run, run_command)); } pub fn get(&self, pane_id: &PaneId) -> Option<&Box> { self.panes.get(pane_id) diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index 67a882fb4..d9d60fefe 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -1533,6 +1533,7 @@ impl Grid { self.sixel_scrolling = false; self.mouse_mode = MouseMode::NoEncoding; self.mouse_tracking = MouseTracking::Off; + self.cursor_is_hidden = false; if let Some(images_to_reap) = self.sixel_grid.clear() { self.sixel_grid.reap_images(images_to_reap); } diff --git a/zellij-server/src/panes/terminal_character.rs b/zellij-server/src/panes/terminal_character.rs index c6fa0adf1..68813e7a9 100644 --- a/zellij-server/src/panes/terminal_character.rs +++ b/zellij-server/src/panes/terminal_character.rs @@ -3,7 +3,12 @@ use std::fmt::{self, Debug, Display, Formatter}; use std::ops::{Index, IndexMut}; use unicode_width::UnicodeWidthChar; -use zellij_utils::{data::PaletteColor, vte::ParamsIter}; +use unicode_width::UnicodeWidthStr; +use zellij_utils::input::command::RunCommand; +use zellij_utils::{ + data::{PaletteColor, Style}, + vte::ParamsIter, +}; use crate::panes::alacritty_functions::parse_sgr_color; @@ -736,3 +741,113 @@ impl ::std::fmt::Debug for TerminalCharacter { write!(f, "{}", self.character) } } + +pub fn render_first_run_banner( + columns: usize, + rows: usize, + style: &Style, + run_command: Option<&RunCommand>, +) -> String { + let middle_row = rows / 2; + let middle_column = columns / 2; + match run_command { + Some(run_command) => { + let bold_text = RESET_STYLES.bold(Some(AnsiCode::On)); + let command_color_text = RESET_STYLES + .foreground(Some(AnsiCode::from(style.colors.green))) + .bold(Some(AnsiCode::On)); + let waiting_to_run_text = "Waiting to run: "; + let command_text = run_command.to_string(); + let waiting_to_run_text_width = waiting_to_run_text.width() + command_text.width(); + let column_start_postion = middle_column.saturating_sub(waiting_to_run_text_width / 2); + let waiting_to_run_line = format!( + "\u{1b}[{};{}H{}{}{}{}{}", + middle_row, + column_start_postion, + bold_text, + waiting_to_run_text, + command_color_text, + command_text, + RESET_STYLES + ); + + let controls_bare_text_first_part = "<"; + let enter_bare_text = "ENTER"; + let controls_bare_text_second_part = "> to run, <"; + let ctrl_c_bare_text = "Ctrl-c"; + let controls_bare_text_third_part = "> to exit"; + let controls_color = RESET_STYLES + .foreground(Some(AnsiCode::from(style.colors.orange))) + .bold(Some(AnsiCode::On)); + let controls_line_length = controls_bare_text_first_part.len() + + enter_bare_text.len() + + controls_bare_text_second_part.len() + + ctrl_c_bare_text.len() + + controls_bare_text_third_part.len(); + let controls_column_start_position = + middle_column.saturating_sub(controls_line_length / 2); + let controls_line = format!( + "\u{1b}[{};{}H{}<{}{}{}{}> to run, <{}{}{}{}> to exit", + middle_row + 2, + controls_column_start_position, + bold_text, + controls_color, + enter_bare_text, + RESET_STYLES, + bold_text, + controls_color, + ctrl_c_bare_text, + RESET_STYLES, + bold_text + ); + format!( + "\u{1b}[?25l{}{}{}{}", + RESET_STYLES, waiting_to_run_line, controls_line, RESET_STYLES + ) + }, + None => { + let bare_text = format!("Waiting to start..."); + let bare_text_width = bare_text.width(); + let column_start_postion = middle_column.saturating_sub(bare_text_width / 2); + let bold_text = RESET_STYLES.bold(Some(AnsiCode::On)); + let waiting_to_run_line = format!( + "\u{1b}[?25l\u{1b}[{};{}H{}{}{}", + middle_row, column_start_postion, bold_text, bare_text, RESET_STYLES + ); + + let controls_bare_text_first_part = "<"; + let enter_bare_text = "ENTER"; + let controls_bare_text_second_part = "> to run, <"; + let ctrl_c_bare_text = "Ctrl-c"; + let controls_bare_text_third_part = "> to exit"; + let controls_color = RESET_STYLES + .foreground(Some(AnsiCode::from(style.colors.orange))) + .bold(Some(AnsiCode::On)); + let controls_line_length = controls_bare_text_first_part.len() + + enter_bare_text.len() + + controls_bare_text_second_part.len() + + ctrl_c_bare_text.len() + + controls_bare_text_third_part.len(); + let controls_column_start_position = + middle_column.saturating_sub(controls_line_length / 2); + let controls_line = format!( + "\u{1b}[{};{}H{}<{}{}{}{}> to run, <{}{}{}{}> to exit", + middle_row + 2, + controls_column_start_position, + bold_text, + controls_color, + enter_bare_text, + RESET_STYLES, + bold_text, + controls_color, + ctrl_c_bare_text, + RESET_STYLES, + bold_text + ); + format!( + "\u{1b}[?25l{}{}{}{}", + RESET_STYLES, waiting_to_run_line, controls_line, RESET_STYLES + ) + }, + } +} diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 631df0c84..5726ef44b 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -2,7 +2,7 @@ use crate::output::{CharacterChunk, SixelImageChunk}; use crate::panes::sixel::SixelImageStore; use crate::panes::{ grid::Grid, - terminal_character::{TerminalCharacter, EMPTY_TERMINAL_CHARACTER}, + terminal_character::{render_first_run_banner, TerminalCharacter, EMPTY_TERMINAL_CHARACTER}, }; use crate::panes::{AnsiCode, LinkHandler}; use crate::pty::VteBytes; @@ -83,6 +83,8 @@ pub enum PaneId { Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct? } +type IsFirstRun = bool; + // FIXME: This should hold an os_api handle so that terminal panes can set their own size via FD in // their `reflow_lines()` method. Drop a Box in here somewhere. #[allow(clippy::too_many_arguments)] @@ -104,8 +106,10 @@ pub struct TerminalPane { borderless: bool, fake_cursor_locations: HashSet<(usize, usize)>, // (x, y) - these hold a record of previous fake cursors which we need to clear on render search_term: String, - is_held: Option<(Option, RunCommand)>, // a "held" pane means that its command has exited and its waiting for a - // possible user instruction to be re-run + is_held: Option<(Option, IsFirstRun, RunCommand)>, // a "held" pane means that its command has either exited and the pane is waiting for a + // possible user instruction to be re-run, or that the command has not yet been run + banner: Option, // a banner to be rendered inside this TerminalPane, used for panes + // held on startup and can possibly be used to display some errors } impl Pane for TerminalPane { @@ -170,13 +174,14 @@ impl Pane for TerminalPane { // needs to be adjusted. // here we match against those cases - if need be, we adjust the input and if not // we send back the original input - if let Some((_exit_status, run_command)) = &self.is_held { + if let Some((_exit_status, _is_first_run, run_command)) = &self.is_held { match input_bytes.as_slice() { ENTER_CARRIAGE_RETURN | ENTER_NEWLINE | SPACE => { let run_command = run_command.clone(); self.is_held = None; self.grid.reset_terminal_state(); self.set_should_render(true); + self.remove_banner(); Some(AdjustedInput::ReRunCommandInThisPane(run_command)) }, CTRL_C => Some(AdjustedInput::CloseThisPane), @@ -395,8 +400,12 @@ impl Pane for TerminalPane { pane_title, frame_params, ); - if let Some((exit_status, _run_command)) = &self.is_held { - frame.add_exit_status(exit_status.as_ref().copied()); + if let Some((exit_status, is_first_run, _run_command)) = &self.is_held { + if *is_first_run { + frame.indicate_first_run(); + } else { + frame.add_exit_status(exit_status.as_ref().copied()); + } } let res = match self.frame.get(&client_id) { @@ -701,8 +710,11 @@ impl Pane for TerminalPane { fn is_alternate_mode_active(&self) -> bool { self.grid.is_alternate_mode_active() } - fn hold(&mut self, exit_status: Option, run_command: RunCommand) { - self.is_held = Some((exit_status, run_command)); + fn hold(&mut self, exit_status: Option, is_first_run: bool, run_command: RunCommand) { + self.is_held = Some((exit_status, is_first_run, run_command)); + if is_first_run { + self.render_first_run_banner(); + } self.set_should_render(true); } } @@ -752,6 +764,7 @@ impl TerminalPane { fake_cursor_locations: HashSet::new(), search_term: String::new(), is_held: None, + banner: None, } } pub fn get_x(&self) -> usize { @@ -782,6 +795,10 @@ impl TerminalPane { let rows = self.get_content_rows(); let cols = self.get_content_columns(); self.grid.change_size(rows, cols); + if self.banner.is_some() { + self.grid.reset_terminal_state(); + self.render_first_run_banner(); + } self.set_should_render(true); } pub fn read_buffer_as_lines(&self) -> Vec> { @@ -791,6 +808,25 @@ impl TerminalPane { // (x, y) self.grid.cursor_coordinates() } + fn render_first_run_banner(&mut self) { + let columns = self.get_content_columns(); + let rows = self.get_content_rows(); + let banner = match &self.is_held { + Some((_exit_status, _is_first_run, run_command)) => { + render_first_run_banner(columns, rows, &self.style, Some(run_command)) + }, + None => render_first_run_banner(columns, rows, &self.style, None), + }; + self.banner = Some(banner.clone()); + self.handle_pty_bytes(banner.as_bytes().to_vec()); + } + fn remove_banner(&mut self) { + if self.banner.is_some() { + self.grid.reset_terminal_state(); + self.set_should_render(true); + self.banner = None; + } + } } #[cfg(test)] diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index 59657c153..7e7b7a1f9 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -1010,11 +1010,12 @@ impl TiledPanes { &mut self, pane_id: PaneId, exit_status: Option, + is_first_run: bool, run_command: RunCommand, ) { self.panes .get_mut(&pane_id) - .map(|p| p.hold(exit_status, run_command)); + .map(|p| p.hold(exit_status, is_first_run, run_command)); } pub fn panes_to_hide_contains(&self, pane_id: PaneId) -> bool { self.panes_to_hide.contains(&pane_id) diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index bb5beb5dd..0ca0b5860 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -108,25 +108,29 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { _ => (false, None, name), }; match pty.spawn_terminal(terminal_action, client_or_tab_index) { - Ok(pid) => { + Ok((pid, starts_held)) => { + let hold_for_command = if starts_held { run_command } else { None }; pty.bus .senders .send_to_screen(ScreenInstruction::NewPane( PaneId::Terminal(pid), pane_title, should_float, + hold_for_command, client_or_tab_index, )) .with_context(err_context)?; }, Err(SpawnTerminalError::CommandNotFound(pid)) => { if hold_on_close { + let hold_for_command = None; // we do not hold an "error" pane pty.bus .senders .send_to_screen(ScreenInstruction::NewPane( PaneId::Terminal(pid), pane_title, should_float, + hold_for_command, client_or_tab_index, )) .with_context(err_context)?; @@ -154,7 +158,7 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { Some(TerminalAction::OpenFile(temp_file, line_number)), ClientOrTabIndex::ClientId(client_id), ) { - Ok(pid) => { + Ok((pid, _starts_held)) => { pty.bus .senders .send_to_screen(ScreenInstruction::OpenInPlaceEditor( @@ -178,23 +182,27 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { _ => (false, None, name), }; match pty.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)) { - Ok(pid) => { + Ok((pid, starts_held)) => { + let hold_for_command = if starts_held { run_command } else { None }; pty.bus .senders .send_to_screen(ScreenInstruction::VerticalSplit( PaneId::Terminal(pid), pane_title, + hold_for_command, client_id, )) .with_context(err_context)?; }, Err(SpawnTerminalError::CommandNotFound(pid)) => { if hold_on_close { + let hold_for_command = None; // error panes are never held pty.bus .senders .send_to_screen(ScreenInstruction::VerticalSplit( PaneId::Terminal(pid), pane_title, + hold_for_command, client_id, )) .with_context(err_context)?; @@ -238,23 +246,27 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { _ => (false, None, name), }; match pty.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)) { - Ok(pid) => { + Ok((pid, starts_held)) => { + let hold_for_command = if starts_held { run_command } else { None }; pty.bus .senders .send_to_screen(ScreenInstruction::HorizontalSplit( PaneId::Terminal(pid), pane_title, + hold_for_command, client_id, )) .with_context(err_context)?; }, Err(SpawnTerminalError::CommandNotFound(pid)) => { if hold_on_close { + let hold_for_command = None; // error panes are never held pty.bus .senders .send_to_screen(ScreenInstruction::HorizontalSplit( PaneId::Terminal(pid), pane_title, + hold_for_command, client_id, )) .with_context(err_context)?; @@ -391,6 +403,7 @@ impl Pty { command: PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable")), cwd, // note: this might also be filled by the calling function, eg. spawn_terminal hold_on_close: false, + hold_on_start: false, }) } fn fill_cwd(&self, terminal_action: &mut TerminalAction, client_id: ClientId) { @@ -416,7 +429,8 @@ impl Pty { &mut self, terminal_action: Option, client_or_tab_index: ClientOrTabIndex, - ) -> Result { + ) -> Result<(u32, bool), SpawnTerminalError> { + // bool is starts_held // returns the terminal id let terminal_action = match client_or_tab_index { ClientOrTabIndex::ClientId(client_id) => { @@ -429,10 +443,20 @@ impl Pty { terminal_action.unwrap_or_else(|| self.get_default_terminal(None)) }, }; - let hold_on_close = match &terminal_action { - TerminalAction::RunCommand(run_command) => run_command.hold_on_close, - _ => false, + let (hold_on_start, hold_on_close) = match &terminal_action { + TerminalAction::RunCommand(run_command) => { + (run_command.hold_on_start, run_command.hold_on_close) + }, + _ => (false, false), }; + + if hold_on_start { + // we don't actually open a terminal in this case, just wait for the user to run it + let starts_held = hold_on_start; + let terminal_id = self.bus.os_input.as_mut().unwrap().reserve_terminal_id()?; + return Ok((terminal_id, starts_held)); + } + let quit_cb = Box::new({ let senders = self.bus.senders.clone(); move |pane_id, exit_status, command| { @@ -477,7 +501,8 @@ impl Pty { self.task_handles.insert(terminal_id, terminal_bytes); self.id_to_child_pid.insert(terminal_id, child_fd); - Ok(terminal_id) + let starts_held = false; + Ok((terminal_id, starts_held)) } pub fn spawn_terminals_for_layout( &mut self, @@ -489,10 +514,15 @@ impl Pty { let mut default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal(None)); self.fill_cwd(&mut default_shell, client_id); let extracted_run_instructions = layout.extract_run_instructions(); - let mut new_pane_pids: Vec<(u32, Option, Result)> = - vec![]; // (terminal_id, - // run_command - // file_descriptor) + let mut new_pane_pids: Vec<( + u32, + bool, + Option, + Result, + )> = vec![]; // (terminal_id, + // starts_held, + // run_command, + // file_descriptor) for run_instruction in extracted_run_instructions { let quit_cb = Box::new({ let senders = self.bus.senders.clone(); @@ -502,6 +532,7 @@ impl Pty { }); match run_instruction { Some(Run::Command(command)) => { + let starts_held = command.hold_on_start; let hold_on_close = command.hold_on_close; let quit_cb = Box::new({ let senders = self.bus.senders.clone(); @@ -520,34 +551,56 @@ impl Pty { } }); let cmd = TerminalAction::RunCommand(command.clone()); - match self - .bus - .os_input - .as_mut() - .with_context(err_context)? - .spawn_terminal(cmd, quit_cb, self.default_editor.clone()) - { - Ok((terminal_id, pid_primary, child_fd)) => { - self.id_to_child_pid.insert(terminal_id, child_fd); - new_pane_pids.push(( - terminal_id, - Some(command.clone()), - Ok(pid_primary), - )); - }, - Err(SpawnTerminalError::CommandNotFound(terminal_id)) => { - new_pane_pids.push(( - terminal_id, - Some(command.clone()), - Err(SpawnTerminalError::CommandNotFound(terminal_id)), - )); - }, - Err(e) => { - log::error!("Failed to spawn terminal: {}", e); - }, + if starts_held { + // we don't actually open a terminal in this case, just wait for the user to run it + match self.bus.os_input.as_mut().unwrap().reserve_terminal_id() { + Ok(terminal_id) => { + new_pane_pids.push(( + terminal_id, + starts_held, + Some(command.clone()), + Ok(terminal_id as i32), // this is not actually correct but gets + // stripped later + )); + }, + Err(e) => { + log::error!("Failed to spawn terminal: {}", e); + }, + } + } else { + match self + .bus + .os_input + .as_mut() + .with_context(err_context)? + .spawn_terminal(cmd, quit_cb, self.default_editor.clone()) + { + Ok((terminal_id, pid_primary, child_fd)) => { + self.id_to_child_pid.insert(terminal_id, child_fd); + new_pane_pids.push(( + terminal_id, + starts_held, + Some(command.clone()), + Ok(pid_primary), + )); + }, + Err(SpawnTerminalError::CommandNotFound(terminal_id)) => { + let starts_held = false; // we do not hold error panes + new_pane_pids.push(( + terminal_id, + starts_held, + Some(command.clone()), + Err(SpawnTerminalError::CommandNotFound(terminal_id)), + )); + }, + Err(e) => { + log::error!("Failed to spawn terminal: {}", e); + }, + } } }, Some(Run::Cwd(cwd)) => { + let starts_held = false; // we do not hold Cwd panes let shell = self.get_default_terminal(Some(cwd)); match self .bus @@ -558,11 +611,12 @@ impl Pty { { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); - new_pane_pids.push((terminal_id, None, Ok(pid_primary))); + new_pane_pids.push((terminal_id, starts_held, None, Ok(pid_primary))); }, Err(SpawnTerminalError::CommandNotFound(terminal_id)) => { new_pane_pids.push(( terminal_id, + starts_held, None, Err(SpawnTerminalError::CommandNotFound(terminal_id)), )); @@ -573,6 +627,7 @@ impl Pty { } }, Some(Run::EditFile(path_to_file, line_number)) => { + let starts_held = false; // we do not hold edit panes (for now?) match self .bus .os_input @@ -585,11 +640,13 @@ impl Pty { ) { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); - new_pane_pids.push((terminal_id, None, Ok(pid_primary))); + new_pane_pids.push((terminal_id, starts_held, None, Ok(pid_primary))); }, Err(SpawnTerminalError::CommandNotFound(terminal_id)) => { + let starts_held = false; // we do not hold error panes new_pane_pids.push(( terminal_id, + starts_held, None, Err(SpawnTerminalError::CommandNotFound(terminal_id)), )); @@ -600,6 +657,7 @@ impl Pty { } }, None => { + let starts_held = false; match self .bus .os_input @@ -609,11 +667,12 @@ impl Pty { { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); - new_pane_pids.push((terminal_id, None, Ok(pid_primary))); + new_pane_pids.push((terminal_id, starts_held, None, Ok(pid_primary))); }, Err(SpawnTerminalError::CommandNotFound(terminal_id)) => { new_pane_pids.push(( terminal_id, + starts_held, None, Err(SpawnTerminalError::CommandNotFound(terminal_id)), )); @@ -627,10 +686,17 @@ impl Pty { Some(Run::Plugin(_)) => {}, } } - let new_tab_pane_ids: Vec = new_pane_pids + // Option should only be Some if the pane starts held + let new_tab_pane_ids: Vec<(u32, Option)> = new_pane_pids .iter() - .map(|(terminal_id, _, _)| *terminal_id) - .collect::>(); + .map(|(terminal_id, starts_held, run_command, _)| { + if *starts_held { + (*terminal_id, run_command.clone()) + } else { + (*terminal_id, None) + } + }) + .collect(); self.bus .senders .send_to_screen(ScreenInstruction::NewTab( @@ -639,7 +705,11 @@ impl Pty { client_id, )) .with_context(err_context)?; - for (terminal_id, run_command, pid_primary) in new_pane_pids { + for (terminal_id, starts_held, run_command, pid_primary) in new_pane_pids { + if starts_held { + // we do not run a command or start listening for bytes on held panes + continue; + } match pid_primary { Ok(pid_primary) => { let terminal_bytes = task::spawn({ @@ -744,16 +814,21 @@ impl Pty { let _ = self.task_handles.remove(&id); // if all is well, this shouldn't be here let _ = self.id_to_child_pid.remove(&id); // if all is wlel, this shouldn't be here + let hold_on_close = run_command.hold_on_close; let quit_cb = Box::new({ let senders = self.bus.senders.clone(); move |pane_id, exit_status, command| { - // we only re-run held panes, so we'll never close them from Pty - let _ = senders.send_to_screen(ScreenInstruction::HoldPane( - pane_id, - exit_status, - command, - None, - )); + if hold_on_close { + let _ = senders.send_to_screen(ScreenInstruction::HoldPane( + pane_id, + exit_status, + command, + None, + )); + } else { + let _ = + senders.send_to_screen(ScreenInstruction::ClosePane(pane_id, None)); + } } }); let (pid_primary, child_fd): (RawFd, RawFd) = self diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index c1fcabc78..42d0c093c 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -112,19 +112,27 @@ macro_rules! active_tab_and_connected_client_id { }; } +type InitialTitle = String; +type ShouldFloat = bool; +type HoldForCommand = Option; + /// Instructions that can be sent to the [`Screen`]. #[derive(Debug, Clone)] pub enum ScreenInstruction { PtyBytes(u32, VteBytes), Render, - NewPane(PaneId, Option, Option, ClientOrTabIndex), // String is initial title, - // bool (if Some) is - // should_float + NewPane( + PaneId, + Option, + Option, + HoldForCommand, + ClientOrTabIndex, + ), OpenInPlaceEditor(PaneId, ClientId), TogglePaneEmbedOrFloating(ClientId), ToggleFloatingPanes(ClientId, Option), - HorizontalSplit(PaneId, Option, ClientId), // String is initial title - VerticalSplit(PaneId, Option, ClientId), // String is initial title + HorizontalSplit(PaneId, Option, HoldForCommand, ClientId), + VerticalSplit(PaneId, Option, HoldForCommand, ClientId), WriteCharacter(Vec, ClientId), ResizeLeft(ClientId), ResizeRight(ClientId), @@ -167,7 +175,7 @@ pub enum ScreenInstruction { HoldPane(PaneId, Option, RunCommand, Option), // Option is the exit status UpdatePaneName(Vec, ClientId), UndoRenamePane(ClientId), - NewTab(PaneLayout, Vec, ClientId), + NewTab(PaneLayout, Vec<(u32, HoldForCommand)>, ClientId), SwitchTabNext(ClientId), SwitchTabPrev(ClientId), ToggleActiveSyncTab(ClientId), @@ -811,7 +819,7 @@ impl Screen { pub fn new_tab( &mut self, layout: PaneLayout, - new_ids: Vec, + new_ids: Vec<(u32, HoldForCommand)>, client_id: ClientId, ) -> Result<()> { let client_id = if self.get_active_tab(client_id).is_some() { @@ -1227,6 +1235,7 @@ pub(crate) fn screen_thread_main( pid, initial_pane_title, should_float, + hold_for_command, client_or_tab_index, ) => { match client_or_tab_index { @@ -1237,10 +1246,27 @@ pub(crate) fn screen_thread_main( should_float, Some(client_id)), ?); + if let Some(hold_for_command) = hold_for_command { + let is_first_run = true; + active_tab_and_connected_client_id!( + screen, + client_id, + |tab: &mut Tab, _client_id: ClientId| tab.hold_pane( + pid, + None, + is_first_run, + hold_for_command + ) + ) + } }, ClientOrTabIndex::TabIndex(tab_index) => { if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { active_tab.new_pane(pid, initial_pane_title, should_float, None)?; + if let Some(hold_for_command) = hold_for_command { + let is_first_run = true; + active_tab.hold_pane(pid, None, is_first_run, hold_for_command); + } } else { log::error!("Tab index not found: {:?}", tab_index); } @@ -1275,24 +1301,60 @@ pub(crate) fn screen_thread_main( screen.render()?; }, - ScreenInstruction::HorizontalSplit(pid, initial_pane_title, client_id) => { + ScreenInstruction::HorizontalSplit( + pid, + initial_pane_title, + hold_for_command, + client_id, + ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.horizontal_split(pid, initial_pane_title, client_id), ? ); + if let Some(hold_for_command) = hold_for_command { + let is_first_run = true; + active_tab_and_connected_client_id!( + screen, + client_id, + |tab: &mut Tab, _client_id: ClientId| tab.hold_pane( + pid, + None, + is_first_run, + hold_for_command + ) + ); + } screen.unblock_input()?; screen.update_tabs()?; screen.render()?; }, - ScreenInstruction::VerticalSplit(pid, initial_pane_title, client_id) => { + ScreenInstruction::VerticalSplit( + pid, + initial_pane_title, + hold_for_command, + client_id, + ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.vertical_split(pid, initial_pane_title, client_id), ? ); + if let Some(hold_for_command) = hold_for_command { + let is_first_run = true; + active_tab_and_connected_client_id!( + screen, + client_id, + |tab: &mut Tab, _client_id: ClientId| tab.hold_pane( + pid, + None, + is_first_run, + hold_for_command + ) + ); + } screen.unblock_input()?; screen.update_tabs()?; screen.render()?; @@ -1644,18 +1706,20 @@ pub(crate) fn screen_thread_main( screen.unblock_input()?; }, ScreenInstruction::HoldPane(id, exit_status, run_command, client_id) => { + let is_first_run = false; match client_id { Some(client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab.hold_pane( id, exit_status, + is_first_run, run_command )); }, None => { for tab in screen.tabs.values_mut() { if tab.get_all_pane_ids().contains(&id) { - tab.hold_pane(id, exit_status, run_command); + tab.hold_pane(id, exit_status, is_first_run, run_command); break; } } diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 9efcf9012..df12b2df9 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -61,6 +61,8 @@ macro_rules! resize_pty { }; } +type HoldForCommand = Option; + // FIXME: This should be replaced by `RESIZE_PERCENT` at some point pub const MIN_TERMINAL_HEIGHT: usize = 5; pub const MIN_TERMINAL_WIDTH: usize = 5; @@ -354,7 +356,7 @@ pub trait Pane { // False by default (only terminal-panes support alternate mode) false } - fn hold(&mut self, _exit_status: Option, _run_command: RunCommand) { + fn hold(&mut self, _exit_status: Option, _is_first_run: bool, _run_command: RunCommand) { // No-op by default, only terminal panes support holding } } @@ -470,7 +472,7 @@ impl Tab { pub fn apply_layout( &mut self, layout: PaneLayout, - new_ids: Vec, + new_ids: Vec<(u32, HoldForCommand)>, tab_index: usize, client_id: ClientId, ) -> Result<()> { @@ -532,7 +534,7 @@ impl Tab { set_focus_pane_id(layout, PaneId::Plugin(pid)); } else { // there are still panes left to fill, use the pids we received in this method - if let Some(pid) = new_ids.next() { + if let Some((pid, hold_for_command)) = new_ids.next() { let next_terminal_position = self.get_next_terminal_position(); let initial_title = match &layout.run { Some(Run::Command(run_command)) => Some(run_command.to_string()), @@ -552,6 +554,9 @@ impl Tab { initial_title, ); new_pane.set_borderless(layout.borderless); + if let Some(held_command) = hold_for_command { + new_pane.hold(None, true, held_command.clone()); + } self.tiled_panes.add_pane_with_existing_geom( PaneId::Terminal(*pid), Box::new(new_pane), @@ -560,7 +565,7 @@ impl Tab { } } } - for unused_pid in new_ids { + for (unused_pid, _) in new_ids { // this is a bit of a hack and happens because we don't have any central location that // can query the screen as to how many panes it needs to create a layout // fixing this will require a bit of an architecture change @@ -601,7 +606,7 @@ impl Tab { Ok(()) }, Err(e) => { - for unused_pid in new_ids { + for (unused_pid, _) in new_ids { self.senders .send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(unused_pid))) .with_context(err_context)?; @@ -1746,11 +1751,19 @@ impl Tab { closed_pane } } - pub fn hold_pane(&mut self, id: PaneId, exit_status: Option, run_command: RunCommand) { + pub fn hold_pane( + &mut self, + id: PaneId, + exit_status: Option, + is_first_run: bool, + run_command: RunCommand, + ) { if self.floating_panes.panes_contain(&id) { - self.floating_panes.hold_pane(id, exit_status, run_command); + self.floating_panes + .hold_pane(id, exit_status, is_first_run, run_command); } else { - self.tiled_panes.hold_pane(id, exit_status, run_command); + self.tiled_panes + .hold_pane(id, exit_status, is_first_run, run_command); } } pub fn replace_pane_with_suppressed_pane(&mut self, pane_id: PaneId) -> Option> { diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 5944f84d0..3f4290fa6 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -223,7 +223,7 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab { terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout(PaneLayout::default(), vec![1], index, client_id) + tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) .unwrap(); tab } @@ -277,7 +277,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str) .extract_run_instructions() .iter() .enumerate() - .map(|(i, _)| i as u32) + .map(|(i, _)| (i as u32, None)) .collect(); tab.apply_layout(tab_layout, pane_ids, index, client_id) .unwrap(); @@ -332,14 +332,8 @@ fn create_new_tab_with_mock_pty_writer( terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout( - // LayoutTemplate::default().try_into().unwrap(), - PaneLayout::default(), - vec![1], - index, - client_id, - ) - .unwrap(); + tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) + .unwrap(); tab } @@ -393,7 +387,7 @@ fn create_new_tab_with_sixel_support( terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout(PaneLayout::default(), vec![1], index, client_id) + tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) .unwrap(); tab } diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index 6b100cf62..2539b7a93 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -142,7 +142,7 @@ fn create_new_tab(size: Size) -> Tab { terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout(PaneLayout::default(), vec![1], index, client_id) + tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) .unwrap(); tab } @@ -189,7 +189,7 @@ fn create_new_tab_with_cell_size( terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout(PaneLayout::default(), vec![1], index, client_id) + tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) .unwrap(); tab } diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index 61df8470a..6209466c4 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -89,6 +89,7 @@ pub struct PaneFrame { pub other_cursors_exist_in_session: bool, pub other_focused_clients: Vec, exit_status: Option, + is_first_run: bool, } impl PaneFrame { @@ -109,6 +110,7 @@ impl PaneFrame { other_focused_clients: frame_params.other_focused_clients, other_cursors_exist_in_session: frame_params.other_cursors_exist_in_session, exit_status: None, + is_first_run: false, } } pub fn add_exit_status(&mut self, exit_status: Option) { @@ -117,6 +119,9 @@ impl PaneFrame { None => Some(ExitStatus::Exited), }; } + pub fn indicate_first_run(&mut self) { + self.is_first_run = true; + } fn client_cursor(&self, client_id: ClientId) -> Vec { let color = client_id_to_colors(client_id, self.style.colors); background_color(" ", color.map(|c| c.0)) @@ -611,11 +616,7 @@ impl PaneFrame { } fn render_held_undertitle(&self) -> Result> { let max_undertitle_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners - let exit_status = self - .exit_status - .with_context(|| format!("failed to render command pane status '{}'", self.title))?; // unwrap is safe because we only call this if - - let (mut first_part, first_part_len) = self.first_held_title_part_full(exit_status); + let (mut first_part, first_part_len) = self.first_exited_held_title_part_full(); let mut left_boundary = foreground_color(self.get_corner(boundary_type::BOTTOM_LEFT), self.color); let mut right_boundary = @@ -683,7 +684,7 @@ impl PaneFrame { character_chunks.push(CharacterChunk::new(title, x, y)); } else if row == self.geom.rows - 1 { // bottom row - if self.exit_status.is_some() { + if self.exit_status.is_some() || self.is_first_run { let x = self.geom.x; let y = self.geom.y + row; character_chunks.push(CharacterChunk::new( @@ -727,13 +728,10 @@ impl PaneFrame { } Ok((character_chunks, None)) } - fn first_held_title_part_full( - &self, - exit_status: ExitStatus, - ) -> (Vec, usize) { + fn first_exited_held_title_part_full(&self) -> (Vec, usize) { // (title part, length) - match exit_status { - ExitStatus::Code(exit_code) => { + match self.exit_status { + Some(ExitStatus::Code(exit_code)) => { let mut first_part = vec![]; let left_bracket = " [ "; let exited_text = "EXIT CODE: "; @@ -759,7 +757,7 @@ impl PaneFrame { + right_bracket.len(), ) }, - ExitStatus::Exited => { + Some(ExitStatus::Exited) => { let mut first_part = vec![]; let left_bracket = " [ "; let exited_text = "EXITED"; @@ -775,15 +773,20 @@ impl PaneFrame { left_bracket.len() + exited_text.len() + right_bracket.len(), ) }, + None => (foreground_color(boundary_type::HORIZONTAL, self.color), 1), } } fn second_held_title_part_full(&self) -> (Vec, usize) { // (title part, length) let mut second_part = vec![]; - let left_enter_bracket = "<"; + let left_enter_bracket = if self.is_first_run { " <" } else { "<" }; let enter_text = "ENTER"; let right_enter_bracket = ">"; - let enter_tip = " to re-run, "; + let enter_tip = if self.is_first_run { + " to run, " + } else { + " to re-run, " + }; let left_break_bracket = "<"; let break_text = "Ctrl-c"; let right_break_bracket = ">"; diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 299bab6a3..8fd1a6217 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -284,7 +284,7 @@ impl MockScreen { let pane_count = pane_layout.extract_run_instructions().len(); let mut pane_ids = vec![]; for i in 0..pane_count { - pane_ids.push(i as u32); + pane_ids.push((i as u32, None)); } let _ = self.to_screen.send(ScreenInstruction::NewTab( pane_layout, @@ -297,7 +297,7 @@ impl MockScreen { let pane_count = tab_layout.extract_run_instructions().len(); let mut pane_ids = vec![]; for i in 0..pane_count { - pane_ids.push(i as u32); + pane_ids.push((i as u32, None)); } let _ = self.to_screen.send(ScreenInstruction::NewTab( tab_layout, @@ -427,7 +427,7 @@ macro_rules! log_actions_in_thread { fn new_tab(screen: &mut Screen, pid: u32) { let client_id = 1; screen - .new_tab(PaneLayout::default(), vec![pid], client_id) + .new_tab(PaneLayout::default(), vec![(pid, None)], client_id) .expect("TEST"); } @@ -1822,6 +1822,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() { floating: false, name: None, close_on_exit: false, + start_suspended: false, }; send_cli_action_to_server( &session_metadata, @@ -1861,6 +1862,7 @@ pub fn send_cli_new_pane_action_with_split_direction() { floating: false, name: None, close_on_exit: false, + start_suspended: false, }; send_cli_action_to_server( &session_metadata, @@ -1900,6 +1902,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() { floating: false, name: None, close_on_exit: false, + start_suspended: false, }; send_cli_action_to_server( &session_metadata, diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap index e8f819d8f..92e668f41 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 1981 +assertion_line: 1989 expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" --- [SpawnTerminal(Some(OpenFile("/file/to/edit", Some(100))), Some(false), Some("Editing: /file/to/edit"), ClientId(10)), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap index 62d76f63b..1e88d09ca 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 1907 +assertion_line: 1915 expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" --- -[SpawnTerminalVertically(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true })), None, 10), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit] +[SpawnTerminalVertically(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false })), None, 10), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit] diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index f860f05f9..baa53b9a7 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -141,6 +141,10 @@ pub enum Sessions { /// Close the pane immediately when its command exits #[clap(short, long, value_parser, default_value("false"), takes_value(false))] close_on_exit: bool, + + /// Start the command suspended, only running after you first presses ENTER + #[clap(short, long, value_parser, default_value("false"), takes_value(false))] + start_suspended: bool, }, /// Edit file with default $EDITOR / $VISUAL #[clap(visible_alias = "e")] @@ -252,6 +256,16 @@ pub enum CliAction { requires("command") )] close_on_exit: bool, + /// Start the command suspended, only running it after the you first press ENTER + #[clap( + short, + long, + value_parser, + default_value("false"), + takes_value(false), + requires("command") + )] + start_suspended: 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 f63390ad7..189f3cd96 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -260,11 +260,13 @@ impl Action { floating, name, close_on_exit, + start_suspended, } => { 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_start = start_suspended; let hold_on_close = !close_on_exit; let run_command_action = RunCommandAction { command, @@ -272,6 +274,7 @@ impl Action { cwd, direction, hold_on_close, + hold_on_start, }; if floating { Ok(vec![Action::NewFloatingPane( diff --git a/zellij-utils/src/input/command.rs b/zellij-utils/src/input/command.rs index abb7c1e7f..14b87755f 100644 --- a/zellij-utils/src/input/command.rs +++ b/zellij-utils/src/input/command.rs @@ -19,6 +19,8 @@ pub struct RunCommand { pub cwd: Option, #[serde(default)] pub hold_on_close: bool, + #[serde(default)] + pub hold_on_start: bool, } impl std::fmt::Display for RunCommand { @@ -50,6 +52,8 @@ pub struct RunCommandAction { pub direction: Option, #[serde(default)] pub hold_on_close: bool, + #[serde(default)] + pub hold_on_start: bool, } impl From for RunCommand { @@ -59,6 +63,7 @@ impl From for RunCommand { args: action.args, cwd: action.cwd, hold_on_close: action.hold_on_close, + hold_on_start: action.hold_on_start, } } } diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index fa50237a9..38806c21a 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -142,7 +142,7 @@ impl Run { } } pub fn add_close_on_exit(&mut self, close_on_exit: Option) { - // overrides the args of a Run::Command if they are Some + // overrides the hold_on_close of a Run::Command if it is Some // and not empty if let Some(close_on_exit) = close_on_exit { if let Run::Command(run_command) = self { @@ -150,6 +150,15 @@ impl Run { } } } + pub fn add_start_suspended(&mut self, start_suspended: Option) { + // overrides the hold_on_start of a Run::Command if they are Some + // and not empty + if let Some(start_suspended) = start_suspended { + if let Run::Command(run_command) = self { + run_command.hold_on_start = start_suspended; + } + } + } } #[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 9614eef46..f11c86869 100644 --- a/zellij-utils/src/input/unit/layout_test.rs +++ b/zellij-utils/src/input/unit/layout_test.rs @@ -281,6 +281,19 @@ fn layout_with_command_panes_and_close_on_exit() { assert_snapshot!(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, "layout_file_name".into(), None).unwrap(); + assert_snapshot!(format!("{:#?}", layout)); +} + #[test] fn layout_with_plugin_panes() { let kdl_layout = r#" diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_added_to_args_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_added_to_args_in_template.snap index 6ff62582d..8b6d9050f 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_added_to_args_in_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_added_to_args_in_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1050 +assertion_line: 1081 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -23,6 +23,7 @@ Layout { args: [], cwd: None, hold_on_close: true, + hold_on_start: false, }, ), ), @@ -45,6 +46,7 @@ Layout { ], cwd: None, hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_override_args_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_override_args_in_template.snap index 4a449d35a..4b026d751 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_override_args_in_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_override_args_in_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1033 +assertion_line: 1046 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -26,6 +26,7 @@ Layout { ], cwd: None, hold_on_close: true, + hold_on_start: false, }, ), ), @@ -48,6 +49,7 @@ Layout { ], cwd: None, hold_on_close: true, + hold_on_start: false, }, ), ), 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 index da83cc62c..d23bb40c5 100644 --- 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 @@ -23,6 +23,7 @@ Layout { args: [], cwd: None, hold_on_close: true, + hold_on_start: false, }, ), ), @@ -42,6 +43,7 @@ Layout { args: [], cwd: None, hold_on_close: false, + hold_on_start: false, }, ), ), 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 index 3cb80cf74..abce2964d 100644 --- 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 @@ -23,6 +23,7 @@ Layout { args: [], cwd: None, hold_on_close: true, + hold_on_start: false, }, ), ), @@ -42,6 +43,7 @@ Layout { args: [], cwd: None, hold_on_close: false, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_added_to_cwd_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_added_to_cwd_in_template.snap index 3c26fbdd6..48bb11838 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_added_to_cwd_in_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_added_to_cwd_in_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1085 +assertion_line: 1133 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -23,6 +23,7 @@ Layout { args: [], cwd: None, hold_on_close: true, + hold_on_start: false, }, ), ), @@ -44,6 +45,7 @@ Layout { "/home", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_override_cwd_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_override_cwd_in_template.snap index fd1bc15a1..7d57e6d06 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_override_cwd_in_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_override_cwd_in_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1068 +assertion_line: 1116 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -25,6 +25,7 @@ Layout { "/tmp", ), hold_on_close: true, + hold_on_start: false, }, ), ), @@ -46,6 +47,7 @@ Layout { "/", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd.snap index a83674d2d..58a786789 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1441 +assertion_line: 1516 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -39,6 +39,7 @@ Layout { "/tmp/./foo/./bar", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_pane_templates.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_pane_templates.snap index 8381dcd1d..c6b3ed5f4 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_pane_templates.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_pane_templates.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1462 +assertion_line: 1537 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -43,6 +43,7 @@ Layout { "/tmp/./foo/./bar", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_tab_templates.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_tab_templates.snap index 65f287042..cd1a51e1d 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_tab_templates.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_tab_templates.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1481 +assertion_line: 1556 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -39,6 +39,7 @@ Layout { "/tmp/./foo/./bar", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap index 548ae756f..ba246e06d 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1283 +assertion_line: 1403 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -39,6 +39,7 @@ Layout { "/tmp", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap index d88a8ecec..3b546891e 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1309 +assertion_line: 1435 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -39,6 +39,7 @@ Layout { "/tmp", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap index 0710ae130..50b92892e 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1324 +assertion_line: 1455 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -39,6 +39,7 @@ Layout { "/tmp", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap index 956df1a5d..f5d6b95ac 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1295 +assertion_line: 1416 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -39,6 +39,7 @@ Layout { "/home/foo", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_with_tab_cwd_given_to_panes_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_with_tab_cwd_given_to_panes_without_cwd.snap index 4bc2f412f..09d209c17 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_with_tab_cwd_given_to_panes_without_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_with_tab_cwd_given_to_panes_without_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1396 +assertion_line: 1471 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -39,6 +39,7 @@ Layout { "/tmp/./foo", ), hold_on_close: true, + hold_on_start: false, }, ), ), 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 index 69e3d7f1f..32d1341c1 100644 --- 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 @@ -23,6 +23,7 @@ Layout { args: [], cwd: None, hold_on_close: false, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_start_suspended.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_start_suspended.snap new file mode 100644 index 000000000..136c95fb0 --- /dev/null +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_start_suspended.snap @@ -0,0 +1,42 @@ +--- +source: zellij-utils/src/input/./unit/layout_test.rs +assertion_line: 294 +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: true, + hold_on_start: true, + }, + ), + ), + 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_tab_and_pane_templates.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tab_and_pane_templates.snap index f32b346df..fbe111520 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tab_and_pane_templates.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tab_and_pane_templates.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 570 +assertion_line: 583 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -41,6 +41,7 @@ Layout { args: [], cwd: None, hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap index 03adaf85a..94a52aac8 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1199 +assertion_line: 1287 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -25,6 +25,7 @@ Layout { "/tmp/bar", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap index d01c65169..6f2255cd1 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1144 +assertion_line: 1233 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -25,6 +25,7 @@ Layout { "/tmp/foo", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap index 596c492be..d659eba32 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1161 +assertion_line: 1250 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -25,6 +25,7 @@ Layout { "/tmp/bar", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap index 25bca8dbe..feb8a88bd 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1180 +assertion_line: 1268 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -25,6 +25,7 @@ Layout { "/tmp/bar", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap index 05e377b90..6cad38df7 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1216 +assertion_line: 1305 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -25,6 +25,7 @@ Layout { "/tmp/bar", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap index 8c3863c31..42fb84d2c 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1269 +assertion_line: 1357 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -25,6 +25,7 @@ Layout { "/tmp/bar", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap index ba91e5740..a09f2ab20 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1251 +assertion_line: 1339 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -25,6 +25,7 @@ Layout { "/tmp/foo", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_given_to_panes_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_given_to_panes_without_cwd.snap index 8f05eda17..4508cb074 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_given_to_panes_without_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_given_to_panes_without_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1411 +assertion_line: 1486 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -39,6 +39,7 @@ Layout { "/tmp", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_prepended_to_panes_with_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_prepended_to_panes_with_cwd.snap index 32eb17d06..ac1d4240b 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_prepended_to_panes_with_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_prepended_to_panes_with_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1426 +assertion_line: 1501 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -39,6 +39,7 @@ Layout { "/tmp/./foo", ), hold_on_close: true, + hold_on_start: false, }, ), ), diff --git a/zellij-utils/src/kdl/kdl_layout_parser.rs b/zellij-utils/src/kdl/kdl_layout_parser.rs index b92c6a1b2..c5a84a7ba 100644 --- a/zellij-utils/src/kdl/kdl_layout_parser.rs +++ b/zellij-utils/src/kdl/kdl_layout_parser.rs @@ -54,6 +54,7 @@ impl<'a> KdlLayoutParser<'a> { || word == "tab" || word == "args" || word == "close_on_exit" + || word == "start_suspended" || word == "borderless" || word == "focus" || word == "name" @@ -72,6 +73,7 @@ impl<'a> KdlLayoutParser<'a> { || property_name == "cwd" || property_name == "args" || property_name == "close_on_exit" + || property_name == "start_suspended" || property_name == "split_direction" || property_name == "pane" || property_name == "children" @@ -241,15 +243,19 @@ impl<'a> KdlLayoutParser<'a> { let args = self.parse_args(pane_node)?; let close_on_exit = kdl_get_bool_property_or_child_value_with_error!(pane_node, "close_on_exit"); + let start_suspended = + kdl_get_bool_property_or_child_value_with_error!(pane_node, "start_suspended"); if !is_template { self.assert_no_bare_attributes_in_pane_node( &command, &args, &close_on_exit, + &start_suspended, pane_node, )?; } let hold_on_close = close_on_exit.map(|c| !c).unwrap_or(true); + let hold_on_start = start_suspended.map(|c| c).unwrap_or(false); match (command, edit, cwd) { (None, None, Some(cwd)) => Ok(Some(Run::Cwd(cwd))), (Some(command), None, cwd) => Ok(Some(Run::Command(RunCommand { @@ -257,6 +263,7 @@ impl<'a> KdlLayoutParser<'a> { args: args.unwrap_or_else(|| vec![]), cwd, hold_on_close, + hold_on_start, }))), (None, Some(edit), Some(cwd)) => Ok(Some(Run::EditFile(cwd.join(edit), None))), (None, Some(edit), None) => Ok(Some(Run::EditFile(edit, None))), @@ -380,6 +387,8 @@ impl<'a> KdlLayoutParser<'a> { let args = self.parse_args(kdl_node)?; let close_on_exit = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "close_on_exit"); + let start_suspended = + kdl_get_bool_property_or_child_value_with_error!(kdl_node, "start_suspended"); let split_size = self.parse_split_size(kdl_node)?; let run = self.parse_command_plugin_or_edit_block_for_template(kdl_node)?; self.assert_no_bare_attributes_in_pane_node_with_template( @@ -387,6 +396,7 @@ impl<'a> KdlLayoutParser<'a> { &pane_template.run, &args, &close_on_exit, + &start_suspended, kdl_node, )?; self.insert_children_to_pane_template( @@ -396,10 +406,11 @@ impl<'a> KdlLayoutParser<'a> { )?; pane_template.run = Run::merge(&pane_template.run, &run); if let Some(pane_template_run_command) = pane_template.run.as_mut() { - // we need to do this because panes consuming a pane_templates + // we need to do this because panes consuming a pane_template // can have bare args without a command pane_template_run_command.add_args(args); pane_template_run_command.add_close_on_exit(close_on_exit); + pane_template_run_command.add_start_suspended(start_suspended); }; if let Some(borderless) = borderless { pane_template.borderless = borderless; @@ -600,6 +611,7 @@ impl<'a> KdlLayoutParser<'a> { pane_template_run: &Option, args: &Option>, close_on_exit: &Option, + start_suspended: &Option, pane_node: &KdlNode, ) -> Result<(), ConfigError> { if let (None, None, true) = (pane_run, pane_template_run, args.is_some()) { @@ -614,6 +626,12 @@ impl<'a> KdlLayoutParser<'a> { pane_node )); } + if let (None, None, true) = (pane_run, pane_template_run, start_suspended.is_some()) { + return Err(kdl_parsing_error!( + format!("start_suspended can only be specified if a command was specified either in the pane_template or in the pane"), + pane_node + )); + } Ok(()) } fn assert_no_bare_attributes_in_pane_node( @@ -621,6 +639,7 @@ impl<'a> KdlLayoutParser<'a> { command: &Option, args: &Option>, close_on_exit: &Option, + start_suspended: &Option, pane_node: &KdlNode, ) -> Result<(), ConfigError> { if command.is_none() { @@ -631,6 +650,13 @@ impl<'a> KdlLayoutParser<'a> { pane_node.span().len(), )); } + if start_suspended.is_some() { + return Err(ConfigError::new_layout_kdl_error( + "start_suspended can only be set if a command was specified".into(), + pane_node.span().offset(), + pane_node.span().len(), + )); + } if args.is_some() { return Err(ConfigError::new_layout_kdl_error( "args can only be set if a command was specified".into(), diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 92abcac18..54560bf58 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -1,7 +1,6 @@ mod kdl_layout_parser; use crate::data::{InputMode, Key, Palette, PaletteColor}; use crate::envs::EnvironmentVariables; -use crate::input::command::RunCommand; use crate::input::config::{Config, ConfigError, KdlError}; use crate::input::keybinds::Keybinds; use crate::input::layout::{Layout, RunPlugin, RunPluginLocation}; @@ -339,6 +338,16 @@ pub fn kdl_child_string_value_for_entry<'a>( .and_then(|cwd_value| cwd_value.value().as_string()) } +pub fn kdl_child_bool_value_for_entry<'a>( + command_metadata: &'a KdlDocument, + entry_name: &'a str, +) -> Option { + command_metadata + .get(entry_name) + .and_then(|cwd| cwd.entries().iter().next()) + .and_then(|cwd_value| cwd_value.value().as_bool()) +} + impl Action { pub fn new_from_bytes( action_name: &str, @@ -741,12 +750,20 @@ impl TryFrom<&KdlNode> for Action { let direction = command_metadata .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "direction")) .and_then(|direction_string| Direction::from_str(direction_string).ok()); + let hold_on_close = command_metadata + .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "close_on_exit")) + .and_then(|close_on_exit| Some(!close_on_exit)) + .unwrap_or(true); + let hold_on_start = command_metadata + .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "start_suspended")) + .unwrap_or(false); let run_command_action = RunCommandAction { command: PathBuf::from(command), args, cwd, direction, - hold_on_close: true, + hold_on_close, + hold_on_start, }; Ok(Action::Run(run_command_action)) }, @@ -1464,32 +1481,6 @@ impl Keybinds { } } -impl RunCommand { - pub fn from_kdl(kdl_node: &KdlNode) -> Result { - let command = PathBuf::from(kdl_get_child_entry_string_value!(kdl_node, "cmd").ok_or( - ConfigError::new_kdl_error( - "Command must have a cmd value".into(), - kdl_node.span().offset(), - kdl_node.span().len(), - ), - )?); - let cwd = kdl_get_child_entry_string_value!(kdl_node, "cwd").map(|c| PathBuf::from(c)); - let args = match kdl_get_child!(kdl_node, "args") { - Some(kdl_args) => kdl_string_arguments!(kdl_args) - .iter() - .map(|s| String::from(*s)) - .collect(), - None => vec![], - }; - Ok(RunCommand { - command, - args, - cwd, - hold_on_close: true, - }) - } -} - impl Config { pub fn from_kdl(kdl_config: &str, base_config: Option) -> Result { let mut config = base_config.unwrap_or_else(|| Config::default());