121 lines
3.9 KiB
Rust
121 lines
3.9 KiB
Rust
//! Log panel widget and state for kiln-tui.
|
|
//!
|
|
//! [`LogWidget`] renders the scrollable log panel as a ratatui [`StatefulWidget`];
|
|
//! [`LogState`] holds whether the panel is open, its height, and scroll position.
|
|
|
|
use kiln_core::log::{LogLine, LogSpan};
|
|
use ratatui::buffer::Buffer;
|
|
use ratatui::layout::Rect;
|
|
use ratatui::style::{Color, Style};
|
|
use ratatui::symbols::merge::MergeStrategy;
|
|
use ratatui::text::{Line, Span};
|
|
use ratatui::widgets::{Block, Paragraph, StatefulWidget, Widget, Wrap};
|
|
use crate::utils::rgba8_to_color;
|
|
|
|
/// View state for the log panel.
|
|
pub struct LogState {
|
|
/// Whether the log panel is currently open.
|
|
pub open: bool,
|
|
/// Height of the panel in terminal rows (borders included) when open.
|
|
pub height: u16,
|
|
/// Scroll offset in lines from the newest message (0 = newest at top).
|
|
pub scroll: u16,
|
|
}
|
|
|
|
impl Default for LogState {
|
|
fn default() -> Self {
|
|
Self { open: false, height: 10, scroll: 0 }
|
|
}
|
|
}
|
|
|
|
impl LogState {
|
|
/// Creates a `LogState` with sensible defaults (closed, 10 rows tall).
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Toggles the panel open or closed, resetting scroll to the top on open.
|
|
pub fn toggle(&mut self) {
|
|
self.open = !self.open;
|
|
self.scroll = 0;
|
|
}
|
|
|
|
/// Scrolls by `delta` lines, clamped so the offset stays within `[0, log_len - 1]`.
|
|
pub fn scroll_by(&mut self, delta: i32, log_len: usize) {
|
|
let max = log_len.saturating_sub(1) as i32;
|
|
self.scroll = (self.scroll as i32 + delta).clamp(0, max) as u16;
|
|
}
|
|
|
|
/// Resizes the panel, clamping height so both the panel and the board remain usable.
|
|
pub fn resize(&mut self, target: u16, total: u16) {
|
|
self.height = target.clamp(3, total.saturating_sub(3).max(3));
|
|
}
|
|
}
|
|
|
|
/// A ratatui widget that renders the log panel.
|
|
///
|
|
/// Displays all log lines newest-first in a bordered block, wrapped to width.
|
|
/// Scrolled by [`LogState::scroll`].
|
|
pub struct LogWidget<'a> {
|
|
/// The log lines to display.
|
|
pub logs: &'a [LogLine],
|
|
}
|
|
|
|
impl<'a> LogWidget<'a> {
|
|
/// Creates a widget for the given log slice.
|
|
pub fn new(logs: &'a [LogLine]) -> Self {
|
|
Self { logs }
|
|
}
|
|
}
|
|
|
|
impl StatefulWidget for LogWidget<'_> {
|
|
type State = LogState;
|
|
|
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut LogState) {
|
|
let block = Block::bordered()
|
|
.merge_borders(MergeStrategy::Exact)
|
|
.title(Span::styled(" [l]og: ", Style::default().fg(Color::DarkGray)));
|
|
let lines: Vec<Line> = self.logs.iter().rev().map(logline_to_line).collect();
|
|
Paragraph::new(lines)
|
|
.block(block)
|
|
.scroll((state.scroll, 0))
|
|
.wrap(Wrap { trim: false })
|
|
.render(area, buf);
|
|
}
|
|
}
|
|
|
|
/// Builds the `[l]og: <latest message>` preview for the board's bottom border.
|
|
pub fn log_preview_line(logs: &[LogLine]) -> Line<'static> {
|
|
let mut spans = vec![Span::styled(
|
|
" [l]og: ",
|
|
Style::default().fg(Color::DarkGray),
|
|
)];
|
|
if let Some(latest) = logs.last() {
|
|
spans.extend(logline_to_line(latest).spans);
|
|
}
|
|
spans.push(Span::raw(" "));
|
|
Line::from(spans)
|
|
}
|
|
|
|
/// Converts a core [`LogLine`] into a styled ratatui [`Line`] for display.
|
|
///
|
|
/// Each [`LogSpan`] becomes a ratatui [`Span`]; a span's foreground/background
|
|
/// color is applied only when present, so `None` colors inherit the surrounding style.
|
|
pub fn logline_to_line(line: &LogLine) -> Line<'static> {
|
|
let spans = line
|
|
.spans
|
|
.iter()
|
|
.map(|s: &LogSpan| {
|
|
let mut style = Style::default();
|
|
if let Some(fg) = s.fg {
|
|
style = style.fg(rgba8_to_color(fg));
|
|
}
|
|
if let Some(bg) = s.bg {
|
|
style = style.bg(rgba8_to_color(bg));
|
|
}
|
|
Span::styled(s.text.clone(), style)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
Line::from(spans)
|
|
}
|