use std::{cmp::min, collections::HashSet};
use nix::sys::signal::Signal;
use ratatui::widgets::TableState;
use crate::{
model::ProcRow,
process::FilterSpec,
signal::signal_from_digit,
tree::{display_order_indices, display_rows},
};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct ProcessIdentity {
pid: i32,
start_time: u64,
}
#[derive(Debug)]
pub struct FilterInput {
pub text: String,
pub compiled: Option<FilterSpec>,
}
#[derive(Debug)]
pub struct App {
pub filter: Option<String>,
pub compiled_filter: Option<FilterSpec>,
pub rows: Vec<ProcRow>,
pub table_state: TableState,
pub status: String,
pub pending_confirmation: Option<SignalConfirmation>,
pub collapsed_pids: HashSet<i32>,
pub filter_input: Option<FilterInput>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignalConfirmation {
pub digit: u8,
pub signal: Signal,
pub pid: i32,
pub start_time: u64,
pub process_name: String,
}
impl App {
pub fn with_rows(filter: Option<String>, rows: Vec<ProcRow>) -> Self {
let mut table_state = TableState::default();
table_state.select(if rows.is_empty() { None } else { Some(0) });
Self {
filter,
compiled_filter: None,
rows,
table_state,
status: String::new(),
pending_confirmation: None,
collapsed_pids: HashSet::new(),
filter_input: None,
}
}
pub fn filter(&self) -> Option<&str> {
self.filter.as_deref()
}
pub fn active_filter(&self) -> Option<&FilterSpec> {
self.filter_input
.as_ref()
.and_then(|fi| fi.compiled.as_ref())
.or(self.compiled_filter.as_ref())
}
pub fn refresh(&mut self, rows: Vec<ProcRow>) {
self.apply_rows(rows);
self.status.clear();
}
pub fn refresh_preserving_status(&mut self, rows: Vec<ProcRow>) {
self.apply_rows(rows);
}
fn apply_rows(&mut self, rows: Vec<ProcRow>) {
let selected_before = self.table_state.selected().unwrap_or(0);
let selected_identity = self.selected_row().map(ProcessIdentity::from_row);
let collapsed_identities: HashSet<ProcessIdentity> = self
.rows
.iter()
.filter(|row| self.collapsed_pids.contains(&row.pid))
.map(ProcessIdentity::from_row)
.collect();
self.rows = rows;
self.collapsed_pids = self
.rows
.iter()
.filter(|row| collapsed_identities.contains(&ProcessIdentity::from_row(row)))
.map(|row| row.pid)
.collect();
let visible_count = self.visible_row_count();
if visible_count == 0 {
self.table_state.select(None);
} else if let Some(identity) = selected_identity
&& let Some(index) = display_order_indices(&self.rows, &self.collapsed_pids)
.iter()
.position(|row_index| ProcessIdentity::from_row(&self.rows[*row_index]) == identity)
{
self.table_state.select(Some(index));
} else {
self.table_state
.select(Some(min(selected_before, visible_count - 1)));
}
}
pub fn move_up(&mut self) {
if let Some(selected) = self.table_state.selected()
&& selected > 0
{
self.table_state.select(Some(selected - 1));
}
}
pub fn move_down(&mut self) {
let visible_count = self.visible_row_count();
if let Some(selected) = self.table_state.selected() {
if selected + 1 < visible_count {
self.table_state.select(Some(selected + 1));
}
} else if visible_count > 0 {
self.table_state.select(Some(0));
}
}
pub fn page_up(&mut self, step: usize) {
if step == 0 {
return;
}
if let Some(selected) = self.table_state.selected() {
self.table_state.select(Some(selected.saturating_sub(step)));
}
}
pub fn page_down(&mut self, step: usize) {
if step == 0 {
return;
}
if let Some(selected) = self.table_state.selected() {
let visible_count = self.visible_row_count();
if visible_count == 0 {
self.table_state.select(None);
return;
}
let last_index = visible_count - 1;
let next_index = selected.saturating_add(step);
self.table_state.select(Some(min(next_index, last_index)));
} else {
let visible_count = self.visible_row_count();
if visible_count == 0 {
return;
}
self.table_state
.select(Some(min(step - 1, visible_count - 1)));
}
}
pub fn collapse_selected(&mut self) -> bool {
let Some(display_row) = self.selected_display_row() else {
return false;
};
if !display_row.has_children || display_row.is_collapsed {
return false;
}
let pid = self.rows[display_row.row_index].pid;
self.collapsed_pids.insert(pid)
}
pub fn expand_selected(&mut self) -> bool {
let Some(display_row) = self.selected_display_row() else {
return false;
};
if !display_row.is_collapsed {
return false;
}
let pid = self.rows[display_row.row_index].pid;
self.collapsed_pids.remove(&pid)
}
pub fn send_digit(
&mut self,
digit: u8,
sender: &mut dyn FnMut(i32, Signal) -> Result<(), String>,
) {
let signal = match signal_from_digit(digit) {
Some(value) => value,
None => return,
};
let row = match self.selected_row() {
Some(value) => value,
None => return,
};
match sender(row.pid, signal) {
Ok(()) => {
self.status = format!("sent {:?} ({}) to pid {}", signal, digit, row.pid);
}
Err(err) => {
self.status = format!("failed to signal pid {}: {}", row.pid, err);
}
}
}
pub fn begin_signal_confirmation(&mut self, digit: u8) {
let signal = match signal_from_digit(digit) {
Some(value) => value,
None => return,
};
let row = match self.selected_row() {
Some(value) => value,
None => return,
};
self.pending_confirmation = Some(SignalConfirmation {
digit,
signal,
pid: row.pid,
start_time: row.start_time,
process_name: row.name.clone(),
});
}
pub fn cancel_signal_confirmation(&mut self) {
self.pending_confirmation = None;
}
pub fn confirm_signal(
&mut self,
sender: &mut dyn FnMut(i32, Signal) -> Result<(), String>,
) -> Option<i32> {
let pending = self.pending_confirmation.take()?;
match sender(pending.pid, pending.signal) {
Ok(()) => {
self.status = format!(
"sent {:?} ({}) to pid {}",
pending.signal, pending.digit, pending.pid
);
Some(pending.pid)
}
Err(err) => {
self.status = format!("failed to signal pid {}: {}", pending.pid, err);
None
}
}
}
pub fn pending_target_matches_current_rows(&self) -> bool {
let Some(pending) = self.pending_confirmation.as_ref() else {
return false;
};
self.rows
.iter()
.any(|row| row.pid == pending.pid && row.start_time == pending.start_time)
}
pub fn abort_pending_target_changed(&mut self) {
let Some(pending) = self.pending_confirmation.take() else {
return;
};
self.status = format!(
"aborted: process {} ({}) no longer matches confirmation target",
pending.process_name, pending.pid
);
}
pub fn confirmation_prompt(&self) -> Option<String> {
self.pending_confirmation.as_ref().map(|pending| {
format!(
"confirm sending {:?} ({}) to process {} ({}) (y/n)",
pending.signal, pending.digit, pending.process_name, pending.pid
)
})
}
fn selected_row(&self) -> Option<&ProcRow> {
let selected_display_index = self.table_state.selected()?;
let display_to_data = display_order_indices(&self.rows, &self.collapsed_pids);
let row_index = *display_to_data.get(selected_display_index)?;
self.rows.get(row_index)
}
fn selected_display_row(&self) -> Option<crate::tree::DisplayRow> {
let selected_display_index = self.table_state.selected()?;
display_rows(&self.rows, &self.collapsed_pids)
.get(selected_display_index)
.cloned()
}
pub fn select_first(&mut self) {
let visible_count = self.visible_row_count();
self.table_state
.select(if visible_count == 0 { None } else { Some(0) });
}
fn visible_row_count(&self) -> usize {
display_order_indices(&self.rows, &self.collapsed_pids).len()
}
}
impl ProcessIdentity {
fn from_row(row: &ProcRow) -> Self {
Self {
pid: row.pid,
start_time: row.start_time,
}
}
}