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