use crate::color::ColorSystem;
use crate::console::detect_color_system_from;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsoleCapabilities {
pub color_system: ColorSystem,
pub is_terminal: bool,
pub truecolor: bool,
pub synchronized_output: bool,
pub unicode_version: Option<u32>,
pub kitty: bool,
pub sixel: bool,
pub iterm: bool,
}
impl ConsoleCapabilities {
pub fn from_env_parts(
colorterm: Option<&str>,
term: Option<&str>,
is_terminal: bool,
unicode_version_str: Option<&str>,
kitty_window_id: Option<&str>,
term_program: Option<&str>,
) -> Self {
let truecolor = colorterm.is_some_and(|ct| {
let ct_lower = ct.to_lowercase();
ct_lower.contains("truecolor") || ct_lower.contains("24bit")
});
let color_system = detect_color_system_from(colorterm, term);
let unicode_version = unicode_version_str.and_then(|s| s.parse::<u32>().ok());
let kitty = term.is_some_and(|t| t == "xterm-kitty")
|| kitty_window_id.is_some()
|| term_program.is_some_and(|tp| {
let tp_lower = tp.to_lowercase();
tp_lower == "wezterm" || tp_lower == "ghostty"
});
let iterm = term_program.is_some_and(|tp| tp == "iTerm.app");
let sixel = false;
ConsoleCapabilities {
color_system,
is_terminal,
truecolor,
synchronized_output: true,
unicode_version,
kitty,
sixel,
iterm,
}
}
pub fn from_env(is_terminal: bool) -> Self {
let colorterm = std::env::var("COLORTERM").ok();
let term = std::env::var("TERM").ok();
let unicode_version_str = std::env::var("UNICODE_VERSION").ok();
let kitty_window_id = std::env::var("KITTY_WINDOW_ID").ok();
let term_program = std::env::var("TERM_PROGRAM").ok();
Self::from_env_parts(
colorterm.as_deref(),
term.as_deref(),
is_terminal,
unicode_version_str.as_deref(),
kitty_window_id.as_deref(),
term_program.as_deref(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSystem;
#[test]
fn caps_truecolor_flag_when_colorterm_truecolor() {
let caps =
ConsoleCapabilities::from_env_parts(Some("truecolor"), None, true, None, None, None);
assert!(caps.truecolor, "truecolor flag should be true");
assert_eq!(caps.color_system, ColorSystem::TrueColor);
assert!(caps.is_terminal);
}
#[test]
fn caps_truecolor_flag_when_colorterm_24bit() {
let caps = ConsoleCapabilities::from_env_parts(Some("24bit"), None, true, None, None, None);
assert!(caps.truecolor, "24bit should set truecolor flag");
}
#[test]
fn caps_no_truecolor_when_no_colorterm() {
let caps = ConsoleCapabilities::from_env_parts(
None,
Some("xterm-256color"),
true,
None,
None,
None,
);
assert!(!caps.truecolor, "no COLORTERM → truecolor should be false");
assert_eq!(caps.color_system, ColorSystem::EightBit);
}
#[test]
fn caps_synchronized_output_defaults_true() {
let caps = ConsoleCapabilities::from_env_parts(None, None, false, None, None, None);
assert!(
caps.synchronized_output,
"synchronized_output should default to true"
);
}
#[test]
fn caps_unicode_version_parsed_from_env_parts() {
let caps = ConsoleCapabilities::from_env_parts(None, None, false, Some("15"), None, None);
assert_eq!(caps.unicode_version, Some(15));
}
#[test]
fn caps_unicode_version_none_on_non_numeric() {
let caps =
ConsoleCapabilities::from_env_parts(None, None, false, Some("fifteen"), None, None);
assert_eq!(caps.unicode_version, None);
}
#[test]
fn caps_unicode_version_none_when_absent() {
let caps = ConsoleCapabilities::from_env_parts(None, None, false, None, None, None);
assert_eq!(caps.unicode_version, None);
}
#[test]
fn caps_is_terminal_propagated() {
let tty = ConsoleCapabilities::from_env_parts(None, None, true, None, None, None);
assert!(tty.is_terminal);
let piped = ConsoleCapabilities::from_env_parts(None, None, false, None, None, None);
assert!(!piped.is_terminal);
}
#[test]
fn caps_from_env_smoke() {
let _caps = ConsoleCapabilities::from_env(false);
}
}