diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-15 07:20:00 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-15 07:20:00 +0800 |
| commit | f1c48e304a6da6f5098aa18f9e5595ffa229378a (patch) | |
| tree | d4d6776150947376fae4f614e3710b95b1ab0336 | |
| parent | d175e9fee6ab1f76042280f22a86e5764863c642 (diff) | |
feat(res): add OSC94 resource and setup for terminal progress
| -rw-r--r-- | CHANGELOG.md | 67 | ||||
| -rw-r--r-- | mingling/src/res.rs | 3 | ||||
| -rw-r--r-- | mingling/src/res/osc94.rs | 262 | ||||
| -rw-r--r-- | mingling/src/res/osc94/state.rs | 74 | ||||
| -rw-r--r-- | mingling/src/setups.rs | 3 | ||||
| -rw-r--r-- | mingling/src/setups/osc94.rs | 89 |
6 files changed, 498 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e663276..9029c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -412,6 +412,73 @@ None - Empty input produces no arguments. - **Note:** the setup does **not** validate input — stdin content is treated as trusted arguments appended directly, so untrusted input can inject arbitrary arguments. It also has no per-subcommand granularity; if different subcommands need different stdin behavior, do not use this setup. +16. **[`res:osc94`]** **[`setups:osc94`]** Added the `OSC94` resource and `OSC94Setup` for managing terminal `OSC 9;4` protocol status: + + ### `OSC94` resource + - **`mingling::res::OSC94`** — A new resource type providing support for the [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. It is typically registered via [`OSC94Setup`] and injected into functions through Mingling's resource injection system. + + - **`OSC94::get_mut(&self) -> OSC94Guard`** — Returns an [`OSC94Guard`] with an initial state of [`OSC94State::Clean`]. If the current environment supports the `OSC 9;4` protocol, state changes will be sent to the terminal in real time. + + Derives `Debug`, `Default`, `Clone`, `Copy`. + + ### `OSC94Guard` + - **`mingling::res::OSC94Guard`** — A guard for modifying process state, obtained via [`OSC94::get_mut`]. When the guard is dropped, the process state is automatically restored to [`OSC94State::Clean`], so no manual cleanup is needed. + + - **`set_clean_state(&mut self)`** — Sets the process state to Clean, indicating the process has finished or is in a normal, problem-free state. + - **`set_error_state(&mut self)`** — Sets the process state to Error, indicating an error occurred during process execution. + - **`set_warn_state(&mut self)`** — Sets the process state to Warn, indicating a warning occurred but hasn't reached error level. + - **`set_unknown_state(&mut self)`** — Sets the process state to Unknown, indicating the process state cannot be determined or has not been defined. + - **`set_progress(&mut self, progress: f32)`** — Sets the progress value (should be between `0.0` and `1.0`; values outside this range are not clamped, but it is recommended to keep them within range). + - **`state(&self) -> OSC94State`** — Returns the current process state. + - **`progress(&self) -> f32`** — Returns the actual progress value only when the state is `OSC94State::Normal`; otherwise returns `0.0`. + + ### `OSC94State` enum + - **`mingling::res::OSC94State`** — Represents the `OSC 9;4` protocol message state: + + - **`Clean`** — Clears/hides progress (used when task completes), corresponding to state code `0`. + - **`Normal(f32)`** — Normal state, state code `1`, requires a progress value (0-100). + - **`Error`** — Error state, state code `2` (usually displayed in red). + - **`Unknown`** — Uncertain state, state code `3` (shown as an indeterminate animation for unknown progress). + - **`Warn`** — Warning state, state code `4` (usually displayed in yellow). + + - **`state_code(&self) -> u8`** — Returns the state code for the `OSC 9;4` protocol. + - **`progress(&self) -> f32`** — Returns the progress value (0-100) for the `Normal` state, clamped to the valid range. + - **`to_escape_sequence(&self) -> String`** — Converts the message into the corresponding `OSC 9;4` escape sequence string. + - **`send(&self)`** — Sends the `OSC 9;4` message to the terminal via stdout. Panics if the stdout stream cannot be flushed. + + Implements `Display` (formats as the escape sequence), `From<OSC94State> for String`, and `From<&OSC94State> for String`. Derives `Debug`, `Clone`, `Copy`, `PartialEq`. + + ### `OSC94Setup` + - **`mingling::setup::OSC94Setup`** — A `ProgramSetup` that registers an `OSC94` resource in the program's resource store, with its `is_support` flag determined at setup time by inspecting environment variables. The support check looks at: + + - **`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` + + Registered via `program.with_setup(OSC94Setup)`. + + Usage example: + + ```rust,ignore + use mingling::{macros::command, res::OSC94, setup::OSC94Setup}; + + fn main() { + let mut program = ThisProgram::new(); + program.with_setup(OSC94Setup); + program.exec_and_exit(); + } + + #[command] + fn hello(osc: &OSC94) { + let mut guard = osc.get_mut(); + guard.set_progress(0.5); + // ... do work ... + guard.set_warn_state(); + // ... guard is dropped, state automatically restored to Clean + } + ``` + #### **BREAKING CHANGES** (API CHANGES): 1. **[`macros`]** **[BREAKING]** Renamed the `extra_macros` feature to `extras`. All feature-gated macro re-exports in `mingling/src/lib.rs` (and throughout the codebase) have been updated from `#[cfg(feature = "extra_macros")]` to `#[cfg(feature = "extras")]`. diff --git a/mingling/src/res.rs b/mingling/src/res.rs index ab524cd..82c4e00 100644 --- a/mingling/src/res.rs +++ b/mingling/src/res.rs @@ -9,3 +9,6 @@ pub use exit_code::*; mod confirmer; pub use confirmer::*; + +mod osc94; +pub use osc94::*; diff --git a/mingling/src/res/osc94.rs b/mingling/src/res/osc94.rs new file mode 100644 index 0000000..0c5a5a9 --- /dev/null +++ b/mingling/src/res/osc94.rs @@ -0,0 +1,262 @@ +mod state; +pub use state::*; + +/// 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 +/// 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::{OSC94, OSC94State}; +/// +/// let osc94 = OSC94::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(crate) is_support: bool, +} + +impl OSC94 { + /// 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::{OSC94, OSC94State}; + /// + /// let osc94 = OSC94::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, + } + } +} + +/// 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 new file mode 100644 index 0000000..c3d2bdf --- /dev/null +++ b/mingling/src/res/osc94/state.rs @@ -0,0 +1,74 @@ +/// `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() + } +} diff --git a/mingling/src/setups.rs b/mingling/src/setups.rs index b2af90c..7a523cc 100644 --- a/mingling/src/setups.rs +++ b/mingling/src/setups.rs @@ -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/osc94.rs b/mingling/src/setups/osc94.rs new file mode 100644 index 0000000..2f9a319 --- /dev/null +++ b/mingling/src/setups/osc94.rs @@ -0,0 +1,89 @@ +use mingling_core::{Program, ProgramCollect, setup::ProgramSetup}; + +use crate::res::OSC94; + +/// `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 [`OSC94`] 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 [`OSC94`] 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(OSC94 { + 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 +} |
