#[cfg(unix)]
use tracing::{event, Level};
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub struct DeviceAttributes {
pub sixel: bool,
}
#[cfg(unix)]
pub fn probe_da1(timeout: std::time::Duration) -> Option<DeviceAttributes> {
use std::fs::OpenOptions;
use std::os::fd::AsFd;
use rustix::termios::{tcgetattr, tcsetattr, LocalModes, OptionalActions, SpecialCodeIndex};
let mut tty = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")
.ok()?;
let original = tcgetattr(tty.as_fd()).ok()?;
let mut raw = original.clone();
raw.local_modes
.remove(LocalModes::ICANON | LocalModes::ECHO | LocalModes::ECHONL);
let deciseconds = timeout.as_millis().div_ceil(100).min(255) as u8;
raw.special_codes[SpecialCodeIndex::VMIN] = 0;
raw.special_codes[SpecialCodeIndex::VTIME] = deciseconds;
tcsetattr(tty.as_fd(), OptionalActions::Now, &raw).ok()?;
let result = perform_da1_exchange(&mut tty);
let _ = tcsetattr(tty.as_fd(), OptionalActions::Now, &original);
let response = result?;
event!(Level::TRACE, ?response, "DA1 response");
Some(parse_da1(&response))
}
#[cfg(unix)]
fn perform_da1_exchange(tty: &mut std::fs::File) -> Option<Vec<u8>> {
use std::io::{Read, Write};
tty.write_all(b"\x1b[c").ok()?;
tty.flush().ok()?;
let mut buffer = Vec::with_capacity(64);
let mut chunk = [0u8; 32];
loop {
match tty.read(&mut chunk) {
Ok(0) => break,
Ok(n) => {
buffer.extend_from_slice(&chunk[..n]);
if buffer.contains(&b'c') {
break;
}
}
Err(_) => break,
}
}
if buffer.is_empty() {
None
} else {
Some(buffer)
}
}
#[cfg(not(unix))]
pub fn probe_da1(_timeout: std::time::Duration) -> Option<DeviceAttributes> {
None
}
#[cfg(unix)]
fn parse_da1(response: &[u8]) -> DeviceAttributes {
let text = std::str::from_utf8(response).unwrap_or("");
let Some(start) = text.find("\x1b[?") else {
return DeviceAttributes::default();
};
let remainder = &text[start + 3..];
let end = remainder.find('c').unwrap_or(remainder.len());
let params = &remainder[..end];
let sixel = params.split(';').any(|p| p == "4");
DeviceAttributes { sixel }
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
#[test]
fn parses_sixel_capable_response() {
let resp = b"\x1b[?63;1;2;4;6;9;15c";
assert!(parse_da1(resp).sixel);
}
#[test]
fn parses_non_sixel_response() {
let resp = b"\x1b[?61;6;22c";
assert!(!parse_da1(resp).sixel);
}
#[test]
fn handles_garbage() {
assert_eq!(parse_da1(b""), DeviceAttributes::default());
assert_eq!(
parse_da1(b"not a DA1 response"),
DeviceAttributes::default()
);
}
#[test]
fn param_4_inside_other_numbers_is_not_a_match() {
let resp = b"\x1b[?14;22c";
assert!(!parse_da1(resp).sixel);
}
}