Skip to main content

minus/input/
mod.rs

1//! Manage keyboard/mouse-bindings while running `minus`.
2//!
3//! > **Terminology in this module**: We will call any keyboard/mouse event from the terminal as a **binding**
4//! > and its associated predefined action as **callback**.
5//!
6//! There are two ways to define binding in minus as you will see below.
7//!
8//! # Newer (Recommended) Method
9//! ## Description
10//! This method offers a much improved and ergonomic API for defining bindings and callbacks.
11//! You use the [`HashedEventRegister`] for registering bindings and their associated callback.
12//! It provides functions like [`add_key_events`](HashedEventRegister::add_key_events) and
13//! [`add_mouse_events`](HashedEventRegister::add_mouse_events) which take `&[&str]` as its first
14//! argument and a callback `cb` as its second argument and maps all `&str` in the `&[&str]` to
15//! same callback function `cb`. Each `&str` of the `&[&str]` contains a description of the
16//! key/mouse binding needed to activate it. For example `c-c` means pressing a `Ctrl+c` on the
17//! keyboard. See [Writing Binding Descriptions](#writing-binding-descriptions) to know more on
18//! writing these descriptions.
19//
20//! ## Example
21//! ```
22//! use minus::input::{InputEvent, HashedEventRegister, crossterm_event::Event};
23//!
24//! let mut input_register = HashedEventRegister::default();
25//!
26//! input_register.add_key_events(&["down"], |_, ps| {
27//!     InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(1))
28//! });
29//!
30//! input_register.add_mouse_events(&["scroll:up"], |_, ps| {
31//!     InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(5))
32//! });
33//!
34//! input_register.add_resize_event(|ev, _| {
35//!     let (cols, rows) = if let Event::Resize(cols, rows) = ev {
36//!         (cols, rows)
37//!     } else {
38//!        unreachable!();
39//!     };
40//!     InputEvent::UpdateTermArea(cols as usize, rows as usize)
41//! });
42//! ```
43//!
44//! ## Writing Binding Descriptions
45//! ### Defining Keybindings
46//! The general syntax for defining keybindings is `[MODIFIER]-[MODIFIER]-[MODIFIER]-{SINGLE KEY}`
47//!
48//! `MODIFIER`s include or or more of the `Ctrl` `Alt` and `Shift` keys. They are writeen with
49//! the shorthands `c`, `m` and `s` respectively.
50//!
51//! `SINGLE CHAR` includes any key on the keyboard which is not a modifier like `a`, `z`, `1`, `F1`
52//! or `enter`. Each of these pieces are separated by a `-`.
53//!
54//! Here are some examples
55//!
56//! | Key Input    | Mean ing                                   |
57//! |--------------|--------------------------------------------|
58//! | `a`          | A literal `a`                              |
59//! | `Z`          | A `Z`. Matched only when a caps lock is on |
60//! | `c-q`        | `Ctrl+q`                                   |
61//! | `enter`      | `ENTER` key                                |
62//! | `c-m-pageup` | `Ctrl+Alt+PageUp`                          |
63//! | `s-2`        | `Shift+2`                                  |
64//! | `backspace`  | `Backspace` Key                            |
65//! | `left`       | `Left Arrow` key                           |
66//!
67//! ### Defining Mouse Bindings
68//!
69//! The general syntax for defining keybindings is `[MODIFIER]-[MODIFIER]-[MODIFIER]-{MOUSE ACTION}`
70//!
71//! `MODIFIER`s include or or more of the `Ctrl` `Alt` and `Shift` keys which are pressed along
72//! with the mouse action. They are writeen with the shorthands `c`, `m` and `s` respectively.
73//!
74//! `MOUSE ACTION` includes actions like pressing down the left mouse button or taking up the right
75//! mouse button. It also includes scrolling up/down or pressing the middle click.
76//!
77//! Here are some examples
78//!
79//! | Key Input     | Mean ing                                   |
80//! |---------------|--------------------------------------------|
81//! | `left:up`     | Releasing the left mouse button            |
82//! | `right:down`  | Pressing the right mouse button            |
83//! | `c-mid:down`  | Middle click in pressed along with Ctrl key|
84//! | `m-scroll:up` | Scrolled down while pressing the Alt key   |
85//!
86//! **NOTE:** Although minus's description parser can correctly parse almost all if not all the
87//!   events that you can possibly register, not all of them are correctly registered by crossterm
88//!   itself. For example minus corrctly parses `c-s-h` as  `ctrl+shift-h` but crossterm
89//!   categorically recognizes it as `ctrl+h` when reading events from the terminal.
90//!
91//! # Legacy method
92//! This method relies heavily on the [`InputClassifier`] trait and end-applications were needed to
93//! manually copy the [default definitions](DefaultInputClassifier) and make the required
94//! modifications yourself in this method. This lead to very messy and error-prone system for
95//! defining bindings and also required application authors to bring in the the underlying
96//! [crossterm](https://docs.rs/crossterm/latest) crate to define the events.
97//!
98//! ## Example
99//! ```
100//! use minus::{input::{InputEvent, InputClassifier}, Pager, PagerState};
101//! use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
102//!
103//! struct CustomInputClassifier;
104//! impl InputClassifier for CustomInputClassifier {
105//!     fn classify_input(
106//!         &self,
107//!         ev: Event,
108//!         ps: &PagerState
109//!     ) -> Option<InputEvent> {
110//!             match ev {
111//!                 Event::Key(KeyEvent {
112//!                     code: KeyCode::Up,
113//!                     modifiers: KeyModifiers::NONE,
114//!                     ..
115//!                 })
116//!                 | Event::Key(KeyEvent {
117//!                     code: KeyCode::Char('j'),
118//!                     modifiers: KeyModifiers::NONE,
119//!                     ..
120//!                 }) => Some(InputEvent::UpdateUpperMark
121//!                       (ps.upper_mark.saturating_sub(1))),
122//!                 _ => None
123//!         }
124//!     }
125//! }
126//!
127//! let mut pager = Pager::new();
128//! pager.set_input_classifier(
129//!                 Box::new(CustomInputClassifier)
130//!             );
131//! ```
132//!
133//! **NOTE:** Although you can define almost every combination of bindings that crossterm supports,
134//!   not all of them are correctly registered by crossterm itself. For example you can define
135//!   ```text
136//!   Event::Key(KeyEvent {
137//!       code: KeyCode::Char(`h`),
138//!       modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
139//!       ..
140//!   })
141//!   ```
142//!   but crossterm will not match to it as crossterm
143//!   recognizes a `ctrl+shift+h` as `ctrl+h` when reading events from the terminal.
144//!
145//! # Custom Actions on User Events
146//!
147//! Sometimes you want to execute arbitrary code when a key/mouse action is pressed like fetching
148//! more data from a server but not necessarily sending it to minus. In these types of scenarios,
149//! you can leverage [`InputEvent::Ignore`]. When this is returned by a callback
150//! function, minus will execute your code but not do anything special for the event on its part.
151//! ```no_test
152//! input_register.add_key_events(&["f"], |_, ps| {
153//!     fetch_data_from_server(...);
154//!     InputEvent::Ignore
155//! });
156//! ```
157//! It can be used with the legacy method too.
158//! ```no_test
159//! struct CustomInputClassifier;
160//! impl InputClassifier for CustomInputClassifier {
161//!     fn classify_input(
162//!         &self,
163//!         ev: Event,
164//!         ps: &PagerState
165//!     ) -> Option<InputEvent> {
166//!             match ev {
167//!                 Event::Key(KeyEvent {
168//!                     code: KeyCode::Char('f'),
169//!                     modifiers: KeyModifiers::NONE,
170//!                     ..
171//!                 }) => {
172//!                     fetch_data_from_server(...);
173//!                     InputEvent::Ignore
174//!                 },
175//!                 _ => None
176//!         }
177//!     }
178//! }
179//! ```
180
181pub(crate) mod definitions;
182pub(crate) mod hashed_event_register;
183
184pub use crossterm::event as crossterm_event;
185
186#[cfg(feature = "search")]
187use crate::search::SearchMode;
188use crate::{LineNumbers, PagerState};
189use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
190
191#[cfg_attr(
192    docsrs,
193    deprecated = "See [#163](https://github.com/AMythicDev/minus/pull/163)."
194)]
195pub use hashed_event_register::HashedEventRegister;
196
197/// Events handled by the `minus` pager.
198#[derive(Debug, Copy, Clone, PartialEq, Eq)]
199#[allow(clippy::module_name_repetitions)]
200#[non_exhaustive]
201pub enum InputEvent {
202    /// `Ctrl+C` or `Q`, exits the application.
203    Exit,
204    /// The terminal was resized. Contains the new number of rows.
205    UpdateTermArea(usize, usize),
206    /// Sent by movement keys like `Up` `Down`, `PageUp`, `PageDown`, 'g', `G` etc.
207    /// Contains the new value for the upper mark.
208    UpdateUpperMark(usize),
209    /// `Ctrl+L`, inverts the line number display. Contains the new value.
210    UpdateLineNumber(LineNumbers),
211    /// A number key has been pressed. This inner value is stored as a `char`.
212    /// The input loop will append this number to its `count` string variable
213    Number(char),
214    /// Restore the original prompt
215    RestorePrompt,
216    /// Whether to allow Horizontal scrolling
217    HorizontalScroll(bool),
218    /// Sets the left mark of Horizontal scrolling
219    ///
220    /// Sent by keys like `l`, `h`, `right`, `left` etc.
221    UpdateLeftMark(usize),
222    /// Start a mouse selection at the given screen coordinates.
223    StartSelection { x: u16, y: u16 },
224    /// Update the current mouse selection to the given screen coordinates.
225    UpdateSelection { x: u16, y: u16 },
226    /// Clear the current mouse selection.
227    ClearSelection,
228    /// Copy the current selection.
229    #[cfg(feature = "clipboard")]
230    CopySelection,
231    /// Tells the event hadler to not do anything for this event
232    ///
233    /// This is extremely useful when you want to execute arbitrary code on events without
234    /// necessarily asking the event handler to do anything special for this event. See [Custom
235    /// Actions on User Events](./index.html#custom-actions-on-user-events).
236    Ignore,
237    /// `/`, Searching for certain pattern of text
238    #[cfg(feature = "search")]
239    Search(SearchMode),
240    /// Get to the next match in forward mode
241    ///
242    /// **WARNING: This has been deprecated in favour of `MoveToNextMatch`. This will likely be
243    /// removed in the next major release.**
244    #[cfg(feature = "search")]
245    #[deprecated = "Use [InputEvent::MoveToNextMatch(1)](InputEvent::MoveToNextMatch) for the same effect."]
246    NextMatch,
247    /// Get to the previous match in forward mode
248    ///
249    /// **WARNING: This has been deprecated in favour of `MoveToPrevMatch`. This will likely be
250    /// removed in the next major release.**
251    #[deprecated = "Use [InputEvent::MoveToPrevMatch(1)](InputEvent::MoveToPrevMatch) for the same effect."]
252    #[cfg(feature = "search")]
253    PrevMatch,
254    /// Move to the next nth match in the given direction
255    #[cfg(feature = "search")]
256    MoveToNextMatch(usize),
257    /// Move to the previous nth match in the given direction
258    #[cfg(feature = "search")]
259    MoveToPrevMatch(usize),
260    /// Control follow mode.
261    ///
262    /// When set to true, minus ensures that the user's screen always follows the end part of the
263    /// output. By default it is turned off.
264    ///
265    /// This is similar to [`Pager::follow_output`](crate::pager::Pager::follow_output) except that
266    /// this is used to control it from the user's side.
267    FollowOutput(bool),
268    #[cfg(feature = "search")]
269    /// Toggle smart case searching mode.
270    ToggleSmartCase,
271    /// Show help message in the prompt area.
272    ShowHelp,
273}
274
275/// Classifies the input and returns the appropriate [`InputEvent`]
276///
277/// If you are using the newer method for input definition, you don't need to take care of this.
278///
279/// If you are using the legacy method, see the sources of [`DefaultInputClassifier`] on how to
280/// inplement this trait.
281#[allow(clippy::module_name_repetitions)]
282#[cfg_attr(
283    docsrs,
284    deprecated = "See [#163](https://github.com/AMythicDev/minus/pull/163)."
285)]
286pub trait InputClassifier {
287    fn classify_input(&self, ev: Event, ps: &PagerState) -> Option<InputEvent>;
288
289    /// Format dynamic help text from registered bindings, if supported.
290    fn format_help(&self) -> Option<String> {
291        None
292    }
293}
294
295/// Insert the default set of actions into the [`HashedEventRegister`]
296#[allow(clippy::module_name_repetitions)]
297#[cfg_attr(
298    docsrs,
299    deprecated = "See [#163](https://github.com/AMythicDev/minus/pull/163)."
300)]
301#[allow(clippy::too_many_lines)]
302pub fn generate_default_bindings<S>(map: &mut HashedEventRegister<S>)
303where
304    S: std::hash::BuildHasher,
305{
306    map.add_described_key_events(&["q", "c-c"], "quit", |_, _| InputEvent::Exit);
307
308    map.add_described_key_events(&["up", "k"], "scroll up", |_, ps| {
309        let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
310        InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(position))
311    });
312    map.add_described_key_events(&["down", "j"], "scroll down", |_, ps| {
313        let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
314        InputEvent::UpdateUpperMark(ps.upper_mark.saturating_add(position))
315    });
316    map.add_described_key_events(&["c-f"], "toggle follow", |_, ps| {
317        InputEvent::FollowOutput(!ps.follow_output)
318    });
319    map.add_described_key_events(&["enter"], "scroll lines", |_, ps| {
320        if ps.message.is_some() {
321            InputEvent::RestorePrompt
322        } else {
323            let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
324            InputEvent::UpdateUpperMark(ps.upper_mark.saturating_add(position))
325        }
326    });
327    map.add_described_key_events(&["u", "c-u"], "half-page up", |_, ps| {
328        let half_screen = ps.rows / 2;
329        InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(half_screen))
330    });
331    map.add_described_key_events(&["d", "c-d"], "half-page down", |_, ps| {
332        let half_screen = ps.rows / 2;
333        InputEvent::UpdateUpperMark(ps.upper_mark.saturating_add(half_screen))
334    });
335    map.add_described_key_events(&["g", "home"], "top", |_, _| InputEvent::UpdateUpperMark(0));
336
337    map.add_described_key_events(&["s-g", "G"], "bottom", |_, ps| {
338        let mut position = ps
339            .prefix_num
340            .parse::<usize>()
341            .unwrap_or(usize::MAX)
342            // Reduce 1 here, because line numbering starts from 1
343            // while upper_mark starts from 0
344            .saturating_sub(1);
345        if position == 0 {
346            position = usize::MAX;
347        }
348        // Get the exact row number where first row of this line is placed in
349        // [`PagerState::formatted_lines`] and jump to that location.If the line number does not
350        // exist, directly jump to the bottom of text.
351        let row_to_go = *ps
352            .lines_to_row_map
353            .get(position)
354            .unwrap_or(&(usize::MAX - 1));
355        InputEvent::UpdateUpperMark(row_to_go)
356    });
357    map.add_described_key_events(&["pageup"], "page up", |_, ps| {
358        InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(ps.rows - 1))
359    });
360    map.add_described_key_events(&["pagedown", "space"], "page down", |_, ps| {
361        InputEvent::UpdateUpperMark(ps.upper_mark.saturating_add(ps.rows - 1))
362    });
363    map.add_described_key_events(&["c-l"], "toggle line numbers", |_, ps| {
364        InputEvent::UpdateLineNumber(!ps.line_numbers)
365    });
366    map.add_described_key_events(&["end"], "bottom", |_, _| InputEvent::UpdateUpperMark(usize::MAX - 1));
367    #[cfg(feature = "search")]
368    {
369        map.add_described_key_events(&["/"], "search forward", |_, _| InputEvent::Search(SearchMode::Forward));
370        map.add_described_key_events(&["?"], "search backward", |_, _| InputEvent::Search(SearchMode::Reverse));
371        map.add_described_key_events(&["m-i"], "toggle smart case", |_, _| InputEvent::ToggleSmartCase);
372        map.add_described_key_events(&["n"], "next match", |_, ps| {
373            let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
374
375            if ps.search_state.search_mode == SearchMode::Forward {
376                InputEvent::MoveToNextMatch(position)
377            } else if ps.search_state.search_mode == SearchMode::Reverse {
378                InputEvent::MoveToPrevMatch(position)
379            } else {
380                InputEvent::Ignore
381            }
382        });
383        map.add_described_key_events(&["p", "s-n"], "previous match", |_, ps| {
384            let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
385
386            if ps.search_state.search_mode == SearchMode::Forward {
387                InputEvent::MoveToPrevMatch(position)
388            } else if ps.search_state.search_mode == SearchMode::Reverse {
389                InputEvent::MoveToNextMatch(position)
390            } else {
391                InputEvent::Ignore
392            }
393        });
394    }
395
396    map.add_mouse_events(&["scroll:up"], |_, ps| {
397        InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(5))
398    });
399    map.add_mouse_events(&["scroll:down"], |_, ps| {
400        InputEvent::UpdateUpperMark(ps.upper_mark.saturating_add(5))
401    });
402    map.add_mouse_events(&["left:down"], |ev, _| {
403        let Event::Mouse(MouseEvent { column, row, .. }) = ev else {
404            unreachable!();
405        };
406        InputEvent::StartSelection { x: column, y: row }
407    });
408    map.add_mouse_events(&["left:drag"], |ev, _| {
409        let Event::Mouse(MouseEvent { column, row, .. }) = ev else {
410            unreachable!();
411        };
412        InputEvent::UpdateSelection { x: column, y: row }
413    });
414
415    #[cfg(feature = "clipboard")]
416    {
417        map.add_mouse_events(&["left:up"], |_, _| InputEvent::CopySelection);
418        map.add_key_events(&["y"], |_, _| InputEvent::CopySelection);
419    }
420
421    map.add_described_key_events(&["c-s-h", "c-h"], "toggle line wrap", |_, ps| {
422        InputEvent::HorizontalScroll(!ps.screen.line_wrapping)
423    });
424    map.add_described_key_events(&["h", "left"], "scroll left", |_, ps| {
425        let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
426        InputEvent::UpdateLeftMark(ps.left_mark.saturating_sub(position))
427    });
428    map.add_described_key_events(&["l", "right"], "scroll right", |_, ps| {
429        let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
430        InputEvent::UpdateLeftMark(ps.left_mark.saturating_add(position))
431    });
432    // TODO: Add keybindings for left right scrolling
433
434    map.add_resize_event(|ev, _| {
435        let Event::Resize(cols, rows) = ev else {
436            unreachable!();
437        };
438        InputEvent::UpdateTermArea(cols as usize, rows as usize)
439    });
440
441    map.insert_wild_event_matcher(|ev, _| {
442        if let Event::Key(KeyEvent {
443            code: KeyCode::Char(c),
444            modifiers: KeyModifiers::NONE,
445            ..
446        }) = ev
447        {
448            if c.is_ascii_digit() {
449                InputEvent::Number(c)
450            } else {
451                InputEvent::Ignore
452            }
453        } else {
454            InputEvent::Ignore
455        }
456    });
457}
458
459/// The default set of input definitions
460///
461/// **This is kept only for legacy purposes and may not be well updated with all the latest changes**
462#[cfg_attr(
463    docsrs,
464    deprecated = "See [#163](https://github.com/AMythicDev/minus/pull/163)."
465)]
466pub struct DefaultInputClassifier;
467
468impl InputClassifier for DefaultInputClassifier {
469    #[allow(clippy::too_many_lines)]
470    fn classify_input(&self, ev: Event, ps: &PagerState) -> Option<InputEvent> {
471        #[allow(clippy::unnested_or_patterns)]
472        match ev {
473            // Scroll up by one.
474            Event::Key(KeyEvent {
475                code,
476                modifiers: KeyModifiers::NONE,
477                ..
478            }) if code == KeyCode::Up || code == KeyCode::Char('k') => {
479                let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
480                Some(InputEvent::UpdateUpperMark(
481                    ps.upper_mark.saturating_sub(position),
482                ))
483            }
484
485            // Scroll down by one.
486            Event::Key(KeyEvent {
487                code,
488                modifiers: KeyModifiers::NONE,
489                ..
490            }) if code == KeyCode::Down || code == KeyCode::Char('j') => {
491                let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
492                Some(InputEvent::UpdateUpperMark(
493                    ps.upper_mark.saturating_add(position),
494                ))
495            }
496
497            // Toggle output following
498            Event::Key(KeyEvent {
499                code,
500                modifiers: KeyModifiers::CONTROL,
501                ..
502            }) if code == KeyCode::Char('f') => Some(InputEvent::FollowOutput(!ps.follow_output)),
503
504            // For number keys
505            Event::Key(KeyEvent {
506                code: KeyCode::Char(c),
507                modifiers: KeyModifiers::NONE,
508                ..
509            }) if c.is_ascii_digit() => Some(InputEvent::Number(c)),
510
511            // Enter key
512            Event::Key(KeyEvent {
513                code: KeyCode::Enter,
514                modifiers: KeyModifiers::NONE,
515                ..
516            }) => {
517                if ps.message.is_some() {
518                    Some(InputEvent::RestorePrompt)
519                } else {
520                    let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
521                    Some(InputEvent::UpdateUpperMark(
522                        ps.upper_mark.saturating_add(position),
523                    ))
524                }
525            }
526
527            // Scroll up by half screen height.
528            Event::Key(KeyEvent {
529                code: KeyCode::Char('u'),
530                modifiers,
531                ..
532            }) if modifiers == KeyModifiers::CONTROL || modifiers == KeyModifiers::NONE => {
533                let half_screen = ps.rows / 2;
534                Some(InputEvent::UpdateUpperMark(
535                    ps.upper_mark.saturating_sub(half_screen),
536                ))
537            }
538            // Scroll down by half screen height.
539            Event::Key(KeyEvent {
540                code: KeyCode::Char('d'),
541                modifiers,
542                ..
543            }) if modifiers == KeyModifiers::CONTROL || modifiers == KeyModifiers::NONE => {
544                let half_screen = ps.rows / 2;
545                Some(InputEvent::UpdateUpperMark(
546                    ps.upper_mark.saturating_add(half_screen),
547                ))
548            }
549
550            // Mouse scroll up/down
551            Event::Mouse(MouseEvent {
552                kind: MouseEventKind::ScrollUp,
553                ..
554            }) => Some(InputEvent::UpdateUpperMark(ps.upper_mark.saturating_sub(5))),
555            Event::Mouse(MouseEvent {
556                kind: MouseEventKind::ScrollDown,
557                ..
558            }) => Some(InputEvent::UpdateUpperMark(ps.upper_mark.saturating_add(5))),
559            // Go to top.
560            Event::Key(KeyEvent {
561                code: KeyCode::Char('g'),
562                modifiers: KeyModifiers::NONE,
563                ..
564            }) => Some(InputEvent::UpdateUpperMark(0)),
565            // Go to bottom.
566            Event::Key(KeyEvent {
567                code: KeyCode::Char('g'),
568                modifiers: KeyModifiers::SHIFT,
569                ..
570            })
571            | Event::Key(KeyEvent {
572                code: KeyCode::Char('G'),
573                modifiers: KeyModifiers::SHIFT,
574                ..
575            })
576            | Event::Key(KeyEvent {
577                code: KeyCode::Char('G'),
578                modifiers: KeyModifiers::NONE,
579                ..
580            }) => {
581                let mut position = ps
582                    .prefix_num
583                    .parse::<usize>()
584                    .unwrap_or(usize::MAX)
585                    // Reduce 1 here, because line numbering starts from 1
586                    // while upper_mark starts from 0
587                    .saturating_sub(1);
588                if position == 0 {
589                    position = usize::MAX;
590                }
591                Some(InputEvent::UpdateUpperMark(position))
592            }
593
594            // Page Up/Down
595            Event::Key(KeyEvent {
596                code: KeyCode::PageUp,
597                modifiers: KeyModifiers::NONE,
598                ..
599            }) => Some(InputEvent::UpdateUpperMark(
600                ps.upper_mark.saturating_sub(ps.rows - 1),
601            )),
602            Event::Key(KeyEvent {
603                code: c,
604                modifiers: KeyModifiers::NONE,
605                ..
606            }) if c == KeyCode::PageDown || c == KeyCode::Char(' ') => Some(
607                InputEvent::UpdateUpperMark(ps.upper_mark.saturating_add(ps.rows - 1)),
608            ),
609
610            // Resize event from the terminal.
611            Event::Resize(cols, rows) => {
612                Some(InputEvent::UpdateTermArea(cols as usize, rows as usize))
613            }
614            // Switch line number display.
615            Event::Key(KeyEvent {
616                code: KeyCode::Char('l'),
617                modifiers: KeyModifiers::CONTROL,
618                ..
619            }) => Some(InputEvent::UpdateLineNumber(!ps.line_numbers)),
620
621            // Quit.
622            Event::Key(KeyEvent {
623                code: KeyCode::Char('q'),
624                modifiers: KeyModifiers::NONE,
625                ..
626            })
627            | Event::Key(KeyEvent {
628                code: KeyCode::Char('c'),
629                modifiers: KeyModifiers::CONTROL,
630                ..
631            }) => Some(InputEvent::Exit),
632
633            // Horizontal scrolling
634            Event::Key(KeyEvent {
635                code: KeyCode::Char('h'),
636                modifiers,
637                ..
638            }) if modifiers == KeyModifiers::CONTROL.intersection(KeyModifiers::SHIFT) => {
639                Some(InputEvent::HorizontalScroll(!ps.screen.line_wrapping))
640            }
641
642            Event::Key(KeyEvent {
643                code: KeyCode::Char('h'),
644                modifiers: KeyModifiers::NONE,
645                ..
646            })
647            | Event::Key(KeyEvent {
648                code: KeyCode::Left,
649                modifiers: KeyModifiers::NONE,
650                ..
651            }) => Some(InputEvent::UpdateLeftMark(ps.left_mark.saturating_sub(1))),
652            Event::Key(KeyEvent {
653                code: KeyCode::Char('l'),
654                modifiers: KeyModifiers::NONE,
655                ..
656            })
657            | Event::Key(KeyEvent {
658                code: KeyCode::Right,
659                modifiers: KeyModifiers::NONE,
660                ..
661            }) => Some(InputEvent::UpdateLeftMark(ps.left_mark.saturating_add(1))),
662
663            // Search
664            #[cfg(feature = "search")]
665            Event::Key(KeyEvent {
666                code: KeyCode::Char('/'),
667                modifiers: KeyModifiers::NONE,
668                ..
669            }) => Some(InputEvent::Search(SearchMode::Forward)),
670            #[cfg(feature = "search")]
671            Event::Key(KeyEvent {
672                code: KeyCode::Char('?'),
673                modifiers: KeyModifiers::NONE,
674                ..
675            }) => Some(InputEvent::Search(SearchMode::Reverse)),
676            #[cfg(feature = "search")]
677            Event::Key(KeyEvent {
678                code: KeyCode::Char('n'),
679                modifiers: KeyModifiers::NONE,
680                ..
681            }) => {
682                let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
683                if ps.search_state.search_mode == SearchMode::Reverse {
684                    Some(InputEvent::MoveToPrevMatch(position))
685                } else {
686                    Some(InputEvent::MoveToNextMatch(position))
687                }
688            }
689            #[cfg(feature = "search")]
690            Event::Key(KeyEvent {
691                code: KeyCode::Char('p'),
692                modifiers: KeyModifiers::NONE,
693                ..
694            }) => {
695                let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
696                if ps.search_state.search_mode == SearchMode::Reverse {
697                    Some(InputEvent::MoveToNextMatch(position))
698                } else {
699                    Some(InputEvent::MoveToPrevMatch(position))
700                }
701            }
702            #[cfg(feature = "search")]
703            Event::Key(KeyEvent {
704                code: KeyCode::Char('i'),
705                modifiers: KeyModifiers::ALT,
706                ..
707            }) => Some(InputEvent::ToggleSmartCase),
708            _ => None,
709        }
710    }
711}
712#[cfg(test)]
713mod tests;