pacsea/logic/
selection.rs1use tokio::sync::mpsc;
4
5use crate::state::{AppState, PackageItem};
6
7pub fn move_sel_cached(
28 app: &mut AppState,
29 delta: isize,
30 details_tx: &mpsc::UnboundedSender<PackageItem>,
31 comments_tx: &mpsc::UnboundedSender<String>,
32) {
33 if app.results.is_empty() {
34 return;
35 }
36 let len = isize::try_from(app.results.len()).unwrap_or(isize::MAX);
37 let mut idx = isize::try_from(app.selected).unwrap_or(0) + delta;
38 if idx < 0 {
39 idx = 0;
40 }
41 if idx >= len {
42 idx = len - 1;
43 }
44 app.selected = usize::try_from(idx).unwrap_or(0);
45 app.list_state.select(Some(app.selected));
46 if let Some(item) = app.results.get(app.selected).cloned() {
47 app.details_focus = Some(item.name.clone());
49
50 app.details.name.clone_from(&item.name);
52 app.details.version.clone_from(&item.version);
53 app.details.description.clear();
54 match &item.source {
55 crate::state::Source::Official { repo, arch } => {
56 app.details.repository.clone_from(repo);
57 app.details.architecture.clone_from(arch);
58 }
59 crate::state::Source::Aur => {
60 app.details.repository = "AUR".to_string();
61 app.details.architecture = "any".to_string();
62 }
63 }
64
65 if let Some(cached) = app.details_cache.get(&item.name).cloned() {
66 app.details = cached;
67 } else {
68 let _ = details_tx.send(item.clone());
69 }
70
71 if app.pkgb_visible {
73 let needs_reload = app.pkgb_package_name.as_deref() != Some(item.name.as_str());
74 if needs_reload {
75 app.pkgb_reload_requested_at = Some(std::time::Instant::now());
77 app.pkgb_reload_requested_for = Some(item.name.clone());
78 app.pkgb_text = None; }
80 }
81
82 if app.comments_visible && matches!(item.source, crate::state::Source::Aur) {
84 let needs_update = app
85 .comments_package_name
86 .as_deref()
87 .is_none_or(|cached_name| cached_name != item.name.as_str());
88 if needs_update {
89 if app
91 .comments_package_name
92 .as_ref()
93 .is_some_and(|cached_name| {
94 cached_name == &item.name && !app.comments.is_empty()
95 })
96 {
97 app.comments_scroll = 0;
99 } else {
100 app.comments.clear();
102 app.comments_package_name = None;
103 app.comments_fetched_at = None;
104 app.comments_scroll = 0;
105 app.comments_loading = true;
106 app.comments_error = None;
107 let _ = comments_tx.send(item.name.clone());
108 }
109 }
110 }
111 }
112
113 let abs_delta_usize: usize = if delta < 0 {
115 usize::try_from(-delta).unwrap_or(0)
116 } else {
117 usize::try_from(delta).unwrap_or(0)
118 };
119 if abs_delta_usize > 0 {
120 let add = u32::try_from(abs_delta_usize.min(u32::MAX as usize))
121 .expect("value is bounded by u32::MAX");
122 app.scroll_moves = app.scroll_moves.saturating_add(add);
123 }
124 if app.need_ring_prefetch {
125 crate::logic::set_allowed_only_selected(app);
127 app.ring_resume_at =
128 Some(std::time::Instant::now() + std::time::Duration::from_millis(200));
129 return;
130 }
131 if app.scroll_moves > 5 {
132 app.need_ring_prefetch = true;
133 crate::logic::set_allowed_only_selected(app);
134 app.ring_resume_at =
135 Some(std::time::Instant::now() + std::time::Duration::from_millis(200));
136 return;
137 }
138
139 crate::logic::set_allowed_ring(app, 30);
141 crate::logic::ring_prefetch_from_selected(app, details_tx);
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn item_official(name: &str, repo: &str) -> crate::state::PackageItem {
149 crate::state::PackageItem {
150 name: name.to_string(),
151 version: "1.0".to_string(),
152 description: format!("{name} desc"),
153 source: crate::state::Source::Official {
154 repo: repo.to_string(),
155 arch: "x86_64".to_string(),
156 },
157 popularity: None,
158 out_of_date: None,
159 orphaned: false,
160 }
161 }
162
163 #[tokio::test]
164 async fn move_sel_cached_clamps_and_requests_details() {
176 let mut app = crate::state::AppState {
177 results: vec![
178 crate::state::PackageItem {
179 name: "aur1".into(),
180 version: "1".into(),
181 description: String::new(),
182 source: crate::state::Source::Aur,
183 popularity: None,
184 out_of_date: None,
185 orphaned: false,
186 },
187 item_official("pkg2", "core"),
188 ],
189 selected: 0,
190 ..Default::default()
191 };
192
193 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
194 let (comments_tx, _comments_rx) = tokio::sync::mpsc::unbounded_channel();
195 move_sel_cached(&mut app, 1, &tx, &comments_tx);
196 assert_eq!(app.selected, 1);
197 assert_eq!(app.details.repository.to_lowercase(), "core");
198 assert_eq!(app.details.architecture.to_lowercase(), "x86_64");
199 let got = tokio::time::timeout(std::time::Duration::from_millis(50), rx.recv())
200 .await
201 .ok()
202 .flatten();
203 assert!(got.is_some());
204 move_sel_cached(&mut app, -100, &tx, &comments_tx);
205 assert_eq!(app.selected, 0);
206 move_sel_cached(&mut app, 0, &tx, &comments_tx);
207 assert_eq!(app.details.repository, "AUR");
208 assert_eq!(app.details.architecture, "any");
209 }
210
211 #[tokio::test]
212 async fn move_sel_cached_uses_details_cache() {
223 let mut app = crate::state::AppState::default();
224 let pkg = item_official("pkg", "core");
225 app.results = vec![pkg.clone()];
226 app.details_cache.insert(
227 pkg.name.clone(),
228 crate::state::PackageDetails {
229 repository: "core".into(),
230 name: pkg.name.clone(),
231 version: pkg.version.clone(),
232 architecture: "x86_64".into(),
233 ..Default::default()
234 },
235 );
236 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
237 let (comments_tx, _comments_rx) = tokio::sync::mpsc::unbounded_channel();
238 move_sel_cached(&mut app, 0, &tx, &comments_tx);
239 let none = tokio::time::timeout(std::time::Duration::from_millis(30), rx.recv())
240 .await
241 .ok()
242 .flatten();
243 assert!(none.is_none());
244 assert_eq!(app.details.name, "pkg");
245 }
246
247 #[test]
248 fn fast_scroll_sets_gating_and_defers_ring() {
261 let mut app = crate::state::AppState {
262 results: vec![
263 item_official("a", "core"),
264 item_official("b", "extra"),
265 item_official("c", "extra"),
266 item_official("d", "extra"),
267 item_official("e", "extra"),
268 item_official("f", "extra"),
269 item_official("g", "extra"),
270 ],
271 ..Default::default()
272 };
273 let (tx, _rx) = tokio::sync::mpsc::unbounded_channel::<crate::state::PackageItem>();
274 let (comments_tx, _comments_rx) = tokio::sync::mpsc::unbounded_channel();
275 move_sel_cached(&mut app, 6, &tx, &comments_tx);
276 assert!(app.need_ring_prefetch);
277 assert!(app.ring_resume_at.is_some());
278 crate::logic::set_allowed_only_selected(&app);
279 assert!(crate::logic::is_allowed(&app.results[app.selected].name));
280 }
281}