mod app;
mod i18n;
mod metrics;
mod ui;
#[cfg(test)]
mod tests;
use std::{
io,
time::{Duration, Instant},
};
use anyhow::Result;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use app::App;
use i18n::detect_lang_code;
#[cfg(target_os = "linux")]
const TAB_COUNT: usize = 7;
#[cfg(not(target_os = "linux"))]
const TAB_COUNT: usize = 6;
const DEFAULT_INTERVAL_MS: u64 = 1000;
fn parse_args() -> (u64, String) {
let mut interval_ms = DEFAULT_INTERVAL_MS;
let mut lang = detect_lang_code();
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--interval" => match args.next() {
Some(val) => match val.parse::<u64>() {
Ok(0) => {
eprintln!("error: --interval must be greater than 0");
std::process::exit(1);
}
Ok(ms) => interval_ms = ms,
Err(_) => {
eprintln!(
"error: --interval expects a positive integer (milliseconds), got {:?}",
val
);
std::process::exit(1);
}
},
None => {
eprintln!("error: --interval requires a value");
std::process::exit(1);
}
},
"--lang" => match args.next() {
Some(code) => {
lang = code;
}
None => {
eprintln!("error: --lang requires an ISO language code (e.g. en, de, fr, es)");
std::process::exit(1);
}
},
"--help" | "-h" => {
let detected = detect_lang_code();
println!("Usage: narsil [--interval <ms>] [--lang <code>]");
println!();
println!("Options:");
println!(
" --interval <ms> Refresh interval in milliseconds [default: {}]",
DEFAULT_INTERVAL_MS
);
println!(
" --lang <code> UI language ISO code (e.g. en, de, fr, es) [auto-detected: {}]",
detected
);
println!(" -h, --help Print this help message");
std::process::exit(0);
}
_ => {}
}
}
(interval_ms, lang)
}
fn main() -> Result<()> {
let (interval_ms, lang) = parse_args();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = run_app(&mut terminal, interval_ms, &lang);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(e) = result {
eprintln!("Error: {e}");
}
Ok(())
}
fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, interval_ms: u64, lang_code: &str) -> Result<()>
where
<B as ratatui::backend::Backend>::Error: Send + Sync + 'static,
{
let mut app = App::new(lang_code);
app.tick_rate_ms = interval_ms;
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(app.tick_rate_ms);
loop {
terminal.draw(|f| ui::draw(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or(Duration::ZERO);
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match (key.code, key.modifiers) {
(KeyCode::Char('q'), _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
return Ok(());
}
(KeyCode::Tab, _) | (KeyCode::Right, _) | (KeyCode::Char('l'), _) => {
app.selected_tab = (app.selected_tab + 1) % TAB_COUNT;
}
(KeyCode::BackTab, _) | (KeyCode::Left, _) | (KeyCode::Char('h'), _) => {
app.selected_tab = (app.selected_tab + TAB_COUNT - 1) % TAB_COUNT;
}
(KeyCode::Down, _) | (KeyCode::Char('j'), _) => {
match app.selected_tab {
4 => {
app.disk_scroll = (app.disk_scroll + 1)
.min(app.disks.len().saturating_sub(1));
}
5 => {
app.process_scroll = (app.process_scroll + 1)
.min(app.processes.len().saturating_sub(1));
}
#[cfg(target_os = "linux")]
6 => {
app.gpu_scroll = (app.gpu_scroll + 1)
.min(app.gpus.len().saturating_sub(1));
}
_ => {}
}
}
(KeyCode::Up, _) | (KeyCode::Char('k'), _) => {
match app.selected_tab {
4 => {
app.disk_scroll = app.disk_scroll.saturating_sub(1);
}
5 => {
app.process_scroll = app.process_scroll.saturating_sub(1);
}
#[cfg(target_os = "linux")]
6 => {
app.gpu_scroll = app.gpu_scroll.saturating_sub(1);
}
_ => {}
}
}
(KeyCode::Char('1'), _) => app.selected_tab = 0,
(KeyCode::Char('2'), _) => app.selected_tab = 1,
(KeyCode::Char('3'), _) => app.selected_tab = 2,
(KeyCode::Char('4'), _) => app.selected_tab = 3,
(KeyCode::Char('5'), _) => app.selected_tab = 4,
(KeyCode::Char('6'), _) => app.selected_tab = 5,
#[cfg(target_os = "linux")]
(KeyCode::Char('7'), _) => app.selected_tab = 6,
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}