use crate::{builder, Result};
use std::env::var;
use terminfo::{self, capability as cap};
pub mod probed;
const NO_COLOR_ENV: &str = "NO_COLOR";
builder! {
#[derive(Debug, Default, Clone)]
pub struct ProbeHints {
term: Option<String>,
colorterm: Option<String>,
term_program: Option<String>,
color_level: Option<ColorLevel>,
term_program_version: Option<String>,
hyperlinks: Option<bool>,
sixel: Option<bool>,
iterm2_image: Option<bool>,
bce: Option<bool>,
colorterm_bce: Option<String>,
terminfo_db: Option<terminfo::Database>,
bracketed_paste: Option<bool>,
mouse_reporting: Option<bool>,
force_terminfo_render_to_use_ansi_sgr: Option<bool>,
}
}
impl ProbeHints {
pub fn new_from_env() -> Self {
let mut probe_hints = ProbeHints::default()
.term(var("TERM").ok())
.colorterm(var("COLORTERM").ok())
.colorterm_bce(var("COLORTERM_BCE").ok())
.term_program(var("TERM_PROGRAM").ok())
.term_program_version(var("TERM_PROGRAM_VERSION").ok());
if !std::env::var(NO_COLOR_ENV)
.unwrap_or("".to_string())
.is_empty()
{
probe_hints.color_level = Some(ColorLevel::MonoChrome);
}
probe_hints
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorLevel {
Sixteen,
TwoFiftySix,
TrueColor,
MonoChrome,
}
#[derive(Debug, Clone)]
pub struct Capabilities {
color_level: ColorLevel,
hyperlinks: bool,
sixel: bool,
iterm2_image: bool,
bce: bool,
terminfo_db: Option<terminfo::Database>,
bracketed_paste: bool,
mouse_reporting: bool,
force_terminfo_render_to_use_ansi_sgr: bool,
}
impl Capabilities {
pub fn new_from_env() -> Result<Self> {
Self::new_with_hints(ProbeHints::new_from_env())
}
#[cfg(windows)]
pub(crate) fn apply_builtin_terminfo(mut self) -> Self {
let data = include_bytes!("../../data/xterm-256color");
let db = terminfo::Database::from_buffer(data.as_ref()).unwrap();
self.terminfo_db = Some(db);
self.color_level = ColorLevel::TrueColor;
self
}
pub fn new_with_hints(hints: ProbeHints) -> Result<Self> {
let terminfo_db = hints.terminfo_db.as_ref().cloned();
let terminfo_db = if cfg!(test) {
terminfo_db
} else {
terminfo_db.or_else(|| match hints.term.as_ref() {
Some(t) => terminfo::Database::from_name(t).ok(),
None => terminfo::Database::from_env().ok(),
})
};
let color_level = hints.color_level.unwrap_or_else(|| {
match hints.colorterm.as_ref().map(String::as_ref) {
Some("truecolor") | Some("24bit") => ColorLevel::TrueColor,
Some(_) => ColorLevel::TwoFiftySix,
_ => {
if let Some(ref db) = terminfo_db.as_ref() {
let has_true_color = db
.get::<cap::TrueColor>()
.unwrap_or(cap::TrueColor(false))
.0;
if has_true_color {
ColorLevel::TrueColor
} else if let Some(cap::MaxColors(n)) = db.get::<cap::MaxColors>() {
if n >= 16777216 {
ColorLevel::TrueColor
} else if n >= 256 {
ColorLevel::TwoFiftySix
} else {
ColorLevel::Sixteen
}
} else {
ColorLevel::Sixteen
}
} else if let Some(ref term) = hints.term {
if term.contains("256color") {
ColorLevel::TwoFiftySix
} else {
ColorLevel::Sixteen
}
} else {
ColorLevel::Sixteen
}
}
}
});
let sixel = hints.sixel.unwrap_or(false);
let hyperlinks = hints.hyperlinks.unwrap_or(true);
let bce = hints.bce.unwrap_or_else(|| {
match hints.colorterm_bce.as_ref().map(String::as_ref) {
Some("1") => true,
_ => {
terminfo_db
.as_ref()
.map(|db| {
db.get::<cap::BackColorErase>()
.unwrap_or(cap::BackColorErase(false))
.0
})
.unwrap_or(false)
}
}
});
let iterm2_image = hints.iterm2_image.unwrap_or_else(|| {
match hints.term_program.as_ref().map(String::as_ref) {
Some("iTerm.app") => {
version_ge(
hints
.term_program_version
.as_ref()
.unwrap_or(&"0.0.0".to_owned()),
"2.9.20150512",
)
}
Some("WezTerm") => true,
_ => false,
}
});
let bracketed_paste = hints.bracketed_paste.unwrap_or(true);
let mouse_reporting = hints.mouse_reporting.unwrap_or(true);
let force_terminfo_render_to_use_ansi_sgr =
hints.force_terminfo_render_to_use_ansi_sgr.unwrap_or(false);
Ok(Self {
color_level,
sixel,
hyperlinks,
iterm2_image,
bce,
terminfo_db,
bracketed_paste,
mouse_reporting,
force_terminfo_render_to_use_ansi_sgr,
})
}
pub fn color_level(&self) -> ColorLevel {
self.color_level
}
pub fn sixel(&self) -> bool {
self.sixel
}
pub fn hyperlinks(&self) -> bool {
self.hyperlinks
}
pub fn iterm2_image(&self) -> bool {
self.iterm2_image
}
pub fn bce(&self) -> bool {
self.bce
}
pub fn terminfo_db(&self) -> Option<&terminfo::Database> {
self.terminfo_db.as_ref()
}
pub fn bracketed_paste(&self) -> bool {
self.bracketed_paste
}
pub fn mouse_reporting(&self) -> bool {
self.mouse_reporting
}
pub fn force_terminfo_render_to_use_ansi_sgr(&self) -> bool {
self.force_terminfo_render_to_use_ansi_sgr
}
}
fn version_ge(a: &str, b: &str) -> bool {
let mut a = a.split('.');
let mut b = b.split('.');
loop {
match (a.next(), b.next()) {
(Some(a), Some(b)) => match (a.parse::<u64>(), b.parse::<u64>()) {
(Ok(a), Ok(b)) => {
if a > b {
return true;
}
if a < b {
return false;
}
}
_ => {
if a > b {
return true;
}
if a < b {
return false;
}
}
},
(Some(_), None) => {
return true;
}
(None, Some(_)) => {
return false;
}
(None, None) => {
return true;
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn version_cmp() {
assert!(version_ge("1", "0"));
assert!(version_ge("1.0", "0"));
assert!(!version_ge("0", "1"));
assert!(version_ge("3.2", "2.9"));
assert!(version_ge("3.2.0beta5", "2.9"));
assert!(version_ge("3.2.0beta5", "3.2.0"));
assert!(version_ge("3.2.0beta5", "3.2.0beta1"));
}
fn load_terminfo() -> terminfo::Database {
let data = include_bytes!("../../data/xterm-256color");
terminfo::Database::from_buffer(data.as_ref()).unwrap()
}
#[test]
fn empty_hint() {
let caps = Capabilities::new_with_hints(ProbeHints::default()).unwrap();
assert_eq!(caps.color_level(), ColorLevel::Sixteen);
assert_eq!(caps.sixel(), false);
assert_eq!(caps.hyperlinks(), true);
assert_eq!(caps.iterm2_image(), false);
assert_eq!(caps.bce(), false);
}
#[test]
fn bce() {
let caps =
Capabilities::new_with_hints(ProbeHints::default().colorterm_bce(Some("1".into())))
.unwrap();
assert_eq!(caps.bce(), true);
}
#[test]
fn bce_terminfo() {
let caps =
Capabilities::new_with_hints(ProbeHints::default().terminfo_db(Some(load_terminfo())))
.unwrap();
assert_eq!(caps.bce(), true);
}
#[test]
fn terminfo_color() {
let caps =
Capabilities::new_with_hints(ProbeHints::default().terminfo_db(Some(load_terminfo())))
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TrueColor);
}
#[test]
fn term_but_not_colorterm() {
let caps =
Capabilities::new_with_hints(ProbeHints::default().term(Some("xterm-256color".into())))
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TwoFiftySix);
}
#[test]
fn colorterm_but_no_term() {
let caps =
Capabilities::new_with_hints(ProbeHints::default().colorterm(Some("24bit".into())))
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TrueColor);
}
#[test]
fn term_and_colorterm() {
let caps = Capabilities::new_with_hints(
ProbeHints::default()
.term(Some("xterm-256color".into()))
.colorterm(Some("24bot".into())),
)
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TwoFiftySix);
let caps = Capabilities::new_with_hints(
ProbeHints::default()
.term(Some("xterm-256color".into()))
.colorterm(Some("24bit".into())),
)
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TrueColor);
let caps = Capabilities::new_with_hints(
ProbeHints::default()
.term(Some("xterm-256color".into()))
.colorterm(Some("truecolor".into())),
)
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TrueColor);
}
#[test]
fn iterm2_image() {
let caps = Capabilities::new_with_hints(
ProbeHints::default()
.term_program(Some("iTerm.app".into()))
.term_program_version(Some("1.0.0".into())),
)
.unwrap();
assert_eq!(caps.iterm2_image(), false);
let caps = Capabilities::new_with_hints(
ProbeHints::default()
.term_program(Some("iTerm.app".into()))
.term_program_version(Some("2.9.0".into())),
)
.unwrap();
assert_eq!(caps.iterm2_image(), false);
let caps = Capabilities::new_with_hints(
ProbeHints::default()
.term_program(Some("iTerm.app".into()))
.term_program_version(Some("2.9.20150512".into())),
)
.unwrap();
assert_eq!(caps.iterm2_image(), true);
let caps = Capabilities::new_with_hints(
ProbeHints::default()
.term_program(Some("iTerm.app".into()))
.term_program_version(Some("3.2.0beta5".into())),
)
.unwrap();
assert_eq!(caps.iterm2_image(), true);
}
}