open_sesame/core/
matcher.rs1use crate::core::hint::WindowHint;
6use crate::core::window::WindowId;
7
8#[derive(Debug, Clone, PartialEq)]
27pub enum MatchResult {
28 None,
30 Partial(Vec<usize>),
32 Exact {
34 index: usize,
36 window_id: WindowId,
38 },
39}
40
41impl MatchResult {
42 pub fn is_exact(&self) -> bool {
44 matches!(self, MatchResult::Exact { .. })
45 }
46
47 pub fn is_none(&self) -> bool {
49 matches!(self, MatchResult::None)
50 }
51
52 pub fn window_id(&self) -> Option<&WindowId> {
54 match self {
55 MatchResult::Exact { window_id, .. } => Some(window_id),
56 _ => None,
57 }
58 }
59}
60
61pub struct HintMatcher<'a> {
95 hints: &'a [WindowHint],
96}
97
98impl<'a> HintMatcher<'a> {
99 pub fn new(hints: &'a [WindowHint]) -> Self {
101 Self { hints }
102 }
103
104 pub fn match_input(&self, input: &str) -> MatchResult {
106 if input.is_empty() {
107 return MatchResult::Partial(self.hints.iter().map(|h| h.index).collect());
108 }
109
110 let matches: Vec<_> = self
112 .hints
113 .iter()
114 .filter(|h| h.hint.matches_input(input))
115 .collect();
116
117 match matches.len() {
118 0 => MatchResult::None,
119 1 => MatchResult::Exact {
120 index: matches[0].index,
121 window_id: matches[0].window_id.clone(),
122 },
123 _ => {
124 if let Some(exact) = matches.iter().find(|h| h.hint.equals_input(input)) {
126 MatchResult::Exact {
127 index: exact.index,
128 window_id: exact.window_id.clone(),
129 }
130 } else {
131 MatchResult::Partial(matches.iter().map(|h| h.index).collect())
132 }
133 }
134 }
135 }
136
137 pub fn filter_hints(&self, input: &str) -> Vec<&WindowHint> {
139 if input.is_empty() {
140 self.hints.iter().collect()
141 } else {
142 self.hints
143 .iter()
144 .filter(|h| h.hint.matches_input(input))
145 .collect()
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::core::hint::HintAssignment;
154 use crate::core::window::Window;
155
156 fn create_test_hints() -> Vec<WindowHint> {
157 let windows = vec![
158 Window::mock("firefox", "Tab 1"),
159 Window::mock("firefox", "Tab 2"),
160 Window::mock("ghostty", "Terminal"),
161 ];
162
163 HintAssignment::assign(&windows, |app_id| match app_id.as_str() {
164 "firefox" => Some('f'),
165 "ghostty" => Some('g'),
166 _ => None,
167 })
168 .hints
169 }
170
171 #[test]
172 fn test_match_exact_single() {
173 let hints = create_test_hints();
174 let matcher = HintMatcher::new(&hints);
175
176 let result = matcher.match_input("g");
178 assert!(result.is_exact());
179 }
180
181 #[test]
182 fn test_match_exact_with_multiple_windows() {
183 let hints = create_test_hints();
184 let matcher = HintMatcher::new(&hints);
185
186 let result = matcher.match_input("f");
188 assert!(result.is_exact());
189
190 let result = matcher.match_input("ff");
192 assert!(result.is_exact());
193 }
194
195 #[test]
196 fn test_match_none() {
197 let hints = create_test_hints();
198 let matcher = HintMatcher::new(&hints);
199
200 let result = matcher.match_input("x");
201 assert!(result.is_none());
202 }
203
204 #[test]
205 fn test_match_number_pattern() {
206 let hints = create_test_hints();
207 let matcher = HintMatcher::new(&hints);
208
209 let result = matcher.match_input("g1");
211 assert!(result.is_exact());
212
213 let result = matcher.match_input("f2");
215 assert!(result.is_exact());
216 }
217
218 #[test]
219 fn test_filter_hints() {
220 let hints = create_test_hints();
221 let matcher = HintMatcher::new(&hints);
222
223 let filtered = matcher.filter_hints("");
225 assert_eq!(filtered.len(), 3);
226
227 let filtered = matcher.filter_hints("f");
229 assert_eq!(filtered.len(), 2);
230
231 let filtered = matcher.filter_hints("ff");
233 assert_eq!(filtered.len(), 1);
234 }
235}