Files
furumi_tui/src/ui/logs.rs
T
2026-06-10 16:23:20 +01:00

97 lines
3.2 KiB
Rust

use ratatui::Frame;
use ratatui::layout::{Alignment, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use super::theme;
use crate::app::state::{AppState, LOG_LEVELS};
use crate::config::logging;
pub fn draw(frame: &mut Frame, area: Rect, state: &AppState) {
let logs = &state.logs;
let level = LOG_LEVELS[logs.level_index];
let mode = if logs.follow { "follow" } else { "scroll" };
let block = Block::bordered()
.title(format!(" Logs — {level}+ · {mode} "))
.title_style(theme::header())
.border_style(theme::dim());
let inner = block.inner(area);
frame.render_widget(block, area);
let Some(buffer) = logging::buffer() else {
return centered(frame, inner, "in-app log buffer unavailable");
};
let visible = usize::from(inner.height.max(1));
let skip = if logs.follow { 0 } else { logs.scroll_from_end };
let (entries, matched) = buffer.window(level, skip, visible);
if entries.is_empty() {
return centered(frame, inner, "no log entries at this level yet");
}
for (row_index, entry) in entries.iter().enumerate() {
let row = Rect {
x: inner.x,
y: inner.y + row_index as u16,
width: inner.width,
height: 1,
};
let line = Line::from(vec![
Span::styled(format!("{} ", entry.time), theme::dim()),
level_span(entry.level),
Span::styled(format!(" {}: ", short_target(&entry.target)), theme::dim()),
Span::raw(entry.message.clone()),
]);
frame.render_widget(Paragraph::new(line), row);
}
// Footer hint with position info while scrolled back.
if !logs.follow && inner.height > 1 {
let footer = Rect {
y: inner.y + inner.height - 1,
height: 1,
..inner
};
frame.render_widget(
Paragraph::new(Line::styled(
format!("{skip} of {matched} · shift-g: follow · v: level "),
theme::tab_active(),
))
.alignment(Alignment::Right),
footer,
);
}
}
fn centered(frame: &mut Frame, area: Rect, text: &str) {
if area.height == 0 {
return;
}
let middle = Rect { y: area.y + area.height / 2, height: 1, ..area };
frame.render_widget(
Paragraph::new(Line::styled(text.to_string(), theme::dim()))
.alignment(Alignment::Center),
middle,
);
}
fn level_span(level: tracing::Level) -> Span<'static> {
match level {
tracing::Level::ERROR => {
Span::styled("ERROR", Style::new().fg(Color::Red).add_modifier(Modifier::BOLD))
}
tracing::Level::WARN => Span::styled("WARN ", Style::new().fg(Color::Yellow)),
tracing::Level::INFO => Span::styled("INFO ", theme::accent()),
tracing::Level::DEBUG => Span::styled("DEBUG", theme::dim()),
tracing::Level::TRACE => Span::styled("TRACE", theme::dim()),
}
}
/// `furumi_tui::app::update` → `app::update` — the crate prefix is noise.
fn short_target(target: &str) -> &str {
target
.split_once("::")
.map_or(target, |(_, rest)| rest)
}