Skip to main content

cli/info/
mod.rs

1mod collect;
2mod render;
3mod resolve;
4
5use crate::config::Config;
6use crate::status::FileStatus;
7use crate::{apps, colors, path_display, shells};
8use anyhow::{Result, bail};
9use resolve::InfoRef;
10
11pub(crate) struct UpdateDiffs {
12    app_files: Vec<collect::AppInfoFile>,
13    shell_files: Vec<collect::ShellInfoFile>,
14}
15
16impl UpdateDiffs {
17    pub(crate) async fn collect(config: &Config) -> Result<Self> {
18        Ok(Self {
19            app_files: collect::collect_app_files(config).await?,
20            shell_files: collect::collect_shell_files(config).await?,
21        })
22    }
23
24    pub(crate) async fn print_shell_for_row(&self, config: &Config, label: &str) -> Result<()> {
25        for file in self.shell_files.iter().filter(|file| {
26            file.status == "update available"
27                && format!("{}/{}", file.category.name, file.file.command_name) == label
28        }) {
29            render::print_shell_update_diff(config, file).await?;
30        }
31        Ok(())
32    }
33
34    pub(crate) async fn print_app_for_row(&self, config: &Config, label: &str) -> Result<()> {
35        for file in self.app_files.iter().filter(|file| {
36            if file.status != FileStatus::UpdateAvail {
37                return false;
38            }
39            if file.category.has_explicit_files
40                && file.category.list_mode == crate::apps::AppListMode::Files
41            {
42                app_file_label(file) == label
43            } else {
44                file.category.name == label
45            }
46        }) {
47            render::print_app_update_diff(config, file).await?;
48        }
49        Ok(())
50    }
51}
52
53fn app_file_label(file: &collect::AppInfoFile) -> String {
54    file.file
55        .display_name
56        .clone()
57        .unwrap_or_else(|| format!("{}/{}", file.category.name, file.file.source_rel.display()))
58}
59
60pub async fn handle_update_target(config: &Config, target: &str) -> Result<()> {
61    crate::config::print_presets_note(config);
62    let diffs = UpdateDiffs::collect(config).await?;
63
64    if diffs.app_files.is_empty() && diffs.shell_files.is_empty() {
65        bail!("nothing installed yet. Run `shine shell install` or `shine app install`.");
66    }
67
68    let candidates = resolve::build_candidates(&diffs.app_files, &diffs.shell_files);
69    let refs = resolve::resolve_target(target, &candidates)?;
70    let mut printed = false;
71
72    for item in refs {
73        match item {
74            InfoRef::AppCategory(category) => {
75                let mut files = diffs
76                    .app_files
77                    .iter()
78                    .filter(|file| {
79                        file.category.name == category && file.status == FileStatus::UpdateAvail
80                    })
81                    .collect::<Vec<_>>();
82                files.sort_by_key(|file| file.file.source_rel.clone());
83                for file in files {
84                    print_update_separator(printed);
85                    print_app_update_row(config, file);
86                    render::print_app_update_diff(config, file).await?;
87                    printed = true;
88                }
89            }
90            InfoRef::AppFile { category, source } => {
91                if let Some(file) = diffs.app_files.iter().find(|file| {
92                    file.category.name == category
93                        && file.file.source_rel == source
94                        && file.status == FileStatus::UpdateAvail
95                }) {
96                    print_app_update_row(config, file);
97                    render::print_app_update_diff(config, file).await?;
98                    printed = true;
99                }
100            }
101            InfoRef::ShellCategory(category) => {
102                let mut files = diffs
103                    .shell_files
104                    .iter()
105                    .filter(|file| {
106                        file.category.name == category && file.status == "update available"
107                    })
108                    .collect::<Vec<_>>();
109                files.sort_by_key(|file| file.file.command_name.clone());
110                for file in files {
111                    print_update_separator(printed);
112                    print_shell_update_row(file);
113                    render::print_shell_update_diff(config, file).await?;
114                    printed = true;
115                }
116            }
117            InfoRef::ShellFile { category, command } => {
118                if let Some(file) = diffs.shell_files.iter().find(|file| {
119                    file.category.name == category
120                        && file.file.command_name == command
121                        && file.status == "update available"
122                }) {
123                    print_shell_update_row(file);
124                    render::print_shell_update_diff(config, file).await?;
125                    printed = true;
126                }
127            }
128        }
129    }
130
131    if !printed {
132        println!(
133            "{}",
134            colors::dim(&format!("No update available for {target}."))
135        );
136    }
137
138    Ok(())
139}
140
141fn print_update_separator(printed: bool) {
142    if printed {
143        println!();
144    }
145}
146
147fn print_shell_update_row(file: &collect::ShellInfoFile) {
148    println!(
149        "  {}  {}/{}  {}",
150        colors::symbol("↑"),
151        file.category.name,
152        file.file.command_name,
153        colors::status_label("update available", "↑"),
154    );
155}
156
157fn print_app_update_row(config: &Config, file: &collect::AppInfoFile) {
158    println!(
159        "  {}  {}  {}  {}  {}",
160        colors::symbol("↑"),
161        app_file_label(file),
162        colors::dim("→"),
163        colors::dim(&path_display::format_home(
164            &file.destination,
165            &config.home_dir
166        )),
167        colors::status_label("update available", "↑"),
168    );
169}
170
171pub async fn handle_info(config: &Config, target: &str, diff: bool, verbose: bool) -> Result<()> {
172    let app_files = collect::collect_app_files(config).await?;
173    let shell_files = collect::collect_shell_files(config).await?;
174
175    let candidates = resolve::build_candidates(&app_files, &shell_files);
176    let refs = match resolve::resolve_target(target, &candidates) {
177        Ok(refs) => refs,
178        Err(installed_error) => {
179            if diff || verbose {
180                return Err(installed_error
181                    .context("--diff and --verbose require an installed app or shell target"));
182            }
183            return handle_available_info(config, target, installed_error).await;
184        }
185    };
186
187    crate::config::print_presets_note(config);
188
189    let mut first = true;
190    for item in refs {
191        if !first {
192            println!();
193        }
194        first = false;
195        match item {
196            InfoRef::AppCategory(category) => {
197                let mut files: Vec<_> = app_files
198                    .iter()
199                    .filter(|f| f.category.name == category)
200                    .cloned()
201                    .collect();
202                files.sort_by_key(|f| f.file.source_rel.clone());
203                for (index, file) in files.iter().enumerate() {
204                    if index > 0 {
205                        println!();
206                    }
207                    render::print_app_file(config, file, diff, verbose).await?;
208                }
209            }
210            InfoRef::AppFile { category, source } => {
211                let file = app_files
212                    .iter()
213                    .find(|f| f.category.name == category && f.file.source_rel == source)
214                    .ok_or_else(|| anyhow::anyhow!("installed app config not found"))?;
215                render::print_app_file(config, file, diff, verbose).await?;
216            }
217            InfoRef::ShellCategory(category) => {
218                let mut files: Vec<_> = shell_files
219                    .iter()
220                    .filter(|f| f.category.name == category)
221                    .cloned()
222                    .collect();
223                files.sort_by_key(|f| f.file.command_name.clone());
224                for (index, file) in files.iter().enumerate() {
225                    if index > 0 {
226                        println!();
227                    }
228                    render::print_shell_file(config, file, diff, verbose).await?;
229                }
230            }
231            InfoRef::ShellFile { category, command } => {
232                let file = shell_files
233                    .iter()
234                    .find(|f| f.category.name == category && f.file.command_name == command)
235                    .ok_or_else(|| anyhow::anyhow!("installed shell preset not found"))?;
236                render::print_shell_file(config, file, diff, verbose).await?;
237            }
238        }
239    }
240
241    Ok(())
242}
243
244async fn handle_available_info(
245    config: &Config,
246    target: &str,
247    installed_error: anyhow::Error,
248) -> Result<()> {
249    let target = target.trim();
250    if let Some(rest) = target.strip_prefix("app/") {
251        let category = rest.split('/').next().unwrap_or_default();
252        if category.is_empty() || rest.contains('/') {
253            return Err(installed_error);
254        }
255        return Box::pin(apps::handle_info(config, category)).await;
256    }
257    if let Some(rest) = target.strip_prefix("shell/") {
258        if rest.is_empty() {
259            return Err(installed_error);
260        }
261        return Box::pin(shells::handle_info(config, rest)).await;
262    }
263
264    let app_matches = apps::load_active_categories(config, Some(target))
265        .await?
266        .into_iter()
267        .any(|category| category.name == target);
268    let shell_categories = shells::metadata::load_active_categories(config, None).await?;
269    let shell_matches = shell_categories.iter().any(|category| {
270        category.name == target
271            || category
272                .files
273                .iter()
274                .any(|file| file.command_name == target)
275    });
276
277    match (app_matches, shell_matches) {
278        (true, false) => Box::pin(apps::handle_info(config, target)).await,
279        (false, true) => Box::pin(shells::handle_info(config, target)).await,
280        (true, true) => {
281            bail!("ambiguous available target `{target}`; use `app/{target}` or `shell/{target}`")
282        }
283        (false, false) => Err(installed_error),
284    }
285}