use ratatui::Frame;
use ratatui::prelude::Rect;
use crate::state::{
AppState, Modal,
modal::PreflightHeaderChips,
types::{OptionalDepRow, RepositoryModalRow},
};
use crate::ui::modals::{
alert, announcement, confirm, foreign_overlap, help, misc, news, password, post_summary,
preflight, preflight_exec, system_update, updates,
};
fn render_confirm_reinstall_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: ConfirmReinstallContext,
) -> Modal {
confirm::render_confirm_reinstall(f, app, area, &ctx.items);
Modal::ConfirmReinstall {
items: ctx.items,
all_items: ctx.all_items,
header_chips: ctx.header_chips,
}
}
fn render_confirm_batch_update_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: ConfirmBatchUpdateContext,
) -> Modal {
confirm::render_confirm_batch_update(f, app, area, &ctx.items);
Modal::ConfirmBatchUpdate {
items: ctx.items,
dry_run: ctx.dry_run,
}
}
fn render_confirm_aur_update_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: ConfirmAurUpdateContext,
) -> Modal {
confirm::render_confirm_aur_update(f, app, area, &ctx.message);
Modal::ConfirmAurUpdate {
message: ctx.message,
}
}
fn render_confirm_aur_vote_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: ConfirmAurVoteContext,
) -> Modal {
confirm::render_confirm_aur_vote(f, app, area, ctx.action, &ctx.message);
Modal::ConfirmAurVote {
pkgbase: ctx.pkgbase,
action: ctx.action,
message: ctx.message,
}
}
struct PreflightExecContext {
items: Vec<crate::state::PackageItem>,
action: crate::state::PreflightAction,
tab: crate::state::PreflightTab,
verbose: bool,
log_lines: Vec<String>,
abortable: bool,
header_chips: PreflightHeaderChips,
success: Option<bool>,
}
#[allow(clippy::struct_excessive_bools)]
struct SystemUpdateContext {
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,
}
struct PostSummaryContext {
success: bool,
changed_files: usize,
pacnew_count: usize,
pacsave_count: usize,
services_pending: Vec<String>,
snapshot_label: Option<String>,
}
#[allow(clippy::struct_excessive_bools)]
struct ScanConfigContext {
do_clamav: bool,
do_trivy: bool,
do_semgrep: bool,
do_shellcheck: bool,
do_virustotal: bool,
do_custom: bool,
do_sleuth: bool,
cursor: usize,
}
struct AlertContext {
message: String,
}
struct ConfirmInstallContext {
items: Vec<crate::state::PackageItem>,
}
struct ConfirmRemoveContext {
items: Vec<crate::state::PackageItem>,
}
struct ConfirmReinstallContext {
items: Vec<crate::state::PackageItem>,
all_items: Vec<crate::state::PackageItem>,
header_chips: PreflightHeaderChips,
}
struct ConfirmBatchUpdateContext {
items: Vec<crate::state::PackageItem>,
dry_run: bool,
}
struct ConfirmAurUpdateContext {
message: String,
}
struct ConfirmAurVoteContext {
pkgbase: String,
action: crate::sources::VoteAction,
message: String,
}
struct NewsContext {
items: Vec<crate::state::types::NewsFeedItem>,
selected: usize,
scroll: u16,
}
struct AnnouncementContext {
title: String,
content: String,
id: String,
scroll: u16,
}
struct UpdatesContext {
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: std::collections::HashSet<String>,
}
struct OptionalDepsContext {
rows: Vec<OptionalDepRow>,
selected: usize,
selected_pkg_names: std::collections::HashSet<String>,
}
struct RepositoriesContext {
rows: Vec<RepositoryModalRow>,
selected: usize,
scroll: u16,
repos_conf_error: Option<String>,
pacman_warnings: Vec<String>,
}
struct SshAurSetupContext {
step: crate::state::SshSetupStep,
status_lines: Vec<String>,
existing_host_block: Option<String>,
}
struct VirusTotalSetupContext {
input: String,
cursor: usize,
}
struct SudoTimestampSetupContext {
setup: crate::state::modal::SudoTimestampSetupModalState,
}
struct DoasPersistSetupContext {
setup: crate::state::modal::DoasPersistSetupModalState,
}
#[allow(clippy::struct_excessive_bools)]
struct NewsSetupContext {
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,
}
struct StartupSetupSelectorContext {
cursor: usize,
selected: std::collections::HashSet<crate::state::modal::StartupSetupTask>,
active_privilege_tool: Option<crate::logic::privilege::PrivilegeTool>,
}
struct WarnAurRepoDuplicateContext {
dup_names: Vec<String>,
packages: Vec<crate::state::PackageItem>,
header_chips: PreflightHeaderChips,
}
struct ForeignRepoOverlapContext {
repo_name: String,
entries: Vec<(String, String)>,
phase: crate::state::modal::ForeignRepoOverlapPhase,
}
struct PasswordPromptContext {
purpose: crate::state::modal::PasswordPurpose,
items: Vec<crate::state::PackageItem>,
input: crate::state::SecureString,
cursor: usize,
error: Option<String>,
}
trait ModalRenderer {
fn render(self, f: &mut Frame, app: &mut AppState, area: Rect) -> Modal;
}
impl ModalRenderer for Modal {
#[allow(clippy::too_many_lines)] fn render(self, f: &mut Frame, app: &mut AppState, area: Rect) -> Modal {
match self {
Self::Alert { message } => {
let ctx = AlertContext { message };
render_alert_modal(f, app, area, ctx)
}
Self::Loading { message } => {
render_loading_modal(f, area, &message);
Self::Loading { message }
}
Self::ConfirmInstall { items } => {
let ctx = ConfirmInstallContext { items };
render_confirm_install_modal(f, app, area, ctx)
}
Self::Preflight { .. } => {
unreachable!("Preflight should be handled separately before trait dispatch")
}
Self::PreflightExec {
items,
action,
tab,
verbose,
log_lines,
abortable,
header_chips,
success,
} => {
let ctx = PreflightExecContext {
items,
action,
tab,
verbose,
log_lines,
abortable,
header_chips,
success,
};
render_preflight_exec_modal(f, app, area, ctx)
}
Self::PostSummary {
success,
changed_files,
pacnew_count,
pacsave_count,
services_pending,
snapshot_label,
} => {
let ctx = PostSummaryContext {
success,
changed_files,
pacnew_count,
pacsave_count,
services_pending,
snapshot_label,
};
render_post_summary_modal(f, app, area, ctx)
}
Self::ConfirmRemove { items } => {
let ctx = ConfirmRemoveContext { items };
render_confirm_remove_modal(f, app, area, ctx)
}
Self::ConfirmReinstall {
items,
all_items,
header_chips,
} => {
let ctx = ConfirmReinstallContext {
items,
all_items,
header_chips,
};
render_confirm_reinstall_modal(f, app, area, ctx)
}
Self::ConfirmBatchUpdate { items, dry_run } => {
let ctx = ConfirmBatchUpdateContext { items, dry_run };
render_confirm_batch_update_modal(f, app, area, ctx)
}
Self::ConfirmAurUpdate { message } => {
let ctx = ConfirmAurUpdateContext { message };
render_confirm_aur_update_modal(f, app, area, ctx)
}
Self::ConfirmAurVote {
pkgbase,
action,
message,
} => {
let ctx = ConfirmAurVoteContext {
pkgbase,
action,
message,
};
render_confirm_aur_vote_modal(f, app, area, ctx)
}
Self::SystemUpdate {
do_mirrors,
do_pacman,
force_sync,
do_aur,
do_cache,
country_idx,
countries,
mirror_count,
cursor,
} => {
let ctx = SystemUpdateContext {
do_mirrors,
do_pacman,
force_sync,
do_aur,
do_cache,
country_idx,
countries,
mirror_count,
cursor,
};
render_system_update_modal(f, app, area, ctx)
}
Self::Help => render_help_modal(f, app, area),
Self::News {
items,
selected,
scroll,
} => {
let ctx = NewsContext {
items,
selected,
scroll,
};
render_news_modal(f, app, area, ctx)
}
Self::Announcement {
title,
content,
id,
scroll,
} => {
let ctx = AnnouncementContext {
title,
content,
id,
scroll,
};
render_announcement_modal(f, app, area, ctx)
}
Self::Updates {
entries,
scroll,
selected,
filter_active,
filter_query,
filter_caret,
last_selected_pkg_name,
filtered_indices,
selected_pkg_names,
} => {
let ctx = UpdatesContext {
entries,
scroll,
selected,
filter_active,
filter_query,
filter_caret,
last_selected_pkg_name,
filtered_indices,
selected_pkg_names,
};
render_updates_modal(f, app, area, ctx)
}
Self::OptionalDeps {
rows,
selected,
selected_pkg_names,
} => {
let ctx = OptionalDepsContext {
rows,
selected,
selected_pkg_names,
};
render_optional_deps_modal(f, area, ctx, app)
}
Self::Repositories {
rows,
selected,
scroll,
repos_conf_error,
pacman_warnings,
} => {
let ctx = RepositoriesContext {
rows,
selected,
scroll,
repos_conf_error,
pacman_warnings,
};
render_repositories_modal(f, area, ctx, app)
}
Self::SshAurSetup {
step,
status_lines,
existing_host_block,
} => {
let ctx = SshAurSetupContext {
step,
status_lines,
existing_host_block,
};
render_ssh_setup_modal(f, app, area, ctx)
}
Self::ScanConfig {
do_clamav,
do_trivy,
do_semgrep,
do_shellcheck,
do_virustotal,
do_custom,
do_sleuth,
cursor,
} => {
let ctx = ScanConfigContext {
do_clamav,
do_trivy,
do_semgrep,
do_shellcheck,
do_virustotal,
do_custom,
do_sleuth,
cursor,
};
render_scan_config_modal(f, area, &ctx)
}
Self::GnomeTerminalPrompt => render_gnome_terminal_prompt_modal(f, area),
Self::VirusTotalSetup { input, cursor } => {
let ctx = VirusTotalSetupContext { input, cursor };
render_virustotal_setup_modal(f, app, area, ctx)
}
Self::SudoTimestampSetup { setup } => {
let ctx = SudoTimestampSetupContext { setup };
render_sudo_timestamp_setup_modal(f, app, area, ctx)
}
Self::DoasPersistSetup { setup } => {
let ctx = DoasPersistSetupContext { setup };
render_doas_persist_setup_modal(f, app, area, ctx)
}
Self::ImportHelp => render_import_help_modal(f, app, area),
Self::NewsSetup {
show_arch_news,
show_advisories,
show_aur_updates,
show_aur_comments,
show_pkg_updates,
max_age_days,
cursor,
} => {
let ctx = NewsSetupContext {
show_arch_news,
show_advisories,
show_aur_updates,
show_aur_comments,
show_pkg_updates,
max_age_days,
cursor,
};
render_news_setup_modal(f, app, area, ctx)
}
Self::StartupSetupSelector {
cursor,
selected,
active_privilege_tool,
} => {
let ctx = StartupSetupSelectorContext {
cursor,
selected,
active_privilege_tool,
};
render_startup_setup_selector_modal(f, app, area, ctx)
}
Self::WarnAurRepoDuplicate {
dup_names,
packages,
header_chips,
} => {
let ctx = WarnAurRepoDuplicateContext {
dup_names,
packages,
header_chips,
};
render_warn_aur_repo_duplicate_modal(f, app, area, ctx)
}
Self::ForeignRepoOverlap {
repo_name,
entries,
phase,
} => {
let ctx = ForeignRepoOverlapContext {
repo_name,
entries,
phase,
};
render_foreign_repo_overlap_modal(f, app, area, ctx)
}
Self::PasswordPrompt {
purpose,
items,
input,
cursor,
error,
} => {
let ctx = PasswordPromptContext {
purpose,
items,
input,
cursor,
error,
};
render_password_prompt_modal(f, app, area, ctx)
}
Self::None => Self::None,
}
}
}
fn render_alert_modal(f: &mut Frame, app: &AppState, area: Rect, ctx: AlertContext) -> Modal {
alert::render_alert(f, app, area, &ctx.message);
Modal::Alert {
message: ctx.message,
}
}
fn render_loading_modal(f: &mut Frame, area: Rect, message: &str) {
misc::render_loading(f, area, message);
}
fn render_confirm_install_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: ConfirmInstallContext,
) -> Modal {
confirm::render_confirm_install(f, app, area, &ctx.items);
Modal::ConfirmInstall { items: ctx.items }
}
fn render_preflight_exec_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: PreflightExecContext,
) -> Modal {
preflight_exec::render_preflight_exec(
f,
app,
area,
&ctx.items,
ctx.action,
ctx.tab,
ctx.verbose,
&ctx.log_lines,
ctx.abortable,
&ctx.header_chips,
);
Modal::PreflightExec {
items: ctx.items,
action: ctx.action,
tab: ctx.tab,
verbose: ctx.verbose,
log_lines: ctx.log_lines,
abortable: ctx.abortable,
header_chips: ctx.header_chips,
success: ctx.success,
}
}
fn render_post_summary_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: PostSummaryContext,
) -> Modal {
post_summary::render_post_summary(
f,
app,
area,
ctx.success,
ctx.changed_files,
ctx.pacnew_count,
ctx.pacsave_count,
&ctx.services_pending,
ctx.snapshot_label.as_ref(),
);
Modal::PostSummary {
success: ctx.success,
changed_files: ctx.changed_files,
pacnew_count: ctx.pacnew_count,
pacsave_count: ctx.pacsave_count,
services_pending: ctx.services_pending,
snapshot_label: ctx.snapshot_label,
}
}
fn render_confirm_remove_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: ConfirmRemoveContext,
) -> Modal {
confirm::render_confirm_remove(f, app, area, &ctx.items);
Modal::ConfirmRemove { items: ctx.items }
}
fn render_system_update_modal(
f: &mut Frame,
app: &mut AppState,
area: Rect,
ctx: SystemUpdateContext,
) -> Modal {
system_update::render_system_update(
f,
app,
area,
ctx.do_mirrors,
ctx.do_pacman,
ctx.force_sync,
ctx.do_aur,
ctx.do_cache,
ctx.country_idx,
&ctx.countries,
ctx.mirror_count,
ctx.cursor,
);
Modal::SystemUpdate {
do_mirrors: ctx.do_mirrors,
do_pacman: ctx.do_pacman,
force_sync: ctx.force_sync,
do_aur: ctx.do_aur,
do_cache: ctx.do_cache,
country_idx: ctx.country_idx,
countries: ctx.countries,
mirror_count: ctx.mirror_count,
cursor: ctx.cursor,
}
}
fn render_help_modal(f: &mut Frame, app: &mut AppState, area: Rect) -> Modal {
help::render_help(f, app, area);
Modal::Help
}
fn render_news_modal(f: &mut Frame, app: &mut AppState, area: Rect, ctx: NewsContext) -> Modal {
news::render_news(f, app, area, &ctx.items, ctx.selected, ctx.scroll);
Modal::News {
items: ctx.items,
selected: ctx.selected,
scroll: ctx.scroll,
}
}
fn render_announcement_modal(
f: &mut Frame,
app: &mut AppState,
area: Rect,
ctx: AnnouncementContext,
) -> Modal {
announcement::render_announcement(f, app, area, &ctx.title, &ctx.content, ctx.scroll);
Modal::Announcement {
title: ctx.title,
content: ctx.content,
id: ctx.id,
scroll: ctx.scroll,
}
}
fn render_updates_modal(
f: &mut Frame,
app: &mut AppState,
area: Rect,
ctx: UpdatesContext,
) -> Modal {
updates::render_updates(
f,
app,
area,
&ctx.entries,
&ctx.filtered_indices,
ctx.scroll,
ctx.selected,
ctx.filter_active,
&ctx.filter_query,
ctx.filter_caret,
&ctx.selected_pkg_names,
);
Modal::Updates {
entries: ctx.entries,
scroll: ctx.scroll,
selected: ctx.selected,
filter_active: ctx.filter_active,
filter_query: ctx.filter_query,
filter_caret: ctx.filter_caret,
last_selected_pkg_name: ctx.last_selected_pkg_name,
filtered_indices: ctx.filtered_indices,
selected_pkg_names: ctx.selected_pkg_names,
}
}
fn render_optional_deps_modal(
f: &mut Frame,
area: Rect,
ctx: OptionalDepsContext,
app: &mut AppState,
) -> Modal {
misc::render_optional_deps(
f,
area,
&ctx.rows,
ctx.selected,
&ctx.selected_pkg_names,
app,
);
Modal::OptionalDeps {
rows: ctx.rows,
selected: ctx.selected,
selected_pkg_names: ctx.selected_pkg_names,
}
}
fn render_repositories_modal(
f: &mut Frame,
area: Rect,
ctx: RepositoriesContext,
app: &mut AppState,
) -> Modal {
misc::render_repositories(
f,
area,
&ctx.rows,
ctx.selected,
ctx.scroll,
ctx.repos_conf_error.as_deref(),
&ctx.pacman_warnings,
app,
);
Modal::Repositories {
rows: ctx.rows,
selected: ctx.selected,
scroll: ctx.scroll,
repos_conf_error: ctx.repos_conf_error,
pacman_warnings: ctx.pacman_warnings,
}
}
fn render_ssh_setup_modal(
f: &mut Frame,
app: &mut AppState,
area: Rect,
ctx: SshAurSetupContext,
) -> Modal {
misc::render_ssh_aur_setup(
f,
area,
ctx.step,
&ctx.status_lines,
ctx.existing_host_block.as_deref(),
app,
);
Modal::SshAurSetup {
step: ctx.step,
status_lines: ctx.status_lines,
existing_host_block: ctx.existing_host_block,
}
}
fn render_scan_config_modal(f: &mut Frame, area: Rect, ctx: &ScanConfigContext) -> Modal {
misc::render_scan_config(
f,
area,
ctx.do_clamav,
ctx.do_trivy,
ctx.do_semgrep,
ctx.do_shellcheck,
ctx.do_virustotal,
ctx.do_custom,
ctx.do_sleuth,
ctx.cursor,
);
Modal::ScanConfig {
do_clamav: ctx.do_clamav,
do_trivy: ctx.do_trivy,
do_semgrep: ctx.do_semgrep,
do_shellcheck: ctx.do_shellcheck,
do_virustotal: ctx.do_virustotal,
do_custom: ctx.do_custom,
do_sleuth: ctx.do_sleuth,
cursor: ctx.cursor,
}
}
fn render_gnome_terminal_prompt_modal(f: &mut Frame, area: Rect) -> Modal {
misc::render_gnome_terminal_prompt(f, area);
Modal::GnomeTerminalPrompt
}
fn render_virustotal_setup_modal(
f: &mut Frame,
app: &mut AppState,
area: Rect,
ctx: VirusTotalSetupContext,
) -> Modal {
misc::render_virustotal_setup(f, app, area, &ctx.input);
Modal::VirusTotalSetup {
input: ctx.input,
cursor: ctx.cursor,
}
}
#[allow(clippy::needless_pass_by_value)]
fn render_sudo_timestamp_setup_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: SudoTimestampSetupContext,
) -> Modal {
misc::render_sudo_timestamp_setup(f, area, app, ctx.setup);
Modal::SudoTimestampSetup { setup: ctx.setup }
}
#[allow(clippy::needless_pass_by_value)]
fn render_doas_persist_setup_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: DoasPersistSetupContext,
) -> Modal {
misc::render_doas_persist_setup(f, area, app, ctx.setup);
Modal::DoasPersistSetup { setup: ctx.setup }
}
fn render_import_help_modal(f: &mut Frame, app: &AppState, area: Rect) -> Modal {
misc::render_import_help(f, area, app);
Modal::ImportHelp
}
#[allow(clippy::needless_pass_by_value)]
fn render_news_setup_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: NewsSetupContext,
) -> Modal {
let NewsSetupContext {
show_arch_news,
show_advisories,
show_aur_updates,
show_aur_comments,
show_pkg_updates,
max_age_days,
cursor,
} = ctx;
misc::render_news_setup(
f,
area,
app,
show_arch_news,
show_advisories,
show_aur_updates,
show_aur_comments,
show_pkg_updates,
max_age_days,
cursor,
);
Modal::NewsSetup {
show_arch_news,
show_advisories,
show_aur_updates,
show_aur_comments,
show_pkg_updates,
max_age_days,
cursor,
}
}
#[allow(clippy::needless_pass_by_value)]
fn render_startup_setup_selector_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: StartupSetupSelectorContext,
) -> Modal {
misc::render_startup_setup_selector(
f,
area,
app,
ctx.cursor,
&ctx.selected,
ctx.active_privilege_tool,
);
Modal::StartupSetupSelector {
cursor: ctx.cursor,
selected: ctx.selected,
active_privilege_tool: ctx.active_privilege_tool,
}
}
fn render_warn_aur_repo_duplicate_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: WarnAurRepoDuplicateContext,
) -> Modal {
foreign_overlap::render_warn_aur_repo_duplicate(f, app, area, &ctx.dup_names);
Modal::WarnAurRepoDuplicate {
dup_names: ctx.dup_names,
packages: ctx.packages,
header_chips: ctx.header_chips,
}
}
fn render_foreign_repo_overlap_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: ForeignRepoOverlapContext,
) -> Modal {
foreign_overlap::render_foreign_repo_overlap(
f,
app,
area,
&ctx.repo_name,
&ctx.entries,
&ctx.phase,
);
Modal::ForeignRepoOverlap {
repo_name: ctx.repo_name,
entries: ctx.entries,
phase: ctx.phase,
}
}
fn render_password_prompt_modal(
f: &mut Frame,
app: &AppState,
area: Rect,
ctx: PasswordPromptContext,
) -> Modal {
password::render_password_prompt(
f,
app,
area,
ctx.purpose,
&ctx.items,
&ctx.input,
ctx.error.as_deref(),
);
Modal::PasswordPrompt {
purpose: ctx.purpose,
items: ctx.items,
input: ctx.input,
cursor: ctx.cursor,
error: ctx.error,
}
}
pub fn render_modal(modal: Modal, f: &mut Frame, app: &mut AppState, area: Rect) -> Modal {
if let Modal::Preflight { .. } = modal {
let mut preflight_modal = modal;
preflight::render_preflight(f, area, app, &mut preflight_modal);
return preflight_modal;
}
modal.render(f, app, area)
}