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}