summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.cargo/config.toml1
-rw-r--r--Cargo.lock11
-rw-r--r--Cargo.toml1
-rw-r--r--src/commands.rs68
-rw-r--r--src/main.rs3
-rw-r--r--zellij-client/src/fake_client.rs181
-rw-r--r--zellij-client/src/input_handler.rs117
-rw-r--r--zellij-client/src/lib.rs14
-rw-r--r--zellij-client/src/sessions.rs18
-rw-r--r--zellij-client/src/unit/input_handler_tests.rs2
-rw-r--r--zellij-server/src/lib.rs58
-rw-r--r--zellij-server/src/route.rs12
-rw-r--r--zellij-utils/src/cli.rs2
-rw-r--r--zellij-utils/src/errors.rs2
-rw-r--r--zellij-utils/src/input/actions.rs10
-rw-r--r--zellij-utils/src/ipc.rs10
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