diff options
Diffstat (limited to 'mingling')
| -rw-r--r-- | mingling/src/example_docs.rs | 1 | ||||
| -rw-r--r-- | mingling/src/res.rs | 12 | ||||
| -rw-r--r-- | mingling/src/res/confirmer.rs | 268 | ||||
| -rw-r--r-- | mingling/src/setups.rs | 31 | ||||
| -rw-r--r-- | mingling/src/setups/confirmer.rs | 60 | ||||
| -rw-r--r-- | mingling/src/setups/stdin_args.rs | 82 |
6 files changed, 435 insertions, 19 deletions
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index 55aabdf..c292598 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -2970,6 +2970,7 @@ pub mod example_setup {} /// path = "../../mingling" /// features = [ /// "structural_renderer", +/// "yaml_serde_fmt", /// "parser", /// ] /// diff --git a/mingling/src/res.rs b/mingling/src/res.rs index a35559c..ab524cd 100644 --- a/mingling/src/res.rs +++ b/mingling/src/res.rs @@ -1,9 +1,11 @@ -// Doc Not Optimize -mod exit_code; -pub use exit_code::*; +#[allow(unused_imports)] +pub use mingling_core::core_res::*; mod dirs; pub use dirs::*; -#[allow(unused_imports)] -pub use mingling_core::core_res::*; +mod exit_code; +pub use exit_code::*; + +mod confirmer; +pub use confirmer::*; diff --git a/mingling/src/res/confirmer.rs b/mingling/src/res/confirmer.rs new file mode 100644 index 0000000..900562b --- /dev/null +++ b/mingling/src/res/confirmer.rs @@ -0,0 +1,268 @@ +use std::io::{BufRead, Write}; + +/// A confirmer for interactive confirmation. +/// +/// This structure caches the confirmed state to avoid repeated prompts. +/// +/// Typically, `Confirmer` is registered via [`ConfirmerSetup`], and then injected into functions +/// through Mingling's resource injection system. +/// +/// # Registration +/// +/// Before use, the [`ConfirmerSetup`] must be registered with the program: +/// +/// ``` +/// # use mingling::MockProgramCollect as ThisProgram; +/// use mingling::setup::ConfirmerSetup; +/// use mingling::Program; +/// +/// let mut program = Program::<ThisProgram>::new(); +/// program.with_setup(ConfirmerSetup); +/// ``` +/// +/// # Examples +/// +/// ``` +/// use mingling::res::{Confirmer, YesConfirm}; +/// +/// // In actual use, obtain the registered confirmer through the resource injection system +/// let confirmer = Confirmer::new_confirmed(); +/// assert!(confirmer.ask::<YesConfirm>("Continue? [y/n] ")); +/// ``` +#[derive(Debug, Default, Clone, Copy)] +pub struct Confirmer { + pub(crate) confirmed: bool, +} + +impl Confirmer { + /// Creates a new `Confirmer` instance. + /// + /// # Examples + /// + /// ``` + /// use mingling::res::Confirmer; + /// + /// let confirmer = Confirmer::new(); + /// ``` + #[must_use] + pub const fn new() -> Self { + Self { confirmed: false } + } + + /// Creates a `Confirmer` instance in the confirmed state. + /// + /// The returned `Confirmer` will directly return `true` when calling [`ask`](Confirmer::ask) or + /// [`try_ask`](Confirmer::try_ask), without prompting the user. + /// + /// # Examples + /// + /// ``` + /// use mingling::res::{Confirmer, YesConfirm}; + /// + /// let confirmer = Confirmer::new_confirmed(); + /// assert!(confirmer.ask::<YesConfirm>("Continue? [y/n] ")); + /// ``` + #[must_use] + pub const fn new_confirmed() -> Self { + Self { confirmed: true } + } + + /// Marks the confirmer as confirmed. + /// + /// After calling this method, subsequent calls to [`ask`](Confirmer::ask) or + /// [`try_ask`](Confirmer::try_ask) on this confirmer will directly return `true` + /// without prompting the user. + /// + /// # Examples + /// + /// ``` + /// use mingling::res::{Confirmer, YesConfirm}; + /// + /// let mut confirmer = Confirmer::new(); + /// confirmer.set_confirmed(); + /// assert!(confirmer.ask::<YesConfirm>("Continue? [y/n] ")); + /// ``` + pub const fn set_confirmed(&mut self) { + self.confirmed = true; + } + + /// Asks the user a confirmation question, with at most one attempt. + /// + /// Returns `false` if the user provides an unrecognizable answer. + /// Returns `true` directly if already confirmed previously. + /// + /// # Parameters + /// + /// * `ask` - The prompt text to display to the user. + /// + /// # Returns + /// + /// Returns a boolean indicating whether the user confirmed. Returns `false` if the user's input + /// could not be parsed or the maximum number of attempts was reached. + /// + /// # Examples + /// + /// ``` + /// use mingling::res::{Confirmer, YesConfirm}; + /// + /// let confirmer = Confirmer::new_confirmed(); + /// let confirmed = confirmer.ask::<YesConfirm>("Delete this file? [y/n] "); + /// ``` + pub fn ask<P: ConfirmerPredicate>(&self, ask: impl AsRef<str>) -> bool { + self.try_ask::<P>(ask, ConfirmerCount::Max(1)) + .unwrap_or(false) + } + + /// Asks the user a confirmation question, allowing a specified maximum number of attempts. + /// + /// # Parameters + /// + /// * `ask` - The prompt text to display to the user. + /// * `count` - The maximum number of attempts. Passing `0` means unlimited attempts (loop + /// indefinitely), passing a positive integer means at most that many attempts. + /// + /// # Returns + /// + /// Returns `Some(true)` for confirmation, `Some(false)` for rejection. + /// Returns `None` if the maximum number of attempts is reached without being able to parse + /// the user's input. + /// + /// # Panics + /// + /// This function panics when the standard error output (`stderr`) cannot be flushed or when + /// reading from standard input fails. + /// + /// # Examples + /// + /// ``` + /// use mingling::res::{Confirmer, YesConfirm}; + /// + /// let confirmer = Confirmer::new_confirmed(); + /// let confirmed = confirmer.try_ask::<YesConfirm>("Confirm execution? [y/n] ", 3); + /// ``` + pub fn try_ask<P: ConfirmerPredicate>( + &self, + ask: impl AsRef<str>, + count: impl Into<ConfirmerCount>, + ) -> Option<bool> { + if self.confirmed { + return Some(true); + } + + let count = count.into(); + let mut attempts = 0usize; + + loop { + eprint!("{}", ask.as_ref()); + std::io::stderr().flush().unwrap(); + + let stdin = std::io::stdin(); + let mut input = String::new(); + stdin.lock().read_line(&mut input).unwrap(); + if let Some(result) = P::is_yes(&input) { + return Some(result); + } + + attempts += 1; + match count { + ConfirmerCount::Loop => {} + ConfirmerCount::Max(max) => { + if attempts >= max { + return None; + } + } + } + } + } +} + +/// Specifies the maximum number of attempts for a confirmation prompt. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfirmerCount { + /// Loop indefinitely until the user gives a parseable answer. + Loop, + /// Ask at most the specified number of times. + Max(usize), +} + +macro_rules! impl_from_for_confirmer_count { + ($($t:ty),*) => { + $( + impl From<$t> for ConfirmerCount { + fn from(n: $t) -> Self { + if n == 0 { + ConfirmerCount::Loop + } else { + match usize::try_from(n) { + Ok(max) => ConfirmerCount::Max(max), + Err(_) => ConfirmerCount::Max(usize::MAX), + } + } + } + } + )* + }; +} + +impl_from_for_confirmer_count!( + i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize +); + +/// Defines how to parse user confirmation input. +/// +/// A type implementing this trait determines which user input strings are treated as "yes" or "no". +pub trait ConfirmerPredicate { + /// Parses the user's input string, returning whether it is "yes". + /// + /// Returns `Some(true)` for yes, `Some(false)` for no, + /// and `None` if the input cannot be parsed (requiring re-entry). + fn is_yes(str: &str) -> Option<bool>; +} + +/// A `ConfirmerPredicate` implementation that accepts "y"/"yes" as yes and "n"/"no" as no. +/// +/// Input comparison is case-insensitive and automatically trims leading/trailing whitespace. +/// +/// # Examples +/// +/// ``` +/// use mingling::res::{Confirmer, YesConfirm}; +/// +/// let confirmer = Confirmer::default(); +/// let confirmed = confirmer.ask::<YesConfirm>("Continue? [y/n] "); +/// ``` +pub struct YesConfirm; + +/// A `ConfirmerPredicate` implementation that accepts "true"/"t" as yes and "false"/"f" as no. +/// +/// Input comparison is case-insensitive and automatically trims leading/trailing whitespace. +/// +/// # Examples +/// +/// ``` +/// use mingling::res::{Confirmer, TrueConfirm}; +/// +/// let confirmer = Confirmer::default(); +/// let confirmed = confirmer.ask::<TrueConfirm>("Enable this feature? [true/false] "); +/// ``` +pub struct TrueConfirm; + +impl ConfirmerPredicate for YesConfirm { + fn is_yes(str: &str) -> Option<bool> { + match str.trim().to_lowercase().as_str() { + "y" | "yes" => Some(true), + "n" | "no" => Some(false), + _ => None, + } + } +} + +impl ConfirmerPredicate for TrueConfirm { + fn is_yes(str: &str) -> Option<bool> { + match str.trim().to_lowercase().as_str() { + "true" | "t" => Some(true), + "false" | "f" => Some(false), + _ => None, + } + } +} diff --git a/mingling/src/setups.rs b/mingling/src/setups.rs index 3a3f13d..b2af90c 100644 --- a/mingling/src/setups.rs +++ b/mingling/src/setups.rs @@ -1,28 +1,31 @@ -// Doc Not Optimize +/// Picker's `ProgramSetup` variant. +/// +/// Internally does not use its own argument parsing, +/// but relies on `arg_picker`'s argument parsing capability. +#[cfg(feature = "picker")] +pub mod picker; + mod basic; pub use basic::*; +mod confirmer; +pub use confirmer::*; + mod dirs; pub use dirs::*; mod exit_code; pub use exit_code::*; -/// Picker's `ProgramSetup` variant. -/// -/// Internally does not use its own argument parsing, -/// but relies on `arg_picker`'s argument parsing capability. -#[cfg(feature = "picker")] -pub mod picker; +#[cfg(feature = "repl")] +mod repl_basic; +#[cfg(feature = "repl")] +pub use repl_basic::*; + +mod stdin_args; +pub use stdin_args::*; #[cfg(feature = "structural_renderer")] mod structural_renderer; - #[cfg(feature = "structural_renderer")] pub use structural_renderer::*; - -#[cfg(feature = "repl")] -mod repl_basic; - -#[cfg(feature = "repl")] -pub use repl_basic::*; diff --git a/mingling/src/setups/confirmer.rs b/mingling/src/setups/confirmer.rs new file mode 100644 index 0000000..1824745 --- /dev/null +++ b/mingling/src/setups/confirmer.rs @@ -0,0 +1,60 @@ +use mingling_core::{ + Program, ProgramCollect, config, hook::ProgramHook, setup::ProgramSetup, this, +}; + +use crate::res::Confirmer; + +/// Confirmer setup for managing confirmation state +/// +/// This Setup manages the confirmation flag within the program's resource +/// store. It registers a [`Confirmer`] resource and sets up a hook that +/// checks the user's confirmation mode during program execution. +/// +/// # Usage +/// +/// This Setup can be registered using the +/// [`Program`](https://docs.rs/mingling/latest/mingling/struct.Program.html) +/// `with_setup` method, for example: +/// +/// ```rust +/// # use mingling::MockProgramCollect as ThisProgram; +/// use mingling::Program; +/// use mingling::setup::ConfirmerSetup; +/// +/// let mut program = Program::<ThisProgram>::new(); +/// program.with_setup(ConfirmerSetup); +/// ``` +/// +/// # Behavior +/// +/// - Registers a [`Confirmer`] resource that tracks confirmation state. +/// - At the beginning of command execution, checks whether the user's +/// confirmation mode is set to `Skip`. +/// - If confirmation is skipped, the [`Confirmer`] resource is updated +/// to record the confirmed state. +/// +/// # Notes +/// +/// - This Setup applies uniformly to all subcommands of the entire program. +/// - The confirmation state is determined by the global `config` setting; +/// it does not support per-command overrides. +pub struct ConfirmerSetup; + +impl<C> ProgramSetup<C> for ConfirmerSetup +where + C: ProgramCollect<Enum = C> + 'static, +{ + fn setup(self, program: &mut Program<C>) { + program.with_resource(Confirmer::new()); + + program.with_hook(ProgramHook::empty().on_pre_dispatch::<_, ()>(|_| { + let p = this::<C>(); + let confirmed = p.user_context.confirmation == config::ConfirmationMode::Skip; + if confirmed { + p.modify_res(|c: &mut Confirmer| { + c.set_confirmed(); + }); + } + })); + } +} diff --git a/mingling/src/setups/stdin_args.rs b/mingling/src/setups/stdin_args.rs new file mode 100644 index 0000000..ca55ca7 --- /dev/null +++ b/mingling/src/setups/stdin_args.rs @@ -0,0 +1,82 @@ +use std::io::{IsTerminal, Read}; + +use mingling_core::{ + Program, ProgramCollect, hook::ProgramHook, setup::ProgramSetup, utils::ArgumentSplitter, +}; + +/// Uses the standard input as arguments for the program +/// +/// This Setup can take standard input supplied via a pipe or redirect, +/// split it according to whitespace and quoting rules, and append +/// the resulting arguments to the end of the command argument list. +/// +/// # Usage +/// +/// This Setup can be registered using the +/// [`Program`](https://docs.rs/mingling/latest/mingling/struct.Program.html) +/// `with_setup` method, for example: +/// +/// ```rust +/// # use mingling::MockProgramCollect as ThisProgram; +/// use mingling::Program; +/// use mingling::setup::StandardInputArgsSetup; +/// +/// let mut program = Program::<ThisProgram>::new(); +/// program.with_setup(StandardInputArgsSetup); +/// ``` +/// +/// # Behavior +/// +/// - Standard input is only read when it is not a terminal (i.e., when +/// there is piped or redirected input). +/// - The read content is split into multiple arguments according to +/// whitespace and quoting rules. +/// - If the standard input content is empty, no arguments are produced. +/// - All input is converted to UTF-8 encoding (lossy conversion is used +/// when strict parsing is not possible). +/// +/// # Notes +/// +/// - This Setup applies uniformly to all subcommands of the entire program +/// and does not provide fine-grained control. If you need different +/// standard input behavior across different subcommands (e.g., some +/// subcommands read stdin while others ignore it), **do not use this Setup**. +/// - This Setup does **not** provide any validation rules. Content provided +/// via standard input is treated as trusted arguments and appended directly. +/// As a result, the input source can also inject arbitrary arguments into +/// the command, so you should be careful when processing untrusted input. +pub struct StandardInputArgsSetup; + +impl<C> ProgramSetup<C> for StandardInputArgsSetup +where + C: ProgramCollect<Enum = C>, +{ + fn setup(self, program: &mut Program<C>) { + program.with_hook(ProgramHook::empty().on_pre_dispatch(|ctx| { + let pipe_input = read_stdin(); + if let Some(pipe_input) = pipe_input { + ctx.arguments.append(&mut pipe_input.trim().split_args()); + } + })); + } +} + +fn read_stdin() -> Option<String> { + // Check if stdin is a terminal (no piped input) or has data available + if std::io::stdin().is_terminal() { + return None; + } + + let mut bytes = Vec::new(); + match std::io::stdin().read_to_end(&mut bytes) { + Ok(_) => { + if bytes.is_empty() { + return None; + } + // Handle encoding differences, ensure output is always UTF-8. + // First try strict UTF-8 parsing; fall back to lossy conversion + Some(String::from_utf8_lossy(&bytes).into_owned()) + } + Err(_) => None, + } +} |
