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 | 61deca80edb1632eb8ca22f627c6001b757021dc (diff) |
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 | 342 | ||||
-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, 687 insertions, 947 deletions
diff --git a/zellij-client/src/fake_client.rs b/zellij-client/src/fake_client.rs index 7338d633a..88b41c9e8 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 3f92c5376..5f8cc0639 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 9790984ee..3b81694b4 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 60c22cc98..f9f3c590b 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 25f0e4fbb..09247bf9b 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(), - )); - } - 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.push(color_registers); + } + 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(AnsiStdinInstru |