tailspin 7.0.0

A log file highlighter
Documentation
use crate::theme::Theme;
use serde::Serialize;
use std::fmt::Write;
use tailspin::config::*;
use tailspin::style::Style;

/// The default theme as a `theme.toml` the user can copy and edit. Every table
/// and style is rendered from the same `Default` impls the highlighter uses,
/// so the output cannot drift from the code.
///
/// The exhaustive destructure below fails to compile when `Theme` gains a
/// field, and a field that is never emitted is an unused-variable warning
/// (an error under CI's `-D warnings`).
pub fn default_theme_toml() -> String {
    let Theme {
        keywords: _,
        regexes: _,
        numbers,
        uuids,
        quotes,
        ipv4,
        ipv6,
        dates,
        durations,
        paths,
        urls,
        emails,
        pointers,
        processes,
        key_value_pairs,
        json,
        jvm_stack_traces,
    } = Theme::default();

    let mut out = String::from(
        "\
# Generated by `tspin --generate-default-theme` — DO NOT EDIT.
#
# Every value below is a default: copying this file to
# ~/.config/tailspin/theme.toml and leaving it unchanged does nothing.
# Edit the styles you want to override and delete the rest.
#
# Keyword ([[keywords]]) and regex ([[regexes]]) highlights are additive
# lists and default to empty; the built-in keywords are compiled into tspin.
# See the README for their syntax.
",
    );

    push_config(&mut out, "numbers", &numbers);
    push_config(&mut out, "uuids", &uuids);
    push_quotes(&mut out, quotes);
    push_config(&mut out, "ipv4", &ipv4);
    push_config(&mut out, "ipv6", &ipv6);
    push_config(&mut out, "dates", &dates);
    push_config(&mut out, "durations", &durations);
    push_config(&mut out, "paths", &paths);
    push_config(&mut out, "urls", &urls);
    push_config(&mut out, "emails", &emails);
    push_config(&mut out, "pointers", &pointers);
    push_config(&mut out, "processes", &processes);
    push_config(&mut out, "key_value_pairs", &key_value_pairs);
    push_config(&mut out, "json", &json);
    push_config(&mut out, "jvm_stack_traces", &jvm_stack_traces);

    out
}

/// Writes a config struct as a `[name]` table, one `field = { ... }` per style.
fn push_config<T: Serialize>(out: &mut String, name: &str, config: &T) {
    let table = toml::Value::try_from(config).expect("config structs serialize to tables");
    let table = table.as_table().expect("config structs serialize to tables");

    let styles: Vec<(&str, Style)> = table
        .iter()
        .map(|(field, style)| {
            let style = style.clone().try_into().expect("config fields are styles");
            (field.as_str(), style)
        })
        .collect();

    push_table(out, name, &styles);
}

fn push_table(out: &mut String, name: &str, styles: &[(&str, Style)]) {
    writeln!(out, "\n[{name}]").unwrap();
    for (field, style) in styles {
        writeln!(out, "{field} = {}", inline_style(*style)).unwrap();
    }
}

/// `[quotes]` is the one table with a non-style field.
fn push_quotes(out: &mut String, config: QuoteConfig) {
    writeln!(out, "\n[quotes]\nquote_token = '{}'", config.quote_token as char).unwrap();
    writeln!(out, "style = {}", inline_style(config.style)).unwrap();
}

/// A style as a TOML inline table, e.g. `{ fg = "magenta", italic = true }`.
fn inline_style(style: Style) -> String {
    let table = toml::Value::try_from(style).expect("styles serialize to tables");
    let table = table.as_table().expect("styles serialize to tables");

    let fields: Vec<String> = table.iter().map(|(key, value)| format!("{key} = {value}")).collect();

    format!("{{ {} }}", fields.join(", "))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn generated_theme_parses_and_resolves_to_the_defaults() {
        let generated = default_theme_toml();
        let theme: Theme = toml::from_str(&generated).expect("generated theme must parse cleanly");

        assert_eq!(theme.uuids.letter, UuidConfig::default().letter);
        assert_eq!(theme.durations.unit, DurationConfig::default().unit);
        assert_eq!(theme.numbers.style, NumberConfig::default().style);
        assert_eq!(theme.quotes.quote_token, b'"');
        assert_eq!(theme.ipv4.separator, IpV4Config::default().separator);
        assert_eq!(theme.ipv6.letter, IpV6Config::default().letter);
    }
}