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}