Skip to main content

matchmaker/
event.rs

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