use std::time::{Duration, Instant}; 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, }; #[derive(Debug)] pub struct StdinAnsiParser { raw_buffer: Vec, pending_color_sequences: Vec<(usize, String)>, pending_events: Vec, parse_deadline: Option, } impl StdinAnsiParser { pub fn new() -> Self { StdinAnsiParser { raw_buffer: vec![], pending_color_sequences: vec![], pending_events: vec![], parse_deadline: None, } } 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) // [14t => get text area size in pixels, // [16t => get character cell size in pixels // ]11;?\ => get background color // ]10;?\ => 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. ]4;5;?\ => 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 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) // [14t => get text area size in pixels, // [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 { 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) { 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; } } false } pub fn startup_query_duration(&self) -> u64 { STARTUP_PARSE_DEADLINE_MS } pub fn parse(&mut self, mut raw_bytes: Vec) -> Vec { 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.raw_buffer.clear(); }, } } 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.raw_buffer.push(byte); } } } #[derive(Debug, Clone)] pub enum AnsiStdinInstruction { PixelDimensions(PixelDimensions), BackgroundColor(String), ForegroundColor(String), ColorRegisters(Vec<(usize, String)>), } impl AnsiStdinInstruction { pub fn pixel_dimensions_from_bytes(bytes: &[u8]) -> Result { // eg. [4;21;8t lazy_static! { static ref RE: Regex = Regex::new(r"^\u{1b}\[(\d+);(\d+);(\d+)t$").unwrap(); } let key_string = String::from_utf8_lossy(bytes); // TODO: handle error 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(AnsiStdinInstruction::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(AnsiStdinInstruction::PixelDimensions(PixelDimensions { character_cell_size: Some(SizeInPixels { height: first_field.unwrap(), width: second_field.unwrap(), }), text_area_size: None, })) }, _ => Err("invalid sequence"), } } pub fn bg_or_fg_from_bytes(bytes: &[u8]) -> Result { // eg. ]11;rgb:0000/0000/0000\ lazy_static! { static ref BACKGROUND_RE: Regex = Regex::new(r"\]11;(.*)\u{1b}\\$").unwrap(); } // eg. ]10;rgb:ffff/ffff/ffff\ lazy_static! { static ref FOREGROUND_RE: Regex = Regex::new(r"\]10;(.*)\u{1b}\\$").unwrap(); } let key_string = String::from_utf8_lossy(bytes); if let Some(captures) = BACKGROUND_RE.captures_iter(&key_string).next() { let background_query_response = captures[1].parse::(); match background_query_response { Ok(background_query_response) => Ok(AnsiStdinInstruction::BackgroundColor( background_query_response, )), _ => Err("invalid_instruction"), } } else if let Some(captures) = FOREGROUND_RE.captures_iter(&key_string).next() { let foreground_query_response = captures[1].parse::(); match foreground_query_response { Ok(foreground_query_response) => Ok(AnsiStdinInstruction::ForegroundColor( foreground_query_response, )), _ => Err("invalid_instruction"), } } else { Err("invalid_instruction") } } pub fn color_registers_from_bytes(color_sequences: &mut Vec<(usize, String)>) -> Option { if color_sequences.is_empty() { return None; } let mut registers = vec![]; for (color_register, color_sequence) in color_sequences.drain(..) { registers.push((color_register, color_sequence)); } Some(AnsiStdinInstruction::ColorRegisters(registers)) } } fn color_sequence_from_bytes(bytes: &[u8]) -> Result<(usize, String), &'static str> { lazy_static! { static ref COLOR_REGISTER_RE: Regex = Regex::new(r"\]4;(.*);(.*)\u{1b}\\$").unwrap(); } lazy_static! { // this form is used by eg. Alacritty, where the leading 4 is dropped in the response static ref ALTERNATIVE_COLOR_REGISTER_RE: Regex = Regex::new(r"\](.*);(.*)\u{1b}\\$").unwrap(); } let key_string = String::from_utf8_lossy(bytes); if let Some(captures) = COLOR_REGISTER_RE.captures_iter(&key_string).next() { let color_register_response = captures[1].parse::(); let color_response = captures[2].parse::(); match (color_register_response, color_response) { (Ok(crr), Ok(cr)) => Ok((crr, cr)), _ => Err("invalid_instruction"), } } else if let Some(captures) = ALTERNATIVE_COLOR_REGISTER_RE .captures_iter(&key_string) .next() { let color_register_response = captures[1].parse::(); let color_response = captures[2].parse::(); match (color_register_response, color_response) { (Ok(crr), Ok(cr)) => Ok((crr, cr)), _ => Err("invalid_instruction"), } } else { Err("invalid_instruction") } }