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);
std::process::exit(0);
}
if let Some(Command::Sessions(Sessions::Command {
if let Some(Command::Sessions(Sessions::Run {
command,
direction,
cwd,

View file

@ -13,6 +13,7 @@ use super::remote_runner::{RemoteRunner, RemoteTerminal, Step};
pub const QUIT: [u8; 1] = [17]; // ctrl-q
pub const ESC: [u8; 1] = [27];
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 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);
}
#[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")
.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) {
@ -198,6 +200,7 @@ fn read_from_channel(
sixel_image_store,
Rc::new(RefCell::new(Palette::default())),
Rc::new(RefCell::new(HashMap::new())),
None,
); // 0 is the pane index
loop {
if !should_keep_running.load(Ordering::SeqCst) {
@ -339,6 +342,16 @@ impl RemoteTerminal {
channel.flush().unwrap();
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) {
let mut channel = self.channel.lock().unwrap();
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 crate::panes::PaneId;
@ -39,7 +39,7 @@ pub use nix::unistd::Pid;
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
use libc::ioctl;
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
/// 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 attempts = 3;
let mut signals = signal_hook::iterator::Signals::new(&[SIGINT, SIGTERM]).unwrap();
'handle_exit: loop {
// test whether the child process has exited
match child.try_wait() {
Ok(Some(_status)) => {
Ok(Some(status)) => {
// if the child process has exited, break outside of the loop
// and exit this function
// TODO: handle errors?
break 'handle_exit;
break 'handle_exit status.code();
},
Ok(None) => {
::std::thread::sleep(::std::time::Duration::from_millis(10));
@ -94,50 +95,80 @@ fn handle_command_exit(mut child: Child) {
} else {
// when I say whoa, I mean WHOA!
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(
open_pty_res: OpenptyResult,
cmd: RunCommand,
quit_cb: Box<dyn Fn(PaneId) + Send>,
) -> (RawFd, RawFd) {
quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, // u32 is the exit status
terminal_id: u32,
) -> Result<(RawFd, RawFd), SpawnTerminalError> {
// primary side of pty and child fd
let pid_primary = open_pty_res.master;
let pid_secondary = open_pty_res.slave;
let mut child = unsafe {
let command = &mut Command::new(cmd.command);
if let Some(current_dir) = cmd.cwd {
if current_dir.exists() {
command.current_dir(current_dir);
}
}
command
.args(&cmd.args)
.pre_exec(move || -> std::io::Result<()> {
if libc::login_tty(pid_secondary) != 0 {
panic!("failed to set controlling terminal");
if command_exists(&cmd) {
let mut child = unsafe {
let cmd = cmd.clone();
let command = &mut Command::new(cmd.command);
if let Some(current_dir) = cmd.cwd {
if current_dir.exists() {
command.current_dir(current_dir);
}
close_fds::close_open_fds(3, &[]);
Ok(())
})
.spawn()
.expect("failed to spawn")
};
}
command
.args(&cmd.args)
.pre_exec(move || -> std::io::Result<()> {
if libc::login_tty(pid_secondary) != 0 {
panic!("failed to set controlling terminal");
}
close_fds::close_open_fds(3, &[]);
Ok(())
})
.spawn()
.expect("failed to spawn")
};
let child_id = child.id();
std::thread::spawn(move || {
child.wait().unwrap();
handle_command_exit(child);
let _ = nix::unistd::close(pid_primary);
let _ = nix::unistd::close(pid_secondary);
quit_cb(PaneId::Terminal(pid_primary));
});
let child_id = child.id();
std::thread::spawn(move || {
child.wait().unwrap();
let exit_status = handle_command_exit(child);
let _ = nix::unistd::close(pid_secondary);
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)
@ -147,16 +178,20 @@ fn handle_terminal(
cmd: RunCommand,
failover_cmd: Option<RunCommand>,
orig_termios: termios::Termios,
quit_cb: Box<dyn Fn(PaneId) + Send>,
) -> (RawFd, RawFd) {
// Create a pipe to allow the child the communicate the shell's pid to it's
quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>,
terminal_id: u32,
) -> Result<(RawFd, RawFd), SpawnTerminalError> {
// Create a pipe to allow the child the communicate the shell's pid to its
// parent.
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 {
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 => {
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
/// set.
pub fn spawn_terminal(
fn spawn_terminal(
terminal_action: TerminalAction,
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>,
) -> 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 cmd = match terminal_action {
TerminalAction::OpenFile(file_to_open, line_number) => {
@ -205,9 +243,7 @@ pub fn spawn_terminal(
&& env::var("EDITOR").is_err()
&& env::var("VISUAL").is_err()
{
return Err(
"No Editor found, consider setting a path to one in $EDITOR or $VISUAL",
);
return Err(SpawnTerminalError::NoEditorFound);
}
let mut command = default_editor.unwrap_or_else(|| {
@ -239,6 +275,7 @@ pub fn spawn_terminal(
command,
args,
cwd: None,
hold_on_close: false,
}
},
TerminalAction::RunCommand(command) => command,
@ -251,13 +288,52 @@ pub fn spawn_terminal(
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)]
pub struct ServerOsInputOutput {
orig_termios: Arc<Mutex<termios::Termios>>,
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
@ -291,25 +367,24 @@ impl AsyncReader for RawFdAsyncReader {
/// The `ServerOsApi` trait represents an abstract interface to the features of an operating system that
/// Zellij server requires.
pub trait ServerOsApi: Send + Sync {
/// Sets the size of the terminal associated to file descriptor `fd`.
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);
/// 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
/// the forked child process.
fn spawn_terminal(
&self,
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>,
) -> Result<(RawFd, RawFd), &'static str>;
) -> Result<(u32, RawFd, RawFd), SpawnTerminalError>;
/// Read bytes from the standard output of the virtual terminal referred to by `fd`.
fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
/// Creates an `AsyncReader` that can be used to read from `fd` in an async context
fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader>;
/// 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.
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)
fn kill(&self, pid: Pid) -> Result<(), nix::Error>;
/// 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>;
/// Writes the given buffer to a 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 {
fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16) {
if cols > 0 && rows > 0 {
set_terminal_size_using_fd(fd, cols, rows);
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 {
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(
&self,
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>,
) -> Result<(RawFd, RawFd), &'static str> {
) -> Result<(u32, RawFd, RawFd), SpawnTerminalError> {
let orig_termios = self.orig_termios.lock().unwrap();
spawn_terminal(
terminal_action,
orig_termios.clone(),
quit_cb,
default_editor,
)
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,
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((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> {
unistd::read(fd, buf)
@ -360,11 +486,25 @@ impl ServerOsApi for ServerOsInputOutput {
fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader> {
Box::new(RawFdAsyncReader::new(fd))
}
fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result<usize, nix::Error> {
unistd::write(fd, buf)
fn write_to_tty_stdin(&self, terminal_id: u32, buf: &[u8]) -> Result<usize, nix::Error> {
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, fd: RawFd) -> Result<(), nix::Error> {
termios::tcdrain(fd)
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 box_clone(&self) -> Box<dyn ServerOsApi> {
Box::new((*self).clone())
@ -430,6 +570,37 @@ impl ServerOsApi for ServerOsInputOutput {
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> {
@ -444,6 +615,7 @@ pub fn get_server_os_input() -> Result<ServerOsInputOutput, nix::Error> {
Ok(ServerOsInputOutput {
orig_termios,
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 zellij_utils::{
data::{ModeInfo, Style},
input::command::RunCommand,
pane_size::{Offset, PaneGeom, Size, Viewport},
};
macro_rules! resize_pty {
($pane:expr, $os_input:expr) => {
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`
$os_input.set_terminal_size_using_fd(
$os_input.set_terminal_size_using_terminal_id(
*pid,
$pane.get_content_columns() as u16,
$pane.get_content_rows() as u16,
@ -150,6 +151,16 @@ impl FloatingPanes {
self.desired_pane_positions.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>> {
self.panes.get(pane_id)
}

View file

@ -1504,7 +1504,7 @@ impl Grid {
pub fn mark_for_rerender(&mut self) {
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_below = vec![];
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)> {
None
}
fn adjust_input_to_terminal(&self, _input_bytes: Vec<u8>) -> Vec<u8> {
// noop
vec![]
}
fn position_and_size(&self) -> PaneGeom {
self.geom
}

View file

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

View file

@ -14,6 +14,7 @@ use std::rc::Rc;
use std::time::Instant;
use zellij_utils::{
data::{ModeInfo, Style},
input::command::RunCommand,
input::layout::SplitDirection,
pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport},
};
@ -21,9 +22,9 @@ use zellij_utils::{
macro_rules! resize_pty {
($pane:expr, $os_input:expr) => {
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`
$os_input.set_terminal_size_using_fd(
$os_input.set_terminal_size_using_terminal_id(
*pid,
$pane.get_content_columns() as u16,
$pane.get_content_rows() as u16,
@ -993,6 +994,16 @@ impl TiledPanes {
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 {
self.panes_to_hide.contains(&pane_id)
}

View file

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

View file

@ -47,6 +47,7 @@ pub fn scrolling_inside_a_pane() {
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..30 {
@ -87,6 +88,7 @@ pub fn sixel_image_inside_terminal_pane() {
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
let sixel_image_bytes = "\u{1b}Pq
#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,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
let pane_content = read_fixture("sixel-image-500px.six");
terminal_pane.handle_pty_bytes(pane_content);
@ -161,6 +164,7 @@ pub fn overflowing_sixel_image_inside_terminal_pane() {
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
let pane_content = read_fixture("sixel-image-500px.six");
terminal_pane.handle_pty_bytes(pane_content);
@ -194,6 +198,7 @@ pub fn scrolling_through_a_sixel_image() {
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..30 {
@ -238,6 +243,7 @@ pub fn multiple_sixel_images_in_pane() {
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..5 {
@ -280,6 +286,7 @@ pub fn resizing_pane_with_sixel_images() {
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..5 {
@ -325,6 +332,7 @@ pub fn changing_character_cell_size_with_sixel_images() {
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..5 {
@ -375,6 +383,7 @@ pub fn keep_working_after_corrupted_sixel_image() {
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
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,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
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,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
terminal_pane.set_content_offset(Offset::shift(1, 1));
@ -591,6 +602,7 @@ pub fn frameless_pane_position_is_on_frame() {
sixel_image_store,
terminal_emulator_colors,
terminal_emulator_color_codes,
None,
); // 0 is the pane index
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::{
panes::PaneId, screen::ScreenInstruction, thread_bus::Bus, wasm_vm::PluginInstruction,
panes::PaneId,
screen::ScreenInstruction,
thread_bus::{Bus, ThreadSenders},
wasm_vm::PluginInstruction,
ClientId, ServerInstruction,
};
use async_std::task::{self, JoinHandle};
@ -27,7 +31,8 @@ pub enum ClientOrTabIndex {
/// Instructions related to PTYs (pseudoterminals).
#[derive(Clone, Debug)]
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
SpawnTerminalVertically(Option<TerminalAction>, ClientId),
SpawnTerminalHorizontally(Option<TerminalAction>, ClientId),
@ -41,6 +46,7 @@ pub(crate) enum PtyInstruction {
), // the String is the tab name
ClosePane(PaneId),
CloseTab(Vec<PaneId>),
ReRunCommandInPane(PaneId, RunCommand),
Exit,
}
@ -56,6 +62,7 @@ impl From<&PtyInstruction> for PtyContext {
PtyInstruction::ClosePane(_) => PtyContext::ClosePane,
PtyInstruction::CloseTab(_) => PtyContext::CloseTab,
PtyInstruction::NewTab(..) => PtyContext::NewTab,
PtyInstruction::ReRunCommandInPane(..) => PtyContext::ReRunCommandInPane,
PtyInstruction::Exit => PtyContext::Exit,
}
}
@ -64,9 +71,9 @@ impl From<&PtyInstruction> for PtyContext {
pub(crate) struct Pty {
pub active_panes: HashMap<ClientId, PaneId>,
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,
task_handles: HashMap<RawFd, JoinHandle<()>>,
task_handles: HashMap<u32, JoinHandle<()>>, // terminal_id to join-handle
default_editor: Option<PathBuf>,
}
@ -75,17 +82,53 @@ 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");
err_ctx.add_call(ContextType::Pty((&event).into()));
match event {
PtyInstruction::SpawnTerminal(terminal_action, client_or_tab_index) => {
let pid = pty
.spawn_terminal(terminal_action, client_or_tab_index)
.unwrap(); // TODO: handle error here
pty.bus
.senders
.send_to_screen(ScreenInstruction::NewPane(
PaneId::Terminal(pid),
client_or_tab_index,
))
.unwrap();
PtyInstruction::SpawnTerminal(terminal_action, should_float, client_or_tab_index) => {
let (hold_on_close, run_command, pane_title) = match &terminal_action {
Some(TerminalAction::RunCommand(run_command)) => (
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
.senders
.send_to_screen(ScreenInstruction::NewPane(
PaneId::Terminal(pid),
pane_title,
should_float,
client_or_tab_index,
))
.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) => {
match pty.spawn_terminal(
@ -107,28 +150,124 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) {
}
},
PtyInstruction::SpawnTerminalVertically(terminal_action, client_id) => {
let pid = pty
.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id))
.unwrap(); // TODO: handle error here
pty.bus
.senders
.send_to_screen(ScreenInstruction::VerticalSplit(
PaneId::Terminal(pid),
client_id,
))
.unwrap();
let (hold_on_close, run_command, pane_title) = match &terminal_action {
Some(TerminalAction::RunCommand(run_command)) => (
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
.senders
.send_to_screen(ScreenInstruction::VerticalSplit(
PaneId::Terminal(pid),
pane_title,
client_id,
))
.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) => {
let pid = pty
.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id))
.unwrap(); // TODO: handle error here
pty.bus
.senders
.send_to_screen(ScreenInstruction::HorizontalSplit(
PaneId::Terminal(pid),
client_id,
))
.unwrap();
let (hold_on_close, run_command, pane_title) = match &terminal_action {
Some(TerminalAction::RunCommand(run_command)) => (
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
.senders
.send_to_screen(ScreenInstruction::HorizontalSplit(
PaneId::Terminal(pid),
pane_title,
client_id,
))
.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) => {
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)
.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,
}
}
@ -200,6 +369,7 @@ impl Pty {
args: vec![],
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
hold_on_close: false,
})
}
fn fill_cwd(&self, terminal_action: &mut TerminalAction, client_id: ClientId) {
@ -225,7 +395,8 @@ impl Pty {
&mut self,
terminal_action: Option<TerminalAction>,
client_or_tab_index: ClientOrTabIndex,
) -> Result<RawFd, &'static str> {
) -> Result<u32, SpawnTerminalError> {
// returns the terminal id
let terminal_action = match client_or_tab_index {
ClientOrTabIndex::ClientId(client_id) => {
let mut terminal_action =
@ -237,13 +408,26 @@ impl Pty {
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 senders = self.bus.senders.clone();
move |pane_id| {
let _ = senders.send_to_screen(ScreenInstruction::ClosePane(pane_id, None));
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 (pid_primary, child_fd): (RawFd, RawFd) = self
let (terminal_id, pid_primary, child_fd): (u32, RawFd, RawFd) = self
.bus
.os_input
.as_mut()
@ -254,15 +438,15 @@ impl Pty {
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)
TerminalBytes::new(pid_primary, senders, os_input, debug_to_file, terminal_id)
.listen()
.await;
}
});
self.task_handles.insert(pid_primary, terminal_bytes);
self.id_to_child_pid.insert(pid_primary, child_fd);
Ok(pid_primary)
self.task_handles.insert(terminal_id, terminal_bytes);
self.id_to_child_pid.insert(terminal_id, child_fd);
Ok(terminal_id)
}
pub fn spawn_terminals_for_layout(
&mut self,
@ -273,77 +457,155 @@ impl Pty {
let mut default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal());
self.fill_cwd(&mut default_shell, client_id);
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 {
let quit_cb = Box::new({
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));
}
});
match run_instruction {
Some(Run::Command(command)) => {
let cmd = TerminalAction::RunCommand(command);
let (pid_primary, child_fd): (RawFd, RawFd) = self
.bus
.os_input
.as_mut()
.unwrap()
.spawn_terminal(cmd, quit_cb, self.default_editor.clone())
.unwrap(); // TODO: handle error here
self.id_to_child_pid.insert(pid_primary, child_fd);
new_pane_pids.push(pid_primary);
let hold_on_close = command.hold_on_close;
let quit_cb = Box::new({
let senders = self.bus.senders.clone();
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 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 => {
let (pid_primary, child_fd): (RawFd, RawFd) = self
.bus
.os_input
.as_mut()
.unwrap()
.spawn_terminal(default_shell.clone(), quit_cb, self.default_editor.clone())
.unwrap(); // TODO: handle error here
self.id_to_child_pid.insert(pid_primary, child_fd);
new_pane_pids.push(pid_primary);
match self.bus.os_input.as_mut().unwrap().spawn_terminal(
default_shell.clone(),
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, None, Ok(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.
Some(Run::Plugin(_)) => {},
}
}
let new_tab_pane_ids: Vec<u32> = new_pane_pids
.iter()
.map(|(terminal_id, _, _)| *terminal_id)
.collect::<Vec<u32>>();
self.bus
.senders
.send_to_screen(ScreenInstruction::NewTab(
layout,
new_pane_pids.clone(),
new_tab_pane_ids,
client_id,
))
.unwrap();
for id in new_pane_pids {
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(id, senders, os_input, debug_to_file)
.listen()
.await;
}
});
self.task_handles.insert(id, terminal_bytes);
for (terminal_id, run_command, pid_primary) in new_pane_pids {
match pid_primary {
Ok(pid_primary) => {
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,
terminal_id,
)
.listen()
.await;
}
});
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) {
match id {
PaneId::Terminal(id) => {
let child_fd = self.id_to_child_pid.remove(&id).unwrap();
self.task_handles.remove(&id).unwrap();
task::block_on(async {
self.bus
.os_input
.as_mut()
.unwrap()
.kill(Pid::from_raw(child_fd))
.unwrap();
});
self.task_handles.remove(&id);
if let Some(child_fd) = self.id_to_child_pid.remove(&id) {
task::block_on(async {
self.bus
.os_input
.as_mut()
.unwrap()
.kill(Pid::from_raw(child_fd))
.unwrap();
});
}
self.bus.os_input.as_ref().unwrap().clear_terminal_id(id);
},
PaneId::Plugin(pid) => drop(
self.bus
@ -362,13 +624,84 @@ impl Pty {
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 {
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 {
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)]
pub(crate) enum PtyWriteInstruction {
Write(Vec<u8>, i32),
Write(Vec<u8>, u32),
Exit,
}

View file

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

View file

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

View file

@ -8,6 +8,7 @@ use copy_command::CopyCommand;
use std::env::temp_dir;
use uuid::Uuid;
use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand;
use zellij_utils::position::{Column, Line};
use zellij_utils::{position::Position, serde};
@ -29,7 +30,6 @@ use crate::{
};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::os::unix::io::RawFd;
use std::rc::Rc;
use std::sync::mpsc::channel;
use std::time::Instant;
@ -50,9 +50,9 @@ use zellij_utils::{
macro_rules! resize_pty {
($pane:expr, $os_input:expr) => {
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`
$os_input.set_terminal_size_using_fd(
$os_input.set_terminal_size_using_terminal_id(
*pid,
$pane.get_content_columns() as u16,
$pane.get_content_rows() as u16,
@ -89,7 +89,7 @@ pub(crate) struct Tab {
pub style: Style,
connected_clients: Rc<RefCell<HashSet<ClientId>>>,
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
link_handler: Rc<RefCell<LinkHandler>>,
clipboard_provider: ClipboardProvider,
@ -100,6 +100,7 @@ pub(crate) struct Tab {
last_mouse_hold_position: Option<Position>,
terminal_emulator_colors: Rc<RefCell<Palette>>,
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)]
@ -127,7 +128,9 @@ pub trait Pane {
fn set_geom_override(&mut self, pane_geom: PaneGeom);
fn handle_pty_bytes(&mut self, bytes: VteBytes);
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 current_geom(&self) -> PaneGeom;
fn geom_override(&self) -> Option<PaneGeom>;
@ -351,6 +354,16 @@ pub trait Pane {
// False by default (only terminal-panes support alternate mode)
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 {
@ -450,13 +463,14 @@ impl Tab {
last_mouse_hold_position: None,
terminal_emulator_colors,
terminal_emulator_color_codes,
pids_waiting_resize: HashSet::new(),
}
}
pub fn apply_layout(
&mut self,
layout: PaneLayout,
new_pids: Vec<RawFd>,
new_ids: Vec<u32>,
tab_index: usize,
client_id: ClientId,
) -> Result<()> {
@ -481,7 +495,7 @@ impl Tab {
let positions_in_layout = layout.position_panes_in_space(&free_space);
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 set_focus_pane_id = |layout: &PaneLayout, pane_id: PaneId| {
@ -516,27 +530,33 @@ impl Tab {
set_focus_pane_id(layout, PaneId::Plugin(pid));
} else {
// there are still panes left to fill, use the pids we received in this method
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
let next_terminal_position = self.get_next_terminal_position();
let mut new_pane = TerminalPane::new(
*pid,
*position_and_size,
self.style,
next_terminal_position,
layout.name.clone().unwrap_or_default(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
);
new_pane.set_borderless(layout.borderless);
self.tiled_panes
.add_pane_with_existing_geom(PaneId::Terminal(*pid), Box::new(new_pane));
set_focus_pane_id(layout, PaneId::Terminal(*pid));
if let Some(pid) = new_ids.next() {
let next_terminal_position = self.get_next_terminal_position();
let initial_title = match &layout.run {
Some(Run::Command(run_command)) => Some(run_command.to_string()),
_ => None,
};
let mut new_pane = TerminalPane::new(
*pid,
*position_and_size,
self.style,
next_terminal_position,
layout.name.clone().unwrap_or_default(),
self.link_handler.clone(),
self.character_cell_size.clone(),
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
initial_title,
);
new_pane.set_borderless(layout.borderless);
self.tiled_panes
.add_pane_with_existing_geom(PaneId::Terminal(*pid), Box::new(new_pane));
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
// 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
@ -745,18 +765,10 @@ impl Tab {
}
},
None => {
// there aren't any floating panes, we need to open a new one
//
// ************************************************************************************************
// 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 should_float = true;
let instruction = PtyInstruction::SpawnTerminal(
default_shell,
Some(should_float),
ClientOrTabIndex::ClientId(client_id),
);
self.senders.send_to_pty(instruction).with_context(|| {
@ -769,18 +781,18 @@ impl Tab {
self.set_force_render();
Ok(())
}
pub fn show_floating_panes(&mut self) {
self.floating_panes.toggle_show_panes(true);
self.set_force_render();
}
pub fn hide_floating_panes(&mut self) {
self.floating_panes.toggle_show_panes(false);
self.set_force_render();
}
pub fn new_pane(&mut self, pid: PaneId, client_id: Option<ClientId>) -> Result<()> {
pub fn new_pane(
&mut self,
pid: PaneId,
initial_pane_title: Option<String>,
should_float: Option<bool>,
client_id: Option<ClientId>,
) -> Result<()> {
match should_float {
Some(true) => self.floating_panes.toggle_show_panes(true),
Some(false) => self.floating_panes.toggle_show_panes(false),
None => {},
};
self.close_down_to_max_terminals()
.with_context(|| format!("failed to create new pane with id {pid:?}"))?;
if self.floating_panes.panes_are_visible() {
@ -798,6 +810,7 @@ impl Tab {
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
initial_pane_title,
);
new_pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame
resize_pty!(new_pane, self.os_api);
@ -823,6 +836,7 @@ impl Tab {
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
initial_pane_title,
);
self.tiled_panes.insert_pane(pid, Box::new(new_terminal));
self.should_clear_display_before_rendering = true;
@ -841,7 +855,7 @@ impl Tab {
match pid {
PaneId::Terminal(pid) => {
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,
PaneGeom::default(), // the initial size will be set later
self.style,
@ -852,7 +866,11 @@ impl Tab {
self.sixel_image_store.clone(),
self.terminal_emulator_colors.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() {
self.floating_panes
.replace_active_pane(Box::new(new_pane), client_id)
@ -882,11 +900,14 @@ impl Tab {
}
Ok(())
}
pub fn horizontal_split(&mut self, pid: PaneId, client_id: ClientId) -> Result<()> {
pub fn horizontal_split(
&mut self,
pid: PaneId,
initial_pane_title: Option<String>,
client_id: ClientId,
) -> Result<()> {
let err_context =
|| format!("failed to split pane {pid:?} horizontally for client {client_id}");
if self.floating_panes.panes_are_visible() {
return Ok(());
}
@ -909,6 +930,7 @@ impl Tab {
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
initial_pane_title,
);
self.tiled_panes
.split_pane_horizontally(pid, Box::new(new_terminal), client_id);
@ -918,11 +940,14 @@ impl Tab {
}
Ok(())
}
pub fn vertical_split(&mut self, pid: PaneId, client_id: ClientId) -> Result<()> {
pub fn vertical_split(
&mut self,
pid: PaneId,
initial_pane_title: Option<String>,
client_id: ClientId,
) -> Result<()> {
let err_context =
|| format!("failed to split pane {pid:?} vertically for client {client_id}");
if self.floating_panes.panes_are_visible() {
return Ok(());
}
@ -945,6 +970,7 @@ impl Tab {
self.sixel_image_store.clone(),
self.terminal_emulator_colors.clone(),
self.terminal_emulator_color_codes.clone(),
initial_pane_title,
);
self.tiled_panes
.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)
}
}
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) {
Some(pid)
} else {
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.floating_panes.panes_contain(&PaneId::Terminal(pid))
|| self
@ -1005,9 +1031,8 @@ impl Tab {
.values()
.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}");
if let Some(terminal_output) = self
.tiled_panes
.get_pane_mut(PaneId::Terminal(pid))
@ -1034,8 +1059,7 @@ impl Tab {
}
self.process_pty_bytes(pid, bytes).with_context(err_context)
}
pub fn process_pending_vte_events(&mut self, pid: RawFd) -> Result<()> {
pub fn process_pending_vte_events(&mut self, pid: u32) -> Result<()> {
if let Some(pending_vte_events) = self.pending_vte_events.get_mut(&pid) {
let vte_events: Vec<VteBytes> = pending_vte_events.drain(..).collect();
for vte_event in vte_events {
@ -1045,10 +1069,8 @@ impl Tab {
}
Ok(())
}
fn process_pty_bytes(&mut self, pid: RawFd, bytes: VteBytes) -> Result<()> {
fn process_pty_bytes(&mut self, pid: u32, bytes: VteBytes) -> Result<()> {
let err_context = || format!("failed to process pty bytes from pid {pid}");
if let Some(terminal_output) = self
.tiled_panes
.get_pane_mut(PaneId::Terminal(pid))
@ -1059,6 +1081,9 @@ impl Tab {
.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);
let messages_to_pty = terminal_output.drain_messages_to_pty();
let clipboard_update = terminal_output.drain_clipboard_update();
@ -1149,19 +1174,34 @@ impl Tab {
PaneId::Terminal(active_terminal_id) => {
let active_terminal = self
.floating_panes
.get(&pane_id)
.or_else(|| self.tiled_panes.get_pane(pane_id))
.or_else(|| self.suppressed_panes.get(&pane_id))
.get_mut(&pane_id)
.or_else(|| self.tiled_panes.get_pane_mut(pane_id))
.or_else(|| self.suppressed_panes.get_mut(&pane_id))
.ok_or_else(|| anyhow!(format!("failed to find pane with id {pane_id:?}")))
.with_context(err_context)?;
let adjusted_input = active_terminal.adjust_input_to_terminal(input_bytes);
self.senders
.send_to_pty_writer(PtyWriteInstruction::Write(
adjusted_input,
active_terminal_id,
))
.with_context(err_context)?;
match active_terminal.adjust_input_to_terminal(input_bytes) {
Some(AdjustedInput::WriteBytesToTerminal(adjusted_input)) => {
self.senders
.send_to_pty_writer(PtyWriteInstruction::Write(
adjusted_input,
active_terminal_id,
))
.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) => {
for key in parse_keys(&input_bytes) {
@ -1686,6 +1726,13 @@ impl Tab {
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>> {
self.suppressed_panes
.remove(&pane_id)
@ -2335,7 +2382,6 @@ impl Tab {
let err_context = || {
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
// determine if event is repeated to enable smooth scrolling
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
assertion_line: 1474
assertion_line: 1801
expression: snapshot
---
00 (C): ┌ Pane #1 ────────────────────┌ Pane #3 ─────────────────────────────────────────────────┐─────────┐
00 (C): ┌ Pane #1 ────────────────────┌ EDITING SCROLLBACK ──────────────────────────────────────┐─────────┐
01 (C): │ │ │ │
02 (C): │ │ │ │
03 (C): │ │ │ │

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ use crate::screen::CopyOptions;
use crate::Arc;
use crate::Mutex;
use crate::{
os_input_output::{AsyncReader, Pid, ServerOsApi},
os_input_output::{AsyncReader, Pid, ServerOsApi, SpawnTerminalError},
panes::PaneId,
thread_bus::ThreadSenders,
ClientId,
@ -30,7 +30,7 @@ use zellij_utils::nix;
use zellij_utils::{
data::{InputMode, ModeInfo, Palette, Style},
input::command::TerminalAction,
input::command::{RunCommand, TerminalAction},
interprocess::local_socket::LocalSocketStream,
ipc::{ClientToServerMsg, ServerToClientMsg},
};
@ -41,15 +41,15 @@ struct 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
}
fn spawn_terminal(
&self,
_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>,
) -> Result<(RawFd, RawFd), &'static str> {
) -> Result<(u32, RawFd, RawFd), SpawnTerminalError> {
unimplemented!()
}
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> {
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!()
}
fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> {
fn tcdrain(&self, _id: u32) -> Result<(), nix::Error> {
unimplemented!()
}
fn kill(&self, _pid: Pid) -> Result<(), nix::Error> {
@ -103,6 +103,17 @@ impl ServerOsApi for FakeInputOutput {
};
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 {
@ -266,7 +277,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str)
.extract_run_instructions()
.iter()
.enumerate()
.map(|(i, _)| i as i32)
.map(|(i, _)| i as u32)
.collect();
tab.apply_layout(tab_layout, pane_ids, index, client_id)
.unwrap();
@ -491,7 +502,8 @@ fn dump_screen() {
file_dumps: map.clone(),
});
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()))
.unwrap();
let file = "/tmp/log.sh";
@ -514,7 +526,8 @@ fn new_floating_pane() {
let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default();
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(
2,
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 mut output = Output::default();
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();
// 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
@ -572,7 +586,8 @@ fn toggle_floating_panes_off() {
let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default();
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(
2,
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 mut output = Output::default();
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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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(
2,
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 mut output = Output::default();
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(
2,
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 mut output = Output::default();
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(
2,
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 mut output = Output::default();
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(
2,
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 mut output = Output::default();
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(
2,
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 mut output = Output::default();
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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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_2, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_3, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_4, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_5, Some(client_id)).unwrap();
tab.new_pane(new_pane_id_1, None, None, Some(client_id))
.unwrap();
tab.new_pane(new_pane_id_2, None, None, 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(
2,
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 mut output = Output::default();
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(
2,
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 new_pane_id = PaneId::Terminal(2);
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(
2,
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 new_pane_id = PaneId::Terminal(2);
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(
2,
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);
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");
tab.handle_pty_bytes(2, fixture).unwrap();
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);
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");
tab.handle_pty_bytes(1, fixture).unwrap();
tab.handle_left_click(&Position::new(5, 71), client_id)
@ -1711,7 +1798,8 @@ fn suppress_floating_pane() {
let mut output = Output::default();
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.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes()))
.unwrap();
@ -1764,7 +1852,8 @@ fn close_suppressing_floating_pane() {
let mut output = Output::default();
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.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes()))
.unwrap();
@ -1821,7 +1910,8 @@ fn suppress_floating_pane_embed_it_and_close_it() {
let mut output = Output::default();
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.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes()))
.unwrap();
@ -1879,7 +1969,8 @@ fn resize_whole_tab_while_floting_pane_is_suppressed() {
let mut output = Output::default();
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.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes()))
.unwrap();
@ -1978,7 +2069,8 @@ fn enter_search_floating_pane() {
let new_pane_id = PaneId::Terminal(2);
let mut output = Output::default();
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");
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 {
pid: RawFd,
terminal_id: u32,
senders: ThreadSenders,
async_reader: Box<dyn AsyncReader>,
debug: bool,
@ -47,9 +48,11 @@ impl TerminalBytes {
senders: ThreadSenders,
os_input: Box<dyn ServerOsApi>,
debug: bool,
terminal_id: u32,
) -> Self {
TerminalBytes {
pid,
terminal_id,
senders,
debug,
async_reader: os_input.async_file_reader(pid),
@ -77,7 +80,6 @@ impl TerminalBytes {
let mut buf = [0u8; 65536];
loop {
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::Timeout => {
let time_to_send_render =
@ -93,7 +95,7 @@ impl TerminalBytes {
let _ = debug_to_file(bytes, self.pid);
}
self.async_send_to_screen(ScreenInstruction::PtyBytes(
self.pid,
self.terminal_id,
bytes.to_vec(),
))
.await;

View file

@ -61,6 +61,12 @@ fn background_color(characters: &str, color: Option<PaletteColor>) -> Vec<Termin
colored_string
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ExitStatus {
Code(i32),
Exited,
}
pub struct FrameParams {
pub focused_client: Option<ClientId>,
pub is_main_client: bool,
@ -81,6 +87,7 @@ pub struct PaneFrame {
pub is_main_client: bool,
pub other_cursors_exist_in_session: bool,
pub other_focused_clients: Vec<ClientId>,
exit_status: Option<ExitStatus>,
}
impl PaneFrame {
@ -100,8 +107,15 @@ impl PaneFrame {
is_main_client: frame_params.is_main_client,
other_focused_clients: frame_params.other_focused_clients,
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> {
let color = client_id_to_colors(client_id, self.style.colors);
background_color(" ", color.map(|c| c.0))
@ -594,6 +608,65 @@ impl PaneFrame {
.or_else(|| Some(self.title_line_without_middle()))
.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>) {
let mut character_chunks = vec![];
for row in 0..self.geom.rows {
@ -605,24 +678,30 @@ impl PaneFrame {
character_chunks.push(CharacterChunk::new(title, x, y));
} else if row == self.geom.rows - 1 {
// bottom row
let mut bottom_row = vec![];
for col in 0..self.geom.cols {
let boundary = if col == 0 {
// bottom left corner
self.get_corner(boundary_type::BOTTOM_LEFT)
} else if col == self.geom.cols - 1 {
// bottom right corner
self.get_corner(boundary_type::BOTTOM_RIGHT)
} else {
boundary_type::HORIZONTAL
};
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![];
for col in 0..self.geom.cols {
let boundary = if col == 0 {
// bottom left corner
self.get_corner(boundary_type::BOTTOM_LEFT)
} else if col == self.geom.cols - 1 {
// bottom right corner
self.get_corner(boundary_type::BOTTOM_RIGHT)
} else {
boundary_type::HORIZONTAL
};
let mut boundary_character = foreground_color(boundary, self.color);
bottom_row.append(&mut boundary_character);
let mut boundary_character = foreground_color(boundary, self.color);
bottom_row.append(&mut boundary_character);
}
let x = self.geom.x;
let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new(bottom_row, x, y));
}
let x = self.geom.x;
let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new(bottom_row, x, y));
} else {
let boundary_character_left = foreground_color(boundary_type::VERTICAL, self.color);
let boundary_character_right =
@ -639,4 +718,106 @@ impl PaneFrame {
}
(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 {
orig_termios: Arc::new(Mutex::new(test_termios)),
client_senders: Arc::default(),
terminal_id_to_raw_fd: Arc::default(),
};
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::{
channels::SenderWithContext,
os_input_output::{AsyncReader, Pid, ServerOsApi},
os_input_output::{AsyncReader, Pid, ServerOsApi, SpawnTerminalError},
route::route_action,
thread_bus::Bus,
ClientId, ServerInstruction, SessionMetaData, ThreadSenders,
@ -12,7 +12,7 @@ use std::path::PathBuf;
use zellij_utils::cli::CliAction;
use zellij_utils::errors::ErrorContext;
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::options::Options;
use zellij_utils::ipc::IpcReceiverWithContext;
@ -126,15 +126,15 @@ struct 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
}
fn spawn_terminal(
&self,
_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>,
) -> Result<(RawFd, RawFd), &'static str> {
) -> Result<(u32, RawFd, RawFd), SpawnTerminalError> {
unimplemented!()
}
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> {
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!()
}
fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> {
fn tcdrain(&self, _id: u32) -> Result<(), nix::Error> {
unimplemented!()
}
fn kill(&self, _pid: Pid) -> Result<(), nix::Error> {
@ -195,6 +195,17 @@ impl ServerOsApi for FakeInputOutput {
.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 {
@ -272,7 +283,7 @@ impl MockScreen {
let pane_count = pane_layout.extract_run_instructions().len();
let mut pane_ids = vec![];
for i in 0..pane_count {
pane_ids.push(i as i32);
pane_ids.push(i as u32);
}
let _ = self.to_screen.send(ScreenInstruction::NewTab(
pane_layout,
@ -285,7 +296,7 @@ impl MockScreen {
let pane_count = tab_layout.extract_run_instructions().len();
let mut pane_ids = vec![];
for i in 0..pane_count {
pane_ids.push(i as i32);
pane_ids.push(i as u32);
}
let _ = self.to_screen.send(ScreenInstruction::NewTab(
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;
screen
.new_tab(PaneLayout::default(), vec![pid], client_id)
@ -746,7 +757,9 @@ fn switch_to_tab_with_fullscreen() {
new_tab(&mut screen, 1);
{
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);
}
new_tab(&mut screen, 2);
@ -859,7 +872,9 @@ fn attach_after_first_tab_closed() {
new_tab(&mut screen, 1);
{
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);
}
new_tab(&mut screen, 2);
@ -1802,7 +1817,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() {
direction: None,
command: None,
cwd: None,
floating: None,
floating: false,
};
send_cli_action_to_server(
&session_metadata,
@ -1839,7 +1854,7 @@ pub fn send_cli_new_pane_action_with_split_direction() {
direction: Some(Direction::Right),
command: None,
cwd: None,
floating: None,
floating: false,
};
send_cli_action_to_server(
&session_metadata,
@ -1876,7 +1891,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() {
direction: Some(Direction::Right),
command: Some("htop".into()),
cwd: Some("/some/folder".into()),
floating: None,
floating: false,
};
send_cli_action_to_server(
&session_metadata,
@ -1913,7 +1928,7 @@ pub fn send_cli_edit_action_with_default_parameters() {
file: PathBuf::from("/file/to/edit"),
direction: None,
line_number: None,
floating: None,
floating: false,
};
send_cli_action_to_server(
&session_metadata,
@ -1950,7 +1965,7 @@ pub fn send_cli_edit_action_with_line_number() {
file: PathBuf::from("/file/to/edit"),
direction: None,
line_number: Some(100),
floating: None,
floating: false,
};
send_cli_action_to_server(
&session_metadata,
@ -1987,7 +2002,7 @@ pub fn send_cli_edit_action_with_split_direction() {
file: PathBuf::from("/file/to/edit"),
direction: Some(Direction::Down),
line_number: None,
floating: None,
floating: false,
};
send_cli_action_to_server(
&session_metadata,

View file

@ -1,6 +1,6 @@
---
source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1618
assertion_line: 1937
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
assertion_line: 1650
assertion_line: 1974
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
assertion_line: 1682
assertion_line: 2011
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]

View file

@ -1,6 +1,6 @@
---
source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1889
assertion_line: 1900
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
assertion_line: 1566
assertion_line: 1826
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
assertion_line: 1599
assertion_line: 1863
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
.send_to_pty(PtyInstruction::SpawnTerminal(
Some(TerminalAction::OpenFile(path, None)),
None,
ClientOrTabIndex::TabIndex(plugin_env.tab_index),
))
.unwrap();

View file

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

View file

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

View file

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

View file

@ -17,6 +17,24 @@ pub struct RunCommand {
pub args: Vec<String>,
#[serde(default)]
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
@ -30,6 +48,8 @@ pub struct RunCommandAction {
pub cwd: Option<PathBuf>,
#[serde(default)]
pub direction: Option<Direction>,
#[serde(default)]
pub hold_on_close: bool,
}
impl From<RunCommandAction> for RunCommand {
@ -38,6 +58,7 @@ impl From<RunCommandAction> for RunCommand {
command: action.command,
args: action.args,
cwd: action.cwd,
hold_on_close: action.hold_on_close,
}
}
}

View file

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

View file

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

View file

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

View file

@ -590,12 +590,10 @@ impl TryFrom<(&str, &KdlDocument)> for PaletteColor {
}
impl TryFrom<&KdlNode> for Action {
// type Error = Box<dyn std::error::Error>;
type Error = ConfigError;
fn try_from(kdl_action: &KdlNode) -> Result<Self, Self::Error> {
let action_name = kdl_name!(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);
match action_name {
"Quit" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
@ -744,6 +742,7 @@ impl TryFrom<&KdlNode> for Action {
args,
cwd,
direction,
hold_on_close: true,
};
Ok(Action::Run(run_command_action))
},
@ -1473,7 +1472,12 @@ impl RunCommand {
.collect(),
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 GRAY: u8 = 238;
pub const BRIGHT_GRAY: u8 = 245;
pub const RED: u8 = 88;
pub const RED: u8 = 124;
pub const ORANGE: u8 = 166;
pub const BLACK: u8 = 16;
pub const MAGENTA: u8 = 201;