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}