diff options
Diffstat (limited to 'mingling/src/res')
| -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 | 215 | ||||
| -rw-r--r-- | mingling/src/res/osc94/state.rs | 74 |
4 files changed, 195 insertions, 546 deletions
diff --git a/mingling/src/res/confirm.rs b/mingling/src/res/confirm.rs new file mode 100644 index 0000000..baf8caf --- /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`](Confirm::ask) or + /// [`try_ask`](Confirm::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`](Confirm::ask) or + /// [`try_ask`](Confirm::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 index 0c5a5a9..ea8538d 100644 --- a/mingling/src/res/osc94.rs +++ b/mingling/src/res/osc94.rs @@ -1,17 +1,16 @@ -mod state; -pub use state::*; +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, `OSC94` is registered via [`OSC94Setup`], and then injected into functions +/// 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: +/// Before use, the `OSC94Setup` must be registered with the program: /// /// ``` /// # use mingling::MockProgramCollect as ThisProgram; @@ -25,20 +24,21 @@ pub use state::*; /// # Example /// /// ``` -/// use mingling::res::{OSC94, OSC94State}; +/// use mingling::res::ResOSC94; +/// use mingling::osc94::OSC94State; /// -/// let osc94 = OSC94::default(); +/// 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 OSC94 { +pub struct ResOSC94 { pub(crate) is_support: bool, } -impl OSC94 { +impl ResOSC94 { /// Get a guard for modifying progress. /// /// The returned [`OSC94Guard`] allows you to set the process state and progress. @@ -52,9 +52,10 @@ impl OSC94 { /// # Example /// /// ``` - /// use mingling::res::{OSC94, OSC94State}; + /// use mingling::res::ResOSC94; + /// use mingling::osc94::OSC94State; /// - /// let osc94 = OSC94::default(); + /// let osc94 = ResOSC94::default(); /// let guard = osc94.get_mut(); /// assert_eq!(guard.state(), OSC94State::Clean); /// ``` @@ -66,197 +67,3 @@ impl OSC94 { } } } - -/// A guard for modifying process state. -/// -/// Obtained via [`OSC94::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 [`OSC94`], and the state is automatically restored to Clean -/// when the guard is dropped: -/// -/// ``` -/// use mingling::res::OSC94; -/// -/// let osc94 = OSC94::default(); -/// { -/// let mut guard = 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, - 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::{OSC94, OSC94State}; - /// - /// let osc94 = OSC94::default(); - /// let mut guard = 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::{OSC94, OSC94State}; - /// - /// let osc94 = OSC94::default(); - /// let mut guard = 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::{OSC94, OSC94State}; - /// - /// let osc94 = OSC94::default(); - /// let mut guard = 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::{OSC94, OSC94State}; - /// - /// let osc94 = OSC94::default(); - /// let mut guard = 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::OSC94; - /// - /// let osc94 = OSC94::default(); - /// let mut guard = 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::{OSC94, OSC94State}; - /// - /// let osc94 = OSC94::default(); - /// let guard = 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::OSC94; - /// - /// let osc94 = OSC94::default(); - /// let mut guard = 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/res/osc94/state.rs b/mingling/src/res/osc94/state.rs deleted file mode 100644 index c3d2bdf..0000000 --- a/mingling/src/res/osc94/state.rs +++ /dev/null @@ -1,74 +0,0 @@ -/// `OSC 9;4` 协议消息 -/// -/// 用于通过 ANSI 转义序列向终端发送任务进度通知消息 -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum OSC94State { - /// 清除/隐藏进度(任务完成时使用),对应状态码 `0` - Clean, - /// 正常状态,对应状态码 `1`,需要配合进度值(0-100) - Normal(f32), - /// 错误状态,对应状态码 `2`(通常显示为红色) - Error, - /// 不确定状态,对应状态码 `3`(显示为无限循环的动画,用于进度未知的任务) - Unknown, - /// 警告状态,对应状态码 `4`(通常显示为黄色) - Warn, -} - -impl OSC94State { - /// Returns the state code for the `OSC 9;4` protocol. - #[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) for the `Normal` state, clamped to the valid range. - #[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 into the corresponding `OSC 9;4` escape sequence string. - #[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. - /// - /// # 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() - } -} |
