use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph},
};
use super::format::{format_count, format_uptime, help_text, status_badge, truncate};
use super::types::{
DaemonPanel, DashboardState, Focus, KEY_HINT, MemoryData, PanelStatus, SearchData, VERSION,
};
use super::format::panel_layout;
pub fn search_panel_lines(panel: &DaemonPanel<SearchData>) -> Vec<String> {
match &panel.status {
PanelStatus::Connecting => vec![format!("connecting to {}…", panel.base_url)],
PanelStatus::Offline { last_error } => vec![
format!("daemon unreachable at {}", panel.base_url),
format!("last error: {last_error}"),
"retrying every 5s…".to_string(),
],
PanelStatus::Online(data) => {
let mut lines = vec![
format!("Uptime: {}", format_uptime(data.uptime_secs)),
format!("Indexes: {}", data.indexes.len()),
format!("Total chunks: {}", format_count(data.total_chunks())),
String::new(),
];
if data.indexes.is_empty() {
lines.push("(no indexes registered)".to_string());
} else {
for idx in &data.indexes {
lines.push(format!(
"{:<16} {:>10} chunks",
truncate(&idx.id, 16),
format_count(idx.chunk_count),
));
}
}
lines
}
}
}
pub fn memory_panel_lines(panel: &DaemonPanel<MemoryData>) -> Vec<String> {
match &panel.status {
PanelStatus::Connecting => vec![format!("connecting to {}…", panel.base_url)],
PanelStatus::Offline { last_error } => vec![
format!("daemon unreachable at {}", panel.base_url),
format!("last error: {last_error}"),
"retrying every 5s…".to_string(),
],
PanelStatus::Online(data) => {
let mut lines = vec![
format!("Palaces: {}", data.palace_count),
format!("Drawers: {}", format_count(data.total_drawers)),
format!("Vectors: {}", format_count(data.total_vectors)),
format!("KG triples: {}", format_count(data.total_kg_triples)),
String::new(),
];
if data.palaces.is_empty() {
lines.push("(no palaces)".to_string());
} else {
for palace in &data.palaces {
let label = if palace.name.is_empty() {
truncate(&palace.id, 16)
} else {
truncate(&palace.name, 16)
};
lines.push(format!(
"{:<16} {:>10} vectors",
label,
format_count(palace.vector_count),
));
}
}
lines
}
}
}
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let w = width.min(area.width);
let h = height.min(area.height);
Rect {
x: area.x + (area.width.saturating_sub(w)) / 2,
y: area.y + (area.height.saturating_sub(h)) / 2,
width: w,
height: h,
}
}
fn panel_block(name: &str, badge: (char, &str, Color), focused: bool) -> Block<'static> {
let (glyph, label, color) = badge;
let title = Line::from(vec![
Span::styled(
format!(" {name} "),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::styled(format!("{glyph} {label} "), Style::default().fg(color)),
]);
let border_style = if focused {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::DarkGray)
};
Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.title(title)
}
fn render_help_overlay(frame: &mut Frame) {
let area = centered_rect(56, 11, frame.area());
frame.render_widget(Clear, area);
frame.render_widget(
Paragraph::new(help_text())
.style(Style::default().fg(Color::White))
.block(
Block::default()
.borders(Borders::ALL)
.title(" Help — press ? or Esc to close "),
),
area,
);
}
fn search_version_badge(panel: &DaemonPanel<SearchData>) -> (char, &'static str, Color) {
status_badge(&panel.status)
}
fn memory_version_badge(panel: &DaemonPanel<MemoryData>) -> (char, &'static str, Color) {
status_badge(&panel.status)
}
pub fn render(frame: &mut Frame, state: &DashboardState) {
let outer = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(6)])
.split(frame.area());
let header_lines = vec![
Line::from(Span::styled(
format!(" trusty-monitor v{VERSION} "),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
state
.last_action
.clone()
.unwrap_or_else(|| KEY_HINT.to_string()),
Style::default().fg(Color::Gray),
)),
];
frame.render_widget(
Paragraph::new(header_lines).block(Block::default().borders(Borders::ALL)),
outer[0],
);
let (direction, constraints) = panel_layout(frame.area().width);
let panels = Layout::default()
.direction(direction)
.constraints(constraints)
.split(outer[1]);
let search_block = panel_block(
"SEARCH",
search_version_badge(&state.search),
state.focus == Focus::Search,
);
frame.render_widget(
Paragraph::new(
search_panel_lines(&state.search)
.into_iter()
.map(Line::from)
.collect::<Vec<_>>(),
)
.block(search_block),
panels[0],
);
let memory_block = panel_block(
"MEMORY",
memory_version_badge(&state.memory),
state.focus == Focus::Memory,
);
frame.render_widget(
List::new(
memory_panel_lines(&state.memory)
.into_iter()
.map(ListItem::new)
.collect::<Vec<_>>(),
)
.block(memory_block),
panels[1],
);
if state.show_help {
render_help_overlay(frame);
}
}