use crate::target::{CapabilityOrigin, DetectedCapabilities, TargetObservations};
use rich::protocol::{Support, TargetCapabilities};
use rich::{ColorSystem, Console, ConsoleOptions, Renderable, Segment, Table, Text};
use std::collections::BTreeMap;
use std::io::IsTerminal;
pub trait Environment {
fn var(&self, name: &str) -> Option<String>;
fn is_terminal(&self) -> bool;
fn size(&self) -> Option<(usize, usize)>;
fn is_windows(&self) -> bool {
false
}
fn cell_pixels(&self) -> Option<(u16, u16)> {
None
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SystemEnvironment;
impl Environment for SystemEnvironment {
fn var(&self, name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn is_terminal(&self) -> bool {
std::io::stdout().is_terminal()
}
fn size(&self) -> Option<(usize, usize)> {
if !self.is_terminal() {
return None;
}
let console = Console::builder().force_terminal(true).build();
Some((console.width(), console.height()))
}
fn is_windows(&self) -> bool {
cfg!(windows)
}
fn cell_pixels(&self) -> Option<(u16, u16)> {
if !self.is_terminal() {
return None;
}
let (cells, pixels) = crate::graphics::stdout_window()?;
crate::graphics::CellPixels::from_window(pixels, cells).map(|c| (c.width, c.height))
}
}
#[derive(Clone, Debug, Default)]
pub struct MapEnvironment {
pub vars: BTreeMap<String, String>,
pub terminal: bool,
pub size: Option<(usize, usize)>,
pub windows: bool,
pub cell_pixels: Option<(u16, u16)>,
}
impl MapEnvironment {
pub fn new() -> Self {
Self::default()
}
pub fn tty() -> Self {
Self {
terminal: true,
..Self::default()
}
}
pub fn var(mut self, name: &str, value: &str) -> Self {
self.vars.insert(name.into(), value.into());
self
}
pub fn terminal(mut self, terminal: bool) -> Self {
self.terminal = terminal;
self
}
pub fn size(mut self, width: usize, height: usize) -> Self {
self.size = Some((width, height));
self
}
pub fn windows(mut self, windows: bool) -> Self {
self.windows = windows;
self
}
pub fn cell_pixels(mut self, width: u16, height: u16) -> Self {
self.cell_pixels = Some((width, height));
self
}
}
impl Environment for MapEnvironment {
fn var(&self, name: &str) -> Option<String> {
self.vars.get(name).cloned()
}
fn is_terminal(&self) -> bool {
self.terminal
}
fn size(&self) -> Option<(usize, usize)> {
self.size
}
fn is_windows(&self) -> bool {
self.windows
}
fn cell_pixels(&self) -> Option<(u16, u16)> {
self.cell_pixels
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Origin {
Override,
Environment(String),
Default,
Inferred,
}
impl std::fmt::Display for Origin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Origin::Override => f.write_str("override"),
Origin::Environment(name) => write!(f, "environment ({name})"),
Origin::Default => f.write_str("default"),
Origin::Inferred => f.write_str("inferred"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Field<T> {
pub value: T,
pub origin: Origin,
pub reason: String,
}
impl<T> Field<T> {
pub fn new(value: T, origin: Origin, reason: impl Into<String>) -> Self {
Self {
value,
origin,
reason: reason.into(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum ColorDepth {
None,
Ansi16,
Ansi256,
TrueColor,
}
impl ColorDepth {
pub fn color_system(self) -> Option<ColorSystem> {
match self {
ColorDepth::None => None,
ColorDepth::Ansi16 => Some(ColorSystem::Standard),
ColorDepth::Ansi256 => Some(ColorSystem::EightBit),
ColorDepth::TrueColor => Some(ColorSystem::Truecolor),
}
}
pub fn from_color_system(system: Option<ColorSystem>) -> Self {
match system {
None => ColorDepth::None,
Some(ColorSystem::Standard | ColorSystem::Windows) => ColorDepth::Ansi16,
Some(ColorSystem::EightBit) => ColorDepth::Ansi256,
Some(ColorSystem::Truecolor) => ColorDepth::TrueColor,
}
}
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"none" | "0" | "no" | "off" | "false" => Some(ColorDepth::None),
"16" | "standard" | "ansi" => Some(ColorDepth::Ansi16),
"256" | "8bit" | "eightbit" => Some(ColorDepth::Ansi256),
"truecolor" | "24bit" | "16m" => Some(ColorDepth::TrueColor),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
ColorDepth::None => "none",
ColorDepth::Ansi16 => "16",
ColorDepth::Ansi256 => "256",
ColorDepth::TrueColor => "truecolor",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Graphics {
None,
Sixel,
Kitty,
Iterm,
}
impl Graphics {
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"none" | "0" | "no" | "off" | "false" => Some(Graphics::None),
"sixel" => Some(Graphics::Sixel),
"kitty" => Some(Graphics::Kitty),
"iterm" | "iterm2" => Some(Graphics::Iterm),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Graphics::None => "none",
Graphics::Sixel => "sixel",
Graphics::Kitty => "kitty",
Graphics::Iterm => "iterm",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Overrides {
pub color: Option<ColorDepth>,
pub unicode: Option<bool>,
pub hyperlinks: Option<bool>,
pub graphics: Option<Graphics>,
pub sixel: Option<bool>,
pub animation: Option<bool>,
pub interactive: Option<bool>,
pub width: Option<usize>,
pub height: Option<usize>,
}
pub const OVERRIDE_VARS: &[&str] = &[
"RICH_COLOR",
"RICH_UNICODE",
"RICH_HYPERLINKS",
"RICH_GRAPHICS",
"RICH_SIXEL",
"RICH_ANIMATION",
"RICH_WIDTH",
"RICH_HEIGHT",
];
pub fn parse_bool(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => None,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Report {
pub color: Field<ColorDepth>,
pub unicode: Field<bool>,
pub hyperlinks: Field<bool>,
pub graphics: Field<Graphics>,
pub sixel: Field<bool>,
pub width: Field<usize>,
pub height: Field<usize>,
pub interactive: Field<bool>,
pub animation: Field<bool>,
pub terminal: Option<String>,
pub ci: Option<String>,
pub warnings: Vec<String>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Capabilities;
const CI_PROVIDERS: &[(&str, &str, ColorDepth)] = &[
("GITHUB_ACTIONS", "GitHub Actions", ColorDepth::TrueColor),
("GITEA_ACTIONS", "Gitea Actions", ColorDepth::TrueColor),
("GITLAB_CI", "GitLab CI", ColorDepth::Ansi16),
("BUILDKITE", "Buildkite", ColorDepth::Ansi16),
("CIRCLECI", "CircleCI", ColorDepth::Ansi16),
("TRAVIS", "Travis CI", ColorDepth::Ansi16),
("APPVEYOR", "AppVeyor", ColorDepth::Ansi16),
("DRONE", "Drone", ColorDepth::Ansi16),
("TF_BUILD", "Azure Pipelines", ColorDepth::Ansi16),
("TEAMCITY_VERSION", "TeamCity", ColorDepth::Ansi16),
("JENKINS_URL", "Jenkins", ColorDepth::None),
(
"BITBUCKET_BUILD_NUMBER",
"Bitbucket Pipelines",
ColorDepth::None,
),
];
struct Probe<'a> {
env: &'a dyn Environment,
}
impl Probe<'_> {
fn get(&self, name: &str) -> Option<String> {
self.env.var(name).filter(|v| !v.is_empty())
}
fn term(&self) -> String {
self.get("TERM").unwrap_or_default().to_ascii_lowercase()
}
fn program(&self) -> String {
self.get("TERM_PROGRAM").unwrap_or_default()
}
fn kitty(&self) -> Option<String> {
if self.term() == "xterm-kitty" {
Some("TERM=xterm-kitty".into())
} else {
self.get("KITTY_WINDOW_ID")
.map(|_| "KITTY_WINDOW_ID".into())
}
}
fn ci(&self) -> Option<(&'static str, &'static str, ColorDepth)> {
CI_PROVIDERS
.iter()
.find(|(var, _, _)| self.get(var).is_some_and(|v| v != "false"))
.copied()
.or_else(|| {
self.get("CI")
.filter(|v| !matches!(v.to_ascii_lowercase().as_str(), "0" | "false"))
.map(|_| ("CI", "CI", ColorDepth::None))
})
}
fn terminal(&self) -> Option<String> {
if self.get("WT_SESSION").is_some() {
return Some("Windows Terminal".into());
}
if self.kitty().is_some() {
return Some("kitty".into());
}
if let Some(p) = self.get("TERM_PROGRAM") {
return Some(p);
}
self.get("VTE_VERSION").map(|v| format!("VTE {v}"))
}
fn sixel(&self) -> Option<String> {
if self.get("WT_SESSION").is_some() {
return Some("WT_SESSION".into());
}
let term = self.term();
if ["sixel", "mlterm", "foot"].iter().any(|t| term.contains(t)) {
return Some(format!("TERM={}", self.get("TERM").unwrap_or_default()));
}
let program = self.program().to_ascii_lowercase();
if program.contains("wezterm") || program.contains("mintty") {
return Some(format!("TERM_PROGRAM={}", self.program()));
}
None
}
fn reduced_motion(&self) -> Option<String> {
let list = self.get("RICH_A11Y")?;
list.split(',')
.map(|item| item.trim().to_ascii_lowercase())
.find(|item| {
matches!(
item.as_str(),
"reduced-motion" | "no-animation" | "screen-reader"
)
})
.map(|item| format!("RICH_A11Y={item}"))
}
}
fn from_var(name: &str) -> Origin {
Origin::Environment(name.into())
}
impl Capabilities {
pub fn detect(env: &dyn Environment) -> Report {
Self::detect_with(env, &Overrides::default())
}
pub fn system() -> Report {
Self::detect(&SystemEnvironment)
}
pub fn detect_with(env: &dyn Environment, overrides: &Overrides) -> Report {
let p = Probe { env };
let mut warnings = Vec::new();
let tty = env.is_terminal();
let term = p.term();
let program = p.program();
let ci = p.ci();
fn parsed<T>(
p: &Probe<'_>,
name: &str,
parse: impl Fn(&str) -> Option<T>,
warnings: &mut Vec<String>,
) -> Option<Field<T>> {
let raw = p.get(name)?;
match parse(&raw) {
Some(value) => Some(Field::new(value, from_var(name), format!("{name}={raw}"))),
None => {
warnings.push(format!("ignored {name}={raw:?}: unrecognised value"));
None
}
}
}
let size = |v: &str| v.trim().parse::<usize>().ok().filter(|v| *v > 0);
let color = parsed(&p, "RICH_COLOR", ColorDepth::parse, &mut warnings)
.unwrap_or_else(|| detect_color(&p, tty, &term, &program, ci, env.is_windows()));
let unicode = parsed(&p, "RICH_UNICODE", parse_bool, &mut warnings).unwrap_or_else(|| {
for name in ["LC_ALL", "LC_CTYPE", "LANG"] {
if let Some(value) = p.get(name) {
let lower = value.to_ascii_lowercase();
let charset = lower.split('@').next().unwrap_or("");
let reason = format!("{name}={value}");
return if charset.contains("utf-8") || charset.contains("utf8") {
Field::new(true, from_var(name), reason)
} else if matches!(charset, "c" | "posix") {
Field::new(true, from_var(name), format!("{reason} (coerced to UTF-8)"))
} else {
Field::new(false, from_var(name), format!("{reason} is not UTF-8"))
};
}
}
if p.get("WT_SESSION").is_some() {
return Field::new(true, from_var("WT_SESSION"), "Windows Terminal");
}
Field::new(true, Origin::Default, "no locale set; UTF-8 assumed")
});
let hyperlinks = parsed(&p, "RICH_HYPERLINKS", parse_bool, &mut warnings)
.unwrap_or_else(|| detect_hyperlinks(&p, tty, &term, &program));
let sixel_alias = parsed(&p, "RICH_SIXEL", parse_bool, &mut warnings);
let graphics_var = parsed(&p, "RICH_GRAPHICS", Graphics::parse, &mut warnings);
let sixel = if let Some(g) = &graphics_var {
Field::new(
g.value == Graphics::Sixel,
g.origin.clone(),
g.reason.clone(),
)
} else if let Some(s) = &sixel_alias {
s.clone()
} else if !tty {
Field::new(false, Origin::Inferred, "stdout is not a terminal")
} else if let Some(reason) = p.sixel() {
let origin = from_var(reason.split('=').next().unwrap_or(&reason));
Field::new(true, origin, reason)
} else {
Field::new(
false,
Origin::Default,
"no sixel-capable terminal identified",
)
};
let graphics = if let Some(g) = graphics_var {
g
} else if let Some(s) = sixel_alias {
let value = if s.value {
Graphics::Sixel
} else {
Graphics::None
};
Field::new(value, s.origin, s.reason)
} else if !tty {
Field::new(Graphics::None, Origin::Inferred, "stdout is not a terminal")
} else if let Some(reason) = p.kitty() {
let origin = from_var(reason.split('=').next().unwrap_or(&reason));
Field::new(Graphics::Kitty, origin, reason)
} else if program == "iTerm.app" || program.eq_ignore_ascii_case("wezterm") {
Field::new(
Graphics::Iterm,
from_var("TERM_PROGRAM"),
format!("TERM_PROGRAM={program}"),
)
} else if sixel.value {
Field::new(Graphics::Sixel, sixel.origin.clone(), sixel.reason.clone())
} else {
Field::new(
Graphics::None,
Origin::Default,
"no graphics protocol identified",
)
};
let dimension =
|rich: &str, var: &str, index: usize, default: usize, w: &mut Vec<String>| {
parsed(&p, rich, size, w)
.or_else(|| parsed(&p, var, size, w))
.unwrap_or_else(|| match env.size() {
Some(s) => {
let v = if index == 0 { s.0 } else { s.1 };
Field::new(v, Origin::Inferred, "terminal size")
}
None => Field::new(default, Origin::Default, format!("default {default}")),
})
};
let width = dimension("RICH_WIDTH", "COLUMNS", 0, 80, &mut warnings);
let height = dimension("RICH_HEIGHT", "LINES", 1, 25, &mut warnings);
let interactive = if tty {
Field::new(true, Origin::Inferred, "stdout is a terminal")
} else {
Field::new(false, Origin::Inferred, "stdout is not a terminal")
};
let mut report = Report {
color,
unicode,
hyperlinks,
graphics,
sixel,
width,
height,
interactive,
animation: Field::new(false, Origin::Default, ""),
terminal: p.terminal(),
ci: ci.map(|(_, name, _)| name.to_owned()),
warnings,
};
report.apply(overrides);
let animation_var = parsed(&p, "RICH_ANIMATION", parse_bool, &mut report.warnings);
report.animation = match (overrides.animation, animation_var) {
(Some(v), _) => Field::new(v, Origin::Override, "override"),
(None, Some(field)) => field,
(None, None) => {
if !report.interactive.value {
Field::new(false, Origin::Inferred, "not interactive")
} else if let Some((var, name, _)) = ci {
Field::new(false, from_var(var), format!("running in {name}"))
} else if term == "dumb" {
Field::new(false, from_var("TERM"), "TERM=dumb")
} else if let Some(reason) = p.reduced_motion() {
Field::new(false, from_var("RICH_A11Y"), reason)
} else {
Field::new(true, Origin::Inferred, "interactive terminal")
}
}
};
report.escape_controls();
report
}
}
fn escape_controls(text: &str) -> String {
if !text
.chars()
.any(|c| c.is_control() || crate::sanitize::is_bidi_control(c))
{
return text.to_owned();
}
let mut out = String::with_capacity(text.len() + 8);
for c in text.chars() {
if c.is_control() || crate::sanitize::is_bidi_control(c) {
out.push_str(&format!("\\u{{{:x}}}", c as u32));
} else {
out.push(c);
}
}
out
}
impl Report {
fn escape_controls(&mut self) {
for reason in [
&mut self.color.reason,
&mut self.unicode.reason,
&mut self.hyperlinks.reason,
&mut self.graphics.reason,
&mut self.sixel.reason,
&mut self.width.reason,
&mut self.height.reason,
&mut self.interactive.reason,
&mut self.animation.reason,
] {
*reason = escape_controls(reason);
}
for warning in &mut self.warnings {
*warning = escape_controls(warning);
}
for value in [&mut self.terminal, &mut self.ci].into_iter().flatten() {
*value = escape_controls(value);
}
}
}
fn detect_color(
p: &Probe<'_>,
tty: bool,
term: &str,
program: &str,
ci: Option<(&str, &str, ColorDepth)>,
windows: bool,
) -> Field<ColorDepth> {
use ColorDepth::*;
if let Some(v) = p.get("NO_COLOR") {
return Field::new(None, from_var("NO_COLOR"), format!("NO_COLOR={v}"));
}
let force = p.get("FORCE_COLOR");
if let Some(force) = &force {
let reason = format!("FORCE_COLOR={force}");
match force.trim().to_ascii_lowercase().as_str() {
"0" | "false" => return Field::new(None, from_var("FORCE_COLOR"), reason),
"1" => return Field::new(Ansi16, from_var("FORCE_COLOR"), reason),
"2" => return Field::new(Ansi256, from_var("FORCE_COLOR"), reason),
"3" => return Field::new(TrueColor, from_var("FORCE_COLOR"), reason),
_ => {}
}
}
if !tty && force.is_none() {
if let Some((var, name, depth)) = ci.filter(|c| c.2 != None) {
return Field::new(depth, from_var(var), format!("{name} renders ANSI logs"));
}
return Field::new(None, Origin::Inferred, "stdout is not a terminal");
}
if term == "dumb" {
return Field::new(None, from_var("TERM"), "TERM=dumb");
}
let terminal = detect_terminal_depth(p, term, program, windows);
if let Some(force) = force {
let depth = terminal.value.max(Ansi16);
let reason = if terminal.value >= Ansi16 && terminal.origin != Origin::Default {
terminal.reason
} else {
format!("FORCE_COLOR={force}")
};
return Field::new(depth, from_var("FORCE_COLOR"), reason);
}
terminal
}
fn detect_terminal_depth(
p: &Probe<'_>,
term: &str,
program: &str,
windows: bool,
) -> Field<ColorDepth> {
use ColorDepth::*;
if let Some(v) = p.get("COLORTERM") {
let lower = v.to_ascii_lowercase();
if lower.contains("truecolor") || lower.contains("24bit") {
return Field::new(TrueColor, from_var("COLORTERM"), format!("COLORTERM={v}"));
}
}
if p.get("WT_SESSION").is_some() {
return Field::new(TrueColor, from_var("WT_SESSION"), "Windows Terminal");
}
if let Some(reason) = p.kitty() {
let origin = from_var(reason.split('=').next().unwrap_or(&reason));
return Field::new(TrueColor, origin, reason);
}
if ["iTerm.app", "WezTerm", "vscode", "ghostty"]
.iter()
.any(|t| program.eq_ignore_ascii_case(t))
{
return Field::new(
TrueColor,
from_var("TERM_PROGRAM"),
format!("TERM_PROGRAM={program}"),
);
}
let raw_term = p.get("TERM").unwrap_or_default();
if term.contains("direct") || term.contains("truecolor") {
return Field::new(TrueColor, from_var("TERM"), format!("TERM={raw_term}"));
}
if program == "Apple_Terminal" {
return Field::new(
Ansi256,
from_var("TERM_PROGRAM"),
"TERM_PROGRAM=Apple_Terminal",
);
}
if term.contains("256") {
return Field::new(Ansi256, from_var("TERM"), format!("TERM={raw_term}"));
}
if windows {
return Field::new(TrueColor, Origin::Inferred, "Windows console");
}
if !term.is_empty() {
return Field::new(Ansi16, from_var("TERM"), format!("TERM={raw_term}"));
}
Field::new(Ansi16, Origin::Default, "terminal with no TERM")
}
fn detect_hyperlinks(p: &Probe<'_>, tty: bool, term: &str, program: &str) -> Field<bool> {
if !tty {
return Field::new(false, Origin::Inferred, "stdout is not a terminal");
}
if term == "dumb" {
return Field::new(false, from_var("TERM"), "TERM=dumb");
}
if p.get("TMUX").is_some() || term.starts_with("screen") || term.starts_with("tmux") {
let origin = if p.get("TMUX").is_some() {
from_var("TMUX")
} else {
from_var("TERM")
};
return Field::new(false, origin, "inside a multiplexer");
}
if p.get("WT_SESSION").is_some() {
return Field::new(true, from_var("WT_SESSION"), "Windows Terminal");
}
if let Some(reason) = p.kitty() {
let origin = from_var(reason.split('=').next().unwrap_or(&reason));
return Field::new(true, origin, reason);
}
if [
"iTerm.app",
"WezTerm",
"vscode",
"ghostty",
"contour",
"DomTerm",
]
.iter()
.any(|t| program.eq_ignore_ascii_case(t))
{
return Field::new(
true,
from_var("TERM_PROGRAM"),
format!("TERM_PROGRAM={program}"),
);
}
if let Some(v) = p.get("VTE_VERSION") {
let ok = v.trim().parse::<u32>().is_ok_and(|n| n >= 5000);
let reason = format!("VTE_VERSION={v}");
return Field::new(ok, from_var("VTE_VERSION"), reason);
}
if ["foot", "contour", "xterm-ghostty"]
.iter()
.any(|t| term.starts_with(t))
{
return Field::new(true, from_var("TERM"), format!("TERM={term}"));
}
if p.get("DOMTERM").is_some() {
return Field::new(true, from_var("DOMTERM"), "DOMTERM");
}
Field::new(
false,
Origin::Default,
"terminal not known to support OSC 8",
)
}
impl Report {
pub fn apply(&mut self, o: &Overrides) {
fn set<T>(field: &mut Field<T>, value: Option<T>) {
if let Some(value) = value {
*field = Field::new(value, Origin::Override, "override");
}
}
set(&mut self.color, o.color);
set(&mut self.unicode, o.unicode);
set(&mut self.hyperlinks, o.hyperlinks);
if let Some(g) = o.graphics {
set(&mut self.graphics, Some(g));
if o.sixel.is_none() {
set(&mut self.sixel, Some(g == Graphics::Sixel));
}
}
set(&mut self.sixel, o.sixel);
set(&mut self.animation, o.animation);
set(&mut self.interactive, o.interactive);
set(&mut self.width, o.width);
set(&mut self.height, o.height);
}
pub fn to_target_capabilities(&self) -> TargetCapabilities {
TargetCapabilities {
width: self.width.value,
height: self.height.value,
color_system: self.color.value.color_system(),
interactive: self.interactive.value,
unicode: self.unicode.value,
hyperlinks: self.hyperlinks.value,
sixel: self.sixel_support(),
}
}
fn sixel_support(&self) -> Support {
match (&self.sixel.value, &self.sixel.origin) {
(false, _) => Support::Unsupported,
(true, Origin::Override) => Support::Confirmed,
(true, Origin::Environment(name)) if name.starts_with("RICH_") => Support::Confirmed,
(true, _) => Support::Inferred,
}
}
pub fn observations(&self) -> TargetObservations {
TargetObservations {
width: Some(self.width.value),
height: Some(self.height.value),
is_terminal: self.interactive.value,
color_system: self.color.value.color_system(),
unicode: self.unicode.value,
hyperlinks: self.hyperlinks.value,
sixel_hint: self.sixel_support(),
}
}
pub fn to_detected(&self) -> DetectedCapabilities {
fn origin(o: &Origin) -> CapabilityOrigin {
match o {
Origin::Override => CapabilityOrigin::Configured,
Origin::Environment(name) if name.starts_with("RICH_") => {
CapabilityOrigin::Configured
}
Origin::Environment(_) => CapabilityOrigin::Detected,
Origin::Default => CapabilityOrigin::Default,
Origin::Inferred => CapabilityOrigin::Inferred,
}
}
let origins = [
("width", &self.width.origin),
("height", &self.height.origin),
("color_system", &self.color.origin),
("interactive", &self.interactive.origin),
("unicode", &self.unicode.origin),
("hyperlinks", &self.hyperlinks.origin),
("sixel", &self.sixel.origin),
]
.into_iter()
.map(|(name, o)| (name.to_owned(), origin(o)))
.collect();
DetectedCapabilities {
capabilities: self.to_target_capabilities(),
origins,
}
}
pub fn rows(&self) -> Vec<(&'static str, String, &Origin, &str)> {
let yes = |b: bool| if b { "yes" } else { "no" }.to_owned();
vec![
(
"color",
self.color.value.name().into(),
&self.color.origin,
&self.color.reason,
),
(
"unicode",
yes(self.unicode.value),
&self.unicode.origin,
&self.unicode.reason,
),
(
"hyperlinks",
yes(self.hyperlinks.value),
&self.hyperlinks.origin,
&self.hyperlinks.reason,
),
(
"graphics",
self.graphics.value.name().into(),
&self.graphics.origin,
&self.graphics.reason,
),
(
"sixel",
yes(self.sixel.value),
&self.sixel.origin,
&self.sixel.reason,
),
(
"width",
self.width.value.to_string(),
&self.width.origin,
&self.width.reason,
),
(
"height",
self.height.value.to_string(),
&self.height.origin,
&self.height.reason,
),
(
"interactive",
yes(self.interactive.value),
&self.interactive.origin,
&self.interactive.reason,
),
(
"animation",
yes(self.animation.value),
&self.animation.origin,
&self.animation.reason,
),
]
}
}
pub struct CapabilityReport<'a> {
report: &'a Report,
}
impl<'a> CapabilityReport<'a> {
pub fn new(report: &'a Report) -> Self {
Self { report }
}
}
impl Renderable for CapabilityReport<'_> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let mut table = Table::new();
table.add_column("Capability");
table.add_column("Value");
table.add_column("Source");
for (name, value, origin, reason) in self.report.rows() {
let source = if reason.is_empty() || reason == "override" {
origin.to_string()
} else {
format!("{origin}: {reason}")
};
table.add_row_text(vec![Text::new(name), Text::new(value), Text::new(source)]);
}
let facts = [("terminal", &self.report.terminal), ("ci", &self.report.ci)];
for (name, value) in facts {
if let Some(value) = value {
table.add_row_text(vec![
Text::new(name),
Text::new(value.as_str()),
Text::new("inferred"),
]);
}
}
for warning in &self.report.warnings {
table.add_row_text(vec![
Text::new("warning"),
Text::new(""),
Text::new(warning.as_str()),
]);
}
table.rich_render(console, options)
}
}