diff options
Diffstat (limited to 'mingling/src')
| -rw-r--r-- | mingling/src/confirm.rs | 5 | ||||
| -rw-r--r-- | mingling/src/confirm/count.rs | 75 | ||||
| -rw-r--r-- | mingling/src/confirm/predicate.rs | 60 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 6 | ||||
| -rw-r--r-- | mingling/src/osc94.rs | 5 | ||||
| -rw-r--r-- | mingling/src/osc94/guard.rs | 203 | ||||
| -rw-r--r-- | mingling/src/osc94/state.rs | 243 | ||||
| -rw-r--r-- | mingling/src/res.rs | 7 | ||||
| -rw-r--r-- | mingling/src/res/confirm.rs | 184 | ||||
| -rw-r--r-- | mingling/src/res/confirmer.rs | 268 | ||||
| -rw-r--r-- | mingling/src/res/osc94.rs | 69 | ||||
| -rw-r--r-- | mingling/src/setups.rs | 7 | ||||
| -rw-r--r-- | mingling/src/setups/confirm.rs (renamed from mingling/src/setups/confirmer.rs) | 22 | ||||
| -rw-r--r-- | mingling/src/setups/osc94.rs | 89 |
14 files changed, 960 insertions, 283 deletions
diff --git a/mingling/src/confirm.rs b/mingling/src/confirm.rs new file mode 100644 index 0000000..45a12d8 --- /dev/null +++ b/mingling/src/confirm.rs @@ -0,0 +1,5 @@ +mod predicate; +pub use predicate::*; + +mod count; +pub use count::*; diff --git a/mingling/src/confirm/count.rs b/mingling/src/confirm/count.rs new file mode 100644 index 0000000..c9f9db7 --- /dev/null +++ b/mingling/src/confirm/count.rs @@ -0,0 +1,75 @@ +/// Specifies the maximum number of attempts for a confirmation prompt. +/// +/// # Default Implementations +/// +/// `ConfirmCount` implements the following traits by default: +/// +/// - [`Debug`] — for formatted output and debugging. +/// - [`Clone`] — to create a copy of the value. +/// - [`Copy`] — since the enum holds no heap-allocated data, it can be trivially copied. +/// - [`PartialEq`] — allows comparing two `ConfirmCount` values for equality. +/// - [`Eq`] — provides full equality semantics (as opposed to just partial). +/// - [`From<T>`] for all primitive integer types (`i8`–`i128`, `isize`, `u8`–`u128`, `usize`), +/// allowing convenient conversion from a raw number. +/// +/// # What the Numbers Mean +/// +/// The numeric value passed to a `From` conversion represents the **maximum number of times** +/// the confirmation prompt will be shown to the user. For example: +/// +/// - `ConfirmCount::from(3)` → asks at most **3** times before giving up. +/// - `ConfirmCount::from(1)` → asks exactly **1** time. +/// - `ConfirmCount::from(0)` → interpreted as [`ConfirmCount::Loop`], meaning it will keep asking +/// indefinitely until a valid answer is parsed. +/// +/// # Examples +/// +/// ``` +/// use mingling::confirm::ConfirmCount; +/// +/// // Convert from a numeric value +/// let count: ConfirmCount = 3.into(); +/// assert_eq!(count, ConfirmCount::Max(3)); +/// +/// // Zero means loop forever +/// let loop_count: ConfirmCount = 0.into(); +/// assert_eq!(loop_count, ConfirmCount::Loop); +/// +/// // Large values are capped at usize::MAX +/// let big: ConfirmCount = i128::MAX.into(); +/// assert_eq!(big, ConfirmCount::Max(usize::MAX)); +/// +/// // From a usize directly +/// let from_usize = ConfirmCount::from(5usize); +/// assert_eq!(from_usize, ConfirmCount::Max(5)); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfirmCount { + /// 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_Confirm_count { + ($($t:ty),*) => { + $( + impl From<$t> for ConfirmCount { + fn from(n: $t) -> Self { + if n == 0 { + ConfirmCount::Loop + } else { + match usize::try_from(n) { + Ok(max) => ConfirmCount::Max(max), + Err(_) => ConfirmCount::Max(usize::MAX), + } + } + } + } + )* + }; +} + +impl_from_for_Confirm_count!( + i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize +); diff --git a/mingling/src/confirm/predicate.rs b/mingling/src/confirm/predicate.rs new file mode 100644 index 0000000..786a459 --- /dev/null +++ b/mingling/src/confirm/predicate.rs @@ -0,0 +1,60 @@ +/// 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 ConfirmPredicate { + /// 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 `ConfirmPredicate` 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::ResConfirm; +/// use mingling::confirm::YesConfirm; +/// +/// let confirm = ResConfirm::default(); +/// let confirmed = confirm.ask::<YesConfirm>("Continue? [y/n] "); +/// ``` +pub struct YesConfirm; + +/// A `ConfirmPredicate` 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::ResConfirm; +/// use mingling::confirm::TrueConfirm; +/// +/// let confirm = ResConfirm::default(); +/// let confirmed = confirm.ask::<TrueConfirm>("Enable this feature? [true/false] "); +/// ``` +pub struct TrueConfirm; + +impl ConfirmPredicate 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 ConfirmPredicate 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/lib.rs b/mingling/src/lib.rs index 59ef7a6..9d38a2a 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -23,6 +23,9 @@ pub mod CRATE_ROOT { #[cfg(feature = "core")] pub mod metadata; +/// Support for the `OSC 9;4` protocol +pub mod osc94; + #[cfg(feature = "core")] mod example_docs; @@ -143,6 +146,9 @@ pub use mingling_macros::Grouped; #[cfg(feature = "structural_renderer")] pub use mingling_macros::StructuralData; +/// Mingling's confirmation module, providing core support for Confirmer +pub mod confirm; + #[doc = include_str!("docs/docsrs_examples.md")] #[cfg(all(feature = "core", feature = "docs_rs"))] #[allow(nonstandard_style)] diff --git a/mingling/src/osc94.rs b/mingling/src/osc94.rs new file mode 100644 index 0000000..7dabb4a --- /dev/null +++ b/mingling/src/osc94.rs @@ -0,0 +1,5 @@ +mod guard; +pub use guard::*; + +mod state; +pub use state::*; diff --git a/mingling/src/osc94/guard.rs b/mingling/src/osc94/guard.rs new file mode 100644 index 0000000..d17f483 --- /dev/null +++ b/mingling/src/osc94/guard.rs @@ -0,0 +1,203 @@ +use crate::osc94::OSC94State; + +/// A guard for modifying process state. +/// +/// Obtained via [`crate::res::ResOSC94::get_mut`]. When the guard is dropped, the process state is +/// automatically restored to `OS94State::Clean`, so no manual cleanup is needed. +/// +/// # Example +/// +/// Create a guard via [`crate::res::ResOSC94`], and the state is automatically restored to Clean +/// when the guard is dropped: +/// +/// ``` +/// use mingling::res::ResOSC94; +/// use mingling::osc94::OSC94Guard; +/// +/// let osc94 = ResOSC94::default(); +/// { +/// let mut guard: OSC94Guard = osc94.get_mut(); +/// guard.set_progress(0.5); +/// // When leaving this scope, the guard is dropped and the process state is automatically restored to Clean +/// } +/// ``` +pub struct OSC94Guard { + pub(crate) is_support: bool, + pub(crate) msg: OSC94State, +} + +impl OSC94Guard { + /// Set the process state to Clean. + /// + /// Indicates that the process has finished or is in a normal, problem-free state. + /// + /// # Example + /// + /// ``` + /// use mingling::res::ResOSC94; + /// use mingling::osc94::{OSC94Guard, OSC94State}; + /// + /// let osc94 = ResOSC94::default(); + /// let mut guard: OSC94Guard = osc94.get_mut(); + /// guard.set_progress(0.5); + /// guard.set_clean_state(); + /// assert_eq!(guard.state(), OSC94State::Clean); + /// ``` + pub fn set_clean_state(&mut self) { + self.msg = OSC94State::Clean; + if self.is_support { + self.msg.send(); + } + } + + /// Set the process state to Error. + /// + /// Indicates that an error occurred during process execution. + /// + /// # Example + /// + /// ``` + /// use mingling::res::ResOSC94; + /// use mingling::osc94::{OSC94Guard, OSC94State}; + /// + /// let osc94 = ResOSC94::default(); + /// let mut guard: OSC94Guard = osc94.get_mut(); + /// guard.set_error_state(); + /// assert_eq!(guard.state(), OSC94State::Error); + /// ``` + pub fn set_error_state(&mut self) { + self.msg = OSC94State::Error; + if self.is_support { + self.msg.send(); + } + } + + /// Set the process state to Warn. + /// + /// Indicates that a warning occurred during process execution, but it has not + /// reached the level of an error. + /// + /// # Example + /// + /// ``` + /// use mingling::res::ResOSC94; + /// use mingling::osc94::{OSC94Guard, OSC94State}; + /// + /// let osc94 = ResOSC94::default(); + /// let mut guard: OSC94Guard = osc94.get_mut(); + /// guard.set_warn_state(); + /// assert_eq!(guard.state(), OSC94State::Warn); + /// ``` + pub fn set_warn_state(&mut self) { + self.msg = OSC94State::Warn; + if self.is_support { + self.msg.send(); + } + } + + /// Set the process state to Unknown. + /// + /// Indicates that the process state cannot be determined or has not been defined. + /// + /// # Example + /// + /// ``` + /// use mingling::res::ResOSC94; + /// use mingling::osc94::{OSC94Guard, OSC94State}; + /// + /// let osc94 = ResOSC94::default(); + /// let mut guard: OSC94Guard = osc94.get_mut(); + /// guard.set_unknown_state(); + /// assert_eq!(guard.state(), OSC94State::Unknown); + /// ``` + pub fn set_unknown_state(&mut self) { + self.msg = OSC94State::Unknown; + if self.is_support { + self.msg.send(); + } + } + + /// Set the progress of the process. + /// + /// The `progress` parameter should be between `0.0` and `1.0`. `0.0` indicates + /// the start of the task, and `1.0` indicates the completion of the task. + /// Values outside this range are not clamped, but it is recommended to keep them + /// within this range. + /// + /// # Parameters + /// + /// * `progress` - The progress value, ranging from `0.0` to `1.0`. + /// + /// # Example + /// + /// ``` + /// use mingling::res::ResOSC94; + /// use mingling::osc94::OSC94Guard; + /// + /// let osc94 = ResOSC94::default(); + /// let mut guard: OSC94Guard = osc94.get_mut(); + /// guard.set_progress(0.5); + /// assert_eq!(guard.progress(), 0.5); + /// ``` + pub fn set_progress(&mut self, progress: f32) { + self.msg = OSC94State::Normal(progress); + if self.is_support { + self.msg.send(); + } + } + + /// Get the current process state. + /// + /// # Returns + /// + /// Returns the current [`OSC94State`] value, representing the state of the process. + /// + /// # Example + /// + /// ``` + /// use mingling::res::ResOSC94; + /// use mingling::osc94::{OSC94Guard, OSC94State}; + /// + /// let osc94 = ResOSC94::default(); + /// let guard: OSC94Guard = osc94.get_mut(); + /// assert_eq!(guard.state(), OSC94State::Clean); + /// ``` + #[must_use] + pub const fn state(&self) -> OSC94State { + self.msg + } + + /// Get the current progress value. + /// + /// Returns the actual progress value only when the state is [`OSC94State::Normal`]; + /// otherwise returns `0.0`. + /// + /// # Returns + /// + /// Returns an `f32` progress value, ranging from `0.0` to `1.0`. + /// + /// # Example + /// + /// ``` + /// use mingling::res::ResOSC94; + /// use mingling::osc94::OSC94Guard; + /// + /// let osc94 = ResOSC94::default(); + /// let mut guard: OSC94Guard = osc94.get_mut(); + /// guard.set_progress(0.25); + /// assert_eq!(guard.progress(), 0.25); + /// ``` + #[must_use] + pub const fn progress(&self) -> f32 { + match self.msg { + OSC94State::Normal(progress) => progress, + _ => 0.0, + } + } +} + +impl Drop for OSC94Guard { + fn drop(&mut self) { + OSC94State::Clean.send(); + } +} diff --git a/mingling/src/osc94/state.rs b/mingling/src/osc94/state.rs new file mode 100644 index 0000000..1fff7ac --- /dev/null +++ b/mingling/src/osc94/state.rs @@ -0,0 +1,243 @@ +/// `OSC 9;4` protocol message +/// +/// Used to send task progress notification messages to the terminal via ANSI escape sequences. +/// +/// This protocol follows the [Windows Terminal Progress Bar Sequences](https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences) specification. +/// The status codes (0-4) represent: clear progress, normal, error, indeterminate, and warning states, respectively. +/// +/// # Examples +/// +/// ```rust +/// use mingling::osc94::OSC94State; +/// +/// // Set progress to 50% +/// let state = OSC94State::Normal(0.5); +/// assert_eq!(state.state_code(), 1); +/// assert_eq!(state.progress(), 50.0); +/// +/// // Generate escape sequence string +/// let seq = state.to_escape_sequence(); +/// assert_eq!(seq, "\x1b]9;4;1;50\x07"); +/// +/// // Convert to string (Display implementation) +/// let s = format!("{state}"); +/// assert_eq!(s, "\x1b]9;4;1;50\x07"); +/// +/// // Convert via From +/// let s2: String = state.into(); +/// assert_eq!(s2, "\x1b]9;4;1;50\x07"); +/// +/// // Error state +/// let err = OSC94State::Error; +/// assert_eq!(err.state_code(), 2); +/// ``` +/// +/// # Use Cases +/// +/// In command-line tools or scripts, the [`OSC94State::send`] method can be used to directly send progress notifications to the terminal. +/// Supported terminals include: `Windows Terminal`, `kitty`, `iTerm2`, `WezTerm`, `foot`, etc. +/// +/// ``` +/// use mingling::osc94::OSC94State; +/// +/// // Send progress 100% +/// OSC94State::Normal(1.0).send(); +/// // Send completion (clear) message +/// OSC94State::Clean.send(); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum OSC94State { + /// Clear/hide progress + Clean, + /// Normal state, corresponding to status code `1`, carries a progress value (0.0 to 1.0) + Normal(f32), + /// Error state, corresponding to status code `2` + Error, + /// Indeterminate state, corresponding to status code `3` + Unknown, + /// Warning state, corresponding to status code `4` + Warn, +} + +impl OSC94State { + /// Returns the status code for the `OSC 9;4` protocol. + /// + /// Status code meanings: + /// - `0`: Clear progress (`Clean`) + /// - `1`: Normal state (`Normal`) + /// - `2`: Error state (`Error`) + /// - `3`: Indeterminate state (`Unknown`) + /// - `4`: Warning state (`Warn`) + /// + /// # Examples + /// + /// ``` + /// use mingling::osc94::OSC94State; + /// + /// assert_eq!(OSC94State::Clean.state_code(), 0); + /// assert_eq!(OSC94State::Normal(0.5).state_code(), 1); + /// assert_eq!(OSC94State::Error.state_code(), 2); + /// assert_eq!(OSC94State::Unknown.state_code(), 3); + /// assert_eq!(OSC94State::Warn.state_code(), 4); + /// ``` + /// + /// # Return Value + /// + /// Returns the corresponding status code (`u8` type), ranging from `0` to `4`. + #[must_use] + pub const fn state_code(&self) -> u8 { + match self { + Self::Clean => 0, + Self::Normal(_) => 1, + Self::Error => 2, + Self::Unknown => 3, + Self::Warn => 4, + } + } + + /// Returns the progress value (0-100), used for the `Normal` state, clamped to a valid range. + /// + /// This function converts the progress value (between 0.0 and 1.0) stored in the `Normal` variant + /// into a percentage (0 to 100) and rounds it. For non-`Normal` states (such as `Clean`, `Error`, + /// `Unknown`, `Warn`), it returns a fixed `0.0`, because only the `Normal` state carries progress information. + /// + /// # Examples + /// + /// ``` + /// use mingling::osc94::OSC94State; + /// + /// // Progress conversion in normal state + /// assert_eq!(OSC94State::Normal(0.5).progress(), 50.0); + /// assert_eq!(OSC94State::Normal(1.0).progress(), 100.0); + /// assert_eq!(OSC94State::Normal(0.0).progress(), 0.0); + /// + /// // Out-of-range values are clamped to 0-100 + /// assert_eq!(OSC94State::Normal(1.5).progress(), 100.0); + /// assert_eq!(OSC94State::Normal(-0.5).progress(), 0.0); + /// + /// // Rounding behavior + /// assert_eq!(OSC94State::Normal(0.335).progress(), 34.0); + /// assert_eq!(OSC94State::Normal(0.999).progress(), 100.0); + /// + /// // Non-Normal states return 0.0 + /// assert_eq!(OSC94State::Clean.progress(), 0.0); + /// assert_eq!(OSC94State::Error.progress(), 0.0); + /// assert_eq!(OSC94State::Unknown.progress(), 0.0); + /// assert_eq!(OSC94State::Warn.progress(), 0.0); + /// ``` + /// + /// # Return Value + /// + /// Returns an `f32` progress percentage, ranging from `0.0` to `100.0` (inclusive). + /// For the `Normal` state, returns the rounded result of converting its progress value to a percentage; + /// for other states, always returns `0.0`. + #[must_use] + pub const fn progress(&self) -> f32 { + match self { + Self::Normal(progress) => (progress.clamp(0.0, 1.0) * 100.0).round(), + _ => 0.0, + } + } + + /// Converts the message to the corresponding `OSC 9;4` escape sequence string. + /// + /// This method generates an ANSI escape sequence conforming to the + /// [Windows Terminal Progress Bar Sequences](https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences) + /// protocol based on the current state, in the format `\x1b]9;4;{status_code};{progress}\x07`. + /// + /// Escape sequence format description: + /// - `\x1b]`: ESC character followed by `]`, marking the start of an OSC (Operating System Command) sequence. + /// - `9;4`: Indicates the `OSC 9;4` protocol (task progress notification). + /// - `{status_code}`: Task status, ranging from `0` (clear), `1` (normal), `2` (error), `3` (indeterminate), to `4` (warning). + /// - `{progress}`: Task progress percentage (0-100), only meaningful for the `Normal` state. + /// - `\x07`: BEL character, marking the end of the OSC sequence. + /// + /// # Examples + /// + /// ``` + /// use mingling::osc94::OSC94State; + /// + /// // Clear progress + /// let clean = OSC94State::Clean; + /// assert_eq!(clean.to_escape_sequence(), "\x1b]9;4;0;0\x07"); + /// + /// // Normal state, progress 50% + /// let normal = OSC94State::Normal(0.5); + /// assert_eq!(normal.to_escape_sequence(), "\x1b]9;4;1;50\x07"); + /// + /// // Normal state, progress 100% + /// let complete = OSC94State::Normal(1.0); + /// assert_eq!(complete.to_escape_sequence(), "\x1b]9;4;1;100\x07"); + /// + /// // Error state + /// let error = OSC94State::Error; + /// assert_eq!(error.to_escape_sequence(), "\x1b]9;4;2;0\x07"); + /// + /// // Indeterminate state + /// let unknown = OSC94State::Unknown; + /// assert_eq!(unknown.to_escape_sequence(), "\x1b]9;4;3;0\x07"); + /// + /// // Warning state + /// let warn = OSC94State::Warn; + /// assert_eq!(warn.to_escape_sequence(), "\x1b]9;4;4;0\x07"); + /// ``` + /// + /// # Return Value + /// + /// Returns a `String` containing an ANSI escape sequence conforming to the `OSC 9;4` protocol standard. + /// This string can be directly output to a terminal that supports this protocol (such as `Windows Terminal`, + /// `kitty`, `iTerm2`, `WezTerm`, `foot`, etc.) to display a task progress notification. + #[must_use] + pub fn to_escape_sequence(&self) -> String { + format!("\x1b]9;4;{};{}\x07", self.state_code(), self.progress()) + } + + /// Sends the OSC 9;4 message to the terminal via stdout. + /// + /// This method outputs the escape sequence of the current state to standard output and flushes the buffer, + /// allowing terminals that support the + /// [Windows Terminal Progress Bar Sequences](https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences) + /// protocol to display the corresponding task progress notification. + /// + /// # Examples + /// + /// ``` + /// use mingling::osc94::OSC94State; + /// + /// // Send normal state, progress 50% + /// OSC94State::Normal(0.5).send(); + /// + /// // Send error state + /// OSC94State::Error.send(); + /// + /// // Send clear progress message + /// OSC94State::Clean.send(); + /// ``` + /// + /// # Panics + /// + /// Panics if the stdout stream cannot be flushed. + pub fn send(&self) { + use std::io::Write; + print!("{}", self.to_escape_sequence()); + std::io::stdout().flush().unwrap(); + } +} + +impl std::fmt::Display for OSC94State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_escape_sequence()) + } +} + +impl From<OSC94State> for String { + fn from(msg: OSC94State) -> Self { + msg.to_escape_sequence() + } +} + +impl From<&OSC94State> for String { + fn from(msg: &OSC94State) -> Self { + msg.to_escape_sequence() + } +} diff --git a/mingling/src/res.rs b/mingling/src/res.rs index ab524cd..0314e39 100644 --- a/mingling/src/res.rs +++ b/mingling/src/res.rs @@ -7,5 +7,8 @@ pub use dirs::*; mod exit_code; pub use exit_code::*; -mod confirmer; -pub use confirmer::*; +mod confirm; +pub use confirm::*; + +mod osc94; +pub use osc94::*; diff --git a/mingling/src/res/confirm.rs b/mingling/src/res/confirm.rs new file mode 100644 index 0000000..e07e9a2 --- /dev/null +++ b/mingling/src/res/confirm.rs @@ -0,0 +1,184 @@ +use std::io::{BufRead, Write}; + +use crate::confirm::{ConfirmCount, ConfirmPredicate}; + +/// A confirm for interactive confirmation. +/// +/// This structure caches the confirmed state to avoid repeated prompts. +/// +/// Typically, `ResConfirm` is registered via `ConfirmSetup`, and then injected into functions +/// through Mingling's resource injection system. +/// +/// # Registration +/// +/// Before use, the `ConfirmSetup` must be registered with the program: +/// +/// ``` +/// # use mingling::MockProgramCollect as ThisProgram; +/// use mingling::setup::ConfirmSetup; +/// use mingling::Program; +/// +/// let mut program = Program::<ThisProgram>::new(); +/// program.with_setup(ConfirmSetup); +/// ``` +/// +/// # Examples +/// +/// ``` +/// use mingling::res::ResConfirm; +/// use mingling::confirm::YesConfirm; +/// +/// // In actual use, obtain the registered Confirm through the resource injection system +/// let confirm = ResConfirm::new_confirmed(); +/// assert!(confirm.ask::<YesConfirm>("Continue? [y/n] ")); +/// ``` +#[derive(Debug, Default, Clone, Copy)] +pub struct ResConfirm { + pub(crate) confirmed: bool, +} + +impl ResConfirm { + /// Creates a new `ResConfirm` instance. + /// + /// # Examples + /// + /// ``` + /// use mingling::res::ResConfirm; + /// + /// let confirm = ResConfirm::new(); + /// ``` + #[must_use] + pub const fn new() -> Self { + Self { confirmed: false } + } + + /// Creates a `Confirm` instance in the confirmed state. + /// + /// The returned `Confirm` will directly return `true` when calling [`ask`](ResConfirm::ask) or + /// [`try_ask`](ResConfirm::try_ask), without prompting the user. + /// + /// # Examples + /// + /// ``` + /// use mingling::res::ResConfirm; + /// use mingling::confirm::YesConfirm; + /// + /// let confirm = ResConfirm::new_confirmed(); + /// assert!(confirm.ask::<YesConfirm>("Continue? [y/n] ")); + /// ``` + #[must_use] + pub const fn new_confirmed() -> Self { + Self { confirmed: true } + } + + /// Marks the Confirm as confirmed. + /// + /// After calling this method, subsequent calls to [`ask`](ResConfirm::ask) or + /// [`try_ask`](ResConfirm::try_ask) on this Confirm will directly return `true` + /// without prompting the user. + /// + /// # Examples + /// + /// ``` + /// use mingling::res::ResConfirm; + /// use mingling::confirm::YesConfirm; + /// + /// let mut confirm = ResConfirm::new(); + /// confirm.set_confirmed(); + /// assert!(confirm.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::ResConfirm; + /// use mingling::confirm::YesConfirm; + /// + /// let confirm = ResConfirm::new_confirmed(); + /// let confirmed = confirm.ask::<YesConfirm>("Delete this file? [y/n] "); + /// ``` + pub fn ask<P: ConfirmPredicate>(&self, ask: impl AsRef<str>) -> bool { + self.try_ask::<P>(ask, ConfirmCount::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::ResConfirm; + /// use mingling::confirm::YesConfirm; + /// + /// let confirm = ResConfirm::new_confirmed(); + /// let confirmed = confirm.try_ask::<YesConfirm>("Confirm execution? [y/n] ", 3); + /// ``` + pub fn try_ask<P: ConfirmPredicate>( + &self, + ask: impl AsRef<str>, + count: impl Into<ConfirmCount>, + ) -> 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 { + ConfirmCount::Loop => {} + ConfirmCount::Max(max) => { + if attempts >= max { + return None; + } + } + } + } + } +} diff --git a/mingling/src/res/confirmer.rs b/mingling/src/res/confirmer.rs deleted file mode 100644 index 900562b..0000000 --- a/mingling/src/res/confirmer.rs +++ /dev/null @@ -1,268 +0,0 @@ -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/res/osc94.rs b/mingling/src/res/osc94.rs new file mode 100644 index 0000000..ea8538d --- /dev/null +++ b/mingling/src/res/osc94.rs @@ -0,0 +1,69 @@ +use crate::osc94::{OSC94Guard, OSC94State}; + +/// Process `OSC 9;4` status. +/// +/// Provides support for the `OSC 9;4` protocol. You can inject it into the execution flow +/// through Mingling's resource injection system, and use it to control your process state. +/// +/// Typically, `ResOSC94` is registered via `OSC94Setup`, and then injected into functions +/// through Mingling's resource injection system. +/// +/// # Registration +/// +/// Before use, the `OSC94Setup` must be registered with the program: +/// +/// ``` +/// # use mingling::MockProgramCollect as ThisProgram; +/// use mingling::setup::OSC94Setup; +/// use mingling::Program; +/// +/// let mut program = Program::<ThisProgram>::new(); +/// program.with_setup(OSC94Setup); +/// ``` +/// +/// # Example +/// +/// ``` +/// use mingling::res::ResOSC94; +/// use mingling::osc94::OSC94State; +/// +/// let osc94 = ResOSC94::default(); +/// let mut guard = osc94.get_mut(); +/// +/// guard.set_progress(0.5); +/// assert_eq!(guard.state(), OSC94State::Normal(0.5)); +/// ``` +#[derive(Debug, Default, Clone, Copy)] +pub struct ResOSC94 { + pub(crate) is_support: bool, +} + +impl ResOSC94 { + /// Get a guard for modifying progress. + /// + /// The returned [`OSC94Guard`] allows you to set the process state and progress. + /// If the current environment supports the `OSC 9;4` protocol, state changes will + /// be sent to the terminal in real time. + /// + /// # Returns + /// + /// Returns an [`OSC94Guard`] with an initial state of [`OSC94State::Clean`]. + /// + /// # Example + /// + /// ``` + /// use mingling::res::ResOSC94; + /// use mingling::osc94::OSC94State; + /// + /// let osc94 = ResOSC94::default(); + /// let guard = osc94.get_mut(); + /// assert_eq!(guard.state(), OSC94State::Clean); + /// ``` + #[must_use] + pub const fn get_mut(&self) -> OSC94Guard { + OSC94Guard { + is_support: self.is_support, + msg: OSC94State::Clean, + } + } +} diff --git a/mingling/src/setups.rs b/mingling/src/setups.rs index b2af90c..e1835e7 100644 --- a/mingling/src/setups.rs +++ b/mingling/src/setups.rs @@ -8,8 +8,8 @@ pub mod picker; mod basic; pub use basic::*; -mod confirmer; -pub use confirmer::*; +mod confirm; +pub use confirm::*; mod dirs; pub use dirs::*; @@ -17,6 +17,9 @@ pub use dirs::*; mod exit_code; pub use exit_code::*; +mod osc94; +pub use osc94::*; + #[cfg(feature = "repl")] mod repl_basic; #[cfg(feature = "repl")] diff --git a/mingling/src/setups/confirmer.rs b/mingling/src/setups/confirm.rs index 1824745..46ead36 100644 --- a/mingling/src/setups/confirmer.rs +++ b/mingling/src/setups/confirm.rs @@ -2,12 +2,12 @@ use mingling_core::{ Program, ProgramCollect, config, hook::ProgramHook, setup::ProgramSetup, this, }; -use crate::res::Confirmer; +use crate::res::ResConfirm; -/// Confirmer setup for managing confirmation state +/// Confirm 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 +/// store. It registers a [`ResConfirm`] resource and sets up a hook that /// checks the user's confirmation mode during program execution. /// /// # Usage @@ -19,18 +19,18 @@ use crate::res::Confirmer; /// ```rust /// # use mingling::MockProgramCollect as ThisProgram; /// use mingling::Program; -/// use mingling::setup::ConfirmerSetup; +/// use mingling::setup::ConfirmSetup; /// /// let mut program = Program::<ThisProgram>::new(); -/// program.with_setup(ConfirmerSetup); +/// program.with_setup(ConfirmSetup); /// ``` /// /// # Behavior /// -/// - Registers a [`Confirmer`] resource that tracks confirmation state. +/// - Registers a [`ResConfirm`] 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 +/// - If confirmation is skipped, the [`ResConfirm`] resource is updated /// to record the confirmed state. /// /// # Notes @@ -38,20 +38,20 @@ use crate::res::Confirmer; /// - 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; +pub struct ConfirmSetup; -impl<C> ProgramSetup<C> for ConfirmerSetup +impl<C> ProgramSetup<C> for ConfirmSetup where C: ProgramCollect<Enum = C> + 'static, { fn setup(self, program: &mut Program<C>) { - program.with_resource(Confirmer::new()); + program.with_resource(ResConfirm::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| { + p.modify_res(|c: &mut ResConfirm| { c.set_confirmed(); }); } diff --git a/mingling/src/setups/osc94.rs b/mingling/src/setups/osc94.rs new file mode 100644 index 0000000..b08312f --- /dev/null +++ b/mingling/src/setups/osc94.rs @@ -0,0 +1,89 @@ +use mingling_core::{Program, ProgramCollect, setup::ProgramSetup}; + +use crate::res::ResOSC94; + +/// `OSC 9;4` Setup for managing terminal progress notification state +/// +/// This Setup manages the terminal's `OSC 9;4` protocol support state within the +/// program's resource store. It registers an [`ResOSC94`] resource that tracks whether +/// the current terminal supports the protocol, and provides a helper resource that +/// can be used to send progress notification messages. +/// +/// # 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::OSC94Setup; +/// +/// let mut program = Program::<ThisProgram>::new(); +/// program.with_setup(OSC94Setup); +/// ``` +/// +/// # Behavior +/// +/// - Registers an [`ResOSC94`] resource that tracks whether the current terminal +/// supports the `OSC 9;4` protocol. +/// - The support check inspects various environment variables such as `TERM_PROGRAM`, +/// `WT_SESSION`, `VTE_VERSION`, and `TERM`. +/// +/// # Notes +/// +/// - The support state is determined at setup time and stored in the resource store. +/// - Use `OSC94Message` to construct and send progress notification messages. +pub struct OSC94Setup; + +impl<C> ProgramSetup<C> for OSC94Setup +where + C: ProgramCollect<Enum = C> + 'static, +{ + fn setup(self, program: &mut Program<C>) { + program.with_resource(ResOSC94 { + is_support: is_support_osc94(), + }); + } +} + +/// Check whether the current terminal environment supports the `OSC 9;4` protocol +/// +/// This function inspects various environment variables to determine whether the +/// current terminal supports Microsoft's +/// [OSC 9;4 protocol](https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences), +/// which allows sending task progress notifications via ANSI escape sequences. +/// +/// Supported terminal environments include: +/// - **`TERM_PROGRAM`**: `ghostty`, `WezTerm`, `iTerm.app` +/// - **`WT_SESSION`**: Windows Terminal +/// - **`VTE_VERSION`**: VTE-based terminals (such as GNOME Terminal, Konsole, etc.) +/// - **`TERM`**: terminal emulators containing `xterm` +/// +/// Returns `true` if the current terminal supports the `OSC 9;4` protocol, so that +/// progress notification escape sequences can be safely sent. +fn is_support_osc94() -> bool { + if let Ok(program) = std::env::var("TERM_PROGRAM") { + match program.as_str() { + "ghostty" | "WezTerm" | "iTerm.app" => return true, + _ => {} + } + } + + if std::env::var("WT_SESSION").is_ok() { + return true; + } + + if std::env::var("VTE_VERSION").is_ok() { + return true; + } + + if let Ok(term) = std::env::var("TERM") + && term.contains("xterm") + { + return true; + } + + false +} |
