feat: add capability to dispatch actions from cli (#1265)

* feat: add capability to dispatch actions from cli

Add capability to dispatch actions from the cli.

Can be invoked through `zellij action [actions]`

Automatically sends the action either to the current session,
or if there is only one session to the single session.

If there are multiple sessions, and no session is specified it will
error out.

Example:
1.
```
zellij action "[NewTab: , NewTab: ]"
```
2.
```
zellij -s fluffy-cat action '[NewPane: , WriteChars: "echo Purrr\n" ]'
```
3.
```
zellij -s fluffy-cat action '[ CloseTab, ]
```

* add: error message on malformed input

Add an error message on malformed input, for the `action`'s dispatch.
Rather than resulting in a panic.

* add: function to query the client id

* add: send specific actions to certain clients

Adds ability to send actions, that don't impact the server state
to all connected clients. For example `MoveFocus`

* add: client_id to non blocking actions

* chore(fmt): `cargo fmt`

* add: pick correct session, if there is exactly one

* add: use correct `client_id` for detach action

* add: make `[ ]` opaque to the user

* add: miette to toplevel to improve error message

* add: fake client reading configuration

Add the fake client reading configuration files,
this allows actions, that rely on configuration work
correctly. This is an intermediate solution, and should ideally not
be needed. It would be better if most of this state would be handled
by the server itself.

* chore(fmt): rustmt

* add: ability to detach multiple clients

Add ability to detach multiple clients at the same time.

* remove: obsolete functionality

* remove: unused functionality

* add: send correct action upon exiting

* chore(update): cargo update
This commit is contained in:
a-kenji 2022-06-15 11:20:06 +02:00 committed by GitHub
parent 253a140804
commit 0b6001305b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 455 additions and 55 deletions

1
.cargo/config.toml Normal file
View file

@ -0,0 +1 @@
parallel-compiler = true

11
Cargo.lock generated
View file

@ -355,16 +355,16 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.2.2" version = "3.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e538f9ee5aa3b3963f09a997035f883677966ed50fce0292611927ce6f6d8c6" checksum = "6d20de3739b4fb45a17837824f40aa1769cc7655d7a83e68739a77fe7b30c87a"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
"clap_derive", "clap_derive",
"clap_lex", "clap_lex",
"indexmap", "indexmap",
"lazy_static", "once_cell",
"strsim", "strsim",
"termcolor", "termcolor",
"textwrap 0.15.0", "textwrap 0.15.0",
@ -381,9 +381,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "3.2.2" version = "3.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f98063cac4652f23ccda556b8d04347a7fc4b2cff1f7577cc8c6546e0d8078" checksum = "026baf08b89ffbd332836002ec9378ef0e69648cbfadd68af7cd398ca5bf98f7"
dependencies = [ dependencies = [
"heck 0.4.0", "heck 0.4.0",
"proc-macro-error", "proc-macro-error",
@ -3248,6 +3248,7 @@ dependencies = [
"dialoguer", "dialoguer",
"insta", "insta",
"log", "log",
"miette",
"names", "names",
"rand 0.8.5", "rand 0.8.5",
"ssh2", "ssh2",

View file

@ -15,6 +15,7 @@ rust-version = "1.59"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
names = { version = "0.13.0", default-features = false } 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-client = { path = "zellij-client/", version = "0.31.0" }
zellij-server = { path = "zellij-server/", version = "0.31.0" } zellij-server = { path = "zellij-server/", version = "0.31.0" }
zellij-utils = { path = "zellij-utils/", version = "0.31.0" } zellij-utils = { path = "zellij-utils/", version = "0.31.0" }

View file

@ -6,12 +6,14 @@ use crate::sessions::{
session_exists, ActiveSession, SessionNameMatch, session_exists, ActiveSession, SessionNameMatch,
}; };
use dialoguer::Confirm; use dialoguer::Confirm;
use miette::{IntoDiagnostic, Result};
use std::path::PathBuf; use std::path::PathBuf;
use std::process; use std::process;
use zellij_client::start_client as start_client_impl; use zellij_client::start_client as start_client_impl;
use zellij_client::{os_input_output::get_client_os_input, ClientInfo}; use zellij_client::{os_input_output::get_client_os_input, ClientInfo};
use zellij_server::os_input_output::get_server_os_input; use zellij_server::os_input_output::get_server_os_input;
use zellij_server::start_server as start_server_impl; use zellij_server::start_server as start_server_impl;
use zellij_utils::input::actions::ActionsFromYaml;
use zellij_utils::input::options::Options; use zellij_utils::input::options::Options;
use zellij_utils::nix; use zellij_utils::nix;
use zellij_utils::{ 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 { fn attach_with_session_index(config_options: Options, index: usize, create: bool) -> ClientInfo {
// Ignore the session_name when `--index` is provided // Ignore the session_name when `--index` is provided
match get_sessions_sorted_by_mtime() { match get_sessions_sorted_by_mtime() {

View file

@ -16,6 +16,9 @@ fn main() {
if let Some(Command::Sessions(Sessions::ListSessions)) = opts.command { if let Some(Command::Sessions(Sessions::ListSessions)) = opts.command {
commands::list_sessions(); 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 { } else if let Some(Command::Sessions(Sessions::KillAllSessions { yes })) = opts.command {
commands::kill_all_sessions(yes); commands::kill_all_sessions(yes);
} else if let Some(Command::Sessions(Sessions::KillSession { ref target_session })) = } else if let Some(Command::Sessions(Sessions::KillSession { ref target_session })) =

View file

@ -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();
}

View file

@ -11,7 +11,7 @@ use zellij_utils::{
use crate::{ use crate::{
os_input_output::ClientOsApi, os_input_output::ClientOsApi,
stdin_ansi_parser::{AnsiStdinInstructionOrKeys, StdinAnsiParser}, stdin_ansi_parser::{AnsiStdinInstructionOrKeys, StdinAnsiParser},
ClientInstruction, CommandIsExecuting, InputInstruction, ClientId, ClientInstruction, CommandIsExecuting, InputInstruction,
}; };
use zellij_utils::{ use zellij_utils::{
channels::{Receiver, SenderWithContext, OPENCALLS}, channels::{Receiver, SenderWithContext, OPENCALLS},
@ -108,11 +108,18 @@ impl InputHandler {
}, },
InputEvent::Paste(pasted_text) => { InputEvent::Paste(pasted_text) => {
if self.mode == InputMode::Normal || self.mode == InputMode::Locked { if self.mode == InputMode::Normal || self.mode == InputMode::Locked {
self.dispatch_action(Action::Write(bracketed_paste_start.clone())); self.dispatch_action(
self.dispatch_action(Action::Write( Action::Write(bracketed_paste_start.clone()),
pasted_text.as_bytes().to_vec(), None,
)); );
self.dispatch_action(Action::Write(bracketed_paste_end.clone())); 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>) { fn handle_key(&mut self, key: &Key, raw_bytes: Vec<u8>) {
let keybinds = &self.config.keybinds; let keybinds = &self.config.keybinds;
for action in Keybinds::key_to_actions(key, raw_bytes, &self.mode, 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 { if should_exit {
self.should_exit = true; self.should_exit = true;
} }
@ -175,39 +182,80 @@ impl InputHandler {
match *mouse_event { match *mouse_event {
MouseEvent::Press(button, point) => match button { MouseEvent::Press(button, point) => match button {
MouseButton::WheelUp => { MouseButton::WheelUp => {
self.dispatch_action(Action::ScrollUpAt(point)); self.dispatch_action(Action::ScrollUpAt(point), None);
}, },
MouseButton::WheelDown => { MouseButton::WheelDown => {
self.dispatch_action(Action::ScrollDownAt(point)); self.dispatch_action(Action::ScrollDownAt(point), None);
}, },
MouseButton::Left => { MouseButton::Left => {
if self.holding_mouse { if self.holding_mouse {
self.dispatch_action(Action::MouseHold(point)); self.dispatch_action(Action::MouseHold(point), None);
} else { } else {
self.dispatch_action(Action::LeftClick(point)); self.dispatch_action(Action::LeftClick(point), None);
} }
self.holding_mouse = true; self.holding_mouse = true;
}, },
MouseButton::Right => { MouseButton::Right => {
if self.holding_mouse { if self.holding_mouse {
self.dispatch_action(Action::MouseHold(point)); self.dispatch_action(Action::MouseHold(point), None);
} else { } else {
self.dispatch_action(Action::RightClick(point)); self.dispatch_action(Action::RightClick(point), None);
} }
self.holding_mouse = true; self.holding_mouse = true;
}, },
_ => {}, _ => {},
}, },
MouseEvent::Release(point) => { MouseEvent::Release(point) => {
self.dispatch_action(Action::MouseRelease(point)); self.dispatch_action(Action::MouseRelease(point), None);
self.holding_mouse = false; self.holding_mouse = false;
}, },
MouseEvent::Hold(point) => { MouseEvent::Hold(point) => {
self.dispatch_action(Action::MouseHold(point)); self.dispatch_action(Action::MouseHold(point), None);
self.holding_mouse = true; 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`]. /// Dispatches an [`Action`].
/// ///
@ -220,14 +268,14 @@ impl InputHandler {
/// This is a temporary measure that is only necessary due to the way that the /// 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 /// 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). /// 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; let mut should_break = false;
match action { match action {
Action::NoOp => {}, Action::NoOp => {},
Action::Quit | Action::Detach => { Action::Quit | Action::Detach => {
self.os_input self.os_input
.send_to_server(ClientToServerMsg::Action(action)); .send_to_server(ClientToServerMsg::Action(action, client_id));
self.exit(); self.exit();
should_break = true; should_break = true;
}, },
@ -236,10 +284,11 @@ impl InputHandler {
// server later that atomically changes the mode as well // server later that atomically changes the mode as well
self.mode = mode; self.mode = mode;
self.os_input self.os_input
.send_to_server(ClientToServerMsg::Action(action)); .send_to_server(ClientToServerMsg::Action(action, None));
}, },
Action::CloseFocus Action::CloseFocus
| Action::NewPane(_) | Action::NewPane(_)
| Action::Run(_)
| Action::ToggleFloatingPanes | Action::ToggleFloatingPanes
| Action::TogglePaneEmbedOrFloating | Action::TogglePaneEmbedOrFloating
| Action::NewTab(_) | Action::NewTab(_)
@ -250,14 +299,15 @@ impl InputHandler {
| Action::ToggleTab | Action::ToggleTab
| Action::MoveFocusOrTab(_) => { | Action::MoveFocusOrTab(_) => {
self.command_is_executing.blocking_input_thread(); self.command_is_executing.blocking_input_thread();
log::error!("Blocking input thread.");
self.os_input self.os_input
.send_to_server(ClientToServerMsg::Action(action)); .send_to_server(ClientToServerMsg::Action(action, client_id));
self.command_is_executing self.command_is_executing
.wait_until_input_thread_is_unblocked(); .wait_until_input_thread_is_unblocked();
}, },
_ => self _ => self
.os_input .os_input
.send_to_server(ClientToServerMsg::Action(action)), .send_to_server(ClientToServerMsg::Action(action, client_id)),
} }
should_break should_break
@ -295,6 +345,33 @@ pub(crate) fn input_loop(
.handle_input(); .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)] #[cfg(test)]
#[path = "./unit/input_handler_tests.rs"] #[path = "./unit/input_handler_tests.rs"]
mod input_handler_tests; mod input_handler_tests;

View file

@ -1,7 +1,9 @@
pub mod os_input_output; pub mod os_input_output;
mod command_is_executing; mod command_is_executing;
pub mod fake_client;
mod input_handler; mod input_handler;
mod sessions;
mod stdin_ansi_parser; mod stdin_ansi_parser;
mod stdin_handler; mod stdin_handler;
@ -12,7 +14,7 @@ use std::io::{self, Write};
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::thread; use std::thread;
use zellij_tile::prelude::Style; use zellij_tile::prelude::{ClientId, Style};
use crate::{ use crate::{
command_is_executing::CommandIsExecuting, input_handler::input_loop, command_is_executing::CommandIsExecuting, input_handler::input_loop,
@ -39,6 +41,7 @@ pub(crate) enum ClientInstruction {
Exit(ExitReason), Exit(ExitReason),
SwitchToMode(InputMode), SwitchToMode(InputMode),
Connected, Connected,
ActiveClients(Vec<ClientId>),
} }
impl From<ServerToClientMsg> for ClientInstruction { impl From<ServerToClientMsg> for ClientInstruction {
@ -51,6 +54,7 @@ impl From<ServerToClientMsg> for ClientInstruction {
ClientInstruction::SwitchToMode(input_mode) ClientInstruction::SwitchToMode(input_mode)
}, },
ServerToClientMsg::Connected => ClientInstruction::Connected, ServerToClientMsg::Connected => ClientInstruction::Connected,
ServerToClientMsg::ActiveClients(clients) => ClientInstruction::ActiveClients(clients),
} }
} }
} }
@ -64,6 +68,7 @@ impl From<&ClientInstruction> for ClientContext {
ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread, ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread,
ClientInstruction::SwitchToMode(_) => ClientContext::SwitchToMode, ClientInstruction::SwitchToMode(_) => ClientContext::SwitchToMode,
ClientInstruction::Connected => ClientContext::Connected, ClientInstruction::Connected => ClientContext::Connected,
ClientInstruction::ActiveClients(_) => ClientContext::ActiveClients,
} }
} }
} }
@ -259,7 +264,10 @@ pub fn start_client(
Box::new({ Box::new({
let os_api = os_input.clone(); let os_api = os_input.clone();
move || { 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; break;
}, },
ClientInstruction::Error(backtrace) => { 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); handle_error(backtrace);
}, },
ClientInstruction::Render(output) => { ClientInstruction::Render(output) => {

View file

@ -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);
},
};
}

View file

@ -176,7 +176,7 @@ fn extract_actions_sent_to_server(
) -> Vec<Action> { ) -> Vec<Action> {
let events_sent_to_server = events_sent_to_server.lock().unwrap(); let events_sent_to_server = events_sent_to_server.lock().unwrap();
events_sent_to_server.iter().fold(vec![], |mut acc, event| { 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.push(action.clone());
} }
acc acc

View file

@ -72,9 +72,10 @@ pub enum ServerInstruction {
RemoveClient(ClientId), RemoveClient(ClientId),
Error(String), Error(String),
KillSession, KillSession,
DetachSession(ClientId), DetachSession(Vec<ClientId>),
AttachClient(ClientAttributes, Options, ClientId), AttachClient(ClientAttributes, Options, ClientId),
ConnStatus(ClientId), ConnStatus(ClientId),
ActiveClients(ClientId),
} }
impl From<&ServerInstruction> for ServerContext { impl From<&ServerInstruction> for ServerContext {
@ -90,6 +91,7 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::DetachSession(..) => ServerContext::DetachSession, ServerInstruction::DetachSession(..) => ServerContext::DetachSession,
ServerInstruction::AttachClient(..) => ServerContext::AttachClient, ServerInstruction::AttachClient(..) => ServerContext::AttachClient,
ServerInstruction::ConnStatus(..) => ServerContext::ConnStatus, ServerInstruction::ConnStatus(..) => ServerContext::ConnStatus,
ServerInstruction::ActiveClients(_) => ServerContext::ActiveClients,
} }
} }
} }
@ -469,10 +471,12 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
} }
break; break;
}, },
ServerInstruction::DetachSession(client_id) => { ServerInstruction::DetachSession(client_ids) => {
for client_id in client_ids {
os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal));
remove_client!(client_id, os_input, session_state); remove_client!(client_id, os_input, session_state);
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() { if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size()
{
session_data session_data
.write() .write()
.unwrap() .unwrap()
@ -498,6 +502,7 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.senders .senders
.send_to_plugin(PluginInstruction::RemoveClient(client_id)) .send_to_plugin(PluginInstruction::RemoveClient(client_id))
.unwrap(); .unwrap();
}
}, },
ServerInstruction::Render(serialized_output) => { ServerInstruction::Render(serialized_output) => {
let client_ids = session_state.read().unwrap().client_ids(); let client_ids = session_state.read().unwrap().client_ids();
@ -534,6 +539,15 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
os_input.send_to_client(client_id, ServerToClientMsg::Connected); os_input.send_to_client(client_id, ServerToClientMsg::Connected);
remove_client!(client_id, os_input, session_state); 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));
},
} }
} }

View file

@ -338,7 +338,7 @@ fn route_action(
}, },
Action::Detach => { Action::Detach => {
to_server to_server
.send(ServerInstruction::DetachSession(client_id)) .send(ServerInstruction::DetachSession(vec![client_id]))
.unwrap(); .unwrap();
should_break = true; should_break = true;
}, },
@ -415,7 +415,8 @@ pub(crate) fn route_thread_main(
let rlocked_sessions = session_data.read().unwrap(); let rlocked_sessions = session_data.read().unwrap();
match instruction { 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 Some(rlocked_sessions) = rlocked_sessions.as_ref() {
if let Action::SwitchToMode(input_mode) = action { if let Action::SwitchToMode(input_mode) = action {
os_input.send_to_client( os_input.send_to_client(
@ -516,6 +517,13 @@ pub(crate) fn route_thread_main(
let _ = to_server.send(ServerInstruction::ConnStatus(client_id)); let _ = to_server.send(ServerInstruction::ConnStatus(client_id));
break; 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 => { None => {

View file

@ -109,4 +109,6 @@ pub enum Sessions {
#[clap(short, long, value_parser)] #[clap(short, long, value_parser)]
yes: bool, yes: bool,
}, },
/// Send actions to a specific session
Action { action: Option<String> },
} }

View file

@ -327,6 +327,7 @@ pub enum ClientContext {
ServerError, ServerError,
SwitchToMode, SwitchToMode,
Connected, Connected,
ActiveClients,
} }
/// Stack call representations corresponding to the different types of [`ServerInstruction`]s. /// Stack call representations corresponding to the different types of [`ServerInstruction`]s.
@ -342,6 +343,7 @@ pub enum ServerContext {
DetachSession, DetachSession,
AttachClient, AttachClient,
ConnStatus, ConnStatus,
ActiveClients,
} }
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]

View file

@ -129,3 +129,13 @@ impl From<OnForceClose> for Action {
} }
} }
} }
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct ActionsFromYaml(Vec<Action>);
impl ActionsFromYaml {
/// Get a reference to the actions from yaml's actions.
pub fn actions(&self) -> &[Action] {
self.0.as_ref()
}
}

View file

@ -17,7 +17,10 @@ use std::{
os::unix::io::{AsRawFd, FromRawFd}, os::unix::io::{AsRawFd, FromRawFd},
}; };
use zellij_tile::{data::InputMode, prelude::Style}; use zellij_tile::{
data::InputMode,
prelude::{ClientId, Style},
};
type SessionId = u64; type SessionId = u64;
@ -75,6 +78,7 @@ pub enum ClientToServerMsg {
DetachSession(SessionId), DetachSession(SessionId),
// Disconnect from the session we're connected to // Disconnect from the session we're connected to
DisconnectFromSession,*/ DisconnectFromSession,*/
DetachSession(Vec<ClientId>),
TerminalPixelDimensions(PixelDimensions), TerminalPixelDimensions(PixelDimensions),
BackgroundColor(String), BackgroundColor(String),
ForegroundColor(String), ForegroundColor(String),
@ -87,10 +91,11 @@ pub enum ClientToServerMsg {
Option<PluginsConfig>, Option<PluginsConfig>,
), ),
AttachClient(ClientAttributes, Options), AttachClient(ClientAttributes, Options),
Action(Action), Action(Action, Option<ClientId>),
ClientExited, ClientExited,
KillSession, KillSession,
ConnStatus, ConnStatus,
ListClients,
} }
// Types of messages sent from the server to the client // Types of messages sent from the server to the client
@ -105,6 +110,7 @@ pub enum ServerToClientMsg {
Exit(ExitReason), Exit(ExitReason),
SwitchToMode(InputMode), SwitchToMode(InputMode),
Connected, Connected,
ActiveClients(Vec<ClientId>),
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]