rucline/prompt/
mod.rs

1//! Provides a method for presenting a prompt for user input that can be customized with [`actions`]
2//! and [`completions`].
3//!
4//! The core functionality of this module is [`read_line`]. Its invocation can be cumbersome due
5//! to required type annotations, therefore this module also provider a [`Builder`] which helps to
6//! craft the invocation to [`read_line`].
7//!
8//! ### Basic usage:
9//!
10//! ```no_run
11//! use rucline::Outcome::Accepted;
12//! use rucline::prompt::{Builder, Prompt};
13//!
14//! if let Ok(Accepted(string)) = Prompt::from("What's you favorite website? ")
15//!     // Add some tab completions (Optional)
16//!     .suggester(vec![
17//!         "https://www.rust-lang.org/",
18//!         "https://docs.rs/",
19//!         "https://crates.io/",
20//!     ])
21//!     //Block until value is ready
22//!     .read_line()
23//! {
24//!     println!("'{}' seems to be your favorite website", string);
25//! }
26//! ```
27//!
28//! [`actions`]: ../actions/enum.Action.html
29//! [`completions`]: ../completion/index.html
30//! [`read_line`]: fn.read_line.html
31//! [`Builder`]: trait.Builder.html
32
33mod builder;
34mod context;
35mod writer;
36
37use context::Context;
38use writer::Writer;
39
40use crate::actions::{action_for, Action, Direction, Overrider, Range, Scope};
41use crate::completion::{Completer, Suggester};
42use crate::Buffer;
43
44pub use builder::{Builder, Prompt};
45
46/// The outcome of [`read_line`], being either accepted or canceled by the user.
47///
48/// [`read_line`]: fn.read_line.html
49pub enum Outcome {
50    /// If the user accepts the prompt input, i.e. an [`Accept`] event was emitted. this variant will
51    /// contain the accepted text.
52    ///
53    /// [`Accept`]: ../actions/enum.Action.html#variant.Accept
54    Accepted(String),
55    /// If the user cancels the prompt input, i.e. a [`Cancel`] event was emitted. this variant will
56    /// contain the rejected buffer, with text and cursor position intact from the moment of
57    /// rejection.
58    ///
59    /// [`Cancel`]: ../actions/enum.Action.html#variant.Cancel
60    Canceled(Buffer),
61}
62
63impl Outcome {
64    /// Returns true if the outcome was accepted.
65    #[must_use]
66    pub fn was_acceoted(&self) -> bool {
67        matches!(self, Outcome::Accepted(_))
68    }
69
70    /// Returns accepted text.
71    ///
72    /// # Panics
73    ///
74    /// Panics if the [`Outcome`] is [`Canceled`]
75    ///
76    /// [`Outcome`]: enum.Outcome.html
77    /// [`Canceled`]: enum.Outcome.html#variant.Canceled
78    #[must_use]
79    pub fn unwrap(self) -> String {
80        if let Outcome::Accepted(string) = self {
81            string
82        } else {
83            panic!("called `Outcome::unwrap()` on a `Canceled` value")
84        }
85    }
86
87    /// Converts this [`Outcome`] into an optional containing the accepted text.
88    ///
89    /// # Return
90    /// * `Some(String)` - If the [`Outcome`] is [`accepted`].
91    /// * `None` - If the [`Outcome`] is [`canceled`].
92    ///
93    /// [`Outcome`]: enum.Outcome.html
94    /// [`accepted`]: enum.Outcome.html#variant.Accepted
95    /// [`canceled`]: enum.Outcome.html#variant.Canceled
96    #[must_use]
97    pub fn some(self) -> Option<String> {
98        match self {
99            Outcome::Accepted(string) => Some(string),
100            Outcome::Canceled(_) => None,
101        }
102    }
103
104    /// Converts this [`Outcome`] into a result containing the accepted text or the canceled buffer.
105    ///
106    /// # Return
107    /// * `Ok(String)` - If the [`Outcome`] is [`accepted`].
108    /// * `Err(Buffer)` - If the [`Outcome`] is [`canceled`].
109    ///
110    /// # Errors
111    /// * [`Buffer`] - If the user canceled the input.
112    ///
113    /// [`Outcome`]: enum.Outcome.html
114    /// [`Buffer`]: ../buffer/struct.Buffer.html
115    /// [`accepted`]: enum.Outcome.html#variant.Accepted
116    /// [`canceled`]: enum.Outcome.html#variant.Canceled
117    pub fn ok(self) -> Result<String, Buffer> {
118        match self {
119            Outcome::Accepted(string) => Ok(string),
120            Outcome::Canceled(buffer) => Err(buffer),
121        }
122    }
123}
124
125// TODO: Support crossterm async
126/// Analogous to `std::io::stdin().read_line()`, however providing all the customization
127/// configured in the passed parameters.
128///
129/// This method will block until an input is committed by the user.
130///
131/// Calling this method directly can be cumbersome, therefore it is recommended to use the helper
132/// [`Prompt`] and [`Builder`] to craft the call.
133///
134/// # Return
135/// * [`Outcome`] - Either [`Accepted`] containing the user input, or [`Canceled`]
136/// containing the rejected [`buffer`].
137///
138/// # Errors
139/// * [`Error`] - If an error occurred while reading the user input.
140///
141/// [`Accepted`]: enum.Outcome.html#variant.Accepted
142/// [`Builder`]: trait.Builder.html
143/// [`Canceled`]: enum.Outcome.html#variant.Canceled
144/// [`Error`]: ../enum.Error.html
145/// [`Outcome`]: enum.Outcome.html
146/// [`Prompt`]: struct.Prompt.html
147/// [`buffer`]: ../buffer/struct.Buffer.html
148pub fn read_line<O, C, S>(
149    prompt: Option<&str>,
150    buffer: Option<Buffer>,
151    erase_after_read: bool,
152    overrider: Option<&O>,
153    completer: Option<&C>,
154    suggester: Option<&S>,
155) -> Result<Outcome, crate::Error>
156where
157    O: Overrider + ?Sized,
158    C: Completer + ?Sized,
159    S: Suggester + ?Sized,
160{
161    let mut context = Context::new(
162        erase_after_read,
163        prompt.as_deref(),
164        buffer,
165        completer,
166        suggester,
167    )?;
168
169    context.print()?;
170    loop {
171        if let crossterm::event::Event::Key(e) = crossterm::event::read()? {
172            match action_for(overrider, e, &context) {
173                Action::Write(c) => context.write(c)?,
174                Action::Delete(scope) => context.delete(scope)?,
175                Action::Move(range, direction) => context.move_cursor(range, direction)?,
176                Action::Complete(range) => context.complete(range)?,
177                Action::Suggest(direction) => context.suggest(direction)?,
178                Action::Noop => continue,
179                Action::Cancel => {
180                    if context.is_suggesting() {
181                        context.cancel_suggestion()?;
182                    } else {
183                        return Ok(Outcome::Canceled(context.into()));
184                    }
185                }
186                Action::Accept => return Ok(Outcome::Accepted(context.buffer_as_string())),
187            }
188        }
189    }
190}