use std::sync::{Arc, Mutex};
use crate::state::{AppState, PackageItem};
use crate::events::preflight::modal::close_preflight_modal;
pub(super) fn handle_f_key(app: &mut AppState) -> bool {
if let crate::state::Modal::Preflight { tab, .. } = &app.modal {
if *tab != crate::state::PreflightTab::Files {
return false;
}
} else {
return false;
}
app.toast_message = Some("File database sync starting...".to_string());
app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(5));
let sync_result: crate::state::app_state::FileSyncResult = Arc::new(Mutex::new(None));
let sync_result_clone = Arc::clone(&sync_result);
std::thread::spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let sync_result_inner = crate::logic::files::ensure_file_db_synced(true, 7);
match sync_result_inner {
Ok(synced) => {
if synced {
tracing::info!("File database sync completed successfully");
} else {
tracing::info!("File database is already fresh");
}
if let Ok(mut guard) = sync_result_clone.lock() {
*guard = Some(Ok(synced));
}
}
Err(e) => {
tracing::warn!("File database sync failed: {}, will prompt for password", e);
if let Ok(mut guard) = sync_result_clone.lock() {
*guard = Some(Err(e));
}
}
}
}));
if let Err(panic_info) = result {
tracing::error!("File database sync thread panicked: {:?}", panic_info);
}
});
app.pending_file_sync_result = Some(sync_result);
if let crate::state::Modal::Preflight {
file_info,
file_selected,
..
} = &mut app.modal
{
*file_info = Vec::new();
*file_selected = 0;
}
false
}
pub(super) fn handle_s_key(app: &mut AppState) -> bool {
let names = if let crate::state::Modal::Preflight { items, .. } = &app.modal {
items
.iter()
.filter(|p| matches!(p.source, crate::state::Source::Aur))
.map(|p| p.name.clone())
.collect::<Vec<_>>()
} else {
return false;
};
if names.is_empty() {
app.modal = crate::state::Modal::Alert {
message: "No AUR packages selected to scan.\nAdd AUR packages to scan, then press 's'."
.into(),
};
} else {
app.pending_install_names = Some(names);
let prefs = crate::theme::settings();
app.previous_modal = Some(app.modal.clone());
app.modal = crate::state::Modal::ScanConfig {
do_clamav: prefs.scan_do_clamav,
do_trivy: prefs.scan_do_trivy,
do_semgrep: prefs.scan_do_semgrep,
do_shellcheck: prefs.scan_do_shellcheck,
do_virustotal: prefs.scan_do_virustotal,
do_custom: prefs.scan_do_custom,
do_sleuth: prefs.scan_do_sleuth,
cursor: 0,
};
}
false
}
pub(super) fn handle_dry_run_key(app: &mut AppState) -> bool {
app.dry_run = !app.dry_run;
let toast_key = if app.dry_run {
"app.toasts.dry_run_enabled"
} else {
"app.toasts.dry_run_disabled"
};
app.toast_message = Some(crate::i18n::t(app, toast_key));
false
}
pub(super) fn handle_m_key(app: &mut AppState) -> bool {
let next_mode_opt = if let crate::state::Modal::Preflight {
action: crate::state::PreflightAction::Remove,
cascade_mode,
..
} = &mut app.modal
{
let next_mode = cascade_mode.next();
*cascade_mode = next_mode;
Some(next_mode)
} else {
None
};
if let Some(next_mode) = next_mode_opt {
app.remove_cascade_mode = next_mode;
app.toast_message = Some(format!(
"Cascade mode set to {} ({})",
next_mode.flag(),
next_mode.description()
));
app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(4));
}
false
}
fn extract_install_targets(
items: &[PackageItem],
selected_optdepends: &std::collections::HashMap<String, std::collections::HashSet<String>>,
) -> Vec<PackageItem> {
let mut packages = items.to_vec();
for optdeps in selected_optdepends.values() {
for optdep in optdeps {
let optdep_pkg_name = crate::logic::sandbox::extract_package_name(optdep);
if !packages.iter().any(|p| p.name == optdep_pkg_name) {
packages.push(PackageItem {
name: optdep_pkg_name,
version: String::new(),
description: String::new(),
source: crate::state::Source::Official {
repo: String::new(),
arch: String::new(),
},
popularity: None,
out_of_date: None,
orphaned: false,
});
}
}
}
packages
}
pub(super) fn handle_proceed_install(
app: &mut AppState,
packages: Vec<PackageItem>,
header_chips: crate::state::modal::PreflightHeaderChips,
) -> bool {
if crate::events::install::try_open_warn_aur_repo_duplicate_modal(
app,
&packages,
header_chips.clone(),
) {
return false;
}
let installed_set = crate::logic::deps::get_installed_packages();
let provided_set = crate::logic::deps::get_provided_packages(&installed_set);
let upgradable_set = crate::logic::deps::get_upgradable_packages();
let installed_packages: Vec<crate::state::PackageItem> = packages
.iter()
.filter(|item| {
let is_installed = crate::logic::deps::is_package_installed_or_provided(
&item.name,
&installed_set,
&provided_set,
);
if !is_installed {
return false;
}
let has_update = if upgradable_set.contains(&item.name) {
true
} else if !item.version.is_empty() {
let normalized_target_version =
item.version.split('-').next().unwrap_or(&item.version);
crate::logic::deps::get_installed_version(&item.name)
.is_ok_and(|installed_version| normalized_target_version != installed_version)
} else {
false
};
!has_update
})
.cloned()
.collect();
if !installed_packages.is_empty() {
app.modal = crate::state::Modal::ConfirmReinstall {
items: installed_packages,
all_items: packages,
header_chips,
};
return false; }
let upgradable_set = crate::logic::deps::get_upgradable_packages();
let has_versions = packages.iter().any(|item| {
matches!(item.source, crate::state::Source::Official { .. }) && !item.version.is_empty()
});
let has_upgrade_available = packages.iter().any(|item| {
matches!(item.source, crate::state::Source::Official { .. })
&& upgradable_set.contains(&item.name)
});
let has_installed_required_by = packages.iter().any(|item| {
matches!(item.source, crate::state::Source::Official { .. })
&& crate::index::is_installed(&item.name)
&& crate::logic::deps::has_installed_required_by(&item.name)
});
if has_versions && has_upgrade_available && has_installed_required_by {
app.modal = crate::state::Modal::ConfirmBatchUpdate {
items: packages,
dry_run: app.dry_run,
};
return false; }
let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
if let Some(lockout_msg) = crate::logic::faillock::get_lockout_message_if_locked(&username, app)
{
app.modal = crate::state::Modal::Alert {
message: lockout_msg,
};
return false;
}
let settings = crate::theme::settings();
if crate::logic::password::should_use_interactive_auth_handoff(&settings) {
match crate::events::try_interactive_auth_handoff() {
Ok(true) => super::start_execution(
app,
&packages,
crate::state::PreflightAction::Install,
header_chips,
None,
),
Ok(false) => {
app.modal = crate::state::Modal::Alert {
message: crate::i18n::t(app, "app.errors.authentication_failed"),
};
}
Err(e) => {
app.modal = crate::state::Modal::Alert { message: e };
}
}
} else if crate::logic::password::resolve_auth_mode(&settings)
== crate::logic::privilege::AuthMode::PasswordlessOnly
&& crate::logic::password::should_use_passwordless_sudo(&settings)
{
super::start_execution(
app,
&packages,
crate::state::PreflightAction::Install,
header_chips,
None,
);
} else {
app.modal = crate::state::Modal::PasswordPrompt {
purpose: crate::state::modal::PasswordPurpose::Install,
items: packages,
input: crate::state::SecureString::default(),
cursor: 0,
error: None,
};
app.pending_exec_header_chips = Some(header_chips);
}
false
}
pub(super) fn handle_proceed_remove(
app: &mut AppState,
items: Vec<PackageItem>,
mode: crate::state::modal::CascadeMode,
header_chips: crate::state::modal::PreflightHeaderChips,
) -> bool {
app.remove_cascade_mode = mode;
let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
if let Some(lockout_msg) = crate::logic::faillock::get_lockout_message_if_locked(&username, app)
{
app.modal = crate::state::Modal::Alert {
message: lockout_msg,
};
return false;
}
let settings = crate::theme::settings();
if crate::logic::password::should_use_interactive_auth_handoff(&settings) {
match crate::events::try_interactive_auth_handoff() {
Ok(true) => super::start_execution(
app,
&items,
crate::state::PreflightAction::Remove,
header_chips,
None,
),
Ok(false) => {
app.modal = crate::state::Modal::Alert {
message: crate::i18n::t(app, "app.errors.authentication_failed"),
};
}
Err(e) => {
app.modal = crate::state::Modal::Alert { message: e };
}
}
} else {
app.modal = crate::state::Modal::PasswordPrompt {
purpose: crate::state::modal::PasswordPurpose::Remove,
items,
input: crate::state::SecureString::default(),
cursor: 0,
error: None,
};
app.pending_exec_header_chips = Some(header_chips);
}
false
}
#[allow(clippy::too_many_lines)] pub(super) fn handle_p_key(app: &mut AppState) -> bool {
let new_summary: Option<Vec<crate::state::modal::ReverseRootSummary>> = None;
let mut blocked_dep_count: Option<usize> = None;
let mut removal_names: Option<Vec<String>> = None;
let mut removal_mode: Option<crate::state::modal::CascadeMode> = None;
let mut install_targets: Option<Vec<PackageItem>> = None;
let mut service_info_for_plan: Option<Vec<crate::state::modal::ServiceImpact>> = None;
let mut deps_not_resolved_message: Option<String> = None;
{
if let crate::state::Modal::Preflight {
action,
items,
dependency_info,
cascade_mode,
selected_optdepends,
service_info,
..
} = &mut app.modal
{
match action {
crate::state::PreflightAction::Install => {
let packages = extract_install_targets(&*items, selected_optdepends);
install_targets = Some(packages);
}
crate::state::PreflightAction::Remove => {
if dependency_info.is_empty() {
if !cascade_mode.allows_dependents() {
deps_not_resolved_message = Some(
"Warning: Dependencies not resolved yet. Package may have dependents. Switch to Dependencies tab to check."
.to_string(),
);
}
removal_names = Some(items.iter().map(|p| p.name.clone()).collect());
removal_mode = Some(*cascade_mode);
} else if cascade_mode.allows_dependents() {
removal_names = Some(items.iter().map(|p| p.name.clone()).collect());
removal_mode = Some(*cascade_mode);
} else {
blocked_dep_count = Some(dependency_info.len());
}
}
crate::state::PreflightAction::Downgrade => {
}
}
if !service_info.is_empty() {
service_info_for_plan = Some(service_info.clone());
}
}
}
if let Some(msg) = deps_not_resolved_message {
app.toast_message = Some(msg);
app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(5));
}
if let Some(summary) = new_summary {
app.remove_preflight_summary = summary;
}
if let Some(plan) = service_info_for_plan {
app.pending_service_plan = plan;
} else {
app.pending_service_plan.clear();
}
if let Some(packages) = install_targets {
let header_chips = if let crate::state::Modal::Preflight { header_chips, .. } = &app.modal {
header_chips.clone()
} else {
crate::state::modal::PreflightHeaderChips::default()
};
let service_info = if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
service_info.clone()
} else {
Vec::new()
};
crate::events::preflight::modal::close_preflight_modal(app, &service_info);
return handle_proceed_install(app, packages, header_chips);
} else if let Some(names) = removal_names {
let mode = removal_mode.unwrap_or(crate::state::modal::CascadeMode::Basic);
let header_chips = if let crate::state::Modal::Preflight { header_chips, .. } = &app.modal {
header_chips.clone()
} else {
crate::state::modal::PreflightHeaderChips::default()
};
let service_info = if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
service_info.clone()
} else {
Vec::new()
};
let items = if let crate::state::Modal::Preflight { items, .. } = &app.modal {
items
.iter()
.filter(|p| names.contains(&p.name))
.cloned()
.collect()
} else {
Vec::new()
};
crate::events::preflight::modal::close_preflight_modal(app, &service_info);
return handle_proceed_remove(app, items, mode, header_chips);
}
let is_downgrade = if let crate::state::Modal::Preflight { action, .. } = &app.modal {
matches!(action, crate::state::PreflightAction::Downgrade)
} else {
false
};
if is_downgrade {
let (items, _action, header_chips, cascade_mode) = if let crate::state::Modal::Preflight {
action,
items,
header_chips,
cascade_mode,
..
} = &app.modal
{
(items.clone(), *action, header_chips.clone(), *cascade_mode)
} else {
return false;
};
app.remove_cascade_mode = cascade_mode;
let service_info = if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
service_info.clone()
} else {
Vec::new()
};
crate::events::preflight::modal::close_preflight_modal(app, &service_info);
let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
if let Some(lockout_msg) =
crate::logic::faillock::get_lockout_message_if_locked(&username, app)
{
app.modal = crate::state::Modal::Alert {
message: lockout_msg,
};
return false;
}
let settings = crate::theme::settings();
if crate::logic::password::should_use_interactive_auth_handoff(&settings) {
crate::events::spawn_downgrade_in_terminal(app, &items);
} else {
app.modal = crate::state::Modal::PasswordPrompt {
purpose: crate::state::modal::PasswordPurpose::Downgrade,
items,
input: crate::state::SecureString::default(),
cursor: 0,
error: None,
};
app.pending_exec_header_chips = Some(header_chips);
}
return false;
}
if let Some(count) = blocked_dep_count {
let root_list: Vec<String> = app
.remove_preflight_summary
.iter()
.filter(|summary| summary.total_dependents > 0)
.map(|summary| summary.package.clone())
.collect();
let subject = if root_list.is_empty() {
"the selected packages".to_string()
} else {
root_list.join(", ")
};
app.toast_message = Some(format!(
"Removal blocked: {count} dependent package(s) rely on {subject}. Enable cascade removal to proceed."
));
app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(6));
}
false
}
pub(super) fn handle_c_key(app: &mut AppState) -> bool {
app.toast_message = Some(crate::i18n::t(app, "app.toasts.snapshot_placeholder"));
false
}
pub(super) fn handle_q_key(app: &mut AppState) -> bool {
let service_info = if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
service_info.clone()
} else {
Vec::new()
};
close_preflight_modal(app, &service_info);
false
}
pub(super) fn handle_help_key(app: &mut AppState) -> bool {
let help_message = if let crate::state::Modal::Preflight { tab, .. } = &app.modal {
if *tab == crate::state::PreflightTab::Deps {
crate::i18n::t(app, "app.modals.preflight.help.deps_tab")
} else {
crate::i18n::t(app, "app.modals.preflight.help.general")
}
} else {
return false;
};
app.previous_modal = Some(app.modal.clone());
app.modal = crate::state::Modal::Alert {
message: help_message,
};
false
}