aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-15 07:20:00 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-15 07:20:00 +0800
commitf1c48e304a6da6f5098aa18f9e5595ffa229378a (patch)
treed4d6776150947376fae4f614e3710b95b1ab0336
parentd175e9fee6ab1f76042280f22a86e5764863c642 (diff)
feat(res): add OSC94 resource and setup for terminal progress
-rw-r--r--CHANGELOG.md67
-rw-r--r--mingling/src/res.rs3
-rw-r--r--mingling/src/res/osc94.rs262
-rw-r--r--mingling/src/res/osc94/state.rs74
-rw-r--r--mingling/src/setups.rs3
-rw-r--r--mingling/src/setups/osc94.rs89
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
+}