use std::io::IsTerminal;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tone {
Red,
Yellow,
Green,
Dim,
}
impl Tone {
fn code(self) -> &'static str {
match self {
Self::Red => "\u{1b}[31m",
Self::Yellow => "\u{1b}[33m",
Self::Green => "\u{1b}[32m",
Self::Dim => "\u{1b}[2m",
}
}
}
const RESET: &str = "\u{1b}[0m";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Style {
color: bool,
}
impl Style {
#[cfg(test)]
pub const PLAIN: Self = Self { color: false };
#[cfg(test)]
pub const COLOR: Self = Self { color: true };
pub fn detect() -> Self {
Self {
color: std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none(),
}
}
pub fn paint(self, tone: Option<Tone>, text: &str) -> String {
match tone.filter(|_| self.color) {
Some(tone) => format!("{}{text}{RESET}", tone.code()),
None => text.to_owned(),
}
}
pub fn column(self, tone: Option<Tone>, text: &str, width: usize) -> String {
let mut padded = self.paint(tone, text);
for _ in text.chars().count()..width {
padded.push(' ');
}
padded
}
}
pub fn state_tone(state: &str) -> Option<Tone> {
match state {
"failed" => Some(Tone::Red),
"needs_review" => Some(Tone::Yellow),
"merged" => Some(Tone::Dim),
_ => None,
}
}
pub fn marker_tone(marker: &str) -> Option<Tone> {
match marker {
"FAIL" => Some(Tone::Red),
"warn" => Some(Tone::Yellow),
"ok" => Some(Tone::Green),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{Style, Tone, marker_tone, state_tone};
#[test]
fn the_plain_style_never_emits_an_escape() {
assert_eq!(Style::PLAIN.paint(Some(Tone::Red), "FAIL"), "FAIL");
assert_eq!(
Style::PLAIN.column(Some(Tone::Red), "failed", 8),
"failed "
);
assert!(
!Style::PLAIN
.paint(Some(Tone::Dim), "merged")
.contains('\u{1b}')
);
}
#[test]
fn the_color_style_wraps_the_token_and_resets() {
assert_eq!(
Style::COLOR.paint(Some(Tone::Red), "FAIL"),
"\u{1b}[31mFAIL\u{1b}[0m"
);
assert_eq!(Style::COLOR.paint(None, "ready"), "ready");
}
#[test]
fn padding_counts_visible_columns_only() {
let colored = Style::COLOR.column(Some(Tone::Yellow), "needs_review", 14);
assert_eq!(colored, "\u{1b}[33mneeds_review\u{1b}[0m ");
assert_eq!(Style::COLOR.column(None, "ok", 4), "ok ");
}
#[test]
fn only_the_states_and_markers_worth_flagging_have_a_tone() {
assert_eq!(state_tone("failed"), Some(Tone::Red));
assert_eq!(state_tone("needs_review"), Some(Tone::Yellow));
assert_eq!(state_tone("merged"), Some(Tone::Dim));
assert_eq!(state_tone("ready"), None);
assert_eq!(state_tone("claimed"), None);
assert_eq!(marker_tone("FAIL"), Some(Tone::Red));
assert_eq!(marker_tone("warn"), Some(Tone::Yellow));
assert_eq!(marker_tone("ok"), Some(Tone::Green));
assert_eq!(marker_tone(".."), None);
assert_eq!(marker_tone("-"), None);
}
}