Skip to main content

zoi_cli/cmd/
installed_select.rs

1use crate::pkg::{db, local, types};
2use anyhow::{Result, anyhow};
3use colored::*;
4use comfy_table::{Table, presets::UTF8_FULL};
5use dialoguer::{Select, theme::ColorfulTheme};
6use std::collections::HashMap;
7
8#[derive(Clone)]
9struct CandidateDisplay {
10    manifest: types::InstallManifest,
11    description: String,
12}
13
14fn scope_label(scope: types::Scope) -> &'static str {
15    match scope {
16        types::Scope::User => "user",
17        types::Scope::System => "system",
18        types::Scope::Project => "project",
19    }
20}
21
22fn lookup_descriptions() -> HashMap<(String, String, Option<String>, &'static str, String), String>
23{
24    let mut descriptions = HashMap::new();
25    if let Ok(packages) = db::list_all_packages("local") {
26        for pkg in packages {
27            let key = (
28                pkg.name.clone(),
29                pkg.repo.clone(),
30                pkg.sub_package.clone(),
31                scope_label(pkg.scope),
32                pkg.registry_handle.unwrap_or_else(|| "local".to_string()),
33            );
34            descriptions.insert(key, pkg.description);
35        }
36    }
37    descriptions
38}
39
40fn build_candidate_displays(candidates: &[types::InstallManifest]) -> Vec<CandidateDisplay> {
41    let descriptions = lookup_descriptions();
42    candidates
43        .iter()
44        .cloned()
45        .map(|manifest| {
46            let description = descriptions
47                .get(&(
48                    manifest.name.clone(),
49                    manifest.repo.clone(),
50                    manifest.sub_package.clone(),
51                    scope_label(manifest.scope),
52                    manifest.registry_handle.clone(),
53                ))
54                .cloned()
55                .or_else(|| {
56                    let source_path = local::get_package_source_path(&manifest).ok()?;
57                    let path = source_path.to_str()?;
58                    let pkg = crate::pkg::lua::parser::parse_lua_package(
59                        path,
60                        Some(&manifest.version),
61                        Some(manifest.scope),
62                        true,
63                    )
64                    .ok()?;
65                    Some(pkg.description)
66                })
67                .unwrap_or_else(|| "Description unavailable".to_string());
68
69            CandidateDisplay {
70                manifest,
71                description,
72            }
73        })
74        .collect()
75}
76
77pub fn choose_installed_manifest(
78    package_name: &str,
79    candidates: &[types::InstallManifest],
80    yes: bool,
81) -> Result<types::InstallManifest> {
82    if candidates.is_empty() {
83        return Err(anyhow!("Package '{}' is not installed.", package_name));
84    }
85    if candidates.len() == 1 {
86        return Ok(candidates[0].clone());
87    }
88    if yes {
89        return Err(anyhow!(
90            "Package '{}' matches multiple installed packages. Use an explicit source like '#handle@repo/name[:sub]@version'.",
91            package_name
92        ));
93    }
94
95    let displays = build_candidate_displays(candidates);
96
97    println!(
98        "Found multiple installed packages matching '{}'. Please choose one:",
99        package_name.cyan()
100    );
101
102    let mut table = Table::new();
103    table.load_preset(UTF8_FULL);
104    table.set_header(vec!["#", "Scope", "Source", "Version", "Description"]);
105
106    for (i, display) in displays.iter().enumerate() {
107        table.add_row(vec![
108            (i + 1).to_string(),
109            scope_label(display.manifest.scope).to_string(),
110            local::installed_manifest_source(&display.manifest),
111            display.manifest.version.clone(),
112            display.description.clone(),
113        ]);
114    }
115    println!("{table}");
116
117    let items: Vec<String> = displays
118        .iter()
119        .map(|display| {
120            format!(
121                "{} ({}, v{})",
122                local::installed_manifest_source(&display.manifest),
123                scope_label(display.manifest.scope),
124                display.manifest.version
125            )
126        })
127        .collect();
128
129    let selection = Select::with_theme(&ColorfulTheme::default())
130        .with_prompt("Select an installed package")
131        .items(&items)
132        .default(0)
133        .interact()?;
134
135    Ok(displays[selection].manifest.clone())
136}