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);
}
}