diff options
author | Aram Drevekenin <aram@poor.dev> | 2023-02-17 19:32:07 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-17 19:32:07 +0100 |
commit | 3a0e56afd821c725f0eaac12d8c56df96686ee38 (patch) | |
tree | 330b37d666f1b13a7bfb20d84823cf22bcb9d36f /zellij-client | |
parent | 5235407a5b3eff0e67db83b650066e7c71c68dba (diff) |
fix(ux): cache stdin queries on startup (remove startup delay) (#2173)
* fix(ux): cache stdin queries on startup
* style(fmt): rustfmt
Diffstat (limited to 'zellij-client')
-rw-r--r-- | zellij-client/Cargo.toml | 1 | ||||
-rw-r--r-- | zellij-client/src/lib.rs | 14 | ||||
-rw-r--r-- | zellij-client/src/stdin_ansi_parser.rs | 53 | ||||
-rw-r--r-- | zellij-client/src/stdin_handler.rs | 53 | ||||
-rw-r--r-- | zellij-client/src/unit/stdin_tests.rs | 95 |
5 files changed, 80 insertions, 136 deletions
diff --git a/zellij-client/Cargo.toml b/zellij-client/Cargo.toml index 114257b8e..f8d6045a1 100644 --- a/zellij-client/Cargo.toml +++ b/zellij-client/Cargo.toml @@ -13,6 +13,7 @@ mio = { version = "0.7.11", features = ['os-ext'] } serde = { version = "1.0", features = ["derive"] } url = { version = "2.2.2", features = ["serde"] } serde_yaml = "0.8" +serde_json = "1.0" zellij-utils = { path = "../zellij-utils/", version = "0.34.5" } log = "0.4.17" diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 51d6c99d6..67a7fb902 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -265,16 +265,6 @@ pub fn start_client( os_api.send_to_server(ClientToServerMsg::TerminalResize( os_api.get_terminal_size_using_fd(0), )); - // 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({ @@ -348,7 +338,7 @@ pub fn start_client( let mut stdout = os_input.get_stdout_writer(); stdout - .write_all("\u{1b}[1mLoading Zellij\u{1b}[m".as_bytes()) + .write_all("\u{1b}[1mLoading Zellij\u{1b}[m\n\r".as_bytes()) .expect("cannot write to stdout"); stdout.flush().expect("could not flush"); @@ -368,7 +358,7 @@ pub fn start_client( match client_instruction { ClientInstruction::StartedParsingStdinQuery => { stdout - .write_all("\n\rQuerying terminal emulator for \u{1b}[32;1mdefault colors\u{1b}[m and \u{1b}[32;1mpixel/cell\u{1b}[m ratio...".as_bytes()) + .write_all("Querying terminal emulator for \u{1b}[32;1mdefault colors\u{1b}[m and \u{1b}[32;1mpixel/cell\u{1b}[m ratio...".as_bytes()) .expect("cannot write to stdout"); stdout.flush().expect("could not flush"); }, diff --git a/zellij-client/src/stdin_ansi_parser.rs b/zellij-client/src/stdin_ansi_parser.rs index 23e869b2b..4891c4f73 100644 --- a/zellij-client/src/stdin_ansi_parser.rs +++ b/zellij-client/src/stdin_ansi_parser.rs @@ -1,11 +1,17 @@ use std::time::{Duration, Instant}; +use zellij_utils::consts::{VERSION, ZELLIJ_CACHE_DIR}; const STARTUP_PARSE_DEADLINE_MS: u64 = 500; -const SIGWINCH_PARSE_DEADLINE_MS: u64 = 200; use zellij_utils::{ ipc::PixelDimensions, lazy_static::lazy_static, pane_size::SizeInPixels, regex::Regex, }; +use serde::{Deserialize, Serialize}; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::PathBuf; +use zellij_utils::anyhow::Result; + #[derive(Debug)] pub struct StdinAnsiParser { raw_buffer: Vec<u8>, @@ -43,18 +49,6 @@ impl StdinAnsiParser { Some(Instant::now() + Duration::from_millis(STARTUP_PARSE_DEADLINE_MS)); query_string } - 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 - } fn drain_pending_events(&mut self) -> Vec<AnsiStdinInstruction> { let mut events = vec![]; events.append(&mut self.pending_events); @@ -82,6 +76,34 @@ impl StdinAnsiParser { } self.drain_pending_events() } + pub fn read_cache(&self) -> Option<Vec<AnsiStdinInstruction>> { + let path = self.cache_dir_path(); + match OpenOptions::new().read(true).open(path.as_path()) { + Ok(mut file) => { + let mut json_cache = String::new(); + file.read_to_string(&mut json_cache).ok()?; + let instructions = + serde_json::from_str::<Vec<AnsiStdinInstruction>>(&json_cache).ok()?; + if instructions.is_empty() { + None + } else { + Some(instructions) + } + }, + Err(e) => { + log::error!("Failed to open STDIN cache file: {:?}", e); + None + }, + } + } + pub fn write_cache(&self, events: Vec<AnsiStdinInstruction>) { + let path = self.cache_dir_path(); + if let Ok(serialized_events) = serde_json::to_string(&events) { + if let Ok(mut file) = File::create(path.as_path()) { + let _ = file.write_all(serialized_events.as_bytes()); + } + }; + } fn parse_byte(&mut self, byte: u8) { if byte == b't' { self.raw_buffer.push(byte); @@ -112,9 +134,12 @@ impl StdinAnsiParser { self.raw_buffer.push(byte); } } + fn cache_dir_path(&self) -> PathBuf { + ZELLIJ_CACHE_DIR.join(&format!("zellij-stdin-cache-v{}", VERSION)) + } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum AnsiStdinInstruction { PixelDimensions(PixelDimensions), BackgroundColor(String), diff --git a/zellij-client/src/stdin_handler.rs b/zellij-client/src/stdin_handler.rs index 75634a05b..01f1ab232 100644 --- a/zellij-client/src/stdin_handler.rs +++ b/zellij-client/src/stdin_handler.rs @@ -27,21 +27,37 @@ pub(crate) fn stdin_loop( let mut holding_mouse = false; let mut input_parser = InputParser::new(); let mut current_buffer = vec![]; - // on startup we send a query to the terminal emulator for stuff like the pixel size and colors - // we get a response through STDIN, so it makes sense to do this here - send_input_instructions - .send(InputInstruction::StartedParsing) - .unwrap(); - let terminal_emulator_query_string = stdin_ansi_parser - .lock() - .unwrap() - .terminal_emulator_query_string(); - let _ = os_input - .get_stdout_writer() - .write(terminal_emulator_query_string.as_bytes()) - .unwrap(); - let query_duration = stdin_ansi_parser.lock().unwrap().startup_query_duration(); - send_done_parsing_after_query_timeout(send_input_instructions.clone(), query_duration); + { + // on startup we send a query to the terminal emulator for stuff like the pixel size and colors + // we get a response through STDIN, so it makes sense to do this here + let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap(); + match stdin_ansi_parser.read_cache() { + Some(events) => { + let _ = + send_input_instructions.send(InputInstruction::AnsiStdinInstructions(events)); + let _ = send_input_instructions + .send(InputInstruction::DoneParsing) + .unwrap(); + }, + None => { + send_input_instructions + .send(InputInstruction::StartedParsing) + .unwrap(); + let terminal_emulator_query_string = + stdin_ansi_parser.terminal_emulator_query_string(); + let _ = os_input + .get_stdout_writer() + .write(terminal_emulator_query_string.as_bytes()) + .unwrap(); + let query_duration = stdin_ansi_parser.startup_query_duration(); + send_done_parsing_after_query_timeout( + send_input_instructions.clone(), + query_duration, + ); + }, + } + } + let mut ansi_stdin_events = vec![]; loop { let buf = os_input.read_from_stdin(); { @@ -54,12 +70,19 @@ pub(crate) fn stdin_loop( if stdin_ansi_parser.should_parse() { let events = stdin_ansi_parser.parse(buf); if !events.is_empty() { + ansi_stdin_events.append(&mut events.clone()); let _ = send_input_instructions .send(InputInstruction::AnsiStdinInstructions(events)); } continue; } } + if !ansi_stdin_events.is_empty() { + stdin_ansi_parser + .lock() + .unwrap() + .write_cache(ansi_stdin_events.drain(..).collect()); + } current_buffer.append(&mut buf.to_vec()); let maybe_more = false; // read_from_stdin should (hopefully) always empty the STDIN buffer completely let mut events = vec![]; diff --git a/zellij-client/src/unit/stdin_tests.rs b/zellij-client/src/unit/stdin_tests.rs index c55283964..6bdd489bc 100644 --- a/zellij-client/src/unit/stdin_tests.rs +++ b/zellij-client/src/unit/stdin_tests.rs @@ -317,98 +317,3 @@ pub fn move_focus_left_in_normal_mode() { "All actions sent to server properly" ); } - -#[test] -pub fn terminal_info_queried_from_terminal_emulator() { - let events_sent_to_server = Arc::new(Mutex::new(vec![])); - let command_is_executing = CommandIsExecuting::new(); - let client_os_api = FakeClientOsApi::new(events_sent_to_server, command_is_executing); - - let client_os_api_clone = client_os_api.clone(); - let (send_input_instructions, _receive_input_instructions): ChannelWithContext< - InputInstruction, - > = channels::bounded(50); - let send_input_instructions = SenderWithContext::new(send_input_instructions); - let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new())); - - let stdin_thread = thread::Builder::new() - .name("stdin_handler".to_string()) - .spawn({ - move || { - stdin_loop( - Box::new(client_os_api), - send_input_instructions, - stdin_ansi_parser, - ) - } - }); - std::thread::sleep(std::time::Duration::from_millis(500)); // wait for initial query to be sent - - let extracted_stdout_buffer = client_os_api_clone.stdout_buffer(); - let mut expected_query = - String::from("\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}"); - for i in 0..256 { - expected_query.push_str(&format!("\u{1b}]4;{};?\u{1b}\u{5c}", i)); - } - assert_eq!( - String::from_utf8(extracted_stdout_buffer), - Ok(expected_query), - ); - drop(stdin_thread); -} - -#[test] -pub fn pixel_info_sent_to_server() { - let fake_stdin_buffer = read_fixture("terminal_emulator_startup_response"); - let events_sent_to_server = Arc::new(Mutex::new(vec![])); - let command_is_executing = CommandIsExecuting::new(); - let client_os_api = - FakeClientOsApi::new(events_sent_to_server.clone(), command_is_executing.clone()) - .with_stdin_buffer(fake_stdin_buffer); - let config = Config::from_default_assets().unwrap(); - let options = Options::default(); - - let (send_client_instructions, _receive_client_instructions): ChannelWithContext< - ClientInstruction, - > = channels::bounded(50); - let send_client_instructions = SenderWithContext::new(send_client_instructions); - - let (send_input_instructions, receive_input_instructions): ChannelWithContext< - InputInstruction, - > = channels::bounded(50); - let send_input_instructions = SenderWithContext::new(send_input_instructions); - let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new())); - let stdin_thread = thread::Builder::new() - .name("stdin_handler".to_string()) - .spawn({ - let client_os_api = client_os_api.clone(); - move || { - stdin_loop( - Box::new(client_os_api), - send_input_instructions, - stdin_ansi_parser, - ) - } - }); - - let default_mode = InputMode::Normal; - let input_thread = thread::Builder::new() - .name("input_handler".to_string()) - .spawn({ - move || { - input_loop( - Box::new(client_os_api), - config, - options, - command_is_executing, - send_client_instructions, - default_mode, - receive_input_instructions, - ) - } - }); - std::thread::sleep(std::time::Duration::from_millis(1000)); // wait for initial query to be sent - assert_snapshot!(*format!("{:?}", events_sent_to_server.lock().unwrap())); - drop(stdin_thread); - drop(input_thread); -} |