1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! Provides a method for presenting a prompt for user input that can be customized with [`actions`]
//! and [`completions`].
//!
//! The core functionality of this module is [`read_line`]. Its invocation can be cumbersome due
//! to required type annotations, therefore this module also provider a [`Builder`] which helps to
//! craft the invocation to [`read_line`].
//!
//! ### Basic usage:
//!
//! ```no_run
//! use rucline::Outcome::Accepted;
//! use rucline::prompt::{Builder, Prompt};
//!
//! if let Ok(Accepted(string)) = Prompt::from("What's you favorite website? ")
//!     // Add some tab completions (Optional)
//!     .suggester(vec![
//!         "https://www.rust-lang.org/",
//!         "https://docs.rs/",
//!         "https://crates.io/",
//!     ])
//!     //Block until value is ready
//!     .read_line()
//! {
//!     println!("'{}' seems to be your favorite website", string);
//! }
//! ```
//!
//! [`actions`]: ../actions/enum.Action.html
//! [`completions`]: ../completion/index.html
//! [`read_line`]: fn.read_line.html
//! [`Builder`]: trait.Builder.html

mod builder;
mod context;
mod writer;

use context::Context;
use writer::Writer;

use crate::actions::{action_for, Action, Direction, Overrider, Range, Scope};
use crate::completion::{Completer, Suggester};
use crate::Buffer;

pub use builder::{Builder, Prompt};

/// The outcome of [`read_line`], being either accepted or canceled by the user.
///
/// [`read_line`]: fn.read_line.html
pub enum Outcome {
    /// If the user accepts the prompt input, i.e. an [`Accept`] event was emitted. this variant will
    /// contain the accepted text.
    ///
    /// [`Accept`]: ../actions/enum.Action.html#variant.Accept
    Accepted(String),
    /// If the user cancels the prompt input, i.e. a [`Cancel`] event was emitted. this variant will
    /// contain the rejected buffer, with text and cursor position intact from the moment of
    /// rejection.
    ///
    /// [`Cancel`]: ../actions/enum.Action.html#variant.Cancel
    Canceled(Buffer),
}

impl Outcome {
    /// Returns true if the outcome was accepted.
    #[must_use]
    pub fn was_acceoted(&self) -> bool {
        matches!(self, Outcome::Accepted(_))
    }

    /// Returns accepted text.
    ///
    /// # Panics
    ///
    /// Panics if the [`Outcome`] is [`Canceled`]
    ///
    /// [`Outcome`]: enum.Outcome.html
    /// [`Canceled`]: enum.Outcome.html#variant.Canceled
    #[must_use]
    pub fn unwrap(self) -> String {
        if let Outcome::Accepted(string) = self {
            string
        } else {
            panic!("called `Outcome::unwrap()` on a `Canceled` value")
        }
    }

    /// Converts this [`Outcome`] into an optional containing the accepted text.
    ///
    /// # Return
    /// * `Some(String)` - If the [`Outcome`] is [`accepted`].
    /// * `None` - If the [`Outcome`] is [`canceled`].
    ///
    /// [`Outcome`]: enum.Outcome.html
    /// [`accepted`]: enum.Outcome.html#variant.Accepted
    /// [`canceled`]: enum.Outcome.html#variant.Canceled
    #[must_use]
    pub fn some(self) -> Option<String> {
        match self {
            Outcome::Accepted(string) => Some(string),
            Outcome::Canceled(_) => None,
        }
    }

    /// Converts this [`Outcome`] into a result containing the accepted text or the canceled buffer.
    ///
    /// # Return
    /// * `Ok(String)` - If the [`Outcome`] is [`accepted`].
    /// * `Err(Buffer)` - If the [`Outcome`] is [`canceled`].
    ///
    /// # Errors
    /// * [`Buffer`] - If the user canceled the input.
    ///
    /// [`Outcome`]: enum.Outcome.html
    /// [`Buffer`]: ../buffer/struct.Buffer.html
    /// [`accepted`]: enum.Outcome.html#variant.Accepted
    /// [`canceled`]: enum.Outcome.html#variant.Canceled
    pub fn ok(self) -> Result<String, Buffer> {
        match self {
            Outcome::Accepted(string) => Ok(string),
            Outcome::Canceled(buffer) => Err(buffer),
        }
    }
}

// TODO: Support crossterm async
/// Analogous to `std::io::stdin().read_line()`, however providing all the customization
/// configured in the passed parameters.
///
/// This method will block until an input is committed by the user.
///
/// Calling this method directly can be cumbersome, therefore it is recommended to use the helper
/// [`Prompt`] and [`Builder`] to craft the call.
///
/// # Return
/// * [`Outcome`] - Either [`Accepted`] containing the user input, or [`Canceled`]
/// containing the rejected [`buffer`].
///
/// # Errors
/// * [`Error`] - If an error occurred while reading the user input.
///
/// [`Accepted`]: enum.Outcome.html#variant.Accepted
/// [`Builder`]: trait.Builder.html
/// [`Canceled`]: enum.Outcome.html#variant.Canceled
/// [`Error`]: ../enum.Error.html
/// [`Outcome`]: enum.Outcome.html
/// [`Prompt`]: struct.Prompt.html
/// [`buffer`]: ../buffer/struct.Buffer.html
pub fn read_line<O, C, S>(
    prompt: Option<&str>,
    buffer: Option<Buffer>,
    erase_after_read: bool,
    overrider: Option<&O>,
    completer: Option<&C>,
    suggester: Option<&S>,
) -> Result<Outcome, crate::Error>
where
    O: Overrider + ?Sized,
    C: Completer + ?Sized,
    S: Suggester + ?Sized,
{
    let mut context = Context::new(
        erase_after_read,
        prompt.as_deref(),
        buffer,
        completer,
        suggester,
    )?;

    context.print()?;
    loop {
        if let crossterm::event::Event::Key(e) = crossterm::event::read()? {
            match action_for(overrider, e, &context) {
                Action::Write(c) => context.write(c)?,
                Action::Delete(scope) => context.delete(scope)?,
                Action::Move(range, direction) => context.move_cursor(range, direction)?,
                Action::Complete(range) => context.complete(range)?,
                Action::Suggest(direction) => context.suggest(direction)?,
                Action::Noop => continue,
                Action::Cancel => {
                    if context.is_suggesting() {
                        context.cancel_suggestion()?;
                    } else {
                        return Ok(Outcome::Canceled(context.into()));
                    }
                }
                Action::Accept => return Ok(Outcome::Accepted(context.buffer_as_string())),
            }
        }
    }
}