pagers 0.2.0

Page cache diagnostics and control tool for Linux and macOS
use std::path::PathBuf;

use clap::{Parser, Subcommand, ValueEnum, ValueHint};

// Only include `size_range` for normal builds (not when compiling `build.rs` where
// the module machinery does not work)
#[cfg(pagers_normal_build)]
use crate::{SizeRange, parse_size};

/// Fast page cache control
#[derive(Parser, Debug)]
#[command(name = "pagers", version, arg_required_else_help = true)]
#[command(styles = styles())]
pub struct Cli {
    #[command(subcommand)]
    pub command: Command,
}

#[derive(clap::Args, Debug)]
pub struct WithCommon<T: clap::Args> {
    #[command(flatten)]
    pub common: CommonArgs,

    #[command(flatten)]
    pub output: OutputArgs,

    #[command(flatten)]
    pub inner: T,
}

#[derive(Subcommand, Debug)]
pub enum Command {
    /// Show page cache residency
    Query(WithCommon<()>),
    /// Touch pages into memory
    Touch(WithCommon<()>),
    /// Evict pages from memory
    Evict(WithCommon<()>),
    /// Lock pages with mlock(2)
    Lock(WithCommon<LockInner>),
    /// Lock all pages with mlockall(2)
    Lockall(WithCommon<LockInner>),
}

impl Command {
    pub fn output(&self) -> &OutputArgs {
        match self {
            Self::Query(a) | Self::Touch(a) | Self::Evict(a) => &a.output,
            Self::Lock(a) | Self::Lockall(a) => &a.output,
        }
    }
}

#[derive(clap::Args, Debug)]
pub struct OutputArgs {
    /// Output format (enables CLI mode; omit for TUI)
    #[arg(short = 'o', long, value_enum)]
    pub format: Option<OutputFormatArg>,

    #[command(flatten)]
    pub verbosity: clap_verbosity_flag::Verbosity<clap_verbosity_flag::WarnLevel>,
}

impl OutputArgs {
    /// Any `-q` flag was passed (verbosity reduced below the default warn level).
    pub fn is_quiet(&self) -> bool {
        use clap_verbosity_flag::VerbosityFilter;
        matches!(
            self.verbosity.filter(),
            VerbosityFilter::Off | VerbosityFilter::Error
        )
    }
}

#[derive(Clone, clap::Args, Debug)]
pub struct FilterArgs {
    /// Ignore files matching glob pattern
    #[arg(short = 'i', long)]
    pub ignore: Vec<String>,

    /// Only process files matching glob pattern
    #[arg(short = 'I', long = "filter")]
    pub filter: Vec<String>,
}

#[derive(clap::Args, Debug)]
pub struct CommonArgs {
    /// Files or directories to process
    #[arg(required_unless_present = "batch", value_hint = ValueHint::AnyPath)]
    pub paths: Vec<PathBuf>,

    /// Follow symbolic links
    #[arg(short = 'f')]
    pub follow_symlinks: bool,

    /// Stay on same filesystem
    #[arg(short = 'F')]
    pub single_filesystem: bool,

    /// Count hardlinked copies separately
    #[arg(short = 'H')]
    pub count_hardlinks: bool,

    /// Max file size (e.g. 4k, 100M, 1.5G)
    #[arg(short = 'm', long, value_parser = parse_size)]
    pub max_file_size: Option<u64>,

    /// Byte range (e.g. 10K-20G, 100M..500M, 0,1G)
    #[arg(short = 'p', long, value_parser = clap::value_parser!(SizeRange))]
    pub range: Option<SizeRange>,

    #[command(flatten)]
    pub filter: FilterArgs,

    /// Read paths from file (- for stdin)
    #[arg(short = 'b', long, value_hint = ValueHint::FilePath)]
    pub batch: Option<PathBuf>,

    /// NUL-delimited paths in batch mode
    #[arg(short = '0', requires = "batch")]
    pub nul_delim: bool,

    /// Number of threads (0 = all cores)
    #[arg(short = 'j', long, default_value_t, value_parser = clap::value_parser!(pagers_core::crawl::Threads))]
    pub threads: pagers_core::crawl::Threads,
}

#[derive(clap::Args, Debug)]
pub struct LockInner {
    /// Run as daemon (block until signal)
    #[arg(short, long)]
    pub daemon: bool,

    /// Wait until all pages are locked (with -d)
    #[arg(short, long, requires = "daemon")]
    pub wait: bool,

    /// Write pidfile
    #[arg(short = 'P', long, value_hint = ValueHint::FilePath)]
    pub pidfile: Option<PathBuf>,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
pub enum OutputFormatArg {
    /// Human-readable output
    #[default]
    Human,
    /// Key=value pairs
    Kv,
    /// JSON output
    Json,
}

fn styles() -> clap::builder::Styles {
    use anstyle::{AnsiColor, Style};
    clap::builder::Styles::styled()
        .header(Style::new().bold().fg_color(Some(AnsiColor::Green.into())))
        .usage(Style::new().bold().fg_color(Some(AnsiColor::Green.into())))
        .literal(Style::new().fg_color(Some(AnsiColor::Cyan.into())))
        .placeholder(Style::new().fg_color(Some(AnsiColor::BrightBlack.into())))
        .error(Style::new().bold().fg_color(Some(AnsiColor::Red.into())))
        .valid(Style::new().fg_color(Some(AnsiColor::Green.into())))
        .invalid(Style::new().fg_color(Some(AnsiColor::Yellow.into())))
}