winbrew_cli\commands/
list.rs

1//! List command wrapper for installed-package views.
2//!
3//! The wrapper handles empty-state messaging and the final installed-package
4//! total while the app layer provides the filtered package data.
5
6use anyhow::Result;
7use std::io::Write;
8
9use crate::{CommandContext, app::list};
10use winbrew_ui::Ui;
11
12pub fn run(ctx: &CommandContext, query: &[String]) -> Result<()> {
13    let mut ui = ctx.ui();
14    run_with_ui(&mut ui, query)
15}
16
17fn run_with_ui<W: Write>(ui: &mut Ui<W>, query: &[String]) -> Result<()> {
18    ui.page_title("Installed Packages");
19
20    let query_text = (!query.is_empty()).then(|| query.join(" "));
21
22    let packages = list::list_packages(query_text.as_deref())?;
23
24    if packages.is_empty() {
25        match query_text {
26            Some(q) => ui.notice(format!("No installed packages matching '{q}'.")),
27            None => ui.notice("No packages are currently installed."),
28        }
29
30        return Ok(());
31    }
32
33    ui.display_packages(&packages);
34    ui.info(format!("\nTotal: {} package(s) installed.", packages.len()));
35
36    Ok(())
37}
38
39#[cfg(test)]
40mod tests {
41    use super::run_with_ui;
42    use crate::commands::test_support::{buffer_text, buffered_ui};
43    use crate::database::{self, Config};
44    use crate::models::domains::install::{EngineKind, InstallerType};
45    use crate::models::domains::installed::{InstalledPackage, PackageStatus};
46    use tempfile::tempdir;
47    use winbrew_ui::UiSettings;
48
49    fn sample_package() -> InstalledPackage {
50        InstalledPackage {
51            name: "Contoso App".to_string(),
52            version: "1.2.3".to_string(),
53            kind: InstallerType::Portable,
54            deployment_kind: InstallerType::Portable.deployment_kind(),
55            engine_kind: EngineKind::Portable,
56            engine_metadata: None,
57            install_dir: r"C:\Apps\Contoso".to_string(),
58            dependencies: Vec::new(),
59            status: PackageStatus::Ok,
60            installed_at: "2026-04-07T12:00:00Z".to_string(),
61        }
62    }
63
64    #[test]
65    fn run_with_ui_reports_query_specific_empty_state() {
66        let temp_dir = tempdir().expect("temp dir");
67        let config = Config::load_at(temp_dir.path()).expect("config should load");
68        database::init(&config.resolved_paths()).expect("database should initialize");
69
70        let (mut ui, out, err) = buffered_ui(UiSettings::default());
71        let query = ["Contoso".to_string(), "App".to_string()];
72
73        run_with_ui(&mut ui, &query).expect("list should succeed");
74
75        assert!(buffer_text(&out).trim().is_empty());
76        assert!(buffer_text(&err).contains("No installed packages matching 'Contoso App'."));
77    }
78
79    #[test]
80    fn run_with_ui_renders_installed_packages() {
81        let temp_dir = tempdir().expect("temp dir");
82        let config = Config::load_at(temp_dir.path()).expect("config should load");
83        database::init(&config.resolved_paths()).expect("database should initialize");
84
85        {
86            let conn = database::get_conn().expect("database connection");
87            database::insert_package(&conn, &sample_package()).expect("package should seed");
88        }
89
90        let (mut ui, out, err) = buffered_ui(UiSettings::default());
91
92        run_with_ui(&mut ui, &[]).expect("list should succeed");
93
94        let out = buffer_text(&out);
95        let err = buffer_text(&err);
96
97        assert!(out.contains("installed packages"));
98        assert!(out.contains("Contoso App"));
99        assert!(out.contains("1.2.3"));
100        assert!(err.contains("Total: 1 package(s) installed."));
101    }
102}