crabtop 0.1.0

Terminal-based Linux system monitor with CPU, memory, GPU and thermal metrics
mod app;
mod config;
mod history;
mod metrics;
mod ui;

use std::io;
use std::sync::Arc;
use std::time::Duration;

use anyhow::Result;
use clap::Parser;
use crossterm::event::{
    DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode, KeyEventKind,
    KeyModifiers,
};
use crossterm::execute;
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use futures::StreamExt;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use tokio::sync::RwLock;

use app::{AppState, render_interval, spawn_poller};
use config::{CliArgs, Config};

fn install_panic_hook() {
    let default = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let _ = disable_raw_mode();
        let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture);
        default(info);
    }));
}

#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() -> Result<()> {
    let args = CliArgs::parse();
    let config: Config = args.into();

    install_panic_hook();
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    let state = Arc::new(RwLock::new(AppState::new(config)));
    let _poller = spawn_poller(Arc::clone(&state));

    let result = run_loop(&mut terminal, Arc::clone(&state)).await;

    disable_raw_mode()?;
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    )?;
    terminal.show_cursor()?;

    result
}

async fn run_loop<B: ratatui::backend::Backend>(
    terminal: &mut Terminal<B>,
    state: Arc<RwLock<AppState>>,
) -> Result<()> {
    let mut events = EventStream::new();
    let mut ticker = tokio::time::interval(render_interval());

    loop {
        tokio::select! {
            _ = ticker.tick() => {
                let s = state.read().await;
                terminal.draw(|f| ui::render(f, &s))?;
            }
            Some(Ok(ev)) = events.next() => {
                if let Event::Key(k) = ev {
                    if k.kind != KeyEventKind::Press {
                        continue;
                    }
                    match k.code {
                        KeyCode::Char('q') | KeyCode::Esc => break,
                        KeyCode::Char('c') if k.modifiers.contains(KeyModifiers::CONTROL) => break,
                        KeyCode::Char(' ') => {
                            let mut s = state.write().await;
                            s.paused = !s.paused;
                        }
                        KeyCode::Char('+') | KeyCode::Char('=') => {
                            let mut s = state.write().await;
                            s.config.interval = (s.config.interval / 2).max(Duration::from_millis(50));
                        }
                        KeyCode::Char('-') | KeyCode::Char('_') => {
                            let mut s = state.write().await;
                            s.config.interval = (s.config.interval * 2).min(Duration::from_secs(10));
                        }
                        _ => {}
                    }
                }
            }
        }
    }

    Ok(())
}