use std::io::IsTerminal;
use std::sync::OnceLock;
use std::time::Duration;
use super::{Graphics, Protocol, detect, probe, query};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Capabilities {
pub protocols: Vec<Protocol>,
pub cell_size: Option<(u16, u16)>,
pub source: Source,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Source {
Probed,
Sniffed,
Declared,
}
impl Capabilities {
pub fn new(
protocols: impl IntoIterator<Item = Protocol>,
cell_size: Option<(u16, u16)>,
) -> Capabilities {
Capabilities {
protocols: protocols.into_iter().collect(),
cell_size,
source: Source::Declared,
}
}
pub fn detect() -> Capabilities {
Capabilities::detect_for(&std::io::stdout())
}
pub fn detect_for(destination: &impl IsTerminal) -> Capabilities {
let variable = |name: &str| std::env::var(name).ok().filter(|value| !value.is_empty());
let sniffed = detect::sniff(&variable);
let fallback = detect::cell_size();
if !probing_is_safe(destination.is_terminal(), &variable) {
return resolve(None, sniffed, fallback);
}
resolve(probed(), sniffed, fallback)
}
pub fn best(&self) -> Option<Graphics> {
let graphics = Graphics::new(*self.protocols.first()?);
match self.cell_size {
Some((width, height)) => Some(graphics.cell_size(width, height)),
None => Some(graphics),
}
}
}
fn resolve(
report: Option<&probe::Report>,
sniffed: Vec<Protocol>,
fallback_cell: Option<(u16, u16)>,
) -> Capabilities {
let mut protocols = Vec::new();
let mut source = Source::Sniffed;
let mut cell_size = None;
if let Some(report) = report.filter(|report| report.answered) {
source = Source::Probed;
cell_size = report.cell_size;
if report.kitty {
protocols.push(Protocol::Kitty);
}
let name = report
.terminal
.as_deref()
.unwrap_or_default()
.to_ascii_lowercase();
if ["iterm", "wezterm", "mintty"]
.iter()
.any(|t| name.contains(t))
{
protocols.push(Protocol::ITerm2);
}
if report.sixel {
protocols.push(Protocol::Sixel);
}
}
for protocol in sniffed {
if !protocols.contains(&protocol) {
protocols.push(protocol);
}
}
protocols.sort_by_key(|protocol| match protocol {
Protocol::Kitty => 0,
Protocol::ITerm2 => 1,
Protocol::Sixel => 2,
});
Capabilities {
protocols,
cell_size: cell_size.or(fallback_cell),
source,
}
}
fn probing_is_safe(
destination_is_terminal: bool,
variable: &impl Fn(&str) -> Option<String>,
) -> bool {
if !destination_is_terminal || variable("TMUX").is_some() {
return false;
}
if variable("MALEVICH_GRAPHICS").is_some_and(|value| detect::named_protocols(&value).is_some())
{
return false;
}
let term = variable("TERM").unwrap_or_default();
term != "dumb" && term != "unknown" && !term.starts_with("screen") && !term.starts_with("tmux")
}
fn probed() -> Option<&'static probe::Report> {
static PROBE: OnceLock<Option<probe::Report>> = OnceLock::new();
PROBE
.get_or_init(|| {
let replies = query::exchange(probe::QUERIES, Duration::from_millis(300), probe::done)?;
Some(probe::parse(&replies))
})
.as_ref()
}
#[cfg(test)]
#[path = "tests/capabilities_tests.rs"]
mod tests;