use std::env;
use std::io::{self, IsTerminal};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnsiColors;
impl AnsiColors {
pub const RESET: &'static str = "\x1b[0m";
pub const BOLD: &'static str = "\x1b[1m";
pub const DIM: &'static str = "\x1b[2m";
pub const ITALIC: &'static str = "\x1b[3m";
pub const UNDERLINE: &'static str = "\x1b[4m";
pub const RED: &'static str = "\x1b[31m";
pub const GREEN: &'static str = "\x1b[32m";
pub const YELLOW: &'static str = "\x1b[33m";
pub const BLUE: &'static str = "\x1b[34m";
pub const MAGENTA: &'static str = "\x1b[35m";
pub const CYAN: &'static str = "\x1b[36m";
pub const WHITE: &'static str = "\x1b[37m";
pub const GRAY: &'static str = "\x1b[90m";
pub const BRIGHT_RED: &'static str = "\x1b[91m";
pub const BRIGHT_GREEN: &'static str = "\x1b[92m";
pub const BRIGHT_YELLOW: &'static str = "\x1b[93m";
pub const BRIGHT_BLUE: &'static str = "\x1b[94m";
pub const BRIGHT_MAGENTA: &'static str = "\x1b[95m";
pub const BRIGHT_CYAN: &'static str = "\x1b[96m";
pub const BRIGHT_WHITE: &'static str = "\x1b[97m";
pub const BG_RED: &'static str = "\x1b[41m";
pub const BG_GREEN: &'static str = "\x1b[42m";
pub const BG_YELLOW: &'static str = "\x1b[43m";
pub const BG_BLUE: &'static str = "\x1b[44m";
pub const BG_MAGENTA: &'static str = "\x1b[45m";
pub const BG_CYAN: &'static str = "\x1b[46m";
pub const BG_WHITE: &'static str = "\x1b[47m";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorScheme {
Error,
Warning,
Info,
Success,
Highlight,
Secondary,
LineNumbers,
ErrorCode,
Context,
}
impl ColorScheme {
pub fn color_code(&self) -> &'static str {
match self {
ColorScheme::Error => AnsiColors::BRIGHT_RED,
ColorScheme::Warning => AnsiColors::BRIGHT_YELLOW,
ColorScheme::Info => AnsiColors::BRIGHT_BLUE,
ColorScheme::Success => AnsiColors::BRIGHT_GREEN,
ColorScheme::Highlight => AnsiColors::BRIGHT_CYAN,
ColorScheme::Secondary => AnsiColors::GRAY,
ColorScheme::LineNumbers => AnsiColors::BLUE,
ColorScheme::ErrorCode => AnsiColors::MAGENTA,
ColorScheme::Context => AnsiColors::WHITE,
}
}
pub fn bold_color_code(&self) -> String {
format!("{}{}", AnsiColors::BOLD, self.color_code())
}
}
#[derive(Debug, Clone)]
pub struct TerminalFormatter {
colors_enabled: bool,
force_colors: bool,
}
impl Default for TerminalFormatter {
fn default() -> Self {
Self::new()
}
}
impl TerminalFormatter {
pub fn new() -> Self {
let colors_enabled = Self::should_use_colors();
Self {
colors_enabled,
force_colors: false,
}
}
pub fn with_colors() -> Self {
Self {
colors_enabled: true,
force_colors: true,
}
}
pub fn colored() -> Self {
Self::with_colors()
}
pub fn without_colors() -> Self {
Self {
colors_enabled: false,
force_colors: false,
}
}
pub fn plain() -> Self {
Self::without_colors()
}
pub fn should_use_colors() -> bool {
if env::var("NO_COLOR").is_ok() {
return false;
}
if env::var("FORCE_COLOR").is_ok() {
return true;
}
if !io::stderr().is_terminal() {
return false;
}
match env::var("TERM") {
Ok(term) => {
if term == "dumb" {
return false;
}
true
}
Err(_) => {
false
}
}
}
pub fn colorize(&self, text: &str, scheme: ColorScheme) -> String {
if !self.colors_enabled {
return text.to_string();
}
format!("{}{}{}", scheme.color_code(), text, AnsiColors::RESET)
}
pub fn format_text(&self, text: &str, scheme: ColorScheme) -> String {
self.colorize(text, scheme)
}
pub fn bold_colorize(&self, text: &str, scheme: ColorScheme) -> String {
if !self.colors_enabled {
return text.to_string();
}
format!("{}{}{}", scheme.bold_color_code(), text, AnsiColors::RESET)
}
pub fn underline_colorize(&self, text: &str, scheme: ColorScheme) -> String {
if !self.colors_enabled {
return text.to_string();
}
format!(
"{}{}{}{}",
AnsiColors::UNDERLINE,
scheme.color_code(),
text,
AnsiColors::RESET
)
}
pub fn format_error_pointer(&self, column: usize, length: usize) -> String {
if !self.colors_enabled {
let spaces = " ".repeat(column.saturating_sub(1));
let arrows = "^".repeat(length.max(1));
return format!(" | {spaces}{arrows}");
}
let spaces = " ".repeat(column.saturating_sub(1));
let arrows = "^".repeat(length.max(1));
let colored_arrows = self.colorize(&arrows, ColorScheme::Error);
format!(
"{} | {}{}",
self.colorize("", ColorScheme::LineNumbers),
spaces,
colored_arrows
)
}
pub fn format_line_number(&self, line_num: usize) -> String {
let line_str = format!("{line_num:4}");
if !self.colors_enabled {
return format!("{line_str} | ");
}
format!("{} | ", self.colorize(&line_str, ColorScheme::LineNumbers))
}
pub fn format_error_code(&self, code: &str) -> String {
if !self.colors_enabled {
return format!("[{code}]");
}
let formatted_code = format!("[{code}]");
self.bold_colorize(&formatted_code, ColorScheme::ErrorCode)
}
pub fn format_suggestion(&self, index: usize, suggestion: &str) -> String {
if !self.colors_enabled {
return format!(" {index}. {suggestion}");
}
let number = format!("{index}.");
let colored_number = self.colorize(&number, ColorScheme::Info);
let colored_suggestion = self.colorize(suggestion, ColorScheme::Context);
format!(" {colored_number} {colored_suggestion}")
}
pub fn colors_enabled(&self) -> bool {
self.colors_enabled
}
pub fn set_colors_enabled(&mut self, enabled: bool) {
if !self.force_colors {
self.colors_enabled = enabled;
}
}
}
pub struct TerminalUtils;
impl TerminalUtils {
pub fn separator(formatter: &TerminalFormatter, width: usize) -> String {
let line = "─".repeat(width);
formatter.colorize(&line, ColorScheme::Secondary)
}
pub fn section_header(formatter: &TerminalFormatter, title: &str) -> String {
formatter.bold_colorize(title, ColorScheme::Highlight)
}
pub fn key_value(formatter: &TerminalFormatter, key: &str, value: &str) -> String {
let colored_key = formatter.colorize(key, ColorScheme::Info);
let colored_value = formatter.colorize(value, ColorScheme::Context);
format!("{colored_key}: {colored_value}")
}
pub fn file_path(formatter: &TerminalFormatter, path: &str) -> String {
formatter.underline_colorize(path, ColorScheme::Info)
}
pub fn location(formatter: &TerminalFormatter, location: &str) -> String {
formatter.colorize(location, ColorScheme::Secondary)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_scheme_codes() {
assert_eq!(ColorScheme::Error.color_code(), AnsiColors::BRIGHT_RED);
assert_eq!(ColorScheme::Warning.color_code(), AnsiColors::BRIGHT_YELLOW);
assert_eq!(ColorScheme::Info.color_code(), AnsiColors::BRIGHT_BLUE);
assert_eq!(ColorScheme::Success.color_code(), AnsiColors::BRIGHT_GREEN);
}
#[test]
fn test_formatter_without_colors() {
let formatter = TerminalFormatter::without_colors();
assert!(!formatter.colors_enabled());
let result = formatter.colorize("test", ColorScheme::Error);
assert_eq!(result, "test");
}
#[test]
fn test_formatter_with_colors() {
let formatter = TerminalFormatter::with_colors();
assert!(formatter.colors_enabled());
let result = formatter.colorize("test", ColorScheme::Error);
assert!(result.contains("test"));
assert!(result.contains(AnsiColors::BRIGHT_RED));
assert!(result.contains(AnsiColors::RESET));
}
#[test]
fn test_bold_colorize() {
let formatter = TerminalFormatter::with_colors();
let result = formatter.bold_colorize("test", ColorScheme::Error);
assert!(result.contains("test"));
assert!(result.contains(AnsiColors::BOLD));
assert!(result.contains(AnsiColors::BRIGHT_RED));
assert!(result.contains(AnsiColors::RESET));
}
#[test]
fn test_format_error_code() {
let formatter = TerminalFormatter::without_colors();
let result = formatter.format_error_code("E1001");
assert_eq!(result, "[E1001]");
let formatter = TerminalFormatter::with_colors();
let result = formatter.format_error_code("E1001");
assert!(result.contains("[E1001]"));
}
#[test]
fn test_format_line_number() {
let formatter = TerminalFormatter::without_colors();
let result = formatter.format_line_number(42);
assert_eq!(result, " 42 | ");
}
#[test]
fn test_format_suggestion() {
let formatter = TerminalFormatter::without_colors();
let result = formatter.format_suggestion(1, "Fix the error");
assert_eq!(result, " 1. Fix the error");
}
}