use std::{
error::Error,
io::{stdout, Stdout},
ops::ControlFlow,
time::Duration,
};
use clap::Parser;
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use log::{info, warn};
use procfs::process::{all_processes, Process};
use ratatui::layout::Constraint::Percentage;
use ratatui::widgets::{Cell, Row, Table};
use ratatui::{
prelude::*,
widgets::{block::Title, Block, BorderType, Borders},
};
use uzers::get_user_by_uid;
mod logger;
mod model;
const NAME: &str = env!("CARGO_PKG_NAME");
const VERSION: &str = env!("CARGO_PKG_VERSION");
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {}
fn main() -> Result<()> {
logger::initialize_logging();
initialize_panic_handler();
info!("{NAME} ({VERSION}) started.");
let _cli = Cli::parse();
let mut terminal = setup_terminal()?;
let result = run(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = result {
eprintln!("{err:?}");
}
Ok(())
}
fn setup_terminal() -> Result<Terminal> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}
fn run(terminal: &mut Terminal) -> Result<()> {
loop {
terminal.draw(ui)?;
if handle_events()?.is_break() {
return Ok(());
}
}
}
fn handle_events() -> Result<ControlFlow<()>> {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(ControlFlow::Break(()));
}
}
}
Ok(ControlFlow::Continue(()))
}
pub fn initialize_panic_handler() {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap();
crossterm::terminal::disable_raw_mode().unwrap();
original_hook(panic_info);
}));
}
fn ui(frame: &mut Frame) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(100)])
.split(frame.size());
let mut rows = Vec::new();
let all_processes: Vec<Process> = all_processes()
.expect("Can't read /proc")
.filter_map(|p| match p {
Ok(p) => Some(p),
Err(e) => match e {
procfs::ProcError::NotFound(_) => None,
procfs::ProcError::Io(_e, _path) => None,
x => {
println!("Can't read process due to error {x:?}");
None
}
},
})
.collect();
for process in all_processes {
let mut cells = Vec::new();
let stat_result = process.stat();
match stat_result {
Ok(stat) => {
let pid = Cell::new(stat.pid.to_string());
cells.push(pid);
let ppid = Cell::new(stat.ppid.to_string());
cells.push(ppid);
let command = Cell::new(stat.comm);
cells.push(command);
let number_of_threads = Cell::new(stat.num_threads.to_string());
cells.push(number_of_threads);
let mut user_name: String = "unknown".to_string();
let uid_result = process.uid();
match uid_result {
Ok(uid) => {
let user_option = get_user_by_uid(uid);
match user_option {
Some(u) => {
user_name = u.name().to_os_string().into_string().unwrap();
}
None => {
warn!("Nu user found found for {}", process.pid().to_string());
}
}
info!(
"user {}; {}; {:?}",
uid,
process.pid().to_string(),
user_name
);
}
Err(_e) => {
warn!("Nu user found for {}", process.pid().to_string());
break;
}
}
let user = Cell::new(user_name);
cells.push(user);
let mem = Cell::new(stat.vsize.to_string());
cells.push(mem);
let cpu = Cell::new("n/a".to_string());
cells.push(cpu);
}
Err(_e) => {
warn!("Stat not found for {}", process.pid().to_string());
break;
}
}
rows.push(Row::new(cells));
}
info!("Battery: {}", model::get_battery());
let header = [
"Pid:", "Ppid:", "Command:", "Threads:", "User:", "MemB", "Cpu%",
]
.iter()
.cloned()
.map(Cell::from)
.collect::<Row>()
.height(1);
let block = Block::default()
.title(Title::from("brt").alignment(Alignment::Center))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::White))
.border_type(BorderType::Rounded);
let widths = [
Percentage(5),
Percentage(5),
Percentage(70),
Percentage(5),
Percentage(5),
Percentage(5),
Percentage(5),
];
let table = Table::new(rows, widths).block(block).header(header);
frame.render_widget(table, layout[0]);
}
#[cfg(test)]
mod tests {
use crate::Cli;
#[test]
fn verify_cli() {
use clap::CommandFactory;
Cli::command().debug_assert()
}
}