1#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
74enum NameMatch {
75 Contains,
76 StartsWith,
77 Exact,
78}
79
80pub fn select_monitors(query: &str, names: &[String]) -> Vec<usize> {
93 let query = query.trim().to_lowercase();
94 if query.is_empty() {
95 return Vec::new();
96 }
97 let ranked: Vec<(NameMatch, usize)> = names
98 .iter()
99 .enumerate()
100 .filter_map(|(i, name)| {
101 let name = name.to_lowercase();
102 if name == query {
103 return Some((NameMatch::Exact, i));
104 }
105 if name.starts_with(&query) {
106 return Some((NameMatch::StartsWith, i));
107 }
108 if name.contains(&query) {
109 return Some((NameMatch::Contains, i));
110 }
111 None
112 })
113 .collect();
114 let Some(best) = ranked.iter().map(|(kind, _)| *kind).max() else {
115 return Vec::new();
116 };
117 ranked
118 .into_iter()
119 .filter(|(kind, _)| *kind == best)
120 .map(|(_, i)| i)
121 .collect()
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 fn win(title: &str, app: &str, z: i32) -> WindowCandidate {
129 WindowCandidate {
130 title: title.into(),
131 app: app.into(),
132 z,
133 }
134 }
135
136 #[test]
137 fn exact_title_beats_substring() {
138 let candidates = [
139 win("Notepad - notes.txt", "Notepad", 5),
140 win("Notepad", "Notepad", 1),
141 ];
142 assert_eq!(select("notepad", &candidates), Some(1));
143 }
144
145 #[test]
146 fn title_match_beats_app_match() {
147 let candidates = [
148 win("Untitled", "Safari", 9),
149 win("Safari release notes", "TextEdit", 1),
150 ];
151 assert_eq!(select("safari", &candidates), Some(1));
152 }
153
154 #[test]
155 fn front_most_wins_within_a_kind() {
156 let candidates = [
157 win("report draft", "Word", 1),
158 win("report final", "Word", 7),
159 win("report old", "Word", 3),
160 ];
161 assert_eq!(select("report", &candidates), Some(1));
162 }
163
164 #[test]
165 fn matching_is_case_insensitive() {
166 let candidates = [win("My APP Window", "Thing", 0)];
167 assert_eq!(select("my app", &candidates), Some(0));
168 }
169
170 #[test]
171 fn no_match_and_empty_query_return_none() {
172 let candidates = [win("Something", "App", 0)];
173 assert_eq!(select("zzz", &candidates), None);
174 assert_eq!(select("", &candidates), None);
175 assert_eq!(select(" ", &candidates), None);
176 assert_eq!(select("x", &[]), None);
177 }
178
179 fn names(list: &[&str]) -> Vec<String> {
180 list.iter().map(|s| (*s).to_string()).collect()
181 }
182
183 #[test]
184 fn monitor_exact_beats_prefix_beats_substring() {
185 let all = names(&["DELL U2723QE Secondary", "DELL U2723QE", "Old DELL U2723QE"]);
186 assert_eq!(select_monitors("dell u2723qe", &all), vec![1]);
187 }
188
189 #[test]
190 fn a_monitor_prefix_beats_a_substring() {
191 let all = names(&["Thunderbolt Display", "Built-in Thunderbolt"]);
192 assert_eq!(select_monitors("thunderbolt", &all), vec![0]);
193 }
194
195 #[test]
196 fn monitor_matching_is_case_insensitive_and_trims() {
197 let all = names(&["Built-in Retina Display"]);
198 assert_eq!(select_monitors(" BUILT-IN ", &all), vec![0]);
199 }
200
201 #[test]
202 fn every_equally_good_monitor_comes_back_so_the_caller_can_refuse() {
203 let all = names(&["DELL U2723QE", "DELL U2723QE"]);
207 assert_eq!(select_monitors("dell", &all), vec![0, 1]);
208 }
209
210 #[test]
211 fn a_better_rank_still_wins_over_several_worse_ones() {
212 let all = names(&["DELL U2723QE Left", "DELL", "DELL U2723QE Right"]);
215 assert_eq!(select_monitors("dell", &all), vec![1]);
216 }
217
218 #[test]
219 fn no_monitor_match_and_empty_query_return_nothing() {
220 let all = names(&["Built-in Retina Display"]);
221 assert!(select_monitors("zzz", &all).is_empty());
222 assert!(select_monitors("", &all).is_empty());
223 assert!(select_monitors(" ", &all).is_empty());
224 assert!(select_monitors("x", &[]).is_empty());
225 }
226}