use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use zellij_utils::{
data::HostTerminalThemeMode,
ipc::PixelDimensions,
pane_size::SizeInPixels,
vendored::termwiz::input::{InputEvent, InputParser},
};
#[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, Clone, Serialize, Deserialize)]
pub enum HostReply {
PixelDimensions(PixelDimensions),
BackgroundColor(String),
ForegroundColor(String),
ColorRegisters(Vec<(usize, String)>),
SynchronizedOutput(Option<SyncOutput>),
HostTerminalThemeChanged(HostTerminalThemeMode),
}
pub type AnsiStdinInstruction = HostReply;
impl HostReply {
pub fn from_osc_payload(payload: &[u8]) -> Option<HostReply> {
lazy_static! {
static ref FG_RE: Regex = Regex::new(r"^10;(.*)$").unwrap();
static ref BG_RE: Regex = Regex::new(r"^11;(.*)$").unwrap();
static ref COLOR_REGISTER_RE: Regex = Regex::new(r"^4;(\d+);(.*)$").unwrap();
}
let s = std::str::from_utf8(payload).ok()?;
if let Some(caps) = BG_RE.captures(s) {
return Some(HostReply::BackgroundColor(caps[1].to_string()));
}
if let Some(caps) = FG_RE.captures(s) {
return Some(HostReply::ForegroundColor(caps[1].to_string()));
}
if let Some(caps) = COLOR_REGISTER_RE.captures(s) {
let index: usize = caps[1].parse().ok()?;
let color = caps[2].to_string();
return Some(HostReply::ColorRegisters(vec![(index, color)]));
}
None
}
pub fn from_csi_report(raw: &[u8]) -> Option<HostReply> {
let s = std::str::from_utf8(raw).ok()?;
lazy_static! {
static ref PIX_RE: Regex = Regex::new(r"^\u{1b}\[(\d+);(\d+);(\d+)t$").unwrap();
static ref SYNC_RE: Regex = Regex::new(r"^\u{1b}\[\?2026;([0-4])\$y$").unwrap();
static ref THEME_RE: Regex = Regex::new(r"^\u{1b}\[\?997;([12])n$").unwrap();
}
if let Some(caps) = PIX_RE.captures(s) {
let which: usize = caps[1].parse().ok()?;
let first: usize = caps[2].parse().ok()?;
let second: usize = caps[3].parse().ok()?;
return match which {
4 => Some(HostReply::PixelDimensions(PixelDimensions {
character_cell_size: None,
text_area_size: Some(SizeInPixels {
height: first,
width: second,
}),
})),
6 => Some(HostReply::PixelDimensions(PixelDimensions {
character_cell_size: Some(SizeInPixels {
height: first,
width: second,
}),
text_area_size: None,
})),
_ => None,
};
}
if let Some(caps) = SYNC_RE.captures(s) {
let code: usize = caps[1].parse().ok()?;
return match code {
1 | 2 | 3 => Some(HostReply::SynchronizedOutput(Some(SyncOutput::CSI))),
_ => Some(HostReply::SynchronizedOutput(None)),
};
}
if let Some(caps) = THEME_RE.captures(s) {
let mode = match &caps[1] {
"1" => HostTerminalThemeMode::Dark,
"2" => HostTerminalThemeMode::Light,
_ => return None,
};
return Some(HostReply::HostTerminalThemeChanged(mode));
}
None
}
}
#[derive(Debug, Clone)]
pub struct ForwardSlot {
pub token: u32,
pub reply_bytes: Vec<u8>,
}
#[derive(Debug, Clone, Default)]
pub struct ParseOutput {
pub replies: Vec<HostReply>,
pub completed_forward: Option<(u32, Vec<u8>)>,
pub desktop_notifications: Vec<Vec<u8>>,
pub residue: Vec<u8>,
pub has_partial_state: bool,
}
const PARTIAL_BUFFER_CAP_BYTES: usize = 100 * 1024 * 1024;
#[derive(Debug, Clone, Copy)]
enum SeqStatus {
Complete(usize),
NeedMore,
Malformed,
}
pub struct StdinAnsiParser {
inner: InputParser,
active_forward: Option<ForwardSlot>,
partial_osc: Vec<u8>,
partial_csi: Vec<u8>,
}
impl std::fmt::Debug for StdinAnsiParser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StdinAnsiParser")
.field("active_forward", &self.active_forward)
.field("partial_osc_len", &self.partial_osc.len())
.field("partial_csi_len", &self.partial_csi.len())
.finish()
}
}
impl StdinAnsiParser {
pub fn new() -> Self {
StdinAnsiParser {
inner: InputParser::new(),
active_forward: None,
partial_osc: Vec::new(),
partial_csi: Vec::new(),
}
}
pub fn open_forward(&mut self, token: u32) {
debug_assert!(
self.active_forward.is_none(),
"open_forward({}) called while slot for token {:?} is still active",
token,
self.active_forward.as_ref().map(|s| s.token),
);
if let Some(existing) = self.active_forward.as_ref() {
log::warn!(
"open_forward({}) re-entered with existing slot token={} ({} accumulated bytes \
will be dropped); server serialization should have prevented this",
token,
existing.token,
existing.reply_bytes.len(),
);
}
self.active_forward = Some(ForwardSlot {
token,
reply_bytes: Vec::new(),
});
}
pub fn close_forward_on_timeout(&mut self, token: u32) -> Option<(u32, Vec<u8>)> {
match &self.active_forward {
Some(slot) if slot.token == token => {
let slot = self.active_forward.take().unwrap();
Some((slot.token, slot.reply_bytes))
},
_ => None,
}
}
#[cfg(test)]
pub fn active_forward_token(&self) -> Option<u32> {
self.active_forward.as_ref().map(|s| s.token)
}
pub fn feed(&mut self, bytes: &[u8]) -> ParseOutput {
let mut out = ParseOutput::default();
let mut events = Vec::new();
let mut residue = Vec::new();
self.inner.parse(
bytes,
|event| {
events.push(event);
},
true, );
for event in events {
match event {
InputEvent::OperatingSystemCommand(payload) => {
if payload.starts_with(b"99;") {
out.desktop_notifications
.push(payload.get(3..).unwrap_or_default().to_vec());
} else if let Some(reply) = HostReply::from_osc_payload(&payload) {
out.replies.push(reply);
}
if let Some(slot) = self.active_forward.as_mut() {
slot.reply_bytes.extend_from_slice(b"\x1b]");
slot.reply_bytes.extend_from_slice(&payload);
slot.reply_bytes.extend_from_slice(b"\x1b\\");
}
},
InputEvent::DeviceControlReply {
params,
final_byte,
raw,
..
} => {
match final_byte {
b'c' => {
if let Some(slot) = self.active_forward.take() {
out.completed_forward = Some((slot.token, slot.reply_bytes));
}
},
_ => {
if let Some(reply) = HostReply::from_csi_report(&raw) {
out.replies.push(reply);
}
if let Some(slot) = self.active_forward.as_mut() {
slot.reply_bytes.extend_from_slice(&raw);
}
let _ = params;
},
}
},
_ => {},
}
}
residue.extend(self.strip_replies(bytes));
out.residue = residue;
out.has_partial_state = !self.partial_osc.is_empty() || !self.partial_csi.is_empty();
out
}
pub fn finalize(&mut self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.partial_osc.len() + self.partial_csi.len());
out.append(&mut self.partial_osc);
out.append(&mut self.partial_csi);
out
}
fn strip_replies(&mut self, bytes: &[u8]) -> Vec<u8> {
let mut working: Vec<u8> =
Vec::with_capacity(self.partial_osc.len() + self.partial_csi.len() + bytes.len());
working.append(&mut self.partial_osc);
working.append(&mut self.partial_csi);
working.extend_from_slice(bytes);
let mut out = Vec::with_capacity(working.len());
let mut i = 0;
while i < working.len() {
let rest = &working[i..];
if rest.len() >= 2 && rest[0] == 0x1b && rest[1] == b']' {
match osc_status(rest) {
SeqStatus::Complete(len) => {
i += len;
continue;
},
SeqStatus::NeedMore => {
let tail = rest.to_vec();
if tail.len() > PARTIAL_BUFFER_CAP_BYTES {
out.extend_from_slice(&tail);
} else {
self.partial_osc = tail;
}
return out;
},
SeqStatus::Malformed => {
out.push(working[i]);
i += 1;
continue;
},
}
}
if rest.len() >= 2 && rest[0] == 0x1b && rest[1] == b'[' {
match csi_status(rest) {
SeqStatus::Complete(len) => {
i += len;
continue;
},
SeqStatus::NeedMore => {
let tail = rest.to_vec();
if tail.len() > PARTIAL_BUFFER_CAP_BYTES {
out.extend_from_slice(&tail);
} else {
self.partial_csi = tail;
}
return out;
},
SeqStatus::Malformed => {
out.push(working[i]);
i += 1;
continue;
},
}
}
if rest.len() == 1 && rest[0] == 0x1b {
self.partial_osc = vec![0x1b];
return out;
}
out.push(working[i]);
i += 1;
}
out
}
}
fn osc_status(buf: &[u8]) -> SeqStatus {
if buf.get(0) != Some(&0x1b) || buf.get(1) != Some(&b']') {
return SeqStatus::Malformed;
}
let mut i = 2;
while i < buf.len() {
match buf[i] {
0x07 => return SeqStatus::Complete(i + 1),
0x1b => match buf.get(i + 1) {
Some(&b'\\') => return SeqStatus::Complete(i + 2),
Some(_) => return SeqStatus::Malformed,
None => return SeqStatus::NeedMore,
},
_ => i += 1,
}
}
SeqStatus::NeedMore
}
fn csi_status(buf: &[u8]) -> SeqStatus {
if buf.get(0) != Some(&0x1b) || buf.get(1) != Some(&b'[') {
return SeqStatus::Malformed;
}
let mut i = 2;
let max = 256;
while i < buf.len() && i < max {
let b = buf[i];
match b {
0x30..=0x3F | 0x20..=0x2F => i += 1,
b't' | b'y' | b'c' | b'n' => return SeqStatus::Complete(i + 1),
0x40..=0x7E => return SeqStatus::Malformed, _ => return SeqStatus::Malformed,
}
}
if i >= max {
SeqStatus::Malformed
} else {
SeqStatus::NeedMore
}
}
use std::sync::{Arc, Mutex, OnceLock};
static FORWARD_TIMEOUT_RUNTIME: OnceLock<Arc<tokio::runtime::Runtime>> = OnceLock::new();
pub fn forward_timeout_runtime() -> &'static Arc<tokio::runtime::Runtime> {
FORWARD_TIMEOUT_RUNTIME.get_or_init(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.expect("failed to build forward-timeout runtime");
let rt = Arc::new(rt);
let rt_for_driver = rt.clone();
std::thread::Builder::new()
.name("zellij-client-forward-timeout".into())
.spawn(move || {
rt_for_driver.block_on(std::future::pending::<()>());
})
.expect("failed to spawn forward-timeout driver thread");
rt
})
}
pub fn schedule_forward_timeout<F>(
runtime: &tokio::runtime::Handle,
parser: Arc<Mutex<StdinAnsiParser>>,
token: u32,
deadline: std::time::Duration,
on_timeout: F,
) where
F: FnOnce(u32, Vec<u8>) + Send + 'static,
{
runtime.spawn(async move {
tokio::time::sleep(deadline).await;
let payload = parser.lock().unwrap().close_forward_on_timeout(token);
if let Some((t, bytes)) = payload {
on_timeout(t, bytes);
}
});
}
#[cfg(test)]
#[path = "stdin_ansi_parser_tests.rs"]
mod tests;