overbrainer 0.10.0

Distill a large LLM into a smaller open-weights model: generate questions, collect answers, fine-tune, visualize.
Documentation
//! The Logs view: the captured log lines, newest at the bottom.

use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Margin, Rect};
use ratatui::text::{Line, Span};
use ratatui::widgets::{List, ListItem, Paragraph};
use tracing::Level;

use crate::logging::LogLine;
use crate::tui::app::App;
use crate::tui::format::clock;
use crate::tui::pod_logs::LogSource;

/// Draws the Logs view in `area`: no frame, a two-column margin, a title row,
/// a blank row, then the lines. Records the rows the lines have, which bound
/// how far back the view scrolls.
pub(in crate::tui) fn render(frame: &mut Frame, area: Rect, app: &mut App) {
    let area = area.inner(Margin::new(2, 0));
    let [title_row, _, lines] = Layout::vertical([
        Constraint::Length(1),
        Constraint::Length(1),
        Constraint::Fill(1),
    ])
    .areas(area);
    app.log_view.height = usize::from(lines.height);
    let theme = &app.theme;
    let (logs, view) = app.shown_logs();
    let offset = view.offset(logs);
    let window = logs.window(view.min, view.height, offset);
    let pod = view.source == LogSource::Pod;
    let heading = match (&app.pod_logs.run, pod) {
        (Some(run), true) => format!("Logs · pod of run {run}"),
        _ => format!("Logs · {} and above", level_name(view.min)),
    };
    let mut title = vec![Span::styled(heading, theme.title)];
    if pod && let Some(error) = &app.pod_logs.error {
        title.push(Span::styled(format!("  {error}"), theme.warn));
    }
    if view.anchor.is_some() {
        title.push(Span::styled(
            format!("  ({offset} newer lines below: G follows)"),
            theme.dim,
        ));
    }
    frame.render_widget(Paragraph::new(Line::from(title)), title_row);
    let mut items: Vec<ListItem> = window
        .lines
        .iter()
        .map(|line| {
            ListItem::new(if pod {
                render_pod_line(line, theme)
            } else {
                render_line(line, theme)
            })
        })
        .collect();
    if items.is_empty() && pod {
        items.push(ListItem::new(Span::styled(
            "nothing kept yet: the pod's log is kept while overbrainer follows the run",
            theme.dim,
        )));
    } else if items.is_empty() && matches!(view.min, Level::DEBUG | Level::TRACE) {
        items.push(ListItem::new(Span::styled(
            "nothing captured at this level: OVERBRAINER_LOG sets what is captured",
            theme.dim,
        )));
    }
    frame.render_widget(List::new(items), lines);
}

fn render_line<'a>(line: &'a LogLine, theme: &crate::tui::theme::Theme) -> Line<'a> {
    Line::from(vec![
        Span::styled(clock(line.time), theme.dim),
        Span::raw(" "),
        Span::styled(
            format!("{:<5}", level_name(line.level)),
            theme.level(line.level),
        ),
        Span::raw(" "),
        Span::styled(format!("{}: ", short_target(&line.target)), theme.dim),
        Span::raw(line.message.as_str()),
    ])
}

/// A line of the pod's log: its time, its source (`sys` or `ctr`) where
/// overbrainer's lines have their level, then the line.
fn render_pod_line<'a>(line: &'a LogLine, theme: &crate::tui::theme::Theme) -> Line<'a> {
    Line::from(vec![
        Span::styled(clock(line.time), theme.dim),
        Span::raw(" "),
        Span::styled(format!("{:<5}", line.target), theme.dim),
        Span::raw(" "),
        Span::raw(line.message.as_str()),
    ])
}

/// A log target without the crate's own `overbrainer::` prefix.
fn short_target(target: &str) -> &str {
    target.strip_prefix("overbrainer::").unwrap_or(target)
}

/// Maps a tracing [`Level`] to its upper-case name (`ERROR`, `WARN`, `INFO`,
/// `DEBUG`, `TRACE`).
pub(in crate::tui) fn level_name(level: Level) -> &'static str {
    match level {
        Level::ERROR => "ERROR",
        Level::WARN => "WARN",
        Level::INFO => "INFO",
        Level::DEBUG => "DEBUG",
        _ => "TRACE",
    }
}

/// `line` formatted for the Logs export file: `2026-09-28T01:02:03Z INFO
/// pipeline: message`, on one line: a newline and a carriage return are written
/// `\n` and `\r`, other control characters `\u{..}`.
pub(in crate::tui) fn export_line(line: &LogLine) -> String {
    let line = format!(
        "{} {} {}: {}",
        crate::runs::rfc3339(line.time),
        level_name(line.level),
        short_target(&line.target),
        line.message
    );
    let mut escaped = String::with_capacity(line.len());
    for c in line.chars() {
        match c {
            '\n' => escaped.push_str("\\n"),
            '\r' => escaped.push_str("\\r"),
            c if c.is_control() => escaped.extend(c.escape_unicode()),
            c => escaped.push(c),
        }
    }
    escaped
}

#[cfg(test)]
mod tests {
    use std::time::{Duration, UNIX_EPOCH};

    use super::*;

    #[test]
    fn export_line_formats_time_level_target_and_message() {
        let line = LogLine {
            seq: 1,
            level: Level::INFO,
            target: "overbrainer::pipeline".into(),
            time: UNIX_EPOCH + Duration::from_secs(1_790_000_000),
            message: "did a thing".into(),
        };
        assert_eq!(
            export_line(&line),
            "2026-09-21T14:13:20Z INFO pipeline: did a thing"
        );
    }

    #[test]
    fn export_line_escapes_control_characters_to_stay_on_one_line() {
        let line = LogLine {
            seq: 1,
            level: Level::WARN,
            target: "overbrainer::llm".into(),
            time: UNIX_EPOCH + Duration::from_secs(1_790_000_000),
            message: "first\r\nsecond\tthird\u{1b}[0m".into(),
        };
        assert_eq!(
            export_line(&line),
            "2026-09-21T14:13:20Z WARN llm: first\\r\\nsecond\\u{9}third\\u{1b}[0m"
        );
    }
}