feat(ux): rerun command pane (#1787)

* chore(config): default kdl keybindings config

* tests

* work

* refactor(config): move stuff around

* work

* tab merge layout

* work

* work

* layouts working

* work

* layout tests

* work

* work

* feat(parsing): kdl layouts without config

* refactor(kdl): move stuff around

* work

* tests(layout): add cases and fix bugs

* work

* fix(kdl): various bugs

* chore(layouts): move all layouts to kdl

* feat(kdl): shared keybidns

* fix(layout): do not count fixed panes toward percentile

* fix(keybinds): missing keybinds and actions

* fix(config): adjust default tips

* refactor(config): move stuff around

* fix(tests): make e2e tests pass

* fix(kdl): add verbose parsing errors

* fix(kdl): focused tab

* fix(layout): corret default_tab_template behavior

* style(code): fix compile warnings

* feat(cli): send actions through the cli

* fix(cli): exit only when action is done

* fix(cli): open embedded pane from floating pane

* fix(cli): send actions to other sessions

* feat(cli): command alias

* feat(converter): convert old config

* feat(converter): convert old layout and theme files

* feat(kdl): pretty errors

* feat(client): convert old YAML files on startup

* fix: various bugs and styling issues

* fix: e2e tests

* fix(screen): propagate errors after merge

* style(clippy): lower clippy level

* fix(tests): own session_name variable

* style(fmt): rustfmt

* fix(cli): various action fixes

* style(fmt): rustfmt

* fix(themes): loading of theme files

* style(fmt): rustfmt

* fix(tests): theme fixtures

* fix(layouts): better errors on unknown nodes

* fix(kdl): clarify valid node terminator error

* fix(e2e): adjust close tab test

* fix(e2e): adjust close tab test again

* style(code): cleanup some comments

* get command panes not to exit on command exit

* separate terminal pane_ids from raw_fds

* render frame according to exit status

* re-run command on enter and close pane on ctrl-c

* proper error when command is not found

* make ui nicer

* initial pane title for command panes

* fix pane override bug

* reap terminal_ids from os_input_output on pane close

* bool floating flag

* some ui tweaks

* fix tests

* make rustfmt happy

* e2e test for command pane

* fix various concurrency issues

* rename command to run in the cli

* rustfmt

* style(fmt): rustfmt

* fix(e2e): command => run

* fix(e2e): command => run in snapshot too!
This commit is contained in:
Aram Drevekenin 2022-10-11 16:45:46 +02:00 committed by GitHub
parent cb926119bc
commit c64bf5207a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1925 additions and 810 deletions

View file

@ -19,7 +19,7 @@ fn main() {
commands::send_action_to_session(cli_action, opts.session); commands::send_action_to_session(cli_action, opts.session);
std::process::exit(0); std::process::exit(0);
} }
if let Some(Command::Sessions(Sessions::Command { if let Some(Command::Sessions(Sessions::Run {
command, command,
direction, direction,
cwd, cwd,

View file

@ -13,6 +13,7 @@ use super::remote_runner::{RemoteRunner, RemoteTerminal, Step};
pub const QUIT: [u8; 1] = [17]; // ctrl-q pub const QUIT: [u8; 1] = [17]; // ctrl-q
pub const ESC: [u8; 1] = [27]; pub const ESC: [u8; 1] = [27];
pub const ENTER: [u8; 1] = [10]; // char '\n' pub const ENTER: [u8; 1] = [10]; // char '\n'
pub const SPACE: [u8; 1] = [32];
pub const LOCK_MODE: [u8; 1] = [7]; // ctrl-g pub const LOCK_MODE: [u8; 1] = [7]; // ctrl-g
pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = [27, 104]; // alt-h pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = [27, 104]; // alt-h
@ -1876,3 +1877,93 @@ pub fn undo_rename_pane() {
}; };
assert_snapshot!(last_snapshot); assert_snapshot!(last_snapshot);
} }
#[test]
#[ignore]
pub fn send_command_through_the_cli() {
// here we test the following flow:
// - send a command through the cli to run a bash script in a temporary folder
// - have it open a "command pane" that can be re-run with Enter
// - press Enter in the command pane to re-run the script
//
// the script appends the word "foo" to a temporary file and then `cat`s that file,
// so when we press "Enter", it will run again and we'll see two "foo"s one after the other,
// that's how we know the whole flow is working
let fake_win_size = Size {
cols: 120,
rows: 24,
};
let mut test_attempts = 10;
let last_snapshot = loop {
RemoteRunner::kill_running_sessions(fake_win_size);
let mut runner = RemoteRunner::new(fake_win_size)
.add_step(Step {
name: "Run command through the cli",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.status_bar_appears()
&& remote_terminal.cursor_position_is(3, 2)
{
let fixture_folder = remote_terminal.path_to_fixture_folder();
remote_terminal.send_command_through_the_cli(&format!(
"{}/append-echo-script.sh",
fixture_folder
));
step_is_complete = true;
}
step_is_complete
},
})
.add_step(Step {
name: "Wait for command to run",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("<Ctrl-c>")
&& remote_terminal.cursor_position_is(61, 3)
{
remote_terminal.send_key(&SPACE); // re-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 script to run again",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("<Ctrl-c>")
&& remote_terminal.cursor_position_is(61, 4)
{
step_is_complete = true
}
step_is_complete
},
});
runner.run_all_steps();
let last_snapshot = runner.take_snapshot_after(Step {
name: "Wait for script to run twice",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("foo")
&& remote_terminal.cursor_position_is(61, 4)
{
step_is_complete = true
}
step_is_complete
},
});
if runner.test_timed_out && test_attempts > 0 {
test_attempts -= 1;
continue;
} else {
break last_snapshot;
}
};
assert_snapshot!(last_snapshot);
}

View file

@ -64,6 +64,8 @@ fn stop_zellij(channel: &mut ssh2::Channel) {
.write_all(b"find /tmp | grep status-bar-tips | xargs rm\n") .write_all(b"find /tmp | grep status-bar-tips | xargs rm\n")
.unwrap(); .unwrap();
channel.write_all(b"killall -KILL zellij\n").unwrap(); channel.write_all(b"killall -KILL zellij\n").unwrap();
channel.write_all(b"rm -rf /tmp/*\n").unwrap(); // remove temporary artifacts from previous
// tests
} }
fn start_zellij(channel: &mut ssh2::Channel) { fn start_zellij(channel: &mut ssh2::Channel) {
@ -198,6 +200,7 @@ fn read_from_channel(
sixel_image_store, sixel_image_store,
Rc::new(RefCell::new(Palette::default())), Rc::new(RefCell::new(Palette::default())),
Rc::new(RefCell::new(HashMap::new())), Rc::new(RefCell::new(HashMap::new())),
None,
); // 0 is the pane index ); // 0 is the pane index
loop { loop {
if !should_keep_running.load(Ordering::SeqCst) { if !should_keep_running.load(Ordering::SeqCst) {
@ -339,6 +342,16 @@ impl RemoteTerminal {
channel.flush().unwrap(); channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
} }
pub fn send_command_through_the_cli(&mut self, command: &str) {
let mut channel = self.channel.lock().unwrap();
channel
.write_all(format!("{} run \"{}\"\n", ZELLIJ_EXECUTABLE_LOCATION, command).as_bytes())
.unwrap();
channel.flush().unwrap();
}
pub fn path_to_fixture_folder(&self) -> String {
ZELLIJ_FIXTURE_PATH.to_string()
}
pub fn load_fixture(&mut self, name: &str) { pub fn load_fixture(&mut self, name: &str) {
let mut channel = self.channel.lock().unwrap(); let mut channel = self.channel.lock().unwrap();
channel channel

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
assertion_line: 1968
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 │
│$ ││█ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
└──────────────────────────────────────────────────────────┘└ [ EXIT CODE: 0 ] <ENTER> to re-run, <Ctrl-c> to exit ────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.

2
src/tests/fixtures/append-echo-script.sh vendored Executable file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env bash
echo foo >> /tmp/foo && cat /tmp/foo

View file

@ -1,4 +1,4 @@
use std::collections::HashMap; use std::collections::{BTreeMap, HashMap, HashSet};
use std::{fs::File, io::Write}; use std::{fs::File, io::Write};
use crate::panes::PaneId; use crate::panes::PaneId;
@ -39,7 +39,7 @@ pub use nix::unistd::Pid;
use crate::ClientId; use crate::ClientId;
pub(crate) fn set_terminal_size_using_fd(fd: RawFd, columns: u16, rows: u16) { fn set_terminal_size_using_fd(fd: RawFd, columns: u16, rows: u16) {
// TODO: do this with the nix ioctl // TODO: do this with the nix ioctl
use libc::ioctl; use libc::ioctl;
use libc::TIOCSWINSZ; use libc::TIOCSWINSZ;
@ -61,18 +61,19 @@ pub(crate) fn set_terminal_size_using_fd(fd: RawFd, columns: u16, rows: u16) {
/// Handle some signals for the child process. This will loop until the child /// Handle some signals for the child process. This will loop until the child
/// process exits. /// process exits.
fn handle_command_exit(mut child: Child) { fn handle_command_exit(mut child: Child) -> Option<i32> {
// returns the exit status, if any
let mut should_exit = false; let mut should_exit = false;
let mut attempts = 3; let mut attempts = 3;
let mut signals = signal_hook::iterator::Signals::new(&[SIGINT, SIGTERM]).unwrap(); let mut signals = signal_hook::iterator::Signals::new(&[SIGINT, SIGTERM]).unwrap();
'handle_exit: loop { 'handle_exit: loop {
// test whether the child process has exited // test whether the child process has exited
match child.try_wait() { match child.try_wait() {
Ok(Some(_status)) => { Ok(Some(status)) => {
// if the child process has exited, break outside of the loop // if the child process has exited, break outside of the loop
// and exit this function // and exit this function
// TODO: handle errors? // TODO: handle errors?
break 'handle_exit; break 'handle_exit status.code();
}, },
Ok(None) => { Ok(None) => {
::std::thread::sleep(::std::time::Duration::from_millis(10)); ::std::thread::sleep(::std::time::Duration::from_millis(10));
@ -94,21 +95,49 @@ fn handle_command_exit(mut child: Child) {
} else { } else {
// when I say whoa, I mean WHOA! // when I say whoa, I mean WHOA!
let _ = child.kill(); let _ = child.kill();
break 'handle_exit; break 'handle_exit None;
} }
} }
} }
fn command_exists(cmd: &RunCommand) -> bool {
let command = &cmd.command;
match cmd.cwd.as_ref() {
Some(cwd) => {
if cwd.join(&command).exists() {
return true;
}
},
None => {
if command.exists() {
return true;
}
},
}
if let Some(paths) = env::var_os("PATH") {
for path in env::split_paths(&paths) {
if path.join(command).exists() {
return true;
}
}
}
false
}
fn handle_openpty( fn handle_openpty(
open_pty_res: OpenptyResult, open_pty_res: OpenptyResult,
cmd: RunCommand, cmd: RunCommand,
quit_cb: Box<dyn Fn(PaneId) + Send>, quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
) -> (RawFd, RawFd) { terminal_id: u32,
) -> Result<(RawFd, RawFd), SpawnTerminalError> {
// primary side of pty and child fd // primary side of pty and child fd
let pid_primary = open_pty_res.master; let pid_primary = open_pty_res.master;
let pid_secondary = open_pty_res.slave; let pid_secondary = open_pty_res.slave;
if command_exists(&cmd) {
let mut child = unsafe { let mut child = unsafe {
let cmd = cmd.clone();
let command = &mut Command::new(cmd.command); let command = &mut Command::new(cmd.command);
if let Some(current_dir) = cmd.cwd { if let Some(current_dir) = cmd.cwd {
if current_dir.exists() { if current_dir.exists() {
@ -131,13 +160,15 @@ fn handle_openpty(
let child_id = child.id(); let child_id = child.id();
std::thread::spawn(move || { std::thread::spawn(move || {
child.wait().unwrap(); child.wait().unwrap();
handle_command_exit(child); let exit_status = handle_command_exit(child);
let _ = nix::unistd::close(pid_primary);
let _ = nix::unistd::close(pid_secondary); let _ = nix::unistd::close(pid_secondary);
quit_cb(PaneId::Terminal(pid_primary)); quit_cb(PaneId::Terminal(terminal_id), exit_status, cmd);
}); });
(pid_primary, child_id as RawFd) Ok((pid_primary, child_id as RawFd))
} else {
Err(SpawnTerminalError::CommandNotFound(terminal_id))
}
} }
/// Spawns a new terminal from the parent terminal with [`termios`](termios::Termios) /// Spawns a new terminal from the parent terminal with [`termios`](termios::Termios)
@ -147,16 +178,20 @@ fn handle_terminal(
cmd: RunCommand, cmd: RunCommand,
failover_cmd: Option<RunCommand>, failover_cmd: Option<RunCommand>,
orig_termios: termios::Termios, orig_termios: termios::Termios,
quit_cb: Box<dyn Fn(PaneId) + Send>, quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>,
) -> (RawFd, RawFd) { terminal_id: u32,
// Create a pipe to allow the child the communicate the shell's pid to it's ) -> Result<(RawFd, RawFd), SpawnTerminalError> {
// Create a pipe to allow the child the communicate the shell's pid to its
// parent. // parent.
match openpty(None, Some(&orig_termios)) { match openpty(None, Some(&orig_termios)) {
Ok(open_pty_res) => handle_openpty(open_pty_res, cmd, quit_cb), Ok(open_pty_res) => handle_openpty(open_pty_res, cmd, quit_cb, terminal_id),
Err(e) => match failover_cmd { Err(e) => match failover_cmd {
Some(failover_cmd) => handle_terminal(failover_cmd, None, orig_termios, quit_cb), Some(failover_cmd) => {
handle_terminal(failover_cmd, None, orig_termios, quit_cb, terminal_id)
},
None => { None => {
panic!("failed to start pty{:?}", e); log::error!("Failed to start pty: {:?}", e);
Err(SpawnTerminalError::FailedToStartPty)
}, },
}, },
} }
@ -192,12 +227,15 @@ fn separate_command_arguments(command: &mut PathBuf, args: &mut Vec<String>) {
/// ///
/// This function will panic if both the `EDITOR` and `VISUAL` environment variables are not /// This function will panic if both the `EDITOR` and `VISUAL` environment variables are not
/// set. /// set.
pub fn spawn_terminal( fn spawn_terminal(
terminal_action: TerminalAction, terminal_action: TerminalAction,
orig_termios: termios::Termios, orig_termios: termios::Termios,
quit_cb: Box<dyn Fn(PaneId) + Send>, quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit_status
default_editor: Option<PathBuf>, default_editor: Option<PathBuf>,
) -> Result<(RawFd, RawFd), &'static str> { terminal_id: u32,
) -> Result<(RawFd, RawFd), SpawnTerminalError> {
// returns the terminal_id, the primary fd and the
// secondary fd
let mut failover_cmd_args = None; let mut failover_cmd_args = None;
let cmd = match terminal_action { let cmd = match terminal_action {
TerminalAction::OpenFile(file_to_open, line_number) => { TerminalAction::OpenFile(file_to_open, line_number) => {
@ -205,9 +243,7 @@ pub fn spawn_terminal(
&& env::var("EDITOR").is_err() && env::var("EDITOR").is_err()
&& env::var("VISUAL").is_err() && env::var("VISUAL").is_err()
{ {
return Err( return Err(SpawnTerminalError::NoEditorFound);
"No Editor found, consider setting a path to one in $EDITOR or $VISUAL",
);
} }
let mut command = default_editor.unwrap_or_else(|| { let mut command = default_editor.unwrap_or_else(|| {
@ -239,6 +275,7 @@ pub fn spawn_terminal(
command, command,
args, args,
cwd: None, cwd: None,
hold_on_close: false,
} }
}, },
TerminalAction::RunCommand(command) => command, TerminalAction::RunCommand(command) => command,
@ -251,13 +288,52 @@ pub fn spawn_terminal(
None None
}; };
Ok(handle_terminal(cmd, failover_cmd, orig_termios, quit_cb)) handle_terminal(cmd, failover_cmd, orig_termios, quit_cb, terminal_id)
}
#[derive(Debug, Clone, Copy)]
pub enum SpawnTerminalError {
CommandNotFound(u32), // u32 is the terminal id
NoEditorFound,
NoMoreTerminalIds,
FailedToStartPty,
GenericSpawnError(&'static str),
}
impl std::fmt::Display for SpawnTerminalError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
match self {
SpawnTerminalError::CommandNotFound(terminal_id) => {
write!(f, "Command not found for terminal_id: {}", terminal_id)
},
SpawnTerminalError::NoEditorFound => {
write!(
f,
"No Editor found, consider setting a path to one in $EDITOR or $VISUAL"
)
},
SpawnTerminalError::NoMoreTerminalIds => {
write!(f, "No more terminal ids left to allocate.")
},
SpawnTerminalError::FailedToStartPty => {
write!(f, "Failed to start pty")
},
SpawnTerminalError::GenericSpawnError(msg) => {
write!(f, "{}", msg)
},
}
}
} }
#[derive(Clone)] #[derive(Clone)]
pub struct ServerOsInputOutput { pub struct ServerOsInputOutput {
orig_termios: Arc<Mutex<termios::Termios>>, orig_termios: Arc<Mutex<termios::Termios>>,
client_senders: Arc<Mutex<HashMap<ClientId, IpcSenderWithContext<ServerToClientMsg>>>>, client_senders: Arc<Mutex<HashMap<ClientId, IpcSenderWithContext<ServerToClientMsg>>>>,
terminal_id_to_raw_fd: Arc<Mutex<BTreeMap<u32, Option<RawFd>>>>, // A value of None means the
// terminal_id exists but is
// not connected to an fd (eg.
// a command pane with a
// non-existing command)
} }
// async fn in traits is not supported by rust, so dtolnay's excellent async_trait macro is being // async fn in traits is not supported by rust, so dtolnay's excellent async_trait macro is being
@ -291,25 +367,24 @@ impl AsyncReader for RawFdAsyncReader {
/// The `ServerOsApi` trait represents an abstract interface to the features of an operating system that /// The `ServerOsApi` trait represents an abstract interface to the features of an operating system that
/// Zellij server requires. /// Zellij server requires.
pub trait ServerOsApi: Send + Sync { pub trait ServerOsApi: Send + Sync {
/// Sets the size of the terminal associated to file descriptor `fd`. fn set_terminal_size_using_terminal_id(&self, id: u32, cols: u16, rows: u16);
fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16);
/// Spawn a new terminal, with a terminal action. The returned tuple contains the master file /// Spawn a new terminal, with a terminal action. The returned tuple contains the master file
/// descriptor of the forked pseudo terminal and a [ChildId] struct containing process id's for /// descriptor of the forked pseudo terminal and a [ChildId] struct containing process id's for
/// the forked child process. /// the forked child process.
fn spawn_terminal( fn spawn_terminal(
&self, &self,
terminal_action: TerminalAction, terminal_action: TerminalAction,
quit_cb: Box<dyn Fn(PaneId) + Send>, quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
default_editor: Option<PathBuf>, default_editor: Option<PathBuf>,
) -> Result<(RawFd, RawFd), &'static str>; ) -> Result<(u32, RawFd, RawFd), SpawnTerminalError>;
/// Read bytes from the standard output of the virtual terminal referred to by `fd`. /// Read bytes from the standard output of the virtual terminal referred to by `fd`.
fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>; fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
/// Creates an `AsyncReader` that can be used to read from `fd` in an async context /// Creates an `AsyncReader` that can be used to read from `fd` in an async context
fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader>; fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader>;
/// Write bytes to the standard input of the virtual terminal referred to by `fd`. /// Write bytes to the standard input of the virtual terminal referred to by `fd`.
fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result<usize, nix::Error>; fn write_to_tty_stdin(&self, terminal_id: u32, buf: &[u8]) -> Result<usize, nix::Error>;
/// Wait until all output written to the object referred to by `fd` has been transmitted. /// Wait until all output written to the object referred to by `fd` has been transmitted.
fn tcdrain(&self, fd: RawFd) -> Result<(), nix::Error>; fn tcdrain(&self, terminal_id: u32) -> Result<(), nix::Error>;
/// Terminate the process with process ID `pid`. (SIGTERM) /// Terminate the process with process ID `pid`. (SIGTERM)
fn kill(&self, pid: Pid) -> Result<(), nix::Error>; fn kill(&self, pid: Pid) -> Result<(), nix::Error>;
/// Terminate the process with process ID `pid`. (SIGKILL) /// Terminate the process with process ID `pid`. (SIGKILL)
@ -332,27 +407,78 @@ pub trait ServerOsApi: Send + Sync {
fn get_cwd(&self, pid: Pid) -> Option<PathBuf>; fn get_cwd(&self, pid: Pid) -> Option<PathBuf>;
/// Writes the given buffer to a string /// Writes the given buffer to a string
fn write_to_file(&mut self, buf: String, file: Option<String>); fn write_to_file(&mut self, buf: String, file: Option<String>);
fn re_run_command_in_terminal(
&self,
terminal_id: u32,
run_command: RunCommand,
quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
) -> Result<(RawFd, RawFd), SpawnTerminalError>;
fn clear_terminal_id(&self, terminal_id: u32);
} }
impl ServerOsApi for ServerOsInputOutput { impl ServerOsApi for ServerOsInputOutput {
fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16) { fn set_terminal_size_using_terminal_id(&self, id: u32, cols: u16, rows: u16) {
match self.terminal_id_to_raw_fd.lock().unwrap().get(&id) {
Some(Some(fd)) => {
if cols > 0 && rows > 0 { if cols > 0 && rows > 0 {
set_terminal_size_using_fd(fd, cols, rows); set_terminal_size_using_fd(*fd, cols, rows);
}
},
_ => {
log::error!("Failed to find terminal fd for id: {id}, so cannot resize terminal");
},
} }
} }
fn spawn_terminal( fn spawn_terminal(
&self, &self,
terminal_action: TerminalAction, terminal_action: TerminalAction,
quit_cb: Box<dyn Fn(PaneId) + Send>, quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
default_editor: Option<PathBuf>, default_editor: Option<PathBuf>,
) -> Result<(RawFd, RawFd), &'static str> { ) -> Result<(u32, RawFd, RawFd), SpawnTerminalError> {
let orig_termios = self.orig_termios.lock().unwrap(); let orig_termios = self.orig_termios.lock().unwrap();
spawn_terminal( 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);
match spawn_terminal(
terminal_action, terminal_action,
orig_termios.clone(), orig_termios.clone(),
quit_cb, quit_cb,
default_editor, default_editor,
) terminal_id,
) {
Ok((pid_primary, pid_secondary)) => {
self.terminal_id_to_raw_fd
.lock()
.unwrap()
.insert(terminal_id, Some(pid_primary));
Ok((terminal_id, pid_primary, pid_secondary))
},
Err(e) => Err(e),
}
},
None => Err(SpawnTerminalError::NoMoreTerminalIds),
}
} }
fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> { fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
unistd::read(fd, buf) unistd::read(fd, buf)
@ -360,11 +486,25 @@ impl ServerOsApi for ServerOsInputOutput {
fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader> { fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader> {
Box::new(RawFdAsyncReader::new(fd)) Box::new(RawFdAsyncReader::new(fd))
} }
fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result<usize, nix::Error> { fn write_to_tty_stdin(&self, terminal_id: u32, buf: &[u8]) -> Result<usize, nix::Error> {
unistd::write(fd, buf) match self.terminal_id_to_raw_fd.lock().unwrap().get(&terminal_id) {
Some(Some(fd)) => unistd::write(*fd, buf),
_ => {
// TODO: propagate this error
log::error!("Failed to write to terminal with {terminal_id} - could not find its file descriptor");
Ok(0)
},
}
}
fn tcdrain(&self, terminal_id: u32) -> Result<(), nix::Error> {
match self.terminal_id_to_raw_fd.lock().unwrap().get(&terminal_id) {
Some(Some(fd)) => termios::tcdrain(*fd),
_ => {
// TODO: propagate this error
log::error!("Failed to tcdrain to terminal with {terminal_id} - could not find its file descriptor");
Ok(())
},
} }
fn tcdrain(&self, fd: RawFd) -> Result<(), nix::Error> {
termios::tcdrain(fd)
} }
fn box_clone(&self) -> Box<dyn ServerOsApi> { fn box_clone(&self) -> Box<dyn ServerOsApi> {
Box::new((*self).clone()) Box::new((*self).clone())
@ -430,6 +570,37 @@ impl ServerOsApi for ServerOsInputOutput {
log::error!("could not write to file: {}", e); log::error!("could not write to file: {}", e);
} }
} }
fn re_run_command_in_terminal(
&self,
terminal_id: u32,
run_command: RunCommand,
quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
) -> Result<(RawFd, RawFd), SpawnTerminalError> {
let orig_termios = self.orig_termios.lock().unwrap();
let default_editor = None; // no need for a default editor when running an explicit command
match spawn_terminal(
TerminalAction::RunCommand(run_command),
orig_termios.clone(),
quit_cb,
default_editor,
terminal_id,
) {
Ok((pid_primary, pid_secondary)) => {
self.terminal_id_to_raw_fd
.lock()
.unwrap()
.insert(terminal_id, Some(pid_primary));
Ok((pid_primary, pid_secondary))
},
Err(e) => Err(e),
}
}
fn clear_terminal_id(&self, terminal_id: u32) {
self.terminal_id_to_raw_fd
.lock()
.unwrap()
.remove(&terminal_id);
}
} }
impl Clone for Box<dyn ServerOsApi> { impl Clone for Box<dyn ServerOsApi> {
@ -444,6 +615,7 @@ pub fn get_server_os_input() -> Result<ServerOsInputOutput, nix::Error> {
Ok(ServerOsInputOutput { Ok(ServerOsInputOutput {
orig_termios, orig_termios,
client_senders: Arc::new(Mutex::new(HashMap::new())), client_senders: Arc::new(Mutex::new(HashMap::new())),
terminal_id_to_raw_fd: Arc::new(Mutex::new(BTreeMap::new())),
}) })
} }

View file

@ -17,15 +17,16 @@ use std::rc::Rc;
use std::time::Instant; use std::time::Instant;
use zellij_utils::{ use zellij_utils::{
data::{ModeInfo, Style}, data::{ModeInfo, Style},
input::command::RunCommand,
pane_size::{Offset, PaneGeom, Size, Viewport}, pane_size::{Offset, PaneGeom, Size, Viewport},
}; };
macro_rules! resize_pty { macro_rules! resize_pty {
($pane:expr, $os_input:expr) => { ($pane:expr, $os_input:expr) => {
if let PaneId::Terminal(ref pid) = $pane.pid() { if let PaneId::Terminal(ref pid) = $pane.pid() {
// FIXME: This `set_terminal_size_using_fd` call would be best in // FIXME: This `set_terminal_size_using_terminal_id` call would be best in
// `TerminalPane::reflow_lines` // `TerminalPane::reflow_lines`
$os_input.set_terminal_size_using_fd( $os_input.set_terminal_size_using_terminal_id(
*pid, *pid,
$pane.get_content_columns() as u16, $pane.get_content_columns() as u16,
$pane.get_content_rows() as u16, $pane.get_content_rows() as u16,
@ -150,6 +151,16 @@ impl FloatingPanes {
self.desired_pane_positions.remove(&pane_id); self.desired_pane_positions.remove(&pane_id);
self.panes.remove(&pane_id) self.panes.remove(&pane_id)
} }
pub fn hold_pane(
&mut self,
pane_id: PaneId,
exit_status: Option<i32>,
run_command: RunCommand,
) {
self.panes
.get_mut(&pane_id)
.map(|p| p.hold(exit_status, run_command));
}
pub fn get(&self, pane_id: &PaneId) -> Option<&Box<dyn Pane>> { pub fn get(&self, pane_id: &PaneId) -> Option<&Box<dyn Pane>> {
self.panes.get(pane_id) self.panes.get(pane_id)
} }

View file

@ -1504,7 +1504,7 @@ impl Grid {
pub fn mark_for_rerender(&mut self) { pub fn mark_for_rerender(&mut self) {
self.should_render = true; self.should_render = true;
} }
fn reset_terminal_state(&mut self) { pub fn reset_terminal_state(&mut self) {
self.lines_above = VecDeque::with_capacity(*SCROLL_BUFFER_SIZE.get().unwrap()); self.lines_above = VecDeque::with_capacity(*SCROLL_BUFFER_SIZE.get().unwrap());
self.lines_below = vec![]; self.lines_below = vec![];
self.viewport = vec![Row::new(self.width).canonical()]; self.viewport = vec![Row::new(self.width).canonical()];

View file

@ -113,10 +113,6 @@ impl Pane for PluginPane {
fn cursor_coordinates(&self) -> Option<(usize, usize)> { fn cursor_coordinates(&self) -> Option<(usize, usize)> {
None None
} }
fn adjust_input_to_terminal(&self, _input_bytes: Vec<u8>) -> Vec<u8> {
// noop
vec![]
}
fn position_and_size(&self) -> PaneGeom { fn position_and_size(&self) -> PaneGeom {
self.geom self.geom
} }

View file

@ -6,14 +6,14 @@ use crate::panes::{
}; };
use crate::panes::{AnsiCode, LinkHandler}; use crate::panes::{AnsiCode, LinkHandler};
use crate::pty::VteBytes; use crate::pty::VteBytes;
use crate::tab::Pane; use crate::tab::{AdjustedInput, Pane};
use crate::ClientId; use crate::ClientId;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt::Debug; use std::fmt::Debug;
use std::os::unix::io::RawFd;
use std::rc::Rc; use std::rc::Rc;
use std::time::{self, Instant}; use std::time::{self, Instant};
use zellij_utils::input::command::RunCommand;
use zellij_utils::pane_size::Offset; use zellij_utils::pane_size::Offset;
use zellij_utils::{ use zellij_utils::{
data::{InputMode, Palette, PaletteColor, Style}, data::{InputMode, Palette, PaletteColor, Style},
@ -37,6 +37,10 @@ const HOME_KEY: &[u8] = &[27, 91, 72];
const END_KEY: &[u8] = &[27, 91, 70]; const END_KEY: &[u8] = &[27, 91, 70];
const BRACKETED_PASTE_BEGIN: &[u8] = &[27, 91, 50, 48, 48, 126]; const BRACKETED_PASTE_BEGIN: &[u8] = &[27, 91, 50, 48, 48, 126];
const BRACKETED_PASTE_END: &[u8] = &[27, 91, 50, 48, 49, 126]; const BRACKETED_PASTE_END: &[u8] = &[27, 91, 50, 48, 49, 126];
const ENTER_NEWLINE: &[u8] = &[10];
const ENTER_CARRIAGE_RETURN: &[u8] = &[13];
const SPACE: &[u8] = &[32];
const CTRL_C: &[u8] = &[3]; // TODO: check this to be sure it fits all types of CTRL_C (with mac, etc)
const TERMINATING_STRING: &str = "\0"; const TERMINATING_STRING: &str = "\0";
const DELETE_KEY: &str = "\u{007F}"; const DELETE_KEY: &str = "\u{007F}";
const BACKSPACE_KEY: &str = "\u{0008}"; const BACKSPACE_KEY: &str = "\u{0008}";
@ -74,7 +78,7 @@ impl AnsiEncoding {
#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug)] #[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug)]
pub enum PaneId { pub enum PaneId {
Terminal(RawFd), Terminal(u32),
Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct? Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct?
} }
@ -83,7 +87,7 @@ pub enum PaneId {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub struct TerminalPane { pub struct TerminalPane {
pub grid: Grid, pub grid: Grid,
pub pid: RawFd, pub pid: u32,
pub selectable: bool, pub selectable: bool,
pub geom: PaneGeom, pub geom: PaneGeom,
pub geom_override: Option<PaneGeom>, pub geom_override: Option<PaneGeom>,
@ -99,6 +103,8 @@ pub struct TerminalPane {
borderless: bool, borderless: bool,
fake_cursor_locations: HashSet<(usize, usize)>, // (x, y) - these hold a record of previous fake cursors which we need to clear on render fake_cursor_locations: HashSet<(usize, usize)>, // (x, y) - these hold a record of previous fake cursors which we need to clear on render
search_term: String, search_term: String,
is_held: Option<(Option<i32>, RunCommand)>, // a "held" pane means that its command has exited and its waiting for a
// possible user instruction to be re-run
} }
impl Pane for TerminalPane { impl Pane for TerminalPane {
@ -157,37 +163,64 @@ impl Pane for TerminalPane {
.cursor_coordinates() .cursor_coordinates()
.map(|(x, y)| (x + left, y + top)) .map(|(x, y)| (x + left, y + top))
} }
fn adjust_input_to_terminal(&self, input_bytes: Vec<u8>) -> Vec<u8> { fn adjust_input_to_terminal(&mut self, input_bytes: Vec<u8>) -> Option<AdjustedInput> {
// there are some cases in which the terminal state means that input sent to it // there are some cases in which the terminal state means that input sent to it
// needs to be adjusted. // needs to be adjusted.
// here we match against those cases - if need be, we adjust the input and if not // here we match against those cases - if need be, we adjust the input and if not
// we send back the original input // we send back the original input
if let Some((_exit_status, run_command)) = &self.is_held {
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);
Some(AdjustedInput::ReRunCommandInThisPane(run_command))
},
CTRL_C => Some(AdjustedInput::CloseThisPane),
_ => None,
}
} else {
if self.grid.new_line_mode { if self.grid.new_line_mode {
if let &[13] = input_bytes.as_slice() { if let &[13] = input_bytes.as_slice() {
// LNM - carriage return is followed by linefeed // LNM - carriage return is followed by linefeed
return "\u{0d}\u{0a}".as_bytes().to_vec(); return Some(AdjustedInput::WriteBytesToTerminal(
"\u{0d}\u{0a}".as_bytes().to_vec(),
));
}; };
} }
if self.grid.cursor_key_mode { if self.grid.cursor_key_mode {
match input_bytes.as_slice() { match input_bytes.as_slice() {
LEFT_ARROW => { LEFT_ARROW => {
return AnsiEncoding::Left.as_vec_bytes(); return Some(AdjustedInput::WriteBytesToTerminal(
AnsiEncoding::Left.as_vec_bytes(),
));
}, },
RIGHT_ARROW => { RIGHT_ARROW => {
return AnsiEncoding::Right.as_vec_bytes(); return Some(AdjustedInput::WriteBytesToTerminal(
AnsiEncoding::Right.as_vec_bytes(),
));
}, },
UP_ARROW => { UP_ARROW => {
return AnsiEncoding::Up.as_vec_bytes(); return Some(AdjustedInput::WriteBytesToTerminal(
AnsiEncoding::Up.as_vec_bytes(),
));
}, },
DOWN_ARROW => { DOWN_ARROW => {
return AnsiEncoding::Down.as_vec_bytes(); return Some(AdjustedInput::WriteBytesToTerminal(
AnsiEncoding::Down.as_vec_bytes(),
));
}, },
HOME_KEY => { HOME_KEY => {
return AnsiEncoding::Home.as_vec_bytes(); return Some(AdjustedInput::WriteBytesToTerminal(
AnsiEncoding::Home.as_vec_bytes(),
));
}, },
END_KEY => { END_KEY => {
return AnsiEncoding::End.as_vec_bytes(); return Some(AdjustedInput::WriteBytesToTerminal(
AnsiEncoding::End.as_vec_bytes(),
));
}, },
_ => {}, _ => {},
}; };
@ -199,11 +232,14 @@ impl Pane for TerminalPane {
// when pasting input. We only need to make sure not to send them to terminal // when pasting input. We only need to make sure not to send them to terminal
// panes who do not work in this mode // panes who do not work in this mode
match input_bytes.as_slice() { match input_bytes.as_slice() {
BRACKETED_PASTE_BEGIN | BRACKETED_PASTE_END => return vec![], BRACKETED_PASTE_BEGIN | BRACKETED_PASTE_END => {
return Some(AdjustedInput::WriteBytesToTerminal(vec![]))
},
_ => {}, _ => {},
} }
} }
input_bytes Some(AdjustedInput::WriteBytesToTerminal(input_bytes))
}
} }
fn position_and_size(&self) -> PaneGeom { fn position_and_size(&self) -> PaneGeom {
self.geom self.geom
@ -307,7 +343,9 @@ impl Pane for TerminalPane {
input_mode: InputMode, input_mode: InputMode,
) -> Option<(Vec<CharacterChunk>, Option<String>)> { ) -> Option<(Vec<CharacterChunk>, Option<String>)> {
// TODO: remove the cursor stuff from here // TODO: remove the cursor stuff from here
let pane_title = if self.pane_name.is_empty() let pane_title = if let Some((_exit_status, run_command)) = &self.is_held {
format!("{}", run_command)
} else if self.pane_name.is_empty()
&& input_mode == InputMode::RenamePane && input_mode == InputMode::RenamePane
&& frame_params.is_main_client && frame_params.is_main_client
{ {
@ -346,12 +384,15 @@ impl Pane for TerminalPane {
self.pane_name.clone() self.pane_name.clone()
}; };
let frame = PaneFrame::new( let mut frame = PaneFrame::new(
self.current_geom().into(), self.current_geom().into(),
self.grid.scrollback_position_and_length(), self.grid.scrollback_position_and_length(),
pane_title, pane_title,
frame_params, frame_params,
); );
if let Some((exit_status, _run_command)) = &self.is_held {
frame.add_exit_status(exit_status.as_ref().copied());
}
match self.frame.get(&client_id) { match self.frame.get(&client_id) {
// TODO: use and_then or something? // TODO: use and_then or something?
@ -654,12 +695,16 @@ impl Pane for TerminalPane {
fn is_alternate_mode_active(&self) -> bool { fn is_alternate_mode_active(&self) -> bool {
self.grid.is_alternate_mode_active() self.grid.is_alternate_mode_active()
} }
fn hold(&mut self, exit_status: Option<i32>, run_command: RunCommand) {
self.is_held = Some((exit_status, run_command));
self.set_should_render(true);
}
} }
impl TerminalPane { impl TerminalPane {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
pid: RawFd, pid: u32,
position_and_size: PaneGeom, position_and_size: PaneGeom,
style: Style, style: Style,
pane_index: usize, pane_index: usize,
@ -669,8 +714,10 @@ impl TerminalPane {
sixel_image_store: Rc<RefCell<SixelImageStore>>, sixel_image_store: Rc<RefCell<SixelImageStore>>,
terminal_emulator_colors: Rc<RefCell<Palette>>, terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>, terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
initial_pane_title: Option<String>,
) -> TerminalPane { ) -> TerminalPane {
let initial_pane_title = format!("Pane #{}", pane_index); let initial_pane_title =
initial_pane_title.unwrap_or_else(|| format!("Pane #{}", pane_index));
let grid = Grid::new( let grid = Grid::new(
position_and_size.rows.as_usize(), position_and_size.rows.as_usize(),
position_and_size.cols.as_usize(), position_and_size.cols.as_usize(),
@ -698,6 +745,7 @@ impl TerminalPane {
borderless: false, borderless: false,
fake_cursor_locations: HashSet::new(), fake_cursor_locations: HashSet::new(),
search_term: String::new(), search_term: String::new(),
is_held: None,
} }
} }
pub fn get_x(&self) -> usize { pub fn get_x(&self) -> usize {

View file

@ -14,6 +14,7 @@ use std::rc::Rc;
use std::time::Instant; use std::time::Instant;
use zellij_utils::{ use zellij_utils::{
data::{ModeInfo, Style}, data::{ModeInfo, Style},
input::command::RunCommand,
input::layout::SplitDirection, input::layout::SplitDirection,
pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport}, pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport},
}; };
@ -21,9 +22,9 @@ use zellij_utils::{
macro_rules! resize_pty { macro_rules! resize_pty {
($pane:expr, $os_input:expr) => { ($pane:expr, $os_input:expr) => {
if let PaneId::Terminal(ref pid) = $pane.pid() { if let PaneId::Terminal(ref pid) = $pane.pid() {
// FIXME: This `set_terminal_size_using_fd` call would be best in // FIXME: This `set_terminal_size_using_terminal_id` call would be best in
// `TerminalPane::reflow_lines` // `TerminalPane::reflow_lines`
$os_input.set_terminal_size_using_fd( $os_input.set_terminal_size_using_terminal_id(
*pid, *pid,
$pane.get_content_columns() as u16, $pane.get_content_columns() as u16,
$pane.get_content_rows() as u16, $pane.get_content_rows() as u16,
@ -993,6 +994,16 @@ impl TiledPanes {
None None
} }
} }
pub fn hold_pane(
&mut self,
pane_id: PaneId,
exit_status: Option<i32>,
run_command: RunCommand,
) {
self.panes
.get_mut(&pane_id)
.map(|p| p.hold(exit_status, run_command));
}
pub fn panes_to_hide_contains(&self, pane_id: PaneId) -> bool { pub fn panes_to_hide_contains(&self, pane_id: PaneId) -> bool {
self.panes_to_hide.contains(&pane_id) self.panes_to_hide.contains(&pane_id)
} }

View file

@ -39,6 +39,7 @@ fn create_pane() -> TerminalPane {
sixel_image_store, sixel_image_store,
Rc::new(RefCell::new(Palette::default())), Rc::new(RefCell::new(Palette::default())),
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
let content = read_fixture(); let content = read_fixture();
terminal_pane.handle_pty_bytes(content); terminal_pane.handle_pty_bytes(content);

View file

@ -47,6 +47,7 @@ pub fn scrolling_inside_a_pane() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
let mut text_to_fill_pane = String::new(); let mut text_to_fill_pane = String::new();
for i in 0..30 { for i in 0..30 {
@ -87,6 +88,7 @@ pub fn sixel_image_inside_terminal_pane() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
let sixel_image_bytes = "\u{1b}Pq let sixel_image_bytes = "\u{1b}Pq
#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0 #0;2;0;0;0#1;2;100;100;0#2;2;0;100;0
@ -127,6 +129,7 @@ pub fn partial_sixel_image_inside_terminal_pane() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
let pane_content = read_fixture("sixel-image-500px.six"); let pane_content = read_fixture("sixel-image-500px.six");
terminal_pane.handle_pty_bytes(pane_content); terminal_pane.handle_pty_bytes(pane_content);
@ -161,6 +164,7 @@ pub fn overflowing_sixel_image_inside_terminal_pane() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
let pane_content = read_fixture("sixel-image-500px.six"); let pane_content = read_fixture("sixel-image-500px.six");
terminal_pane.handle_pty_bytes(pane_content); terminal_pane.handle_pty_bytes(pane_content);
@ -194,6 +198,7 @@ pub fn scrolling_through_a_sixel_image() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
let mut text_to_fill_pane = String::new(); let mut text_to_fill_pane = String::new();
for i in 0..30 { for i in 0..30 {
@ -238,6 +243,7 @@ pub fn multiple_sixel_images_in_pane() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
let mut text_to_fill_pane = String::new(); let mut text_to_fill_pane = String::new();
for i in 0..5 { for i in 0..5 {
@ -280,6 +286,7 @@ pub fn resizing_pane_with_sixel_images() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
let mut text_to_fill_pane = String::new(); let mut text_to_fill_pane = String::new();
for i in 0..5 { for i in 0..5 {
@ -325,6 +332,7 @@ pub fn changing_character_cell_size_with_sixel_images() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
let mut text_to_fill_pane = String::new(); let mut text_to_fill_pane = String::new();
for i in 0..5 { for i in 0..5 {
@ -375,6 +383,7 @@ pub fn keep_working_after_corrupted_sixel_image() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
let sixel_image_bytes = "\u{1b}PI AM CORRUPTED BWAHAHAq let sixel_image_bytes = "\u{1b}PI AM CORRUPTED BWAHAHAq
@ -423,6 +432,7 @@ pub fn pane_with_frame_position_is_on_frame() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
terminal_pane.set_content_offset(Offset::frame(1)); terminal_pane.set_content_offset(Offset::frame(1));
@ -507,6 +517,7 @@ pub fn pane_with_bottom_and_right_borders_position_is_on_frame() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
terminal_pane.set_content_offset(Offset::shift(1, 1)); terminal_pane.set_content_offset(Offset::shift(1, 1));
@ -591,6 +602,7 @@ pub fn frameless_pane_position_is_on_frame() {
sixel_image_store, sixel_image_store,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
None,
); // 0 is the pane index ); // 0 is the pane index
terminal_pane.set_content_offset(Offset::default()); terminal_pane.set_content_offset(Offset::default());

View file

@ -1,6 +1,10 @@
use crate::os_input_output::SpawnTerminalError;
use crate::terminal_bytes::TerminalBytes; use crate::terminal_bytes::TerminalBytes;
use crate::{ use crate::{
panes::PaneId, screen::ScreenInstruction, thread_bus::Bus, wasm_vm::PluginInstruction, panes::PaneId,
screen::ScreenInstruction,
thread_bus::{Bus, ThreadSenders},
wasm_vm::PluginInstruction,
ClientId, ServerInstruction, ClientId, ServerInstruction,
}; };
use async_std::task::{self, JoinHandle}; use async_std::task::{self, JoinHandle};
@ -27,7 +31,8 @@ pub enum ClientOrTabIndex {
/// Instructions related to PTYs (pseudoterminals). /// Instructions related to PTYs (pseudoterminals).
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) enum PtyInstruction { pub(crate) enum PtyInstruction {
SpawnTerminal(Option<TerminalAction>, ClientOrTabIndex), SpawnTerminal(Option<TerminalAction>, Option<bool>, ClientOrTabIndex), // bool (if Some) is
// should_float
OpenInPlaceEditor(PathBuf, Option<usize>, ClientId), // Option<usize> is the optional line number OpenInPlaceEditor(PathBuf, Option<usize>, ClientId), // Option<usize> is the optional line number
SpawnTerminalVertically(Option<TerminalAction>, ClientId), SpawnTerminalVertically(Option<TerminalAction>, ClientId),
SpawnTerminalHorizontally(Option<TerminalAction>, ClientId), SpawnTerminalHorizontally(Option<TerminalAction>, ClientId),
@ -41,6 +46,7 @@ pub(crate) enum PtyInstruction {
), // the String is the tab name ), // the String is the tab name
ClosePane(PaneId), ClosePane(PaneId),
CloseTab(Vec<PaneId>), CloseTab(Vec<PaneId>),
ReRunCommandInPane(PaneId, RunCommand),
Exit, Exit,
} }
@ -56,6 +62,7 @@ impl From<&PtyInstruction> for PtyContext {
PtyInstruction::ClosePane(_) => PtyContext::ClosePane, PtyInstruction::ClosePane(_) => PtyContext::ClosePane,
PtyInstruction::CloseTab(_) => PtyContext::CloseTab, PtyInstruction::CloseTab(_) => PtyContext::CloseTab,
PtyInstruction::NewTab(..) => PtyContext::NewTab, PtyInstruction::NewTab(..) => PtyContext::NewTab,
PtyInstruction::ReRunCommandInPane(..) => PtyContext::ReRunCommandInPane,
PtyInstruction::Exit => PtyContext::Exit, PtyInstruction::Exit => PtyContext::Exit,
} }
} }
@ -64,9 +71,9 @@ impl From<&PtyInstruction> for PtyContext {
pub(crate) struct Pty { pub(crate) struct Pty {
pub active_panes: HashMap<ClientId, PaneId>, pub active_panes: HashMap<ClientId, PaneId>,
pub bus: Bus<PtyInstruction>, pub bus: Bus<PtyInstruction>,
pub id_to_child_pid: HashMap<RawFd, RawFd>, // pty_primary => child raw fd pub id_to_child_pid: HashMap<u32, RawFd>, // terminal_id => child raw fd
debug_to_file: bool, debug_to_file: bool,
task_handles: HashMap<RawFd, JoinHandle<()>>, task_handles: HashMap<u32, JoinHandle<()>>, // terminal_id to join-handle
default_editor: Option<PathBuf>, default_editor: Option<PathBuf>,
} }
@ -75,18 +82,54 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) {
let (event, mut err_ctx) = pty.bus.recv().expect("failed to receive event on channel"); let (event, mut err_ctx) = pty.bus.recv().expect("failed to receive event on channel");
err_ctx.add_call(ContextType::Pty((&event).into())); err_ctx.add_call(ContextType::Pty((&event).into()));
match event { match event {
PtyInstruction::SpawnTerminal(terminal_action, client_or_tab_index) => { PtyInstruction::SpawnTerminal(terminal_action, should_float, client_or_tab_index) => {
let pid = pty let (hold_on_close, run_command, pane_title) = match &terminal_action {
.spawn_terminal(terminal_action, client_or_tab_index) Some(TerminalAction::RunCommand(run_command)) => (
.unwrap(); // TODO: handle error here run_command.hold_on_close,
Some(run_command.clone()),
Some(run_command.to_string()),
),
_ => (false, None, None),
};
match pty.spawn_terminal(terminal_action, client_or_tab_index) {
Ok(pid) => {
pty.bus pty.bus
.senders .senders
.send_to_screen(ScreenInstruction::NewPane( .send_to_screen(ScreenInstruction::NewPane(
PaneId::Terminal(pid), PaneId::Terminal(pid),
pane_title,
should_float,
client_or_tab_index, client_or_tab_index,
)) ))
.unwrap(); .unwrap();
}, },
Err(SpawnTerminalError::CommandNotFound(pid)) => {
if hold_on_close {
pty.bus
.senders
.send_to_screen(ScreenInstruction::NewPane(
PaneId::Terminal(pid),
pane_title,
should_float,
client_or_tab_index,
))
.unwrap();
if let Some(run_command) = run_command {
send_command_not_found_to_screen(
pty.bus.senders.clone(),
pid,
run_command.clone(),
);
}
} else {
pty.close_pane(PaneId::Terminal(pid));
}
},
Err(e) => {
log::error!("Failed to spawn terminal: {}", e);
},
}
},
PtyInstruction::OpenInPlaceEditor(temp_file, line_number, client_id) => { PtyInstruction::OpenInPlaceEditor(temp_file, line_number, client_id) => {
match pty.spawn_terminal( match pty.spawn_terminal(
Some(TerminalAction::OpenFile(temp_file, line_number)), Some(TerminalAction::OpenFile(temp_file, line_number)),
@ -107,29 +150,125 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) {
} }
}, },
PtyInstruction::SpawnTerminalVertically(terminal_action, client_id) => { PtyInstruction::SpawnTerminalVertically(terminal_action, client_id) => {
let pid = pty let (hold_on_close, run_command, pane_title) = match &terminal_action {
.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)) Some(TerminalAction::RunCommand(run_command)) => (
.unwrap(); // TODO: handle error here run_command.hold_on_close,
Some(run_command.clone()),
Some(run_command.to_string()),
),
_ => (false, None, None),
};
match pty.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)) {
Ok(pid) => {
pty.bus pty.bus
.senders .senders
.send_to_screen(ScreenInstruction::VerticalSplit( .send_to_screen(ScreenInstruction::VerticalSplit(
PaneId::Terminal(pid), PaneId::Terminal(pid),
pane_title,
client_id, client_id,
)) ))
.unwrap(); .unwrap();
}, },
Err(SpawnTerminalError::CommandNotFound(pid)) => {
if hold_on_close {
pty.bus
.senders
.send_to_screen(ScreenInstruction::VerticalSplit(
PaneId::Terminal(pid),
pane_title,
client_id,
))
.unwrap();
if let Some(run_command) = run_command {
pty.bus
.senders
.send_to_screen(ScreenInstruction::PtyBytes(
pid,
format!(
"Command not found: {}",
run_command.command.display()
)
.as_bytes()
.to_vec(),
))
.unwrap();
pty.bus
.senders
.send_to_screen(ScreenInstruction::HoldPane(
PaneId::Terminal(pid),
Some(2), // exit status
run_command,
None,
))
.unwrap();
}
}
},
Err(e) => {
log::error!("Failed to spawn terminal: {}", e);
},
}
},
PtyInstruction::SpawnTerminalHorizontally(terminal_action, client_id) => { PtyInstruction::SpawnTerminalHorizontally(terminal_action, client_id) => {
let pid = pty let (hold_on_close, run_command, pane_title) = match &terminal_action {
.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)) Some(TerminalAction::RunCommand(run_command)) => (
.unwrap(); // TODO: handle error here run_command.hold_on_close,
Some(run_command.clone()),
Some(run_command.to_string()),
),
_ => (false, None, None),
};
match pty.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)) {
Ok(pid) => {
pty.bus pty.bus
.senders .senders
.send_to_screen(ScreenInstruction::HorizontalSplit( .send_to_screen(ScreenInstruction::HorizontalSplit(
PaneId::Terminal(pid), PaneId::Terminal(pid),
pane_title,
client_id, client_id,
)) ))
.unwrap(); .unwrap();
}, },
Err(SpawnTerminalError::CommandNotFound(pid)) => {
if hold_on_close {
pty.bus
.senders
.send_to_screen(ScreenInstruction::HorizontalSplit(
PaneId::Terminal(pid),
pane_title,
client_id,
))
.unwrap();
if let Some(run_command) = run_command {
pty.bus
.senders
.send_to_screen(ScreenInstruction::PtyBytes(
pid,
format!(
"Command not found: {}",
run_command.command.display()
)
.as_bytes()
.to_vec(),
))
.unwrap();
pty.bus
.senders
.send_to_screen(ScreenInstruction::HoldPane(
PaneId::Terminal(pid),
Some(2), // exit status
run_command,
None,
))
.unwrap();
}
}
},
Err(e) => {
log::error!("Failed to spawn terminal: {}", e);
},
}
},
PtyInstruction::UpdateActivePane(pane_id, client_id) => { PtyInstruction::UpdateActivePane(pane_id, client_id) => {
pty.set_active_pane(pane_id, client_id); pty.set_active_pane(pane_id, client_id);
}, },
@ -175,6 +314,36 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) {
.send_to_server(ServerInstruction::UnblockInputThread) .send_to_server(ServerInstruction::UnblockInputThread)
.unwrap(); .unwrap();
}, },
PtyInstruction::ReRunCommandInPane(pane_id, run_command) => {
match pty.rerun_command_in_pane(pane_id, run_command.clone()) {
Ok(..) => {},
Err(SpawnTerminalError::CommandNotFound(pid)) => {
if run_command.hold_on_close {
pty.bus
.senders
.send_to_screen(ScreenInstruction::PtyBytes(
pid,
format!("Command not found: {}", run_command.command.display())
.as_bytes()
.to_vec(),
))
.unwrap();
pty.bus
.senders
.send_to_screen(ScreenInstruction::HoldPane(
PaneId::Terminal(pid),
Some(2), // exit status
run_command,
None,
))
.unwrap();
}
},
Err(e) => {
log::error!("Failed to spawn terminal: {}", e);
},
}
},
PtyInstruction::Exit => break, PtyInstruction::Exit => break,
} }
} }
@ -200,6 +369,7 @@ impl Pty {
args: vec![], args: vec![],
command: PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable")), command: PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable")),
cwd: None, // this should be filled by the calling function, eg. spawn_terminal cwd: None, // this should be filled by the calling function, eg. spawn_terminal
hold_on_close: false,
}) })
} }
fn fill_cwd(&self, terminal_action: &mut TerminalAction, client_id: ClientId) { fn fill_cwd(&self, terminal_action: &mut TerminalAction, client_id: ClientId) {
@ -225,7 +395,8 @@ impl Pty {
&mut self, &mut self,
terminal_action: Option<TerminalAction>, terminal_action: Option<TerminalAction>,
client_or_tab_index: ClientOrTabIndex, client_or_tab_index: ClientOrTabIndex,
) -> Result<RawFd, &'static str> { ) -> Result<u32, SpawnTerminalError> {
// returns the terminal id
let terminal_action = match client_or_tab_index { let terminal_action = match client_or_tab_index {
ClientOrTabIndex::ClientId(client_id) => { ClientOrTabIndex::ClientId(client_id) => {
let mut terminal_action = let mut terminal_action =
@ -237,13 +408,26 @@ impl Pty {
terminal_action.unwrap_or_else(|| self.get_default_terminal()) terminal_action.unwrap_or_else(|| self.get_default_terminal())
}, },
}; };
let hold_on_close = match &terminal_action {
TerminalAction::RunCommand(run_command) => run_command.hold_on_close,
_ => false,
};
let quit_cb = Box::new({ let quit_cb = Box::new({
let senders = self.bus.senders.clone(); let senders = self.bus.senders.clone();
move |pane_id| { move |pane_id, exit_status, command| {
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 _ = senders.send_to_screen(ScreenInstruction::ClosePane(pane_id, None));
} }
}
}); });
let (pid_primary, child_fd): (RawFd, RawFd) = self let (terminal_id, pid_primary, child_fd): (u32, RawFd, RawFd) = self
.bus .bus
.os_input .os_input
.as_mut() .as_mut()
@ -254,15 +438,15 @@ impl Pty {
let os_input = self.bus.os_input.as_ref().unwrap().clone(); let os_input = self.bus.os_input.as_ref().unwrap().clone();
let debug_to_file = self.debug_to_file; let debug_to_file = self.debug_to_file;
async move { async move {
TerminalBytes::new(pid_primary, senders, os_input, debug_to_file) TerminalBytes::new(pid_primary, senders, os_input, debug_to_file, terminal_id)
.listen() .listen()
.await; .await;
} }
}); });
self.task_handles.insert(pid_primary, terminal_bytes); self.task_handles.insert(terminal_id, terminal_bytes);
self.id_to_child_pid.insert(pid_primary, child_fd); self.id_to_child_pid.insert(terminal_id, child_fd);
Ok(pid_primary) Ok(terminal_id)
} }
pub fn spawn_terminals_for_layout( pub fn spawn_terminals_for_layout(
&mut self, &mut self,
@ -273,69 +457,145 @@ impl Pty {
let mut default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal()); let mut default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal());
self.fill_cwd(&mut default_shell, client_id); self.fill_cwd(&mut default_shell, client_id);
let extracted_run_instructions = layout.extract_run_instructions(); let extracted_run_instructions = layout.extract_run_instructions();
let mut new_pane_pids = vec![]; let mut new_pane_pids: Vec<(u32, Option<RunCommand>, Result<RawFd, SpawnTerminalError>)> =
vec![]; // (terminal_id,
// run_command
// file_descriptor)
for run_instruction in extracted_run_instructions { for run_instruction in extracted_run_instructions {
let quit_cb = Box::new({ let quit_cb = Box::new({
let senders = self.bus.senders.clone(); let senders = self.bus.senders.clone();
move |pane_id| { move |pane_id, _exit_status, _command| {
let _ = senders.send_to_screen(ScreenInstruction::ClosePane(pane_id, None)); let _ = senders.send_to_screen(ScreenInstruction::ClosePane(pane_id, None));
} }
}); });
match run_instruction { match run_instruction {
Some(Run::Command(command)) => { Some(Run::Command(command)) => {
let cmd = TerminalAction::RunCommand(command); let hold_on_close = command.hold_on_close;
let (pid_primary, child_fd): (RawFd, RawFd) = self let quit_cb = Box::new({
.bus let senders = self.bus.senders.clone();
.os_input move |pane_id, exit_status, command| {
.as_mut() if hold_on_close {
.unwrap() let _ = senders.send_to_screen(ScreenInstruction::HoldPane(
.spawn_terminal(cmd, quit_cb, self.default_editor.clone()) pane_id,
.unwrap(); // TODO: handle error here exit_status,
self.id_to_child_pid.insert(pid_primary, child_fd); command,
new_pane_pids.push(pid_primary); None,
));
} else {
let _ = senders
.send_to_screen(ScreenInstruction::ClosePane(pane_id, None));
}
}
});
let cmd = TerminalAction::RunCommand(command.clone());
match self.bus.os_input.as_mut().unwrap().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);
},
}
}, },
None => { None => {
let (pid_primary, child_fd): (RawFd, RawFd) = self match self.bus.os_input.as_mut().unwrap().spawn_terminal(
.bus default_shell.clone(),
.os_input quit_cb,
.as_mut() self.default_editor.clone(),
.unwrap() ) {
.spawn_terminal(default_shell.clone(), quit_cb, self.default_editor.clone()) Ok((terminal_id, pid_primary, child_fd)) => {
.unwrap(); // TODO: handle error here self.id_to_child_pid.insert(terminal_id, child_fd);
self.id_to_child_pid.insert(pid_primary, child_fd); new_pane_pids.push((terminal_id, None, Ok(pid_primary)));
new_pane_pids.push(pid_primary); },
Err(SpawnTerminalError::CommandNotFound(terminal_id)) => {
new_pane_pids.push((
terminal_id,
None,
Err(SpawnTerminalError::CommandNotFound(terminal_id)),
));
},
Err(e) => {
log::error!("Failed to spawn terminal: {}", e);
},
}
}, },
// Investigate moving plugin loading to here. // Investigate moving plugin loading to here.
Some(Run::Plugin(_)) => {}, Some(Run::Plugin(_)) => {},
} }
} }
let new_tab_pane_ids: Vec<u32> = new_pane_pids
.iter()
.map(|(terminal_id, _, _)| *terminal_id)
.collect::<Vec<u32>>();
self.bus self.bus
.senders .senders
.send_to_screen(ScreenInstruction::NewTab( .send_to_screen(ScreenInstruction::NewTab(
layout, layout,
new_pane_pids.clone(), new_tab_pane_ids,
client_id, client_id,
)) ))
.unwrap(); .unwrap();
for id in new_pane_pids { for (terminal_id, run_command, pid_primary) in new_pane_pids {
match pid_primary {
Ok(pid_primary) => {
let terminal_bytes = task::spawn({ let terminal_bytes = task::spawn({
let senders = self.bus.senders.clone(); let senders = self.bus.senders.clone();
let os_input = self.bus.os_input.as_ref().unwrap().clone(); let os_input = self.bus.os_input.as_ref().unwrap().clone();
let debug_to_file = self.debug_to_file; let debug_to_file = self.debug_to_file;
async move { async move {
TerminalBytes::new(id, senders, os_input, debug_to_file) TerminalBytes::new(
pid_primary,
senders,
os_input,
debug_to_file,
terminal_id,
)
.listen() .listen()
.await; .await;
} }
}); });
self.task_handles.insert(id, terminal_bytes); self.task_handles.insert(terminal_id, terminal_bytes);
},
_ => match run_command {
Some(run_command) => {
if run_command.hold_on_close {
send_command_not_found_to_screen(
self.bus.senders.clone(),
terminal_id,
run_command.clone(),
);
} else {
self.close_pane(PaneId::Terminal(terminal_id));
}
},
None => {
self.close_pane(PaneId::Terminal(terminal_id));
},
},
}
} }
} }
pub fn close_pane(&mut self, id: PaneId) { pub fn close_pane(&mut self, id: PaneId) {
match id { match id {
PaneId::Terminal(id) => { PaneId::Terminal(id) => {
let child_fd = self.id_to_child_pid.remove(&id).unwrap(); self.task_handles.remove(&id);
self.task_handles.remove(&id).unwrap(); if let Some(child_fd) = self.id_to_child_pid.remove(&id) {
task::block_on(async { task::block_on(async {
self.bus self.bus
.os_input .os_input
@ -344,6 +604,8 @@ impl Pty {
.kill(Pid::from_raw(child_fd)) .kill(Pid::from_raw(child_fd))
.unwrap(); .unwrap();
}); });
}
self.bus.os_input.as_ref().unwrap().clear_terminal_id(id);
}, },
PaneId::Plugin(pid) => drop( PaneId::Plugin(pid) => drop(
self.bus self.bus
@ -362,13 +624,84 @@ impl Pty {
self.active_panes.insert(client_id, pane_id); self.active_panes.insert(client_id, pane_id);
} }
} }
pub fn rerun_command_in_pane(
&mut self,
pane_id: PaneId,
run_command: RunCommand,
) -> Result<(), SpawnTerminalError> {
match pane_id {
PaneId::Terminal(id) => {
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 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,
));
}
});
let (pid_primary, child_fd): (RawFd, RawFd) =
self.bus
.os_input
.as_mut()
.unwrap()
.re_run_command_in_terminal(id, run_command, quit_cb)?;
let terminal_bytes = task::spawn({
let senders = self.bus.senders.clone();
let os_input = self.bus.os_input.as_ref().unwrap().clone();
let debug_to_file = self.debug_to_file;
async move {
TerminalBytes::new(pid_primary, senders, os_input, debug_to_file, id)
.listen()
.await;
}
});
self.task_handles.insert(id, terminal_bytes);
self.id_to_child_pid.insert(id, child_fd);
Ok(())
},
_ => Err(SpawnTerminalError::GenericSpawnError(
"Cannot respawn plugin panes",
)),
}
}
} }
impl Drop for Pty { impl Drop for Pty {
fn drop(&mut self) { fn drop(&mut self) {
let child_ids: Vec<RawFd> = self.id_to_child_pid.keys().copied().collect(); let child_ids: Vec<u32> = self.id_to_child_pid.keys().copied().collect();
for id in child_ids { for id in child_ids {
self.close_pane(PaneId::Terminal(id)); self.close_pane(PaneId::Terminal(id));
} }
} }
} }
fn send_command_not_found_to_screen(
senders: ThreadSenders,
terminal_id: u32,
run_command: RunCommand,
) {
senders
.send_to_screen(ScreenInstruction::PtyBytes(
terminal_id,
format!("Command not found: {}", run_command.command.display())
.as_bytes()
.to_vec(),
))
.unwrap();
senders
.send_to_screen(ScreenInstruction::HoldPane(
PaneId::Terminal(terminal_id),
Some(2),
run_command.clone(),
None,
))
.unwrap();
}

View file

@ -4,7 +4,7 @@ use crate::thread_bus::Bus;
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) enum PtyWriteInstruction { pub(crate) enum PtyWriteInstruction {
Write(Vec<u8>, i32), Write(Vec<u8>, u32),
Exit, Exit,
} }

View file

@ -245,29 +245,17 @@ pub(crate) fn route_action(
PtyInstruction::SpawnTerminalHorizontally(shell, client_id) PtyInstruction::SpawnTerminalHorizontally(shell, client_id)
}, },
// No direction specified - try to put it in the biggest available spot // No direction specified - try to put it in the biggest available spot
None => PtyInstruction::SpawnTerminal(shell, ClientOrTabIndex::ClientId(client_id)), None => PtyInstruction::SpawnTerminal(
shell,
None,
ClientOrTabIndex::ClientId(client_id),
),
}; };
session.senders.send_to_pty(pty_instr).unwrap(); session.senders.send_to_pty(pty_instr).unwrap();
}, },
Action::EditFile(path_to_file, line_number, split_direction, should_float) => { Action::EditFile(path_to_file, line_number, split_direction, should_float) => {
match should_float {
Some(true) => {
session
.senders
.send_to_screen(ScreenInstruction::ShowFloatingPanes(client_id))
.unwrap();
},
Some(false) => {
session
.senders
.send_to_screen(ScreenInstruction::HideFloatingPanes(client_id))
.unwrap();
},
None => {},
};
let open_file = TerminalAction::OpenFile(path_to_file, line_number); let open_file = TerminalAction::OpenFile(path_to_file, line_number);
let pty_instr = match (split_direction, should_float.unwrap_or(false)) { let pty_instr = match (split_direction, should_float) {
(Some(Direction::Left), false) => { (Some(Direction::Left), false) => {
PtyInstruction::SpawnTerminalVertically(Some(open_file), client_id) PtyInstruction::SpawnTerminalVertically(Some(open_file), client_id)
}, },
@ -283,6 +271,7 @@ pub(crate) fn route_action(
// No direction specified or should float - defer placement to screen // No direction specified or should float - defer placement to screen
(None, _) | (_, true) => PtyInstruction::SpawnTerminal( (None, _) | (_, true) => PtyInstruction::SpawnTerminal(
Some(open_file), Some(open_file),
Some(should_float),
ClientOrTabIndex::ClientId(client_id), ClientOrTabIndex::ClientId(client_id),
), ),
}; };
@ -308,10 +297,7 @@ pub(crate) fn route_action(
.unwrap(); .unwrap();
}, },
Action::NewFloatingPane(run_command) => { Action::NewFloatingPane(run_command) => {
session let should_float = true;
.senders
.send_to_screen(ScreenInstruction::ShowFloatingPanes(client_id))
.unwrap();
let run_cmd = run_command let run_cmd = run_command
.map(|cmd| TerminalAction::RunCommand(cmd.into())) .map(|cmd| TerminalAction::RunCommand(cmd.into()))
.or_else(|| session.default_shell.clone()); .or_else(|| session.default_shell.clone());
@ -319,15 +305,13 @@ pub(crate) fn route_action(
.senders .senders
.send_to_pty(PtyInstruction::SpawnTerminal( .send_to_pty(PtyInstruction::SpawnTerminal(
run_cmd, run_cmd,
Some(should_float),
ClientOrTabIndex::ClientId(client_id), ClientOrTabIndex::ClientId(client_id),
)) ))
.unwrap(); .unwrap();
}, },
Action::NewTiledPane(direction, run_command) => { Action::NewTiledPane(direction, run_command) => {
session let should_float = false;
.senders
.send_to_screen(ScreenInstruction::HideFloatingPanes(client_id))
.unwrap();
let run_cmd = run_command let run_cmd = run_command
.map(|cmd| TerminalAction::RunCommand(cmd.into())) .map(|cmd| TerminalAction::RunCommand(cmd.into()))
.or_else(|| session.default_shell.clone()); .or_else(|| session.default_shell.clone());
@ -345,9 +329,11 @@ pub(crate) fn route_action(
PtyInstruction::SpawnTerminalHorizontally(run_cmd, client_id) PtyInstruction::SpawnTerminalHorizontally(run_cmd, client_id)
}, },
// No direction specified - try to put it in the biggest available spot // No direction specified - try to put it in the biggest available spot
None => { None => PtyInstruction::SpawnTerminal(
PtyInstruction::SpawnTerminal(run_cmd, ClientOrTabIndex::ClientId(client_id)) run_cmd,
}, Some(should_float),
ClientOrTabIndex::ClientId(client_id),
),
}; };
session.senders.send_to_pty(pty_instr).unwrap(); session.senders.send_to_pty(pty_instr).unwrap();
}, },
@ -394,9 +380,11 @@ pub(crate) fn route_action(
PtyInstruction::SpawnTerminalHorizontally(run_cmd, client_id) PtyInstruction::SpawnTerminalHorizontally(run_cmd, client_id)
}, },
// No direction specified - try to put it in the biggest available spot // No direction specified - try to put it in the biggest available spot
None => { None => PtyInstruction::SpawnTerminal(
PtyInstruction::SpawnTerminal(run_cmd, ClientOrTabIndex::ClientId(client_id)) run_cmd,
}, None,
ClientOrTabIndex::ClientId(client_id),
),
}; };
session.senders.send_to_pty(pty_instr).unwrap(); session.senders.send_to_pty(pty_instr).unwrap();
}, },

View file

@ -2,11 +2,11 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::os::unix::io::RawFd;
use std::rc::Rc; use std::rc::Rc;
use std::str; use std::str;
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand;
use zellij_utils::input::options::Clipboard; use zellij_utils::input::options::Clipboard;
use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::pane_size::{Size, SizeInPixels};
use zellij_utils::{input::command::TerminalAction, input::layout::PaneLayout, position::Position}; use zellij_utils::{input::command::TerminalAction, input::layout::PaneLayout, position::Position};
@ -115,16 +115,16 @@ macro_rules! active_tab_and_connected_client_id {
/// Instructions that can be sent to the [`Screen`]. /// Instructions that can be sent to the [`Screen`].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ScreenInstruction { pub enum ScreenInstruction {
PtyBytes(RawFd, VteBytes), PtyBytes(u32, VteBytes),
Render, Render,
NewPane(PaneId, ClientOrTabIndex), NewPane(PaneId, Option<String>, Option<bool>, ClientOrTabIndex), // String is initial title,
// bool (if Some) is
// should_float
OpenInPlaceEditor(PaneId, ClientId), OpenInPlaceEditor(PaneId, ClientId),
TogglePaneEmbedOrFloating(ClientId), TogglePaneEmbedOrFloating(ClientId),
ToggleFloatingPanes(ClientId, Option<TerminalAction>), ToggleFloatingPanes(ClientId, Option<TerminalAction>),
ShowFloatingPanes(ClientId), HorizontalSplit(PaneId, Option<String>, ClientId), // String is initial title
HideFloatingPanes(ClientId), VerticalSplit(PaneId, Option<String>, ClientId), // String is initial title
HorizontalSplit(PaneId, ClientId),
VerticalSplit(PaneId, ClientId),
WriteCharacter(Vec<u8>, ClientId), WriteCharacter(Vec<u8>, ClientId),
ResizeLeft(ClientId), ResizeLeft(ClientId),
ResizeRight(ClientId), ResizeRight(ClientId),
@ -164,9 +164,10 @@ pub enum ScreenInstruction {
TogglePaneFrames, TogglePaneFrames,
SetSelectable(PaneId, bool, usize), SetSelectable(PaneId, bool, usize),
ClosePane(PaneId, Option<ClientId>), ClosePane(PaneId, Option<ClientId>),
HoldPane(PaneId, Option<i32>, RunCommand, Option<ClientId>), // Option<i32> is the exit status
UpdatePaneName(Vec<u8>, ClientId), UpdatePaneName(Vec<u8>, ClientId),
UndoRenamePane(ClientId), UndoRenamePane(ClientId),
NewTab(PaneLayout, Vec<RawFd>, ClientId), NewTab(PaneLayout, Vec<u32>, ClientId),
SwitchTabNext(ClientId), SwitchTabNext(ClientId),
SwitchTabPrev(ClientId), SwitchTabPrev(ClientId),
ToggleActiveSyncTab(ClientId), ToggleActiveSyncTab(ClientId),
@ -217,8 +218,6 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenContext::TogglePaneEmbedOrFloating ScreenContext::TogglePaneEmbedOrFloating
}, },
ScreenInstruction::ToggleFloatingPanes(..) => ScreenContext::ToggleFloatingPanes, ScreenInstruction::ToggleFloatingPanes(..) => ScreenContext::ToggleFloatingPanes,
ScreenInstruction::ShowFloatingPanes(..) => ScreenContext::ShowFloatingPanes,
ScreenInstruction::HideFloatingPanes(..) => ScreenContext::HideFloatingPanes,
ScreenInstruction::HorizontalSplit(..) => ScreenContext::HorizontalSplit, ScreenInstruction::HorizontalSplit(..) => ScreenContext::HorizontalSplit,
ScreenInstruction::VerticalSplit(..) => ScreenContext::VerticalSplit, ScreenInstruction::VerticalSplit(..) => ScreenContext::VerticalSplit,
ScreenInstruction::WriteCharacter(..) => ScreenContext::WriteCharacter, ScreenInstruction::WriteCharacter(..) => ScreenContext::WriteCharacter,
@ -264,6 +263,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::TogglePaneFrames => ScreenContext::TogglePaneFrames, ScreenInstruction::TogglePaneFrames => ScreenContext::TogglePaneFrames,
ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable, ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable,
ScreenInstruction::ClosePane(..) => ScreenContext::ClosePane, ScreenInstruction::ClosePane(..) => ScreenContext::ClosePane,
ScreenInstruction::HoldPane(..) => ScreenContext::HoldPane,
ScreenInstruction::UpdatePaneName(..) => ScreenContext::UpdatePaneName, ScreenInstruction::UpdatePaneName(..) => ScreenContext::UpdatePaneName,
ScreenInstruction::UndoRenamePane(..) => ScreenContext::UndoRenamePane, ScreenInstruction::UndoRenamePane(..) => ScreenContext::UndoRenamePane,
ScreenInstruction::NewTab(..) => ScreenContext::NewTab, ScreenInstruction::NewTab(..) => ScreenContext::NewTab,
@ -811,7 +811,7 @@ impl Screen {
pub fn new_tab( pub fn new_tab(
&mut self, &mut self,
layout: PaneLayout, layout: PaneLayout,
new_pids: Vec<RawFd>, new_ids: Vec<u32>,
client_id: ClientId, client_id: ClientId,
) -> Result<()> { ) -> Result<()> {
let client_id = if self.get_active_tab(client_id).is_some() { let client_id = if self.get_active_tab(client_id).is_some() {
@ -821,7 +821,7 @@ impl Screen {
} else { } else {
client_id client_id
}; };
let err_context = || format!("failed to create new tab for client {client_id:?}"); let err_context = || format!("failed to create new tab for client {client_id:?}",);
let tab_index = self.get_new_tab_index(); let tab_index = self.get_new_tab_index();
let position = self.tabs.len(); let position = self.tabs.len();
let mut tab = Tab::new( let mut tab = Tab::new(
@ -848,7 +848,7 @@ impl Screen {
self.terminal_emulator_colors.clone(), self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(), self.terminal_emulator_color_codes.clone(),
); );
tab.apply_layout(layout, new_pids, tab_index, client_id) tab.apply_layout(layout, new_ids, tab_index, client_id)
.with_context(err_context)?; .with_context(err_context)?;
if self.session_is_mirrored { if self.session_is_mirrored {
if let Some(active_tab) = self.get_active_tab_mut(client_id) { if let Some(active_tab) = self.get_active_tab_mut(client_id) {
@ -1002,9 +1002,8 @@ impl Screen {
} }
}, },
} }
self.update_tabs().with_context(|| { self.update_tabs()
format!("failed to update active tabs name for client id: {client_id:?}") .context("failed to update active tabs name for client id: {client_id:?}")
})
} else { } else {
log::error!("Active tab not found for client id: {client_id:?}"); log::error!("Active tab not found for client id: {client_id:?}");
Ok(()) Ok(())
@ -1092,7 +1091,6 @@ impl Screen {
} }
Ok(()) Ok(())
} }
pub fn change_mode_for_all_clients(&mut self, mode_info: ModeInfo) -> Result<()> { pub fn change_mode_for_all_clients(&mut self, mode_info: ModeInfo) -> Result<()> {
let err_context = || { let err_context = || {
format!( format!(
@ -1225,17 +1223,24 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::Render => { ScreenInstruction::Render => {
screen.render()?; screen.render()?;
}, },
ScreenInstruction::NewPane(pid, client_or_tab_index) => { ScreenInstruction::NewPane(
pid,
initial_pane_title,
should_float,
client_or_tab_index,
) => {
match client_or_tab_index { match client_or_tab_index {
ClientOrTabIndex::ClientId(client_id) => { ClientOrTabIndex::ClientId(client_id) => {
active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab,
client_id: ClientId| tab .new_pane(pid, client_id: ClientId| tab .new_pane(pid,
initial_pane_title,
should_float,
Some(client_id)), Some(client_id)),
?); ?);
}, },
ClientOrTabIndex::TabIndex(tab_index) => { ClientOrTabIndex::TabIndex(tab_index) => {
if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { if let Some(active_tab) = screen.tabs.get_mut(&tab_index) {
active_tab.new_pane(pid, None)?; active_tab.new_pane(pid, initial_pane_title, should_float, None)?;
} else { } else {
log::error!("Tab index not found: {:?}", tab_index); log::error!("Tab index not found: {:?}", tab_index);
} }
@ -1270,44 +1275,22 @@ pub(crate) fn screen_thread_main(
screen.render()?; screen.render()?;
}, },
ScreenInstruction::ShowFloatingPanes(client_id) => { ScreenInstruction::HorizontalSplit(pid, initial_pane_title, client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
screen, screen,
client_id, client_id,
|tab: &mut Tab, _client_id: ClientId| tab.show_floating_panes() |tab: &mut Tab, client_id: ClientId| tab.horizontal_split(pid, initial_pane_title, client_id),
);
screen.unblock_input()?;
screen.update_tabs()?; // update tabs so that the ui indication will be send to the plugins
screen.render()?;
},
ScreenInstruction::HideFloatingPanes(client_id) => {
active_tab_and_connected_client_id!(
screen,
client_id,
|tab: &mut Tab, _client_id: ClientId| tab.hide_floating_panes()
);
screen.unblock_input()?;
screen.update_tabs()?; // update tabs so that the ui indication will be send to the plugins
screen.render()?;
},
ScreenInstruction::HorizontalSplit(pid, client_id) => {
active_tab_and_connected_client_id!(
screen,
client_id,
|tab: &mut Tab, client_id: ClientId| tab.horizontal_split(pid, client_id),
? ?
); );
screen.unblock_input()?; screen.unblock_input()?;
screen.update_tabs()?; screen.update_tabs()?;
screen.render()?; screen.render()?;
}, },
ScreenInstruction::VerticalSplit(pid, client_id) => { ScreenInstruction::VerticalSplit(pid, initial_pane_title, client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
screen, screen,
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.vertical_split(pid, client_id), |tab: &mut Tab, client_id: ClientId| tab.vertical_split(pid, initial_pane_title, client_id),
? ?
); );
screen.unblock_input()?; screen.unblock_input()?;
@ -1657,6 +1640,27 @@ pub(crate) fn screen_thread_main(
screen.update_tabs()?; screen.update_tabs()?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::HoldPane(id, exit_status, run_command, client_id) => {
match client_id {
Some(client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab.hold_pane(
id,
exit_status,
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);
break;
}
}
},
}
screen.update_tabs()?;
screen.unblock_input()?;
},
ScreenInstruction::UpdatePaneName(c, client_id) => { ScreenInstruction::UpdatePaneName(c, client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
screen, screen,

View file

@ -8,6 +8,7 @@ use copy_command::CopyCommand;
use std::env::temp_dir; use std::env::temp_dir;
use uuid::Uuid; use uuid::Uuid;
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand;
use zellij_utils::position::{Column, Line}; use zellij_utils::position::{Column, Line};
use zellij_utils::{position::Position, serde}; use zellij_utils::{position::Position, serde};
@ -29,7 +30,6 @@ use crate::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cell::RefCell; use std::cell::RefCell;
use std::os::unix::io::RawFd;
use std::rc::Rc; use std::rc::Rc;
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use std::time::Instant; use std::time::Instant;
@ -50,9 +50,9 @@ use zellij_utils::{
macro_rules! resize_pty { macro_rules! resize_pty {
($pane:expr, $os_input:expr) => { ($pane:expr, $os_input:expr) => {
if let PaneId::Terminal(ref pid) = $pane.pid() { if let PaneId::Terminal(ref pid) = $pane.pid() {
// FIXME: This `set_terminal_size_using_fd` call would be best in // FIXME: This `set_terminal_size_using_terminal_id` call would be best in
// `TerminalPane::reflow_lines` // `TerminalPane::reflow_lines`
$os_input.set_terminal_size_using_fd( $os_input.set_terminal_size_using_terminal_id(
*pid, *pid,
$pane.get_content_columns() as u16, $pane.get_content_columns() as u16,
$pane.get_content_rows() as u16, $pane.get_content_rows() as u16,
@ -89,7 +89,7 @@ pub(crate) struct Tab {
pub style: Style, pub style: Style,
connected_clients: Rc<RefCell<HashSet<ClientId>>>, connected_clients: Rc<RefCell<HashSet<ClientId>>>,
draw_pane_frames: bool, draw_pane_frames: bool,
pending_vte_events: HashMap<RawFd, Vec<VteBytes>>, pending_vte_events: HashMap<u32, Vec<VteBytes>>,
pub selecting_with_mouse: bool, // this is only pub for the tests TODO: remove this once we combine write_text_to_clipboard with render pub selecting_with_mouse: bool, // this is only pub for the tests TODO: remove this once we combine write_text_to_clipboard with render
link_handler: Rc<RefCell<LinkHandler>>, link_handler: Rc<RefCell<LinkHandler>>,
clipboard_provider: ClipboardProvider, clipboard_provider: ClipboardProvider,
@ -100,6 +100,7 @@ pub(crate) struct Tab {
last_mouse_hold_position: Option<Position>, last_mouse_hold_position: Option<Position>,
terminal_emulator_colors: Rc<RefCell<Palette>>, terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>, terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
pids_waiting_resize: HashSet<u32>, // u32 is the terminal_id
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Serialize, Deserialize)]
@ -127,7 +128,9 @@ pub trait Pane {
fn set_geom_override(&mut self, pane_geom: PaneGeom); fn set_geom_override(&mut self, pane_geom: PaneGeom);
fn handle_pty_bytes(&mut self, bytes: VteBytes); fn handle_pty_bytes(&mut self, bytes: VteBytes);
fn cursor_coordinates(&self) -> Option<(usize, usize)>; fn cursor_coordinates(&self) -> Option<(usize, usize)>;
fn adjust_input_to_terminal(&self, input_bytes: Vec<u8>) -> Vec<u8>; fn adjust_input_to_terminal(&mut self, _input_bytes: Vec<u8>) -> Option<AdjustedInput> {
None
}
fn position_and_size(&self) -> PaneGeom; fn position_and_size(&self) -> PaneGeom;
fn current_geom(&self) -> PaneGeom; fn current_geom(&self) -> PaneGeom;
fn geom_override(&self) -> Option<PaneGeom>; fn geom_override(&self) -> Option<PaneGeom>;
@ -351,6 +354,16 @@ pub trait Pane {
// False by default (only terminal-panes support alternate mode) // False by default (only terminal-panes support alternate mode)
false false
} }
fn hold(&mut self, _exit_status: Option<i32>, _run_command: RunCommand) {
// No-op by default, only terminal panes support holding
}
}
#[derive(Clone, Debug)]
pub enum AdjustedInput {
WriteBytesToTerminal(Vec<u8>),
ReRunCommandInThisPane(RunCommand),
CloseThisPane,
} }
impl Tab { impl Tab {
@ -450,13 +463,14 @@ impl Tab {
last_mouse_hold_position: None, last_mouse_hold_position: None,
terminal_emulator_colors, terminal_emulator_colors,
terminal_emulator_color_codes, terminal_emulator_color_codes,
pids_waiting_resize: HashSet::new(),
} }
} }
pub fn apply_layout( pub fn apply_layout(
&mut self, &mut self,
layout: PaneLayout, layout: PaneLayout,
new_pids: Vec<RawFd>, new_ids: Vec<u32>,
tab_index: usize, tab_index: usize,
client_id: ClientId, client_id: ClientId,
) -> Result<()> { ) -> Result<()> {
@ -481,7 +495,7 @@ impl Tab {
let positions_in_layout = layout.position_panes_in_space(&free_space); let positions_in_layout = layout.position_panes_in_space(&free_space);
let positions_and_size = positions_in_layout.iter(); let positions_and_size = positions_in_layout.iter();
let mut new_pids = new_pids.iter(); let mut new_ids = new_ids.iter();
let mut focus_pane_id: Option<PaneId> = None; let mut focus_pane_id: Option<PaneId> = None;
let mut set_focus_pane_id = |layout: &PaneLayout, pane_id: PaneId| { let mut set_focus_pane_id = |layout: &PaneLayout, pane_id: PaneId| {
@ -516,8 +530,12 @@ impl Tab {
set_focus_pane_id(layout, PaneId::Plugin(pid)); set_focus_pane_id(layout, PaneId::Plugin(pid));
} else { } else {
// there are still panes left to fill, use the pids we received in this method // there are still panes left to fill, use the pids we received in this method
let pid = new_pids.next().with_context(err_context)?; // if this crashes it means we got less pids than there are panes in this layout if let Some(pid) = new_ids.next() {
let next_terminal_position = self.get_next_terminal_position(); let next_terminal_position = self.get_next_terminal_position();
let initial_title = match &layout.run {
Some(Run::Command(run_command)) => Some(run_command.to_string()),
_ => None,
};
let mut new_pane = TerminalPane::new( let mut new_pane = TerminalPane::new(
*pid, *pid,
*position_and_size, *position_and_size,
@ -529,6 +547,7 @@ impl Tab {
self.sixel_image_store.clone(), self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(), self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(), self.terminal_emulator_color_codes.clone(),
initial_title,
); );
new_pane.set_borderless(layout.borderless); new_pane.set_borderless(layout.borderless);
self.tiled_panes self.tiled_panes
@ -536,7 +555,8 @@ impl Tab {
set_focus_pane_id(layout, PaneId::Terminal(*pid)); set_focus_pane_id(layout, PaneId::Terminal(*pid));
} }
} }
for unused_pid in new_pids { }
for unused_pid in new_ids {
// this is a bit of a hack and happens because we don't have any central location that // this is a bit of a hack and happens because we don't have any central location that
// can query the screen as to how many panes it needs to create a layout // can query the screen as to how many panes it needs to create a layout
// fixing this will require a bit of an architecture change // fixing this will require a bit of an architecture change
@ -745,18 +765,10 @@ impl Tab {
} }
}, },
None => { None => {
// there aren't any floating panes, we need to open a new one let should_float = true;
//
// ************************************************************************************************
// BEWARE - THIS IS NOT ATOMIC - this sends an instruction to the pty thread to open a new terminal
// the pty thread will do its thing and eventually come back to the new_pane
// method on this tab which will open a new floating pane because we just
// toggled their visibility above us.
// If the pty thread takes too long, weird things can happen...
// ************************************************************************************************
//
let instruction = PtyInstruction::SpawnTerminal( let instruction = PtyInstruction::SpawnTerminal(
default_shell, default_shell,
Some(should_float),
ClientOrTabIndex::ClientId(client_id), ClientOrTabIndex::ClientId(client_id),
); );
self.senders.send_to_pty(instruction).with_context(|| { self.senders.send_to_pty(instruction).with_context(|| {
@ -769,18 +781,18 @@ impl Tab {
self.set_force_render(); self.set_force_render();
Ok(()) Ok(())
} }
pub fn new_pane(
pub fn show_floating_panes(&mut self) { &mut self,
self.floating_panes.toggle_show_panes(true); pid: PaneId,
self.set_force_render(); initial_pane_title: Option<String>,
} should_float: Option<bool>,
client_id: Option<ClientId>,
pub fn hide_floating_panes(&mut self) { ) -> Result<()> {
self.floating_panes.toggle_show_panes(false); match should_float {
self.set_force_render(); Some(true) => self.floating_panes.toggle_show_panes(true),
} Some(false) => self.floating_panes.toggle_show_panes(false),
None => {},
pub fn new_pane(&mut self, pid: PaneId, client_id: Option<ClientId>) -> Result<()> { };
self.close_down_to_max_terminals() self.close_down_to_max_terminals()
.with_context(|| format!("failed to create new pane with id {pid:?}"))?; .with_context(|| format!("failed to create new pane with id {pid:?}"))?;
if self.floating_panes.panes_are_visible() { if self.floating_panes.panes_are_visible() {
@ -798,6 +810,7 @@ impl Tab {
self.sixel_image_store.clone(), self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(), self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(), self.terminal_emulator_color_codes.clone(),
initial_pane_title,
); );
new_pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame new_pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame
resize_pty!(new_pane, self.os_api); resize_pty!(new_pane, self.os_api);
@ -823,6 +836,7 @@ impl Tab {
self.sixel_image_store.clone(), self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(), self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(), self.terminal_emulator_color_codes.clone(),
initial_pane_title,
); );
self.tiled_panes.insert_pane(pid, Box::new(new_terminal)); self.tiled_panes.insert_pane(pid, Box::new(new_terminal));
self.should_clear_display_before_rendering = true; self.should_clear_display_before_rendering = true;
@ -841,7 +855,7 @@ impl Tab {
match pid { match pid {
PaneId::Terminal(pid) => { PaneId::Terminal(pid) => {
let next_terminal_position = self.get_next_terminal_position(); // TODO: this is not accurate in this case let next_terminal_position = self.get_next_terminal_position(); // TODO: this is not accurate in this case
let new_pane = TerminalPane::new( let mut new_pane = TerminalPane::new(
pid, pid,
PaneGeom::default(), // the initial size will be set later PaneGeom::default(), // the initial size will be set later
self.style, self.style,
@ -852,7 +866,11 @@ impl Tab {
self.sixel_image_store.clone(), self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(), self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(), self.terminal_emulator_color_codes.clone(),
None,
); );
new_pane.update_name("EDITING SCROLLBACK"); // we do this here and not in the
// constructor so it won't be overrided
// by the editor
let replaced_pane = if self.floating_panes.panes_are_visible() { let replaced_pane = if self.floating_panes.panes_are_visible() {
self.floating_panes self.floating_panes
.replace_active_pane(Box::new(new_pane), client_id) .replace_active_pane(Box::new(new_pane), client_id)
@ -882,11 +900,14 @@ impl Tab {
} }
Ok(()) Ok(())
} }
pub fn horizontal_split(
pub fn horizontal_split(&mut self, pid: PaneId, client_id: ClientId) -> Result<()> { &mut self,
pid: PaneId,
initial_pane_title: Option<String>,
client_id: ClientId,
) -> Result<()> {
let err_context = let err_context =
|| format!("failed to split pane {pid:?} horizontally for client {client_id}"); || format!("failed to split pane {pid:?} horizontally for client {client_id}");
if self.floating_panes.panes_are_visible() { if self.floating_panes.panes_are_visible() {
return Ok(()); return Ok(());
} }
@ -909,6 +930,7 @@ impl Tab {
self.sixel_image_store.clone(), self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(), self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(), self.terminal_emulator_color_codes.clone(),
initial_pane_title,
); );
self.tiled_panes self.tiled_panes
.split_pane_horizontally(pid, Box::new(new_terminal), client_id); .split_pane_horizontally(pid, Box::new(new_terminal), client_id);
@ -918,11 +940,14 @@ impl Tab {
} }
Ok(()) Ok(())
} }
pub fn vertical_split(
pub fn vertical_split(&mut self, pid: PaneId, client_id: ClientId) -> Result<()> { &mut self,
pid: PaneId,
initial_pane_title: Option<String>,
client_id: ClientId,
) -> Result<()> {
let err_context = let err_context =
|| format!("failed to split pane {pid:?} vertically for client {client_id}"); || format!("failed to split pane {pid:?} vertically for client {client_id}");
if self.floating_panes.panes_are_visible() { if self.floating_panes.panes_are_visible() {
return Ok(()); return Ok(());
} }
@ -945,6 +970,7 @@ impl Tab {
self.sixel_image_store.clone(), self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(), self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(), self.terminal_emulator_color_codes.clone(),
initial_pane_title,
); );
self.tiled_panes self.tiled_panes
.split_pane_vertically(pid, Box::new(new_terminal), client_id); .split_pane_vertically(pid, Box::new(new_terminal), client_id);
@ -990,14 +1016,14 @@ impl Tab {
self.tiled_panes.get_active_pane_id(client_id) self.tiled_panes.get_active_pane_id(client_id)
} }
} }
fn get_active_terminal_id(&self, client_id: ClientId) -> Option<RawFd> { fn get_active_terminal_id(&self, client_id: ClientId) -> Option<u32> {
if let Some(PaneId::Terminal(pid)) = self.get_active_pane_id(client_id) { if let Some(PaneId::Terminal(pid)) = self.get_active_pane_id(client_id) {
Some(pid) Some(pid)
} else { } else {
None None
} }
} }
pub fn has_terminal_pid(&self, pid: RawFd) -> bool { pub fn has_terminal_pid(&self, pid: u32) -> bool {
self.tiled_panes.panes_contain(&PaneId::Terminal(pid)) self.tiled_panes.panes_contain(&PaneId::Terminal(pid))
|| self.floating_panes.panes_contain(&PaneId::Terminal(pid)) || self.floating_panes.panes_contain(&PaneId::Terminal(pid))
|| self || self
@ -1005,9 +1031,8 @@ impl Tab {
.values() .values()
.any(|s_p| s_p.pid() == PaneId::Terminal(pid)) .any(|s_p| s_p.pid() == PaneId::Terminal(pid))
} }
pub fn handle_pty_bytes(&mut self, pid: RawFd, bytes: VteBytes) -> Result<()> { pub fn handle_pty_bytes(&mut self, pid: u32, bytes: VteBytes) -> Result<()> {
let err_context = || format!("failed to handle pty bytes from fd {pid}"); let err_context = || format!("failed to handle pty bytes from fd {pid}");
if let Some(terminal_output) = self if let Some(terminal_output) = self
.tiled_panes .tiled_panes
.get_pane_mut(PaneId::Terminal(pid)) .get_pane_mut(PaneId::Terminal(pid))
@ -1034,8 +1059,7 @@ impl Tab {
} }
self.process_pty_bytes(pid, bytes).with_context(err_context) self.process_pty_bytes(pid, bytes).with_context(err_context)
} }
pub fn process_pending_vte_events(&mut self, pid: u32) -> Result<()> {
pub fn process_pending_vte_events(&mut self, pid: RawFd) -> Result<()> {
if let Some(pending_vte_events) = self.pending_vte_events.get_mut(&pid) { if let Some(pending_vte_events) = self.pending_vte_events.get_mut(&pid) {
let vte_events: Vec<VteBytes> = pending_vte_events.drain(..).collect(); let vte_events: Vec<VteBytes> = pending_vte_events.drain(..).collect();
for vte_event in vte_events { for vte_event in vte_events {
@ -1045,10 +1069,8 @@ impl Tab {
} }
Ok(()) Ok(())
} }
fn process_pty_bytes(&mut self, pid: u32, bytes: VteBytes) -> Result<()> {
fn process_pty_bytes(&mut self, pid: RawFd, bytes: VteBytes) -> Result<()> {
let err_context = || format!("failed to process pty bytes from pid {pid}"); let err_context = || format!("failed to process pty bytes from pid {pid}");
if let Some(terminal_output) = self if let Some(terminal_output) = self
.tiled_panes .tiled_panes
.get_pane_mut(PaneId::Terminal(pid)) .get_pane_mut(PaneId::Terminal(pid))
@ -1059,6 +1081,9 @@ impl Tab {
.find(|s_p| s_p.pid() == PaneId::Terminal(pid)) .find(|s_p| s_p.pid() == PaneId::Terminal(pid))
}) })
{ {
if self.pids_waiting_resize.remove(&pid) {
resize_pty!(terminal_output, self.os_api);
}
terminal_output.handle_pty_bytes(bytes); terminal_output.handle_pty_bytes(bytes);
let messages_to_pty = terminal_output.drain_messages_to_pty(); let messages_to_pty = terminal_output.drain_messages_to_pty();
let clipboard_update = terminal_output.drain_clipboard_update(); let clipboard_update = terminal_output.drain_clipboard_update();
@ -1149,13 +1174,13 @@ impl Tab {
PaneId::Terminal(active_terminal_id) => { PaneId::Terminal(active_terminal_id) => {
let active_terminal = self let active_terminal = self
.floating_panes .floating_panes
.get(&pane_id) .get_mut(&pane_id)
.or_else(|| self.tiled_panes.get_pane(pane_id)) .or_else(|| self.tiled_panes.get_pane_mut(pane_id))
.or_else(|| self.suppressed_panes.get(&pane_id)) .or_else(|| self.suppressed_panes.get_mut(&pane_id))
.ok_or_else(|| anyhow!(format!("failed to find pane with id {pane_id:?}"))) .ok_or_else(|| anyhow!(format!("failed to find pane with id {pane_id:?}")))
.with_context(err_context)?; .with_context(err_context)?;
let adjusted_input = active_terminal.adjust_input_to_terminal(input_bytes); match active_terminal.adjust_input_to_terminal(input_bytes) {
Some(AdjustedInput::WriteBytesToTerminal(adjusted_input)) => {
self.senders self.senders
.send_to_pty_writer(PtyWriteInstruction::Write( .send_to_pty_writer(PtyWriteInstruction::Write(
adjusted_input, adjusted_input,
@ -1163,6 +1188,21 @@ impl Tab {
)) ))
.with_context(err_context)?; .with_context(err_context)?;
}, },
Some(AdjustedInput::ReRunCommandInThisPane(command)) => {
self.pids_waiting_resize.insert(active_terminal_id);
self.senders
.send_to_pty(PtyInstruction::ReRunCommandInPane(
PaneId::Terminal(active_terminal_id),
command,
))
.with_context(err_context)?;
},
Some(AdjustedInput::CloseThisPane) => {
self.close_pane(PaneId::Terminal(active_terminal_id), false);
},
None => {},
}
},
PaneId::Plugin(pid) => { PaneId::Plugin(pid) => {
for key in parse_keys(&input_bytes) { for key in parse_keys(&input_bytes) {
self.senders self.senders
@ -1686,6 +1726,13 @@ impl Tab {
closed_pane closed_pane
} }
} }
pub fn hold_pane(&mut self, id: PaneId, exit_status: Option<i32>, run_command: RunCommand) {
if self.floating_panes.panes_contain(&id) {
self.floating_panes.hold_pane(id, exit_status, run_command);
} else {
self.tiled_panes.hold_pane(id, exit_status, run_command);
}
}
pub fn replace_pane_with_suppressed_pane(&mut self, pane_id: PaneId) -> Option<Box<dyn Pane>> { pub fn replace_pane_with_suppressed_pane(&mut self, pane_id: PaneId) -> Option<Box<dyn Pane>> {
self.suppressed_panes self.suppressed_panes
.remove(&pane_id) .remove(&pane_id)
@ -2335,7 +2382,6 @@ impl Tab {
let err_context = || { let err_context = || {
format!("failed to handle left mouse hold at position {position_on_screen:?} for client {client_id}") format!("failed to handle left mouse hold at position {position_on_screen:?} for client {client_id}")
}; };
// return value indicates whether we should trigger a render // return value indicates whether we should trigger a render
// determine if event is repeated to enable smooth scrolling // determine if event is repeated to enable smooth scrolling
let is_repeated = if let Some(last_position) = self.last_mouse_hold_position { let is_repeated = if let Some(last_position) = self.last_mouse_hold_position {

View file

@ -1,9 +1,9 @@
--- ---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs source: zellij-server/src/tab/./unit/tab_integration_tests.rs
assertion_line: 1474 assertion_line: 1801
expression: snapshot expression: snapshot
--- ---
00 (C): ┌ Pane #1 ────────────────────┌ Pane #3 ─────────────────────────────────────────────────┐─────────┐ 00 (C): ┌ Pane #1 ────────────────────┌ EDITING SCROLLBACK ──────────────────────────────────────┐─────────┐
01 (C): │ │ │ │ 01 (C): │ │ │ │
02 (C): │ │ │ │ 02 (C): │ │ │ │
03 (C): │ │ │ │ 03 (C): │ │ │ │

View file

@ -1,9 +1,9 @@
--- ---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs source: zellij-server/src/tab/./unit/tab_integration_tests.rs
assertion_line: 1444 assertion_line: 1771
expression: snapshot expression: snapshot
--- ---
00 (C): ┌ Pane #2 ─────────────────────────────────────────────────────────────────────────────────────────┐ 00 (C): ┌ EDITING SCROLLBACK ──────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │ 01 (C): │ │
02 (C): │ │ 02 (C): │ │
03 (C): │ │ 03 (C): │ │

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs source: zellij-server/src/tab/./unit/tab_integration_tests.rs
assertion_line: 1288 assertion_line: 1639
expression: snapshot expression: snapshot
--- ---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
@ -8,7 +8,7 @@ expression: snapshot
02 (C): │ │ 02 (C): │ │
03 (C): │ │ 03 (C): │ │
04 (C): │ │ 04 (C): │ │
05 (C): │ ┌ Pane #3 ─────────────────────────────────────────────────┐ │ 05 (C): │ ┌ EDITING SCROLLBACK ──────────────────────────────────────┐ │
06 (C): │ │ │ │ 06 (C): │ │ │ │
07 (C): │ │ │ │ 07 (C): │ │ │ │
08 (C): │ │ │ │ 08 (C): │ │ │ │

View file

@ -1,9 +1,9 @@
--- ---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs source: zellij-server/src/tab/./unit/tab_integration_tests.rs
assertion_line: 1259 assertion_line: 1613
expression: snapshot expression: snapshot
--- ---
00 (C): ┌ Pane #2 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 00 (C): ┌ EDITING SCROLLBACK ───────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │ 01 (C): │ │
02 (C): │ │ 02 (C): │ │
03 (C): │ │ 03 (C): │ │

View file

@ -4,7 +4,7 @@ use crate::screen::CopyOptions;
use crate::Arc; use crate::Arc;
use crate::Mutex; use crate::Mutex;
use crate::{ use crate::{
os_input_output::{AsyncReader, Pid, ServerOsApi}, os_input_output::{AsyncReader, Pid, ServerOsApi, SpawnTerminalError},
panes::PaneId, panes::PaneId,
thread_bus::ThreadSenders, thread_bus::ThreadSenders,
ClientId, ClientId,
@ -30,7 +30,7 @@ use zellij_utils::nix;
use zellij_utils::{ use zellij_utils::{
data::{InputMode, ModeInfo, Palette, Style}, data::{InputMode, ModeInfo, Palette, Style},
input::command::TerminalAction, input::command::{RunCommand, TerminalAction},
interprocess::local_socket::LocalSocketStream, interprocess::local_socket::LocalSocketStream,
ipc::{ClientToServerMsg, ServerToClientMsg}, ipc::{ClientToServerMsg, ServerToClientMsg},
}; };
@ -41,15 +41,15 @@ struct FakeInputOutput {
} }
impl ServerOsApi for FakeInputOutput { impl ServerOsApi for FakeInputOutput {
fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) { fn set_terminal_size_using_terminal_id(&self, _id: u32, _cols: u16, _rows: u16) {
// noop // noop
} }
fn spawn_terminal( fn spawn_terminal(
&self, &self,
_file_to_open: TerminalAction, _file_to_open: TerminalAction,
_quit_cb: Box<dyn Fn(PaneId) + Send>, _quit_db: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>,
_default_editor: Option<PathBuf>, _default_editor: Option<PathBuf>,
) -> Result<(RawFd, RawFd), &'static str> { ) -> Result<(u32, RawFd, RawFd), SpawnTerminalError> {
unimplemented!() unimplemented!()
} }
fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> { fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> {
@ -58,10 +58,10 @@ impl ServerOsApi for FakeInputOutput {
fn async_file_reader(&self, _fd: RawFd) -> Box<dyn AsyncReader> { fn async_file_reader(&self, _fd: RawFd) -> Box<dyn AsyncReader> {
unimplemented!() unimplemented!()
} }
fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result<usize, nix::Error> { fn write_to_tty_stdin(&self, _id: u32, _buf: &[u8]) -> Result<usize, nix::Error> {
unimplemented!() unimplemented!()
} }
fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> { fn tcdrain(&self, _id: u32) -> Result<(), nix::Error> {
unimplemented!() unimplemented!()
} }
fn kill(&self, _pid: Pid) -> Result<(), nix::Error> { fn kill(&self, _pid: Pid) -> Result<(), nix::Error> {
@ -103,6 +103,17 @@ impl ServerOsApi for FakeInputOutput {
}; };
self.file_dumps.lock().unwrap().insert(f, buf); self.file_dumps.lock().unwrap().insert(f, buf);
} }
fn re_run_command_in_terminal(
&self,
_terminal_id: u32,
_run_command: RunCommand,
_quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
) -> Result<(RawFd, RawFd), SpawnTerminalError> {
unimplemented!()
}
fn clear_terminal_id(&self, _terminal_id: u32) {
unimplemented!()
}
} }
struct MockPtyInstructionBus { struct MockPtyInstructionBus {
@ -266,7 +277,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str)
.extract_run_instructions() .extract_run_instructions()
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, _)| i as i32) .map(|(i, _)| i as u32)
.collect(); .collect();
tab.apply_layout(tab_layout, pane_ids, index, client_id) tab.apply_layout(tab_layout, pane_ids, index, client_id)
.unwrap(); .unwrap();
@ -491,7 +502,8 @@ fn dump_screen() {
file_dumps: map.clone(), file_dumps: map.clone(),
}); });
let new_pane_id = PaneId::Terminal(2); let new_pane_id = PaneId::Terminal(2);
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes(2, Vec::from("scratch".as_bytes())) tab.handle_pty_bytes(2, Vec::from("scratch".as_bytes()))
.unwrap(); .unwrap();
let file = "/tmp/log.sh"; let file = "/tmp/log.sh";
@ -514,7 +526,8 @@ fn new_floating_pane() {
let new_pane_id = PaneId::Terminal(2); let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -541,7 +554,8 @@ fn floating_panes_persist_across_toggles() {
let new_pane_id = PaneId::Terminal(2); let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
// here we send bytes to the pane when it's not visible to make sure they're still handled and // here we send bytes to the pane when it's not visible to make sure they're still handled and
// we see them once we toggle the panes back // we see them once we toggle the panes back
@ -572,7 +586,8 @@ fn toggle_floating_panes_off() {
let new_pane_id = PaneId::Terminal(2); let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -600,7 +615,8 @@ fn toggle_floating_panes_on() {
let new_pane_id = PaneId::Terminal(2); let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -633,11 +649,16 @@ fn five_new_floating_panes() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -672,7 +693,8 @@ fn increase_floating_pane_size() {
let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_1 = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -700,7 +722,8 @@ fn decrease_floating_pane_size() {
let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_1 = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -728,7 +751,8 @@ fn resize_floating_pane_left() {
let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_1 = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -756,7 +780,8 @@ fn resize_floating_pane_right() {
let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_1 = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -784,7 +809,8 @@ fn resize_floating_pane_up() {
let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_1 = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -812,7 +838,8 @@ fn resize_floating_pane_down() {
let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_1 = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -844,11 +871,16 @@ fn move_floating_pane_focus_left() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -894,11 +926,16 @@ fn move_floating_pane_focus_right() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -945,11 +982,16 @@ fn move_floating_pane_focus_up() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -995,11 +1037,16 @@ fn move_floating_pane_focus_down() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -1046,11 +1093,16 @@ fn move_floating_pane_focus_with_mouse() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -1099,11 +1151,16 @@ fn move_pane_focus_with_mouse_to_non_floating_pane() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -1152,11 +1209,16 @@ fn drag_pane_with_mouse() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -1205,11 +1267,16 @@ fn mark_text_inside_floating_pane() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -1266,11 +1333,16 @@ fn resize_tab_with_floating_panes() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -1314,11 +1386,16 @@ fn shrink_whole_tab_with_floating_panes_horizontally_and_vertically() {
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -1359,11 +1436,16 @@ fn shrink_whole_tab_with_floating_panes_horizontally_and_vertically_and_expand_b
let new_pane_id_5 = PaneId::Terminal(6); let new_pane_id_5 = PaneId::Terminal(6);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id_1, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_1, None, None, Some(client_id))
tab.new_pane(new_pane_id_2, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_2, None, None, Some(client_id))
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap(); .unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap(); tab.new_pane(new_pane_id_3, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_4, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_5, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -1404,7 +1486,8 @@ fn embed_floating_pane() {
let new_pane_id = PaneId::Terminal(2); let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am scratch terminal".as_bytes()), Vec::from("\n\n\n I am scratch terminal".as_bytes()),
@ -1431,7 +1514,8 @@ fn float_embedded_pane() {
let mut tab = create_new_tab(size, ModeInfo::default()); let mut tab = create_new_tab(size, ModeInfo::default());
let new_pane_id = PaneId::Terminal(2); let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am an embedded pane".as_bytes()), Vec::from("\n\n\n I am an embedded pane".as_bytes()),
@ -1533,7 +1617,8 @@ fn rename_floating_pane() {
let mut tab = create_new_tab(size, ModeInfo::default()); let mut tab = create_new_tab(size, ModeInfo::default());
let new_pane_id = PaneId::Terminal(2); let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.handle_pty_bytes( tab.handle_pty_bytes(
2, 2,
Vec::from("\n\n\n I am a floating pane".as_bytes()), Vec::from("\n\n\n I am a floating pane".as_bytes()),
@ -1618,7 +1703,8 @@ fn move_floating_pane_with_sixel_image() {
let mut output = Output::new(sixel_image_store.clone(), character_cell_size); let mut output = Output::new(sixel_image_store.clone(), character_cell_size);
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
let fixture = read_fixture("sixel-image-500px.six"); let fixture = read_fixture("sixel-image-500px.six");
tab.handle_pty_bytes(2, fixture).unwrap(); tab.handle_pty_bytes(2, fixture).unwrap();
tab.handle_left_click(&Position::new(5, 71), client_id) tab.handle_left_click(&Position::new(5, 71), client_id)
@ -1655,7 +1741,8 @@ fn floating_pane_above_sixel_image() {
let mut output = Output::new(sixel_image_store.clone(), character_cell_size); let mut output = Output::new(sixel_image_store.clone(), character_cell_size);
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
let fixture = read_fixture("sixel-image-500px.six"); let fixture = read_fixture("sixel-image-500px.six");
tab.handle_pty_bytes(1, fixture).unwrap(); tab.handle_pty_bytes(1, fixture).unwrap();
tab.handle_left_click(&Position::new(5, 71), client_id) tab.handle_left_click(&Position::new(5, 71), client_id)
@ -1711,7 +1798,8 @@ fn suppress_floating_pane() {
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.suppress_active_pane(editor_pane_id, client_id).unwrap(); tab.suppress_active_pane(editor_pane_id, client_id).unwrap();
tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes()))
.unwrap(); .unwrap();
@ -1764,7 +1852,8 @@ fn close_suppressing_floating_pane() {
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.suppress_active_pane(editor_pane_id, client_id).unwrap(); tab.suppress_active_pane(editor_pane_id, client_id).unwrap();
tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes()))
.unwrap(); .unwrap();
@ -1821,7 +1910,8 @@ fn suppress_floating_pane_embed_it_and_close_it() {
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.suppress_active_pane(editor_pane_id, client_id).unwrap(); tab.suppress_active_pane(editor_pane_id, client_id).unwrap();
tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes()))
.unwrap(); .unwrap();
@ -1879,7 +1969,8 @@ fn resize_whole_tab_while_floting_pane_is_suppressed() {
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
tab.suppress_active_pane(editor_pane_id, client_id).unwrap(); tab.suppress_active_pane(editor_pane_id, client_id).unwrap();
tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes()))
.unwrap(); .unwrap();
@ -1978,7 +2069,8 @@ fn enter_search_floating_pane() {
let new_pane_id = PaneId::Terminal(2); let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default(); let mut output = Output::default();
tab.toggle_floating_panes(client_id, None).unwrap(); tab.toggle_floating_panes(client_id, None).unwrap();
tab.new_pane(new_pane_id, Some(client_id)).unwrap(); tab.new_pane(new_pane_id, None, None, Some(client_id))
.unwrap();
let pane_content = read_fixture("grid_copy"); let pane_content = read_fixture("grid_copy");
tab.handle_pty_bytes(2, pane_content).unwrap(); tab.handle_pty_bytes(2, pane_content).unwrap();

File diff suppressed because it is too large Load diff

View file

@ -31,6 +31,7 @@ impl From<std::io::Result<usize>> for ReadResult {
pub(crate) struct TerminalBytes { pub(crate) struct TerminalBytes {
pid: RawFd, pid: RawFd,
terminal_id: u32,
senders: ThreadSenders, senders: ThreadSenders,
async_reader: Box<dyn AsyncReader>, async_reader: Box<dyn AsyncReader>,
debug: bool, debug: bool,
@ -47,9 +48,11 @@ impl TerminalBytes {
senders: ThreadSenders, senders: ThreadSenders,
os_input: Box<dyn ServerOsApi>, os_input: Box<dyn ServerOsApi>,
debug: bool, debug: bool,
terminal_id: u32,
) -> Self { ) -> Self {
TerminalBytes { TerminalBytes {
pid, pid,
terminal_id,
senders, senders,
debug, debug,
async_reader: os_input.async_file_reader(pid), async_reader: os_input.async_file_reader(pid),
@ -77,7 +80,6 @@ impl TerminalBytes {
let mut buf = [0u8; 65536]; let mut buf = [0u8; 65536];
loop { loop {
match self.deadline_read(&mut buf).await { match self.deadline_read(&mut buf).await {
// match deadline_read(async_reader.as_mut(), self.render_deadline, &mut buf).await {
ReadResult::Ok(0) | ReadResult::Err(_) => break, // EOF or error ReadResult::Ok(0) | ReadResult::Err(_) => break, // EOF or error
ReadResult::Timeout => { ReadResult::Timeout => {
let time_to_send_render = let time_to_send_render =
@ -93,7 +95,7 @@ impl TerminalBytes {
let _ = debug_to_file(bytes, self.pid); let _ = debug_to_file(bytes, self.pid);
} }
self.async_send_to_screen(ScreenInstruction::PtyBytes( self.async_send_to_screen(ScreenInstruction::PtyBytes(
self.pid, self.terminal_id,
bytes.to_vec(), bytes.to_vec(),
)) ))
.await; .await;

View file

@ -61,6 +61,12 @@ fn background_color(characters: &str, color: Option<PaletteColor>) -> Vec<Termin
colored_string colored_string
} }
#[derive(Debug, Clone, Copy, PartialEq)]
enum ExitStatus {
Code(i32),
Exited,
}
pub struct FrameParams { pub struct FrameParams {
pub focused_client: Option<ClientId>, pub focused_client: Option<ClientId>,
pub is_main_client: bool, pub is_main_client: bool,
@ -81,6 +87,7 @@ pub struct PaneFrame {
pub is_main_client: bool, pub is_main_client: bool,
pub other_cursors_exist_in_session: bool, pub other_cursors_exist_in_session: bool,
pub other_focused_clients: Vec<ClientId>, pub other_focused_clients: Vec<ClientId>,
exit_status: Option<ExitStatus>,
} }
impl PaneFrame { impl PaneFrame {
@ -100,8 +107,15 @@ impl PaneFrame {
is_main_client: frame_params.is_main_client, is_main_client: frame_params.is_main_client,
other_focused_clients: frame_params.other_focused_clients, other_focused_clients: frame_params.other_focused_clients,
other_cursors_exist_in_session: frame_params.other_cursors_exist_in_session, other_cursors_exist_in_session: frame_params.other_cursors_exist_in_session,
exit_status: None,
} }
} }
pub fn add_exit_status(&mut self, exit_status: Option<i32>) {
self.exit_status = match exit_status {
Some(exit_status) => Some(ExitStatus::Code(exit_status)),
None => Some(ExitStatus::Exited),
};
}
fn client_cursor(&self, client_id: ClientId) -> Vec<TerminalCharacter> { fn client_cursor(&self, client_id: ClientId) -> Vec<TerminalCharacter> {
let color = client_id_to_colors(client_id, self.style.colors); let color = client_id_to_colors(client_id, self.style.colors);
background_color(" ", color.map(|c| c.0)) background_color(" ", color.map(|c| c.0))
@ -594,6 +608,65 @@ impl PaneFrame {
.or_else(|| Some(self.title_line_without_middle())) .or_else(|| Some(self.title_line_without_middle()))
.unwrap() .unwrap()
} }
fn render_held_undertitle(&self) -> 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.unwrap(); // unwrap is safe because we only call this if
let (mut first_part, first_part_len) = self.first_held_title_part_full(exit_status);
let mut left_boundary =
foreground_color(self.get_corner(boundary_type::BOTTOM_LEFT), self.color);
let mut right_boundary =
foreground_color(self.get_corner(boundary_type::BOTTOM_RIGHT), self.color);
if self.is_main_client {
let (mut second_part, second_part_len) = self.second_held_title_part_full();
let full_text_len = first_part_len + second_part_len;
if full_text_len <= max_undertitle_length {
// render exit status and tips
let mut padding = String::new();
for _ in full_text_len..max_undertitle_length {
padding.push_str(boundary_type::HORIZONTAL);
}
let mut ret = vec![];
ret.append(&mut left_boundary);
ret.append(&mut first_part);
ret.append(&mut second_part);
ret.append(&mut foreground_color(&padding, self.color));
ret.append(&mut right_boundary);
ret
} else if first_part_len <= max_undertitle_length {
// render only exit status
let mut padding = String::new();
for _ in first_part_len..max_undertitle_length {
padding.push_str(boundary_type::HORIZONTAL);
}
let mut ret = vec![];
ret.append(&mut left_boundary);
ret.append(&mut first_part);
ret.append(&mut foreground_color(&padding, self.color));
ret.append(&mut right_boundary);
ret
} else {
self.empty_undertitle(max_undertitle_length)
}
} else {
if first_part_len <= max_undertitle_length {
// render first part
let full_text_len = first_part_len;
let mut padding = String::new();
for _ in full_text_len..max_undertitle_length {
padding.push_str(boundary_type::HORIZONTAL);
}
let mut ret = vec![];
ret.append(&mut left_boundary);
ret.append(&mut first_part);
ret.append(&mut foreground_color(&padding, self.color));
ret.append(&mut right_boundary);
ret
} else {
self.empty_undertitle(max_undertitle_length)
}
}
}
pub fn render(&self) -> (Vec<CharacterChunk>, Option<String>) { pub fn render(&self) -> (Vec<CharacterChunk>, Option<String>) {
let mut character_chunks = vec![]; let mut character_chunks = vec![];
for row in 0..self.geom.rows { for row in 0..self.geom.rows {
@ -605,6 +678,11 @@ impl PaneFrame {
character_chunks.push(CharacterChunk::new(title, x, y)); character_chunks.push(CharacterChunk::new(title, x, y));
} else if row == self.geom.rows - 1 { } else if row == self.geom.rows - 1 {
// bottom row // bottom row
if self.exit_status.is_some() {
let x = self.geom.x;
let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new(self.render_held_undertitle(), x, y));
} else {
let mut bottom_row = vec![]; let mut bottom_row = vec![];
for col in 0..self.geom.cols { for col in 0..self.geom.cols {
let boundary = if col == 0 { let boundary = if col == 0 {
@ -623,6 +701,7 @@ impl PaneFrame {
let x = self.geom.x; let x = self.geom.x;
let y = self.geom.y + row; let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new(bottom_row, x, y)); character_chunks.push(CharacterChunk::new(bottom_row, x, y));
}
} else { } else {
let boundary_character_left = foreground_color(boundary_type::VERTICAL, self.color); let boundary_character_left = foreground_color(boundary_type::VERTICAL, self.color);
let boundary_character_right = let boundary_character_right =
@ -639,4 +718,106 @@ impl PaneFrame {
} }
(character_chunks, None) (character_chunks, None)
} }
fn first_held_title_part_full(
&self,
exit_status: ExitStatus,
) -> (Vec<TerminalCharacter>, usize) {
// (title part, length)
match exit_status {
ExitStatus::Code(exit_code) => {
let mut first_part = vec![];
let left_bracket = " [ ";
let exited_text = "EXIT CODE: ";
let exit_code_text = format!("{}", exit_code);
let exit_code_color = if exit_code == 0 {
self.style.colors.green
} else {
self.style.colors.red
};
let right_bracket = " ] ";
first_part.append(&mut foreground_color(left_bracket, self.color));
first_part.append(&mut foreground_color(exited_text, self.color));
first_part.append(&mut foreground_color(
&exit_code_text,
Some(exit_code_color),
));
first_part.append(&mut foreground_color(right_bracket, self.color));
(
first_part,
left_bracket.len()
+ exited_text.len()
+ exit_code_text.len()
+ right_bracket.len(),
)
},
ExitStatus::Exited => {
let mut first_part = vec![];
let left_bracket = " [ ";
let exited_text = "EXITED";
let right_bracket = " ] ";
first_part.append(&mut foreground_color(left_bracket, self.color));
first_part.append(&mut foreground_color(
exited_text,
Some(self.style.colors.red),
));
first_part.append(&mut foreground_color(right_bracket, self.color));
(
first_part,
left_bracket.len() + exited_text.len() + right_bracket.len(),
)
},
}
}
fn second_held_title_part_full(&self) -> (Vec<TerminalCharacter>, usize) {
// (title part, length)
let mut second_part = vec![];
let left_enter_bracket = "<";
let enter_text = "ENTER";
let right_enter_bracket = ">";
let enter_tip = " to re-run, ";
let left_break_bracket = "<";
let break_text = "Ctrl-c";
let right_break_bracket = ">";
let break_tip = " to exit ";
second_part.append(&mut foreground_color(left_enter_bracket, self.color));
second_part.append(&mut foreground_color(
enter_text,
Some(self.style.colors.orange),
));
second_part.append(&mut foreground_color(right_enter_bracket, self.color));
second_part.append(&mut foreground_color(enter_tip, self.color));
second_part.append(&mut foreground_color(left_break_bracket, self.color));
second_part.append(&mut foreground_color(
break_text,
Some(self.style.colors.orange),
));
second_part.append(&mut foreground_color(right_break_bracket, self.color));
second_part.append(&mut foreground_color(break_tip, self.color));
(
second_part,
left_enter_bracket.len()
+ enter_text.len()
+ right_enter_bracket.len()
+ enter_tip.len()
+ left_break_bracket.len()
+ break_text.len()
+ right_break_bracket.len()
+ break_tip.len(),
)
}
fn empty_undertitle(&self, max_undertitle_length: usize) -> Vec<TerminalCharacter> {
let mut left_boundary =
foreground_color(self.get_corner(boundary_type::BOTTOM_LEFT), self.color);
let mut right_boundary =
foreground_color(self.get_corner(boundary_type::BOTTOM_RIGHT), self.color);
let mut ret = vec![];
let mut padding = String::new();
for _ in 0..max_undertitle_length {
padding.push_str(boundary_type::HORIZONTAL);
}
ret.append(&mut left_boundary);
ret.append(&mut foreground_color(&padding, self.color));
ret.append(&mut right_boundary);
ret
}
} }

View file

@ -38,6 +38,7 @@ fn get_cwd() {
let server = ServerOsInputOutput { let server = ServerOsInputOutput {
orig_termios: Arc::new(Mutex::new(test_termios)), orig_termios: Arc::new(Mutex::new(test_termios)),
client_senders: Arc::default(), client_senders: Arc::default(),
terminal_id_to_raw_fd: Arc::default(),
}; };
let pid = nix::unistd::getpid(); let pid = nix::unistd::getpid();

View file

@ -2,7 +2,7 @@ use super::{screen_thread_main, CopyOptions, Screen, ScreenInstruction};
use crate::panes::PaneId; use crate::panes::PaneId;
use crate::{ use crate::{
channels::SenderWithContext, channels::SenderWithContext,
os_input_output::{AsyncReader, Pid, ServerOsApi}, os_input_output::{AsyncReader, Pid, ServerOsApi, SpawnTerminalError},
route::route_action, route::route_action,
thread_bus::Bus, thread_bus::Bus,
ClientId, ServerInstruction, SessionMetaData, ThreadSenders, ClientId, ServerInstruction, SessionMetaData, ThreadSenders,
@ -12,7 +12,7 @@ use std::path::PathBuf;
use zellij_utils::cli::CliAction; use zellij_utils::cli::CliAction;
use zellij_utils::errors::ErrorContext; use zellij_utils::errors::ErrorContext;
use zellij_utils::input::actions::{Action, Direction, ResizeDirection}; use zellij_utils::input::actions::{Action, Direction, ResizeDirection};
use zellij_utils::input::command::TerminalAction; use zellij_utils::input::command::{RunCommand, TerminalAction};
use zellij_utils::input::layout::{PaneLayout, SplitDirection}; use zellij_utils::input::layout::{PaneLayout, SplitDirection};
use zellij_utils::input::options::Options; use zellij_utils::input::options::Options;
use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::ipc::IpcReceiverWithContext;
@ -126,15 +126,15 @@ struct FakeInputOutput {
} }
impl ServerOsApi for FakeInputOutput { impl ServerOsApi for FakeInputOutput {
fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) { fn set_terminal_size_using_terminal_id(&self, _terminal_id: u32, _cols: u16, _rows: u16) {
// noop // noop
} }
fn spawn_terminal( fn spawn_terminal(
&self, &self,
_file_to_open: TerminalAction, _file_to_open: TerminalAction,
_quit_db: Box<dyn Fn(PaneId) + Send>, _quit_db: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>,
_default_editor: Option<PathBuf>, _default_editor: Option<PathBuf>,
) -> Result<(RawFd, RawFd), &'static str> { ) -> Result<(u32, RawFd, RawFd), SpawnTerminalError> {
unimplemented!() unimplemented!()
} }
fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> { fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> {
@ -143,10 +143,10 @@ impl ServerOsApi for FakeInputOutput {
fn async_file_reader(&self, _fd: RawFd) -> Box<dyn AsyncReader> { fn async_file_reader(&self, _fd: RawFd) -> Box<dyn AsyncReader> {
unimplemented!() unimplemented!()
} }
fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result<usize, nix::Error> { fn write_to_tty_stdin(&self, _id: u32, _buf: &[u8]) -> Result<usize, nix::Error> {
unimplemented!() unimplemented!()
} }
fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> { fn tcdrain(&self, _id: u32) -> Result<(), nix::Error> {
unimplemented!() unimplemented!()
} }
fn kill(&self, _pid: Pid) -> Result<(), nix::Error> { fn kill(&self, _pid: Pid) -> Result<(), nix::Error> {
@ -195,6 +195,17 @@ impl ServerOsApi for FakeInputOutput {
.insert(filename, contents); .insert(filename, contents);
} }
} }
fn re_run_command_in_terminal(
&self,
_terminal_id: u32,
_run_command: RunCommand,
_quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
) -> Result<(RawFd, RawFd), SpawnTerminalError> {
unimplemented!()
}
fn clear_terminal_id(&self, _terminal_id: u32) {
unimplemented!()
}
} }
fn create_new_screen(size: Size) -> Screen { fn create_new_screen(size: Size) -> Screen {
@ -272,7 +283,7 @@ impl MockScreen {
let pane_count = pane_layout.extract_run_instructions().len(); let pane_count = pane_layout.extract_run_instructions().len();
let mut pane_ids = vec![]; let mut pane_ids = vec![];
for i in 0..pane_count { for i in 0..pane_count {
pane_ids.push(i as i32); pane_ids.push(i as u32);
} }
let _ = self.to_screen.send(ScreenInstruction::NewTab( let _ = self.to_screen.send(ScreenInstruction::NewTab(
pane_layout, pane_layout,
@ -285,7 +296,7 @@ impl MockScreen {
let pane_count = tab_layout.extract_run_instructions().len(); let pane_count = tab_layout.extract_run_instructions().len();
let mut pane_ids = vec![]; let mut pane_ids = vec![];
for i in 0..pane_count { for i in 0..pane_count {
pane_ids.push(i as i32); pane_ids.push(i as u32);
} }
let _ = self.to_screen.send(ScreenInstruction::NewTab( let _ = self.to_screen.send(ScreenInstruction::NewTab(
tab_layout, tab_layout,
@ -412,7 +423,7 @@ macro_rules! log_actions_in_thread {
}; };
} }
fn new_tab(screen: &mut Screen, pid: i32) { fn new_tab(screen: &mut Screen, pid: u32) {
let client_id = 1; let client_id = 1;
screen screen
.new_tab(PaneLayout::default(), vec![pid], client_id) .new_tab(PaneLayout::default(), vec![pid], client_id)
@ -746,7 +757,9 @@ fn switch_to_tab_with_fullscreen() {
new_tab(&mut screen, 1); new_tab(&mut screen, 1);
{ {
let active_tab = screen.get_active_tab_mut(1).unwrap(); let active_tab = screen.get_active_tab_mut(1).unwrap();
active_tab.new_pane(PaneId::Terminal(2), Some(1)).unwrap(); active_tab
.new_pane(PaneId::Terminal(2), None, None, Some(1))
.unwrap();
active_tab.toggle_active_pane_fullscreen(1); active_tab.toggle_active_pane_fullscreen(1);
} }
new_tab(&mut screen, 2); new_tab(&mut screen, 2);
@ -859,7 +872,9 @@ fn attach_after_first_tab_closed() {
new_tab(&mut screen, 1); new_tab(&mut screen, 1);
{ {
let active_tab = screen.get_active_tab_mut(1).unwrap(); let active_tab = screen.get_active_tab_mut(1).unwrap();
active_tab.new_pane(PaneId::Terminal(2), Some(1)).unwrap(); active_tab
.new_pane(PaneId::Terminal(2), None, None, Some(1))
.unwrap();
active_tab.toggle_active_pane_fullscreen(1); active_tab.toggle_active_pane_fullscreen(1);
} }
new_tab(&mut screen, 2); new_tab(&mut screen, 2);
@ -1802,7 +1817,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() {
direction: None, direction: None,
command: None, command: None,
cwd: None, cwd: None,
floating: None, floating: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,
@ -1839,7 +1854,7 @@ pub fn send_cli_new_pane_action_with_split_direction() {
direction: Some(Direction::Right), direction: Some(Direction::Right),
command: None, command: None,
cwd: None, cwd: None,
floating: None, floating: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,
@ -1876,7 +1891,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() {
direction: Some(Direction::Right), direction: Some(Direction::Right),
command: Some("htop".into()), command: Some("htop".into()),
cwd: Some("/some/folder".into()), cwd: Some("/some/folder".into()),
floating: None, floating: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,
@ -1913,7 +1928,7 @@ pub fn send_cli_edit_action_with_default_parameters() {
file: PathBuf::from("/file/to/edit"), file: PathBuf::from("/file/to/edit"),
direction: None, direction: None,
line_number: None, line_number: None,
floating: None, floating: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,
@ -1950,7 +1965,7 @@ pub fn send_cli_edit_action_with_line_number() {
file: PathBuf::from("/file/to/edit"), file: PathBuf::from("/file/to/edit"),
direction: None, direction: None,
line_number: Some(100), line_number: Some(100),
floating: None, floating: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,
@ -1987,7 +2002,7 @@ pub fn send_cli_edit_action_with_split_direction() {
file: PathBuf::from("/file/to/edit"), file: PathBuf::from("/file/to/edit"),
direction: Some(Direction::Down), direction: Some(Direction::Down),
line_number: None, line_number: None,
floating: None, floating: false,
}; };
send_cli_action_to_server( send_cli_action_to_server(
&session_metadata, &session_metadata,

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1618 assertion_line: 1937
expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())"
--- ---
[SpawnTerminal(Some(OpenFile("/file/to/edit", None)), ClientId(10)), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit] [SpawnTerminal(Some(OpenFile("/file/to/edit", None)), Some(false), ClientId(10)), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit]

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1650 assertion_line: 1974
expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())"
--- ---
[SpawnTerminal(Some(OpenFile("/file/to/edit", Some(100))), ClientId(10)), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit] [SpawnTerminal(Some(OpenFile("/file/to/edit", Some(100))), Some(false), ClientId(10)), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit]

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1682 assertion_line: 2011
expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())"
--- ---
[SpawnTerminalHorizontally(Some(OpenFile("/file/to/edit", None)), 10), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit] [SpawnTerminalHorizontally(Some(OpenFile("/file/to/edit", None)), 10), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit]

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1889 assertion_line: 1900
expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())"
--- ---
[SpawnTerminalVertically(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder") })), 10), UpdateActivePane(Some(Terminal(0)), 1), 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 })), 10), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit]

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1566 assertion_line: 1826
expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())"
--- ---
[SpawnTerminal(None, ClientId(10)), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit] [SpawnTerminal(None, Some(false), ClientId(10)), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit]

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1599 assertion_line: 1863
expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())"
--- ---
[SpawnTerminalVertically(None, 10), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit] [SpawnTerminalVertically(None, 10), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit]

View file

@ -394,6 +394,7 @@ fn host_open_file(plugin_env: &PluginEnv) {
.senders .senders
.send_to_pty(PtyInstruction::SpawnTerminal( .send_to_pty(PtyInstruction::SpawnTerminal(
Some(TerminalAction::OpenFile(path, None)), Some(TerminalAction::OpenFile(path, None)),
None,
ClientOrTabIndex::TabIndex(plugin_env.tab_index), ClientOrTabIndex::TabIndex(plugin_env.tab_index),
)) ))
.unwrap(); .unwrap();

View file

@ -124,18 +124,18 @@ pub enum Sessions {
#[clap(visible_alias = "ac")] #[clap(visible_alias = "ac")]
#[clap(subcommand)] #[clap(subcommand)]
Action(CliAction), Action(CliAction),
/// Send actions to a specific session /// Run a command in a new pane
#[clap(visible_alias = "c")] #[clap(visible_alias = "r")]
Command { Run {
command: Option<String>, command: Option<String>,
#[clap(short, long, value_parser, conflicts_with("floating"))] #[clap(short, long, value_parser, conflicts_with("floating"))]
direction: Option<Direction>, direction: Option<Direction>,
#[clap(long, value_parser)] #[clap(long, value_parser)]
cwd: Option<PathBuf>, cwd: Option<PathBuf>,
#[clap(short, long, value_parser, default_missing_value("true"))] #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
floating: Option<bool>, floating: bool,
}, },
/// Edit file with default $EDITOR / $VISUAL in a specific session /// Edit file with default $EDITOR / $VISUAL
#[clap(visible_alias = "e")] #[clap(visible_alias = "e")]
Edit { Edit {
file: PathBuf, file: PathBuf,
@ -143,8 +143,8 @@ pub enum Sessions {
line_number: Option<usize>, line_number: Option<usize>,
#[clap(short, long, value_parser, conflicts_with("floating"))] #[clap(short, long, value_parser, conflicts_with("floating"))]
direction: Option<Direction>, direction: Option<Direction>,
#[clap(short, long, value_parser, default_missing_value("true"))] #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
floating: Option<bool>, floating: bool,
}, },
ConvertConfig { ConvertConfig {
old_config_file: PathBuf, old_config_file: PathBuf,
@ -201,7 +201,7 @@ pub enum CliAction {
TogglePaneFrames, TogglePaneFrames,
/// Toggle between sending text commands to all panes on the current tab and normal mode. /// Toggle between sending text commands to all panes on the current tab and normal mode.
ToggleActiveSyncTab, ToggleActiveSyncTab,
/// Open a new pane in the specified direction [right|left|up|down] /// Open a new pane in the specified direction [right|down]
/// If no direction is specified, will try to use the biggest available space. /// If no direction is specified, will try to use the biggest available space.
NewPane { NewPane {
#[clap(short, long, value_parser, conflicts_with("floating"))] #[clap(short, long, value_parser, conflicts_with("floating"))]
@ -210,8 +210,8 @@ pub enum CliAction {
command: Option<String>, command: Option<String>,
#[clap(long, value_parser)] #[clap(long, value_parser)]
cwd: Option<PathBuf>, cwd: Option<PathBuf>,
#[clap(short, long, value_parser, default_missing_value("true"))] #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
floating: Option<bool>, floating: bool,
}, },
/// Open the specified file in a new zellij pane with your default EDITOR /// Open the specified file in a new zellij pane with your default EDITOR
Edit { Edit {
@ -220,8 +220,8 @@ pub enum CliAction {
direction: Option<Direction>, direction: Option<Direction>,
#[clap(short, long, value_parser)] #[clap(short, long, value_parser)]
line_number: Option<usize>, line_number: Option<usize>,
#[clap(short, long, value_parser, default_missing_value("true"))] #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
floating: Option<bool>, floating: bool,
}, },
/// Switch input mode of all connected clients [locked|pane|tab|resize|move|search|session] /// Switch input mode of all connected clients [locked|pane|tab|resize|move|search|session]
SwitchMode { input_mode: InputMode }, SwitchMode { input_mode: InputMode },

View file

@ -273,6 +273,7 @@ pub enum ScreenContext {
SetFixedHeight, SetFixedHeight,
SetFixedWidth, SetFixedWidth,
ClosePane, ClosePane,
HoldPane,
UpdatePaneName, UpdatePaneName,
UndoRenamePane, UndoRenamePane,
NewTab, NewTab,
@ -326,6 +327,7 @@ pub enum PtyContext {
NewTab, NewTab,
ClosePane, ClosePane,
CloseTab, CloseTab,
ReRunCommandInPane,
Exit, Exit,
} }

View file

@ -200,7 +200,7 @@ pub enum Action {
/// If no direction is specified, will try to use the biggest available space. /// If no direction is specified, will try to use the biggest available space.
NewPane(Option<Direction>), NewPane(Option<Direction>),
/// Open the file in a new pane using the default editor /// Open the file in a new pane using the default editor
EditFile(PathBuf, Option<usize>, Option<Direction>, Option<bool>), // usize is an optional line number, bool is floating true/false EditFile(PathBuf, Option<usize>, Option<Direction>, bool), // usize is an optional line number, bool is floating true/false
/// Open a new floating pane /// Open a new floating pane
NewFloatingPane(Option<RunCommandAction>), NewFloatingPane(Option<RunCommandAction>),
/// Open a new tiled (embedded, non-floating) pane /// Open a new tiled (embedded, non-floating) pane
@ -288,23 +288,29 @@ impl Action {
} => match command { } => match command {
Some(command) => { Some(command) => {
let (command, args) = split_command_and_args(command); let (command, args) = split_command_and_args(command);
let cwd = cwd.or_else(|| std::env::current_dir().ok());
let run_command_action = RunCommandAction { let run_command_action = RunCommandAction {
command, command,
args, args,
cwd, cwd,
direction, direction,
hold_on_close: true,
}; };
match floating { if floating {
Some(true) => Ok(vec![Action::NewFloatingPane(Some(run_command_action))]), Ok(vec![Action::NewFloatingPane(Some(run_command_action))])
_ => Ok(vec![Action::NewTiledPane( } else {
Ok(vec![Action::NewTiledPane(
direction, direction,
Some(run_command_action), Some(run_command_action),
)]), )])
} }
}, },
None => match floating { None => {
Some(true) => Ok(vec![Action::NewFloatingPane(None)]), if floating {
_ => Ok(vec![Action::NewTiledPane(direction, None)]), Ok(vec![Action::NewFloatingPane(None)])
} else {
Ok(vec![Action::NewTiledPane(direction, None)])
}
}, },
}, },
CliAction::Edit { CliAction::Edit {

View file

@ -17,6 +17,24 @@ pub struct RunCommand {
pub args: Vec<String>, pub args: Vec<String>,
#[serde(default)] #[serde(default)]
pub cwd: Option<PathBuf>, pub cwd: Option<PathBuf>,
#[serde(default)]
pub hold_on_close: bool,
}
impl std::fmt::Display for RunCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut command: String = self
.command
.as_path()
.as_os_str()
.to_string_lossy()
.to_string();
for arg in &self.args {
command.push(' ');
command.push_str(arg);
}
write!(f, "{}", command)
}
} }
/// Intermediate representation /// Intermediate representation
@ -30,6 +48,8 @@ pub struct RunCommandAction {
pub cwd: Option<PathBuf>, pub cwd: Option<PathBuf>,
#[serde(default)] #[serde(default)]
pub direction: Option<Direction>, pub direction: Option<Direction>,
#[serde(default)]
pub hold_on_close: bool,
} }
impl From<RunCommandAction> for RunCommand { impl From<RunCommandAction> for RunCommand {
@ -38,6 +58,7 @@ impl From<RunCommandAction> for RunCommand {
command: action.command, command: action.command,
args: action.args, args: action.args,
cwd: action.cwd, cwd: action.cwd,
hold_on_close: action.hold_on_close,
} }
} }
} }

View file

@ -201,6 +201,7 @@ fn layout_with_command_panes() {
children: vec![PaneLayout { children: vec![PaneLayout {
run: Some(Run::Command(RunCommand { run: Some(Run::Command(RunCommand {
command: PathBuf::from("htop"), command: PathBuf::from("htop"),
hold_on_close: true,
..Default::default() ..Default::default()
})), })),
..Default::default() ..Default::default()
@ -226,6 +227,7 @@ fn layout_with_command_panes_and_cwd() {
run: Some(Run::Command(RunCommand { run: Some(Run::Command(RunCommand {
command: PathBuf::from("htop"), command: PathBuf::from("htop"),
cwd: Some(PathBuf::from("/path/to/my/cwd")), cwd: Some(PathBuf::from("/path/to/my/cwd")),
hold_on_close: true,
..Default::default() ..Default::default()
})), })),
..Default::default() ..Default::default()
@ -254,6 +256,7 @@ fn layout_with_command_panes_and_cwd_and_args() {
command: PathBuf::from("htop"), command: PathBuf::from("htop"),
cwd: Some(PathBuf::from("/path/to/my/cwd")), cwd: Some(PathBuf::from("/path/to/my/cwd")),
args: vec![String::from("-h"), String::from("-v")], args: vec![String::from("-h"), String::from("-v")],
hold_on_close: true,
..Default::default() ..Default::default()
})), })),
..Default::default() ..Default::default()

View file

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

View file

@ -177,6 +177,7 @@ impl<'a> KdlLayoutParser<'a> {
command, command,
args: args.unwrap_or_else(|| vec![]), args: args.unwrap_or_else(|| vec![]),
cwd, cwd,
hold_on_close: true,
}))), }))),
_ => Ok(None), _ => Ok(None),
} }

View file

@ -590,12 +590,10 @@ impl TryFrom<(&str, &KdlDocument)> for PaletteColor {
} }
impl TryFrom<&KdlNode> for Action { impl TryFrom<&KdlNode> for Action {
// type Error = Box<dyn std::error::Error>;
type Error = ConfigError; type Error = ConfigError;
fn try_from(kdl_action: &KdlNode) -> Result<Self, Self::Error> { fn try_from(kdl_action: &KdlNode) -> Result<Self, Self::Error> {
let action_name = kdl_name!(kdl_action); let action_name = kdl_name!(kdl_action);
let action_arguments: Vec<&KdlEntry> = kdl_argument_values!(kdl_action); let action_arguments: Vec<&KdlEntry> = kdl_argument_values!(kdl_action);
// let action_arguments: Vec<&KdlValue> = kdl_argument_values!(kdl_action);
let action_children: Vec<&KdlDocument> = kdl_children!(kdl_action); let action_children: Vec<&KdlDocument> = kdl_children!(kdl_action);
match action_name { match action_name {
"Quit" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), "Quit" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
@ -744,6 +742,7 @@ impl TryFrom<&KdlNode> for Action {
args, args,
cwd, cwd,
direction, direction,
hold_on_close: true,
}; };
Ok(Action::Run(run_command_action)) Ok(Action::Run(run_command_action))
}, },
@ -1473,7 +1472,12 @@ impl RunCommand {
.collect(), .collect(),
None => vec![], None => vec![],
}; };
Ok(RunCommand { command, args, cwd }) Ok(RunCommand {
command,
args,
cwd,
hold_on_close: true,
})
} }
} }

View file

@ -62,7 +62,7 @@ pub mod colors {
pub const GREEN: u8 = 154; pub const GREEN: u8 = 154;
pub const GRAY: u8 = 238; pub const GRAY: u8 = 238;
pub const BRIGHT_GRAY: u8 = 245; pub const BRIGHT_GRAY: u8 = 245;
pub const RED: u8 = 88; pub const RED: u8 = 124;
pub const ORANGE: u8 = 166; pub const ORANGE: u8 = 166;
pub const BLACK: u8 = 16; pub const BLACK: u8 = 16;
pub const MAGENTA: u8 = 201; pub const MAGENTA: u8 = 201;