From 19adb29be516a871620071b289594bacf3d3056c Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 12 Apr 2022 18:07:32 +0200 Subject: [PATCH] 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 --- Cargo.lock | 5 +- src/tests/e2e/remote_runner.rs | 1 + .../fixtures/terminal_pixel_size_reports | 1 + zellij-client/src/input_handler.rs | 47 +- zellij-client/src/lib.rs | 5 + zellij-client/src/pixel_csi_parser.rs | 146 ++++++ zellij-client/src/unit/input_handler_tests.rs | 474 +++++++++++++++++- zellij-server/src/panes/grid.rs | 26 +- zellij-server/src/panes/terminal_pane.rs | 3 + zellij-server/src/panes/tiled_panes/mod.rs | 20 +- .../src/panes/tiled_panes/tiled_pane_grid.rs | 15 +- zellij-server/src/panes/unit/grid_tests.rs | 141 +++++- .../src/panes/unit/terminal_pane_tests.rs | 1 + zellij-server/src/route.rs | 8 + zellij-server/src/screen.rs | 30 +- zellij-server/src/tab/mod.rs | 11 +- .../src/tab/unit/tab_integration_tests.rs | 4 + zellij-server/src/tab/unit/tab_tests.rs | 76 ++- zellij-server/src/unit/screen_tests.rs | 82 ++- zellij-utils/Cargo.toml | 1 + zellij-utils/src/errors.rs | 1 + zellij-utils/src/ipc.rs | 20 +- zellij-utils/src/lib.rs | 2 + zellij-utils/src/pane_size.rs | 6 + 24 files changed, 1099 insertions(+), 27 deletions(-) create mode 100644 src/tests/fixtures/terminal_pixel_size_reports create mode 100644 zellij-client/src/pixel_csi_parser.rs diff --git a/Cargo.lock b/Cargo.lock index a9a0471e..de711a15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2804,9 +2804,9 @@ dependencies = [ [[package]] name = "vtparse" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f41c9314c4dde1f43dd0c46c67bb5ae73850ce11eebaf7d8b912e178bda5401" +checksum = "36ce903972602c84dd48f488cdce39edcba03a93b7ca67b146ae862568f48c5c" dependencies = [ "utf8parse", ] @@ -3298,6 +3298,7 @@ dependencies = [ "miette", "nix", "once_cell", + "regex", "serde", "serde_json", "serde_yaml", diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 042ae9b6..5a3faaff 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -161,6 +161,7 @@ fn read_from_channel( 0, String::new(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); // 0 is the pane index loop { if !should_keep_running.load(Ordering::SeqCst) { diff --git a/src/tests/fixtures/terminal_pixel_size_reports b/src/tests/fixtures/terminal_pixel_size_reports new file mode 100644 index 00000000..243c3c18 --- /dev/null +++ b/src/tests/fixtures/terminal_pixel_size_reports @@ -0,0 +1 @@ +; diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 250485eb..b2ef1e58 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(); } + // [14t => get text area size in pixels, [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, + ) { + 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 a1784532..556f1812 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), 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 00000000..fc2df627 --- /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)>, +} + +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) -> Option { + 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)>), +} + +impl PixelDimensionsOrKeys { + pub fn pixel_dimensions_from_keys(keys: &Vec<(Key, Vec)>) -> Result { + lazy_static! { + static ref RE: Regex = Regex::new(r"^\u{1b}\[(\d+);(\d+);(\d+)t$").unwrap(); + } + let key_sequence: Vec> = 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::(); + let first_field = captures[2].parse::(); + let second_field = captures[3].parse::(); + 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 a83719a9..1c5180c8 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>>, +} +impl FakeStdoutWriter { + pub fn new(buffer: Arc>>) -> Self { + FakeStdoutWriter { buffer } + } +} +impl io::Write for FakeStdoutWriter { + fn write(&mut self, mut buf: &[u8]) -> Result { + 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>>, command_is_executing: Arc>, + stdout_buffer: Arc>>, } impl FakeClientOsApi { @@ -85,11 +106,16 @@ impl FakeClientOsApi { // Arc 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 { + 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 { - unimplemented!() + let fake_stdout_writer = FakeStdoutWriter::new(self.stdout_buffer.clone()); + Box::new(fake_stdout_writer) } fn get_stdin_reader(&self) -> Box { unimplemented!() @@ -155,6 +182,18 @@ fn extract_actions_sent_to_server( }) } +fn extract_pixel_events_sent_to_server( + events_sent_to_server: Arc>>, +) -> Vec { + 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 { + 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.clone()); + 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-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index 56e3732c..320308e2 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -11,6 +11,7 @@ use std::{ use zellij_utils::{ consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE}, + pane_size::SizeInPixels, position::Position, vte, zellij_tile, }; @@ -289,6 +290,7 @@ pub struct Grid { colors: Palette, output_buffer: OutputBuffer, title_stack: Vec, + character_cell_size: Rc>>, pub changed_colors: Option<[Option; 256]>, pub should_render: bool, pub cursor_key_mode: bool, // DECCKM - when set, cursor keys should send ANSI direction codes (eg. "OD") instead of the arrow keys (eg. "") @@ -328,6 +330,7 @@ impl Grid { columns: usize, colors: Palette, link_handler: Rc>, + character_cell_size: Rc>>, ) -> Self { Grid { lines_above: VecDeque::with_capacity( @@ -365,6 +368,7 @@ impl Grid { ring_bell: false, scrollback_buffer_lines: 0, mouse_mode: false, + character_cell_size, } } pub fn render_full_viewport(&mut self) { @@ -2009,9 +2013,25 @@ impl Perform for Grid { } else if c == 't' { match next_param_or(1) as usize { 14 => { - // TODO: report text area size in pixels, currently unimplemented - // to solve this we probably need to query the user's terminal for the cursor - // size and then use it as a multiplier + if let Some(character_cell_size) = *self.character_cell_size.borrow() { + let text_area_pixel_size_report = format!( + "\x1b[4;{};{}t", + character_cell_size.height * self.height, + character_cell_size.width * self.width + ); + self.pending_messages_to_pty + .push(text_area_pixel_size_report.as_bytes().to_vec()); + } + } + 16 => { + if let Some(character_cell_size) = *self.character_cell_size.borrow() { + let character_cell_size_report = format!( + "\x1b[6;{};{}t", + character_cell_size.height, character_cell_size.width + ); + self.pending_messages_to_pty + .push(character_cell_size_report.as_bytes().to_vec()); + } } 18 => { // report text area diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index fb6a0722..26b4ac30 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -16,6 +16,7 @@ use std::time::{self, Instant}; use zellij_tile::prelude::Style; use zellij_utils::pane_size::Offset; use zellij_utils::{ + pane_size::SizeInPixels, pane_size::{Dimension, PaneGeom}, position::Position, shared::make_terminal_title, @@ -487,6 +488,7 @@ impl TerminalPane { pane_index: usize, pane_name: String, link_handler: Rc>, + character_cell_size: Rc>>, ) -> TerminalPane { let initial_pane_title = format!("Pane #{}", pane_index); let grid = Grid::new( @@ -494,6 +496,7 @@ impl TerminalPane { position_and_size.cols.as_usize(), style.colors, link_handler, + character_cell_size, ); TerminalPane { frame: HashMap::new(), diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index 8112d896..468303b7 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -18,7 +18,7 @@ use std::time::Instant; use zellij_tile::data::ModeInfo; use zellij_utils::{ input::layout::Direction, - pane_size::{Offset, PaneGeom, Size, Viewport}, + pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport}, }; macro_rules! resize_pty { @@ -61,6 +61,7 @@ pub struct TiledPanes { connected_clients: Rc>>, connected_clients_in_app: Rc>>, mode_info: Rc>>, + character_cell_size: Rc>>, default_mode_info: ModeInfo, style: Style, session_is_mirrored: bool, @@ -79,6 +80,7 @@ impl TiledPanes { connected_clients: Rc>>, connected_clients_in_app: Rc>>, mode_info: Rc>>, + character_cell_size: Rc>>, session_is_mirrored: bool, draw_pane_frames: bool, default_mode_info: ModeInfo, @@ -92,6 +94,7 @@ impl TiledPanes { connected_clients, connected_clients_in_app, mode_info, + character_cell_size, default_mode_info, style, session_is_mirrored, @@ -106,12 +109,14 @@ impl TiledPanes { self.panes.insert(pane_id, pane); } pub fn insert_pane(&mut self, pane_id: PaneId, mut pane: Box) { + let cursor_height_width_ratio = self.cursor_height_width_ratio(); let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); - let pane_id_and_split_direction = pane_grid.find_room_for_new_pane(); + let pane_id_and_split_direction = + pane_grid.find_room_for_new_pane(cursor_height_width_ratio); if let Some((pane_id_to_split, split_direction)) = pane_id_and_split_direction { // this unwrap is safe because floating panes should not be visible if there are no floating panes let pane_to_split = self.panes.get_mut(&pane_id_to_split).unwrap(); @@ -125,12 +130,15 @@ impl TiledPanes { } } pub fn has_room_for_new_pane(&mut self) -> bool { + let cursor_height_width_ratio = self.cursor_height_width_ratio(); let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); - pane_grid.find_room_for_new_pane().is_some() + pane_grid + .find_room_for_new_pane(cursor_height_width_ratio) + .is_some() } pub fn fixed_pane_geoms(&self) -> Vec { self.panes @@ -528,6 +536,12 @@ impl TiledPanes { pane.set_active_at(Instant::now()); } } + pub fn cursor_height_width_ratio(&self) -> Option { + let character_cell_size = self.character_cell_size.borrow(); + character_cell_size.map(|size_in_pixels| { + (size_in_pixels.height as f64 / size_in_pixels.width as f64).round() as usize + }) + } pub fn move_focus_left(&mut self, client_id: ClientId) -> bool { match self.get_active_pane_id(client_id) { Some(active_pane_id) => { diff --git a/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs b/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs index 27acc1e5..0eeaeee7 100644 --- a/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs +++ b/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs @@ -13,7 +13,7 @@ use std::cell::RefCell; use std::rc::Rc; const RESIZE_PERCENT: f64 = 5.0; -const CURSOR_HEIGHT_WIDTH_RATIO: usize = 4; // this is not accurate and kind of a magic number, TODO: look into this +const DEFAULT_CURSOR_HEIGHT_WIDTH_RATIO: usize = 4; type BorderAndPaneIds = (usize, Vec); @@ -1606,7 +1606,10 @@ impl<'a> TiledPaneGrid<'a> { } false } - pub fn find_room_for_new_pane(&self) -> Option<(PaneId, Direction)> { + pub fn find_room_for_new_pane( + &self, + cursor_height_width_ratio: Option, + ) -> Option<(PaneId, Direction)> { let panes = self.panes.borrow(); let pane_sequence: Vec<(&PaneId, &&mut Box)> = panes.iter().filter(|(_, p)| p.selectable()).collect(); @@ -1614,8 +1617,9 @@ impl<'a> TiledPaneGrid<'a> { (0, None), |(current_largest_pane_size, current_pane_id_to_split), id_and_pane_to_check| { let (id_of_pane_to_check, pane_to_check) = id_and_pane_to_check; - let pane_size = - (pane_to_check.rows() * CURSOR_HEIGHT_WIDTH_RATIO) * pane_to_check.cols(); + let pane_size = (pane_to_check.rows() + * cursor_height_width_ratio.unwrap_or(DEFAULT_CURSOR_HEIGHT_WIDTH_RATIO)) + * pane_to_check.cols(); let pane_can_be_split = pane_to_check.cols() >= MIN_TERMINAL_WIDTH && pane_to_check.rows() >= MIN_TERMINAL_HEIGHT && ((pane_to_check.cols() > pane_to_check.min_width() * 2) @@ -1629,7 +1633,8 @@ impl<'a> TiledPaneGrid<'a> { ); pane_id_to_split.and_then(|t_id_to_split| { let pane_to_split = panes.get(t_id_to_split).unwrap(); - let direction = if pane_to_split.rows() * CURSOR_HEIGHT_WIDTH_RATIO + let direction = if pane_to_split.rows() + * cursor_height_width_ratio.unwrap_or(DEFAULT_CURSOR_HEIGHT_WIDTH_RATIO) > pane_to_split.cols() && pane_to_split.rows() > pane_to_split.min_height() * 2 { diff --git a/zellij-server/src/panes/unit/grid_tests.rs b/zellij-server/src/panes/unit/grid_tests.rs index a03849a4..2790c585 100644 --- a/zellij-server/src/panes/unit/grid_tests.rs +++ b/zellij-server/src/panes/unit/grid_tests.rs @@ -3,7 +3,7 @@ use crate::panes::link_handler::LinkHandler; use ::insta::assert_snapshot; use std::cell::RefCell; use std::rc::Rc; -use zellij_utils::{position::Position, vte, zellij_tile::data::Palette}; +use zellij_utils::{pane_size::SizeInPixels, position::Position, vte, zellij_tile::data::Palette}; fn read_fixture(fixture_name: &str) -> Vec { let mut path_to_file = std::path::PathBuf::new(); @@ -23,6 +23,7 @@ fn vttest1_0() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest1-0"; let content = read_fixture(fixture_name); @@ -40,6 +41,7 @@ fn vttest1_1() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest1-1"; let content = read_fixture(fixture_name); @@ -57,6 +59,7 @@ fn vttest1_2() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest1-2"; let content = read_fixture(fixture_name); @@ -74,6 +77,7 @@ fn vttest1_3() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest1-3"; let content = read_fixture(fixture_name); @@ -91,6 +95,7 @@ fn vttest1_4() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest1-4"; let content = read_fixture(fixture_name); @@ -108,6 +113,7 @@ fn vttest1_5() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest1-5"; let content = read_fixture(fixture_name); @@ -125,6 +131,7 @@ fn vttest2_0() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-0"; let content = read_fixture(fixture_name); @@ -142,6 +149,7 @@ fn vttest2_1() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-1"; let content = read_fixture(fixture_name); @@ -159,6 +167,7 @@ fn vttest2_2() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-2"; let content = read_fixture(fixture_name); @@ -176,6 +185,7 @@ fn vttest2_3() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-3"; let content = read_fixture(fixture_name); @@ -193,6 +203,7 @@ fn vttest2_4() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-4"; let content = read_fixture(fixture_name); @@ -210,6 +221,7 @@ fn vttest2_5() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-5"; let content = read_fixture(fixture_name); @@ -227,6 +239,7 @@ fn vttest2_6() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-6"; let content = read_fixture(fixture_name); @@ -244,6 +257,7 @@ fn vttest2_7() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-7"; let content = read_fixture(fixture_name); @@ -261,6 +275,7 @@ fn vttest2_8() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-8"; let content = read_fixture(fixture_name); @@ -278,6 +293,7 @@ fn vttest2_9() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-9"; let content = read_fixture(fixture_name); @@ -295,6 +311,7 @@ fn vttest2_10() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-10"; let content = read_fixture(fixture_name); @@ -312,6 +329,7 @@ fn vttest2_11() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-11"; let content = read_fixture(fixture_name); @@ -329,6 +347,7 @@ fn vttest2_12() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-12"; let content = read_fixture(fixture_name); @@ -346,6 +365,7 @@ fn vttest2_13() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-13"; let content = read_fixture(fixture_name); @@ -363,6 +383,7 @@ fn vttest2_14() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest2-14"; let content = read_fixture(fixture_name); @@ -380,6 +401,7 @@ fn vttest3_0() { 110, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest3-0"; let content = read_fixture(fixture_name); @@ -397,6 +419,7 @@ fn vttest8_0() { 97, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest8-0"; let content = read_fixture(fixture_name); @@ -414,6 +437,7 @@ fn vttest8_1() { 97, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest8-1"; let content = read_fixture(fixture_name); @@ -431,6 +455,7 @@ fn vttest8_2() { 97, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest8-2"; let content = read_fixture(fixture_name); @@ -448,6 +473,7 @@ fn vttest8_3() { 97, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest8-3"; let content = read_fixture(fixture_name); @@ -465,6 +491,7 @@ fn vttest8_4() { 97, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest8-4"; let content = read_fixture(fixture_name); @@ -482,6 +509,7 @@ fn vttest8_5() { 97, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vttest8-5"; let content = read_fixture(fixture_name); @@ -499,6 +527,7 @@ fn csi_b() { 97, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "csi-b"; let content = read_fixture(fixture_name); @@ -516,6 +545,7 @@ fn csi_capital_i() { 97, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "csi-capital-i"; let content = read_fixture(fixture_name); @@ -533,6 +563,7 @@ fn csi_capital_z() { 97, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "csi-capital-z"; let content = read_fixture(fixture_name); @@ -550,6 +581,7 @@ fn terminal_reports() { 97, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "terminal_reports"; let content = read_fixture(fixture_name); @@ -567,6 +599,7 @@ fn wide_characters() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "wide_characters"; let content = read_fixture(fixture_name); @@ -584,6 +617,7 @@ fn wide_characters_line_wrap() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "wide_characters_line_wrap"; let content = read_fixture(fixture_name); @@ -601,6 +635,7 @@ fn insert_character_in_line_with_wide_character() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "wide_characters_middle_line_insert"; let content = read_fixture(fixture_name); @@ -618,6 +653,7 @@ fn delete_char_in_middle_of_line_with_widechar() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "wide-chars-delete-middle"; let content = read_fixture(fixture_name); @@ -635,6 +671,7 @@ fn delete_char_in_middle_of_line_with_multiple_widechars() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "wide-chars-delete-middle-after-multi"; let content = read_fixture(fixture_name); @@ -652,6 +689,7 @@ fn fish_wide_characters_override_clock() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "fish_wide_characters_override_clock"; let content = read_fixture(fixture_name); @@ -669,6 +707,7 @@ fn bash_delete_wide_characters() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "bash_delete_wide_characters"; let content = read_fixture(fixture_name); @@ -686,6 +725,7 @@ fn delete_wide_characters_before_cursor() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "delete_wide_characters_before_cursor"; let content = read_fixture(fixture_name); @@ -703,6 +743,7 @@ fn delete_wide_characters_before_cursor_when_cursor_is_on_wide_character() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "delete_wide_characters_before_cursor_when_cursor_is_on_wide_character"; let content = read_fixture(fixture_name); @@ -720,6 +761,7 @@ fn delete_wide_character_under_cursor() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "delete_wide_character_under_cursor"; let content = read_fixture(fixture_name); @@ -737,6 +779,7 @@ fn replace_wide_character_under_cursor() { 104, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "replace_wide_character_under_cursor"; let content = read_fixture(fixture_name); @@ -754,6 +797,7 @@ fn wrap_wide_characters() { 90, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "wide_characters_full"; let content = read_fixture(fixture_name); @@ -771,6 +815,7 @@ fn wrap_wide_characters_on_size_change() { 93, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "wide_characters_full"; let content = read_fixture(fixture_name); @@ -789,6 +834,7 @@ fn unwrap_wide_characters_on_size_change() { 93, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "wide_characters_full"; let content = read_fixture(fixture_name); @@ -808,6 +854,7 @@ fn wrap_wide_characters_in_the_middle_of_the_line() { 91, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "wide_characters_line_middle"; let content = read_fixture(fixture_name); @@ -825,6 +872,7 @@ fn wrap_wide_characters_at_the_end_of_the_line() { 90, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "wide_characters_line_end"; let content = read_fixture(fixture_name); @@ -842,6 +890,7 @@ fn copy_selected_text_from_viewport() { 125, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "grid_copy"; let content = read_fixture(fixture_name); @@ -867,6 +916,7 @@ fn copy_wrapped_selected_text_from_viewport() { 73, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "grid_copy_wrapped"; let content = read_fixture(fixture_name); @@ -891,6 +941,7 @@ fn copy_selected_text_from_lines_above() { 125, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "grid_copy"; let content = read_fixture(fixture_name); @@ -916,6 +967,7 @@ fn copy_selected_text_from_lines_below() { 125, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "grid_copy"; let content = read_fixture(fixture_name); @@ -949,6 +1001,7 @@ fn run_bandwhich_from_fish_shell() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "fish_and_bandwhich"; let content = read_fixture(fixture_name); @@ -966,6 +1019,7 @@ fn fish_tab_completion_options() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "fish_tab_completion_options"; let content = read_fixture(fixture_name); @@ -988,6 +1042,7 @@ pub fn fish_select_tab_completion_options() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "fish_select_tab_completion_options"; let content = read_fixture(fixture_name); @@ -1013,6 +1068,7 @@ pub fn vim_scroll_region_down() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vim_scroll_region_down"; let content = read_fixture(fixture_name); @@ -1036,6 +1092,7 @@ pub fn vim_ctrl_d() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vim_ctrl_d"; let content = read_fixture(fixture_name); @@ -1058,6 +1115,7 @@ pub fn vim_ctrl_u() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vim_ctrl_u"; let content = read_fixture(fixture_name); @@ -1075,6 +1133,7 @@ pub fn htop() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "htop"; let content = read_fixture(fixture_name); @@ -1092,6 +1151,7 @@ pub fn htop_scrolling() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "htop_scrolling"; let content = read_fixture(fixture_name); @@ -1109,6 +1169,7 @@ pub fn htop_right_scrolling() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "htop_right_scrolling"; let content = read_fixture(fixture_name); @@ -1134,6 +1195,7 @@ pub fn vim_overwrite() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "vim_overwrite"; let content = read_fixture(fixture_name); @@ -1153,6 +1215,7 @@ pub fn clear_scroll_region() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "clear_scroll_region"; let content = read_fixture(fixture_name); @@ -1170,6 +1233,7 @@ pub fn display_tab_characters_properly() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "tab_characters"; let content = read_fixture(fixture_name); @@ -1187,6 +1251,7 @@ pub fn neovim_insert_mode() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "nvim_insert"; let content = read_fixture(fixture_name); @@ -1204,6 +1269,7 @@ pub fn bash_cursor_linewrap() { 116, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "bash_cursor_linewrap"; let content = read_fixture(fixture_name); @@ -1223,6 +1289,7 @@ pub fn fish_paste_multiline() { 149, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "fish_paste_multiline"; let content = read_fixture(fixture_name); @@ -1240,6 +1307,7 @@ pub fn git_log() { 149, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "git_log"; let content = read_fixture(fixture_name); @@ -1259,6 +1327,7 @@ pub fn git_diff_scrollup() { 149, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "git_diff_scrollup"; let content = read_fixture(fixture_name); @@ -1276,6 +1345,7 @@ pub fn emacs_longbuf() { 284, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "emacs_longbuf_tutorial"; let content = read_fixture(fixture_name); @@ -1293,6 +1363,7 @@ pub fn top_and_quit() { 235, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "top_and_quit"; let content = read_fixture(fixture_name); @@ -1316,6 +1387,7 @@ pub fn exa_plus_omf_theme() { 235, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "exa_plus_omf_theme"; let content = read_fixture(fixture_name); @@ -1333,6 +1405,7 @@ pub fn scroll_up() { 50, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); @@ -1351,6 +1424,7 @@ pub fn scroll_down() { 50, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); @@ -1370,6 +1444,7 @@ pub fn scroll_up_with_line_wraps() { 25, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); @@ -1388,6 +1463,7 @@ pub fn scroll_down_with_line_wraps() { 25, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); @@ -1407,6 +1483,7 @@ pub fn scroll_up_decrease_width_and_scroll_down() { 50, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); @@ -1431,6 +1508,7 @@ pub fn scroll_up_increase_width_and_scroll_down() { 25, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); @@ -1455,6 +1533,7 @@ pub fn move_cursor_below_scroll_region() { 114, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "move_cursor_below_scroll_region"; let content = read_fixture(fixture_name); @@ -1472,6 +1551,7 @@ pub fn insert_wide_characters_in_existing_line() { 86, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "chinese_characters_line_middle"; let content = read_fixture(fixture_name); @@ -1494,6 +1574,7 @@ pub fn full_screen_scroll_region_and_scroll_up() { 80, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "scroll_region_full_screen"; let content = read_fixture(fixture_name); @@ -1514,6 +1595,7 @@ pub fn ring_bell() { 64, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "ring_bell"; let content = read_fixture(fixture_name); @@ -1531,6 +1613,7 @@ pub fn alternate_screen_change_size() { 20, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "alternate_screen_change_size"; let content = read_fixture(fixture_name); @@ -1552,6 +1635,7 @@ pub fn fzf_fullscreen() { 112, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "fzf_fullscreen"; let content = read_fixture(fixture_name); @@ -1573,6 +1657,7 @@ pub fn replace_multiple_wide_characters_under_cursor() { 112, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "replace_multiple_wide_characters"; let content = read_fixture(fixture_name); @@ -1594,6 +1679,7 @@ pub fn replace_non_wide_characters_with_wide_characters() { 112, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "replace_non_wide_characters_with_wide_characters"; let content = read_fixture(fixture_name); @@ -1611,6 +1697,7 @@ pub fn scroll_down_ansi() { 112, Palette::default(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let fixture_name = "scroll_down"; let content = read_fixture(fixture_name); @@ -1619,3 +1706,55 @@ pub fn scroll_down_ansi() { } assert_snapshot!(format!("{:?}", grid)); } + +#[test] +fn terminal_pixel_size_reports() { + let mut vte_parser = vte::Parser::new(); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(Some(SizeInPixels { + height: 21, + width: 8, + }))), + ); + let fixture_name = "terminal_pixel_size_reports"; + let content = read_fixture(fixture_name); + for byte in content { + vte_parser.advance(&mut grid, byte); + } + assert_eq!( + grid.pending_messages_to_pty + .iter() + .map(|bytes| String::from_utf8(bytes.clone()).unwrap()) + .collect::>(), + vec!["\x1b[4;1071;776t", "\x1b[6;21;8t"] + ); +} + +#[test] +fn terminal_pixel_size_reports_in_unsupported_terminals() { + let mut vte_parser = vte::Parser::new(); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), // in an unsupported terminal, we don't have this info + ); + let fixture_name = "terminal_pixel_size_reports"; + let content = read_fixture(fixture_name); + for byte in content { + vte_parser.advance(&mut grid, byte); + } + let expected: Vec = vec![]; + assert_eq!( + grid.pending_messages_to_pty + .iter() + .map(|bytes| String::from_utf8(bytes.clone()).unwrap()) + .collect::>(), + expected, + ); +} diff --git a/zellij-server/src/panes/unit/terminal_pane_tests.rs b/zellij-server/src/panes/unit/terminal_pane_tests.rs index adcdbd08..f03f170c 100644 --- a/zellij-server/src/panes/unit/terminal_pane_tests.rs +++ b/zellij-server/src/panes/unit/terminal_pane_tests.rs @@ -25,6 +25,7 @@ pub fn scrolling_inside_a_pane() { 0, String::new(), Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); // 0 is the pane index let mut text_to_fill_pane = String::new(); for i in 0..30 { diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index e5489818..dfc9ab29 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -430,6 +430,14 @@ pub(crate) fn route_thread_main( .send_to_screen(ScreenInstruction::TerminalResize(min_size)) .unwrap(); } + ClientToServerMsg::TerminalPixelDimensions(pixel_dimensions) => { + rlocked_sessions + .as_ref() + .unwrap() + .senders + .send_to_screen(ScreenInstruction::TerminalPixelDimensions(pixel_dimensions)) + .unwrap(); + } ClientToServerMsg::NewClient( client_attributes, cli_args, diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index e5ad0edd..ea36ea5f 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -8,7 +8,7 @@ use std::str; use zellij_tile::prelude::Style; use zellij_utils::input::options::Clipboard; -use zellij_utils::pane_size::Size; +use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::{ input::command::TerminalAction, input::layout::Layout, position::Position, zellij_tile, }; @@ -27,7 +27,7 @@ use zellij_tile::data::{Event, InputMode, ModeInfo, PluginCapabilities, TabInfo} use zellij_utils::{ errors::{ContextType, ScreenContext}, input::{get_mode_info, options::Options}, - ipc::ClientAttributes, + ipc::{ClientAttributes, PixelDimensions}, }; /// Instructions that can be sent to the [`Screen`]. @@ -87,6 +87,7 @@ pub enum ScreenInstruction { ToggleTab(ClientId), UpdateTabName(Vec, ClientId), TerminalResize(Size), + TerminalPixelDimensions(PixelDimensions), ChangeMode(ModeInfo, ClientId), LeftClick(Position, ClientId), RightClick(Position, ClientId), @@ -162,6 +163,9 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::GoToTab(..) => ScreenContext::GoToTab, ScreenInstruction::UpdateTabName(..) => ScreenContext::UpdateTabName, ScreenInstruction::TerminalResize(..) => ScreenContext::TerminalResize, + ScreenInstruction::TerminalPixelDimensions(..) => { + ScreenContext::TerminalPixelDimensions + } ScreenInstruction::ChangeMode(..) => ScreenContext::ChangeMode, ScreenInstruction::ToggleActiveSyncTab(..) => ScreenContext::ToggleActiveSyncTab, ScreenInstruction::ScrollUpAt(..) => ScreenContext::ScrollUpAt, @@ -193,6 +197,8 @@ pub(crate) struct Screen { tabs: BTreeMap, /// The full size of this [`Screen`]. size: Size, + pixel_dimensions: PixelDimensions, + character_cell_size: Rc>>, /// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`] overlay: OverlayWindow, connected_clients: Rc>>, @@ -225,6 +231,8 @@ impl Screen { bus, max_panes, size: client_attributes.size, + pixel_dimensions: Default::default(), + character_cell_size: Rc::new(RefCell::new(None)), style: client_attributes.style, connected_clients: Rc::new(RefCell::new(HashSet::new())), active_tab_indices: BTreeMap::new(), @@ -434,6 +442,20 @@ impl Screen { } self.render(); } + pub fn update_pixel_dimensions(&mut self, pixel_dimensions: PixelDimensions) { + self.pixel_dimensions.merge(pixel_dimensions); + if let Some(character_cell_size) = self.pixel_dimensions.character_cell_size { + *self.character_cell_size.borrow_mut() = Some(character_cell_size); + } else if let Some(text_area_size) = self.pixel_dimensions.text_area_size { + let character_cell_size_height = text_area_size.height / self.size.rows; + let character_cell_size_width = text_area_size.width / self.size.cols; + let character_cell_size = SizeInPixels { + height: character_cell_size_height, + width: character_cell_size_width, + }; + *self.character_cell_size.borrow_mut() = Some(character_cell_size); + } + } /// Renders this [`Screen`], which amounts to rendering its active [`Tab`]. pub fn render(&mut self) { @@ -514,6 +536,7 @@ impl Screen { position, String::new(), self.size, + self.character_cell_size.clone(), self.bus.os_input.as_ref().unwrap().clone(), self.bus.senders.clone(), self.max_panes, @@ -1254,6 +1277,9 @@ pub(crate) fn screen_thread_main( screen.render(); } + ScreenInstruction::TerminalPixelDimensions(pixel_dimensions) => { + screen.update_pixel_dimensions(pixel_dimensions); + } ScreenInstruction::ChangeMode(mode_info, client_id) => { screen.change_mode(mode_info, client_id); diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index baa69175..99e9e2f4 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -40,7 +40,7 @@ use zellij_utils::{ layout::{Layout, Run}, parse_keys, }, - pane_size::{Offset, PaneGeom, Size, Viewport}, + pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport}, }; macro_rules! resize_pty { @@ -72,6 +72,7 @@ pub(crate) struct Tab { max_panes: Option, viewport: Rc>, // includes all non-UI panes display_area: Rc>, // includes all panes (including eg. the status bar and tab bar in the default layout) + character_cell_size: Rc>>, os_api: Box, pub senders: ThreadSenders, synchronize_is_active: bool, @@ -270,6 +271,7 @@ impl Tab { position: usize, name: String, display_area: Size, + character_cell_size: Rc>>, os_api: Box, senders: ThreadSenders, max_panes: Option, @@ -302,6 +304,7 @@ impl Tab { connected_clients.clone(), connected_clients_in_app.clone(), mode_info.clone(), + character_cell_size.clone(), session_is_mirrored, draw_pane_frames, default_mode_info.clone(), @@ -333,6 +336,7 @@ impl Tab { max_panes, viewport, display_area, + character_cell_size, synchronize_is_active: false, os_api, senders, @@ -413,6 +417,7 @@ impl Tab { next_terminal_position, layout.pane_name.clone().unwrap_or_default(), self.link_handler.clone(), + self.character_cell_size.clone(), ); new_pane.set_borderless(layout.borderless); self.tiled_panes @@ -640,6 +645,7 @@ impl Tab { next_terminal_position, String::new(), self.link_handler.clone(), + self.character_cell_size.clone(), ); new_pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame resize_pty!(new_pane, self.os_api); @@ -661,6 +667,7 @@ impl Tab { next_terminal_position, String::new(), self.link_handler.clone(), + self.character_cell_size.clone(), ); self.tiled_panes.insert_pane(pid, Box::new(new_terminal)); self.should_clear_display_before_rendering = true; @@ -689,6 +696,7 @@ impl Tab { next_terminal_position, String::new(), self.link_handler.clone(), + self.character_cell_size.clone(), ); self.tiled_panes .split_pane_horizontally(pid, Box::new(new_terminal), client_id); @@ -715,6 +723,7 @@ impl Tab { next_terminal_position, String::new(), self.link_handler.clone(), + self.character_cell_size.clone(), ); self.tiled_panes .split_pane_vertically(pid, Box::new(new_terminal), client_id); diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 79199c95..76ab9cae 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -103,12 +103,14 @@ fn create_new_tab(size: Size) -> Tab { connected_clients.insert(client_id); let connected_clients = Rc::new(RefCell::new(connected_clients)); let copy_command = None; + let character_cell_info = Rc::new(RefCell::new(None)); let clipboard = Clipboard::default(); let mut tab = Tab::new( index, position, name, size, + character_cell_info, os_api, senders, max_panes, @@ -151,6 +153,7 @@ fn take_snapshot(ansi_instructions: &str, rows: usize, columns: usize, palette: columns, palette, Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let mut vte_parser = vte::Parser::new(); for &byte in ansi_instructions.as_bytes() { @@ -171,6 +174,7 @@ fn take_snapshot_and_cursor_position( columns, palette, Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), ); let mut vte_parser = vte::Parser::new(); for &byte in ansi_instructions.as_bytes() { diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index 1a368f20..44de0842 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -12,7 +12,7 @@ use zellij_tile::prelude::Style; use zellij_utils::input::layout::LayoutTemplate; use zellij_utils::input::options::Clipboard; use zellij_utils::ipc::IpcReceiverWithContext; -use zellij_utils::pane_size::Size; +use zellij_utils::pane_size::{Size, SizeInPixels}; use std::cell::RefCell; use std::collections::HashSet; @@ -96,6 +96,7 @@ fn create_new_tab(size: Size) -> Tab { let client_id = 1; let session_is_mirrored = true; let mut connected_clients = HashSet::new(); + let character_cell_info = Rc::new(RefCell::new(None)); connected_clients.insert(client_id); let connected_clients = Rc::new(RefCell::new(connected_clients)); let copy_command = None; @@ -105,6 +106,54 @@ fn create_new_tab(size: Size) -> Tab { position, name, size, + character_cell_info, + os_api, + senders, + max_panes, + style, + mode_info, + draw_pane_frames, + connected_clients, + session_is_mirrored, + client_id, + copy_command, + copy_clipboard, + ); + tab.apply_layout( + LayoutTemplate::default().try_into().unwrap(), + vec![1], + index, + client_id, + ); + tab +} + +fn create_new_tab_with_cell_size( + size: Size, + character_cell_size: Rc>>, +) -> Tab { + let index = 0; + let position = 0; + let name = String::new(); + let os_api = Box::new(FakeInputOutput {}); + let senders = ThreadSenders::default().silently_fail_on_send(); + let max_panes = None; + let mode_info = ModeInfo::default(); + let style = Style::default(); + let draw_pane_frames = true; + let client_id = 1; + let session_is_mirrored = true; + let mut connected_clients = HashSet::new(); + connected_clients.insert(client_id); + let connected_clients = Rc::new(RefCell::new(connected_clients)); + let copy_command = None; + let copy_clipboard = Clipboard::default(); + let mut tab = Tab::new( + index, + position, + name, + size, + character_cell_size, os_api, senders, max_panes, @@ -13897,3 +13946,28 @@ pub fn nondirectional_resize_increase_with_pane_above_aligned_right_with_current "Pane 3 col count" ); } + +#[test] +pub fn custom_cursor_height_width_ratio() { + let size = Size { + cols: 121, + rows: 20, + }; + let character_cell_size = Rc::new(RefCell::new(None)); + let tab = create_new_tab_with_cell_size(size, character_cell_size.clone()); + let initial_cursor_height_width_ratio = tab.tiled_panes.cursor_height_width_ratio(); + *character_cell_size.borrow_mut() = Some(SizeInPixels { + height: 10, + width: 4, + }); + let cursor_height_width_ratio_after_update = tab.tiled_panes.cursor_height_width_ratio(); + assert_eq!( + initial_cursor_height_width_ratio, None, + "initially no ratio " + ); + assert_eq!( + cursor_height_width_ratio_after_update, + Some(3), + "ratio updated successfully" + ); // 10 / 4 == 2.5, rounded: 3 +} diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index cf9b89b5..333d85b0 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -12,11 +12,11 @@ use zellij_utils::input::command::TerminalAction; use zellij_utils::input::layout::LayoutTemplate; use zellij_utils::input::options::Clipboard; use zellij_utils::ipc::IpcReceiverWithContext; -use zellij_utils::pane_size::Size; +use zellij_utils::pane_size::{Size, SizeInPixels}; use std::os::unix::io::RawFd; -use zellij_utils::ipc::ClientAttributes; +use zellij_utils::ipc::{ClientAttributes, PixelDimensions}; use zellij_utils::nix; use zellij_utils::{ @@ -464,3 +464,81 @@ fn switch_to_tab_with_fullscreen() { "Active pane is still the fullscreen pane" ); } + +#[test] +fn update_screen_pixel_dimensions() { + let size = Size { + cols: 121, + rows: 20, + }; + let mut screen = create_new_screen(size); + let initial_pixel_dimensions = screen.pixel_dimensions; + screen.update_pixel_dimensions(PixelDimensions { + character_cell_size: Some(SizeInPixels { + height: 10, + width: 5, + }), + text_area_size: None, + }); + let pixel_dimensions_after_first_update = screen.pixel_dimensions; + screen.update_pixel_dimensions(PixelDimensions { + character_cell_size: None, + text_area_size: Some(SizeInPixels { + height: 100, + width: 50, + }), + }); + let pixel_dimensions_after_second_update = screen.pixel_dimensions; + screen.update_pixel_dimensions(PixelDimensions { + character_cell_size: None, + text_area_size: None, + }); + let pixel_dimensions_after_third_update = screen.pixel_dimensions; + assert_eq!( + initial_pixel_dimensions, + PixelDimensions { + character_cell_size: None, + text_area_size: None + }, + "Initial pixel dimensions empty" + ); + assert_eq!( + pixel_dimensions_after_first_update, + PixelDimensions { + character_cell_size: Some(SizeInPixels { + height: 10, + width: 5 + }), + text_area_size: None + }, + "character_cell_size updated properly", + ); + assert_eq!( + pixel_dimensions_after_second_update, + PixelDimensions { + character_cell_size: Some(SizeInPixels { + height: 10, + width: 5 + }), + text_area_size: Some(SizeInPixels { + height: 100, + width: 50, + }), + }, + "text_area_size updated properly without overriding character_cell_size", + ); + assert_eq!( + pixel_dimensions_after_third_update, + PixelDimensions { + character_cell_size: Some(SizeInPixels { + height: 10, + width: 5 + }), + text_area_size: Some(SizeInPixels { + height: 100, + width: 50, + }), + }, + "empty update does not delete existing data", + ); +} diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index 1cae4a70..8ef514df 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -37,6 +37,7 @@ log = "0.4.16" log4rs = "1.0.0" unicode-width = "0.1.8" miette = { version = "3.3.0", features = ["fancy"] } +regex = "1.5.5" termwiz = "0.16.0" diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index d1ab1426..4dca51fd 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -267,6 +267,7 @@ pub enum ScreenContext { GoToTab, UpdateTabName, TerminalResize, + TerminalPixelDimensions, ChangeMode, LeftClick, RightClick, diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index 05bece84..3d95cd83 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -4,7 +4,7 @@ use crate::{ cli::CliArgs, errors::{get_current_ctx, ErrorContext}, input::{actions::Action, layout::LayoutFromYaml, options::Options, plugins::PluginsConfig}, - pane_size::Size, + pane_size::{Size, SizeInPixels}, }; use interprocess::local_socket::LocalSocketStream; use nix::unistd::dup; @@ -43,6 +43,23 @@ pub struct ClientAttributes { pub style: Style, } +#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +pub struct PixelDimensions { + pub text_area_size: Option, + pub character_cell_size: Option, +} + +impl PixelDimensions { + pub fn merge(&mut self, other: PixelDimensions) { + if let Some(text_area_size) = other.text_area_size { + self.text_area_size = Some(text_area_size); + } + if let Some(character_cell_size) = other.character_cell_size { + self.character_cell_size = Some(character_cell_size); + } + } +} + // Types of messages sent from the client to the server #[allow(clippy::large_enum_variant)] #[derive(Serialize, Deserialize, Debug, Clone)] @@ -57,6 +74,7 @@ pub enum ClientToServerMsg { DetachSession(SessionId), // Disconnect from the session we're connected to DisconnectFromSession,*/ + TerminalPixelDimensions(PixelDimensions), TerminalResize(Size), NewClient( ClientAttributes, diff --git a/zellij-utils/src/lib.rs b/zellij-utils/src/lib.rs index 5e42edf1..2a635867 100644 --- a/zellij-utils/src/lib.rs +++ b/zellij-utils/src/lib.rs @@ -15,8 +15,10 @@ pub use anyhow; pub use async_std; pub use clap; pub use interprocess; +pub use lazy_static; pub use libc; pub use nix; +pub use regex; pub use serde; pub use serde_yaml; pub use signal_hook; diff --git a/zellij-utils/src/pane_size.rs b/zellij-utils/src/pane_size.rs index f95945a4..2a9f54c0 100644 --- a/zellij-utils/src/pane_size.rs +++ b/zellij-utils/src/pane_size.rs @@ -34,6 +34,12 @@ pub struct Size { pub cols: usize, } +#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct SizeInPixels { + pub height: usize, + pub width: usize, +} + #[derive(Eq, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] pub struct Dimension { pub constraint: Constraint,