Skip to main content

zoi_cli/cmd/
show.rs

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