mod action;
mod app;
mod config;
mod event;
mod process;
mod tui;
mod ui;
use std::time::Duration;
use ratatui::layout::{Constraint, Layout};
use ratatui::widgets::Block;
use tokio::sync::mpsc;
use crate::app::{ActiveView, App, KeyContext};
use crate::event::{Event, EventHandler};
use crate::process::{display_name, ProcessInfo, ProcessScanner, SystemStats};
#[tokio::main]
async fn main() -> color_eyre::Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "--version" || a == "-V") {
println!("agentop {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
if args.iter().any(|a| a == "--help" || a == "-h") {
println!("agentop {}", env!("CARGO_PKG_VERSION"));
println!("A TUI process inspector for Claude Code and OpenAI Codex CLI\n");
println!("Usage: agentop\n");
println!("Options:");
println!(" -h, --help Show this help message");
println!(" -V, --version Print version");
return Ok(());
}
tui::install_panic_hook();
let mut terminal = tui::init()?;
run(&mut terminal).await?;
tui::restore()?;
Ok(())
}
async fn run(terminal: &mut tui::Tui) -> color_eyre::Result<()> {
let mut app = App::new();
let mut event_handler = EventHandler::new(Duration::from_secs(2), Duration::from_millis(33));
let (scan_trigger_tx, scan_trigger_rx) = mpsc::channel::<()>(1);
let (scan_result_tx, mut scan_result_rx) =
mpsc::unbounded_channel::<(Vec<ProcessInfo>, SystemStats)>();
tokio::task::spawn_blocking(move || {
scanner_task(scan_trigger_rx, scan_result_tx);
});
scan_trigger_tx
.try_send(())
.expect("initial scan trigger failed");
loop {
match event_handler.next().await? {
Event::Key(key) => {
let ctx = KeyContext {
active_view: &app.active_view,
confirming_kill: app.confirm_kill_pid.is_some(),
config_open: app.config_popup.is_some(),
filter_active: app.filter_active,
};
if let Some(action) = App::map_key_to_action(key, &ctx) {
app.handle_action(action);
}
}
Event::Tick => {
let _ = scan_trigger_tx.try_send(());
}
Event::Render => {
let mut latest: Option<(Vec<ProcessInfo>, SystemStats)> = None;
while let Ok(data) = scan_result_rx.try_recv() {
latest = Some(data);
}
if let Some((procs, stats)) = latest {
app.update_processes(procs, stats);
}
terminal.draw(|f| draw(f, &mut app))?;
}
Event::Resize => {
terminal.draw(|f| draw(f, &mut app))?;
}
}
if app.should_quit {
break;
}
}
Ok(())
}
fn scanner_task(
mut trigger_rx: mpsc::Receiver<()>,
result_tx: mpsc::UnboundedSender<(Vec<ProcessInfo>, SystemStats)>,
) {
let mut scanner = ProcessScanner::new();
let handle = tokio::runtime::Handle::current();
loop {
let received = handle.block_on(trigger_rx.recv());
if received.is_none() {
break;
}
let data = scanner.refresh();
if result_tx.send(data).is_err() {
break;
}
}
}
fn draw(f: &mut ratatui::Frame, app: &mut App) {
let [status_area, main_area, footer_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.areas(f.area());
let palette = &app.palette;
f.render_widget(Block::default().style(palette.base_style()), f.area());
ui::render_status_bar(f, status_area, &app.system_stats, &app.agent_summary);
match app.active_view {
ActiveView::Tree => {
ui::render_tree_view(
f,
main_area,
&app.flat_list,
&mut app.table_state,
app.sort_column,
app.sort_direction,
palette,
);
}
ActiveView::Detail => {
if let Some(ref info) = app.selected_detail {
let cpu_hist: Vec<f32> = app
.cpu_history
.get(&info.pid)
.map(|d| d.iter().copied().collect())
.unwrap_or_default();
let mem_hist: Vec<u64> = app
.mem_history
.get(&info.pid)
.map(|d| d.iter().copied().collect())
.unwrap_or_default();
ui::render_detail_view(
f,
main_area,
info,
&cpu_hist,
&mem_hist,
app.selected_detail_subtree,
app.graph_style,
palette,
);
}
}
}
ui::render_footer(
f,
footer_area,
&app.active_view,
palette,
app.filter_active,
&app.filter_text,
);
if let Some(ref config_state) = app.config_popup {
ui::render_config_popup(f, config_state, app.graph_style, app.theme, palette);
} else if let Some(pid) = app.confirm_kill_pid {
let name = app
.flat_list
.iter()
.find(|e| e.info.pid == pid)
.map(|e| display_name(&e.info))
.unwrap_or("unknown");
ui::render_kill_confirm(f, pid, name, palette);
} else if let Some(ref msg) = app.kill_result {
ui::render_kill_result(f, msg, palette);
}
}