amphetamine 0.1.1

Reclaim memory and win scheduler contention on Apple Silicon, safely.
//! Interactive selection of which apps to close or deprioritise.

use crate::{apps::App, guard, manage, proc, ui};
use anyhow::{Result, bail};
use dialoguer::MultiSelect;
use dialoguer::theme::ColorfulTheme;
use std::io::IsTerminal;

/// Shows the running apps, pre-ticked to match the current config, and writes
/// back whatever the user confirms.
///
/// Only apps that could actually be acted on are offered: protected ones are
/// filtered out rather than shown and then refused, and helpers nested inside
/// another app are hidden because closing a parent takes them anyway.
pub fn run(
    list: manage::List,
    apps: &[App],
    table: &proc::Table,
    current: &[String],
) -> Result<Vec<manage::Change>> {
    if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
        bail!("`amph pick` needs an interactive terminal; use `amph add <name>` instead");
    }

    let candidates: Vec<&App> = apps
        .iter()
        .filter(|a| !a.nested)
        .filter(|a| guard::protected_match(&a.identities()).is_none())
        .filter(|a| a.rss > 0)
        .collect();

    if candidates.is_empty() {
        ui::skipped("nothing eligible is running");
        return Ok(Vec::new());
    }

    let labels: Vec<String> = candidates
        .iter()
        .map(|a| {
            format!(
                "{:<26} {:>9}  {:>3} proc{}",
                a.name.chars().take(26).collect::<String>(),
                ui::bytes(a.rss),
                table.tree(a.pid).len(),
                if a.foreground { "" } else { "  (background)" }
            )
        })
        .collect();

    let checked: Vec<bool> = candidates
        .iter()
        .map(|a| guard::any_matches(&a.identities(), current))
        .collect();

    println!();
    let chosen = MultiSelect::with_theme(&ColorfulTheme::default())
        .with_prompt(format!("Space to toggle, Enter to save — {}", list.label()))
        .items(&labels)
        .defaults(&checked)
        .interact_opt()?;

    let Some(indices) = chosen else {
        ui::skipped("cancelled; nothing was changed");
        return Ok(Vec::new());
    };

    let selected: Vec<String> = indices
        .iter()
        .map(|&i| candidates[i].name.clone())
        .collect();
    let offered: Vec<String> = candidates.iter().map(|a| a.name.clone()).collect();
    manage::set_within(list, &selected, &offered)
}