pacsea 0.8.2

A fast, friendly TUI for browsing and installing Arch and AUR packages with built-in news and security scanning
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
//! Command key handlers for Preflight modal.

use std::sync::{Arc, Mutex};

use crate::state::{AppState, PackageItem};

use crate::events::preflight::modal::close_preflight_modal;

/// What: Handle F key - sync file database.
///
/// Inputs:
/// - `app`: Mutable application state
///
/// Output:
/// - Always returns `false` to continue event processing.
///
/// Details:
/// - Runs the sync operation in a background thread to avoid blocking the UI.
/// - If sync requires root privileges, shows password prompt and uses integrated executor.
pub(super) fn handle_f_key(app: &mut AppState) -> bool {
    if let crate::state::Modal::Preflight { tab, .. } = &app.modal {
        if *tab != crate::state::PreflightTab::Files {
            return false;
        }
    } else {
        return false;
    }

    // Show initial message that sync is starting
    app.toast_message = Some("File database sync starting...".to_string());
    app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(5));

    // Run sync in background thread to avoid blocking the UI
    // Use catch_unwind to prevent panics from crashing the TUI
    // Store result in Arc<Mutex<Option<...>>> so we can check it in tick handler
    let sync_result: crate::state::app_state::FileSyncResult = Arc::new(Mutex::new(None));
    let sync_result_clone = Arc::clone(&sync_result);

    std::thread::spawn(move || {
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            // Use the new ensure_file_db_synced function with force=true
            // This will attempt to sync regardless of timestamp
            let sync_result_inner = crate::logic::files::ensure_file_db_synced(true, 7);
            match sync_result_inner {
                Ok(synced) => {
                    if synced {
                        tracing::info!("File database sync completed successfully");
                    } else {
                        tracing::info!("File database is already fresh");
                    }
                    if let Ok(mut guard) = sync_result_clone.lock() {
                        *guard = Some(Ok(synced));
                    }
                }
                Err(e) => {
                    // Sync failed (likely requires root), show password prompt
                    tracing::warn!("File database sync failed: {}, will prompt for password", e);
                    if let Ok(mut guard) = sync_result_clone.lock() {
                        *guard = Some(Err(e));
                    }
                }
            }
        }));

        if let Err(panic_info) = result {
            tracing::error!("File database sync thread panicked: {:?}", panic_info);
        }
    });

    // Store the sync result Arc in AppState so we can check it in tick handler
    app.pending_file_sync_result = Some(sync_result);

    // Clear file_info to trigger re-resolution after sync completes
    if let crate::state::Modal::Preflight {
        file_info,
        file_selected,
        ..
    } = &mut app.modal
    {
        *file_info = Vec::new();
        *file_selected = 0;
    }

    false
}

/// What: Handle S key - open scan configuration modal.
///
/// Inputs:
/// - `app`: Mutable application state
///
/// Output:
/// - Always returns `false`.
pub(super) fn handle_s_key(app: &mut AppState) -> bool {
    // Build AUR package name list to scan
    let names = if let crate::state::Modal::Preflight { items, .. } = &app.modal {
        items
            .iter()
            .filter(|p| matches!(p.source, crate::state::Source::Aur))
            .map(|p| p.name.clone())
            .collect::<Vec<_>>()
    } else {
        return false;
    };

    if names.is_empty() {
        app.modal = crate::state::Modal::Alert {
            message: "No AUR packages selected to scan.\nAdd AUR packages to scan, then press 's'."
                .into(),
        };
    } else {
        app.pending_install_names = Some(names);
        // Open Scan Configuration modal initialized from settings.conf
        let prefs = crate::theme::settings();
        // Store current Preflight modal state before opening ScanConfig
        app.previous_modal = Some(app.modal.clone());
        app.modal = crate::state::Modal::ScanConfig {
            do_clamav: prefs.scan_do_clamav,
            do_trivy: prefs.scan_do_trivy,
            do_semgrep: prefs.scan_do_semgrep,
            do_shellcheck: prefs.scan_do_shellcheck,
            do_virustotal: prefs.scan_do_virustotal,
            do_custom: prefs.scan_do_custom,
            do_sleuth: prefs.scan_do_sleuth,
            cursor: 0,
        };
    }
    false
}

/// What: Handle d key - toggle dry-run mode.
///
/// Inputs:
/// - `app`: Mutable application state
///
/// Output:
/// - Always returns `false`.
pub(super) fn handle_dry_run_key(app: &mut AppState) -> bool {
    app.dry_run = !app.dry_run;
    let toast_key = if app.dry_run {
        "app.toasts.dry_run_enabled"
    } else {
        "app.toasts.dry_run_disabled"
    };
    app.toast_message = Some(crate::i18n::t(app, toast_key));
    false
}

/// What: Handle m key - cycle cascade mode.
///
/// Inputs:
/// - `app`: Mutable application state
///
/// Output:
/// - Always returns `false`.
pub(super) fn handle_m_key(app: &mut AppState) -> bool {
    let next_mode_opt = if let crate::state::Modal::Preflight {
        action: crate::state::PreflightAction::Remove,
        cascade_mode,
        ..
    } = &mut app.modal
    {
        let next_mode = cascade_mode.next();
        *cascade_mode = next_mode;
        Some(next_mode)
    } else {
        None
    };

    if let Some(next_mode) = next_mode_opt {
        app.remove_cascade_mode = next_mode;
        app.toast_message = Some(format!(
            "Cascade mode set to {} ({})",
            next_mode.flag(),
            next_mode.description()
        ));
        app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(4));
    }
    false
}

/// What: Extract install targets from preflight modal.
///
/// Inputs:
/// - `items`: Items from preflight modal
/// - `selected_optdepends`: Selected optional dependencies
///
/// Output: Packages to install including optional dependencies.
///
/// Details: Adds selected optional dependencies to the install list.
fn extract_install_targets(
    items: &[PackageItem],
    selected_optdepends: &std::collections::HashMap<String, std::collections::HashSet<String>>,
) -> Vec<PackageItem> {
    let mut packages = items.to_vec();
    // Add selected optional dependencies as additional packages to install
    for optdeps in selected_optdepends.values() {
        for optdep in optdeps {
            let optdep_pkg_name = crate::logic::sandbox::extract_package_name(optdep);
            if !packages.iter().any(|p| p.name == optdep_pkg_name) {
                packages.push(PackageItem {
                    name: optdep_pkg_name,
                    version: String::new(),
                    description: String::new(),
                    source: crate::state::Source::Official {
                        repo: String::new(),
                        arch: String::new(),
                    },
                    popularity: None,
                    out_of_date: None,
                    orphaned: false,
                });
            }
        }
    }
    packages
}

/// What: Handle proceed action for install targets.
///
/// Inputs:
/// - `app`: Mutable application state
/// - `packages`: Packages to install
/// - `header_chips`: Header chip metrics
///
/// Output: `false` to keep TUI open.
///
/// Details: Checks for reinstalls first, then batch updates (only if update available), handles password prompt if needed, or starts execution.
pub(super) fn handle_proceed_install(
    app: &mut AppState,
    packages: Vec<PackageItem>,
    header_chips: crate::state::modal::PreflightHeaderChips,
) -> bool {
    if crate::events::install::try_open_warn_aur_repo_duplicate_modal(
        app,
        &packages,
        header_chips.clone(),
    ) {
        return false;
    }
    // First, check if we're installing packages that are already installed (reinstall scenario)
    // This check happens BEFORE password prompt
    // BUT exclude packages that have updates available (those should go through normal update flow)
    let installed_set = crate::logic::deps::get_installed_packages();
    let provided_set = crate::logic::deps::get_provided_packages(&installed_set);
    let upgradable_set = crate::logic::deps::get_upgradable_packages();

    let installed_packages: Vec<crate::state::PackageItem> = packages
        .iter()
        .filter(|item| {
            // Check if package is installed or provided by an installed package
            let is_installed = crate::logic::deps::is_package_installed_or_provided(
                &item.name,
                &installed_set,
                &provided_set,
            );

            if !is_installed {
                return false;
            }

            // Check if package has an update available
            // For official packages: check if it's in upgradable_set OR version differs from installed
            // For AUR packages: check if target version is different from installed version
            let has_update = if upgradable_set.contains(&item.name) {
                // Package is in upgradable set (pacman -Qu)
                true
            } else if !item.version.is_empty() {
                // Normalize target version by removing revision suffix (same as installed version normalization)
                let normalized_target_version =
                    item.version.split('-').next().unwrap_or(&item.version);
                // Compare normalized target version with normalized installed version
                // This works for both official and AUR packages
                crate::logic::deps::get_installed_version(&item.name)
                    .is_ok_and(|installed_version| normalized_target_version != installed_version)
            } else {
                // No version info available, no update
                false
            };

            // Only show reinstall confirmation if installed AND no update available
            // If update is available, it should go through normal update flow
            !has_update
        })
        .cloned()
        .collect();

    if !installed_packages.is_empty() {
        // Show reinstall confirmation modal (before password prompt)
        // Store both installed packages (for display) and all packages (for installation)
        app.modal = crate::state::Modal::ConfirmReinstall {
            items: installed_packages,
            all_items: packages,
            header_chips,
        };
        return false; // Don't close modal yet, wait for confirmation
    }

    // Check if this is a batch update scenario requiring confirmation
    // Only show if there's actually an update available (package is upgradable)
    // AND the package has installed packages in its "Required By" field (dependency risk)
    let upgradable_set = crate::logic::deps::get_upgradable_packages();
    let has_versions = packages.iter().any(|item| {
        matches!(item.source, crate::state::Source::Official { .. }) && !item.version.is_empty()
    });
    let has_upgrade_available = packages.iter().any(|item| {
        matches!(item.source, crate::state::Source::Official { .. })
            && upgradable_set.contains(&item.name)
    });

    // Only show warning if package has installed packages in "Required By" (dependency risk)
    let has_installed_required_by = packages.iter().any(|item| {
        matches!(item.source, crate::state::Source::Official { .. })
            && crate::index::is_installed(&item.name)
            && crate::logic::deps::has_installed_required_by(&item.name)
    });

    if has_versions && has_upgrade_available && has_installed_required_by {
        // Show confirmation modal for batch updates (only if update is actually available
        // AND package has installed dependents that could be affected)
        app.modal = crate::state::Modal::ConfirmBatchUpdate {
            items: packages,
            dry_run: app.dry_run,
        };
        return false; // Don't close modal yet, wait for confirmation
    }

    // Check faillock status before proceeding
    let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
    if let Some(lockout_msg) = crate::logic::faillock::get_lockout_message_if_locked(&username, app)
    {
        // User is locked out - show warning
        app.modal = crate::state::Modal::Alert {
            message: lockout_msg,
        };
        return false;
    }

    let settings = crate::theme::settings();
    if crate::logic::password::should_use_interactive_auth_handoff(&settings) {
        match crate::events::try_interactive_auth_handoff() {
            Ok(true) => super::start_execution(
                app,
                &packages,
                crate::state::PreflightAction::Install,
                header_chips,
                None,
            ),
            Ok(false) => {
                app.modal = crate::state::Modal::Alert {
                    message: crate::i18n::t(app, "app.errors.authentication_failed"),
                };
            }
            Err(e) => {
                app.modal = crate::state::Modal::Alert { message: e };
            }
        }
    } else if crate::logic::password::resolve_auth_mode(&settings)
        == crate::logic::privilege::AuthMode::PasswordlessOnly
        && crate::logic::password::should_use_passwordless_sudo(&settings)
    {
        super::start_execution(
            app,
            &packages,
            crate::state::PreflightAction::Install,
            header_chips,
            None,
        );
    } else {
        app.modal = crate::state::Modal::PasswordPrompt {
            purpose: crate::state::modal::PasswordPurpose::Install,
            items: packages,
            input: crate::state::SecureString::default(),
            cursor: 0,
            error: None,
        };
        app.pending_exec_header_chips = Some(header_chips);
    }
    false
}

/// What: Handle proceed action for remove targets.
///
/// Inputs:
/// - `app`: Mutable application state
/// - `items`: Items to remove
/// - `mode`: Cascade removal mode
/// - `header_chips`: Header chip metrics
///
/// Output: `false` to keep TUI open.
///
/// Details: Handles password prompt if needed, or starts execution.
pub(super) fn handle_proceed_remove(
    app: &mut AppState,
    items: Vec<PackageItem>,
    mode: crate::state::modal::CascadeMode,
    header_chips: crate::state::modal::PreflightHeaderChips,
) -> bool {
    // Store cascade mode for executor (needed in both password and non-password paths)
    app.remove_cascade_mode = mode;

    // Remove operations always need sudo (pacman -R requires sudo regardless of package source)
    // Check faillock status before showing password prompt
    let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
    if let Some(lockout_msg) = crate::logic::faillock::get_lockout_message_if_locked(&username, app)
    {
        // User is locked out - show warning and don't show password prompt
        app.modal = crate::state::Modal::Alert {
            message: lockout_msg,
        };
        return false;
    }
    let settings = crate::theme::settings();
    if crate::logic::password::should_use_interactive_auth_handoff(&settings) {
        match crate::events::try_interactive_auth_handoff() {
            Ok(true) => super::start_execution(
                app,
                &items,
                crate::state::PreflightAction::Remove,
                header_chips,
                None,
            ),
            Ok(false) => {
                app.modal = crate::state::Modal::Alert {
                    message: crate::i18n::t(app, "app.errors.authentication_failed"),
                };
            }
            Err(e) => {
                app.modal = crate::state::Modal::Alert { message: e };
            }
        }
    } else {
        app.modal = crate::state::Modal::PasswordPrompt {
            purpose: crate::state::modal::PasswordPurpose::Remove,
            items,
            input: crate::state::SecureString::default(),
            cursor: 0,
            error: None,
        };
        app.pending_exec_header_chips = Some(header_chips);
    }
    false
}

/// What: Handle p key - proceed with install/remove.
///
/// Inputs:
/// - `app`: Mutable application state
///
/// Output:
/// - Always returns `false` to continue event processing.
///
/// Details:
/// - Closes the modal if install/remove is triggered, but TUI remains open.
#[allow(clippy::too_many_lines)] // Function handles complex preflight proceed logic (function has 208 lines)
pub(super) fn handle_p_key(app: &mut AppState) -> bool {
    let new_summary: Option<Vec<crate::state::modal::ReverseRootSummary>> = None;
    let mut blocked_dep_count: Option<usize> = None;
    let mut removal_names: Option<Vec<String>> = None;
    let mut removal_mode: Option<crate::state::modal::CascadeMode> = None;
    let mut install_targets: Option<Vec<PackageItem>> = None;
    let mut service_info_for_plan: Option<Vec<crate::state::modal::ServiceImpact>> = None;
    let mut deps_not_resolved_message: Option<String> = None;

    // Scope for borrowing app.modal
    {
        if let crate::state::Modal::Preflight {
            action,
            items,
            dependency_info,
            cascade_mode,
            selected_optdepends,
            service_info,
            ..
        } = &mut app.modal
        {
            match action {
                crate::state::PreflightAction::Install => {
                    let packages = extract_install_targets(&*items, selected_optdepends);
                    install_targets = Some(packages);
                }
                crate::state::PreflightAction::Remove => {
                    // If dependency_info is empty, dependencies haven't been resolved yet.
                    // Show a warning but allow removal to proceed (don't block).
                    if dependency_info.is_empty() {
                        // Show warning if cascade mode is Basic (might have dependents we don't know about)
                        if !cascade_mode.allows_dependents() {
                            deps_not_resolved_message = Some(
                                "Warning: Dependencies not resolved yet. Package may have dependents. Switch to Dependencies tab to check."
                                    .to_string(),
                            );
                        }
                        // Always allow removal to proceed, even if dependencies aren't resolved
                        removal_names = Some(items.iter().map(|p| p.name.clone()).collect());
                        removal_mode = Some(*cascade_mode);
                    } else if cascade_mode.allows_dependents() {
                        // Cascade mode allows dependents, proceed with removal
                        removal_names = Some(items.iter().map(|p| p.name.clone()).collect());
                        removal_mode = Some(*cascade_mode);
                    } else {
                        // Dependencies are resolved, cascade mode is Basic, and there are dependents
                        // Block removal since Basic mode doesn't allow removing packages with dependents
                        blocked_dep_count = Some(dependency_info.len());
                    }
                }
                crate::state::PreflightAction::Downgrade => {
                    // For downgrade, we don't need to check dependencies
                    // Downgrade tool handles its own logic
                    // Just allow downgrade to proceed - handled separately below
                }
            }

            if !service_info.is_empty() {
                service_info_for_plan = Some(service_info.clone());
            }
        }
    }

    // Set toast message if dependencies not resolved
    if let Some(msg) = deps_not_resolved_message {
        app.toast_message = Some(msg);
        app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(5));
    }

    if let Some(summary) = new_summary {
        app.remove_preflight_summary = summary;
    }

    if let Some(plan) = service_info_for_plan {
        app.pending_service_plan = plan;
    } else {
        app.pending_service_plan.clear();
    }

    if let Some(packages) = install_targets {
        // Get header_chips and service_info before closing modal
        let header_chips = if let crate::state::Modal::Preflight { header_chips, .. } = &app.modal {
            header_chips.clone()
        } else {
            crate::state::modal::PreflightHeaderChips::default()
        };
        let service_info = if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
            service_info.clone()
        } else {
            Vec::new()
        };

        // Close preflight modal
        crate::events::preflight::modal::close_preflight_modal(app, &service_info);

        return handle_proceed_install(app, packages, header_chips);
    } else if let Some(names) = removal_names {
        let mode = removal_mode.unwrap_or(crate::state::modal::CascadeMode::Basic);

        // Get header_chips and service_info before closing modal
        let header_chips = if let crate::state::Modal::Preflight { header_chips, .. } = &app.modal {
            header_chips.clone()
        } else {
            crate::state::modal::PreflightHeaderChips::default()
        };
        let service_info = if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
            service_info.clone()
        } else {
            Vec::new()
        };

        // Get items for removal
        let items = if let crate::state::Modal::Preflight { items, .. } = &app.modal {
            items
                .iter()
                .filter(|p| names.contains(&p.name))
                .cloned()
                .collect()
        } else {
            Vec::new()
        };

        // Close preflight modal
        crate::events::preflight::modal::close_preflight_modal(app, &service_info);

        return handle_proceed_remove(app, items, mode, header_chips);
    }

    // Check if this is a downgrade action
    let is_downgrade = if let crate::state::Modal::Preflight { action, .. } = &app.modal {
        matches!(action, crate::state::PreflightAction::Downgrade)
    } else {
        false
    };

    if is_downgrade {
        // Get items, action, header_chips, and cascade_mode before closing modal
        let (items, _action, header_chips, cascade_mode) = if let crate::state::Modal::Preflight {
            action,
            items,
            header_chips,
            cascade_mode,
            ..
        } = &app.modal
        {
            (items.clone(), *action, header_chips.clone(), *cascade_mode)
        } else {
            return false;
        };

        // Downgrade operations always need sudo (downgrade tool requires sudo)
        // Always show password prompt - user can press Enter if passwordless sudo is configured
        // Store cascade mode for consistency (though downgrade doesn't use it)
        app.remove_cascade_mode = cascade_mode;

        // Get service_info before closing modal
        let service_info = if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
            service_info.clone()
        } else {
            Vec::new()
        };

        // Close preflight modal
        crate::events::preflight::modal::close_preflight_modal(app, &service_info);

        // Check faillock status before showing password prompt for downgrade
        let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
        if let Some(lockout_msg) =
            crate::logic::faillock::get_lockout_message_if_locked(&username, app)
        {
            // User is locked out - show warning and don't show password prompt
            app.modal = crate::state::Modal::Alert {
                message: lockout_msg,
            };
            return false;
        }
        let settings = crate::theme::settings();
        if crate::logic::password::should_use_interactive_auth_handoff(&settings) {
            crate::events::spawn_downgrade_in_terminal(app, &items);
        } else {
            app.modal = crate::state::Modal::PasswordPrompt {
                purpose: crate::state::modal::PasswordPurpose::Downgrade,
                items,
                input: crate::state::SecureString::default(),
                cursor: 0,
                error: None,
            };
            app.pending_exec_header_chips = Some(header_chips);
        }
        return false;
    }

    if let Some(count) = blocked_dep_count {
        let root_list: Vec<String> = app
            .remove_preflight_summary
            .iter()
            .filter(|summary| summary.total_dependents > 0)
            .map(|summary| summary.package.clone())
            .collect();
        let subject = if root_list.is_empty() {
            "the selected packages".to_string()
        } else {
            root_list.join(", ")
        };
        app.toast_message = Some(format!(
            "Removal blocked: {count} dependent package(s) rely on {subject}. Enable cascade removal to proceed."
        ));
        app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(6));
    }

    false
}

/// What: Handle c key - snapshot placeholder.
///
/// Inputs:
/// - `app`: Mutable application state
///
/// Output:
/// - Always returns `false`.
pub(super) fn handle_c_key(app: &mut AppState) -> bool {
    // TODO: implement Logic for creating a snapshot
    app.toast_message = Some(crate::i18n::t(app, "app.toasts.snapshot_placeholder"));
    false
}

/// What: Handle q key - close modal.
///
/// Inputs:
/// - `app`: Mutable application state
///
/// Output:
/// - Always returns `false` to continue event processing.
///
/// Details:
/// - Closes the modal but keeps the TUI open.
pub(super) fn handle_q_key(app: &mut AppState) -> bool {
    let service_info = if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
        service_info.clone()
    } else {
        Vec::new()
    };
    close_preflight_modal(app, &service_info);
    // Return false to keep TUI open - modal is closed but app continues
    false
}

/// What: Handle ? key - show help.
///
/// Inputs:
/// - `app`: Mutable application state
///
/// Output:
/// - Always returns `false`.
pub(super) fn handle_help_key(app: &mut AppState) -> bool {
    let help_message = if let crate::state::Modal::Preflight { tab, .. } = &app.modal {
        if *tab == crate::state::PreflightTab::Deps {
            crate::i18n::t(app, "app.modals.preflight.help.deps_tab")
        } else {
            crate::i18n::t(app, "app.modals.preflight.help.general")
        }
    } else {
        return false;
    };

    app.previous_modal = Some(app.modal.clone());
    app.modal = crate::state::Modal::Alert {
        message: help_message,
    };
    false
}