use ratatui::{
Frame,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, HighlightSpacing, List, ListItem, ListState, Paragraph},
};
use crate::tui::health::activity::{activity_color, palace_activity};
use crate::tui::health::screen::{
collections_lines, header_lines, health_tab_lines, index_tab_lines, service_name, tab_bar,
};
use crate::tui::health::types::{Daemon, HealthScreen, HealthTab, PalaceActivity};
pub fn render(frame: &mut Frame, screen: &HealthScreen) {
let outer = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(6), Constraint::Length(3), ])
.split(frame.area());
render_header(frame, outer[0], screen);
let body = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(30), Constraint::Min(20), ])
.split(outer[1]);
render_collections(frame, body[0], screen);
render_tab_panel(frame, body[1], screen);
render_footer(frame, outer[2], screen);
}
fn render_header(frame: &mut Frame, area: ratatui::layout::Rect, screen: &HealthScreen) {
let online = match screen.focus {
Daemon::Search => screen.search.is_online(),
Daemon::Memory => screen.memory.is_online(),
};
let lines = header_lines(screen);
let title_color = if online { Color::Green } else { Color::Red };
let body: Vec<Line> = lines.into_iter().map(Line::from).collect();
frame.render_widget(
Paragraph::new(body).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(title_color))
.title(Span::styled(
format!(" {} ", service_name(screen.focus)),
Style::default()
.fg(title_color)
.add_modifier(Modifier::BOLD),
)),
),
area,
);
}
fn render_collections(frame: &mut Frame, area: ratatui::layout::Rect, screen: &HealthScreen) {
let rows = screen.focused_collections();
let label = match screen.focus {
Daemon::Search => "COLLECTIONS",
Daemon::Memory => "PALACES",
};
let title = format!(" {label} ({}) ", rows.len());
let lines = collections_lines(screen);
let items: Vec<ListItem<'static>> = lines
.into_iter()
.enumerate()
.map(|(i, line)| {
let item = ListItem::new(line);
if matches!(screen.focus, Daemon::Memory)
&& let Some(row) = rows.get(i)
{
let activity = palace_activity(row);
if !matches!(activity, PalaceActivity::Idle) {
return item.style(Style::default().fg(activity_color(activity)));
}
}
item
})
.collect();
let block = Block::default().borders(Borders::ALL).title(Span::styled(
title,
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
let list = List::new(items)
.block(block)
.highlight_style(
Style::default()
.fg(Color::White)
.bg(Color::Blue)
.add_modifier(Modifier::BOLD),
)
.highlight_spacing(HighlightSpacing::Always);
let mut state = ListState::default();
if !rows.is_empty() {
state.select(Some(screen.selected_collection.min(rows.len() - 1)));
}
frame.render_stateful_widget(list, area, &mut state);
}
fn render_tab_panel(frame: &mut Frame, area: ratatui::layout::Rect, screen: &HealthScreen) {
let block = Block::default().borders(Borders::ALL).title(Span::styled(
" DETAILS ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
let inner = block.inner(area);
frame.render_widget(block, area);
let split = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.split(inner);
let mut spans: Vec<Span> = Vec::new();
for (label, active) in tab_bar(screen.tab) {
let style = if active {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED)
} else {
Style::default().fg(Color::DarkGray)
};
spans.push(Span::styled(label, style));
spans.push(Span::raw(" "));
}
frame.render_widget(Paragraph::new(Line::from(spans)), split[0]);
match screen.tab {
HealthTab::Health => render_health_tab(frame, split[1], screen),
HealthTab::Logs => render_logs_tab(frame, split[1], screen),
HealthTab::Search => render_search_tab(frame, split[1], screen),
HealthTab::Index => render_index_tab(frame, split[1], screen),
}
}
fn render_health_tab(frame: &mut Frame, area: ratatui::layout::Rect, screen: &HealthScreen) {
let lines: Vec<Line> = health_tab_lines(screen)
.into_iter()
.map(Line::from)
.collect();
frame.render_widget(Paragraph::new(lines), area);
}
fn render_logs_tab(frame: &mut Frame, area: ratatui::layout::Rect, screen: &HealthScreen) {
let buf = screen.focused_logs();
if buf.lines.is_empty() {
let hint = "Log streaming not available — start daemon with RUST_LOG=debug";
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
hint,
Style::default().fg(Color::DarkGray),
))),
area,
);
return;
}
let height = area.height as usize;
let total = buf.lines.len();
let end = total.saturating_sub(buf.scroll_offset);
let start = end.saturating_sub(height);
let body: Vec<Line> = buf
.lines
.iter()
.skip(start)
.take(end - start)
.map(|l| Line::from(l.clone()))
.collect();
frame.render_widget(Paragraph::new(body), area);
}
fn render_search_tab(frame: &mut Frame, area: ratatui::layout::Rect, screen: &HealthScreen) {
let lines = if screen.search_query.is_empty() {
vec![
Line::from(Span::styled(
"Type a query in the search bar below.",
Style::default().fg(Color::DarkGray),
)),
Line::from(""),
Line::from(match screen.focus {
Daemon::Search => "Searches the focused index for code chunks.",
Daemon::Memory => "Recalls memories from the focused palace.",
}),
]
} else {
vec![
Line::from(Span::styled(
format!("Query: {}", screen.search_query),
Style::default().add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from("(press Enter in the search bar to run — execution not yet wired)"),
]
};
frame.render_widget(Paragraph::new(lines), area);
}
fn render_index_tab(frame: &mut Frame, area: ratatui::layout::Rect, screen: &HealthScreen) {
let lines: Vec<Line> = index_tab_lines(screen)
.into_iter()
.map(|l| {
if l.starts_with("--") {
Line::from(Span::styled(
l,
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
} else {
Line::from(l)
}
})
.collect();
frame.render_widget(Paragraph::new(lines), area);
}
fn render_footer(frame: &mut Frame, area: ratatui::layout::Rect, screen: &HealthScreen) {
let cursor = if screen.search_input_focused { "_" } else { "" };
let prompt = match screen.focus {
Daemon::Search => "SEARCH ▶",
Daemon::Memory => "RECALL ▶",
};
let input_line = format!("{prompt} {}{cursor}", screen.search_query);
let input_style = if screen.search_input_focused {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
frame.render_widget(
Paragraph::new(Line::from(input_line))
.style(input_style)
.block(Block::default().borders(Borders::ALL)),
area,
);
}
#[allow(dead_code)]
fn render_panel(
frame: &mut Frame,
area: ratatui::layout::Rect,
name: &str,
lines: &[String],
online: bool,
focused: bool,
) {
let title_color = if online { Color::Green } else { Color::Red };
let border_style = if focused {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::DarkGray)
};
let body: Vec<Line> = lines.iter().map(|l| Line::from(l.clone())).collect();
frame.render_widget(
Paragraph::new(body).block(
Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.title(Span::styled(
format!(" {name} "),
Style::default()
.fg(title_color)
.add_modifier(Modifier::BOLD),
)),
),
area,
);
}