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::ProcessKind;
use crate::process::tree::FlatEntry;
use super::format::{format_duration_compact, format_memory};
use super::styles::{
BORDER_STYLE, CHILD_STYLE, CLAUDE_COLOR, CODEX_COLOR, HEADER_STYLE, SELECTED_STYLE, TITLE_STYLE,
};
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) -> Style {
match (&entry.kind, entry.is_root) {
(Some(ProcessKind::Claude), true) => Style::new().fg(CLAUDE_COLOR),
(Some(ProcessKind::Codex), true) => Style::new().fg(CODEX_COLOR),
_ => CHILD_STYLE,
}
}
fn name_cell(entry: &FlatEntry) -> String {
let prefix = tree_prefix(entry);
let indicator = if entry.has_children {
if entry.expanded {
"▼ "
} else {
"▶ "
}
} else {
""
};
let display_name = if entry.is_root {
match &entry.kind {
Some(ProcessKind::Claude) => "claude",
Some(ProcessKind::Codex) => "codex",
_ => &entry.info.name,
}
} else {
&entry.info.name
};
format!("{}{}{}", prefix, indicator, display_name)
}
fn build_rows(flat_list: &[FlatEntry]) -> Vec<Row<'_>> {
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))
})
.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,
) {
let header = Row::new(header_labels(sort_column, sort_direction))
.style(HEADER_STYLE)
.bottom_margin(1);
let rows = build_rows(flat_list);
let block = Block::default()
.title(" agentop ")
.title_style(TITLE_STYLE)
.borders(Borders::ALL)
.border_style(BORDER_STYLE);
let table = Table::new(rows, WIDTHS)
.header(header)
.block(block)
.row_highlight_style(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,
);
}