streampager/
bindings.rs

1//! Key bindings.
2
3use std::collections::HashMap;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::Arc;
6
7use indexmap::IndexMap;
8use thiserror::Error;
9
10use crate::action::Action;
11use crate::file::FileIndex;
12
13/// Key codes for key bindings.
14///
15pub use termwiz::input::KeyCode;
16
17/// Keyboard modifiers for key bindings.
18///
19pub use termwiz::input::Modifiers;
20
21/// Errors specific to bindings.
22#[derive(Debug, Error)]
23pub enum BindingError {
24    /// Error when a binding is invalid.
25    #[error("invalid keybinding: {0}")]
26    Invalid(String),
27
28    /// Binding is missing a parameter.
29    #[error("{0} missing parameter {1}")]
30    MissingParameter(String, usize),
31
32    /// Integer parsing error.
33    #[error("invalid integer")]
34    InvalidInt(#[from] std::num::ParseIntError),
35
36    /// Wrapped error within the context of a binding parameter.
37    #[error("invalid {binding} parameter {index}")]
38    ForParameter {
39        /// Wrapped error.
40        #[source]
41        error: Box<BindingError>,
42
43        /// Binding.
44        binding: String,
45
46        /// Parameter index.
47        index: usize,
48    },
49}
50
51impl BindingError {
52    fn for_parameter(self, binding: String, index: usize) -> Self {
53        Self::ForParameter {
54            error: Box::new(self),
55            binding,
56            index,
57        }
58    }
59}
60
61type Result<T> = std::result::Result<T, BindingError>;
62
63/// A key binding category.
64///
65/// Key bindings are listed by category in the help screen.
66#[derive(Copy, Clone, Debug, PartialEq, Eq)]
67pub enum Category {
68    /// Uncategorized actions.
69    None,
70
71    /// Actions for controlling the pager.
72    General,
73
74    /// Actions for moving around the file.
75    Navigation,
76
77    /// Actions that affect the presentation of the file.
78    Presentation,
79
80    /// Actions that initiate or modify searches.
81    Searching,
82
83    /// Actions that are hidden in help view (for example, too verbose).
84    Hidden,
85}
86
87impl Category {
88    /// Non-hidden categories.
89    pub(crate) fn categories() -> impl Iterator<Item = Category> {
90        [
91            Category::General,
92            Category::Navigation,
93            Category::Presentation,
94            Category::Searching,
95            Category::None,
96        ]
97        .iter()
98        .cloned()
99    }
100}
101
102impl std::fmt::Display for Category {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        match *self {
105            Category::None => f.write_str("Other"),
106            Category::General => f.write_str("General"),
107            Category::Navigation => f.write_str("Navigation"),
108            Category::Presentation => f.write_str("Presentation"),
109            Category::Searching => f.write_str("Searching"),
110            Category::Hidden => f.write_str("Hidden"),
111        }
112    }
113}
114
115/// An action that may be bound to a key.
116#[derive(Clone, Debug, Hash, PartialEq, Eq)]
117pub enum Binding {
118    /// An action.
119    Action(Action),
120
121    /// A custom binding.
122    Custom(CustomBinding),
123
124    /// An unrecognised binding.
125    Unrecognized(String),
126}
127
128impl Binding {
129    /// Create new custom binding.
130    ///
131    /// When this binding is invoked, the callback is called.  The callback is provided with the
132    /// file index of the file that is currently being displayed.  Note that this may differ from
133    /// any of the file indexes returned by the `add` methods on the `Pager`, as additional file
134    /// indexes can be allocated, e.g. for the help screen.
135    pub fn custom(
136        category: Category,
137        description: impl Into<String>,
138        callback: impl Fn(FileIndex) + Send + Sync + 'static,
139    ) -> Self {
140        Binding::Custom(CustomBinding::new(category, description, callback))
141    }
142
143    pub(crate) fn category(&self) -> Category {
144        match self {
145            Binding::Action(action) => {
146                use Action::*;
147                match action {
148                    Quit | Refresh | Help | Cancel => Category::General,
149                    PreviousFile
150                    | NextFile
151                    | ScrollUpLines(_)
152                    | ScrollDownLines(_)
153                    | ScrollUpScreenFraction(_)
154                    | ScrollDownScreenFraction(_)
155                    | ScrollToTop
156                    | ScrollToBottom
157                    | ScrollLeftColumns(_)
158                    | ScrollRightColumns(_)
159                    | ScrollLeftScreenFraction(_)
160                    | ScrollRightScreenFraction(_)
161                    | PromptGoToLine => Category::Navigation,
162                    ToggleRuler | ToggleLineNumbers | ToggleLineWrapping => Category::Presentation,
163                    PromptSearchFromStart
164                    | PromptSearchForwards
165                    | PromptSearchBackwards
166                    | NextMatch
167                    | PreviousMatch
168                    | NextMatchLine
169                    | PreviousMatchLine
170                    | PreviousMatchScreen
171                    | NextMatchScreen
172                    | FirstMatch
173                    | LastMatch => Category::Searching,
174                    AppendDigitToRepeatCount(_) => Category::Hidden,
175                }
176            }
177            Binding::Custom(binding) => binding.category,
178            Binding::Unrecognized(_) => Category::None,
179        }
180    }
181
182    /// Parse a keybinding identifier and list of parameters into a key binding.
183    pub fn parse(ident: String, params: Vec<String>) -> Result<Self> {
184        use Action::*;
185
186        let param_usize = |index| -> Result<usize> {
187            let value: &String = params
188                .get(index)
189                .ok_or_else(|| BindingError::MissingParameter(ident.clone(), index))?;
190            let value = value
191                .parse::<usize>()
192                .map_err(|err| BindingError::from(err).for_parameter(ident.clone(), index))?;
193            Ok(value)
194        };
195
196        let action = match ident.as_str() {
197            "Quit" => Quit,
198            "Refresh" => Refresh,
199            "Help" => Help,
200            "Cancel" => Cancel,
201            "PreviousFile" => PreviousFile,
202            "NextFile" => NextFile,
203            "ToggleRuler" => ToggleRuler,
204            "ScrollUpLines" => ScrollUpLines(param_usize(0)?),
205            "ScrollDownLines" => ScrollDownLines(param_usize(0)?),
206            "ScrollUpScreenFraction" => ScrollUpScreenFraction(param_usize(0)?),
207            "ScrollDownScreenFraction" => ScrollDownScreenFraction(param_usize(0)?),
208            "ScrollToTop" => ScrollToTop,
209            "ScrollToBottom" => ScrollToBottom,
210            "ScrollLeftColumns" => ScrollLeftColumns(param_usize(0)?),
211            "ScrollRightColumns" => ScrollRightColumns(param_usize(0)?),
212            "ScrollLeftScreenFraction" => ScrollLeftScreenFraction(param_usize(0)?),
213            "ScrollRightScreenFraction" => ScrollRightScreenFraction(param_usize(0)?),
214            "ToggleLineNumbers" => ToggleLineNumbers,
215            "ToggleLineWrapping" => ToggleLineWrapping,
216            "PromptGoToLine" => PromptGoToLine,
217            "PromptSearchFromStart" => PromptSearchFromStart,
218            "PromptSearchForwards" => PromptSearchForwards,
219            "PromptSearchBackwards" => PromptSearchBackwards,
220            "PreviousMatch" => PreviousMatch,
221            "NextMatch" => NextMatch,
222            "PreviousMatchLine" => PreviousMatchLine,
223            "NextMatchLine" => NextMatchLine,
224            "FirstMatch" => FirstMatch,
225            "LastMatch" => LastMatch,
226            _ => return Ok(Binding::Unrecognized(ident)),
227        };
228
229        Ok(Binding::Action(action))
230    }
231}
232
233impl From<Action> for Binding {
234    fn from(action: Action) -> Binding {
235        Binding::Action(action)
236    }
237}
238
239impl From<Action> for Option<Binding> {
240    fn from(action: Action) -> Option<Binding> {
241        Some(Binding::Action(action))
242    }
243}
244
245impl std::fmt::Display for Binding {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        match *self {
248            Binding::Action(ref a) => write!(f, "{}", a),
249            Binding::Custom(ref b) => write!(f, "{}", b.description),
250            Binding::Unrecognized(ref s) => write!(f, "Unrecognized binding ({})", s),
251        }
252    }
253}
254
255static CUSTOM_BINDING_ID: AtomicUsize = AtomicUsize::new(0);
256
257/// A custom binding.  This can be used by applications using streampager
258/// to add custom actions on keys.
259#[derive(Clone)]
260pub struct CustomBinding {
261    /// The id of this binding.  This is unique for each binding.
262    id: usize,
263
264    /// The category of this binding.
265    category: Category,
266
267    /// The description of this binding.
268    description: String,
269
270    /// Called when the action is triggered.
271    callback: Arc<dyn Fn(FileIndex) + Sync + Send>,
272}
273
274impl CustomBinding {
275    /// Create a new custom binding.
276    ///
277    /// The category and description are used in the help screen.  The
278    /// callback is executed whenever the binding is triggered.
279    pub fn new(
280        category: Category,
281        description: impl Into<String>,
282        callback: impl Fn(FileIndex) + Sync + Send + 'static,
283    ) -> CustomBinding {
284        CustomBinding {
285            id: CUSTOM_BINDING_ID.fetch_add(1, Ordering::SeqCst),
286            category,
287            description: description.into(),
288            callback: Arc::new(callback),
289        }
290    }
291
292    /// Trigger the binding and run its callback.
293    pub fn run(&self, file_index: FileIndex) {
294        (self.callback)(file_index)
295    }
296}
297
298impl PartialEq for CustomBinding {
299    fn eq(&self, other: &Self) -> bool {
300        self.id == other.id
301    }
302}
303
304impl Eq for CustomBinding {}
305
306impl std::hash::Hash for CustomBinding {
307    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
308        self.id.hash(state);
309    }
310}
311
312impl std::fmt::Debug for CustomBinding {
313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        f.debug_tuple("CustomBinding")
315            .field(&self.id)
316            .field(&self.description)
317            .finish()
318    }
319}
320
321/// A binding to a key and its associated help visibility.  Used by
322/// the keymaps macro to provide binding configuration.
323#[derive(Clone, Debug)]
324#[doc(hidden)]
325pub struct BindingConfig {
326    /// The binding.
327    pub binding: Binding,
328
329    /// Whether this binding is visible in the help screen.
330    pub visible: bool,
331}
332
333/// A collection of key bindings.
334#[derive(PartialEq, Eq)]
335pub struct Keymap {
336    /// Map of bindings from keys.
337    bindings: HashMap<(Modifiers, KeyCode), Binding>,
338
339    /// Map of visible keys from bindings.
340    keys: IndexMap<Binding, Vec<(Modifiers, KeyCode)>>,
341}
342
343impl<'a, I: IntoIterator<Item = &'a ((Modifiers, KeyCode), BindingConfig)>> From<I> for Keymap {
344    fn from(iter: I) -> Keymap {
345        let iter = iter.into_iter();
346        let size_hint = iter.size_hint();
347        let mut bindings = HashMap::with_capacity(size_hint.0);
348        let mut keys = IndexMap::with_capacity(size_hint.0);
349        for &((modifiers, keycode), ref binding_config) in iter {
350            bindings.insert((modifiers, keycode), binding_config.binding.clone());
351            if binding_config.visible {
352                keys.entry(binding_config.binding.clone())
353                    .or_insert_with(Vec::new)
354                    .push((modifiers, keycode));
355            }
356        }
357        Keymap { bindings, keys }
358    }
359}
360
361impl Keymap {
362    /// Create a new, empty, keymap.
363    pub fn new() -> Self {
364        Keymap {
365            bindings: HashMap::new(),
366            keys: IndexMap::new(),
367        }
368    }
369
370    /// Get the binding associated with a key combination.
371    pub fn get(&self, modifiers: Modifiers, keycode: KeyCode) -> Option<&Binding> {
372        self.bindings.get(&(modifiers, keycode))
373    }
374
375    /// Bind (or unbind) a key combination.
376    pub fn bind(
377        &mut self,
378        modifiers: Modifiers,
379        keycode: KeyCode,
380        binding: impl Into<Option<Binding>>,
381    ) -> &mut Self {
382        self.bind_impl(modifiers, keycode, binding.into(), true)
383    }
384
385    /// Bind (or unbind) a key combination, but exclude it from the help screen.
386    pub fn bind_hidden(
387        &mut self,
388        modifiers: Modifiers,
389        keycode: KeyCode,
390        binding: impl Into<Option<Binding>>,
391    ) -> &mut Self {
392        self.bind_impl(modifiers, keycode, binding.into(), false)
393    }
394
395    fn bind_impl(
396        &mut self,
397        modifiers: Modifiers,
398        keycode: KeyCode,
399        binding: Option<Binding>,
400        visible: bool,
401    ) -> &mut Self {
402        if let Some(old_binding) = self.bindings.remove(&(modifiers, keycode)) {
403            if let Some(keys) = self.keys.get_mut(&old_binding) {
404                keys.retain(|&item| item != (modifiers, keycode));
405            }
406        }
407        if let Some(binding) = binding {
408            self.bindings.insert((modifiers, keycode), binding.clone());
409            if visible {
410                self.keys
411                    .entry(binding)
412                    .or_insert_with(Vec::new)
413                    .push((modifiers, keycode));
414            }
415        }
416        self
417    }
418
419    pub(crate) fn iter_keys(&self) -> impl Iterator<Item = (&Binding, &Vec<(Modifiers, KeyCode)>)> {
420        self.keys.iter()
421    }
422}
423
424impl Default for Keymap {
425    fn default() -> Self {
426        Keymap::from(crate::keymaps::default::KEYMAP.iter())
427    }
428}
429
430impl std::fmt::Debug for Keymap {
431    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432        f.debug_tuple("Keymap")
433            .field(&format!("<{} keys bound>", self.bindings.len()))
434            .finish()
435    }
436}