* 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
736 lines
24 KiB
Rust
736 lines
24 KiB
Rust
use super::input_loop;
|
|
use zellij_utils::input::actions::{Action, Direction};
|
|
use zellij_utils::input::config::Config;
|
|
use zellij_utils::input::options::Options;
|
|
use zellij_utils::nix;
|
|
use zellij_utils::pane_size::{Size, SizeInPixels};
|
|
use zellij_utils::termwiz::input::{InputEvent, KeyCode, KeyEvent, Modifiers};
|
|
use zellij_utils::zellij_tile::data::Palette;
|
|
|
|
use crate::InputInstruction;
|
|
use crate::{
|
|
os_input_output::{ClientOsApi, StdinPoller},
|
|
ClientInstruction, CommandIsExecuting,
|
|
};
|
|
|
|
use std::path::Path;
|
|
|
|
use zellij_utils::zellij_tile;
|
|
|
|
use std::io;
|
|
use std::os::unix::io::RawFd;
|
|
use std::sync::{Arc, Mutex};
|
|
use zellij_tile::data::InputMode;
|
|
use zellij_utils::{
|
|
errors::ErrorContext,
|
|
ipc::{ClientToServerMsg, PixelDimensions, ServerToClientMsg},
|
|
};
|
|
|
|
use zellij_utils::channels::{self, ChannelWithContext, SenderWithContext};
|
|
|
|
#[allow(unused)]
|
|
pub mod commands {
|
|
pub const QUIT: [u8; 1] = [17]; // ctrl-q
|
|
pub const ESC: [u8; 1] = [27];
|
|
pub const ENTER: [u8; 1] = [10]; // char '\n'
|
|
|
|
pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = [27, 104]; // alt-h
|
|
pub const MOVE_FOCUS_RIGHT_IN_NORMAL_MODE: [u8; 2] = [27, 108]; // alt-l
|
|
|
|
pub const PANE_MODE: [u8; 1] = [16]; // ctrl-p
|
|
pub const SPAWN_TERMINAL_IN_PANE_MODE: [u8; 1] = [110]; // n
|
|
pub const MOVE_FOCUS_IN_PANE_MODE: [u8; 1] = [112]; // p
|
|
pub const SPLIT_DOWN_IN_PANE_MODE: [u8; 1] = [100]; // d
|
|
pub const SPLIT_RIGHT_IN_PANE_MODE: [u8; 1] = [114]; // r
|
|
pub const TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE: [u8; 1] = [102]; // f
|
|
pub const CLOSE_PANE_IN_PANE_MODE: [u8; 1] = [120]; // x
|
|
pub const MOVE_FOCUS_DOWN_IN_PANE_MODE: [u8; 1] = [106]; // j
|
|
pub const MOVE_FOCUS_UP_IN_PANE_MODE: [u8; 1] = [107]; // k
|
|
pub const MOVE_FOCUS_LEFT_IN_PANE_MODE: [u8; 1] = [104]; // h
|
|
pub const MOVE_FOCUS_RIGHT_IN_PANE_MODE: [u8; 1] = [108]; // l
|
|
|
|
pub const SCROLL_MODE: [u8; 1] = [19]; // ctrl-s
|
|
pub const SCROLL_UP_IN_SCROLL_MODE: [u8; 1] = [107]; // k
|
|
pub const SCROLL_DOWN_IN_SCROLL_MODE: [u8; 1] = [106]; // j
|
|
pub const SCROLL_PAGE_UP_IN_SCROLL_MODE: [u8; 1] = [2]; // ctrl-b
|
|
pub const SCROLL_PAGE_DOWN_IN_SCROLL_MODE: [u8; 1] = [6]; // ctrl-f
|
|
|
|
pub const RESIZE_MODE: [u8; 1] = [18]; // ctrl-r
|
|
pub const RESIZE_DOWN_IN_RESIZE_MODE: [u8; 1] = [106]; // j
|
|
pub const RESIZE_UP_IN_RESIZE_MODE: [u8; 1] = [107]; // k
|
|
pub const RESIZE_LEFT_IN_RESIZE_MODE: [u8; 1] = [104]; // h
|
|
pub const RESIZE_RIGHT_IN_RESIZE_MODE: [u8; 1] = [108]; // l
|
|
|
|
pub const TAB_MODE: [u8; 1] = [20]; // ctrl-t
|
|
pub const NEW_TAB_IN_TAB_MODE: [u8; 1] = [110]; // n
|
|
pub const SWITCH_NEXT_TAB_IN_TAB_MODE: [u8; 1] = [108]; // l
|
|
pub const SWITCH_PREV_TAB_IN_TAB_MODE: [u8; 1] = [104]; // h
|
|
pub const CLOSE_TAB_IN_TAB_MODE: [u8; 1] = [120]; // x
|
|
|
|
pub const BRACKETED_PASTE_START: [u8; 6] = [27, 91, 50, 48, 48, 126]; // \u{1b}[200~
|
|
pub const BRACKETED_PASTE_END: [u8; 6] = [27, 91, 50, 48, 49, 126]; // \u{1b}[201
|
|
pub const SLEEP: [u8; 0] = [];
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
struct FakeStdoutWriter {
|
|
buffer: Arc<Mutex<Vec<u8>>>,
|
|
}
|
|
impl FakeStdoutWriter {
|
|
pub fn new(buffer: Arc<Mutex<Vec<u8>>>) -> Self {
|
|
FakeStdoutWriter { buffer }
|
|
}
|
|
}
|
|
impl io::Write for FakeStdoutWriter {
|
|
fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
|
|
self.buffer.lock().unwrap().extend_from_slice(buf);
|
|
Ok(buf.len())
|
|
}
|
|
fn flush(&mut self) -> Result<(), io::Error> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct FakeClientOsApi {
|
|
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
|
|
command_is_executing: Arc<Mutex<CommandIsExecuting>>,
|
|
stdout_buffer: Arc<Mutex<Vec<u8>>>,
|
|
}
|
|
|
|
impl FakeClientOsApi {
|
|
pub fn new(
|
|
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
|
|
command_is_executing: CommandIsExecuting,
|
|
) -> Self {
|
|
// while command_is_executing itself is implemented with an Arc<Mutex>, we have to have an
|
|
// Arc<Mutex> here because we need interior mutability, otherwise we'll have to change the
|
|
// ClientOsApi trait, and that will cause a lot of havoc
|
|
let command_is_executing = Arc::new(Mutex::new(command_is_executing));
|
|
let stdout_buffer = Arc::new(Mutex::new(vec![]));
|
|
FakeClientOsApi {
|
|
events_sent_to_server,
|
|
command_is_executing,
|
|
stdout_buffer,
|
|
}
|
|
}
|
|
pub fn stdout_buffer(&self) -> Vec<u8> {
|
|
self.stdout_buffer.lock().unwrap().drain(..).collect()
|
|
}
|
|
}
|
|
|
|
impl ClientOsApi for FakeClientOsApi {
|
|
fn get_terminal_size_using_fd(&self, _fd: RawFd) -> Size {
|
|
unimplemented!()
|
|
}
|
|
fn set_raw_mode(&mut self, _fd: RawFd) {
|
|
unimplemented!()
|
|
}
|
|
fn unset_raw_mode(&self, _fd: RawFd) -> Result<(), nix::Error> {
|
|
unimplemented!()
|
|
}
|
|
fn get_stdout_writer(&self) -> Box<dyn io::Write> {
|
|
let fake_stdout_writer = FakeStdoutWriter::new(self.stdout_buffer.clone());
|
|
Box::new(fake_stdout_writer)
|
|
}
|
|
fn get_stdin_reader(&self) -> Box<dyn io::Read> {
|
|
unimplemented!()
|
|
}
|
|
fn read_from_stdin(&self) -> Vec<u8> {
|
|
unimplemented!()
|
|
}
|
|
fn box_clone(&self) -> Box<dyn ClientOsApi> {
|
|
unimplemented!()
|
|
}
|
|
fn send_to_server(&self, msg: ClientToServerMsg) {
|
|
{
|
|
let mut events_sent_to_server = self.events_sent_to_server.lock().unwrap();
|
|
events_sent_to_server.push(msg);
|
|
}
|
|
{
|
|
let mut command_is_executing = self.command_is_executing.lock().unwrap();
|
|
command_is_executing.unblock_input_thread();
|
|
}
|
|
}
|
|
fn recv_from_server(&self) -> Option<(ServerToClientMsg, ErrorContext)> {
|
|
unimplemented!()
|
|
}
|
|
fn handle_signals(&self, _sigwinch_cb: Box<dyn Fn()>, _quit_cb: Box<dyn Fn()>) {
|
|
unimplemented!()
|
|
}
|
|
fn connect_to_server(&self, _path: &Path) {
|
|
unimplemented!()
|
|
}
|
|
fn load_palette(&self) -> Palette {
|
|
unimplemented!()
|
|
}
|
|
fn enable_mouse(&self) {}
|
|
fn disable_mouse(&self) {}
|
|
fn stdin_poller(&self) -> StdinPoller {
|
|
unimplemented!()
|
|
}
|
|
}
|
|
|
|
fn extract_actions_sent_to_server(
|
|
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
|
|
) -> Vec<Action> {
|
|
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, None) = event {
|
|
acc.push(action.clone());
|
|
}
|
|
acc
|
|
})
|
|
}
|
|
|
|
fn extract_pixel_events_sent_to_server(
|
|
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
|
|
) -> Vec<PixelDimensions> {
|
|
let events_sent_to_server = events_sent_to_server.lock().unwrap();
|
|
events_sent_to_server.iter().fold(vec![], |mut acc, event| {
|
|
if let ClientToServerMsg::TerminalPixelDimensions(pixel_dimensions) = event {
|
|
acc.push(*pixel_dimensions);
|
|
}
|
|
acc
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
pub fn quit_breaks_input_loop() {
|
|
let stdin_events = vec![(
|
|
commands::QUIT.to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('q'),
|
|
modifiers: Modifiers::CTRL,
|
|
}),
|
|
)];
|
|
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
|
|
let command_is_executing = CommandIsExecuting::new();
|
|
let client_os_api = Box::new(FakeClientOsApi::new(
|
|
events_sent_to_server.clone(),
|
|
command_is_executing.clone(),
|
|
));
|
|
let config = Config::from_default_assets().unwrap();
|
|
let options = Options::default();
|
|
|
|
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);
|
|
for event in stdin_events {
|
|
send_input_instructions
|
|
.send(InputInstruction::KeyEvent(event.1, event.0))
|
|
.unwrap();
|
|
}
|
|
|
|
let default_mode = InputMode::Normal;
|
|
input_loop(
|
|
client_os_api,
|
|
config,
|
|
options,
|
|
command_is_executing,
|
|
send_client_instructions,
|
|
default_mode,
|
|
receive_input_instructions,
|
|
);
|
|
let expected_actions_sent_to_server = vec![Action::Quit];
|
|
let received_actions = extract_actions_sent_to_server(events_sent_to_server);
|
|
assert_eq!(
|
|
expected_actions_sent_to_server, received_actions,
|
|
"All actions sent to server properly"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn move_focus_left_in_normal_mode() {
|
|
let stdin_events = vec![
|
|
(
|
|
commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('h'),
|
|
modifiers: Modifiers::ALT,
|
|
}),
|
|
),
|
|
(
|
|
commands::QUIT.to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('q'),
|
|
modifiers: Modifiers::CTRL,
|
|
}),
|
|
),
|
|
];
|
|
|
|
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
|
|
let command_is_executing = CommandIsExecuting::new();
|
|
let client_os_api = Box::new(FakeClientOsApi::new(
|
|
events_sent_to_server.clone(),
|
|
command_is_executing.clone(),
|
|
));
|
|
let config = Config::from_default_assets().unwrap();
|
|
let options = Options::default();
|
|
|
|
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);
|
|
for event in stdin_events {
|
|
send_input_instructions
|
|
.send(InputInstruction::KeyEvent(event.1, event.0))
|
|
.unwrap();
|
|
}
|
|
|
|
let default_mode = InputMode::Normal;
|
|
input_loop(
|
|
client_os_api,
|
|
config,
|
|
options,
|
|
command_is_executing,
|
|
send_client_instructions,
|
|
default_mode,
|
|
receive_input_instructions,
|
|
);
|
|
let expected_actions_sent_to_server =
|
|
vec![Action::MoveFocusOrTab(Direction::Left), Action::Quit];
|
|
let received_actions = extract_actions_sent_to_server(events_sent_to_server);
|
|
assert_eq!(
|
|
expected_actions_sent_to_server, received_actions,
|
|
"All actions sent to server properly"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn pixel_info_queried_from_terminal_emulator() {
|
|
let stdin_events = vec![(
|
|
commands::QUIT.to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('q'),
|
|
modifiers: Modifiers::CTRL,
|
|
}),
|
|
)];
|
|
|
|
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
|
|
let command_is_executing = CommandIsExecuting::new();
|
|
let client_os_api = FakeClientOsApi::new(events_sent_to_server, command_is_executing.clone());
|
|
let config = Config::from_default_assets().unwrap();
|
|
let options = Options::default();
|
|
|
|
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);
|
|
for event in stdin_events {
|
|
send_input_instructions
|
|
.send(InputInstruction::KeyEvent(event.1, event.0))
|
|
.unwrap();
|
|
}
|
|
|
|
let default_mode = InputMode::Normal;
|
|
let client_os_api_clone = client_os_api.clone();
|
|
input_loop(
|
|
Box::new(client_os_api),
|
|
config,
|
|
options,
|
|
command_is_executing,
|
|
send_client_instructions,
|
|
default_mode,
|
|
receive_input_instructions,
|
|
);
|
|
let extracted_stdout_buffer = client_os_api_clone.stdout_buffer();
|
|
assert_eq!(
|
|
String::from_utf8(extracted_stdout_buffer),
|
|
Ok(String::from(
|
|
"\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\\\u{1b}]10;?\u{1b}\\"
|
|
)),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn pixel_info_sent_to_server() {
|
|
let stdin_events = vec![
|
|
(
|
|
vec![27],
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Escape,
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"[".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('['),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"6".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('6'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
";".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char(';'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"1".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('1'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"0".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('0'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
";".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char(';'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"5".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('5'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"t".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('t'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
commands::QUIT.to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('q'),
|
|
modifiers: Modifiers::CTRL,
|
|
}),
|
|
),
|
|
];
|
|
|
|
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
|
|
let command_is_executing = CommandIsExecuting::new();
|
|
let client_os_api =
|
|
FakeClientOsApi::new(events_sent_to_server.clone(), command_is_executing.clone());
|
|
let config = Config::from_default_assets().unwrap();
|
|
let options = Options::default();
|
|
|
|
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);
|
|
for event in stdin_events {
|
|
send_input_instructions
|
|
.send(InputInstruction::KeyEvent(event.1, event.0))
|
|
.unwrap();
|
|
}
|
|
|
|
let default_mode = InputMode::Normal;
|
|
input_loop(
|
|
Box::new(client_os_api),
|
|
config,
|
|
options,
|
|
command_is_executing,
|
|
send_client_instructions,
|
|
default_mode,
|
|
receive_input_instructions,
|
|
);
|
|
let actions_sent_to_server = extract_actions_sent_to_server(events_sent_to_server.clone());
|
|
let pixel_events_sent_to_server = extract_pixel_events_sent_to_server(events_sent_to_server);
|
|
assert_eq!(actions_sent_to_server, vec![Action::Quit]);
|
|
assert_eq!(
|
|
pixel_events_sent_to_server,
|
|
vec![PixelDimensions {
|
|
character_cell_size: Some(SizeInPixels {
|
|
height: 10,
|
|
width: 5
|
|
}),
|
|
text_area_size: None
|
|
}],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
pub fn corrupted_pixel_info_sent_as_key_events() {
|
|
let stdin_events = vec![
|
|
(
|
|
vec![27],
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Escape,
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"[".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('['),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"f".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('f'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
";".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char(';'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"1".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('1'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"0".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('0'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
";".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char(';'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"5".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('5'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"t".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('t'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
commands::QUIT.to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('q'),
|
|
modifiers: Modifiers::CTRL,
|
|
}),
|
|
),
|
|
];
|
|
|
|
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
|
|
let command_is_executing = CommandIsExecuting::new();
|
|
let client_os_api =
|
|
FakeClientOsApi::new(events_sent_to_server.clone(), command_is_executing.clone());
|
|
let config = Config::from_default_assets().unwrap();
|
|
let options = Options::default();
|
|
|
|
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);
|
|
for event in stdin_events {
|
|
send_input_instructions
|
|
.send(InputInstruction::KeyEvent(event.1, event.0))
|
|
.unwrap();
|
|
}
|
|
|
|
let default_mode = InputMode::Normal;
|
|
input_loop(
|
|
Box::new(client_os_api),
|
|
config,
|
|
options,
|
|
command_is_executing,
|
|
send_client_instructions,
|
|
default_mode,
|
|
receive_input_instructions,
|
|
);
|
|
let actions_sent_to_server = extract_actions_sent_to_server(events_sent_to_server.clone());
|
|
let pixel_events_sent_to_server = extract_pixel_events_sent_to_server(events_sent_to_server);
|
|
assert_eq!(
|
|
actions_sent_to_server,
|
|
vec![
|
|
Action::Write(vec![27]),
|
|
Action::Write(vec![b'[']),
|
|
Action::Write(vec![b'f']),
|
|
Action::Write(vec![b';']),
|
|
Action::Write(vec![b'1']),
|
|
Action::Write(vec![b'0']),
|
|
Action::Write(vec![b';']),
|
|
Action::Write(vec![b'5']),
|
|
Action::Write(vec![b't']),
|
|
Action::Quit
|
|
]
|
|
);
|
|
assert_eq!(pixel_events_sent_to_server, vec![],);
|
|
}
|
|
|
|
#[test]
|
|
pub fn esc_in_the_middle_of_pixelinfo_breaks_out_of_it() {
|
|
let stdin_events = vec![
|
|
(
|
|
vec![27],
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Escape,
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"[".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('['),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
vec![27],
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Escape,
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
";".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char(';'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"1".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('1'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"0".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('0'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
";".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char(';'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"5".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('5'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
"t".as_bytes().to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('t'),
|
|
modifiers: Modifiers::NONE,
|
|
}),
|
|
),
|
|
(
|
|
commands::QUIT.to_vec(),
|
|
InputEvent::Key(KeyEvent {
|
|
key: KeyCode::Char('q'),
|
|
modifiers: Modifiers::CTRL,
|
|
}),
|
|
),
|
|
];
|
|
|
|
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
|
|
let command_is_executing = CommandIsExecuting::new();
|
|
let client_os_api =
|
|
FakeClientOsApi::new(events_sent_to_server.clone(), command_is_executing.clone());
|
|
let config = Config::from_default_assets().unwrap();
|
|
let options = Options::default();
|
|
|
|
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);
|
|
for event in stdin_events {
|
|
send_input_instructions
|
|
.send(InputInstruction::KeyEvent(event.1, event.0))
|
|
.unwrap();
|
|
}
|
|
|
|
let default_mode = InputMode::Normal;
|
|
input_loop(
|
|
Box::new(client_os_api),
|
|
config,
|
|
options,
|
|
command_is_executing,
|
|
send_client_instructions,
|
|
default_mode,
|
|
receive_input_instructions,
|
|
);
|
|
let actions_sent_to_server = extract_actions_sent_to_server(events_sent_to_server.clone());
|
|
let pixel_events_sent_to_server = extract_pixel_events_sent_to_server(events_sent_to_server);
|
|
assert_eq!(
|
|
actions_sent_to_server,
|
|
vec![
|
|
Action::Write(vec![27]),
|
|
Action::Write(vec![b'[']),
|
|
Action::Write(vec![27]),
|
|
Action::Write(vec![b';']),
|
|
Action::Write(vec![b'1']),
|
|
Action::Write(vec![b'0']),
|
|
Action::Write(vec![b';']),
|
|
Action::Write(vec![b'5']),
|
|
Action::Write(vec![b't']),
|
|
Action::Quit
|
|
]
|
|
);
|
|
assert_eq!(pixel_events_sent_to_server, vec![],);
|
|
}
|