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,
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);

View File

@ -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("<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 {
name: "Wait for command to run",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {

View File

@ -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();

View File

@ -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 │
" ││█ │
$ ││ │
│ ││ │
│ ││ │
│ ││ │

View File

@ -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<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
default_editor: Option<PathBuf>,
) -> 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`.
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
@ -484,6 +489,35 @@ impl ServerOsApi for ServerOsInputOutput {
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> {
unistd::read(fd, buf)
}

View File

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

View File

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

View File

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

View File

@ -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<dyn ServerOsApi> 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<i32>, 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<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, 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 {
@ -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<i32>, run_command: RunCommand) {
self.is_held = Some((exit_status, run_command));
fn hold(&mut self, exit_status: Option<i32>, 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<Vec<TerminalCharacter>> {
@ -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)]

View File

@ -1010,11 +1010,12 @@ impl TiledPanes {
&mut self,
pane_id: PaneId,
exit_status: Option<i32>,
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)

View File

@ -108,25 +108,29 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> 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<Layout>) -> 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<Layout>) -> 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<Layout>) -> 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<TerminalAction>,
client_or_tab_index: ClientOrTabIndex,
) -> Result<u32, SpawnTerminalError> {
) -> 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<RunCommand>, Result<RawFd, SpawnTerminalError>)> =
vec![]; // (terminal_id,
// run_command
// file_descriptor)
let mut new_pane_pids: Vec<(
u32,
bool,
Option<RunCommand>,
Result<RawFd, SpawnTerminalError>,
)> = 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<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()
.map(|(terminal_id, _, _)| *terminal_id)
.collect::<Vec<u32>>();
.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

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`].
#[derive(Debug, Clone)]
pub enum ScreenInstruction {
PtyBytes(u32, VteBytes),
Render,
NewPane(PaneId, Option<String>, Option<bool>, ClientOrTabIndex), // String is initial title,
// bool (if Some) is
// should_float
NewPane(
PaneId,
Option<InitialTitle>,
Option<ShouldFloat>,
HoldForCommand,
ClientOrTabIndex,
),
OpenInPlaceEditor(PaneId, ClientId),
TogglePaneEmbedOrFloating(ClientId),
ToggleFloatingPanes(ClientId, Option<TerminalAction>),
HorizontalSplit(PaneId, Option<String>, ClientId), // String is initial title
VerticalSplit(PaneId, Option<String>, ClientId), // String is initial title
HorizontalSplit(PaneId, Option<InitialTitle>, HoldForCommand, ClientId),
VerticalSplit(PaneId, Option<InitialTitle>, HoldForCommand, ClientId),
WriteCharacter(Vec<u8>, ClientId),
ResizeLeft(ClientId),
ResizeRight(ClientId),
@ -167,7 +175,7 @@ pub enum ScreenInstruction {
HoldPane(PaneId, Option<i32>, RunCommand, Option<ClientId>), // Option<i32> is the exit status
UpdatePaneName(Vec<u8>, ClientId),
UndoRenamePane(ClientId),
NewTab(PaneLayout, Vec<u32>, 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<u32>,
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;
}
}

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
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<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
}
}
@ -470,7 +472,7 @@ impl Tab {
pub fn apply_layout(
&mut self,
layout: PaneLayout,
new_ids: Vec<u32>,
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<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) {
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<Box<dyn Pane>> {

View File

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

View File

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

View File

@ -89,6 +89,7 @@ pub struct PaneFrame {
pub other_cursors_exist_in_session: bool,
pub other_focused_clients: Vec<ClientId>,
exit_status: Option<ExitStatus>,
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<i32>) {
@ -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<TerminalCharacter> {
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<Vec<TerminalCharacter>> {
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<TerminalCharacter>, usize) {
fn first_exited_held_title_part_full(&self) -> (Vec<TerminalCharacter>, 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<TerminalCharacter>, 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 = ">";

View File

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

View File

@ -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]

View File

@ -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]

View File

@ -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 {

View File

@ -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(

View File

@ -19,6 +19,8 @@ pub struct RunCommand {
pub cwd: Option<PathBuf>,
#[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<Direction>,
#[serde(default)]
pub hold_on_close: bool,
#[serde(default)]
pub hold_on_start: bool,
}
impl From<RunCommandAction> for RunCommand {
@ -59,6 +63,7 @@ impl From<RunCommandAction> for RunCommand {
args: action.args,
cwd: action.cwd,
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>) {
// 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<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)]

View File

@ -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#"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@ Layout {
args: [],
cwd: None,
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
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,
},
),
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Run>,
args: &Option<Vec<String>>,
close_on_exit: &Option<bool>,
start_suspended: &Option<bool>,
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<PathBuf>,
args: &Option<Vec<String>>,
close_on_exit: &Option<bool>,
start_suspended: &Option<bool>,
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(),

View File

@ -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<bool> {
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<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 {
pub fn from_kdl(kdl_config: &str, base_config: Option<Config>) -> Result<Config, ConfigError> {
let mut config = base_config.unwrap_or_else(|| Config::default());