diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..11571a0 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,71 @@ +use ratatui::{ + Frame, + layout::Constraint, + style::{Color, Style}, + widgets::{Block, Borders, Row, Table, TableState}, +}; +use sysinfo::System; + +pub struct SystemStats { + pub sys: System, + pub cpu_count: f32, +} + +pub fn init_system() -> SystemStats { + let mut sys = System::new_all(); + sys.refresh_cpu_usage(); + let cpu_count = sys.cpus().len() as f32; + SystemStats { sys, cpu_count } +} + +pub fn draw_ui(f: &mut Frame, sys: &mut System, cpu_count: f32, state: &mut TableState) { + let mut processes: Vec<_> = sys.processes().values().collect(); + processes.sort_by(|a, b| { + b.cpu_usage() + .partial_cmp(&a.cpu_usage()) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + let rows: Vec = processes + .iter() + .map(|p| { + let cpu_usage = p.cpu_usage(); + Row::new(vec![ + p.pid().to_string(), + p.name().to_string_lossy().to_string(), + format!("{:.1}%", cpu_usage / cpu_count), + format!("{:.1}%", cpu_usage), + ]) + }) + .collect(); + + let widths = [ + Constraint::Length(10), + Constraint::Fill(1), + Constraint::Length(10), + Constraint::Length(15), + ]; + + let table = Table::new(rows, widths) + .header( + Row::new(vec!["PID", "Name", "CPU %", "CPU Core %"]).style(Style::new().blue().bold()), + ) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Process Manager ") + .title_bottom(" Use ↑/↓ to Scroll, 'q' to Quit "), + ) + .row_highlight_style(Style::new().bg(Color::Cyan).fg(Color::Black).bold()) + .highlight_symbol(">> "); + + f.render_stateful_widget(table, f.area(), state); +} + +pub fn refresh_system_data(sys: &mut System) { + sys.refresh_processes_specifics( + sysinfo::ProcessesToUpdate::All, + true, + sysinfo::ProcessRefreshKind::nothing().with_cpu(), + ); +} diff --git a/src/main.rs b/src/main.rs index c73e7d4..b071078 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,41 +1,66 @@ -use std::thread; -use std::time::Duration; -use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System}; +mod app; -fn main() { - let mut sys = System::new_all(); - sys.refresh_cpu_usage(); - let cpu_count = sys.cpus().len() as f32; +use crossterm::event::{self, Event, KeyCode}; +use ratatui::widgets::TableState; +use ratatui::{Terminal, backend::CrosstermBackend}; +use std::io::{self}; +use std::time::{Duration, Instant}; + +use app::{draw_ui, init_system, refresh_system_data}; + +fn main() -> Result<(), Box> { + crossterm::terminal::enable_raw_mode()?; + let mut stdout = io::stdout(); + crossterm::execute!(stdout, crossterm::terminal::EnterAlternateScreen)?; + + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + terminal.clear()?; + let mut stats = init_system(); + let mut table_state = TableState::default(); + table_state.select(Some(0)); + let mut last_tick = Instant::now(); + let tick_rate = Duration::from_secs(1); loop { - sys.refresh_processes_specifics( - sysinfo::ProcessesToUpdate::All, - true, - sysinfo::ProcessRefreshKind::nothing().with_cpu(), - ); - print!("{esc}c", esc = 27 as char); - println!( - "{:<10} {:<30} {:<10} {:<10}", - "PID", "Name", "CPU %", "CPU Core %" - ); - println!("{}", "-".repeat(50)); - let mut procs: Vec<_> = sys.processes().values().collect(); - procs.sort_by(|a, b| b.cpu_usage().partial_cmp(&a.cpu_usage()).unwrap()); - for p in procs.iter() { - let normalized_cpu = p.cpu_usage() / cpu_count; - let mut name = p.name().to_string_lossy().into_owned(); - if name.len() > 30 { - name.truncate(27); - name.push_str("..."); - } - println!( - "{:<10} {:<30} {:<.2}% {:<.2}%", - p.pid(), - p.name().to_string_lossy(), - normalized_cpu, - p.cpu_usage(), - ); + terminal.draw(|f| { + draw_ui(f, &mut stats.sys, stats.cpu_count, &mut table_state); + })?; + + if last_tick.elapsed() >= tick_rate { + refresh_system_data(&mut stats.sys); + last_tick = Instant::now(); + } + + if event::poll(Duration::from_millis(50))? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => break, + KeyCode::Down => { + let i = match table_state.selected() { + Some(i) => (i + 1).min(stats.sys.processes().len() - 1), + None => 0, + }; + table_state.select(Some(i)); + } + KeyCode::Up => { + let i = match table_state.selected() { + Some(i) => i.saturating_sub(1), + None => 0, + }; + table_state.select(Some(i)); + } + _ => {} + } + } } - thread::sleep(Duration::from_millis(1000)); } + + crossterm::execute!( + io::stdout(), + crossterm::terminal::LeaveAlternateScreen, + crossterm::cursor::Show + )?; + crossterm::terminal::disable_raw_mode()?; + Ok(()) }