Implement user input for `pijul-interaction`

finchie
Jul 21, 2023, 6:12 AM
JTELS6L36GEOOST2SUNCJIK5TBJDNLQWCF4IRF7QSHMMVCXSSESAC

Dependencies

  • [2] ABPFWGKH Create `pijul-interaction` crate
  • [*] EJ7TFFOW Re-adding Cargo.lock

Change contents

  • edit in pijul-interaction/src/lib.rs at line 1
    [2.3869]
    [2.3870]
    //! Wrapper functions around `dialoguer` to support Pijul's different modes of interactivity.
    mod input;
  • edit in pijul-interaction/src/lib.rs at line 5
    [2.3888]
    use input::{DefaultPrompt, PasswordPrompt, SelectionPrompt, TextPrompt};
    use std::sync::OnceLock;
    /// Global state for setting interactivity. Should be set to `Option::None`
    /// if no interactivity is possible, for example running Pijul with `--no-prompt`.
    static INTERACTIVE_CONTEXT: OnceLock<InteractiveContext> = OnceLock::new();
    /// Get the interactive context. If not set, returns an error.
    pub fn get_context() -> Result<InteractiveContext, InteractionError> {
    if let Some(context) = INTERACTIVE_CONTEXT.get() {
    Ok(*context)
    } else {
    Err(InteractionError::NoContext)
    }
    }
    /// Set the interactive context, panicking if already set.
    pub fn set_context(value: InteractiveContext) {
    // There probably isn't any reason for changing contexts at runtime
    INTERACTIVE_CONTEXT
    .set(value)
    .expect("Interactive context is already set!");
    }
    /// The different kinds of available prompts
    #[derive(Clone, Copy, Debug)]
    #[non_exhaustive]
    pub enum PromptType {
    Confirm,
    Input,
    Select,
    Password,
    }
    impl core::fmt::Display for PromptType {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
    let name = match *self {
    Self::Confirm => "confirm",
    Self::Input => "input",
    Self::Select => "fuzzy selection",
    Self::Password => "password",
    };
    write!(f, "{name}")
    }
    }
    /// Errors that can occur while attempting to interact with the user
    #[derive(thiserror::Error, Debug)]
    #[non_exhaustive]
    pub enum InteractionError {
    #[error("mode of interactivity not set")]
    NoContext,
    #[error("unable to provide interactivity in this context, and no valid default value for {0} prompt `{1}`")]
    NotInteractive(PromptType, String),
    #[error("I/O error while interacting with terminal")]
    IO(#[from] std::io::Error),
    }
    /// Different contexts for interacting with Pijul, for example terminal or web browser
    #[derive(Clone, Copy, Debug)]
    #[non_exhaustive]
    pub enum InteractiveContext {
    Terminal,
    NotInteractive,
    }
    /// A prompt that asks the user to select yes or no
    pub struct Confirm(Box<dyn DefaultPrompt<bool>>);
    /// A prompt that asks the user to choose from a list of items.
    pub struct Select(Box<dyn SelectionPrompt<usize>>);
    /// A prompt that asks the user to enter text input
    pub struct Input(Box<dyn TextPrompt<String>>);
    /// A prompt that asks the user to enter a password
    pub struct Password(Box<dyn PasswordPrompt<String>>);
  • file addition: input (d--r------)
    [2.1223]
  • file addition: terminal.rs (----------)
    [0.2632]
    use super::{BasePrompt, InteractionError, PasswordPrompt, TextPrompt, ValidationPrompt};
    use super::{DefaultPrompt, SelectionPrompt};
    pub use dialoguer::{Confirm, FuzzySelect as Select, Input, Password};
    use duplicate::duplicate_item;
    #[duplicate_item(
    handler with_generics return_type;
    [Confirm] [Confirm<'_>] [bool];
    [Input] [Input<'_, String>] [String];
    [Select] [Select<'_>] [usize];
    [Password] [Password<'_>] [String];
    )]
    impl BasePrompt<return_type> for with_generics {
    fn set_prompt(&mut self, prompt: String) {
    self.with_prompt(prompt);
    }
    fn interact(&mut self) -> Result<return_type, InteractionError> {
    Ok(handler::interact(self)?)
    }
    }
    #[duplicate_item(
    handler with_generics return_type;
    [Confirm] [Confirm<'_>] [bool];
    [Input] [Input<'_, String>] [String];
    [Select] [Select<'_>] [usize];
    )]
    impl DefaultPrompt<return_type> for with_generics {
    fn set_default(&mut self, value: return_type) {
    self.default(value);
    }
    }
    impl SelectionPrompt<usize> for Select<'_> {
    fn add_items(&mut self, items: &[String]) {
    Select::items(self, items);
    }
    }
    impl ValidationPrompt<String> for Input<'_, String> {
    fn set_validator(&mut self, validator: Box<dyn Fn(&String) -> Result<(), String>>) {
    self.validate_with(validator);
    }
    }
    impl ValidationPrompt<String> for Password<'_> {
    fn set_validator(&mut self, validator: Box<dyn Fn(&String) -> Result<(), String>>) {
    self.validate_with(validator);
    }
    }
    impl PasswordPrompt<String> for Password<'_> {
    fn set_confirmation(&mut self, confirm_prompt: String, mismatch_err: String) {
    self.with_confirmation(confirm_prompt, mismatch_err);
    }
    }
    impl TextPrompt<String> for Input<'_, String> {}
  • file addition: non_interactive.rs (----------)
    [0.2632]
    use super::{
    BasePrompt, DefaultPrompt, InteractionError, PasswordPrompt, PromptType, SelectionPrompt,
    TextPrompt, ValidationPrompt,
    };
    use core::fmt::Debug;
    use log::{error, info, warn};
    /// Holds state for non-interactive contexts so that non-interactive contexts
    /// such as `pijul XXX --no-prompt` can use the same interface, and to produce
    /// nicer debugging output.
    pub struct PseudoInteractive<T: Clone + Debug> {
    prompt_type: PromptType,
    prompt: Option<String>,
    default: Option<T>,
    items: Vec<String>,
    validator: Option<Box<dyn Fn(&T) -> Result<(), String>>>,
    confirmation: Option<(String, String)>,
    }
    impl<T: Clone + Debug> PseudoInteractive<T> {
    pub fn new(prompt_type: PromptType) -> Self {
    Self {
    prompt_type,
    prompt: None,
    default: None,
    items: Vec::new(),
    validator: None,
    confirmation: None,
    }
    }
    }
    impl<T: Clone + Debug> BasePrompt<T> for PseudoInteractive<T> {
    fn set_prompt(&mut self, prompt: String) {
    self.prompt = Some(prompt);
    }
    fn interact(&mut self) -> Result<T, InteractionError> {
    let prompt = self
    .prompt
    .clone()
    .unwrap_or_else(|| "[NO PROMPT SET]".to_owned());
    if let Some(default) = self.default.as_mut() {
    warn!(
    "Non-interactive context. The {:?} prompt `{prompt}` will default to {default:#?} .",
    self.prompt_type
    );
    if let Some(validator) = self.validator.as_mut() {
    warn!(
    "Non-interactive context. The {:?} prompt `{prompt}` will default to {default:#?} if valid.",
    self.prompt_type
    );
    match validator(default) {
    Ok(_) => {
    info!("Default value passed validation.");
    Ok(default.to_owned())
    }
    Err(err) => {
    error!("Default value failed validation: {err}");
    Err(InteractionError::NotInteractive(self.prompt_type, prompt))
    }
    }
    } else {
    warn!(
    "Non-interactive context. The {:?} prompt `{prompt}` will default to {default:#?}.",
    self.prompt_type
    );
    Ok(default.to_owned())
    }
    } else {
    error!("No default value found.");
    Err(InteractionError::NotInteractive(self.prompt_type, prompt))
    }
    }
    }
    impl<T: Clone + Debug> DefaultPrompt<T> for PseudoInteractive<T> {
    fn set_default(&mut self, value: T) {
    self.default = Some(value);
    }
    }
    impl<T: Clone + Debug> SelectionPrompt<T> for PseudoInteractive<T> {
    fn add_items(&mut self, items: &[String]) {
    self.items = Vec::from(items);
    }
    }
    impl<T: Clone + Debug> ValidationPrompt<T> for PseudoInteractive<T> {
    fn set_validator(&mut self, validator: Box<dyn Fn(&T) -> Result<(), String>>) {
    self.validator = Some(validator);
    }
    }
    impl<T: Clone + Debug> PasswordPrompt<T> for PseudoInteractive<T> {
    fn set_confirmation(&mut self, confirm_prompt: String, mismatch_err: String) {
    self.confirmation = Some((confirm_prompt, mismatch_err));
    }
    }
    impl<T: Clone + Debug> TextPrompt<T> for PseudoInteractive<T> {}
  • file addition: mod.rs (----------)
    [0.2632]
    //! Implement the various prompt types defined in `lib.rs`
    mod non_interactive;
    mod terminal;
    use crate::{Confirm, Input, Password, Select};
    use crate::{InteractionError, InteractiveContext, PromptType};
    use dialoguer::theme;
    use duplicate::duplicate_item;
    use lazy_static::lazy_static;
    use non_interactive::PseudoInteractive;
    lazy_static! {
    static ref THEME: Box<dyn theme::Theme + Send + Sync> = {
    use dialoguer::theme;
    use pijul_config::{self as config, Choice};
    if let Ok((config, _)) = config::Global::load() {
    let color_choice = config.colors.unwrap_or_default();
    match color_choice {
    Choice::Auto | Choice::Always => Box::<theme::ColorfulTheme>::default(),
    Choice::Never => Box::new(theme::SimpleTheme),
    }
    } else {
    Box::<theme::ColorfulTheme>::default()
    }
    };
    }
    /// A common interface shared by every prompt type.
    /// May be useful if you wish to abstract over different kinds of prompt.
    pub trait BasePrompt<T> {
    fn set_prompt(&mut self, prompt: String);
    fn interact(&mut self) -> Result<T, InteractionError>;
    }
    /// A trait for prompts that allow a default selection.
    pub trait DefaultPrompt<T>: BasePrompt<T> {
    fn set_default(&mut self, value: T);
    }
    /// A trait for prompts that may need validation of user input.
    ///
    /// This is mostly useful in contexts such as plain-text input or passwords,
    /// rather than on controlled input such as confirmation prompts.
    pub trait ValidationPrompt<T>: BasePrompt<T> {
    fn set_validator(&mut self, validator: Box<dyn Fn(&T) -> Result<(), String>>);
    }
    /// A trait for prompts that accept a password.
    pub trait PasswordPrompt<T>: ValidationPrompt<T> {
    fn set_confirmation(&mut self, confirm_prompt: String, mismatch_err: String);
    }
    /// A trait for prompts that accept text with a default value.
    /// Notably, this does NOT include passwords.
    pub trait TextPrompt<T>: ValidationPrompt<T> + DefaultPrompt<T> {}
    /// A trait for prompts where the user may choose from a selection of items.
    pub trait SelectionPrompt<T>: DefaultPrompt<T> {
    fn add_items(&mut self, items: &[String]);
    }
    #[duplicate_item(
    handler prompt_type return_type;
    [Confirm] [PromptType::Confirm] [bool];
    [Input] [PromptType::Input] [String];
    [Select] [PromptType::Select] [usize];
    [Password] [PromptType::Password] [String];
    )]
    impl handler {
    /// Create the prompt, returning an error if interactive context is incorrectly set.
    pub fn new() -> Result<Self, InteractionError> {
    Ok(Self(match crate::get_context()? {
    InteractiveContext::Terminal => Box::new(terminal::handler::with_theme(THEME.as_ref())),
    InteractiveContext::NotInteractive => Box::new(PseudoInteractive::new(prompt_type)),
    }))
    }
    /// Set the prompt.
    pub fn set_prompt(&mut self, prompt: String) {
    self.0.set_prompt(prompt);
    }
    /// Builder pattern for [`Self::set_prompt`]
    pub fn with_prompt<S: ToString>(&mut self, prompt: S) -> &mut Self {
    self.set_prompt(prompt.to_string());
    self
    }
    /// Present the prompt to the user. May return an error if in a non-interactive context,
    /// or interaction fails for any other reason
    pub fn interact(&mut self) -> Result<return_type, InteractionError> {
    self.0.interact()
    }
    }
    #[duplicate_item(
    handler return_type;
    [Confirm] [bool];
    [Input] [String];
    [Select] [usize];
    )]
    impl handler {
    /// Set the default selection. If the user does not input anything, this value will be used instead.
    pub fn set_default(&mut self, value: return_type) {
    self.0.set_default(value);
    }
    /// Builder pattern for [`Self::set_default`]
    pub fn with_default<I: Into<return_type>>(&mut self, value: I) -> &mut Self {
    self.set_default(value.into());
    self
    }
    }
    // TODO: check if clippy catches Into<String> -> ToString
    impl Select {
    /// Add items to be displayed in the selection prompt.
    pub fn add_items<S: ToString>(&mut self, items: &[S]) {
    let string_items: Vec<String> = items.iter().map(ToString::to_string).collect();
    self.0.add_items(string_items.as_slice());
    }
    /// Builder pattern for [`Self::add_items`].
    ///
    /// NOTE: if this function is called multiple times, it will add ALL items to the builder.
    pub fn with_items<S: ToString>(&mut self, items: &[S]) -> &mut Self {
    self.add_items(items);
    self
    }
    }
    impl Password {
    /// Ask the user to confirm the password with the provided prompt & error message.
    pub fn set_confirmation<S: ToString>(&mut self, confirm_prompt: S, mismatch_err: S) {
    self.0
    .set_confirmation(confirm_prompt.to_string(), mismatch_err.to_string());
    }
    /// Builder pattern for [`Self::set_confirmation`]
    pub fn with_confirmation<S: ToString>(
    &mut self,
    confirm_prompt: S,
    mismatch_err: S,
    ) -> &mut Self {
    self.set_confirmation(confirm_prompt, mismatch_err);
    self
    }
    }
    #[duplicate_item(
    handler prompt_type;
    [Input] [PromptType::Input];
    [Password] [PromptType::Password];
    )]
    impl handler {
    /// Set a validator to be run on input. If the validator returns [`Ok`], the input will be deemed
    /// valid. If the validator returns [`Err`], the prompt will display the error message
    pub fn set_validator<V, E>(&mut self, validator: V)
    where
    V: Fn(&String) -> Result<(), E> + 'static,
    E: ToString,
    {
    self.0
    .set_validator(Box::new(move |input| match validator(input) {
    Ok(()) => Ok(()),
    Err(e) => Err(e.to_string()),
    }));
    }
    /// Builder pattern for [`Self::set_validator`]
    pub fn with_validator<V, E>(&mut self, validator: V) -> &mut Self
    where
    V: Fn(&String) -> Result<(), E> + 'static,
    E: ToString,
    {
    self.set_validator(validator);
    self
    }
    }
  • edit in pijul-interaction/Cargo.toml at line 13
    [2.4308]
    [2.4308]
    dialoguer = { version = "0.10.4", features = ["fuzzy-select"] }
    duplicate = "1.0.0"
  • edit in pijul-interaction/Cargo.toml at line 17
    [2.4394]
    log = "0.4.19"
    thiserror = "1.0.43"
    pijul-config = { path = "../pijul-config" }
  • edit in Cargo.lock at line 836
    [4.17880]
    [4.17880]
    name = "duplicate"
    version = "1.0.0"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "de78e66ac9061e030587b2a2e75cc88f22304913c907b11307bca737141230cb"
    dependencies = [
    "heck",
    "proc-macro-error",
    ]
    [[package]]
  • edit in Cargo.lock at line 2262
    [2.662]
    [2.662]
    "dialoguer",
    "duplicate",
  • edit in Cargo.lock at line 2266
    [2.698]
    [4.48756]
    "log",
    "pijul-config",
    "thiserror",