use std::env;
use std::fmt;
use std::io::IsTerminal;
use std::path::Path;
use std::sync::OnceLock;
#[derive(Debug)]
pub struct Colors {
directory: &'static str,
executable: &'static str,
reset: &'static str,
enabled: bool,
}
pub struct ColoredPath<'a> {
colors: &'a Colors,
name: &'a str,
path: &'a Path,
}
impl fmt::Display for ColoredPath<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.colors.enabled {
return f.write_str(self.name);
}
let Ok(metadata) = std::fs::metadata(self.path) else {
return f.write_str(self.name);
};
if metadata.is_dir() {
return write!(
f,
"{}{}{}",
self.colors.directory, self.name, self.colors.reset
);
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if metadata.permissions().mode() & 0o111 != 0 {
return write!(
f,
"{}{}{}",
self.colors.executable, self.name, self.colors.reset
);
}
}
#[cfg(windows)]
{
if let Some(ext) = self.path.extension() {
if let Some(ext_str) = ext.to_str() {
let ext_lower = ext_str.to_lowercase();
if matches!(ext_lower.as_str(), "exe" | "bat" | "cmd" | "com" | "ps1") {
return write!(
f,
"{}{}{}",
self.colors.executable, self.name, self.colors.reset
);
}
}
}
}
f.write_str(self.name)
}
}
static COLORS: OnceLock<Colors> = OnceLock::new();
impl Colors {
#[must_use]
pub fn get() -> &'static Self {
COLORS.get_or_init(Self::new_impl)
}
fn new_impl() -> Self {
let enabled = Self::should_enable_colors();
if enabled {
Self {
directory: "\x1b[1;34m", executable: "\x1b[1;31m", reset: "\x1b[0m",
enabled: true,
}
} else {
Self {
directory: "",
executable: "",
reset: "",
enabled: false,
}
}
}
#[must_use]
pub fn new() -> Self {
Self::new_impl()
}
fn should_enable_colors() -> bool {
if !std::io::stdout().is_terminal() {
return false;
}
if env::var("NO_COLOR").is_ok() {
return false;
}
if let Ok(val) = env::var("CLICOLOR") {
if val == "0" {
return false;
}
}
true
}
#[must_use]
pub const fn colorize<'a>(&'a self, name: &'a str, path: &'a Path) -> ColoredPath<'a> {
ColoredPath {
colors: self,
name,
path,
}
}
#[must_use]
pub const fn directory(&self) -> &str {
self.directory
}
#[must_use]
pub const fn executable(&self) -> &str {
self.executable
}
#[must_use]
pub const fn reset(&self) -> &str {
self.reset
}
#[must_use]
pub const fn enabled(&self) -> bool {
self.enabled
}
}
impl Default for Colors {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_colors_new() {
let colors = Colors::new();
let _ = colors.enabled();
}
#[test]
fn test_colors_get_is_cached() {
let colors1 = Colors::get();
let colors2 = Colors::get();
assert!(std::ptr::eq(colors1, colors2));
}
#[test]
fn test_colors_accessors() {
let colors = Colors::get();
let _ = colors.directory();
let _ = colors.executable();
let _ = colors.reset();
}
#[test]
fn test_colors_disabled_when_not_tty() {
let colors = Colors::get();
if !std::io::stdout().is_terminal() {
assert!(
!colors.enabled(),
"Colors should be disabled when stdout is not a TTY"
);
}
}
#[test]
fn test_display_formatting() {
let colors = Colors::get();
let result = colors.colorize("test", Path::new("/tmp/test"));
let _formatted = format!("File: {result}");
assert_eq!(result.to_string(), "test");
}
}