#![allow(clippy::unreadable_literal)]
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Color {
#[must_use]
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
#[must_use]
pub const fn from_hex(hex: u32) -> Self {
Self {
r: ((hex >> 16) & 0xFF) as u8,
g: ((hex >> 8) & 0xFF) as u8,
b: (hex & 0xFF) as u8,
}
}
#[must_use]
pub fn to_hex(&self) -> String {
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
#[must_use]
pub const fn to_rgb(&self) -> (u8, u8, u8) {
(self.r, self.g, self.b)
}
#[must_use]
pub fn to_ansi_fg(&self) -> String {
format!("\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
}
#[must_use]
pub fn to_ansi_bg(&self) -> String {
format!("\x1b[48;2;{};{};{}m", self.r, self.g, self.b)
}
#[must_use]
pub fn luminance(&self) -> f64 {
fn channel_luminance(c: u8) -> f64 {
let c = f64::from(c) / 255.0;
if c <= 0.03928 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
0.2126 * channel_luminance(self.r)
+ 0.7152 * channel_luminance(self.g)
+ 0.0722 * channel_luminance(self.b)
}
#[must_use]
pub fn contrast_ratio(&self, other: &Color) -> f64 {
let l1 = self.luminance();
let l2 = other.luminance();
let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) };
(lighter + 0.05) / (darker + 0.05)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThemeIcons {
pub success: &'static str,
pub failure: &'static str,
pub warning: &'static str,
pub info: &'static str,
pub arrow_right: &'static str,
pub arrow_left: &'static str,
pub bullet: &'static str,
pub lock: &'static str,
pub unlock: &'static str,
pub http: &'static str,
pub loading: &'static str,
pub route: &'static str,
pub database: &'static str,
pub time: &'static str,
pub size: &'static str,
}
impl ThemeIcons {
#[must_use]
pub const fn unicode() -> Self {
Self {
success: "\u{2713}", failure: "\u{2717}", warning: "\u{26A0}", info: "\u{2139}", arrow_right: "\u{2192}", arrow_left: "\u{2190}", bullet: "\u{2022}", lock: "\u{1F512}", unlock: "\u{1F513}", http: "\u{1F310}", loading: "\u{25CF}", route: "\u{2192}", database: "\u{1F5C4}", time: "\u{23F1}", size: "\u{1F4BE}", }
}
#[must_use]
pub const fn ascii() -> Self {
Self {
success: "[OK]",
failure: "[X]",
warning: "[!]",
info: "[i]",
arrow_right: "->",
arrow_left: "<-",
bullet: "*",
lock: "[#]",
unlock: "[ ]",
http: "[H]",
loading: "...",
route: "->",
database: "[D]",
time: "[T]",
size: "[S]",
}
}
#[must_use]
pub const fn compact() -> Self {
Self {
success: "\u{2713}", failure: "\u{2717}", warning: "!",
info: "i",
arrow_right: ">",
arrow_left: "<",
bullet: "\u{2022}", lock: "#",
unlock: "o",
http: "@",
loading: ".",
route: "/",
database: "D",
time: "T",
size: "S",
}
}
#[must_use]
pub fn auto() -> Self {
if std::env::var("TERM").is_ok_and(|t| t == "dumb")
|| std::env::var("CI").is_ok()
|| std::env::var("CLAUDE_CODE").is_ok()
|| std::env::var("CODEX_CLI").is_ok()
{
Self::ascii()
} else {
Self::unicode()
}
}
}
impl Default for ThemeIcons {
fn default() -> Self {
Self::unicode()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ThemeSpacing {
pub indent: usize,
pub panel_padding: usize,
pub table_cell_padding: usize,
pub section_gap: usize,
pub item_gap: usize,
pub method_width: usize,
pub status_width: usize,
}
impl ThemeSpacing {
#[must_use]
pub const fn default_spacing() -> Self {
Self {
indent: 2,
panel_padding: 1,
table_cell_padding: 1,
section_gap: 1,
item_gap: 0,
method_width: 7, status_width: 3, }
}
#[must_use]
pub const fn compact() -> Self {
Self {
indent: 1,
panel_padding: 0,
table_cell_padding: 1,
section_gap: 0,
item_gap: 0,
method_width: 6,
status_width: 3,
}
}
#[must_use]
pub const fn spacious() -> Self {
Self {
indent: 4,
panel_padding: 2,
table_cell_padding: 2,
section_gap: 2,
item_gap: 1,
method_width: 8,
status_width: 4,
}
}
}
impl Default for ThemeSpacing {
fn default() -> Self {
Self::default_spacing()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BoxStyle {
pub top_left: char,
pub top_right: char,
pub bottom_left: char,
pub bottom_right: char,
pub horizontal: char,
pub vertical: char,
pub left_tee: char,
pub right_tee: char,
pub top_tee: char,
pub bottom_tee: char,
pub cross: char,
}
impl BoxStyle {
#[must_use]
pub const fn rounded() -> Self {
Self {
top_left: '\u{256D}', top_right: '\u{256E}', bottom_left: '\u{2570}', bottom_right: '\u{256F}', horizontal: '\u{2500}', vertical: '\u{2502}', left_tee: '\u{251C}', right_tee: '\u{2524}', top_tee: '\u{252C}', bottom_tee: '\u{2534}', cross: '\u{253C}', }
}
#[must_use]
pub const fn square() -> Self {
Self {
top_left: '\u{250C}', top_right: '\u{2510}', bottom_left: '\u{2514}', bottom_right: '\u{2518}', horizontal: '\u{2500}', vertical: '\u{2502}', left_tee: '\u{251C}', right_tee: '\u{2524}', top_tee: '\u{252C}', bottom_tee: '\u{2534}', cross: '\u{253C}', }
}
#[must_use]
pub const fn heavy() -> Self {
Self {
top_left: '\u{250F}', top_right: '\u{2513}', bottom_left: '\u{2517}', bottom_right: '\u{251B}', horizontal: '\u{2501}', vertical: '\u{2503}', left_tee: '\u{2523}', right_tee: '\u{252B}', top_tee: '\u{2533}', bottom_tee: '\u{253B}', cross: '\u{254B}', }
}
#[must_use]
pub const fn double() -> Self {
Self {
top_left: '\u{2554}', top_right: '\u{2557}', bottom_left: '\u{255A}', bottom_right: '\u{255D}', horizontal: '\u{2550}', vertical: '\u{2551}', left_tee: '\u{2560}', right_tee: '\u{2563}', top_tee: '\u{2566}', bottom_tee: '\u{2569}', cross: '\u{256C}', }
}
#[must_use]
pub const fn ascii() -> Self {
Self {
top_left: '+',
top_right: '+',
bottom_left: '+',
bottom_right: '+',
horizontal: '-',
vertical: '|',
left_tee: '+',
right_tee: '+',
top_tee: '+',
bottom_tee: '+',
cross: '+',
}
}
#[must_use]
pub const fn none() -> Self {
Self {
top_left: ' ',
top_right: ' ',
bottom_left: ' ',
bottom_right: ' ',
horizontal: ' ',
vertical: ' ',
left_tee: ' ',
right_tee: ' ',
top_tee: ' ',
bottom_tee: ' ',
cross: ' ',
}
}
#[must_use]
pub fn horizontal_line(&self, width: usize) -> String {
std::iter::repeat_n(self.horizontal, width).collect()
}
#[must_use]
pub fn top_border(&self, width: usize) -> String {
format!(
"{}{}{}",
self.top_left,
self.horizontal_line(width.saturating_sub(2)),
self.top_right
)
}
#[must_use]
pub fn bottom_border(&self, width: usize) -> String {
format!(
"{}{}{}",
self.bottom_left,
self.horizontal_line(width.saturating_sub(2)),
self.bottom_right
)
}
}
impl Default for BoxStyle {
fn default() -> Self {
Self::rounded()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BoxStylePreset {
#[default]
Rounded,
Square,
Heavy,
Double,
Ascii,
None,
}
impl BoxStylePreset {
#[must_use]
pub const fn style(&self) -> BoxStyle {
match self {
Self::Rounded => BoxStyle::rounded(),
Self::Square => BoxStyle::square(),
Self::Heavy => BoxStyle::heavy(),
Self::Double => BoxStyle::double(),
Self::Ascii => BoxStyle::ascii(),
Self::None => BoxStyle::none(),
}
}
}
impl FromStr for BoxStylePreset {
type Err = BoxStyleParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"rounded" => Ok(Self::Rounded),
"square" => Ok(Self::Square),
"heavy" | "bold" => Ok(Self::Heavy),
"double" => Ok(Self::Double),
"ascii" | "plain" => Ok(Self::Ascii),
"none" | "invisible" => Ok(Self::None),
_ => Err(BoxStyleParseError(s.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub struct BoxStyleParseError(String);
impl std::fmt::Display for BoxStyleParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"unknown box style '{}', available: rounded, square, heavy, double, ascii, none",
self.0
)
}
}
impl std::error::Error for BoxStyleParseError {}
#[must_use]
pub fn rgb_to_hex(rgb: (u8, u8, u8)) -> String {
format!("#{:02x}{:02x}{:02x}", rgb.0, rgb.1, rgb.2)
}
#[must_use]
pub fn hex_to_rgb(hex: &str) -> Option<(u8, u8, u8)> {
let hex = hex.trim_start_matches('#');
if hex.len() == 6 {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some((r, g, b))
} else if hex.len() == 3 {
let r = u8::from_str_radix(&hex[0..1], 16).ok()? * 17;
let g = u8::from_str_radix(&hex[1..2], 16).ok()? * 17;
let b = u8::from_str_radix(&hex[2..3], 16).ok()? * 17;
Some((r, g, b))
} else {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FastApiTheme {
pub primary: Color,
pub secondary: Color,
pub accent: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
pub info: Color,
pub http_get: Color,
pub http_post: Color,
pub http_put: Color,
pub http_delete: Color,
pub http_patch: Color,
pub http_options: Color,
pub http_head: Color,
pub status_1xx: Color,
pub status_2xx: Color,
pub status_3xx: Color,
pub status_4xx: Color,
pub status_5xx: Color,
pub border: Color,
pub header: Color,
pub muted: Color,
pub highlight_bg: Color,
}
impl FastApiTheme {
#[must_use]
pub fn from_preset(preset: ThemePreset) -> Self {
match preset {
ThemePreset::FastApi | ThemePreset::Default => Self::fastapi(),
ThemePreset::Neon => Self::neon(),
ThemePreset::Minimal => Self::minimal(),
ThemePreset::Monokai => Self::monokai(),
ThemePreset::Light => Self::light(),
ThemePreset::Accessible => Self::accessible(),
}
}
#[must_use]
pub fn fastapi() -> Self {
Self {
primary: Color::from_hex(0x009688), secondary: Color::from_hex(0x4CAF50), accent: Color::from_hex(0xFF9800),
success: Color::from_hex(0x4CAF50), warning: Color::from_hex(0xFF9800), error: Color::from_hex(0xF44336), info: Color::from_hex(0x2196F3),
http_get: Color::from_hex(0x61AFFE), http_post: Color::from_hex(0x49CC90), http_put: Color::from_hex(0xFCA130), http_delete: Color::from_hex(0xF93E3E), http_patch: Color::from_hex(0x50E3C2), http_options: Color::from_hex(0x808080), http_head: Color::from_hex(0x9370DB),
status_1xx: Color::from_hex(0x808080), status_2xx: Color::from_hex(0x4CAF50), status_3xx: Color::from_hex(0x00BCD4), status_4xx: Color::from_hex(0xFFC107), status_5xx: Color::from_hex(0xF44336),
border: Color::from_hex(0x9E9E9E), header: Color::from_hex(0x009688), muted: Color::from_hex(0x757575), highlight_bg: Color::from_hex(0x263238), }
}
#[must_use]
pub fn neon() -> Self {
Self {
primary: Color::from_hex(0x00FFFF), secondary: Color::from_hex(0xFF00FF), accent: Color::from_hex(0xFFFF00),
success: Color::from_hex(0x00FF80), warning: Color::from_hex(0xFFFF00), error: Color::from_hex(0xFF0040), info: Color::from_hex(0x0080FF),
http_get: Color::from_hex(0x00FFFF),
http_post: Color::from_hex(0x00FF80),
http_put: Color::from_hex(0xFFA500),
http_delete: Color::from_hex(0xFF0040),
http_patch: Color::from_hex(0xFF00FF),
http_options: Color::from_hex(0x808080),
http_head: Color::from_hex(0x9400D3),
status_1xx: Color::from_hex(0x808080),
status_2xx: Color::from_hex(0x00FF80),
status_3xx: Color::from_hex(0x00FFFF),
status_4xx: Color::from_hex(0xFFFF00),
status_5xx: Color::from_hex(0xFF0040),
border: Color::from_hex(0x00FFFF),
header: Color::from_hex(0xFF00FF),
muted: Color::from_hex(0x646464),
highlight_bg: Color::from_hex(0x141428),
}
}
#[must_use]
pub fn minimal() -> Self {
Self {
primary: Color::from_hex(0xC8C8C8),
secondary: Color::from_hex(0xB4B4B4),
accent: Color::from_hex(0xFF9800),
success: Color::from_hex(0x64C864),
warning: Color::from_hex(0xFFB400),
error: Color::from_hex(0xFF6464),
info: Color::from_hex(0x6496FF),
http_get: Color::from_hex(0x9696C8),
http_post: Color::from_hex(0x96C896),
http_put: Color::from_hex(0xC8B464),
http_delete: Color::from_hex(0xC86464),
http_patch: Color::from_hex(0x64C8C8),
http_options: Color::from_hex(0x808080),
http_head: Color::from_hex(0xB496C8),
status_1xx: Color::from_hex(0x808080),
status_2xx: Color::from_hex(0x64C864),
status_3xx: Color::from_hex(0x64C8C8),
status_4xx: Color::from_hex(0xC8B464),
status_5xx: Color::from_hex(0xC86464),
border: Color::from_hex(0x646464),
header: Color::from_hex(0xDCDCDC),
muted: Color::from_hex(0x505050),
highlight_bg: Color::from_hex(0x1E1E1E),
}
}
#[must_use]
pub fn monokai() -> Self {
Self {
primary: Color::from_hex(0xA6E22E), secondary: Color::from_hex(0x66D9EF), accent: Color::from_hex(0xFD971F),
success: Color::from_hex(0xA6E22E),
warning: Color::from_hex(0xFD971F),
error: Color::from_hex(0xF92672), info: Color::from_hex(0x66D9EF),
http_get: Color::from_hex(0x66D9EF),
http_post: Color::from_hex(0xA6E22E),
http_put: Color::from_hex(0xFD971F),
http_delete: Color::from_hex(0xF92672),
http_patch: Color::from_hex(0xAE81FF), http_options: Color::from_hex(0x75715E),
http_head: Color::from_hex(0xAE81FF),
status_1xx: Color::from_hex(0x75715E),
status_2xx: Color::from_hex(0xA6E22E),
status_3xx: Color::from_hex(0x66D9EF),
status_4xx: Color::from_hex(0xFD971F),
status_5xx: Color::from_hex(0xF92672),
border: Color::from_hex(0x75715E),
header: Color::from_hex(0xF8F8F2),
muted: Color::from_hex(0x75715E),
highlight_bg: Color::from_hex(0x272822),
}
}
#[must_use]
pub fn light() -> Self {
Self {
primary: Color::from_hex(0x00796B), secondary: Color::from_hex(0x388E3C), accent: Color::from_hex(0xE65100),
success: Color::from_hex(0x2E7D32), warning: Color::from_hex(0xE65100), error: Color::from_hex(0xC62828), info: Color::from_hex(0x1565C0),
http_get: Color::from_hex(0x1976D2), http_post: Color::from_hex(0x2E7D32), http_put: Color::from_hex(0xE65100), http_delete: Color::from_hex(0xC62828), http_patch: Color::from_hex(0x00838F), http_options: Color::from_hex(0x616161), http_head: Color::from_hex(0x6A1B9A),
status_1xx: Color::from_hex(0x616161), status_2xx: Color::from_hex(0x2E7D32), status_3xx: Color::from_hex(0x00838F), status_4xx: Color::from_hex(0xE65100), status_5xx: Color::from_hex(0xC62828),
border: Color::from_hex(0x9E9E9E), header: Color::from_hex(0x212121), muted: Color::from_hex(0x757575), highlight_bg: Color::from_hex(0xE3F2FD), }
}
#[must_use]
pub fn accessible() -> Self {
Self {
primary: Color::from_hex(0x00FFFF), secondary: Color::from_hex(0x00FF00), accent: Color::from_hex(0xFFFF00),
success: Color::from_hex(0x00FF00), warning: Color::from_hex(0xFFFF00), error: Color::from_hex(0xFF0000), info: Color::from_hex(0x00FFFF),
http_get: Color::from_hex(0x00FFFF), http_post: Color::from_hex(0x00FF00), http_put: Color::from_hex(0xFFFF00), http_delete: Color::from_hex(0xFF0000), http_patch: Color::from_hex(0xFF00FF), http_options: Color::from_hex(0xFFFFFF), http_head: Color::from_hex(0xFF00FF),
status_1xx: Color::from_hex(0xFFFFFF), status_2xx: Color::from_hex(0x00FF00), status_3xx: Color::from_hex(0x00FFFF), status_4xx: Color::from_hex(0xFFFF00), status_5xx: Color::from_hex(0xFF0000),
border: Color::from_hex(0xFFFFFF), header: Color::from_hex(0xFFFFFF), muted: Color::from_hex(0xC0C0C0), highlight_bg: Color::from_hex(0x000080), }
}
#[must_use]
pub fn http_method_color(&self, method: &str) -> Color {
match method.to_uppercase().as_str() {
"GET" => self.http_get,
"POST" => self.http_post,
"PUT" => self.http_put,
"DELETE" => self.http_delete,
"PATCH" => self.http_patch,
"OPTIONS" => self.http_options,
"HEAD" => self.http_head,
_ => self.muted,
}
}
#[must_use]
pub fn status_code_color(&self, code: u16) -> Color {
match code {
100..=199 => self.status_1xx,
200..=299 => self.status_2xx,
300..=399 => self.status_3xx,
400..=499 => self.status_4xx,
500..=599 => self.status_5xx,
_ => self.muted,
}
}
#[must_use]
pub fn primary_hex(&self) -> String {
self.primary.to_hex()
}
#[must_use]
pub fn success_hex(&self) -> String {
self.success.to_hex()
}
#[must_use]
pub fn error_hex(&self) -> String {
self.error.to_hex()
}
#[must_use]
pub fn warning_hex(&self) -> String {
self.warning.to_hex()
}
#[must_use]
pub fn info_hex(&self) -> String {
self.info.to_hex()
}
#[must_use]
pub fn accent_hex(&self) -> String {
self.accent.to_hex()
}
#[must_use]
pub fn http_method_hex(&self, method: &str) -> String {
self.http_method_color(method).to_hex()
}
#[must_use]
pub fn status_code_hex(&self, code: u16) -> String {
self.status_code_color(code).to_hex()
}
}
impl Default for FastApiTheme {
fn default() -> Self {
Self::fastapi()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThemePreset {
#[default]
Default,
FastApi,
Neon,
Minimal,
Monokai,
Light,
Accessible,
}
impl ThemePreset {
#[must_use]
pub fn theme(&self) -> FastApiTheme {
FastApiTheme::from_preset(*self)
}
#[must_use]
pub fn available_presets() -> &'static [&'static str] {
&[
"default",
"fastapi",
"neon",
"minimal",
"monokai",
"light",
"accessible",
]
}
}
impl std::fmt::Display for ThemePreset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Default => write!(f, "default"),
Self::FastApi => write!(f, "fastapi"),
Self::Neon => write!(f, "neon"),
Self::Minimal => write!(f, "minimal"),
Self::Monokai => write!(f, "monokai"),
Self::Light => write!(f, "light"),
Self::Accessible => write!(f, "accessible"),
}
}
}
impl FromStr for ThemePreset {
type Err = ThemePresetParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"default" | "fastapi" => Ok(Self::FastApi),
"neon" | "cyberpunk" => Ok(Self::Neon),
"minimal" | "gray" | "grey" => Ok(Self::Minimal),
"monokai" | "dark" => Ok(Self::Monokai),
"light" => Ok(Self::Light),
"accessible" | "a11y" => Ok(Self::Accessible),
_ => Err(ThemePresetParseError(s.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub struct ThemePresetParseError(String);
impl ThemePresetParseError {
#[must_use]
pub fn invalid_name(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ThemePresetParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"unknown theme preset '{}', available: {}",
self.0,
ThemePreset::available_presets().join(", ")
)
}
}
impl std::error::Error for ThemePresetParseError {}
#[cfg(test)]
mod tests {
use super::*;
fn is_not_black(c: Color) -> bool {
c.r > 0 || c.g > 0 || c.b > 0
}
#[test]
fn test_color_from_hex() {
let color = Color::from_hex(0xFF5500);
assert_eq!(color.r, 0xFF);
assert_eq!(color.g, 0x55);
assert_eq!(color.b, 0x00);
}
#[test]
fn test_color_to_hex() {
let color = Color::new(255, 85, 0);
assert_eq!(color.to_hex(), "#ff5500");
}
#[test]
fn test_color_to_rgb() {
let color = Color::new(100, 150, 200);
assert_eq!(color.to_rgb(), (100, 150, 200));
}
#[test]
fn test_color_to_ansi() {
let color = Color::new(255, 128, 64);
assert_eq!(color.to_ansi_fg(), "\x1b[38;2;255;128;64m");
assert_eq!(color.to_ansi_bg(), "\x1b[48;2;255;128;64m");
}
#[test]
fn test_rgb_to_hex() {
assert_eq!(rgb_to_hex((0, 150, 136)), "#009688");
assert_eq!(rgb_to_hex((255, 255, 255)), "#ffffff");
assert_eq!(rgb_to_hex((0, 0, 0)), "#000000");
}
#[test]
fn test_hex_to_rgb_6_digit() {
assert_eq!(hex_to_rgb("#009688"), Some((0, 150, 136)));
assert_eq!(hex_to_rgb("009688"), Some((0, 150, 136)));
assert_eq!(hex_to_rgb("#FF5500"), Some((255, 85, 0)));
assert_eq!(hex_to_rgb("#ffffff"), Some((255, 255, 255)));
}
#[test]
fn test_hex_to_rgb_3_digit() {
assert_eq!(hex_to_rgb("#F00"), Some((255, 0, 0)));
assert_eq!(hex_to_rgb("0F0"), Some((0, 255, 0)));
assert_eq!(hex_to_rgb("#FFF"), Some((255, 255, 255)));
}
#[test]
fn test_hex_to_rgb_invalid() {
assert_eq!(hex_to_rgb("invalid"), None);
assert_eq!(hex_to_rgb("#12345"), None);
assert_eq!(hex_to_rgb(""), None);
assert_eq!(hex_to_rgb("#GGG"), None);
}
#[test]
fn test_theme_default_has_all_colors() {
let theme = FastApiTheme::default();
assert!(is_not_black(theme.primary));
assert!(is_not_black(theme.secondary));
assert!(is_not_black(theme.accent));
assert!(is_not_black(theme.success));
assert!(is_not_black(theme.warning));
assert!(is_not_black(theme.error));
assert!(is_not_black(theme.info));
assert!(is_not_black(theme.http_get));
assert!(is_not_black(theme.http_post));
assert!(is_not_black(theme.http_put));
assert!(is_not_black(theme.http_delete));
}
#[test]
fn test_theme_presets_differ() {
let fastapi = FastApiTheme::fastapi();
let neon = FastApiTheme::neon();
let minimal = FastApiTheme::minimal();
let monokai = FastApiTheme::monokai();
let light = FastApiTheme::light();
let accessible = FastApiTheme::accessible();
assert_ne!(fastapi, neon);
assert_ne!(fastapi, minimal);
assert_ne!(fastapi, monokai);
assert_ne!(fastapi, light);
assert_ne!(fastapi, accessible);
assert_ne!(neon, minimal);
assert_ne!(neon, monokai);
assert_ne!(neon, light);
assert_ne!(neon, accessible);
assert_ne!(minimal, monokai);
assert_ne!(minimal, light);
assert_ne!(minimal, accessible);
assert_ne!(monokai, light);
assert_ne!(monokai, accessible);
assert_ne!(light, accessible);
}
#[test]
fn test_theme_from_preset() {
assert_eq!(
FastApiTheme::from_preset(ThemePreset::Default),
FastApiTheme::fastapi()
);
assert_eq!(
FastApiTheme::from_preset(ThemePreset::FastApi),
FastApiTheme::fastapi()
);
assert_eq!(
FastApiTheme::from_preset(ThemePreset::Neon),
FastApiTheme::neon()
);
assert_eq!(
FastApiTheme::from_preset(ThemePreset::Minimal),
FastApiTheme::minimal()
);
assert_eq!(
FastApiTheme::from_preset(ThemePreset::Monokai),
FastApiTheme::monokai()
);
assert_eq!(
FastApiTheme::from_preset(ThemePreset::Light),
FastApiTheme::light()
);
assert_eq!(
FastApiTheme::from_preset(ThemePreset::Accessible),
FastApiTheme::accessible()
);
}
#[test]
fn test_http_method_colors() {
let theme = FastApiTheme::default();
assert_eq!(theme.http_method_color("GET"), theme.http_get);
assert_eq!(theme.http_method_color("get"), theme.http_get);
assert_eq!(theme.http_method_color("POST"), theme.http_post);
assert_eq!(theme.http_method_color("PUT"), theme.http_put);
assert_eq!(theme.http_method_color("DELETE"), theme.http_delete);
assert_eq!(theme.http_method_color("PATCH"), theme.http_patch);
assert_eq!(theme.http_method_color("OPTIONS"), theme.http_options);
assert_eq!(theme.http_method_color("HEAD"), theme.http_head);
assert_eq!(theme.http_method_color("UNKNOWN"), theme.muted);
}
#[test]
fn test_http_method_hex() {
let theme = FastApiTheme::default();
assert_eq!(theme.http_method_hex("GET"), theme.http_get.to_hex());
assert_eq!(theme.http_method_hex("POST"), theme.http_post.to_hex());
}
#[test]
fn test_status_code_colors() {
let theme = FastApiTheme::default();
assert_eq!(theme.status_code_color(100), theme.status_1xx);
assert_eq!(theme.status_code_color(199), theme.status_1xx);
assert_eq!(theme.status_code_color(200), theme.status_2xx);
assert_eq!(theme.status_code_color(201), theme.status_2xx);
assert_eq!(theme.status_code_color(301), theme.status_3xx);
assert_eq!(theme.status_code_color(404), theme.status_4xx);
assert_eq!(theme.status_code_color(500), theme.status_5xx);
assert_eq!(theme.status_code_color(503), theme.status_5xx);
assert_eq!(theme.status_code_color(600), theme.muted);
assert_eq!(theme.status_code_color(99), theme.muted);
}
#[test]
fn test_status_code_hex() {
let theme = FastApiTheme::default();
assert_eq!(theme.status_code_hex(200), theme.status_2xx.to_hex());
assert_eq!(theme.status_code_hex(500), theme.status_5xx.to_hex());
}
#[test]
fn test_hex_helpers() {
let theme = FastApiTheme::default();
assert_eq!(theme.primary_hex(), theme.primary.to_hex());
assert_eq!(theme.success_hex(), theme.success.to_hex());
assert_eq!(theme.error_hex(), theme.error.to_hex());
assert_eq!(theme.warning_hex(), theme.warning.to_hex());
assert_eq!(theme.info_hex(), theme.info.to_hex());
assert_eq!(theme.accent_hex(), theme.accent.to_hex());
}
#[test]
fn test_theme_preset_display() {
assert_eq!(ThemePreset::Default.to_string(), "default");
assert_eq!(ThemePreset::FastApi.to_string(), "fastapi");
assert_eq!(ThemePreset::Neon.to_string(), "neon");
assert_eq!(ThemePreset::Minimal.to_string(), "minimal");
assert_eq!(ThemePreset::Monokai.to_string(), "monokai");
assert_eq!(ThemePreset::Light.to_string(), "light");
assert_eq!(ThemePreset::Accessible.to_string(), "accessible");
}
#[test]
fn test_theme_preset_from_str() {
assert_eq!(
"default".parse::<ThemePreset>().unwrap(),
ThemePreset::FastApi
);
assert_eq!(
"fastapi".parse::<ThemePreset>().unwrap(),
ThemePreset::FastApi
);
assert_eq!(
"FASTAPI".parse::<ThemePreset>().unwrap(),
ThemePreset::FastApi
);
assert_eq!("neon".parse::<ThemePreset>().unwrap(), ThemePreset::Neon);
assert_eq!(
"cyberpunk".parse::<ThemePreset>().unwrap(),
ThemePreset::Neon
);
assert_eq!(
"minimal".parse::<ThemePreset>().unwrap(),
ThemePreset::Minimal
);
assert_eq!("gray".parse::<ThemePreset>().unwrap(), ThemePreset::Minimal);
assert_eq!("grey".parse::<ThemePreset>().unwrap(), ThemePreset::Minimal);
assert_eq!(
"monokai".parse::<ThemePreset>().unwrap(),
ThemePreset::Monokai
);
assert_eq!("dark".parse::<ThemePreset>().unwrap(), ThemePreset::Monokai);
assert_eq!("light".parse::<ThemePreset>().unwrap(), ThemePreset::Light);
assert_eq!(
"accessible".parse::<ThemePreset>().unwrap(),
ThemePreset::Accessible
);
assert_eq!(
"a11y".parse::<ThemePreset>().unwrap(),
ThemePreset::Accessible
);
}
#[test]
fn test_theme_preset_from_str_invalid() {
let err = "invalid".parse::<ThemePreset>().unwrap_err();
assert_eq!(err.invalid_name(), "invalid");
assert!(err.to_string().contains("invalid"));
assert!(err.to_string().contains("available"));
}
#[test]
fn test_theme_preset_theme() {
assert_eq!(ThemePreset::FastApi.theme(), FastApiTheme::fastapi());
assert_eq!(ThemePreset::Neon.theme(), FastApiTheme::neon());
assert_eq!(ThemePreset::Light.theme(), FastApiTheme::light());
assert_eq!(ThemePreset::Accessible.theme(), FastApiTheme::accessible());
}
#[test]
fn test_available_presets() {
let presets = ThemePreset::available_presets();
assert!(presets.contains(&"default"));
assert!(presets.contains(&"fastapi"));
assert!(presets.contains(&"neon"));
assert!(presets.contains(&"minimal"));
assert!(presets.contains(&"monokai"));
assert!(presets.contains(&"light"));
assert!(presets.contains(&"accessible"));
}
#[test]
fn test_theme_icons_unicode() {
let icons = ThemeIcons::unicode();
assert!(!icons.success.is_empty());
assert!(!icons.failure.is_empty());
assert!(!icons.warning.is_empty());
assert!(!icons.info.is_empty());
}
#[test]
fn test_theme_icons_ascii() {
let icons = ThemeIcons::ascii();
assert!(icons.success.is_ascii());
assert!(icons.failure.is_ascii());
assert!(icons.warning.is_ascii());
assert!(icons.info.is_ascii());
}
#[test]
fn test_theme_icons_compact() {
let icons = ThemeIcons::compact();
assert!(!icons.success.is_empty());
assert!(!icons.arrow_right.is_empty());
}
#[test]
fn test_theme_spacing_default() {
let spacing = ThemeSpacing::default();
assert!(spacing.indent > 0);
assert!(spacing.method_width >= 7); }
#[test]
fn test_theme_spacing_compact() {
let compact = ThemeSpacing::compact();
let default = ThemeSpacing::default();
assert!(compact.indent <= default.indent);
}
#[test]
fn test_theme_spacing_spacious() {
let spacious = ThemeSpacing::spacious();
let default = ThemeSpacing::default();
assert!(spacious.indent >= default.indent);
}
#[test]
fn test_box_style_rounded() {
let style = BoxStyle::rounded();
assert_ne!(style.top_left, style.horizontal);
assert_ne!(style.vertical, style.horizontal);
}
#[test]
fn test_box_style_ascii() {
let style = BoxStyle::ascii();
assert_eq!(style.top_left, '+');
assert_eq!(style.horizontal, '-');
assert_eq!(style.vertical, '|');
}
#[test]
fn test_box_style_horizontal_line() {
let style = BoxStyle::rounded();
let line = style.horizontal_line(5);
assert_eq!(line.chars().count(), 5);
}
#[test]
fn test_box_style_borders() {
let style = BoxStyle::rounded();
let top = style.top_border(10);
let bottom = style.bottom_border(10);
assert!(top.starts_with(style.top_left));
assert!(top.ends_with(style.top_right));
assert!(bottom.starts_with(style.bottom_left));
assert!(bottom.ends_with(style.bottom_right));
}
#[test]
fn test_box_style_preset_parse() {
assert_eq!(
"rounded".parse::<BoxStylePreset>().unwrap(),
BoxStylePreset::Rounded
);
assert_eq!(
"heavy".parse::<BoxStylePreset>().unwrap(),
BoxStylePreset::Heavy
);
assert_eq!(
"ascii".parse::<BoxStylePreset>().unwrap(),
BoxStylePreset::Ascii
);
assert!("invalid".parse::<BoxStylePreset>().is_err());
}
#[test]
fn test_color_luminance() {
let black = Color::new(0, 0, 0);
let white = Color::new(255, 255, 255);
assert!(black.luminance() < 0.01);
assert!(white.luminance() > 0.99);
}
#[test]
fn test_color_contrast_ratio() {
let black = Color::new(0, 0, 0);
let white = Color::new(255, 255, 255);
let ratio = black.contrast_ratio(&white);
assert!(ratio > 20.0);
assert!(ratio <= 21.0);
}
#[test]
fn test_accessible_theme_high_contrast() {
let accessible = FastApiTheme::accessible();
let black = Color::new(0, 0, 0);
assert!(accessible.success.contrast_ratio(&black) >= 4.5);
assert!(accessible.error.contrast_ratio(&black) >= 4.5);
assert!(accessible.warning.contrast_ratio(&black) >= 4.5);
assert!(accessible.info.contrast_ratio(&black) >= 4.5);
}
}