diff options
-rw-r--r-- | .cargo/config.toml | 1 | ||||
-rw-r--r-- | Cargo.lock | 11 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/commands.rs | 68 | ||||
-rw-r--r-- | src/main.rs | 3 | ||||
-rw-r--r-- | zellij-client/src/fake_client.rs | 181 | ||||
-rw-r--r-- | zellij-client/src/input_handler.rs | 117 | ||||
-rw-r--r-- | zellij-client/src/lib.rs | 14 | ||||
-rw-r--r-- | zellij-client/src/sessions.rs | 18 | ||||
-rw-r--r-- | zellij-client/src/unit/input_handler_tests.rs | 2 | ||||
-rw-r--r-- | zellij-server/src/lib.rs | 58 | ||||
-rw-r--r-- | zellij-server/src/route.rs | 12 | ||||
-rw-r--r-- | zellij-utils/src/cli.rs | 2 | ||||
-rw-r--r-- | zellij-utils/src/errors.rs | 2 | ||||
-rw-r--r-- | zellij-utils/src/input/actions.rs | 10 | ||||
-rw-r--r-- | zellij-utils/src/ipc.rs | 10 |
16 files changed, 455 insertions, 55 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..d9436ca1c --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1 @@ +parallel-compiler = true diff --git a/Cargo.lock b/Cargo.lock index 6a7ff22a8..d189731f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,16 +355,16 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.2" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e538f9ee5aa3b3963f09a997035f883677966ed50fce0292611927ce6f6d8c6" +checksum = "6d20de3739b4fb45a17837824f40aa1769cc7655d7a83e68739a77fe7b30c87a" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static", + "once_cell", "strsim", "termcolor", "textwrap 0.15.0", @@ -381,9 +381,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.2" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f98063cac4652f23ccda556b8d04347a7fc4b2cff1f7577cc8c6546e0d8078" +checksum = "026baf08b89ffbd332836002ec9378ef0e69648cbfadd68af7cd398ca5bf98f7" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -3248,6 +3248,7 @@ dependencies = [ "dialoguer", "insta", "log", + "miette", "names", "rand 0.8.5", "ssh2", diff --git a/Cargo.toml b/Cargo.toml index 2234bd90c..c8ea31bf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ rust-version = "1.59" [dependencies] anyhow = "1.0" names = { version = "0.13.0", default-features = false } +miette = { version = "3.3.0", features = ["fancy"] } zellij-client = { path = "zellij-client/", version = "0.31.0" } zellij-server = { path = "zellij-server/", version = "0.31.0" } zellij-utils = { path = "zellij-utils/", version = "0.31.0" } diff --git a/src/commands.rs b/src/commands.rs index f92ba8b43..7715d4353 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,12 +6,14 @@ use crate::sessions::{ session_exists, ActiveSession, SessionNameMatch, }; use dialoguer::Confirm; +use miette::{IntoDiagnostic, Result}; use std::path::PathBuf; use std::process; use zellij_client::start_client as start_client_impl; use zellij_client::{os_input_output::get_client_os_input, ClientInfo}; use zellij_server::os_input_output::get_server_os_input; use zellij_server::start_server as start_server_impl; +use zellij_utils::input::actions::ActionsFromYaml; use zellij_utils::input::options::Options; use zellij_utils::nix; use zellij_utils::{ @@ -112,6 +114,72 @@ fn find_indexed_session( } } +/// Send a vec of `[Action]` to a currently running session. +pub(crate) fn send_action_to_session(opts: zellij_utils::cli::CliArgs) { + match get_active_session() { + ActiveSession::None => { + eprintln!("There is no active session!"); + std::process::exit(1); + }, + ActiveSession::One(session_name) => { + attach_with_fake_client(opts, &session_name); + }, + ActiveSession::Many => { + if let Some(session_name) = opts.session.clone() { + attach_with_fake_client(opts, &session_name); + } else if let Ok(session_name) = envs::get_session_name() { + attach_with_fake_client(opts, &session_name); + } else { + println!("Please specify the session name to send actions to. The following sessions are active:"); + print_sessions(get_sessions().unwrap()); + std::process::exit(1); + } + }, + }; +} + +fn attach_with_fake_client(opts: zellij_utils::cli::CliArgs, name: &str) { + if let Some(zellij_utils::cli::Command::Sessions(zellij_utils::cli::Sessions::Action { + action, + })) = opts.command.clone() + { + if let Some(action) = action.clone() { + let action = format!("[{}]", action); + match zellij_utils::serde_yaml::from_str::<ActionsFromYaml>(&action).into_diagnostic() { + Ok(parsed) => { + let (config, _, config_options) = match Setup::from_options(&opts) { + Ok(results) => results, + Err(e) => { + eprintln!("{}", e); + process::exit(1); + }, + }; + let os_input = + get_os_input(zellij_client::os_input_output::get_client_os_input); + + let actions = parsed.actions().to_vec(); + log::debug!("Starting fake Zellij client!"); + zellij_client::fake_client::start_fake_client( + Box::new(os_input), + opts, + *Box::new(config), + config_options, + ClientInfo::New(name.to_string()), + None, + actions, + ); + log::debug!("Quitting fake client now."); + std::process::exit(0); + }, + Err(e) => { + eprintln!("{:?}", e); + std::process::exit(1); + }, + }; + } + }; +} + fn attach_with_session_index(config_options: Options, index: usize, create: bool) -> ClientInfo { // Ignore the session_name when `--index` is provided match get_sessions_sorted_by_mtime() { diff --git a/src/main.rs b/src/main.rs index 98b27581a..6e5cd9e95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,9 @@ fn main() { if let Some(Command::Sessions(Sessions::ListSessions)) = opts.command { commands::list_sessions(); + } + if let Some(Command::Sessions(Sessions::Action { .. })) = opts.command { + commands::send_action_to_session(opts); } else if let Some(Command::Sessions(Sessions::KillAllSessions { yes })) = opts.command { commands::kill_all_sessions(yes); } else if let Some(Command::Sessions(Sessions::KillSession { ref target_session })) = diff --git a/zellij-client/src/fake_client.rs b/zellij-client/src/fake_client.rs new file mode 100644 index 000000000..d3528d875 --- /dev/null +++ b/zellij-client/src/fake_client.rs @@ -0,0 +1,181 @@ +//! The `[fake_client]` is used to attach to a running server session +//! and dispatch actions, that are specificed through the command line. +//! Multiple actions at the same time can be dispatched. +use log::debug; +use std::{fs, path::PathBuf, thread}; +use zellij_tile::prelude::{ClientId, Style}; +use zellij_utils::errors::ContextType; + +use crate::{ + command_is_executing::CommandIsExecuting, input_handler::input_actions, + os_input_output::ClientOsApi, stdin_handler::stdin_loop, ClientInfo, ClientInstruction, + InputInstruction, +}; +use zellij_utils::{ + channels::{self, ChannelWithContext, SenderWithContext}, + cli::CliArgs, + input::{actions::Action, config::Config, layout::LayoutFromYaml, options::Options}, + ipc::{ClientAttributes, ClientToServerMsg, ServerToClientMsg}, +}; + +pub fn start_fake_client( + os_input: Box<dyn ClientOsApi>, + _opts: CliArgs, + config: Config, + config_options: Options, + info: ClientInfo, + _layout: Option<LayoutFromYaml>, + actions: Vec<Action>, +) { + debug!("Starting fake Zellij client!"); + let session_name = info.get_session_name(); + + // TODO: Ideally the `fake_client` would not need to specify these options, + // but the `[NewTab:]` action depends on this state being + // even in this client. + let palette = config.themes.clone().map_or_else( + || os_input.load_palette(), + |t| { + t.theme_config(&config_options) + .unwrap_or_else(|| os_input.load_palette()) + }, + ); + + let full_screen_ws = os_input.get_terminal_size_using_fd(0); + let client_attributes = ClientAttributes { + size: full_screen_ws, + style: Style { + colors: palette, + rounded_corners: config.ui.unwrap_or_default().pane_frames.rounded_corners, + }, + }; + + let first_msg = ClientToServerMsg::AttachClient(client_attributes, config_options.clone()); + + let zellij_ipc_pipe: PathBuf = { + let mut sock_dir = zellij_utils::consts::ZELLIJ_SOCK_DIR.clone(); + fs::create_dir_all(&sock_dir).unwrap(); + zellij_utils::shared::set_permissions(&sock_dir, 0o700).unwrap(); + sock_dir.push(session_name); + sock_dir + }; + os_input.connect_to_server(&*zellij_ipc_pipe); + os_input.send_to_server(first_msg); + + let mut command_is_executing = CommandIsExecuting::new(); + + let (send_client_instructions, receive_client_instructions): ChannelWithContext< + ClientInstruction, + > = channels::bounded(50); + let send_client_instructions = SenderWithContext::new(send_client_instructions); + + let (send_input_instructions, receive_input_instructions): ChannelWithContext< + InputInstruction, + > = channels::bounded(50); + let send_input_instructions = SenderWithContext::new(send_input_instructions); + + std::panic::set_hook({ + use zellij_utils::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 os_input = os_input.clone(); + let send_input_instructions = send_input_instructions.clone(); + move || stdin_loop(os_input, send_input_instructions) + }); + + let clients: Vec<ClientId>; + os_input.send_to_server(ClientToServerMsg::ListClients); + #[allow(clippy::collapsible_match)] + loop { + if let Some((msg, _)) = os_input.recv_from_server() { + if let ServerToClientMsg::ActiveClients(active_clients) = msg { + clients = active_clients; + break; + } + } + } + debug!("The connected client id's are: {:?}.", clients); + + let _input_thread = thread::Builder::new() + .name("input_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(); + let default_mode = config_options.default_mode.unwrap_or_default(); + let session_name = session_name.to_string(); + move || { + input_actions( + os_input, + config, + config_options, + command_is_executing, + clients, + send_client_instructions, + default_mode, + receive_input_instructions, + actions, + session_name, + ) + } + }); + + let router_thread = thread::Builder::new() + .name("router".to_string()) + .spawn({ + let os_input = os_input.clone(); + let mut should_break = false; + move || loop { + if let Some((instruction, err_ctx)) = os_input.recv_from_server() { + err_ctx.update_thread_ctx(); + if let ServerToClientMsg::Exit(_) = instruction { + should_break = true; + } + send_client_instructions.send(instruction.into()).unwrap(); + if should_break { + break; + } + } + } + }) + .unwrap(); + + 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((&client_instruction).into())); + match client_instruction { + ClientInstruction::Exit(_) => { + os_input.send_to_server(ClientToServerMsg::ClientExited); + break; + }, + ClientInstruction::Error(_) => { + let _ = os_input.send_to_server(ClientToServerMsg::Action(Action::Quit, None)); + // handle_error(backtrace); + }, + ClientInstruction::Render(_) => { + // This is a fake client, that doesn't render, but + // dispatches actions. + }, + ClientInstruction::UnblockInputThread => { + command_is_executing.unblock_input_thread(); + }, + ClientInstruction::SwitchToMode(input_mode) => { + send_input_instructions + .send(InputInstruction::SwitchToMode(input_mode)) + .unwrap(); + }, + _ => {}, + } + } + router_thread.join().unwrap(); +} diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 6e3c71e1d..d0bfe26b1 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -11,7 +11,7 @@ use zellij_utils::{ use crate::{ os_input_output::ClientOsApi, stdin_ansi_parser::{AnsiStdinInstructionOrKeys, StdinAnsiParser}, - ClientInstruction, CommandIsExecuting, InputInstruction, + ClientId, ClientInstruction, CommandIsExecuting, InputInstruction, }; use zellij_utils::{ channels::{Receiver, SenderWithContext, OPENCALLS}, @@ -108,11 +108,18 @@ impl InputHandler { }, InputEvent::Paste(pasted_text) => { if self.mode == InputMode::Normal || self.mode == InputMode::Locked { - self.dispatch_action(Action::Write(bracketed_paste_start.clone())); - self.dispatch_action(Action::Write( - pasted_text.as_bytes().to_vec(), - )); - self.dispatch_action(Action::Write(bracketed_paste_end.clone())); + self.dispatch_action( + Action::Write(bracketed_paste_start.clone()), + None, + ); + self.dispatch_action( + Action::Write(pasted_text.as_bytes().to_vec()), + None, + ); + self.dispatch_action( + Action::Write(bracketed_paste_end.clone()), + None, + ); } }, _ => {}, @@ -136,7 +143,7 @@ impl InputHandler { fn handle_key(&mut self, key: &Key, raw_bytes: Vec<u8>) { let keybinds = &self.config.keybinds; for action in Keybinds::key_to_actions(key, raw_bytes, &self.mode, keybinds) { - let should_exit = self.dispatch_action(action); + let should_exit = self.dispatch_action(action, None); if should_exit { self.should_exit = true; } @@ -175,39 +182,80 @@ impl InputHandler { match *mouse_event { MouseEvent::Press(button, point) => match button { MouseButton::WheelUp => { - self.dispatch_action(Action::ScrollUpAt(point)); + self.dispatch_action(Action::ScrollUpAt(point), None); }, MouseButton::WheelDown => { - self.dispatch_action(Action::ScrollDownAt(point)); + self.dispatch_action(Action::ScrollDownAt(point), None); }, MouseButton::Left => { if self.holding_mouse { - self.dispatch_action(Action::MouseHold(point)); + self.dispatch_action(Action::MouseHold(point), None); } else { - self.dispatch_action(Action::LeftClick(point)); + self.dispatch_action(Action::LeftClick(point), None); } self.holding_mouse = true; }, MouseButton::Right => { if self.holding_mouse { - self.dispatch_action(Action::MouseHold(point)); + self.dispatch_action(Action::MouseHold(point), None); } else { - self.dispatch_action(Action::RightClick(point)); + self.dispatch_action(Action::RightClick(point), None); } self.holding_mouse = true; }, _ => {}, }, MouseEvent::Release(point) => { - self.dispatch_action(Action::MouseRelease(point)); + self.dispatch_action(Action::MouseRelease(point), None); self.holding_mouse = false; }, MouseEvent::Hold(point) => { - self.dispatch_action(Action::MouseHold(point)); + self.dispatch_action(Action::MouseHold(point), None); self.holding_mouse = true; }, } } + fn handle_actions(&mut self, actions: Vec<Action>, session_name: &str, clients: Vec<ClientId>) { + // TODO: handle Detach correctly + for action in actions { + match action { + Action::Quit => { + crate::sessions::kill_session(session_name); + break; + }, + Action::Detach => { + // self.should_exit = true; + // clients.split_last().into_iter().for_each(|(client_id, _)| { + let first = clients.first().unwrap(); + let last = clients.last().unwrap(); + self.os_input + .send_to_server(ClientToServerMsg::DetachSession(vec![*first, *last])); + // }); + break; + }, + // Actions, that are indepenedent from the specific client + // should be specified here. + Action::NewTab(_) | Action::Run(_) | Action::NewPane(_) => { + let client_id = clients.first().unwrap(); + log::error!("Sending action to client: {}", client_id); + self.dispatch_action(action, Some(*client_id)); + }, + _ => { + // TODO only dispatch for each client, for actions that need it + for client_id in &clients { + self.dispatch_action(action.clone(), Some(*client_id)); + } + }, + } + } + self.dispatch_action(Action::Detach, None); + // is this correct? should be just for this current client + self.should_exit = true; + log::error!("Quitting Now. Dispatched the actions"); + // std::process::exit(0); + //self.dispatch_action(Action::NoOp); + self.exit(); + } /// Dispatches an [`Action`]. /// @@ -220,14 +268,14 @@ impl InputHandler { /// This is a temporary measure that is only necessary due to the way that the /// framework works, and shouldn't be necessary anymore once the test framework /// is revised. See [issue#183](https://github.com/zellij-org/zellij/issues/183). - fn dispatch_action(&mut self, action: Action) -> bool { + fn dispatch_action(&mut self, action: Action, client_id: Option<ClientId>) -> bool { let mut should_break = false; match action { Action::NoOp => {}, Action::Quit | Action::Detach => { self.os_input - .send_to_server(ClientToServerMsg::Action(action)); + .send_to_server(ClientToServerMsg::Action(action, client_id)); self.exit(); should_break = true; }, @@ -236,10 +284,11 @@ impl InputHandler { // server later that atomically changes the mode as well self.mode = mode; self.os_input - .send_to_server(ClientToServerMsg::Action(action)); + .send_to_server(ClientToServerMsg::Action(action, None)); }, Action::CloseFocus | Action::NewPane(_) + | Action::Run(_) | Action::ToggleFloatingPanes | Action::TogglePaneEmbedOrFloating | Action::NewTab(_) @@ -250,14 +299,15 @@ impl InputHandler { | Action::ToggleTab | Action::MoveFocusOrTab(_) => { self.command_is_executing.blocking_input_thread(); + log::error!("Blocking input thread."); self.os_input - .send_to_server(ClientToServerMsg::Action(action)); + .send_to_server(ClientToServerMsg::Action(action, client_id)); self.command_is_executing .wait_until_input_thread_is_unblocked(); }, _ => self .os_input - .send_to_server(ClientToServerMsg::Action(action)), + .send_to_server(ClientToServerMsg::Action(action, client_id)), } should_break @@ -295,6 +345,33 @@ pub(crate) fn input_loop( .handle_input(); } +/// Entry point to the module. Instantiates an [`InputHandler`] and starts +/// its [`InputHandler::handle_input()`] loop. +#[allow(clippy::too_many_arguments)] +pub(crate) fn input_actions( + os_input: Box<dyn ClientOsApi>, + config: Config, + options: Options, + command_is_executing: CommandIsExecuting, + clients: Vec<ClientId>, + send_client_instructions: SenderWithContext<ClientInstruction>, + default_mode: InputMode, + receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, + actions: Vec<Action>, + session_name: String, +) { + let _handler = InputHandler::new( + os_input, + command_is_executing, + config, + options, + send_client_instructions, + default_mode, + receive_input_instructions, + ) + .handle_actions(actions, &session_name, clients); +} + #[cfg(test)] #[path = "./unit/input_handler_tests.rs"] mod input_handler_tests; diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 2d7764961..0afbe913b 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -1,7 +1,9 @@ pub mod os_input_output; mod command_is_executing; +pub mod fake_client; mod input_handler; +mod sessions; mod stdin_ansi_parser; mod stdin_handler; @@ -12,7 +14,7 @@ use std::io::{self, Write}; use std::path::Path; use std::process::Command; use std::thread; -use zellij_tile::prelude::Style; +use zellij_tile::prelude::{ClientId, Style}; use crate::{ command_is_executing::CommandIsExecuting, input_handler::input_loop, @@ -39,6 +41,7 @@ pub(crate) enum ClientInstruction { Exit(ExitReason), SwitchToMode(InputMode), Connected, + ActiveClients(Vec<ClientId>), } impl From<ServerToClientMsg> for ClientInstruction { @@ -51,6 +54,7 @@ impl From<ServerToClientMsg> for ClientInstruction { ClientInstruction::SwitchToMode(input_mode) }, ServerToClientMsg::Connected => ClientInstruction::Connected, + ServerToClientMsg::ActiveClients(clients) => ClientInstruction::ActiveClients(clients), } } } @@ -64,6 +68,7 @@ impl From<&ClientInstruction> for ClientContext { ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread, ClientInstruction::SwitchToMode(_) => ClientContext::SwitchToMode, ClientInstruction::Connected => ClientContext::Connected, + ClientInstruction::A |