matchmaker/
event.rs

1use crate::Result;
2use crate::action::{Action, ActionExt, Count};
3use crate::binds::BindMap;
4use crate::message::{Event, RenderCommand};
5use anyhow::bail;
6use crokey::{Combiner, KeyCombination, KeyCombinationFormat, key};
7use crossterm::event::{Event as CrosstermEvent, EventStream, KeyModifiers, MouseEvent};
8use futures::stream::StreamExt;
9use log::{debug, error, info, warn};
10use ratatui::layout::Rect;
11use tokio::sync::mpsc;
12use tokio::time::{self};
13
14pub type RenderSender<A> = mpsc::UnboundedSender<RenderCommand<A>>;
15#[derive(Debug)]
16pub struct EventLoop<A: ActionExt> {
17    txs: Vec<mpsc::UnboundedSender<RenderCommand<A>>>,
18    tick_interval: time::Duration,
19
20    pub binds: BindMap<A>,
21    combiner: Combiner,
22    fmt: KeyCombinationFormat,
23
24    mouse_events: bool,
25    paused: bool,
26    event_stream: Option<EventStream>,
27    controller_rx: mpsc::UnboundedReceiver<Event>,
28    controller_tx: mpsc::UnboundedSender<Event>,
29}
30
31impl<A: ActionExt> Default for EventLoop<A> {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl<A: ActionExt> EventLoop<A> {
38    pub fn new() -> Self {
39        let combiner = Combiner::default();
40        let fmt = KeyCombinationFormat::default();
41        let (controller_tx, controller_rx) = tokio::sync::mpsc::unbounded_channel();
42
43        Self {
44            txs: vec![],
45            tick_interval: time::Duration::from_secs(1),
46
47            binds: BindMap::new(),
48            combiner,
49            fmt,
50            event_stream: None, // important not to initialize it too early?
51            controller_rx,
52            controller_tx,
53
54            mouse_events: false,
55            paused: false,
56        }
57    }
58
59    pub fn with_binds(binds: BindMap<A>) -> Self {
60        let mut ret = Self::new();
61        ret.binds = binds;
62        ret
63    }
64
65    pub fn with_tick_rate(mut self, tick_rate: u8) -> Self {
66        self.tick_interval = time::Duration::from_secs_f64(1.0 / tick_rate as f64);
67        self
68    }
69
70    pub fn add_tx(&mut self, handler: mpsc::UnboundedSender<RenderCommand<A>>) -> &mut Self {
71        self.txs.push(handler);
72        self
73    }
74
75    pub fn with_mouse_events(mut self) -> Self {
76        self.mouse_events = true;
77        self
78    }
79
80    pub fn clear_txs(&mut self) {
81        self.txs.clear();
82    }
83
84    pub fn get_controller(&self) -> mpsc::UnboundedSender<Event> {
85        self.controller_tx.clone()
86    }
87
88    fn handle_event(&mut self, e: Event) -> bool {
89        debug!("Received: {e}");
90
91        match e {
92            Event::Pause => {
93                self.paused = true;
94                self.send(RenderCommand::Ack);
95                self.event_stream = None;
96            }
97            Event::Refresh => {
98                self.send(RenderCommand::Refresh);
99            }
100            _ => {}
101        }
102        if let Some(actions) = self.binds.get(&e.into()) {
103            self.send_actions(actions);
104        }
105
106        self.paused
107    }
108
109    pub fn binds(&mut self, binds: BindMap<A>) -> &mut Self {
110        self.binds = binds;
111        self
112    }
113
114    // todo: should its return type carry info
115    pub async fn run(&mut self) -> Result<()> {
116        self.event_stream = Some(EventStream::new());
117        let mut interval = time::interval(self.tick_interval);
118
119        // this loops infinitely until all readers are closed
120        loop {
121            while self.paused {
122                if let Some(event) = self.controller_rx.recv().await {
123                    if matches!(event, Event::Resume) {
124                        log::debug!("Resumed from pause");
125                        self.paused = false;
126                        self.send(RenderCommand::Ack);
127                        self.event_stream = Some(EventStream::new());
128                        continue;
129                    }
130                } else {
131                    error!("Event controller closed while paused.");
132                    break;
133                }
134            }
135
136            // flush controller events
137            while let Ok(event) = self.controller_rx.try_recv() {
138                // todo: note that our dynamic event handlers don't detect events originating outside of render currently, tho maybe we could reseed here
139                self.handle_event(event);
140            }
141
142            self.txs.retain(|tx| !tx.is_closed());
143            if self.txs.is_empty() {
144                break;
145            }
146
147            let event = if let Some(stream) = &mut self.event_stream {
148                stream.next()
149            } else {
150                bail!("No event stream (this should be unreachable)");
151            };
152
153            tokio::select! {
154                    _ = interval.tick() => {
155                        self.send(RenderCommand::Tick)
156                    }
157
158                    // In case ctrl-c manifests as a signal instead of a key
159                    _ = tokio::signal::ctrl_c() => {
160                        if let Some(actions) = self.binds.get(&key!(ctrl-c).into()) {
161                            self.send_actions(actions);
162                        } else {
163                            self.send(RenderCommand::quit());
164                            info!("Received ctrl-c");
165                        }
166                    }
167
168                    Some(event) = self.controller_rx.recv() => {
169                        self.handle_event(event);
170                    }
171
172                    // Input ready
173                    maybe_event = event => {
174                        match maybe_event {
175                            Some(Ok(event)) => {
176                                if !matches!(
177                                    event,
178                                    CrosstermEvent::Mouse(MouseEvent {
179                                        kind: crossterm::event::MouseEventKind::Moved,
180                                        ..
181                                    })
182                                ) {
183                                    info!("Event {event:?}");
184                                }
185                                match event {
186                                    CrosstermEvent::Key(k) => {
187                                        info!("{k:?}");
188                                        if let Some(key) = self.combiner.transform(k) {
189                                            info!("{key:?}");
190                                            let key = KeyCombination::normalized(key);
191                                            if let Some(actions) = self.binds.get(&key.into()) {
192                                                self.send_actions(actions);
193                                            } else if let Some(c) = key_code_as_letter(key) {
194                                                self.send(RenderCommand::Action(Action::Input(c)));
195                                            } else {
196                                                // a basic set of keys to prevent confusion
197                                                match key {
198                                                    key!(ctrl-c) | key!(esc) => self.send(RenderCommand::quit()),
199                                                    key!(up) => self.send_action(Action::Up(Count(1))),
200                                                    key!(down) => self.send_action(Action::Down(Count(1))),
201                                                    key!(enter) => self.send_action(Action::Accept),
202                                                    key!(right) => self.send_action(Action::ForwardChar),
203                                                    key!(left) => self.send_action(Action::BackwardChar),
204                                                    key!(ctrl-right) => self.send_action(Action::ForwardWord),
205                                                    key!(ctrl-left) => self.send_action(Action::BackwardWord),
206                                                    key!(backspace) => self.send_action(Action::DeleteChar),
207                                                    key!(ctrl-h) => self.send_action(Action::DeleteWord),
208                                                    key!(ctrl-u) => self.send_action(Action::Cancel),
209                                                    key!(alt-h) => self.send_action(Action::Help("".to_string())),
210                                                    _ => {}
211                                                }
212                                            }
213                                        }
214                                    }
215                                    CrosstermEvent::Mouse(mouse) => {
216                                        if let Some(actions) = self.binds.get(&mouse.into()) {
217                                            self.send_actions(actions);
218                                        }
219                                    }
220                                    CrosstermEvent::Resize(width, height) => {
221                                        self.send(RenderCommand::Resize(Rect::new(0, 0, width, height)));
222                                    }
223                                    // CrosstermEvent::FocusLost => {
224                                    // }
225                                    // CrosstermEvent::FocusGained => {
226                                    // }
227                                    #[allow(unused_variables)]
228                                    CrosstermEvent::Paste(content) => {
229                                        #[cfg(feature = "bracketed-paste")]
230                                        {
231                                            self.send(RenderCommand::Paste(content));
232                                        }
233                                        #[cfg(not(feature = "bracketed-paste"))]
234                                        {
235                                            unreachable!()
236                                            // let actions: Vec<_> = x.chars().map(|c| {
237                                            //         let a: Action<A> = Action::Input(c);
238                                            //         a
239                                            //     }
240                                            // ).collect();
241                                            // self.send_actions(
242                                            //     &actions
243                                            // )
244                                    }
245                                }
246                                _ => {},
247                            }
248                        }
249                        Some(Err(e)) => warn!("Failed to read crossterm event: {e}"),
250                        None => {
251                            warn!("Reader closed");
252                            break
253                        }
254                    }
255                }
256            }
257        }
258        Ok(())
259    }
260
261    fn send(&self, action: RenderCommand<A>) {
262        for tx in &self.txs {
263            tx.send(action.clone())
264                .unwrap_or_else(|_| debug!("Failed to send {action}"));
265        }
266    }
267
268    fn send_actions<'a>(&self, actions: impl IntoIterator<Item = &'a Action<A>>) {
269        for action in actions {
270            self.send(action.into());
271        }
272    }
273
274    pub fn print_key(&self, key_combination: KeyCombination) -> String {
275        self.fmt.to_string(key_combination)
276    }
277
278    fn send_action(&self, action: Action<A>) {
279        self.send(RenderCommand::Action(action));
280    }
281}
282
283fn key_code_as_letter(key: KeyCombination) -> Option<char> {
284    match key {
285        KeyCombination {
286            codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char(l)),
287            modifiers: KeyModifiers::NONE,
288        } => Some(l),
289        KeyCombination {
290            codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char(l)),
291            modifiers: KeyModifiers::SHIFT,
292        } => Some(l.to_ascii_uppercase()),
293        _ => None,
294    }
295}