use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tokio::sync::mpsc;
use crate::sources::parse_news_html;
use crate::state::{AppState, PackageItem};
use super::utils::{
find_in_install, matches_any, refresh_install_details, refresh_remove_details,
refresh_selected_details,
};
mod aur_dup_warn;
mod preflight;
#[cfg(test)]
mod tests;
pub use aur_dup_warn::try_open_warn_aur_repo_duplicate_modal;
pub use preflight::{
open_preflight_downgrade_modal, open_preflight_install_modal, open_preflight_remove_modal,
};
fn handle_pane_next_navigation(
app: &mut AppState,
details_tx: &mpsc::UnboundedSender<PackageItem>,
preview_tx: &mpsc::UnboundedSender<PackageItem>,
) {
if app.installed_only_mode {
match app.right_pane_focus {
crate::state::RightPaneFocus::Downgrade => {
app.right_pane_focus = crate::state::RightPaneFocus::Remove;
if app.remove_state.selected().is_none() && !app.remove_list.is_empty() {
app.remove_state.select(Some(0));
}
refresh_remove_details(app, details_tx);
return;
}
crate::state::RightPaneFocus::Remove => {
if app.history_state.selected().is_none() && !app.recent.is_empty() {
app.history_state.select(Some(0));
}
app.focus = crate::state::Focus::Recent;
crate::ui::helpers::trigger_recent_preview(app, preview_tx);
return;
}
crate::state::RightPaneFocus::Install => {}
}
}
if app.history_state.selected().is_none() && !app.recent.is_empty() {
app.history_state.select(Some(0));
}
app.focus = crate::state::Focus::Recent;
crate::ui::helpers::trigger_recent_preview(app, preview_tx);
}
fn handle_left_arrow_navigation(
app: &mut AppState,
details_tx: &mpsc::UnboundedSender<PackageItem>,
) {
if app.installed_only_mode {
match app.right_pane_focus {
crate::state::RightPaneFocus::Remove => {
app.right_pane_focus = crate::state::RightPaneFocus::Downgrade;
if app.downgrade_state.selected().is_none() && !app.downgrade_list.is_empty() {
app.downgrade_state.select(Some(0));
}
super::utils::refresh_downgrade_details(app, details_tx);
}
crate::state::RightPaneFocus::Downgrade => {
app.focus = crate::state::Focus::Search;
refresh_selected_details(app, details_tx);
}
crate::state::RightPaneFocus::Install => {
app.focus = crate::state::Focus::Search;
refresh_selected_details(app, details_tx);
}
}
} else {
app.focus = crate::state::Focus::Search;
refresh_selected_details(app, details_tx);
}
}
fn ensure_news_bookmark_selection(app: &mut AppState) {
let len = app.news_bookmarks.len();
if len == 0 {
app.install_state.select(None);
return;
}
let sel = app
.install_state
.selected()
.filter(|i| *i < len)
.unwrap_or(0);
app.install_state.select(Some(sel));
}
fn move_news_bookmark_selection(app: &mut AppState, down: bool) {
ensure_news_bookmark_selection(app);
let len = app.news_bookmarks.len();
if len == 0 {
return;
}
let current = app.install_state.selected().unwrap_or(0);
let next = if down {
std::cmp::min(current + 1, len.saturating_sub(1))
} else {
current.saturating_sub(1)
};
app.install_state.select(Some(next));
}
fn delete_news_bookmark_at_selection(app: &mut AppState) {
ensure_news_bookmark_selection(app);
let Some(sel) = app.install_state.selected() else {
return;
};
if let Some(removed) = app.remove_news_bookmark_at(sel)
&& let Some(path) = removed.html_path
{
let _ = std::fs::remove_file(path);
}
let len = app.news_bookmarks.len();
if len == 0 {
app.install_state.select(None);
} else if sel >= len {
app.install_state.select(Some(len.saturating_sub(1)));
}
}
fn load_news_bookmark(app: &mut AppState) {
ensure_news_bookmark_selection(app);
let Some(sel) = app.install_state.selected() else {
return;
};
let Some(bookmark) = app.news_bookmarks.get(sel).cloned() else {
return;
};
let idx = if let Some(pos) = app
.news_results
.iter()
.position(|it| it.id == bookmark.item.id)
{
pos
} else {
app.news_results.insert(0, bookmark.item.clone());
0
};
app.news_selected = idx;
app.news_list_state.select(Some(idx));
let mut content = bookmark.content.clone();
if content.is_none()
&& let Some(path) = bookmark.html_path.as_ref()
&& let Ok(html) = std::fs::read_to_string(path)
{
content = Some(parse_news_html(&html));
}
if let Some(url) = bookmark.item.url.as_ref() {
if let Some(ref c) = content {
app.news_content_cache.insert(url.clone(), c.clone());
}
app.details.url.clone_from(url);
} else {
app.details.url.clear();
}
app.news_content = content;
app.news_content_loading = false;
}
fn handle_news_bookmarks_key(ke: KeyEvent, app: &mut AppState) -> bool {
match ke.code {
KeyCode::Char('j') | KeyCode::Down => {
move_news_bookmark_selection(app, true);
}
KeyCode::Char('k') | KeyCode::Up => {
move_news_bookmark_selection(app, false);
}
code if matches_any(&ke, &app.keymap.pane_next) && code == ke.code => {
if app.history_state.selected().is_none() && !app.news_recent_values().is_empty() {
app.history_state.select(Some(0));
}
app.focus = crate::state::Focus::Recent;
}
KeyCode::Enter | KeyCode::Char('\n' | '\r') => {
load_news_bookmark(app);
}
KeyCode::Delete => {
delete_news_bookmark_at_selection(app);
}
KeyCode::Esc | KeyCode::Left => {
app.focus = crate::state::Focus::Search;
app.search_normal_mode = true;
}
KeyCode::Right => {
app.focus = crate::state::Focus::Recent;
}
_ => {}
}
false
}
fn handle_right_arrow_navigation(
app: &mut AppState,
details_tx: &mpsc::UnboundedSender<PackageItem>,
preview_tx: &mpsc::UnboundedSender<PackageItem>,
) {
if app.installed_only_mode {
match app.right_pane_focus {
crate::state::RightPaneFocus::Downgrade => {
app.right_pane_focus = crate::state::RightPaneFocus::Remove;
if app.remove_state.selected().is_none() && !app.remove_list.is_empty() {
app.remove_state.select(Some(0));
}
refresh_remove_details(app, details_tx);
}
crate::state::RightPaneFocus::Remove => {
if app.history_state.selected().is_none() && !app.recent.is_empty() {
app.history_state.select(Some(0));
}
app.focus = crate::state::Focus::Recent;
crate::ui::helpers::trigger_recent_preview(app, preview_tx);
}
crate::state::RightPaneFocus::Install => {
if app.history_state.selected().is_none() && !app.recent.is_empty() {
app.history_state.select(Some(0));
}
app.focus = crate::state::Focus::Recent;
crate::ui::helpers::trigger_recent_preview(app, preview_tx);
}
}
} else {
if app.history_state.selected().is_none() && !app.recent.is_empty() {
app.history_state.select(Some(0));
}
app.focus = crate::state::Focus::Recent;
crate::ui::helpers::trigger_recent_preview(app, preview_tx);
}
}
pub fn handle_install_key(
ke: KeyEvent,
app: &mut AppState,
details_tx: &mpsc::UnboundedSender<PackageItem>,
preview_tx: &mpsc::UnboundedSender<PackageItem>,
_add_tx: &mpsc::UnboundedSender<PackageItem>,
) -> bool {
if matches!(app.app_mode, crate::state::types::AppMode::News) {
return handle_news_bookmarks_key(ke, app);
}
if ke.code == KeyCode::Char('c') && ke.modifiers.contains(KeyModifiers::CONTROL) {
return true;
}
if app.pane_find.is_some() && handle_pane_find_mode(ke, app, details_tx) {
return false;
}
if crate::events::search::handle_shift_keybinds(&ke, app) {
return false;
}
let km = &app.keymap;
let matches_any = |list: &Vec<crate::theme::KeyChord>| {
list.iter().any(|c| {
if (c.code, c.mods) == (ke.code, ke.modifiers) {
return true;
}
match (c.code, ke.code) {
(
crossterm::event::KeyCode::Char(cfg_ch),
crossterm::event::KeyCode::Char(ev_ch),
) => {
let cfg_has_shift = c.mods.contains(crossterm::event::KeyModifiers::SHIFT);
let ev_has_no_shift =
!ke.modifiers.contains(crossterm::event::KeyModifiers::SHIFT);
cfg_has_shift && ev_has_no_shift && ev_ch == cfg_ch.to_ascii_uppercase()
}
_ => false,
}
})
};
match ke.code {
KeyCode::Char('j') => {
handle_navigation_down(app, details_tx);
}
KeyCode::Char('k') => {
handle_navigation_up(app, details_tx);
}
KeyCode::Char('/') => {
app.pane_find = Some(String::new());
}
KeyCode::Enter | KeyCode::Char('\n' | '\r') => {
handle_enter_key(app);
}
KeyCode::Esc => {
app.focus = crate::state::Focus::Search;
app.search_normal_mode = true;
refresh_selected_details(app, details_tx);
}
code if matches_any(&km.pane_next) && code == ke.code => {
if matches!(app.app_mode, crate::state::types::AppMode::News) {
app.focus = crate::state::Focus::Recent;
} else {
handle_pane_next_navigation(app, details_tx, preview_tx);
}
}
KeyCode::Left => {
if matches!(app.app_mode, crate::state::types::AppMode::News) {
app.focus = crate::state::Focus::Search;
} else {
handle_left_arrow_navigation(app, details_tx);
}
}
KeyCode::Right => {
if matches!(app.app_mode, crate::state::types::AppMode::News) {
app.focus = crate::state::Focus::Recent;
} else {
handle_right_arrow_navigation(app, details_tx, preview_tx);
}
}
KeyCode::Delete if !ke.modifiers.contains(KeyModifiers::SHIFT) => {
handle_delete_item(app, details_tx);
}
code if matches_any(&km.install_clear) && code == ke.code => {
handle_clear_list(app);
}
code if matches_any(&km.install_remove) && code == ke.code => {
handle_delete_item(app, details_tx);
}
KeyCode::Up => {
handle_navigation_up(app, details_tx);
}
KeyCode::Down => {
handle_navigation_down(app, details_tx);
}
_ => {}
}
false
}
fn handle_pane_find_mode(
ke: KeyEvent,
app: &mut AppState,
details_tx: &mpsc::UnboundedSender<PackageItem>,
) -> bool {
match ke.code {
KeyCode::Enter | KeyCode::Char('\n' | '\r') => {
find_in_install(app, true);
refresh_install_details(app, details_tx);
}
KeyCode::Esc => {
app.pane_find = None;
}
KeyCode::Backspace => {
if let Some(buf) = &mut app.pane_find {
buf.pop();
}
}
KeyCode::Char(ch) => {
if let Some(buf) = &mut app.pane_find {
buf.push(ch);
}
}
_ => {}
}
true
}
fn handle_enter_key(app: &mut AppState) {
let skip_preflight_for_modals = matches!(
app.modal,
crate::state::Modal::SystemUpdate { .. }
| crate::state::Modal::OptionalDeps { .. }
| crate::state::Modal::Repositories { .. }
);
let skip = crate::theme::settings().skip_preflight || skip_preflight_for_modals;
if !app.installed_only_mode && !app.install_list.is_empty() {
if skip {
let install_snapshot = app.install_list.clone();
if try_open_warn_aur_repo_duplicate_modal(
app,
&install_snapshot,
crate::state::modal::PreflightHeaderChips::default(),
) {
return;
}
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> = app
.install_list
.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() {
let has_versions = app.install_list.iter().any(|item| {
matches!(item.source, crate::state::Source::Official { .. })
&& !item.version.is_empty()
});
let has_upgrade_available = app.install_list.iter().any(|item| {
matches!(item.source, crate::state::Source::Official { .. })
&& upgradable_set.contains(&item.name)
});
let has_installed_required_by = app.install_list.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: app.install_list.clone(),
dry_run: app.dry_run,
};
} else {
let items = app.install_list.clone();
crate::install::start_integrated_install_all(app, &items, app.dry_run);
app.toast_message = Some(crate::i18n::t(
app,
"app.toasts.installing_preflight_skipped",
));
app.toast_expires_at =
Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
}
} else {
app.modal = crate::state::Modal::ConfirmReinstall {
items: installed_packages,
all_items: app.install_list.clone(),
header_chips: crate::state::modal::PreflightHeaderChips::default(),
};
}
} else {
open_preflight_install_modal(app);
}
} else if app.installed_only_mode
&& matches!(app.right_pane_focus, crate::state::RightPaneFocus::Remove)
{
if !app.remove_list.is_empty() {
if skip {
let names: Vec<String> = app.remove_list.iter().map(|p| p.name.clone()).collect();
crate::install::start_integrated_remove_all(
app,
&names,
app.dry_run,
app.remove_cascade_mode,
);
app.toast_message =
Some(crate::i18n::t(app, "app.toasts.removing_preflight_skipped"));
app.toast_expires_at =
Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
app.remove_list.clear();
app.remove_list_names.clear();
app.remove_state.select(None);
} else {
open_preflight_remove_modal(app);
}
}
} else if app.installed_only_mode
&& matches!(
app.right_pane_focus,
crate::state::RightPaneFocus::Downgrade
)
&& !app.downgrade_list.is_empty()
{
if skip {
let names: Vec<String> = app.downgrade_list.iter().map(|p| p.name.clone()).collect();
let joined = names.join(" ");
let tool = match crate::logic::privilege::active_tool() {
Ok(t) => t,
Err(msg) => {
app.toast_message = Some(msg);
app.toast_expires_at =
Some(std::time::Instant::now() + std::time::Duration::from_secs(8));
return;
}
};
let bin = tool.binary_name();
let cmd = if app.dry_run {
format!("echo DRY RUN: {bin} downgrade {joined}")
} else {
format!(
"if (command -v downgrade >/dev/null 2>&1) || {bin} pacman -Qi downgrade >/dev/null 2>&1; then {bin} downgrade {joined}; else echo 'downgrade tool not found. Install \"downgrade\" package.'; fi"
)
};
crate::install::spawn_shell_commands_in_terminal(&[cmd]);
app.downgrade_list.clear();
app.downgrade_list_names.clear();
app.downgrade_state.select(None);
} else {
open_preflight_downgrade_modal(app);
}
}
}
fn handle_navigation_down(app: &mut AppState, details_tx: &mpsc::UnboundedSender<PackageItem>) {
if !app.installed_only_mode
|| matches!(app.right_pane_focus, crate::state::RightPaneFocus::Install)
{
let inds = crate::ui::helpers::filtered_install_indices(app);
if inds.is_empty() {
return;
}
let sel = app.install_state.selected().unwrap_or(0);
let max = inds.len().saturating_sub(1);
let new = std::cmp::min(sel + 1, max);
app.install_state.select(Some(new));
refresh_install_details(app, details_tx);
} else if matches!(app.right_pane_focus, crate::state::RightPaneFocus::Remove) {
let len = app.remove_list.len();
if len == 0 {
return;
}
let sel = app.remove_state.selected().unwrap_or(0);
let max = len.saturating_sub(1);
let new = std::cmp::min(sel + 1, max);
app.remove_state.select(Some(new));
refresh_remove_details(app, details_tx);
} else if matches!(
app.right_pane_focus,
crate::state::RightPaneFocus::Downgrade
) {
let len = app.downgrade_list.len();
if len == 0 {
return;
}
let sel = app.downgrade_state.selected().unwrap_or(0);
let max = len.saturating_sub(1);
let new = std::cmp::min(sel + 1, max);
app.downgrade_state.select(Some(new));
super::utils::refresh_downgrade_details(app, details_tx);
}
}
fn handle_navigation_up(app: &mut AppState, details_tx: &mpsc::UnboundedSender<PackageItem>) {
if !app.installed_only_mode
|| matches!(app.right_pane_focus, crate::state::RightPaneFocus::Install)
{
let inds = crate::ui::helpers::filtered_install_indices(app);
if inds.is_empty() {
return;
}
if let Some(sel) = app.install_state.selected() {
let new = sel.saturating_sub(1);
app.install_state.select(Some(new));
refresh_install_details(app, details_tx);
}
} else if matches!(app.right_pane_focus, crate::state::RightPaneFocus::Remove) {
if let Some(sel) = app.remove_state.selected() {
let new = sel.saturating_sub(1);
app.remove_state.select(Some(new));
refresh_remove_details(app, details_tx);
}
} else if matches!(
app.right_pane_focus,
crate::state::RightPaneFocus::Downgrade
) && let Some(sel) = app.downgrade_state.selected()
{
let new = sel.saturating_sub(1);
app.downgrade_state.select(Some(new));
super::utils::refresh_downgrade_details(app, details_tx);
}
}
fn handle_delete_item(app: &mut AppState, details_tx: &mpsc::UnboundedSender<PackageItem>) {
if app.installed_only_mode {
match app.right_pane_focus {
crate::state::RightPaneFocus::Downgrade => {
if let Some(sel) = app.downgrade_state.selected()
&& sel < app.downgrade_list.len()
{
if let Some(removed_item) = app.downgrade_list.get(sel) {
app.downgrade_list_names
.remove(&removed_item.name.to_lowercase());
}
app.downgrade_list.remove(sel);
let len = app.downgrade_list.len();
if len == 0 {
app.downgrade_state.select(None);
} else {
let new_sel = sel.min(len - 1);
app.downgrade_state.select(Some(new_sel));
super::utils::refresh_downgrade_details(app, details_tx);
}
}
}
crate::state::RightPaneFocus::Remove => {
if let Some(sel) = app.remove_state.selected()
&& sel < app.remove_list.len()
{
if let Some(removed_item) = app.remove_list.get(sel) {
app.remove_list_names
.remove(&removed_item.name.to_lowercase());
}
app.remove_list.remove(sel);
let len = app.remove_list.len();
if len == 0 {
app.remove_state.select(None);
} else {
let new_sel = sel.min(len - 1);
app.remove_state.select(Some(new_sel));
refresh_remove_details(app, details_tx);
}
}
}
crate::state::RightPaneFocus::Install => {
delete_from_install_list(app, details_tx);
}
}
} else {
delete_from_install_list(app, details_tx);
}
}
fn delete_from_install_list(app: &mut AppState, details_tx: &mpsc::UnboundedSender<PackageItem>) {
let inds = crate::ui::helpers::filtered_install_indices(app);
if inds.is_empty() {
return;
}
if let Some(vsel) = app.install_state.selected() {
let i = inds.get(vsel).copied().unwrap_or(0);
if i < app.install_list.len() {
if let Some(removed_item) = app.install_list.get(i) {
app.install_list_names
.remove(&removed_item.name.to_lowercase());
}
app.install_list.remove(i);
app.install_dirty = true;
app.install_list_deps.clear();
app.install_list_files.clear();
app.deps_resolving = false;
app.files_resolving = false;
let vis_len = inds.len().saturating_sub(1); if vis_len == 0 {
app.install_state.select(None);
} else {
let new_sel = if vsel >= vis_len { vis_len - 1 } else { vsel };
app.install_state.select(Some(new_sel));
refresh_install_details(app, details_tx);
}
}
}
}
fn handle_clear_list(app: &mut AppState) {
if app.installed_only_mode {
match app.right_pane_focus {
crate::state::RightPaneFocus::Downgrade => {
app.downgrade_list.clear();
app.downgrade_list_names.clear();
app.downgrade_state.select(None);
}
crate::state::RightPaneFocus::Remove => {
app.remove_list.clear();
app.remove_list_names.clear();
app.remove_state.select(None);
}
crate::state::RightPaneFocus::Install => {
app.install_list.clear();
app.install_list_names.clear();
app.install_state.select(None);
app.install_dirty = true;
app.install_list_deps.clear();
app.install_list_files.clear();
app.deps_resolving = false;
app.files_resolving = false;
}
}
} else {
app.install_list.clear();
app.install_list_names.clear();
app.install_state.select(None);
app.install_dirty = true;
app.install_list_deps.clear();
app.deps_resolving = false;
}
}