diff options
| author | Aram Drevekenin <aram@poor.dev> | 2022-07-08 17:19:42 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-07-08 17:19:42 +0200 |
| commit | c89b416d764d80a72130821506f36157a08321e9 (patch) | |
| tree | 6d1cc2d7fd5fddbb33fcbff192153700a9788bec /zellij-client | |
| parent | fix: typo (#1567) (diff) | |
| download | sl0ppij-c89b416d764d80a72130821506f36157a08321e9.tar.gz sl0ppij-c89b416d764d80a72130821506f36157a08321e9.tar.xz sl0ppij-c89b416d764d80a72130821506f36157a08321e9.zip | |
feat(terminal): sixel support (#1557)
* work
* work
* work
* work
* work
* more work
* work
* work
* work
* hack around stdin repeater
* refactor(sixel): rename sixel structs
* feat(sixel): render text above images
* fix(sixel): reap images once they're past the end of the scrollbuffer
* fix(sixel): display images in the middle of the line
* fix(sixel): render crash
* fix(sixel): react to SIGWINCH
* fix(sixel): behave properly in alternate screen mode
* fix(sixel): reap images on terminal reset
* feat(sixel): handle DECSDM
* fix(terminal): properly respond to XTSMGRAPHICS and device attributes with Sixel
* Add comment
* fix(sixel): hack for unknown event overflow until we fix the api
* feat(input): query terminal for all OSC 4 colors and respond to them in a buggy way
* fix(sixel): do not render corrupted image
* feat(input): improve STDIN queries
* fix(client): mistake in clear terminal attributes string
* fix(ansi): report correct number of supported color registers
* fix(sixel): reap images that are completely covered
* style(comment): fix name
* test(sixel): infra
* test(sixel): cases and fixes
* fix(sixel): forward dcs bytes to sixel parser
* refactor(client): ansi stdin parser
* refactor(output): cleanup
* some refactorings
* fix test
* refactor(grid): sixel-grid / sixel-image-store
* refactor(grid): grid debug method
* refactor(grid): move various logic to sixel.rs
* refactor(grid): remove unused methods
* fix(sixel): work with multiple users
* refactor(pane): remove unused z_index
* style(fmt): prepend unused variable
* style(fmt): rustfmt
* fix(tests): various apis
* chore(dependencies): use published version of sixel crates
* style(fmt): rustfmt
* style(fmt): rustfmt
* style(lint): make clippy happy
* style(lint): make clippy happy... again
* style(lint): make clippy happy... again (chapter 2)
* style(comment): remove unused
* fix(colors): export COLORTERM and respond to XTVERSION
* fix(test): color register count
* fix(stdin): adjust STDIN sleep times
Diffstat (limited to 'zellij-client')
| -rw-r--r-- | zellij-client/src/fake_client.rs | 8 | ||||
| -rw-r--r-- | zellij-client/src/input_handler.rs | 66 | ||||
| -rw-r--r-- | zellij-client/src/lib.rs | 29 | ||||
| -rw-r--r-- | zellij-client/src/os_input_output.rs | 4 | ||||
| -rw-r--r-- | zellij-client/src/stdin_ansi_parser.rs | 338 | ||||
| -rw-r--r-- | zellij-client/src/stdin_handler.rs | 31 | ||||
| -rw-r--r-- | zellij-client/src/unit/input_handler_tests.rs | 733 | ||||
| -rw-r--r-- | zellij-client/src/unit/snapshots/zellij_client__input_handler__input_handler_tests__pixel_info_sent_to_server.snap | 6 | ||||
| -rw-r--r-- | zellij-client/src/unit/snapshots/zellij_client__stdin_tests__pixel_info_sent_to_server.snap | 6 | ||||
| -rw-r--r-- | zellij-client/src/unit/stdin_tests.rs | 409 |
10 files changed, 685 insertions, 945 deletions
diff --git a/zellij-client/src/fake_client.rs b/zellij-client/src/fake_client.rs index 7338d633..88b41c9e 100644 --- a/zellij-client/src/fake_client.rs +++ b/zellij-client/src/fake_client.rs @@ -2,12 +2,13 @@ //! and dispatch actions, that are specified through the command line. //! Multiple actions at the same time can be dispatched. use log::debug; +use std::sync::{Arc, Mutex}; use std::{fs, path::PathBuf, thread}; use crate::{ command_is_executing::CommandIsExecuting, input_handler::input_actions, - os_input_output::ClientOsApi, stdin_handler::stdin_loop, ClientInfo, ClientInstruction, - InputInstruction, + os_input_output::ClientOsApi, stdin_ansi_parser::StdinAnsiParser, stdin_handler::stdin_loop, + ClientInfo, ClientInstruction, InputInstruction, }; use zellij_utils::{ channels::{self, ChannelWithContext, SenderWithContext}, @@ -82,12 +83,13 @@ pub fn start_fake_client( }) }); + let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new())); 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) + move || stdin_loop(os_input, send_input_instructions, stdin_ansi_parser) }); let clients: Vec<ClientId>; diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 3f92c537..5f8cc063 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -1,8 +1,7 @@ //! Main input logic. use crate::{ - os_input_output::ClientOsApi, - stdin_ansi_parser::{AnsiStdinInstructionOrKeys, StdinAnsiParser}, - ClientId, ClientInstruction, CommandIsExecuting, InputInstruction, + os_input_output::ClientOsApi, stdin_ansi_parser::AnsiStdinInstruction, ClientId, + ClientInstruction, CommandIsExecuting, InputInstruction, }; use zellij_utils::{ channels::{Receiver, SenderWithContext, OPENCALLS}, @@ -69,19 +68,6 @@ impl InputHandler { if self.options.mouse_mode.unwrap_or(true) { self.os_input.enable_mouse(); } - // <ESC>[14t => get text area size in pixels, - // <ESC>[16t => get character cell size in pixels - // <ESC>]11;?<ESC>\ => get background color - // <ESC>]10;?<ESC>\ => get foreground color - let get_cell_pixel_info = - "\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}"; - let _ = self - .os_input - .get_stdout_writer() - .write(get_cell_pixel_info.as_bytes()) - .unwrap(); - let mut ansi_stdin_parser = StdinAnsiParser::new(); - ansi_stdin_parser.increment_expected_ansi_instructions(4); loop { if self.should_exit { break; @@ -91,13 +77,7 @@ impl InputHandler { match input_event { InputEvent::Key(key_event) => { let key = cast_termwiz_key(key_event, &raw_bytes); - if ansi_stdin_parser.expected_instructions() > 0 { - self.handle_possible_pixel_instruction( - ansi_stdin_parser.parse(key, raw_bytes), - ); - } else { - self.handle_key(&key, raw_bytes); - } + self.handle_key(&key, raw_bytes); }, InputEvent::Mouse(mouse_event) => { let mouse_event = @@ -126,13 +106,13 @@ impl InputHandler { Ok((InputInstruction::SwitchToMode(input_mode), _error_context)) => { self.mode = input_mode; }, - Ok((InputInstruction::PossiblePixelRatioChange, _error_context)) => { - let _ = self - .os_input - .get_stdout_writer() - .write(get_cell_pixel_info.as_bytes()) - .unwrap(); - ansi_stdin_parser.increment_expected_ansi_instructions(4); + Ok(( + InputInstruction::AnsiStdinInstructions(ansi_stdin_instructions), + _error_context, + )) => { + for ansi_instruction in ansi_stdin_instructions { + self.handle_stdin_ansi_instruction(ansi_instruction); + } }, Err(err) => panic!("Encountered read error: {:?}", err), } @@ -147,33 +127,28 @@ impl InputHandler { } } } - fn handle_possible_pixel_instruction( - &mut self, - pixel_instruction_or_keys: Option<AnsiStdinInstructionOrKeys>, - ) { - match pixel_instruction_or_keys { - Some(AnsiStdinInstructionOrKeys::PixelDimensions(pixel_dimensions)) => { + fn handle_stdin_ansi_instruction(&mut self, ansi_stdin_instructions: AnsiStdinInstruction) { + match ansi_stdin_instructions { + AnsiStdinInstruction::PixelDimensions(pixel_dimensions) => { self.os_input .send_to_server(ClientToServerMsg::TerminalPixelDimensions(pixel_dimensions)); }, - Some(AnsiStdinInstructionOrKeys::BackgroundColor(background_color_instruction)) => { + AnsiStdinInstruction::BackgroundColor(background_color_instruction) => { self.os_input .send_to_server(ClientToServerMsg::BackgroundColor( background_color_instruction, )); }, - Some(AnsiStdinInstructionOrKeys::ForegroundColor(foreground_color_instruction)) => { + AnsiStdinInstruction::ForegroundColor(foreground_color_instruction) => { self.os_input .send_to_server(ClientToServerMsg::ForegroundColor( foreground_color_instruction, )); }, - Some(AnsiStdinInstructionOrKeys::Keys(keys)) => { - for (key, raw_bytes) in keys { - self.handle_key(&key, raw_bytes); - } + AnsiStdinInstruction::ColorRegisters(color_registers) => { + self.os_input + .send_to_server(ClientToServerMsg::ColorRegisters(color_registers)); }, - None => {}, } } fn handle_mouse_event(&mut self, mouse_event: &MouseEvent) { @@ -352,7 +327,6 @@ 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)] @@ -379,7 +353,3 @@ pub(crate) fn input_actions( ) .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 9790984e..3b81694b 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -13,8 +13,10 @@ use std::env::current_exe; use std::io::{self, Write}; use std::path::Path; use std::process::Command; +use std::sync::{Arc, Mutex}; use std::thread; +use crate::stdin_ansi_parser::{AnsiStdinInstruction, StdinAnsiParser}; use crate::{ command_is_executing::CommandIsExecuting, input_handler::input_loop, os_input_output::ClientOsApi, stdin_handler::stdin_loop, @@ -114,7 +116,7 @@ impl ClientInfo { pub(crate) enum InputInstruction { KeyEvent(InputEvent, Vec<u8>), SwitchToMode(InputMode), - PossiblePixelRatioChange, + AnsiStdinInstructions(Vec<AnsiStdinInstruction>), } pub fn start_client( @@ -126,7 +128,7 @@ pub fn start_client( layout: Option<LayoutFromYaml>, ) { info!("Starting Zellij client!"); - let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}12l\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l"; + let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l"; let take_snapshot = "\u{1b}[?1049h"; let bracketed_paste = "\u{1b}[?2004h"; os_input.unset_raw_mode(0).unwrap(); @@ -162,11 +164,13 @@ pub fn start_client( let first_msg = match info { ClientInfo::Attach(name, config_options) => { envs::set_session_name(name); + envs::set_initial_environment_vars(); ClientToServerMsg::AttachClient(client_attributes, config_options) }, ClientInfo::New(name) => { envs::set_session_name(name); + envs::set_initial_environment_vars(); spawn_server(&*ZELLIJ_IPC_PIPE).unwrap(); @@ -214,13 +218,15 @@ pub fn start_client( }); let on_force_close = config_options.on_force_close.unwrap_or_default(); + let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new())); 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 stdin_ansi_parser = stdin_ansi_parser.clone(); + move || stdin_loop(os_input, send_input_instructions, stdin_ansi_parser) }); let _input_thread = thread::Builder::new() @@ -246,7 +252,6 @@ pub fn start_client( let _signal_thread = thread::Builder::new() .name("signal_listener".to_string()) .spawn({ - let send_input_instructions = send_input_instructions.clone(); let os_input = os_input.clone(); move || { os_input.handle_signals( @@ -256,8 +261,16 @@ pub fn start_client( os_api.send_to_server(ClientToServerMsg::TerminalResize( os_api.get_terminal_size_using_fd(0), )); - let _ = send_input_instructions - .send(InputInstruction::PossiblePixelRatioChange); + // send a query to the terminal emulator in case the font size changed + // as well - we'll parse the response through STDIN + let terminal_emulator_query_string = stdin_ansi_parser + .lock() + .unwrap() + .window_size_change_query_string(); + let _ = os_api + .get_stdout_writer() + .write(terminal_emulator_query_string.as_bytes()) + .unwrap(); } }), Box::new({ @@ -385,3 +398,7 @@ pub fn start_client( let _ = stdout.write(goodbye_message.as_bytes()).unwrap(); stdout.flush().unwrap(); } + +#[cfg(test)] +#[path = "./unit/stdin_tests.rs"] +mod stdin_tests; diff --git a/zellij-client/src/os_input_output.rs b/zellij-client/src/os_input_output.rs index 60c22cc9..f9f3c590 100644 --- a/zellij-client/src/os_input_output.rs +++ b/zellij-client/src/os_input_output.rs @@ -94,7 +94,7 @@ pub trait ClientOsApi: Send + Sync { fn get_stdout_writer(&self) -> Box<dyn io::Write>; fn get_stdin_reader(&self) -> Box<dyn io::Read>; /// Returns the raw contents of standard input. - fn read_from_stdin(&self) -> Vec<u8>; + fn read_from_stdin(&mut self) -> Vec<u8>; /// Returns a [`Box`] pointer to this [`ClientOsApi`] struct. fn box_clone(&self) -> Box<dyn ClientOsApi>; /// Sends a message to the server. @@ -126,7 +126,7 @@ impl ClientOsApi for ClientOsInputOutput { fn box_clone(&self) -> Box<dyn ClientOsApi> { Box::new((*self).clone()) } - fn read_from_stdin(&self) -> Vec<u8> { + fn read_from_stdin(&mut self) -> Vec<u8> { let stdin = std::io::stdin(); let mut stdin = stdin.lock(); let buffer = stdin.fill_buf().unwrap(); diff --git a/zellij-client/src/stdin_ansi_parser.rs b/zellij-client/src/stdin_ansi_parser.rs index 25f0e4fb..09247bf9 100644 --- a/zellij-client/src/stdin_ansi_parser.rs +++ b/zellij-client/src/stdin_ansi_parser.rs @@ -1,200 +1,234 @@ +use std::time::{Duration, Instant}; + +const STARTUP_PARSE_DEADLINE_MS: u64 = 500; +const SIGWINCH_PARSE_DEADLINE_MS: u64 = 200; use zellij_utils::{ - data::{CharOrArrow, Key}, - ipc::PixelDimensions, - lazy_static::lazy_static, - pane_size::SizeInPixels, - regex::Regex, + ipc::PixelDimensions, lazy_static::lazy_static, pane_size::SizeInPixels, regex::Regex, }; +#[derive(Debug)] pub struct StdinAnsiParser { - expected_ansi_instructions: usize, - current_buffer: Vec<(Key, Vec<u8>)>, + raw_buffer: Vec<u8>, + pending_color_sequences: Vec<(usize, String)>, + pending_events: Vec<AnsiStdinInstruction>, + parse_deadline: Option<Instant>, } impl StdinAnsiParser { pub fn new() -> Self { StdinAnsiParser { - expected_ansi_instructions: 0, - current_buffer: vec![], + raw_buffer: vec![], + pending_color_sequences: vec![], + pending_events: vec![], + parse_deadline: None, } } - pub fn increment_expected_ansi_instructions(&mut self, to: usize) { - self.expected_ansi_instructions += to; - } - pub fn decrement_expected_ansi_instructions(&mut self, by: usize) { - self.expected_ansi_instructions = self.expected_ansi_instructions.saturating_sub(by); + pub fn terminal_emulator_query_string(&mut self) -> String { + // note that this assumes the String will be sent to the terminal emulator and so starts a + // deadline timeout (self.parse_deadline) + + // <ESC>[14t => get text area size in pixels, + // <ESC>[16t => get character cell size in pixels + // <ESC>]11;?<ESC>\ => get background color + // <ESC>]10;?<ESC>\ => get foreground color + let mut query_string = + String::from("\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}"); + + // query colors + // eg. <ESC>]4;5;?<ESC>\ => query color register number 5 + for i in 0..256 { + query_string.push_str(&format!("\u{1b}]4;{};?\u{1b}\u{5c}", i)); + } + self.parse_deadline = + Some(Instant::now() + Duration::from_millis(STARTUP_PARSE_DEADLINE_MS)); + query_string } - pub fn expected_instructions(&self) -> usize { - self.expected_ansi_instructions + pub fn window_size_change_query_string(&mut self) -> String { + // note that this assumes the String will be sent to the terminal emulator and so starts a + // deadline timeout (self.parse_deadline) + + // <ESC>[14t => get text area size in pixels, + // <ESC>[16t => get character cell size in pixels + let query_string = String::from("\u{1b}[14t\u{1b}[16t"); + + self.parse_deadline = + Some(Instant::now() + Duration::from_millis(SIGWINCH_PARSE_DEADLINE_MS)); + query_string } - pub fn parse(&mut self, key: Key, raw_bytes: Vec<u8>) -> Option<AnsiStdinInstructionOrKeys> { - if self.current_buffer.is_empty() - && (key != Key::Esc && key != Key::Alt(CharOrArrow::Char(']'))) + fn drain_pending_events(&mut self) -> Vec<AnsiStdinInstruction> { + let mut events = vec![]; + events.append(&mut self.pending_events); + if let Some(color_registers) = + AnsiStdinInstruction::color_registers_from_bytes(&mut self.pending_color_sequences) { - // the first key of a sequence is always Esc, but termwiz interprets esc + ] as Alt+] - self.current_buffer.push((key, raw_bytes)); - self.expected_ansi_instructions = 0; - return Some(AnsiStdinInstructionOrKeys::Keys( - self.current_buffer.drain(..).collect(), - )); + events.push(color_registers); } - if let Key::Char('t') = key { - self.current_buffer.push((key, raw_bytes)); - match AnsiStdinInstructionOrKeys::pixel_dimensions_from_keys(&self.current_buffer) { - Ok(pixel_instruction) => { - self.decrement_expected_ansi_instructions(1); - self.current_buffer.clear(); - Some(pixel_instruction) - }, - Err(_) => { - self.expected_ansi_instructions = 0; - Some(AnsiStdinInstructionOrKeys::Keys( - self.current_buffer.drain(..).collect(), - )) - }, + events + } + pub fn should_parse(&self) -> bool { + if let Some(parse_deadline) = self.parse_deadline { + if parse_deadline >= Instant::now() { + return true; } - } else if let Key::Alt(CharOrArrow::Char('\\')) | Key::Ctrl('g') = key { - match AnsiStdinInstructionOrKeys::color_sequence_from_keys(&self.current_buffer) { - Ok(color_instruction) => { - self.decrement_expected_ansi_instructions(1); - self.current_buffer.clear(); - Some(color_instruction) + } + false + } + pub fn parse(&mut self, mut raw_bytes: Vec<u8>) -> Vec<AnsiStdinInstruction> { + for byte in raw_bytes.drain(..) { + self.parse_byte(byte); + } + self.drain_pending_events() + } + fn parse_byte(&mut self, byte: u8) { + if byte == b't' { + self.raw_buffer.push(byte); + match AnsiStdinInstruction::pixel_dimensions_from_bytes(&self.raw_buffer) { + Ok(ansi_sequence) => { + self.pending_events.push(ansi_sequence); + self.raw_buffer.clear(); }, Err(_) => { - self.expected_ansi_instructions = 0; - Some(AnsiStdinInstructionOrKeys::Keys( - self.current_buffer.drain(..).collect(), - )) + self.raw_buffer.clear(); }, } - } else if self.key_is_valid(key) { - self.current_buffer.push((key, raw_bytes)); - None + } else if byte == b'\\' { + self.raw_buffer.push(byte); + if let Ok(ansi_sequence) = AnsiStdinInstruction::bg_or_fg_from_bytes(&self.raw_buffer) { + self.pending_events.push(ansi_sequence); + self.raw_buffer.clear(); + } else if let Ok((color_register, color_sequence)) = + color_sequence_from_bytes(&self.raw_buffer) + { + self.raw_buffer.clear(); + self.pending_color_sequences + .push((color_register, color_sequence)); + } else { + self.raw_buffer.clear(); + } } else { - self.current_buffer.push((key, raw_bytes)); - self.expected_ansi_instructions = 0; - Some(AnsiStdinInstructionOrKeys::Keys( - self.current_buffer.drain(..).collect(), - )) - } - } - fn key_is_valid(&self, key: Key) -> bool { - match key { - Key::Esc => { - // this is a UX improvement - // in case the user's terminal doesn't support one or more of these signals, - // if they spam ESC they need to be able to get back to normal mode and not "us - // waiting for ansi instructions" mode - !self.current_buffer.iter().any(|(key, _)| *key == Key::Esc) - }, - Key::Char(';') - | Key::Char('[') - | Key::Char(']') - | Key::Char('r') - | Key::Char('g') - | Key::Char('b') - | Key::Char('\\') - | Key::Char(':') - | Key::Char('/') => true, - Key::Alt(CharOrArrow::Char(']')) => true, - Key::Alt(CharOrArrow::Char('\\')) => true, - Key::Char(c) => { - matches!(c, '0'..='9' | 'a'..='f') - }, - _ => false, + self.raw_buffer.push(byte); } } } -#[derive(Debug)] -pub enum AnsiStdinInstructionOrKeys { +#[derive(Debug, Clone)] +pub enum AnsiStdinInstruction { PixelDimensions(PixelDimensions), BackgroundColor(String), ForegroundColor(String), - Keys(Vec<(Key, Vec<u8>)>), + ColorRegisters(Vec<(usize, String)>), } -impl AnsiStdinInstructionOrKeys { - pub fn pixel_dimensions_from_keys(keys: &[(Key, Vec<u8>)]) -> Result<Self, &'static str> { +impl AnsiStdinInstruction { + pub fn pixel_dimensions_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> { + // eg. <ESC>[4;21;8t lazy_static! { static ref RE: Regex = Regex::new(r"^\u{1b}\[(\d+);(\d+);(\d+)t$").unwrap(); } - let key_sequence: Vec<Option<char>> = keys - .iter() - .map(|(key, _)| match key { - Key::Char(c) => Some(*c), - Key::Esc => Some('\u{1b}'), - _ => None, - }) - .collect(); - if key_sequence.iter().all(|k| k.is_some()) { - let key_string: String = key_sequence.iter().map(|k| k.unwrap()).collect(); - let captures = RE - .captures_iter(&key_string) - .next() - .ok_or("invalid_instruction")?; - let csi_index = captures[1].parse::<usize>(); - let first_field = captures[2].parse::<usize>(); - let second_field = captures[3].parse::<usize>(); - if csi_index.is_err() || first_field.is_err() || second_field.is_err() { - return Err("invalid_instruction"); - } - match csi_index { - Ok(4) => { - // text area size - Ok(AnsiStdinInstructionOrKeys::PixelDimensions( - PixelDimensions { - character_cell_size: None, - text_area_size: Some(SizeInPixels { - height: first_field.unwrap(), - width: second_field.unwrap(), - }), - }, - )) - }, - Ok(6) => { - // character cell size - Ok(AnsiStdinInstructionOrKeys::PixelDimensions( - PixelDimensions { - character_cell_size: Some(SizeInPixels { - height: first_field.unwrap(), - width: second_field.unwrap(), - }), - text_area_size: None, - }, - )) - }, - _ => Err("invalid sequence"), - } - } else { - Err("invalid sequence") + let key_string = String::from_utf8_lossy(bytes); // TODO: handle error + let captures = RE + .captures_iter(&key_string) + .next() + .ok_or("invalid_instruction")?; + let csi_index = captures[1].parse::<usize>(); + let first_field = captures[2].parse::<usize>(); + let second_field = captures[3].parse::<usize>(); + if csi_index.is_err() || first_field.is_err() || second_field.is_err() { + return Err("invalid_instruction"); + } + match csi_index { + Ok(4) => { + // text area size + Ok(AnsiStdinInstruction::PixelDimensions(PixelDimensions { + character_cell_size: None, + text_area_size: Some(SizeInPixels { + height: first_field.unwrap(), + width: second_field.unwrap(), + }), + })) + }, + Ok(6) => { + // character cell size + Ok(AnsiStdinInstruction::PixelDimensions(PixelDimensions { + character_cell_size: Some(SizeInPixels { + height: first_field.unwrap(), + width: second_field.unwrap(), + }), + text_area_size: None, + })) + }, + _ => Err("invalid sequence"), } } - pub fn color_sequence_from_keys(keys: &[(Key, Vec<u8>)]) -> Result<Self, &'static str> { + pub fn bg_or_fg_from_bytes(bytes: &[u8]) -> Result<Self, &'static str> { + // eg. <ESC>]11;rgb:0000/0000/0000\ lazy_static! { - static ref BACKGROUND_RE: Regex = Regex::new(r"11;(.*)$").unwrap(); + static ref BACKGROUND_RE: Regex = Regex::new(r"\]11;(.*)\u{1b}\\$").unwrap(); } + // eg. <ESC>]10;rgb:ffff/ffff/ffff\ lazy_static! { - static ref FOREGROUND_RE: Regex = Regex::new(r"10;(.*)$").unwrap(); + static ref FOREGROUND_RE: Regex = Regex::new(r"\]10;(.*)\u{1b}\\$").unwrap(); } - let key_string = keys.iter().fold(String::new(), |mut acc, (key, _)| { - if let Key::Char(c) = key { - acc.push(*c) - }; - acc - }); + let key_string = String::from_utf8_lossy(bytes); if let Some(captures) = BACKGROUND_RE.captures_iter(&key_string).next() { let background_query_response = captures[1].parse::<String>(); - Ok(AnsiStdinInstructionOrKeys::BackgroundColor( - background_query_response.unwrap(), - )) + match background_query_response { + Ok(background_query_response) => Ok(AnsiStdinInstruction::BackgroundColor( + background_query_response, + )), + _ => Err("invalid_instruction"), + } } else if let Some(captures) = FOREGROUND_RE.captures_iter(&key_string).next() { let foreground_query_response = captures[1].parse::<String>(); - Ok(AnsiStdinInstructionOrKeys::ForegroundColor( - foreground_query_response.unwrap(), - )) + match foreground_query_response { + Ok(foreground_query_response) => Ok(AnsiStdinInstruction::ForegroundColor( + foreground_query_response, + )), + _ => Err("invalid_instruction"), + } } else { Err("invalid_instruction") } } + pub fn color_registers_from_bytes(color_sequences: &mut Vec<(usize, String)>) -> Option<Self> { + if color_sequences.is_empty() { + return None; + } + let mut registers = vec![]; + for (color_register, color_sequence) in color_sequences.drain(..) { + registers.push((color_register, color_sequence)); + } + Some(AnsiStdinInstruction::ColorRegisters(registers)) + } +} + +fn color_sequence_from_bytes(bytes: &[u8]) -> Result<(usize, String), &'static str> { + lazy_static! { + static ref COLOR_REGISTER_RE: Regex = Regex::new(r"\]4;(.*);(.*)\u{1b}\\$").unwrap(); + } + lazy_static! { + // this form is used by eg. Alacritty, where the leading 4 is dropped in the response + static ref ALTERNATIVE_COLOR_REGISTER_RE: Regex = Regex::new(r"\](.*);(.*)\u{1b}\\$").unwrap(); + } + let key_string = String::from_utf8_lossy(bytes); + if let Some(captures) = COLOR_REGISTER_RE.captures_iter(&key_string).next() { + let color_register_response = captures[1].parse::<usize>(); + let color_response = captures[2].parse::<String>(); + match (color_register_response, color_response) { + (Ok(crr), Ok(cr)) => Ok((crr, cr)), + _ => Err("invalid_instruction"), + } + } else if let Some(captures) = ALTERNATIVE_COLOR_REGISTER_RE + .captures_iter(&key_string) + .next() + { + let color_register_response = captures[1].parse::<usize>(); + let color_response = captures[2].parse::<String>(); + match (color_register_response, color_response) { + (Ok(crr), Ok(cr)) => Ok((crr, cr)), + _ => Err("invalid_instruction"), + } + } else { + Err("invalid_instruction") + } } diff --git a/zellij-client/src/stdin_handler.rs b/zellij-client/src/stdin_handler.rs index 687ecd10..8d6e51b0 100644 --- a/zellij-client/src/stdin_handler.rs +++ b/zellij-client/src/stdin_handler.rs @@ -1,17 +1,46 @@ use crate::os_input_output::ClientOsApi; +use crate::stdin_ansi_parser::StdinAnsiParser; use crate::InputInstruction; +use std::sync::{Arc, Mutex}; use zellij_utils::channels::SenderWithContext; use zellij_utils::termwiz::input::{InputEvent, InputParser, MouseButtons}; pub(crate) fn stdin_loop( - os_input: Box<dyn ClientOsApi>, + mut os_input: Box<dyn ClientOsApi>, send_input_instructions: SenderWithContext<InputInstruction>, + stdin_ansi_parser: Arc<Mutex<StdinAnsiParser>>, ) { let mut holding_mouse = false; let mut input_parser = InputParser::new(); let mut current_buffer = vec![]; + // on startup we send a query to the terminal emulator for stuff like the pixel size and colors + // we get a response through STDIN, so it makes sense to do this here + let terminal_emulator_query_string = stdin_ansi_parser + .lock() + .unwrap() + .terminal_emulator_query_string(); + let _ = os_input + .get_stdout_writer() + .write(terminal_emulator_query_string.as_bytes()) + .unwrap(); loop { let buf = os_input.read_from_stdin(); + { + // here we check if we need to parse specialized ANSI instructions sent over STDIN + // this happens either on startup (see above) or on SIGWINCH + // + // if we need to parse them, we do so with an internal timeout - anything else we + // receive on STDIN during that timeout is unceremoniously dropped + let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap(); + if stdin_ansi_parser.should_parse() { + let events = stdin_ansi_parser.parse(buf); + if !events.is_empty() { + let _ = send_input_instructions + .send(InputInstruction::AnsiStdinInstructions(events)); + } + continue; + } + } current_buffer.append(&mut buf.to_vec()); let maybe_more = false; // read_from_stdin should (hopefully) always empty the STDIN buffer completely let mut events = vec![]; diff --git a/zellij-client/src/unit/input_handler_tests.rs b/zellij-client/src/unit/input_handler_tests.rs deleted file mode 100644 index b772eb5c..00000000 --- a/zellij-client/src/unit/input_handler_tests.rs +++ /dev/null @@ -1,733 +0,0 @@ -use super::input_loop; -use zellij_utils::data::{InputMode, Palette}; -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 crate::InputInstruction; -use crate::{ - os_input_output::{ClientOsApi, StdinPoller}, - ClientInstruction, CommandIsExecuting, -}; - -use std::path::Path; - -use std::io; -use std::os::unix::io::RawFd; -use std::sync::{Arc, Mutex}; -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![],); -} diff --git a/zellij-client/src/unit/snapshots/zellij_client__input_handler__input_handler_tests__pixel_info_sent_to_server.snap b/zellij-client/src/unit/snapshots/zellij_client__input_handler__input_handler_tests__pixel_info_sent_to_server.snap new file mode 100644 index 00000000..14e5d101 --- /dev/null +++ b/zellij-client/src/unit/snapshots/zellij_client__input_handler__input_handler_tests__pixel_info_sent_to_server.snap @@ -0,0 +1,6 @@ +--- +source: zellij-client/src/./unit/input_handler_tests.rs +assertion_line: 429 +expression: "*format!(\"{:?}\", events_sent_to_server.lock().unwrap())" +--- +[TerminalPixelDimensions(PixelDimensions { text_area_size: Some(SizeInPixels { height: 401, width: 1566 }), character_cell_size: None }), TerminalPixelDimensions(PixelDimensions { text_area_size: None, character_cell_size: Some(SizeInPixels { height: 21, width: 8 }) }), BackgroundColor("rgb:0000/0000/0000"), ForegroundColor("rgb:ffff/ffff/ffff"), ColorRegisters([(0, "rgb:0000/0000/0000"), (1, "rgb:cdcd/0000/0000"), (2, "rgb:0000/cdcd/0000"), (3, "rgb:cdcd/cdcd/0000"), (4, "rgb:0000/0000/eeee"), (5, "rgb:cdcd/0000/cdcd"), (6, "rgb:0000/cdcd/cdcd"), (7, "rgb:e5e5/e5e5/e5e5"), (8, "rgb:7f7f/7f7f/7f7f"), (9, "rgb:ffff/0000/0000"), (10, "rgb:0000/ffff/0000"), (11, "rgb:ffff/ffff/0000"), (12, "rgb:5c5c/5c5c/ffff"), (13, "rgb:ffff/0000/ffff"), (14, "rgb:0000/ffff/ffff"), (15, "rgb:ffff/ffff/ffff"), (16, "rgb:0000/0000/0000"), (17, "rgb:0000/0000/5f5f"), (18, "rgb:0000/0000/8787"), (19, "rgb:0000/0000/afaf"), (20, "rgb:0000/0000/d7d7"), (21, "rgb:0000/0000/ffff"), (22, "rgb:0000/5f5f/0000"), (23, "rgb:0000/5f5f/5f5f"), (24, "rgb:0000/5f5f/8787"), (25, "rgb:0000/5f5f/afaf"), (26, "rgb:0000/5f5f/d7d7"), (27, "rgb:0000/5f5f/ffff"), (28, "rgb:0000/8787/0000"), (29, "rgb:0000/8787/5f5f"), (30, "rgb:0000/8787/8787"), (31, "rgb:0000/8787/afaf"), (32, "rgb:0000/8787/d7d7"), (33, "rgb:0000/8787/ffff"), (34, "rgb:0000/afaf/0000"), (35, "rgb:0000/afaf/5f5f"), (36, "rgb:0000/afaf/8787"), (37, "rgb:0000/afaf/afaf"), (38, "rgb:0000/afaf/d7d7"), (39, "rgb:0000/afaf/ffff"), (40, "rgb:0000/d7d7/0000"), (41, "rgb:0000/d7d7/5f5f"), (42, "rgb:0000/d7d7/8787"), (43, "rgb:0000/d7d7/afaf"), (44, "rgb:0000/d7d7/d7d7"), (45, "rgb:0000/d7d7/ffff"), (46, "rgb:0000/ffff/0000"), (47, "rgb:0000/ffff/5f5f"), (48, "rgb:0000/ffff/8787"), (49, "rgb:0000/ffff/afaf"), (50, "rgb:0000/ffff/d7d7"), (51, "rgb:0000/ffff/ffff"), (52, "rgb:5f5f/0000/0000"), (53, "rgb:5f5f/0000/5f5f"), (54, "rgb:5f5f/0000/8787"), (55, "rgb:5f5f/0000/afaf"), (56, "rgb:5f5f/0000/d7d7"), (57, "rgb:5f5f/0000/ffff"), (58, "rgb:5f5f/5f5f/0000"), (59, "rgb:5f5f/5f5f/5f5f"), (60, "rgb:5f5f/5f5f/8787"), (61, "rgb:5f5f/5f5f/afaf"), (62, "rgb:5f5f/5f5f/d7d7"), (63, "rgb:5f5f/5f5f/ffff"), (64, "rgb:5f5f/8787/0000"), (65, "rgb:5f5f/8787/5f5f"), (66, "rgb:5f5f/8787/8787"), (67, "rgb:5f5f/8787/afaf"), (68, "rgb:5f5f/8787/d7d7"), (69, "rgb:5f5f/8787/ffff"), (70, "rgb:5f5f/afaf/0000"), (71, "rgb:5f5f/afaf/5f5f"), (72, "rgb:5f5f/afaf/8787"), (73, "rgb:5f5f/afaf/afaf"), (74, "rgb:5f5f/afaf/d7d7"), (75, "rgb:5f5f/afaf/ffff"), (76, "rgb:5f5f/d7d7/0000"), (77, "rgb:5f5f/d7d7/5f5f"), (78, "rgb:5f5f/d7d7/8787"), (79, "rgb:5f5f/d7d7/afaf"), (80, "rgb:5f5f/d7d7/d7d7"), (81, "rgb:5f5f/d7d7/ffff"), (82, "rgb:5f5f/ffff/0000"), (83, "rgb:5f5f/ffff/5f5f"), (84, "rgb:5f5f/ffff/8787"), (85, "rgb:5f5f/ffff/afaf"), (86, "rgb:5f5f/ffff/d7d7"), (87, "rgb:5f5f/ffff/ffff"), (88, "rgb:8787/0000/0000"), (89, "rgb:8787/0000/5f5f"), (90, "rgb:8787/0000/8787"), (91, "rgb:8787/0000/afaf"), (92, "rgb:8787/0000/d7d7"), (93, "rgb:8787/0000/ffff"), (94, "rgb:8787/5f5f/0000"), (95, "rgb:8787/5f5f/5f5f"), (96, "rgb:8787/5f5f/8787"), (97, "rgb:8787/5f5f/afaf"), (98, "rgb:8787/5f5f/d7d7"), (99, "rgb:8787/5f5f/ffff"), (100, "rgb:8787/8787/0000"), (101, "rgb:8787/8787/5f5f"), (102, "rgb:8787/8787/8787"), (103, "rgb:8787/8787/afaf"), (104, "rgb:8787/8787/d7d7"), (105, "rgb:8787/8787/ffff"), (106, "rgb:8787/afaf/0000"), (107, "rgb:8787/afaf/5f5f"), (108, "rgb:8787/afaf/8787"), (109, "rgb:8787/afaf/afaf"), (110, "rgb:8787/afaf/d7d7"), (111, "rgb:8787/afaf/ffff"), (112, "rgb:8787/d7d7/0000"), (113, "rgb:8787/d7d7/5f5f"), (114, "rgb:8787/d7d7/8787"), (115, "rgb:8787/d7d7/afaf"), (116, "rgb:8787/d7d7/d7d7"), (117, "rgb:8787/d7d7/ffff"), (118, "rgb:8787/ffff/0000"), (119, "rgb:8787/ffff/5f5f"), (120, "rgb:8787/ffff/8787"), (121, "rgb:8787/ffff/afaf"), (122, "rgb:8787/ffff/d7d7"), (123, "rgb:8787/ffff/ffff"), (124, "rgb:afaf/0000/0000"), (125, "rgb:afaf/0000/5f5f"), (126, "rgb:afaf/0000/8787"), (127, "rgb:afaf/0000/afaf"), (128, "rgb:afaf/0000/d7d7"), (129, "rgb:afaf/0000/ffff"), (130, "rgb:afaf/5f5f/0000"), (131, "rgb:afaf/5f5f/5f5f"), (132, "rgb:afaf/5f5f/8787"), (133, "rgb:afaf/5f5f/afaf"), (134, "rgb:afaf/5f5f/d7d7"), (135, "rgb:afaf/5f5f/ffff"), (136, "rgb:afaf/8787/0000"), (137, "rgb:afaf/8787/5f5f"), (138, "rgb:afaf/8787/8787"), (139, "rgb:afaf/8787/afaf"), (140, "rgb:afaf/8787/d7d7"), (141, "rgb:afaf/8787/ffff"), (142, "rgb:afaf/afaf/0000"), (143, "rgb:afaf/afaf/5f5f"), (144, "rgb:afaf/afaf/8787"), (145, "rgb:afaf/afaf/afaf"), (146, "rgb:afaf/afaf/d7d7"), (147, "rgb:afaf/afaf/ffff"), (148, "rgb:afaf/d7d7/0000"), (149, "rgb:afaf/d7d7/5f5f"), (150, "rgb:afaf/d7d7/8787"), (151, "rgb:afaf/d7d7/afaf"), (152, "rgb:afaf/d7d7/d7d7"), (153, "rgb:afaf/d7d7/ffff"), (154, "rgb:afaf/ffff/0000"), (155, "rgb:afaf/ffff/5f5f"), (156, "rgb:afaf/ffff/8787"), (157, "rgb:afaf/ffff/afaf"), (158, "rgb:afaf/ffff/d7d7"), (159, "rgb:afaf/ffff/ffff"), (160, "rgb:d7d7/0000/0000"), (161, "rgb:d7d7/0000/5f5f"), (162, "rgb:d7d7/0000/8787"), (163, "rgb:d7d7/0000/afaf"), (164, "rgb:d7d7/0000/d7d7"), (165, "rgb:d7d7/0000/ffff"), (166, "rgb:d7d7/5f5f/0000"), (167, "rgb:d7d7/5f5f/5f5f"), (168, "rgb:d7d7/5f5f/8787"), (169, "rgb:d7d7/5f5f/afaf"), (170, "rgb:d7d7/5f5f/d7d7"), (171, "rgb:d7d7/5f5f/ffff"), (172, "rgb:d7d7/8787/0000"), (173, "rgb:d7d7/8787/5f5f"), (174, "rgb:d7d7/8787/8787"), (175, "rgb:d7d7/8787/afaf"), (176, "rgb:d7d7/8787/d7d7"), (177, "rgb:d7d7/8787/ffff"), (178, "rgb:d7d7/afaf/0000"), (179, "rgb:d7d7/afaf/5f5f"), (180, "rgb:d7d7/afaf/8787"), (181, "rgb:d7d7/afaf/afaf"), (182, "rgb:d7d7/afaf/d7d7"), (183, "rgb:d7d7/afaf/ffff"), (184, "rgb:d7d7/d7d7/0000"), (185, "rgb:d7d7/d7d7/5f5f"), (186, "rgb:d7d7/d7d7/8787"), (187, "rgb:d7d7/d7d7/afaf"), (188, "rgb:d7d7/d7d7/d7d7"), (189, "rgb:d7d7/d7d7/ffff"), (190, "rgb:d7d7/ffff/0000"), (191, "rgb:d7d7/ffff/5f5f"), (192, "rgb:d7d7/ffff/8787"), (193, "rgb:d7d7/ffff/afaf"), (194, "rgb:d7d7/ffff/d7d7"), (195, "rgb:d7d7/ffff/ffff"), (196, "rgb:ffff/0000/0000"), (197, "rgb:ffff/0000/5f5f"), (198, "rgb:ffff/0000/8787"), (199, "rgb:ffff/0000/afaf"), (200, "rgb:ffff/0000/d7d7"), (201, "rgb:ffff/0000/ffff"), (202, "rgb:ffff/5f5f/0000"), (203, "rgb:ffff/5f5f/5f5f"), (204, "rgb:ffff/5f5f/8787"), (205, "rgb:ffff/5f5f/afaf"), (206, "rgb:ffff/5f5f/d7d7"), (207, "rgb:ffff/5f5f/ffff"), (208, "rgb:ffff/8787/0000"), (209, "rgb:ffff/8787/5f5f"), (210, "rgb:ffff/8787/8787"), (211, "rgb:ffff/8787/afaf"), (212, "rgb:ffff/8787/d7d7"), (213, "rgb:ffff/8787/ffff"), (214, "rgb:ffff/afaf/0000"), (215, "rgb:ffff/afaf/5f5f"), (216, "rgb:ffff/afaf/8787"), (217, "rgb:ffff/afaf/afaf"), (218, "rgb:ffff/afaf/d7d7"), (219, "rgb:ffff/afaf/ffff"), (220, "rgb:ffff/d7d7/0000"), (221, "rgb:ffff/d7d7/5f5f"), (222, "rgb:ffff/d7d7/8787"), (223, "rgb:ffff/d7d7/afaf"), (224, "rgb:ffff/d7d7/d7d7"), (225, "rgb:ffff/d7d7/ffff"), (226, "rgb:ffff/ffff/0000"), (227, "rgb:ffff/ffff/5f5f"), (228, "rgb:ffff/ffff/8787"), (229, "rgb:ffff/ffff/afaf"), (230, "rgb:ffff/ffff/d7d7"), (231, "rgb:ffff/ffff/ffff"), (232, "rgb:0808/0808/0808"), (233, "rgb:1212/1212/1212"), (234, "rgb:1c1c/1c1c/1c1c"), (235, "rgb:2626/2626/2626"), (236, "rgb:3030/3030/3030"), (237, "rgb:3a3a/3a3a/3a3a"), (238, "rgb:4444/4444/4444"), (239, "rgb:4e4e/4e4e/4e4e"), (240, "rgb:5858/5858/5858"), (241, "rgb:6262/6262/6262"), (242, "rgb:6c6c/6c6c/6c6c"), (243, "rgb:7676/7676/7676"), (244, "rgb:8080/8080/8080"), (245, "rgb:8a8a/8a8a/8a8a"), (246, "rgb:9494/9494/9494"), (247, "rgb:9e9e/9e9e/9e9e"), (248, "rgb:a8a8/a8a8/a8a8"), (249, "rgb:b2b2/b2b2/b2b2"), (250, "rgb:bcbc/bcbc/bcbc"), (251, "rgb:c6c6/c6c6/c6c6"), (252, "rgb:d0d0/d0d0/d0d0"), (253, "rgb:dada/dada/dada"), (254, "rgb:e4e4/e4e4/e4e4"), (255, "rgb:eeee/eeee/eeee")])] diff --git a/zellij-client/src/unit/snapshots/zellij_client__stdin_tests__pixel_info_sent_to_server.snap b/zellij-client/src/unit/snapshots/zellij_client__stdin_tests__pixel_info_sent_to_server.snap new file mode 100644 index 00000000..c58a2f5a --- /dev/null +++ b/zellij-client/src/unit/snapshots/zellij_client__stdin_tests__pixel_info_sent_to_server.snap @@ -0,0 +1,6 @@ +--- +source: zellij-client/src/./unit/stdin_tests.rs +assertion_line: 399 +expression: "*format!(\"{:?}\", events_sent_to_server.lock().unwrap())" +--- +[TerminalPixelDimensions(PixelDimensions { text_area_size: Some(SizeInPixels { height: 401, width: 1566 }), character_cell_size: None }), TerminalPixelDimensions(PixelDimensions { text_area_size: None, character_cell_size: Some(SizeInPixels { height: 21, width: 8 }) }), BackgroundColor("rgb:0000/0000/0000"), ForegroundColor("rgb:ffff/ffff/ffff"), ColorRegisters([(0, "rgb:0000/0000/0000"), (1, "rgb:cdcd/0000/0000"), (2, "rgb:0000/cdcd/0000"), (3, "rgb:cdcd/cdcd/0000"), (4, "rgb:0000/0000/eeee"), (5, "rgb:cdcd/0000/cdcd"), (6, "rgb:0000/cdcd/cdcd"), (7, "rgb:e5e5/e5e5/e5e5"), (8, "rgb:7f7f/7f7f/7f7f"), (9, "rgb:ffff/0000/0000"), (10, "rgb:0000/ffff/0000"), (11, "rgb:ffff/ffff/0000"), (12, "rgb:5c5c/5c5c/ffff"), (13, "rgb:ffff/0000/ffff"), (14, "rgb:0000/ffff/ffff"), (15, "rgb:ffff/ffff/ffff"), (16, "rgb:0000/0000/0000"), (17, "rgb:0000/0000/5f5f"), (18, "rgb:0000/0000/8787"), (19, "rgb:0000/0000/afaf"), (20, "rgb:0000/0000/d7d7"), (21, "rgb:0000/0000/ffff"), (22, "rgb:0000/5f5f/0000"), (23, "rgb:0000/5f5f/5f5f"), (24, "rgb:0000/5f5f/8787"), (25, "rgb:0000/5f5f/afaf"), (26, "rgb:0000/5f5f/d7d7"), (27, "rgb:0000/5f5f/ffff"), (28, "rgb:0000/8787/0000"), (29, "rgb:0000/8787/5f5f"), (30, "rgb:0000/8787/8787"), (31, "rgb:0000/8787/afaf"), (32, "rgb:0000/8787/d7d7"), (33, "rgb:0000/8787/ffff"), (34, "rgb:0000/afaf/0000"), (35, "rgb:0000/afaf/5f5f"), (36, "rgb:0000/afaf/8787"), (37, "rgb:0000/afaf/afaf"), (38, "rgb:0000/afaf/d7d7"), (39, "rgb:0000/afaf/ffff"), (40, "rgb:0000/d7d7/0000"), (41, "rgb:0000/d7d7/5f5f"), (42, "rgb:0000/d7d7/8787"), (43, "rgb:0000/d7d7/afaf"), (44, "rgb:0000/d7d7/d7d7"), (45, "rgb:0000/d7d7/ffff"), (46, "rgb:0000/ffff/0000"), (47, "rgb:0000/ffff/5f5f"), (48, "rgb:0000/ffff/8787"), (49, "rgb:0000/ffff/afaf"), (50, "rgb:0000/ffff/d7d7"), (51, "rgb:0000/ffff/ffff"), (52, "rgb:5f5f/0000/0000"), (53, "rgb:5f5f/0000/5f5f"), (54, "rgb:5f5f/0000/8787"), (55, "rgb:5f5f/0000/afaf"), (56, "rgb:5f5f/0000/d7d7"), (57, "rgb:5f5f/0000/ffff"), (58, "rgb:5f5f/5f5f/0000"), (59, "rgb:5f5f/5f5f/5f5f"), (60, "rgb:5f5f/5f5f/8787"), (61, "rgb:5f5f/5f5f/afaf"), (62, "rgb:5f5f/5f5f/d7d7"), (63, "rgb:5f5f/5f5f/ffff"), (64, "rgb:5f5f/8787/0000"), (65, "rgb:5f5f/8787/5f5f"), (66, "rgb:5f5f/8787/8787"), (67, "rgb:5f5f/8787/afaf"), (68, "rgb:5f5f/8787/d7d7"), (69, "rgb:5f5f/8787/ffff"), (70, "rgb:5f5f/afaf/0000"), (71, "rgb:5f5f/afaf/5f5f"), (72, "rgb:5f5f/afaf/8787"), (73, "rgb:5f5f/afaf/afaf"), (74, "rgb:5f5f/afaf/d7d7"), (75, "rgb:5f5f/afaf/ffff"), (76, "rgb:5f5f/d7d7/0000"), (77, "rgb:5f5f/d7d7/5f5f"), (78, "rgb:5f5f/d7d7/8787"), (79, "rgb:5f5f/d7d7/afaf"), (80, "rgb:5f5f/d7d7/d7d7"), (81, "rgb:5f5f/d7d7/ffff"), (82, "rgb:5f5f/ffff/0000"), (83, "rgb:5f5f/ffff/5f5f"), (84, "rgb:5f5f/ffff/8787"), (85, "rgb:5f5f/ffff/afaf"), (86, "rgb:5f5f/ffff/d7d7"), (87, "rgb:5f5f/ffff/ffff"), (88, "rgb:8787/0000/0000"), (89, "rgb:8787/0000/5f5f"), (90, "rgb:8787/0000/8787"), (91, "rgb:8787/0000/afaf"), (92, "rgb:8787/0000/d7d7"), (93, "rgb:8787/0000/ffff"), (94, "rgb:8787/5f5f/0000"), (95, "rgb:8787/5f5f/5f5f"), (96, "rgb:8787/5f5f/8787"), (97, "rgb:8787/5f5f/afaf"), (98, "rgb:8787/5f5f/d7d7"), (99, "rgb:8787/5f5f/ffff"), (100, "rgb:8787/8787/0000"), (101, "rgb:8787/8787/5f5f"), (102, "rgb:8787/8787/8787"), (103, "rgb:8787/8787/afaf"), (104, "rgb:8787/8787/d7d7"), (105, "rgb:8787/8787/ffff"), (106, "rgb:8787/afaf/0000"), (107, "rgb:8787/afaf/5f5f"), (108, "rgb:8787/afaf/8787"), (109, "rgb:8787/afaf/afaf"), (110, "rgb:8787/afaf/d7d7"), (111, "rgb:8787/afaf/ffff"), (112, "rgb:8787/d7d7/0000"), (113, "rgb:8787/d7d7/5f5f"), (114, "rgb:8787/d7d7/8787"), (115, "rgb:8787/d7d7/afaf"), (116, "rgb:8787/d7d7/d7d7"), (117, "rgb:8787/d7d7/ffff"), (118, "rgb:8787/ffff/0000"), (119, "rgb:8787/ffff/5f5f"), (120, "rgb:8787/ffff/8787"), (121, "rgb:8787/ffff/afaf"), (122, "rgb:8787/ffff/d7d7"), (123, "rgb:8787/ffff/ffff"), (124, "rgb:afaf/0000/0000"), (125, "rgb:afaf/0000/5f5f"), (126, "rgb:afaf/0000/8787"), (127, "rgb:afaf/0000/afaf"), (128, "rgb:afaf/0000/d7d7"), (129, "rgb:afaf/0000/ffff"), (130, "rgb:afaf/5f5f/0000"), (131, "rgb:afaf/5f5f/5f5f"), (132, "rgb:afaf/5f5f/8787"), (133, "rgb:afaf/5f5f/afaf"), (134, "rgb:afaf/5f5f/d7d7"), (135, "rgb:afaf/5f5f/ffff"), (136, "rgb:afaf/8787/0000"), (137, "rgb:afaf/8787/5f5f"), (138, "rgb:afaf/8787/8787"), (139, "rgb:afaf/8787/afaf"), (140, "rgb:afaf/8787/d7d7"), (141, "rgb:afaf/8787/ffff"), (142, "rgb:afaf/afaf/0000"), (143, "rgb:afaf/afaf/5f5f"), (144, "rgb:afaf/afaf/8787"), (145, "rgb:afaf/afaf/afaf"), (146, "rgb:afaf/afaf/d7d7"), (147, "rgb:afaf/afaf/ffff"), (148, "rgb:afaf/d7d7/0000"), (149, "rgb:afaf/d7d7/5f5f"), (150, "rgb:afaf/d7d7/8787"), (151, "rgb:afaf/d7d7/afaf"), (152, "rgb:afaf/d7d7/d7d7"), (153, "rgb:afaf/d7d7/ffff"), (154, "rgb:afaf/ffff/0000"), (155, "rgb:afaf/ffff/5f5f"), (156, "rgb:afaf/ffff/8787"), (157, "rgb:afaf/ffff/afaf"), (158, "rgb:afaf/ffff/d7d7"), (159, "rgb:afaf/ffff/ffff"), (160, "rgb:d7d7/0000/0000"), (161, "rgb:d7d7/0000/5f5f"), (162, "rgb:d7d7/0000/8787"), (163, "rgb:d7d7/0000/afaf"), (164, "rgb:d7d7/0000/d7d7"), (165, "rgb:d7d7/0000/ffff"), (166, "rgb:d7d7/5f5f/0000"), (167, "rgb:d7d7/5f5f/5f5f"), (168, "rgb:d7d7/5f5f/8787"), (169, "rgb:d7d7/5f5f/afaf"), (170, "rgb:d7d7/5f5f/d7d7"), (171, "rgb:d7d7/5f5f/ffff"), (172, "rgb:d7d7/8787/0000"), (173, "rgb:d7d7/8787/5f5f"), (174, "rgb:d7d7/8787/8787"), (175, "rgb:d7d7/8787/afaf"), (176, "rgb:d7d7/8787/d7d7"), (177, "rgb:d7d7/8787/ffff"), (178, "rgb:d7d7/afaf/0000"), (179, "rgb:d7d7/afaf/5f5f"), (180, "rgb:d7d7/afaf/8787"), (181, "rgb:d7d7/afaf/afaf"), (182, "rgb:d7d7/afaf/d7d7"), (183, "rgb:d7d7/afaf/ffff"), (184, "rgb:d7d7/d7d7/0000"), (185, "rgb:d7d7/d7d7/5f5f"), (186, "rgb:d7d7/d7d7/8787"), (187, "rgb:d7d7/d7d7/afaf"), (188, "rgb:d7d7/d7d7/d7d7"), (189, "rgb:d7d7/d7d7/ffff"), (190, "rgb:d7d7/ffff/0000"), (191, "rgb:d7d7/ffff/5f5f"), (192, "rgb:d7d7/ffff/8787"), (193, "rgb:d7d7/ffff/afaf"), (194, "rgb:d7d7/ffff/d7d7"), (195, "rgb:d7d7/ffff/ffff"), (196, "rgb:ffff/0000/0000"), (197, "rgb:ffff/0000/5f5f"), (198, "rgb:ffff/0000/8787"), (199, "rgb:ffff/0000/afaf"), (200, "rgb:ffff/0000/d7d7"), (201, "rgb:ffff/0000/ffff"), (202, "rgb:ffff/5f5f/0000"), (203, "rgb:ffff/5f5f/5f5f"), (204, "rgb:ffff/5f5f/8787"), (205, "rgb:ffff/5f5f/afaf"), (206, "rgb:ffff/5f5f/d7d7"), (207, "rgb:ffff/5f5f/ffff"), (208, "rgb:ffff/8787/0000"), (209, "rgb:ffff/8787/5f5f"), (210, "rgb:ffff/8787/8787"), (211, "rgb:ffff/8787/afaf"), (212, "rgb:ffff/8787/d7d7"), (213, "rgb:ffff/8787/ffff"), (214, "rgb:ffff/afaf/0000"), (215, "rgb:ffff/afaf/5f5f"), (216, "rgb:ffff/afaf/8787"), (217, "rgb:ffff/afaf/afaf"), (218, "rgb:ffff/afaf/d7d7"), (219, "rgb:ffff/afaf/ffff"), (220, "rgb:ffff/d7d7/0000"), (221, "rgb:ffff/d7d7/5f5f"), (222, "rgb:ffff/d7d7/8787"), (223, "rgb:ffff/d7d7/afaf"), (224, "rgb:ffff/d7d7/d7d7"), (225, "rgb:ffff/d7d7/ffff"), (226, "rgb:ffff/ffff/0000"), (227, "rgb:ffff/ffff/5f5f"), (228, "rgb:ffff/ffff/8787"), (229, "rgb:ffff/ffff/afaf"), (230, "rgb:ffff/ffff/d7d7"), (231, "rgb:ffff/ffff/ffff"), (232, "rgb:0808/0808/0808"), (233, "rgb:1212/1212/1212"), (234, "rgb:1c1c/1c1c/1c1c"), (235, "rgb:2626/2626/2626"), (236, "rgb:3030/3030/3030"), (237, "rgb:3a3a/3a3a/3a3a"), (238, "rgb:4444/4444/4444"), (239, "rgb:4e4e/4e4e/4e4e"), (240, "rgb:5858/5858/5858"), (241, "rgb:6262/6262/6262"), (242, "rgb:6c6c/6c6c/6c6c"), (243, "rgb:7676/7676/7676"), (244, "rgb:8080/8080/8080"), (245, "rgb:8a8a/8a8a/8a8a"), (246, "rgb:9494/9494/9494"), (247, "rgb:9e9e/9e9e/9e9e"), (248, "rgb:a8a8/a8a8/a8a8"), (249, "rgb:b2b2/b2b2/b2b2"), (250, "rgb:bcbc/bcbc/bcbc"), (251, "rgb:c6c6/c6c6/c6c6"), (252, "rgb:d0d0/d0d0/d0d0"), (253, "rgb:dada/dada/dada"), (254, "rgb:e4e4/e4e4/e4e4"), (255, "rgb:eeee/eeee/eeee")])] diff --git a/zellij-client/src/unit/stdin_tests.rs b/zellij-client/src/unit/stdin_tests.rs new file mode 100644 index 00000000..d51734ac --- /dev/null +++ b/zellij-client/src/unit/stdin_tests.rs @@ -0,0 +1,409 @@ +use super::input_loop; +use crate::stdin_ansi_parser::StdinAnsiParser; +use crate::stdin_loop; +use zellij_utils::data::{InputMode, Palette}; +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; +use zellij_utils::termwiz::input::{InputEvent, KeyCode, KeyEvent, Modifiers}; + +use crate::InputInstruction; +use crate::{ + os_input_output::{ClientOsApi, StdinPoller}, + ClientInstruction, CommandIsExecuting, +}; + +use ::insta::assert_snapshot; +use std::path::Path; + +use std::io; +use std::os::unix::io::RawFd; +use std::sync::{Arc, Mutex}; +use std::thread; +use zellij_utils::{ + errors::ErrorContext, + ipc::{ClientToServerMsg, ServerToClientMsg}, +}; + +use zellij_utils::channels::{self, ChannelWithContext, SenderWithContext}; + +fn read_fixture(fixture_name: &str) -> Vec<u8> { + let mut path_to_file = std::path::PathBuf::new(); + path_to_file.push("../src"); + path_to_file.push("tests"); + path_to_file.push("fixtures"); + path_to_file.push(fixture_name); + std::fs::read(path_to_file) + .unwrap_or_else(|_| panic!("could not read fixture {:?}", &fixture_name)) +} + +#[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>>>, + stdin_buffer: 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, + stdin_buffer: vec![], + } + } + pub fn with_stdin_buffer(mut self, stdin_buffer: Vec<u8>) -> Self { + self.stdin_buffer = stdin_buffer; + self + } + 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(&mut self) -> Vec<u8> { + self.stdin_buffer.drain(..).collect() + } + 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 + }) +} + +#[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 terminal_info_queried_from_terminal_emulator() { + 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); + + let client_os_api_clone = client_os_api.clone(); + let (send_input_instructions, _receive_input_instructions): ChannelWithContext< + InputInstruction, + > = channels::bounded(50); + let send_input_instructions = SenderWithContext::new(send_input_instructions); + let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new())); + + let stdin_thread = thread::Builder::new() + .name("stdin_handler".to_string()) + .spawn({ + move || { + stdin_loop( + Box::new(client_os_api), + send_input_instructions, + stdin_ansi_parser, + ) + } + }); + std::thread::sleep(std::time::Duration::from_millis(500)); // wait for initial query to be sent + + let extracted_stdout_buffer = client_os_api_clone.stdout_buffer(); + let mut expected_query = + String::from("\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}"); + for i in 0..256 { + expected_query.push_str(&format!("\u{1b}]4;{};?\u{1b}\u{5c}", i)); + } + assert_eq!( + String::from_utf8(extracted_stdout_buffer), + Ok(expected_query), + ); + drop(stdin_thread); +} + +#[test] +pub fn pixel_info_sent_to_server() { + let fake_stdin_buffer = read_fixture("terminal_emulator_startup_response"); + 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()) + .with_stdin_buffer(fake_stdin_buffer); + 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); + let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new())); + let stdin_thread = thread::Builder::new() + .name("stdin_handler".to_string()) + .spawn({ + let client_os_api = client_os_api.clone(); + move || { + stdin_loop( + Box::new(client_os_api), + send_input_instructions, + stdin_ansi_parser, + ) + } + }); + + let default_mode = InputMode::Normal; + let input_thread = thread::Builder::new() + .name("input_handler".to_string()) + .spawn({ + move || { + input_loop( + Box::new(client_os_api), + config, + options, + command_is_executing, + send_client_instructions, + default_mode, + receive_input_instructions, + ) + } + }); + std::thread::sleep(std::time::Duration::from_millis(1000)); // wait for initial query to be sent + assert_snapshot!(*format!("{:?}", events_sent_to_server.lock().unwrap())); + drop(stdin_thread); + drop(input_thread); +} |
