97 lines
3.2 KiB
Rust
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)
|
|
}
|