open_sesame/input/
processor.rs1use crate::core::{HintMatcher, MatchResult, WindowId};
6use crate::input::InputBuffer;
7use crate::util::TimeoutTracker;
8use smithay_client_toolkit::seat::keyboard::Keysym;
9
10#[derive(Debug, Clone)]
12pub enum InputAction {
13 Ignore,
15 BufferChanged,
17 SelectionChanged {
19 direction: SelectionDirection,
21 },
22 PendingActivation {
24 window_id: WindowId,
26 index: usize,
28 },
29 ActivateNow {
31 window_id: WindowId,
33 index: usize,
35 },
36 ActivateSelected,
38 TryLaunch {
40 key: char,
42 },
43 Cancel,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum SelectionDirection {
50 Up,
52 Down,
54}
55
56pub struct InputProcessor {
58 buffer: InputBuffer,
60 timeout: TimeoutTracker,
62 pending_index: Option<usize>,
64 pending_window_id: Option<WindowId>,
66}
67
68impl InputProcessor {
69 pub fn new(activation_delay_ms: u64) -> Self {
71 Self {
72 buffer: InputBuffer::new(),
73 timeout: TimeoutTracker::new(activation_delay_ms),
74 pending_index: None,
75 pending_window_id: None,
76 }
77 }
78
79 pub fn buffer(&self) -> &InputBuffer {
81 &self.buffer
82 }
83
84 pub fn input_string(&self) -> String {
86 self.buffer.as_str()
87 }
88
89 pub fn has_pending(&self) -> bool {
91 self.pending_index.is_some()
92 }
93
94 pub fn pending(&self) -> Option<(usize, &WindowId)> {
96 match (&self.pending_index, &self.pending_window_id) {
97 (Some(idx), Some(id)) => Some((*idx, id)),
98 _ => None,
99 }
100 }
101
102 pub fn timeout_elapsed(&self) -> bool {
104 self.timeout.has_elapsed()
105 }
106
107 pub fn check_timeout(&mut self) -> Option<(usize, WindowId)> {
109 if self.timeout.has_elapsed()
110 && let (Some(idx), Some(id)) =
111 (self.pending_index.take(), self.pending_window_id.take())
112 {
113 self.timeout.cancel();
114 return Some((idx, id));
115 }
116 None
117 }
118
119 pub fn process_key<'a>(
121 &mut self,
122 key: Keysym,
123 matcher: &HintMatcher<'a>,
124 has_launch_config: impl Fn(&str) -> bool,
125 ) -> InputAction {
126 match key {
127 Keysym::Escape => {
128 tracing::debug!("Escape pressed, canceling");
129 InputAction::Cancel
130 }
131 Keysym::BackSpace => {
132 self.buffer.pop();
133 self.clear_pending();
134 self.timeout.reset();
135 tracing::debug!("Input: '{}'", self.buffer);
136 InputAction::BufferChanged
137 }
138 Keysym::Return | Keysym::KP_Enter => {
139 if let Some((idx, id)) = self.pending().map(|(i, id)| (i, id.clone())) {
141 self.clear_pending();
142 return InputAction::ActivateNow {
143 window_id: id,
144 index: idx,
145 };
146 }
147
148 if let MatchResult::Exact { index, window_id } =
150 matcher.match_input(&self.buffer.as_str())
151 {
152 return InputAction::ActivateNow { window_id, index };
153 }
154
155 InputAction::ActivateSelected
157 }
158 Keysym::Up | Keysym::KP_Up => InputAction::SelectionChanged {
159 direction: SelectionDirection::Up,
160 },
161 Keysym::Down | Keysym::KP_Down => InputAction::SelectionChanged {
162 direction: SelectionDirection::Down,
163 },
164 _ => {
166 let Some(c) = keysym_to_char(key) else {
168 return InputAction::Ignore;
169 };
170
171 self.buffer.push(c);
172 self.timeout.reset();
173 tracing::debug!("Input: '{}'", self.buffer);
174
175 match matcher.match_input(&self.buffer.as_str()) {
177 MatchResult::Exact { index, window_id } => {
178 tracing::debug!("Pending match: index={}", index);
179 self.pending_index = Some(index);
180 self.pending_window_id = Some(window_id.clone());
181 self.timeout.start();
182 InputAction::PendingActivation { window_id, index }
183 }
184 MatchResult::None => {
185 let base_key = self.buffer.first_char();
187
188 if let Some(key) = base_key {
189 let key_str = key.to_string();
190 if has_launch_config(&key_str) {
191 tracing::info!("No window match, will launch: {}", key);
192 return InputAction::TryLaunch { key };
193 }
194 }
195
196 self.buffer.pop();
198 tracing::debug!("No match, reverting to: '{}'", self.buffer);
199 InputAction::BufferChanged
200 }
201 MatchResult::Partial(_) => {
202 self.clear_pending();
204 InputAction::BufferChanged
205 }
206 }
207 }
208 }
209 }
210
211 fn clear_pending(&mut self) {
213 self.pending_index = None;
214 self.pending_window_id = None;
215 self.timeout.cancel();
216 }
217}
218
219fn keysym_to_char(key: Keysym) -> Option<char> {
221 key.key_char()
222 .filter(|c| c.is_ascii_alphanumeric())
223 .map(|c| c.to_ascii_lowercase())
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_keysym_to_char() {
232 assert_eq!(keysym_to_char(Keysym::a), Some('a'));
234 assert_eq!(keysym_to_char(Keysym::A), Some('a'));
235 assert_eq!(keysym_to_char(Keysym::_1), Some('1'));
236
237 assert_eq!(keysym_to_char(Keysym::space), None);
239 assert_eq!(keysym_to_char(Keysym::Return), None);
240 }
241
242 #[test]
243 fn test_input_processor_basic() {
244 let processor = InputProcessor::new(200);
245 assert!(processor.buffer().is_empty());
246 assert!(!processor.has_pending());
247 }
248}