#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorChoice {
Auto,
Always,
Never,
}
const BOLD_CYAN: &str = "\x1b[1;36m"; const BOLD: &str = "\x1b[1m"; const RESET: &str = "\x1b[0m";
pub fn should_colorize(choice: ColorChoice, stdout_is_tty: bool, no_color_set: bool) -> bool {
match choice {
ColorChoice::Never => false,
ColorChoice::Always => true,
ColorChoice::Auto => stdout_is_tty && !no_color_set,
}
}
pub fn colorize_block(raw: &str) -> String {
let mut out = String::with_capacity(raw.len() + 32);
for (i, line) in raw.lines().enumerate() {
if i > 0 {
out.push('\n');
}
if line.starts_with("## ") {
out.push_str(&format!("{BOLD_CYAN}{line}{RESET}"));
} else if let Some(rest) = bold_label(line) {
out.push_str(&rest);
} else {
out.push_str(line);
}
}
if raw.ends_with('\n') {
out.push('\n');
}
out
}
fn bold_label(line: &str) -> Option<String> {
if !line.starts_with("**") {
return None;
}
let close = line.find(":**")?;
let label_end = close + 3; let label = &line[..label_end];
let rest = &line[label_end..];
Some(format!("{BOLD}{label}{RESET}{rest}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auto_off_when_piped() {
assert!(!should_colorize(ColorChoice::Auto, false, false));
}
#[test]
fn auto_on_when_tty_and_no_no_color() {
assert!(should_colorize(ColorChoice::Auto, true, false));
}
#[test]
fn auto_off_when_no_color_set_even_on_tty() {
assert!(!should_colorize(ColorChoice::Auto, true, true));
}
#[test]
fn always_colors_even_piped_and_no_color() {
assert!(should_colorize(ColorChoice::Always, false, true));
}
#[test]
fn never_off_even_on_tty() {
assert!(!should_colorize(ColorChoice::Never, true, false));
}
#[test]
fn colorize_bolds_header_and_labels() {
let raw = "## 2026-05-16 — t\n**why:** w\n**tags:** a, b";
let out = colorize_block(raw);
assert!(out.contains("\x1b[1;36m## 2026-05-16 — t\x1b[0m"));
assert!(out.contains("\x1b[1m**why:**\x1b[0m w"));
assert!(out.contains("\x1b[1m**tags:**\x1b[0m a, b"));
}
#[test]
fn colorize_preserves_non_marker_lines_verbatim() {
let raw = "## h\nplain body line\n**why:** w";
let out = colorize_block(raw);
assert!(out.contains("\nplain body line\n"));
}
#[test]
fn colorize_preserves_trailing_newline() {
assert!(colorize_block("## h\n").ends_with('\n'));
assert!(!colorize_block("## h").ends_with('\n'));
}
#[test]
fn stripping_color_codes_recovers_original() {
let raw = "## 2026-05-16 — t\n**why:** w\n**rejected:** redis\n";
let colored = colorize_block(raw);
let stripped = colored
.replace(BOLD_CYAN, "")
.replace(BOLD, "")
.replace(RESET, "");
assert_eq!(stripped, raw);
}
}