elio 1.5.1

Snappy, batteries-included terminal file manager with rich previews, inline images, bulk actions, and trash support.
Documentation
use super::super::{LINE_LIMIT, StructuredPreview, styled};
use super::types::{ParsedLogDocument, ParsedLogEntry};
use crate::{file_info::StructuredFormat, preview::appearance as theme};
use ratatui::{
    style::Modifier,
    text::{Line, Span},
};
use std::collections::BTreeMap;

pub(super) fn render_parsed_log(document: ParsedLogDocument) -> StructuredPreview {
    let palette = theme::code_preview_palette();
    let mut counts = BTreeMap::new();
    for entry in &document.entries {
        if let Some(level) = &entry.level {
            *counts.entry(level.clone()).or_insert(0usize) += 1;
        }
    }

    let mut lines = Vec::new();
    lines.push(Line::from(vec![
        styled("format", palette.parameter, Modifier::BOLD),
        styled(": ", palette.operator, Modifier::empty()),
        Span::raw(document.source.label().to_string()),
        Span::raw("  ".to_string()),
        styled("entries", palette.parameter, Modifier::BOLD),
        styled(": ", palette.operator, Modifier::empty()),
        Span::raw(document.entries.len().to_string()),
    ]));

    if let Some((first, last)) = time_range(&document.entries) {
        let mut spans = vec![
            styled("range", palette.parameter, Modifier::BOLD),
            styled(": ", palette.operator, Modifier::empty()),
            styled(&first, palette.comment, Modifier::empty()),
        ];
        if first != last {
            spans.push(Span::raw("  ->  ".to_string()));
            spans.push(styled(&last, palette.comment, Modifier::empty()));
        }
        lines.push(Line::from(spans));
    }

    if !counts.is_empty() {
        let mut spans = vec![
            styled("levels", palette.parameter, Modifier::BOLD),
            styled(": ", palette.operator, Modifier::empty()),
        ];
        for (index, (level, count)) in counts.iter().enumerate() {
            if index > 0 {
                spans.push(Span::raw("  ".to_string()));
            }
            spans.push(styled(
                level,
                log_level_color(level, palette),
                Modifier::BOLD,
            ));
            spans.push(Span::raw(format!(" {count}")));
        }
        lines.push(Line::from(spans));
    }

    if !lines.is_empty() {
        lines.push(Line::from(""));
    }

    let mut truncated = false;
    for entry in document.entries {
        if lines.len() >= LINE_LIMIT {
            truncated = true;
            break;
        }
        lines.push(render_entry_summary(&entry, palette));

        for (key, value) in entry.fields {
            if lines.len() >= LINE_LIMIT {
                truncated = true;
                break;
            }
            lines.push(Line::from(vec![
                Span::raw("  ".to_string()),
                styled(&key, palette.parameter, Modifier::BOLD),
                styled(": ", palette.operator, Modifier::empty()),
                Span::raw(truncate_display(&value, 96)),
            ]));
        }
        if truncated {
            break;
        }

        for continuation in entry.continuations {
            if lines.len() >= LINE_LIMIT {
                truncated = true;
                break;
            }
            lines.push(Line::from(vec![
                Span::raw("  ".to_string()),
                styled("", palette.comment, Modifier::empty()),
                Span::raw(" ".to_string()),
                styled(
                    &truncate_display(&continuation, 116),
                    palette.comment,
                    Modifier::empty(),
                ),
            ]));
        }
    }

    StructuredPreview {
        lines,
        detail: StructuredFormat::Log.detail_label(),
        truncation_note: truncated.then(|| format!("showing first {LINE_LIMIT} lines")),
    }
}

fn render_entry_summary(
    entry: &ParsedLogEntry,
    palette: theme::CodePreviewPalette,
) -> Line<'static> {
    let mut spans = Vec::new();
    if let Some(timestamp) = &entry.timestamp {
        spans.push(styled(timestamp, palette.comment, Modifier::empty()));
        spans.push(Span::raw("  ".to_string()));
    }
    if let Some(level) = &entry.level {
        spans.push(styled(
            level,
            log_level_color(level, palette),
            Modifier::BOLD,
        ));
        spans.push(Span::raw("  ".to_string()));
    }
    spans.push(Span::raw(truncate_display(&entry.message, 116)));
    Line::from(spans)
}

fn time_range(entries: &[ParsedLogEntry]) -> Option<(String, String)> {
    let first = entries.iter().find_map(|entry| entry.timestamp.clone())?;
    let last = entries
        .iter()
        .rev()
        .find_map(|entry| entry.timestamp.clone())
        .unwrap_or_else(|| first.clone());
    Some((first, last))
}

pub(super) fn truncate_display(value: &str, max_chars: usize) -> String {
    let char_count = value.chars().count();
    if char_count <= max_chars {
        return value.to_string();
    }

    let kept = value
        .chars()
        .take(max_chars.saturating_sub(1))
        .collect::<String>();
    format!("{kept}")
}

fn log_level_color(level: &str, palette: theme::CodePreviewPalette) -> ratatui::style::Color {
    match level {
        "TRACE" | "DEBUG" => palette.comment,
        "INFO" => palette.function,
        "WARN" => palette.constant,
        "ERROR" | "FATAL" => palette.invalid,
        _ => palette.keyword,
    }
}