use crate::entry::Entry;
use lscolors::{Indicator, LsColors, Style};
pub(super) const RESET: &str = "\x1b[0m";
#[derive(Clone, Copy, Default, clap::ValueEnum)]
pub enum ColorMode {
Never,
#[default]
Auto,
Always,
}
impl ColorMode {
pub fn resolve(self, is_tty: bool) -> bool {
match self {
ColorMode::Always => true,
ColorMode::Never => false,
ColorMode::Auto => is_tty && std::env::var_os("NO_COLOR").is_none(),
}
}
}
fn style_to_ansi(style: &Style) -> String {
let mut buf = String::with_capacity(20);
let mut first = true;
let mut sep = |buf: &mut String| {
if first {
buf.push_str("\x1b[");
first = false;
} else {
buf.push(';');
}
};
let fs = &style.font_style;
if fs.bold {
sep(&mut buf);
buf.push('1');
}
if fs.dimmed {
sep(&mut buf);
buf.push('2');
}
if fs.italic {
sep(&mut buf);
buf.push('3');
}
if fs.underline {
sep(&mut buf);
buf.push('4');
}
if fs.slow_blink {
sep(&mut buf);
buf.push('5');
}
if fs.rapid_blink {
sep(&mut buf);
buf.push('6');
}
if fs.reverse {
sep(&mut buf);
buf.push('7');
}
if fs.hidden {
sep(&mut buf);
buf.push('8');
}
if fs.strikethrough {
sep(&mut buf);
buf.push('9');
}
if let Some(ref c) = style.foreground {
sep(&mut buf);
push_color_code(&mut buf, c, true);
}
if let Some(ref c) = style.background {
sep(&mut buf);
push_color_code(&mut buf, c, false);
}
if !buf.is_empty() {
buf.push('m');
}
buf
}
fn push_color_code(buf: &mut String, c: &lscolors::Color, fg: bool) {
use lscolors::Color::*;
use std::fmt::Write as _;
let base: u8 = if fg { 30 } else { 40 };
match c {
Black => write!(buf, "{}", base),
Red => write!(buf, "{}", base + 1),
Green => write!(buf, "{}", base + 2),
Yellow => write!(buf, "{}", base + 3),
Blue => write!(buf, "{}", base + 4),
Magenta => write!(buf, "{}", base + 5),
Cyan => write!(buf, "{}", base + 6),
White => write!(buf, "{}", base + 7),
BrightBlack => write!(buf, "{}", base + 60),
BrightRed => write!(buf, "{}", base + 61),
BrightGreen => write!(buf, "{}", base + 62),
BrightYellow => write!(buf, "{}", base + 63),
BrightBlue => write!(buf, "{}", base + 64),
BrightMagenta => write!(buf, "{}", base + 65),
BrightCyan => write!(buf, "{}", base + 66),
BrightWhite => write!(buf, "{}", base + 67),
Fixed(n) => {
let s = if fg { 38 } else { 48 };
write!(buf, "{s};5;{n}")
}
RGB(r, g, b) => {
let s = if fg { 38 } else { 48 };
write!(buf, "{s};2;{r};{g};{b}")
}
}
.unwrap();
}
pub(super) fn write_entry_color(
w: &mut impl std::io::Write,
lsc: &LsColors,
entry: &Entry,
) -> std::io::Result<bool> {
if entry.is_dir {
if let Some(style) = lsc.style_for_indicator(Indicator::Directory) {
let s = style_to_ansi(style);
if !s.is_empty() {
w.write_all(s.as_bytes())?;
return Ok(true);
}
}
}
if entry.is_symlink {
if let Some(style) = lsc.style_for_indicator(Indicator::SymbolicLink) {
let s = style_to_ansi(style);
if !s.is_empty() {
w.write_all(s.as_bytes())?;
return Ok(true);
}
}
}
if let Some(style) = lsc.style_for_str(entry.name()) {
let s = style_to_ansi(style);
if !s.is_empty() {
w.write_all(s.as_bytes())?;
return Ok(true);
}
}
if entry.is_hidden {
w.write_all(b"\x1b[2m")?;
return Ok(true);
}
Ok(false)
}
#[cfg(target_os = "windows")]
pub fn enable_ansi() {
use windows::Win32::System::Console::{
CONSOLE_MODE, ENABLE_VIRTUAL_TERMINAL_PROCESSING, GetConsoleMode, GetStdHandle,
STD_OUTPUT_HANDLE, SetConsoleMode,
};
unsafe {
let Ok(handle) = GetStdHandle(STD_OUTPUT_HANDLE) else {
return;
};
let mut mode = CONSOLE_MODE::default();
if GetConsoleMode(handle, &mut mode).is_ok() {
let _ = SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
}
#[cfg(not(target_os = "windows"))]
pub fn enable_ansi() {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_always() {
assert!(ColorMode::Always.resolve(false));
assert!(ColorMode::Always.resolve(true));
}
#[test]
fn resolve_never() {
assert!(!ColorMode::Never.resolve(true));
assert!(!ColorMode::Never.resolve(false));
}
#[test]
fn resolve_auto_not_tty() {
assert!(!ColorMode::Auto.resolve(false));
}
#[test]
fn style_to_ansi_bold_blue() {
let style = Style {
font_style: lscolors::FontStyle::bold(),
foreground: Some(lscolors::Color::Blue),
..Default::default()
};
assert_eq!(style_to_ansi(&style), "\x1b[1;34m");
}
#[test]
fn style_to_ansi_empty() {
let style = Style::default();
assert_eq!(style_to_ansi(&style), "");
}
#[test]
fn write_entry_color_uses_ls_colors() {
let lsc = LsColors::default();
let dir = Entry {
relative_path: "src".to_owned(),
depth: 0,
size: 0,
is_dir: true,
is_symlink: false,
is_hidden: false,
modified: 0,
};
let mut buf = Vec::new();
let wrote = write_entry_color(&mut buf, &lsc, &dir).unwrap();
assert!(wrote);
let s = String::from_utf8(buf).unwrap();
assert!(s.contains("34"), "expected blue, got: {s}");
}
#[test]
fn push_color_code_fg_bg() {
let mut buf = String::new();
push_color_code(&mut buf, &lscolors::Color::Red, true);
assert_eq!(buf, "31");
buf.clear();
push_color_code(&mut buf, &lscolors::Color::Red, false);
assert_eq!(buf, "41");
buf.clear();
push_color_code(&mut buf, &lscolors::Color::BrightCyan, true);
assert_eq!(buf, "96");
buf.clear();
push_color_code(&mut buf, &lscolors::Color::BrightCyan, false);
assert_eq!(buf, "106");
}
}