#[cfg(feature = "sysinfo")]
use sysinfo::System;
use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::utils::format_size_compact;
use crate::widget::theme::LIGHT_GRAY;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ProcessSort {
Pid,
Name,
#[default]
Cpu,
Memory,
Status,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ProcessView {
#[default]
All,
User,
Tree,
}
#[derive(Clone, Debug)]
pub struct ProcessInfo {
pub pid: u32,
pub parent_pid: Option<u32>,
pub name: String,
pub cpu: f32,
pub memory: u64,
pub memory_percent: f32,
pub status: String,
pub cmd: String,
pub user: String,
}
#[derive(Clone, Debug)]
pub struct ProcColors {
pub header_bg: Color,
pub header_fg: Color,
pub selected_bg: Color,
pub high_cpu: Color,
pub medium_cpu: Color,
pub low_cpu: Color,
pub high_mem: Color,
pub name: Color,
pub pid: Color,
}
impl Default for ProcColors {
fn default() -> Self {
Self {
header_bg: Color::rgb(40, 40, 60),
header_fg: Color::WHITE,
selected_bg: Color::rgb(60, 80, 120),
high_cpu: Color::RED,
medium_cpu: Color::YELLOW,
low_cpu: Color::GREEN,
high_mem: Color::MAGENTA,
name: Color::WHITE,
pid: Color::CYAN,
}
}
}
pub struct ProcessMonitor {
system: System,
processes: Vec<ProcessInfo>,
sort: ProcessSort,
sort_asc: bool,
filter: String,
selected: usize,
scroll: usize,
view: ProcessView,
colors: ProcColors,
show_cmd: bool,
update_interval: u64,
last_update: std::time::Instant,
props: WidgetProps,
}
impl ProcessMonitor {
pub fn new() -> Self {
let mut sys = System::new_all();
sys.refresh_all();
Self {
system: sys,
processes: Vec::new(),
sort: ProcessSort::default(),
sort_asc: false,
filter: String::new(),
selected: 0,
scroll: 0,
view: ProcessView::default(),
colors: ProcColors::default(),
show_cmd: false,
update_interval: 1000,
last_update: std::time::Instant::now(),
props: WidgetProps::new(),
}
}
pub fn sort_by(mut self, sort: ProcessSort) -> Self {
self.sort = sort;
self
}
pub fn ascending(mut self, asc: bool) -> Self {
self.sort_asc = asc;
self
}
pub fn view(mut self, view: ProcessView) -> Self {
self.view = view;
self
}
pub fn colors(mut self, colors: ProcColors) -> Self {
self.colors = colors;
self
}
pub fn show_cmd(mut self, show: bool) -> Self {
self.show_cmd = show;
self
}
pub fn update_interval(mut self, ms: u64) -> Self {
self.update_interval = ms;
self
}
pub fn filter(&mut self, filter: impl Into<String>) {
self.filter = filter.into().to_lowercase();
self.selected = 0;
self.scroll = 0;
}
pub fn clear_filter(&mut self) {
self.filter.clear();
}
pub fn toggle_sort(&mut self, column: ProcessSort) {
if self.sort == column {
self.sort_asc = !self.sort_asc;
} else {
self.sort = column;
self.sort_asc = false;
}
}
pub fn refresh(&mut self) {
self.system.refresh_all();
self.update_process_list();
self.last_update = std::time::Instant::now();
}
pub fn needs_update(&self) -> bool {
self.last_update.elapsed().as_millis() >= self.update_interval as u128
}
pub fn tick(&mut self) {
if self.needs_update() {
self.refresh();
}
}
fn update_process_list(&mut self) {
let total_memory = self.system.total_memory() as f32;
self.processes = self
.system
.processes()
.iter()
.map(|(pid, proc): (&sysinfo::Pid, &sysinfo::Process)| {
let memory = proc.memory();
ProcessInfo {
pid: pid.as_u32(),
parent_pid: proc.parent().map(|p| p.as_u32()),
name: proc.name().to_string_lossy().into_owned(),
cpu: proc.cpu_usage(),
memory,
memory_percent: (memory as f32 / total_memory) * 100.0,
status: format!("{:?}", proc.status()),
cmd: proc
.cmd()
.iter()
.map(|s| s.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(" "),
user: proc.user_id().map(|u| u.to_string()).unwrap_or_default(),
}
})
.filter(|p| {
if self.filter.is_empty() {
true
} else {
p.name.to_lowercase().contains(&self.filter)
|| p.cmd.to_lowercase().contains(&self.filter)
}
})
.collect();
self.processes.sort_by(|a, b| {
let ord = match self.sort {
ProcessSort::Pid => a.pid.cmp(&b.pid),
ProcessSort::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
ProcessSort::Cpu => a
.cpu
.partial_cmp(&b.cpu)
.unwrap_or(std::cmp::Ordering::Equal),
ProcessSort::Memory => a.memory.cmp(&b.memory),
ProcessSort::Status => a.status.cmp(&b.status),
};
if self.sort_asc {
ord
} else {
ord.reverse()
}
});
if self.selected >= self.processes.len() {
self.selected = self.processes.len().saturating_sub(1);
}
}
pub fn select_next(&mut self) {
if self.selected < self.processes.len().saturating_sub(1) {
self.selected += 1;
}
}
pub fn select_prev(&mut self) {
if self.selected > 0 {
self.selected -= 1;
}
}
pub fn page_down(&mut self, page_size: usize) {
self.selected = (self.selected + page_size).min(self.processes.len().saturating_sub(1));
}
pub fn page_up(&mut self, page_size: usize) {
self.selected = self.selected.saturating_sub(page_size);
}
pub fn selected_process(&self) -> Option<&ProcessInfo> {
self.processes.get(self.selected)
}
pub fn process_count(&self) -> usize {
self.processes.len()
}
pub fn cpu_usage(&self) -> f32 {
self.system.global_cpu_usage()
}
pub fn memory_usage(&self) -> (u64, u64) {
(self.system.used_memory(), self.system.total_memory())
}
fn format_bytes(bytes: u64) -> String {
format_size_compact(bytes)
}
fn render_header(&self, ctx: &mut RenderContext) {
let area = ctx.area;
for x in 0..area.width {
let mut cell = Cell::new(' ');
cell.bg = Some(self.colors.header_bg);
ctx.set(x, 0, cell);
}
let headers = [
("PID", 7, ProcessSort::Pid),
("NAME", 20, ProcessSort::Name),
("CPU%", 7, ProcessSort::Cpu),
("MEM%", 7, ProcessSort::Memory),
("MEM", 8, ProcessSort::Memory),
("STATUS", 10, ProcessSort::Status),
];
let mut x_offset = 0u16;
for (name, width, sort) in headers {
let indicator = if self.sort == sort {
if self.sort_asc {
"â–²"
} else {
"â–¼"
}
} else {
""
};
let text = format!("{}{}", name, indicator);
let mut hx = x_offset;
for ch in text.chars() {
let cw = crate::utils::char_width(ch) as u16;
if hx + cw > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(self.colors.header_fg);
cell.bg = Some(self.colors.header_bg);
cell.modifier = Modifier::BOLD;
ctx.set(hx, 0, cell);
hx += cw;
}
x_offset += width as u16;
}
}
fn render_stats(&self, ctx: &mut RenderContext, y: u16) {
let area = ctx.area;
let (used_mem, total_mem) = self.memory_usage();
let cpu = self.cpu_usage();
let stats = format!(
"CPU: {:5.1}% MEM: {} / {} ({:.1}%) Processes: {}",
cpu,
Self::format_bytes(used_mem),
Self::format_bytes(total_mem),
(used_mem as f64 / total_mem as f64) * 100.0,
self.process_count()
);
let mut sx: u16 = 0;
for ch in stats.chars() {
let cw = crate::utils::char_width(ch) as u16;
if sx + cw > area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(LIGHT_GRAY);
ctx.set(sx, y, cell);
sx += cw;
}
}
}
impl Default for ProcessMonitor {
fn default() -> Self {
Self::new()
}
}
impl View for ProcessMonitor {
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width < 40 || area.height < 5 {
return;
}
self.render_stats(ctx, 0);
let _header_ctx = RenderContext::new(ctx.buffer, ctx.sub_area(0, 1, area.width, 1));
self.render_header(ctx);
let list_start = 2u16;
let visible_rows = (area.height - list_start) as usize;
let scroll = if self.selected < self.scroll {
self.selected
} else if self.selected >= self.scroll + visible_rows {
self.selected - visible_rows + 1
} else {
self.scroll
};
for (i, proc) in self
.processes
.iter()
.skip(scroll)
.take(visible_rows)
.enumerate()
{
let y = list_start + i as u16;
let is_selected = scroll + i == self.selected;
if is_selected {
for x in 0..area.width {
let mut cell = Cell::new(' ');
cell.bg = Some(self.colors.selected_bg);
ctx.set(x, y, cell);
}
}
let bg = if is_selected {
Some(self.colors.selected_bg)
} else {
None
};
let pid_str = format!("{:>6}", proc.pid);
for (j, ch) in pid_str.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = Some(self.colors.pid);
cell.bg = bg;
ctx.set(j as u16, y, cell);
}
let name = crate::utils::truncate_to_width(&proc.name, 19);
let mut nx: u16 = 7;
for ch in name.chars() {
let cw = crate::utils::char_width(ch) as u16;
if nx + cw > 26 {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(self.colors.name);
cell.bg = bg;
ctx.set(nx, y, cell);
nx += cw;
}
let cpu_str = format!("{:>6.1}", proc.cpu);
let cpu_color = if proc.cpu > 80.0 {
self.colors.high_cpu
} else if proc.cpu > 30.0 {
self.colors.medium_cpu
} else {
self.colors.low_cpu
};
for (j, ch) in cpu_str.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = Some(cpu_color);
cell.bg = bg;
ctx.set(27 + j as u16, y, cell);
}
let mem_pct_str = format!("{:>6.1}", proc.memory_percent);
let mem_color = if proc.memory_percent > 10.0 {
self.colors.high_mem
} else {
Color::WHITE
};
for (j, ch) in mem_pct_str.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = Some(mem_color);
cell.bg = bg;
ctx.set(34 + j as u16, y, cell);
}
let mem_str = format!("{:>7}", Self::format_bytes(proc.memory));
for (j, ch) in mem_str.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = Some(Color::WHITE);
cell.bg = bg;
ctx.set(41 + j as u16, y, cell);
}
if area.width > 55 {
let status = crate::utils::truncate_to_width(&proc.status, 8);
let mut stx: u16 = 49;
for ch in status.chars() {
let cw = crate::utils::char_width(ch) as u16;
if stx + cw > 57 {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(LIGHT_GRAY);
cell.bg = bg;
ctx.set(stx, y, cell);
stx += cw;
}
}
}
}
crate::impl_view_meta!("ProcessMonitor");
}
impl_styled_view!(ProcessMonitor);
impl_props_builders!(ProcessMonitor);
pub fn process_monitor() -> ProcessMonitor {
ProcessMonitor::new()
}
pub fn htop() -> ProcessMonitor {
ProcessMonitor::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_info_clone() {
let info = ProcessInfo {
pid: 1234,
parent_pid: Some(1),
name: "test".to_string(),
cpu: 5.0,
memory: 1024,
memory_percent: 0.1,
status: "Running".to_string(),
cmd: "test".to_string(),
user: "user".to_string(),
};
let cloned = info.clone();
assert_eq!(info.pid, cloned.pid);
assert_eq!(info.name, cloned.name);
}
#[test]
fn test_process_info_debug() {
let info = ProcessInfo {
pid: 1,
parent_pid: None,
name: "init".to_string(),
cpu: 0.0,
memory: 0,
memory_percent: 0.0,
status: "Sleeping".to_string(),
cmd: "".to_string(),
user: "root".to_string(),
};
let debug_str = format!("{:?}", info);
assert!(debug_str.contains("ProcessInfo"));
}
#[test]
fn test_update_process_list() {
let mut monitor = ProcessMonitor::new();
if cfg!(feature = "sysinfo") {
monitor.refresh();
let _count = monitor.process_count(); }
}
}