use std::fmt::Write as _;
use std::io::{self, IsTerminal};
#[derive(Debug, Clone, Copy)]
pub struct Style {
enabled: bool,
}
impl Style {
pub fn new(enabled: bool) -> Self {
Self { enabled }
}
pub fn enabled(self) -> bool {
self.enabled
}
pub fn from_when(when: ColorWhen) -> Self {
let enabled = match when {
ColorWhen::Always => true,
ColorWhen::Never => false,
ColorWhen::Auto => {
if std::env::var_os("NO_COLOR").is_some() {
false
} else if matches!(
std::env::var("CLICOLOR_FORCE").as_deref(),
Ok(v) if v != "0" && !v.is_empty()
) {
true
} else if matches!(std::env::var("CLICOLOR").as_deref(), Ok("0")) {
false
} else {
io::stdout().is_terminal()
}
}
};
Self { enabled }
}
fn paint(self, code: &str, s: &str) -> String {
if !self.enabled || s.is_empty() {
return s.to_string();
}
format!("\x1b[{code}m{s}\x1b[0m")
}
pub fn bold(self, s: &str) -> String {
self.paint("1", s)
}
pub fn dim(self, s: &str) -> String {
self.paint("2", s)
}
pub fn cyan(self, s: &str) -> String {
self.paint("36", s)
}
pub fn bright_red(self, s: &str) -> String {
self.paint("91", s)
}
pub fn bright_green(self, s: &str) -> String {
self.paint("92", s)
}
pub fn bright_yellow(self, s: &str) -> String {
self.paint("93", s)
}
pub fn bright_blue(self, s: &str) -> String {
self.paint("94", s)
}
pub fn bright_magenta(self, s: &str) -> String {
self.paint("95", s)
}
pub fn header(self, s: &str) -> String {
self.bold(s)
}
pub fn path(self, s: &str) -> String {
self.cyan(s)
}
pub fn id(self, s: &str) -> String {
self.dim(s)
}
pub fn title(self, s: &str) -> String {
s.to_string()
}
pub fn read_status(self, is_read: bool) -> String {
if is_read {
self.dim("read")
} else {
self.bright_yellow("unread")
}
}
pub fn ok(self, s: &str) -> String {
self.bright_green(s)
}
pub fn error_label(self, s: &str) -> String {
self.bright_red(&self.bold(s))
}
pub fn key(self, s: &str) -> String {
self.dim(s)
}
const TAG_PALETTE: &'static [&'static str] = &[
"red", "orange", "yellow", "green", "teal", "blue", "indigo", "purple",
];
pub fn tag(self, name: &str, color: Option<&str>) -> String {
if name.is_empty() {
return String::new();
}
let id = color
.map(str::trim)
.filter(|c| !c.is_empty())
.filter(|c| Self::TAG_PALETTE.iter().any(|p| p.eq_ignore_ascii_case(c)))
.map(|c| c.to_ascii_lowercase())
.unwrap_or_else(|| default_tag_color_id(name).to_string());
self.paint_tag_color(name, &id)
}
fn paint_tag_color(self, name: &str, id: &str) -> String {
match id {
"red" => self.bright_red(name),
"orange" => self.paint("38;5;208", name),
"yellow" => self.bright_yellow(name),
"green" => self.bright_green(name),
"teal" => self.cyan(name),
"blue" => self.bright_blue(name),
"indigo" => self.paint("38;5;63", name),
"purple" => self.bright_magenta(name),
_ => self.bright_blue(name),
}
}
pub fn tags_join<'a>(
self,
tags: impl IntoIterator<Item = (&'a str, Option<&'a str>)>,
) -> String {
let parts: Vec<String> = tags
.into_iter()
.filter(|(n, _)| !n.is_empty())
.map(|(n, c)| self.tag(n, c))
.collect();
if parts.is_empty() {
return self.dim("-");
}
parts.join(&self.dim(","))
}
}
fn default_tag_color_id(name: &str) -> &'static str {
let mut h: u32 = 2166136261;
for b in name.trim().bytes() {
let b = if b.is_ascii_uppercase() { b + 32 } else { b };
h ^= u32::from(b);
h = h.wrapping_mul(16777619);
}
let palette = Style::TAG_PALETTE;
palette[(h as usize) % palette.len()]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorWhen {
Auto,
Always,
Never,
}
pub fn visible_width(s: &str) -> usize {
let mut w = 0usize;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
if chars.peek() == Some(&'[') {
chars.next();
for x in chars.by_ref() {
if x.is_ascii_alphabetic() {
break;
}
}
}
continue;
}
w += 1;
}
w
}
pub fn pad_right(s: &str, width: usize) -> String {
let vis = visible_width(s);
if vis >= width {
return s.to_string();
}
let mut out = s.to_string();
out.extend(std::iter::repeat_n(' ', width - vis));
out
}
pub fn truncate_chars(s: &str, max: usize) -> String {
if max == 0 {
return String::new();
}
let count = s.chars().count();
if count <= max {
return s.to_string();
}
let t: String = s.chars().take(max.saturating_sub(1)).collect();
format!("{t}…")
}
pub fn format_table(style: Style, headers: &[&str], rows: &[Vec<String>]) -> Vec<String> {
if headers.is_empty() {
return Vec::new();
}
let cols = headers.len();
let mut widths: Vec<usize> = headers.iter().map(|h| visible_width(h)).collect();
for row in rows {
for (i, cell) in row.iter().enumerate().take(cols) {
widths[i] = widths[i].max(visible_width(cell));
}
}
let mut lines = Vec::with_capacity(rows.len() + 2);
let mut header_line = String::new();
for (i, h) in headers.iter().enumerate() {
if i > 0 {
header_line.push_str(" ");
}
let _ = write!(header_line, "{}", pad_right(&style.header(h), widths[i]));
}
lines.push(header_line);
let mut rule = String::new();
for (i, w) in widths.iter().enumerate() {
if i > 0 {
rule.push_str(" ");
}
rule.push_str(&style.dim(&"-".repeat(*w)));
}
lines.push(rule);
for row in rows {
let mut line = String::new();
for (i, width) in widths.iter().enumerate().take(cols) {
if i > 0 {
line.push_str(" ");
}
let cell = row.get(i).map(String::as_str).unwrap_or("");
let _ = write!(line, "{}", pad_right(cell, *width));
}
lines.push(line);
}
lines
}