use derive_builder::*;
use failure::{err_msg, Error};
use semver::Version;
use std::env::var;
use terminfo::{self, capability as cap};
#[derive(Debug, Default, Builder, Clone)]
#[builder(default)]
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>,
}
impl ProbeHintsBuilder {
pub fn new_from_env() -> ProbeHintsBuilder {
let mut hints = ProbeHintsBuilder::default();
hints.term(var("TERM").ok());
hints.colorterm(var("COLORTERM").ok());
hints.colorterm_bce(var("COLORTERM_BCE").ok());
hints.term_program(var("TERM_PROGRAM").ok());
hints.term_program_version(var("TERM_PROGRAM_VERSION").ok());
hints.terminfo_db(terminfo::Database::from_env().ok());
hints
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorLevel {
Sixteen,
TwoFiftySix,
TrueColor,
}
#[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,
}
impl Capabilities {
pub fn new_from_env() -> Result<Self, Error> {
Self::new_with_hints(ProbeHintsBuilder::new_from_env().build().map_err(err_msg)?)
}
pub fn new_with_hints(hints: ProbeHints) -> Result<Self, Error> {
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) = hints.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 >= 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,
_ => {
hints
.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::parse(
hints
.term_program_version
.as_ref()
.unwrap_or(&"0.0.0".to_owned()),
) >= Version::parse("2.9.20150512")
}
_ => false,
}
});
let bracketed_paste = hints.bracketed_paste.unwrap_or(true);
let mouse_reporting = hints.mouse_reporting.unwrap_or(true);
Ok(Self {
color_level,
sixel,
hyperlinks,
iterm2_image,
bce,
terminfo_db: hints.terminfo_db,
bracketed_paste,
mouse_reporting,
})
}
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
}
}
#[cfg(test)]
mod test {
use super::*;
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(ProbeHintsBuilder::default().build().unwrap()).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(
ProbeHintsBuilder::default()
.colorterm_bce(Some("1".into()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.bce(), true);
}
#[test]
fn bce_terminfo() {
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.terminfo_db(Some(load_terminfo()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.bce(), true);
}
#[test]
fn terminfo_color() {
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.terminfo_db(Some(load_terminfo()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TrueColor);
}
#[test]
fn term_but_not_colorterm() {
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.term(Some("xterm-256color".into()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TwoFiftySix);
}
#[test]
fn colorterm_but_no_term() {
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.colorterm(Some("24bit".into()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TrueColor);
}
#[test]
fn term_and_colorterm() {
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.term(Some("xterm-256color".into()))
.colorterm(Some("24bot".into()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TwoFiftySix);
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.term(Some("xterm-256color".into()))
.colorterm(Some("24bit".into()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TrueColor);
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.term(Some("xterm-256color".into()))
.colorterm(Some("truecolor".into()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.color_level(), ColorLevel::TrueColor);
}
#[test]
fn iterm2_image() {
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.term_program(Some("iTerm.app".into()))
.term_program_version(Some("1.0.0".into()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.iterm2_image(), false);
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.term_program(Some("iTerm.app".into()))
.term_program_version(Some("2.9.0".into()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.iterm2_image(), false);
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.term_program(Some("iTerm.app".into()))
.term_program_version(Some("2.9.20150512".into()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.iterm2_image(), true);
let caps = Capabilities::new_with_hints(
ProbeHintsBuilder::default()
.term_program(Some("iTerm.app".into()))
.term_program_version(Some("3.2.0beta5".into()))
.build()
.unwrap(),
)
.unwrap();
assert_eq!(caps.iterm2_image(), true);
}
}