git.lucas.co / cce-window-manager
window management library
git clone https://git.lucas.co/cce-window-manager.git

src/query.rs (3.1K)

 1 // Window-query resolution: how a user-supplied query string (ccectl
 2 // focus-window / center-window, window-stream subscriptions) picks a window.
 3 //
 4 // The candidate list is the mechanism's job: it passes only MAPPED windows,
 5 // in window order. This module owns just the matching rules.
 6 
 7 /// A mapped window's identity, as query resolution sees it.
 8 #[derive(Debug, Clone)]
 9 pub struct QueryCandidate {
10     /// The numeric window id users see (the slotmap key index).
11     pub index: u32,
12     pub app_id: Option<String>,
13 }
14 
15 /// Resolve a query to a position in `candidates`, or None.
16 ///
17 /// An all-numeric query is tried as an exact window id first. Failing that
18 /// (non-numeric, or no window has that id), it is matched case-insensitively
19 /// against app_ids: an exact match beats a substring match, and the first
20 /// window at the best score wins.
21 pub fn find_window(candidates: &[QueryCandidate], query: &str) -> Option<usize> {
22     let query = query.to_lowercase();
23 
24     if let Ok(id) = query.parse::<u32>() {
25         if let Some(pos) = candidates.iter().position(|c| c.index == id) {
26             return Some(pos);
27         }
28     }
29 
30     let mut best: Option<usize> = None;
31     let mut best_score = 0;
32     for (pos, c) in candidates.iter().enumerate() {
33         let Some(aid) = &c.app_id else { continue };
34         let aid = aid.to_lowercase();
35         let score = if aid == query {
36             100
37         } else if aid.contains(&query) {
38             50
39         } else {
40             0
41         };
42         if score > best_score {
43             best_score = score;
44             best = Some(pos);
45         }
46     }
47     best
48 }
49 
50 #[cfg(test)]
51 mod tests {
52     use super::*;
53 
54     fn cand(index: u32, app_id: Option<&str>) -> QueryCandidate {
55         QueryCandidate { index, app_id: app_id.map(str::to_string) }
56     }
57 
58     #[test]
59     fn numeric_query_matches_window_id_first() {
60         let c = [cand(3, Some("2048-game")), cand(7, None)];
61         // "7" is window id 7, even though nothing app_id-matches it.
62         assert_eq!(find_window(&c, "7"), Some(1));
63         // "3" is window id 3, beating the app_id containing "3"... (none here)
64         assert_eq!(find_window(&c, "3"), Some(0));
65     }
66 
67     #[test]
68     fn numeric_query_without_id_match_falls_to_app_ids() {
69         // No window has id 2048, but an app_id contains "2048".
70         let c = [cand(1, Some("2048-game"))];
71         assert_eq!(find_window(&c, "2048"), Some(0));
72     }
73 
74     #[test]
75     fn exact_beats_substring_and_first_best_wins() {
76         let c = [
77             cand(1, Some("cce-mail-helper")),
78             cand(2, Some("cce-mail")),
79             cand(3, Some("cce-mail")),
80         ];
81         // Exact match at position 1 outranks the earlier substring match;
82         // the later equal-score exact match doesn't displace it.
83         assert_eq!(find_window(&c, "CCE-Mail"), Some(1));
84         // Pure substring query: first container wins.
85         assert_eq!(find_window(&c, "mail"), Some(0));
86     }
87 
88     #[test]
89     fn no_match_is_none() {
90         let c = [cand(1, Some("cce-files")), cand(2, None)];
91         assert_eq!(find_window(&c, "firefox"), None);
92         assert_eq!(find_window(&[], "anything"), None);
93     }
94 }