use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tokio::sync::mpsc;
use crate::sources::VoteAction;
use crate::state::{AppState, PackageItem, QueryInput};
use super::super::utils::matches_any;
use super::helpers::{handle_shift_keybinds, navigate_pane};
use super::preflight_helpers::open_preflight_modal;
use crate::events::utils::{char_count, move_sel_cached_with_vote_state, refresh_install_details};
use crate::logic::send_query;
fn handle_character_input(
ch: char,
app: &mut AppState,
query_tx: &mpsc::UnboundedSender<QueryInput>,
) {
if matches!(app.app_mode, crate::state::types::AppMode::News) {
app.news_search_input.push(ch);
app.last_input_change = std::time::Instant::now();
app.last_saved_value = None;
let caret = char_count(&app.news_search_input);
app.news_search_caret = caret;
app.news_search_select_anchor = None;
app.refresh_news_results();
} else {
app.input.push(ch);
app.last_input_change = std::time::Instant::now();
app.last_saved_value = None;
app.search_caret = char_count(&app.input);
app.search_select_anchor = None;
send_query(app, query_tx);
}
}
fn handle_backspace(app: &mut AppState, query_tx: &mpsc::UnboundedSender<QueryInput>) {
if matches!(app.app_mode, crate::state::types::AppMode::News) {
app.news_search_input.pop();
app.last_input_change = std::time::Instant::now();
app.last_saved_value = None;
let caret = char_count(&app.news_search_input);
app.news_search_caret = caret;
app.news_search_select_anchor = None;
app.refresh_news_results();
} else {
app.input.pop();
app.last_input_change = std::time::Instant::now();
app.last_saved_value = None;
app.search_caret = char_count(&app.input);
app.search_select_anchor = None;
send_query(app, query_tx);
}
}
fn handle_navigation_keys(
ke: &KeyEvent,
app: &mut AppState,
details_tx: &mpsc::UnboundedSender<PackageItem>,
comments_tx: &mpsc::UnboundedSender<String>,
) -> bool {
let is_news = matches!(app.app_mode, crate::state::types::AppMode::News);
let km = &app.keymap;
if matches_any(ke, &km.search_move_up) {
if is_news {
crate::events::utils::move_news_selection(app, -1);
} else {
move_sel_cached_with_vote_state(app, -1, details_tx, comments_tx);
}
return true;
}
if matches_any(ke, &km.search_move_down) {
if is_news {
crate::events::utils::move_news_selection(app, 1);
} else {
move_sel_cached_with_vote_state(app, 1, details_tx, comments_tx);
}
return true;
}
if matches_any(ke, &km.search_page_up) {
if is_news {
crate::events::utils::move_news_selection(app, -10);
} else {
move_sel_cached_with_vote_state(app, -10, details_tx, comments_tx);
}
return true;
}
if matches_any(ke, &km.search_page_down) {
if is_news {
crate::events::utils::move_news_selection(app, 10);
} else {
move_sel_cached_with_vote_state(app, 10, details_tx, comments_tx);
}
return true;
}
false
}
fn handle_aur_vote_action(ke: &KeyEvent, app: &mut AppState) -> bool {
if matches!(app.app_mode, crate::state::types::AppMode::News) {
return false;
}
let trigger_pressed = match (ke.code, ke.modifiers) {
(KeyCode::Char('e' | 'E'), mods)
if mods.contains(KeyModifiers::CONTROL) && !mods.contains(KeyModifiers::ALT) =>
{
!mods.contains(KeyModifiers::SHIFT)
}
_ => false,
};
if !trigger_pressed {
return false;
}
let Some(item) = app.results.get(app.selected) else {
return true;
};
if !matches!(item.source, crate::state::Source::Aur) {
app.toast_message = Some("AUR vote is only available for AUR packages.".to_string());
app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
return true;
}
let action = match app.aur_vote_state_by_pkgbase.get(&item.name) {
Some(crate::state::app_state::AurVoteStateUi::Voted) => VoteAction::Unvote,
_ => VoteAction::Vote,
};
let settings = crate::theme::settings();
if !settings.aur_vote_enabled {
app.modal = crate::state::Modal::Alert {
message: "AUR voting is disabled. Enable aur_vote_enabled in settings.conf."
.to_string(),
};
return true;
}
if app.pending_aur_vote_request.is_some() {
app.toast_message = Some("An AUR vote request is already queued.".to_string());
app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
return true;
}
if app.pending_aur_vote_intent.is_some() {
app.toast_message = Some("An AUR vote confirmation is already open.".to_string());
app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
return true;
}
let action_label = match action {
VoteAction::Vote => "vote",
VoteAction::Unvote => "unvote",
};
let dry_run_line = if app.dry_run {
"Dry-run enabled: no remote mutation will be performed."
} else {
"This action will contact aur.archlinux.org over SSH."
};
app.pending_aur_vote_intent = Some((item.name.clone(), action));
app.modal = crate::state::Modal::ConfirmAurVote {
pkgbase: item.name.clone(),
action,
message: format!(
"Confirm AUR {action_label} for '{}'?\n\n{dry_run_line}\n\nPress Enter to continue, Esc/q to cancel.",
item.name
),
};
true
}
pub fn handle_insert_mode(
ke: KeyEvent,
app: &mut AppState,
query_tx: &mpsc::UnboundedSender<QueryInput>,
details_tx: &mpsc::UnboundedSender<PackageItem>,
add_tx: &mpsc::UnboundedSender<PackageItem>,
preview_tx: &mpsc::UnboundedSender<PackageItem>,
comments_tx: &mpsc::UnboundedSender<String>,
) -> bool {
if handle_shift_keybinds(&ke, app) {
return false;
}
if handle_aur_vote_action(&ke, app) {
return false;
}
match (ke.code, ke.modifiers) {
(KeyCode::Char('c'), KeyModifiers::CONTROL) => return true,
(c, m)
if {
let km = &app.keymap;
matches_any(&ke, &km.pane_next) && (c, m) == (ke.code, ke.modifiers)
} =>
{
if matches!(app.app_mode, crate::state::types::AppMode::News) {
app.focus = crate::state::Focus::Install;
} else {
if app.installed_only_mode {
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));
}
app.focus = crate::state::Focus::Install;
crate::events::utils::refresh_downgrade_details(app, details_tx);
} else {
if app.install_state.selected().is_none() && !app.install_list.is_empty() {
app.install_state.select(Some(0));
}
app.focus = crate::state::Focus::Install;
refresh_install_details(app, details_tx);
}
}
}
(KeyCode::Right, _) => {
navigate_pane(app, "right", details_tx, preview_tx);
}
(KeyCode::Left, _) => {
navigate_pane(app, "left", details_tx, preview_tx);
}
(KeyCode::Char(' '), KeyModifiers::CONTROL) => {
if app.installed_only_mode
&& let Some(item) = app.results.get(app.selected).cloned()
{
crate::logic::add_to_downgrade_list(app, item);
crate::events::utils::refresh_downgrade_details(app, details_tx);
}
}
(KeyCode::Char(' '), _) => {
if let Some(item) = app.results.get(app.selected).cloned() {
if app.installed_only_mode {
crate::logic::add_to_remove_list(app, item);
crate::events::utils::refresh_remove_details(app, details_tx);
} else {
let _ = add_tx.send(item);
}
}
}
(KeyCode::Backspace, _) => {
handle_backspace(app, query_tx);
}
(KeyCode::Char('\n' | '\r') | KeyCode::Enter, m) => {
if m.contains(KeyModifiers::CONTROL) {
tracing::debug!(
"[InsertMode] Enter with Ctrl detected, ignoring (likely Ctrl+M interpreted as Enter)"
);
return false;
}
if let Some(item) = app.results.get(app.selected).cloned() {
tracing::debug!(
"[InsertMode] Enter pressed, opening preflight for package: {}",
item.name
);
open_preflight_modal(app, vec![item], false);
}
}
(KeyCode::Char(ch), m) if m.is_empty() => {
handle_character_input(ch, app, query_tx);
}
_ => {
if handle_navigation_keys(&ke, app, details_tx, comments_tx) {
} else {
let km = &app.keymap;
if matches_any(&ke, &km.search_insert_clear) {
if matches!(app.app_mode, crate::state::types::AppMode::News) {
if !app.news_search_input.is_empty() {
app.news_search_input.clear();
app.news_search_caret = 0;
app.news_search_select_anchor = None;
app.last_input_change = std::time::Instant::now();
app.last_saved_value = None;
app.refresh_news_results();
}
} else if !app.input.is_empty() {
app.input.clear();
app.search_caret = 0;
app.search_select_anchor = None;
app.last_input_change = std::time::Instant::now();
app.last_saved_value = None;
send_query(app, query_tx);
}
}
}
}
}
false
}