pub(crate) const MAX_REPLY_BYTES: usize = 4 * 1024;
pub(crate) const QUERIES: &str = concat!(
"\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\",
"\x1b[>q",
"\x1b[?1;1;0S",
"\x1b[16t",
"\x1b[c",
);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct Report {
pub kitty: bool,
pub terminal: Option<String>,
pub sixel: bool,
pub cell_size: Option<(u16, u16)>,
pub answered: bool,
}
pub(crate) fn done(bytes: &[u8]) -> bool {
sequences(bytes).iter().any(|s| {
matches!(
s,
Sequence::Csi {
private: true,
terminator: 'c',
..
}
)
})
}
pub(crate) fn parse(bytes: &[u8]) -> Report {
let mut report = Report::default();
for sequence in sequences(bytes) {
match sequence {
Sequence::Apc(body) => {
if body.contains("i=31") && body.contains("OK") {
report.kitty = true;
}
}
Sequence::Dcs(body) => {
if let Some(name) = body.strip_prefix(">|") {
report.terminal = Some(name.trim().to_string());
}
}
Sequence::Csi {
private: true,
terminator: 'c',
params,
} => {
report.answered = true;
if params.contains(&4) {
report.sixel = true;
}
}
Sequence::Csi {
private: true,
terminator: 'S',
params,
} => {
if params.first() == Some(&1) && params.get(1) == Some(&0) {
report.sixel = true;
}
}
Sequence::Csi {
private: false,
terminator: 't',
params,
} => {
if params.first() == Some(&6)
&& let (Some(&height), Some(&width)) = (params.get(1), params.get(2))
&& width >= 2
&& height >= 4
&& width <= u32::from(u16::MAX)
&& height <= u32::from(u16::MAX)
{
report.cell_size = Some((width as u16, height as u16));
}
}
Sequence::Csi { .. } => {}
}
}
report
}
enum Sequence {
Csi {
private: bool,
params: Vec<u32>,
terminator: char,
},
Dcs(String),
Apc(String),
}
fn sequences(bytes: &[u8]) -> Vec<Sequence> {
let mut out = Vec::new();
let mut i = 0;
while i + 1 < bytes.len() {
if bytes[i] != 0x1b {
i += 1;
continue;
}
match bytes[i + 1] {
b'[' => {
let mut j = i + 2;
while j < bytes.len() && !(0x40..=0x7E).contains(&bytes[j]) {
j += 1;
}
let Some(&terminator) = bytes.get(j) else {
break;
};
let body = &bytes[i + 2..j];
let private = body.first() == Some(&b'?');
let digits = if private { &body[1..] } else { body };
let params = digits
.split(|&b| b == b';')
.filter_map(|segment| std::str::from_utf8(segment).ok()?.parse::<u32>().ok())
.collect();
out.push(Sequence::Csi {
private,
params,
terminator: char::from(terminator),
});
i = j + 1;
}
kind @ (b'P' | b'_') => {
let mut j = i + 2;
while j + 1 < bytes.len() && !(bytes[j] == 0x1b && bytes[j + 1] == b'\\') {
j += 1;
}
if j + 1 >= bytes.len() {
break;
}
let body = String::from_utf8_lossy(&bytes[i + 2..j]).into_owned();
out.push(if kind == b'P' {
Sequence::Dcs(body)
} else {
Sequence::Apc(body)
});
i = j + 2;
}
_ => i += 1,
}
}
out
}
#[cfg(test)]
#[path = "tests/probe_tests.rs"]
mod tests;