diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fd34b4f9..9e45c461 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,7 +3,7 @@ name: "\U0001F41B Bug Report" about: "If something isn't working as expected." labels: bug --- -Thank you for taking the time to file this issue! Please follow the instructions and fill the missing parts below the instructions, if it is meaningful. Try to be brief and concise. +Thank you for taking the time to file this issue! Please follow the instructions and fill in the missing parts below the instructions, if it is meaningful. Try to be brief and concise. **In Case of Graphical or Performance Issues** @@ -26,4 +26,4 @@ List of programs you interact with as, `PROGRAM --version`: output cropped meani `alacritty --version`: alacritty 0.7.2 (5ac8060b) **Further information** -Reproduction steps, noticable behavior, related issues etc +Reproduction steps, noticeable behavior, related issues, etc diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e91ca63..ecd9d79c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * Fix: pasted text performs much faster and doesn't kill Termion (https://github.com/zellij-org/zellij/pull/810) * Fix: resizing/scrolling through heavily wrapped panes no longer hangs (https://github.com/zellij-org/zellij/pull/814) * Terminal compatibility: properly handle HOME/END keys in eg. vim/zsh (https://github.com/zellij-org/zellij/pull/815) +* Fix: Typo (https://github.com/zellij-org/zellij/pull/821) +* Fix: Update `cargo-make` instructions post `v0.35.3` (https://github.com/zellij-org/zellij/pull/819) +* Fix: Unused import for darwin systems (https://github.com/zellij-org/zellij/pull/820) +* Add: `WriteChars` action (https://github.com/zellij-org/zellij/pull/825) +* Fix: typo and grammar (https://github.com/zellij-org/zellij/pull/826) +* Add: `rust-version' - msrv field to `Cargo.toml` (https://github.com/zellij-org/zellij/pull/828) +* Fix: improve memory utilization, reap both sides of pty properly and do not expose open FDs to child processes (https://github.com/zellij-org/zellij/pull/830) +* Fix: move from the deprecated `colors_transform` to `colorsys` (https://github.com/zellij-org/zellij/pull/832) +* Feature: plugins can now detect right mouse clicks (https://github.com/zellij-org/zellij/pull/801) +* Fix: open pane in cwd even when explicitly specifying shell (https://github.com/zellij-org/zellij/pull/834) ## [0.19.0] - 2021-10-20 * Fix: Prevent text overwrite when scrolled up (https://github.com/zellij-org/zellij/pull/655) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a7039c5..11c6d1f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ cargo make build cargo make test # Run Zellij (optionally with additional arguments) cargo make run -cargo make run -- -l strider +cargo make run -l strider # Run Clippy (potentially with additional options) cargo make clippy cargo make clippy -W clippy::pedantic diff --git a/Cargo.lock b/Cargo.lock index 39d449fe..a3ffff35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,6 +351,16 @@ dependencies = [ "vec_map", ] +[[package]] +name = "close_fds" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc416f33de9d59e79e57560f450d21ff8393adcf1cdfc3e6d8fb93d5f88a2ed" +dependencies = [ + "cfg-if 1.0.0", + "libc", +] + [[package]] name = "colored" version = "2.0.0" @@ -363,10 +373,10 @@ dependencies = [ ] [[package]] -name = "colors-transform" -version = "0.2.11" +name = "colorsys" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9226dbc05df4fb986f48d730b001532580883c4c06c5d1c213f4b34c1c157178" +checksum = "4dfdf9179d546b55ff3f88c9d93ecfaa3e9760163da5a1080af5243230dbbb70" [[package]] name = "concurrent-queue" @@ -2824,6 +2834,7 @@ dependencies = [ name = "zellij" version = "0.20.0" dependencies = [ + "anyhow", "insta", "log", "names", @@ -2857,6 +2868,7 @@ dependencies = [ "byteorder", "cassowary", "chrono", + "close_fds", "daemonize", "darwin-libproc", "highway", @@ -2895,7 +2907,7 @@ dependencies = [ "async-std", "backtrace", "bincode", - "colors-transform", + "colorsys", "crossbeam", "directories-next", "interprocess", diff --git a/Cargo.toml b/Cargo.toml index f205b5f0..1c33bae7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,12 @@ repository = "https://github.com/zellij-org/zellij" homepage = "https://zellij.dev" include = ["src/**/*", "assets/plugins/*", "assets/layouts/*", "assets/config/*", "LICENSE.md", "README.md", "!**/*_test.*", "!**/tests/**/*"] resolver = "2" +rust-version = "1.56" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0" names = "0.11.0" zellij-client = { path = "zellij-client/", version = "0.20.0" } zellij-server = { path = "zellij-server/", version = "0.20.0" } diff --git a/src/main.rs b/src/main.rs index 0cacc375..f12eb010 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,9 @@ mod tests; use crate::install::populate_data_dir; use sessions::{ - assert_session, assert_session_ne, get_active_session, get_sessions, kill_session, - list_sessions, print_sessions, session_exists, ActiveSession, + assert_session, assert_session_ne, get_active_session, get_sessions, + get_sessions_sorted_by_creation_date, kill_session, list_sessions, print_sessions, + print_sessions_with_index, session_exists, ActiveSession, }; use std::process; use zellij_client::{os_input_output::get_client_os_input, start_client, ClientInfo}; @@ -62,7 +63,7 @@ pub fn main() { } } Err(e) => { - eprintln!("Error occured: {:?}", e); + eprintln!("Error occurred: {:?}", e); process::exit(1); } } @@ -111,6 +112,7 @@ pub fn main() { if let Some(Command::Sessions(Sessions::Attach { session_name, create, + index, options, })) = opts.command.clone() { @@ -119,47 +121,94 @@ pub fn main() { None => config_options, }; - let (client, attach_layout) = match session_name.as_ref() { - Some(session) => { - if create { - if !session_exists(session).unwrap() { - (ClientInfo::New(session_name.unwrap()), layout) + let (client, attach_layout) = if let Some(idx) = index { + // Ignore session_name when `--index` is provided + match get_sessions_sorted_by_creation_date() { + Ok(sessions) => { + if sessions.is_empty() { + if create { + ( + ClientInfo::New(names::Generator::default().next().unwrap()), + layout, + ) + } else { + println!("No active zellij sessions found."); + process::exit(1); + } } else { + match sessions.get(idx) { + Some(session) => ( + ClientInfo::Attach(session.clone(), config_options.clone()), + None, + ), + None => { + if create { + ( + ClientInfo::New( + names::Generator::default().next().unwrap(), + ), + layout, + ) + } else { + println!("No session indexed by {} found. The following sessions are active:", idx); + print_sessions_with_index(sessions); + process::exit(1); + } + } + } + } + } + Err(e) => { + eprintln!("Error occurred: {:?}", e); + process::exit(1); + } + } + } else { + match session_name.as_ref() { + Some(session) => { + if create { + if !session_exists(session).unwrap() { + (ClientInfo::New(session_name.unwrap()), layout) + } else { + ( + ClientInfo::Attach( + session_name.unwrap(), + config_options.clone(), + ), + None, + ) + } + } else { + assert_session(session); ( ClientInfo::Attach(session_name.unwrap(), config_options.clone()), None, ) } - } else { - assert_session(session); - ( - ClientInfo::Attach(session_name.unwrap(), config_options.clone()), - None, - ) } - } - None => match get_active_session() { - ActiveSession::None => { - if create { - ( - ClientInfo::New(names::Generator::default().next().unwrap()), - layout, - ) - } else { - println!("No active zellij sessions found."); + None => match get_active_session() { + ActiveSession::None => { + if create { + ( + ClientInfo::New(names::Generator::default().next().unwrap()), + layout, + ) + } else { + println!("No active zellij sessions found."); + process::exit(1); + } + } + ActiveSession::One(session_name) => ( + ClientInfo::Attach(session_name, config_options.clone()), + None, + ), + ActiveSession::Many => { + println!("Please specify the session name to attach to. The following sessions are active:"); + print_sessions(get_sessions().unwrap()); process::exit(1); } - } - ActiveSession::One(session_name) => ( - ClientInfo::Attach(session_name, config_options.clone()), - None, - ), - ActiveSession::Many => { - println!("Please specify the session name to attach to. The following sessions are active:"); - print_sessions(get_sessions().unwrap()); - process::exit(1); - } - }, + }, + } }; start_client( diff --git a/src/sessions.rs b/src/sessions.rs index c2d8afea..bf44d088 100644 --- a/src/sessions.rs +++ b/src/sessions.rs @@ -1,4 +1,5 @@ use std::os::unix::fs::FileTypeExt; +use std::time::SystemTime; use std::{fs, io, process}; use zellij_utils::{ consts::ZELLIJ_SOCK_DIR, @@ -29,6 +30,36 @@ pub(crate) fn get_sessions() -> Result, io::ErrorKind> { } } +pub(crate) fn get_sessions_sorted_by_creation_date() -> anyhow::Result> { + match fs::read_dir(&*ZELLIJ_SOCK_DIR) { + Ok(files) => { + let mut sessions_with_creation_date: Vec<(String, SystemTime)> = Vec::new(); + for file in files { + let file = file?; + let file_name = file.file_name().into_string().unwrap(); + let file_created_at = file.metadata()?.created()?; + if file.file_type()?.is_socket() && assert_socket(&file_name) { + sessions_with_creation_date.push((file_name, file_created_at)); + } + } + sessions_with_creation_date.sort_by_key(|x| x.1); // the oldest one will be the first + + let sessions = sessions_with_creation_date + .iter() + .map(|x| x.0.clone()) + .collect(); + Ok(sessions) + } + Err(err) => { + if let io::ErrorKind::NotFound = err.kind() { + Ok(Vec::with_capacity(0)) + } else { + Err(err.into()) + } + } + } +} + fn assert_socket(name: &str) -> bool { let path = &*ZELLIJ_SOCK_DIR.join(name); match LocalSocketStream::connect(path) { @@ -59,6 +90,18 @@ pub(crate) fn print_sessions(sessions: Vec) { }) } +pub(crate) fn print_sessions_with_index(sessions: Vec) { + let curr_session = std::env::var("ZELLIJ_SESSION_NAME").unwrap_or_else(|_| "".into()); + for (i, session) in sessions.iter().enumerate() { + let suffix = if curr_session == *session { + " (current)" + } else { + "" + }; + println!("{}: {}{}", i, session, suffix); + } +} + pub(crate) enum ActiveSession { None, One(String), @@ -78,7 +121,7 @@ pub(crate) fn get_active_session() -> ActiveSession { } } Err(e) => { - eprintln!("Error occured: {:?}", e); + eprintln!("Error occurred: {:?}", e); process::exit(1); } } @@ -91,7 +134,7 @@ pub(crate) fn kill_session(name: &str) { IpcSenderWithContext::new(stream).send(ClientToServerMsg::KillSession); } Err(e) => { - eprintln!("Error occured: {:?}", e); + eprintln!("Error occurred: {:?}", e); process::exit(1); } }; @@ -108,7 +151,7 @@ pub(crate) fn list_sessions() { 0 } Err(e) => { - eprintln!("Error occured: {:?}", e); + eprintln!("Error occurred: {:?}", e); 1 } }; @@ -137,7 +180,7 @@ pub(crate) fn assert_session(name: &str) { } } Err(e) => { - eprintln!("Error occured: {:?}", e); + eprintln!("Error occurred: {:?}", e); } }; process::exit(1); @@ -151,7 +194,7 @@ pub(crate) fn assert_session_ne(name: &str) { } println!("Session with name {:?} aleady exists. Use attach command to connect to it or specify a different name.", name); } - Err(e) => eprintln!("Error occured: {:?}", e), + Err(e) => eprintln!("Error occurred: {:?}", e), }; process::exit(1); } diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 2b89741e..d07c2caa 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -135,6 +135,9 @@ impl InputHandler { MouseButton::Left => { self.dispatch_action(Action::LeftClick(point)); } + MouseButton::Right => { + self.dispatch_action(Action::RightClick(point)); + } _ => {} }, MouseEvent::Release(point) => { diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml index 9d7c075e..ea72f2d0 100644 --- a/zellij-server/Cargo.toml +++ b/zellij-server/Cargo.toml @@ -25,6 +25,7 @@ zellij-utils = { path = "../zellij-utils/", version = "0.20.0" } log = "0.4.14" typetag = "0.1.7" chrono = "0.4.19" +close_fds = "0.3.2" [target.'cfg(target_os = "macos")'.dependencies] darwin-libproc = "0.2.0" diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index 09c5e9d7..f8fe751a 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -1,10 +1,14 @@ use std::collections::HashMap; +use crate::panes::PaneId; + #[cfg(target_os = "macos")] use darwin_libproc; -use std::env; +#[cfg(target_os = "linux")] use std::fs; + +use std::env; use std::os::unix::io::RawFd; use std::os::unix::process::CommandExt; use std::path::PathBuf; @@ -16,11 +20,12 @@ use zellij_utils::{async_std, interprocess, libc, nix, signal_hook, zellij_tile} use async_std::fs::File as AsyncFile; use async_std::os::unix::io::FromRawFd; use interprocess::local_socket::LocalSocketStream; -use nix::pty::{forkpty, ForkptyResult, Winsize}; + +use nix::pty::{openpty, OpenptyResult, Winsize}; use nix::sys::signal::{kill, Signal}; use nix::sys::termios; -use nix::sys::wait::waitpid; -use nix::unistd::{self, ForkResult}; + +use nix::unistd; use signal_hook::consts::*; use zellij_tile::data::Palette; use zellij_utils::{ @@ -31,7 +36,6 @@ use zellij_utils::{ use async_std::io::ReadExt; pub use async_trait::async_trait; -use byteorder::{BigEndian, ByteOrder}; pub use nix::unistd::Pid; @@ -97,96 +101,63 @@ fn handle_command_exit(mut child: Child) { } } -fn handle_fork_pty( - fork_pty_res: ForkptyResult, +fn handle_openpty( + open_pty_res: OpenptyResult, cmd: RunCommand, - parent_fd: RawFd, - child_fd: RawFd, -) -> (RawFd, ChildId) { - let pid_primary = fork_pty_res.master; - let (pid_secondary, pid_shell) = match fork_pty_res.fork_result { - ForkResult::Parent { child } => { - let pid_shell = read_from_pipe(parent_fd, child_fd); - (child, pid_shell) + quit_cb: Box, +) -> (RawFd, RawFd) { + // 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 { + command.current_dir(current_dir); } - ForkResult::Child => { - let child = unsafe { - let command = &mut Command::new(cmd.command); - if let Some(current_dir) = cmd.cwd { - 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"); } - command - .args(&cmd.args) - .pre_exec(|| -> std::io::Result<()> { - // this is the "unsafe" part, for more details please see: - // https://doc.rust-lang.org/std/os/unix/process/trait.CommandExt.html#notes-and-safety - unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0)) - .expect("failed to create a new process group"); - Ok(()) - }) - .spawn() - .expect("failed to spawn") - }; - unistd::tcsetpgrp(0, Pid::from_raw(child.id() as i32)) - .expect("faled to set child's forceground process group"); - write_to_pipe(child.id(), parent_fd, child_fd); - handle_command_exit(child); - ::std::process::exit(0); - } + close_fds::close_open_fds(3, &[]); + Ok(()) + }) + .spawn() + .expect("failed to spawn") }; - ( - pid_primary, - ChildId { - primary: pid_secondary, - shell: pid_shell.map(|pid| Pid::from_raw(pid as i32)), - }, - ) + 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)); + }); + + (pid_primary, child_id as RawFd) } /// Spawns a new terminal from the parent terminal with [`termios`](termios::Termios) /// `orig_termios`. /// -fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, ChildId) { +fn handle_terminal( + cmd: RunCommand, + orig_termios: termios::Termios, + quit_cb: Box, +) -> (RawFd, RawFd) { // Create a pipe to allow the child the communicate the shell's pid to it's // parent. - let (parent_fd, child_fd) = unistd::pipe().expect("failed to create pipe"); - match forkpty(None, Some(&orig_termios)) { - Ok(fork_pty_res) => handle_fork_pty(fork_pty_res, cmd, parent_fd, child_fd), + match openpty(None, Some(&orig_termios)) { + Ok(open_pty_res) => handle_openpty(open_pty_res, cmd, quit_cb), Err(e) => { - panic!("failed to fork {:?}", e); + panic!("failed to start pty{:?}", e); } } } -/// Write to a pipe given both file descriptors -fn write_to_pipe(data: u32, parent_fd: RawFd, child_fd: RawFd) { - let mut buff = [0; 4]; - BigEndian::write_u32(&mut buff, data); - if unistd::close(parent_fd).is_err() { - return; - } - if unistd::write(child_fd, &buff).is_err() { - return; - } - unistd::close(child_fd).unwrap_or_default(); -} - -/// Read from a pipe given both file descriptors -fn read_from_pipe(parent_fd: RawFd, child_fd: RawFd) -> Option { - let mut buffer = [0; 4]; - if unistd::close(child_fd).is_err() { - return None; - } - if unistd::read(parent_fd, &mut buffer).is_err() { - return None; - } - if unistd::close(parent_fd).is_err() { - return None; - } - Some(u32::from_be_bytes(buffer)) -} - /// If a [`TerminalAction::OpenFile(file)`] is given, the text editor specified by environment variable `EDITOR` /// (or `VISUAL`, if `EDITOR` is not set) will be started in the new terminal, with the given /// file open. @@ -202,7 +173,8 @@ fn read_from_pipe(parent_fd: RawFd, child_fd: RawFd) -> Option { pub fn spawn_terminal( terminal_action: TerminalAction, orig_termios: termios::Termios, -) -> (RawFd, ChildId) { + quit_cb: Box, +) -> (RawFd, RawFd) { let cmd = match terminal_action { TerminalAction::OpenFile(file_to_open) => { if env::var("EDITOR").is_err() && env::var("VISUAL").is_err() { @@ -224,7 +196,7 @@ pub fn spawn_terminal( TerminalAction::RunCommand(command) => command, }; - handle_terminal(cmd, orig_termios) + handle_terminal(cmd, orig_termios, quit_cb) } #[derive(Clone)] @@ -269,7 +241,11 @@ pub trait ServerOsApi: Send + Sync { /// Spawn a new terminal, with a terminal action. The returned tuple contains the master file /// descriptor of the forked psuedo terminal and a [ChildId] struct containing process id's for /// the forked child process. - fn spawn_terminal(&self, terminal_action: TerminalAction) -> (RawFd, ChildId); + fn spawn_terminal( + &self, + terminal_action: TerminalAction, + quit_cb: Box, + ) -> (RawFd, RawFd); /// 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; /// Creates an `AsyncReader` that can be used to read from `fd` in an async context @@ -302,9 +278,13 @@ impl ServerOsApi for ServerOsInputOutput { set_terminal_size_using_fd(fd, cols, rows); } } - fn spawn_terminal(&self, terminal_action: TerminalAction) -> (RawFd, ChildId) { + fn spawn_terminal( + &self, + terminal_action: TerminalAction, + quit_cb: Box, + ) -> (RawFd, RawFd) { let orig_termios = self.orig_termios.lock().unwrap(); - spawn_terminal(terminal_action, orig_termios.clone()) + spawn_terminal(terminal_action, orig_termios.clone(), quit_cb) } fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result { unistd::read(fd, buf) @@ -322,8 +302,7 @@ impl ServerOsApi for ServerOsInputOutput { Box::new((*self).clone()) } fn kill(&self, pid: Pid) -> Result<(), nix::Error> { - kill(pid, Some(Signal::SIGTERM)).unwrap(); - waitpid(pid, None).unwrap(); + let _ = kill(pid, Some(Signal::SIGTERM)); Ok(()) } fn force_kill(&self, pid: Pid) -> Result<(), nix::Error> { diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 8659ae15..6b1cb416 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -327,4 +327,12 @@ impl Pane for PluginPane { fn borderless(&self) -> bool { self.borderless } + fn handle_right_click(&mut self, to: &Position) { + self.send_plugin_instructions + .send(PluginInstruction::Update( + Some(self.pid), + Event::Mouse(Mouse::RightClick(to.line.0, to.column.0)), + )) + .unwrap(); + } } diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index 277f572c..8c5a1133 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -1,5 +1,5 @@ use crate::{ - os_input_output::{AsyncReader, ChildId, ServerOsApi}, + os_input_output::{AsyncReader, ServerOsApi}, panes::PaneId, screen::ScreenInstruction, thread_bus::{Bus, ThreadSenders}, @@ -17,6 +17,7 @@ use std::{ path::PathBuf, time::{Duration, Instant}, }; +use zellij_utils::nix::unistd::Pid; use zellij_utils::{ async_std, errors::{get_current_ctx, ContextType, PtyContext}, @@ -66,7 +67,7 @@ impl From<&PtyInstruction> for PtyContext { pub(crate) struct Pty { pub active_panes: HashMap, pub bus: Bus, - pub id_to_child_pid: HashMap, + pub id_to_child_pid: HashMap, // pty_primary => child raw fd debug_to_file: bool, task_handles: HashMap>, } @@ -252,15 +253,6 @@ fn stream_terminal_bytes( } } async_send_to_screen(senders.clone(), ScreenInstruction::Render).await; - - // we send ClosePane here so that the screen knows to close this tab if the process - // inside the terminal exited on its own (eg. the user typed "exit" inside a - // bash shell) - async_send_to_screen( - senders, - ScreenInstruction::ClosePane(PaneId::Terminal(pid), None), - ) - .await; } }) } @@ -275,39 +267,65 @@ impl Pty { task_handles: HashMap::new(), } } - pub fn get_default_terminal(&self, client_id: Option) -> TerminalAction { + pub fn get_default_terminal(&self) -> TerminalAction { TerminalAction::RunCommand(RunCommand { args: vec![], command: PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable")), - cwd: client_id - .and_then(|client_id| self.active_panes.get(&client_id)) - .and_then(|pane| match pane { - PaneId::Plugin(..) => None, - PaneId::Terminal(id) => self.id_to_child_pid.get(id).and_then(|id| id.shell), - }) - .and_then(|id| self.bus.os_input.as_ref().map(|input| input.get_cwd(id))) - .flatten(), + cwd: None, // this should be filled by the calling function, eg. spawn_terminal }) } + fn fill_cwd(&self, terminal_action: &mut TerminalAction, client_id: ClientId) { + if let TerminalAction::RunCommand(run_command) = terminal_action { + if run_command.cwd.is_none() { + run_command.cwd = self + .active_panes + .get(&client_id) + .and_then(|pane| match pane { + PaneId::Plugin(..) => None, + PaneId::Terminal(id) => self.id_to_child_pid.get(id), + }) + .and_then(|id| { + self.bus + .os_input + .as_ref() + .map(|input| input.get_cwd(Pid::from_raw(*id))) + }) + .flatten(); + }; + }; + } pub fn spawn_terminal( &mut self, terminal_action: Option, client_or_tab_index: ClientOrTabIndex, ) -> RawFd { + log::info!( + "spawn_terminal, client_or_tab_index: {:?}", + client_or_tab_index + ); let terminal_action = match client_or_tab_index { ClientOrTabIndex::ClientId(client_id) => { - terminal_action.unwrap_or_else(|| self.get_default_terminal(Some(client_id))) + let mut terminal_action = + terminal_action.unwrap_or_else(|| self.get_default_terminal()); + self.fill_cwd(&mut terminal_action, client_id); + terminal_action } ClientOrTabIndex::TabIndex(_) => { - terminal_action.unwrap_or_else(|| self.get_default_terminal(None)) + terminal_action.unwrap_or_else(|| self.get_default_terminal()) } }; - let (pid_primary, child_id): (RawFd, ChildId) = self + let quit_cb = Box::new({ + let senders = self.bus.senders.clone(); + move |pane_id| { + let _ = senders.send_to_screen(ScreenInstruction::ClosePane(pane_id, None)); + } + }); + let (pid_primary, child_fd): (RawFd, RawFd) = self .bus .os_input .as_mut() .unwrap() - .spawn_terminal(terminal_action); + .spawn_terminal(terminal_action, quit_cb); let task_handle = stream_terminal_bytes( pid_primary, self.bus.senders.clone(), @@ -315,7 +333,7 @@ impl Pty { self.debug_to_file, ); self.task_handles.insert(pid_primary, task_handle); - self.id_to_child_pid.insert(pid_primary, child_id); + self.id_to_child_pid.insert(pid_primary, child_fd); pid_primary } pub fn spawn_terminals_for_layout( @@ -324,27 +342,37 @@ impl Pty { default_shell: Option, client_id: ClientId, ) { - let default_shell = - default_shell.unwrap_or_else(|| self.get_default_terminal(Some(client_id))); + 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![]; for run_instruction in extracted_run_instructions { + let quit_cb = Box::new({ + let senders = self.bus.senders.clone(); + move |pane_id| { + 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_id): (RawFd, ChildId) = - self.bus.os_input.as_mut().unwrap().spawn_terminal(cmd); - self.id_to_child_pid.insert(pid_primary, child_id); - new_pane_pids.push(pid_primary); - } - None => { - let (pid_primary, child_id): (RawFd, ChildId) = self + let (pid_primary, child_fd): (RawFd, RawFd) = self .bus .os_input .as_mut() .unwrap() - .spawn_terminal(default_shell.clone()); - self.id_to_child_pid.insert(pid_primary, child_id); + .spawn_terminal(cmd, quit_cb); + self.id_to_child_pid.insert(pid_primary, child_fd); + new_pane_pids.push(pid_primary); + } + None => { + let (pid_primary, child_fd): (RawFd, RawFd) = self + .bus + .os_input + .as_mut() + .unwrap() + .spawn_terminal(default_shell.clone(), quit_cb); + self.id_to_child_pid.insert(pid_primary, child_fd); new_pane_pids.push(pid_primary); } // Investigate moving plugin loading to here. @@ -372,27 +400,15 @@ impl Pty { pub fn close_pane(&mut self, id: PaneId) { match id { PaneId::Terminal(id) => { - let pids = self.id_to_child_pid.remove(&id).unwrap(); - let handle = self.task_handles.remove(&id).unwrap(); + 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(pids.primary) + .kill(Pid::from_raw(child_fd)) .unwrap(); - let timeout = Duration::from_millis(100); - match async_timeout(timeout, handle.cancel()).await { - Ok(_) => {} - _ => { - self.bus - .os_input - .as_mut() - .unwrap() - .force_kill(pids.primary) - .unwrap(); - } - }; }); } PaneId::Plugin(pid) => drop( diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 9b1201b6..57fea124 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -50,6 +50,17 @@ fn route_action( .send_to_screen(ScreenInstruction::WriteCharacter(val, client_id)) .unwrap(); } + Action::WriteChars(val) => { + session + .senders + .send_to_screen(ScreenInstruction::ClearScroll(client_id)) + .unwrap(); + let val = Vec::from(val.as_bytes()); + session + .senders + .send_to_screen(ScreenInstruction::WriteCharacter(val, client_id)) + .unwrap(); + } Action::SwitchToMode(mode) => { let palette = session.palette; // TODO: use the palette from the client and remove it from the server os api @@ -285,6 +296,13 @@ fn route_action( .send_to_screen(ScreenInstruction::LeftClick(point, client_id)) .unwrap(); } + Action::RightClick(point) => { + session + .senders + .send_to_screen(ScreenInstruction::RightClick(point, client_id)) + .unwrap(); + } + Action::MouseRelease(point) => { session .senders diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 72c681c9..4852b305 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -74,6 +74,7 @@ pub(crate) enum ScreenInstruction { TerminalResize(Size), ChangeMode(ModeInfo, ClientId), LeftClick(Position, ClientId), + RightClick(Position, ClientId), MouseRelease(Position, ClientId), MouseHold(Position, ClientId), Copy(ClientId), @@ -138,6 +139,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ScrollUpAt(..) => ScreenContext::ScrollUpAt, ScreenInstruction::ScrollDownAt(..) => ScreenContext::ScrollDownAt, ScreenInstruction::LeftClick(..) => ScreenContext::LeftClick, + ScreenInstruction::RightClick(..) => ScreenContext::RightClick, ScreenInstruction::MouseRelease(..) => ScreenContext::MouseRelease, ScreenInstruction::MouseHold(..) => ScreenContext::MouseHold, ScreenInstruction::Copy(..) => ScreenContext::Copy, @@ -973,6 +975,14 @@ pub(crate) fn screen_thread_main( screen.render(); } + ScreenInstruction::RightClick(point, client_id) => { + screen + .get_active_tab_mut(client_id) + .unwrap() + .handle_right_click(&point); + + screen.render(); + } ScreenInstruction::MouseRelease(point, client_id) => { screen .get_active_tab_mut(client_id) diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index d50aba42..5c81b9c6 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -269,6 +269,7 @@ pub trait Pane { fn set_boundary_color(&mut self, _color: Option) {} fn set_borderless(&mut self, borderless: bool); fn borderless(&self) -> bool; + fn handle_right_click(&mut self, _to: &Position) {} } macro_rules! resize_pty { @@ -2586,6 +2587,14 @@ impl Tab { pane.start_selection(&relative_position); }; } + pub fn handle_right_click(&mut self, position: &Position) { + self.focus_pane_at(position); + + if let Some(pane) = self.get_pane_at(position, false) { + let relative_position = pane.relative_position(position); + pane.handle_right_click(&relative_position); + }; + } fn focus_pane_at(&mut self, point: &Position) { if let Some(clicked_pane) = self.get_pane_id_at(point, true) { self.active_terminal = Some(clicked_pane); diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index b4d367e4..f9f63195 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -1,7 +1,8 @@ use super::{Screen, ScreenInstruction}; +use crate::panes::PaneId; use crate::zellij_tile::data::{ModeInfo, Palette}; use crate::{ - os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi}, + os_input_output::{AsyncReader, Pid, ServerOsApi}, thread_bus::Bus, ClientId, }; @@ -29,7 +30,11 @@ impl ServerOsApi for FakeInputOutput { fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) { // noop } - fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) { + fn spawn_terminal( + &self, + _file_to_open: TerminalAction, + _quit_db: Box, + ) -> (RawFd, RawFd) { unimplemented!() } fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { diff --git a/zellij-server/src/unit/tab_tests.rs b/zellij-server/src/unit/tab_tests.rs index 41e243cd..3d177ab8 100644 --- a/zellij-server/src/unit/tab_tests.rs +++ b/zellij-server/src/unit/tab_tests.rs @@ -1,7 +1,7 @@ use super::Tab; use crate::zellij_tile::data::{ModeInfo, Palette}; use crate::{ - os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi}, + os_input_output::{AsyncReader, Pid, ServerOsApi}, panes::PaneId, thread_bus::ThreadSenders, ClientId, @@ -29,7 +29,11 @@ impl ServerOsApi for FakeInputOutput { fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) { // noop } - fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) { + fn spawn_terminal( + &self, + _file_to_open: TerminalAction, + _quit_cb: Box, + ) -> (RawFd, RawFd) { unimplemented!() } fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index 9696b356..872a78f9 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -34,6 +34,7 @@ pub enum Mouse { ScrollUp(usize), // number of lines ScrollDown(usize), // number of lines LeftClick(isize, usize), // line and column + RightClick(isize, usize), // line and column Hold(isize, usize), // line and column Release(Option<(isize, usize)>), // line and column } diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index eab045dd..bf4654e9 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" [dependencies] backtrace = "0.3.55" bincode = "1.3.1" -colors-transform = "0.2.5" +colorsys = "0.6.5" crossbeam = "0.8.0" directories-next = "2.0" interprocess = "1.1.1" diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 7ac9e958..78615d92 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -85,6 +85,10 @@ pub enum Sessions { #[structopt(short, long)] create: bool, + /// Number of the session index in the active sessions ordered creation date. + #[structopt(long)] + index: Option, + /// Change the behaviour of zellij #[structopt(subcommand, name = "options")] options: Option, diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index be75e4ad..401cdc10 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -258,6 +258,7 @@ pub enum ScreenContext { TerminalResize, ChangeMode, LeftClick, + RightClick, MouseRelease, MouseHold, Copy, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 8446bf0d..9dd25958 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -28,6 +28,8 @@ pub enum Action { Quit, /// Write to the terminal. Write(Vec), + /// Write Characters to the terminal. + WriteChars(String), /// Switch to the specified input mode. SwitchToMode(InputMode), /// Resize focus pane in specified direction. @@ -85,6 +87,7 @@ pub enum Action { /// Detach session and exit Detach, LeftClick(Position), + RightClick(Position), MouseRelease(Position), MouseHold(Position), Copy, diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index ecfe64c7..66104016 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -5,7 +5,7 @@ use std::fs::File; use std::io::{self, Read}; use std::path::{Path, PathBuf}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::convert::{TryFrom, TryInto}; use super::keybinds::{Keybinds, KeybindsFromYaml}; @@ -20,7 +20,7 @@ const DEFAULT_CONFIG_FILE_NAME: &str = "config.yaml"; type ConfigResult = Result; /// Intermediate deserialization config struct -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct ConfigFromYaml { #[serde(flatten)] pub options: Option, @@ -31,7 +31,7 @@ pub struct ConfigFromYaml { } /// Main configuration. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Config { pub keybinds: Keybinds, pub options: Options, @@ -106,7 +106,8 @@ impl TryFrom<&CliArgs> for Config { impl Config { /// Uses defaults, but lets config override them. pub fn from_yaml(yaml_config: &str) -> ConfigResult { - let config_from_yaml: Option = match serde_yaml::from_str(yaml_config) { + let maybe_config_from_yaml: Option = match serde_yaml::from_str(yaml_config) + { Err(e) => { // needs direct check, as `[ErrorImpl]` is private // https://github.com/dtolnay/serde-yaml/issues/121 @@ -118,20 +119,9 @@ impl Config { Ok(config) => config, }; - match config_from_yaml { + match maybe_config_from_yaml { None => Ok(Config::default()), - Some(config) => { - let keybinds = Keybinds::get_default_keybinds_with_config(config.keybinds); - let options = Options::from_yaml(config.options); - let themes = config.themes; - let plugins = PluginsConfig::get_plugins_with_default(config.plugins.try_into()?); - Ok(Config { - keybinds, - options, - plugins, - themes, - }) - } + Some(config) => config.try_into(), } } @@ -157,6 +147,23 @@ impl Config { } } +impl TryFrom for Config { + type Error = ConfigError; + + fn try_from(config_from_yaml: ConfigFromYaml) -> ConfigResult { + let keybinds = Keybinds::get_default_keybinds_with_config(config_from_yaml.keybinds); + let options = Options::from_yaml(config_from_yaml.options); + let themes = config_from_yaml.themes; + let plugins = PluginsConfig::get_plugins_with_default(config_from_yaml.plugins.try_into()?); + Ok(Self { + keybinds, + options, + plugins, + themes, + }) + } +} + // TODO: Split errors up into separate modules #[derive(Debug, Clone)] pub struct LayoutNameInTabError; diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index 995b89fb..b27a097a 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -104,7 +104,7 @@ impl Display for ExitReason { f, "Session attached to another client. Use --force flag to force connect." ), - Self::Error(e) => write!(f, "Error occured in server:\n{}", e), + Self::Error(e) => write!(f, "Error occurred in server:\n{}", e), } } } diff --git a/zellij-utils/src/shared.rs b/zellij-utils/src/shared.rs index 06d1378d..37dfc3be 100644 --- a/zellij-utils/src/shared.rs +++ b/zellij-utils/src/shared.rs @@ -2,7 +2,7 @@ use std::{iter, str::from_utf8}; -use colors_transform::{Color, Rgb}; +use colorsys::Rgb; use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::{fs, io}; @@ -52,10 +52,9 @@ pub mod colors { } pub fn _hex_to_rgb(hex: &str) -> (u8, u8, u8) { - let rgb = Rgb::from_hex_str(hex) + Rgb::from_hex_str(hex) .expect("The passed argument must be a valid hex color") - .as_tuple(); - (rgb.0 as u8, rgb.1 as u8, rgb.2 as u8) + .into() } pub fn default_palette() -> Palette {