minus/hooks.rs
1//! Manages and runs callbacks for events happening in minus.
2//!
3//! ## Note on Thread Blacking
4//!
5//! Callbacks registered for hooks are run on the same thread as the pager.
6//! This means that if you add a long-running task in a callback, it will block the pager
7//! from rendering, scrolling and responding to events. Hence you should avoid adding
8//! long-running tasks in callbacks. If you have a long running task, you should run it on a
9//! separate thread.
10
11use std::collections::HashMap;
12
13use crate::PagerState;
14
15/// Events that can have callbacks registered
16#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
17pub enum Hook {
18 /// Fired just before the terminal UI is drawn (before switching to the alternate screen).
19 PrePagerStart,
20 /// Fired after the terminal UI is drawn with text.
21 PostPagerStart,
22 /// Fired when the user hits the end of the page
23 EofReached,
24 /// Fired just before the pager exits due to [`InputEvent::Exit`](crate::input::InputEvent::Exit).
25 PrePagerExit,
26 /// Fired after the terminal UI is cleared up and main screen is restored.
27 ///
28 /// For this hook, start your IDs from 2 because 1 is occupied for the
29 /// [`ExitStrategy`](crate::ExitStrategy).
30 PostPagerExit,
31}
32
33/// A callback that can be executed on a hook
34pub type HookCallback = Box<dyn FnMut(&PagerState) + Send + Sync + 'static>;
35
36/// Stores callbacks for all hooks
37#[derive(Default)]
38pub(crate) struct Hooks {
39 hooks: HashMap<Hook, Vec<(u64, HookCallback)>>,
40 next_id: u64,
41}
42
43impl Hooks {
44 #[must_use]
45 pub(crate) fn new() -> Self {
46 Self::default()
47 }
48
49 pub(crate) fn add_callback(&mut self, hook: Hook, mut id: u64, cb: HookCallback) {
50 if id == 0 {
51 id = self.next_id;
52 self.next_id += 1;
53 }
54
55 let callbacks = self.hooks.entry(hook).or_default();
56 assert!(
57 !callbacks.iter().any(|(cb_id, _)| *cb_id == id),
58 "Callback ID {id} already exists for hook {hook:?}"
59 );
60 callbacks.push((id, cb));
61 }
62
63 pub(crate) fn remove_callback(&mut self, hook: Hook, id: u64) -> bool {
64 if let Some(cbs) = self.hooks.get_mut(&hook)
65 && let Some(pos) = cbs.iter().position(|(cb_id, _)| *cb_id == id)
66 {
67 _ = cbs.remove(pos);
68 return true;
69 }
70 false
71 }
72
73 pub(crate) fn run_hooks(&mut self, hook: Hook, pager_state: &PagerState) {
74 if let Some(cbs) = self.hooks.get_mut(&hook) {
75 for (_, cb) in cbs {
76 cb(pager_state);
77 }
78 }
79 }
80}