pacsea/ui/helpers/query.rs
1//! Query resolution and preview utilities.
2//!
3//! This module provides functions for resolving query strings to packages and triggering
4//! asynchronous preview fetches.
5
6use super::filter::filtered_recent_indices;
7use crate::state::AppState;
8
9/// What: Resolve a free-form query string to a best-effort matching package.
10///
11/// Inputs:
12/// - `q`: Query string to resolve
13///
14/// Output:
15/// - `Some(PackageItem)` per the priority rules below; `None` if nothing usable is found.
16///
17/// Details (selection priority):
18/// 1) Exact-name match from the official index;
19/// 2) Exact-name match from AUR;
20/// 3) First official result;
21/// 4) Otherwise, first AUR result.
22///
23/// Performs network I/O for AUR; tolerates errors.
24pub async fn fetch_first_match_for_query(q: String) -> Option<crate::state::PackageItem> {
25 // Prefer exact match from official index, then from AUR, else first official, then first AUR
26 // Use normal substring search for this helper (not fuzzy)
27 let official_results = crate::index::search_official(&q, false);
28 let official: Vec<crate::state::PackageItem> =
29 official_results.into_iter().map(|(item, _)| item).collect();
30 if let Some(off) = official
31 .iter()
32 .find(|it| it.name.eq_ignore_ascii_case(&q))
33 .cloned()
34 {
35 return Some(off);
36 }
37 let (aur, _errors) = crate::sources::fetch_all_with_errors(q.clone()).await;
38 if let Some(a) = aur
39 .iter()
40 .find(|it| it.name.eq_ignore_ascii_case(&q))
41 .cloned()
42 {
43 return Some(a);
44 }
45 if let Some(off) = official.first().cloned() {
46 return Some(off);
47 }
48 aur.into_iter().next()
49}
50
51/// What: Trigger an asynchronous preview fetch for the selected Recent query when applicable.
52///
53/// Inputs:
54/// - `app`: Application state (focus, selection, recent list)
55/// - `preview_tx`: Channel to send the preview `PackageItem`
56///
57/// Output:
58/// - Spawns a task to resolve and send a preview item; no return payload; exits early when inapplicable.
59///
60/// Details:
61/// - Requires: focus on Recent, a valid selection within the filtered view, and a query string present.
62/// - Resolves via [`fetch_first_match_for_query`] and sends over `preview_tx`; ignores send errors.
63pub fn trigger_recent_preview(
64 app: &AppState,
65 preview_tx: &tokio::sync::mpsc::UnboundedSender<crate::state::PackageItem>,
66) {
67 if !matches!(app.focus, crate::state::Focus::Recent)
68 || matches!(app.app_mode, crate::state::types::AppMode::News)
69 {
70 return;
71 }
72 let Some(idx) = app.history_state.selected() else {
73 return;
74 };
75 let inds = filtered_recent_indices(app);
76 if idx >= inds.len() {
77 return;
78 }
79 let Some(q) = app.recent_value_at(inds[idx]) else {
80 return;
81 };
82 let tx = preview_tx.clone();
83 tokio::spawn(async move {
84 if let Some(item) = fetch_first_match_for_query(q).await {
85 let _ = tx.send(item);
86 }
87 });
88}