use ratatui::{
layout::{Constraint, Rect},
style::Style,
widgets::{
Block, Borders, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState,
},
Frame,
};
use crate::app::{SortColumn, SortDirection};
use crate::process::tree::FlatEntry;
use crate::process::{display_name, ProcessKind};
use super::format::{format_duration_compact, format_memory};
use super::styles::Palette;
const WIDTHS: [Constraint; 7] = [
Constraint::Length(8), Constraint::Min(20), Constraint::Length(8), Constraint::Length(10), Constraint::Length(10), Constraint::Min(30), Constraint::Length(12), ];
fn tree_prefix(entry: &FlatEntry) -> String {
let mut prefix = String::with_capacity(entry.depth * 2 + 3);
for _ in 0..entry.depth.saturating_sub(1) {
prefix.push_str(" ");
}
if entry.depth > 0 {
if entry.is_last_sibling {
prefix.push_str("└─ ");
} else {
prefix.push_str("├─ ");
}
}
prefix
}
fn row_style(entry: &FlatEntry, palette: &Palette) -> Style {
match (&entry.kind, entry.is_root) {
(Some(ProcessKind::Claude), true) => palette.claude_style(),
(Some(ProcessKind::Codex), true) => palette.codex_style(),
_ => palette.child_style(),
}
}
fn name_cell(entry: &FlatEntry) -> String {
let prefix = tree_prefix(entry);
let indicator = if entry.has_children {
if entry.expanded {
"▼ "
} else {
"▶ "
}
} else {
""
};
format!("{}{}{}", prefix, indicator, display_name(&entry.info))
}
fn build_rows<'a>(flat_list: &'a [FlatEntry], palette: &Palette) -> Vec<Row<'a>> {
flat_list
.iter()
.map(|entry| {
let cmd = entry.info.cmd.join(" ");
Row::new([
entry.info.pid.to_string(),
name_cell(entry),
format!("{:.1}%", entry.info.cpu_usage),
format_memory(entry.info.memory_bytes),
entry.info.status.clone(),
cmd,
format_duration_compact(entry.info.run_time),
])
.style(row_style(entry, palette))
})
.collect()
}
fn header_labels(column: SortColumn, direction: SortDirection) -> Vec<String> {
let arrow = match direction {
SortDirection::Ascending => " ^",
SortDirection::Descending => " v",
};
let base = ["PID", "Name", "CPU%", "Memory", "Status", "Command", "Uptime"];
let sort_cols: [Option<SortColumn>; 7] = [
Some(SortColumn::Pid),
Some(SortColumn::Name),
Some(SortColumn::Cpu),
Some(SortColumn::Memory),
Some(SortColumn::Status),
None, Some(SortColumn::Uptime),
];
base.iter()
.zip(sort_cols.iter())
.map(|(label, col_opt)| {
if col_opt.is_some_and(|c| c == column) {
format!("{}{}", label, arrow)
} else {
label.to_string()
}
})
.collect()
}
pub fn render_tree_view(
f: &mut Frame,
area: Rect,
flat_list: &[FlatEntry],
table_state: &mut TableState,
sort_column: SortColumn,
sort_direction: SortDirection,
palette: &Palette,
) {
let header = Row::new(header_labels(sort_column, sort_direction))
.style(palette.header_style())
.bottom_margin(1);
let rows = build_rows(flat_list, palette);
let block = Block::default()
.title(" agentop ")
.title_style(palette.title_style())
.borders(Borders::ALL)
.border_style(palette.border_style());
let table = Table::new(rows, WIDTHS)
.header(header)
.block(block)
.row_highlight_style(palette.selected_style())
.highlight_symbol("> ");
f.render_stateful_widget(table, area, table_state);
let mut scroll_state =
ScrollbarState::new(flat_list.len()).position(table_state.selected().unwrap_or(0));
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight),
area,
&mut scroll_state,
);
}