use crate::color::{Color, Named, Style};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub mod tokens {
pub const DEFAULT: &str = "default";
pub const DATE: &str = "date";
pub const TIME: &str = "time";
pub const HOST: &str = "host";
pub const IP: &str = "ip";
pub const MAC: &str = "mac";
pub const PID: &str = "pid";
pub const PROCESS: &str = "process";
pub const ERROR: &str = "error";
pub const WARNING: &str = "warning";
pub const DEBUG: &str = "debug";
pub const INFO: &str = "info";
pub const GOOD: &str = "good";
pub const BAD: &str = "bad";
pub const SYSTEM: &str = "system";
pub const KEYWORD: &str = "keyword";
pub const EMAIL: &str = "email";
pub const URI: &str = "uri";
pub const DIR: &str = "dir";
pub const FILE: &str = "file";
pub const SIZE: &str = "size";
pub const VERSION: &str = "version";
pub const NUMBER: &str = "number";
pub const ADDRESS: &str = "address";
pub const PERCENTAGE: &str = "percentage";
pub const SIGNAL: &str = "signal";
pub const PROTOCOL: &str = "protocol";
pub const SERVICE: &str = "service";
pub const USER: &str = "user";
pub const HTTP_METHOD: &str = "http_method";
pub const HTTP_CODE: &str = "http_code";
pub const STRING: &str = "string";
pub const BRACKET: &str = "bracket";
pub const PUNCT: &str = "punct";
pub const UNKNOWN: &str = "unknown";
pub const GETTIME: &str = "gettime"; pub const GETSIZE: &str = "getsize"; pub const FTP_CODE: &str = "ftp_code";
pub const IDENT: &str = "ident"; pub const CTYPE: &str = "ctype"; pub const SUBJECT: &str = "subject"; pub const FIELD: &str = "field"; pub const CHAIN: &str = "chain"; pub const PKG: &str = "pkg"; pub const PKGSTATUS: &str = "pkgstatus"; pub const INCOMING: &str = "incoming"; pub const OUTGOING: &str = "outgoing"; pub const UNIQUE: &str = "unique"; pub const REPEAT: &str = "repeat"; pub const SWAPNUM: &str = "swapnum"; pub const PROXY_HIT: &str = "proxy_hit";
pub const PROXY_MISS: &str = "proxy_miss";
pub const PROXY_DENIED: &str = "proxy_denied";
pub const PROXY_REFRESH: &str = "proxy_refresh";
pub const PROXY_SWAPFAIL: &str = "proxy_swapfail";
pub const PROXY_DIRECT: &str = "proxy_direct";
pub const PROXY_PARENT: &str = "proxy_parent";
pub const PROXY_CREATE: &str = "proxy_create";
pub const PROXY_SWAPIN: &str = "proxy_swapin";
pub const PROXY_SWAPOUT: &str = "proxy_swapout";
pub const PROXY_RELEASE: &str = "proxy_release";
pub const THREAD: &str = "thread"; pub const LEVEL: &str = "level"; pub const FACILITY: &str = "facility"; pub const DURATION: &str = "duration"; pub const LATENCY: &str = "latency"; pub const HASH: &str = "hash"; pub const JSON_KEY: &str = "json_key"; }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Theme {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub base: Style,
#[serde(default)]
pub styles: BTreeMap<String, Style>,
}
impl Theme {
pub fn style(&self, token: &str) -> Style {
self.styles.get(token).copied().unwrap_or(self.base)
}
pub fn merged_with(mut self, other: &Theme) -> Theme {
if !other.description.is_empty() {
self.description = other.description.clone();
}
if !other.base.is_plain() {
self.base = other.base;
}
for (k, v) in &other.styles {
self.styles.insert(k.clone(), *v);
}
self
}
}
fn theme(name: &str, description: &str, base: Style, entries: &[(&str, Style)]) -> Theme {
Theme {
name: name.to_string(),
description: description.to_string(),
base,
styles: entries.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
}
}
#[derive(Clone, Copy)]
struct Palette {
name: &'static str,
display: &'static str,
c: [u8; 6],
}
const PALETTES: &[Palette] = &[
Palette {
name: "neon-sprawl",
display: "Neon Sprawl",
c: [27, 48, 135, 141, 63, 99],
},
Palette {
name: "acid-rain",
display: "Acid Rain",
c: [28, 46, 34, 40, 22, 35],
},
Palette {
name: "ice-breaker",
display: "Ice Breaker",
c: [19, 39, 25, 33, 21, 32],
},
Palette {
name: "synth-wave",
display: "Synth Wave",
c: [91, 177, 128, 134, 93, 97],
},
Palette {
name: "rust-belt",
display: "Rust Belt",
c: [172, 214, 178, 220, 166, 130],
},
Palette {
name: "ghost-wire",
display: "Ghost Wire",
c: [37, 50, 44, 87, 30, 23],
},
Palette {
name: "red-sector",
display: "Red Sector",
c: [160, 203, 196, 210, 124, 88],
},
Palette {
name: "sakura-den",
display: "Sakura Den",
c: [175, 218, 182, 225, 169, 132],
},
Palette {
name: "data-stream",
display: "Data Stream",
c: [22, 46, 28, 119, 34, 22],
},
Palette {
name: "solar-flare",
display: "Solar Flare",
c: [202, 220, 196, 213, 160, 125],
},
Palette {
name: "neon-noir",
display: "Neon Noir",
c: [201, 231, 93, 219, 57, 53],
},
Palette {
name: "chrome-heart",
display: "Chrome Heart",
c: [250, 255, 246, 253, 243, 239],
},
Palette {
name: "blade-runner",
display: "Blade Runner",
c: [208, 37, 166, 73, 130, 23],
},
Palette {
name: "void-walker",
display: "Void Walker",
c: [55, 99, 54, 141, 92, 17],
},
Palette {
name: "toxic-waste",
display: "Toxic Waste",
c: [118, 190, 154, 226, 82, 58],
},
Palette {
name: "cyber-frost",
display: "Cyber Frost",
c: [159, 195, 153, 189, 111, 67],
},
Palette {
name: "plasma-core",
display: "Plasma Core",
c: [199, 213, 163, 207, 126, 89],
},
Palette {
name: "steel-nerve",
display: "Steel Nerve",
c: [68, 110, 60, 146, 24, 236],
},
Palette {
name: "dark-signal",
display: "Dark Signal",
c: [30, 43, 23, 79, 29, 16],
},
Palette {
name: "glitch-pop",
display: "Glitch Pop",
c: [201, 51, 226, 47, 196, 21],
},
Palette {
name: "holo-shift",
display: "Holo Shift",
c: [123, 219, 159, 183, 87, 133],
},
Palette {
name: "night-city",
display: "Night City",
c: [214, 227, 209, 223, 172, 94],
},
Palette {
name: "deep-net",
display: "Deep Net",
c: [19, 33, 17, 75, 26, 16],
},
Palette {
name: "laser-grid",
display: "Laser Grid",
c: [46, 201, 51, 226, 196, 21],
},
Palette {
name: "quantum-flux",
display: "Quantum Flux",
c: [135, 75, 171, 111, 98, 61],
},
Palette {
name: "bio-hazard",
display: "Bio Hazard",
c: [148, 184, 106, 192, 64, 22],
},
Palette {
name: "darkwave",
display: "Darkwave",
c: [53, 140, 89, 176, 127, 52],
},
Palette {
name: "overlock",
display: "Overlock",
c: [196, 208, 160, 214, 124, 52],
},
Palette {
name: "megacorp",
display: "Megacorp",
c: [252, 39, 245, 81, 242, 236],
},
Palette {
name: "zaibatsu",
display: "Zaibatsu",
c: [167, 216, 131, 224, 95, 52],
},
Palette {
name: "iftopcolor",
display: "iftopcolor",
c: [21, 46, 28, 48, 33, 19],
},
];
fn shift_lighter(c: u8) -> u8 {
if c >= 232 {
c.saturating_add(2)
} else if c >= 16 {
let idx = c - 16;
let b = idx % 6;
let g = (idx / 6) % 6;
let r = idx / 36;
16 + (r + 1).min(5) * 36 + (g + 1).min(5) * 6 + (b + 1).min(5)
} else {
c.saturating_add(8).min(15)
}
}
fn theme_from_palette(p: &Palette) -> Theme {
use tokens::*;
let [c1, c2, c3, c4, c5, _c6] = p.c;
let pl = shift_lighter(c3);
let i = |n: u8| Style::fg(Color::idx(n));
let bi = |n: u8| Style::bold_fg(Color::idx(n));
let ui = |n: u8| Style {
underline: true,
..Style::fg(Color::idx(n))
};
let it = |n: u8| Style {
italic: true,
..Style::fg(Color::idx(n))
};
let entries: Vec<(&str, Style)> = vec![
(DATE, bi(c2)),
(TIME, i(c2)),
(HOST, bi(c3)),
(IP, i(c3)),
(MAC, i(c5)),
(PID, bi(c4)),
(PROCESS, bi(pl)),
(ERROR, bi(c2)),
(WARNING, bi(c4)),
(DEBUG, i(c5)),
(INFO, bi(c1)),
(GOOD, bi(c3)),
(BAD, bi(c2)),
(SYSTEM, i(c4)),
(KEYWORD, bi(c2)),
(EMAIL, i(c3)),
(URI, ui(c3)),
(DIR, i(c4)),
(FILE, i(c1)),
(SIZE, i(c3)),
(VERSION, i(c4)),
(NUMBER, i(c4)),
(ADDRESS, i(c5)),
(PERCENTAGE, bi(c3)),
(SIGNAL, bi(c2)),
(PROTOCOL, i(c3)),
(SERVICE, i(c3)),
(USER, bi(c4)),
(HTTP_METHOD, bi(c3)),
(HTTP_CODE, bi(c4)),
(STRING, i(c3)),
(BRACKET, i(c5)),
(PUNCT, i(c5)),
(UNKNOWN, i(c5)),
(GETTIME, i(c4)),
(GETSIZE, i(c3)),
(FTP_CODE, bi(c4)),
(IDENT, i(c4)),
(CTYPE, i(c3)),
(SUBJECT, bi(c2)),
(FIELD, i(c5)),
(CHAIN, bi(c4)),
(PKG, bi(c3)),
(PKGSTATUS, bi(c4)),
(INCOMING, bi(c3)),
(OUTGOING, bi(c2)),
(UNIQUE, i(c5)),
(REPEAT, it(c5)),
(SWAPNUM, i(c4)),
(PROXY_HIT, bi(c3)),
(PROXY_MISS, bi(c4)),
(PROXY_DENIED, bi(c2)),
(PROXY_REFRESH, i(c1)),
(PROXY_SWAPFAIL, bi(c2)),
(PROXY_DIRECT, i(c3)),
(PROXY_PARENT, i(c4)),
(PROXY_CREATE, i(c3)),
(PROXY_SWAPIN, i(c1)),
(PROXY_SWAPOUT, i(c4)),
(PROXY_RELEASE, i(c2)),
(THREAD, i(c5)),
(LEVEL, bi(c4)),
(FACILITY, i(pl)),
(DURATION, i(c4)),
(LATENCY, i(c4)),
(HASH, i(c5)),
(JSON_KEY, i(c3)),
];
Theme {
name: p.name.to_string(),
description: format!("Cyberpunk palette — {}", p.display),
base: i(c1),
styles: entries
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
}
}
pub fn cyberpunk() -> Theme {
theme_from_palette(&PALETTES[0])
}
pub fn ccze_classic() -> Theme {
use tokens::*;
let n = |c: Named| Style::fg(Color::Named(c));
let b = |c: Named| Style::bold_fg(Color::Named(c));
theme(
"ccze-classic",
"The original ccze 16-color palette",
n(Named::Cyan),
&[
(DATE, b(Named::Cyan)),
(TIME, b(Named::Cyan)),
(HOST, b(Named::Blue)),
(IP, b(Named::Blue)),
(MAC, b(Named::Cyan)),
(PID, b(Named::Yellow)),
(PROCESS, b(Named::Green)),
(ERROR, b(Named::Red)),
(WARNING, b(Named::Yellow)),
(DEBUG, n(Named::Blue)),
(INFO, n(Named::Cyan)),
(GOOD, b(Named::Green)),
(BAD, b(Named::Red)),
(SYSTEM, b(Named::Yellow)),
(KEYWORD, b(Named::Magenta)),
(EMAIL, b(Named::Green)),
(URI, b(Named::Green)),
(DIR, b(Named::Cyan)),
(FILE, n(Named::White)),
(SIZE, b(Named::White)),
(VERSION, b(Named::White)),
(NUMBER, b(Named::White)),
(ADDRESS, b(Named::Cyan)),
(PERCENTAGE, b(Named::Green)),
(SIGNAL, b(Named::Red)),
(PROTOCOL, b(Named::Magenta)),
(SERVICE, b(Named::Magenta)),
(USER, b(Named::Yellow)),
(HTTP_METHOD, b(Named::Green)),
(HTTP_CODE, b(Named::Yellow)),
(NUMBER, b(Named::White)),
],
)
}
pub fn builtins() -> Vec<Theme> {
PALETTES
.iter()
.map(theme_from_palette)
.chain(std::iter::once(ccze_classic()))
.collect()
}
pub const DEFAULT_THEME: &str = "neon-sprawl";
fn normalize(name: &str) -> String {
let n = name.trim().to_ascii_lowercase();
if n == "cyberpunk" || n == "default" {
return DEFAULT_THEME.to_string();
}
n.chars()
.map(|c| if c == ' ' || c == '_' { '-' } else { c })
.collect()
}
pub fn builtin(name: &str) -> Option<Theme> {
let want = normalize(name);
builtins().into_iter().find(|t| t.name == want)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn neon_sprawl_is_default_first() {
assert_eq!(builtins()[0].name, DEFAULT_THEME);
}
#[test]
fn all_31_palettes_present() {
assert_eq!(PALETTES.len(), 31);
assert_eq!(builtins().len(), 32);
}
#[test]
fn cyberpunk_alias_resolves_to_neon_sprawl() {
assert_eq!(builtin("cyberpunk").unwrap().name, "neon-sprawl");
assert_eq!(builtin("Neon Sprawl").unwrap().name, "neon-sprawl");
}
#[test]
fn every_palette_builds_and_styles_error_bold() {
for p in PALETTES {
let t = theme_from_palette(p);
assert!(t.style(tokens::ERROR).bold, "{} error not bold", p.name);
assert!(t.style(tokens::DEFAULT).fg.is_some());
}
}
#[test]
fn unmapped_token_uses_base() {
let t = cyberpunk();
assert_eq!(t.style("nonexistent-token"), t.base);
}
#[test]
fn ccze_classic_is_a_builtin() {
assert!(builtin("ccze-classic").is_some());
assert_eq!(ccze_classic().name, "ccze-classic");
}
#[test]
fn builtin_unknown_returns_none() {
assert!(builtin("no-such-theme").is_none());
}
#[test]
fn normalize_accepts_spaces_underscores_and_default_alias() {
assert_eq!(builtin("neon_sprawl").unwrap().name, "neon-sprawl");
assert_eq!(builtin(" NEON SPRAWL ").unwrap().name, "neon-sprawl");
assert_eq!(builtin("default").unwrap().name, DEFAULT_THEME);
assert_eq!(builtin("Acid Rain").unwrap().name, "acid-rain");
}
#[test]
fn merged_with_overlays_styles_and_keeps_unset_base() {
use crate::color::{Color, Named};
let base = theme(
"base",
"base desc",
Style::fg(Color::Named(Named::Cyan)),
&[
(tokens::ERROR, Style::fg(Color::Named(Named::Red))),
(tokens::INFO, Style::fg(Color::Named(Named::Blue))),
],
);
let other = theme(
"base",
"",
Style::plain(),
&[
(tokens::ERROR, Style::bold_fg(Color::Named(Named::Magenta))),
(tokens::GOOD, Style::fg(Color::Named(Named::Green))),
],
);
let merged = base.merged_with(&other);
assert_eq!(merged.base, Style::fg(Color::Named(Named::Cyan)));
assert_eq!(merged.description, "base desc");
assert_eq!(
merged.style(tokens::ERROR),
Style::bold_fg(Color::Named(Named::Magenta))
);
assert_eq!(
merged.style(tokens::INFO),
Style::fg(Color::Named(Named::Blue))
);
assert_eq!(
merged.style(tokens::GOOD),
Style::fg(Color::Named(Named::Green))
);
}
#[test]
fn merged_with_nonplain_base_and_description_win() {
use crate::color::{Color, Named};
let base = theme("b", "old", Style::fg(Color::Named(Named::Cyan)), &[]);
let other = theme("b", "new", Style::fg(Color::Named(Named::Yellow)), &[]);
let merged = base.merged_with(&other);
assert_eq!(merged.base, Style::fg(Color::Named(Named::Yellow)));
assert_eq!(merged.description, "new");
}
#[test]
fn process_and_host_differ_via_lightened_shade() {
let t = cyberpunk();
assert_ne!(t.style(tokens::PROCESS), t.style(tokens::HOST));
}
#[test]
fn builtin_theme_names_are_unique() {
let names: Vec<String> = builtins().into_iter().map(|t| t.name).collect();
let mut dedup = names.clone();
dedup.sort();
dedup.dedup();
assert_eq!(
dedup.len(),
names.len(),
"builtin theme names must be unique"
);
}
#[test]
fn shift_lighter_clamps_at_palette_edges() {
assert_eq!(shift_lighter(255), 255);
assert!(shift_lighter(1) <= 15);
assert_ne!(shift_lighter(16), 16);
}
#[test]
fn every_builtin_name_resolves_back() {
for t in builtins() {
assert_eq!(
builtin(&t.name).map(|b| b.name).as_deref(),
Some(t.name.as_str())
);
}
}
}