diff options
author | Aram Drevekenin <aram@poor.dev> | 2022-11-01 09:07:25 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-01 09:07:25 +0100 |
commit | abc700fc4d10d61c969ad94fa520d7d9336dcf14 (patch) | |
tree | a2e6318c4fe3951236cfa758befce8b95393a26e /zellij-server | |
parent | 6d29c6951e4768cc6f2f3c7b1bbb708d79d860c9 (diff) |
feat(command-panes): allow to start suspended (#1887)
* feat(command-panes): allow panes to start suspended
* style(fmt): remove unused code
* style(fmt): rustfmt
Diffstat (limited to 'zellij-server')
-rw-r--r-- | zellij-server/src/os_input_output.rs | 34 | ||||
-rw-r--r-- | zellij-server/src/panes/floating_panes/mod.rs | 3 | ||||
-rw-r--r-- | zellij-server/src/panes/grid.rs | 1 | ||||
-rw-r--r-- | zellij-server/src/panes/terminal_character.rs | 117 | ||||
-rw-r--r-- | zellij-server/src/panes/terminal_pane.rs | 52 | ||||
-rw-r--r-- | zellij-server/src/panes/tiled_panes/mod.rs | 3 | ||||
-rw-r--r-- | zellij-server/src/pty.rs | 179 | ||||
-rw-r--r-- | zellij-server/src/screen.rs | 84 | ||||
-rw-r--r-- | zellij-server/src/tab/mod.rs | 29 | ||||
-rw-r--r-- | zellij-server/src/tab/unit/tab_integration_tests.rs | 16 | ||||
-rw-r--r-- | zellij-server/src/tab/unit/tab_tests.rs | 4 | ||||
-rw-r--r-- | zellij-server/src/ui/pane_boundaries_frame.rs | 33 | ||||
-rw-r--r-- | zellij-server/src/unit/screen_tests.rs | 9 | ||||
-rw-r--r-- | zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap | 2 | ||||
-rw-r--r-- | zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap | 4 |
15 files changed, 455 insertions, 115 deletions
diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index 9a8ce0088..02f53dbe5 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -280,6 +280,7 @@ fn spawn_terminal( args, cwd: None, hold_on_close: false, + hold_on_start: false, } }, TerminalAction::RunCommand(command) => command, @@ -381,6 +382,10 @@ pub trait ServerOsApi: Send + Sync { quit_cb: Box<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) } diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index f686f40bb..d17be7557 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -156,11 +156,12 @@ impl FloatingPanes { &mut self, pane_id: PaneId, exit_status: Option<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) diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index 67a882fb4..d9d60fefe 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -1533,6 +1533,7 @@ impl Grid { self.sixel_scrolling = false; self.mouse_mode = MouseMode::NoEncoding; self.mouse_tracking = MouseTracking::Off; + self.cursor_is_hidden = false; if let Some(images_to_reap) = self.sixel_grid.clear() { self.sixel_grid.reap_images(images_to_reap); } diff --git a/zellij-server/src/panes/terminal_character.rs b/zellij-server/src/panes/terminal_character.rs index c6fa0adf1..68813e7a9 100644 --- a/zellij-server/src/panes/terminal_character.rs +++ b/zellij-server/src/panes/terminal_character.rs @@ -3,7 +3,12 @@ use std::fmt::{self, Debug, Display, Formatter}; use std::ops::{Index, IndexMut}; use unicode_width::UnicodeWidthChar; -use zellij_utils::{data::PaletteColor, vte::ParamsIter}; +use unicode_width::UnicodeWidthStr; +use zellij_utils::input::command::RunCommand; +use zellij_utils::{ + data::{PaletteColor, Style}, + vte::ParamsIter, +}; use crate::panes::alacritty_functions::parse_sgr_color; @@ -736,3 +741,113 @@ impl ::std::fmt::Debug for TerminalCharacter { write!(f, "{}", self.character) } } + +pub fn render_first_run_banner( + columns: usize, + rows: usize, + style: &Style, + run_command: Option<&RunCommand>, +) -> String { + let middle_row = rows / 2; + let middle_column = columns / 2; + match run_command { + Some(run_command) => { + let bold_text = RESET_STYLES.bold(Some(AnsiCode::On)); + let command_color_text = RESET_STYLES + .foreground(Some(AnsiCode::from(style.colors.green))) + .bold(Some(AnsiCode::On)); + let waiting_to_run_text = "Waiting to run: "; + let command_text = run_command.to_string(); + let waiting_to_run_text_width = waiting_to_run_text.width() + command_text.width(); + let column_start_postion = middle_column.saturating_sub(waiting_to_run_text_width / 2); + let waiting_to_run_line = format!( + "\u{1b}[{};{}H{}{}{}{}{}", + middle_row, + column_start_postion, + bold_text, + waiting_to_run_text, + command_color_text, + command_text, + RESET_STYLES + ); + + let controls_bare_text_first_part = "<"; + let enter_bare_text = "ENTER"; + let controls_bare_text_second_part = "> to run, <"; + let ctrl_c_bare_text = "Ctrl-c"; + let controls_bare_text_third_part = "> to exit"; + let controls_color = RESET_STYLES + .foreground(Some(AnsiCode::from(style.colors.orange))) + .bold(Some(AnsiCode::On)); + let controls_line_length = controls_bare_text_first_part.len() + + enter_bare_text.len() + + controls_bare_text_second_part.len() + + ctrl_c_bare_text.len() + + controls_bare_text_third_part.len(); + let controls_column_start_position = + middle_column.saturating_sub(controls_line_length / 2); + let controls_line = format!( + "\u{1b}[{};{}H{}<{}{}{}{}> to run, <{}{}{}{}> to exit", + middle_row + 2, + controls_column_start_position, + bold_text, + controls_color, + enter_bare_text, + RESET_STYLES, + bold_text, + controls_color, + ctrl_c_bare_text, + RESET_STYLES, + bold_text + ); + format!( + "\u{1b}[?25l{}{}{}{}", + RESET_STYLES, waiting_to_run_line, controls_line, RESET_STYLES + ) + }, + None => { + let bare_text = format!("Waiting to start..."); + let bare_text_width = bare_text.width(); + let column_start_postion = middle_column.saturating_sub(bare_text_width / 2); + let bold_text = RESET_STYLES.bold(Some(AnsiCode::On)); + let waiting_to_run_line = format!( + "\u{1b}[?25l\u{1b}[{};{}H{}{}{}", + middle_row, column_start_postion, bold_text, bare_text, RESET_STYLES + ); + + let controls_bare_text_first_part = "<"; + let enter_bare_text = "ENTER"; + let controls_bare_text_second_part = "> to run, <"; + let ctrl_c_bare_text = "Ctrl-c"; + let controls_bare_text_third_part = "> to exit"; + let controls_color = RESET_STYLES + .foreground(Some(AnsiCode::from(style.colors.orange))) + .bold(Some(AnsiCode::On)); + let controls_line_length = controls_bare_text_first_part.len() + + enter_bare_text.len() + + controls_bare_text_second_part.len() + + ctrl_c_bare_text.len() + + controls_bare_text_third_part.len(); + let controls_column_start_position = + middle_column.saturating_sub(controls_line_length / 2); + let controls_line = format!( + "\u{1b}[{};{}H{}<{}{}{}{}> to run, <{}{}{}{}> to exit", + middle_row + 2, + controls_column_start_position, + bold_text, + controls_color, + enter_bare_text, + RESET_STYLES, + bold_text, + controls_color, + ctrl_c_bare_text, + RESET_STYLES, + bold_text + ); + format!( + "\u{1b}[?25l{}{}{}{}", + RESET_STYLES, waiting_to_run_line, controls_line, RESET_STYLES + ) + }, + } +} diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 631df0c84..5726ef44b 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -2,7 +2,7 @@ use crate::output::{CharacterChunk, SixelImageChunk}; use crate::panes::sixel::SixelImageStore; use crate::panes::{ grid::Grid, - terminal_character::{TerminalCharacter, EMPTY_TERMINAL_CHARACTER}, + terminal_character::{render_first_run_banner, TerminalCharacter, EMPTY_TERMINAL_CHARACTER}, }; use crate::panes::{AnsiCode, LinkHandler}; use crate::pty::VteBytes; @@ -83,6 +83,8 @@ pub enum PaneId { Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct? } +type IsFirstRun = bool; + // FIXME: This should hold an os_api handle so that terminal panes can set their own size via FD in // their `reflow_lines()` method. Drop a Box<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)] diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index 59657c153..7e7b7a1f9 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -1010,11 +1010,12 @@ impl TiledPanes { &mut self, pane_id: PaneId, exit_status: Option<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) diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index bb5beb5dd..0ca0b5860 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -108,25 +108,29 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<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 |