fix(ux): cache stdin queries on startup (remove startup delay) (#2173)
* fix(ux): cache stdin queries on startup * style(fmt): rustfmt
This commit is contained in:
parent
5235407a5b
commit
3a0e56afd8
7 changed files with 88 additions and 140 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4017,6 +4017,7 @@ dependencies = [
|
|||
"log",
|
||||
"mio",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"url",
|
||||
"zellij-utils",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
.lock()
|
||||
.unwrap()
|
||||
.terminal_emulator_query_string();
|
||||
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.lock().unwrap().startup_query_duration();
|
||||
send_done_parsing_after_query_timeout(send_input_instructions.clone(), query_duration);
|
||||
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![];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -670,7 +671,7 @@ macro_rules! send_to_screen_or_retry_queue {
|
|||
None => {
|
||||
log::warn!("Server not ready, trying to place instruction in retry queue...");
|
||||
if let Some(retry_queue) = $retry_queue.as_mut() {
|
||||
retry_queue.push($instruction);
|
||||
retry_queue.push_back($instruction);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -686,7 +687,7 @@ pub(crate) fn route_thread_main(
|
|||
mut receiver: IpcReceiverWithContext<ClientToServerMsg>,
|
||||
client_id: ClientId,
|
||||
) -> Result<()> {
|
||||
let mut retry_queue = vec![];
|
||||
let mut retry_queue = VecDeque::new();
|
||||
let err_context = || format!("failed to handle instruction for client {client_id}");
|
||||
'route_loop: loop {
|
||||
match receiver.recv() {
|
||||
|
|
@ -694,7 +695,9 @@ pub(crate) fn route_thread_main(
|
|||
err_ctx.update_thread_ctx();
|
||||
let rlocked_sessions = session_data.read().to_anyhow().with_context(err_context)?;
|
||||
let handle_instruction = |instruction: ClientToServerMsg,
|
||||
mut retry_queue: Option<&mut Vec<ClientToServerMsg>>|
|
||||
mut retry_queue: Option<
|
||||
&mut VecDeque<ClientToServerMsg>,
|
||||
>|
|
||||
-> Result<bool> {
|
||||
let mut should_break = false;
|
||||
match instruction {
|
||||
|
|
@ -837,7 +840,7 @@ pub(crate) fn route_thread_main(
|
|||
}
|
||||
Ok(should_break)
|
||||
};
|
||||
for instruction_to_retry in retry_queue.drain(..) {
|
||||
while let Some(instruction_to_retry) = retry_queue.pop_front() {
|
||||
log::warn!("Server ready, retrying sending instruction.");
|
||||
let should_break = handle_instruction(instruction_to_retry, None)?;
|
||||
if should_break {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue