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 if crate::events::install::try_open_warn_aur_repo_duplicate_modal(
204 app,
205 &items,
206 crate::state::modal::PreflightHeaderChips::default(),
207 ) {
208 return;
209 }
210 let installed_set = crate::logic::deps::get_installed_packages();
214 let provided_set = crate::logic::deps::get_provided_packages(&installed_set);
215 let upgradable_set = crate::logic::deps::get_upgradable_packages();
216
217 let installed_packages: Vec<crate::state::PackageItem> = items
218 .iter()
219 .filter(|item| {
220 let is_installed = crate::logic::deps::is_package_installed_or_provided(
222 &item.name,
223 &installed_set,
224 &provided_set,
225 );
226
227 if !is_installed {
228 return false;
229 }
230
231 let has_update = if upgradable_set.contains(&item.name) {
235 true
237 } else if !item.version.is_empty() {
238 let normalized_target_version =
240 item.version.split('-').next().unwrap_or(&item.version);
241 crate::logic::deps::get_installed_version(&item.name).is_ok_and(
244 |installed_version| normalized_target_version != installed_version,
245 )
246 } else {
247 false
249 };
250
251 !has_update
254 })
255 .cloned()
256 .collect();
257
258 if !installed_packages.is_empty() {
259 app.modal = crate::state::Modal::ConfirmReinstall {
262 items: installed_packages,
263 all_items: items,
264 header_chips: crate::state::modal::PreflightHeaderChips::default(),
265 };
266 return;
267 }
268
269 let has_versions = items.iter().any(|item| {
273 matches!(item.source, crate::state::Source::Official { .. }) && !item.version.is_empty()
274 });
275 let has_upgrade_available = items.iter().any(|item| {
276 matches!(item.source, crate::state::Source::Official { .. })
277 && upgradable_set.contains(&item.name)
278 });
279
280 let has_installed_required_by = items.iter().any(|item| {
282 matches!(item.source, crate::state::Source::Official { .. })
283 && crate::index::is_installed(&item.name)
284 && crate::logic::deps::has_installed_required_by(&item.name)
285 });
286
287 if has_versions && has_upgrade_available && has_installed_required_by {
288 app.modal = crate::state::Modal::ConfirmBatchUpdate {
291 items,
292 dry_run: app.dry_run,
293 };
294 return;
295 }
296
297 crate::install::start_integrated_install_all(app, &items, app.dry_run);
298 app.toast_message = Some(crate::i18n::t(app, "app.toasts.installing_skipped"));
299 app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
300 return;
301 }
302
303 if use_cache {
304 app.preflight_cancelled
306 .store(false, std::sync::atomic::Ordering::Relaxed);
307
308 let crate::logic::preflight::PreflightSummaryOutcome {
309 summary,
310 header,
311 reverse_deps_report: _,
312 } = crate::logic::preflight::compute_preflight_summary(
313 &items,
314 crate::state::PreflightAction::Install,
315 );
316 app.pending_service_plan.clear();
317
318 let item_names: std::collections::HashSet<String> =
319 items.iter().map(|i| i.name.clone()).collect();
320 let cached_deps = filter_cached_dependencies(app, &item_names);
321 let cached_files = filter_cached_files(app, &item_names);
322
323 let dependency_info = if cached_deps.is_empty() {
324 tracing::debug!(
325 "[Preflight] Cache empty, will trigger background dependency resolution for {} packages",
326 items.len()
327 );
328 Vec::new()
329 } else {
330 cached_deps
331 };
332
333 trigger_background_resolution(app, &items, &dependency_info, &cached_files);
334 create_preflight_modal_with_cache(
335 app,
336 items,
337 summary,
338 header,
339 dependency_info,
340 cached_files,
341 );
342 } else {
343 create_preflight_modal_insert_mode(app, items);
344 }
345 app.toast_message = Some(if use_cache {
346 crate::i18n::t(app, "app.toasts.preflight_opened")
347 } else {
348 "Preflight opened".to_string()
349 });
350 app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(2));
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 fn new_app() -> AppState {
363 AppState::default()
364 }
365
366 fn test_package(name: &str) -> PackageItem {
373 PackageItem {
374 name: name.to_string(),
375 version: "1.0.0".to_string(),
376 description: "Test package".to_string(),
377 source: crate::state::Source::Official {
378 repo: "extra".to_string(),
379 arch: "x86_64".to_string(),
380 },
381 popularity: None,
382 out_of_date: None,
383 orphaned: false,
384 }
385 }
386
387 #[test]
388 fn trigger_background_resolution_sets_flags_when_cache_empty() {
400 let mut app = new_app();
401 let items = vec![test_package("test-pkg")];
402
403 trigger_background_resolution(&mut app, &items, &[], &[]);
404
405 assert!(app.preflight_deps_resolving);
407 assert!(app.preflight_files_resolving);
408 assert!(app.preflight_services_resolving);
409 assert!(app.preflight_deps_items.is_some());
411 assert!(app.preflight_files_items.is_some());
412 assert!(app.preflight_services_items.is_some());
413 }
414
415 #[test]
416 fn trigger_background_resolution_skips_deps_when_cached() {
428 let mut app = new_app();
429 let items = vec![test_package("test-pkg")];
430 let cached_deps = vec![crate::state::modal::DependencyInfo {
431 name: "cached-dep".to_string(),
432 version: "1.0".to_string(),
433 status: crate::state::modal::DependencyStatus::ToInstall,
434 source: crate::state::modal::DependencySource::Official {
435 repo: "extra".to_string(),
436 },
437 required_by: vec!["test-pkg".to_string()],
438 depends_on: Vec::new(),
439 is_core: false,
440 is_system: false,
441 }];
442
443 trigger_background_resolution(&mut app, &items, &cached_deps, &[]);
444
445 assert!(!app.preflight_deps_resolving);
447 assert!(app.preflight_deps_items.is_none());
448 assert!(app.preflight_files_resolving);
450 assert!(app.preflight_files_items.is_some());
451 }
452
453 #[test]
454 fn create_preflight_modal_insert_mode_resets_cancelled() {
465 let mut app = new_app();
466 app.preflight_cancelled
467 .store(true, std::sync::atomic::Ordering::Relaxed);
468 let items = vec![test_package("test-pkg")];
469
470 create_preflight_modal_insert_mode(&mut app, items);
471
472 assert!(
474 !app.preflight_cancelled
475 .load(std::sync::atomic::Ordering::Relaxed)
476 );
477 }
478
479 #[test]
480 fn filter_cached_dependencies_returns_matching() {
492 let mut app = new_app();
493 app.install_list_deps = vec![
494 crate::state::modal::DependencyInfo {
495 name: "dep-a".to_string(),
496 version: "1.0".to_string(),
497 status: crate::state::modal::DependencyStatus::ToInstall,
498 source: crate::state::modal::DependencySource::Official {
499 repo: "extra".to_string(),
500 },
501 required_by: vec!["pkg-a".to_string()],
502 depends_on: Vec::new(),
503 is_core: false,
504 is_system: false,
505 },
506 crate::state::modal::DependencyInfo {
507 name: "dep-b".to_string(),
508 version: "1.0".to_string(),
509 status: crate::state::modal::DependencyStatus::ToInstall,
510 source: crate::state::modal::DependencySource::Official {
511 repo: "extra".to_string(),
512 },
513 required_by: vec!["pkg-b".to_string()],
514 depends_on: Vec::new(),
515 is_core: false,
516 is_system: false,
517 },
518 ];
519
520 let mut item_names = std::collections::HashSet::new();
521 item_names.insert("pkg-a".to_string());
522
523 let result = filter_cached_dependencies(&app, &item_names);
524
525 assert_eq!(result.len(), 1);
526 assert_eq!(result[0].name, "dep-a");
527 }
528
529 #[test]
530 fn filter_cached_files_returns_matching() {
542 let mut app = new_app();
543 app.install_list_files = vec![
544 crate::state::modal::PackageFileInfo {
545 name: "pkg-a".to_string(),
546 files: vec![crate::state::modal::FileChange {
547 path: "/usr/bin/a".to_string(),
548 change_type: crate::state::modal::FileChangeType::New,
549 package: "pkg-a".to_string(),
550 is_config: false,
551 predicted_pacnew: false,
552 predicted_pacsave: false,
553 }],
554 total_count: 1,
555 new_count: 1,
556 changed_count: 0,
557 removed_count: 0,
558 config_count: 0,
559 pacnew_candidates: 0,
560 pacsave_candidates: 0,
561 },
562 crate::state::modal::PackageFileInfo {
563 name: "pkg-b".to_string(),
564 files: vec![crate::state::modal::FileChange {
565 path: "/usr/bin/b".to_string(),
566 change_type: crate::state::modal::FileChangeType::New,
567 package: "pkg-b".to_string(),
568 is_config: false,
569 predicted_pacnew: false,
570 predicted_pacsave: false,
571 }],
572 total_count: 1,
573 new_count: 1,
574 changed_count: 0,
575 removed_count: 0,
576 config_count: 0,
577 pacnew_candidates: 0,
578 pacsave_candidates: 0,
579 },
580 ];
581
582 let mut item_names = std::collections::HashSet::new();
583 item_names.insert("pkg-a".to_string());
584
585 let result = filter_cached_files(&app, &item_names);
586
587 assert_eq!(result.len(), 1);
588 assert_eq!(result[0].name, "pkg-a");
589 }
590}