diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..d9436ca1 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1 @@ +parallel-compiler = true diff --git a/Cargo.lock b/Cargo.lock index 6a7ff22a..d189731f 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 2234bd90..c8ea31bf 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 f92ba8b4..7715d435 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::(&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 98b27581..6e5cd9e9 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 00000000..d3528d87 --- /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, + _opts: CliArgs, + config: Config, + config_options: Options, + info: ClientInfo, + _layout: Option, + actions: Vec, +) { + 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; + 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 6e3c71e1..d0bfe26b 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) { 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, session_name: &str, clients: Vec) { + // 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) -> 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, + config: Config, + options: Options, + command_is_executing: CommandIsExecuting, + clients: Vec, + send_client_instructions: SenderWithContext, + default_mode: InputMode, + receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, + actions: Vec, + 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 2d776496..0afbe913 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), } impl From for ClientInstruction { @@ -51,6 +54,7 @@ impl From 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::ActiveClients(_) => ClientContext::ActiveClients, } } } @@ -259,7 +264,10 @@ pub fn start_client( Box::new({ let os_api = os_input.clone(); move || { - os_api.send_to_server(ClientToServerMsg::Action(on_force_close.into())); + os_api.send_to_server(ClientToServerMsg::Action( + on_force_close.into(), + None, + )); } }), ); @@ -331,7 +339,7 @@ pub fn start_client( break; }, ClientInstruction::Error(backtrace) => { - let _ = os_input.send_to_server(ClientToServerMsg::Action(Action::Quit)); + let _ = os_input.send_to_server(ClientToServerMsg::Action(Action::Quit, None)); handle_error(backtrace); }, ClientInstruction::Render(output) => { diff --git a/zellij-client/src/sessions.rs b/zellij-client/src/sessions.rs new file mode 100644 index 00000000..e3e9c4c3 --- /dev/null +++ b/zellij-client/src/sessions.rs @@ -0,0 +1,18 @@ +use std::process; + +use zellij_utils::consts::ZELLIJ_SOCK_DIR; +use zellij_utils::interprocess::local_socket::LocalSocketStream; +use zellij_utils::ipc::{ClientToServerMsg, IpcSenderWithContext}; + +pub(crate) fn kill_session(name: &str) { + let path = &*ZELLIJ_SOCK_DIR.join(name); + match LocalSocketStream::connect(path) { + Ok(stream) => { + IpcSenderWithContext::new(stream).send(ClientToServerMsg::KillSession); + }, + Err(e) => { + eprintln!("Error occurred: {:?}", e); + process::exit(1); + }, + }; +} diff --git a/zellij-client/src/unit/input_handler_tests.rs b/zellij-client/src/unit/input_handler_tests.rs index ba5ac11c..8fe4a5af 100644 --- a/zellij-client/src/unit/input_handler_tests.rs +++ b/zellij-client/src/unit/input_handler_tests.rs @@ -176,7 +176,7 @@ fn extract_actions_sent_to_server( ) -> Vec { let events_sent_to_server = events_sent_to_server.lock().unwrap(); events_sent_to_server.iter().fold(vec![], |mut acc, event| { - if let ClientToServerMsg::Action(action) = event { + if let ClientToServerMsg::Action(action, None) = event { acc.push(action.clone()); } acc diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 2841c932..00b5dbb8 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -72,9 +72,10 @@ pub enum ServerInstruction { RemoveClient(ClientId), Error(String), KillSession, - DetachSession(ClientId), + DetachSession(Vec), AttachClient(ClientAttributes, Options, ClientId), ConnStatus(ClientId), + ActiveClients(ClientId), } impl From<&ServerInstruction> for ServerContext { @@ -90,6 +91,7 @@ impl From<&ServerInstruction> for ServerContext { ServerInstruction::DetachSession(..) => ServerContext::DetachSession, ServerInstruction::AttachClient(..) => ServerContext::AttachClient, ServerInstruction::ConnStatus(..) => ServerContext::ConnStatus, + ServerInstruction::ActiveClients(_) => ServerContext::ActiveClients, } } } @@ -469,35 +471,38 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { } break; }, - ServerInstruction::DetachSession(client_id) => { - os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); - remove_client!(client_id, os_input, session_state); - if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() { + ServerInstruction::DetachSession(client_ids) => { + for client_id in client_ids { + os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); + remove_client!(client_id, os_input, session_state); + if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() + { + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) + .unwrap(); + } session_data .write() .unwrap() .as_ref() .unwrap() .senders - .send_to_screen(ScreenInstruction::TerminalResize(min_size)) + .send_to_screen(ScreenInstruction::RemoveClient(client_id)) + .unwrap(); + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_plugin(PluginInstruction::RemoveClient(client_id)) .unwrap(); } - session_data - .write() - .unwrap() - .as_ref() - .unwrap() - .senders - .send_to_screen(ScreenInstruction::RemoveClient(client_id)) - .unwrap(); - session_data - .write() - .unwrap() - .as_ref() - .unwrap() - .senders - .send_to_plugin(PluginInstruction::RemoveClient(client_id)) - .unwrap(); }, ServerInstruction::Render(serialized_output) => { let client_ids = session_state.read().unwrap().client_ids(); @@ -534,6 +539,15 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { os_input.send_to_client(client_id, ServerToClientMsg::Connected); remove_client!(client_id, os_input, session_state); }, + ServerInstruction::ActiveClients(client_id) => { + let client_ids = session_state.read().unwrap().client_ids(); + log::error!( + "Sending client_ids {:?} to client {}", + client_ids, + client_id + ); + os_input.send_to_client(client_id, ServerToClientMsg::ActiveClients(client_ids)); + }, } } diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 3057ec0e..f95f17ec 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -338,7 +338,7 @@ fn route_action( }, Action::Detach => { to_server - .send(ServerInstruction::DetachSession(client_id)) + .send(ServerInstruction::DetachSession(vec![client_id])) .unwrap(); should_break = true; }, @@ -415,7 +415,8 @@ pub(crate) fn route_thread_main( let rlocked_sessions = session_data.read().unwrap(); match instruction { - ClientToServerMsg::Action(action) => { + ClientToServerMsg::Action(action, maybe_client_id) => { + let client_id = maybe_client_id.unwrap_or(client_id); if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { if let Action::SwitchToMode(input_mode) = action { os_input.send_to_client( @@ -516,6 +517,13 @@ pub(crate) fn route_thread_main( let _ = to_server.send(ServerInstruction::ConnStatus(client_id)); break; }, + ClientToServerMsg::DetachSession(client_id) => { + let _ = to_server.send(ServerInstruction::DetachSession(client_id)); + break; + }, + ClientToServerMsg::ListClients => { + let _ = to_server.send(ServerInstruction::ActiveClients(client_id)); + }, } }, None => { diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 7f153249..4b33fc6c 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -109,4 +109,6 @@ pub enum Sessions { #[clap(short, long, value_parser)] yes: bool, }, + /// Send actions to a specific session + Action { action: Option }, } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 10c7330b..13cf516b 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -327,6 +327,7 @@ pub enum ClientContext { ServerError, SwitchToMode, Connected, + ActiveClients, } /// Stack call representations corresponding to the different types of [`ServerInstruction`]s. @@ -342,6 +343,7 @@ pub enum ServerContext { DetachSession, AttachClient, ConnStatus, + ActiveClients, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 3c863f8b..4bc03c42 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -129,3 +129,13 @@ impl From for Action { } } } + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct ActionsFromYaml(Vec); + +impl ActionsFromYaml { + /// Get a reference to the actions from yaml's actions. + pub fn actions(&self) -> &[Action] { + self.0.as_ref() + } +} diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index a07a6785..4ba42515 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -17,7 +17,10 @@ use std::{ os::unix::io::{AsRawFd, FromRawFd}, }; -use zellij_tile::{data::InputMode, prelude::Style}; +use zellij_tile::{ + data::InputMode, + prelude::{ClientId, Style}, +}; type SessionId = u64; @@ -75,6 +78,7 @@ pub enum ClientToServerMsg { DetachSession(SessionId), // Disconnect from the session we're connected to DisconnectFromSession,*/ + DetachSession(Vec), TerminalPixelDimensions(PixelDimensions), BackgroundColor(String), ForegroundColor(String), @@ -87,10 +91,11 @@ pub enum ClientToServerMsg { Option, ), AttachClient(ClientAttributes, Options), - Action(Action), + Action(Action, Option), ClientExited, KillSession, ConnStatus, + ListClients, } // Types of messages sent from the server to the client @@ -105,6 +110,7 @@ pub enum ServerToClientMsg { Exit(ExitReason), SwitchToMode(InputMode), Connected, + ActiveClients(Vec), } #[derive(Serialize, Deserialize, Debug, Clone)]