Skip to main content

matchmaker/
event.rs

1use crate::Result;
2use crate::action::{Action, ActionExt, NullActionExt};
3use crate::binds::BindMap;
4use crate::message::{Event, RenderCommand};
5use crokey::{Combiner, KeyCombination, KeyCombinationFormat, key};
6use crossterm::event::{
7    Event as CrosstermEvent, EventStream, KeyModifiers, MouseEvent, MouseEventKind,
8};
9use futures::stream::StreamExt;
10use log::{debug, error, info, warn};
11use ratatui::layout::Rect;
12use tokio::sync::mpsc;
13use tokio::time::{self};
14
15pub type RenderSender<A = NullActionExt> = mpsc::UnboundedSender<RenderCommand<A>>;
16#[derive(Debug)]
17pub struct EventLoop<A: ActionExt> {
18    txs: Vec<mpsc::UnboundedSender<RenderCommand<A>>>,
19    tick_interval: time::Duration,
20
21    pub binds: BindMap<A>,
22    combiner: Combiner,
23    fmt: KeyCombinationFormat,
24
25    mouse_events: bool,
26    paused: bool,
27    event_stream: Option<EventStream>,
28    controller_rx: mpsc::UnboundedReceiver<Event>,
29    controller_tx: mpsc::UnboundedSender<Event>,
30}
31
32impl<A: ActionExt> Default for EventLoop<A> {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl<A: ActionExt> EventLoop<A> {
39    pub fn new() -> Self {
40        let combiner = Combiner::default();
41        let fmt = KeyCombinationFormat::default();
42        let (controller_tx, controller_rx) = tokio::sync::mpsc::unbounded_channel();
43
44        Self {
45            txs: vec![],
46            tick_interval: time::Duration::from_secs(1),
47
48            binds: BindMap::new(),
49            combiner,
50            fmt,
51            event_stream: None, // important not to initialize it too early?
52            controller_rx,
53            controller_tx,
54
55            mouse_events: false,
56            paused: false,
57        }
58    }
59
60    pub fn with_binds(binds: BindMap<A>) -> Self {
61        let mut ret = Self::new();
62        ret.binds = binds;
63        ret
64    }
65
66    pub fn with_tick_rate(mut self, tick_rate: u8) -> Self {
67        self.tick_interval = time::Duration::from_secs_f64(1.0 / tick_rate as f64);
68        self
69    }
70
71    pub fn add_tx(&mut self, handler: mpsc::UnboundedSender<RenderCommand<A>>) -> &mut Self {
72        self.txs.push(handler);
73        self
74    }
75
76    pub fn with_mouse_events(mut self) -> Self {
77        self.mouse_events = true;
78        self
79    }
80
81    pub fn clear_txs(&mut self) {
82        self.txs.clear();
83    }
84
85    pub fn get_controller(&self) -> mpsc::UnboundedSender<Event> {
86        self.controller_tx.clone()
87    }
88
89    fn handle_event(&mut self, e: Event) {
90        debug!("Received: {e}");
91
92        match e {
93            Event::Pause => {
94                self.paused = true;
95                self.send(RenderCommand::Ack);
96                self.event_stream = None; // drop because EventStream "buffers" event
97            }
98            Event::Refresh => {
99                self.send(RenderCommand::Refresh);
100            }
101            _ => {}
102        }
103        if let Some(actions) = self.binds.get(&e.into()) {
104            self.send_actions(actions);
105        }
106    }
107
108    pub fn binds(&mut self, binds: BindMap<A>) -> &mut Self {
109        self.binds = binds;
110        self
111    }
112
113    // todo: should its return type carry info
114    pub async fn run(&mut self) -> Result<()> {
115        self.event_stream = Some(EventStream::new());
116        let mut interval = time::interval(self.tick_interval);
117
118        // this loops infinitely until all readers are closed
119        loop {
120            // wait for resume signal
121            while self.paused {
122                if let Some(event) = self.controller_rx.recv().await {
123                    if matches!(event, Event::Resume) {
124                        debug!("Resumed from pause");
125                        self.paused = false;
126                        self.send(RenderCommand::Ack);
127                        self.event_stream = Some(EventStream::new());
128                        break;
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                continue; // event stream is removed when paused by handle_event
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(1)),
200                                                key!(down) => self.send_action(Action::Down(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                                                key!(ctrl-'[') => self.send_action(Action::ToggleWrap),
211                                                key!(ctrl-']') => self.send_action(Action::ToggleWrapPreview),
212                                                _ => {}
213                                            }
214                                        }
215                                    }
216                                }
217                                CrosstermEvent::Mouse(mouse) => {
218                                    if let Some(actions) = self.binds.get(&mouse.into()) {
219                                        self.send_actions(actions);
220                                    } else if !matches!(mouse.kind, MouseEventKind::Moved) {
221                                        // mouse binds can be disabled by overriding with empty action
222                                        // preview scroll can be disabled by overriding scroll event with scroll action
223                                        self.send(RenderCommand::Mouse(mouse));
224                                    }
225                                }
226                                CrosstermEvent::Resize(width, height) => {
227                                    self.send(RenderCommand::Resize(Rect::new(0, 0, width, height)));
228                                }
229                                #[allow(unused_variables)]
230                                CrosstermEvent::Paste(content) => {
231                                    #[cfg(feature = "bracketed-paste")]
232                                    {
233                                        self.send(RenderCommand::Paste(content));
234                                    }
235                                    #[cfg(not(feature = "bracketed-paste"))]
236                                    {
237                                        unreachable!()
238                                    }
239                                }
240                                // CrosstermEvent::FocusLost => {
241                                // }
242                                // CrosstermEvent::FocusGained => {
243                                // }
244                                _ => {},
245                            }
246                        }
247                        Some(Err(e)) => warn!("Failed to read crossterm event: {e}"),
248                        None => {
249                            warn!("Reader closed");
250                            break
251                        }
252                    }
253                }
254            }
255        }
256        Ok(())
257    }
258
259    fn send(&self, action: RenderCommand<A>) {
260        for tx in &self.txs {
261            tx.send(action.clone())
262                .unwrap_or_else(|_| debug!("Failed to send {action}"));
263        }
264    }
265
266    fn send_actions<'a>(&self, actions: impl IntoIterator<Item = &'a Action<A>>) {
267        for action in actions {
268            self.send(action.into());
269        }
270    }
271
272    pub fn print_key(&self, key_combination: KeyCombination) -> String {
273        self.fmt.to_string(key_combination)
274    }
275
276    fn send_action(&self, action: Action<A>) {
277        self.send(RenderCommand::Action(action));
278    }
279}
280
281fn key_code_as_letter(key: KeyCombination) -> Option<char> {
282    match key {
283        KeyCombination {
284            codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char(l)),
285            modifiers: KeyModifiers::NONE,
286        } => Some(l),
287        KeyCombination {
288            codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char(l)),
289            modifiers: KeyModifiers::SHIFT,
290        } => Some(l.to_ascii_uppercase()),
291        _ => None,
292    }
293}