minus/input/hashed_event_register.rs
1//! Provides the [`HashedEventRegister`] and related items
2//!
3//! This module holds the [`HashedEventRegister`] which is a [`HashMap`] that stores events and their associated
4//! callbacks. When the user does an action on the terminal, the event is scanned and matched against this register.
5//! If their is a match related to that event, the associated callback is called
6
7use super::{InputClassifier, InputEvent};
8use crate::PagerState;
9use crossterm::event::{Event, MouseEvent};
10use std::{
11 collections::HashMap, collections::hash_map::RandomState, hash::BuildHasher, hash::Hash,
12 sync::Arc,
13};
14
15use std::borrow::Cow;
16
17/// A convenient type for the return type of [`HashedEventRegister::get`]
18type EventReturnType = Arc<dyn Fn(Event, &PagerState) -> InputEvent + Send + Sync>;
19
20#[derive(Clone)]
21struct EventCallback {
22 cb: EventReturnType,
23 desc: Cow<'static, str>,
24}
25
26// //////////////////////////////
27// EVENTWRAPPER TYPE
28// //////////////////////////////
29
30#[derive(Clone, Eq)]
31enum EventWrapper {
32 ExactMatchEvent(Event),
33 WildEvent,
34}
35
36impl From<Event> for EventWrapper {
37 fn from(e: Event) -> Self {
38 Self::ExactMatchEvent(e)
39 }
40}
41
42impl From<&Event> for EventWrapper {
43 fn from(e: &Event) -> Self {
44 Self::ExactMatchEvent(e.clone())
45 }
46}
47
48impl PartialEq for EventWrapper {
49 fn eq(&self, other: &Self) -> bool {
50 match (self, other) {
51 (
52 Self::ExactMatchEvent(Event::Mouse(MouseEvent {
53 kind, modifiers, ..
54 })),
55 Self::ExactMatchEvent(Event::Mouse(MouseEvent {
56 kind: o_kind,
57 modifiers: o_modifiers,
58 ..
59 })),
60 ) => kind == o_kind && modifiers == o_modifiers,
61 (
62 Self::ExactMatchEvent(Event::Resize(..)),
63 Self::ExactMatchEvent(Event::Resize(..)),
64 )
65 | (Self::WildEvent, Self::WildEvent) => true,
66 (Self::ExactMatchEvent(ev), Self::ExactMatchEvent(o_ev)) => ev == o_ev,
67 _ => false,
68 }
69 }
70}
71
72impl Hash for EventWrapper {
73 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
74 let tag = std::mem::discriminant(self);
75 tag.hash(state);
76 match self {
77 Self::ExactMatchEvent(Event::Mouse(MouseEvent {
78 kind, modifiers, ..
79 })) => {
80 kind.hash(state);
81 modifiers.hash(state);
82 }
83 Self::WildEvent | Self::ExactMatchEvent(Event::Resize(..)) => {}
84 Self::ExactMatchEvent(v) => {
85 v.hash(state);
86 }
87 }
88 }
89}
90
91// /////////////////////////////////////////////////
92// HASHED EVENT REGISTER TYPE AND ITS APIs
93// ////////////////////////////////////////////////
94
95/// A hash store for events and it's related callback
96///
97/// Each item is a key value pair, where the key is a event and it's value is a callback. When a
98/// event occurs, it is matched inside and when the related match is found, it's related callback
99/// is called.
100pub struct HashedEventRegister<S>(HashMap<EventWrapper, EventCallback, S>);
101
102impl HashedEventRegister<RandomState> {
103 /// Create a new [`HashedEventRegister`] with the default hasher
104 #[must_use]
105 pub fn with_default_hasher() -> Self {
106 Self::new(RandomState::new())
107 }
108}
109
110impl Default for HashedEventRegister<RandomState> {
111 /// Create a new [`HashedEventRegister`] with the default hasher and insert the default bindings
112 fn default() -> Self {
113 let mut event_register = Self::new(RandomState::new());
114 super::generate_default_bindings(&mut event_register);
115 event_register
116 }
117}
118
119impl<S> InputClassifier for HashedEventRegister<S>
120where
121 S: BuildHasher,
122{
123 fn classify_input(&self, ev: Event, ps: &crate::PagerState) -> Option<InputEvent> {
124 self.get(&ev).map(|c| c(ev, ps))
125 }
126
127 fn format_help(&self) -> Option<String> {
128 let h = self.format_help();
129 if h.is_empty() {
130 None
131 } else {
132 Some(h)
133 }
134 }
135}
136
137// ####################
138// GENERAL FUNCTIONS
139// ####################
140impl<S> HashedEventRegister<S>
141where
142 S: BuildHasher,
143{
144 /// Create a new `HashedEventRegister` with the Hasher `s`
145 pub const fn new(s: S) -> Self {
146 Self(HashMap::with_hasher(s))
147 }
148
149 /// Format dynamic help table from all registered key bindings that have non-empty descriptions.
150 #[must_use]
151 pub fn format_help(&self) -> String {
152 let entries = self.0.iter().filter_map(|(k, v)| match k {
153 EventWrapper::ExactMatchEvent(Event::Key(ke)) => Some((ke, v.desc.as_ref())),
154 _ => None,
155 });
156 crate::help::format_help_table_from_entries(entries)
157 }
158
159 /// Adds a callback to handle all events that failed to match
160 ///
161 /// Sometimes there are bunch of keys having equal importance that should have the same
162 /// callback, for instance all the numbers on the keyboard. To handle these types of scenerios
163 /// this is extremely useful. This callback is called when no event matches the incoming event,
164 /// then we just match whether the event is a keyboard number and perform the required action.
165 ///
166 /// This is also helpful when you need to do some action, like sending a message when the user
167 /// presses wrong keyboard/mouse buttons.
168 pub fn insert_wild_event_matcher(
169 &mut self,
170 cb: impl Fn(Event, &PagerState) -> InputEvent + Send + Sync + 'static,
171 ) {
172 self.0.insert(
173 EventWrapper::WildEvent,
174 EventCallback {
175 cb: Arc::new(cb),
176 desc: Cow::Borrowed(""),
177 },
178 );
179 }
180
181 fn get(&self, k: &Event) -> Option<&EventReturnType> {
182 self.0
183 .get(&k.into())
184 .map_or_else(|| self.0.get(&EventWrapper::WildEvent), Some)
185 .map(|entry| &entry.cb)
186 }
187
188 /// Adds a callback for handling resize events
189 ///
190 /// # Example
191 /// These are from the original sources
192 /// ```
193 /// use minus::input::{InputEvent, HashedEventRegister, crossterm_event::Event};
194 ///
195 /// let mut input_register = HashedEventRegister::default();
196 ///
197 /// input_register.add_resize_event(|ev, _| {
198 /// let (cols, rows) = if let Event::Resize(cols, rows) = ev {
199 /// (cols, rows)
200 /// } else {
201 /// unreachable!();
202 /// };
203 /// InputEvent::UpdateTermArea(cols as usize, rows as usize)
204 /// });
205 /// ```
206 pub fn add_resize_event(
207 &mut self,
208 cb: impl Fn(Event, &PagerState) -> InputEvent + Send + Sync + 'static,
209 ) {
210 let v = Arc::new(cb);
211 // The 0, 0 are present just to ensure everything compiles and they can be anything.
212 // These values are never hashed or stored into the HashedEventRegister
213 self.0.insert(
214 EventWrapper::ExactMatchEvent(Event::Resize(0, 0)),
215 EventCallback {
216 cb: v,
217 desc: Cow::Borrowed(""),
218 },
219 );
220 }
221
222 /// Removes the currently active resize event callback
223 pub fn remove_resize_event(&mut self) {
224 self.0
225 .remove(&EventWrapper::ExactMatchEvent(Event::Resize(0, 0)));
226 }
227}
228
229// ###############################
230// KEYBOARD SPECIFIC FUNCTIONS
231// ###############################
232impl<S> HashedEventRegister<S>
233where
234 S: BuildHasher,
235{
236 /// Add all elements of `desc` as key bindings that minus should respond to with the callback `cb`
237 ///
238 /// You should prefer using the [`add_key_events_checked`](HashedEventRegister::add_key_events_checked)
239 /// over this one.
240 ///
241 /// # Example
242 /// ```
243 /// use minus::input::{InputEvent, HashedEventRegister, crossterm_event};
244 ///
245 /// let mut input_register = HashedEventRegister::default();
246 ///
247 /// input_register.add_key_events(&["down"], |_, ps| {
248 /// InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(1))
249 /// });
250 /// ```
251 pub fn add_key_events(
252 &mut self,
253 desc: &[&str],
254 cb: impl Fn(Event, &PagerState) -> InputEvent + Send + Sync + 'static,
255 ) {
256 self.add_described_key_events(desc, "", cb);
257 }
258
259 /// Add all elements of `keys` as key bindings with a description that minus should respond to with the callback `cb`.
260 pub fn add_described_key_events(
261 &mut self,
262 keys: &[&str],
263 desc: impl Into<Cow<'static, str>>,
264 cb: impl Fn(Event, &PagerState) -> InputEvent + Send + Sync + 'static,
265 ) {
266 let v = Arc::new(cb);
267 let d = desc.into();
268 for k in keys {
269 self.0.insert(
270 Event::Key(super::definitions::keydefs::parse_key_event(k)).into(),
271 EventCallback {
272 cb: v.clone(),
273 desc: d.clone(),
274 },
275 );
276 }
277 }
278
279 /// Add all elements of `desc` as key bindings that minus should respond to with the callback `cb`.
280 ///
281 /// Prefer using this over [`add_key_events`](HashedEventRegister::add_key_events).
282 ///
283 /// # Panics
284 ///
285 /// This will panic if you the keybinding has been previously defined, unless the `remap`
286 /// is set to true. This helps preventing accidental overrides of your keybindings.
287 ///
288 /// # Example
289 /// ```should_panic
290 /// use minus::input::{InputEvent, HashedEventRegister, crossterm_event};
291 ///
292 /// let mut input_register = HashedEventRegister::default();
293 ///
294 /// input_register.add_key_events_checked(&["down"], |_, ps| {
295 /// InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(1))
296 /// }, false);
297 /// ```
298 pub fn add_key_events_checked(
299 &mut self,
300 desc: &[&str],
301 cb: impl Fn(Event, &PagerState) -> InputEvent + Send + Sync + 'static,
302 remap: bool,
303 ) {
304 self.add_described_key_events_checked(desc, "", cb, remap);
305 }
306
307 /// Add all elements of `keys` as key bindings with a description that minus should respond to with the callback `cb`, with conflict checking.
308 ///
309 /// # Panics
310 /// Panics if a key already exists and `remap` is `false`.
311 pub fn add_described_key_events_checked(
312 &mut self,
313 keys: &[&str],
314 desc: impl Into<Cow<'static, str>>,
315 cb: impl Fn(Event, &PagerState) -> InputEvent + Send + Sync + 'static,
316 remap: bool,
317 ) {
318 let v = Arc::new(cb);
319 let d = desc.into();
320 for k in keys {
321 let def: EventWrapper =
322 Event::Key(super::definitions::keydefs::parse_key_event(k)).into();
323 assert!(self.0.contains_key(&def) && remap, "");
324 self.0.insert(
325 def,
326 EventCallback {
327 cb: v.clone(),
328 desc: d.clone(),
329 },
330 );
331 }
332 }
333
334 /// Removes the callback associated with the all the elements of `desc`.
335 ///
336 /// ```
337 /// use minus::input::{InputEvent, HashedEventRegister, crossterm_event};
338 ///
339 /// let mut input_register = HashedEventRegister::default();
340 ///
341 /// input_register.remove_key_events(&["down"])
342 /// ```
343 pub fn remove_key_events(&mut self, desc: &[&str]) {
344 for k in desc {
345 self.0
346 .remove(&Event::Key(super::definitions::keydefs::parse_key_event(k)).into());
347 }
348 }
349
350 /// Add key binding(s) to show help in the pager prompt.
351 ///
352 /// If `desc` is empty, defaults to `&["m-h"]`.
353 ///
354 /// # Example
355 /// ```
356 /// use minus::input::HashedEventRegister;
357 ///
358 /// let mut input_register = HashedEventRegister::default();
359 /// // Bind default Meta/Alt-h key to show help
360 /// input_register.add_help_key(&[]);
361 /// // Or specify custom keys
362 /// input_register.add_help_key(&["f1"]);
363 /// ```
364 pub fn add_help_key(&mut self, desc: &[&str]) {
365 let keys = if desc.is_empty() { &["m-h"][..] } else { desc };
366 self.add_described_key_events(keys, "help", |_, _| InputEvent::ShowHelp);
367 }
368
369 /// Add key binding(s) to show help in the pager prompt with conflict checking.
370 ///
371 /// If `desc` is empty, defaults to `&["m-h"]`.
372 ///
373 /// # Panics
374 /// This will panic if any of the keybindings has been previously defined, unless `remap`
375 /// is set to true.
376 pub fn add_help_key_checked(&mut self, desc: &[&str], remap: bool) {
377 let keys = if desc.is_empty() { &["m-h"][..] } else { desc };
378 self.add_described_key_events_checked(keys, "help", |_, _| InputEvent::ShowHelp, remap);
379 }
380}
381
382// ###############################
383// MOUSE SPECIFIC FUNCTIONS
384// ###############################
385impl<S> HashedEventRegister<S>
386where
387 S: BuildHasher,
388{
389 /// Add all elemnts of `desc` as mouse bindings that minus should respond to with the callback `cb`
390 ///
391 /// You should prefer using the [`add_mouse_events_checked`](HashedEventRegister::add_mouse_events_checked)
392 /// over this one.
393 ///
394 /// # Example
395 /// ```
396 /// use minus::input::{InputEvent, HashedEventRegister};
397 ///
398 /// let mut input_register = HashedEventRegister::default();
399 ///
400 /// input_register.add_mouse_events(&["scroll:down"], |_, ps| {
401 /// InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(5))
402 /// });
403 /// ```
404 pub fn add_mouse_events(
405 &mut self,
406 desc: &[&str],
407 cb: impl Fn(Event, &PagerState) -> InputEvent + Send + Sync + 'static,
408 ) {
409 let v = Arc::new(cb);
410 for k in desc {
411 self.0.insert(
412 Event::Mouse(super::definitions::mousedefs::parse_mouse_event(k)).into(),
413 EventCallback {
414 cb: v.clone(),
415 desc: Cow::Borrowed(""),
416 },
417 );
418 }
419 }
420
421 /// Add all elemnts of `desc` as mouse bindings that minus should respond to with the callback `cb`.
422 ///
423 /// Prefer using this over [`add_mouse_events`](HashedEventRegister::add_mouse_events).
424 ///
425 /// # Panics
426 /// This will panic if you the keybinding has been previously defined, unless the `remap`
427 /// is set to true. This helps preventing accidental overrides of your keybindings.
428 ///
429 /// # Example
430 /// ```should_panic
431 /// use minus::input::{InputEvent, HashedEventRegister};
432 ///
433 /// let mut input_register = HashedEventRegister::default();
434 ///
435 /// input_register.add_mouse_events_checked(&["scroll:down"], |_, ps| {
436 /// InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(5))
437 /// }, false);
438 /// ```
439 pub fn add_mouse_events_checked(
440 &mut self,
441 desc: &[&str],
442 cb: impl Fn(Event, &PagerState) -> InputEvent + Send + Sync + 'static,
443 remap: bool,
444 ) {
445 let v = Arc::new(cb);
446 for k in desc {
447 let def: EventWrapper =
448 Event::Mouse(super::definitions::mousedefs::parse_mouse_event(k)).into();
449 assert!(self.0.contains_key(&def) && remap, "");
450 self.0.insert(
451 def,
452 EventCallback {
453 cb: v.clone(),
454 desc: Cow::Borrowed(""),
455 },
456 );
457 }
458 }
459
460 /// Removes the callback associated with the all the elements of `desc`.
461 ///
462 /// ```
463 /// use minus::input::{InputEvent, HashedEventRegister, crossterm_event};
464 ///
465 /// let mut input_register = HashedEventRegister::default();
466 ///
467 /// input_register.remove_mouse_events(&["scroll:down"])
468 /// ```
469 pub fn remove_mouse_events(&mut self, mouse: &[&str]) {
470 for k in mouse {
471 self.0
472 .remove(&Event::Mouse(super::definitions::mousedefs::parse_mouse_event(k)).into());
473 }
474 }
475}