1use crate::state::{AppState, PackageItem};
2
3fn filter_cached_dependencies(
12 app: &AppState,
13 item_names: &std::collections::HashSet<String>,
14) -> Vec<crate::state::modal::DependencyInfo> {
15 app.install_list_deps
16 .iter()
17 .filter(|dep| {
18 dep.required_by
19 .iter()
20 .any(|req_by| item_names.contains(req_by))
21 })
22 .cloned()
23 .collect()
24}
25
26fn filter_cached_files(
35 app: &AppState,
36 item_names: &std::collections::HashSet<String>,
37) -> Vec<crate::state::modal::PackageFileInfo> {
38 app.install_list_files
39 .iter()
40 .filter(|file_info| item_names.contains(&file_info.name))
41 .cloned()
42 .collect()
43}
44
45fn trigger_background_resolution(
56 app: &mut AppState,
57 items: &[PackageItem],
58 dependency_info: &[crate::state::modal::DependencyInfo],
59 cached_files: &[crate::state::modal::PackageFileInfo],
60) {
61 if dependency_info.is_empty() {
62 app.preflight_deps_items = Some((
63 items.to_vec(),
64 crate::state::modal::PreflightAction::Install,
65 ));
66 app.preflight_deps_resolving = true;
67 }
68 if cached_files.is_empty() {
69 app.preflight_files_items = Some(items.to_vec());
70 app.preflight_files_resolving = true;
71 }
72 app.preflight_services_items = Some(items.to_vec());
73 app.preflight_services_resolving = true;
74 let aur_items: Vec<_> = items
75 .iter()
76 .filter(|p| matches!(p.source, crate::state::Source::Aur))
77 .cloned()
78 .collect();
79 if !aur_items.is_empty() {
80 app.preflight_sandbox_items = Some(aur_items);
81 app.preflight_sandbox_resolving = true;
82 }
83}
84
85fn create_preflight_modal_with_cache(
98 app: &mut AppState,
99 items: Vec<PackageItem>,
100 summary: crate::state::modal::PreflightSummaryData,
101 header: crate::state::modal::PreflightHeaderChips,
102 dependency_info: Vec<crate::state::modal::DependencyInfo>,
103 cached_files: Vec<crate::state::modal::PackageFileInfo>,
104) {
105 app.modal = crate::state::Modal::Preflight {
106 items,
107 action: crate::state::PreflightAction::Install,
108 tab: crate::state::PreflightTab::Deps,
109 summary: Some(Box::new(summary)),
110 summary_scroll: 0,
111 header_chips: header,
112 dependency_info,
113 dep_selected: 0,
114 dep_tree_expanded: std::collections::HashSet::new(),
115 deps_error: None,
116 file_info: cached_files,
117 file_selected: 0,
118 file_tree_expanded: std::collections::HashSet::new(),
119 files_error: None,
120 service_info: Vec::new(),
121 service_selected: 0,
122 services_loaded: false,
123 services_error: None,
124 sandbox_info: Vec::new(),
125 sandbox_selected: 0,
126 sandbox_tree_expanded: std::collections::HashSet::new(),
127 sandbox_loaded: false,
128 sandbox_error: None,
129 selected_optdepends: std::collections::HashMap::new(),
130 cascade_mode: app.remove_cascade_mode,
131 cached_reverse_deps_report: None,
132 };
133}
134
135fn create_preflight_modal_insert_mode(app: &mut AppState, items: Vec<PackageItem>) {
144 let items_clone = items.clone();
145 app.preflight_cancelled
146 .store(false, std::sync::atomic::Ordering::Relaxed);
147 app.preflight_summary_items = Some((items_clone, crate::state::PreflightAction::Install));
148 app.preflight_summary_resolving = true;
149 app.pending_service_plan.clear();
150 app.modal = crate::state::Modal::Preflight {
151 items,
152 action: crate::state::PreflightAction::Install,
153 tab: crate::state::PreflightTab::Summary,
154 summary: None,
155 summary_scroll: 0,
156 header_chips: crate::state::modal::PreflightHeaderChips {
157 package_count: 1,
158 download_bytes: 0,
159 install_delta_bytes: 0,
160 aur_count: 0,
161 risk_score: 0,
162 risk_level: crate::state::modal::RiskLevel::Low,
163 },
164 dependency_info: Vec::new(),
165 dep_selected: 0,
166 dep_tree_expanded: std::collections::HashSet::new(),
167 deps_error: None,
168 file_info: Vec::new(),
169 file_selected: 0,
170 file_tree_expanded: std::collections::HashSet::new(),
171 files_error: None,
172 service_info: Vec::new(),
173 service_selected: 0,
174 services_loaded: false,
175 services_error: None,
176 sandbox_info: Vec::new(),
177 sandbox_selected: 0,
178 sandbox_tree_expanded: std::collections::HashSet::new(),
179 sandbox_loaded: false,
180 sandbox_error: None,
181 selected_optdepends: std::collections::HashMap::new(),
182 cascade_mode: app.remove_cascade_mode,
183 cached_reverse_deps_report: None,
184 };
185}
186
187pub fn open_preflight_modal(app: &mut AppState, items: Vec<PackageItem>, use_cache: bool) {
202 if crate::theme::settings().skip_preflight {
203 let installed_set = crate::logic::deps::get_installed_packages();
207 let provided_set = crate::logic::deps::get_provided_packages(&installed_set);
208 let upgradable_set = crate::logic::deps::get_upgradable_packages();
209
210 let installed_packages: Vec<crate::state::PackageItem> = items
211 .iter()
212 .filter(|item| {
213 let is_installed = crate::logic::deps::is_package_installed_or_provided(
215 &item.name,
216 &installed_set,
217 &provided_set,
218 );
219
220 if !is_installed {
221 return false;
222 }
223
224 let has_update = if upgradable_set.contains(&item.name) {
228 true
230 } else if !item.version.is_empty() {
231 let normalized_target_version =
233 item.version.split('-').next().unwrap_or(&item.version);
234 crate::logic::deps::get_installed_version(&item.name).is_ok_and(
237 |installed_version| normalized_target_version != installed_version,
238 )
239 } else {
240 false
242 };
243
244 !has_update
247 })
248 .cloned()
249 .collect();
250
251 if !installed_packages.is_empty() {
252 app.modal = crate::state::Modal::ConfirmReinstall {
255 items: installed_packages,
256 all_items: items,
257 header_chips: crate::state::modal::PreflightHeaderChips::default(),
258 };
259 return;
260 }
261
262 let has_versions = items.iter().any(|item| {
266 matches!(item.source, crate::state::Source::Official { .. }) && !item.version.is_empty()
267 });
268 let has_upgrade_available = items.iter().any(|item| {
269 matches!(item.source, crate::state::Source::Official { .. })
270 && upgradable_set.contains(&item.name)
271 });
272
273 let has_installed_required_by = items.iter().any(|item| {
275 matches!(item.source, crate::state::Source::Official { .. })
276 && crate::index::is_installed(&item.name)
277 && crate::logic::deps::has_installed_required_by(&item.name)
278 });
279
280 if has_versions && has_upgrade_available && has_installed_required_by {
281 app.modal = crate::state::Modal::ConfirmBatchUpdate {
284 items,
285 dry_run: app.dry_run,
286 };
287 return;
288 }
289
290 crate::install::start_integrated_install_all(app, &items, app.dry_run);
291 app.toast_message = Some(crate::i18n::t(app, "app.toasts.installing_skipped"));
292 app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
293 return;
294 }
295
296 if use_cache {
297 app.preflight_cancelled
299 .store(false, std::sync::atomic::Ordering::Relaxed);
300
301 let crate::logic::preflight::PreflightSummaryOutcome {
302 summary,
303 header,
304 reverse_deps_report: _,
305 } = crate::logic::preflight::compute_preflight_summary(
306 &items,
307 crate::state::PreflightAction::Install,
308 );
309 app.pending_service_plan.clear();
310
311 let item_names: std::collections::HashSet<String> =
312 items.iter().map(|i| i.name.clone()).collect();
313 let cached_deps = filter_cached_dependencies(app, &item_names);
314 let cached_files = filter_cached_files(app, &item_names);
315
316 let dependency_info = if cached_deps.is_empty() {
317 tracing::debug!(
318 "[Preflight] Cache empty, will trigger background dependency resolution for {} packages",
319 items.len()
320 );
321 Vec::new()
322 } else {
323 cached_deps
324 };
325
326 trigger_background_resolution(app, &items, &dependency_info, &cached_files);
327 create_preflight_modal_with_cache(
328 app,
329 items,
330 summary,
331 header,
332 dependency_info,
333 cached_files,
334 );
335 } else {
336 create_preflight_modal_insert_mode(app, items);
337 }
338 app.toast_message = Some(if use_cache {
339 crate::i18n::t(app, "app.toasts.preflight_opened")
340 } else {
341 "Preflight opened".to_string()
342 });
343 app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(2));
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 fn new_app() -> AppState {
356 AppState::default()
357 }
358
359 fn test_package(name: &str) -> PackageItem {
366 PackageItem {
367 name: name.to_string(),
368 version: "1.0.0".to_string(),
369 description: "Test package".to_string(),
370 source: crate::state::Source::Official {
371 repo: "extra".to_string(),
372 arch: "x86_64".to_string(),
373 },
374 popularity: None,
375 out_of_date: None,
376 orphaned: false,
377 }
378 }
379
380 #[test]
381 fn trigger_background_resolution_sets_flags_when_cache_empty() {
393 let mut app = new_app();
394 let items = vec![test_package("test-pkg")];
395
396 trigger_background_resolution(&mut app, &items, &[], &[]);
397
398 assert!(app.preflight_deps_resolving);
400 assert!(app.preflight_files_resolving);
401 assert!(app.preflight_services_resolving);
402 assert!(app.preflight_deps_items.is_some());
404 assert!(app.preflight_files_items.is_some());
405 assert!(app.preflight_services_items.is_some());
406 }
407
408 #[test]
409 fn trigger_background_resolution_skips_deps_when_cached() {
421 let mut app = new_app();
422 let items = vec![test_package("test-pkg")];
423 let cached_deps = vec![crate::state::modal::DependencyInfo {
424 name: "cached-dep".to_string(),
425 version: "1.0".to_string(),
426 status: crate::state::modal::DependencyStatus::ToInstall,
427 source: crate::state::modal::DependencySource::Official {
428 repo: "extra".to_string(),
429 },
430 required_by: vec!["test-pkg".to_string()],
431 depends_on: Vec::new(),
432 is_core: false,
433 is_system: false,
434 }];
435
436 trigger_background_resolution(&mut app, &items, &cached_deps, &[]);
437
438 assert!(!app.preflight_deps_resolving);
440 assert!(app.preflight_deps_items.is_none());
441 assert!(app.preflight_files_resolving);
443 assert!(app.preflight_files_items.is_some());
444 }
445
446 #[test]
447 fn create_preflight_modal_insert_mode_resets_cancelled() {
458 let mut app = new_app();
459 app.preflight_cancelled
460 .store(true, std::sync::atomic::Ordering::Relaxed);
461 let items = vec![test_package("test-pkg")];
462
463 create_preflight_modal_insert_mode(&mut app, items);
464
465 assert!(
467 !app.preflight_cancelled
468 .load(std::sync::atomic::Ordering::Relaxed)
469 );
470 }
471
472 #[test]
473 fn filter_cached_dependencies_returns_matching() {
485 let mut app = new_app();
486 app.install_list_deps = vec![
487 crate::state::modal::DependencyInfo {
488 name: "dep-a".to_string(),
489 version: "1.0".to_string(),
490 status: crate::state::modal::DependencyStatus::ToInstall,
491 source: crate::state::modal::DependencySource::Official {
492 repo: "extra".to_string(),
493 },
494 required_by: vec!["pkg-a".to_string()],
495 depends_on: Vec::new(),
496 is_core: false,
497 is_system: false,
498 },
499 crate::state::modal::DependencyInfo {
500 name: "dep-b".to_string(),
501 version: "1.0".to_string(),
502 status: crate::state::modal::DependencyStatus::ToInstall,
503 source: crate::state::modal::DependencySource::Official {
504 repo: "extra".to_string(),
505 },
506 required_by: vec!["pkg-b".to_string()],
507 depends_on: Vec::new(),
508 is_core: false,
509 is_system: false,
510 },
511 ];
512
513 let mut item_names = std::collections::HashSet::new();
514 item_names.insert("pkg-a".to_string());
515
516 let result = filter_cached_dependencies(&app, &item_names);
517
518 assert_eq!(result.len(), 1);
519 assert_eq!(result[0].name, "dep-a");
520 }
521
522 #[test]
523 fn filter_cached_files_returns_matching() {
535 let mut app = new_app();
536 app.install_list_files = vec![
537 crate::state::modal::PackageFileInfo {
538 name: "pkg-a".to_string(),
539 files: vec![crate::state::modal::FileChange {
540 path: "/usr/bin/a".to_string(),
541 change_type: crate::state::modal::FileChangeType::New,
542 package: "pkg-a".to_string(),
543 is_config: false,
544 predicted_pacnew: false,
545 predicted_pacsave: false,
546 }],
547 total_count: 1,
548 new_count: 1,
549 changed_count: 0,
550 removed_count: 0,
551 config_count: 0,
552 pacnew_candidates: 0,
553 pacsave_candidates: 0,
554 },
555 crate::state::modal::PackageFileInfo {
556 name: "pkg-b".to_string(),
557 files: vec![crate::state::modal::FileChange {
558 path: "/usr/bin/b".to_string(),
559 change_type: crate::state::modal::FileChangeType::New,
560 package: "pkg-b".to_string(),
561 is_config: false,
562 predicted_pacnew: false,
563 predicted_pacsave: false,
564 }],
565 total_count: 1,
566 new_count: 1,
567 changed_count: 0,
568 removed_count: 0,
569 config_count: 0,
570 pacnew_candidates: 0,
571 pacsave_candidates: 0,
572 },
573 ];
574
575 let mut item_names = std::collections::HashSet::new();
576 item_names.insert("pkg-a".to_string());
577
578 let result = filter_cached_files(&app, &item_names);
579
580 assert_eq!(result.len(), 1);
581 assert_eq!(result[0].name, "pkg-a");
582 }
583}