Skip to main content

pacsea/state/
modal.rs

1//! Modal dialog state for the UI.
2
3use crate::sources::VoteAction;
4use crate::state::types::{OptionalDepRow, PackageItem, RepositoryModalRow, Source};
5use std::collections::HashSet;
6
7/// What: Enumerates the high-level operations represented in the preflight
8/// workflow.
9///
10/// - Input: Selected by callers when presenting confirmation or preflight
11///   dialogs.
12/// - Output: Indicates whether the UI should prepare for an install or remove
13///   transaction.
14/// - Details: Drives copy, button labels, and logging in the preflight and
15///   execution flows.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PreflightAction {
18    /// Install packages action.
19    Install,
20    /// Remove packages action.
21    Remove,
22    /// Downgrade packages action.
23    Downgrade,
24}
25
26/// What: Purpose for password prompt.
27///
28/// Inputs:
29/// - Set when showing password prompt modal.
30///
31/// Output:
32/// - Used to customize prompt message and context.
33///
34/// Details:
35/// - Indicates which operation requires sudo authentication.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum PasswordPurpose {
38    /// Installing packages.
39    Install,
40    /// Removing packages.
41    Remove,
42    /// Updating system.
43    Update,
44    /// Downgrading packages.
45    Downgrade,
46    /// Syncing file database.
47    FileSync,
48    /// Applying custom repository configuration (`repos.conf` → managed drop-in, keys).
49    RepoApply,
50    /// Migrating foreign packages to sync after repository overlap detection.
51    RepoForeignMigrate,
52}
53
54/// What: Remembers which repository section was fully applied so a follow-up overlap scan can run.
55///
56/// Inputs:
57/// - Set in [`crate::events::modals::repositories::enter_repo_apply`] when a full apply is queued.
58///
59/// Output:
60/// - Consumed after successful `PreflightExec` when the user dismisses the log (Enter).
61///
62/// Details:
63/// - `repo_section` is normalized lowercase for `pacman -Sl`.
64/// - `pre_apply_foreign_snapshot` is `pacman -Qm` captured when apply was **queued** (before sudo/sync).
65///   After the repo is enabled, pacman may stop classifying matching installs as foreign; the snapshot
66///   preserves the pre-apply set for overlap detection without disabling repositories globally.
67#[derive(Debug, Clone)]
68pub struct RepoOverlapApplyPending {
69    /// Pacman repository name from the applied `[[repo]]` row.
70    pub repo_section: String,
71    /// Foreign packages from `pacman -Qm` before privileged apply commands ran.
72    ///
73    /// `None` if the snapshot failed (overlap analysis falls back to live `-Qm` at completion).
74    pub pre_apply_foreign_snapshot: Option<Vec<(String, String)>>,
75}
76
77/// What: Restore the Repositories modal after a successful repo apply preflight flow ends.
78///
79/// Inputs:
80/// - Set when queuing [`PasswordPurpose::RepoApply`] (full apply, not key-only refresh).
81///
82/// Output:
83/// - Consumed when the UI returns to `Modal::None` and rescans pacman rows.
84///
85/// Details:
86/// - `section_name` selects the same `[[repo]]` row after refresh; `scroll` is re-clamped to the new row count.
87#[derive(Debug, Clone)]
88pub struct RepositoriesModalResume {
89    /// Pacman `[repo]` section name from the row that was focused when apply started.
90    pub section_name: String,
91    /// First visible list index to restore when possible.
92    pub scroll: u16,
93}
94
95/// What: Step state for the post-apply foreign vs sync overlap workflow.
96///
97/// Inputs:
98/// - Owned by [`Modal::ForeignRepoOverlap`].
99///
100/// Output:
101/// - Drives which screen and scroll position the renderer shows.
102///
103/// Details:
104/// - `WarnAck` uses two substeps before package selection; `Select` supports multi-toggle migration targets.
105#[derive(Debug, Clone)]
106pub enum ForeignRepoOverlapPhase {
107    /// Red warning screens (`step` 0 then 1) before package selection.
108    WarnAck {
109        /// `0` = primary warning, `1` = secondary acknowledgment.
110        step: u8,
111        /// Vertical scroll for the overlap list on warning steps 0 and 1.
112        list_scroll: u16,
113    },
114    /// Multi-select which overlapping packages to migrate (Space toggles).
115    Select {
116        /// Focused row index into `entries`.
117        cursor: usize,
118        /// Scroll for the selectable list.
119        list_scroll: u16,
120        /// Selected package names to migrate.
121        selected: HashSet<String>,
122    },
123    /// Final confirmation before the password prompt (Esc returns to [`Self::Select`]).
124    FinalConfirm {
125        /// Cursor to restore when backing out.
126        select_cursor: usize,
127        /// Scroll to restore when backing out.
128        select_scroll: u16,
129        /// Packages slated for `pacman -Rns` / `pacman -S`.
130        selected: HashSet<String>,
131    },
132}
133
134/// What: Identifies which tab within the preflight modal is active.
135///
136/// - Input: Set by UI event handlers responding to user navigation.
137/// - Output: Informs the renderer which data set to display (summary, deps,
138///   files, etc.).
139/// - Details: Enables multi-step review of package operations without losing
140///   context between tabs.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum PreflightTab {
143    /// Summary tab showing overview of package operations.
144    Summary,
145    /// Dependencies tab showing dependency analysis.
146    Deps,
147    /// Files tab showing file change analysis.
148    Files,
149    /// Services tab showing service impact analysis.
150    Services,
151    /// Sandbox tab showing sandbox analysis.
152    Sandbox,
153}
154
155/// Removal cascade strategy for `pacman` operations.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum CascadeMode {
158    /// `pacman -R` – remove targets only.
159    Basic,
160    /// `pacman -Rs` – remove targets and orphaned dependencies.
161    Cascade,
162    /// `pacman -Rns` – cascade removal and prune configuration files.
163    CascadeWithConfigs,
164}
165
166/// What: Step identifier for the guided AUR SSH setup workflow.
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum SshSetupStep {
169    /// Intro/instructions step before executing the setup flow.
170    Intro,
171    /// Confirmation step shown when an existing host block needs overwrite approval.
172    ConfirmOverwrite,
173    /// Step where user copies key, applies it on AUR, then confirms for validation.
174    ApplyKeyOnAur,
175    /// Result step containing final status lines.
176    Result,
177}
178
179/// What: Selectable startup setup tasks presented in the first-run setup selector.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
181pub enum StartupSetupTask {
182    /// Configure startup Arch/feeds news preferences.
183    ArchNews,
184    /// Configure SSH for AUR vote/unvote operations.
185    SshAurSetup,
186    /// Review and install missing optional dependencies.
187    OptionalDepsMissing,
188    /// Optional wizard: extend `sudo` credential cache for long installs/updates (`sudoers`).
189    SudoTimestampSetup,
190    /// Optional wizard: configure `doas` persist guidance for long installs/updates.
191    DoasPersistSetup,
192    /// Configure aur-sleuth integration.
193    AurSleuthSetup,
194    /// Configure `VirusTotal` API key.
195    VirusTotalSetup,
196}
197
198/// What: User-selected `sudo` credential cache duration in the optional setup wizard.
199///
200/// Inputs:
201/// - Chosen by the user in [`Modal::SudoTimestampSetup`].
202///
203/// Output:
204/// - Maps to `timestamp_timeout` minutes in `sudoers`, or `-1` for no expiry in the session.
205///
206/// Details:
207/// - See `sudoers(5)` `timestamp_timeout`. This only affects policy once applied on the system.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum SudoTimestampChoice {
210    /// Cache sudo credentials for ten minutes after a successful password prompt.
211    TenMinutes,
212    /// Cache sudo credentials for thirty minutes after a successful password prompt.
213    ThirtyMinutes,
214    /// Use `timestamp_timeout=-1` (do not expire until `sudo -k` or reboot, per sudo policy).
215    Infinity,
216}
217
218/// What: Active phase of the sudo timestamp setup wizard.
219///
220/// Inputs:
221/// - Driven by key events in the sudo timestamp setup handler.
222///
223/// Output:
224/// - Tells the renderer whether to show the option list or the instruction pane.
225///
226/// Details:
227/// - The select phase uses [`SudoTimestampSetupModalState::select_cursor`]; instructions carry their own scroll.
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub enum SudoTimestampSetupPhase {
230    /// User is picking a recommended `timestamp_timeout` or skipping.
231    Select,
232    /// User is reading copy-paste / terminal instructions for the chosen option.
233    Instructions {
234        /// Selected duration mapping.
235        choice: SudoTimestampChoice,
236        /// Vertical scroll offset in lines for long instruction text.
237        scroll: u16,
238    },
239}
240
241/// What: Stateful fields for [`Modal::SudoTimestampSetup`].
242///
243/// Inputs:
244/// - Constructed when opening the wizard from optional deps or startup setup.
245///
246/// Output:
247/// - Updated by the sudo timestamp setup key handler and read by the renderer.
248///
249/// Details:
250/// - `select_cursor` is kept when switching to instructions so Esc returns to the same row.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub struct SudoTimestampSetupModalState {
253    /// Current wizard phase.
254    pub phase: SudoTimestampSetupPhase,
255    /// Row index in the select phase (`0..SUDO_TIMESTAMP_SELECT_ROWS`).
256    pub select_cursor: usize,
257}
258
259/// Row count for [`SudoTimestampSetupModalState::select_cursor`] (10m, 30m, infinity, skip).
260pub const SUDO_TIMESTAMP_SELECT_ROWS: usize = 4;
261
262/// What: Preset scope for suggested `doas.conf` persist snippets.
263///
264/// Inputs:
265/// - Selected by the user in [`Modal::DoasPersistSetup`].
266///
267/// Output:
268/// - Controls whether generated guidance targets group-level or user-level `permit persist` entries.
269///
270/// Details:
271/// - These options only generate guidance text; the app does not write `/etc/doas.conf` automatically.
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub enum DoasPersistChoice {
274    /// Suggest a group-scoped rule (`:wheel`) for systems using wheel-based admin policy.
275    WheelScoped,
276    /// Suggest a user-scoped rule (`$USER`) for least-privilege single-user setups.
277    UserScoped,
278    /// Skip setup and close the wizard.
279    Skip,
280}
281
282/// What: Active phase of the doas persist setup wizard.
283///
284/// Inputs:
285/// - Driven by key events in the doas persist setup handler.
286///
287/// Output:
288/// - Tells the renderer whether to show the option list or checklist instructions.
289///
290/// Details:
291/// - The select phase uses [`DoasPersistSetupModalState::select_cursor`]; instructions carry their own scroll.
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum DoasPersistSetupPhase {
294    /// User is picking a recommended `doas.conf` persist snippet profile or skipping.
295    Select,
296    /// User is reading copy/paste checklist instructions for the chosen option.
297    Instructions {
298        /// Selected snippet profile.
299        choice: DoasPersistChoice,
300        /// Vertical scroll offset in lines for long instruction text.
301        scroll: u16,
302    },
303}
304
305/// What: Stateful fields for [`Modal::DoasPersistSetup`].
306///
307/// Inputs:
308/// - Constructed when opening the wizard from optional deps or startup setup.
309///
310/// Output:
311/// - Updated by the doas persist setup key handler and read by the renderer.
312///
313/// Details:
314/// - `select_cursor` is preserved so Esc from instructions returns to the same row.
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub struct DoasPersistSetupModalState {
317    /// Current wizard phase.
318    pub phase: DoasPersistSetupPhase,
319    /// Row index in the select phase (`0..DOAS_PERSIST_SELECT_ROWS`).
320    pub select_cursor: usize,
321}
322
323/// Row count for [`DoasPersistSetupModalState::select_cursor`] (wheel, user, skip).
324pub const DOAS_PERSIST_SELECT_ROWS: usize = 3;
325
326impl CascadeMode {
327    /// Return the `pacman` flag sequence corresponding to this `CascadeMode`.
328    #[must_use]
329    pub const fn flag(self) -> &'static str {
330        match self {
331            Self::Basic => "-R",
332            Self::Cascade => "-Rs",
333            Self::CascadeWithConfigs => "-Rns",
334        }
335    }
336
337    /// Short text describing the effect of this `CascadeMode`.
338    #[must_use]
339    pub const fn description(self) -> &'static str {
340        match self {
341            Self::Basic => "targets only",
342            Self::Cascade => "remove dependents",
343            Self::CascadeWithConfigs => "dependents + configs",
344        }
345    }
346
347    /// Whether this `CascadeMode` allows removal when dependents exist.
348    #[must_use]
349    pub const fn allows_dependents(self) -> bool {
350        !matches!(self, Self::Basic)
351    }
352
353    /// Cycle to the next `CascadeMode`.
354    #[must_use]
355    pub const fn next(self) -> Self {
356        match self {
357            Self::Basic => Self::Cascade,
358            Self::Cascade => Self::CascadeWithConfigs,
359            Self::CascadeWithConfigs => Self::Basic,
360        }
361    }
362}
363
364/// Dependency information for a package in the preflight dependency view.
365#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
366pub struct DependencyInfo {
367    /// Package name.
368    pub name: String,
369    /// Required version constraint (e.g., ">=1.2.3" or "1.2.3").
370    pub version: String,
371    /// Current status of this dependency.
372    pub status: DependencyStatus,
373    /// Source repository or origin.
374    pub source: DependencySource,
375    /// Packages that require this dependency.
376    pub required_by: Vec<String>,
377    /// Packages that this dependency depends on (transitive deps).
378    pub depends_on: Vec<String>,
379    /// Whether this is a core repository package.
380    pub is_core: bool,
381    /// Whether this is a critical system package.
382    pub is_system: bool,
383}
384
385/// Summary statistics for reverse dependency analysis of removal targets.
386#[derive(Clone, Debug, Default)]
387pub struct ReverseRootSummary {
388    /// Package slated for removal.
389    pub package: String,
390    /// Number of packages that directly depend on this package (depth 1).
391    pub direct_dependents: usize,
392    /// Number of packages that depend on this package through other packages (depth ≥ 2).
393    pub transitive_dependents: usize,
394    /// Total number of dependents (direct + transitive).
395    pub total_dependents: usize,
396}
397
398/// Status of a dependency relative to the current system state.
399#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
400pub enum DependencyStatus {
401    /// Already installed and version matches requirement.
402    Installed {
403        /// Installed version of the package.
404        version: String,
405    },
406    /// Not installed, needs to be installed.
407    ToInstall,
408    /// Installed but outdated, needs upgrade.
409    ToUpgrade {
410        /// Current installed version.
411        current: String,
412        /// Required version for upgrade.
413        required: String,
414    },
415    /// Conflicts with existing packages.
416    Conflict {
417        /// Reason for the conflict.
418        reason: String,
419    },
420    /// Cannot be found in configured repositories or AUR.
421    Missing,
422}
423
424/// Source of a dependency package.
425#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
426pub enum DependencySource {
427    /// Official repository package.
428    Official {
429        /// Repository name (e.g., "core", "extra").
430        repo: String,
431    },
432    /// AUR package.
433    Aur,
434    /// Local package (not in repos).
435    Local,
436}
437
438/// What: Restart preference applied to an impacted `systemd` service.
439///
440/// Inputs:
441/// - Assigned automatically from heuristics or by user toggles within the Services tab.
442///
443/// Output:
444/// - Guides post-transaction actions responsible for restarting (or deferring) service units.
445///
446/// Details:
447/// - Provides a simplified binary choice: restart immediately or defer for later manual handling.
448#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
449pub enum ServiceRestartDecision {
450    /// Explicitly restart the unit after the transaction.
451    Restart,
452    /// Defer restarting the unit.
453    Defer,
454}
455
456/// What: Aggregated information about a `systemd` unit affected by the pending operation.
457///
458/// Inputs:
459/// - Populated by the service impact resolver which correlates package file lists and
460///   `systemctl` state.
461///
462/// Output:
463/// - Supplies UI rendering with package provenance, restart status, and the current user choice.
464///
465/// Details:
466/// - `providers` lists packages that ship the unit. `is_active` flags if the unit currently runs.
467///   `needs_restart` indicates detected impact. `recommended_decision` records the resolver default,
468///   and `restart_decision` reflects any user override applied in the UI.
469#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
470pub struct ServiceImpact {
471    /// Fully-qualified unit name (e.g., `sshd.service`).
472    pub unit_name: String,
473    /// Packages contributing this unit.
474    pub providers: Vec<String>,
475    /// Whether the unit is active (`systemctl is-active == active`).
476    pub is_active: bool,
477    /// Whether a restart is recommended because files/configs will change.
478    pub needs_restart: bool,
479    /// Resolver-suggested action prior to user adjustments.
480    pub recommended_decision: ServiceRestartDecision,
481    /// Restart decision currently applied to the unit (may differ from recommendation).
482    pub restart_decision: ServiceRestartDecision,
483}
484
485/// Type of file change in a package operation.
486#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
487pub enum FileChangeType {
488    /// File will be newly installed (not currently on system).
489    New,
490    /// File exists but will be replaced/updated.
491    Changed,
492    /// File will be removed (for Remove operations).
493    Removed,
494}
495
496/// Information about a file change in a package operation.
497#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
498pub struct FileChange {
499    /// Full path of the file.
500    pub path: String,
501    /// Type of change (new/changed/removed).
502    pub change_type: FileChangeType,
503    /// Package that owns this file.
504    pub package: String,
505    /// Whether this is a configuration file (under /etc or marked as backup).
506    pub is_config: bool,
507    /// Whether this file is predicted to create a .pacnew file (config conflict).
508    pub predicted_pacnew: bool,
509    /// Whether this file is predicted to create a .pacsave file (config removal).
510    pub predicted_pacsave: bool,
511}
512
513/// File information for a package in the preflight file view.
514#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
515pub struct PackageFileInfo {
516    /// Package name.
517    pub name: String,
518    /// List of file changes for this package.
519    pub files: Vec<FileChange>,
520    /// Total number of files (including directories).
521    pub total_count: usize,
522    /// Number of new files.
523    pub new_count: usize,
524    /// Number of changed files.
525    pub changed_count: usize,
526    /// Number of removed files.
527    pub removed_count: usize,
528    /// Number of configuration files.
529    pub config_count: usize,
530    /// Number of files predicted to create .pacnew files.
531    pub pacnew_candidates: usize,
532    /// Number of files predicted to create .pacsave files.
533    pub pacsave_candidates: usize,
534}
535
536/// What: Risk severity buckets used by the preflight summary header and messaging.
537///
538/// Inputs:
539/// - Assigned by the summary resolver based on aggregate risk score thresholds.
540///
541/// Output:
542/// - Guides color selection and descriptive labels for risk indicators across the UI.
543///
544/// Details:
545/// - Defaults to `Low` so callers without computed risk can render a safe baseline.
546#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
547pub enum RiskLevel {
548    /// Low risk level.
549    Low,
550    /// Medium risk level.
551    Medium,
552    /// High risk level.
553    High,
554}
555
556impl Default for RiskLevel {
557    /// What: Provide a baseline risk level when no assessment has been computed yet.
558    ///
559    /// Inputs: None.
560    ///
561    /// Output: Always returns `RiskLevel::Low`.
562    ///
563    /// Details:
564    /// - Keeps `Default` implementations for composite structs simple while biasing towards safety.
565    fn default() -> Self {
566        Self::Low
567    }
568}
569
570/// What: Aggregated chip metrics displayed in the Preflight header, execution sidebar, and post-summary.
571///
572/// Inputs:
573/// - Populated by the summary planner once package metadata and risk scores are available.
574///
575/// Output:
576/// - Supplies counts and byte deltas for UI components needing condensed statistics.
577///
578/// Details:
579/// - Stores signed install deltas so removals show negative values without additional conversion.
580#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
581pub struct PreflightHeaderChips {
582    /// Number of packages in the operation.
583    pub package_count: usize,
584    /// Total download size in bytes.
585    pub download_bytes: u64,
586    /// Net change in installed size in bytes (positive for installs, negative for removals).
587    pub install_delta_bytes: i64,
588    /// Number of AUR packages in the operation.
589    pub aur_count: usize,
590    /// Risk score (0-255) computed from various risk factors.
591    pub risk_score: u8,
592    /// Risk level category (Low/Medium/High).
593    pub risk_level: RiskLevel,
594}
595
596impl Default for PreflightHeaderChips {
597    /// What: Provide neutral header chip values prior to summary computation.
598    ///
599    /// Inputs: None.
600    ///
601    /// Output: Returns a struct with zeroed counters and low risk classification.
602    ///
603    /// Details:
604    /// - Facilitates cheap initialization for modals created before async planners finish.
605    fn default() -> Self {
606        Self {
607            package_count: 0,
608            download_bytes: 0,
609            install_delta_bytes: 0,
610            aur_count: 0,
611            risk_score: 0,
612            risk_level: RiskLevel::Low,
613        }
614    }
615}
616
617/// What: Version comparison details for a single package in the preflight summary.
618///
619/// Inputs:
620/// - Filled with installed and target versions, plus classification flags.
621///
622/// Output:
623/// - Enables the UI to display per-package version deltas, major bumps, and downgrade warnings.
624///
625/// Details:
626/// - Notes array allows the planner to surface auxiliary hints (e.g., pacnew prediction or service impacts).
627#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
628pub struct PreflightPackageSummary {
629    /// Package name.
630    pub name: String,
631    /// Package source (official/AUR/local).
632    pub source: Source,
633    /// Installed version, if present.
634    pub installed_version: Option<String>,
635    /// Target version to be installed.
636    pub target_version: String,
637    /// Whether the operation downgrades the package.
638    pub is_downgrade: bool,
639    /// Whether the update is a major version bump.
640    pub is_major_bump: bool,
641    /// Download size contribution for this package when available.
642    pub download_bytes: Option<u64>,
643    /// Net installed size delta contributed by this package (signed).
644    pub install_delta_bytes: Option<i64>,
645    /// Notes or warnings specific to this package.
646    pub notes: Vec<String>,
647}
648
649/// What: Comprehensive dataset backing the Preflight Summary tab.
650///
651/// Inputs:
652/// - Populated by summary resolution logic once package metadata, sizes, and risk heuristics are computed.
653///
654/// Output:
655/// - Delivers structured information for tab body rendering, risk callouts, and contextual notes.
656///
657/// Details:
658/// - `summary_notes` aggregates high-impact bullet points (e.g., kernel updates, pacnew predictions).
659#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
660pub struct PreflightSummaryData {
661    /// Per-package summaries for the operation.
662    pub packages: Vec<PreflightPackageSummary>,
663    /// Total number of packages represented in `packages`.
664    pub package_count: usize,
665    /// Number of AUR-sourced packages participating in the plan.
666    pub aur_count: usize,
667    /// Total download size for the plan.
668    pub download_bytes: u64,
669    /// Net install size delta for the plan (signed).
670    pub install_delta_bytes: i64,
671    /// Aggregate risk score (0-255).
672    pub risk_score: u8,
673    /// Aggregate risk level (Low/Medium/High).
674    pub risk_level: RiskLevel,
675    /// Reasons contributing to the risk score.
676    pub risk_reasons: Vec<String>,
677    /// Packages classified as major version bumps (e.g., 1.x -> 2.0).
678    pub major_bump_packages: Vec<String>,
679    /// Core/system packages flagged as high impact (kernel, glibc, etc.).
680    pub core_system_updates: Vec<String>,
681    /// Total predicted .pacnew files across all packages.
682    pub pacnew_candidates: usize,
683    /// Total predicted .pacsave files across all packages.
684    pub pacsave_candidates: usize,
685    /// Packages with configuration merge warnings (.pacnew expected).
686    pub config_warning_packages: Vec<String>,
687    /// Services likely requiring restart after the transaction.
688    pub service_restart_units: Vec<String>,
689    /// Free-form warnings assembled by the summary planner to highlight notable risks.
690    pub summary_warnings: Vec<String>,
691    /// Notes summarizing key items in the plan.
692    pub summary_notes: Vec<String>,
693}
694
695/// What: Captures all dialog state for the various modal overlays presented in
696/// the Pacsea TUI.
697///
698/// - Input: Mutated by event handlers in response to user actions or
699///   background updates.
700/// - Output: Drives conditional rendering and behavior of each modal type.
701/// - Details: Acts as a tagged union so only one modal can be active at a time
702///   while carrying the precise data needed for that modal's UI.
703#[derive(Debug, Clone, Default)]
704#[allow(clippy::large_enum_variant)]
705pub enum Modal {
706    /// No modal is currently displayed.
707    #[default]
708    None,
709    /// Informational alert with a non-interactive message.
710    Alert {
711        /// Alert message text.
712        message: String,
713    },
714    /// Loading indicator shown during background computation.
715    Loading {
716        /// Loading message text.
717        message: String,
718    },
719    /// Confirmation dialog for installing the given items.
720    ConfirmInstall {
721        /// Package items to install.
722        items: Vec<PackageItem>,
723    },
724    /// Confirmation dialog for reinstalling already installed packages.
725    ConfirmReinstall {
726        /// Packages that are already installed (shown in the confirmation dialog).
727        items: Vec<PackageItem>,
728        /// All packages to install (including both installed and not installed).
729        all_items: Vec<PackageItem>,
730        /// Header chip metrics for the operation.
731        header_chips: PreflightHeaderChips,
732    },
733    /// Confirmation dialog for batch updates that may cause dependency conflicts.
734    ConfirmBatchUpdate {
735        /// Package items to update.
736        items: Vec<PackageItem>,
737        /// Whether this is a dry-run operation.
738        dry_run: bool,
739    },
740    /// Confirmation dialog for continuing AUR update after pacman failed.
741    ConfirmAurUpdate {
742        /// Message explaining the situation.
743        message: String,
744    },
745    /// Warning: AUR install targets also appear as official/sync rows in current results.
746    WarnAurRepoDuplicate {
747        /// `pkgname` values that are both AUR-selected and present as official rows.
748        dup_names: Vec<String>,
749        /// Full install set to resume after continue.
750        packages: Vec<PackageItem>,
751        /// Preflight header chips to restore [`handle_proceed_install`] context.
752        header_chips: PreflightHeaderChips,
753    },
754    /// Post full repo-apply: foreign packages that share a name with the new sync repository.
755    ForeignRepoOverlap {
756        /// Repository that was applied (for copy and `pacman -Sl`).
757        repo_name: String,
758        /// Overlapping `(pkgname, installed version)` rows sorted by name.
759        entries: Vec<(String, String)>,
760        /// Current wizard phase.
761        phase: ForeignRepoOverlapPhase,
762    },
763    /// Confirmation dialog for AUR vote/unvote actions.
764    ConfirmAurVote {
765        /// AUR package base the action targets.
766        pkgbase: String,
767        /// Vote action to execute on confirmation.
768        action: VoteAction,
769        /// Confirmation message shown to the user.
770        message: String,
771    },
772    /// Preflight summary before executing any action.
773    Preflight {
774        /// Packages selected for the operation.
775        items: Vec<PackageItem>,
776        /// Action to perform (install/remove/downgrade).
777        action: PreflightAction,
778        /// Currently active preflight tab.
779        tab: PreflightTab,
780        /// Aggregated summary information for versions, sizes, and risk cues.
781        summary: Option<Box<PreflightSummaryData>>,
782        /// Scroll offset (lines) for the Summary tab content (mouse scrolling only).
783        summary_scroll: u16,
784        /// Header chip data shared across summary, execution, and post-summary screens.
785        header_chips: PreflightHeaderChips,
786        /// Resolved dependency information (populated when Deps tab is accessed).
787        dependency_info: Vec<DependencyInfo>,
788        /// Selected index in the dependency list (for navigation).
789        dep_selected: usize,
790        /// Set of dependency names with expanded tree nodes (for tree view).
791        dep_tree_expanded: HashSet<String>,
792        /// Error message from dependency resolution, if any.
793        deps_error: Option<String>,
794        /// File information (populated when Files tab is accessed).
795        file_info: Vec<PackageFileInfo>,
796        /// Selected index in the file list (for navigation).
797        file_selected: usize,
798        /// Set of package names with expanded file lists (for Files tab tree view).
799        file_tree_expanded: HashSet<String>,
800        /// Error message from file resolution, if any.
801        files_error: Option<String>,
802        /// Service impact information (populated when Services tab is accessed).
803        service_info: Vec<ServiceImpact>,
804        /// Selected index in the service impact list (for navigation).
805        service_selected: usize,
806        /// Whether service impacts have been resolved for the current session.
807        services_loaded: bool,
808        /// Error message from service resolution, if any.
809        services_error: Option<String>,
810        /// Sandbox information for AUR packages (populated when Sandbox tab is accessed).
811        sandbox_info: Vec<crate::logic::sandbox::SandboxInfo>,
812        /// Selected index in the sandbox display list (for navigation - can be package or dependency).
813        sandbox_selected: usize,
814        /// Set of package names with expanded dependency lists (for Sandbox tab tree view).
815        sandbox_tree_expanded: HashSet<String>,
816        /// Whether sandbox info has been resolved for the current session.
817        sandbox_loaded: bool,
818        /// Error message from sandbox resolution, if any.
819        sandbox_error: Option<String>,
820        /// Selected optional dependencies to install with their packages.
821        /// Maps package name -> set of selected optional dependency names.
822        selected_optdepends: std::collections::HashMap<String, std::collections::HashSet<String>>,
823        /// Current cascade removal strategy for this session.
824        cascade_mode: CascadeMode,
825        /// Cached reverse dependency report for Remove actions (populated during summary computation).
826        /// This avoids redundant resolution when switching to the Deps tab.
827        cached_reverse_deps_report: Option<crate::logic::deps::ReverseDependencyReport>,
828    },
829    /// Preflight execution screen with log and sticky sidebar.
830    PreflightExec {
831        /// Packages being processed.
832        items: Vec<PackageItem>,
833        /// Action being executed (install/remove/downgrade).
834        action: PreflightAction,
835        /// Tab to display while executing.
836        tab: PreflightTab,
837        /// Whether verbose logging is enabled.
838        verbose: bool,
839        /// Execution log lines.
840        log_lines: Vec<String>,
841        /// Whether the operation can be aborted.
842        abortable: bool,
843        /// Header chip metrics displayed in the sidebar.
844        header_chips: PreflightHeaderChips,
845        /// Execution result: `Some(true)` for success, `Some(false)` for failure, `None` if not yet completed.
846        success: Option<bool>,
847    },
848    /// Post-transaction summary with results and follow-ups.
849    PostSummary {
850        /// Whether the operation succeeded.
851        success: bool,
852        /// Number of files changed.
853        changed_files: usize,
854        /// Number of .pacnew files created.
855        pacnew_count: usize,
856        /// Number of .pacsave files created.
857        pacsave_count: usize,
858        /// Services pending restart.
859        services_pending: Vec<String>,
860        /// Snapshot label if created.
861        snapshot_label: Option<String>,
862    },
863    /// Help overlay with keybindings. Non-interactive; dismissed with Esc/Enter.
864    Help,
865    /// Confirmation dialog for removing the given items.
866    ConfirmRemove {
867        /// Package items to remove.
868        items: Vec<PackageItem>,
869    },
870    /// System update dialog with multi-select options and optional country.
871    SystemUpdate {
872        /// Whether to update Arch mirrors using reflector.
873        do_mirrors: bool,
874        /// Whether to update system packages via pacman.
875        do_pacman: bool,
876        /// Whether to force sync databases (pacman -Syyu instead of -Syu).
877        force_sync: bool,
878        /// Whether to update AUR packages via paru/yay.
879        do_aur: bool,
880        /// Whether to remove caches (pacman and AUR helper).
881        do_cache: bool,
882        /// Index into `countries` for the reflector `--country` argument.
883        country_idx: usize,
884        /// Available countries to choose from for reflector.
885        countries: Vec<String>,
886        /// Requested mirror count to fetch/rank.
887        mirror_count: u16,
888        /// Cursor row in the dialog (0..=4)
889        cursor: usize,
890    },
891    /// Arch Linux News: list of recent items with selection.
892    News {
893        /// Latest news feed items (Arch news, advisories, updates, comments).
894        items: Vec<crate::state::types::NewsFeedItem>,
895        /// Selected row index.
896        selected: usize,
897        /// Scroll offset (lines) for the news list.
898        scroll: u16,
899    },
900    /// Application announcement: markdown content displayed at startup.
901    Announcement {
902        /// Title to display in the modal header.
903        title: String,
904        /// Markdown content to display.
905        content: String,
906        /// Unique identifier for this announcement (version string or remote ID).
907        id: String,
908        /// Scroll offset (lines) for long content.
909        scroll: u16,
910    },
911    /// Available package updates: list of update entries with scroll support.
912    Updates {
913        /// Update entries with package name, old version, and new version.
914        entries: Vec<(String, String, String)>, // (name, old_version, new_version)
915        /// Scroll offset (lines) for the updates list.
916        scroll: u16,
917        /// Selected row index.
918        selected: usize,
919        /// Whether slash-filter text mode is active.
920        filter_active: bool,
921        /// Current slash-filter query text.
922        filter_query: String,
923        /// Caret position (character index) within `filter_query`.
924        filter_caret: usize,
925        /// Last selected package identity used for restoration across filter changes.
926        last_selected_pkg_name: Option<String>,
927        /// Visible updates rows as original-entry indices after applying filter.
928        filtered_indices: Vec<usize>,
929        /// Selected package names for batch preflight actions.
930        selected_pkg_names: HashSet<String>,
931    },
932    /// TUI Optional Dependencies chooser: selectable rows with install status.
933    OptionalDeps {
934        /// Rows to display (pre-filtered by environment/distro).
935        rows: Vec<OptionalDepRow>,
936        /// Selected row index.
937        selected: usize,
938        /// Selected package names for batch install actions.
939        selected_pkg_names: HashSet<String>,
940    },
941    /// Read-only Repositories viewer: `repos.conf` vs live `pacman.conf` / includes.
942    Repositories {
943        /// Merged rows for each configured `[[repo]]`.
944        rows: Vec<RepositoryModalRow>,
945        /// Selected row index.
946        selected: usize,
947        /// Scroll offset: index of first visible data row.
948        scroll: u16,
949        /// Error loading or parsing `repos.conf`, if any.
950        repos_conf_error: Option<String>,
951        /// Warnings from reading `pacman.conf` or includes.
952        pacman_warnings: Vec<String>,
953    },
954    /// Guided SSH setup workflow for AUR voting.
955    SshAurSetup {
956        /// Active setup step in the wizard-like flow.
957        step: SshSetupStep,
958        /// Status and instruction lines shown in the modal body.
959        status_lines: Vec<String>,
960        /// Existing `Host aur.archlinux.org` block shown when overwrite confirmation is required.
961        existing_host_block: Option<String>,
962    },
963    /// Select which scans to run before executing the AUR scan.
964    ScanConfig {
965        /// Whether to run `ClamAV` (clamscan).
966        do_clamav: bool,
967        /// Whether to run Trivy filesystem scan.
968        do_trivy: bool,
969        /// Whether to run Semgrep static analysis.
970        do_semgrep: bool,
971        /// Whether to run `ShellCheck` on `PKGBUILD`/.install.
972        do_shellcheck: bool,
973        /// Whether to run `VirusTotal` hash lookups.
974        do_virustotal: bool,
975        /// Whether to run custom suspicious-pattern scan (PKGBUILD/.install).
976        do_custom: bool,
977        /// Whether to run aur-sleuth (LLM audit).
978        do_sleuth: bool,
979        /// Cursor row in the dialog.
980        cursor: usize,
981    },
982    /// Prompt to install `GNOME Terminal` at startup on GNOME when not present.
983    GnomeTerminalPrompt,
984    /// Setup dialog for `VirusTotal` API key.
985    VirusTotalSetup {
986        /// User-entered API key buffer.
987        input: String,
988        /// Cursor position within the input buffer.
989        cursor: usize,
990    },
991    /// Optional wizard: configure `sudo` `timestamp_timeout` via `sudoers` drop-in (instructions / terminal).
992    SudoTimestampSetup {
993        /// Wizard phase and cursor state.
994        setup: SudoTimestampSetupModalState,
995    },
996    /// Optional wizard: guide `doas` `persist` policy setup via `doas.conf` snippets and checks.
997    DoasPersistSetup {
998        /// Wizard phase and cursor state.
999        setup: DoasPersistSetupModalState,
1000    },
1001    /// Information dialog explaining the Import file format.
1002    ImportHelp,
1003    /// Setup dialog for startup news popup configuration.
1004    NewsSetup {
1005        /// Whether to show Arch news.
1006        show_arch_news: bool,
1007        /// Whether to show security advisories.
1008        show_advisories: bool,
1009        /// Whether to show AUR updates.
1010        show_aur_updates: bool,
1011        /// Whether to show AUR comments.
1012        show_aur_comments: bool,
1013        /// Whether to show official package updates.
1014        show_pkg_updates: bool,
1015        /// Maximum age of news items in days (7, 30, or 90).
1016        max_age_days: Option<u32>,
1017        /// Current cursor position (0-5 for toggles, 6-8 for date buttons).
1018        cursor: usize,
1019    },
1020    /// First-startup selector for choosing which setup flows to run.
1021    StartupSetupSelector {
1022        /// Currently highlighted row index.
1023        cursor: usize,
1024        /// Selected startup setup tasks to execute.
1025        selected: std::collections::HashSet<StartupSetupTask>,
1026        /// Cached active privilege tool resolved when opening the selector.
1027        ///
1028        /// Avoids repeated PATH lookups during render passes.
1029        active_privilege_tool: Option<crate::logic::privilege::PrivilegeTool>,
1030    },
1031    /// Password prompt for sudo authentication.
1032    PasswordPrompt {
1033        /// Purpose of the password prompt.
1034        purpose: PasswordPurpose,
1035        /// Packages involved in the operation.
1036        items: Vec<PackageItem>,
1037        /// User input buffer for password.
1038        input: crate::state::SecureString,
1039        /// Cursor position within the input buffer.
1040        cursor: usize,
1041        /// Error message if password was incorrect.
1042        error: Option<String>,
1043    },
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048    #[test]
1049    /// What: Confirm each `Modal` variant can be constructed and the `Default` implementation returns `Modal::None`.
1050    ///
1051    /// Inputs:
1052    /// - No external inputs; instantiates representative variants directly inside the test.
1053    ///
1054    /// Output:
1055    /// - Ensures `Default::default()` yields `Modal::None` and variant constructors remain stable.
1056    ///
1057    /// Details:
1058    /// - Acts as a regression guard when fields or defaults change, catching compile-time or panicking construction paths.
1059    fn modal_default_and_variants_construct() {
1060        let m = super::Modal::default();
1061        matches!(m, super::Modal::None);
1062        let _ = super::Modal::Alert {
1063            message: "hi".into(),
1064        };
1065        let _ = super::Modal::ConfirmInstall { items: Vec::new() };
1066        let _ = super::Modal::ConfirmReinstall {
1067            items: Vec::new(),
1068            all_items: Vec::new(),
1069            header_chips: crate::state::modal::PreflightHeaderChips::default(),
1070        };
1071        let _ = super::Modal::Help;
1072        let _ = super::Modal::ConfirmRemove { items: Vec::new() };
1073        let _ = super::Modal::SystemUpdate {
1074            do_mirrors: true,
1075            do_pacman: true,
1076            force_sync: false,
1077            do_aur: true,
1078            do_cache: false,
1079            country_idx: 0,
1080            countries: vec!["US".into()],
1081            mirror_count: 20,
1082            cursor: 0,
1083        };
1084        let _ = super::Modal::ConfirmAurVote {
1085            pkgbase: "pacsea-bin".into(),
1086            action: crate::sources::VoteAction::Vote,
1087            message: "confirm".into(),
1088        };
1089        let _ = super::Modal::WarnAurRepoDuplicate {
1090            dup_names: vec!["foo".into()],
1091            packages: Vec::new(),
1092            header_chips: super::PreflightHeaderChips::default(),
1093        };
1094        let _ = super::Modal::ForeignRepoOverlap {
1095            repo_name: "extra".into(),
1096            entries: vec![("a".into(), "1-1".into())],
1097            phase: super::ForeignRepoOverlapPhase::FinalConfirm {
1098                select_cursor: 0,
1099                select_scroll: 0,
1100                selected: std::collections::HashSet::new(),
1101            },
1102        };
1103        let _ = super::Modal::News {
1104            items: Vec::new(),
1105            selected: 0,
1106            scroll: 0,
1107        };
1108        let _ = super::Modal::Updates {
1109            entries: vec![("pkg".into(), "1".into(), "2".into())],
1110            scroll: 0,
1111            selected: 0,
1112            filter_active: false,
1113            filter_query: String::new(),
1114            filter_caret: 0,
1115            last_selected_pkg_name: None,
1116            filtered_indices: vec![0],
1117            selected_pkg_names: std::collections::HashSet::new(),
1118        };
1119        let _ = super::Modal::OptionalDeps {
1120            rows: Vec::new(),
1121            selected: 0,
1122            selected_pkg_names: std::collections::HashSet::new(),
1123        };
1124        let _ = super::Modal::Repositories {
1125            rows: Vec::new(),
1126            selected: 0,
1127            scroll: 0,
1128            repos_conf_error: None,
1129            pacman_warnings: Vec::new(),
1130        };
1131        let _ = super::Modal::SshAurSetup {
1132            step: super::SshSetupStep::Intro,
1133            status_lines: Vec::new(),
1134            existing_host_block: None,
1135        };
1136        let _ = super::Modal::GnomeTerminalPrompt;
1137        let _ = super::Modal::VirusTotalSetup {
1138            input: String::new(),
1139            cursor: 0,
1140        };
1141        let _ = super::Modal::SudoTimestampSetup {
1142            setup: super::SudoTimestampSetupModalState {
1143                phase: super::SudoTimestampSetupPhase::Select,
1144                select_cursor: 0,
1145            },
1146        };
1147        let _ = super::Modal::DoasPersistSetup {
1148            setup: super::DoasPersistSetupModalState {
1149                phase: super::DoasPersistSetupPhase::Select,
1150                select_cursor: 0,
1151            },
1152        };
1153        let _ = super::Modal::ImportHelp;
1154        let _ = super::Modal::PasswordPrompt {
1155            purpose: super::PasswordPurpose::Install,
1156            items: Vec::new(),
1157            input: crate::state::SecureString::default(),
1158            cursor: 0,
1159            error: None,
1160        };
1161        let _ = super::Modal::PasswordPrompt {
1162            purpose: super::PasswordPurpose::RepoForeignMigrate,
1163            items: Vec::new(),
1164            input: crate::state::SecureString::default(),
1165            cursor: 0,
1166            error: None,
1167        };
1168        let _ = super::Modal::StartupSetupSelector {
1169            cursor: 0,
1170            selected: std::collections::HashSet::new(),
1171            active_privilege_tool: None,
1172        };
1173        let _ = super::Modal::Preflight {
1174            items: Vec::new(),
1175            action: super::PreflightAction::Install,
1176            tab: super::PreflightTab::Summary,
1177            summary: None,
1178            summary_scroll: 0,
1179            header_chips: super::PreflightHeaderChips::default(),
1180            dependency_info: Vec::new(),
1181            dep_selected: 0,
1182            dep_tree_expanded: std::collections::HashSet::new(),
1183            deps_error: None,
1184            file_info: Vec::new(),
1185            file_selected: 0,
1186            file_tree_expanded: std::collections::HashSet::new(),
1187            files_error: None,
1188            service_info: Vec::new(),
1189            service_selected: 0,
1190            services_loaded: false,
1191            services_error: None,
1192            sandbox_info: Vec::new(),
1193            sandbox_selected: 0,
1194            sandbox_tree_expanded: std::collections::HashSet::new(),
1195            sandbox_loaded: false,
1196            sandbox_error: None,
1197            selected_optdepends: std::collections::HashMap::new(),
1198            cascade_mode: super::CascadeMode::Basic,
1199            cached_reverse_deps_report: None,
1200        };
1201    }
1202}