use super::Component;
use crate::action::Action;
use color_eyre::Result;
use humansize::{format_size, FormatSizeOptions, BINARY};
use procfs::process::{all_processes, Process, Stat};
use ratatui::layout::{Constraint, Layout, Margin, Rect, Size};
use ratatui::prelude::{Alignment, Color, Line, Modifier, Style};
use ratatui::widgets::{
Block, BorderType, Borders, Cell, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table,
TableState,
};
use ratatui::Frame;
use std::collections::VecDeque;
use std::sync::mpsc as std_mpsc;
use std::sync::mpsc::{Receiver as ThreadReceiver, Sender as ThreadSender};
use std::thread;
use tracing::info;
use uzers::get_user_by_uid;
#[allow(dead_code)]
#[derive(Default, Clone, Debug)]
pub struct BrtProcess {
pub pid: i32,
pub ppid: i32,
pub program: String,
pub command: String,
pub number_of_threads: i64,
pub user: String,
pub resident_memory: u64,
pub cpus: VecDeque<f64>,
pub cpu_graph: String,
pub cpu: f64,
pub stat: Option<Stat>,
pub cmdline: Option<Vec<String>>,
}
fn get_uid_as_string(process: &Process) -> String {
match process.uid() {
Ok(uid) => {
if let Some(user) = get_user_by_uid(uid) {
if let Some(user) = user.name().to_str() {
user.to_string()
} else {
"<unknown>".to_string()
}
} else {
"<unknown>".to_string()
}
}
Err(_) => "<unknown>".to_string(),
}
}
impl TryFrom<Process> for BrtProcess {
type Error = ();
fn try_from(process: Process) -> Result<Self, Self::Error> {
if let Ok(stat) = process.stat() {
let new_stat = stat.clone();
Ok(Self {
pid: stat.pid,
ppid: stat.ppid,
program: stat.comm,
command: get_cmdline_as_string(&process),
number_of_threads: stat.num_threads,
user: get_uid_as_string(&process),
resident_memory: get_memory(&process),
cpus: Default::default(),
cpu_graph: "foo".to_string(),
cpu: 0.0,
stat: Some(new_stat),
cmdline: None,
})
} else {
Err(())
}
}
}
pub fn get_memory(process: &Process) -> u64 {
if let Ok(statm) = process.statm() {
let page_size = procfs::page_size();
statm.resident * page_size
} else {
0
}
}
fn get_cmdline_as_string(process: &Process) -> String {
if let Ok(cmdline) = process.cmdline() {
cmdline.join(" ")
} else {
"<zombie>".to_string()
}
}
#[derive(Default)]
pub struct ProcessesComponent {
processes: Vec<BrtProcess>,
scrollbar_state: ScrollbarState,
state: TableState,
height: i64,
rx: Option<ThreadReceiver<Vec<BrtProcess>>>,
}
impl ProcessesComponent {
pub fn jump(&mut self, steps: i64) {
let location = self.state.selected().unwrap_or(0) as i64;
let length = self.processes.len() as i64;
info!(
"Move {} steps in [{}..{}] when current location is {}.",
steps, 0, length, location
);
let mut index = location + steps;
while index < 0 {
index += length;
}
let new_location = (index % length) as usize;
info!("New location is {}.", new_location);
self.state.select(Some(new_location));
self.scrollbar_state = self.scrollbar_state.position(new_location);
}
}
pub fn create_row<'a>(process: &BrtProcess) -> Row<'a> {
let special_style = Style::default().fg(Color::Rgb(0x0D, 0xE7, 0x56));
let humansize_options: FormatSizeOptions = FormatSizeOptions::from(BINARY)
.space_after_value(false)
.decimal_places(1)
.decimal_zeroes(0);
Row::new([
Cell::new(Line::from(process.pid.to_string()).alignment(Alignment::Right)),
Cell::new(process.program.to_string()).style(special_style),
Cell::new(process.command.to_string()),
Cell::new(
Line::from(process.number_of_threads.to_string())
.alignment(Alignment::Right)
.style(special_style),
),
Cell::new(Line::from(process.user.to_string())),
Cell::new(
Line::from(format_size(process.resident_memory, humansize_options))
.style(special_style)
.alignment(Alignment::Right),
),
Cell::new(Line::from("process.cpu_graph.to_string()")),
Cell::new(format!("{:.2}", process.cpu)).style(special_style),
])
}
pub fn create_rows(processes: &Vec<BrtProcess>) -> Vec<Row<'_>> {
let mut rows = Vec::new();
for process in processes {
let row = create_row(process);
rows.push(row);
}
rows
}
pub fn get_all_processes() -> Vec<BrtProcess> {
info!("Getting all processes...");
let mut processes = Vec::new();
for p in all_processes().expect("Could not get processes").flatten() {
if let Ok(process) = BrtProcess::try_from(p) {
processes.push(process);
}
}
info!("{} processes found.", processes.len());
processes
}
fn processes_thread(thread_tx: ThreadSender<Vec<BrtProcess>>) {
loop {
let processes = get_all_processes();
thread_tx.send(processes).unwrap();
}
}
impl Component for ProcessesComponent {
fn init(&mut self, _area: Size) -> Result<()> {
info!("ProcessesComponent::init start");
let (thread_tx, thread_rx) = std_mpsc::channel();
self.rx = Some(thread_rx);
let _ = thread::spawn(move || {
processes_thread(thread_tx);
});
info!("ProcessesComponent::init end");
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::Tick => {
}
Action::Render => {
}
Action::Up => self.jump(-1),
Action::Down => self.jump(1),
Action::PageUp => self.jump(-self.height),
Action::PageDown => self.jump(self.height),
Action::Update(_since) => {
if let Some(rx) = self.rx.as_mut() {
let mut latest: Option<Vec<BrtProcess>> = None;
#[allow(clippy::while_let_loop)]
loop {
match rx.try_recv() {
Ok(v) => latest = Some(v),
Err(_) => break,
}
}
if let Some(processes) = latest {
self.processes = processes;
info!("Updated {} processes.", self.processes.len());
self.scrollbar_state =
self.scrollbar_state.content_length(self.processes.len());
if self.state.selected().is_none() && !self.processes.is_empty() {
self.state.select(Some(0));
}
}
}
}
_ => {}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [_, layout, _] = Layout::vertical([
Constraint::Length(1), Constraint::Fill(frame.area().height - 2), Constraint::Length(1), ])
.areas(area);
self.height = (layout.height - 4) as i64;
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"))
.track_symbol(Some(" "))
.style(Color::White);
let selected_row_style = Style::default()
.bg(Color::Rgb(0xd4, 0x54, 0x54))
.add_modifier(Modifier::BOLD);
let header = [
Cell::new(Line::from("Pid:").alignment(Alignment::Right)),
Cell::new("Program:"),
Cell::new("Command:"),
Cell::new(Line::from("Threads:").alignment(Alignment::Right)),
Cell::new("User:"),
Cell::new("MemB"),
Cell::new(""),
Cell::new("Cpu%"),
]
.iter()
.cloned()
.collect::<Row>()
.height(1)
.style(
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::White),
);
let rows = create_rows(&self.processes);
let processes = self.processes.len();
let process = format!("{}/{}", self.state.selected().unwrap_or(0) + 1, processes);
let block = Block::default()
.title_top(Line::from("proc").alignment(Alignment::Left))
.title_bottom(Line::from(process.to_string()).alignment(Alignment::Right))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::White))
.border_type(BorderType::Rounded);
let widths = [
Constraint::Percentage(5),
Constraint::Percentage(15),
Constraint::Fill(1),
Constraint::Percentage(5),
Constraint::Percentage(5),
Constraint::Length(7),
Constraint::Length(5),
Constraint::Length(5),
];
let table = Table::new(rows, widths)
.block(block)
.header(header)
.row_highlight_style(selected_row_style);
frame.render_stateful_widget(table, layout, &mut self.state);
frame.render_stateful_widget(
scrollbar,
layout.inner(Margin {
vertical: 1,
horizontal: 1,
}),
&mut self.scrollbar_state,
);
Ok(())
}
}