use crate::gpu::{GpuUse, Gpus};
use crate::sys::{Proc, Sys};
use crate::ui;
use ratatui::DefaultTerminal;
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::time::{Duration, Instant};
use sysinfo::Signal;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SortBy {
Cpu,
Mem,
GpuMem,
Sm,
Pid,
User,
}
impl SortBy {
pub fn label(self) -> &'static str {
match self {
SortBy::Cpu => "CPU",
SortBy::Mem => "memory",
SortBy::GpuMem => "GPU memory",
SortBy::Sm => "SM",
SortBy::Pid => "PID",
SortBy::User => "user",
}
}
fn next(self, gpu: bool) -> Self {
match self {
SortBy::Cpu => SortBy::Mem,
SortBy::Mem if gpu => SortBy::GpuMem,
SortBy::Mem => SortBy::Pid,
SortBy::GpuMem => SortBy::Sm,
SortBy::Sm => SortBy::Pid,
SortBy::Pid => SortBy::User,
SortBy::User => SortBy::Cpu,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Charts {
Split,
Together,
Combined,
}
impl Charts {
pub fn next(self) -> Self {
match self {
Charts::Split => Charts::Together,
Charts::Together => Charts::Combined,
Charts::Combined => Charts::Split,
}
}
pub fn label(self) -> &'static str {
match self {
Charts::Split => "split",
Charts::Together => "per GPU",
Charts::Combined => "combined",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Only {
All,
AnyGpu,
Gpu(u32),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Normal,
Search,
Kill(u32, u64),
}
pub struct App {
pub sys: Sys,
pub gpus: Gpus,
pub interval: Duration,
pub sort: SortBy,
pub descending: bool,
pub only: Only,
pub show_charts: bool,
pub charts: Charts,
pub mode: Mode,
pub query: String,
pub rows: Vec<usize>,
selected: Option<(u32, usize)>,
pub offset: usize,
pub message: Option<(String, Instant)>,
quit: bool,
}
impl App {
pub fn new(sys: Sys, gpus: Gpus, interval: Duration) -> Self {
let mut app = App {
sys,
gpus,
interval,
sort: SortBy::Cpu,
descending: true,
only: Only::All,
show_charts: true,
charts: Charts::Split,
mode: Mode::Normal,
query: String::new(),
rows: Vec::new(),
selected: None,
offset: 0,
message: None,
quit: false,
};
app.update_rows();
app
}
pub fn row(&self, row: usize) -> (&Proc, Option<&GpuUse>) {
let p = &self.sys.procs[self.rows[row]];
(p, self.gpus.procs.get(&p.pid))
}
pub fn selected(&self) -> Option<usize> {
self.selected.map(|(_, i)| i)
}
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> anyhow::Result<()> {
let mut last = Instant::now();
while !self.quit {
terminal.draw(|f| ui::draw(f, self))?;
let wait = (last + self.interval).saturating_duration_since(Instant::now());
if event::poll(wait)? {
let before = self.listing();
loop {
if let Event::Key(k) = event::read()?
&& k.kind == KeyEventKind::Press
{
self.key(k);
}
if self.quit || !event::poll(Duration::ZERO)? {
break;
}
}
if self.listing() != before {
self.update_rows();
}
}
if last.elapsed() >= self.interval {
self.sample();
last = Instant::now();
}
}
Ok(())
}
fn listing(&self) -> (String, SortBy, bool, Only) {
(self.query.clone(), self.sort, self.descending, self.only)
}
fn sample(&mut self) {
self.sys.sample();
self.gpus.sample();
self.update_rows();
}
fn key(&mut self, k: KeyEvent) {
if k.code == KeyCode::Char('c') && k.modifiers.contains(KeyModifiers::CONTROL) {
self.quit = true;
return;
}
match self.mode {
Mode::Search => match k.code {
KeyCode::Enter => self.mode = Mode::Normal,
KeyCode::Esc => {
self.query.clear();
self.mode = Mode::Normal;
}
KeyCode::Backspace => {
self.query.pop();
}
KeyCode::Up => self.step(-1),
KeyCode::Down => self.step(1),
KeyCode::Char(c) => self.query.push(c),
_ => {}
},
Mode::Kill(pid, start) => {
let signal = match k.code {
KeyCode::Char('t') => Some(Signal::Term),
KeyCode::Char('9') => Some(Signal::Kill),
_ => None,
};
if let Some(s) = signal {
let text = match self.sys.signal(pid, start, s) {
Ok(()) => format!("sent {s:?} to {pid}"),
Err(e) => e,
};
self.message = Some((text, Instant::now()));
}
self.mode = Mode::Normal;
}
Mode::Normal => self.normal_key(k.code),
}
}
fn normal_key(&mut self, code: KeyCode) {
match code {
KeyCode::Char('q') => self.quit = true,
KeyCode::Esc if !self.query.is_empty() => self.query.clear(),
KeyCode::Esc => self.quit = true,
KeyCode::Char('/') => self.mode = Mode::Search,
KeyCode::Char('k') | KeyCode::F(9) => {
if let Some(i) = self.selected() {
let p = self.row(i).0;
self.mode = Mode::Kill(p.pid, p.start);
}
}
KeyCode::Down | KeyCode::Char('j') => self.step(1),
KeyCode::Up => self.step(-1),
KeyCode::PageDown => self.step(20),
KeyCode::PageUp => self.step(-20),
KeyCode::Home => self.step(isize::MIN / 2),
KeyCode::End => self.step(isize::MAX / 2),
KeyCode::Char('s') => self.sort = self.sort.next(self.gpus.per_process()),
KeyCode::Char('r') => self.descending = !self.descending,
KeyCode::Char('h') => self.show_charts = !self.show_charts,
KeyCode::Char('v') => self.charts = self.charts.next(),
KeyCode::Char('g') if self.gpus.per_process() => {
let n = self.gpus.gpus.len() as u32;
self.only = match self.only {
Only::All => Only::AnyGpu,
Only::AnyGpu if n > 1 => Only::Gpu(0),
Only::Gpu(i) if i + 1 < n => Only::Gpu(i + 1),
_ => Only::All,
};
}
KeyCode::Char('+') | KeyCode::Char('=') => {
self.interval = (self.interval / 2).max(Duration::from_millis(100));
}
KeyCode::Char('-') => {
self.interval = (self.interval * 2).min(Duration::from_secs(60));
}
_ => {}
}
}
fn select(&mut self, i: usize) {
self.selected = self.rows.get(i).map(|&p| (self.sys.procs[p].pid, i));
}
fn step(&mut self, by: isize) {
let last = self.rows.len().saturating_sub(1);
let i = self
.selected()
.map_or(0, |i| i.saturating_add_signed(by).min(last));
self.select(i);
}
fn update_rows(&mut self) {
let query = self.query.to_lowercase();
let (procs, gpus) = (&self.sys.procs, &self.gpus.procs);
let gpu = |p: &Proc| gpus.get(&p.pid);
let mut rows: Vec<usize> = (0..procs.len())
.filter(|&i| {
let p = &procs[i];
let on = match (self.only, gpu(p)) {
(Only::All, _) => true,
(Only::AnyGpu, g) => g.is_some(),
(Only::Gpu(i), g) => g.is_some_and(|g| g.gpus.contains(&i)),
};
on && p.search.contains(&query)
})
.collect();
let gmem = |p: &Proc| gpu(p).and_then(|g| g.mem);
let sm = |p: &Proc| gpu(p).and_then(|g| g.sm);
rows.sort_by(|&a, &b| {
let (a, b) = (&procs[a], &procs[b]);
let o = match self.sort {
SortBy::Cpu => a.cpu.total_cmp(&b.cpu),
SortBy::Mem => a.mem.cmp(&b.mem),
SortBy::GpuMem => gmem(a).cmp(&gmem(b)),
SortBy::Sm => sm(a).cmp(&sm(b)),
SortBy::Pid => a.pid.cmp(&b.pid),
SortBy::User => a.user.cmp(&b.user),
}
.then(a.pid.cmp(&b.pid));
if self.descending { o.reverse() } else { o }
});
let pid = self.selected.map(|(pid, _)| pid);
let i = rows
.iter()
.position(|&r| Some(procs[r].pid) == pid)
.unwrap_or(0);
self.rows = rows;
self.select(i);
}
}