Skip to main content

matchmaker/
event.rs

1use crate::action::{Action, ActionExt, NullActionExt};
2use crate::binds::{BindMap, BindMapExt, Trigger};
3use crate::message::{BindDirective, Event, RenderCommand};
4use anyhow::Result;
5use cba::bait::ResultExt;
6use cba::bath::PathExt;
7use cba::unwrap;
8use crokey::{Combiner, KeyCombination, KeyCombinationFormat, key};
9use crossterm::event::{
10    Event as CrosstermEvent, EventStream, KeyModifiers, MouseEvent, MouseEventKind,
11};
12use futures::stream::StreamExt;
13use log::{debug, error, info, warn};
14use ratatui::layout::Rect;
15use std::path::PathBuf;
16use tokio::sync::mpsc;
17use tokio::time::{self};
18
19pub type RenderSender<A = NullActionExt> = mpsc::UnboundedSender<RenderCommand<A>>;
20pub type EventSender = mpsc::UnboundedSender<Event>;
21pub type BindSender<A> = mpsc::UnboundedSender<BindDirective<A>>;
22
23#[derive(Debug)]
24pub struct EventLoop<A: ActionExt> {
25    txs: Vec<mpsc::UnboundedSender<RenderCommand<A>>>,
26    tick_interval: time::Duration,
27
28    pub binds: BindMap<A>,
29    combiner: Combiner,
30    fmt: KeyCombinationFormat,
31
32    mouse_events: bool,
33    paused: bool,
34    event_stream: Option<EventStream>,
35
36    rx: mpsc::UnboundedReceiver<Event>,
37    controller_tx: mpsc::UnboundedSender<Event>,
38
39    bind_rx: mpsc::UnboundedReceiver<BindDirective<A>>,
40    bind_tx: BindSender<A>,
41
42    key_file: Option<PathBuf>,
43    current_task: Option<tokio::task::JoinHandle<Result<()>>>,
44}
45
46impl<A: ActionExt> Default for EventLoop<A> {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl<A: ActionExt> EventLoop<A> {
53    pub fn new() -> Self {
54        let combiner = Combiner::default();
55        let fmt = KeyCombinationFormat::default();
56        let (controller_tx, controller_rx) = tokio::sync::mpsc::unbounded_channel();
57
58        let (bind_tx, bind_rx) = tokio::sync::mpsc::unbounded_channel();
59
60        Self {
61            txs: vec![],
62            tick_interval: time::Duration::from_millis(200),
63
64            binds: BindMap::new(),
65            combiner,
66            fmt,
67            event_stream: None, // important not to initialize it too early?
68            rx: controller_rx,
69            controller_tx,
70
71            mouse_events: false,
72            paused: false,
73            key_file: None,
74            current_task: None,
75
76            bind_rx,
77            bind_tx,
78        }
79    }
80
81    pub fn with_binds(binds: BindMap<A>) -> Self {
82        let mut ret = Self::new();
83        ret.binds = binds;
84        ret
85    }
86
87    pub fn check_binds(&self) -> Result<(), String> {
88        self.binds.check_cycles()
89    }
90
91    pub fn record_last_key(&mut self, path: PathBuf) -> &mut Self {
92        self.key_file = Some(path);
93        self
94    }
95
96    pub fn with_tick_rate(mut self, tick_rate: u8) -> Self {
97        self.tick_interval = time::Duration::from_secs_f64(1.0 / tick_rate as f64);
98        self
99    }
100
101    pub fn add_tx(&mut self, handler: mpsc::UnboundedSender<RenderCommand<A>>) -> &mut Self {
102        self.txs.push(handler);
103        self
104    }
105
106    pub fn with_mouse_events(mut self) -> Self {
107        self.mouse_events = true;
108        self
109    }
110
111    pub fn clear_txs(&mut self) {
112        self.txs.clear();
113    }
114
115    pub fn controller(&self) -> EventSender {
116        self.controller_tx.clone()
117    }
118    pub fn bind_controller(&self) -> BindSender<A> {
119        self.bind_tx.clone()
120    }
121
122    fn handle_event(&mut self, e: Event) {
123        debug!("Received: {e}");
124
125        match e {
126            Event::Pause => {
127                self.paused = true;
128                self.send(RenderCommand::Ack);
129                self.event_stream = None; // drop because EventStream "buffers" event
130            }
131            Event::Refresh => {
132                self.send(RenderCommand::Refresh);
133            }
134            _ => {}
135        }
136        if let Some(actions) = self.binds.get(&e.into()).cloned() {
137            self.send_actions(actions, None);
138        }
139    }
140
141    fn handle_rebind(&mut self, e: BindDirective<A>) {
142        debug!("Received: {e:?}");
143
144        match e {
145            BindDirective::Bind(k, v) => {
146                self.binds.insert(k, v);
147            }
148
149            BindDirective::PushBind(k, v) => {
150                self.binds.entry(k).or_default().0.push(v);
151            }
152
153            BindDirective::Unbind(k) => {
154                self.binds.remove(&k);
155            }
156
157            BindDirective::PopBind(k) => {
158                if let Some(actions) = self.binds.get_mut(&k) {
159                    actions.0.pop();
160
161                    if actions.0.is_empty() {
162                        self.binds.remove(&k);
163                    }
164                }
165            }
166        }
167    }
168
169    pub fn binds(&mut self, binds: BindMap<A>) -> &mut Self {
170        self.binds = binds;
171        self
172    }
173
174    // todo: should its return type carry info
175    pub async fn run(&mut self) {
176        self.event_stream = Some(EventStream::new());
177        let mut interval = time::interval(self.tick_interval);
178
179        if let Some(path) = self.key_file.clone() {
180            log::debug!("Cleaning up temp files @ {path:?}");
181            tokio::spawn(async move {
182                cleanup_tmp_files(&path).await._elog();
183            });
184        }
185
186        // this loops infinitely until all readers are closed
187        loop {
188            self.txs.retain(|tx| !tx.is_closed());
189            if self.txs.is_empty() {
190                break;
191            }
192
193            // wait for resume signal
194            while self.paused {
195                if let Some(event) = self.rx.recv().await {
196                    if matches!(event, Event::Resume) {
197                        debug!("Resumed from pause");
198                        self.paused = false;
199                        self.send(RenderCommand::Ack);
200                        self.event_stream = Some(EventStream::new());
201                        break;
202                    }
203                } else {
204                    error!("Event controller closed while paused.");
205                    break;
206                }
207            }
208
209            // // flush controller events
210            // while let Ok(event) = self.rx.try_recv() {
211            //    self.handle_event(event)
212            // }
213
214            let event = if let Some(stream) = &mut self.event_stream {
215                stream.next()
216            } else {
217                continue; // event stream is removed when paused by handle_event
218            };
219
220            tokio::select! {
221                _ = interval.tick() => {
222                    self.send(RenderCommand::Tick)
223                }
224
225                // In case ctrl-c manifests as a signal instead of a key
226                _ = tokio::signal::ctrl_c() => {
227                    self.record_key("ctrl-c".into());
228                    if let Some(actions) = self.binds.get(&key!(ctrl-c).into()).cloned() {
229                        self.send_actions(actions, Some("ctrl-c".into()));
230                    } else {
231                        self.send(RenderCommand::quit());
232                        info!("Received ctrl-c");
233                    }
234                }
235
236                Some(event) = self.rx.recv() => {
237                    self.handle_event(event)
238                }
239
240                Some(directive) = self.bind_rx.recv() => {
241                    self.handle_rebind(directive)
242                }
243
244                // Input ready
245                maybe_event = event => {
246                    match maybe_event {
247                        Some(Ok(event)) => {
248                            if !matches!(
249                                event,
250                                CrosstermEvent::Mouse(MouseEvent {
251                                    kind: crossterm::event::MouseEventKind::Moved,
252                                    ..
253                                }) |  CrosstermEvent::Key {..}
254                            ) {
255                                info!("Event {event:?}");
256                            }
257                            match event {
258                                CrosstermEvent::Key(k) => {
259                                    if let Some(key) = self.combiner.transform(k) {
260                                        info!("{key:?}");
261                                        let key = KeyCombination::normalized(key);
262                                        if let Some(actions) = self.binds.get(&key.into()).cloned() {
263                                            self.record_key(key.to_string());
264                                            self.send_actions(actions, Some(key.to_string()));
265                                        } else if let Some(c) = key_code_as_letter(key) {
266                                            self.send(RenderCommand::Action(Action::Char(c)));
267                                        } else {
268                                            let mut matched = true;
269                                            // a basic set of keys to ensure basic usability
270                                            match key {
271                                                key!(ctrl-c) | key!(esc) => {
272                                                    self.send(RenderCommand::quit())
273                                                },
274                                                key!(up) => self.send_action(Action::Up(1)),
275                                                key!(down) => self.send_action(Action::Down(1)),
276                                                key!(enter) => self.send_action(Action::Accept),
277                                                key!(right) => self.send_action(Action::ForwardChar),
278                                                key!(left) => self.send_action(Action::BackwardChar),
279                                                key!(ctrl-right) => self.send_action(Action::ForwardWord),
280                                                key!(ctrl-left) => self.send_action(Action::BackwardWord),
281                                                key!(backspace) => self.send_action(Action::DeleteChar),
282                                                key!(ctrl-h) => self.send_action(Action::DeleteWord),
283                                                key!(ctrl-u) => self.send_action(Action::Cancel),
284                                                key!(alt-h) => self.send_action(Action::Help("".to_string())),
285                                                key!(ctrl-'[') => self.send_action(Action::ToggleWrap),
286                                                key!(ctrl-']') => self.send_action(Action::TogglePreviewWrap),
287                                                _ => {
288                                                    matched = false
289                                                }
290                                            }
291                                            if matched {
292                                                self.record_key(key.to_string());
293                                            }
294                                        }
295                                    }
296                                }
297                                CrosstermEvent::Mouse(mouse) => {
298                                    if let Some(actions) = self.binds.get(&mouse.into()).cloned() {
299                                        self.send_actions(actions, None);
300                                    } else if !matches!(mouse.kind, MouseEventKind::Moved) {
301                                        // mouse binds can be disabled by overriding with empty action
302                                        // preview scroll can be disabled by overriding scroll event with scroll action
303                                        self.send(RenderCommand::Mouse(mouse));
304                                    }
305                                }
306                                CrosstermEvent::Resize(width, height) => {
307                                    self.send(RenderCommand::Resize(Rect::new(0, 0, width, height)));
308                                }
309                                #[allow(unused_variables)]
310                                CrosstermEvent::Paste(content) => {
311                                    #[cfg(feature = "bracketed-paste")]
312                                    {
313                                        self.send(RenderCommand::Paste(content));
314                                    }
315                                    #[cfg(not(feature = "bracketed-paste"))]
316                                    {
317                                        unreachable!()
318                                    }
319                                }
320                                // CrosstermEvent::FocusLost => {
321                                // }
322                                // CrosstermEvent::FocusGained => {
323                                // }
324                                _ => {},
325                            }
326                        }
327                        Some(Err(e)) => warn!("Failed to read crossterm event: {e}"),
328                        None => {
329                            warn!("Reader closed");
330                            break
331                        }
332                    }
333                }
334            }
335        }
336    }
337
338    fn send(&self, action: RenderCommand<A>) {
339        for tx in &self.txs {
340            tx.send(action.clone())
341                .unwrap_or_else(|_| debug!("Failed to send {action}"));
342        }
343    }
344
345    fn record_key(&mut self, content: String) {
346        let Some(path) = self.key_file.clone() else {
347            return;
348        };
349
350        // Cancel previous task if still running
351        if let Some(handle) = self.current_task.take() {
352            handle.abort();
353        }
354
355        let handle = tokio::spawn(write_to_file(path, content));
356
357        self.current_task = Some(handle);
358    }
359
360    fn send_actions<'a>(&self, actions: impl IntoIterator<Item = Action<A>>, key: Option<String>) {
361        for action in actions {
362            match action {
363                Action::PrintKey => {
364                    if let Some(k) = &key {
365                        self.send(Action::Print(k.clone()).into());
366                    }
367                }
368                Action::Semantic(s) => {
369                    if let Some(actions) = self.binds.get(&Trigger::Semantic(s)) {
370                        self.send_actions(actions.clone(), None);
371                    }
372                }
373                _ => self.send(action.into()),
374            }
375        }
376    }
377
378    pub fn print_key(&self, key_combination: KeyCombination) -> String {
379        self.fmt.to_string(key_combination)
380    }
381
382    fn send_action(&self, action: Action<A>) {
383        self.send(RenderCommand::Action(action));
384    }
385}
386
387fn key_code_as_letter(key: KeyCombination) -> Option<char> {
388    match key {
389        KeyCombination {
390            codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char(l)),
391            modifiers: KeyModifiers::NONE,
392        } => Some(l),
393        KeyCombination {
394            codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char(l)),
395            modifiers: KeyModifiers::SHIFT,
396        } => Some(l.to_ascii_uppercase()),
397        _ => None,
398    }
399}
400
401use std::path::Path;
402use tokio::fs;
403
404/// Cleanup files in the same directory with the same basename, and a .tmp extension
405async fn cleanup_tmp_files(path: &Path) -> Result<()> {
406    let parent = unwrap!(path.parent(); Ok(()));
407    let name = unwrap!(path.file_name().and_then(|s| s.to_str()); Ok(()));
408
409    let mut entries = fs::read_dir(parent).await?;
410
411    while let Some(entry) = entries.next_entry().await? {
412        let entry_path = entry.path();
413
414        if let Ok(filename) = entry_path._filename()
415            && let Some(e) = filename.strip_prefix(name)
416            && e.starts_with('.')
417            && e.ends_with(".tmp")
418        {
419            fs::remove_file(entry_path).await._elog();
420        }
421    }
422
423    Ok(())
424}
425
426/// Spawns a thread that writes `content` to `path` atomically using a temp file.
427/// Returns the `JoinHandle` so you can wait for it if desired.
428pub async fn write_to_file(path: PathBuf, content: String) -> Result<()> {
429    let suffix = std::time::SystemTime::now()
430        .duration_since(std::time::UNIX_EPOCH)
431        .unwrap()
432        .as_nanos();
433
434    let tmp_path = path.with_file_name(format!("{}.{}.tmp", path._filename()?, suffix));
435
436    // Write temp file
437    fs::write(&tmp_path, &content).await?;
438
439    // Atomically replace target
440    fs::rename(&tmp_path, &path).await?;
441
442    Ok(())
443}