use std::io::{self, BufRead, BufReader, IsTerminal, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use clap::Parser;
use zcolorizer::config::Config;
use zcolorizer::Colorizer;
#[derive(Parser, Debug)]
#[command(
name = "zcolorizer",
version,
about = "Real-time log colorizer — fully customizable rules, swappable themes (cyberpunk by default)",
long_about = None,
// House-style cyberpunk help is rendered by `print_cyberpunk_help`, not clap.
disable_help_flag = true,
)]
struct Cli {
#[arg(short = 'h', long = "help")]
help: bool,
#[arg(short, long)]
theme: Option<String>,
#[arg(short = 'm', long = "module")]
modules: Vec<String>,
#[arg(short, long)]
config: Option<PathBuf>,
files: Vec<PathBuf>,
#[arg(short = 'C', long)]
force_color: bool,
#[arg(long)]
no_color: bool,
#[arg(long)]
list_themes: bool,
#[arg(long)]
list_modules: bool,
#[arg(long)]
themes_json: bool,
#[arg(long)]
list_rules: bool,
#[arg(long)]
dump_config: bool,
}
const BANNER: &str = r#"███████╗ ██████╗ ██████╗ ██╗ ██████╗ ██████╗ ██╗███████╗███████╗██████╗
╚══███╔╝██╔════╝██╔═══██╗██║ ██╔═══██╗██╔══██╗██║╚══███╔╝██╔════╝██╔══██╗
███╔╝ ██║ ██║ ██║██║ ██║ ██║██████╔╝██║ ███╔╝ █████╗ ██████╔╝
███╔╝ ██║ ██║ ██║██║ ██║ ██║██╔══██╗██║ ███╔╝ ██╔══╝ ██╔══██╗
███████╗╚██████╗╚██████╔╝███████╗╚██████╔╝██║ ██║██║███████╗███████╗██║ ██║
╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝"#;
fn print_cyberpunk_help() {
let bin = env!("CARGO_BIN_NAME");
let version = env!("CARGO_PKG_VERSION");
const C: &str = "\x1b[36m"; const M: &str = "\x1b[35m"; const Y: &str = "\x1b[33m"; const G: &str = "\x1b[32m"; const D: &str = "\x1b[2m"; const N: &str = "\x1b[0m";
let lines: Vec<&str> = BANNER.lines().collect();
let last = (lines.len().saturating_sub(1)).max(1) as f32;
for (i, line) in lines.iter().enumerate() {
let t = i as f32 / last;
let r = (0.0 + t * 255.0) as u8;
let g = (229.0 - t * 186.0) as u8;
let b = (255.0 - t * 41.0) as u8;
println!("\x1b[1;38;2;{r};{g};{b}m{line}{N}");
}
println!();
println!(" {M}Real-time log colorizer{N} — ccze/pygments port · 31 cyberpunk themes · 20 modules");
println!();
println!("{Y} USAGE:{N} {bin} [OPTIONS] [FILES]... {G}//{N} reads stdin when no FILES");
println!(" tail -f /var/log/syslog | {bin} -m syslog");
println!();
let row = |flags: &str, desc: &str| println!(" {flags:<24}{G}//{N} {desc}");
println!("{C} ── INPUT ─────────────────────────────────────────────{N}");
row("FILES...", "files to colorize (default: stdin, line-buffered)");
println!();
println!("{C} ── THEME ─────────────────────────────────────────────{N}");
row("-t, --theme NAME", "theme to use (default: neon-sprawl, alias cyberpunk)");
row(" --list-themes", "list all themes (active marked with *)");
row(" --themes-json", "emit every theme as JSON (for tooling)");
println!();
println!("{C} ── RULES & MODULES ───────────────────────────────────{N}");
row("-m, --module NAME", "enable a ccze format module (repeatable; `all`)");
row(" --list-modules", "list the 20 format modules");
row(" --list-rules", "list effective rules after config merge");
row("-c, --config PATH", "config file (default ~/.config/zcolorizer/config.toml)");
row(" --dump-config", "print the resolved config as TOML");
println!();
println!("{C} ── OUTPUT ────────────────────────────────────────────{N}");
row("-C, --force-color", "color even when stdout is not a TTY");
row(" --no-color", "never color (passthrough)");
println!();
println!("{C} ── INFO ──────────────────────────────────────────────{N}");
row("-h, --help", "print this help");
row("-V, --version", "print version");
println!();
println!("{D} zcolorizer v{version} · MenkeTechnologies · cyberpunk by default{N}");
}
fn main() -> ExitCode {
let cli = Cli::parse();
match run(cli) {
Ok(code) => code,
Err(e) => {
eprintln!("zcolorizer: {e}");
ExitCode::FAILURE
}
}
}
fn run(cli: Cli) -> zcolorizer::Result<ExitCode> {
if cli.help {
print_cyberpunk_help();
return Ok(ExitCode::SUCCESS);
}
let mut config = match &cli.config {
Some(p) => Config::load(p)?,
None => Config::load_default()?,
};
for m in &cli.modules {
if !config.modules.iter().any(|x| x.eq_ignore_ascii_case(m)) {
config.modules.push(m.clone());
}
}
if cli.list_modules {
for m in zcolorizer::modules::all() {
println!("{:<12} {}", m.name, m.description);
}
return Ok(ExitCode::SUCCESS);
}
if cli.list_themes {
let active = config
.resolve_theme(cli.theme.as_deref())
.map(|t| t.name)
.unwrap_or_default();
for name in config.available_theme_names() {
let marker = if name == active { "*" } else { " " };
println!("{marker} {name}");
}
return Ok(ExitCode::SUCCESS);
}
if cli.themes_json {
let themes: Vec<_> = config
.available_theme_names()
.into_iter()
.filter_map(|n| config.resolve_theme(Some(&n)).ok())
.collect();
let active = config
.resolve_theme(cli.theme.as_deref())
.map(|t| t.name)
.unwrap_or_default();
let doc = serde_json::json!({ "active": active, "themes": themes });
println!("{}", serde_json::to_string_pretty(&doc).unwrap_or_default());
return Ok(ExitCode::SUCCESS);
}
if cli.list_rules {
for def in config.resolve_rule_defs() {
let tok = def.token.as_deref().unwrap_or("(named groups)");
println!("{:<22} {:<14} {}", def.name, tok, def.pattern);
}
return Ok(ExitCode::SUCCESS);
}
if cli.dump_config {
let s = toml::to_string_pretty(&config).unwrap_or_default();
print!("{s}");
return Ok(ExitCode::SUCCESS);
}
let unknown = config.unknown_modules();
if !unknown.is_empty() {
eprintln!(
"zcolorizer: unknown module(s): {} (see --list-modules)",
unknown.join(", ")
);
}
let colorizer = Colorizer::from_config(&config, cli.theme.as_deref())?;
let stdout = io::stdout();
let want_color = cli.force_color || (!cli.no_color && stdout.is_terminal());
let mut out = io::BufWriter::new(stdout.lock());
let result = if cli.files.is_empty() {
process_reader(BufReader::new(io::stdin().lock()), &colorizer, want_color, &mut out)
} else {
let mut last = Ok(());
for path in &cli.files {
match std::fs::File::open(path) {
Ok(f) => {
if let Err(e) =
process_reader(BufReader::new(f), &colorizer, want_color, &mut out)
{
last = Err(e);
}
}
Err(e) => {
let _ = out.flush();
eprintln!("zcolorizer: {}: {e}", path.display());
last = Err(e);
}
}
}
last
};
let _ = out.flush();
match result {
Ok(()) => Ok(ExitCode::SUCCESS),
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(ExitCode::SUCCESS),
Err(e) => {
eprintln!("zcolorizer: {e}");
Ok(ExitCode::FAILURE)
}
}
}
fn process_reader<R: BufRead, W: Write>(
mut reader: R,
colorizer: &Colorizer,
want_color: bool,
out: &mut W,
) -> io::Result<()> {
if !want_color {
io::copy(&mut reader, out)?;
return Ok(());
}
let mut buf: Vec<u8> = Vec::new();
loop {
buf.clear();
let n = reader.read_until(b'\n', &mut buf)?;
if n == 0 {
break;
}
let had_nl = buf.last() == Some(&b'\n');
let end = if had_nl { buf.len() - 1 } else { buf.len() };
let body = String::from_utf8_lossy(&buf[..end]);
out.write_all(colorizer.colorize_line(&body).as_bytes())?;
if had_nl {
out.write_all(b"\n")?;
}
out.flush()?;
}
Ok(())
}