Skip to main content

running_process/
terminal_graphics.rs

1//! Terminal graphics capability detection and reporting.
2//!
3//! The API is intentionally evidence-shaped instead of boolean-shaped. A
4//! caller such as `clud` needs to know whether graphics support came from a
5//! live probe, a strong host hint, weak environment identity, or a hard
6//! negative. `auto` policies can then stay conservative while still surfacing
7//! useful diagnostics.
8
9use 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")]
16/// Terminal graphics protocols recognized by capability detection.
17pub enum GraphicsProtocol {
18    /// Sixel bitmap graphics escape sequences.
19    Sixel,
20    /// Kitty graphics protocol escape sequences.
21    Kitty,
22    /// iTerm2 inline file protocol escape sequences.
23    Iterm2File,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "kebab-case")]
28/// Detection result for a terminal graphics protocol.
29pub enum CapabilityStatus {
30    /// The protocol is available for use.
31    Supported,
32    /// The protocol is known to be unavailable.
33    Unsupported,
34    /// Available evidence is not strong enough to decide.
35    Unknown,
36    /// The current terminal/session prevents using the protocol safely.
37    Blocked,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "kebab-case")]
42/// Confidence level for graphics capability evidence.
43pub enum EvidenceStrength {
44    /// A live terminal probe confirmed the capability.
45    Probe,
46    /// A strong terminal-host signal identified expected support.
47    StrongHostSignal,
48    /// Terminfo advertised the capability.
49    Terminfo,
50    /// Environment identity provides only weak evidence.
51    WeakEnv,
52    /// A user setting supplied the evidence.
53    UserOverride,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57/// Detection result for one terminal graphics protocol.
58pub struct GraphicsCapability {
59    /// Protocol described by this result.
60    pub protocol: GraphicsProtocol,
61    /// Whether the protocol is usable, unavailable, unknown, or blocked.
62    pub status: CapabilityStatus,
63    /// Strength of the evidence behind `status`.
64    pub evidence: EvidenceStrength,
65    /// Human-readable source of the evidence.
66    pub source: String,
67    /// Context markers that may affect rendering reliability.
68    pub risks: Vec<String>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72/// Graphics capability set detected for a terminal.
73pub struct TerminalGraphicsCapabilities {
74    /// Per-protocol capability results.
75    pub protocols: Vec<GraphicsCapability>,
76    /// Preferred protocol when one is supported.
77    pub preferred: Option<GraphicsProtocol>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81/// Terminal capability snapshot used by clients and diagnostics.
82pub struct TerminalCapabilities {
83    /// Whether standard input and output are attached to a terminal.
84    pub is_tty: bool,
85    /// Value of the `TERM` environment variable, when present.
86    pub term: Option<String>,
87    /// Value of the `TERM_PROGRAM` environment variable, when present.
88    pub terminal_program: Option<String>,
89    /// Detected terminal graphics capabilities.
90    pub graphics: TerminalGraphicsCapabilities,
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Eq)]
94/// Raw terminal replies collected during active graphics probing.
95pub struct TerminalProbeEvidence {
96    /// Reply to the Sixel XTSMGRAPHICS query.
97    pub sixel_xtsmgraphics: Option<String>,
98    /// Primary device attributes reply used for Sixel detection.
99    pub sixel_da1: Option<String>,
100    /// Reply to the Kitty graphics query.
101    pub kitty_graphics: Option<String>,
102    /// Reply to the iTerm2 capabilities query.
103    pub iterm2_capabilities: Option<String>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107/// Inputs used to detect terminal capabilities.
108pub struct TerminalCapabilityInput {
109    /// Whether the inspected streams are attached to a terminal.
110    pub is_tty: bool,
111    /// Environment variables used as host and session hints.
112    pub env: BTreeMap<String, String>,
113    /// Active probe replies to incorporate into detection.
114    pub probe: TerminalProbeEvidence,
115}
116
117impl TerminalCapabilityInput {
118    /// Builds detection input from the current process environment.
119    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    /// Returns this input with active probe evidence attached.
128    pub fn with_probe(mut self, probe: TerminalProbeEvidence) -> Self {
129        self.probe = probe;
130        self
131    }
132}
133
134impl TerminalGraphicsCapabilities {
135    /// Returns an all-protocol unknown capability set.
136    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    /// Finds the capability result for `protocol`.
166    pub fn by_protocol(&self, protocol: GraphicsProtocol) -> Option<&GraphicsCapability> {
167        self.protocols.iter().find(|c| c.protocol == protocol)
168    }
169}
170
171/// Detects capabilities for the current terminal with the default probe timeout.
172pub fn current_terminal_capabilities() -> TerminalCapabilities {
173    current_terminal_capabilities_with_timeout(Duration::from_millis(80))
174}
175
176/// Detects capabilities for the current terminal using `timeout` for active probes.
177pub 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
187/// Detects terminal capabilities from explicit environment and probe evidence.
188pub 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
513/// Returns whether a primary device attributes reply advertises Sixel support.
514pub 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
523/// Returns whether an XTSMGRAPHICS reply indicates Sixel graphics support.
524pub fn xtsmgraphics_reports_sixel(reply: &str) -> bool {
525    reply.contains("\x1b[?") && reply.contains('S')
526}
527
528fn active_probe(timeout: Duration) -> TerminalProbeEvidence {
529    let reply =
530        running_process_platform_internal::platform::terminal_graphics::active_graphics_probe(
531            timeout,
532        );
533    TerminalProbeEvidence {
534        sixel_xtsmgraphics: reply.sixel_xtsmgraphics,
535        sixel_da1: reply.sixel_da1,
536        kitty_graphics: reply.kitty_graphics,
537        iterm2_capabilities: reply.iterm2_capabilities,
538    }
539}
540
541#[cfg(feature = "client")]
542/// Converts terminal graphics capabilities into the daemon protobuf form.
543pub fn terminal_graphics_capabilities_to_proto(
544    caps: &TerminalGraphicsCapabilities,
545) -> crate::proto::daemon::TerminalGraphicsCapabilities {
546    crate::proto::daemon::TerminalGraphicsCapabilities {
547        protocols: caps
548            .protocols
549            .iter()
550            .map(graphics_capability_to_proto)
551            .collect(),
552        preferred: caps
553            .preferred
554            .map(proto_graphics_protocol)
555            .unwrap_or(crate::proto::daemon::GraphicsProtocol::Unspecified)
556            as i32,
557    }
558}
559
560#[cfg(feature = "client")]
561/// Converts daemon protobuf terminal graphics capabilities into the Rust form.
562pub fn terminal_graphics_capabilities_from_proto(
563    caps: &crate::proto::daemon::TerminalGraphicsCapabilities,
564) -> TerminalGraphicsCapabilities {
565    let protocols = caps
566        .protocols
567        .iter()
568        .map(graphics_capability_from_proto)
569        .collect();
570    TerminalGraphicsCapabilities {
571        protocols,
572        preferred: graphics_protocol_from_i32(caps.preferred),
573    }
574}
575
576#[cfg(feature = "client")]
577fn graphics_capability_to_proto(
578    capability: &GraphicsCapability,
579) -> crate::proto::daemon::TerminalGraphicsCapability {
580    crate::proto::daemon::TerminalGraphicsCapability {
581        protocol: proto_graphics_protocol(capability.protocol) as i32,
582        status: proto_capability_status(capability.status) as i32,
583        evidence: proto_evidence_strength(capability.evidence) as i32,
584        source: capability.source.clone(),
585        risks: capability.risks.clone(),
586    }
587}
588
589#[cfg(feature = "client")]
590fn graphics_capability_from_proto(
591    capability: &crate::proto::daemon::TerminalGraphicsCapability,
592) -> GraphicsCapability {
593    GraphicsCapability {
594        protocol: graphics_protocol_from_i32(capability.protocol)
595            .unwrap_or(GraphicsProtocol::Sixel),
596        status: capability_status_from_i32(capability.status),
597        evidence: evidence_strength_from_i32(capability.evidence),
598        source: capability.source.clone(),
599        risks: capability.risks.clone(),
600    }
601}
602
603#[cfg(feature = "client")]
604fn proto_graphics_protocol(protocol: GraphicsProtocol) -> crate::proto::daemon::GraphicsProtocol {
605    match protocol {
606        GraphicsProtocol::Sixel => crate::proto::daemon::GraphicsProtocol::Sixel,
607        GraphicsProtocol::Kitty => crate::proto::daemon::GraphicsProtocol::Kitty,
608        GraphicsProtocol::Iterm2File => crate::proto::daemon::GraphicsProtocol::Iterm2File,
609    }
610}
611
612#[cfg(feature = "client")]
613fn graphics_protocol_from_i32(protocol: i32) -> Option<GraphicsProtocol> {
614    match crate::proto::daemon::GraphicsProtocol::try_from(protocol).ok()? {
615        crate::proto::daemon::GraphicsProtocol::Sixel => Some(GraphicsProtocol::Sixel),
616        crate::proto::daemon::GraphicsProtocol::Kitty => Some(GraphicsProtocol::Kitty),
617        crate::proto::daemon::GraphicsProtocol::Iterm2File => Some(GraphicsProtocol::Iterm2File),
618        crate::proto::daemon::GraphicsProtocol::Unspecified => None,
619    }
620}
621
622#[cfg(feature = "client")]
623fn proto_capability_status(status: CapabilityStatus) -> crate::proto::daemon::CapabilityStatus {
624    match status {
625        CapabilityStatus::Supported => crate::proto::daemon::CapabilityStatus::Supported,
626        CapabilityStatus::Unsupported => crate::proto::daemon::CapabilityStatus::Unsupported,
627        CapabilityStatus::Unknown => crate::proto::daemon::CapabilityStatus::Unknown,
628        CapabilityStatus::Blocked => crate::proto::daemon::CapabilityStatus::Blocked,
629    }
630}
631
632#[cfg(feature = "client")]
633fn capability_status_from_i32(status: i32) -> CapabilityStatus {
634    match crate::proto::daemon::CapabilityStatus::try_from(status)
635        .unwrap_or(crate::proto::daemon::CapabilityStatus::Unknown)
636    {
637        crate::proto::daemon::CapabilityStatus::Supported => CapabilityStatus::Supported,
638        crate::proto::daemon::CapabilityStatus::Unsupported => CapabilityStatus::Unsupported,
639        crate::proto::daemon::CapabilityStatus::Unknown
640        | crate::proto::daemon::CapabilityStatus::Unspecified => CapabilityStatus::Unknown,
641        crate::proto::daemon::CapabilityStatus::Blocked => CapabilityStatus::Blocked,
642    }
643}
644
645#[cfg(feature = "client")]
646fn proto_evidence_strength(evidence: EvidenceStrength) -> crate::proto::daemon::EvidenceStrength {
647    match evidence {
648        EvidenceStrength::Probe => crate::proto::daemon::EvidenceStrength::Probe,
649        EvidenceStrength::StrongHostSignal => {
650            crate::proto::daemon::EvidenceStrength::StrongHostSignal
651        }
652        EvidenceStrength::Terminfo => crate::proto::daemon::EvidenceStrength::Terminfo,
653        EvidenceStrength::WeakEnv => crate::proto::daemon::EvidenceStrength::WeakEnv,
654        EvidenceStrength::UserOverride => crate::proto::daemon::EvidenceStrength::UserOverride,
655    }
656}
657
658#[cfg(feature = "client")]
659fn evidence_strength_from_i32(evidence: i32) -> EvidenceStrength {
660    match crate::proto::daemon::EvidenceStrength::try_from(evidence)
661        .unwrap_or(crate::proto::daemon::EvidenceStrength::WeakEnv)
662    {
663        crate::proto::daemon::EvidenceStrength::Probe => EvidenceStrength::Probe,
664        crate::proto::daemon::EvidenceStrength::StrongHostSignal => {
665            EvidenceStrength::StrongHostSignal
666        }
667        crate::proto::daemon::EvidenceStrength::Terminfo => EvidenceStrength::Terminfo,
668        crate::proto::daemon::EvidenceStrength::WeakEnv
669        | crate::proto::daemon::EvidenceStrength::Unspecified => EvidenceStrength::WeakEnv,
670        crate::proto::daemon::EvidenceStrength::UserOverride => EvidenceStrength::UserOverride,
671    }
672}
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677
678    fn input(term: &str, pairs: &[(&str, &str)]) -> TerminalCapabilityInput {
679        let mut env = BTreeMap::new();
680        if !term.is_empty() {
681            env.insert("TERM".into(), term.into());
682        }
683        for (k, v) in pairs {
684            env.insert((*k).into(), (*v).into());
685        }
686        TerminalCapabilityInput {
687            is_tty: true,
688            env,
689            probe: TerminalProbeEvidence::default(),
690        }
691    }
692
693    #[test]
694    fn weak_xterm_does_not_confirm_sixel() {
695        let caps = detect_terminal_capabilities(input("xterm-256color", &[]));
696        let sixel = caps.graphics.by_protocol(GraphicsProtocol::Sixel).unwrap();
697        assert_eq!(sixel.status, CapabilityStatus::Unknown);
698        assert_eq!(sixel.evidence, EvidenceStrength::WeakEnv);
699        assert_eq!(caps.graphics.preferred, None);
700    }
701
702    #[test]
703    fn da1_probe_confirms_sixel() {
704        let mut case = input("xterm-256color", &[]);
705        case.probe.sixel_da1 = Some("\x1b[?62;4;22c".into());
706        let caps = detect_terminal_capabilities(case);
707        let sixel = caps.graphics.by_protocol(GraphicsProtocol::Sixel).unwrap();
708        assert_eq!(sixel.status, CapabilityStatus::Supported);
709        assert_eq!(sixel.evidence, EvidenceStrength::Probe);
710        assert_eq!(caps.graphics.preferred, Some(GraphicsProtocol::Sixel));
711    }
712
713    #[test]
714    fn vt100_da_does_not_confirm_sixel() {
715        assert!(!primary_da_reports_sixel("\x1b[?1;2c"));
716        assert!(!primary_da_reports_sixel("\x1b[?62;22c"));
717    }
718}
719
720#[cfg(test)]
721#[path = "tests/terminal_graphics_coverage.rs"]
722mod coverage_tests;