use anstyle::{AnsiColor, Color, Style};
pub const ERROR: Style = Style::new()
.fg_color(Some(Color::Ansi(AnsiColor::Red)))
.bold();
pub const WARNING: Style = Style::new()
.fg_color(Some(Color::Ansi(AnsiColor::Yellow)))
.bold();
pub const INFO: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan)));
pub const SUCCESS: Style = Style::new()
.fg_color(Some(Color::Ansi(AnsiColor::Green)))
.bold();
pub const PATH: Style = Style::new().bold();
pub const RULE_ID: Style = Style::new().dimmed();
pub const DOCS: Style = Style::new()
.fg_color(Some(Color::Ansi(AnsiColor::Blue)))
.underline();
pub const DOCS_LINKED: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Blue)));
pub const FIXABLE: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)));
pub const DIM: Style = Style::new().dimmed();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GlyphSet {
pub error: &'static str,
pub warning: &'static str,
pub info: &'static str,
pub success: &'static str,
pub rule: &'static str,
pub bullet: &'static str,
pub arrow: &'static str,
}
impl GlyphSet {
pub const UNICODE: Self = Self {
error: "✗",
warning: "⚠",
info: "ℹ",
success: "✓",
rule: "─",
bullet: "·",
arrow: "→",
};
pub const ASCII: Self = Self {
error: "x",
warning: "!",
info: "i",
success: "v",
rule: "-",
bullet: "*",
arrow: "->",
};
#[must_use]
pub fn detect(force_ascii: bool) -> Self {
Self::decide(force_ascii, std::env::var("TERM").ok().as_deref())
}
#[must_use]
pub fn decide(force_ascii: bool, term: Option<&str>) -> Self {
if force_ascii || matches!(term, Some("dumb")) {
Self::ASCII
} else {
Self::UNICODE
}
}
}
impl Default for GlyphSet {
fn default() -> Self {
Self::UNICODE
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
impl ColorChoice {
#[must_use]
pub fn resolve(self) -> Self {
if matches!(self, Self::Auto) && cliclor_force_is_set() {
return Self::Always;
}
self
}
#[must_use]
pub fn to_anstream(self) -> anstream::ColorChoice {
match self {
Self::Auto => anstream::ColorChoice::Auto,
Self::Always => anstream::ColorChoice::Always,
Self::Never => anstream::ColorChoice::Never,
}
}
}
fn cliclor_force_is_set() -> bool {
cliclor_force_is_set_in(std::env::var_os("CLICOLOR_FORCE").as_deref())
}
fn cliclor_force_is_set_in(env_var: Option<&std::ffi::OsStr>) -> bool {
match env_var {
Some(v) => v != "0",
None => false,
}
}
impl std::str::FromStr for ColorChoice {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"auto" | "" => Ok(Self::Auto),
"always" | "yes" | "true" | "on" => Ok(Self::Always),
"never" | "no" | "false" | "off" => Ok(Self::Never),
other => Err(format!(
"invalid --color value {other:?}; expected auto|always|never"
)),
}
}
}
pub fn write_hyperlink(
w: &mut dyn std::io::Write,
url: &str,
text: &str,
enabled: bool,
) -> std::io::Result<()> {
if enabled {
write!(w, "\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\")
} else {
write!(w, "{text}")
}
}
#[derive(Debug, Clone, Copy)]
pub struct HumanOptions {
pub glyphs: GlyphSet,
pub hyperlinks: bool,
pub width: Option<usize>,
pub compact: bool,
pub show_docs: bool,
}
impl Default for HumanOptions {
fn default() -> Self {
Self {
glyphs: GlyphSet::default(),
hyperlinks: false,
width: None,
compact: false,
show_docs: true,
}
}
}
impl HumanOptions {
pub const DEFAULT_WIDTH: usize = 80;
#[must_use]
pub fn effective_width(&self) -> usize {
self.width.unwrap_or(Self::DEFAULT_WIDTH).clamp(40, 120)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_choice_parses_common_forms() {
assert_eq!("auto".parse::<ColorChoice>().unwrap(), ColorChoice::Auto);
assert_eq!(
"always".parse::<ColorChoice>().unwrap(),
ColorChoice::Always
);
assert_eq!("never".parse::<ColorChoice>().unwrap(), ColorChoice::Never);
assert_eq!("YES".parse::<ColorChoice>().unwrap(), ColorChoice::Always);
assert_eq!("off".parse::<ColorChoice>().unwrap(), ColorChoice::Never);
assert!("sparkles".parse::<ColorChoice>().is_err());
}
#[test]
fn glyph_set_decide_respects_dumb_term() {
assert_eq!(GlyphSet::decide(false, Some("dumb")), GlyphSet::ASCII);
assert_eq!(
GlyphSet::decide(false, Some("xterm-256color")),
GlyphSet::UNICODE
);
assert_eq!(GlyphSet::decide(false, None), GlyphSet::UNICODE);
}
#[test]
fn glyph_set_force_ascii_overrides_term() {
assert_eq!(
GlyphSet::decide(true, Some("xterm-256color")),
GlyphSet::ASCII
);
assert_eq!(GlyphSet::decide(true, Some("dumb")), GlyphSet::ASCII);
assert_eq!(GlyphSet::decide(true, None), GlyphSet::ASCII);
}
#[test]
fn hyperlink_enabled_emits_osc8_sequence() {
let mut out = Vec::new();
write_hyperlink(&mut out, "https://example.com", "click", true).unwrap();
let s = String::from_utf8(out).unwrap();
assert_eq!(s, "\x1b]8;;https://example.com\x1b\\click\x1b]8;;\x1b\\");
}
#[test]
fn hyperlink_disabled_emits_plain_text() {
let mut out = Vec::new();
write_hyperlink(&mut out, "https://example.com", "click", false).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), "click");
}
#[test]
fn docs_style_carries_underline_and_blue() {
let s = format!("{DOCS}link{DOCS:#}");
assert!(s.contains("\x1b[4m"), "DOCS must carry underline: {s:?}");
assert!(s.contains("\x1b[34m"), "DOCS must carry blue: {s:?}");
}
#[test]
fn docs_linked_drops_underline_keeps_blue() {
let s = format!("{DOCS_LINKED}link{DOCS_LINKED:#}");
assert!(
!s.contains("\x1b[4m"),
"DOCS_LINKED must NOT carry an explicit underline: {s:?}"
);
assert!(
s.contains("\x1b[34m"),
"DOCS_LINKED must keep blue as the link convention: {s:?}"
);
}
use std::ffi::OsStr;
#[test]
fn cliclor_force_helper_treats_one_as_set() {
assert!(cliclor_force_is_set_in(Some(OsStr::new("1"))));
}
#[test]
fn cliclor_force_helper_treats_other_truthy_values_as_set() {
assert!(cliclor_force_is_set_in(Some(OsStr::new("yes"))));
assert!(cliclor_force_is_set_in(Some(OsStr::new("true"))));
assert!(cliclor_force_is_set_in(Some(OsStr::new(""))));
}
#[test]
fn cliclor_force_helper_treats_zero_as_no_force() {
assert!(!cliclor_force_is_set_in(Some(OsStr::new("0"))));
}
#[test]
fn cliclor_force_helper_unset_returns_false() {
assert!(!cliclor_force_is_set_in(None));
}
#[test]
fn resolve_is_idempotent_for_explicit_choices() {
assert_eq!(ColorChoice::Always.resolve(), ColorChoice::Always);
assert_eq!(ColorChoice::Never.resolve(), ColorChoice::Never);
}
}