memview 1.0.4

Linux-only ncdu-like TUI for attributing RAM across processes, tmpfs, shm, and kernel counters
mod app;
mod model;
mod nav;
mod probe;
mod search;
mod ui;

use app::{App, WorkerCommand, spawn_worker};
use clap::Parser;
use color_eyre::eyre::Result;
use crossterm::cursor::{Hide, Show};
use crossterm::event::{
    self, DisableFocusChange, DisableMouseCapture, EnableFocusChange, EnableMouseCapture, Event,
    KeyEventKind,
};
use crossterm::execute;
use crossterm::terminal::{
    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use std::io::{self, Stdout};
use std::time::{Duration, Instant};

const FOCUSED_IDLE_POLL: Duration = Duration::from_millis(500);
const BACKGROUND_IDLE_POLL: Duration = Duration::from_secs(1);
const ANIMATED_REDRAW: Duration = Duration::from_millis(250);

#[derive(Debug, Parser)]
#[command(author, version, about = "ncdu-like RAM accounting for Linux /proc")]
struct Cli {
    #[arg(long, default_value_t = 5000)]
    refresh_ms: u64,
}

pub type MainResult = Result<()>;

pub fn run() -> MainResult {
    color_eyre::install()?;
    let cli = Cli::parse();
    let (commands, events) = spawn_worker(Duration::from_millis(cli.refresh_ms));
    let mut terminal = TerminalGuard::enter()?;
    let mut app = App::new();
    app.set_terminal_height(terminal.height()?);
    app.start_visible_work(&commands);
    let mut dirty = true;
    let mut next_animated_redraw = Instant::now();

    loop {
        while let Ok(event) = events.try_recv() {
            app.apply_worker_event(event, &commands);
            dirty = true;
        }
        if app.poll_deletion(&commands) {
            dirty = true;
        }

        let now = Instant::now();
        let redraw_animation = app.needs_periodic_redraw() && now >= next_animated_redraw;
        if app.is_focused() && (dirty || redraw_animation) {
            terminal.draw(|frame| ui::render(frame, &app))?;
            dirty = false;
            next_animated_redraw = Instant::now() + ANIMATED_REDRAW;
        }

        if event::poll(poll_timeout(&app, next_animated_redraw))? {
            match event::read()? {
                Event::Key(key) => {
                    if key.kind != KeyEventKind::Press {
                        continue;
                    }
                    if app.handle_key(key, &commands) {
                        break;
                    }
                    dirty = true;
                }
                Event::Mouse(mouse) => {
                    if app.handle_mouse(mouse, &commands) {
                        dirty = true;
                    }
                }
                Event::Resize(_, height) => {
                    app.set_terminal_height(height);
                    dirty = true;
                }
                Event::FocusGained => {
                    app.set_focused(true, &commands);
                    dirty = true;
                }
                Event::FocusLost => {
                    app.set_focused(false, &commands);
                    dirty = false;
                }
                Event::Paste(_) => {}
            }
        }
    }

    let _ = commands.send(WorkerCommand::Shutdown);
    Ok(())
}

fn poll_timeout(app: &App, next_animated_redraw: Instant) -> Duration {
    if !app.is_focused() {
        return BACKGROUND_IDLE_POLL;
    }
    if app.needs_periodic_redraw() {
        FOCUSED_IDLE_POLL.min(next_animated_redraw.saturating_duration_since(Instant::now()))
    } else {
        FOCUSED_IDLE_POLL
    }
}

struct TerminalGuard {
    terminal: Terminal<CrosstermBackend<Stdout>>,
}

impl TerminalGuard {
    fn enter() -> Result<Self> {
        enable_raw_mode()?;
        let mut stdout = io::stdout();
        execute!(
            stdout,
            EnterAlternateScreen,
            EnableMouseCapture,
            EnableFocusChange,
            Hide
        )?;
        let backend = CrosstermBackend::new(stdout);
        let terminal = Terminal::new(backend)?;
        Ok(Self { terminal })
    }

    fn draw<F>(&mut self, draw: F) -> Result<()>
    where
        F: FnOnce(&mut ratatui::Frame<'_>),
    {
        let _ = self.terminal.draw(draw)?;
        Ok(())
    }

    fn height(&self) -> Result<u16> {
        Ok(self.terminal.size()?.height)
    }
}

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        let _ = disable_raw_mode();
        let _ = execute!(
            self.terminal.backend_mut(),
            Show,
            DisableFocusChange,
            DisableMouseCapture,
            LeaveAlternateScreen
        );
        let _ = self.terminal.show_cursor();
    }
}