summaryrefslogtreecommitdiffstats
path: root/zellij-server
diff options
context:
space:
mode:
Diffstat (limited to 'zellij-server')
-rw-r--r--zellij-server/src/os_input_output.rs34
-rw-r--r--zellij-server/src/panes/floating_panes/mod.rs3
-rw-r--r--zellij-server/src/panes/grid.rs1
-rw-r--r--zellij-server/src/panes/terminal_character.rs117
-rw-r--r--zellij-server/src/panes/terminal_pane.rs52
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs3
-rw-r--r--zellij-server/src/pty.rs179
-rw-r--r--zellij-server/src/screen.rs84
-rw-r--r--zellij-server/src/tab/mod.rs29
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs16
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs4
-rw-r--r--zellij-server/src/ui/pane_boundaries_frame.rs33
-rw-r--r--zellij-server/src/unit/screen_tests.rs9
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap4
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 = 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) => {