use crate::sources::VoteAction;
use crate::state::types::{OptionalDepRow, PackageItem, RepositoryModalRow, Source};
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreflightAction {
Install,
Remove,
Downgrade,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PasswordPurpose {
Install,
Remove,
Update,
Downgrade,
FileSync,
RepoApply,
RepoForeignMigrate,
}
#[derive(Debug, Clone)]
pub struct RepoOverlapApplyPending {
pub repo_section: String,
pub pre_apply_foreign_snapshot: Option<Vec<(String, String)>>,
}
#[derive(Debug, Clone)]
pub struct RepositoriesModalResume {
pub section_name: String,
pub scroll: u16,
}
#[derive(Debug, Clone)]
pub enum ForeignRepoOverlapPhase {
WarnAck {
step: u8,
list_scroll: u16,
},
Select {
cursor: usize,
list_scroll: u16,
selected: HashSet<String>,
},
FinalConfirm {
select_cursor: usize,
select_scroll: u16,
selected: HashSet<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreflightTab {
Summary,
Deps,
Files,
Services,
Sandbox,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CascadeMode {
Basic,
Cascade,
CascadeWithConfigs,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SshSetupStep {
Intro,
ConfirmOverwrite,
ApplyKeyOnAur,
Result,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StartupSetupTask {
ArchNews,
SshAurSetup,
OptionalDepsMissing,
SudoTimestampSetup,
DoasPersistSetup,
AurSleuthSetup,
VirusTotalSetup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SudoTimestampChoice {
TenMinutes,
ThirtyMinutes,
Infinity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SudoTimestampSetupPhase {
Select,
Instructions {
choice: SudoTimestampChoice,
scroll: u16,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SudoTimestampSetupModalState {
pub phase: SudoTimestampSetupPhase,
pub select_cursor: usize,
}
pub const SUDO_TIMESTAMP_SELECT_ROWS: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DoasPersistChoice {
WheelScoped,
UserScoped,
Skip,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DoasPersistSetupPhase {
Select,
Instructions {
choice: DoasPersistChoice,
scroll: u16,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DoasPersistSetupModalState {
pub phase: DoasPersistSetupPhase,
pub select_cursor: usize,
}
pub const DOAS_PERSIST_SELECT_ROWS: usize = 3;
impl CascadeMode {
#[must_use]
pub const fn flag(self) -> &'static str {
match self {
Self::Basic => "-R",
Self::Cascade => "-Rs",
Self::CascadeWithConfigs => "-Rns",
}
}
#[must_use]
pub const fn description(self) -> &'static str {
match self {
Self::Basic => "targets only",
Self::Cascade => "remove dependents",
Self::CascadeWithConfigs => "dependents + configs",
}
}
#[must_use]
pub const fn allows_dependents(self) -> bool {
!matches!(self, Self::Basic)
}
#[must_use]
pub const fn next(self) -> Self {
match self {
Self::Basic => Self::Cascade,
Self::Cascade => Self::CascadeWithConfigs,
Self::CascadeWithConfigs => Self::Basic,
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct DependencyInfo {
pub name: String,
pub version: String,
pub status: DependencyStatus,
pub source: DependencySource,
pub required_by: Vec<String>,
pub depends_on: Vec<String>,
pub is_core: bool,
pub is_system: bool,
}
#[derive(Clone, Debug, Default)]
pub struct ReverseRootSummary {
pub package: String,
pub direct_dependents: usize,
pub transitive_dependents: usize,
pub total_dependents: usize,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum DependencyStatus {
Installed {
version: String,
},
ToInstall,
ToUpgrade {
current: String,
required: String,
},
Conflict {
reason: String,
},
Missing,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum DependencySource {
Official {
repo: String,
},
Aur,
Local,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ServiceRestartDecision {
Restart,
Defer,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct ServiceImpact {
pub unit_name: String,
pub providers: Vec<String>,
pub is_active: bool,
pub needs_restart: bool,
pub recommended_decision: ServiceRestartDecision,
pub restart_decision: ServiceRestartDecision,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum FileChangeType {
New,
Changed,
Removed,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct FileChange {
pub path: String,
pub change_type: FileChangeType,
pub package: String,
pub is_config: bool,
pub predicted_pacnew: bool,
pub predicted_pacsave: bool,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PackageFileInfo {
pub name: String,
pub files: Vec<FileChange>,
pub total_count: usize,
pub new_count: usize,
pub changed_count: usize,
pub removed_count: usize,
pub config_count: usize,
pub pacnew_candidates: usize,
pub pacsave_candidates: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum RiskLevel {
Low,
Medium,
High,
}
impl Default for RiskLevel {
fn default() -> Self {
Self::Low
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PreflightHeaderChips {
pub package_count: usize,
pub download_bytes: u64,
pub install_delta_bytes: i64,
pub aur_count: usize,
pub risk_score: u8,
pub risk_level: RiskLevel,
}
impl Default for PreflightHeaderChips {
fn default() -> Self {
Self {
package_count: 0,
download_bytes: 0,
install_delta_bytes: 0,
aur_count: 0,
risk_score: 0,
risk_level: RiskLevel::Low,
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PreflightPackageSummary {
pub name: String,
pub source: Source,
pub installed_version: Option<String>,
pub target_version: String,
pub is_downgrade: bool,
pub is_major_bump: bool,
pub download_bytes: Option<u64>,
pub install_delta_bytes: Option<i64>,
pub notes: Vec<String>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PreflightSummaryData {
pub packages: Vec<PreflightPackageSummary>,
pub package_count: usize,
pub aur_count: usize,
pub download_bytes: u64,
pub install_delta_bytes: i64,
pub risk_score: u8,
pub risk_level: RiskLevel,
pub risk_reasons: Vec<String>,
pub major_bump_packages: Vec<String>,
pub core_system_updates: Vec<String>,
pub pacnew_candidates: usize,
pub pacsave_candidates: usize,
pub config_warning_packages: Vec<String>,
pub service_restart_units: Vec<String>,
pub summary_warnings: Vec<String>,
pub summary_notes: Vec<String>,
}
#[derive(Debug, Clone, Default)]
#[allow(clippy::large_enum_variant)]
pub enum Modal {
#[default]
None,
Alert {
message: String,
},
Loading {
message: String,
},
ConfirmInstall {
items: Vec<PackageItem>,
},
ConfirmReinstall {
items: Vec<PackageItem>,
all_items: Vec<PackageItem>,
header_chips: PreflightHeaderChips,
},
ConfirmBatchUpdate {
items: Vec<PackageItem>,
dry_run: bool,
},
ConfirmAurUpdate {
message: String,
},
WarnAurRepoDuplicate {
dup_names: Vec<String>,
packages: Vec<PackageItem>,
header_chips: PreflightHeaderChips,
},
ForeignRepoOverlap {
repo_name: String,
entries: Vec<(String, String)>,
phase: ForeignRepoOverlapPhase,
},
ConfirmAurVote {
pkgbase: String,
action: VoteAction,
message: String,
},
Preflight {
items: Vec<PackageItem>,
action: PreflightAction,
tab: PreflightTab,
summary: Option<Box<PreflightSummaryData>>,
summary_scroll: u16,
header_chips: PreflightHeaderChips,
dependency_info: Vec<DependencyInfo>,
dep_selected: usize,
dep_tree_expanded: HashSet<String>,
deps_error: Option<String>,
file_info: Vec<PackageFileInfo>,
file_selected: usize,
file_tree_expanded: HashSet<String>,
files_error: Option<String>,
service_info: Vec<ServiceImpact>,
service_selected: usize,
services_loaded: bool,
services_error: Option<String>,
sandbox_info: Vec<crate::logic::sandbox::SandboxInfo>,
sandbox_selected: usize,
sandbox_tree_expanded: HashSet<String>,
sandbox_loaded: bool,
sandbox_error: Option<String>,
selected_optdepends: std::collections::HashMap<String, std::collections::HashSet<String>>,
cascade_mode: CascadeMode,
cached_reverse_deps_report: Option<crate::logic::deps::ReverseDependencyReport>,
},
PreflightExec {
items: Vec<PackageItem>,
action: PreflightAction,
tab: PreflightTab,
verbose: bool,
log_lines: Vec<String>,
abortable: bool,
header_chips: PreflightHeaderChips,
success: Option<bool>,
},
PostSummary {
success: bool,
changed_files: usize,
pacnew_count: usize,
pacsave_count: usize,
services_pending: Vec<String>,
snapshot_label: Option<String>,
},
Help,
ConfirmRemove {
items: Vec<PackageItem>,
},
SystemUpdate {
do_mirrors: bool,
do_pacman: bool,
force_sync: bool,
do_aur: bool,
do_cache: bool,
country_idx: usize,
countries: Vec<String>,
mirror_count: u16,
cursor: usize,
},
News {
items: Vec<crate::state::types::NewsFeedItem>,
selected: usize,
scroll: u16,
},
Announcement {
title: String,
content: String,
id: String,
scroll: u16,
},
Updates {
entries: Vec<(String, String, String)>, scroll: u16,
selected: usize,
filter_active: bool,
filter_query: String,
filter_caret: usize,
last_selected_pkg_name: Option<String>,
filtered_indices: Vec<usize>,
selected_pkg_names: HashSet<String>,
},
OptionalDeps {
rows: Vec<OptionalDepRow>,
selected: usize,
selected_pkg_names: HashSet<String>,
},
Repositories {
rows: Vec<RepositoryModalRow>,
selected: usize,
scroll: u16,
repos_conf_error: Option<String>,
pacman_warnings: Vec<String>,
},
SshAurSetup {
step: SshSetupStep,
status_lines: Vec<String>,
existing_host_block: Option<String>,
},
ScanConfig {
do_clamav: bool,
do_trivy: bool,
do_semgrep: bool,
do_shellcheck: bool,
do_virustotal: bool,
do_custom: bool,
do_sleuth: bool,
cursor: usize,
},
GnomeTerminalPrompt,
VirusTotalSetup {
input: String,
cursor: usize,
},
SudoTimestampSetup {
setup: SudoTimestampSetupModalState,
},
DoasPersistSetup {
setup: DoasPersistSetupModalState,
},
ImportHelp,
NewsSetup {
show_arch_news: bool,
show_advisories: bool,
show_aur_updates: bool,
show_aur_comments: bool,
show_pkg_updates: bool,
max_age_days: Option<u32>,
cursor: usize,
},
StartupSetupSelector {
cursor: usize,
selected: std::collections::HashSet<StartupSetupTask>,
active_privilege_tool: Option<crate::logic::privilege::PrivilegeTool>,
},
PasswordPrompt {
purpose: PasswordPurpose,
items: Vec<PackageItem>,
input: crate::state::SecureString,
cursor: usize,
error: Option<String>,
},
}
#[cfg(test)]
mod tests {
#[test]
fn modal_default_and_variants_construct() {
let m = super::Modal::default();
matches!(m, super::Modal::None);
let _ = super::Modal::Alert {
message: "hi".into(),
};
let _ = super::Modal::ConfirmInstall { items: Vec::new() };
let _ = super::Modal::ConfirmReinstall {
items: Vec::new(),
all_items: Vec::new(),
header_chips: crate::state::modal::PreflightHeaderChips::default(),
};
let _ = super::Modal::Help;
let _ = super::Modal::ConfirmRemove { items: Vec::new() };
let _ = super::Modal::SystemUpdate {
do_mirrors: true,
do_pacman: true,
force_sync: false,
do_aur: true,
do_cache: false,
country_idx: 0,
countries: vec!["US".into()],
mirror_count: 20,
cursor: 0,
};
let _ = super::Modal::ConfirmAurVote {
pkgbase: "pacsea-bin".into(),
action: crate::sources::VoteAction::Vote,
message: "confirm".into(),
};
let _ = super::Modal::WarnAurRepoDuplicate {
dup_names: vec!["foo".into()],
packages: Vec::new(),
header_chips: super::PreflightHeaderChips::default(),
};
let _ = super::Modal::ForeignRepoOverlap {
repo_name: "extra".into(),
entries: vec![("a".into(), "1-1".into())],
phase: super::ForeignRepoOverlapPhase::FinalConfirm {
select_cursor: 0,
select_scroll: 0,
selected: std::collections::HashSet::new(),
},
};
let _ = super::Modal::News {
items: Vec::new(),
selected: 0,
scroll: 0,
};
let _ = super::Modal::Updates {
entries: vec![("pkg".into(), "1".into(), "2".into())],
scroll: 0,
selected: 0,
filter_active: false,
filter_query: String::new(),
filter_caret: 0,
last_selected_pkg_name: None,
filtered_indices: vec![0],
selected_pkg_names: std::collections::HashSet::new(),
};
let _ = super::Modal::OptionalDeps {
rows: Vec::new(),
selected: 0,
selected_pkg_names: std::collections::HashSet::new(),
};
let _ = super::Modal::Repositories {
rows: Vec::new(),
selected: 0,
scroll: 0,
repos_conf_error: None,
pacman_warnings: Vec::new(),
};
let _ = super::Modal::SshAurSetup {
step: super::SshSetupStep::Intro,
status_lines: Vec::new(),
existing_host_block: None,
};
let _ = super::Modal::GnomeTerminalPrompt;
let _ = super::Modal::VirusTotalSetup {
input: String::new(),
cursor: 0,
};
let _ = super::Modal::SudoTimestampSetup {
setup: super::SudoTimestampSetupModalState {
phase: super::SudoTimestampSetupPhase::Select,
select_cursor: 0,
},
};
let _ = super::Modal::DoasPersistSetup {
setup: super::DoasPersistSetupModalState {
phase: super::DoasPersistSetupPhase::Select,
select_cursor: 0,
},
};
let _ = super::Modal::ImportHelp;
let _ = super::Modal::PasswordPrompt {
purpose: super::PasswordPurpose::Install,
items: Vec::new(),
input: crate::state::SecureString::default(),
cursor: 0,
error: None,
};
let _ = super::Modal::PasswordPrompt {
purpose: super::PasswordPurpose::RepoForeignMigrate,
items: Vec::new(),
input: crate::state::SecureString::default(),
cursor: 0,
error: None,
};
let _ = super::Modal::StartupSetupSelector {
cursor: 0,
selected: std::collections::HashSet::new(),
active_privilege_tool: None,
};
let _ = super::Modal::Preflight {
items: Vec::new(),
action: super::PreflightAction::Install,
tab: super::PreflightTab::Summary,
summary: None,
summary_scroll: 0,
header_chips: super::PreflightHeaderChips::default(),
dependency_info: Vec::new(),
dep_selected: 0,
dep_tree_expanded: std::collections::HashSet::new(),
deps_error: None,
file_info: Vec::new(),
file_selected: 0,
file_tree_expanded: std::collections::HashSet::new(),
files_error: None,
service_info: Vec::new(),
service_selected: 0,
services_loaded: false,
services_error: None,
sandbox_info: Vec::new(),
sandbox_selected: 0,
sandbox_tree_expanded: std::collections::HashSet::new(),
sandbox_loaded: false,
sandbox_error: None,
selected_optdepends: std::collections::HashMap::new(),
cascade_mode: super::CascadeMode::Basic,
cached_reverse_deps_report: None,
};
}
}