zoi_cli/cmd/
installed_select.rs1use 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}