use std::collections::{HashMap, HashSet, VecDeque};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::widgets::TableState;
use crate::action::Action;
use crate::process::{
build_forest, collect_expansion, flatten_visible, preserve_expansion, toggle_expand, FlatEntry,
ProcessInfo, ProcessNode, SystemStats,
};
const HISTORY_LEN: usize = 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortColumn {
#[default]
Pid,
Name,
Cpu,
Memory,
Status,
Uptime,
}
impl SortColumn {
const ALL: [SortColumn; 6] = [
Self::Pid, Self::Name, Self::Cpu, Self::Memory, Self::Status, Self::Uptime,
];
pub fn next(self) -> Self {
let idx = Self::ALL
.iter()
.position(|&c| c == self)
.expect("SortColumn variant missing from ALL array");
Self::ALL[(idx + 1) % Self::ALL.len()]
}
pub fn prev(self) -> Self {
let idx = Self::ALL
.iter()
.position(|&c| c == self)
.expect("SortColumn variant missing from ALL array");
Self::ALL[(idx + Self::ALL.len() - 1) % Self::ALL.len()]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortDirection {
Ascending,
#[default]
Descending,
}
impl SortDirection {
pub fn toggle(self) -> Self {
match self {
Self::Ascending => Self::Descending,
Self::Descending => Self::Ascending,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ActiveView {
#[default]
Tree,
Detail,
}
#[derive(Debug, Default)]
pub struct App {
pub should_quit: bool,
pub active_view: ActiveView,
pub forest: Vec<ProcessNode>,
pub flat_list: Vec<FlatEntry>,
pub table_state: TableState,
pub selected_detail: Option<ProcessInfo>,
pub cpu_history: HashMap<u32, VecDeque<f32>>,
pub mem_history: HashMap<u32, VecDeque<u64>>,
pub sort_column: SortColumn,
pub sort_direction: SortDirection,
pub confirm_kill_pid: Option<u32>,
pub kill_result: Option<String>,
pub system_stats: SystemStats,
}
impl App {
pub fn new() -> Self {
let mut table_state = TableState::default();
table_state.select(Some(0));
Self {
table_state,
..Default::default()
}
}
pub fn handle_action(&mut self, action: Action) {
if !matches!(action, Action::KillRequest | Action::ConfirmKill | Action::CancelKill) {
self.kill_result = None;
}
match action {
Action::Quit => self.should_quit = true,
Action::MoveUp => self.move_selection(-1),
Action::MoveDown => self.move_selection(1),
Action::ToggleExpand => {
if let Some(idx) = self.table_state.selected() {
if let Some(entry) = self.flat_list.get(idx) {
let pid = entry.info.pid;
toggle_expand(&mut self.forest, pid);
self.rebuild_flat_list();
}
}
}
Action::SelectProcess => {
if let Some(idx) = self.table_state.selected() {
if let Some(entry) = self.flat_list.get(idx) {
self.selected_detail = Some(entry.info.clone());
self.active_view = ActiveView::Detail;
}
}
}
Action::BackToTree => {
self.active_view = ActiveView::Tree;
}
Action::SortNext => {
self.sort_column = self.sort_column.next();
self.rebuild_flat_list();
}
Action::SortPrev => {
self.sort_column = self.sort_column.prev();
self.rebuild_flat_list();
}
Action::SortToggleDirection => {
self.sort_direction = self.sort_direction.toggle();
self.rebuild_flat_list();
}
Action::KillRequest => {
let pid = self.selected_pid();
if pid.is_some() {
self.confirm_kill_pid = pid;
self.kill_result = None;
}
}
Action::ConfirmKill => {
if let Some(pid) = self.confirm_kill_pid.take() {
self.kill_result = Some(kill_process(pid));
}
}
Action::CancelKill => {
self.confirm_kill_pid = None;
}
}
}
fn move_selection(&mut self, delta: i32) {
let len = self.flat_list.len();
if len == 0 {
return;
}
let current = self.table_state.selected().unwrap_or(0) as i32;
let next = (current + delta).clamp(0, (len as i32) - 1) as usize;
self.table_state.select(Some(next));
}
pub fn update_processes(&mut self, processes: Vec<ProcessInfo>, stats: SystemStats) {
self.system_stats = stats;
let old_expansion = collect_expansion(&self.forest);
self.update_history(&processes);
let live_pids: HashSet<u32> = processes.iter().map(|p| p.pid).collect();
self.cpu_history.retain(|pid, _| live_pids.contains(pid));
self.mem_history.retain(|pid, _| live_pids.contains(pid));
self.forest = build_forest(&processes);
preserve_expansion(&mut self.forest, &old_expansion);
if let Some(ref mut detail) = self.selected_detail {
if let Some(updated) = processes.iter().find(|p| p.pid == detail.pid) {
*detail = updated.clone();
}
}
self.rebuild_flat_list();
}
fn sort_flat_list(&mut self) {
sort_forest(&mut self.forest, self.sort_column, self.sort_direction);
self.flat_list = flatten_visible(&self.forest);
}
fn rebuild_flat_list(&mut self) {
self.sort_flat_list();
self.clamp_selection();
}
fn selected_pid(&self) -> Option<u32> {
match self.active_view {
ActiveView::Tree => {
let idx = self.table_state.selected()?;
Some(self.flat_list.get(idx)?.info.pid)
}
ActiveView::Detail => self.selected_detail.as_ref().map(|d| d.pid),
}
}
fn clamp_selection(&mut self) {
let len = self.flat_list.len();
if len == 0 {
self.table_state.select(None);
return;
}
let clamped = self.table_state.selected().unwrap_or(0).min(len - 1);
self.table_state.select(Some(clamped));
}
fn update_history(&mut self, processes: &[ProcessInfo]) {
for proc in processes {
let cpu_buf = self.cpu_history.entry(proc.pid).or_default();
if cpu_buf.len() == HISTORY_LEN {
cpu_buf.pop_front();
}
cpu_buf.push_back(proc.cpu_usage);
let mem_buf = self.mem_history.entry(proc.pid).or_default();
if mem_buf.len() == HISTORY_LEN {
mem_buf.pop_front();
}
mem_buf.push_back(proc.memory_bytes);
}
}
pub fn map_key_to_action(
key: KeyEvent,
active_view: &ActiveView,
confirming_kill: bool,
) -> Option<Action> {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
return Some(Action::Quit);
}
if confirming_kill {
return match key.code {
KeyCode::Char('y') => Some(Action::ConfirmKill),
KeyCode::Char('n') | KeyCode::Esc => Some(Action::CancelKill),
_ => None,
};
}
match active_view {
ActiveView::Tree => match key.code {
KeyCode::Char('q') => Some(Action::Quit),
KeyCode::Up | KeyCode::Char('k') => Some(Action::MoveUp),
KeyCode::Down | KeyCode::Char('j') => Some(Action::MoveDown),
KeyCode::Char(' ') => Some(Action::ToggleExpand),
KeyCode::Enter => Some(Action::SelectProcess),
KeyCode::Tab => Some(Action::SortNext),
KeyCode::BackTab => Some(Action::SortPrev),
KeyCode::Char('s') => Some(Action::SortToggleDirection),
KeyCode::Char('x') => Some(Action::KillRequest),
_ => None,
},
ActiveView::Detail => match key.code {
KeyCode::Char('q') => Some(Action::Quit),
KeyCode::Esc => Some(Action::BackToTree),
KeyCode::Char('x') => Some(Action::KillRequest),
_ => None,
},
}
}
}
fn kill_process(pid: u32) -> String {
let pid_i32 = pid as i32;
let result = unsafe { libc::kill(pid_i32, libc::SIGTERM) };
if result == 0 {
return format!("Sent SIGTERM to PID {}", pid);
}
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(libc::ESRCH) => format!("PID {} not found", pid),
Some(libc::EPERM) => format!("Permission denied for PID {}", pid),
_ => format!("Failed to kill PID {}: {}", pid, err),
}
}
fn sort_forest(nodes: &mut [ProcessNode], column: SortColumn, direction: SortDirection) {
nodes.sort_by(|a, b| {
let cmp = compare_by_column(&a.info, &b.info, column);
match direction {
SortDirection::Ascending => cmp,
SortDirection::Descending => cmp.reverse(),
}
});
for node in nodes.iter_mut() {
sort_forest(&mut node.children, column, direction);
}
}
fn compare_by_column(a: &ProcessInfo, b: &ProcessInfo, column: SortColumn) -> std::cmp::Ordering {
match column {
SortColumn::Pid => a.pid.cmp(&b.pid),
SortColumn::Name => a.name.cmp(&b.name),
SortColumn::Cpu => a
.cpu_usage
.partial_cmp(&b.cpu_usage)
.unwrap_or(std::cmp::Ordering::Equal),
SortColumn::Memory => a.memory_bytes.cmp(&b.memory_bytes),
SortColumn::Status => a.status.cmp(&b.status),
SortColumn::Uptime => a.run_time.cmp(&b.run_time),
}
}