rucline/
actions.rs

1//! Provides mappings and actions to change the behavior of the [`prompt`] when reading user input.
2//!
3//! There is a built-in set of default [`Action`]s that will be executed upon user interaction.
4//! These are meant to feel natural when coming from the default terminal, while also adding further
5//! functionality and editing commands.
6//!
7//! However, bindings that override the default behavior can be given to the [`prompt`] to cause
8//! a different [`Action`] to be taken.
9//!
10//! # Examples
11//!
12//! Changing the behavior of `TAB` from the default [`Suggest`] action to actually printing `\t`.
13//!
14//! Using [`KeyBindings`]:
15//! ```
16//! use rucline::actions::{Action, Event, KeyBindings, KeyCode};
17//! use rucline::prompt::{Builder, Prompt};
18//!
19//! let mut bindings = KeyBindings::new();
20//! bindings.insert(Event::from(KeyCode::Tab), Action::Write('\t'));
21//!
22//! let prompt = Prompt::new().overrider(bindings);
23//! ```
24//!
25//! Using a closure:
26//! ```
27//! use rucline::actions::{Action, Event, KeyCode};
28//! use rucline::prompt::{Builder, Prompt};
29//!
30//! let prompt = Prompt::new().overrider_fn(|e, _| {
31//!     if e == Event::from(KeyCode::Tab) {
32//!         Some(Action::Write('\t'))
33//!     } else {
34//!         None
35//!     }
36//! });
37//! ```
38//!
39//! # Overriding a default action
40//!
41//! The [`KeyBindings`] map provides a way to store the association
42//! between [`Event`] and [`Action`] which will override the default behavior.
43//!
44//! ```
45//! use rucline::actions::{Action, Event, KeyBindings, KeyCode};
46//!
47//! let mut bindings = KeyBindings::new();
48//! bindings.insert(Event::from(KeyCode::Tab), Action::Write('\t'));
49//! ```
50//!
51//! # Disabling a default action
52//!
53//! To explicitly remove an [`Action`] from the default behavior, the [`Noop`] action can be
54//! set as the override.
55//!
56//! ```
57//! use rucline::actions::{Action, Event, KeyBindings, KeyCode};
58//!
59//! let mut bindings = KeyBindings::new();
60//! bindings.insert(Event::from(KeyCode::Tab), Action::Noop);
61//! ```
62//!
63//! # Saving key binding configurations
64//!
65//! If the feature `serialize` is enabled, [`KeyBindings`] can be serialized, stored, and loaded
66//! at runtime.
67//!
68//! # Default behavior
69//!
70//! In the absence of [`KeyBindings`] or an entry for a given [`Event`], the default behavior
71//! will be as follows:
72//!
73//! ```no_run
74//! # fn default_action(event: rucline::actions::Event) -> rucline::actions::Action {
75//! # use rucline::actions::{Action::*, Direction::*, KeyCode, Range::*, Scope::* };
76//! # match event.code {
77//! KeyCode::Enter => Accept,
78//! KeyCode::Esc => Cancel,
79//! KeyCode::Tab => Suggest(Forward),
80//! KeyCode::BackTab => Suggest(Backward),
81//! KeyCode::Backspace => Delete(Relative(Single, Backward)),
82//! KeyCode::Delete => Delete(Relative(Single, Forward)),
83//! KeyCode::Right => Move(Single, Forward),
84//! KeyCode::Left => Move(Single, Backward),
85//! KeyCode::Home => Move(Line, Backward),
86//! KeyCode::End => Move(Line, Forward),
87//! KeyCode::Char(c) => {
88//!     if event.modifiers == crossterm::event::KeyModifiers::CONTROL {
89//!         match c {
90//!             'm' | 'd' => Accept,
91//!             'c' => Cancel,
92//!
93//!             'b' => Move(Single, Backward),
94//!             'f' => Move(Single, Forward),
95//!             'a' => Move(Line, Backward),
96//!             'e' => Move(Line, Forward),
97//!
98//!             'j' => Delete(Relative(Word, Backward)),
99//!             'k' => Delete(Relative(Word, Forward)),
100//!             'h' => Delete(Relative(Line, Backward)),
101//!             'l' => Delete(Relative(Line, Forward)),
102//!             'w' => Delete(WholeWord),
103//!             'u' => Delete(WholeLine),
104//!             _ => Noop,
105//!         }
106//!     } else if event.modifiers == crossterm::event::KeyModifiers::ALT {
107//!         match c {
108//!             'b' => Move(Word, Backward),
109//!             'f' => Move(Word, Forward),
110//!             _ => Noop,
111//!         }
112//!     } else {
113//!         Write(c)
114//!     }
115//! }
116//! _ => Noop,
117//! # }}
118//! ```
119//!
120//!  > Check the test cases for [`Buffer`] to see how line edits are expected to behave.
121//!
122//! [`Action`]: enum.Action.html
123//! [`Event`]: type.Event.html
124//! [`KeyBindings`]: type.KeyBindings.html
125//! [`Noop`]: enum.Action.html#variant.Noop
126//! [`Suggest`]: enum.Action.html#variant.Suggest
127//! [`prompt`]: ../prompt/index.html
128//! [`Buffer`]: ../buffer/struct.Buffer.html
129
130use crate::Buffer;
131
132#[cfg(feature = "serde")]
133use serde::{Deserialize, Serialize};
134
135/// Alias to `crossterm::event::KeyEvent` from [`crossterm`](https://docs.rs/crossterm/).
136pub use crossterm::event::KeyEvent as Event;
137
138/// Alias to `crossterm::event::KeyCode` from [`crossterm`](https://docs.rs/crossterm/).
139pub use crossterm::event::KeyCode;
140
141/// Alias to [`HashMap<Event, Action>`](std::collections::HashMap)
142pub type KeyBindings = std::collections::HashMap<Event, Action>;
143
144/// An action that can be performed while reading a line
145#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
146#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
147pub enum Action {
148    /// Write a single character where the cursor is
149    Write(char),
150    /// Delete a section based on the cursor, defined by [`Scope`](enum.Scope.html)
151    Delete(Scope),
152    /// Move the cursor for a [`Range`](enum.Range.html) in a [`Direction`](enum.Direction.html)
153    Move(Range, Direction),
154    /// Trigger the [`suggester`](../completion/trait.Suggester.html)
155    Suggest(Direction),
156    /// Accept [`Range`](enum.Range.html) from the current completion presented by
157    /// [`completer`](../completion/trait.Completer.html), if any
158    Complete(Range),
159    /// Accept the current line
160    Accept,
161    /// Cancel the suggestions, if any. Else, discard the whole line
162    Cancel,
163    /// Do nothing and wait for the next [`Event`](type.Event.html)
164    Noop,
165}
166
167/// The scope an [`Action`](enum.Action.html) should be applied on
168#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
169#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
170pub enum Scope {
171    /// Represents a whole line
172    WholeLine,
173    /// Represents a whole word
174    WholeWord,
175    /// Represents a relative scope, with a [`Range`](enum.Range.html)
176    /// and [`Direction`](enum.Direction.html)
177    Relative(Range, Direction),
178}
179
180/// The range an [`Action`](enum.Action.html) should extend for
181#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
182#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
183pub enum Range {
184    /// Represents the remainder of the line
185    Line,
186    /// Represents a single word
187    Word,
188    /// Represents a single character
189    Single,
190}
191
192/// The direction an [`Action`](enum.Action.html) may take
193#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
194#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
195pub enum Direction {
196    /// Represents a "right" or "down" direction
197    Forward,
198    /// Represents a "left" or "up" direction
199    Backward,
200}
201
202/// Overrides the behavior for a given [`Event`].
203///
204/// This trait has a convenience implementation for [`KeyBindings`] and also a conversion
205/// from closures.
206///
207/// # Example
208///
209/// ```
210/// use rucline::actions::{Action, Event, KeyCode};
211/// use rucline::prompt::{Builder, Prompt};
212///
213/// let prompt = Prompt::new().overrider_fn(|e, _| if e == Event::from(KeyCode::Tab) {
214///     Some(Action::Write('\t'))
215/// } else {
216///     None
217/// });
218/// ```
219///
220/// [`Event`]: type.Event.html
221/// [`KeyBindings`]: type.KeyBindings.html
222pub trait Overrider {
223    /// Overrides the behavior for the given [`Event`].
224    ///
225    /// [`Buffer`] will contain the current context of the prompt, in case the behavior should
226    /// be contextual.
227    ///
228    /// # Arguments
229    /// * [`event`] - The incoming event to be processed.
230    /// * [`buffer`] - The current context in which this event is coming in.
231    ///
232    /// [`Event`]: type.Event.html
233    /// [`Buffer`]: ../buffer/struct.Buffer.html
234    fn override_for(&self, event: Event, buffer: &Buffer) -> Option<Action>;
235}
236
237impl Overrider for KeyBindings {
238    fn override_for(&self, event: Event, _: &Buffer) -> Option<Action> {
239        self.get(&event).copied()
240    }
241}
242
243impl<F> Overrider for F
244where
245    F: Fn(Event, &Buffer) -> Option<Action>,
246{
247    fn override_for(&self, event: Event, buffer: &Buffer) -> Option<Action> {
248        self(event, buffer)
249    }
250}
251
252pub(super) fn action_for<O: Overrider + ?Sized>(
253    overrider: Option<&O>,
254    event: Event,
255    buffer: &Buffer,
256) -> Action {
257    overrider
258        .as_ref()
259        .and_then(|b| b.override_for(event, buffer))
260        .unwrap_or_else(|| default_action(event, buffer))
261}
262
263#[inline]
264fn control_pressed(event: &Event) -> bool {
265    event.modifiers == crossterm::event::KeyModifiers::CONTROL
266}
267
268#[inline]
269fn alt_pressed(event: &Event) -> bool {
270    event.modifiers == crossterm::event::KeyModifiers::ALT
271}
272
273#[inline]
274fn complete_if_at_end_else_move(buffer: &Buffer, range: Range) -> Action {
275    if buffer.cursor() == buffer.len() {
276        if range == Range::Word {
277            Action::Complete(Range::Word)
278        } else {
279            Action::Complete(Range::Line)
280        }
281    } else {
282        Action::Move(range, Direction::Forward)
283    }
284}
285
286fn default_action(event: Event, buffer: &Buffer) -> Action {
287    use Action::{Accept, Cancel, Delete, Move, Noop, Suggest, Write};
288    use Direction::{Backward, Forward};
289    use Range::{Line, Single, Word};
290    use Scope::{Relative, WholeLine, WholeWord};
291
292    match event.code {
293        KeyCode::Enter => Accept,
294        KeyCode::Esc => Cancel,
295        KeyCode::Tab => Suggest(Forward),
296        KeyCode::BackTab => Suggest(Backward),
297        KeyCode::Backspace => Delete(Relative(Single, Backward)),
298        KeyCode::Delete => Delete(Relative(Single, Forward)),
299        KeyCode::Right => complete_if_at_end_else_move(buffer, Single),
300        KeyCode::Left => Move(Single, Backward),
301        KeyCode::Home => Move(Line, Backward),
302        KeyCode::End => complete_if_at_end_else_move(buffer, Line),
303        KeyCode::Char(c) => {
304            if control_pressed(&event) {
305                match c {
306                    'm' | 'd' => Accept,
307                    'c' => Cancel,
308
309                    'b' => Move(Single, Backward),
310                    'f' => complete_if_at_end_else_move(buffer, Single),
311                    'a' => Move(Line, Backward),
312                    'e' => complete_if_at_end_else_move(buffer, Line),
313
314                    'j' => Delete(Relative(Word, Backward)),
315                    'k' => Delete(Relative(Word, Forward)),
316                    'h' => Delete(Relative(Line, Backward)),
317                    'l' => Delete(Relative(Line, Forward)),
318                    'w' => Delete(WholeWord),
319                    'u' => Delete(WholeLine),
320                    _ => Noop,
321                }
322            } else if alt_pressed(&event) {
323                match c {
324                    'b' => Move(Word, Backward),
325                    'f' => complete_if_at_end_else_move(buffer, Word),
326                    _ => Noop,
327                }
328            } else {
329                Write(c)
330            }
331        }
332        _ => Noop,
333    }
334}
335
336#[cfg(test)]
337mod test {
338    use super::{action_for, default_action, Action, Buffer, Direction, Event, KeyCode, Range};
339
340    #[test]
341    fn should_complete_if_at_end() {
342        use crossterm::event::KeyModifiers;
343        use Action::{Complete, Move};
344        use Direction::Forward;
345        use KeyCode::{Char, End, Right};
346        use Range::{Line, Single, Word};
347
348        let mut c = "a".into();
349
350        assert_eq!(default_action(Event::from(Right), &c), Complete(Line));
351        assert_eq!(default_action(Event::from(End), &c), Complete(Line));
352        assert_eq!(
353            default_action(Event::new(Char('f'), KeyModifiers::CONTROL), &c),
354            Complete(Line)
355        );
356        assert_eq!(
357            default_action(Event::new(Char('f'), KeyModifiers::ALT), &c),
358            Complete(Word)
359        );
360
361        c.set_cursor(0).unwrap();
362
363        assert_eq!(
364            default_action(Event::from(Right), &c),
365            Move(Single, Forward)
366        );
367        assert_eq!(default_action(Event::from(End), &c), Move(Line, Forward));
368        assert_eq!(
369            default_action(Event::new(Char('f'), KeyModifiers::CONTROL), &c),
370            Move(Single, Forward)
371        );
372        assert_eq!(
373            default_action(Event::new(Char('f'), KeyModifiers::ALT), &c),
374            Move(Word, Forward)
375        );
376    }
377
378    #[test]
379    fn should_default_if_no_mapping() {
380        use super::KeyBindings;
381        use KeyCode::Tab;
382        let action = action_for::<KeyBindings>(None, Event::from(Tab), &Buffer::new());
383        assert_eq!(action, Action::Suggest(Direction::Forward));
384    }
385
386    mod basic {
387        use super::super::{
388            action_for, Action, Buffer, Direction, Event, KeyBindings, KeyCode::Tab,
389        };
390
391        #[test]
392        fn should_default_if_event_missing_form_mapping() {
393            let overrider = KeyBindings::new();
394            let action = action_for(Some(&overrider), Event::from(Tab), &Buffer::new());
395            assert_eq!(action, Action::Suggest(Direction::Forward));
396        }
397
398        #[test]
399        fn should_override_if_defined() {
400            let mut bindings = KeyBindings::new();
401            bindings.insert(Event::from(Tab), Action::Write('\t'));
402            let action = action_for(Some(&bindings), Event::from(Tab), &Buffer::new());
403            assert_eq!(action, Action::Write('\t'));
404        }
405    }
406
407    mod closure {
408        use super::super::{action_for, Action, Buffer, Direction, Event, KeyCode::Tab};
409
410        #[test]
411        fn should_default_if_event_missing_form_mapping() {
412            let overrider = |_, _: &Buffer| None;
413            let action = action_for(Some(&overrider), Event::from(Tab), &Buffer::new());
414            assert_eq!(action, Action::Suggest(Direction::Forward));
415        }
416
417        #[test]
418        fn should_override_if_defined() {
419            let overrider = |e, _: &Buffer| {
420                if e == Event::from(Tab) {
421                    Some(Action::Write('\t'))
422                } else {
423                    None
424                }
425            };
426            let action = action_for(Some(&overrider), Event::from(Tab), &Buffer::new());
427            assert_eq!(action, Action::Write('\t'));
428        }
429    }
430}