use super::caps::{Capabilities, CapsReply, WrapKind};
use super::verbs::tmux_wrap;
use super::Terminal;
use crate::base::PixelSize;
const KITTY_GFX_PROBE_ID: &str = "4242";
const KITTY_GFX_WRAPPED_ID: &str = "4343";
pub const TMUX_GRACE: std::time::Duration = std::time::Duration::from_millis(150);
#[derive(Debug, Default)]
pub struct ActiveProbe {
done: bool,
saw_any: bool,
tmux: bool,
sentinel_seen: bool,
wrapped_kitty_seen: bool,
xtversion_seen: u8,
}
impl ActiveProbe {
pub fn new() -> Self {
Self::default()
}
pub fn for_caps(caps: &Capabilities) -> Self {
ActiveProbe {
tmux: caps.in_tmux,
..Self::default()
}
}
fn direct_queries(b: &mut Vec<u8>) {
b.extend_from_slice(b"\x1b[?u");
b.extend_from_slice(b"\x1b[?2026$p");
b.extend_from_slice(b"\x1b[?1016$p");
b.extend_from_slice(b"\x1b[>0q");
b.extend_from_slice(b"\x1b[?1;1;0S");
b.extend_from_slice(b"\x1b[16t");
b.extend_from_slice(
format!("\x1b_Gi={KITTY_GFX_PROBE_ID},s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\").as_bytes(),
);
}
fn wrapped_queries(b: &mut Vec<u8>) {
b.extend_from_slice(&tmux_wrap(b"\x1b[>0q"));
b.extend_from_slice(&tmux_wrap(
format!("\x1b_Gi={KITTY_GFX_WRAPPED_ID},s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\").as_bytes(),
));
}
pub fn query_bytes() -> Vec<u8> {
let mut b = Vec::with_capacity(160);
Self::direct_queries(&mut b);
b.extend_from_slice(b"\x1b[c"); b
}
pub fn full_query_bytes(&self) -> Vec<u8> {
let mut b = Vec::with_capacity(224);
Self::direct_queries(&mut b);
if self.tmux {
Self::wrapped_queries(&mut b);
}
b.extend_from_slice(b"\x1b[c");
b
}
pub fn on_reply(&mut self, reply: &CapsReply, caps: &mut Capabilities) -> bool {
self.saw_any = true;
match reply {
CapsReply::KittyKeyboard { .. } => {
caps.kitty_keyboard = true;
}
CapsReply::DecMode { mode: 2026, status } => {
caps.sync_output_2026 = matches!(status, 1 | 2);
}
CapsReply::DecMode { mode: 1016, status } => {
caps.sgr_pixel_mouse = matches!(status, 1 | 2);
}
CapsReply::DecMode { .. } => {}
CapsReply::XtVersion { text } => {
self.xtversion_seen = self.xtversion_seen.saturating_add(1);
if !self.tmux {
caps.term_version = Some(text.clone());
} else if self.xtversion_seen == 1 {
caps.tmux_version = Some(text.clone());
} else {
caps.term_version = Some(text.clone());
caps.graphics_wrap = Some(WrapKind::Tmux);
let lower = text.to_ascii_lowercase();
if lower.contains("iterm") || lower.contains("wezterm") {
caps.iterm2_images = true;
}
}
}
CapsReply::KittyGraphics { raw } => {
let s = String::from_utf8_lossy(raw);
let ok = s.contains("OK");
if ok && s.contains(&format!("i={KITTY_GFX_PROBE_ID}")) {
caps.kitty_graphics = true;
}
if ok && self.tmux && s.contains(&format!("i={KITTY_GFX_WRAPPED_ID}")) {
caps.kitty_graphics = true;
caps.graphics_wrap = Some(WrapKind::Tmux);
self.wrapped_kitty_seen = true;
}
}
CapsReply::XtSmGraphics {
item: 1,
status: 0,
value,
} => {
caps.sixel_max_registers = Some((*value).min(u16::MAX as u32) as u16);
}
CapsReply::XtSmGraphics { .. } => {}
CapsReply::WindowOp { op: 6, a, b } => {
let (h, w) = (*a, *b);
if (1..=512).contains(&w) && (1..=512).contains(&h) {
caps.cell_pixel_size = Some(PixelSize::new(w as u16, h as u16));
}
}
CapsReply::WindowOp { .. } => {}
CapsReply::PrimaryDa { params } => {
if params.contains(&4) {
caps.sixel = true;
}
self.sentinel_seen = true;
self.done = !self.awaiting_wrapped();
}
CapsReply::XtGetTcap { .. } | CapsReply::CursorPos { .. } | CapsReply::Osc { .. } => {}
}
if self.sentinel_seen && !self.awaiting_wrapped() {
self.done = true;
}
self.done
}
pub fn is_done(&self) -> bool {
self.done
}
pub fn sentinel_passed(&self) -> bool {
self.sentinel_seen
}
pub fn awaiting_wrapped(&self) -> bool {
self.tmux && !(self.wrapped_kitty_seen && self.xtversion_seen >= 2)
}
pub fn saw_any_reply(&self) -> bool {
self.saw_any
}
}
pub fn refresh_cell_pixel_size(
term: &mut dyn Terminal,
caps: &mut Capabilities,
) -> Option<PixelSize> {
if let Some(px) = term.cell_pixel_size() {
caps.cell_pixel_size = Some(px);
}
caps.cell_pixel_size
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probe_folds_replies_and_ends_on_da1() {
let mut caps = Capabilities::default();
let mut probe = ActiveProbe::new();
assert!(!probe.on_reply(&CapsReply::KittyKeyboard { flags: 0 }, &mut caps));
assert!(caps.kitty_keyboard);
assert!(!probe.on_reply(
&CapsReply::DecMode {
mode: 2026,
status: 2
},
&mut caps
));
assert!(caps.sync_output_2026);
assert!(!probe.on_reply(
&CapsReply::XtVersion {
text: "kitty 0.38.1".into()
},
&mut caps
));
assert_eq!(caps.term_version.as_deref(), Some("kitty 0.38.1"));
assert!(!probe.on_reply(
&CapsReply::KittyGraphics {
raw: b"Gi=4242;OK".to_vec()
},
&mut caps
));
assert!(caps.kitty_graphics);
assert!(!probe.on_reply(
&CapsReply::XtSmGraphics {
item: 1,
status: 0,
value: 1024
},
&mut caps
));
assert_eq!(caps.sixel_max_registers, Some(1024));
assert!(!probe.on_reply(&CapsReply::WindowOp { op: 6, a: 18, b: 9 }, &mut caps));
assert_eq!(caps.cell_pixel_size, Some(PixelSize::new(9, 18)));
assert!(probe.on_reply(
&CapsReply::PrimaryDa {
params: vec![62, 4, 22]
},
&mut caps
));
assert!(caps.sixel && probe.is_done());
}
#[test]
fn probe_lowers_env_optimism_with_evidence() {
let mut caps = Capabilities {
sync_output_2026: true,
..Capabilities::default()
};
let mut probe = ActiveProbe::new();
probe.on_reply(
&CapsReply::DecMode {
mode: 2026,
status: 4,
},
&mut caps,
);
assert!(!caps.sync_output_2026);
}
#[test]
fn probe_rejects_foreign_graphics_reply() {
let mut caps = Capabilities::default();
let mut probe = ActiveProbe::new();
probe.on_reply(
&CapsReply::KittyGraphics {
raw: b"Gi=31;OK".to_vec(),
},
&mut caps,
);
assert!(!caps.kitty_graphics);
probe.on_reply(
&CapsReply::KittyGraphics {
raw: b"Gi=4242;EINVAL:bad".to_vec(),
},
&mut caps,
);
assert!(!caps.kitty_graphics);
}
#[test]
fn probe_rejects_insane_replies() {
let mut caps = Capabilities::default();
let mut probe = ActiveProbe::new();
probe.on_reply(
&CapsReply::XtSmGraphics {
item: 1,
status: 3,
value: 0,
},
&mut caps,
);
assert_eq!(caps.sixel_max_registers, None);
probe.on_reply(
&CapsReply::XtSmGraphics {
item: 1,
status: 0,
value: 1_000_000,
},
&mut caps,
);
assert_eq!(caps.sixel_max_registers, Some(u16::MAX));
probe.on_reply(&CapsReply::WindowOp { op: 6, a: 0, b: 9 }, &mut caps);
assert_eq!(caps.cell_pixel_size, None);
probe.on_reply(
&CapsReply::WindowOp {
op: 6,
a: 20_000,
b: 9,
},
&mut caps,
);
assert_eq!(caps.cell_pixel_size, None);
probe.on_reply(
&CapsReply::WindowOp {
op: 4,
a: 720,
b: 1280,
},
&mut caps,
);
assert_eq!(caps.cell_pixel_size, None);
}
#[test]
fn query_batch_ends_with_da1_sentinel() {
let q = ActiveProbe::query_bytes();
assert!(q.ends_with(b"\x1b[c"));
let s = String::from_utf8(q).unwrap();
assert!(s.contains("\x1b[?u"));
assert!(s.contains("\x1b[?2026$p"));
assert!(s.contains("\x1b[?1016$p"));
assert!(s.contains("\x1b[>0q"));
assert!(s.contains("\x1b[?1;1;0S"));
assert!(s.contains("\x1b[16t"));
assert!(s.contains("a=q"));
}
fn tmux_caps() -> Capabilities {
Capabilities {
in_tmux: true,
needs_tmux_passthrough: true,
..Capabilities::default()
}
}
#[test]
fn tmux_probe_batch_appends_wrapped_section() {
let plain = ActiveProbe::for_caps(&Capabilities::default());
assert_eq!(plain.full_query_bytes(), ActiveProbe::query_bytes());
let probe = ActiveProbe::for_caps(&tmux_caps());
let q = probe.full_query_bytes();
assert!(q.ends_with(b"\x1b[c"), "sentinel stays last");
let s = String::from_utf8_lossy(&q).into_owned();
assert!(s.contains("\x1bPtmux;"), "wrapped section present");
assert!(s.contains("\x1b\x1b_Gi=4343"), "{s:?}");
assert!(s.contains("\x1b_Gi=4242,"));
}
#[test]
fn tmux_passthrough_verified_by_wrapped_replies() {
let mut caps = tmux_caps();
let mut probe = ActiveProbe::for_caps(&caps.clone());
probe.on_reply(
&CapsReply::XtVersion {
text: "tmux 3.5a".into(),
},
&mut caps,
);
assert_eq!(caps.tmux_version.as_deref(), Some("tmux 3.5a"));
assert_eq!(caps.term_version, None);
probe.on_reply(
&CapsReply::KittyGraphics {
raw: b"Gi=4343;OK".to_vec(),
},
&mut caps,
);
assert!(caps.kitty_graphics);
assert_eq!(caps.graphics_wrap, Some(WrapKind::Tmux));
let done_before_sentinel = probe.on_reply(
&CapsReply::XtVersion {
text: "kitty 0.38.1".into(),
},
&mut caps,
);
assert!(!done_before_sentinel, "sentinel still owed");
assert_eq!(caps.term_version.as_deref(), Some("kitty 0.38.1"));
assert!(probe.on_reply(&CapsReply::PrimaryDa { params: vec![62] }, &mut caps));
assert!(!probe.awaiting_wrapped());
}
#[test]
fn tmux_passthrough_off_stays_conservative() {
let mut caps = tmux_caps();
let mut probe = ActiveProbe::for_caps(&caps.clone());
probe.on_reply(
&CapsReply::XtVersion {
text: "tmux 3.3".into(),
},
&mut caps,
);
assert!(!probe.on_reply(&CapsReply::PrimaryDa { params: vec![62] }, &mut caps));
assert!(probe.sentinel_passed() && probe.awaiting_wrapped());
assert!(!caps.kitty_graphics);
assert_eq!(caps.graphics_wrap, None);
assert!(!caps.iterm2_images);
}
#[test]
fn tmux_outer_iterm2_unlocks_iterm_images_only() {
let mut caps = tmux_caps();
let mut probe = ActiveProbe::for_caps(&caps.clone());
probe.on_reply(
&CapsReply::XtVersion {
text: "tmux 3.4".into(),
},
&mut caps,
);
assert!(!probe.on_reply(&CapsReply::PrimaryDa { params: vec![64] }, &mut caps));
probe.on_reply(
&CapsReply::XtVersion {
text: "iTerm2 3.5.10".into(),
},
&mut caps,
);
assert!(caps.iterm2_images, "outer iTerm2 named through the wrapper");
assert_eq!(caps.graphics_wrap, Some(WrapKind::Tmux));
assert!(!caps.kitty_graphics, "no kitty proof, no kitty claim");
assert_eq!(caps.term_version.as_deref(), Some("iTerm2 3.5.10"));
}
#[test]
fn outside_tmux_wrapped_ids_and_second_versions_are_inert() {
let mut caps = Capabilities::default();
let mut probe = ActiveProbe::new();
probe.on_reply(
&CapsReply::KittyGraphics {
raw: b"Gi=4343;OK".to_vec(),
},
&mut caps,
);
assert_eq!(caps.graphics_wrap, None);
assert!(!caps.kitty_graphics);
probe.on_reply(
&CapsReply::XtVersion {
text: "kitty 0.1".into(),
},
&mut caps,
);
probe.on_reply(
&CapsReply::XtVersion {
text: "kitty 0.2".into(),
},
&mut caps,
);
assert_eq!(caps.term_version.as_deref(), Some("kitty 0.2"));
assert_eq!(caps.graphics_wrap, None);
}
#[test]
fn decrqm_1016_folds_pixel_mouse_bit() {
let mut caps = Capabilities::default();
let mut probe = ActiveProbe::new();
probe.on_reply(
&CapsReply::DecMode {
mode: 1016,
status: 2,
},
&mut caps,
);
assert!(caps.sgr_pixel_mouse);
probe.on_reply(
&CapsReply::DecMode {
mode: 1016,
status: 0,
},
&mut caps,
);
assert!(!caps.sgr_pixel_mouse);
}
}