feat(command-panes): allow to start suspended (#1887)

* feat(command-panes): allow panes to start suspended

* style(fmt): remove unused code

* style(fmt): rustfmt
This commit is contained in:
Aram Drevekenin 2022-11-01 09:07:25 +01:00 committed by GitHub
parent 6d29c6951e
commit abc700fc4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 668 additions and 172 deletions

View File

@ -26,6 +26,7 @@ fn main() {
floating, floating,
name, name,
close_on_exit, close_on_exit,
start_suspended,
})) = opts.command })) = opts.command
{ {
let command_cli_action = CliAction::NewPane { let command_cli_action = CliAction::NewPane {
@ -35,6 +36,7 @@ fn main() {
floating, floating,
name, name,
close_on_exit, close_on_exit,
start_suspended,
}; };
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

@ -1914,6 +1914,25 @@ pub fn send_command_through_the_cli() {
step_is_complete 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("<Ctrl-c>")
&& 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 { .add_step(Step {
name: "Wait for command to run", name: "Wait for command to run",
instruction: |mut remote_terminal: RemoteTerminal| -> bool { instruction: |mut remote_terminal: RemoteTerminal| -> bool {

View File

@ -346,7 +346,8 @@ impl RemoteTerminal {
let mut channel = self.channel.lock().unwrap(); let mut channel = self.channel.lock().unwrap();
channel channel
.write_all( .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(); .unwrap();
channel.flush().unwrap(); channel.flush().unwrap();

View File

@ -1,14 +1,14 @@
--- ---
source: src/tests/e2e/cases.rs source: src/tests/e2e/cases.rs
assertion_line: 1968 assertion_line: 1998
expression: last_snapshot expression: last_snapshot
--- ---
Zellij (e2e-test)  Tab #1  Zellij (e2e-test)  Tab #1 
┌ Pane #1 ─────────────────────────────────────────────────┐┌ /usr/src/zellij/fixtures/append-echo-script.sh ──────────┐ ┌ Pane #1 ─────────────────────────────────────────────────┐┌ /usr/src/zellij/fixtures/append-echo-script.sh ──────────┐
│$ /usr/src/zellij/x86_64-unknown-linux-musl/release/zellij││foo │ │$ /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 │
$ ││█ │ " ││█ │
││ │ $ ││ │
│ ││ │ │ ││ │
│ ││ │ │ ││ │
│ ││ │ │ ││ │

View File

@ -280,6 +280,7 @@ fn spawn_terminal(
args, args,
cwd: None, cwd: None,
hold_on_close: false, hold_on_close: false,
hold_on_start: false,
} }
}, },
TerminalAction::RunCommand(command) => command, TerminalAction::RunCommand(command) => command,
@ -381,6 +382,10 @@ pub trait ServerOsApi: Send + Sync {
quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
default_editor: Option<PathBuf>, default_editor: Option<PathBuf>,
) -> Result<(u32, RawFd, RawFd), SpawnTerminalError>; ) -> Result<(u32, RawFd, RawFd), SpawnTerminalError>;
// reserves a terminal id without actually opening a terminal
fn reserve_terminal_id(&self) -> Result<u32, SpawnTerminalError> {
unimplemented!()
}
/// Read bytes from the standard output of the virtual terminal referred to by `fd`. /// 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<usize, nix::Error>; fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
/// Creates an `AsyncReader` that can be used to read from `fd` in an async context /// 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), None => Err(SpawnTerminalError::NoMoreTerminalIds),
} }
} }
fn reserve_terminal_id(&self) -> Result<u32, SpawnTerminalError> {
let mut terminal_id = None;
{
let current_ids: HashSet<u32> = 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<usize, nix::Error> { fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
unistd::read(fd, buf) unistd::read(fd, buf)
} }

View File

@ -156,11 +156,12 @@ impl FloatingPanes {
&mut self, &mut self,
pane_id: PaneId, pane_id: PaneId,
exit_status: Option<i32>, exit_status: Option<i32>,
is_first_run: bool,
run_command: RunCommand, run_command: RunCommand,
) { ) {
self.panes self.panes
.get_mut(&pane_id) .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<dyn Pane>> { pub fn get(&self, pane_id: &PaneId) -> Option<&Box<dyn Pane>> {
self.panes.get(pane_id) self.panes.get(pane_id)

View File

@ -1533,6 +1533,7 @@ impl Grid {
self.sixel_scrolling = false; self.sixel_scrolling = false;
self.mouse_mode = MouseMode::NoEncoding; self.mouse_mode = MouseMode::NoEncoding;
self.mouse_tracking = MouseTracking::Off; self.mouse_tracking = MouseTracking::Off;
self.cursor_is_hidden = false;
if let Some(images_to_reap) = self.sixel_grid.clear() { if let Some(images_to_reap) = self.sixel_grid.clear() {
self.sixel_grid.reap_images(images_to_reap); self.sixel_grid.reap_images(images_to_reap);
} }

View File

@ -3,7 +3,12 @@ use std::fmt::{self, Debug, Display, Formatter};
use std::ops::{Index, IndexMut}; use std::ops::{Index, IndexMut};
use unicode_width::UnicodeWidthChar; 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; use crate::panes::alacritty_functions::parse_sgr_color;
@ -736,3 +741,113 @@ impl ::std::fmt::Debug for TerminalCharacter {
write!(f, "{}", self.character) 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
)
},
}
}

View File

@ -2,7 +2,7 @@ use crate::output::{CharacterChunk, SixelImageChunk};
use crate::panes::sixel::SixelImageStore; use crate::panes::sixel::SixelImageStore;
use crate::panes::{ use crate::panes::{
grid::Grid, 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::panes::{AnsiCode, LinkHandler};
use crate::pty::VteBytes; 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? 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 // 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<dyn ServerOsApi> in here somewhere. // their `reflow_lines()` method. Drop a Box<dyn ServerOsApi> in here somewhere.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -104,8 +106,10 @@ pub struct TerminalPane {
borderless: bool, 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 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, search_term: String,
is_held: Option<(Option<i32>, RunCommand)>, // a "held" pane means that its command has exited and its waiting for a is_held: Option<(Option<i32>, 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 // possible user instruction to be re-run, or that the command has not yet been run
banner: Option<String>, // 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 { impl Pane for TerminalPane {
@ -170,13 +174,14 @@ impl Pane for TerminalPane {
// needs to be adjusted. // needs to be adjusted.
// here we match against those cases - if need be, we adjust the input and if not // here we match against those cases - if need be, we adjust the input and if not
// we send back the original input // 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() { match input_bytes.as_slice() {
ENTER_CARRIAGE_RETURN | ENTER_NEWLINE | SPACE => { ENTER_CARRIAGE_RETURN | ENTER_NEWLINE | SPACE => {
let run_command = run_command.clone(); let run_command = run_command.clone();
self.is_held = None; self.is_held = None;
self.grid.reset_terminal_state(); self.grid.reset_terminal_state();
self.set_should_render(true); self.set_should_render(true);
self.remove_banner();
Some(AdjustedInput::ReRunCommandInThisPane(run_command)) Some(AdjustedInput::ReRunCommandInThisPane(run_command))
}, },
CTRL_C => Some(AdjustedInput::CloseThisPane), CTRL_C => Some(AdjustedInput::CloseThisPane),
@ -395,8 +400,12 @@ impl Pane for TerminalPane {
pane_title, pane_title,
frame_params, frame_params,
); );
if let Some((exit_status, _run_command)) = &self.is_held { if let Some((exit_status, is_first_run, _run_command)) = &self.is_held {
frame.add_exit_status(exit_status.as_ref().copied()); 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) { let res = match self.frame.get(&client_id) {
@ -701,8 +710,11 @@ impl Pane for TerminalPane {
fn is_alternate_mode_active(&self) -> bool { fn is_alternate_mode_active(&self) -> bool {
self.grid.is_alternate_mode_active() self.grid.is_alternate_mode_active()
} }
fn hold(&mut self, exit_status: Option<i32>, run_command: RunCommand) { fn hold(&mut self, exit_status: Option<i32>, is_first_run: bool, run_command: RunCommand) {
self.is_held = Some((exit_status, run_command)); 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); self.set_should_render(true);
} }
} }
@ -752,6 +764,7 @@ impl TerminalPane {
fake_cursor_locations: HashSet::new(), fake_cursor_locations: HashSet::new(),
search_term: String::new(), search_term: String::new(),
is_held: None, is_held: None,
banner: None,
} }
} }
pub fn get_x(&self) -> usize { pub fn get_x(&self) -> usize {
@ -782,6 +795,10 @@ impl TerminalPane {
let rows = self.get_content_rows(); let rows = self.get_content_rows();
let cols = self.get_content_columns(); let cols = self.get_content_columns();
self.grid.change_size(rows, cols); 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); self.set_should_render(true);
} }
pub fn read_buffer_as_lines(&self) -> Vec<Vec<TerminalCharacter>> { pub fn read_buffer_as_lines(&self) -> Vec<Vec<TerminalCharacter>> {
@ -791,6 +808,25 @@ impl TerminalPane {
// (x, y) // (x, y)
self.grid.cursor_coordinates() 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)] #[cfg(test)]

View File

@ -1010,11 +1010,12 @@ impl TiledPanes {
&mut self, &mut self,
pane_id: PaneId, pane_id: PaneId,
exit_status: Option<i32>, exit_status: Option<i32>,
is_first_run: bool,
run_command: RunCommand, run_command: RunCommand,
) { ) {
self.panes self.panes
.get_mut(&pane_id) .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 { pub fn panes_to_hide_contains(&self, pane_id: PaneId) -> bool {
self.panes_to_hide.contains(&pane_id) self.panes_to_hide.contains(&pane_id)

View File

@ -108,25 +108,29 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
_ => (false, None, name), _ => (false, None, name),
}; };
match pty.spawn_terminal(terminal_action, client_or_tab_index) { 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 pty.bus
.senders .senders
.send_to_screen(ScreenInstruction::NewPane( .send_to_screen(ScreenInstruction::NewPane(
PaneId::Terminal(pid), PaneId::Terminal(pid),
pane_title, pane_title,
should_float, should_float,
hold_for_command,
client_or_tab_index, client_or_tab_index,
)) ))
.with_context(err_context)?; .with_context(err_context)?;
}, },
Err(SpawnTerminalError::CommandNotFound(pid)) => { Err(SpawnTerminalError::CommandNotFound(pid)) => {
if hold_on_close { if hold_on_close {
let hold_for_command = None; // we do not hold an "error" pane
pty.bus pty.bus
.senders .senders
.send_to_screen(ScreenInstruction::NewPane( .send_to_screen(ScreenInstruction::NewPane(
PaneId::Terminal(pid), PaneId::Terminal(pid),
pane_title, pane_title,
should_float, should_float,
hold_for_command,
client_or_tab_index, client_or_tab_index,
)) ))
.with_context(err_context)?; .with_context(err_context)?;
@ -154,7 +158,7 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
Some(TerminalAction::OpenFile(temp_file, line_number)), Some(TerminalAction::OpenFile(temp_file, line_number)),
ClientOrTabIndex::ClientId(client_id), ClientOrTabIndex::ClientId(client_id),
) { ) {
Ok(pid) => { Ok((pid, _starts_held)) => {
pty.bus pty.bus
.senders .senders
.send_to_screen(ScreenInstruction::OpenInPlaceEditor( .send_to_screen(ScreenInstruction::OpenInPlaceEditor(
@ -178,23 +182,27 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
_ => (false, None, name), _ => (false, None, name),
}; };
match pty.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)) { 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 pty.bus
.senders .senders
.send_to_screen(ScreenInstruction::VerticalSplit( .send_to_screen(ScreenInstruction::VerticalSplit(
PaneId::Terminal(pid), PaneId::Terminal(pid),
pane_title, pane_title,
hold_for_command,
client_id, client_id,
)) ))
.with_context(err_context)?; .with_context(err_context)?;
}, },
Err(SpawnTerminalError::CommandNotFound(pid)) => { Err(SpawnTerminalError::CommandNotFound(pid)) => {
if hold_on_close { if hold_on_close {
let hold_for_command = None; // error panes are never held
pty.bus pty.bus
.senders .senders
.send_to_screen(ScreenInstruction::VerticalSplit( .send_to_screen(ScreenInstruction::VerticalSplit(
PaneId::Terminal(pid), PaneId::Terminal(pid),
pane_title, pane_title,
hold_for_command,
client_id, client_id,
)) ))
.with_context(err_context)?; .with_context(err_context)?;
@ -238,23 +246,27 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
_ => (false, None, name), _ => (false, None, name),
}; };
match pty.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)) { 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 pty.bus
.senders .senders
.send_to_screen(ScreenInstruction::HorizontalSplit( .send_to_screen(ScreenInstruction::HorizontalSplit(
PaneId::Terminal(pid), PaneId::Terminal(pid),
pane_title, pane_title,
hold_for_command,
client_id, client_id,
)) ))
.with_context(err_context)?; .with_context(err_context)?;
}, },
Err(SpawnTerminalError::CommandNotFound(pid)) => { Err(SpawnTerminalError::CommandNotFound(pid)) => {
if hold_on_close { if hold_on_close {
let hold_for_command = None; // error panes are never held
pty.bus pty.bus
.senders .senders
.send_to_screen(ScreenInstruction::HorizontalSplit( .send_to_screen(ScreenInstruction::HorizontalSplit(
PaneId::Terminal(pid), PaneId::Terminal(pid),
pane_title, pane_title,
hold_for_command,
client_id, client_id,
)) ))
.with_context(err_context)?; .with_context(err_context)?;
@ -391,6 +403,7 @@ impl Pty {
command: PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable")), 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 cwd, // note: this might also be filled by the calling function, eg. spawn_terminal
hold_on_close: false, hold_on_close: false,
hold_on_start: false,
}) })
} }
fn fill_cwd(&self, terminal_action: &mut TerminalAction, client_id: ClientId) { fn fill_cwd(&self, terminal_action: &mut TerminalAction, client_id: ClientId) {
@ -416,7 +429,8 @@ impl Pty {
&mut self, &mut self,
terminal_action: Option<TerminalAction>, terminal_action: Option<TerminalAction>,
client_or_tab_index: ClientOrTabIndex, client_or_tab_index: ClientOrTabIndex,
) -> Result<u32, SpawnTerminalError> { ) -> Result<(u32, bool), SpawnTerminalError> {
// bool is starts_held
// returns the terminal id // returns the terminal id
let terminal_action = match client_or_tab_index { let terminal_action = match client_or_tab_index {
ClientOrTabIndex::ClientId(client_id) => { ClientOrTabIndex::ClientId(client_id) => {
@ -429,10 +443,20 @@ impl Pty {
terminal_action.unwrap_or_else(|| self.get_default_terminal(None)) terminal_action.unwrap_or_else(|| self.get_default_terminal(None))
}, },
}; };
let hold_on_close = match &terminal_action { let (hold_on_start, hold_on_close) = match &terminal_action {
TerminalAction::RunCommand(run_command) => run_command.hold_on_close, TerminalAction::RunCommand(run_command) => {
_ => false, (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 quit_cb = Box::new({
let senders = self.bus.senders.clone(); let senders = self.bus.senders.clone();
move |pane_id, exit_status, command| { move |pane_id, exit_status, command| {
@ -477,7 +501,8 @@ impl Pty {
self.task_handles.insert(terminal_id, terminal_bytes); self.task_handles.insert(terminal_id, terminal_bytes);
self.id_to_child_pid.insert(terminal_id, child_fd); 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( pub fn spawn_terminals_for_layout(
&mut self, &mut self,
@ -489,10 +514,15 @@ impl Pty {
let mut default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal(None)); let mut default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal(None));
self.fill_cwd(&mut default_shell, client_id); self.fill_cwd(&mut default_shell, client_id);
let extracted_run_instructions = layout.extract_run_instructions(); let extracted_run_instructions = layout.extract_run_instructions();
let mut new_pane_pids: Vec<(u32, Option<RunCommand>, Result<RawFd, SpawnTerminalError>)> = let mut new_pane_pids: Vec<(
vec![]; // (terminal_id, u32,
// run_command bool,
// file_descriptor) Option<RunCommand>,
Result<RawFd, SpawnTerminalError>,
)> = vec![]; // (terminal_id,
// starts_held,
// run_command,
// file_descriptor)
for run_instruction in extracted_run_instructions { for run_instruction in extracted_run_instructions {
let quit_cb = Box::new({ let quit_cb = Box::new({
let senders = self.bus.senders.clone(); let senders = self.bus.senders.clone();
@ -502,6 +532,7 @@ impl Pty {
}); });
match run_instruction { match run_instruction {
Some(Run::Command(command)) => { Some(Run::Command(command)) => {
let starts_held = command.hold_on_start;
let hold_on_close = command.hold_on_close; let hold_on_close = command.hold_on_close;
let quit_cb = Box::new({ let quit_cb = Box::new({
let senders = self.bus.senders.clone(); let senders = self.bus.senders.clone();
@ -520,34 +551,56 @@ impl Pty {
} }
}); });
let cmd = TerminalAction::RunCommand(command.clone()); let cmd = TerminalAction::RunCommand(command.clone());
match self if starts_held {
.bus // we don't actually open a terminal in this case, just wait for the user to run it
.os_input match self.bus.os_input.as_mut().unwrap().reserve_terminal_id() {
.as_mut() Ok(terminal_id) => {
.with_context(err_context)? new_pane_pids.push((
.spawn_terminal(cmd, quit_cb, self.default_editor.clone()) terminal_id,
{ starts_held,
Ok((terminal_id, pid_primary, child_fd)) => { Some(command.clone()),
self.id_to_child_pid.insert(terminal_id, child_fd); Ok(terminal_id as i32), // this is not actually correct but gets
new_pane_pids.push(( // stripped later
terminal_id, ));
Some(command.clone()), },
Ok(pid_primary), Err(e) => {
)); log::error!("Failed to spawn terminal: {}", e);
}, },
Err(SpawnTerminalError::CommandNotFound(terminal_id)) => { }
new_pane_pids.push(( } else {
terminal_id, match self
Some(command.clone()), .bus
Err(SpawnTerminalError::CommandNotFound(terminal_id)), .os_input
)); .as_mut()
}, .with_context(err_context)?
Err(e) => { .spawn_terminal(cmd, quit_cb, self.default_editor.clone())
log::error!("Failed to spawn terminal: {}", e); {
}, 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)) => { Some(Run::Cwd(cwd)) => {
let starts_held = false; // we do not hold Cwd panes
let shell = self.get_default_terminal(Some(cwd)); let shell = self.get_default_terminal(Some(cwd));
match self match self
.bus .bus
@ -558,11 +611,12 @@ impl Pty {
{ {
Ok((terminal_id, pid_primary, child_fd)) => { Ok((terminal_id, pid_primary, child_fd)) => {
self.id_to_child_pid.insert(terminal_id, 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)) => { Err(SpawnTerminalError::CommandNotFound(terminal_id)) => {
new_pane_pids.push(( new_pane_pids.push((
terminal_id, terminal_id,
starts_held,
None, None,
Err(SpawnTerminalError::CommandNotFound(terminal_id)), Err(SpawnTerminalError::CommandNotFound(terminal_id)),
)); ));
@ -573,6 +627,7 @@ impl Pty {
} }
}, },
Some(Run::EditFile(path_to_file, line_number)) => { Some(Run::EditFile(path_to_file, line_number)) => {
let starts_held = false; // we do not hold edit panes (for now?)
match self match self
.bus .bus
.os_input .os_input
@ -585,11 +640,13 @@ impl Pty {
) { ) {
Ok((terminal_id, pid_primary, child_fd)) => { Ok((terminal_id, pid_primary, child_fd)) => {
self.id_to_child_pid.insert(terminal_id, 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)) => { Err(SpawnTerminalError::CommandNotFound(terminal_id)) => {
let starts_held = false; // we do not hold error panes
new_pane_pids.push(( new_pane_pids.push((
terminal_id, terminal_id,
starts_held,
None, None,
Err(SpawnTerminalError::CommandNotFound(terminal_id)), Err(SpawnTerminalError::CommandNotFound(terminal_id)),
)); ));
@ -600,6 +657,7 @@ impl Pty {
} }
}, },
None => { None => {
let starts_held = false;
match self match self
.bus .bus
.os_input .os_input
@ -609,11 +667,12 @@ impl Pty {
{ {
Ok((terminal_id, pid_primary, child_fd)) => { Ok((terminal_id, pid_primary, child_fd)) => {
self.id_to_child_pid.insert(terminal_id, 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)) => { Err(SpawnTerminalError::CommandNotFound(terminal_id)) => {
new_pane_pids.push(( new_pane_pids.push((
terminal_id, terminal_id,
starts_held,
None, None,
Err(SpawnTerminalError::CommandNotFound(terminal_id)), Err(SpawnTerminalError::CommandNotFound(terminal_id)),
)); ));
@ -627,10 +686,17 @@ impl Pty {
Some(Run::Plugin(_)) => {}, Some(Run::Plugin(_)) => {},
} }
} }
let new_tab_pane_ids: Vec<u32> = new_pane_pids // Option<RunCommand> should only be Some if the pane starts held
let new_tab_pane_ids: Vec<(u32, Option<RunCommand>)> = new_pane_pids
.iter() .iter()
.map(|(terminal_id, _, _)| *terminal_id) .map(|(terminal_id, starts_held, run_command, _)| {
.collect::<Vec<u32>>(); if *starts_held {
(*terminal_id, run_command.clone())
} else {
(*terminal_id, None)
}
})
.collect();
self.bus self.bus
.senders .senders
.send_to_screen(ScreenInstruction::NewTab( .send_to_screen(ScreenInstruction::NewTab(
@ -639,7 +705,11 @@ impl Pty {
client_id, client_id,
)) ))
.with_context(err_context)?; .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 { match pid_primary {
Ok(pid_primary) => { Ok(pid_primary) => {
let terminal_bytes = task::spawn({ 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.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 _ = 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 quit_cb = Box::new({
let senders = self.bus.senders.clone(); let senders = self.bus.senders.clone();
move |pane_id, exit_status, command| { move |pane_id, exit_status, command| {
// we only re-run held panes, so we'll never close them from Pty if hold_on_close {
let _ = senders.send_to_screen(ScreenInstruction::HoldPane( let _ = senders.send_to_screen(ScreenInstruction::HoldPane(
pane_id, pane_id,
exit_status, exit_status,
command, command,
None, None,
)); ));
} else {
let _ =
senders.send_to_screen(ScreenInstruction::ClosePane(pane_id, None));
}
} }
}); });
let (pid_primary, child_fd): (RawFd, RawFd) = self let (pid_primary, child_fd): (RawFd, RawFd) = self

View File

@ -112,19 +112,27 @@ macro_rules! active_tab_and_connected_client_id {
}; };
} }
type InitialTitle = String;
type ShouldFloat = bool;
type HoldForCommand = Option<RunCommand>;
/// Instructions that can be sent to the [`Screen`]. /// Instructions that can be sent to the [`Screen`].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ScreenInstruction { pub enum ScreenInstruction {
PtyBytes(u32, VteBytes), PtyBytes(u32, VteBytes),
Render, Render,
NewPane(PaneId, Option<String>, Option<bool>, ClientOrTabIndex), // String is initial title, NewPane(
// bool (if Some) is PaneId,
// should_float Option<InitialTitle>,
Option<ShouldFloat>,
HoldForCommand,
ClientOrTabIndex,
),
OpenInPlaceEditor(PaneId, ClientId), OpenInPlaceEditor(PaneId, ClientId),
TogglePaneEmbedOrFloating(ClientId), TogglePaneEmbedOrFloating(ClientId),
ToggleFloatingPanes(ClientId, Option<TerminalAction>), ToggleFloatingPanes(ClientId, Option<TerminalAction>),
HorizontalSplit(PaneId, Option<String>, ClientId), // String is initial title HorizontalSplit(PaneId, Option<InitialTitle>, HoldForCommand, ClientId),
VerticalSplit(PaneId, Option<String>, ClientId), // String is initial title VerticalSplit(PaneId, Option<InitialTitle>, HoldForCommand, ClientId),
WriteCharacter(Vec<u8>, ClientId), WriteCharacter(Vec<u8>, ClientId),
ResizeLeft(ClientId), ResizeLeft(ClientId),
ResizeRight(ClientId), ResizeRight(ClientId),
@ -167,7 +175,7 @@ pub enum ScreenInstruction {
HoldPane(PaneId, Option<i32>, RunCommand, Option<ClientId>), // Option<i32> is the exit status HoldPane(PaneId, Option<i32>, RunCommand, Option<ClientId>), // Option<i32> is the exit status
UpdatePaneName(Vec<u8>, ClientId), UpdatePaneName(Vec<u8>, ClientId),
UndoRenamePane(ClientId), UndoRenamePane(ClientId),
NewTab(PaneLayout, Vec<u32>, ClientId), NewTab(PaneLayout, Vec<(u32, HoldForCommand)>, ClientId),
SwitchTabNext(ClientId), SwitchTabNext(ClientId),
SwitchTabPrev(ClientId), SwitchTabPrev(ClientId),
ToggleActiveSyncTab(ClientId), ToggleActiveSyncTab(ClientId),
@ -811,7 +819,7 @@ impl Screen {
pub fn new_tab( pub fn new_tab(
&mut self, &mut self,
layout: PaneLayout, layout: PaneLayout,
new_ids: Vec<u32>, new_ids: Vec<(u32, HoldForCommand)>,
client_id: ClientId, client_id: ClientId,
) -> Result<()> { ) -> Result<()> {
let client_id = if self.get_active_tab(client_id).is_some() { let client_id = if self.get_active_tab(client_id).is_some() {
@ -1227,6 +1235,7 @@ pub(crate) fn screen_thread_main(
pid, pid,
initial_pane_title, initial_pane_title,
should_float, should_float,
hold_for_command,
client_or_tab_index, client_or_tab_index,
) => { ) => {
match client_or_tab_index { match client_or_tab_index {
@ -1237,10 +1246,27 @@ pub(crate) fn screen_thread_main(
should_float, should_float,
Some(client_id)), 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) => { ClientOrTabIndex::TabIndex(tab_index) => {
if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { if let Some(active_tab) = screen.tabs.get_mut(&tab_index) {
active_tab.new_pane(pid, initial_pane_title, should_float, None)?; 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 { } else {
log::error!("Tab index not found: {:?}", tab_index); log::error!("Tab index not found: {:?}", tab_index);
} }
@ -1275,24 +1301,60 @@ pub(crate) fn screen_thread_main(
screen.render()?; 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!( active_tab_and_connected_client_id!(
screen, screen,
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.horizontal_split(pid, initial_pane_title, 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.unblock_input()?;
screen.update_tabs()?; screen.update_tabs()?;
screen.render()?; 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!( active_tab_and_connected_client_id!(
screen, screen,
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.vertical_split(pid, initial_pane_title, 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.unblock_input()?;
screen.update_tabs()?; screen.update_tabs()?;
screen.render()?; screen.render()?;
@ -1644,18 +1706,20 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::HoldPane(id, exit_status, run_command, client_id) => { ScreenInstruction::HoldPane(id, exit_status, run_command, client_id) => {
let is_first_run = false;
match client_id { match client_id {
Some(client_id) => { Some(client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab.hold_pane( active_tab!(screen, client_id, |tab: &mut Tab| tab.hold_pane(
id, id,
exit_status, exit_status,
is_first_run,
run_command run_command
)); ));
}, },
None => { None => {
for tab in screen.tabs.values_mut() { for tab in screen.tabs.values_mut() {
if tab.get_all_pane_ids().contains(&id) { 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; break;
} }
} }

View File

@ -61,6 +61,8 @@ macro_rules! resize_pty {
}; };
} }
type HoldForCommand = Option<RunCommand>;
// FIXME: This should be replaced by `RESIZE_PERCENT` at some point // FIXME: This should be replaced by `RESIZE_PERCENT` at some point
pub const MIN_TERMINAL_HEIGHT: usize = 5; pub const MIN_TERMINAL_HEIGHT: usize = 5;
pub const MIN_TERMINAL_WIDTH: 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 by default (only terminal-panes support alternate mode)
false false
} }
fn hold(&mut self, _exit_status: Option<i32>, _run_command: RunCommand) { fn hold(&mut self, _exit_status: Option<i32>, _is_first_run: bool, _run_command: RunCommand) {
// No-op by default, only terminal panes support holding // No-op by default, only terminal panes support holding
} }
} }
@ -470,7 +472,7 @@ impl Tab {
pub fn apply_layout( pub fn apply_layout(
&mut self, &mut self,
layout: PaneLayout, layout: PaneLayout,
new_ids: Vec<u32>, new_ids: Vec<(u32, HoldForCommand)>,
tab_index: usize, tab_index: usize,
client_id: ClientId, client_id: ClientId,
) -> Result<()> { ) -> Result<()> {
@ -532,7 +534,7 @@ impl Tab {
set_focus_pane_id(layout, PaneId::Plugin(pid)); set_focus_pane_id(layout, PaneId::Plugin(pid));
} else { } else {
// there are still panes left to fill, use the pids we received in this method // 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 next_terminal_position = self.get_next_terminal_position();
let initial_title = match &layout.run { let initial_title = match &layout.run {
Some(Run::Command(run_command)) => Some(run_command.to_string()), Some(Run::Command(run_command)) => Some(run_command.to_string()),
@ -552,6 +554,9 @@ impl Tab {
initial_title, initial_title,
); );
new_pane.set_borderless(layout.borderless); 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( self.tiled_panes.add_pane_with_existing_geom(
PaneId::Terminal(*pid), PaneId::Terminal(*pid),
Box::new(new_pane), 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 // 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 // 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 // fixing this will require a bit of an architecture change
@ -601,7 +606,7 @@ impl Tab {
Ok(()) Ok(())
}, },
Err(e) => { Err(e) => {
for unused_pid in new_ids { for (unused_pid, _) in new_ids {
self.senders self.senders
.send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(unused_pid))) .send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(unused_pid)))
.with_context(err_context)?; .with_context(err_context)?;
@ -1746,11 +1751,19 @@ impl Tab {
closed_pane closed_pane
} }
} }
pub fn hold_pane(&mut self, id: PaneId, exit_status: Option<i32>, run_command: RunCommand) { pub fn hold_pane(
&mut self,
id: PaneId,
exit_status: Option<i32>,
is_first_run: bool,
run_command: RunCommand,
) {
if self.floating_panes.panes_contain(&id) { 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 { } 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<Box<dyn Pane>> { pub fn replace_pane_with_suppressed_pane(&mut self, pane_id: PaneId) -> Option<Box<dyn Pane>> {

View File

@ -223,7 +223,7 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab {
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, 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(); .unwrap();
tab tab
} }
@ -277,7 +277,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str)
.extract_run_instructions() .extract_run_instructions()
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, _)| i as u32) .map(|(i, _)| (i as u32, None))
.collect(); .collect();
tab.apply_layout(tab_layout, pane_ids, index, client_id) tab.apply_layout(tab_layout, pane_ids, index, client_id)
.unwrap(); .unwrap();
@ -332,14 +332,8 @@ fn create_new_tab_with_mock_pty_writer(
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
); );
tab.apply_layout( tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id)
// LayoutTemplate::default().try_into().unwrap(), .unwrap();
PaneLayout::default(),
vec![1],
index,
client_id,
)
.unwrap();
tab tab
} }
@ -393,7 +387,7 @@ fn create_new_tab_with_sixel_support(
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, 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(); .unwrap();
tab tab
} }

View File

@ -142,7 +142,7 @@ fn create_new_tab(size: Size) -> Tab {
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, 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(); .unwrap();
tab tab
} }
@ -189,7 +189,7 @@ fn create_new_tab_with_cell_size(
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, 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(); .unwrap();
tab tab
} }

View File

@ -89,6 +89,7 @@ pub struct PaneFrame {
pub other_cursors_exist_in_session: bool, pub other_cursors_exist_in_session: bool,
pub other_focused_clients: Vec<ClientId>, pub other_focused_clients: Vec<ClientId>,
exit_status: Option<ExitStatus>, exit_status: Option<ExitStatus>,
is_first_run: bool,
} }
impl PaneFrame { impl PaneFrame {
@ -109,6 +110,7 @@ impl PaneFrame {
other_focused_clients: frame_params.other_focused_clients, other_focused_clients: frame_params.other_focused_clients,
other_cursors_exist_in_session: frame_params.other_cursors_exist_in_session, other_cursors_exist_in_session: frame_params.other_cursors_exist_in_session,
exit_status: None, exit_status: None,
is_first_run: false,
} }
} }
pub fn add_exit_status(&mut self, exit_status: Option<i32>) { pub fn add_exit_status(&mut self, exit_status: Option<i32>) {
@ -117,6 +119,9 @@ impl PaneFrame {
None => Some(ExitStatus::Exited), None => Some(ExitStatus::Exited),
}; };
} }
pub fn indicate_first_run(&mut self) {
self.is_first_run = true;
}
fn client_cursor(&self, client_id: ClientId) -> Vec<TerminalCharacter> { fn client_cursor(&self, client_id: ClientId) -> Vec<TerminalCharacter> {
let color = client_id_to_colors(client_id, self.style.colors); let color = client_id_to_colors(client_id, self.style.colors);
background_color(" ", color.map(|c| c.0)) background_color(" ", color.map(|c| c.0))
@ -611,11 +616,7 @@ impl PaneFrame {
} }
fn render_held_undertitle(&self) -> Result<Vec<TerminalCharacter>> { fn render_held_undertitle(&self) -> Result<Vec<TerminalCharacter>> {
let max_undertitle_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners let max_undertitle_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
let exit_status = self let (mut first_part, first_part_len) = self.first_exited_held_title_part_full();
.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 left_boundary = let mut left_boundary =
foreground_color(self.get_corner(boundary_type::BOTTOM_LEFT), self.color); foreground_color(self.get_corner(boundary_type::BOTTOM_LEFT), self.color);
let mut right_boundary = let mut right_boundary =
@ -683,7 +684,7 @@ impl PaneFrame {
character_chunks.push(CharacterChunk::new(title, x, y)); character_chunks.push(CharacterChunk::new(title, x, y));
} else if row == self.geom.rows - 1 { } else if row == self.geom.rows - 1 {
// bottom row // bottom row
if self.exit_status.is_some() { if self.exit_status.is_some() || self.is_first_run {
let x = self.geom.x; let x = self.geom.x;
let y = self.geom.y + row; let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new( character_chunks.push(CharacterChunk::new(
@ -727,13 +728,10 @@ impl PaneFrame {
} }
Ok((character_chunks, None)) Ok((character_chunks, None))
} }
fn first_held_title_part_full( fn first_exited_held_title_part_full(&self) -> (Vec<TerminalCharacter>, usize) {
&self,
exit_status: ExitStatus,
) -> (Vec<TerminalCharacter>, usize) {
// (title part, length) // (title part, length)
match exit_status { match self.exit_status {
ExitStatus::Code(exit_code) => { Some(ExitStatus::Code(exit_code)) => {
let mut first_part = vec![]; let mut first_part = vec![];
let left_bracket = " [ "; let left_bracket = " [ ";
let exited_text = "EXIT CODE: "; let exited_text = "EXIT CODE: ";
@ -759,7 +757,7 @@ impl PaneFrame {
+ right_bracket.len(), + right_bracket.len(),
) )
}, },
ExitStatus::Exited => { Some(ExitStatus::Exited) => {
let mut first_part = vec![]; let mut first_part = vec![];
let left_bracket = " [ "; let left_bracket = " [ ";
let exited_text = "EXITED"; let exited_text = "EXITED";
@ -775,15 +773,20 @@ impl PaneFrame {
left_bracket.len() + exited_text.len() + right_bracket.len(), 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<TerminalCharacter>, usize) { fn second_held_title_part_full(&self) -> (Vec<TerminalCharacter>, usize) {
// (title part, length) // (title part, length)
let mut second_part = vec![]; let mut second_part = vec![];
let left_enter_bracket = "<"; let left_enter_bracket = if self.is_first_run { " <" } else { "<" };
let enter_text = "ENTER"; let enter_text = "ENTER";
let right_enter_bracket = ">"; 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 left_break_bracket = "<";
let break_text = "Ctrl-c"; let break_text = "Ctrl-c";
let right_break_bracket = ">"; let right_break_bracket = ">";

View File

@ -284,7 +284,7 @@ impl MockScreen {
let pane_count = pane_layout.extract_run_instructions().len(); let pane_count = pane_layout.extract_run_instructions().len();
let mut pane_ids = vec![]; let mut pane_ids = vec![];
for i in 0..pane_count { 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( let _ = self.to_screen.send(ScreenInstruction::NewTab(
pane_layout, pane_layout,
@ -297,7 +297,7 @@ impl MockScreen {
let pane_count = tab_layout.extract_run_instructions().len(); let pane_count = tab_layout.extract_run_instructions().len();
let mut pane_ids = vec![]; let mut pane_ids = vec![];
for i in 0..pane_count { 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( let _ = self.to_screen.send(ScreenInstruction::NewTab(
tab_layout, tab_layout,
@ -427,7 +427,7 @@ macro_rules! log_actions_in_thread {
fn new_tab(screen: &mut Screen, pid: u32) { fn new_tab(screen: &mut Screen, pid: u32) {
let client_id = 1; let client_id = 1;
screen screen
.new_tab(PaneLayout::default(), vec![pid], client_id) .new_tab(PaneLayout::default(), vec![(pid, None)], client_id)
.expect("TEST"); .expect("TEST");
} }
@ -1822,6 +1822,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() {
floating: false, floating: false,
name: None, name: None,
close_on_exit: false, close_on_exit: false,
start_suspended: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,
@ -1861,6 +1862,7 @@ pub fn send_cli_new_pane_action_with_split_direction() {
floating: false, floating: false,
name: None, name: None,
close_on_exit: false, close_on_exit: false,
start_suspended: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,
@ -1900,6 +1902,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() {
floating: false, floating: false,
name: None, name: None,
close_on_exit: false, close_on_exit: false,
start_suspended: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1981 assertion_line: 1989
expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" 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] [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]

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1907 assertion_line: 1915
expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" 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]

View File

@ -141,6 +141,10 @@ pub enum Sessions {
/// Close the pane immediately when its command exits /// Close the pane immediately when its command exits
#[clap(short, long, value_parser, default_value("false"), takes_value(false))] #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
close_on_exit: bool, 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 /// Edit file with default $EDITOR / $VISUAL
#[clap(visible_alias = "e")] #[clap(visible_alias = "e")]
@ -252,6 +256,16 @@ pub enum CliAction {
requires("command") requires("command")
)] )]
close_on_exit: bool, 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 /// Open the specified file in a new zellij pane with your default EDITOR
Edit { Edit {

View File

@ -260,11 +260,13 @@ impl Action {
floating, floating,
name, name,
close_on_exit, close_on_exit,
start_suspended,
} => { } => {
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_start = start_suspended;
let hold_on_close = !close_on_exit; let hold_on_close = !close_on_exit;
let run_command_action = RunCommandAction { let run_command_action = RunCommandAction {
command, command,
@ -272,6 +274,7 @@ impl Action {
cwd, cwd,
direction, direction,
hold_on_close, hold_on_close,
hold_on_start,
}; };
if floating { if floating {
Ok(vec![Action::NewFloatingPane( Ok(vec![Action::NewFloatingPane(

View File

@ -19,6 +19,8 @@ pub struct RunCommand {
pub cwd: Option<PathBuf>, pub cwd: Option<PathBuf>,
#[serde(default)] #[serde(default)]
pub hold_on_close: bool, pub hold_on_close: bool,
#[serde(default)]
pub hold_on_start: bool,
} }
impl std::fmt::Display for RunCommand { impl std::fmt::Display for RunCommand {
@ -50,6 +52,8 @@ pub struct RunCommandAction {
pub direction: Option<Direction>, pub direction: Option<Direction>,
#[serde(default)] #[serde(default)]
pub hold_on_close: bool, pub hold_on_close: bool,
#[serde(default)]
pub hold_on_start: bool,
} }
impl From<RunCommandAction> for RunCommand { impl From<RunCommandAction> for RunCommand {
@ -59,6 +63,7 @@ impl From<RunCommandAction> for RunCommand {
args: action.args, args: action.args,
cwd: action.cwd, cwd: action.cwd,
hold_on_close: action.hold_on_close, hold_on_close: action.hold_on_close,
hold_on_start: action.hold_on_start,
} }
} }
} }

View File

@ -142,7 +142,7 @@ impl Run {
} }
} }
pub fn add_close_on_exit(&mut self, close_on_exit: Option<bool>) { pub fn add_close_on_exit(&mut self, close_on_exit: Option<bool>) {
// 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 // and not empty
if let Some(close_on_exit) = close_on_exit { if let Some(close_on_exit) = close_on_exit {
if let Run::Command(run_command) = self { if let Run::Command(run_command) = self {
@ -150,6 +150,15 @@ impl Run {
} }
} }
} }
pub fn add_start_suspended(&mut self, start_suspended: Option<bool>) {
// 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)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]

View File

@ -281,6 +281,19 @@ fn layout_with_command_panes_and_close_on_exit() {
assert_snapshot!(format!("{:#?}", layout)); 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] #[test]
fn layout_with_plugin_panes() { fn layout_with_plugin_panes() {
let kdl_layout = r#" let kdl_layout = r#"

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1050 assertion_line: 1081
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -23,6 +23,7 @@ Layout {
args: [], args: [],
cwd: None, cwd: None,
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),
@ -45,6 +46,7 @@ Layout {
], ],
cwd: None, cwd: None,
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1033 assertion_line: 1046
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -26,6 +26,7 @@ Layout {
], ],
cwd: None, cwd: None,
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),
@ -48,6 +49,7 @@ Layout {
], ],
cwd: None, cwd: None,
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -23,6 +23,7 @@ Layout {
args: [], args: [],
cwd: None, cwd: None,
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),
@ -42,6 +43,7 @@ Layout {
args: [], args: [],
cwd: None, cwd: None,
hold_on_close: false, hold_on_close: false,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -23,6 +23,7 @@ Layout {
args: [], args: [],
cwd: None, cwd: None,
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),
@ -42,6 +43,7 @@ Layout {
args: [], args: [],
cwd: None, cwd: None,
hold_on_close: false, hold_on_close: false,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1085 assertion_line: 1133
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -23,6 +23,7 @@ Layout {
args: [], args: [],
cwd: None, cwd: None,
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),
@ -44,6 +45,7 @@ Layout {
"/home", "/home",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1068 assertion_line: 1116
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -25,6 +25,7 @@ Layout {
"/tmp", "/tmp",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),
@ -46,6 +47,7 @@ Layout {
"/", "/",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1441 assertion_line: 1516
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -39,6 +39,7 @@ Layout {
"/tmp/./foo/./bar", "/tmp/./foo/./bar",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1462 assertion_line: 1537
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -43,6 +43,7 @@ Layout {
"/tmp/./foo/./bar", "/tmp/./foo/./bar",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1481 assertion_line: 1556
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -39,6 +39,7 @@ Layout {
"/tmp/./foo/./bar", "/tmp/./foo/./bar",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1283 assertion_line: 1403
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -39,6 +39,7 @@ Layout {
"/tmp", "/tmp",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1309 assertion_line: 1435
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -39,6 +39,7 @@ Layout {
"/tmp", "/tmp",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1324 assertion_line: 1455
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -39,6 +39,7 @@ Layout {
"/tmp", "/tmp",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1295 assertion_line: 1416
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -39,6 +39,7 @@ Layout {
"/home/foo", "/home/foo",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1396 assertion_line: 1471
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -39,6 +39,7 @@ Layout {
"/tmp/./foo", "/tmp/./foo",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -23,6 +23,7 @@ Layout {
args: [], args: [],
cwd: None, cwd: None,
hold_on_close: false, hold_on_close: false,
hold_on_start: false,
}, },
), ),
), ),

View File

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

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 570 assertion_line: 583
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -41,6 +41,7 @@ Layout {
args: [], args: [],
cwd: None, cwd: None,
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1199 assertion_line: 1287
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -25,6 +25,7 @@ Layout {
"/tmp/bar", "/tmp/bar",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1144 assertion_line: 1233
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -25,6 +25,7 @@ Layout {
"/tmp/foo", "/tmp/foo",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1161 assertion_line: 1250
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -25,6 +25,7 @@ Layout {
"/tmp/bar", "/tmp/bar",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1180 assertion_line: 1268
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -25,6 +25,7 @@ Layout {
"/tmp/bar", "/tmp/bar",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1216 assertion_line: 1305
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -25,6 +25,7 @@ Layout {
"/tmp/bar", "/tmp/bar",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1269 assertion_line: 1357
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -25,6 +25,7 @@ Layout {
"/tmp/bar", "/tmp/bar",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1251 assertion_line: 1339
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -25,6 +25,7 @@ Layout {
"/tmp/foo", "/tmp/foo",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1411 assertion_line: 1486
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -39,6 +39,7 @@ Layout {
"/tmp", "/tmp",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/input/./unit/layout_test.rs source: zellij-utils/src/input/./unit/layout_test.rs
assertion_line: 1426 assertion_line: 1501
expression: "format!(\"{:#?}\", layout)" expression: "format!(\"{:#?}\", layout)"
--- ---
Layout { Layout {
@ -39,6 +39,7 @@ Layout {
"/tmp/./foo", "/tmp/./foo",
), ),
hold_on_close: true, hold_on_close: true,
hold_on_start: false,
}, },
), ),
), ),

View File

@ -54,6 +54,7 @@ impl<'a> KdlLayoutParser<'a> {
|| word == "tab" || word == "tab"
|| word == "args" || word == "args"
|| word == "close_on_exit" || word == "close_on_exit"
|| word == "start_suspended"
|| word == "borderless" || word == "borderless"
|| word == "focus" || word == "focus"
|| word == "name" || word == "name"
@ -72,6 +73,7 @@ impl<'a> KdlLayoutParser<'a> {
|| property_name == "cwd" || property_name == "cwd"
|| property_name == "args" || property_name == "args"
|| property_name == "close_on_exit" || property_name == "close_on_exit"
|| property_name == "start_suspended"
|| property_name == "split_direction" || property_name == "split_direction"
|| property_name == "pane" || property_name == "pane"
|| property_name == "children" || property_name == "children"
@ -241,15 +243,19 @@ impl<'a> KdlLayoutParser<'a> {
let args = self.parse_args(pane_node)?; let args = self.parse_args(pane_node)?;
let close_on_exit = let close_on_exit =
kdl_get_bool_property_or_child_value_with_error!(pane_node, "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 { if !is_template {
self.assert_no_bare_attributes_in_pane_node( self.assert_no_bare_attributes_in_pane_node(
&command, &command,
&args, &args,
&close_on_exit, &close_on_exit,
&start_suspended,
pane_node, pane_node,
)?; )?;
} }
let hold_on_close = close_on_exit.map(|c| !c).unwrap_or(true); 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) { match (command, edit, cwd) {
(None, None, Some(cwd)) => Ok(Some(Run::Cwd(cwd))), (None, None, Some(cwd)) => Ok(Some(Run::Cwd(cwd))),
(Some(command), None, cwd) => Ok(Some(Run::Command(RunCommand { (Some(command), None, cwd) => Ok(Some(Run::Command(RunCommand {
@ -257,6 +263,7 @@ impl<'a> KdlLayoutParser<'a> {
args: args.unwrap_or_else(|| vec![]), args: args.unwrap_or_else(|| vec![]),
cwd, cwd,
hold_on_close, hold_on_close,
hold_on_start,
}))), }))),
(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))),
@ -380,6 +387,8 @@ impl<'a> KdlLayoutParser<'a> {
let args = self.parse_args(kdl_node)?; let args = self.parse_args(kdl_node)?;
let close_on_exit = let close_on_exit =
kdl_get_bool_property_or_child_value_with_error!(kdl_node, "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 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_attributes_in_pane_node_with_template( self.assert_no_bare_attributes_in_pane_node_with_template(
@ -387,6 +396,7 @@ impl<'a> KdlLayoutParser<'a> {
&pane_template.run, &pane_template.run,
&args, &args,
&close_on_exit, &close_on_exit,
&start_suspended,
kdl_node, kdl_node,
)?; )?;
self.insert_children_to_pane_template( self.insert_children_to_pane_template(
@ -396,10 +406,11 @@ impl<'a> KdlLayoutParser<'a> {
)?; )?;
pane_template.run = Run::merge(&pane_template.run, &run); pane_template.run = Run::merge(&pane_template.run, &run);
if let Some(pane_template_run_command) = pane_template.run.as_mut() { 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 // can have bare args without a command
pane_template_run_command.add_args(args); pane_template_run_command.add_args(args);
pane_template_run_command.add_close_on_exit(close_on_exit); 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 { if let Some(borderless) = borderless {
pane_template.borderless = borderless; pane_template.borderless = borderless;
@ -600,6 +611,7 @@ impl<'a> KdlLayoutParser<'a> {
pane_template_run: &Option<Run>, pane_template_run: &Option<Run>,
args: &Option<Vec<String>>, args: &Option<Vec<String>>,
close_on_exit: &Option<bool>, close_on_exit: &Option<bool>,
start_suspended: &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()) {
@ -614,6 +626,12 @@ impl<'a> KdlLayoutParser<'a> {
pane_node 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(()) Ok(())
} }
fn assert_no_bare_attributes_in_pane_node( fn assert_no_bare_attributes_in_pane_node(
@ -621,6 +639,7 @@ impl<'a> KdlLayoutParser<'a> {
command: &Option<PathBuf>, command: &Option<PathBuf>,
args: &Option<Vec<String>>, args: &Option<Vec<String>>,
close_on_exit: &Option<bool>, close_on_exit: &Option<bool>,
start_suspended: &Option<bool>,
pane_node: &KdlNode, pane_node: &KdlNode,
) -> Result<(), ConfigError> { ) -> Result<(), ConfigError> {
if command.is_none() { if command.is_none() {
@ -631,6 +650,13 @@ impl<'a> KdlLayoutParser<'a> {
pane_node.span().len(), 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() { if args.is_some() {
return Err(ConfigError::new_layout_kdl_error( return Err(ConfigError::new_layout_kdl_error(
"args can only be set if a command was specified".into(), "args can only be set if a command was specified".into(),

View File

@ -1,7 +1,6 @@
mod kdl_layout_parser; mod kdl_layout_parser;
use crate::data::{InputMode, Key, Palette, PaletteColor}; use crate::data::{InputMode, Key, Palette, PaletteColor};
use crate::envs::EnvironmentVariables; use crate::envs::EnvironmentVariables;
use crate::input::command::RunCommand;
use crate::input::config::{Config, ConfigError, KdlError}; use crate::input::config::{Config, ConfigError, KdlError};
use crate::input::keybinds::Keybinds; use crate::input::keybinds::Keybinds;
use crate::input::layout::{Layout, RunPlugin, RunPluginLocation}; 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()) .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<bool> {
command_metadata
.get(entry_name)
.and_then(|cwd| cwd.entries().iter().next())
.and_then(|cwd_value| cwd_value.value().as_bool())
}
impl Action { impl Action {
pub fn new_from_bytes( pub fn new_from_bytes(
action_name: &str, action_name: &str,
@ -741,12 +750,20 @@ impl TryFrom<&KdlNode> for Action {
let direction = command_metadata let direction = command_metadata
.and_then(|c_m| kdl_child_string_value_for_entry(c_m, "direction")) .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "direction"))
.and_then(|direction_string| Direction::from_str(direction_string).ok()); .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 { let run_command_action = RunCommandAction {
command: PathBuf::from(command), command: PathBuf::from(command),
args, args,
cwd, cwd,
direction, direction,
hold_on_close: true, hold_on_close,
hold_on_start,
}; };
Ok(Action::Run(run_command_action)) Ok(Action::Run(run_command_action))
}, },
@ -1464,32 +1481,6 @@ impl Keybinds {
} }
} }
impl RunCommand {
pub fn from_kdl(kdl_node: &KdlNode) -> Result<Self, ConfigError> {
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 { impl Config {
pub fn from_kdl(kdl_config: &str, base_config: Option<Config>) -> Result<Config, ConfigError> { pub fn from_kdl(kdl_config: &str, base_config: Option<Config>) -> Result<Config, ConfigError> {
let mut config = base_config.unwrap_or_else(|| Config::default()); let mut config = base_config.unwrap_or_else(|| Config::default());