Skip to main content

zoi_cli/cmd/
show.rs

1//! Logic for the `show` command.
2
3use std::fmt::Write;
4use std::fs;
5
6use anyhow::{Result, anyhow};
7use colored::Colorize;
8
9use crate::pkg::{local, resolve, types};
10use crate::utils;
11
12/// Prints a dependency group with the specified indentation.
13fn print_dependency_group(group: &types::DependencyGroup, indent: usize) {
14    let prefix = " ".repeat(indent * 2);
15    let mut count = 0;
16
17    let required = group.required();
18    if !required.is_empty() {
19        for dep in required {
20            println!("{prefix}- {dep} (required)");
21            count += 1;
22        }
23    }
24
25    let options = group.options();
26    if !options.is_empty() {
27        for opt_group in options {
28            println!(
29                "{}{}: {} (choose {})",
30                prefix,
31                opt_group.name.bold(),
32                opt_group.desc,
33                if opt_group.all { "any" } else { "one" }
34            );
35            for dep in &opt_group.depends {
36                println!("{prefix}  - {dep}");
37                count += 1;
38            }
39        }
40    }
41
42    let optional = group.optional();
43    if !optional.is_empty() {
44        for dep in optional {
45            println!("{prefix}- {dep} (optional)");
46            count += 1;
47        }
48    }
49
50    if let types::DependencyGroup::Complex(complex) = group
51        && let Some(subs) = &complex.sub_packages
52    {
53        for (sub_name, sub_group) in subs {
54            println!("{}{}:", prefix, sub_name.bold().cyan());
55            print_dependency_group(sub_group, indent + 1);
56            count += 1;
57        }
58    }
59
60    if count == 0 {
61        println!("{}- {}", prefix, "None".italic());
62    }
63}
64
65/// Displays detailed information about a package.
66///
67/// This command fetches and parses the package definition (possibly from a
68/// PURL), and prints metadata such as description, version, dependencies, and
69/// installation status.
70///
71/// # Errors
72///
73/// Returns an error if:
74/// - PURL fetching fails.
75/// - The package source cannot be resolved.
76/// - The package definition cannot be parsed.
77pub fn run(source: &str, raw: bool, purl: bool) -> Result<()> {
78    let mut source_str = source.trim().to_string();
79    if purl {
80        println!(
81            "{} Fetching PURL package '{}'...",
82            "::".bold().blue(),
83            source_str
84        );
85        source_str =
86            crate::pkg::purl::fetch_and_store_purl_package(&source_str)?;
87    }
88    let source = source_str.as_str();
89    let resolved_source = resolve::resolve_source(source, None, false, false)?;
90
91    if raw {
92        let content = fs::read_to_string(&resolved_source.path)?;
93        println!("{content}");
94        return Ok(());
95    }
96    let mut pkg: types::Package = crate::pkg::lua::parser::parse_lua_package(
97        resolved_source.path.to_str().ok_or_else(|| {
98            anyhow!(
99                "Path contains invalid UTF-8 characters: {}",
100                resolved_source.path.display()
101            )
102        })?,
103        None,
104        None,
105        false
106    )?;
107    if let Some(repo_name) = resolved_source.repo_name {
108        pkg.repo = repo_name;
109    }
110    pkg.version = Some(
111        resolve::get_default_version(
112            &pkg,
113            resolved_source.registry_handle.as_deref()
114        )
115        .unwrap_or_else(|_| "N/A".to_string())
116    );
117
118    let request = resolve::parse_source_string(source)?;
119    let installed_manifest = match find_installed_manifest(&request) {
120        Ok(manifest) => manifest,
121        Err(e) => {
122            eprintln!("Warning: could not check installation status: {e}");
123            None
124        }
125    };
126
127    print_beautiful(&pkg, installed_manifest.as_ref());
128    Ok(())
129}
130
131/// Finds the installed manifest for a package request.
132fn find_installed_manifest(
133    request: &crate::pkg::resolve::PackageRequest
134) -> Result<Option<types::InstallManifest>> {
135    let mut candidates = Vec::new();
136    for scope in [
137        types::Scope::User,
138        types::Scope::System,
139        types::Scope::Project
140    ] {
141        candidates
142            .extend(local::find_installed_manifests_matching(request, scope)?);
143    }
144
145    if candidates.is_empty() {
146        return Ok(None);
147    }
148
149    let package_name = if let Some(sub) = &request.sub_package {
150        format!("{}:{}", request.name, sub)
151    } else {
152        request.name.clone()
153    };
154    Ok(Some(
155        crate::cmd::installed_select::choose_installed_manifest(
156            &package_name,
157            &candidates,
158            false
159        )?
160    ))
161}
162
163/// Prints the package information in a beautiful format.
164fn print_beautiful(
165    pkg: &crate::pkg::types::Package,
166    installed_manifest: Option<&types::InstallManifest>
167) {
168    let mut version_display = if pkg.epoch > 0 {
169        format!("{}:", pkg.epoch)
170    } else {
171        String::new()
172    };
173
174    version_display.push_str(pkg.version.as_deref().unwrap_or("N/A"));
175
176    if pkg.revision != "1" {
177        version_display = format!("{}-{}", version_display, pkg.revision);
178    }
179
180    let repo_display = if pkg.repo.starts_with("git/") {
181        format!("#git@{}", &pkg.repo[4..])
182    } else {
183        pkg.repo.clone()
184    };
185
186    println!(
187        "{} {} - {}",
188        pkg.name.bold().green(),
189        version_display.dimmed(),
190        repo_display
191    );
192    if let Some(website) = &pkg.website {
193        println!("Website: {}", website.cyan().underline());
194    }
195    if !pkg.git.is_empty() {
196        println!("Git Repo: {}", pkg.git.cyan().underline());
197    }
198    println!("{}", pkg.description);
199
200    if let Some(subs) = &pkg.sub_packages {
201        println!("{}: {}", "Sub-packages".bold(), subs.join(", "));
202        if let Some(main_subs) = &pkg.main_subs {
203            println!(
204                "{}: {}",
205                "Main sub-packages".bold(),
206                main_subs.join(", ")
207            );
208        }
209    }
210
211    if let Some(manifest) = installed_manifest {
212        let status_text = if let Some(sub) = &manifest.sub_package {
213            format!("Installed ({sub})")
214        } else {
215            "Installed".to_string()
216        };
217
218        let installed_version_display = if manifest.revision == "1" {
219            manifest.version.clone()
220        } else {
221            format!("{}-{}", manifest.version, manifest.revision)
222        };
223
224        println!(
225            "{}: {} ({})",
226            "Status".bold(),
227            status_text.green(),
228            installed_version_display
229        );
230    } else {
231        println!("{}: {}", "Status".bold(), "Not Installed".red());
232    }
233
234    if !pkg.license.is_empty() {
235        println!("{}: {}", "License".bold(), pkg.license);
236        utils::check_license(&pkg.license);
237    }
238
239    let mut maintainer_line = format!(
240        "{}: {} <{}>",
241        "Maintainer".bold(),
242        pkg.maintainer.name,
243        pkg.maintainer.email
244    );
245    if let Some(website) = &pkg.maintainer.website {
246        let _ = write!(maintainer_line, " - {}", website.cyan().underline());
247    }
248    println!("{maintainer_line}");
249
250    if let Some(author) = &pkg.author {
251        let mut author_line = format!("{}: {}", "Author".bold(), author.name);
252        if let Some(email) = &author.email {
253            let _ = write!(author_line, " <{email}>");
254        }
255        if let Some(website) = &author.website {
256            let _ = write!(author_line, " - {}", website.cyan().underline());
257        }
258        println!("{author_line}");
259    }
260
261    let type_display = match pkg.package_type {
262        crate::pkg::types::PackageType::Package => "Package",
263        crate::pkg::types::PackageType::Collection => "Collection",
264        crate::pkg::types::PackageType::App => "App",
265        crate::pkg::types::PackageType::Extension => "Extension"
266    };
267    println!("{}: {}", "Type".bold(), type_display);
268
269    let scope_display = match pkg.scope {
270        crate::pkg::types::Scope::User => "User",
271        crate::pkg::types::Scope::System => "System",
272        crate::pkg::types::Scope::Project => "Project"
273    };
274    println!("{}: {}", "Scope".bold(), scope_display);
275
276    if !pkg.tags.is_empty() {
277        println!("{}: {}", "Tags".bold(), pkg.tags.join(", "));
278    }
279
280    if let Some(bins) = &pkg.bins
281        && !bins.is_empty()
282    {
283        println!("{}: {}", "Provides".bold(), bins.join(", ").green());
284    }
285
286    if let Some(conflicts) = &pkg.conflicts
287        && !conflicts.is_empty()
288    {
289        println!("{}: {}", "Conflicts".bold(), conflicts.join(", ").red());
290    }
291
292    if pkg.package_type == crate::pkg::types::PackageType::Package {
293        println!("{}: {}", "Available types".bold(), pkg.types.join(", "));
294    }
295
296    if let Some(service) = &pkg.service {
297        println!("\n{}:", "Service".bold());
298        println!("  Run: {}", service.run.cyan());
299        println!(
300            "  Run at load: {}",
301            if service.run_at_load {
302                "Yes".green()
303            } else {
304                "No".yellow()
305            }
306        );
307    }
308
309    if let Some(deps) = &pkg.dependencies {
310        println!("\n{}:", "Dependencies".bold());
311
312        if let Some(runtime) = &deps.runtime {
313            println!("  Runtime:");
314            print_dependency_group(runtime, 2);
315        }
316
317        if let Some(build_deps) = &deps.build {
318            println!("  Build Dependencies:");
319            match build_deps {
320                types::BuildDependencies::Group(group) => {
321                    print_dependency_group(group, 2);
322                }
323                types::BuildDependencies::Typed(typed_build_deps) => {
324                    for (name, group) in &typed_build_deps.types {
325                        println!("    {}:", name.cyan());
326                        print_dependency_group(group, 3);
327                    }
328                }
329            }
330        }
331
332        if let Some(test_deps) = &deps.test {
333            println!("  Test Dependencies:");
334            print_dependency_group(test_deps, 2);
335        }
336    }
337}