This should make it much easier to run Pijul in a bunch of different contexts, and better handle scenarios such as --no-prompt. If the changes do prove to be actually useful, they might even be worth upstreaming into the dialoguer crate.
JTELS6L36GEOOST2SUNCJIK5TBJDNLQWCF4IRF7QSHMMVCXSSESAC 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 runtimeINTERACTIVE_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 nopub 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 inputpub struct Input(Box<dyn TextPrompt<String>>);/// A prompt that asks the user to enter a passwordpub struct Password(Box<dyn PasswordPrompt<String>>);
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> {}
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> {}
//! 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 reasonpub 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> -> ToStringimpl 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 messagepub fn set_validator<V, E>(&mut self, validator: V)whereV: 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 SelfwhereV: Fn(&String) -> Result<(), E> + 'static,E: ToString,{self.set_validator(validator);self}}
log = "0.4.19"thiserror = "1.0.43"pijul-config = { path = "../pijul-config" }