use crate::ansi::sequences;
const MAX_DCS_RESPONSE_LENGTH: usize = 64 * 1024;
pub mod query_constants {
pub use crate::ansi::sequences::query::DEVICE_ATTRIBUTES as DA1;
pub use crate::ansi::sequences::query::DEVICE_ATTRIBUTES_SECONDARY as DA2;
pub use crate::ansi::sequences::query::KITTY_KEYBOARD;
pub use crate::ansi::sequences::query::PIXEL_RESOLUTION;
pub use crate::ansi::sequences::query::XTVERSION;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TerminalResponse {
DeviceAttributes {
primary: bool,
params: Vec<u32>,
},
XtVersion {
name: String,
version: String,
},
PixelSize {
width: u16,
height: u16,
},
KittyKeyboard {
flags: u32,
},
Unknown(Vec<u8>),
}
impl TerminalResponse {
#[must_use]
pub fn parse(input: &[u8]) -> Option<Self> {
if input.len() < 3 {
return None;
}
if input[0] != 0x1b {
return None;
}
if let Some(resp) = Self::parse_da1(input) {
return Some(resp);
}
if let Some(resp) = Self::parse_da2(input) {
return Some(resp);
}
if let Some(resp) = Self::parse_xtversion(input) {
return Some(resp);
}
if let Some(resp) = Self::parse_pixel_size(input) {
return Some(resp);
}
if let Some(resp) = Self::parse_kitty_keyboard(input) {
return Some(resp);
}
Some(TerminalResponse::Unknown(input.to_vec()))
}
fn parse_da1(input: &[u8]) -> Option<Self> {
if input.len() < 4 || input[1] != b'[' || input[2] != b'?' {
return None;
}
let end = input.iter().position(|&b| b == b'c')?;
if end < 3 {
return None;
}
let params_str = std::str::from_utf8(&input[3..end]).ok()?;
let params: Vec<u32> = params_str
.split(';')
.filter_map(|s| s.parse().ok())
.collect();
Some(TerminalResponse::DeviceAttributes {
primary: true,
params,
})
}
fn parse_da2(input: &[u8]) -> Option<Self> {
if input.len() < 4 || input[1] != b'[' || input[2] != b'>' {
return None;
}
let end = input.iter().position(|&b| b == b'c')?;
if end < 3 {
return None;
}
let params_str = std::str::from_utf8(&input[3..end]).ok()?;
let params: Vec<u32> = params_str
.split(';')
.filter_map(|s| s.parse().ok())
.collect();
Some(TerminalResponse::DeviceAttributes {
primary: false,
params,
})
}
fn parse_xtversion(input: &[u8]) -> Option<Self> {
if input.len() < 5 || input.len() > MAX_DCS_RESPONSE_LENGTH {
return None;
}
let start = if input[0] == 0x1b && input[1] == b'P' {
2
} else if input[0] == 0x90 {
1
} else {
return None;
};
if input.get(start) != Some(&b'>') || input.get(start + 1) != Some(&b'|') {
return None;
}
let content_start = start + 2;
let st_pos = input[content_start..]
.windows(2)
.position(|w| w == b"\x1b\\")
.map(|p| content_start + p)
.or_else(|| {
input[content_start..]
.iter()
.position(|&b| b == 0x9c)
.map(|p| content_start + p)
})?;
let content = std::str::from_utf8(&input[content_start..st_pos]).ok()?;
let (name, version) = content.find(' ').map_or_else(
|| (content.to_string(), String::new()),
|space_pos| {
(
content[..space_pos].to_string(),
content[space_pos + 1..].to_string(),
)
},
);
Some(TerminalResponse::XtVersion { name, version })
}
fn parse_pixel_size(input: &[u8]) -> Option<Self> {
if input.len() < 6 || input[1] != b'[' || input[2] != b'4' || input[3] != b';' {
return None;
}
let end = input.iter().position(|&b| b == b't')?;
if end < 5 {
return None;
}
let params_str = std::str::from_utf8(&input[4..end]).ok()?;
let mut parts = params_str.split(';');
let height: u16 = parts.next()?.parse().ok()?;
let width: u16 = parts.next()?.parse().ok()?;
Some(TerminalResponse::PixelSize { width, height })
}
fn parse_kitty_keyboard(input: &[u8]) -> Option<Self> {
if input.len() < 4 || input[1] != b'[' || input[2] != b'?' {
return None;
}
let end = input.iter().position(|&b| b == b'u')?;
if end < 3 {
return None;
}
let flags_str = std::str::from_utf8(&input[3..end]).ok()?;
let flags: u32 = flags_str.parse().ok()?;
Some(TerminalResponse::KittyKeyboard { flags })
}
#[must_use]
pub fn has_sixel(&self) -> bool {
if let TerminalResponse::DeviceAttributes {
primary: true,
params,
} = self
{
params.contains(&4)
} else {
false
}
}
#[must_use]
pub fn terminal_name(&self) -> Option<&str> {
if let TerminalResponse::XtVersion { name, .. } = self {
Some(name)
} else {
None
}
}
}
#[must_use]
pub fn all_queries() -> String {
format!(
"{}{}{}{}{}",
sequences::query::DEVICE_ATTRIBUTES,
sequences::query::DEVICE_ATTRIBUTES_SECONDARY,
sequences::query::XTVERSION,
sequences::query::PIXEL_RESOLUTION,
sequences::query::KITTY_KEYBOARD,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_da1_response_basic() {
let input = b"\x1b[?1;2c";
let response = TerminalResponse::parse(input).unwrap();
match response {
TerminalResponse::DeviceAttributes { primary, params } => {
assert!(primary, "Should be DA1 (primary)");
assert_eq!(params, vec![1, 2]);
}
other => {
assert!(
matches!(other, TerminalResponse::DeviceAttributes { .. }),
"Expected DeviceAttributes"
);
}
}
}
#[test]
fn test_parse_da1_response_with_sixel() {
let input = b"\x1b[?62;4;6c";
let response = TerminalResponse::parse(input).unwrap();
assert!(response.has_sixel(), "Should detect sixel support");
}
#[test]
fn test_parse_da2_response() {
let input = b"\x1b[>1;4000;20c";
let response = TerminalResponse::parse(input).unwrap();
match response {
TerminalResponse::DeviceAttributes { primary, params } => {
assert!(!primary, "Should be DA2 (secondary)");
assert_eq!(params, vec![1, 4000, 20]);
}
other => {
assert!(
matches!(other, TerminalResponse::DeviceAttributes { .. }),
"Expected DeviceAttributes"
);
}
}
}
#[test]
fn test_parse_xtversion_kitty() {
let input = b"\x1bP>|kitty(0.26.5)\x1b\\";
let response = TerminalResponse::parse(input).unwrap();
match response {
TerminalResponse::XtVersion { name, version } => {
assert!(name.contains("kitty"), "Should detect kitty");
assert!(version.contains("0.26.5") || name.contains("0.26.5"));
}
other => {
assert!(
matches!(other, TerminalResponse::XtVersion { .. }),
"Expected XtVersion"
);
}
}
}
#[test]
fn test_parse_xtversion_alacritty() {
let input = b"\x1bP>|alacritty 0.12.0\x1b\\";
let response = TerminalResponse::parse(input).unwrap();
match response {
TerminalResponse::XtVersion { name, version } => {
assert_eq!(name, "alacritty");
assert_eq!(version, "0.12.0");
}
other => {
assert!(
matches!(other, TerminalResponse::XtVersion { .. }),
"Expected XtVersion"
);
}
}
}
#[test]
fn test_parse_pixel_size_response() {
let input = b"\x1b[4;900;1440t";
let response = TerminalResponse::parse(input).unwrap();
match response {
TerminalResponse::PixelSize { width, height } => {
assert_eq!(width, 1440);
assert_eq!(height, 900);
}
other => {
assert!(
matches!(other, TerminalResponse::PixelSize { .. }),
"Expected PixelSize"
);
}
}
}
#[test]
fn test_parse_kitty_keyboard_response() {
let input = b"\x1b[?1u";
let response = TerminalResponse::parse(input).unwrap();
match response {
TerminalResponse::KittyKeyboard { flags } => {
assert_eq!(flags, 1);
}
other => {
assert!(
matches!(other, TerminalResponse::KittyKeyboard { .. }),
"Expected KittyKeyboard"
);
}
}
}
#[test]
fn test_parse_unknown_response() {
let input = b"\x1b[99z";
let response = TerminalResponse::parse(input).unwrap();
assert!(matches!(response, TerminalResponse::Unknown(_)));
}
#[test]
fn test_query_sequences_correct() {
assert_eq!(query_constants::DA1, "\x1b[c");
assert_eq!(query_constants::DA2, "\x1b[>c");
assert_eq!(query_constants::XTVERSION, "\x1b[>0q");
assert_eq!(query_constants::PIXEL_RESOLUTION, "\x1b[14t");
assert_eq!(query_constants::KITTY_KEYBOARD, "\x1b[?u");
}
#[test]
fn test_all_queries_sends_all() {
let all = all_queries();
assert!(all.contains("\x1b[c"), "Should contain DA1");
assert!(all.contains("\x1b[>c"), "Should contain DA2");
assert!(all.contains("\x1b[>0q"), "Should contain XTVERSION");
assert!(all.contains("\x1b[14t"), "Should contain pixel resolution");
assert!(all.contains("\x1b[?u"), "Should contain kitty keyboard");
}
#[test]
fn test_capabilities_updated_from_da1() {
let input = b"\x1b[?62;4c";
let response = TerminalResponse::parse(input).unwrap();
assert!(response.has_sixel());
}
#[test]
fn test_terminal_name_extraction() {
let input = b"\x1bP>|foot 1.15.3\x1b\\";
let response = TerminalResponse::parse(input).unwrap();
assert_eq!(response.terminal_name(), Some("foot"));
}
}