amphetamine 0.1.1

Reclaim memory and win scheduler contention on Apple Silicon, safely.
use clap::{Parser, Subcommand};

#[derive(Parser, Debug)]
#[command(
    name = "amph",
    version,
    about = "Reclaim memory and win scheduler contention on Apple Silicon, safely.",
    long_about = "Amphetamine frees memory by closing apps you have explicitly listed, clears \
caches belonging to installed applications, and deprioritises noisy background processes so \
your editor gets more of the CPU.\n\n\
It closes nothing until you name it in the config file, asks apps to quit rather than killing \
them, and refuses to make any change it cannot undo.\n\n\
Note: there is no clock control on Apple Silicon. `amph status` will tell you whether anything \
is actually throttling you; what this tool wins back is memory, swap, and CPU contention."
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Cmd>,
}

#[derive(Subcommand, Debug)]
pub enum Cmd {
    /// Show memory, swap, thermal state and the heaviest apps. Changes nothing.
    Status {
        /// Emit machine-readable JSON.
        #[arg(long)]
        json: bool,
        /// How many apps to list.
        #[arg(long, default_value_t = 10)]
        top: usize,
    },

    /// Close the apps in your config and sweep stale caches.
    Boost {
        /// Show exactly what would happen without doing any of it.
        #[arg(long, short = 'n')]
        dry_run: bool,
        /// Leave caches alone; only close apps.
        #[arg(long)]
        no_caches: bool,
        /// Only sweep caches; close nothing.
        #[arg(long, conflicts_with = "no_caches")]
        only_caches: bool,
        /// Escalate to a hard kill if an app will not quit in time.
        /// This discards unsaved work — an app that stalls is usually asking
        /// you to save something.
        #[arg(long)]
        force: bool,
    },

    /// Hold a focus session: noisy apps deprioritised, idle sleep held off.
    /// Everything is restored when the session ends. Requires `amph setup`.
    Focus {
        /// Minutes to hold the session. Runs until Ctrl-C if omitted.
        #[arg(long = "for", value_name = "MINUTES")]
        minutes: Option<u64>,
        /// Show what would be deprioritised without changing anything.
        #[arg(long, short = 'n')]
        dry_run: bool,
    },

    /// Return any deprioritised process to normal priority. Use this if a
    /// focus session was killed and never got to clean up after itself.
    Restore,

    /// Install the one privilege focus mode needs: permission to return a
    /// process to normal priority without a password prompt. Asks for your
    /// password once, then never again.
    Setup {
        /// Print the exact sudoers rule without installing anything.
        #[arg(long)]
        print: bool,
        /// Remove the rule.
        #[arg(long, conflicts_with = "print")]
        remove: bool,
    },

    /// Add apps to a list. Names match a bundle ID, an app name, or the last
    /// part of a bundle ID, case-insensitively.
    ///
    /// Examples:
    ///   amph add Slack Spotify      close these on `amph boost`
    ///   amph add --demote Dropbox   deprioritise during `amph focus`
    #[command(visible_alias = "a")]
    Add {
        /// App names or bundle IDs.
        #[arg(required = true, value_name = "NAME")]
        names: Vec<String>,
        #[command(flatten)]
        list: ListSelector,
    },

    /// Remove apps from a list.
    #[command(visible_alias = "remove")]
    Rm {
        #[arg(required = true, value_name = "NAME")]
        names: Vec<String>,
        #[command(flatten)]
        list: ListSelector,
    },

    /// Choose apps interactively from what is running, ranked by memory.
    #[command(visible_alias = "p")]
    Pick {
        #[command(flatten)]
        list: ListSelector,
    },

    /// Inspect or create the config file.
    Config {
        #[command(subcommand)]
        action: Option<ConfigCmd>,
    },
}

/// Which list a command applies to. Defaults to the close list, since that is
/// what most edits are.
#[derive(clap::Args, Debug, Clone, Copy)]
#[group(multiple = false)]
pub struct ListSelector {
    /// Apply to the focus deprioritise list instead.
    #[arg(long, short = 'd')]
    pub demote: bool,
    /// Apply to the protect list instead.
    #[arg(long, short = 'p')]
    pub protect: bool,
}

impl From<ListSelector> for crate::manage::List {
    fn from(s: ListSelector) -> Self {
        match s {
            ListSelector { demote: true, .. } => Self::Demote,
            ListSelector { protect: true, .. } => Self::Protect,
            _ => Self::Close,
        }
    }
}

#[derive(Subcommand, Debug)]
pub enum ConfigCmd {
    /// Show the current config, annotated with what is running.
    Show,
    /// Print the config file path.
    Path,
    /// Write a commented starter config.
    Init,
    /// Open the config in $EDITOR.
    Edit,
    /// Print the raw file contents.
    Raw,
}