use colored::{Color as ColoredColor, Colorize};
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct Palette {
pub primary: Color,
pub secondary: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
pub neutral: Color,
pub dimmed: Color,
pub highlight: Color,
}
impl Default for Palette {
fn default() -> Self {
Self {
primary: Color::MutedBlue,
secondary: Color::Purple,
success: Color::Green,
warning: Color::Amber,
error: Color::Red,
neutral: Color::Gray,
dimmed: Color::Gray,
highlight: Color::Cyan,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum Color {
Standard(ColoredColor),
MutedBlue,
Purple,
Amber,
Red,
Green,
Gray,
Cyan,
}
impl Color {
#[allow(dead_code)]
pub fn apply<T: colored::Colorize>(&self, text: T) -> colored::ColoredString {
match self {
Color::Standard(c) => text.color(*c),
Color::MutedBlue => text.cyan(),
Color::Purple => text.purple(),
Color::Amber => text.yellow(),
Color::Red => text.red(),
Color::Green => text.green(),
Color::Gray => text.dimmed(),
Color::Cyan => text.cyan(),
}
}
#[allow(dead_code)]
pub fn to_colored(self) -> ColoredColor {
match self {
Color::Standard(c) => c,
Color::MutedBlue => ColoredColor::Cyan,
Color::Purple => ColoredColor::Magenta,
Color::Amber => ColoredColor::Yellow,
Color::Red => ColoredColor::Red,
Color::Green => ColoredColor::Green,
Color::Gray => ColoredColor::White,
Color::Cyan => ColoredColor::Cyan,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
#[allow(dead_code)]
pub struct Theme {
pub use_colors: bool,
pub use_emoji: bool,
pub divider_char: char,
pub box_chars: BoxStyle,
pub palette: Palette,
}
impl Theme {
pub fn new() -> Self {
Self::default()
}
#[allow(dead_code)]
pub fn minimal() -> Self {
Self {
use_colors: false,
use_emoji: false,
divider_char: '-',
box_chars: BoxStyle::Ascii,
palette: Palette::default(),
}
}
#[allow(dead_code)]
pub fn json() -> Self {
Self {
use_colors: false,
use_emoji: false,
divider_char: '-',
box_chars: BoxStyle::None,
palette: Palette::default(),
}
}
#[allow(dead_code)]
pub fn markdown() -> Self {
Self {
use_colors: false,
use_emoji: true,
divider_char: '-',
box_chars: BoxStyle::None,
palette: Palette::default(),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
#[allow(dead_code)]
pub enum BoxStyle {
#[default]
None,
Ascii,
Unicode,
UnicodeSharp,
}
impl BoxStyle {
pub fn corners(&self) -> (char, char, char, char) {
match self {
BoxStyle::None => (' ', ' ', ' ', ' '),
BoxStyle::Ascii => ('+', '+', '+', '+'),
BoxStyle::Unicode => ('╭', '╮', '╰', '╯'),
BoxStyle::UnicodeSharp => ('┌', '┐', '└', '┘'),
}
}
pub fn horizontal(&self) -> char {
match self {
BoxStyle::None => ' ',
BoxStyle::Ascii => '-',
BoxStyle::Unicode | BoxStyle::UnicodeSharp => '─',
}
}
pub fn vertical(&self) -> char {
match self {
BoxStyle::None | BoxStyle::Ascii => '|',
BoxStyle::Unicode | BoxStyle::UnicodeSharp => '│',
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Styling;
#[allow(dead_code)]
impl Styling {
pub fn header(text: &str) -> String {
format!("{}", text.bold())
}
pub fn subheader(text: &str) -> String {
format!("{}", text.dimmed())
}
pub fn success(text: &str) -> String {
format!("{}", text.green())
}
pub fn warning(text: &str) -> String {
format!("{}", text.yellow())
}
pub fn error(text: &str) -> String {
format!("{}", text.red())
}
pub fn info(text: &str) -> String {
format!("{}", text.cyan())
}
pub fn hint(text: &str) -> String {
format!("{}", text.dimmed())
}
pub fn divider(length: usize) -> String {
"─".repeat(length)
}
pub fn section_box(title: &str, content: &str, theme: &Theme) -> String {
let width = 60;
let horizontal = theme.box_chars.horizontal();
let (tl, tr, bl, br) = theme.box_chars.corners();
let mut result = String::new();
result.push(tl);
result.push_str(&format!("{} ", title).bold().to_string());
for _ in title.len() + 2..width - 1 {
result.push(horizontal);
}
result.push(tr);
result.push('\n');
for line in content.lines() {
result.push(theme.box_chars.vertical());
result.push(' ');
result.push_str(line);
for _ in line.len() + 1..width - 1 {
result.push(' ');
}
result.push(theme.box_chars.vertical());
result.push('\n');
}
result.push(bl);
for _ in 0..width - 1 {
result.push(horizontal);
}
result.push(br);
result
}
pub fn key_value(key: &str, value: &str) -> String {
format!("{}: {}", key.dimmed(), value)
}
pub fn timing(component: &str, duration_ms: u64) -> String {
let duration = if duration_ms < 1000 {
format!("{}ms", duration_ms)
} else {
format!("{:.1}s", duration_ms as f64 / 1000.0)
};
format!("{} {}", component.dimmed(), duration.green())
}
pub fn print_section(title: &str, content: &str) {
let divider = Self::divider(50);
println!("\n{}", title.cyan().bold());
println!("{}", divider.dimmed());
println!("{}", content);
println!("{}", divider.dimmed());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divider_length() {
let d = Styling::divider(10);
assert_eq!(d.chars().count(), 10);
assert!(d.chars().all(|c| c == '─'));
}
#[test]
fn test_key_value_format() {
let kv = Styling::key_value("Key", "Value");
assert!(kv.contains("Key:"));
assert!(kv.contains("Value"));
}
#[test]
fn test_timing_ms() {
let t = Styling::timing("test", 500);
assert!(t.contains("500ms"));
}
#[test]
fn test_timing_seconds() {
let t = Styling::timing("test", 2500);
assert!(t.contains("2.5s"));
}
#[test]
fn test_theme_has_colors_option() {
let theme = Theme::new();
let _ = theme.use_colors;
}
#[test]
fn test_palette_colors() {
let palette = Palette::default();
match palette.primary {
Color::MutedBlue => {}
Color::Standard(_) => {}
_ => panic!("Expected MutedBlue or Standard color"),
}
}
}
#[allow(dead_code)]
pub mod emoji {
use once_cell::sync::Lazy;
pub static CHECK: Lazy<&'static str> = Lazy::new(|| "✓");
pub static CROSS: Lazy<&'static str> = Lazy::new(|| "✗");
pub static HINT: Lazy<&'static str> = Lazy::new(|| "💡");
pub static KEY: Lazy<&'static str> = Lazy::new(|| "🔐");
}