Skip to content

Commit

Permalink
feat(command-panes): allow to start suspended (#1887)
Browse files Browse the repository at this point in the history
* feat(command-panes): allow panes to start suspended

* style(fmt): remove unused code

* style(fmt): rustfmt
  • Loading branch information
imsnif authored Nov 1, 2022
1 parent 6d29c69 commit abc700f
Show file tree
Hide file tree
Showing 52 changed files with 668 additions and 172 deletions.
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fn main() {
floating,
name,
close_on_exit,
start_suspended,
})) = opts.command
{
let command_cli_action = CliAction::NewPane {
Expand All @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions src/tests/e2e/cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/tests/e2e/remote_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
" ││█
$ ││ │
│ ││ │
│ ││ │
│ ││ │
Expand Down
34 changes: 34 additions & 0 deletions zellij-server/src/os_input_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ fn spawn_terminal(
args,
cwd: None,
hold_on_close: false,
hold_on_start: false,
}
},
TerminalAction::RunCommand(command) => command,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion zellij-server/src/panes/floating_panes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions zellij-server/src/panes/grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
117 changes: 116 additions & 1 deletion zellij-server/src/panes/terminal_character.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
)
},
}
}
52 changes: 44 additions & 8 deletions zellij-server/src/panes/terminal_pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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>> {
Expand All @@ -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)]
Expand Down
Loading

0 comments on commit abc700f

Please sign in to comment.