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;