use ratatui::style::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThemeName {
#[default]
NeonSprawl,
AcidRain,
IceBreaker,
SynthWave,
RustBelt,
GhostWire,
RedSector,
SakuraDen,
DataStream,
SolarFlare,
NeonNoir,
ChromeHeart,
BladeRunner,
VoidWalker,
ToxicWaste,
CyberFrost,
PlasmaCore,
SteelNerve,
DarkSignal,
GlitchPop,
HoloShift,
NightCity,
DeepNet,
LaserGrid,
QuantumFlux,
BioHazard,
Darkwave,
Overlock,
Megacorp,
Zaibatsu,
Iftopcolor,
}
impl ThemeName {
pub const ALL: &'static [ThemeName] = &[
ThemeName::NeonSprawl,
ThemeName::AcidRain,
ThemeName::IceBreaker,
ThemeName::SynthWave,
ThemeName::RustBelt,
ThemeName::GhostWire,
ThemeName::RedSector,
ThemeName::SakuraDen,
ThemeName::DataStream,
ThemeName::SolarFlare,
ThemeName::NeonNoir,
ThemeName::ChromeHeart,
ThemeName::BladeRunner,
ThemeName::VoidWalker,
ThemeName::ToxicWaste,
ThemeName::CyberFrost,
ThemeName::PlasmaCore,
ThemeName::SteelNerve,
ThemeName::DarkSignal,
ThemeName::GlitchPop,
ThemeName::HoloShift,
ThemeName::NightCity,
ThemeName::DeepNet,
ThemeName::LaserGrid,
ThemeName::QuantumFlux,
ThemeName::BioHazard,
ThemeName::Darkwave,
ThemeName::Overlock,
ThemeName::Megacorp,
ThemeName::Zaibatsu,
ThemeName::Iftopcolor,
];
pub fn display(self) -> &'static str {
match self {
ThemeName::NeonSprawl => "Neon Sprawl",
ThemeName::AcidRain => "Acid Rain",
ThemeName::IceBreaker => "Ice Breaker",
ThemeName::SynthWave => "Synth Wave",
ThemeName::RustBelt => "Rust Belt",
ThemeName::GhostWire => "Ghost Wire",
ThemeName::RedSector => "Red Sector",
ThemeName::SakuraDen => "Sakura Den",
ThemeName::DataStream => "Data Stream",
ThemeName::SolarFlare => "Solar Flare",
ThemeName::NeonNoir => "Neon Noir",
ThemeName::ChromeHeart => "Chrome Heart",
ThemeName::BladeRunner => "Blade Runner",
ThemeName::VoidWalker => "Void Walker",
ThemeName::ToxicWaste => "Toxic Waste",
ThemeName::CyberFrost => "Cyber Frost",
ThemeName::PlasmaCore => "Plasma Core",
ThemeName::SteelNerve => "Steel Nerve",
ThemeName::DarkSignal => "Dark Signal",
ThemeName::GlitchPop => "Glitch Pop",
ThemeName::HoloShift => "Holo Shift",
ThemeName::NightCity => "Night City",
ThemeName::DeepNet => "Deep Net",
ThemeName::LaserGrid => "Laser Grid",
ThemeName::QuantumFlux => "Quantum Flux",
ThemeName::BioHazard => "Bio Hazard",
ThemeName::Darkwave => "Darkwave",
ThemeName::Overlock => "Overlock",
ThemeName::Megacorp => "Megacorp",
ThemeName::Zaibatsu => "Zaibatsu",
ThemeName::Iftopcolor => "Iftop Color",
}
}
pub fn token(self) -> &'static str {
match self {
ThemeName::NeonSprawl => "neon_sprawl",
ThemeName::AcidRain => "acid_rain",
ThemeName::IceBreaker => "ice_breaker",
ThemeName::SynthWave => "synth_wave",
ThemeName::RustBelt => "rust_belt",
ThemeName::GhostWire => "ghost_wire",
ThemeName::RedSector => "red_sector",
ThemeName::SakuraDen => "sakura_den",
ThemeName::DataStream => "data_stream",
ThemeName::SolarFlare => "solar_flare",
ThemeName::NeonNoir => "neon_noir",
ThemeName::ChromeHeart => "chrome_heart",
ThemeName::BladeRunner => "blade_runner",
ThemeName::VoidWalker => "void_walker",
ThemeName::ToxicWaste => "toxic_waste",
ThemeName::CyberFrost => "cyber_frost",
ThemeName::PlasmaCore => "plasma_core",
ThemeName::SteelNerve => "steel_nerve",
ThemeName::DarkSignal => "dark_signal",
ThemeName::GlitchPop => "glitch_pop",
ThemeName::HoloShift => "holo_shift",
ThemeName::NightCity => "night_city",
ThemeName::DeepNet => "deep_net",
ThemeName::LaserGrid => "laser_grid",
ThemeName::QuantumFlux => "quantum_flux",
ThemeName::BioHazard => "bio_hazard",
ThemeName::Darkwave => "darkwave",
ThemeName::Overlock => "overlock",
ThemeName::Megacorp => "megacorp",
ThemeName::Zaibatsu => "zaibatsu",
ThemeName::Iftopcolor => "iftop_color",
}
}
pub fn from_token(s: &str) -> Option<ThemeName> {
Self::ALL.iter().copied().find(|t| t.token() == s)
}
}
fn palette(name: ThemeName) -> (u8, u8, u8, u8, u8, u8) {
match name {
ThemeName::NeonSprawl => (27, 48, 135, 141, 63, 99),
ThemeName::AcidRain => (28, 46, 34, 40, 22, 35),
ThemeName::IceBreaker => (19, 39, 25, 33, 21, 32),
ThemeName::SynthWave => (91, 177, 128, 134, 93, 97),
ThemeName::RustBelt => (172, 214, 178, 220, 166, 130),
ThemeName::GhostWire => (37, 50, 44, 87, 30, 23),
ThemeName::RedSector => (160, 203, 196, 210, 124, 88),
ThemeName::SakuraDen => (175, 218, 182, 225, 169, 132),
ThemeName::DataStream => (22, 46, 28, 119, 34, 22),
ThemeName::SolarFlare => (202, 220, 196, 213, 160, 125),
ThemeName::NeonNoir => (201, 231, 93, 219, 57, 53),
ThemeName::ChromeHeart => (250, 255, 246, 253, 243, 239),
ThemeName::BladeRunner => (208, 37, 166, 73, 130, 23),
ThemeName::VoidWalker => (55, 99, 54, 141, 92, 17),
ThemeName::ToxicWaste => (118, 190, 154, 226, 82, 58),
ThemeName::CyberFrost => (159, 195, 153, 189, 111, 67),
ThemeName::PlasmaCore => (199, 213, 163, 207, 126, 89),
ThemeName::SteelNerve => (68, 110, 60, 146, 24, 236),
ThemeName::DarkSignal => (30, 43, 23, 79, 29, 16),
ThemeName::GlitchPop => (201, 51, 226, 47, 196, 21),
ThemeName::HoloShift => (123, 219, 159, 183, 87, 133),
ThemeName::NightCity => (214, 227, 209, 223, 172, 94),
ThemeName::DeepNet => (19, 33, 17, 75, 26, 16),
ThemeName::LaserGrid => (46, 201, 51, 226, 196, 21),
ThemeName::QuantumFlux => (135, 75, 171, 111, 98, 61),
ThemeName::BioHazard => (148, 184, 106, 192, 64, 22),
ThemeName::Darkwave => (53, 140, 89, 176, 127, 52),
ThemeName::Overlock => (196, 208, 160, 214, 124, 52),
ThemeName::Megacorp => (252, 39, 245, 81, 242, 236),
ThemeName::Zaibatsu => (167, 216, 131, 224, 95, 52),
ThemeName::Iftopcolor => (21, 46, 28, 48, 33, 19),
}
}
#[derive(Debug, Clone, Copy)]
pub struct Theme {
pub name: ThemeName,
pub accent: Color,
pub primary: Color,
pub alt: Color,
pub label: Color,
pub dim: Color,
pub help_bg: Color,
pub help_border: Color,
pub help_title: Color,
pub help_section: Color,
pub help_key: Color,
pub help_val: Color,
}
impl Default for Theme {
fn default() -> Self {
Theme::from_name(ThemeName::default())
}
}
impl Theme {
pub fn from_name(name: ThemeName) -> Self {
let (c1, c2, c3, c4, c5, c6) = palette(name);
Self::from_palette(name, [c1, c2, c3, c4, c5, c6])
}
pub fn from_palette(name: ThemeName, p: [u8; 6]) -> Self {
Theme {
name,
primary: Color::Indexed(p[0]),
accent: Color::Indexed(p[1]),
alt: Color::Indexed(p[2]),
label: Color::Indexed(p[3]),
dim: Color::Indexed(p[4]),
help_bg: Color::Indexed(236),
help_border: Color::Indexed(p[0]),
help_title: Color::Indexed(p[0]),
help_section: Color::Indexed(p[5]),
help_key: Color::Indexed(p[1]),
help_val: Color::Indexed(p[3]),
}
}
pub fn swatch(name: ThemeName) -> [(Color, &'static str); 6] {
let p = base_palette(name);
[
(Color::Indexed(p[0]), "██"),
(Color::Indexed(p[1]), "██"),
(Color::Indexed(p[2]), "██"),
(Color::Indexed(p[3]), "██"),
(Color::Indexed(p[4]), "██"),
(Color::Indexed(p[5]), "██"),
]
}
}
pub fn base_palette(name: ThemeName) -> [u8; 6] {
let (c1, c2, c3, c4, c5, c6) = palette(name);
[c1, c2, c3, c4, c5, c6]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn palettes_match_iftoprs() {
assert_eq!(
base_palette(ThemeName::NeonSprawl),
[27, 48, 135, 141, 63, 99]
);
assert_eq!(
base_palette(ThemeName::BladeRunner),
[208, 37, 166, 73, 130, 23]
);
}
#[test]
fn every_scheme_resolves_overlay_roles() {
for &name in ThemeName::ALL {
let t = Theme::from_name(name);
for (role, c) in [
("help_bg", t.help_bg),
("help_border", t.help_border),
("help_title", t.help_title),
("help_section", t.help_section),
("help_key", t.help_key),
("help_val", t.help_val),
] {
assert!(
matches!(c, Color::Indexed(_)),
"{}: {role} is not indexed",
name.token()
);
}
}
}
#[test]
fn overlay_roles_follow_edited_palette() {
let t = Theme::from_palette(ThemeName::NeonSprawl, [1, 2, 3, 4, 5, 6]);
assert_eq!(t.help_border, Color::Indexed(1));
assert_eq!(t.help_title, Color::Indexed(1));
assert_eq!(t.help_key, Color::Indexed(2));
assert_eq!(t.help_val, Color::Indexed(4));
assert_eq!(t.help_section, Color::Indexed(6));
assert_eq!(t.help_bg, Color::Indexed(236));
}
#[test]
fn swatch_cells_are_base_palette_in_order() {
for &name in ThemeName::ALL {
let p = base_palette(name);
for (i, (color, block)) in Theme::swatch(name).iter().enumerate() {
assert_eq!(*color, Color::Indexed(p[i]), "{}", name.token());
assert_eq!(*block, "██");
}
}
}
#[test]
fn tokens_are_unique_and_roundtrip() {
let mut seen = std::collections::HashSet::new();
for &name in ThemeName::ALL {
assert!(
seen.insert(name.token()),
"duplicate token {}",
name.token()
);
assert_eq!(ThemeName::from_token(name.token()), Some(name));
}
assert_eq!(ThemeName::from_token("no_such_scheme"), None);
}
#[test]
fn display_names_are_unique_and_non_empty() {
let mut seen = std::collections::HashSet::new();
for &name in ThemeName::ALL {
assert!(!name.display().is_empty());
assert!(
seen.insert(name.display()),
"duplicate name {}",
name.display()
);
}
}
}