diff options
author | Aram Drevekenin <aram@poor.dev> | 2022-04-12 18:07:32 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-12 18:07:32 +0200 |
commit | 19adb29be516a871620071b289594bacf3d3056c (patch) | |
tree | 3ff60c31f6b7fc460089bb5d2da4ad81acc2ec8d /zellij-client/src | |
parent | 028885c82273c2f6c7ce080edeba097ca5f5dfa4 (diff) |
feat(signals): support XTWINOPS 14 and 16 (and query the terminal for them on startup and SIGWINCH) (#1316)
* feat(signals): get pixel info from terminal emulator
* feat(signals): query for pixel info on sigwinch
* feat(signals): reply to csi 14t and csi 16t
* style(fmt): rustfmt
* style(comments): remove outdated
Diffstat (limited to 'zellij-client/src')
-rw-r--r-- | zellij-client/src/input_handler.rs | 47 | ||||
-rw-r--r-- | zellij-client/src/lib.rs | 5 | ||||
-rw-r--r-- | zellij-client/src/pixel_csi_parser.rs | 146 | ||||
-rw-r--r-- | zellij-client/src/unit/input_handler_tests.rs | 474 |
4 files changed, 666 insertions, 6 deletions
diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 250485eb9..b2ef1e587 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -1,5 +1,4 @@ //! Main input logic. - use zellij_utils::{ input::{ mouse::{MouseButton, MouseEvent}, @@ -10,7 +9,9 @@ use zellij_utils::{ }; use crate::{ - os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting, InputInstruction, + os_input_output::ClientOsApi, + pixel_csi_parser::{PixelCsiParser, PixelDimensionsOrKeys}, + ClientInstruction, CommandIsExecuting, InputInstruction, }; use zellij_utils::{ channels::{Receiver, SenderWithContext, OPENCALLS}, @@ -70,6 +71,15 @@ 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 + let get_cell_pixel_info = "\u{1b}[14t\u{1b}[16t"; + let _ = self + .os_input + .get_stdout_writer() + .write(get_cell_pixel_info.as_bytes()) + .unwrap(); + let mut pixel_csi_parser = PixelCsiParser::new(); + pixel_csi_parser.increment_expected_csi_instructions(2); loop { if self.should_exit { break; @@ -79,7 +89,13 @@ impl InputHandler { match input_event { InputEvent::Key(key_event) => { let key = cast_termwiz_key(key_event, &raw_bytes); - self.handle_key(&key, raw_bytes); + if pixel_csi_parser.expected_instructions() > 0 { + self.handle_possible_pixel_instruction( + pixel_csi_parser.parse(key, raw_bytes), + ); + } else { + self.handle_key(&key, raw_bytes); + } } InputEvent::Mouse(mouse_event) => { let mouse_event = @@ -101,6 +117,14 @@ 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(); + pixel_csi_parser.increment_expected_csi_instructions(2); + } Err(err) => panic!("Encountered read error: {:?}", err), } } @@ -114,6 +138,23 @@ impl InputHandler { } } } + fn handle_possible_pixel_instruction( + &mut self, + pixel_instruction_or_keys: Option<PixelDimensionsOrKeys>, + ) { + match pixel_instruction_or_keys { + Some(PixelDimensionsOrKeys::PixelDimensions(pixel_dimensions)) => { + self.os_input + .send_to_server(ClientToServerMsg::TerminalPixelDimensions(pixel_dimensions)); + } + Some(PixelDimensionsOrKeys::Keys(keys)) => { + for (key, raw_bytes) in keys { + self.handle_key(&key, raw_bytes); + } + } + None => {} + } + } fn handle_mouse_event(&mut self, mouse_event: &MouseEvent) { match *mouse_event { MouseEvent::Press(button, point) => match button { diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index a17845320..556f18120 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -2,6 +2,7 @@ pub mod os_input_output; mod command_is_executing; mod input_handler; +mod pixel_csi_parser; mod stdin_handler; use log::info; @@ -108,6 +109,7 @@ impl ClientInfo { pub(crate) enum InputInstruction { KeyEvent(InputEvent, Vec<u8>), SwitchToMode(InputMode), + PossiblePixelRatioChange, } pub fn start_client( @@ -237,6 +239,7 @@ 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( @@ -246,6 +249,8 @@ 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); } }), Box::new({ diff --git a/zellij-client/src/pixel_csi_parser.rs b/zellij-client/src/pixel_csi_parser.rs new file mode 100644 index 000000000..fc2df627a --- /dev/null +++ b/zellij-client/src/pixel_csi_parser.rs @@ -0,0 +1,146 @@ +use zellij_utils::pane_size::SizeInPixels; + +use zellij_utils::{ipc::PixelDimensions, lazy_static::lazy_static, regex::Regex}; + +use zellij_tile::data::Key; + +pub struct PixelCsiParser { + expected_pixel_csi_instructions: usize, + current_buffer: Vec<(Key, Vec<u8>)>, +} + +impl PixelCsiParser { + pub fn new() -> Self { + PixelCsiParser { + expected_pixel_csi_instructions: 0, + current_buffer: vec![], + } + } + pub fn increment_expected_csi_instructions(&mut self, by: usize) { + self.expected_pixel_csi_instructions += by; + } + pub fn decrement_expected_csi_instructions(&mut self, by: usize) { + self.expected_pixel_csi_instructions = + self.expected_pixel_csi_instructions.saturating_sub(by); + } + pub fn expected_instructions(&self) -> usize { + self.expected_pixel_csi_instructions + } + pub fn parse(&mut self, key: Key, raw_bytes: Vec<u8>) -> Option<PixelDimensionsOrKeys> { + if let Key::Char('t') = key { + self.current_buffer.push((key, raw_bytes)); + match PixelDimensionsOrKeys::pixel_dimensions_from_keys(&self.current_buffer) { + Ok(pixel_instruction) => { + self.decrement_expected_csi_instructions(1); + self.current_buffer.clear(); + Some(pixel_instruction) + } + Err(_) => { + self.expected_pixel_csi_instructions = 0; + Some(PixelDimensionsOrKeys::Keys( + self.current_buffer.drain(..).collect(), + )) + } + } + } else if self.key_is_valid(key) { + self.current_buffer.push((key, raw_bytes)); + None + } else { + self.current_buffer.push((key, raw_bytes)); + self.expected_pixel_csi_instructions = 0; + Some(PixelDimensionsOrKeys::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 pixel instructions" mode + if self + .current_buffer + .iter() + .find(|(key, _)| *key == Key::Esc) + .is_none() + { + true + } else { + false + } + } + Key::Char(';') | Key::Char('[') => true, + Key::Char(c) => { + if let '0'..='9' = c { + true + } else { + false + } + } + _ => false, + } + } +} + +#[derive(Debug)] +pub enum PixelDimensionsOrKeys { + // TODO: rename to PixelDimensionsOrKeys + PixelDimensions(PixelDimensions), + Keys(Vec<(Key, Vec<u8>)>), +} + +impl PixelDimensionsOrKeys { + pub fn pixel_dimensions_from_keys(keys: &Vec<(Key, Vec<u8>)>) -> Result<Self, &'static str> { + 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(PixelDimensionsOrKeys::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(PixelDimensionsOrKeys::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") + } + } +} diff --git a/zellij-client/src/unit/input_handler_tests.rs b/zellij-client/src/unit/input_handler_tests.rs index a83719a9d..1c5180c81 100644 --- a/zellij-client/src/unit/input_handler_tests.rs +++ b/zellij-client/src/unit/input_handler_tests.rs @@ -2,7 +2,7 @@ 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::pane_size::Size; +use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::termwiz::input::{InputEvent, KeyCode, KeyEvent, Modifiers}; use zellij_utils::zellij_tile::data::Palette; @@ -22,7 +22,7 @@ use std::sync::{Arc, Mutex}; use zellij_tile::data::InputMode; use zellij_utils::{ errors::ErrorContext, - ipc::{ClientToServerMsg, ServerToClientMsg}, + ipc::{ClientToServerMsg, PixelDimensions, ServerToClientMsg}, }; use zellij_utils::channels::{self, ChannelWithContext, SenderWithContext}; @@ -71,9 +71,30 @@ pub mod commands { 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, mut buf: &[u8]) -> Result<usize, io::Error> { + self.buffer.lock().unwrap().extend_from_slice(&mut 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 { @@ -85,11 +106,16 @@ impl FakeClientOsApi { // 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 { @@ -103,7 +129,8 @@ impl ClientOsApi for FakeClientOsApi { unimplemented!() } fn get_stdout_writer(&self) -> Box<dyn io::Write> { - unimplemented!() + 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!() @@ -155,6 +182,18 @@ fn extract_actions_sent_to_server( }) } +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.clone()); + } + acc + }) +} + #[test] pub fn quit_breaks_input_loop() { let stdin_events = vec![( @@ -267,3 +306,432 @@ pub fn move_focus_left_in_normal_mode() { "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.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; + 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")), + ); +} + +#[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.clone()); + 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.clone()); + 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 {< |