use crate::color::{ColorDepth, ColorMode};
use crate::stream::Probe;
#[cfg(feature = "detect")]
use crate::stream::Stream;
pub const DEFAULT_COLUMN_BUFFER: u16 = 2;
pub const DEFAULT_MINIMUM_WIDTH: u16 = 20;
pub const DEFAULT_FALLBACK_WIDTH: u16 = 80;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DetectOptions {
width: Option<u16>,
column_buffer: Option<u16>,
column_buffer_envs: &'static [&'static str],
minimum_width: u16,
fallback_width: Option<u16>,
}
impl DetectOptions {
#[must_use]
pub const fn new() -> Self {
Self {
width: None,
column_buffer: None,
column_buffer_envs: &[],
minimum_width: DEFAULT_MINIMUM_WIDTH,
fallback_width: Some(DEFAULT_FALLBACK_WIDTH),
}
}
#[must_use]
pub const fn width(mut self, cells: u16) -> Self {
self.width = Some(cells);
self
}
#[must_use]
pub const fn column_buffer(mut self, cells: u16) -> Self {
self.column_buffer = Some(cells);
self
}
#[must_use]
pub const fn column_buffer_envs(mut self, names: &'static [&'static str]) -> Self {
self.column_buffer_envs = names;
self
}
#[must_use]
pub const fn minimum_width(mut self, cells: u16) -> Self {
self.minimum_width = cells;
self
}
#[must_use]
pub const fn fallback_width(mut self, cells: Option<u16>) -> Self {
self.fallback_width = cells;
self
}
}
impl Default for DetectOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Terminal {
pub(crate) attached: bool,
pub(crate) color: Option<ColorDepth>,
pub(crate) width: Option<u16>,
}
impl Terminal {
#[cfg(feature = "detect")]
#[must_use]
pub fn detect(
stream: Stream,
mode: ColorMode,
options: DetectOptions,
env: &dyn Fn(&str) -> Option<String>,
) -> Self {
Self::from_probe(Probe::of(stream), mode, options, env)
}
#[cfg(feature = "detect")]
#[must_use]
pub fn from_process(stream: Stream, mode: ColorMode, options: DetectOptions) -> Self {
Self::detect(stream, mode, options, &process_env)
}
#[must_use]
pub fn from_probe(
probe: Probe,
mode: ColorMode,
options: DetectOptions,
env: &dyn Fn(&str) -> Option<String>,
) -> Self {
let lookup = Lookup(env);
let color = color_is_on(probe, mode, &lookup).then(|| detect_depth(&lookup));
Self {
attached: probe.is_terminal(),
color,
width: detect_width(probe, options, &lookup),
}
}
#[must_use]
pub fn at_depth(self, depth: ColorDepth) -> Self {
Self {
color: self.color.map(|_| depth),
..self
}
}
#[must_use]
pub fn is_attached(self) -> bool {
self.attached
}
#[must_use]
pub fn color(self) -> Option<ColorDepth> {
self.color
}
#[must_use]
pub fn width(self) -> Option<u16> {
self.width
}
}
#[cfg(feature = "detect")]
fn process_env(name: &str) -> Option<String> {
std::env::var_os(name).map(|value| value.to_string_lossy().into_owned())
}
struct Lookup<'a>(&'a dyn Fn(&str) -> Option<String>);
impl Lookup<'_> {
fn get(&self, name: &str) -> Option<String> {
(self.0)(name).filter(|value| !value.is_empty())
}
fn is(&self, name: &str, expected: &str) -> bool {
self.get(name).is_some_and(|value| value == expected)
}
}
fn color_is_on(probe: Probe, mode: ColorMode, env: &Lookup<'_>) -> bool {
match mode {
ColorMode::Always => true,
ColorMode::Never => false,
ColorMode::Auto => auto_color(probe, env),
}
}
fn auto_color(probe: Probe, env: &Lookup<'_>) -> bool {
if env.get("NO_COLOR").is_some() {
return false;
}
if env.get("CLICOLOR_FORCE").is_some_and(|value| value != "0") {
return true;
}
if env
.get("FORCE_COLOR")
.is_some_and(|value| value != "0" && value != "false")
{
return true;
}
if env.is("CLICOLOR", "0") || env.is("TERM", "dumb") {
return false;
}
probe.is_terminal()
}
fn detect_depth(env: &Lookup<'_>) -> ColorDepth {
let truecolor = env.is("COLORTERM", "truecolor")
|| env.is("COLORTERM", "24bit")
|| env.is("FORCE_COLOR", "3");
let palette = env
.get("TERM")
.is_some_and(|term| term.contains("256color"))
|| env.is("FORCE_COLOR", "2");
if truecolor {
ColorDepth::TrueColor
} else if palette {
ColorDepth::Ansi256
} else {
ColorDepth::Ansi16
}
}
fn detect_width(probe: Probe, options: DetectOptions, env: &Lookup<'_>) -> Option<u16> {
let columns = options.width.or_else(|| {
let detected = probe.columns().or_else(|| {
env.get("COLUMNS")?
.parse::<u16>()
.ok()
.filter(|columns| *columns >= 1)
});
match detected {
Some(columns) => Some(effective_width(
columns,
column_buffer(options, env),
options.minimum_width,
)),
None => options.fallback_width,
}
});
columns.map(|columns| columns.max(1))
}
fn column_buffer(options: DetectOptions, env: &Lookup<'_>) -> u16 {
options
.column_buffer
.or_else(|| {
options
.column_buffer_envs
.iter()
.filter_map(|name| env.get(name))
.find_map(|raw| raw.parse().ok())
})
.unwrap_or(DEFAULT_COLUMN_BUFFER)
}
fn effective_width(width: u16, buffer: u16, minimum: u16) -> u16 {
width.saturating_sub(buffer).max(minimum.min(width))
}