zenops 0.19.0

Declarative system configuration management for shell config and dotfiles.
Documentation
//! Implementation of `zenops pkg`.
//!
//! Walks the configured packages, applies the visibility filters
//! ([`Options::all`], [`Options::pattern`], plus the always-on OS / shell
//! filters) and pushes one [`crate::output::PkgEntry::Pkg`] per visible row
//! through the renderer. Emits a
//! [`crate::output::PkgEntry::NoPackageManagerDetected`] preamble when *no*
//! manager — primary or supplementary — is reachable on this host, and one
//! [`crate::output::PkgEntry::AggregateInstall`] footer per detected
//! manager with eligible missing pkgs.

use crate::{
    config::{Config, PkgConfig},
    error::Error,
    output::{Event, Output, PkgEntry, PkgEntryState, PkgInstallHints},
    pkg_manager::{self, DetectedPackageManager},
};

/// Visibility filters for `zenops pkg`. OS and configured-shell filters
/// are always on; this struct only carries the user-configurable knobs.
#[derive(Debug, Clone, Default)]
pub struct Options {
    /// Case-insensitive substrings; a pkg passes if any pattern matches its
    /// display name or map key. Empty = no filter.
    pub pattern: Vec<String>,
    /// Include pkgs whose `enable = "disabled"`.
    pub all: bool,
    /// Show every install hint on a pkg, not just the one for the detected manager.
    pub all_hints: bool,
    /// Show extra diagnostic lines (e.g. the detect strategy that matched).
    pub verbose: bool,
}

/// Walk the configured packages in display order and push one [`PkgEntry`]
/// per visible row through `output`. The renderer chosen by `-o` decides
/// formatting (column-aligned text vs. NDJSON). Emits, in order:
///
/// 1. A single [`PkgEntry::NoPackageManagerDetected`] when no supported
///    package manager is on PATH (so install hints will be hidden).
/// 2. One [`PkgEntry::Pkg`] per visible package, after the same OS / shell
///    filtering that used to happen inside `render`.
/// 3. A single [`PkgEntry::AggregateInstall`] footer summarising the
///    combined install command across every missing pkg, when a manager is
///    detected and at least one pkg contributes packages.
pub fn push(config: &Config, opts: Options, output: &mut dyn Output) -> Result<(), Error> {
    let ctx = config.host_context(None)?;
    let primary = pkg_manager::detect(ctx.path, &ctx.os)?;
    let supplementary = pkg_manager::detect_supplementary(ctx.path)?;
    // Display order: primary first, then supplementary. Per-pkg hints and
    // aggregate footers walk this list in order.
    let detected: Vec<DetectedPackageManager> = primary
        .into_iter()
        .chain(supplementary.iter().copied())
        .collect();

    if detected.is_empty() {
        output.push(Event::PkgEntry(PkgEntry::NoPackageManagerDetected {
            supported: vec![
                "brew".to_string(),
                "dnf".to_string(),
                "apt".to_string(),
                "pacman".to_string(),
                "cargo".to_string(),
            ],
        }))?;
    }

    let conditions = config.conditions();
    let needles: Vec<String> = opts.pattern.iter().map(|p| p.to_lowercase()).collect();
    // Entries carry (display_label, key, pkg). The map key stays distinct
    // from the display label so JSON consumers can correlate even when
    // `pkg.name` overrides the key.
    let mut entries: Vec<(&str, &smol_str::SmolStr, &PkgConfig)> = Vec::new();

    for (key, pkg) in config.pkgs() {
        if (opts.all || !pkg.is_disabled()) && pkg.evaluate_when(conditions, &ctx)? {
            let label = pkg.name.as_deref().unwrap_or(key.as_str());
            if needles.is_empty()
                || needles.iter().any(|n| {
                    label.to_lowercase().contains(n) || key.as_str().to_lowercase().contains(n)
                })
            {
                entries.push((label, key, pkg));
            }
        }
    }
    entries.sort_by_key(|(label, _, _)| *label);

    // Eligible missing packages per detected manager. Each manager's
    // aggregate footer lists every missing pkg with non-empty
    // `packages_for(mgr)` — no de-duplication across managers; `starship`
    // legitimately shows up under both `dnf` and `cargo` on a Fedora host
    // with cargo on PATH.
    let mut eligible: Vec<(DetectedPackageManager, Vec<String>)> =
        detected.iter().map(|&m| (m, Vec::new())).collect();

    for (label, key, pkg) in entries {
        let state = if pkg.is_disabled() {
            PkgEntryState::Disabled
        } else if pkg.is_installed(conditions, &ctx)? {
            PkgEntryState::Installed
        } else {
            PkgEntryState::Missing
        };

        let matched_detect = if opts.verbose {
            pkg.matched_detect(conditions, &ctx)?.map(|d| d.to_string())
        } else {
            None
        };

        // Install hints are only meaningful for missing pkgs. With
        // `--all-hints`, surface every populated manager regardless of
        // detection; without, surface only managers detected on this host.
        let install_hints = if !matches!(state, PkgEntryState::Missing) {
            PkgInstallHints::default()
        } else if opts.all_hints {
            PkgInstallHints {
                brew: pkg.install_hint.brew.packages.clone(),
                dnf5: pkg.install_hint.dnf5.packages.clone(),
                apt: pkg.install_hint.apt.packages.clone(),
                pacman: pkg.install_hint.pacman.packages.clone(),
                cargo: pkg.install_hint.cargo.packages.clone(),
            }
        } else {
            let mut hints = PkgInstallHints::default();
            for (idx, &mgr) in detected.iter().enumerate() {
                let pkgs = mgr.packages_for(&pkg.install_hint);
                if pkgs.is_empty() {
                    continue;
                }
                match mgr {
                    DetectedPackageManager::Brew => hints.brew = pkgs.to_vec(),
                    DetectedPackageManager::Dnf5 => hints.dnf5 = pkgs.to_vec(),
                    DetectedPackageManager::Apt => hints.apt = pkgs.to_vec(),
                    DetectedPackageManager::Pacman => hints.pacman = pkgs.to_vec(),
                    DetectedPackageManager::Cargo => hints.cargo = pkgs.to_vec(),
                }
                eligible[idx].1.extend(pkgs.iter().cloned());
            }
            hints
        };

        output.push(Event::PkgEntry(PkgEntry::Pkg {
            name: smol_str::SmolStr::new(label),
            key: key.clone(),
            description: pkg.description.clone(),
            state,
            matched_detect,
            install_hints,
        }))?;
    }

    if !opts.all_hints {
        for (mgr, packages) in eligible {
            if packages.is_empty() {
                continue;
            }
            output.push(Event::PkgEntry(PkgEntry::AggregateInstall {
                pkg_manager: mgr.name().to_string(),
                command: mgr.install_command(&packages),
                packages,
            }))?;
        }
    }

    Ok(())
}