use crate::caps::Capabilities;
use crate::model::Message;
use crate::scan::{Event, Scanner};
pub const QUERY: &[u8] = b"\x1b]7501;?\x1b\\\x1b[c";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
Supported(Capabilities),
Unsupported,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Da1 {
Idle,
Esc,
Csi,
Params,
}
#[derive(Clone, Debug)]
pub struct Detector {
scanner: Scanner,
caps: Option<Capabilities>,
da1: Da1,
}
impl Default for Detector {
fn default() -> Self {
Self::new()
}
}
impl Detector {
pub const fn new() -> Self {
Detector {
scanner: Scanner::new(),
caps: None,
da1: Da1::Idle,
}
}
pub fn feed(&mut self, bytes: &[u8]) -> Option<Outcome> {
let mut caps = self.caps;
let mut da1 = self.da1;
let mut done = false;
self.scanner.feed(bytes, |event| {
if done {
return;
}
match event {
Event::Sequence {
message: Ok(Message::Feature(c)),
..
} => caps = Some(c),
Event::Sequence { .. } => {}
Event::Passthrough(b) => {
for &byte in b {
da1 = match (da1, byte) {
(_, 0x1b) => Da1::Esc,
(Da1::Esc, b'[') => Da1::Csi,
(Da1::Csi, b'?') => Da1::Params,
(Da1::Params, b'0'..=b'9' | b';') => Da1::Params,
(Da1::Params, b'c') => {
done = true;
break;
}
_ => Da1::Idle,
};
}
}
}
});
self.caps = caps;
self.da1 = da1;
done.then(|| self.outcome())
}
pub fn finish(&self) -> Outcome {
self.outcome()
}
fn outcome(&self) -> Outcome {
self.caps.map_or(Outcome::Unsupported, Outcome::Supported)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Kind, State};
const REPLY: &[u8] =
b"\x1b]7501;?:states=idle,working,done,blocked,error:kinds=permission,question,auth\x1b\\";
const DA1: &[u8] = b"\x1b[?62;22;52c";
#[test]
fn supported_when_reply_precedes_da1() {
let mut d = Detector::new();
assert_eq!(d.feed(REPLY), None);
assert_eq!(
d.feed(DA1),
Some(Outcome::Supported(Capabilities::default()))
);
}
#[test]
fn unsupported_when_da1_arrives_alone() {
let mut d = Detector::new();
assert_eq!(d.feed(DA1), Some(Outcome::Unsupported));
}
#[test]
fn handles_replies_split_byte_by_byte() {
let mut d = Detector::new();
let mut out = None;
for chunk in [REPLY, DA1].concat().chunks(1) {
out = out.or(d.feed(chunk));
}
assert_eq!(out, Some(Outcome::Supported(Capabilities::default())));
}
#[test]
fn reduced_reply_and_timeout() {
let mut d = Detector::new();
assert_eq!(d.feed(b"\x1b]7501;?:states=idle,done:kinds=auth\x07"), None);
let Outcome::Supported(c) = d.finish() else {
panic!()
};
assert!(c.supports_state(State::Done) && !c.supports_state(State::Blocked));
assert!(c.supports_kind(Kind::Auth) && !c.supports_kind(Kind::Question));
assert_eq!(Detector::new().finish(), Outcome::Unsupported);
}
#[test]
fn other_csi_replies_are_not_da1() {
let mut d = Detector::new();
assert_eq!(d.feed(b"\x1b[12;40R\x1b[>1;10;0c"), None);
assert_eq!(d.feed(DA1), Some(Outcome::Unsupported));
}
}