pixelcoords_core/
matcher.rs1#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct WindowCandidate {
14 pub title: String,
15 pub app: String,
16 pub z: i32,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21enum MatchKind {
22 AppContains,
23 AppExact,
24 TitleContains,
25 TitleStartsWith,
26 TitleExact,
27}
28
29fn match_kind(query: &str, candidate: &WindowCandidate) -> Option<MatchKind> {
30 let q = query.to_lowercase();
31 let title = candidate.title.to_lowercase();
32 let app = candidate.app.to_lowercase();
33 if title == q {
36 return Some(MatchKind::TitleExact);
37 }
38 if title.starts_with(&q) {
39 return Some(MatchKind::TitleStartsWith);
40 }
41 if title.contains(&q) {
42 return Some(MatchKind::TitleContains);
43 }
44 if app == q {
45 return Some(MatchKind::AppExact);
46 }
47 if app.contains(&q) {
48 return Some(MatchKind::AppContains);
49 }
50 None
51}
52
53pub fn select(query: &str, candidates: &[WindowCandidate]) -> Option<usize> {
57 let query = query.trim();
58 if query.is_empty() {
59 return None;
60 }
61 candidates
62 .iter()
63 .enumerate()
64 .filter_map(|(i, c)| {
65 match_kind(query, c).map(|kind| ((kind, c.z, std::cmp::Reverse(i)), i))
66 })
67 .max_by_key(|(key, _)| *key)
68 .map(|(_, i)| i)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 fn win(title: &str, app: &str, z: i32) -> WindowCandidate {
76 WindowCandidate {
77 title: title.into(),
78 app: app.into(),
79 z,
80 }
81 }
82
83 #[test]
84 fn exact_title_beats_substring() {
85 let candidates = [
86 win("Notepad - notes.txt", "Notepad", 5),
87 win("Notepad", "Notepad", 1),
88 ];
89 assert_eq!(select("notepad", &candidates), Some(1));
90 }
91
92 #[test]
93 fn title_match_beats_app_match() {
94 let candidates = [
95 win("Untitled", "Safari", 9),
96 win("Safari release notes", "TextEdit", 1),
97 ];
98 assert_eq!(select("safari", &candidates), Some(1));
99 }
100
101 #[test]
102 fn front_most_wins_within_a_kind() {
103 let candidates = [
104 win("report draft", "Word", 1),
105 win("report final", "Word", 7),
106 win("report old", "Word", 3),
107 ];
108 assert_eq!(select("report", &candidates), Some(1));
109 }
110
111 #[test]
112 fn matching_is_case_insensitive() {
113 let candidates = [win("My APP Window", "Thing", 0)];
114 assert_eq!(select("my app", &candidates), Some(0));
115 }
116
117 #[test]
118 fn no_match_and_empty_query_return_none() {
119 let candidates = [win("Something", "App", 0)];
120 assert_eq!(select("zzz", &candidates), None);
121 assert_eq!(select("", &candidates), None);
122 assert_eq!(select(" ", &candidates), None);
123 assert_eq!(select("x", &[]), None);
124 }
125}