zellij/zellij-client/src/stdin_ansi_parser.rs

315 lines
12 KiB
Rust

use std::time::{Duration, Instant};
const STARTUP_PARSE_DEADLINE_MS: u64 = 500;
use zellij_utils::{
consts::ZELLIJ_STDIN_CACHE_FILE, 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 zellij_utils::anyhow::Result;
/// Describe the terminal implementation of synchronised output
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum SyncOutput {
DCS,
CSI,
}
impl SyncOutput {
pub fn start_seq(&self) -> &'static [u8] {
static CSI_BSU_SEQ: &'static [u8] = "\u{1b}[?2026h".as_bytes();
static DCS_BSU_SEQ: &'static [u8] = "\u{1b}P=1s\u{1b}".as_bytes();
match self {
SyncOutput::DCS => DCS_BSU_SEQ,
SyncOutput::CSI => CSI_BSU_SEQ,
}
}
pub fn end_seq(&self) -> &'static [u8] {
static CSI_ESU_SEQ: &'static [u8] = "\u{1b}[?2026l".as_bytes();
static DCS_ESU_SEQ: &'static [u8] = "\u{1b}P=2s\u{1b}".as_bytes();
match self {
SyncOutput::DCS => DCS_ESU_SEQ,
SyncOutput::CSI => CSI_ESU_SEQ,
}
}
}
#[derive(Debug)]
pub struct StdinAnsiParser {
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 {
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)
// <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
// <ESC>[?2026$p => get synchronised output mode
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}\u{1b}[?2026$p",
);
// 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
}
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)
{
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<u8>) -> Vec<AnsiStdinInstruction> {
for byte in raw_bytes.drain(..) {
self.parse_byte(byte);
}
self.drain_pending_events()
}
pub fn read_cache(&self) -> Option<Vec<AnsiStdinInstruction>> {
match OpenOptions::new()
.read(true)
.open(ZELLIJ_STDIN_CACHE_FILE.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>) {
if let Ok(serialized_events) = serde_json::to_string(&events) {
if let Ok(mut file) = File::create(ZELLIJ_STDIN_CACHE_FILE.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);
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 if byte == b'y' {
self.raw_buffer.push(byte);
if let Some(ansi_sequence) =
AnsiStdinInstruction::synchronized_output_from_bytes(&self.raw_buffer)
{
self.pending_events.push(ansi_sequence);
self.raw_buffer.clear();
}
} else {
self.raw_buffer.push(byte);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AnsiStdinInstruction {
PixelDimensions(PixelDimensions),
BackgroundColor(String),
ForegroundColor(String),
ColorRegisters(Vec<(usize, String)>),
SynchronizedOutput(Option<SyncOutput>),
}
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_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::<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(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<Self, &'static str> {
// eg. <ESC>]11;rgb:0000/0000/0000\
lazy_static! {
static ref BACKGROUND_RE: Regex = Regex::new(r"\]11;(.*)\u{1b}\\$").unwrap();
}
// eg. <ESC>]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::<String>();
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::<String>();
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<Self> {
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))
}
pub fn synchronized_output_from_bytes(bytes: &[u8]) -> Option<Self> {
lazy_static! {
static ref RE: Regex = Regex::new(r"^\u{1b}\[\?2026;([0|1|2|3|4])\$y$").unwrap();
}
let key_string = String::from_utf8_lossy(bytes);
if let Some(captures) = RE.captures_iter(&key_string).next() {
match captures[1].parse::<usize>().ok()? {
1 | 2 => Some(AnsiStdinInstruction::SynchronizedOutput(Some(
SyncOutput::CSI,
))),
0 | 4 => Some(AnsiStdinInstruction::SynchronizedOutput(None)),
_ => None,
}
} else {
None
}
}
}
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::<usize>();
let color_response = captures[2].parse::<String>();
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::<usize>();
let color_response = captures[2].parse::<String>();
match (color_register_response, color_response) {
(Ok(crr), Ok(cr)) => Ok((crr, cr)),
_ => Err("invalid_instruction"),
}
} else {
Err("invalid_instruction")
}
}