1use crate::dto::LauncherCatalogEntry;
11
12const DEFAULT_LIMIT: usize = 50;
13
14#[must_use]
15pub fn search_entries(
16 entries: &[LauncherCatalogEntry],
17 query: &str,
18 limit: Option<usize>,
19) -> Vec<LauncherCatalogEntry> {
20 let limit = limit.unwrap_or(DEFAULT_LIMIT).max(1);
21 let query = query.trim();
22 if query.is_empty() {
23 return entries.iter().take(limit).cloned().collect();
24 }
25 let needle = query.to_ascii_lowercase();
26
27 let mut scored: Vec<(u8, usize, &LauncherCatalogEntry)> = entries
28 .iter()
29 .enumerate()
30 .filter_map(|(index, entry)| match_rank(entry, &needle).map(|rank| (rank, index, entry)))
31 .collect();
32 scored.sort_by_key(|(rank, index, _)| (*rank, *index));
33 scored
34 .into_iter()
35 .take(limit)
36 .map(|(_, _, entry)| entry.clone())
37 .collect()
38}
39
40fn match_rank(entry: &LauncherCatalogEntry, needle: &str) -> Option<u8> {
42 if entry.id.to_ascii_lowercase().contains(needle) {
43 return Some(0);
44 }
45 if entry.title.to_ascii_lowercase().contains(needle) {
46 return Some(1);
47 }
48 if entry
49 .category
50 .as_deref()
51 .is_some_and(|category| category.to_ascii_lowercase().contains(needle))
52 {
53 return Some(2);
54 }
55 if entry.description.to_ascii_lowercase().contains(needle) {
56 return Some(3);
57 }
58 None
59}
60
61#[cfg(test)]
62#[path = "search_tests.rs"]
63mod tests;