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",
|
"log",
|
||||||
"mio",
|
"mio",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"url",
|
"url",
|
||||||
"zellij-utils",
|
"zellij-utils",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ mio = { version = "0.7.11", features = ['os-ext'] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
url = { version = "2.2.2", features = ["serde"] }
|
url = { version = "2.2.2", features = ["serde"] }
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
|
serde_json = "1.0"
|
||||||
zellij-utils = { path = "../zellij-utils/", version = "0.34.5" }
|
zellij-utils = { path = "../zellij-utils/", version = "0.34.5" }
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -265,16 +265,6 @@ pub fn start_client(
|
||||||
os_api.send_to_server(ClientToServerMsg::TerminalResize(
|
os_api.send_to_server(ClientToServerMsg::TerminalResize(
|
||||||
os_api.get_terminal_size_using_fd(0),
|
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({
|
Box::new({
|
||||||
|
|
@ -348,7 +338,7 @@ pub fn start_client(
|
||||||
|
|
||||||
let mut stdout = os_input.get_stdout_writer();
|
let mut stdout = os_input.get_stdout_writer();
|
||||||
stdout
|
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");
|
.expect("cannot write to stdout");
|
||||||
stdout.flush().expect("could not flush");
|
stdout.flush().expect("could not flush");
|
||||||
|
|
||||||
|
|
@ -368,7 +358,7 @@ pub fn start_client(
|
||||||
match client_instruction {
|
match client_instruction {
|
||||||
ClientInstruction::StartedParsingStdinQuery => {
|
ClientInstruction::StartedParsingStdinQuery => {
|
||||||
stdout
|
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");
|
.expect("cannot write to stdout");
|
||||||
stdout.flush().expect("could not flush");
|
stdout.flush().expect("could not flush");
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use zellij_utils::consts::{VERSION, ZELLIJ_CACHE_DIR};
|
||||||
|
|
||||||
const STARTUP_PARSE_DEADLINE_MS: u64 = 500;
|
const STARTUP_PARSE_DEADLINE_MS: u64 = 500;
|
||||||
const SIGWINCH_PARSE_DEADLINE_MS: u64 = 200;
|
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
ipc::PixelDimensions, lazy_static::lazy_static, pane_size::SizeInPixels, regex::Regex,
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct StdinAnsiParser {
|
pub struct StdinAnsiParser {
|
||||||
raw_buffer: Vec<u8>,
|
raw_buffer: Vec<u8>,
|
||||||
|
|
@ -43,18 +49,6 @@ impl StdinAnsiParser {
|
||||||
Some(Instant::now() + Duration::from_millis(STARTUP_PARSE_DEADLINE_MS));
|
Some(Instant::now() + Duration::from_millis(STARTUP_PARSE_DEADLINE_MS));
|
||||||
query_string
|
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> {
|
fn drain_pending_events(&mut self) -> Vec<AnsiStdinInstruction> {
|
||||||
let mut events = vec![];
|
let mut events = vec![];
|
||||||
events.append(&mut self.pending_events);
|
events.append(&mut self.pending_events);
|
||||||
|
|
@ -82,6 +76,34 @@ impl StdinAnsiParser {
|
||||||
}
|
}
|
||||||
self.drain_pending_events()
|
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) {
|
fn parse_byte(&mut self, byte: u8) {
|
||||||
if byte == b't' {
|
if byte == b't' {
|
||||||
self.raw_buffer.push(byte);
|
self.raw_buffer.push(byte);
|
||||||
|
|
@ -112,9 +134,12 @@ impl StdinAnsiParser {
|
||||||
self.raw_buffer.push(byte);
|
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 {
|
pub enum AnsiStdinInstruction {
|
||||||
PixelDimensions(PixelDimensions),
|
PixelDimensions(PixelDimensions),
|
||||||
BackgroundColor(String),
|
BackgroundColor(String),
|
||||||
|
|
|
||||||
|
|
@ -27,21 +27,37 @@ pub(crate) fn stdin_loop(
|
||||||
let mut holding_mouse = false;
|
let mut holding_mouse = false;
|
||||||
let mut input_parser = InputParser::new();
|
let mut input_parser = InputParser::new();
|
||||||
let mut current_buffer = vec![];
|
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
|
// on startup we send a query to the terminal emulator for stuff like the pixel size and colors
|
||||||
send_input_instructions
|
// we get a response through STDIN, so it makes sense to do this here
|
||||||
.send(InputInstruction::StartedParsing)
|
let mut stdin_ansi_parser = stdin_ansi_parser.lock().unwrap();
|
||||||
.unwrap();
|
match stdin_ansi_parser.read_cache() {
|
||||||
let terminal_emulator_query_string = stdin_ansi_parser
|
Some(events) => {
|
||||||
.lock()
|
let _ =
|
||||||
.unwrap()
|
send_input_instructions.send(InputInstruction::AnsiStdinInstructions(events));
|
||||||
.terminal_emulator_query_string();
|
let _ = send_input_instructions
|
||||||
let _ = os_input
|
.send(InputInstruction::DoneParsing)
|
||||||
.get_stdout_writer()
|
.unwrap();
|
||||||
.write(terminal_emulator_query_string.as_bytes())
|
},
|
||||||
.unwrap();
|
None => {
|
||||||
let query_duration = stdin_ansi_parser.lock().unwrap().startup_query_duration();
|
send_input_instructions
|
||||||
send_done_parsing_after_query_timeout(send_input_instructions.clone(), query_duration);
|
.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 {
|
loop {
|
||||||
let buf = os_input.read_from_stdin();
|
let buf = os_input.read_from_stdin();
|
||||||
{
|
{
|
||||||
|
|
@ -54,12 +70,19 @@ pub(crate) fn stdin_loop(
|
||||||
if stdin_ansi_parser.should_parse() {
|
if stdin_ansi_parser.should_parse() {
|
||||||
let events = stdin_ansi_parser.parse(buf);
|
let events = stdin_ansi_parser.parse(buf);
|
||||||
if !events.is_empty() {
|
if !events.is_empty() {
|
||||||
|
ansi_stdin_events.append(&mut events.clone());
|
||||||
let _ = send_input_instructions
|
let _ = send_input_instructions
|
||||||
.send(InputInstruction::AnsiStdinInstructions(events));
|
.send(InputInstruction::AnsiStdinInstructions(events));
|
||||||
}
|
}
|
||||||
continue;
|
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());
|
current_buffer.append(&mut buf.to_vec());
|
||||||
let maybe_more = false; // read_from_stdin should (hopefully) always empty the STDIN buffer completely
|
let maybe_more = false; // read_from_stdin should (hopefully) always empty the STDIN buffer completely
|
||||||
let mut events = vec![];
|
let mut events = vec![];
|
||||||
|
|
|
||||||
|
|
@ -317,98 +317,3 @@ pub fn move_focus_left_in_normal_mode() {
|
||||||
"All actions sent to server properly"
|
"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 std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -670,7 +671,7 @@ macro_rules! send_to_screen_or_retry_queue {
|
||||||
None => {
|
None => {
|
||||||
log::warn!("Server not ready, trying to place instruction in retry queue...");
|
log::warn!("Server not ready, trying to place instruction in retry queue...");
|
||||||
if let Some(retry_queue) = $retry_queue.as_mut() {
|
if let Some(retry_queue) = $retry_queue.as_mut() {
|
||||||
retry_queue.push($instruction);
|
retry_queue.push_back($instruction);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
|
@ -686,7 +687,7 @@ pub(crate) fn route_thread_main(
|
||||||
mut receiver: IpcReceiverWithContext<ClientToServerMsg>,
|
mut receiver: IpcReceiverWithContext<ClientToServerMsg>,
|
||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut retry_queue = vec![];
|
let mut retry_queue = VecDeque::new();
|
||||||
let err_context = || format!("failed to handle instruction for client {client_id}");
|
let err_context = || format!("failed to handle instruction for client {client_id}");
|
||||||
'route_loop: loop {
|
'route_loop: loop {
|
||||||
match receiver.recv() {
|
match receiver.recv() {
|
||||||
|
|
@ -694,7 +695,9 @@ pub(crate) fn route_thread_main(
|
||||||
err_ctx.update_thread_ctx();
|
err_ctx.update_thread_ctx();
|
||||||
let rlocked_sessions = session_data.read().to_anyhow().with_context(err_context)?;
|
let rlocked_sessions = session_data.read().to_anyhow().with_context(err_context)?;
|
||||||
let handle_instruction = |instruction: ClientToServerMsg,
|
let handle_instruction = |instruction: ClientToServerMsg,
|
||||||
mut retry_queue: Option<&mut Vec<ClientToServerMsg>>|
|
mut retry_queue: Option<
|
||||||
|
&mut VecDeque<ClientToServerMsg>,
|
||||||
|
>|
|
||||||
-> Result<bool> {
|
-> Result<bool> {
|
||||||
let mut should_break = false;
|
let mut should_break = false;
|
||||||
match instruction {
|
match instruction {
|
||||||
|
|
@ -837,7 +840,7 @@ pub(crate) fn route_thread_main(
|
||||||
}
|
}
|
||||||
Ok(should_break)
|
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.");
|
log::warn!("Server ready, retrying sending instruction.");
|
||||||
let should_break = handle_instruction(instruction_to_retry, None)?;
|
let should_break = handle_instruction(instruction_to_retry, None)?;
|
||||||
if should_break {
|
if should_break {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue