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 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::collections::hash_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::debug!("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 ensure basic usability
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) => self.send_action(Action::Accept),
279                                                key!(right) => self.send_action(Action::ForwardChar),
280                                                key!(left) => self.send_action(Action::BackwardChar),
281                                                key!(ctrl-right) => self.send_action(Action::ForwardWord),
282                                                key!(ctrl-left) => self.send_action(Action::BackwardWord),
283                                                key!(backspace) => self.send_action(Action::DeleteChar),
284                                                key!(ctrl-h) => self.send_action(Action::DeleteWord),
285                                                key!(ctrl-u) => self.send_action(Action::Cancel),
286                                                key!(alt-h) => self.send_action(Action::Help("".to_string())),
287                                                key!(ctrl-'[') => self.send_action(Action::ToggleWrap),
288                                                key!(ctrl-']') => self.send_action(Action::TogglePreviewWrap),
289                                                _ => {
290                                                    matched = false
291                                                }
292                                            }
293                                            if matched {
294                                                self.record_key(key.to_string());
295                                            }
296                                        }
297                                    }
298                                }
299                                CrosstermEvent::Mouse(mouse) => {
300                                    if let Some(actions) = self.binds.get(&mouse.into()).cloned() {
301                                        self.send_actions(actions);
302                                    } else if !matches!(mouse.kind, MouseEventKind::Moved) {
303                                        // mouse binds can be disabled by overriding with empty action
304                                        // preview scroll can be disabled by overriding scroll event with scroll action
305                                        self.send(RenderCommand::Mouse(mouse));
306                                    }
307                                }
308                                CrosstermEvent::Resize(width, height) => {
309                                    self.send(RenderCommand::Resize(Rect::new(0, 0, width, height)));
310                                }
311                                #[allow(unused_variables)]
312                                CrosstermEvent::Paste(content) => {
313                                    #[cfg(feature = "bracketed-paste")]
314                                    {
315                                        self.send(RenderCommand::Paste(content));
316                                    }
317                                    #[cfg(not(feature = "bracketed-paste"))]
318                                    {
319                                        unreachable!()
320                                    }
321                                }
322                                // CrosstermEvent::FocusLost => {
323                                // }
324                                // CrosstermEvent::FocusGained => {
325                                // }
326                                _ => {},
327                            }
328                        }
329                        Some(Err(e)) => warn!("Failed to read crossterm event: {e}"),
330                        None => {
331                            warn!("Reader closed");
332                            break
333                        }
334                    }
335                }
336            }
337        }
338    }
339
340    fn send(&self, action: RenderCommand<A>) {
341        for tx in &self.txs {
342            tx.send(action.clone())
343                .unwrap_or_else(|_| debug!("Failed to send {action}"));
344        }
345    }
346
347    fn record_key(&mut self, content: String) {
348        let Some(path) = self.key_file.clone() else {
349            return;
350        };
351
352        // Cancel previous task if still running
353        if let Some(handle) = self.current_task.take() {
354            handle.abort();
355        }
356
357        let handle = tokio::spawn(write_to_file(path, content));
358
359        self.current_task = Some(handle);
360    }
361
362    fn send_actions<'a>(&self, actions: impl IntoIterator<Item = Action<A>>) {
363        for action in actions {
364            self.send(action.into());
365        }
366    }
367
368    pub fn print_key(&self, key_combination: KeyCombination) -> String {
369        self.fmt.to_string(key_combination)
370    }
371
372    fn send_action(&self, action: Action<A>) {
373        self.send(RenderCommand::Action(action));
374    }
375}
376
377fn key_code_as_letter(key: KeyCombination) -> Option<char> {
378    match key {
379        KeyCombination {
380            codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char(l)),
381            modifiers: KeyModifiers::NONE,
382        } => Some(l),
383        KeyCombination {
384            codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char(l)),
385            modifiers: KeyModifiers::SHIFT,
386        } => Some(l.to_ascii_uppercase()),
387        _ => None,
388    }
389}
390
391use std::path::Path;
392use tokio::fs;
393
394/// Cleanup files in the same directory with the same basename, and a .tmp extension
395async fn cleanup_tmp_files(path: &Path) -> Result<()> {
396    let parent = unwrap!(path.parent(); Ok(()));
397    let name = unwrap!(path.file_name().and_then(|s| s.to_str()); Ok(()));
398
399    let mut entries = fs::read_dir(parent).await?;
400
401    while let Some(entry) = entries.next_entry().await? {
402        let entry_path = entry.path();
403
404        if let Ok(filename) = entry_path._filename()
405            && let Some(e) = filename.strip_prefix(name)
406            && e.starts_with('.')
407            && e.ends_with(".tmp")
408        {
409            fs::remove_file(entry_path).await._elog();
410        }
411    }
412
413    Ok(())
414}
415
416/// Spawns a thread that writes `content` to `path` atomically using a temp file.
417/// Returns the `JoinHandle` so you can wait for it if desired.
418pub async fn write_to_file(path: PathBuf, content: String) -> Result<()> {
419    let suffix = std::time::SystemTime::now()
420        .duration_since(std::time::UNIX_EPOCH)
421        .unwrap()
422        .as_nanos();
423
424    let tmp_path = path.with_file_name(format!("{}.{}.tmp", path._filename()?, suffix));
425
426    // Write temp file
427    fs::write(&tmp_path, &content).await?;
428
429    // Atomically replace target
430    fs::rename(&tmp_path, &path).await?;
431
432    Ok(())
433}