zoi_cli/cmd/
installed_select.rs1use std::collections::HashMap;
4
5use anyhow::{Result, anyhow};
6use colored::Colorize;
7use comfy_table::Table;
8use comfy_table::presets::UTF8_FULL;
9use dialoguer::Select;
10use dialoguer::theme::ColorfulTheme;
11
12use crate::pkg::{db, local, types};
13
14#[derive(Clone)]
16struct CandidateDisplay {
17 manifest: types::InstallManifest,
19 description: String
21}
22
23fn scope_label(scope: types::Scope) -> &'static str {
25 match scope {
26 types::Scope::User => "user",
27 types::Scope::System => "system",
28 types::Scope::Project => "project"
29 }
30}
31
32fn lookup_descriptions()
34-> HashMap<(String, String, Option<String>, &'static str, String), String> {
35 let mut descriptions = HashMap::new();
36 if let Ok(packages) = db::list_all_packages("local") {
37 for pkg in packages {
38 let key = (
39 pkg.name.clone(),
40 pkg.repo.clone(),
41 pkg.sub_package.clone(),
42 scope_label(pkg.scope),
43 pkg.registry_handle.unwrap_or_else(|| "local".to_string())
44 );
45 descriptions.insert(key, pkg.description);
46 }
47 }
48 descriptions
49}
50
51fn build_candidate_displays(
53 candidates: &[types::InstallManifest]
54) -> Vec<CandidateDisplay> {
55 let descriptions = lookup_descriptions();
56 candidates
57 .iter()
58 .cloned()
59 .map(|manifest| {
60 let description = descriptions
61 .get(&(
62 manifest.name.clone(),
63 manifest.repo.clone(),
64 manifest.sub_package.clone(),
65 scope_label(manifest.scope),
66 manifest.registry_handle.clone()
67 ))
68 .cloned()
69 .or_else(|| {
70 let source_path =
71 local::get_package_source_path(&manifest).ok()?;
72 let path = source_path.to_str()?;
73 let pkg = crate::pkg::lua::parser::parse_lua_package(
74 path,
75 Some(&manifest.version),
76 Some(manifest.scope),
77 true
78 )
79 .ok()?;
80 Some(pkg.description)
81 })
82 .unwrap_or_else(|| "Description unavailable".to_string());
83
84 CandidateDisplay {
85 manifest,
86 description
87 }
88 })
89 .collect()
90}
91
92pub fn choose_installed_manifest(
106 package_name: &str,
107 candidates: &[types::InstallManifest],
108 yes: bool
109) -> Result<types::InstallManifest> {
110 if candidates.is_empty() {
111 return Err(anyhow!("Package '{package_name}' is not installed."));
112 }
113 if candidates.len() == 1 {
114 return Ok(candidates
115 .first()
116 .expect("should have at least one candidate")
117 .clone());
118 }
119 if yes {
120 return Err(anyhow!(
121 "Package '{package_name}' matches multiple installed packages. \
122 Use an explicit source like '#handle@repo/name[:sub]@version'."
123 ));
124 }
125
126 let displays = build_candidate_displays(candidates);
127
128 println!(
129 "Found multiple installed packages matching '{}'. Please choose one:",
130 package_name.cyan()
131 );
132
133 let mut table = Table::new();
134 table.load_style(UTF8_FULL);
135 table.set_header(vec!["#", "Scope", "Source", "Version", "Description"]);
136
137 for (i, display) in displays.iter().enumerate() {
138 table.add_row(vec![
139 (i + 1).to_string(),
140 scope_label(display.manifest.scope).to_string(),
141 local::installed_manifest_source(&display.manifest),
142 display.manifest.version.clone(),
143 display.description.clone(),
144 ]);
145 }
146 println!("{table}");
147
148 let items: Vec<String> = displays
149 .iter()
150 .map(|display| {
151 format!(
152 "{} ({}, v{})",
153 local::installed_manifest_source(&display.manifest),
154 scope_label(display.manifest.scope),
155 display.manifest.version
156 )
157 })
158 .collect();
159
160 let selection = Select::with_theme(&ColorfulTheme::default())
161 .with_prompt("Select an installed package")
162 .items(&items)
163 .default(0)
164 .interact()?;
165
166 Ok(displays
167 .get(selection)
168 .ok_or_else(|| anyhow!("Invalid selection"))?
169 .manifest
170 .clone())
171}