zellij/zellij-client/src/fake_client.rs
a-kenji 0b6001305b
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
2022-06-15 11:20:06 +02:00

181 lines
6.5 KiB
Rust

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