streampager/
bindings.rs

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