diff --git a/Cargo.lock b/Cargo.lock index 3d04bc27..b1fbaf3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -598,6 +598,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "funty" version = "1.1.0" @@ -1031,6 +1037,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0debeb9fcf88823ea64d64e4a815ab1643f33127d995978e099942ce38f25238" +[[package]] +name = "names" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef320dab323286b50fb5cdda23f61c796a72a89998ab565ca32525c5c556f2da" +dependencies = [ + "rand 0.3.23", +] + [[package]] name = "nix" version = "0.19.1" @@ -1201,6 +1216,29 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.8.3" @@ -1209,7 +1247,7 @@ checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.2", "rand_hc", ] @@ -1220,9 +1258,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.2", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.6.2" @@ -1238,7 +1291,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" dependencies = [ - "rand_core", + "rand_core 0.6.2", ] [[package]] @@ -1266,6 +1319,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.2.6" @@ -1593,7 +1655,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if 1.0.0", "libc", - "rand", + "rand 0.8.3", "redox_syscall", "remove_dir_all", "winapi", @@ -2205,6 +2267,7 @@ dependencies = [ "interprocess", "lazy_static", "libc", + "names", "nix", "nom", "serde", diff --git a/Cargo.toml b/Cargo.toml index 705a0906..f59b1e8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,8 @@ strum = "0.20.0" lazy_static = "1.4.0" wasmer = "1.0.0" wasmer-wasi = "1.0.0" -interprocess = "1.0.1" +interprocess = "1.1.1" +names = "0.11.0" colors-transform = "0.2.5" zellij-tile = { path = "zellij-tile/", version = "0.7.0" } diff --git a/src/cli.rs b/src/cli.rs index bc4e4a9e..91b0ed39 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,22 +1,11 @@ use super::common::utils::consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV}; +use serde::{Deserialize, Serialize}; use std::path::PathBuf; use structopt::StructOpt; -#[derive(StructOpt, Default, Debug)] +#[derive(StructOpt, Default, Debug, Clone, Serialize, Deserialize)] #[structopt(name = "zellij")] pub struct CliArgs { - /// Send "split (direction h == horizontal / v == vertical)" to active zellij session - #[structopt(short, long)] - pub split: Option, - - /// Send "move focused pane" to active zellij session - #[structopt(short, long)] - pub move_focus: bool, - - /// Send "open file in new pane" to active zellij session - #[structopt(short, long)] - pub open_file: Option, - /// Maximum panes on screen, caution: opening more panes will close old ones #[structopt(long)] pub max_panes: Option, @@ -44,7 +33,7 @@ pub struct CliArgs { pub debug: bool, } -#[derive(Debug, StructOpt)] +#[derive(Debug, StructOpt, Clone, Serialize, Deserialize)] pub enum ConfigCli { /// Change the behaviour of zellij #[structopt(name = "option")] diff --git a/src/client/mod.rs b/src/client/mod.rs index cba12a46..8a5834f0 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -4,4 +4,172 @@ pub mod pane_resizer; pub mod panes; pub mod tab; -pub fn _start_client() {} +use serde::{Deserialize, Serialize}; +use std::io::Write; +use std::sync::mpsc; +use std::thread; + +use crate::cli::CliArgs; +use crate::common::{ + command_is_executing::CommandIsExecuting, + errors::{ClientContext, ContextType}, + input::config::Config, + input::handler::input_loop, + os_input_output::ClientOsApi, + SenderType, SenderWithContext, SyncChannelWithContext, +}; +use crate::server::ServerInstruction; + +/// Instructions related to the client-side application and sent from server to client +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ClientInstruction { + Error(String), + Render(Option), + UnblockInputThread, + Exit, +} + +pub fn start_client(mut os_input: Box, opts: CliArgs, config: Config) { + let take_snapshot = "\u{1b}[?1049h"; + os_input.unset_raw_mode(0); + let _ = os_input + .get_stdout_writer() + .write(take_snapshot.as_bytes()) + .unwrap(); + std::env::set_var(&"ZELLIJ", "0"); + + let mut command_is_executing = CommandIsExecuting::new(); + + let full_screen_ws = os_input.get_terminal_size_using_fd(0); + os_input.connect_to_server(); + os_input.send_to_server(ServerInstruction::NewClient(full_screen_ws, opts)); + os_input.set_raw_mode(0); + + let (send_client_instructions, receive_client_instructions): SyncChannelWithContext< + ClientInstruction, + > = mpsc::sync_channel(50); + let send_client_instructions = + SenderWithContext::new(SenderType::SyncSender(send_client_instructions)); + + #[cfg(not(test))] + std::panic::set_hook({ + use crate::errors::handle_panic; + let send_client_instructions = send_client_instructions.clone(); + Box::new(move |info| { + handle_panic(info, &send_client_instructions); + }) + }); + + let _stdin_thread = thread::Builder::new() + .name("stdin_handler".to_string()) + .spawn({ + let send_client_instructions = send_client_instructions.clone(); + let command_is_executing = command_is_executing.clone(); + let os_input = os_input.clone(); + move || { + input_loop( + os_input, + config, + command_is_executing, + send_client_instructions, + ) + } + }); + + let _signal_thread = thread::Builder::new() + .name("signal_listener".to_string()) + .spawn({ + let os_input = os_input.clone(); + move || { + os_input.receive_sigwinch(Box::new({ + let os_api = os_input.clone(); + move || { + os_api.send_to_server(ServerInstruction::TerminalResize( + os_api.get_terminal_size_using_fd(0), + )); + } + })); + } + }) + .unwrap(); + + let router_thread = thread::Builder::new() + .name("router".to_string()) + .spawn({ + let os_input = os_input.clone(); + move || { + loop { + let (instruction, mut err_ctx) = os_input.recv_from_server(); + err_ctx.add_call(ContextType::Client(ClientContext::from(&instruction))); + if let ClientInstruction::Exit = instruction { + break; + } + send_client_instructions.send(instruction).unwrap(); + } + send_client_instructions + .send(ClientInstruction::Exit) + .unwrap(); + } + }) + .unwrap(); + + #[warn(clippy::never_loop)] + loop { + let (client_instruction, mut err_ctx) = receive_client_instructions + .recv() + .expect("failed to receive app instruction on channel"); + + err_ctx.add_call(ContextType::Client(ClientContext::from( + &client_instruction, + ))); + match client_instruction { + ClientInstruction::Exit => break, + ClientInstruction::Error(backtrace) => { + let _ = os_input.send_to_server(ServerInstruction::ClientExit); + os_input.unset_raw_mode(0); + let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); + let restore_snapshot = "\u{1b}[?1049l"; + let error = format!( + "{}\n{}{}", + goto_start_of_last_line, restore_snapshot, backtrace + ); + let _ = os_input + .get_stdout_writer() + .write(error.as_bytes()) + .unwrap(); + std::process::exit(1); + } + ClientInstruction::Render(output) => { + if output.is_none() { + break; + } + let mut stdout = os_input.get_stdout_writer(); + stdout + .write_all(&output.unwrap().as_bytes()) + .expect("cannot write to stdout"); + stdout.flush().expect("could not flush"); + } + ClientInstruction::UnblockInputThread => { + command_is_executing.unblock_input_thread(); + } + } + } + + let _ = os_input.send_to_server(ServerInstruction::ClientExit); + router_thread.join().unwrap(); + + // cleanup(); + let reset_style = "\u{1b}[m"; + let show_cursor = "\u{1b}[?25h"; + let restore_snapshot = "\u{1b}[?1049l"; + let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); + let goodbye_message = format!( + "{}\n{}{}{}Bye from Zellij!\n", + goto_start_of_last_line, restore_snapshot, reset_style, show_cursor + ); + + os_input.unset_raw_mode(0); + let mut stdout = os_input.get_stdout_writer(); + let _ = stdout.write(goodbye_message.as_bytes()).unwrap(); + stdout.flush().unwrap(); +} diff --git a/src/client/pane_resizer.rs b/src/client/pane_resizer.rs index f9ec71b8..73678499 100644 --- a/src/client/pane_resizer.rs +++ b/src/client/pane_resizer.rs @@ -1,4 +1,4 @@ -use crate::os_input_output::OsApi; +use crate::os_input_output::ServerOsApi; use crate::panes::{PaneId, PositionAndSize}; use crate::tab::Pane; use std::{ @@ -8,7 +8,7 @@ use std::{ pub struct PaneResizer<'a> { panes: &'a mut BTreeMap>, - os_api: &'a mut Box, + os_api: &'a mut Box, } // TODO: currently there are some functions here duplicated with Tab @@ -17,7 +17,7 @@ pub struct PaneResizer<'a> { impl<'a> PaneResizer<'a> { pub fn new( panes: &'a mut BTreeMap>, - os_api: &'a mut Box, + os_api: &'a mut Box, ) -> Self { PaneResizer { panes, os_api } } diff --git a/src/client/panes/terminal_pane.rs b/src/client/panes/terminal_pane.rs index a5a957e6..f58c3a27 100644 --- a/src/client/panes/terminal_pane.rs +++ b/src/client/panes/terminal_pane.rs @@ -1,6 +1,7 @@ use crate::tab::Pane; use ::nix::pty::Winsize; use ::std::os::unix::io::RawFd; +use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::time::Instant; @@ -10,7 +11,7 @@ use crate::panes::terminal_character::{ }; use crate::pty_bus::VteBytes; -#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug)] +#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug, Serialize, Deserialize)] pub enum PaneId { Terminal(RawFd), Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct? @@ -18,7 +19,7 @@ pub enum PaneId { /// Contains the position and size of a [`Pane`], or more generally of any terminal, measured /// in character rows and columns. -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] pub struct PositionAndSize { pub x: usize, pub y: usize, diff --git a/src/client/tab.rs b/src/client/tab.rs index b01e04e5..0098d78b 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -2,22 +2,23 @@ //! as well as how they should be resized use crate::client::pane_resizer::PaneResizer; -use crate::common::{input::handler::parse_keys, AppInstruction, SenderWithContext}; +use crate::common::{input::handler::parse_keys, SenderWithContext}; use crate::layout::Layout; -use crate::os_input_output::OsApi; +use crate::os_input_output::ServerOsApi; use crate::panes::{PaneId, PositionAndSize, TerminalPane}; use crate::pty_bus::{PtyInstruction, VteBytes}; +use crate::server::ServerInstruction; use crate::utils::shared::adjust_to_size; use crate::wasm_vm::PluginInstruction; use crate::{boundaries::Boundaries, panes::PluginPane}; use serde::{Deserialize, Serialize}; use std::os::unix::io::RawFd; +use std::sync::mpsc::channel; use std::time::Instant; use std::{ cmp::Reverse, collections::{BTreeMap, HashSet}, }; -use std::{io::Write, sync::mpsc::channel}; use zellij_tile::data::{Event, InputMode, ModeInfo, Palette}; const CURSOR_HEIGHT_WIDTH_RATIO: usize = 4; // this is not accurate and kind of a magic number, TODO: look into this @@ -67,11 +68,11 @@ pub struct Tab { max_panes: Option, full_screen_ws: PositionAndSize, fullscreen_is_active: bool, + os_api: Box, + send_plugin_instructions: SenderWithContext, + send_pty_instructions: SenderWithContext, + send_server_instructions: SenderWithContext, synchronize_is_active: bool, - os_api: Box, - pub send_pty_instructions: SenderWithContext, - pub send_plugin_instructions: SenderWithContext, - pub send_app_instructions: SenderWithContext, should_clear_display_before_rendering: bool, pub mode_info: ModeInfo, pub input_mode: InputMode, @@ -225,10 +226,10 @@ impl Tab { position: usize, name: String, full_screen_ws: &PositionAndSize, - mut os_api: Box, - send_pty_instructions: SenderWithContext, + mut os_api: Box, send_plugin_instructions: SenderWithContext, - send_app_instructions: SenderWithContext, + send_pty_instructions: SenderWithContext, + send_server_instructions: SenderWithContext, max_panes: Option, pane_id: Option, mode_info: ModeInfo, @@ -260,9 +261,9 @@ impl Tab { fullscreen_is_active: false, synchronize_is_active: false, os_api, - send_app_instructions, - send_pty_instructions, send_plugin_instructions, + send_pty_instructions, + send_server_instructions, should_clear_display_before_rendering: false, mode_info, input_mode, @@ -400,8 +401,8 @@ impl Tab { ); if terminal_id_to_split.is_none() { self.send_pty_instructions - .send(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty - .unwrap(); + .send(PtyInstruction::ClosePane(pid)) + .unwrap(); // we can't open this pane, close the pty return; // likely no terminal large enough to split } let terminal_id_to_split = terminal_id_to_split.unwrap(); @@ -481,8 +482,8 @@ impl Tab { let active_pane = self.panes.get_mut(active_pane_id).unwrap(); if active_pane.rows() < MIN_TERMINAL_HEIGHT * 2 + 1 { self.send_pty_instructions - .send(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty - .unwrap(); + .send(PtyInstruction::ClosePane(pid)) + .unwrap(); // we can't open this pane, close the pty return; } let terminal_ws = PositionAndSize { @@ -538,8 +539,8 @@ impl Tab { let active_pane = self.panes.get_mut(active_pane_id).unwrap(); if active_pane.columns() < MIN_TERMINAL_WIDTH * 2 + 1 { self.send_pty_instructions - .send(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty - .unwrap(); + .send(PtyInstruction::ClosePane(pid)) + .unwrap(); // we can't open this pane, close the pty return; } let terminal_ws = PositionAndSize { @@ -729,20 +730,16 @@ impl Tab { if self.panes_contain_widechar() { self.set_force_render() } - let mut stdout = self.os_api.get_stdout_writer(); + let mut output = String::new(); let mut boundaries = Boundaries::new( self.full_screen_ws.columns as u16, self.full_screen_ws.rows as u16, ); let hide_cursor = "\u{1b}[?25l"; - stdout - .write_all(&hide_cursor.as_bytes()) - .expect("cannot write to stdout"); + output.push_str(hide_cursor); if self.should_clear_display_before_rendering { let clear_display = "\u{1b}[2J"; - stdout - .write_all(&clear_display.as_bytes()) - .expect("cannot write to stdout"); + output.push_str(clear_display); self.should_clear_display_before_rendering = false; } for (kind, pane) in self.panes.iter_mut() { @@ -761,48 +758,39 @@ impl Tab { adjust_to_size(&vte_output, pane.rows(), pane.columns()) }; // FIXME: Use Termion for cursor and style clearing? - write!( - stdout, + output.push_str(&format!( "\u{1b}[{};{}H\u{1b}[m{}", pane.y() + 1, pane.x() + 1, vte_output - ) - .expect("cannot write to stdout"); + )); } } } // TODO: only render (and calculate) boundaries if there was a resize - let vte_output = boundaries.vte_output(); - stdout - .write_all(&vte_output.as_bytes()) - .expect("cannot write to stdout"); + output.push_str(&boundaries.vte_output()); match self.get_active_terminal_cursor_position() { Some((cursor_position_x, cursor_position_y)) => { let show_cursor = "\u{1b}[?25h"; - let goto_cursor_position = format!( + let goto_cursor_position = &format!( "\u{1b}[{};{}H\u{1b}[m", cursor_position_y + 1, cursor_position_x + 1 ); // goto row/col - stdout - .write_all(&show_cursor.as_bytes()) - .expect("cannot write to stdout"); - stdout - .write_all(&goto_cursor_position.as_bytes()) - .expect("cannot write to stdout"); - stdout.flush().expect("could not flush"); + output.push_str(show_cursor); + output.push_str(goto_cursor_position); } None => { let hide_cursor = "\u{1b}[?25l"; - stdout - .write_all(&hide_cursor.as_bytes()) - .expect("cannot write to stdout"); - stdout.flush().expect("could not flush"); + output.push_str(hide_cursor); } } + + self.send_server_instructions + .send(ServerInstruction::Render(Some(output))) + .unwrap(); } fn get_panes(&self) -> impl Iterator)> { self.panes.iter() diff --git a/src/common/command_is_executing.rs b/src/common/command_is_executing.rs index 775a7bfc..ad032557 100644 --- a/src/common/command_is_executing.rs +++ b/src/common/command_is_executing.rs @@ -3,51 +3,31 @@ use std::sync::{Arc, Condvar, Mutex}; #[derive(Clone)] pub struct CommandIsExecuting { - opening_new_pane: Arc<(Mutex, Condvar)>, - closing_pane: Arc<(Mutex, Condvar)>, + input_thread: Arc<(Mutex, Condvar)>, } impl CommandIsExecuting { pub fn new() -> Self { CommandIsExecuting { - opening_new_pane: Arc::new((Mutex::new(false), Condvar::new())), - closing_pane: Arc::new((Mutex::new(false), Condvar::new())), + input_thread: Arc::new((Mutex::new(false), Condvar::new())), } } - pub fn closing_pane(&mut self) { - let (lock, _cvar) = &*self.closing_pane; - let mut closing_pane = lock.lock().unwrap(); - *closing_pane = true; + pub fn blocking_input_thread(&mut self) { + let (lock, _cvar) = &*self.input_thread; + let mut input_thread = lock.lock().unwrap(); + *input_thread = true; } - pub fn done_closing_pane(&mut self) { - let (lock, cvar) = &*self.closing_pane; - let mut closing_pane = lock.lock().unwrap(); - *closing_pane = false; + pub fn unblock_input_thread(&mut self) { + let (lock, cvar) = &*self.input_thread; + let mut input_thread = lock.lock().unwrap(); + *input_thread = false; cvar.notify_all(); } - pub fn opening_new_pane(&mut self) { - let (lock, _cvar) = &*self.opening_new_pane; - let mut opening_new_pane = lock.lock().unwrap(); - *opening_new_pane = true; - } - pub fn done_opening_new_pane(&mut self) { - let (lock, cvar) = &*self.opening_new_pane; - let mut opening_new_pane = lock.lock().unwrap(); - *opening_new_pane = false; - cvar.notify_all(); - } - pub fn wait_until_pane_is_closed(&self) { - let (lock, cvar) = &*self.closing_pane; - let mut closing_pane = lock.lock().unwrap(); - while *closing_pane { - closing_pane = cvar.wait(closing_pane).unwrap(); - } - } - pub fn wait_until_new_pane_is_opened(&self) { - let (lock, cvar) = &*self.opening_new_pane; - let mut opening_new_pane = lock.lock().unwrap(); - while *opening_new_pane { - opening_new_pane = cvar.wait(opening_new_pane).unwrap(); + pub fn wait_until_input_thread_is_unblocked(&self) { + let (lock, cvar) = &*self.input_thread; + let mut input_thread = lock.lock().unwrap(); + while *input_thread { + input_thread = cvar.wait(input_thread).unwrap(); } } } diff --git a/src/common/errors.rs b/src/common/errors.rs index c1c6753f..411e0225 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -1,9 +1,11 @@ //! Error context system based on a thread-local representation of the call stack, itself based on //! the instructions that are sent between threads. -use super::{AppInstruction, ASYNCOPENCALLS, OPENCALLS}; +use super::{ServerInstruction, ASYNCOPENCALLS, OPENCALLS}; +use crate::client::ClientInstruction; use crate::pty_bus::PtyInstruction; use crate::screen::ScreenInstruction; +use serde::{Deserialize, Serialize}; use std::fmt::{Display, Error, Formatter}; @@ -19,7 +21,7 @@ use std::panic::PanicInfo; #[cfg(not(test))] pub fn handle_panic( info: &PanicInfo<'_>, - send_app_instructions: &SenderWithContext, + send_app_instructions: &SenderWithContext, ) { use backtrace::Backtrace; use std::{process, thread}; @@ -66,9 +68,7 @@ pub fn handle_panic( println!("{}", backtrace); process::exit(1); } else { - send_app_instructions - .send(AppInstruction::Error(backtrace)) - .unwrap(); + let _ = send_app_instructions.send(ClientInstruction::Error(backtrace)); } } @@ -79,7 +79,7 @@ pub fn get_current_ctx() -> ErrorContext { } /// A representation of the call stack. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Serialize, Deserialize)] pub struct ErrorContext { calls: [ContextType; MAX_THREAD_CALL_STACK], } @@ -130,8 +130,8 @@ impl Display for ErrorContext { /// /// Complex variants store a variant of a related enum, whose variants can be built from /// the corresponding Zellij MSPC instruction enum variants ([`ScreenInstruction`], -/// [`PtyInstruction`], [`AppInstruction`], etc). -#[derive(Copy, Clone, PartialEq)] +/// [`PtyInstruction`], [`ClientInstruction`], etc). +#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)] pub enum ContextType { /// A screen-related call. Screen(ScreenContext), @@ -140,8 +140,9 @@ pub enum ContextType { /// A plugin-related call. Plugin(PluginContext), /// An app-related call. - App(AppContext), - IpcServer, + Client(ClientContext), + /// A server-related call. + IPCServer(ServerContext), StdinHandler, AsyncTask, /// An empty, placeholder call. This should be thought of as representing no call at all. @@ -157,10 +158,9 @@ impl Display for ContextType { match *self { ContextType::Screen(c) => write!(f, "{}screen_thread: {}{:?}", purple, green, c), ContextType::Pty(c) => write!(f, "{}pty_thread: {}{:?}", purple, green, c), - ContextType::Plugin(c) => write!(f, "{}plugin_thread: {}{:?}", purple, green, c), - ContextType::App(c) => write!(f, "{}main_thread: {}{:?}", purple, green, c), - ContextType::IpcServer => write!(f, "{}ipc_server: {}AcceptInput", purple, green), + ContextType::Client(c) => write!(f, "{}main_thread: {}{:?}", purple, green, c), + ContextType::IPCServer(c) => write!(f, "{}ipc_server: {}{:?}", purple, green, c), ContextType::StdinHandler => { write!(f, "{}stdin_handler_thread: {}AcceptInput", purple, green) } @@ -174,7 +174,7 @@ impl Display for ContextType { // FIXME: Just deriving EnumDiscriminants from strum will remove the need for any of this!!! /// Stack call representations corresponding to the different types of [`ScreenInstruction`]s. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum ScreenContext { HandlePtyBytes, Render, @@ -193,7 +193,7 @@ pub enum ScreenContext { MoveFocusDown, MoveFocusUp, MoveFocusRight, - Quit, + Exit, ScrollUp, ScrollDown, PageScrollUp, @@ -238,7 +238,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::MoveFocusDown => ScreenContext::MoveFocusDown, ScreenInstruction::MoveFocusUp => ScreenContext::MoveFocusUp, ScreenInstruction::MoveFocusRight => ScreenContext::MoveFocusRight, - ScreenInstruction::Quit => ScreenContext::Quit, + ScreenInstruction::Exit => ScreenContext::Exit, ScreenInstruction::ScrollUp => ScreenContext::ScrollUp, ScreenInstruction::ScrollDown => ScreenContext::ScrollDown, ScreenInstruction::PageScrollUp => ScreenContext::PageScrollUp, @@ -252,14 +252,14 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::SetInvisibleBorders(..) => ScreenContext::SetInvisibleBorders, ScreenInstruction::SetMaxHeight(..) => ScreenContext::SetMaxHeight, ScreenInstruction::ClosePane(_) => ScreenContext::ClosePane, - ScreenInstruction::ApplyLayout(_) => ScreenContext::ApplyLayout, + ScreenInstruction::ApplyLayout(..) => ScreenContext::ApplyLayout, ScreenInstruction::NewTab(_) => ScreenContext::NewTab, ScreenInstruction::SwitchTabNext => ScreenContext::SwitchTabNext, ScreenInstruction::SwitchTabPrev => ScreenContext::SwitchTabPrev, ScreenInstruction::CloseTab => ScreenContext::CloseTab, ScreenInstruction::GoToTab(_) => ScreenContext::GoToTab, ScreenInstruction::UpdateTabName(_) => ScreenContext::UpdateTabName, - ScreenInstruction::TerminalResize => ScreenContext::TerminalResize, + ScreenInstruction::TerminalResize(_) => ScreenContext::TerminalResize, ScreenInstruction::ChangeMode(_) => ScreenContext::ChangeMode, ScreenInstruction::ToggleActiveSyncPanes => ScreenContext::ToggleActiveSyncPanes, } @@ -267,7 +267,7 @@ impl From<&ScreenInstruction> for ScreenContext { } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum PtyContext { SpawnTerminal, SpawnTerminalVertically, @@ -275,7 +275,7 @@ pub enum PtyContext { NewTab, ClosePane, CloseTab, - Quit, + Exit, } impl From<&PtyInstruction> for PtyContext { @@ -287,7 +287,7 @@ impl From<&PtyInstruction> for PtyContext { PtyInstruction::ClosePane(_) => PtyContext::ClosePane, PtyInstruction::CloseTab(_) => PtyContext::CloseTab, PtyInstruction::NewTab => PtyContext::NewTab, - PtyInstruction::Quit => PtyContext::Quit, + PtyInstruction::Exit => PtyContext::Exit, } } } @@ -297,13 +297,13 @@ impl From<&PtyInstruction> for PtyContext { use crate::wasm_vm::PluginInstruction; /// Stack call representations corresponding to the different types of [`PluginInstruction`]s. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum PluginContext { Load, Update, Render, Unload, - Quit, + Exit, } impl From<&PluginInstruction> for PluginContext { @@ -313,23 +313,51 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::Update(..) => PluginContext::Update, PluginInstruction::Render(..) => PluginContext::Render, PluginInstruction::Unload(_) => PluginContext::Unload, - PluginInstruction::Quit => PluginContext::Quit, + PluginInstruction::Exit => PluginContext::Exit, } } } -/// Stack call representations corresponding to the different types of [`AppInstruction`]s. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum AppContext { +/// Stack call representations corresponding to the different types of [`ClientInstruction`]s. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum ClientContext { Exit, Error, + UnblockInputThread, + Render, } -impl From<&AppInstruction> for AppContext { - fn from(app_instruction: &AppInstruction) -> Self { - match *app_instruction { - AppInstruction::Exit => AppContext::Exit, - AppInstruction::Error(_) => AppContext::Error, +impl From<&ClientInstruction> for ClientContext { + fn from(client_instruction: &ClientInstruction) -> Self { + match *client_instruction { + ClientInstruction::Exit => ClientContext::Exit, + ClientInstruction::Error(_) => ClientContext::Error, + ClientInstruction::Render(_) => ClientContext::Render, + ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread, + } + } +} + +/// Stack call representations corresponding to the different types of [`ServerInstruction`]s. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum ServerContext { + NewClient, + Action, + Render, + TerminalResize, + UnblockInputThread, + ClientExit, +} + +impl From<&ServerInstruction> for ServerContext { + fn from(server_instruction: &ServerInstruction) -> Self { + match *server_instruction { + ServerInstruction::NewClient(..) => ServerContext::NewClient, + ServerInstruction::Action(_) => ServerContext::Action, + ServerInstruction::TerminalResize(_) => ServerContext::TerminalResize, + ServerInstruction::Render(_) => ServerContext::Render, + ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread, + ServerInstruction::ClientExit => ServerContext::ClientExit, } } } diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index 7304225c..75f9f8b1 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -2,53 +2,43 @@ use super::actions::Action; use super::keybinds::Keybinds; +use crate::client::ClientInstruction; use crate::common::input::config::Config; -use crate::common::{AppInstruction, SenderWithContext, OPENCALLS}; +use crate::common::{SenderWithContext, OPENCALLS}; use crate::errors::ContextType; -use crate::os_input_output::OsApi; -use crate::pty_bus::PtyInstruction; -use crate::screen::ScreenInstruction; -use crate::wasm_vm::PluginInstruction; +use crate::os_input_output::ClientOsApi; +use crate::server::ServerInstruction; use crate::CommandIsExecuting; use termion::input::{TermRead, TermReadEventsAndRaw}; -use zellij_tile::data::{Event, InputMode, Key, ModeInfo, Palette}; +use zellij_tile::data::{InputMode, Key, ModeInfo, Palette}; /// Handles the dispatching of [`Action`]s according to the current /// [`InputMode`], and keep tracks of the current [`InputMode`]. struct InputHandler { /// The current input mode mode: InputMode, - os_input: Box, + os_input: Box, config: Config, command_is_executing: CommandIsExecuting, - send_screen_instructions: SenderWithContext, - send_pty_instructions: SenderWithContext, - send_plugin_instructions: SenderWithContext, - send_app_instructions: SenderWithContext, + send_client_instructions: SenderWithContext, should_exit: bool, } impl InputHandler { /// Returns a new [`InputHandler`] with the attributes specified as arguments. fn new( - os_input: Box, + os_input: Box, command_is_executing: CommandIsExecuting, config: Config, - send_screen_instructions: SenderWithContext, - send_pty_instructions: SenderWithContext, - send_plugin_instructions: SenderWithContext, - send_app_instructions: SenderWithContext, + send_client_instructions: SenderWithContext, ) -> Self { InputHandler { mode: InputMode::Normal, os_input, config, command_is_executing, - send_screen_instructions, - send_pty_instructions, - send_plugin_instructions, - send_app_instructions, + send_client_instructions, should_exit: false, } } @@ -114,162 +104,31 @@ impl InputHandler { let mut should_break = false; match action { - Action::Write(val) => { - self.send_screen_instructions - .send(ScreenInstruction::ClearScroll) - .unwrap(); - self.send_screen_instructions - .send(ScreenInstruction::WriteCharacter(val)) - .unwrap(); - } Action::Quit => { self.exit(); should_break = true; } Action::SwitchToMode(mode) => { self.mode = mode; - self.send_plugin_instructions - .send(PluginInstruction::Update( - None, - Event::ModeUpdate(get_mode_info(mode, self.os_input.load_palette())), - )) - .unwrap(); - self.send_screen_instructions - .send(ScreenInstruction::ChangeMode(get_mode_info( - mode, - self.os_input.load_palette(), - ))) - .unwrap(); - self.send_screen_instructions - .send(ScreenInstruction::Render) - .unwrap(); + self.os_input + .send_to_server(ServerInstruction::Action(action)); } - Action::Resize(direction) => { - let screen_instr = match direction { - super::actions::Direction::Left => ScreenInstruction::ResizeLeft, - super::actions::Direction::Right => ScreenInstruction::ResizeRight, - super::actions::Direction::Up => ScreenInstruction::ResizeUp, - super::actions::Direction::Down => ScreenInstruction::ResizeDown, - }; - self.send_screen_instructions.send(screen_instr).unwrap(); + Action::CloseFocus + | Action::NewPane(_) + | Action::NewTab + | Action::GoToNextTab + | Action::GoToPreviousTab + | Action::CloseTab + | Action::GoToTab(_) => { + self.command_is_executing.blocking_input_thread(); + self.os_input + .send_to_server(ServerInstruction::Action(action)); + self.command_is_executing + .wait_until_input_thread_is_unblocked(); } - Action::SwitchFocus => { - self.send_screen_instructions - .send(ScreenInstruction::SwitchFocus) - .unwrap(); - } - Action::FocusNextPane => { - self.send_screen_instructions - .send(ScreenInstruction::FocusNextPane) - .unwrap(); - } - Action::FocusPreviousPane => { - self.send_screen_instructions - .send(ScreenInstruction::FocusPreviousPane) - .unwrap(); - } - Action::MoveFocus(direction) => { - let screen_instr = match direction { - super::actions::Direction::Left => ScreenInstruction::MoveFocusLeft, - super::actions::Direction::Right => ScreenInstruction::MoveFocusRight, - super::actions::Direction::Up => ScreenInstruction::MoveFocusUp, - super::actions::Direction::Down => ScreenInstruction::MoveFocusDown, - }; - self.send_screen_instructions.send(screen_instr).unwrap(); - } - Action::ScrollUp => { - self.send_screen_instructions - .send(ScreenInstruction::ScrollUp) - .unwrap(); - } - Action::ScrollDown => { - self.send_screen_instructions - .send(ScreenInstruction::ScrollDown) - .unwrap(); - } - Action::PageScrollUp => { - self.send_screen_instructions - .send(ScreenInstruction::PageScrollUp) - .unwrap(); - } - Action::PageScrollDown => { - self.send_screen_instructions - .send(ScreenInstruction::PageScrollDown) - .unwrap(); - } - Action::ToggleFocusFullscreen => { - self.send_screen_instructions - .send(ScreenInstruction::ToggleActiveTerminalFullscreen) - .unwrap(); - } - Action::NewPane(direction) => { - let pty_instr = match direction { - Some(super::actions::Direction::Left) => { - PtyInstruction::SpawnTerminalVertically(None) - } - Some(super::actions::Direction::Right) => { - PtyInstruction::SpawnTerminalVertically(None) - } - Some(super::actions::Direction::Up) => { - PtyInstruction::SpawnTerminalHorizontally(None) - } - Some(super::actions::Direction::Down) => { - PtyInstruction::SpawnTerminalHorizontally(None) - } - // No direction specified - try to put it in the biggest available spot - None => PtyInstruction::SpawnTerminal(None), - }; - self.command_is_executing.opening_new_pane(); - self.send_pty_instructions.send(pty_instr).unwrap(); - self.command_is_executing.wait_until_new_pane_is_opened(); - } - Action::CloseFocus => { - self.command_is_executing.closing_pane(); - self.send_screen_instructions - .send(ScreenInstruction::CloseFocusedPane) - .unwrap(); - self.command_is_executing.wait_until_pane_is_closed(); - } - Action::NewTab => { - self.command_is_executing.opening_new_pane(); - self.send_pty_instructions - .send(PtyInstruction::NewTab) - .unwrap(); - self.command_is_executing.wait_until_new_pane_is_opened(); - } - Action::GoToNextTab => { - self.send_screen_instructions - .send(ScreenInstruction::SwitchTabNext) - .unwrap(); - } - Action::GoToPreviousTab => { - self.send_screen_instructions - .send(ScreenInstruction::SwitchTabPrev) - .unwrap(); - } - Action::ToggleActiveSyncPanes => { - self.send_screen_instructions - .send(ScreenInstruction::ToggleActiveSyncPanes) - .unwrap(); - } - Action::CloseTab => { - self.command_is_executing.closing_pane(); - self.send_screen_instructions - .send(ScreenInstruction::CloseTab) - .unwrap(); - self.command_is_executing.wait_until_pane_is_closed(); - } - Action::GoToTab(i) => { - self.send_screen_instructions - .send(ScreenInstruction::GoToTab(i)) - .unwrap(); - } - Action::TabNameInput(c) => { - self.send_screen_instructions - .send(ScreenInstruction::UpdateTabName(c)) - .unwrap(); - } - Action::NoOp => {} + _ => self + .os_input + .send_to_server(ServerInstruction::Action(action)), } should_break @@ -278,8 +137,8 @@ impl InputHandler { /// Routine to be called when the input handler exits (at the moment this is the /// same as quitting Zellij). fn exit(&mut self) { - self.send_app_instructions - .send(AppInstruction::Exit) + self.send_client_instructions + .send(ClientInstruction::Exit) .unwrap(); } } @@ -328,22 +187,16 @@ pub fn get_mode_info(mode: InputMode, palette: Palette) -> ModeInfo { /// Entry point to the module. Instantiates an [`InputHandler`] and starts /// its [`InputHandler::handle_input()`] loop. pub fn input_loop( - os_input: Box, + os_input: Box, config: Config, command_is_executing: CommandIsExecuting, - send_screen_instructions: SenderWithContext, - send_pty_instructions: SenderWithContext, - send_plugin_instructions: SenderWithContext, - send_app_instructions: SenderWithContext, + send_client_instructions: SenderWithContext, ) { let _handler = InputHandler::new( os_input, command_is_executing, config, - send_screen_instructions, - send_pty_instructions, - send_plugin_instructions, - send_app_instructions, + send_client_instructions, ) .handle_input(); } diff --git a/src/common/ipc.rs b/src/common/ipc.rs index 81576b86..190680dc 100644 --- a/src/common/ipc.rs +++ b/src/common/ipc.rs @@ -1,7 +1,13 @@ //! IPC stuff for starting to split things into a client and server model. +use crate::common::errors::{get_current_ctx, ErrorContext}; +use interprocess::local_socket::LocalSocketStream; +use nix::unistd::dup; use serde::{Deserialize, Serialize}; use std::collections::HashSet; +use std::io::{self, Write}; +use std::marker::PhantomData; +use std::os::unix::io::{AsRawFd, FromRawFd}; type SessionId = u64; @@ -24,7 +30,7 @@ pub enum ClientType { // Types of messages sent from the client to the server #[derive(Serialize, Deserialize)] -pub enum ClientToServerMsg { +pub enum _ClientToServerMsg { // List which sessions are available ListSessions, // Create a new session @@ -45,3 +51,69 @@ pub enum _ServerToClientMsg { // A list of sessions SessionList(HashSet), } + +/// Sends messages on a stream socket, along with an [`ErrorContext`]. +pub struct IpcSenderWithContext { + sender: io::BufWriter, + _phantom: PhantomData, +} + +impl IpcSenderWithContext { + /// Returns a sender to the given [LocalSocketStream](interprocess::local_socket::LocalSocketStream). + pub fn new(sender: LocalSocketStream) -> Self { + Self { + sender: io::BufWriter::new(sender), + _phantom: PhantomData, + } + } + + /// Sends an event, along with the current [`ErrorContext`], on this [`IpcSenderWithContext`]'s socket. + pub fn send(&mut self, msg: T) { + let err_ctx = get_current_ctx(); + bincode::serialize_into(&mut self.sender, &(msg, err_ctx)).unwrap(); + self.sender.flush().unwrap(); + } + + /// Returns an [`IpcReceiverWithContext`] with the same socket as this sender. + pub fn get_receiver(&self) -> IpcReceiverWithContext + where + F: for<'de> Deserialize<'de> + Serialize, + { + let sock_fd = self.sender.get_ref().as_raw_fd(); + let dup_sock = dup(sock_fd).unwrap(); + let socket = unsafe { LocalSocketStream::from_raw_fd(dup_sock) }; + IpcReceiverWithContext::new(socket) + } +} + +/// Receives messages on a stream socket, along with an [`ErrorContext`]. +pub struct IpcReceiverWithContext { + receiver: io::BufReader, + _phantom: PhantomData, +} + +impl IpcReceiverWithContext +where + T: for<'de> Deserialize<'de> + Serialize, +{ + /// Returns a receiver to the given [LocalSocketStream](interprocess::local_socket::LocalSocketStream). + pub fn new(receiver: LocalSocketStream) -> Self { + Self { + receiver: io::BufReader::new(receiver), + _phantom: PhantomData, + } + } + + /// Receives an event, along with the current [`ErrorContext`], on this [`IpcReceiverWithContext`]'s socket. + pub fn recv(&mut self) -> (T, ErrorContext) { + bincode::deserialize_from(&mut self.receiver).unwrap() + } + + /// Returns an [`IpcSenderWithContext`] with the same socket as this receiver. + pub fn get_sender(&self) -> IpcSenderWithContext { + let sock_fd = self.receiver.get_ref().as_raw_fd(); + let dup_sock = dup(sock_fd).unwrap(); + let socket = unsafe { LocalSocketStream::from_raw_fd(dup_sock) }; + IpcSenderWithContext::new(socket) + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index 17c1f4cb..6061f1a6 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -9,49 +9,12 @@ pub mod setup; pub mod utils; pub mod wasm_vm; -use std::cell::RefCell; -use std::path::PathBuf; -use std::sync::mpsc; -use std::thread; -use std::{collections::HashMap, fs}; -use std::{ - collections::HashSet, - env, - io::Write, - str::FromStr, - sync::{Arc, Mutex}, -}; - -use crate::cli::CliArgs; -use crate::common::input::config::Config; -use crate::layout::Layout; use crate::panes::PaneId; +use crate::server::ServerInstruction; use async_std::task_local; -use command_is_executing::CommandIsExecuting; -use directories_next::ProjectDirs; -use errors::{ - get_current_ctx, AppContext, ContextType, ErrorContext, PluginContext, PtyContext, - ScreenContext, -}; -use input::handler::input_loop; -use os_input_output::OsApi; -use pty_bus::{PtyBus, PtyInstruction}; -use screen::{Screen, ScreenInstruction}; -use serde::{Deserialize, Serialize}; -use setup::install; -use utils::consts::ZELLIJ_IPC_PIPE; -use wasm_vm::{wasi_read_string, wasi_write_object, zellij_exports, PluginEnv, PluginInstruction}; -use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value}; -use wasmer_wasi::{Pipe, WasiState}; -use zellij_tile::data::{EventType, InputMode, ModeInfo}; - -#[derive(Serialize, Deserialize, Debug)] -pub enum ApiCommand { - OpenFile(PathBuf), - SplitHorizontally, - SplitVertically, - MoveFocus, -} +use errors::{get_current_ctx, ErrorContext}; +use std::cell::RefCell; +use std::sync::mpsc; /// An [MPSC](mpsc) asynchronous channel with added error context. pub type ChannelWithContext = ( @@ -66,7 +29,7 @@ pub type SyncChannelWithContext = ( /// Wrappers around the two standard [MPSC](mpsc) sender types, [`mpsc::Sender`] and [`mpsc::SyncSender`], with an additional [`ErrorContext`]. #[derive(Clone)] -enum SenderType { +pub enum SenderType { /// A wrapper around an [`mpsc::Sender`], adding an [`ErrorContext`]. Sender(mpsc::Sender<(T, ErrorContext)>), /// A wrapper around an [`mpsc::SyncSender`], adding an [`ErrorContext`]. @@ -81,7 +44,7 @@ pub struct SenderWithContext { } impl SenderWithContext { - fn new(sender: SenderType) -> Self { + pub fn new(sender: SenderType) -> Self { Self { sender } } @@ -102,7 +65,7 @@ unsafe impl Sync for SenderWithContext {} thread_local!( /// A key to some thread local storage (TLS) that holds a representation of the thread's call /// stack in the form of an [`ErrorContext`]. - static OPENCALLS: RefCell = RefCell::default() + pub static OPENCALLS: RefCell = RefCell::default() ); task_local! { @@ -110,600 +73,3 @@ task_local! { /// stack in the form of an [`ErrorContext`]. static ASYNCOPENCALLS: RefCell = RefCell::default() } - -/// Instructions related to the entire application. -#[derive(Clone)] -pub enum AppInstruction { - Exit, - Error(String), -} - -/// Start Zellij with the specified [`OsApi`] and command-line arguments. -// FIXME this should definitely be modularized and split into different functions. -pub fn start(mut os_input: Box, opts: CliArgs, config: Config) { - let take_snapshot = "\u{1b}[?1049h"; - os_input.unset_raw_mode(0); - let _ = os_input - .get_stdout_writer() - .write(take_snapshot.as_bytes()) - .unwrap(); - - env::set_var(&"ZELLIJ", "0"); - - let command_is_executing = CommandIsExecuting::new(); - - let full_screen_ws = os_input.get_terminal_size_using_fd(0); - os_input.set_raw_mode(0); - let (send_screen_instructions, receive_screen_instructions): ChannelWithContext< - ScreenInstruction, - > = mpsc::channel(); - let send_screen_instructions = - SenderWithContext::new(SenderType::Sender(send_screen_instructions)); - - let (send_pty_instructions, receive_pty_instructions): ChannelWithContext = - mpsc::channel(); - let send_pty_instructions = SenderWithContext::new(SenderType::Sender(send_pty_instructions)); - - let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext< - PluginInstruction, - > = mpsc::channel(); - let send_plugin_instructions = - SenderWithContext::new(SenderType::Sender(send_plugin_instructions)); - - let (send_app_instructions, receive_app_instructions): SyncChannelWithContext = - mpsc::sync_channel(0); - let send_app_instructions = - SenderWithContext::new(SenderType::SyncSender(send_app_instructions)); - - let mut pty_bus = PtyBus::new( - receive_pty_instructions, - send_screen_instructions.clone(), - send_plugin_instructions.clone(), - os_input.clone(), - opts.debug, - ); - - // Determine and initialize the data directory - let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); - let data_dir = opts - .data_dir - .unwrap_or_else(|| project_dirs.data_dir().to_path_buf()); - install::populate_data_dir(&data_dir); - - // Don't use default layouts in tests, but do everywhere else - #[cfg(not(test))] - let default_layout = Some(PathBuf::from("default")); - #[cfg(test)] - let default_layout = None; - let maybe_layout = opts - .layout - .map(|p| Layout::new(&p, &data_dir)) - .or_else(|| default_layout.map(|p| Layout::from_defaults(&p, &data_dir))); - - #[cfg(not(test))] - std::panic::set_hook({ - use crate::errors::handle_panic; - let send_app_instructions = send_app_instructions.clone(); - Box::new(move |info| { - handle_panic(info, &send_app_instructions); - }) - }); - - let pty_thread = thread::Builder::new() - .name("pty".to_string()) - .spawn({ - let mut command_is_executing = command_is_executing.clone(); - send_pty_instructions.send(PtyInstruction::NewTab).unwrap(); - move || loop { - let (event, mut err_ctx) = pty_bus - .receive_pty_instructions - .recv() - .expect("failed to receive event on channel"); - err_ctx.add_call(ContextType::Pty(PtyContext::from(&event))); - match event { - PtyInstruction::SpawnTerminal(file_to_open) => { - let pid = pty_bus.spawn_terminal(file_to_open); - pty_bus - .send_screen_instructions - .send(ScreenInstruction::NewPane(PaneId::Terminal(pid))) - .unwrap(); - } - PtyInstruction::SpawnTerminalVertically(file_to_open) => { - let pid = pty_bus.spawn_terminal(file_to_open); - pty_bus - .send_screen_instructions - .send(ScreenInstruction::VerticalSplit(PaneId::Terminal(pid))) - .unwrap(); - } - PtyInstruction::SpawnTerminalHorizontally(file_to_open) => { - let pid = pty_bus.spawn_terminal(file_to_open); - pty_bus - .send_screen_instructions - .send(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid))) - .unwrap(); - } - PtyInstruction::NewTab => { - if let Some(layout) = maybe_layout.clone() { - pty_bus.spawn_terminals_for_layout(layout); - } else { - let pid = pty_bus.spawn_terminal(None); - pty_bus - .send_screen_instructions - .send(ScreenInstruction::NewTab(pid)) - .unwrap(); - } - } - PtyInstruction::ClosePane(id) => { - pty_bus.close_pane(id); - command_is_executing.done_closing_pane(); - } - PtyInstruction::CloseTab(ids) => { - pty_bus.close_tab(ids); - command_is_executing.done_closing_pane(); - } - PtyInstruction::Quit => { - break; - } - } - } - }) - .unwrap(); - - let screen_thread = thread::Builder::new() - .name("screen".to_string()) - .spawn({ - let mut command_is_executing = command_is_executing.clone(); - let os_input = os_input.clone(); - let send_pty_instructions = send_pty_instructions.clone(); - let send_plugin_instructions = send_plugin_instructions.clone(); - let send_app_instructions = send_app_instructions.clone(); - let max_panes = opts.max_panes; - let colors = os_input.load_palette(); - move || { - let mut screen = Screen::new( - receive_screen_instructions, - send_pty_instructions, - send_plugin_instructions, - send_app_instructions, - &full_screen_ws, - os_input, - max_panes, - ModeInfo { - palette: colors, - ..ModeInfo::default() - }, - InputMode::Normal, - colors, - ); - loop { - let (event, mut err_ctx) = screen - .receiver - .recv() - .expect("failed to receive event on channel"); - err_ctx.add_call(ContextType::Screen(ScreenContext::from(&event))); - match event { - ScreenInstruction::PtyBytes(pid, vte_bytes) => { - let active_tab = screen.get_active_tab_mut().unwrap(); - if active_tab.has_terminal_pid(pid) { - // it's most likely that this event is directed at the active tab - // look there first - active_tab.handle_pty_bytes(pid, vte_bytes); - } else { - // if this event wasn't directed at the active tab, start looking - // in other tabs - let all_tabs = screen.get_tabs_mut(); - for tab in all_tabs.values_mut() { - if tab.has_terminal_pid(pid) { - tab.handle_pty_bytes(pid, vte_bytes); - break; - } - } - } - } - ScreenInstruction::Render => { - screen.render(); - } - ScreenInstruction::NewPane(pid) => { - screen.get_active_tab_mut().unwrap().new_pane(pid); - command_is_executing.done_opening_new_pane(); - } - ScreenInstruction::HorizontalSplit(pid) => { - screen.get_active_tab_mut().unwrap().horizontal_split(pid); - command_is_executing.done_opening_new_pane(); - } - ScreenInstruction::VerticalSplit(pid) => { - screen.get_active_tab_mut().unwrap().vertical_split(pid); - command_is_executing.done_opening_new_pane(); - } - ScreenInstruction::WriteCharacter(bytes) => { - let active_tab = screen.get_active_tab_mut().unwrap(); - match active_tab.is_sync_panes_active() { - true => active_tab.write_to_terminals_on_current_tab(bytes), - false => active_tab.write_to_active_terminal(bytes), - } - } - ScreenInstruction::ResizeLeft => { - screen.get_active_tab_mut().unwrap().resize_left(); - } - ScreenInstruction::ResizeRight => { - screen.get_active_tab_mut().unwrap().resize_right(); - } - ScreenInstruction::ResizeDown => { - screen.get_active_tab_mut().unwrap().resize_down(); - } - ScreenInstruction::ResizeUp => { - screen.get_active_tab_mut().unwrap().resize_up(); - } - ScreenInstruction::SwitchFocus => { - screen.get_active_tab_mut().unwrap().move_focus(); - } - ScreenInstruction::FocusNextPane => { - screen.get_active_tab_mut().unwrap().focus_next_pane(); - } - ScreenInstruction::FocusPreviousPane => { - screen.get_active_tab_mut().unwrap().focus_previous_pane(); - } - ScreenInstruction::MoveFocusLeft => { - screen.get_active_tab_mut().unwrap().move_focus_left(); - } - ScreenInstruction::MoveFocusDown => { - screen.get_active_tab_mut().unwrap().move_focus_down(); - } - ScreenInstruction::MoveFocusRight => { - screen.get_active_tab_mut().unwrap().move_focus_right(); - } - ScreenInstruction::MoveFocusUp => { - screen.get_active_tab_mut().unwrap().move_focus_up(); - } - ScreenInstruction::ScrollUp => { - screen - .get_active_tab_mut() - .unwrap() - .scroll_active_terminal_up(); - } - ScreenInstruction::ScrollDown => { - screen - .get_active_tab_mut() - .unwrap() - .scroll_active_terminal_down(); - } - ScreenInstruction::PageScrollUp => { - screen - .get_active_tab_mut() - .unwrap() - .scroll_active_terminal_up_page(); - } - ScreenInstruction::PageScrollDown => { - screen - .get_active_tab_mut() - .unwrap() - .scroll_active_terminal_down_page(); - } - ScreenInstruction::ClearScroll => { - screen - .get_active_tab_mut() - .unwrap() - .clear_active_terminal_scroll(); - } - ScreenInstruction::CloseFocusedPane => { - screen.get_active_tab_mut().unwrap().close_focused_pane(); - screen.render(); - } - ScreenInstruction::SetSelectable(id, selectable) => { - screen - .get_active_tab_mut() - .unwrap() - .set_pane_selectable(id, selectable); - } - ScreenInstruction::SetMaxHeight(id, max_height) => { - screen - .get_active_tab_mut() - .unwrap() - .set_pane_max_height(id, max_height); - } - ScreenInstruction::SetInvisibleBorders(id, invisible_borders) => { - screen - .get_active_tab_mut() - .unwrap() - .set_pane_invisible_borders(id, invisible_borders); - screen.render(); - } - ScreenInstruction::ClosePane(id) => { - screen.get_active_tab_mut().unwrap().close_pane(id); - screen.render(); - } - ScreenInstruction::ToggleActiveTerminalFullscreen => { - screen - .get_active_tab_mut() - .unwrap() - .toggle_active_pane_fullscreen(); - } - ScreenInstruction::NewTab(pane_id) => { - screen.new_tab(pane_id); - command_is_executing.done_opening_new_pane(); - } - ScreenInstruction::SwitchTabNext => screen.switch_tab_next(), - ScreenInstruction::SwitchTabPrev => screen.switch_tab_prev(), - ScreenInstruction::CloseTab => screen.close_tab(), - ScreenInstruction::ApplyLayout((layout, new_pane_pids)) => { - screen.apply_layout(layout, new_pane_pids); - command_is_executing.done_opening_new_pane(); - } - ScreenInstruction::GoToTab(tab_index) => { - screen.go_to_tab(tab_index as usize) - } - ScreenInstruction::UpdateTabName(c) => { - screen.update_active_tab_name(c); - } - ScreenInstruction::TerminalResize => { - screen.resize_to_screen(); - } - ScreenInstruction::ChangeMode(mode_info) => { - screen.change_mode(mode_info); - } - ScreenInstruction::ToggleActiveSyncPanes => { - screen - .get_active_tab_mut() - .unwrap() - .toggle_sync_panes_is_active(); - screen.update_tabs(); - } - ScreenInstruction::Quit => { - break; - } - } - } - } - }) - .unwrap(); - - let wasm_thread = thread::Builder::new() - .name("wasm".to_string()) - .spawn({ - let send_pty_instructions = send_pty_instructions.clone(); - let send_screen_instructions = send_screen_instructions.clone(); - let send_app_instructions = send_app_instructions.clone(); - let send_plugin_instructions = send_plugin_instructions.clone(); - - let store = Store::default(); - let mut plugin_id = 0; - let mut plugin_map = HashMap::new(); - move || loop { - let (event, mut err_ctx) = receive_plugin_instructions - .recv() - .expect("failed to receive event on channel"); - err_ctx.add_call(ContextType::Plugin(PluginContext::from(&event))); - match event { - PluginInstruction::Load(pid_tx, path) => { - let plugin_dir = data_dir.join("plugins/"); - let wasm_bytes = fs::read(&path) - .or_else(|_| fs::read(&path.with_extension("wasm"))) - .or_else(|_| fs::read(&plugin_dir.join(&path).with_extension("wasm"))) - .unwrap_or_else(|_| panic!("cannot find plugin {}", &path.display())); - - // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that - let module = Module::new(&store, &wasm_bytes).unwrap(); - - let output = Pipe::new(); - let input = Pipe::new(); - let mut wasi_env = WasiState::new("Zellij") - .env("CLICOLOR_FORCE", "1") - .preopen(|p| { - p.directory(".") // FIXME: Change this to a more meaningful dir - .alias(".") - .read(true) - .write(true) - .create(true) - }) - .unwrap() - .stdin(Box::new(input)) - .stdout(Box::new(output)) - .finalize() - .unwrap(); - - let wasi = wasi_env.import_object(&module).unwrap(); - - let plugin_env = PluginEnv { - plugin_id, - send_pty_instructions: send_pty_instructions.clone(), - send_screen_instructions: send_screen_instructions.clone(), - send_app_instructions: send_app_instructions.clone(), - send_plugin_instructions: send_plugin_instructions.clone(), - wasi_env, - subscriptions: Arc::new(Mutex::new(HashSet::new())), - }; - - let zellij = zellij_exports(&store, &plugin_env); - let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); - - let start = instance.exports.get_function("_start").unwrap(); - - // This eventually calls the `.load()` method - start.call(&[]).unwrap(); - - plugin_map.insert(plugin_id, (instance, plugin_env)); - pid_tx.send(plugin_id).unwrap(); - plugin_id += 1; - } - PluginInstruction::Update(pid, event) => { - for (&i, (instance, plugin_env)) in &plugin_map { - let subs = plugin_env.subscriptions.lock().unwrap(); - // FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType? - let event_type = EventType::from_str(&event.to_string()).unwrap(); - if (pid.is_none() || pid == Some(i)) && subs.contains(&event_type) { - let update = instance.exports.get_function("update").unwrap(); - wasi_write_object(&plugin_env.wasi_env, &event); - update.call(&[]).unwrap(); - } - } - drop(send_screen_instructions.send(ScreenInstruction::Render)); - } - PluginInstruction::Render(buf_tx, pid, rows, cols) => { - let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); - - let render = instance.exports.get_function("render").unwrap(); - - render - .call(&[Value::I32(rows as i32), Value::I32(cols as i32)]) - .unwrap(); - - buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap(); - } - PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), - PluginInstruction::Quit => break, - } - } - }) - .unwrap(); - - let _signal_thread = thread::Builder::new() - .name("signal_listener".to_string()) - .spawn({ - let os_input = os_input.clone(); - let send_screen_instructions = send_screen_instructions.clone(); - move || { - os_input.receive_sigwinch(Box::new(move || { - let _ = send_screen_instructions.send(ScreenInstruction::TerminalResize); - })); - } - }) - .unwrap(); - - // TODO: currently we don't wait for this to quit - // because otherwise the app will hang. Need to fix this so it both - // listens to the ipc-bus and is able to quit cleanly - #[cfg(not(test))] - let _ipc_thread = thread::Builder::new() - .name("ipc_server".to_string()) - .spawn({ - use std::io::Read; - let send_pty_instructions = send_pty_instructions.clone(); - let send_screen_instructions = send_screen_instructions.clone(); - move || { - std::fs::remove_file(ZELLIJ_IPC_PIPE).ok(); - let listener = std::os::unix::net::UnixListener::bind(ZELLIJ_IPC_PIPE) - .expect("could not listen on ipc socket"); - let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); - err_ctx.add_call(ContextType::IpcServer); - - for stream in listener.incoming() { - match stream { - Ok(mut stream) => { - let mut buffer = [0; 65535]; // TODO: more accurate - let _ = stream - .read(&mut buffer) - .expect("failed to parse ipc message"); - let decoded: ApiCommand = bincode::deserialize(&buffer) - .expect("failed to deserialize ipc message"); - match &decoded { - ApiCommand::OpenFile(file_name) => { - let path = PathBuf::from(file_name); - send_pty_instructions - .send(PtyInstruction::SpawnTerminal(Some(path))) - .unwrap(); - } - ApiCommand::SplitHorizontally => { - send_pty_instructions - .send(PtyInstruction::SpawnTerminalHorizontally(None)) - .unwrap(); - } - ApiCommand::SplitVertically => { - send_pty_instructions - .send(PtyInstruction::SpawnTerminalVertically(None)) - .unwrap(); - } - ApiCommand::MoveFocus => { - send_screen_instructions - .send(ScreenInstruction::FocusNextPane) - .unwrap(); - } - } - } - Err(err) => { - panic!("err {:?}", err); - } - } - } - } - }) - .unwrap(); - - let _stdin_thread = thread::Builder::new() - .name("stdin_handler".to_string()) - .spawn({ - let send_screen_instructions = send_screen_instructions.clone(); - let send_pty_instructions = send_pty_instructions.clone(); - let send_plugin_instructions = send_plugin_instructions.clone(); - let os_input = os_input.clone(); - let config = config; - move || { - input_loop( - os_input, - config, - command_is_executing, - send_screen_instructions, - send_pty_instructions, - send_plugin_instructions, - send_app_instructions, - ) - } - }); - - #[warn(clippy::never_loop)] - loop { - let (app_instruction, mut err_ctx) = receive_app_instructions - .recv() - .expect("failed to receive app instruction on channel"); - - err_ctx.add_call(ContextType::App(AppContext::from(&app_instruction))); - match app_instruction { - AppInstruction::Exit => { - break; - } - AppInstruction::Error(backtrace) => { - let _ = send_screen_instructions.send(ScreenInstruction::Quit); - let _ = screen_thread.join(); - let _ = send_pty_instructions.send(PtyInstruction::Quit); - let _ = pty_thread.join(); - let _ = send_plugin_instructions.send(PluginInstruction::Quit); - let _ = wasm_thread.join(); - os_input.unset_raw_mode(0); - let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); - let restore_snapshot = "\u{1b}[?1049l"; - let error = format!( - "{}\n{}{}", - goto_start_of_last_line, restore_snapshot, backtrace - ); - let _ = os_input - .get_stdout_writer() - .write(error.as_bytes()) - .unwrap(); - std::process::exit(1); - } - } - } - - let _ = send_pty_instructions.send(PtyInstruction::Quit); - pty_thread.join().unwrap(); - let _ = send_screen_instructions.send(ScreenInstruction::Quit); - screen_thread.join().unwrap(); - let _ = send_plugin_instructions.send(PluginInstruction::Quit); - wasm_thread.join().unwrap(); - - // cleanup(); - let reset_style = "\u{1b}[m"; - let show_cursor = "\u{1b}[?25h"; - let restore_snapshot = "\u{1b}[?1049l"; - let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); - let goodbye_message = format!( - "{}\n{}{}{}Bye from Zellij!\n", - goto_start_of_last_line, restore_snapshot, reset_style, show_cursor - ); - - os_input.unset_raw_mode(0); - let _ = os_input - .get_stdout_writer() - .write(goodbye_message.as_bytes()) - .unwrap(); - os_input.get_stdout_writer().flush().unwrap(); -} diff --git a/src/common/os_input_output.rs b/src/common/os_input_output.rs index 74d14036..5539ae70 100644 --- a/src/common/os_input_output.rs +++ b/src/common/os_input_output.rs @@ -1,23 +1,35 @@ +use crate::client::ClientInstruction; +use crate::common::{ + ipc::{IpcReceiverWithContext, IpcSenderWithContext}, + utils::consts::ZELLIJ_IPC_PIPE, +}; +use crate::errors::ErrorContext; use crate::panes::PositionAndSize; +use crate::server::ServerInstruction; use crate::utils::shared::default_palette; +use interprocess::local_socket::LocalSocketStream; use nix::fcntl::{fcntl, FcntlArg, OFlag}; use nix::pty::{forkpty, Winsize}; use nix::sys::signal::{kill, Signal}; use nix::sys::termios; use nix::sys::wait::waitpid; -use nix::unistd; -use nix::unistd::{ForkResult, Pid}; -use std::io; +use nix::unistd::{self, ForkResult, Pid}; +use signal_hook::{consts::signal::*, iterator::Signals}; use std::io::prelude::*; -use std::os::unix::io::RawFd; -use std::path::PathBuf; +use std::os::unix::{fs::PermissionsExt, io::RawFd}; +use std::path::{Path, PathBuf}; use std::process::{Child, Command}; use std::sync::{Arc, Mutex}; +use std::{env, fs, io}; use zellij_tile::data::Palette; -use signal_hook::{consts::signal::*, iterator::Signals}; +const UNIX_PERMISSIONS: u32 = 0o700; -use std::env; +pub fn set_permissions(path: &Path) { + let mut permissions = fs::metadata(path).unwrap().permissions(); + permissions.set_mode(UNIX_PERMISSIONS); + fs::set_permissions(path, permissions).unwrap(); +} fn into_raw_mode(pid: RawFd) { let mut tio = termios::tcgetattr(pid).expect("could not get terminal attribute"); @@ -156,23 +168,17 @@ fn spawn_terminal(file_to_open: Option, orig_termios: termios::Termios) } #[derive(Clone)] -pub struct OsInputOutput { +pub struct ServerOsInputOutput { orig_termios: Arc>, + receive_instructions_from_client: Option>>>, + send_instructions_to_client: Arc>>>, } -/// The `OsApi` trait represents an abstract interface to the features of an operating system that -/// Zellij requires. -pub trait OsApi: Send + Sync { - /// Returns the size of the terminal associated to file descriptor `fd`. - fn get_terminal_size_using_fd(&self, fd: RawFd) -> PositionAndSize; +/// 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(&mut self, fd: RawFd, cols: u16, rows: u16); - /// Set the terminal associated to file descriptor `fd` to - /// [raw mode](https://en.wikipedia.org/wiki/Terminal_mode). - fn set_raw_mode(&mut self, fd: RawFd); - /// Set the terminal associated to file descriptor `fd` to - /// [cooked mode](https://en.wikipedia.org/wiki/Terminal_mode). - fn unset_raw_mode(&mut self, fd: RawFd); /// Spawn a new terminal, with an optional file to open in a terminal program. fn spawn_terminal(&mut self, file_to_open: Option) -> (RawFd, RawFd); /// Read bytes from the standard output of the virtual terminal referred to by `fd`. @@ -186,30 +192,23 @@ pub trait OsApi: Send + Sync { // or a nix::unistd::Pid. See `man kill.3`, nix::sys::signal::kill (both take an argument // called `pid` and of type `pid_t`, and not `fd`) fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error>; - /// Returns the raw contents of standard input. - fn read_from_stdin(&self) -> Vec; - /// Returns the writer that allows writing to standard output. - fn get_stdout_writer(&self) -> Box; - /// Returns a [`Box`] pointer to this [`OsApi`] struct. - fn box_clone(&self) -> Box; - fn receive_sigwinch(&self, cb: Box); + /// Returns a [`Box`] pointer to this [`ServerOsApi`] struct. + fn box_clone(&self) -> Box; + /// Receives a message on server-side IPC channel + fn recv_from_client(&self) -> (ServerInstruction, ErrorContext); + /// Sends a message to client + fn send_to_client(&self, msg: ClientInstruction); + /// Adds a sender to client + fn add_client_sender(&mut self); + /// Update the receiver socket for the client + fn update_receiver(&mut self, stream: LocalSocketStream); fn load_palette(&self) -> Palette; } -impl OsApi for OsInputOutput { - fn get_terminal_size_using_fd(&self, fd: RawFd) -> PositionAndSize { - get_terminal_size_using_fd(fd) - } +impl ServerOsApi for ServerOsInputOutput { fn set_terminal_size_using_fd(&mut self, fd: RawFd, cols: u16, rows: u16) { set_terminal_size_using_fd(fd, cols, rows); } - fn set_raw_mode(&mut self, fd: RawFd) { - into_raw_mode(fd); - } - fn unset_raw_mode(&mut self, fd: RawFd) { - let orig_termios = self.orig_termios.lock().unwrap(); - unset_raw_mode(fd, orig_termios.clone()); - } fn spawn_terminal(&mut self, file_to_open: Option) -> (RawFd, RawFd) { let orig_termios = self.orig_termios.lock().unwrap(); spawn_terminal(file_to_open, orig_termios.clone()) @@ -223,7 +222,118 @@ impl OsApi for OsInputOutput { fn tcdrain(&mut self, fd: RawFd) -> Result<(), nix::Error> { termios::tcdrain(fd) } - fn box_clone(&self) -> Box { + fn box_clone(&self) -> Box { + Box::new((*self).clone()) + } + fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error> { + // TODO: + // Ideally, we should be using SIGINT rather than SIGKILL here, but there are cases in which + // the terminal we're trying to kill hangs on SIGINT and so all the app gets stuck + // that's why we're sending SIGKILL here + // A better solution would be to send SIGINT here and not wait for it, and then have + // a background thread do the waitpid stuff and send SIGKILL if the process is stuck + kill(Pid::from_raw(pid), Some(Signal::SIGKILL)).unwrap(); + waitpid(Pid::from_raw(pid), None).unwrap(); + Ok(()) + } + fn recv_from_client(&self) -> (ServerInstruction, ErrorContext) { + self.receive_instructions_from_client + .as_ref() + .unwrap() + .lock() + .unwrap() + .recv() + } + fn send_to_client(&self, msg: ClientInstruction) { + self.send_instructions_to_client + .lock() + .unwrap() + .as_mut() + .unwrap() + .send(msg); + } + fn add_client_sender(&mut self) { + assert!(self.send_instructions_to_client.lock().unwrap().is_none()); + let sender = self + .receive_instructions_from_client + .as_ref() + .unwrap() + .lock() + .unwrap() + .get_sender(); + *self.send_instructions_to_client.lock().unwrap() = Some(sender); + } + fn update_receiver(&mut self, stream: LocalSocketStream) { + self.receive_instructions_from_client = + Some(Arc::new(Mutex::new(IpcReceiverWithContext::new(stream)))); + } + fn load_palette(&self) -> Palette { + default_palette() + } +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.box_clone() + } +} + +pub fn get_server_os_input() -> ServerOsInputOutput { + let current_termios = termios::tcgetattr(0).unwrap(); + let orig_termios = Arc::new(Mutex::new(current_termios)); + ServerOsInputOutput { + orig_termios, + receive_instructions_from_client: None, + send_instructions_to_client: Arc::new(Mutex::new(None)), + } +} + +#[derive(Clone)] +pub struct ClientOsInputOutput { + orig_termios: Arc>, + send_instructions_to_server: Arc>>>, + receive_instructions_from_server: Arc>>>, +} + +/// The `ClientOsApi` trait represents an abstract interface to the features of an operating system that +/// Zellij client requires. +pub trait ClientOsApi: Send + Sync { + /// Returns the size of the terminal associated to file descriptor `fd`. + fn get_terminal_size_using_fd(&self, fd: RawFd) -> PositionAndSize; + /// Set the terminal associated to file descriptor `fd` to + /// [raw mode](https://en.wikipedia.org/wiki/Terminal_mode). + fn set_raw_mode(&mut self, fd: RawFd); + /// Set the terminal associated to file descriptor `fd` to + /// [cooked mode](https://en.wikipedia.org/wiki/Terminal_mode). + fn unset_raw_mode(&mut self, fd: RawFd); + /// Returns the writer that allows writing to standard output. + fn get_stdout_writer(&self) -> Box; + /// Returns the raw contents of standard input. + fn read_from_stdin(&self) -> Vec; + /// Returns a [`Box`] pointer to this [`ClientOsApi`] struct. + fn box_clone(&self) -> Box; + /// Sends a message to the server. + fn send_to_server(&self, msg: ServerInstruction); + /// Receives a message on client-side IPC channel + // This should be called from the client-side router thread only. + fn recv_from_server(&self) -> (ClientInstruction, ErrorContext); + fn receive_sigwinch(&self, cb: Box); + /// Establish a connection with the server socket. + fn connect_to_server(&self); +} + +impl ClientOsApi for ClientOsInputOutput { + fn get_terminal_size_using_fd(&self, fd: RawFd) -> PositionAndSize { + get_terminal_size_using_fd(fd) + } + fn set_raw_mode(&mut self, fd: RawFd) { + into_raw_mode(fd); + } + fn unset_raw_mode(&mut self, fd: RawFd) { + let orig_termios = self.orig_termios.lock().unwrap(); + unset_raw_mode(fd, orig_termios.clone()); + } + fn box_clone(&self) -> Box { Box::new((*self).clone()) } fn read_from_stdin(&self) -> Vec { @@ -239,16 +349,21 @@ impl OsApi for OsInputOutput { let stdout = ::std::io::stdout(); Box::new(stdout) } - fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error> { - // TODO: - // Ideally, we should be using SIGINT rather than SIGKILL here, but there are cases in which - // the terminal we're trying to kill hangs on SIGINT and so all the app gets stuck - // that's why we're sending SIGKILL here - // A better solution would be to send SIGINT here and not wait for it, and then have - // a background thread do the waitpid stuff and send SIGKILL if the process is stuck - kill(Pid::from_raw(pid), Some(Signal::SIGKILL)).unwrap(); - waitpid(Pid::from_raw(pid), None).unwrap(); - Ok(()) + fn send_to_server(&self, msg: ServerInstruction) { + self.send_instructions_to_server + .lock() + .unwrap() + .as_mut() + .unwrap() + .send(msg); + } + fn recv_from_server(&self) -> (ClientInstruction, ErrorContext) { + self.receive_instructions_from_server + .lock() + .unwrap() + .as_mut() + .unwrap() + .recv() } fn receive_sigwinch(&self, cb: Box) { let mut signals = Signals::new(&[SIGWINCH, SIGTERM, SIGINT, SIGQUIT]).unwrap(); @@ -264,19 +379,33 @@ impl OsApi for OsInputOutput { } } } - fn load_palette(&self) -> Palette { - default_palette() + fn connect_to_server(&self) { + let socket = match LocalSocketStream::connect(&**ZELLIJ_IPC_PIPE) { + Ok(sock) => sock, + Err(_) => { + std::thread::sleep(std::time::Duration::from_millis(20)); + LocalSocketStream::connect(&**ZELLIJ_IPC_PIPE).unwrap() + } + }; + let sender = IpcSenderWithContext::new(socket); + let receiver = sender.get_receiver(); + *self.send_instructions_to_server.lock().unwrap() = Some(sender); + *self.receive_instructions_from_server.lock().unwrap() = Some(receiver); } } -impl Clone for Box { - fn clone(&self) -> Box { +impl Clone for Box { + fn clone(&self) -> Box { self.box_clone() } } -pub fn get_os_input() -> OsInputOutput { +pub fn get_client_os_input() -> ClientOsInputOutput { let current_termios = termios::tcgetattr(0).unwrap(); let orig_termios = Arc::new(Mutex::new(current_termios)); - OsInputOutput { orig_termios } + ClientOsInputOutput { + orig_termios, + send_instructions_to_server: Arc::new(Mutex::new(None)), + receive_instructions_from_server: Arc::new(Mutex::new(None)), + } } diff --git a/src/common/pty_bus.rs b/src/common/pty_bus.rs index 2c132228..31696186 100644 --- a/src/common/pty_bus.rs +++ b/src/common/pty_bus.rs @@ -8,22 +8,24 @@ use ::std::sync::mpsc::Receiver; use ::std::time::{Duration, Instant}; use std::path::PathBuf; -use super::{ScreenInstruction, SenderWithContext}; -use crate::os_input_output::OsApi; +use super::SenderWithContext; +use crate::layout::Layout; +use crate::os_input_output::ServerOsApi; use crate::utils::logging::debug_to_file; use crate::{ errors::{get_current_ctx, ContextType, ErrorContext}, panes::PaneId, + screen::ScreenInstruction, + wasm_vm::PluginInstruction, }; -use crate::{layout::Layout, wasm_vm::PluginInstruction}; pub struct ReadFromPid { pid: RawFd, - os_input: Box, + os_input: Box, } impl ReadFromPid { - pub fn new(pid: &RawFd, os_input: Box) -> ReadFromPid { + pub fn new(pid: &RawFd, os_input: Box) -> ReadFromPid { ReadFromPid { pid: *pid, os_input, @@ -74,15 +76,15 @@ pub enum PtyInstruction { NewTab, ClosePane(PaneId), CloseTab(Vec), - Quit, + Exit, } pub struct PtyBus { - pub send_screen_instructions: SenderWithContext, - pub send_plugin_instructions: SenderWithContext, pub receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>, pub id_to_child_pid: HashMap, - os_input: Box, + pub send_screen_instructions: SenderWithContext, + send_plugin_instructions: SenderWithContext, + os_input: Box, debug_to_file: bool, task_handles: HashMap>, } @@ -90,7 +92,7 @@ pub struct PtyBus { fn stream_terminal_bytes( pid: RawFd, send_screen_instructions: SenderWithContext, - os_input: Box, + os_input: Box, debug: bool, ) -> JoinHandle<()> { let mut err_ctx = get_current_ctx(); @@ -122,7 +124,9 @@ fn stream_terminal_bytes( Some(receive_time) => { if receive_time.elapsed() > max_render_pause { pending_render = false; - let _ = send_screen_instructions.send(ScreenInstruction::Render); + send_screen_instructions + .send(ScreenInstruction::Render) + .unwrap(); last_byte_receive_time = Some(Instant::now()); } else { pending_render = true; @@ -136,7 +140,9 @@ fn stream_terminal_bytes( } else { if pending_render { pending_render = false; - let _ = send_screen_instructions.send(ScreenInstruction::Render); + send_screen_instructions + .send(ScreenInstruction::Render) + .unwrap(); } last_byte_receive_time = None; task::sleep(::std::time::Duration::from_millis(10)).await; @@ -161,15 +167,15 @@ impl PtyBus { receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>, send_screen_instructions: SenderWithContext, send_plugin_instructions: SenderWithContext, - os_input: Box, + os_input: Box, debug_to_file: bool, ) -> Self { PtyBus { - send_screen_instructions, - send_plugin_instructions, receive_pty_instructions, os_input, id_to_child_pid: HashMap::new(), + send_screen_instructions, + send_plugin_instructions, debug_to_file, task_handles: HashMap::new(), } @@ -196,10 +202,10 @@ impl PtyBus { new_pane_pids.push(pid_primary); } self.send_screen_instructions - .send(ScreenInstruction::ApplyLayout(( + .send(ScreenInstruction::ApplyLayout( layout, new_pane_pids.clone(), - ))) + )) .unwrap(); for id in new_pane_pids { let task_handle = stream_terminal_bytes( @@ -221,10 +227,10 @@ impl PtyBus { handle.cancel().await; }); } - PaneId::Plugin(pid) => drop( - self.send_plugin_instructions - .send(PluginInstruction::Unload(pid)), - ), + PaneId::Plugin(pid) => self + .send_plugin_instructions + .send(PluginInstruction::Unload(pid)) + .unwrap(), } } pub fn close_tab(&mut self, ids: Vec) { diff --git a/src/common/screen.rs b/src/common/screen.rs index e6cf73f5..562b0ffe 100644 --- a/src/common/screen.rs +++ b/src/common/screen.rs @@ -5,10 +5,11 @@ use std::os::unix::io::RawFd; use std::str; use std::sync::mpsc::Receiver; -use super::{AppInstruction, SenderWithContext}; -use crate::os_input_output::OsApi; +use crate::common::SenderWithContext; +use crate::os_input_output::ServerOsApi; use crate::panes::PositionAndSize; use crate::pty_bus::{PtyInstruction, VteBytes}; +use crate::server::ServerInstruction; use crate::tab::Tab; use crate::{errors::ErrorContext, wasm_vm::PluginInstruction}; use crate::{layout::Layout, panes::PaneId}; @@ -35,7 +36,7 @@ pub enum ScreenInstruction { MoveFocusDown, MoveFocusUp, MoveFocusRight, - Quit, + Exit, ScrollUp, ScrollDown, PageScrollUp, @@ -47,7 +48,7 @@ pub enum ScreenInstruction { SetMaxHeight(PaneId, usize), SetInvisibleBorders(PaneId, bool), ClosePane(PaneId), - ApplyLayout((Layout, Vec)), + ApplyLayout(Layout, Vec), NewTab(RawFd), SwitchTabNext, SwitchTabPrev, @@ -55,7 +56,7 @@ pub enum ScreenInstruction { CloseTab, GoToTab(u32), UpdateTabName(Vec), - TerminalResize, + TerminalResize(PositionAndSize), ChangeMode(ModeInfo), } @@ -68,18 +69,18 @@ pub struct Screen { max_panes: Option, /// A map between this [`Screen`]'s tabs and their ID/key. tabs: BTreeMap, - /// A [`PtyInstruction`] and [`ErrorContext`] sender. - pub send_pty_instructions: SenderWithContext, /// A [`PluginInstruction`] and [`ErrorContext`] sender. pub send_plugin_instructions: SenderWithContext, - /// An [`AppInstruction`] and [`ErrorContext`] sender. - pub send_app_instructions: SenderWithContext, + /// An [`PtyInstruction`] and [`ErrorContext`] sender. + pub send_pty_instructions: SenderWithContext, + /// An [`ServerInstruction`] and [`ErrorContext`] sender. + pub send_server_instructions: SenderWithContext, /// The full size of this [`Screen`]. full_screen_ws: PositionAndSize, /// The index of this [`Screen`]'s active [`Tab`]. active_tab_index: Option, - /// The [`OsApi`] this [`Screen`] uses. - os_api: Box, + /// The [`ServerOsApi`] this [`Screen`] uses. + os_api: Box, mode_info: ModeInfo, input_mode: InputMode, colors: Palette, @@ -91,11 +92,11 @@ impl Screen { #[allow(clippy::too_many_arguments)] pub fn new( receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>, - send_pty_instructions: SenderWithContext, send_plugin_instructions: SenderWithContext, - send_app_instructions: SenderWithContext, + send_pty_instructions: SenderWithContext, + send_server_instructions: SenderWithContext, full_screen_ws: &PositionAndSize, - os_api: Box, + os_api: Box, max_panes: Option, mode_info: ModeInfo, input_mode: InputMode, @@ -104,9 +105,9 @@ impl Screen { Screen { receiver: receive_screen_instructions, max_panes, - send_pty_instructions, send_plugin_instructions, - send_app_instructions, + send_pty_instructions, + send_server_instructions, full_screen_ws: *full_screen_ws, active_tab_index: None, tabs: BTreeMap::new(), @@ -128,9 +129,9 @@ impl Screen { String::new(), &self.full_screen_ws, self.os_api.clone(), - self.send_pty_instructions.clone(), self.send_plugin_instructions.clone(), - self.send_app_instructions.clone(), + self.send_pty_instructions.clone(), + self.send_server_instructions.clone(), self.max_panes, Some(PaneId::Terminal(pane_id)), self.mode_info.clone(), @@ -214,13 +215,13 @@ impl Screen { // below we don't check the result of sending the CloseTab instruction to the pty thread // because this might be happening when the app is closing, at which point the pty thread // has already closed and this would result in an error - let _ = self - .send_pty_instructions - .send(PtyInstruction::CloseTab(pane_ids)); + self.send_pty_instructions + .send(PtyInstruction::CloseTab(pane_ids)) + .unwrap(); if self.tabs.is_empty() { self.active_tab_index = None; - self.send_app_instructions - .send(AppInstruction::Exit) + self.send_server_instructions + .send(ServerInstruction::Render(None)) .unwrap(); } else { for t in self.tabs.values_mut() { @@ -232,8 +233,7 @@ impl Screen { } } - pub fn resize_to_screen(&mut self) { - let new_screen_size = self.os_api.get_terminal_size_using_fd(0); + pub fn resize_to_screen(&mut self, new_screen_size: PositionAndSize) { self.full_screen_ws = new_screen_size; for (_, tab) in self.tabs.iter_mut() { tab.resize_whole_tab(new_screen_size); @@ -285,9 +285,9 @@ impl Screen { String::new(), &self.full_screen_ws, self.os_api.clone(), - self.send_pty_instructions.clone(), self.send_plugin_instructions.clone(), - self.send_app_instructions.clone(), + self.send_pty_instructions.clone(), + self.send_server_instructions.clone(), self.max_panes, None, self.mode_info.clone(), diff --git a/src/common/setup.rs b/src/common/setup.rs index 70e0ded4..edfa858a 100644 --- a/src/common/setup.rs +++ b/src/common/setup.rs @@ -1,9 +1,9 @@ -use crate::common::utils::consts::SYSTEM_DEFAULT_CONFIG_DIR; +use crate::common::utils::consts::{SYSTEM_DEFAULT_CONFIG_DIR, VERSION}; +use crate::os_input_output::set_permissions; use directories_next::{BaseDirs, ProjectDirs}; use std::io::Write; use std::{fs, path::Path, path::PathBuf}; -const VERSION: &str = env!("CARGO_PKG_VERSION"); const CONFIG_LOCATION: &str = "/.config/zellij"; #[macro_export] @@ -40,7 +40,9 @@ pub mod install { for (path, bytes) in assets { let path = data_dir.join(path); - fs::create_dir_all(path.parent().unwrap()).unwrap(); + let parent_path = path.parent().unwrap(); + fs::create_dir_all(parent_path).unwrap(); + set_permissions(parent_path); if out_of_date || !path.exists() { fs::write(path, bytes).expect("Failed to install default assets!"); } diff --git a/src/common/utils/consts.rs b/src/common/utils/consts.rs index 4bd1b5b1..a46f8243 100644 --- a/src/common/utils/consts.rs +++ b/src/common/utils/consts.rs @@ -1,12 +1,41 @@ //! Zellij program-wide constants. -pub const ZELLIJ_TMP_DIR: &str = "/tmp/zellij"; -pub const ZELLIJ_TMP_LOG_DIR: &str = "/tmp/zellij/zellij-log"; -pub const ZELLIJ_TMP_LOG_FILE: &str = "/tmp/zellij/zellij-log/log.txt"; -pub const ZELLIJ_IPC_PIPE: &str = "/tmp/zellij/ipc"; +use crate::os_input_output::set_permissions; +use directories_next::ProjectDirs; +use lazy_static::lazy_static; +use nix::unistd::Uid; +use std::path::PathBuf; +use std::{env, fs}; pub const ZELLIJ_CONFIG_FILE_ENV: &str = "ZELLIJ_CONFIG_FILE"; pub const ZELLIJ_CONFIG_DIR_ENV: &str = "ZELLIJ_CONFIG_DIR"; +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); // TODO: ${PREFIX} argument in makefile pub const SYSTEM_DEFAULT_CONFIG_DIR: &str = "/etc/zellij"; + +lazy_static! { + static ref UID: Uid = Uid::current(); + pub static ref SESSION_NAME: String = names::Generator::default().next().unwrap(); + pub static ref ZELLIJ_PROJ_DIR: ProjectDirs = + ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap(); + pub static ref ZELLIJ_IPC_PIPE: PathBuf = { + let mut ipc_dir = env::var("ZELLIJ_SOCKET_DIR").map_or_else( + |_| { + ZELLIJ_PROJ_DIR + .runtime_dir() + .map_or_else(|| ZELLIJ_TMP_DIR.clone(), |p| p.to_owned()) + }, + PathBuf::from, + ); + ipc_dir.push(VERSION); + fs::create_dir_all(&ipc_dir).unwrap(); + set_permissions(&ipc_dir); + ipc_dir.push(&*SESSION_NAME); + ipc_dir + }; + pub static ref ZELLIJ_TMP_DIR: PathBuf = + PathBuf::from("/tmp/zellij-".to_string() + &format!("{}", *UID)); + pub static ref ZELLIJ_TMP_LOG_DIR: PathBuf = ZELLIJ_TMP_DIR.join("zellij-log"); + pub static ref ZELLIJ_TMP_LOG_FILE: PathBuf = ZELLIJ_TMP_LOG_DIR.join("log.txt"); +} diff --git a/src/common/utils/logging.rs b/src/common/utils/logging.rs index 06827bfc..ff3fb44c 100644 --- a/src/common/utils/logging.rs +++ b/src/common/utils/logging.rs @@ -4,17 +4,20 @@ use std::{ fs, io::{self, prelude::*}, os::unix::io::RawFd, - path::PathBuf, + path::{Path, PathBuf}, }; +use crate::os_input_output::set_permissions; use crate::utils::consts::{ZELLIJ_TMP_LOG_DIR, ZELLIJ_TMP_LOG_FILE}; -pub fn atomic_create_file(file_name: &str) { +pub fn atomic_create_file(file_name: &Path) { let _ = fs::OpenOptions::new().create(true).open(file_name); + #[cfg(not(test))] + set_permissions(file_name); } -pub fn atomic_create_dir(dir_name: &str) -> io::Result<()> { - if let Err(e) = fs::create_dir(dir_name) { +pub fn atomic_create_dir(dir_name: &Path) -> io::Result<()> { + let result = if let Err(e) = fs::create_dir(dir_name) { if e.kind() == std::io::ErrorKind::AlreadyExists { Ok(()) } else { @@ -22,7 +25,11 @@ pub fn atomic_create_dir(dir_name: &str) -> io::Result<()> { } } else { Ok(()) + }; + if result.is_ok() { + set_permissions(dir_name); } + result } pub fn debug_log_to_file(mut message: String) -> io::Result<()> { @@ -31,11 +38,11 @@ pub fn debug_log_to_file(mut message: String) -> io::Result<()> { } pub fn debug_log_to_file_without_newline(message: String) -> io::Result<()> { - atomic_create_file(ZELLIJ_TMP_LOG_FILE); + atomic_create_file(&*ZELLIJ_TMP_LOG_FILE); let mut file = fs::OpenOptions::new() .append(true) .create(true) - .open(ZELLIJ_TMP_LOG_FILE)?; + .open(&*ZELLIJ_TMP_LOG_FILE)?; file.write_all(message.as_bytes()) } @@ -48,16 +55,16 @@ pub fn _debug_log_to_file_pid_3(message: String, pid: RawFd) -> io::Result<()> { } pub fn _delete_log_file() -> io::Result<()> { - if fs::metadata(ZELLIJ_TMP_LOG_FILE).is_ok() { - fs::remove_file(ZELLIJ_TMP_LOG_FILE) + if fs::metadata(&*ZELLIJ_TMP_LOG_FILE).is_ok() { + fs::remove_file(&*ZELLIJ_TMP_LOG_FILE) } else { Ok(()) } } pub fn _delete_log_dir() -> io::Result<()> { - if fs::metadata(ZELLIJ_TMP_LOG_DIR).is_ok() { - fs::remove_dir_all(ZELLIJ_TMP_LOG_DIR) + if fs::metadata(&*ZELLIJ_TMP_LOG_DIR).is_ok() { + fs::remove_dir_all(&*ZELLIJ_TMP_LOG_DIR) } else { Ok(()) } @@ -65,7 +72,7 @@ pub fn _delete_log_dir() -> io::Result<()> { pub fn debug_to_file(message: u8, pid: RawFd) -> io::Result<()> { let mut path = PathBuf::new(); - path.push(ZELLIJ_TMP_LOG_DIR); + path.push(&*ZELLIJ_TMP_LOG_DIR); path.push(format!("zellij-{}.log", pid.to_string())); let mut file = fs::OpenOptions::new() diff --git a/src/common/wasm_vm.rs b/src/common/wasm_vm.rs index bd98a85b..db52c2bb 100644 --- a/src/common/wasm_vm.rs +++ b/src/common/wasm_vm.rs @@ -11,9 +11,7 @@ use wasmer::{imports, Function, ImportObject, Store, WasmerEnv}; use wasmer_wasi::WasiEnv; use zellij_tile::data::{Event, EventType, PluginIds}; -use super::{ - pty_bus::PtyInstruction, screen::ScreenInstruction, AppInstruction, PaneId, SenderWithContext, -}; +use super::{pty_bus::PtyInstruction, screen::ScreenInstruction, PaneId, SenderWithContext}; #[derive(Clone, Debug)] pub enum PluginInstruction { @@ -21,7 +19,7 @@ pub enum PluginInstruction { Update(Option, Event), // Focused plugin / broadcast, event data Render(Sender, u32, usize, usize), // String buffer, plugin id, rows, cols Unload(u32), - Quit, + Exit, } #[derive(WasmerEnv, Clone)] @@ -29,7 +27,6 @@ pub struct PluginEnv { pub plugin_id: u32, // FIXME: This should be a big bundle of all of the channels pub send_screen_instructions: SenderWithContext, - pub send_app_instructions: SenderWithContext, pub send_pty_instructions: SenderWithContext, pub send_plugin_instructions: SenderWithContext, pub wasi_env: WasiEnv, diff --git a/src/main.rs b/src/main.rs index ece55fa2..44b98549 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,26 @@ mod cli; +mod client; mod common; +mod server; #[cfg(test)] mod tests; -// TODO mod server; -mod client; + +use client::{boundaries, layout, panes, start_client, tab}; +use common::{ + command_is_executing, errors, os_input_output, pty_bus, screen, setup, utils, wasm_vm, +}; +use server::start_server; +use structopt::StructOpt; use crate::cli::CliArgs; use crate::command_is_executing::CommandIsExecuting; use crate::common::input::config::Config; -use crate::os_input_output::get_os_input; +use crate::os_input_output::{get_client_os_input, get_server_os_input, ClientOsApi, ServerOsApi}; use crate::utils::{ - consts::{ZELLIJ_IPC_PIPE, ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR}, + consts::{ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR}, logging::*, }; -use client::{boundaries, layout, panes, tab}; -use common::{ - command_is_executing, errors, os_input_output, pty_bus, screen, setup, start, utils, wasm_vm, - ApiCommand, -}; use std::convert::TryFrom; -use std::io::Write; -use std::os::unix::net::UnixStream; -use structopt::StructOpt; pub fn main() { let opts = CliArgs::from_args(); @@ -32,29 +31,7 @@ pub fn main() { std::process::exit(1); } }; - if let Some(split_dir) = opts.split { - match split_dir { - 'h' => { - let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap(); - let api_command = bincode::serialize(&ApiCommand::SplitHorizontally).unwrap(); - stream.write_all(&api_command).unwrap(); - } - 'v' => { - let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap(); - let api_command = bincode::serialize(&ApiCommand::SplitVertically).unwrap(); - stream.write_all(&api_command).unwrap(); - } - _ => {} - }; - } else if opts.move_focus { - let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap(); - let api_command = bincode::serialize(&ApiCommand::MoveFocus).unwrap(); - stream.write_all(&api_command).unwrap(); - } else if let Some(file_to_open) = opts.open_file { - let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap(); - let api_command = bincode::serialize(&ApiCommand::OpenFile(file_to_open)).unwrap(); - stream.write_all(&api_command).unwrap(); - } else if let Some(crate::cli::ConfigCli::GenerateCompletion { shell }) = opts.option { + if let Some(crate::cli::ConfigCli::GenerateCompletion { shell }) = opts.option { let shell = match shell.as_ref() { "bash" => structopt::clap::Shell::Bash, "fish" => structopt::clap::Shell::Fish, @@ -72,9 +49,20 @@ pub fn main() { setup::dump_default_config().expect("Failed to print to stdout"); std::process::exit(1); } else { - let os_input = get_os_input(); - atomic_create_dir(ZELLIJ_TMP_DIR).unwrap(); - atomic_create_dir(ZELLIJ_TMP_LOG_DIR).unwrap(); - start(Box::new(os_input), opts, config); + atomic_create_dir(&*ZELLIJ_TMP_DIR).unwrap(); + atomic_create_dir(&*ZELLIJ_TMP_LOG_DIR).unwrap(); + let server_os_input = get_server_os_input(); + let os_input = get_client_os_input(); + start(Box::new(os_input), opts, Box::new(server_os_input), config); } } +pub fn start( + client_os_input: Box, + opts: CliArgs, + server_os_input: Box, + config: Config, +) { + let ipc_thread = start_server(server_os_input); + start_client(client_os_input, opts, config); + drop(ipc_thread.join()); +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 626b4e6b..6eb2455b 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,811 @@ -use super::super::common::{screen}; +use interprocess::local_socket::LocalSocketListener; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::sync::mpsc::channel; +use std::thread; +use std::{collections::HashMap, fs}; +use std::{ + collections::HashSet, + str::FromStr, + sync::{Arc, Mutex, RwLock}, +}; +use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value}; +use wasmer_wasi::{Pipe, WasiState}; +use zellij_tile::data::{Event, EventType, InputMode, ModeInfo}; -pub fn start_server() { - // TODO -} \ No newline at end of file +use crate::cli::CliArgs; +use crate::client::ClientInstruction; +use crate::common::{ + errors::{ContextType, PluginContext, PtyContext, ScreenContext, ServerContext}, + input::actions::{Action, Direction}, + input::handler::get_mode_info, + os_input_output::{set_permissions, ServerOsApi}, + pty_bus::{PtyBus, PtyInstruction}, + screen::{Screen, ScreenInstruction}, + setup::install::populate_data_dir, + utils::consts::{ZELLIJ_IPC_PIPE, ZELLIJ_PROJ_DIR}, + wasm_vm::{wasi_read_string, wasi_write_object, zellij_exports, PluginEnv, PluginInstruction}, + ChannelWithContext, SenderType, SenderWithContext, +}; +use crate::layout::Layout; +use crate::panes::PaneId; +use crate::panes::PositionAndSize; + +/// Instructions related to server-side application including the +/// ones sent by client to server +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ServerInstruction { + TerminalResize(PositionAndSize), + NewClient(PositionAndSize, CliArgs), + Action(Action), + Render(Option), + UnblockInputThread, + ClientExit, +} + +struct SessionMetaData { + pub send_pty_instructions: SenderWithContext, + pub send_screen_instructions: SenderWithContext, + pub send_plugin_instructions: SenderWithContext, + screen_thread: Option>, + pty_thread: Option>, + wasm_thread: Option>, +} + +impl Drop for SessionMetaData { + fn drop(&mut self) { + let _ = self.send_pty_instructions.send(PtyInstruction::Exit); + let _ = self.send_screen_instructions.send(ScreenInstruction::Exit); + let _ = self.send_plugin_instructions.send(PluginInstruction::Exit); + let _ = self.screen_thread.take().unwrap().join(); + let _ = self.pty_thread.take().unwrap().join(); + let _ = self.wasm_thread.take().unwrap().join(); + } +} + +pub fn start_server(os_input: Box) -> thread::JoinHandle<()> { + let (send_server_instructions, receive_server_instructions): ChannelWithContext< + ServerInstruction, + > = channel(); + let send_server_instructions = + SenderWithContext::new(SenderType::Sender(send_server_instructions)); + let sessions: Arc>> = Arc::new(RwLock::new(None)); + + #[cfg(test)] + handle_client( + sessions.clone(), + os_input.clone(), + send_server_instructions.clone(), + ); + #[cfg(not(test))] + let _ = thread::Builder::new() + .name("server_listener".to_string()) + .spawn({ + let os_input = os_input.clone(); + let sessions = sessions.clone(); + let send_server_instructions = send_server_instructions.clone(); + move || { + drop(std::fs::remove_file(&*ZELLIJ_IPC_PIPE)); + let listener = LocalSocketListener::bind(&**ZELLIJ_IPC_PIPE).unwrap(); + set_permissions(&*ZELLIJ_IPC_PIPE); + for stream in listener.incoming() { + match stream { + Ok(stream) => { + let mut os_input = os_input.clone(); + os_input.update_receiver(stream); + let sessions = sessions.clone(); + let send_server_instructions = send_server_instructions.clone(); + handle_client(sessions, os_input, send_server_instructions); + } + Err(err) => { + panic!("err {:?}", err); + } + } + } + } + }); + + thread::Builder::new() + .name("server_thread".to_string()) + .spawn({ + move || loop { + let (instruction, mut err_ctx) = receive_server_instructions.recv().unwrap(); + err_ctx.add_call(ContextType::IPCServer(ServerContext::from(&instruction))); + match instruction { + ServerInstruction::NewClient(full_screen_ws, opts) => { + let session_data = init_session( + os_input.clone(), + opts, + send_server_instructions.clone(), + full_screen_ws, + ); + *sessions.write().unwrap() = Some(session_data); + sessions + .read() + .unwrap() + .as_ref() + .unwrap() + .send_pty_instructions + .send(PtyInstruction::NewTab) + .unwrap(); + } + ServerInstruction::UnblockInputThread => { + os_input.send_to_client(ClientInstruction::UnblockInputThread); + } + ServerInstruction::ClientExit => { + *sessions.write().unwrap() = None; + os_input.send_to_client(ClientInstruction::Exit); + drop(std::fs::remove_file(&*ZELLIJ_IPC_PIPE)); + break; + } + ServerInstruction::Render(output) => { + os_input.send_to_client(ClientInstruction::Render(output)) + } + _ => panic!("Received unexpected instruction."), + } + } + }) + .unwrap() +} + +fn handle_client( + sessions: Arc>>, + mut os_input: Box, + send_server_instructions: SenderWithContext, +) { + thread::Builder::new() + .name("server_router".to_string()) + .spawn(move || loop { + let (instruction, mut err_ctx) = os_input.recv_from_client(); + err_ctx.add_call(ContextType::IPCServer(ServerContext::from(&instruction))); + let rlocked_sessions = sessions.read().unwrap(); + match instruction { + ServerInstruction::ClientExit => { + send_server_instructions.send(instruction).unwrap(); + break; + } + ServerInstruction::Action(action) => { + route_action(action, rlocked_sessions.as_ref().unwrap(), &*os_input); + } + ServerInstruction::TerminalResize(new_size) => { + rlocked_sessions + .as_ref() + .unwrap() + .send_screen_instructions + .send(ScreenInstruction::TerminalResize(new_size)) + .unwrap(); + } + ServerInstruction::NewClient(..) => { + os_input.add_client_sender(); + send_server_instructions.send(instruction).unwrap(); + } + _ => { + send_server_instructions.send(instruction).unwrap(); + } + } + }) + .unwrap(); +} + +fn init_session( + os_input: Box, + opts: CliArgs, + send_server_instructions: SenderWithContext, + full_screen_ws: PositionAndSize, +) -> SessionMetaData { + let (send_screen_instructions, receive_screen_instructions): ChannelWithContext< + ScreenInstruction, + > = channel(); + let send_screen_instructions = + SenderWithContext::new(SenderType::Sender(send_screen_instructions)); + + let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext< + PluginInstruction, + > = channel(); + let send_plugin_instructions = + SenderWithContext::new(SenderType::Sender(send_plugin_instructions)); + let (send_pty_instructions, receive_pty_instructions): ChannelWithContext = + channel(); + let send_pty_instructions = SenderWithContext::new(SenderType::Sender(send_pty_instructions)); + + // Determine and initialize the data directory + let data_dir = opts + .data_dir + .unwrap_or_else(|| ZELLIJ_PROJ_DIR.data_dir().to_path_buf()); + populate_data_dir(&data_dir); + + // Don't use default layouts in tests, but do everywhere else + #[cfg(not(test))] + let default_layout = Some(PathBuf::from("default")); + #[cfg(test)] + let default_layout = None; + let maybe_layout = opts + .layout + .map(|p| Layout::new(&p, &data_dir)) + .or_else(|| default_layout.map(|p| Layout::from_defaults(&p, &data_dir))); + + let mut pty_bus = PtyBus::new( + receive_pty_instructions, + send_screen_instructions.clone(), + send_plugin_instructions.clone(), + os_input.clone(), + opts.debug, + ); + + let pty_thread = thread::Builder::new() + .name("pty".to_string()) + .spawn({ + let send_server_instructions = send_server_instructions.clone(); + move || loop { + let (event, mut err_ctx) = pty_bus + .receive_pty_instructions + .recv() + .expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Pty(PtyContext::from(&event))); + match event { + PtyInstruction::SpawnTerminal(file_to_open) => { + let pid = pty_bus.spawn_terminal(file_to_open); + pty_bus + .send_screen_instructions + .send(ScreenInstruction::NewPane(PaneId::Terminal(pid))) + .unwrap(); + } + PtyInstruction::SpawnTerminalVertically(file_to_open) => { + let pid = pty_bus.spawn_terminal(file_to_open); + pty_bus + .send_screen_instructions + .send(ScreenInstruction::VerticalSplit(PaneId::Terminal(pid))) + .unwrap(); + } + PtyInstruction::SpawnTerminalHorizontally(file_to_open) => { + let pid = pty_bus.spawn_terminal(file_to_open); + pty_bus + .send_screen_instructions + .send(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid))) + .unwrap(); + } + PtyInstruction::NewTab => { + if let Some(layout) = maybe_layout.clone() { + pty_bus.spawn_terminals_for_layout(layout); + } else { + let pid = pty_bus.spawn_terminal(None); + pty_bus + .send_screen_instructions + .send(ScreenInstruction::NewTab(pid)) + .unwrap(); + } + } + PtyInstruction::ClosePane(id) => { + pty_bus.close_pane(id); + send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + PtyInstruction::CloseTab(ids) => { + pty_bus.close_tab(ids); + send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + PtyInstruction::Exit => { + break; + } + } + } + }) + .unwrap(); + + let screen_thread = thread::Builder::new() + .name("screen".to_string()) + .spawn({ + let os_input = os_input.clone(); + let send_plugin_instructions = send_plugin_instructions.clone(); + let send_pty_instructions = send_pty_instructions.clone(); + let send_server_instructions = send_server_instructions; + let max_panes = opts.max_panes; + let colors = os_input.load_palette(); + + move || { + let mut screen = Screen::new( + receive_screen_instructions, + send_plugin_instructions, + send_pty_instructions, + send_server_instructions, + &full_screen_ws, + os_input, + max_panes, + ModeInfo { + palette: colors, + ..ModeInfo::default() + }, + InputMode::Normal, + colors, + ); + loop { + let (event, mut err_ctx) = screen + .receiver + .recv() + .expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Screen(ScreenContext::from(&event))); + match event { + ScreenInstruction::PtyBytes(pid, vte_bytes) => { + let active_tab = screen.get_active_tab_mut().unwrap(); + if active_tab.has_terminal_pid(pid) { + // it's most likely that this event is directed at the active tab + // look there first + active_tab.handle_pty_bytes(pid, vte_bytes); + } else { + // if this event wasn't directed at the active tab, start looking + // in other tabs + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_terminal_pid(pid) { + tab.handle_pty_bytes(pid, vte_bytes); + break; + } + } + } + } + ScreenInstruction::Render => { + screen.render(); + } + ScreenInstruction::NewPane(pid) => { + screen.get_active_tab_mut().unwrap().new_pane(pid); + screen + .send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::HorizontalSplit(pid) => { + screen.get_active_tab_mut().unwrap().horizontal_split(pid); + screen + .send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::VerticalSplit(pid) => { + screen.get_active_tab_mut().unwrap().vertical_split(pid); + screen + .send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::WriteCharacter(bytes) => { + let active_tab = screen.get_active_tab_mut().unwrap(); + match active_tab.is_sync_panes_active() { + true => active_tab.write_to_terminals_on_current_tab(bytes), + false => active_tab.write_to_active_terminal(bytes), + } + } + ScreenInstruction::ResizeLeft => { + screen.get_active_tab_mut().unwrap().resize_left(); + } + ScreenInstruction::ResizeRight => { + screen.get_active_tab_mut().unwrap().resize_right(); + } + ScreenInstruction::ResizeDown => { + screen.get_active_tab_mut().unwrap().resize_down(); + } + ScreenInstruction::ResizeUp => { + screen.get_active_tab_mut().unwrap().resize_up(); + } + ScreenInstruction::SwitchFocus => { + screen.get_active_tab_mut().unwrap().move_focus(); + } + ScreenInstruction::FocusNextPane => { + screen.get_active_tab_mut().unwrap().focus_next_pane(); + } + ScreenInstruction::FocusPreviousPane => { + screen.get_active_tab_mut().unwrap().focus_previous_pane(); + } + ScreenInstruction::MoveFocusLeft => { + screen.get_active_tab_mut().unwrap().move_focus_left(); + } + ScreenInstruction::MoveFocusDown => { + screen.get_active_tab_mut().unwrap().move_focus_down(); + } + ScreenInstruction::MoveFocusRight => { + screen.get_active_tab_mut().unwrap().move_focus_right(); + } + ScreenInstruction::MoveFocusUp => { + screen.get_active_tab_mut().unwrap().move_focus_up(); + } + ScreenInstruction::ScrollUp => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_up(); + } + ScreenInstruction::ScrollDown => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_down(); + } + ScreenInstruction::PageScrollUp => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_up_page(); + } + ScreenInstruction::PageScrollDown => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_down_page(); + } + ScreenInstruction::ClearScroll => { + screen + .get_active_tab_mut() + .unwrap() + .clear_active_terminal_scroll(); + } + ScreenInstruction::CloseFocusedPane => { + screen.get_active_tab_mut().unwrap().close_focused_pane(); + screen.render(); + } + ScreenInstruction::SetSelectable(id, selectable) => { + screen + .get_active_tab_mut() + .unwrap() + .set_pane_selectable(id, selectable); + } + ScreenInstruction::SetMaxHeight(id, max_height) => { + screen + .get_active_tab_mut() + .unwrap() + .set_pane_max_height(id, max_height); + } + ScreenInstruction::SetInvisibleBorders(id, invisible_borders) => { + screen + .get_active_tab_mut() + .unwrap() + .set_pane_invisible_borders(id, invisible_borders); + screen.render(); + } + ScreenInstruction::ClosePane(id) => { + screen.get_active_tab_mut().unwrap().close_pane(id); + screen.render(); + } + ScreenInstruction::ToggleActiveTerminalFullscreen => { + screen + .get_active_tab_mut() + .unwrap() + .toggle_active_pane_fullscreen(); + } + ScreenInstruction::NewTab(pane_id) => { + screen.new_tab(pane_id); + screen + .send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::SwitchTabNext => { + screen.switch_tab_next(); + screen + .send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::SwitchTabPrev => { + screen.switch_tab_prev(); + screen + .send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::CloseTab => { + screen.close_tab(); + screen + .send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::ApplyLayout(layout, new_pane_pids) => { + screen.apply_layout(layout, new_pane_pids); + screen + .send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::GoToTab(tab_index) => { + screen.go_to_tab(tab_index as usize); + screen + .send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::UpdateTabName(c) => { + screen.update_active_tab_name(c); + screen + .send_server_instructions + .send(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::TerminalResize(new_size) => { + screen.resize_to_screen(new_size); + } + ScreenInstruction::ChangeMode(mode_info) => { + screen.change_mode(mode_info); + } + ScreenInstruction::ToggleActiveSyncPanes => { + screen + .get_active_tab_mut() + .unwrap() + .toggle_sync_panes_is_active(); + screen.update_tabs(); + } + ScreenInstruction::Exit => { + break; + } + } + } + } + }) + .unwrap(); + + let wasm_thread = thread::Builder::new() + .name("wasm".to_string()) + .spawn({ + let send_screen_instructions = send_screen_instructions.clone(); + let send_pty_instructions = send_pty_instructions.clone(); + let send_plugin_instructions = send_plugin_instructions.clone(); + + let store = Store::default(); + let mut plugin_id = 0; + let mut plugin_map = HashMap::new(); + move || loop { + let (event, mut err_ctx) = receive_plugin_instructions + .recv() + .expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Plugin(PluginContext::from(&event))); + match event { + PluginInstruction::Load(pid_tx, path) => { + let plugin_dir = data_dir.join("plugins/"); + let wasm_bytes = fs::read(&path) + .or_else(|_| fs::read(&path.with_extension("wasm"))) + .or_else(|_| fs::read(&plugin_dir.join(&path).with_extension("wasm"))) + .unwrap_or_else(|_| panic!("cannot find plugin {}", &path.display())); + + // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that + let module = Module::new(&store, &wasm_bytes).unwrap(); + + let output = Pipe::new(); + let input = Pipe::new(); + let mut wasi_env = WasiState::new("Zellij") + .env("CLICOLOR_FORCE", "1") + .preopen(|p| { + p.directory(".") // FIXME: Change this to a more meaningful dir + .alias(".") + .read(true) + .write(true) + .create(true) + }) + .unwrap() + .stdin(Box::new(input)) + .stdout(Box::new(output)) + .finalize() + .unwrap(); + + let wasi = wasi_env.import_object(&module).unwrap(); + + let plugin_env = PluginEnv { + plugin_id, + send_screen_instructions: send_screen_instructions.clone(), + send_pty_instructions: send_pty_instructions.clone(), + send_plugin_instructions: send_plugin_instructions.clone(), + wasi_env, + subscriptions: Arc::new(Mutex::new(HashSet::new())), + }; + + let zellij = zellij_exports(&store, &plugin_env); + let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); + + let start = instance.exports.get_function("_start").unwrap(); + + // This eventually calls the `.init()` method + start.call(&[]).unwrap(); + + plugin_map.insert(plugin_id, (instance, plugin_env)); + pid_tx.send(plugin_id).unwrap(); + plugin_id += 1; + } + PluginInstruction::Update(pid, event) => { + for (&i, (instance, plugin_env)) in &plugin_map { + let subs = plugin_env.subscriptions.lock().unwrap(); + // FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType? + let event_type = EventType::from_str(&event.to_string()).unwrap(); + if (pid.is_none() || pid == Some(i)) && subs.contains(&event_type) { + let update = instance.exports.get_function("update").unwrap(); + wasi_write_object(&plugin_env.wasi_env, &event); + update.call(&[]).unwrap(); + } + } + drop(send_screen_instructions.send(ScreenInstruction::Render)); + } + PluginInstruction::Render(buf_tx, pid, rows, cols) => { + let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); + + let render = instance.exports.get_function("render").unwrap(); + + render + .call(&[Value::I32(rows as i32), Value::I32(cols as i32)]) + .unwrap(); + + buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap(); + } + PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), + PluginInstruction::Exit => break, + } + } + }) + .unwrap(); + SessionMetaData { + send_plugin_instructions, + send_screen_instructions, + send_pty_instructions, + screen_thread: Some(screen_thread), + pty_thread: Some(pty_thread), + wasm_thread: Some(wasm_thread), + } +} + +fn route_action(action: Action, session: &SessionMetaData, os_input: &dyn ServerOsApi) { + match action { + Action::Write(val) => { + session + .send_screen_instructions + .send(ScreenInstruction::ClearScroll) + .unwrap(); + session + .send_screen_instructions + .send(ScreenInstruction::WriteCharacter(val)) + .unwrap(); + } + Action::SwitchToMode(mode) => { + let palette = os_input.load_palette(); + session + .send_plugin_instructions + .send(PluginInstruction::Update( + None, + Event::ModeUpdate(get_mode_info(mode, palette)), + )) + .unwrap(); + session + .send_screen_instructions + .send(ScreenInstruction::ChangeMode(get_mode_info(mode, palette))) + .unwrap(); + session + .send_screen_instructions + .send(ScreenInstruction::Render) + .unwrap(); + } + Action::Resize(direction) => { + let screen_instr = match direction { + Direction::Left => ScreenInstruction::ResizeLeft, + Direction::Right => ScreenInstruction::ResizeRight, + Direction::Up => ScreenInstruction::ResizeUp, + Direction::Down => ScreenInstruction::ResizeDown, + }; + session.send_screen_instructions.send(screen_instr).unwrap(); + } + Action::SwitchFocus => { + session + .send_screen_instructions + .send(ScreenInstruction::SwitchFocus) + .unwrap(); + } + Action::FocusNextPane => { + session + .send_screen_instructions + .send(ScreenInstruction::FocusNextPane) + .unwrap(); + } + Action::FocusPreviousPane => { + session + .send_screen_instructions + .send(ScreenInstruction::FocusPreviousPane) + .unwrap(); + } + Action::MoveFocus(direction) => { + let screen_instr = match direction { + Direction::Left => ScreenInstruction::MoveFocusLeft, + Direction::Right => ScreenInstruction::MoveFocusRight, + Direction::Up => ScreenInstruction::MoveFocusUp, + Direction::Down => ScreenInstruction::MoveFocusDown, + }; + session.send_screen_instructions.send(screen_instr).unwrap(); + } + Action::ScrollUp => { + session + .send_screen_instructions + .send(ScreenInstruction::ScrollUp) + .unwrap(); + } + Action::ScrollDown => { + session + .send_screen_instructions + .send(ScreenInstruction::ScrollDown) + .unwrap(); + } + Action::PageScrollUp => { + session + .send_screen_instructions + .send(ScreenInstruction::PageScrollUp) + .unwrap(); + } + Action::PageScrollDown => { + session + .send_screen_instructions + .send(ScreenInstruction::PageScrollDown) + .unwrap(); + } + Action::ToggleFocusFullscreen => { + session + .send_screen_instructions + .send(ScreenInstruction::ToggleActiveTerminalFullscreen) + .unwrap(); + } + Action::NewPane(direction) => { + let pty_instr = match direction { + Some(Direction::Left) => PtyInstruction::SpawnTerminalVertically(None), + Some(Direction::Right) => PtyInstruction::SpawnTerminalVertically(None), + Some(Direction::Up) => PtyInstruction::SpawnTerminalHorizontally(None), + Some(Direction::Down) => PtyInstruction::SpawnTerminalHorizontally(None), + // No direction specified - try to put it in the biggest available spot + None => PtyInstruction::SpawnTerminal(None), + }; + session.send_pty_instructions.send(pty_instr).unwrap(); + } + Action::CloseFocus => { + session + .send_screen_instructions + .send(ScreenInstruction::CloseFocusedPane) + .unwrap(); + } + Action::NewTab => { + session + .send_pty_instructions + .send(PtyInstruction::NewTab) + .unwrap(); + } + Action::GoToNextTab => { + session + .send_screen_instructions + .send(ScreenInstruction::SwitchTabNext) + .unwrap(); + } + Action::GoToPreviousTab => { + session + .send_screen_instructions + .send(ScreenInstruction::SwitchTabPrev) + .unwrap(); + } + Action::ToggleActiveSyncPanes => { + session + .send_screen_instructions + .send(ScreenInstruction::ToggleActiveSyncPanes) + .unwrap(); + } + Action::CloseTab => { + session + .send_screen_instructions + .send(ScreenInstruction::CloseTab) + .unwrap(); + } + Action::GoToTab(i) => { + session + .send_screen_instructions + .send(ScreenInstruction::GoToTab(i)) + .unwrap(); + } + Action::TabNameInput(c) => { + session + .send_screen_instructions + .send(ScreenInstruction::UpdateTabName(c)) + .unwrap(); + } + Action::NoOp => {} + Action::Quit => panic!("Received unexpected action"), + } +} diff --git a/src/tests/fakes.rs b/src/tests/fakes.rs index 684a24a7..4c780410 100644 --- a/src/tests/fakes.rs +++ b/src/tests/fakes.rs @@ -1,19 +1,23 @@ use crate::panes::PositionAndSize; +use interprocess::local_socket::LocalSocketStream; use std::collections::{HashMap, VecDeque}; use std::io::Write; use std::os::unix::io::RawFd; use std::path::PathBuf; -use std::sync::{Arc, Condvar, Mutex}; +use std::sync::{mpsc, Arc, Condvar, Mutex}; use std::time::{Duration, Instant}; -use crate::os_input_output::OsApi; +use crate::client::ClientInstruction; +use crate::common::{ChannelWithContext, SenderType, SenderWithContext}; +use crate::errors::ErrorContext; +use crate::os_input_output::{ClientOsApi, ServerOsApi}; +use crate::server::ServerInstruction; use crate::tests::possible_tty_inputs::{get_possible_tty_inputs, Bytes}; +use crate::tests::utils::commands::{QUIT, SLEEP}; use crate::utils::shared::default_palette; use zellij_tile::data::Palette; -use crate::tests::utils::commands::{QUIT, SLEEP}; - -const MIN_TIME_BETWEEN_SNAPSHOTS: Duration = Duration::from_millis(50); +const MIN_TIME_BETWEEN_SNAPSHOTS: Duration = Duration::from_millis(150); #[derive(Clone)] pub enum IoEvent { @@ -73,6 +77,10 @@ pub struct FakeInputOutput { win_sizes: Arc>>, possible_tty_inputs: HashMap, last_snapshot_time: Arc>, + send_instructions_to_client: SenderWithContext, + receive_instructions_from_server: Arc>>, + send_instructions_to_server: SenderWithContext, + receive_instructions_from_client: Arc>>, should_trigger_sigwinch: Arc<(Mutex, Condvar)>, sigwinch_event: Option, } @@ -82,6 +90,12 @@ impl FakeInputOutput { let mut win_sizes = HashMap::new(); let last_snapshot_time = Arc::new(Mutex::new(Instant::now())); let stdout_writer = FakeStdoutWriter::new(last_snapshot_time.clone()); + let (client_sender, client_receiver): ChannelWithContext = + mpsc::channel(); + let send_instructions_to_client = SenderWithContext::new(SenderType::Sender(client_sender)); + let (server_sender, server_receiver): ChannelWithContext = + mpsc::channel(); + let send_instructions_to_server = SenderWithContext::new(SenderType::Sender(server_sender)); win_sizes.insert(0, winsize); // 0 is the current terminal FakeInputOutput { read_buffers: Arc::new(Mutex::new(HashMap::new())), @@ -93,6 +107,10 @@ impl FakeInputOutput { io_events: Arc::new(Mutex::new(vec![])), win_sizes: Arc::new(Mutex::new(win_sizes)), possible_tty_inputs: get_possible_tty_inputs(), + receive_instructions_from_client: Arc::new(Mutex::new(server_receiver)), + send_instructions_to_server, + receive_instructions_from_server: Arc::new(Mutex::new(client_receiver)), + send_instructions_to_client, should_trigger_sigwinch: Arc::new((Mutex::new(false), Condvar::new())), sigwinch_event: None, } @@ -116,7 +134,7 @@ impl FakeInputOutput { } } -impl OsApi for FakeInputOutput { +impl ClientOsApi for FakeInputOutput { fn get_terminal_size_using_fd(&self, pid: RawFd) -> PositionAndSize { if let Some(new_position_and_size) = self.sigwinch_event { let (lock, _cvar) = &*self.should_trigger_sigwinch; @@ -129,20 +147,6 @@ impl OsApi for FakeInputOutput { let winsize = win_sizes.get(&pid).unwrap(); *winsize } - fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16) { - let terminal_input = self - .possible_tty_inputs - .get(&cols) - .expect(&format!("could not find input for size {:?}", cols)); - self.read_buffers - .lock() - .unwrap() - .insert(pid, terminal_input.clone()); - self.io_events - .lock() - .unwrap() - .push(IoEvent::SetTerminalSizeUsingFd(pid, cols, rows)); - } fn set_raw_mode(&mut self, pid: RawFd) { self.io_events .lock() @@ -155,43 +159,7 @@ impl OsApi for FakeInputOutput { .unwrap() .push(IoEvent::UnsetRawMode(pid)); } - fn spawn_terminal(&mut self, _file_to_open: Option) -> (RawFd, RawFd) { - let next_terminal_id = self.stdin_writes.lock().unwrap().keys().len() as RawFd + 1; - self.add_terminal(next_terminal_id); - (next_terminal_id as i32, next_terminal_id + 1000) // secondary number is arbitrary here - } - fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result { - let mut read_buffers = self.read_buffers.lock().unwrap(); - let mut bytes_read = 0; - match read_buffers.get_mut(&pid) { - Some(bytes) => { - for i in bytes.read_position..bytes.content.len() { - bytes_read += 1; - buf[i] = bytes.content[i]; - } - if bytes_read > bytes.read_position { - bytes.set_read_position(bytes_read); - } - return Ok(bytes_read); - } - None => Err(nix::Error::Sys(nix::errno::Errno::EAGAIN)), - } - } - fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result { - let mut stdin_writes = self.stdin_writes.lock().unwrap(); - let write_buffer = stdin_writes.get_mut(&pid).unwrap(); - let mut bytes_written = 0; - for byte in buf { - bytes_written += 1; - write_buffer.push(*byte); - } - Ok(bytes_written) - } - fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error> { - self.io_events.lock().unwrap().push(IoEvent::TcDrain(pid)); - Ok(()) - } - fn box_clone(&self) -> Box { + fn box_clone(&self) -> Box { Box::new((*self).clone()) } fn read_from_stdin(&self) -> Vec { @@ -213,30 +181,115 @@ impl OsApi for FakeInputOutput { std::thread::sleep(std::time::Duration::from_millis(200)); } else if command == QUIT && self.sigwinch_event.is_some() { let (lock, cvar) = &*self.should_trigger_sigwinch; - let mut should_trigger_sigwinch = lock.lock().unwrap(); - *should_trigger_sigwinch = true; + { + let mut should_trigger_sigwinch = lock.lock().unwrap(); + *should_trigger_sigwinch = true; + } cvar.notify_one(); ::std::thread::sleep(MIN_TIME_BETWEEN_SNAPSHOTS); // give some time for the app to resize before quitting + } else if command == QUIT { + ::std::thread::sleep(MIN_TIME_BETWEEN_SNAPSHOTS); } command } fn get_stdout_writer(&self) -> Box { Box::new(self.stdout_writer.clone()) } - fn kill(&mut self, fd: RawFd) -> Result<(), nix::Error> { - self.io_events.lock().unwrap().push(IoEvent::Kill(fd)); - Ok(()) + fn send_to_server(&self, msg: ServerInstruction) { + self.send_instructions_to_server.send(msg).unwrap(); + } + fn recv_from_server(&self) -> (ClientInstruction, ErrorContext) { + self.receive_instructions_from_server + .lock() + .unwrap() + .recv() + .unwrap() } fn receive_sigwinch(&self, cb: Box) { if self.sigwinch_event.is_some() { let (lock, cvar) = &*self.should_trigger_sigwinch; - let mut should_trigger_sigwinch = lock.lock().unwrap(); - while !*should_trigger_sigwinch { - should_trigger_sigwinch = cvar.wait(should_trigger_sigwinch).unwrap(); + { + let mut should_trigger_sigwinch = lock.lock().unwrap(); + while !*should_trigger_sigwinch { + should_trigger_sigwinch = cvar.wait(should_trigger_sigwinch).unwrap(); + } } cb(); } } + fn connect_to_server(&self) {} +} + +impl ServerOsApi for FakeInputOutput { + fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16) { + let terminal_input = self + .possible_tty_inputs + .get(&cols) + .expect(&format!("could not find input for size {:?}", cols)); + self.read_buffers + .lock() + .unwrap() + .insert(pid, terminal_input.clone()); + self.io_events + .lock() + .unwrap() + .push(IoEvent::SetTerminalSizeUsingFd(pid, cols, rows)); + } + fn spawn_terminal(&mut self, _file_to_open: Option) -> (RawFd, RawFd) { + let next_terminal_id = self.stdin_writes.lock().unwrap().keys().len() as RawFd + 1; + self.add_terminal(next_terminal_id); + (next_terminal_id as i32, next_terminal_id + 1000) // secondary number is arbitrary here + } + fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result { + let mut stdin_writes = self.stdin_writes.lock().unwrap(); + let write_buffer = stdin_writes.get_mut(&pid).unwrap(); + let mut bytes_written = 0; + for byte in buf { + bytes_written += 1; + write_buffer.push(*byte); + } + Ok(bytes_written) + } + fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result { + let mut read_buffers = self.read_buffers.lock().unwrap(); + let mut bytes_read = 0; + match read_buffers.get_mut(&pid) { + Some(bytes) => { + for i in bytes.read_position..bytes.content.len() { + bytes_read += 1; + buf[i] = bytes.content[i]; + } + if bytes_read > bytes.read_position { + bytes.set_read_position(bytes_read); + } + return Ok(bytes_read); + } + None => Err(nix::Error::Sys(nix::errno::Errno::EAGAIN)), + } + } + fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error> { + self.io_events.lock().unwrap().push(IoEvent::TcDrain(pid)); + Ok(()) + } + fn box_clone(&self) -> Box { + Box::new((*self).clone()) + } + fn kill(&mut self, fd: RawFd) -> Result<(), nix::Error> { + self.io_events.lock().unwrap().push(IoEvent::Kill(fd)); + Ok(()) + } + fn recv_from_client(&self) -> (ServerInstruction, ErrorContext) { + self.receive_instructions_from_client + .lock() + .unwrap() + .recv() + .unwrap() + } + fn send_to_client(&self, msg: ClientInstruction) { + self.send_instructions_to_client.send(msg).unwrap(); + } + fn add_client_sender(&mut self) {} + fn update_receiver(&mut self, _stream: LocalSocketStream) {} fn load_palette(&self) -> Palette { default_palette() } diff --git a/src/tests/integration/basic.rs b/src/tests/integration/basic.rs index f785666a..c8305cab 100644 --- a/src/tests/integration/basic.rs +++ b/src/tests/integration/basic.rs @@ -30,6 +30,7 @@ pub fn starts_with_one_terminal() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -57,6 +58,7 @@ pub fn split_terminals_vertically() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -84,6 +86,7 @@ pub fn split_terminals_horizontally() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -118,6 +121,7 @@ pub fn split_largest_terminal() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -145,6 +149,7 @@ pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -172,6 +177,7 @@ pub fn cannot_split_terminals_horizontally_when_active_terminal_is_too_small() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -199,6 +205,7 @@ pub fn cannot_split_largest_terminal_when_there_is_no_room() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -234,6 +241,7 @@ pub fn scrolling_up_inside_a_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -271,6 +279,7 @@ pub fn scrolling_down_inside_a_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -305,6 +314,7 @@ pub fn scrolling_page_up_inside_a_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -342,6 +352,7 @@ pub fn scrolling_page_down_inside_a_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -377,7 +388,12 @@ pub fn max_panes() { ]); let mut opts = CliArgs::default(); opts.max_panes = Some(4); - start(Box::new(fake_input_output.clone()), opts, Config::default()); + start( + Box::new(fake_input_output.clone()), + opts, + Box::new(fake_input_output.clone()), + Config::default(), + ); let output_frames = fake_input_output .stdout_writer .output_frames @@ -409,7 +425,12 @@ pub fn toggle_focused_pane_fullscreen() { ]); let mut opts = CliArgs::default(); opts.max_panes = Some(4); - start(Box::new(fake_input_output.clone()), opts, Config::default()); + start( + Box::new(fake_input_output.clone()), + opts, + Box::new(fake_input_output.clone()), + Config::default(), + ); let output_frames = fake_input_output .stdout_writer .output_frames diff --git a/src/tests/integration/close_pane.rs b/src/tests/integration/close_pane.rs index f73f4027..869debed 100644 --- a/src/tests/integration/close_pane.rs +++ b/src/tests/integration/close_pane.rs @@ -43,6 +43,7 @@ pub fn close_pane_with_another_pane_above_it() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -85,6 +86,7 @@ pub fn close_pane_with_another_pane_below_it() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -124,6 +126,7 @@ pub fn close_pane_with_another_pane_to_the_left() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -164,6 +167,7 @@ pub fn close_pane_with_another_pane_to_the_right() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -209,6 +213,7 @@ pub fn close_pane_with_multiple_panes_above_it() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -252,6 +257,7 @@ pub fn close_pane_with_multiple_panes_below_it() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -297,6 +303,7 @@ pub fn close_pane_with_multiple_panes_to_the_left() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -340,6 +347,7 @@ pub fn close_pane_with_multiple_panes_to_the_right() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -405,6 +413,7 @@ pub fn close_pane_with_multiple_panes_above_it_away_from_screen_edges() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -466,6 +475,7 @@ pub fn close_pane_with_multiple_panes_below_it_away_from_screen_edges() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -529,6 +539,7 @@ pub fn close_pane_with_multiple_panes_to_the_left_away_from_screen_edges() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -592,6 +603,7 @@ pub fn close_pane_with_multiple_panes_to_the_right_away_from_screen_edges() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -628,6 +640,7 @@ pub fn closing_last_pane_exits_app() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); diff --git a/src/tests/integration/compatibility.rs b/src/tests/integration/compatibility.rs index ef93135e..77d3b67e 100644 --- a/src/tests/integration/compatibility.rs +++ b/src/tests/integration/compatibility.rs @@ -41,10 +41,12 @@ pub fn run_bandwhich_from_fish_shell() { }; let fixture_name = "fish_and_bandwhich"; let mut fake_input_output = get_fake_os_input(&fake_win_size, fixture_name); + fake_input_output.add_terminal_input(&[&QUIT]); start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -73,6 +75,7 @@ pub fn fish_tab_completion_options() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -106,6 +109,7 @@ pub fn fish_select_tab_completion_options() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -143,6 +147,7 @@ pub fn vim_scroll_region_down() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -177,6 +182,7 @@ pub fn vim_ctrl_d() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -210,6 +216,7 @@ pub fn vim_ctrl_u() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -238,6 +245,7 @@ pub fn htop() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -266,6 +274,7 @@ pub fn htop_scrolling() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -294,6 +303,7 @@ pub fn htop_right_scrolling() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -330,6 +340,7 @@ pub fn vim_overwrite() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -361,6 +372,7 @@ pub fn clear_scroll_region() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -389,6 +401,7 @@ pub fn display_tab_characters_properly() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -417,6 +430,7 @@ pub fn neovim_insert_mode() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -447,6 +461,7 @@ pub fn bash_cursor_linewrap() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -477,6 +492,7 @@ pub fn fish_paste_multiline() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -505,6 +521,7 @@ pub fn git_log() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -535,6 +552,7 @@ pub fn git_diff_scrollup() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -563,6 +581,7 @@ pub fn emacs_longbuf() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -591,6 +610,7 @@ pub fn top_and_quit() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output @@ -625,6 +645,7 @@ pub fn exa_plus_omf_theme() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); let output_frames = fake_input_output diff --git a/src/tests/integration/layouts.rs b/src/tests/integration/layouts.rs index a2ac4702..a07995f6 100644 --- a/src/tests/integration/layouts.rs +++ b/src/tests/integration/layouts.rs @@ -28,7 +28,12 @@ pub fn accepts_basic_layout() { "src/tests/fixtures/layouts/three-panes-with-nesting.yaml", )); - start(Box::new(fake_input_output.clone()), opts, Config::default()); + start( + Box::new(fake_input_output.clone()), + opts, + Box::new(fake_input_output.clone()), + Config::default(), + ); let output_frames = fake_input_output .stdout_writer .output_frames diff --git a/src/tests/integration/move_focus_down.rs b/src/tests/integration/move_focus_down.rs index 21fa7925..8ea953c6 100644 --- a/src/tests/integration/move_focus_down.rs +++ b/src/tests/integration/move_focus_down.rs @@ -35,6 +35,7 @@ pub fn move_focus_down() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -71,6 +72,7 @@ pub fn move_focus_down_to_the_most_recently_used_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); diff --git a/src/tests/integration/move_focus_left.rs b/src/tests/integration/move_focus_left.rs index 6441357d..3b87bb95 100644 --- a/src/tests/integration/move_focus_left.rs +++ b/src/tests/integration/move_focus_left.rs @@ -34,6 +34,7 @@ pub fn move_focus_left() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -71,6 +72,7 @@ pub fn move_focus_left_to_the_most_recently_used_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); diff --git a/src/tests/integration/move_focus_right.rs b/src/tests/integration/move_focus_right.rs index bc4f551c..c6c17a75 100644 --- a/src/tests/integration/move_focus_right.rs +++ b/src/tests/integration/move_focus_right.rs @@ -35,6 +35,7 @@ pub fn move_focus_right() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -71,6 +72,7 @@ pub fn move_focus_right_to_the_most_recently_used_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); diff --git a/src/tests/integration/move_focus_up.rs b/src/tests/integration/move_focus_up.rs index 77f06703..62b64ed1 100644 --- a/src/tests/integration/move_focus_up.rs +++ b/src/tests/integration/move_focus_up.rs @@ -34,6 +34,7 @@ pub fn move_focus_up() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -71,6 +72,7 @@ pub fn move_focus_up_to_the_most_recently_used_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); diff --git a/src/tests/integration/resize_down.rs b/src/tests/integration/resize_down.rs index d5ad4ef8..58104fe1 100644 --- a/src/tests/integration/resize_down.rs +++ b/src/tests/integration/resize_down.rs @@ -46,6 +46,7 @@ pub fn resize_down_with_pane_above() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -90,6 +91,7 @@ pub fn resize_down_with_pane_below() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -140,6 +142,7 @@ pub fn resize_down_with_panes_above_and_below() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -189,6 +192,7 @@ pub fn resize_down_with_multiple_panes_above() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -240,6 +244,7 @@ pub fn resize_down_with_panes_above_aligned_left_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -290,6 +295,7 @@ pub fn resize_down_with_panes_below_aligned_left_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -338,6 +344,7 @@ pub fn resize_down_with_panes_above_aligned_right_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -387,6 +394,7 @@ pub fn resize_down_with_panes_below_aligned_right_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -439,6 +447,7 @@ pub fn resize_down_with_panes_above_aligned_left_and_right_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -493,6 +502,7 @@ pub fn resize_down_with_panes_below_aligned_left_and_right_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -564,6 +574,7 @@ pub fn resize_down_with_panes_above_aligned_left_and_right_with_panes_to_the_lef start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -637,6 +648,7 @@ pub fn resize_down_with_panes_below_aligned_left_and_right_with_to_the_left_and_ start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -678,6 +690,7 @@ pub fn cannot_resize_down_when_pane_below_is_at_minimum_height() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); diff --git a/src/tests/integration/resize_left.rs b/src/tests/integration/resize_left.rs index 2444f3a1..d31dd6d2 100644 --- a/src/tests/integration/resize_left.rs +++ b/src/tests/integration/resize_left.rs @@ -42,6 +42,7 @@ pub fn resize_left_with_pane_to_the_left() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -84,6 +85,7 @@ pub fn resize_left_with_pane_to_the_right() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -128,6 +130,7 @@ pub fn resize_left_with_panes_to_the_left_and_right() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -175,6 +178,7 @@ pub fn resize_left_with_multiple_panes_to_the_left() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -224,6 +228,7 @@ pub fn resize_left_with_panes_to_the_left_aligned_top_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -270,6 +275,7 @@ pub fn resize_left_with_panes_to_the_right_aligned_top_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -318,6 +324,7 @@ pub fn resize_left_with_panes_to_the_left_aligned_bottom_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -365,6 +372,7 @@ pub fn resize_left_with_panes_to_the_right_aligned_bottom_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -417,6 +425,7 @@ pub fn resize_left_with_panes_to_the_left_aligned_top_and_bottom_with_current_pa start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -471,6 +480,7 @@ pub fn resize_left_with_panes_to_the_right_aligned_top_and_bottom_with_current_p start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -542,6 +552,7 @@ pub fn resize_left_with_panes_to_the_left_aligned_top_and_bottom_with_panes_abov start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -616,6 +627,7 @@ pub fn resize_left_with_panes_to_the_right_aligned_top_and_bottom_with_panes_abo start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -657,6 +669,7 @@ pub fn cannot_resize_left_when_pane_to_the_left_is_at_minimum_width() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); diff --git a/src/tests/integration/resize_right.rs b/src/tests/integration/resize_right.rs index 3f242f5a..e5ea1b9d 100644 --- a/src/tests/integration/resize_right.rs +++ b/src/tests/integration/resize_right.rs @@ -42,6 +42,7 @@ pub fn resize_right_with_pane_to_the_left() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -84,6 +85,7 @@ pub fn resize_right_with_pane_to_the_right() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -128,6 +130,7 @@ pub fn resize_right_with_panes_to_the_left_and_right() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -175,6 +178,7 @@ pub fn resize_right_with_multiple_panes_to_the_left() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -224,6 +228,7 @@ pub fn resize_right_with_panes_to_the_left_aligned_top_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -270,6 +275,7 @@ pub fn resize_right_with_panes_to_the_right_aligned_top_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -318,6 +324,7 @@ pub fn resize_right_with_panes_to_the_left_aligned_bottom_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -365,6 +372,7 @@ pub fn resize_right_with_panes_to_the_right_aligned_bottom_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -417,6 +425,7 @@ pub fn resize_right_with_panes_to_the_left_aligned_top_and_bottom_with_current_p start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -471,6 +480,7 @@ pub fn resize_right_with_panes_to_the_right_aligned_top_and_bottom_with_current_ start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -542,6 +552,7 @@ pub fn resize_right_with_panes_to_the_left_aligned_top_and_bottom_with_panes_abo start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -615,6 +626,7 @@ pub fn resize_right_with_panes_to_the_right_aligned_top_and_bottom_with_panes_ab start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -656,6 +668,7 @@ pub fn cannot_resize_right_when_pane_to_the_left_is_at_minimum_width() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); diff --git a/src/tests/integration/resize_up.rs b/src/tests/integration/resize_up.rs index e2d0c4ba..14819331 100644 --- a/src/tests/integration/resize_up.rs +++ b/src/tests/integration/resize_up.rs @@ -44,6 +44,7 @@ pub fn resize_up_with_pane_above() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -88,6 +89,7 @@ pub fn resize_up_with_pane_below() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -137,6 +139,7 @@ pub fn resize_up_with_panes_above_and_below() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -185,6 +188,7 @@ pub fn resize_up_with_multiple_panes_above() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -234,6 +238,7 @@ pub fn resize_up_with_panes_above_aligned_left_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -284,6 +289,7 @@ pub fn resize_up_with_panes_below_aligned_left_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -332,6 +338,7 @@ pub fn resize_up_with_panes_above_aligned_right_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -381,6 +388,7 @@ pub fn resize_up_with_panes_below_aligned_right_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -433,6 +441,7 @@ pub fn resize_up_with_panes_above_aligned_left_and_right_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -487,6 +496,7 @@ pub fn resize_up_with_panes_below_aligned_left_and_right_with_current_pane() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -558,6 +568,7 @@ pub fn resize_up_with_panes_above_aligned_left_and_right_with_panes_to_the_left_ start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -631,6 +642,7 @@ pub fn resize_up_with_panes_below_aligned_left_and_right_with_to_the_left_and_ri start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -672,6 +684,7 @@ pub fn cannot_resize_up_when_pane_above_is_at_minimum_height() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); diff --git a/src/tests/integration/tabs.rs b/src/tests/integration/tabs.rs index f62ad0c4..0c751634 100644 --- a/src/tests/integration/tabs.rs +++ b/src/tests/integration/tabs.rs @@ -36,6 +36,7 @@ pub fn open_new_tab() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -71,6 +72,7 @@ pub fn switch_to_prev_tab() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -106,6 +108,7 @@ pub fn switch_to_next_tab() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -141,6 +144,7 @@ pub fn close_tab() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -177,6 +181,7 @@ pub fn close_last_pane_in_a_tab() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -215,6 +220,7 @@ pub fn close_the_middle_tab() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -258,6 +264,7 @@ pub fn close_the_tab_that_has_a_pane_in_fullscreen() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -293,6 +300,7 @@ pub fn closing_last_tab_exits_the_app() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); diff --git a/src/tests/integration/terminal_window_resize.rs b/src/tests/integration/terminal_window_resize.rs index ebea82c0..057c2523 100644 --- a/src/tests/integration/terminal_window_resize.rs +++ b/src/tests/integration/terminal_window_resize.rs @@ -30,7 +30,12 @@ pub fn window_width_decrease_with_one_pane() { ..Default::default() }); let opts = CliArgs::default(); - start(Box::new(fake_input_output.clone()), opts, Config::default()); + start( + Box::new(fake_input_output.clone()), + opts, + Box::new(fake_input_output.clone()), + Config::default(), + ); let output_frames = fake_input_output .stdout_writer .output_frames @@ -61,7 +66,12 @@ pub fn window_width_increase_with_one_pane() { ..Default::default() }); let opts = CliArgs::default(); - start(Box::new(fake_input_output.clone()), opts, Config::default()); + start( + Box::new(fake_input_output.clone()), + opts, + Box::new(fake_input_output.clone()), + Config::default(), + ); let output_frames = fake_input_output .stdout_writer .output_frames @@ -92,7 +102,12 @@ pub fn window_height_increase_with_one_pane() { ..Default::default() }); let opts = CliArgs::default(); - start(Box::new(fake_input_output.clone()), opts, Config::default()); + start( + Box::new(fake_input_output.clone()), + opts, + Box::new(fake_input_output.clone()), + Config::default(), + ); let output_frames = fake_input_output .stdout_writer .output_frames @@ -123,7 +138,12 @@ pub fn window_width_and_height_decrease_with_one_pane() { ..Default::default() }); let opts = CliArgs::default(); - start(Box::new(fake_input_output.clone()), opts, Config::default()); + start( + Box::new(fake_input_output.clone()), + opts, + Box::new(fake_input_output.clone()), + Config::default(), + ); let output_frames = fake_input_output .stdout_writer .output_frames diff --git a/src/tests/integration/toggle_fullscreen.rs b/src/tests/integration/toggle_fullscreen.rs index 9fd22144..0e967bd2 100644 --- a/src/tests/integration/toggle_fullscreen.rs +++ b/src/tests/integration/toggle_fullscreen.rs @@ -35,6 +35,7 @@ pub fn adding_new_terminal_in_fullscreen() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), ); @@ -69,6 +70,7 @@ pub fn move_focus_is_disabled_in_fullscreen() { start( Box::new(fake_input_output.clone()), CliArgs::default(), + Box::new(fake_input_output.clone()), Config::default(), );