1use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11use std::io::IsTerminal;
12use std::time::Duration;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum GraphicsProtocol {
18 Sixel,
20 Kitty,
22 Iterm2File,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "kebab-case")]
28pub enum CapabilityStatus {
30 Supported,
32 Unsupported,
34 Unknown,
36 Blocked,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "kebab-case")]
42pub enum EvidenceStrength {
44 Probe,
46 StrongHostSignal,
48 Terminfo,
50 WeakEnv,
52 UserOverride,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct GraphicsCapability {
59 pub protocol: GraphicsProtocol,
61 pub status: CapabilityStatus,
63 pub evidence: EvidenceStrength,
65 pub source: String,
67 pub risks: Vec<String>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct TerminalGraphicsCapabilities {
74 pub protocols: Vec<GraphicsCapability>,
76 pub preferred: Option<GraphicsProtocol>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct TerminalCapabilities {
83 pub is_tty: bool,
85 pub term: Option<String>,
87 pub terminal_program: Option<String>,
89 pub graphics: TerminalGraphicsCapabilities,
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Eq)]
94pub struct TerminalProbeEvidence {
96 pub sixel_xtsmgraphics: Option<String>,
98 pub sixel_da1: Option<String>,
100 pub kitty_graphics: Option<String>,
102 pub iterm2_capabilities: Option<String>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct TerminalCapabilityInput {
109 pub is_tty: bool,
111 pub env: BTreeMap<String, String>,
113 pub probe: TerminalProbeEvidence,
115}
116
117impl TerminalCapabilityInput {
118 pub fn from_env(is_tty: bool) -> Self {
120 Self {
121 is_tty,
122 env: std::env::vars().collect(),
123 probe: TerminalProbeEvidence::default(),
124 }
125 }
126
127 pub fn with_probe(mut self, probe: TerminalProbeEvidence) -> Self {
129 self.probe = probe;
130 self
131 }
132}
133
134impl TerminalGraphicsCapabilities {
135 pub fn unknown() -> Self {
137 Self {
138 protocols: vec![
139 capability(
140 GraphicsProtocol::Sixel,
141 CapabilityStatus::Unknown,
142 EvidenceStrength::WeakEnv,
143 "missing",
144 Vec::<String>::new(),
145 ),
146 capability(
147 GraphicsProtocol::Kitty,
148 CapabilityStatus::Unknown,
149 EvidenceStrength::WeakEnv,
150 "missing",
151 Vec::<String>::new(),
152 ),
153 capability(
154 GraphicsProtocol::Iterm2File,
155 CapabilityStatus::Unknown,
156 EvidenceStrength::WeakEnv,
157 "missing",
158 Vec::<String>::new(),
159 ),
160 ],
161 preferred: None,
162 }
163 }
164
165 pub fn by_protocol(&self, protocol: GraphicsProtocol) -> Option<&GraphicsCapability> {
167 self.protocols.iter().find(|c| c.protocol == protocol)
168 }
169}
170
171pub fn current_terminal_capabilities() -> TerminalCapabilities {
173 current_terminal_capabilities_with_timeout(Duration::from_millis(80))
174}
175
176pub fn current_terminal_capabilities_with_timeout(timeout: Duration) -> TerminalCapabilities {
178 let is_tty = std::io::stdout().is_terminal() && std::io::stdin().is_terminal();
179 let probe = if is_tty {
180 active_probe(timeout)
181 } else {
182 TerminalProbeEvidence::default()
183 };
184 detect_terminal_capabilities(TerminalCapabilityInput::from_env(is_tty).with_probe(probe))
185}
186
187pub fn detect_terminal_capabilities(input: TerminalCapabilityInput) -> TerminalCapabilities {
189 let term = env_value(&input.env, "TERM");
190 let terminal_program = env_value(&input.env, "TERM_PROGRAM");
191 let risks = base_risks(&input);
192
193 let graphics = if !input.is_tty {
194 blocked_all("non_tty", ["non_tty"])
195 } else if is_linux_console(term.as_deref()) {
196 blocked_all("TERM=linux", ["linux_console"])
197 } else if is_screen(term.as_deref()) {
198 blocked_all("TERM=screen", ["screen"])
199 } else {
200 let sixel = detect_sixel(&input, &risks);
201 let kitty = detect_kitty(&input, &risks);
202 let iterm2 = detect_iterm2(&input, &risks);
203 let preferred = choose_preferred(&[sixel.clone(), kitty.clone(), iterm2.clone()]);
204 TerminalGraphicsCapabilities {
205 protocols: vec![sixel, kitty, iterm2],
206 preferred,
207 }
208 };
209
210 TerminalCapabilities {
211 is_tty: input.is_tty,
212 term,
213 terminal_program,
214 graphics,
215 }
216}
217
218fn detect_sixel(input: &TerminalCapabilityInput, risks: &[String]) -> GraphicsCapability {
219 if let Some(reply) = input.probe.sixel_xtsmgraphics.as_deref() {
220 if xtsmgraphics_reports_sixel(reply) {
221 return capability(
222 GraphicsProtocol::Sixel,
223 CapabilityStatus::Supported,
224 EvidenceStrength::Probe,
225 "XTSMGRAPHICS",
226 risks.to_vec(),
227 );
228 }
229 }
230 if let Some(reply) = input.probe.sixel_da1.as_deref() {
231 if primary_da_reports_sixel(reply) {
232 return capability(
233 GraphicsProtocol::Sixel,
234 CapabilityStatus::Supported,
235 EvidenceStrength::Probe,
236 "DA1",
237 risks.to_vec(),
238 );
239 }
240 }
241
242 let term = env_value(&input.env, "TERM").unwrap_or_default();
243 let program = env_value(&input.env, "TERM_PROGRAM").unwrap_or_default();
244 if contains_any(&term, &["alacritty", "kitty", "ghostty"])
245 || contains_any(&program, &["Alacritty", "kitty", "Ghostty"])
246 || env_value(&input.env, "VTE_VERSION").is_some()
247 || contains_any(&program, &["gnome-terminal"])
248 || contains_any(&term, &["vte"])
249 {
250 return capability(
251 GraphicsProtocol::Sixel,
252 CapabilityStatus::Blocked,
253 EvidenceStrength::StrongHostSignal,
254 first_source(&[
255 ("TERM", &term),
256 ("TERM_PROGRAM", &program),
257 (
258 "VTE_VERSION",
259 &env_value(&input.env, "VTE_VERSION").unwrap_or_default(),
260 ),
261 ]),
262 risks.to_vec(),
263 );
264 }
265
266 if env_value(&input.env, "WT_SESSION").is_some() {
267 let mut local_risks = risks.to_vec();
268 local_risks.push("requires_windows_terminal_1_22".into());
269 return capability(
270 GraphicsProtocol::Sixel,
271 CapabilityStatus::Supported,
272 EvidenceStrength::StrongHostSignal,
273 "WT_SESSION",
274 local_risks,
275 );
276 }
277 if term == "foot"
278 || env_value(&input.env, "KONSOLE_VERSION").is_some()
279 || contains_any(&program, &["WezTerm", "mintty"])
280 || env_value(&input.env, "WEZTERM_PANE").is_some()
281 {
282 return capability(
283 GraphicsProtocol::Sixel,
284 CapabilityStatus::Supported,
285 EvidenceStrength::StrongHostSignal,
286 first_source(&[
287 ("TERM", &term),
288 ("TERM_PROGRAM", &program),
289 (
290 "KONSOLE_VERSION",
291 &env_value(&input.env, "KONSOLE_VERSION").unwrap_or_default(),
292 ),
293 (
294 "WEZTERM_PANE",
295 &env_value(&input.env, "WEZTERM_PANE").unwrap_or_default(),
296 ),
297 ]),
298 risks.to_vec(),
299 );
300 }
301
302 if contains_any(&term, &["xterm"]) {
303 return capability(
304 GraphicsProtocol::Sixel,
305 CapabilityStatus::Unknown,
306 EvidenceStrength::WeakEnv,
307 format!("TERM={term}"),
308 risks.to_vec(),
309 );
310 }
311
312 capability(
313 GraphicsProtocol::Sixel,
314 CapabilityStatus::Unknown,
315 EvidenceStrength::WeakEnv,
316 if term.is_empty() {
317 "TERM missing".to_string()
318 } else {
319 format!("TERM={term}")
320 },
321 risks.to_vec(),
322 )
323}
324
325fn detect_kitty(input: &TerminalCapabilityInput, risks: &[String]) -> GraphicsCapability {
326 if let Some(reply) = input.probe.kitty_graphics.as_deref() {
327 if reply.contains("_G") || reply.contains("OK") {
328 return capability(
329 GraphicsProtocol::Kitty,
330 CapabilityStatus::Supported,
331 EvidenceStrength::Probe,
332 "kitty-query",
333 risks.to_vec(),
334 );
335 }
336 }
337 let term = env_value(&input.env, "TERM").unwrap_or_default();
338 let program = env_value(&input.env, "TERM_PROGRAM").unwrap_or_default();
339 if contains_any(&term, &["kitty", "ghostty"])
340 || contains_any(&program, &["kitty", "Ghostty", "WezTerm"])
341 || env_value(&input.env, "WEZTERM_PANE").is_some()
342 {
343 return capability(
344 GraphicsProtocol::Kitty,
345 CapabilityStatus::Supported,
346 EvidenceStrength::StrongHostSignal,
347 first_source(&[
348 ("TERM", &term),
349 ("TERM_PROGRAM", &program),
350 (
351 "WEZTERM_PANE",
352 &env_value(&input.env, "WEZTERM_PANE").unwrap_or_default(),
353 ),
354 ]),
355 risks.to_vec(),
356 );
357 }
358 capability(
359 GraphicsProtocol::Kitty,
360 CapabilityStatus::Unknown,
361 EvidenceStrength::WeakEnv,
362 if term.is_empty() {
363 "TERM missing".to_string()
364 } else {
365 format!("TERM={term}")
366 },
367 risks.to_vec(),
368 )
369}
370
371fn detect_iterm2(input: &TerminalCapabilityInput, risks: &[String]) -> GraphicsCapability {
372 if let Some(reply) = input.probe.iterm2_capabilities.as_deref() {
373 if reply.contains("Capabilities=") || reply.contains("File=") {
374 return capability(
375 GraphicsProtocol::Iterm2File,
376 CapabilityStatus::Supported,
377 EvidenceStrength::Probe,
378 "OSC 1337;Capabilities",
379 risks.to_vec(),
380 );
381 }
382 }
383 let program = env_value(&input.env, "TERM_PROGRAM").unwrap_or_default();
384 if contains_any(&program, &["iTerm.app", "WezTerm", "mintty"]) {
385 return capability(
386 GraphicsProtocol::Iterm2File,
387 CapabilityStatus::Supported,
388 EvidenceStrength::StrongHostSignal,
389 format!("TERM_PROGRAM={program}"),
390 risks.to_vec(),
391 );
392 }
393 capability(
394 GraphicsProtocol::Iterm2File,
395 CapabilityStatus::Unknown,
396 EvidenceStrength::WeakEnv,
397 if program.is_empty() {
398 "TERM_PROGRAM missing".to_string()
399 } else {
400 format!("TERM_PROGRAM={program}")
401 },
402 risks.to_vec(),
403 )
404}
405
406fn choose_preferred(capabilities: &[GraphicsCapability]) -> Option<GraphicsProtocol> {
407 capabilities
408 .iter()
409 .find(|c| c.status == CapabilityStatus::Supported && c.evidence == EvidenceStrength::Probe)
410 .or_else(|| {
411 capabilities
412 .iter()
413 .find(|c| c.status == CapabilityStatus::Supported)
414 })
415 .map(|c| c.protocol)
416}
417
418fn blocked_all(
419 source: &str,
420 risks: impl IntoIterator<Item = impl Into<String>>,
421) -> TerminalGraphicsCapabilities {
422 let risks: Vec<String> = risks.into_iter().map(Into::into).collect();
423 TerminalGraphicsCapabilities {
424 protocols: vec![
425 capability(
426 GraphicsProtocol::Sixel,
427 CapabilityStatus::Blocked,
428 EvidenceStrength::StrongHostSignal,
429 source,
430 risks.clone(),
431 ),
432 capability(
433 GraphicsProtocol::Kitty,
434 CapabilityStatus::Blocked,
435 EvidenceStrength::StrongHostSignal,
436 source,
437 risks.clone(),
438 ),
439 capability(
440 GraphicsProtocol::Iterm2File,
441 CapabilityStatus::Blocked,
442 EvidenceStrength::StrongHostSignal,
443 source,
444 risks,
445 ),
446 ],
447 preferred: None,
448 }
449}
450
451fn base_risks(input: &TerminalCapabilityInput) -> Vec<String> {
452 let mut risks = Vec::new();
453 if env_value(&input.env, "TMUX").is_some() || is_tmux(env_value(&input.env, "TERM").as_deref())
454 {
455 risks.push("tmux".into());
456 }
457 if env_value(&input.env, "SSH_CONNECTION").is_some()
458 || env_value(&input.env, "SSH_TTY").is_some()
459 {
460 risks.push("ssh".into());
461 }
462 risks
463}
464
465fn capability(
466 protocol: GraphicsProtocol,
467 status: CapabilityStatus,
468 evidence: EvidenceStrength,
469 source: impl Into<String>,
470 risks: impl IntoIterator<Item = impl Into<String>>,
471) -> GraphicsCapability {
472 GraphicsCapability {
473 protocol,
474 status,
475 evidence,
476 source: source.into(),
477 risks: risks.into_iter().map(Into::into).collect(),
478 }
479}
480
481fn env_value(env: &BTreeMap<String, String>, key: &str) -> Option<String> {
482 env.get(key).filter(|v| !v.is_empty()).cloned()
483}
484
485fn contains_any(value: &str, needles: &[&str]) -> bool {
486 let lower = value.to_ascii_lowercase();
487 needles
488 .iter()
489 .any(|needle| lower.contains(&needle.to_ascii_lowercase()))
490}
491
492fn is_linux_console(term: Option<&str>) -> bool {
493 matches!(term, Some("linux"))
494}
495
496fn is_screen(term: Option<&str>) -> bool {
497 term.is_some_and(|t| t.starts_with("screen") || t == "screen")
498}
499
500fn is_tmux(term: Option<&str>) -> bool {
501 term.is_some_and(|t| t.starts_with("tmux") || t.contains("tmux"))
502}
503
504fn first_source(candidates: &[(&str, &str)]) -> String {
505 for (key, value) in candidates {
506 if !value.is_empty() {
507 return format!("{key}={value}");
508 }
509 }
510 "unknown".into()
511}
512
513pub fn primary_da_reports_sixel(reply: &str) -> bool {
515 reply
516 .split('\x1b')
517 .filter_map(|part| part.strip_prefix("[?"))
518 .filter_map(|part| part.split('c').next())
519 .flat_map(|params| params.split(';'))
520 .any(|param| param == "4")
521}
522
523pub fn xtsmgraphics_reports_sixel(reply: &str) -> bool {
525 reply.contains("\x1b[?") && reply.contains('S')
526}
527
528#[cfg(unix)]
529fn active_probe(timeout: Duration) -> TerminalProbeEvidence {
530 use std::fs::OpenOptions;
531 use std::io::{Read, Write};
532 use std::os::fd::AsRawFd;
533 use std::time::Instant;
534
535 let Ok(mut tty) = OpenOptions::new().read(true).write(true).open("/dev/tty") else {
536 return TerminalProbeEvidence::default();
537 };
538 let fd = tty.as_raw_fd();
539 let mut old_termios = std::mem::MaybeUninit::<libc::termios>::uninit();
540 let have_termios = unsafe { libc::tcgetattr(fd, old_termios.as_mut_ptr()) == 0 };
541 let old_termios = if have_termios {
542 Some(unsafe { old_termios.assume_init() })
543 } else {
544 None
545 };
546 if let Some(mut raw) = old_termios {
547 raw.c_lflag &= !(libc::ICANON | libc::ECHO);
548 raw.c_cc[libc::VMIN] = 0;
549 raw.c_cc[libc::VTIME] = 0;
550 let _ = unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) };
551 }
552 let old_flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
553 if old_flags >= 0 {
554 let _ = unsafe { libc::fcntl(fd, libc::F_SETFL, old_flags | libc::O_NONBLOCK) };
555 }
556
557 let _ = tty.write_all(
558 b"\x1b[c\x1b[?2;1;0S\x1b_Gi=running-process-probe,a=q;\x1b\\\x1b]1337;Capabilities\x07",
559 );
560 let _ = tty.flush();
561
562 let deadline = Instant::now() + timeout;
563 let mut buf = Vec::new();
564 while Instant::now() < deadline {
565 let mut chunk = [0_u8; 512];
566 match tty.read(&mut chunk) {
567 Ok(0) => std::thread::sleep(Duration::from_millis(5)),
568 Ok(n) => buf.extend_from_slice(&chunk[..n]),
569 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
570 std::thread::sleep(Duration::from_millis(5));
571 }
572 Err(_) => break,
573 }
574 }
575
576 if old_flags >= 0 {
577 let _ = unsafe { libc::fcntl(fd, libc::F_SETFL, old_flags) };
578 }
579 if let Some(old) = old_termios {
580 let _ = unsafe { libc::tcsetattr(fd, libc::TCSANOW, &old) };
581 }
582
583 let reply = String::from_utf8_lossy(&buf).into_owned();
584 TerminalProbeEvidence {
585 sixel_xtsmgraphics: reply.contains('S').then(|| reply.clone()),
586 sixel_da1: reply.contains("[?").then(|| reply.clone()),
587 kitty_graphics: reply.contains("_G").then(|| reply.clone()),
588 iterm2_capabilities: reply.contains("Capabilities=").then_some(reply),
589 }
590}
591
592#[cfg(not(unix))]
593fn active_probe(_timeout: Duration) -> TerminalProbeEvidence {
594 TerminalProbeEvidence::default()
595}
596
597#[cfg(feature = "client")]
598pub fn terminal_graphics_capabilities_to_proto(
600 caps: &TerminalGraphicsCapabilities,
601) -> crate::proto::daemon::TerminalGraphicsCapabilities {
602 crate::proto::daemon::TerminalGraphicsCapabilities {
603 protocols: caps
604 .protocols
605 .iter()
606 .map(graphics_capability_to_proto)
607 .collect(),
608 preferred: caps
609 .preferred
610 .map(proto_graphics_protocol)
611 .unwrap_or(crate::proto::daemon::GraphicsProtocol::Unspecified)
612 as i32,
613 }
614}
615
616#[cfg(feature = "client")]
617pub fn terminal_graphics_capabilities_from_proto(
619 caps: &crate::proto::daemon::TerminalGraphicsCapabilities,
620) -> TerminalGraphicsCapabilities {
621 let protocols = caps
622 .protocols
623 .iter()
624 .map(graphics_capability_from_proto)
625 .collect();
626 TerminalGraphicsCapabilities {
627 protocols,
628 preferred: graphics_protocol_from_i32(caps.preferred),
629 }
630}
631
632#[cfg(feature = "client")]
633fn graphics_capability_to_proto(
634 capability: &GraphicsCapability,
635) -> crate::proto::daemon::TerminalGraphicsCapability {
636 crate::proto::daemon::TerminalGraphicsCapability {
637 protocol: proto_graphics_protocol(capability.protocol) as i32,
638 status: proto_capability_status(capability.status) as i32,
639 evidence: proto_evidence_strength(capability.evidence) as i32,
640 source: capability.source.clone(),
641 risks: capability.risks.clone(),
642 }
643}
644
645#[cfg(feature = "client")]
646fn graphics_capability_from_proto(
647 capability: &crate::proto::daemon::TerminalGraphicsCapability,
648) -> GraphicsCapability {
649 GraphicsCapability {
650 protocol: graphics_protocol_from_i32(capability.protocol)
651 .unwrap_or(GraphicsProtocol::Sixel),
652 status: capability_status_from_i32(capability.status),
653 evidence: evidence_strength_from_i32(capability.evidence),
654 source: capability.source.clone(),
655 risks: capability.risks.clone(),
656 }
657}
658
659#[cfg(feature = "client")]
660fn proto_graphics_protocol(protocol: GraphicsProtocol) -> crate::proto::daemon::GraphicsProtocol {
661 match protocol {
662 GraphicsProtocol::Sixel => crate::proto::daemon::GraphicsProtocol::Sixel,
663 GraphicsProtocol::Kitty => crate::proto::daemon::GraphicsProtocol::Kitty,
664 GraphicsProtocol::Iterm2File => crate::proto::daemon::GraphicsProtocol::Iterm2File,
665 }
666}
667
668#[cfg(feature = "client")]
669fn graphics_protocol_from_i32(protocol: i32) -> Option<GraphicsProtocol> {
670 match crate::proto::daemon::GraphicsProtocol::try_from(protocol).ok()? {
671 crate::proto::daemon::GraphicsProtocol::Sixel => Some(GraphicsProtocol::Sixel),
672 crate::proto::daemon::GraphicsProtocol::Kitty => Some(GraphicsProtocol::Kitty),
673 crate::proto::daemon::GraphicsProtocol::Iterm2File => Some(GraphicsProtocol::Iterm2File),
674 crate::proto::daemon::GraphicsProtocol::Unspecified => None,
675 }
676}
677
678#[cfg(feature = "client")]
679fn proto_capability_status(status: CapabilityStatus) -> crate::proto::daemon::CapabilityStatus {
680 match status {
681 CapabilityStatus::Supported => crate::proto::daemon::CapabilityStatus::Supported,
682 CapabilityStatus::Unsupported => crate::proto::daemon::CapabilityStatus::Unsupported,
683 CapabilityStatus::Unknown => crate::proto::daemon::CapabilityStatus::Unknown,
684 CapabilityStatus::Blocked => crate::proto::daemon::CapabilityStatus::Blocked,
685 }
686}
687
688#[cfg(feature = "client")]
689fn capability_status_from_i32(status: i32) -> CapabilityStatus {
690 match crate::proto::daemon::CapabilityStatus::try_from(status)
691 .unwrap_or(crate::proto::daemon::CapabilityStatus::Unknown)
692 {
693 crate::proto::daemon::CapabilityStatus::Supported => CapabilityStatus::Supported,
694 crate::proto::daemon::CapabilityStatus::Unsupported => CapabilityStatus::Unsupported,
695 crate::proto::daemon::CapabilityStatus::Unknown
696 | crate::proto::daemon::CapabilityStatus::Unspecified => CapabilityStatus::Unknown,
697 crate::proto::daemon::CapabilityStatus::Blocked => CapabilityStatus::Blocked,
698 }
699}
700
701#[cfg(feature = "client")]
702fn proto_evidence_strength(evidence: EvidenceStrength) -> crate::proto::daemon::EvidenceStrength {
703 match evidence {
704 EvidenceStrength::Probe => crate::proto::daemon::EvidenceStrength::Probe,
705 EvidenceStrength::StrongHostSignal => {
706 crate::proto::daemon::EvidenceStrength::StrongHostSignal
707 }
708 EvidenceStrength::Terminfo => crate::proto::daemon::EvidenceStrength::Terminfo,
709 EvidenceStrength::WeakEnv => crate::proto::daemon::EvidenceStrength::WeakEnv,
710 EvidenceStrength::UserOverride => crate::proto::daemon::EvidenceStrength::UserOverride,
711 }
712}
713
714#[cfg(feature = "client")]
715fn evidence_strength_from_i32(evidence: i32) -> EvidenceStrength {
716 match crate::proto::daemon::EvidenceStrength::try_from(evidence)
717 .unwrap_or(crate::proto::daemon::EvidenceStrength::WeakEnv)
718 {
719 crate::proto::daemon::EvidenceStrength::Probe => EvidenceStrength::Probe,
720 crate::proto::daemon::EvidenceStrength::StrongHostSignal => {
721 EvidenceStrength::StrongHostSignal
722 }
723 crate::proto::daemon::EvidenceStrength::Terminfo => EvidenceStrength::Terminfo,
724 crate::proto::daemon::EvidenceStrength::WeakEnv
725 | crate::proto::daemon::EvidenceStrength::Unspecified => EvidenceStrength::WeakEnv,
726 crate::proto::daemon::EvidenceStrength::UserOverride => EvidenceStrength::UserOverride,
727 }
728}
729
730#[cfg(test)]
731mod tests {
732 use super::*;
733
734 fn input(term: &str, pairs: &[(&str, &str)]) -> TerminalCapabilityInput {
735 let mut env = BTreeMap::new();
736 if !term.is_empty() {
737 env.insert("TERM".into(), term.into());
738 }
739 for (k, v) in pairs {
740 env.insert((*k).into(), (*v).into());
741 }
742 TerminalCapabilityInput {
743 is_tty: true,
744 env,
745 probe: TerminalProbeEvidence::default(),
746 }
747 }
748
749 #[test]
750 fn weak_xterm_does_not_confirm_sixel() {
751 let caps = detect_terminal_capabilities(input("xterm-256color", &[]));
752 let sixel = caps.graphics.by_protocol(GraphicsProtocol::Sixel).unwrap();
753 assert_eq!(sixel.status, CapabilityStatus::Unknown);
754 assert_eq!(sixel.evidence, EvidenceStrength::WeakEnv);
755 assert_eq!(caps.graphics.preferred, None);
756 }
757
758 #[test]
759 fn da1_probe_confirms_sixel() {
760 let mut case = input("xterm-256color", &[]);
761 case.probe.sixel_da1 = Some("\x1b[?62;4;22c".into());
762 let caps = detect_terminal_capabilities(case);
763 let sixel = caps.graphics.by_protocol(GraphicsProtocol::Sixel).unwrap();
764 assert_eq!(sixel.status, CapabilityStatus::Supported);
765 assert_eq!(sixel.evidence, EvidenceStrength::Probe);
766 assert_eq!(caps.graphics.preferred, Some(GraphicsProtocol::Sixel));
767 }
768
769 #[test]
770 fn vt100_da_does_not_confirm_sixel() {
771 assert!(!primary_da_reports_sixel("\x1b[?1;2c"));
772 assert!(!primary_da_reports_sixel("\x1b[?62;22c"));
773 }
774}