Skip to main content

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}