zoi-cli 1.24.6

Advanced Package Manager & Environment Orchestrator
Documentation
use crate::pkg::{db, local, types};
use anyhow::{Result, anyhow};
use colored::*;
use comfy_table::{Table, presets::UTF8_FULL};
use dialoguer::{Select, theme::ColorfulTheme};
use std::collections::HashMap;

#[derive(Clone)]
struct CandidateDisplay {
    manifest: types::InstallManifest,
    description: String,
}

fn scope_label(scope: types::Scope) -> &'static str {
    match scope {
        types::Scope::User => "user",
        types::Scope::System => "system",
        types::Scope::Project => "project",
    }
}

fn lookup_descriptions() -> HashMap<(String, String, Option<String>, &'static str, String), String>
{
    let mut descriptions = HashMap::new();
    if let Ok(packages) = db::list_all_packages("local") {
        for pkg in packages {
            let key = (
                pkg.name.clone(),
                pkg.repo.clone(),
                pkg.sub_package.clone(),
                scope_label(pkg.scope),
                pkg.registry_handle.unwrap_or_else(|| "local".to_string()),
            );
            descriptions.insert(key, pkg.description);
        }
    }
    descriptions
}

fn build_candidate_displays(candidates: &[types::InstallManifest]) -> Vec<CandidateDisplay> {
    let descriptions = lookup_descriptions();
    candidates
        .iter()
        .cloned()
        .map(|manifest| {
            let description = descriptions
                .get(&(
                    manifest.name.clone(),
                    manifest.repo.clone(),
                    manifest.sub_package.clone(),
                    scope_label(manifest.scope),
                    manifest.registry_handle.clone(),
                ))
                .cloned()
                .or_else(|| {
                    let source_path = local::get_package_source_path(&manifest).ok()?;
                    let path = source_path.to_str()?;
                    let pkg = crate::pkg::lua::parser::parse_lua_package(
                        path,
                        Some(&manifest.version),
                        Some(manifest.scope),
                        true,
                    )
                    .ok()?;
                    Some(pkg.description)
                })
                .unwrap_or_else(|| "Description unavailable".to_string());

            CandidateDisplay {
                manifest,
                description,
            }
        })
        .collect()
}

pub fn choose_installed_manifest(
    package_name: &str,
    candidates: &[types::InstallManifest],
    yes: bool,
) -> Result<types::InstallManifest> {
    if candidates.is_empty() {
        return Err(anyhow!("Package '{}' is not installed.", package_name));
    }
    if candidates.len() == 1 {
        return Ok(candidates[0].clone());
    }
    if yes {
        return Err(anyhow!(
            "Package '{}' matches multiple installed packages. Use an explicit source like '#handle@repo/name[:sub]@version'.",
            package_name
        ));
    }

    let displays = build_candidate_displays(candidates);

    println!(
        "Found multiple installed packages matching '{}'. Please choose one:",
        package_name.cyan()
    );

    let mut table = Table::new();
    table.load_preset(UTF8_FULL);
    table.set_header(vec!["#", "Scope", "Source", "Version", "Description"]);

    for (i, display) in displays.iter().enumerate() {
        table.add_row(vec![
            (i + 1).to_string(),
            scope_label(display.manifest.scope).to_string(),
            local::installed_manifest_source(&display.manifest),
            display.manifest.version.clone(),
            display.description.clone(),
        ]);
    }
    println!("{table}");

    let items: Vec<String> = displays
        .iter()
        .map(|display| {
            format!(
                "{} ({}, v{})",
                local::installed_manifest_source(&display.manifest),
                scope_label(display.manifest.scope),
                display.manifest.version
            )
        })
        .collect();

    let selection = Select::with_theme(&ColorfulTheme::default())
        .with_prompt("Select an installed package")
        .items(&items)
        .default(0)
        .interact()?;

    Ok(displays[selection].manifest.clone())
}