aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md191
-rw-r--r--examples/example-structural-renderer/Cargo.lock26
-rw-r--r--examples/example-structural-renderer/Cargo.toml1
-rw-r--r--examples/example-structural-renderer/test.toml7
-rw-r--r--mingling/src/example_docs.rs1
-rw-r--r--mingling/src/res.rs15
-rw-r--r--mingling/src/res/confirmer.rs268
-rw-r--r--mingling/src/res/osc94.rs264
-rw-r--r--mingling/src/res/osc94/state.rs74
-rw-r--r--mingling/src/setups.rs34
-rw-r--r--mingling/src/setups/confirmer.rs60
-rw-r--r--mingling/src/setups/osc94.rs89
-rw-r--r--mingling/src/setups/stdin_args.rs82
-rw-r--r--mingling_core/src/lib.rs3
-rw-r--r--mingling_core/src/program/exec.rs9
-rw-r--r--mingling_core/src/program/hook.rs31
-rw-r--r--mingling_core/src/program/hook/hook_info.rs5
-rw-r--r--mingling_core/src/program/once_exec.rs14
-rw-r--r--mingling_core/src/program/repl_exec.rs14
-rw-r--r--mingling_core/src/renderer/render_result.rs333
-rw-r--r--mingling_core/src/utils.rs3
-rw-r--r--mingling_core/src/utils/splitter.rs (renamed from mingling_core/src/program/repl_exec/splitter.rs)78
22 files changed, 1492 insertions, 110 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77ba50c..9029c61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -323,6 +323,162 @@ None
The suggestion collection was also reworked: it now uses a `BTreeSet<SuggestItem>` for natural ordering and deduplication (replacing the previous `Vec<String>` + manual `sort()`/`dedup()`), carries both the suggested token and the fully-qualified owner node path used for the description lookup, and returns `Suggest::Suggest(suggestions)` directly. `entry_description` returns `None` for intermediate trie segments that have no entry of their own or entries without a registered `Description`, in which case suggestions fall back to plain `SuggestItem::new(token)`. The final empty-suggestions fallback to `file_suggest()` is unchanged.
+12. **[`core:render`]** Reworked the `RenderResult` immediate-output mechanism from a single boolean flag into a general **print-hook** system, enabling multiple user-defined hooks to be invoked with the content and output mode of every write.
+
+ **`RenderResultPrint` struct** — Added a new public struct bundling the emitted text content and its output mode:
+
+ - **`content: String`** — The raw text written. For `println`/`eprintln` it includes the trailing newline; for `print`/`eprint` it is exactly the given text.
+ - **`mode: RenderResultMode`** — The output mode (`Stdout` or `Stderr`) the content was written with, telling the hook where the content belongs.
+
+ Derives `Debug`, `Clone`, `PartialEq`, `Eq`.
+
+ **`print_hook` field** — Replaced `immediate_output: bool` with `print_hook: PrintHook` (a `Vec<Box<dyn FnMut(RenderResultPrint)>>` inside an `Option`). The default is `None`, meaning content is only buffered and output uniformly at the end (e.g. via `std_print`).
+
+ **`bind_print_hook()` method** — New method that pushes a user-provided hook onto the hook list; multiple hooks can be bound and are invoked in binding order. Returns `&mut Self` for chaining.
+
+ **`immediate_output()` behavior change** — Now calls `bind_print_hook()` with a hook that flushes content to stdout/stderr in real time (functionally identical to the old boolean behavior, but implemented via the hook mechanism). No longer `const`.
+
+ **`emit()` private helper** — Iterates bound hooks and invokes each with a `RenderResultPrint { content, mode }` value.
+
+ **Write methods updated** — `print`, `println`, `eprint`, and `eprintln` now call `self.emit(&text, Stdout/Stderr)` (after formatting the trailing newline for `println`/`eprintln`) instead of checking the `immediate_output` flag.
+
+ **`append_other()` semantics** — Now checks whether _self has hooks_ and _other has none_; when true, other's buffered content is emitted through self's hooks while being appended. The other's hooks and `exit_code` are **not** transferred — only its buffered content is merged.
+
+ **Manual trait impls** — Since hooks are opaque closures that cannot be cloned or meaningfully compared, hand-written impls replaced the derives:
+
+ - **`Clone`** — Clones the buffered content and exit code but drops the print hooks (creates a result with no hooks).
+ - **`PartialEq` / `Eq`** — Compares only the render buffer and exit code; hooks are ignored.
+ - **`Debug`** — Prints the buffered content, exit code, and the _number_ of bound hooks (`print_hooks: Vec::len`), avoiding attempting to format opaque closures.
+
+ `Default` is still derived (all fields default to empty/`None`).
+
+13. **[`core:utils`]** Added a new `utils` module to `mingling_core` with the `ArgumentSplitter` trait, providing a reusable implementation of shell-style command-line argument splitting. The trait is implemented for `str` and internally for `String`, and provides a `split_args()` method that splits a string input into a `Vec<String>` of arguments, respecting single quotes, double quotes, and backslash escaping.
+
+ **Trait definition:**
+
+ ```rust
+ pub trait ArgumentSplitter {
+ /// Splits the string into a `Vec<String>` of arguments, respecting
+ /// single quotes, double quotes, and backslash escaping.
+ fn split_args(&self) -> Vec<String>;
+ }
+
+ impl ArgumentSplitter for str { /* ... */ }
+ impl ArgumentSplitter for String { /* ... */ }
+ ```
+
+ **Splitting rules** (identical semantics to the previously private `splitter` module):
+
+ - **Whitespace separation** — Arguments are separated by spaces (`' '`); consecutive spaces collapse and empty tokens are dropped.
+ - **Single/double quotes** — Text inside `'...'` or `"..."` is treated as a single argument; the quote characters are stripped. Within quoted segments, backslash escapes the next character (so `"b c"` produces `b c`, and `"b\"c"` produces `b"c`).
+ - **Backslash escaping** (outside quotes) — A backslash takes the next character literally (e.g., `b\ c` produces `b c`; `b\"c` produces `b"c`); a trailing backslash with no following character is ignored/lost.
+
+ **Integration:** The previously private `split_input` / `split_input_string` functions in `mingling_core::program::repl_exec::splitter` have been removed; `repl_exec` now uses `readline.split_args()` on the input string. The `utils` module is exported as `mingling_core::utils`.
+
+ The `ArgumentSplitter` trait is a public API addition, so downstream code can now reuse the same argument-splitting logic that the REPL uses for parsing input lines.
+
+14. **[`res`]** Added the `Confirmer` resource:
+
+ - **`mingling::res::Confirmer`** — A new resource type for interactive confirmation prompts. It caches the confirmed state to avoid repeated prompts, and is typically registered via [`ConfirmerSetup`] and injected into functions through Mingling's resource injection system.
+
+ - **`Confirmer::new()`** — Creates a new `Confirmer` instance in the unconfirmed state.
+ - **`Confirmer::new_confirmed()`** — Creates a `Confirmer` in the confirmed state; `ask`/`try_ask` return `true` directly without prompting.
+ - **`set_confirmed(&mut self)`** — Marks the confirmer as confirmed; subsequent `ask`/`try_ask` calls return `true` directly.
+ - **`ask<P: ConfirmerPredicate>(&self, ask: impl AsRef<str>) -> bool`** — Prompts the user at most **one** time. Returns `false` if the user provides an unrecognizable answer, `true` if already confirmed.
+ - **`try_ask<P: ConfirmerPredicate>(&self, ask: impl AsRef<str>, count: impl Into<ConfirmerCount>) -> Option<bool>`** — Prompts the user up to `count` times. Returns `Some(true)` for confirmation, `Some(false)` for rejection, and `None` if the maximum attempts are exhausted without a parseable answer. The prompt is written to stderr.
+
+ - **`ConfirmerCount` enum** — Specifies the maximum number of attempts: `Loop` (0, indefinite) or `Max(usize)` (positive integer). `From` impls are provided for all integer types (`i8`, `i16`, `i32`, `i64`, `i128`, `isize`, `u8`, `u16`, `u32`, `u64`, `u128`, `usize`); `0` maps to `Loop`, negative values clamp to `Max(usize::MAX)`.
+
+ - **`ConfirmerPredicate` trait** — Defines how to parse user confirmation input. Implementors provide `is_yes(str: &str) -> Option<bool>`: `Some(true)` for yes, `Some(false)` for no, `None` for unparseable input (requiring re-entry).
+
+ - **`YesConfirm` predicate** — Accepts `"y"`/`"yes"` as yes and `"n"`/`"no"` as no. Case-insensitive with leading/trailing whitespace trimming.
+
+ - **`TrueConfirm` predicate** — Accepts `"true"`/`"t"` as yes and `"false"`/`"f"` as no. Case-insensitive with leading/trailing whitespace trimming.
+
+ Derives `Debug`, `Default`, `Clone`, `Copy`.
+
+15. **[`setups`]** Added the `ConfirmerSetup` and `StandardInputArgsSetup` program setups:
+
+ ### `ConfirmerSetup`
+ - **`mingling::setup::ConfirmerSetup`** — A `ProgramSetup` that registers a `Confirmer` resource and installs a pre-dispatch hook checking the user's `confirmation` config mode. When `program.user_context.confirmation == ConfirmationMode::Skip`, the hook marks the `Confirmer` as confirmed via `modify_res`, so all `ask`/`try_ask` calls return `true` without prompting.
+
+ - Registered via `program.with_setup(ConfirmerSetup)`.
+ - Applies uniformly to all subcommands of the entire program; it does not support per-command overrides.
+
+ ### `StandardInputArgsSetup`
+ - **`mingling::setup::StandardInputArgsSetup`** — A `ProgramSetup` that reads piped/redirected standard input and appends the split arguments to the end of the program's argument list.
+
+ - A `pre_dispatch` hook checks whether stdin is a terminal via `IsTerminal`; if it is **not** a terminal (i.e., there is piped or redirected input), it reads all of stdin to the end, converts it to UTF-8 (strict first, lossy fallback), splits it via `ArgumentSplitter::split_args()` (whitespace, single/double quotes, backslash escaping), and appends the resulting arguments to `ctx.arguments` (a `&mut Vec<String>`, per the `pre_dispatch` hook's mutable-arguments semantics from **BREAKING CHANGE #6** in this release).
+ - 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")]`.
@@ -433,6 +589,41 @@ None
_This is a pure deletion change with no behavioral replacement. If downstream code used the `Title`, `Lower`, or `Upper` variants, it needs to switch to other naming styles (such as `Pascal`, `Kebab`, `Snake`, or `Dot`)._
+6. **[`core:hook`]** **[BREAKING]** Changed `HookPreDispatchInfo.arguments` from `&'a [String]` (immutable slice) to `&'a mut Vec<String>` (mutable reference), and updated the `pre_dispatch` hook signature accordingly. The `pre_dispatch` hook can now **rewrite the program's command-line arguments before they are matched against registered dispatchers**, enabling argument normalization, injection, or filtering at the hook level.
+
+ **Type changes:**
+
+ - `HookPreDispatchInfo.arguments: &'a [String]` → `arguments: &'a mut Vec<String>`
+ - `ProgramHook<C>::pre_dispatch` field type: `Box<dyn for<'a> Fn(&HookPreDispatchInfo<'a>) -> ProgramControls<C>>` → `Box<dyn for<'a> Fn(&mut HookPreDispatchInfo<'a>) -> ProgramControls<C>>`
+ - `ProgramHookBuilder::on_pre_dispatch<F, R>` bound: `F: for<'a> Fn(&HookPreDispatchInfo<'a>) -> R` → `F: for<'a> Fn(&mut HookPreDispatchInfo<'a>) -> R`
+ - `Program::run_hook_pre_dispatch` parameter: `&HookPreDispatchInfo` → `&mut HookPreDispatchInfo`
+
+ **Execution pipeline changes:**
+
+ - `exec()` now clones `program.args` into a local mutable `Vec<String>` and passes `&mut` to `exec_with_args`, so hooks rewriting arguments do not mutate the program's stored args (the rewritten copy is what gets dispatched).
+ - `exec_with_args` now takes `args: &mut Vec<String>` instead of `args: &[String]`, and passes `&mut *args` into `run_hook_pre_dispatch`.
+ - `ProgramOnceExec::once_exec` takes the program args via `std::mem::take` into a local mutable `Vec<String>` and passes `&mut` through to `exec_with_args`, ensuring the same mutable-args semantics in the `once_exec` path.
+ - `ReplExec::exec` and `exec_once` in `repl_exec.rs` now pass `&mut Vec<String>` through the same execution path.
+
+ **Migration guide:**
+
+ - Any `pre_dispatch` hook closures must now accept `&mut HookPreDispatchInfo` instead of `&HookPreDispatchInfo`:
+
+ ```rust
+ // Before
+ .on_pre_dispatch(|info: &HookPreDispatchInfo| { ... })
+
+ // After
+ .on_pre_dispatch(|info: &mut HookPreDispatchInfo| {
+ // info.arguments is now &mut Vec<String> — can be mutated
+ })
+ ```
+
+ - References to `info.arguments` inside hooks that previously treated it as `&[String]` will need to adapt to `&mut Vec<String>` (e.g., `info.arguments.as_slice()` instead of `info.arguments` in comparison positions, or explicit dereference / indexing changes).
+ - While most `&[String]`-style usages are transparently compatible via `Deref`, code that relied on the immutability guarantee of the slice (e.g., passing `info.arguments` to functions expecting `&[String]` via coercion) may need `info.arguments.as_slice()` or `&*info.arguments`.
+
+ _Behavioral change: hooks can now rewrite the argument list before dispatch — e.g., inserting default flags, removing deprecated options, or expanding shorthand syntax. Hook authors should be careful to preserve the program's expected argument layout when mutating the list._
+
---
### Release 0.3.0 (2026-07-27)
diff --git a/examples/example-structural-renderer/Cargo.lock b/examples/example-structural-renderer/Cargo.lock
index 453945b..c3f24c7 100644
--- a/examples/example-structural-renderer/Cargo.lock
+++ b/examples/example-structural-renderer/Cargo.lock
@@ -80,6 +80,7 @@ dependencies = [
"might_be_async",
"serde",
"serde_json",
+ "serde_yaml",
]
[[package]]
@@ -111,6 +112,12 @@ dependencies = [
]
[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -163,6 +170,19 @@ dependencies = [
]
[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
name = "size"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -227,6 +247,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/examples/example-structural-renderer/Cargo.toml b/examples/example-structural-renderer/Cargo.toml
index 2090166..e29e9c3 100644
--- a/examples/example-structural-renderer/Cargo.toml
+++ b/examples/example-structural-renderer/Cargo.toml
@@ -10,6 +10,7 @@ serde = { version = "1.0.228", features = ["derive"] }
path = "../../mingling"
features = [
"structural_renderer",
+ "yaml_serde_fmt",
"parser",
]
diff --git a/examples/example-structural-renderer/test.toml b/examples/example-structural-renderer/test.toml
index 2271a51..3e87151 100644
--- a/examples/example-structural-renderer/test.toml
+++ b/examples/example-structural-renderer/test.toml
@@ -9,3 +9,10 @@ input = [ "render", "Bob", "22", "--json" ]
expect.exit-code = 0
expect.result = "{\"member_name\":\"Bob\",\"member_age\":22}"
+
+[[runs]]
+input = [ "render", "Bob", "22", "--yaml" ]
+
+expect.exit-code = 0
+expect.result = """member_name: Bob
+member_age: 22"""
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs
index 55aabdf..c292598 100644
--- a/mingling/src/example_docs.rs
+++ b/mingling/src/example_docs.rs
@@ -2970,6 +2970,7 @@ pub mod example_setup {}
/// path = "../../mingling"
/// features = [
/// "structural_renderer",
+/// "yaml_serde_fmt",
/// "parser",
/// ]
///
diff --git a/mingling/src/res.rs b/mingling/src/res.rs
index a35559c..82c4e00 100644
--- a/mingling/src/res.rs
+++ b/mingling/src/res.rs
@@ -1,9 +1,14 @@
-// Doc Not Optimize
-mod exit_code;
-pub use exit_code::*;
+#[allow(unused_imports)]
+pub use mingling_core::core_res::*;
mod dirs;
pub use dirs::*;
-#[allow(unused_imports)]
-pub use mingling_core::core_res::*;
+mod exit_code;
+pub use exit_code::*;
+
+mod confirmer;
+pub use confirmer::*;
+
+mod osc94;
+pub use osc94::*;
diff --git a/mingling/src/res/confirmer.rs b/mingling/src/res/confirmer.rs
new file mode 100644
index 0000000..900562b
--- /dev/null
+++ b/mingling/src/res/confirmer.rs
@@ -0,0 +1,268 @@
+use std::io::{BufRead, Write};
+
+/// A confirmer for interactive confirmation.
+///
+/// This structure caches the confirmed state to avoid repeated prompts.
+///
+/// Typically, `Confirmer` is registered via [`ConfirmerSetup`], and then injected into functions
+/// through Mingling's resource injection system.
+///
+/// # Registration
+///
+/// Before use, the [`ConfirmerSetup`] must be registered with the program:
+///
+/// ```
+/// # use mingling::MockProgramCollect as ThisProgram;
+/// use mingling::setup::ConfirmerSetup;
+/// use mingling::Program;
+///
+/// let mut program = Program::<ThisProgram>::new();
+/// program.with_setup(ConfirmerSetup);
+/// ```
+///
+/// # Examples
+///
+/// ```
+/// use mingling::res::{Confirmer, YesConfirm};
+///
+/// // In actual use, obtain the registered confirmer through the resource injection system
+/// let confirmer = Confirmer::new_confirmed();
+/// assert!(confirmer.ask::<YesConfirm>("Continue? [y/n] "));
+/// ```
+#[derive(Debug, Default, Clone, Copy)]
+pub struct Confirmer {
+ pub(crate) confirmed: bool,
+}
+
+impl Confirmer {
+ /// Creates a new `Confirmer` instance.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::res::Confirmer;
+ ///
+ /// let confirmer = Confirmer::new();
+ /// ```
+ #[must_use]
+ pub const fn new() -> Self {
+ Self { confirmed: false }
+ }
+
+ /// Creates a `Confirmer` instance in the confirmed state.
+ ///
+ /// The returned `Confirmer` will directly return `true` when calling [`ask`](Confirmer::ask) or
+ /// [`try_ask`](Confirmer::try_ask), without prompting the user.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::res::{Confirmer, YesConfirm};
+ ///
+ /// let confirmer = Confirmer::new_confirmed();
+ /// assert!(confirmer.ask::<YesConfirm>("Continue? [y/n] "));
+ /// ```
+ #[must_use]
+ pub const fn new_confirmed() -> Self {
+ Self { confirmed: true }
+ }
+
+ /// Marks the confirmer as confirmed.
+ ///
+ /// After calling this method, subsequent calls to [`ask`](Confirmer::ask) or
+ /// [`try_ask`](Confirmer::try_ask) on this confirmer will directly return `true`
+ /// without prompting the user.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::res::{Confirmer, YesConfirm};
+ ///
+ /// let mut confirmer = Confirmer::new();
+ /// confirmer.set_confirmed();
+ /// assert!(confirmer.ask::<YesConfirm>("Continue? [y/n] "));
+ /// ```
+ pub const fn set_confirmed(&mut self) {
+ self.confirmed = true;
+ }
+
+ /// Asks the user a confirmation question, with at most one attempt.
+ ///
+ /// Returns `false` if the user provides an unrecognizable answer.
+ /// Returns `true` directly if already confirmed previously.
+ ///
+ /// # Parameters
+ ///
+ /// * `ask` - The prompt text to display to the user.
+ ///
+ /// # Returns
+ ///
+ /// Returns a boolean indicating whether the user confirmed. Returns `false` if the user's input
+ /// could not be parsed or the maximum number of attempts was reached.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::res::{Confirmer, YesConfirm};
+ ///
+ /// let confirmer = Confirmer::new_confirmed();
+ /// let confirmed = confirmer.ask::<YesConfirm>("Delete this file? [y/n] ");
+ /// ```
+ pub fn ask<P: ConfirmerPredicate>(&self, ask: impl AsRef<str>) -> bool {
+ self.try_ask::<P>(ask, ConfirmerCount::Max(1))
+ .unwrap_or(false)
+ }
+
+ /// Asks the user a confirmation question, allowing a specified maximum number of attempts.
+ ///
+ /// # Parameters
+ ///
+ /// * `ask` - The prompt text to display to the user.
+ /// * `count` - The maximum number of attempts. Passing `0` means unlimited attempts (loop
+ /// indefinitely), passing a positive integer means at most that many attempts.
+ ///
+ /// # Returns
+ ///
+ /// Returns `Some(true)` for confirmation, `Some(false)` for rejection.
+ /// Returns `None` if the maximum number of attempts is reached without being able to parse
+ /// the user's input.
+ ///
+ /// # Panics
+ ///
+ /// This function panics when the standard error output (`stderr`) cannot be flushed or when
+ /// reading from standard input fails.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::res::{Confirmer, YesConfirm};
+ ///
+ /// let confirmer = Confirmer::new_confirmed();
+ /// let confirmed = confirmer.try_ask::<YesConfirm>("Confirm execution? [y/n] ", 3);
+ /// ```
+ pub fn try_ask<P: ConfirmerPredicate>(
+ &self,
+ ask: impl AsRef<str>,
+ count: impl Into<ConfirmerCount>,
+ ) -> Option<bool> {
+ if self.confirmed {
+ return Some(true);
+ }
+
+ let count = count.into();
+ let mut attempts = 0usize;
+
+ loop {
+ eprint!("{}", ask.as_ref());
+ std::io::stderr().flush().unwrap();
+
+ let stdin = std::io::stdin();
+ let mut input = String::new();
+ stdin.lock().read_line(&mut input).unwrap();
+ if let Some(result) = P::is_yes(&input) {
+ return Some(result);
+ }
+
+ attempts += 1;
+ match count {
+ ConfirmerCount::Loop => {}
+ ConfirmerCount::Max(max) => {
+ if attempts >= max {
+ return None;
+ }
+ }
+ }
+ }
+ }
+}
+
+/// Specifies the maximum number of attempts for a confirmation prompt.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ConfirmerCount {
+ /// Loop indefinitely until the user gives a parseable answer.
+ Loop,
+ /// Ask at most the specified number of times.
+ Max(usize),
+}
+
+macro_rules! impl_from_for_confirmer_count {
+ ($($t:ty),*) => {
+ $(
+ impl From<$t> for ConfirmerCount {
+ fn from(n: $t) -> Self {
+ if n == 0 {
+ ConfirmerCount::Loop
+ } else {
+ match usize::try_from(n) {
+ Ok(max) => ConfirmerCount::Max(max),
+ Err(_) => ConfirmerCount::Max(usize::MAX),
+ }
+ }
+ }
+ }
+ )*
+ };
+}
+
+impl_from_for_confirmer_count!(
+ i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
+);
+
+/// Defines how to parse user confirmation input.
+///
+/// A type implementing this trait determines which user input strings are treated as "yes" or "no".
+pub trait ConfirmerPredicate {
+ /// Parses the user's input string, returning whether it is "yes".
+ ///
+ /// Returns `Some(true)` for yes, `Some(false)` for no,
+ /// and `None` if the input cannot be parsed (requiring re-entry).
+ fn is_yes(str: &str) -> Option<bool>;
+}
+
+/// A `ConfirmerPredicate` implementation that accepts "y"/"yes" as yes and "n"/"no" as no.
+///
+/// Input comparison is case-insensitive and automatically trims leading/trailing whitespace.
+///
+/// # Examples
+///
+/// ```
+/// use mingling::res::{Confirmer, YesConfirm};
+///
+/// let confirmer = Confirmer::default();
+/// let confirmed = confirmer.ask::<YesConfirm>("Continue? [y/n] ");
+/// ```
+pub struct YesConfirm;
+
+/// A `ConfirmerPredicate` implementation that accepts "true"/"t" as yes and "false"/"f" as no.
+///
+/// Input comparison is case-insensitive and automatically trims leading/trailing whitespace.
+///
+/// # Examples
+///
+/// ```
+/// use mingling::res::{Confirmer, TrueConfirm};
+///
+/// let confirmer = Confirmer::default();
+/// let confirmed = confirmer.ask::<TrueConfirm>("Enable this feature? [true/false] ");
+/// ```
+pub struct TrueConfirm;
+
+impl ConfirmerPredicate for YesConfirm {
+ fn is_yes(str: &str) -> Option<bool> {
+ match str.trim().to_lowercase().as_str() {
+ "y" | "yes" => Some(true),
+ "n" | "no" => Some(false),
+ _ => None,
+ }
+ }
+}
+
+impl ConfirmerPredicate for TrueConfirm {
+ fn is_yes(str: &str) -> Option<bool> {
+ match str.trim().to_lowercase().as_str() {
+ "true" | "t" => Some(true),
+ "false" | "f" => Some(false),
+ _ => None,
+ }
+ }
+}
diff --git a/mingling/src/res/osc94.rs b/mingling/src/res/osc94.rs
new file mode 100644
index 0000000..8f873aa
--- /dev/null
+++ b/mingling/src/res/osc94.rs
@@ -0,0 +1,264 @@
+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) {
+ if self.is_support {
+ 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 3a3f13d..7a523cc 100644
--- a/mingling/src/setups.rs
+++ b/mingling/src/setups.rs
@@ -1,28 +1,34 @@
-// Doc Not Optimize
+/// Picker's `ProgramSetup` variant.
+///
+/// Internally does not use its own argument parsing,
+/// but relies on `arg_picker`'s argument parsing capability.
+#[cfg(feature = "picker")]
+pub mod picker;
+
mod basic;
pub use basic::*;
+mod confirmer;
+pub use confirmer::*;
+
mod dirs;
pub use dirs::*;
mod exit_code;
pub use exit_code::*;
-/// Picker's `ProgramSetup` variant.
-///
-/// Internally does not use its own argument parsing,
-/// but relies on `arg_picker`'s argument parsing capability.
-#[cfg(feature = "picker")]
-pub mod picker;
-
-#[cfg(feature = "structural_renderer")]
-mod structural_renderer;
-
-#[cfg(feature = "structural_renderer")]
-pub use structural_renderer::*;
+mod osc94;
+pub use osc94::*;
#[cfg(feature = "repl")]
mod repl_basic;
-
#[cfg(feature = "repl")]
pub use repl_basic::*;
+
+mod stdin_args;
+pub use stdin_args::*;
+
+#[cfg(feature = "structural_renderer")]
+mod structural_renderer;
+#[cfg(feature = "structural_renderer")]
+pub use structural_renderer::*;
diff --git a/mingling/src/setups/confirmer.rs b/mingling/src/setups/confirmer.rs
new file mode 100644
index 0000000..1824745
--- /dev/null
+++ b/mingling/src/setups/confirmer.rs
@@ -0,0 +1,60 @@
+use mingling_core::{
+ Program, ProgramCollect, config, hook::ProgramHook, setup::ProgramSetup, this,
+};
+
+use crate::res::Confirmer;
+
+/// Confirmer setup for managing confirmation state
+///
+/// This Setup manages the confirmation flag within the program's resource
+/// store. It registers a [`Confirmer`] resource and sets up a hook that
+/// checks the user's confirmation mode during program execution.
+///
+/// # Usage
+///
+/// This Setup can be registered using the
+/// [`Program`](https://docs.rs/mingling/latest/mingling/struct.Program.html)
+/// `with_setup` method, for example:
+///
+/// ```rust
+/// # use mingling::MockProgramCollect as ThisProgram;
+/// use mingling::Program;
+/// use mingling::setup::ConfirmerSetup;
+///
+/// let mut program = Program::<ThisProgram>::new();
+/// program.with_setup(ConfirmerSetup);
+/// ```
+///
+/// # Behavior
+///
+/// - Registers a [`Confirmer`] resource that tracks confirmation state.
+/// - At the beginning of command execution, checks whether the user's
+/// confirmation mode is set to `Skip`.
+/// - If confirmation is skipped, the [`Confirmer`] resource is updated
+/// to record the confirmed state.
+///
+/// # Notes
+///
+/// - This Setup applies uniformly to all subcommands of the entire program.
+/// - The confirmation state is determined by the global `config` setting;
+/// it does not support per-command overrides.
+pub struct ConfirmerSetup;
+
+impl<C> ProgramSetup<C> for ConfirmerSetup
+where
+ C: ProgramCollect<Enum = C> + 'static,
+{
+ fn setup(self, program: &mut Program<C>) {
+ program.with_resource(Confirmer::new());
+
+ program.with_hook(ProgramHook::empty().on_pre_dispatch::<_, ()>(|_| {
+ let p = this::<C>();
+ let confirmed = p.user_context.confirmation == config::ConfirmationMode::Skip;
+ if confirmed {
+ p.modify_res(|c: &mut Confirmer| {
+ c.set_confirmed();
+ });
+ }
+ }));
+ }
+}
diff --git a/mingling/src/setups/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
+}
diff --git a/mingling/src/setups/stdin_args.rs b/mingling/src/setups/stdin_args.rs
new file mode 100644
index 0000000..ca55ca7
--- /dev/null
+++ b/mingling/src/setups/stdin_args.rs
@@ -0,0 +1,82 @@
+use std::io::{IsTerminal, Read};
+
+use mingling_core::{
+ Program, ProgramCollect, hook::ProgramHook, setup::ProgramSetup, utils::ArgumentSplitter,
+};
+
+/// Uses the standard input as arguments for the program
+///
+/// This Setup can take standard input supplied via a pipe or redirect,
+/// split it according to whitespace and quoting rules, and append
+/// the resulting arguments to the end of the command argument list.
+///
+/// # Usage
+///
+/// This Setup can be registered using the
+/// [`Program`](https://docs.rs/mingling/latest/mingling/struct.Program.html)
+/// `with_setup` method, for example:
+///
+/// ```rust
+/// # use mingling::MockProgramCollect as ThisProgram;
+/// use mingling::Program;
+/// use mingling::setup::StandardInputArgsSetup;
+///
+/// let mut program = Program::<ThisProgram>::new();
+/// program.with_setup(StandardInputArgsSetup);
+/// ```
+///
+/// # Behavior
+///
+/// - Standard input is only read when it is not a terminal (i.e., when
+/// there is piped or redirected input).
+/// - The read content is split into multiple arguments according to
+/// whitespace and quoting rules.
+/// - If the standard input content is empty, no arguments are produced.
+/// - All input is converted to UTF-8 encoding (lossy conversion is used
+/// when strict parsing is not possible).
+///
+/// # Notes
+///
+/// - This Setup applies uniformly to all subcommands of the entire program
+/// and does not provide fine-grained control. If you need different
+/// standard input behavior across different subcommands (e.g., some
+/// subcommands read stdin while others ignore it), **do not use this Setup**.
+/// - This Setup does **not** provide any validation rules. Content provided
+/// via standard input is treated as trusted arguments and appended directly.
+/// As a result, the input source can also inject arbitrary arguments into
+/// the command, so you should be careful when processing untrusted input.
+pub struct StandardInputArgsSetup;
+
+impl<C> ProgramSetup<C> for StandardInputArgsSetup
+where
+ C: ProgramCollect<Enum = C>,
+{
+ fn setup(self, program: &mut Program<C>) {
+ program.with_hook(ProgramHook::empty().on_pre_dispatch(|ctx| {
+ let pipe_input = read_stdin();
+ if let Some(pipe_input) = pipe_input {
+ ctx.arguments.append(&mut pipe_input.trim().split_args());
+ }
+ }));
+ }
+}
+
+fn read_stdin() -> Option<String> {
+ // Check if stdin is a terminal (no piped input) or has data available
+ if std::io::stdin().is_terminal() {
+ return None;
+ }
+
+ let mut bytes = Vec::new();
+ match std::io::stdin().read_to_end(&mut bytes) {
+ Ok(_) => {
+ if bytes.is_empty() {
+ return None;
+ }
+ // Handle encoding differences, ensure output is always UTF-8.
+ // First try strict UTF-8 parsing; fall back to lossy conversion
+ Some(String::from_utf8_lossy(&bytes).into_owned())
+ }
+ Err(_) => None,
+ }
+}
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
index 7048307..641118c 100644
--- a/mingling_core/src/lib.rs
+++ b/mingling_core/src/lib.rs
@@ -110,3 +110,6 @@ pub mod __private {
/// Mingling's convention metadatas, which can be bound to types using `#[metadata]`, to provide identification for types
pub mod metadata;
+
+/// Some common utilities in Mingling, providing a collection of functionality needed by many modules.
+pub mod utils;
diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs
index 923b47b..b0a5b16 100644
--- a/mingling_core/src/program/exec.rs
+++ b/mingling_core/src/program/exec.rs
@@ -15,13 +15,14 @@ pub fn exec<C>(program: &'static Program<C>) -> Result<RenderResult, ProgramInte
where
C: ProgramCollect<Enum = C> + Send + Sync,
{
- might_be_async::invoke!(exec_with_args(program, &program.args))
+ let mut args = program.args.clone();
+ might_be_async::invoke!(exec_with_args(program, &mut args))
}
#[might_be_async::func]
pub fn exec_with_args<C>(
program: &'static Program<C>,
- args: &[String],
+ args: &mut Vec<String>,
) -> Result<RenderResult, ProgramInternalExecuteError>
where
C: ProgramCollect<Enum = C> + Send + Sync,
@@ -51,7 +52,9 @@ where
// Run hooks
control!(
- program.run_hook_pre_dispatch(&crate::hook::HookPreDispatchInfo { arguments: args }),
+ program.run_hook_pre_dispatch(&mut crate::hook::HookPreDispatchInfo {
+ arguments: &mut *args,
+ }),
current
);
diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs
index 8fd2ba1..92106f9 100644
--- a/mingling_core/src/program/hook.rs
+++ b/mingling_core/src/program/hook.rs
@@ -64,8 +64,9 @@ where
pub begin: Option<Box<dyn Fn(&HookBeginInfo) + Send + Sync>>,
/// Executes before the program dispatches
- pub pre_dispatch:
- Option<Box<dyn for<'a> Fn(&HookPreDispatchInfo<'a>) -> ProgramControls<C> + Send + Sync>>,
+ pub pre_dispatch: Option<
+ Box<dyn for<'a> Fn(&mut HookPreDispatchInfo<'a>) -> ProgramControls<C> + Send + Sync>,
+ >,
/// Executes after the program dispatches
pub post_dispatch: Option<
@@ -162,7 +163,10 @@ where
}
}
- pub(crate) fn run_hook_pre_dispatch(&self, info: &HookPreDispatchInfo) -> ProgramControls<C> {
+ pub(crate) fn run_hook_pre_dispatch(
+ &self,
+ info: &mut HookPreDispatchInfo,
+ ) -> ProgramControls<C> {
if !self.user_context.run_hook {
return ProgramControls::Empty;
}
@@ -475,7 +479,7 @@ where
#[must_use]
pub fn on_pre_dispatch<F, R>(mut self, handler: F) -> Self
where
- F: for<'a> Fn(&HookPreDispatchInfo<'a>) -> R + 'static + Send + Sync,
+ F: for<'a> Fn(&mut HookPreDispatchInfo<'a>) -> R + 'static + Send + Sync,
R: Into<ProgramControls<C>>,
{
self.pre_dispatch = Some(Box::new(move |info| handler(info).into()));
@@ -801,16 +805,21 @@ mod tests {
#[test]
fn test_hook_on_pre_dispatch() {
static CALLED: AtomicBool = AtomicBool::new(false);
- let hook =
- ProgramHook::<MockHookEnum>::empty().on_pre_dispatch(|info: &HookPreDispatchInfo| {
- assert_eq!(info.arguments, &["a", "b"]);
+ let mut args = vec!["a".to_string(), "b".to_string()];
+ let hook = ProgramHook::<MockHookEnum>::empty().on_pre_dispatch(
+ |info: &mut HookPreDispatchInfo| {
+ assert_eq!(info.arguments.as_slice(), &["a", "b"]);
+ // The hook may rewrite the arguments before dispatch
+ info.arguments.push("c".to_string());
CALLED.store(true, Ordering::SeqCst);
- });
+ },
+ );
assert!(hook.pre_dispatch.is_some());
- (hook.pre_dispatch.as_ref().unwrap())(&HookPreDispatchInfo {
- arguments: &["a".to_string(), "b".to_string()],
+ (hook.pre_dispatch.as_ref().unwrap())(&mut HookPreDispatchInfo {
+ arguments: &mut args,
});
assert!(CALLED.load(Ordering::SeqCst));
+ assert_eq!(args.as_slice(), &["a", "b", "c"]);
}
#[test]
@@ -910,7 +919,7 @@ mod tests {
fn test_hook_builder_chaining() {
let hook = ProgramHook::<MockHookEnum>::empty()
.on_begin::<_, ()>(|_: &HookBeginInfo| ())
- .on_pre_dispatch(|_: &HookPreDispatchInfo| ())
+ .on_pre_dispatch(|_: &mut HookPreDispatchInfo| ())
.on_post_dispatch(|_: &HookPostDispatchInfo<MockHookEnum>| ())
.on_pre_chain(|_: &HookPreChainInfo<MockHookEnum>| ())
.on_post_chain(|_: &HookPostChainInfo<MockHookEnum>| ())
diff --git a/mingling_core/src/program/hook/hook_info.rs b/mingling_core/src/program/hook/hook_info.rs
index 768f652..25cf272 100644
--- a/mingling_core/src/program/hook/hook_info.rs
+++ b/mingling_core/src/program/hook/hook_info.rs
@@ -7,7 +7,10 @@ pub struct HookBeginInfo {}
/// Represents the data passed to `pre_dispatch` hook.
pub struct HookPreDispatchInfo<'a> {
/// Arguments entered by the user before dispatching
- pub arguments: &'a [String],
+ ///
+ /// The reference is mutable so the hook can rewrite the arguments before
+ /// they are matched against the registered dispatchers.
+ pub arguments: &'a mut Vec<String>,
}
/// Represents the data passed to `post_dispatch` hook.
diff --git a/mingling_core/src/program/once_exec.rs b/mingling_core/src/program/once_exec.rs
index e9927b5..203bbf5 100644
--- a/mingling_core/src/program/once_exec.rs
+++ b/mingling_core/src/program/once_exec.rs
@@ -25,15 +25,19 @@ where
self.run_hook_on_begin(&crate::hook::HookBeginInfo {});
self.args = self.args.iter().skip(1).cloned().collect();
+ let mut args = std::mem::take(&mut self.args);
#[cfg(not(feature = "async"))]
{
#[cfg(panic = "abort")]
- return self.exec_wrapper(|p| crate::exec::exec(p).map_err(|e| e.into()));
+ return self
+ .exec_wrapper(|p| crate::exec::exec_with_args(p, &mut args).map_err(|e| e.into()));
#[cfg(not(panic = "abort"))]
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
- self.exec_wrapper(|p| crate::exec::exec(p).map_err(std::convert::Into::into))
+ self.exec_wrapper(|p| {
+ crate::exec::exec_with_args(p, &mut args).map_err(std::convert::Into::into)
+ })
})) {
Ok(result) => result,
Err(panic_info) => {
@@ -60,7 +64,11 @@ where
#[cfg(feature = "async")]
{
return self
- .exec_wrapper(|p| async { crate::exec::exec(p).await.map_err(Into::into) })
+ .exec_wrapper(|p| async move {
+ crate::exec::exec_with_args(p, &mut args)
+ .await
+ .map_err(Into::into)
+ })
.await;
}
}
diff --git a/mingling_core/src/program/repl_exec.rs b/mingling_core/src/program/repl_exec.rs
index 9d9be30..2db5c5a 100644
--- a/mingling_core/src/program/repl_exec.rs
+++ b/mingling_core/src/program/repl_exec.rs
@@ -7,10 +7,8 @@ use std::io::Write;
#[doc(hidden)]
pub mod res;
-mod splitter;
-
use crate::error::{ProgramInternalExecuteError, ProgramPanic};
-use crate::program::repl_exec::splitter::split_input_string;
+use crate::utils::ArgumentSplitter;
use crate::{Program, ProgramCollect, RenderResult};
use crate::{program::repl_exec::res::ResREPL, this};
@@ -58,10 +56,10 @@ where
line: &mut readline,
});
- let args = split_input_string(&readline);
+ let mut args = readline.split_args();
p.run_hook_repl_pre_exec(&crate::hook::HookREPLPreExecInfo { args: &args });
- match might_be_async::invoke!(exec_once(p, &args)) {
+ match might_be_async::invoke!(exec_once(p, &mut args)) {
Ok(r) => {
p.run_hook_repl_on_receive_result(&crate::hook::HookREPLOnReceiveResultInfo {
result: &r,
@@ -91,13 +89,13 @@ where
#[cfg(not(feature = "async"))]
fn exec_once<C>(
p: &'static Program<C>,
- args: &[String],
+ args: &mut Vec<String>,
) -> Result<RenderResult, ProgramInternalExecuteError>
where
C: ProgramCollect<Enum = C> + Send + Sync + 'static,
{
#[cfg(panic = "abort")]
- let exec_result = super::exec::exec_with_args(p, &args);
+ let exec_result = super::exec::exec_with_args(p, args);
#[cfg(not(panic = "abort"))]
let exec_result = {
@@ -130,7 +128,7 @@ where
#[cfg(feature = "async")]
async fn exec_once<C>(
p: &'static Program<C>,
- args: &[String],
+ args: &mut Vec<String>,
) -> Result<RenderResult, ProgramInternalExecuteError>
where
C: ProgramCollect<Enum = C> + Send + Sync + 'static,
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
index 0351925..22d787c 100644
--- a/mingling_core/src/renderer/render_result.rs
+++ b/mingling_core/src/renderer/render_result.rs
@@ -1,11 +1,99 @@
use std::{
- fmt::{Display, Formatter},
+ fmt::{self, Display, Formatter},
io::Write,
process::{ExitCode, exit},
};
use crate::RenderResultMode::{Stderr, Stdout};
+/// A single emitted output item handed to a print hook.
+///
+/// `RenderResultPrint` bundles the text content and the output mode together
+/// into one value, so a print hook can route the content to stdout/stderr — or
+/// any custom sink — as a unit instead of juggling two separate arguments.
+///
+/// Values of this type are produced whenever a [`RenderResult`] with bound
+/// print hooks (see [`RenderResult::bind_print_hook`] and
+/// [`RenderResult::immediate_output`]) writes content through
+/// `print`/`println`/`eprint`/`eprintln`, and are handed to every hook in
+/// binding order. They are also used to flush another result's buffered content
+/// via [`RenderResult::append_other`] when the destination has hooks bound.
+///
+/// # Fields
+///
+/// * `content` — The raw text that was written, including any trailing newline
+/// added by `println`/`eprintln`.
+/// * `mode` — The output mode (`Stdout` or `Stderr`) the content was written
+/// with, which tells the hook where the content belongs.
+///
+/// # Examples
+///
+/// ```
+/// use mingling_core::{RenderResult, RenderResultMode, RenderResultPrint};
+///
+/// // Build an output item manually
+/// let print = RenderResultPrint {
+/// content: "Hello, world!".to_string(),
+/// mode: RenderResultMode::Stdout,
+/// };
+/// assert_eq!(print.content, "Hello, world!");
+/// assert_eq!(print.mode, RenderResultMode::Stdout);
+///
+/// // Use it inside a print hook
+/// let mut result = RenderResult::default();
+/// result.bind_print_hook(|print| match print.mode {
+/// RenderResultMode::Stdout => print!("{}", print.content),
+/// RenderResultMode::Stderr => eprint!("{}", print.content),
+/// });
+/// result.eprintln("something went wrong"); // goes to stderr via the hook
+/// ```
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RenderResultPrint {
+ /// The emitted text content.
+ ///
+ /// This is the raw text that was written to the render buffer when the
+ /// hook fired. For `println`/`eprintln` it includes the trailing newline;
+ /// for `print`/`eprint` it is exactly the given text.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResultMode, RenderResultPrint};
+ ///
+ /// let print = RenderResultPrint {
+ /// content: "Hello".to_string(),
+ /// mode: RenderResultMode::Stdout,
+ /// };
+ /// assert_eq!(print.content, "Hello");
+ /// ```
+ pub content: String,
+
+ /// The output mode the content was written with.
+ ///
+ /// Indicates whether the content was originally directed to stdout
+ /// (`Stdout`) or stderr (`Stderr`), allowing a hook to route the content
+ /// to the matching stream.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResultMode, RenderResultPrint};
+ ///
+ /// let print = RenderResultPrint {
+ /// content: "error".to_string(),
+ /// mode: RenderResultMode::Stderr,
+ /// };
+ /// assert_eq!(print.mode, RenderResultMode::Stderr);
+ /// ```
+ pub mode: RenderResultMode,
+}
+
+/// Optional list of print hooks bound to a `RenderResult`.
+///
+/// Each hook receives the emitted [`RenderResultPrint`]. See
+/// [`RenderResult::bind_print_hook`] and [`RenderResult::immediate_output`].
+type PrintHook = Option<Vec<Box<dyn FnMut(RenderResultPrint)>>>;
+
/// Render result, containing the rendered text content.
///
/// `RenderResult` is the core data structure used throughout the rendering pipeline
@@ -20,8 +108,9 @@ use crate::RenderResultMode::{Stderr, Stdout};
/// - **Buffered output**: All rendered content is first collected into the buffer
/// and can be output uniformly at a convenient time.
/// - **Immediate output**: Can be enabled via [`immediate_output`](RenderResult::immediate_output),
-/// causing content to be flushed to stdout/stderr in real time while also being
-/// added to the buffer.
+/// which binds a print hook that flushes content to stdout/stderr in real time
+/// while also being added to the buffer. Custom hooks can be bound with
+/// [`bind_print_hook`](RenderResult::bind_print_hook).
/// - **Dual-channel output**: The `Stdout` and `Stderr` modes distinguish between
/// normal output and error output.
/// - **Exit code management**: Supports carrying an exit code to exit the process
@@ -59,19 +148,19 @@ use crate::RenderResultMode::{Stderr, Stdout};
/// let result: RenderResult = (|| RenderResult::from("closure result")).into();
/// assert_eq!(result.to_string(), "closure result");
/// ```
-#[derive(Default, Debug, Clone, PartialEq, Eq)]
+#[derive(Default)]
pub struct RenderResult {
- /// Whether immediate output is enabled.
+ /// Print hooks invoked with the buffered content and its output mode.
///
- /// When set to `true`, rendered content is flushed to stdout/stderr in real time
- /// while also being written to the buffer, enabling live output. This is useful
- /// in scenarios where results should be displayed incrementally, such as in
- /// long-running rendering tasks where the user wants to see partial output
- /// without waiting for the entire rendering process to complete.
+ /// When hooks are bound (via [`immediate_output`](RenderResult::immediate_output)
+ /// or [`bind_print_hook`](RenderResult::bind_print_hook)), every
+ /// `print`/`println`/`eprint`/`eprintln` call additionally emits the content
+ /// through each hook in binding order — typically flushing it to stdout/stderr
+ /// in real time — while the content is still appended to the buffer.
///
- /// The default value is `false`, meaning all content is first written to the
- /// buffer and output uniformly at the end.
- immediate_output: bool,
+ /// The default value is `None`, meaning content is only buffered and output
+ /// uniformly at the end (e.g. via [`std_print`](RenderResult::std_print)).
+ print_hook: PrintHook,
/// Render buffer, stored as a list of (text, output mode) pairs.
///
@@ -111,6 +200,36 @@ pub struct RenderResult {
pub exit_code: i32,
}
+impl fmt::Debug for RenderResult {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ f.debug_struct("RenderResult")
+ .field("render_buffer", &self.render_buffer)
+ .field("exit_code", &self.exit_code)
+ .field("print_hooks", &self.print_hook.as_ref().map(Vec::len))
+ .finish()
+ }
+}
+
+impl Clone for RenderResult {
+ /// The bound print hooks are opaque closures and cannot be cloned, so the
+ /// cloned result is created without any hooks.
+ fn clone(&self) -> Self {
+ Self {
+ print_hook: None,
+ render_buffer: self.render_buffer.clone(),
+ exit_code: self.exit_code,
+ }
+ }
+}
+
+impl PartialEq for RenderResult {
+ fn eq(&self, other: &Self) -> bool {
+ self.render_buffer == other.render_buffer && self.exit_code == other.exit_code
+ }
+}
+
+impl Eq for RenderResult {}
+
/// Enum representing the output mode for render results.
///
/// This determines whether the rendered content should be directed to standard
@@ -244,12 +363,13 @@ impl RenderResult {
Self::default()
}
- /// Marks the render result for immediate output, bypassing any buffering or
- /// deferred rendering.
+ /// Enables immediate output by binding a print hook that flushes content to
+ /// stdout/stderr in real time.
///
- /// When set, the rendered content will be both collected in the result and
- /// immediately flushed to stdout/stderr in real time, rather than being
- /// deferred for later display.
+ /// After this is called, every `print`/`println`/`eprint`/`eprintln` call
+ /// writes its content to the corresponding output stream immediately, while
+ /// also keeping it in the buffer for later use (e.g. [`std_print`](RenderResult::std_print)
+ /// or `to_string()`).
///
/// # Examples
///
@@ -258,9 +378,47 @@ impl RenderResult {
///
/// let mut result = RenderResult::default();
/// result.immediate_output();
+ /// result.print("Hello, ");
+ /// result.print("world!"); // flushed to stdout right away
+ /// assert_eq!(result.to_string(), "Hello, world!");
/// ```
- pub const fn immediate_output(&mut self) -> &mut Self {
- self.immediate_output = true;
+ pub fn immediate_output(&mut self) -> &mut Self {
+ self.bind_print_hook(|RenderResultPrint { content, mode }| match mode {
+ Stdout => print!("{content}"),
+ Stderr => eprint!("{content}"),
+ })
+ }
+
+ /// Binds a custom print hook invoked with the content and output mode of
+ /// every `print`/`println`/`eprint`/`eprintln` call.
+ ///
+ /// Multiple hooks can be bound; they are invoked in binding order. This is
+ /// the building block behind [`immediate_output`](RenderResult::immediate_output)
+ /// and can be used to route output to a custom sink (e.g. for testing).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.bind_print_hook(|print| {
+ /// println!(
+ /// "[{}] {}",
+ /// if print.mode == RenderResultMode::Stdout {
+ /// "out"
+ /// } else {
+ /// "err"
+ /// },
+ /// print.content
+ /// );
+ /// });
+ /// result.print("Hello");
+ /// ```
+ pub fn bind_print_hook(&mut self, hook: impl FnMut(RenderResultPrint) + 'static) -> &mut Self {
+ self.print_hook
+ .get_or_insert_with(Vec::new)
+ .push(Box::new(hook));
self
}
@@ -317,12 +475,12 @@ impl RenderResult {
/// Appends the contents of another `RenderResult` to this one.
///
- /// If this `RenderResult` has `immediate_output` enabled but the other does not,
- /// the other's content will be immediately flushed to the appropriate output stream
- /// (stdout/stderr) while also being appended to the render buffer.
+ /// If this `RenderResult` has print hooks bound but the other does not, the
+ /// other's content is emitted through this result's hooks (e.g. flushed to
+ /// stdout/stderr) while also being appended to the render buffer.
///
- /// The `exit_code` of the other result is **not** transferred — only the buffered
- /// content and the `immediate_output` flag of the other result are merged.
+ /// The `exit_code` and the print hooks of the other result are **not**
+ /// transferred — only its buffered content is merged.
///
/// # Arguments
///
@@ -345,17 +503,15 @@ impl RenderResult {
pub fn append_other(&mut self, other: impl Into<Self>) {
let other = other.into();
- // If self has immediate output enabled, but the input does not, the input needs immediate output.
- let immediate_output = !other.immediate_output && self.immediate_output;
+ // If self has hooks but the other does not, the other's buffered content
+ // was never emitted — flush it through self's hooks while appending.
+ let should_emit = self.print_hook.is_some() && other.print_hook.is_none();
- for i in other.render_buffer {
- if immediate_output {
- match &i.1 {
- Stdout => print!("{}", i.0),
- Stderr => eprint!("{}", i.0),
- }
+ for (content, mode) in other.render_buffer {
+ if should_emit {
+ self.emit(&content, mode);
}
- self.render_buffer.push(i);
+ self.render_buffer.push((content, mode));
}
}
@@ -373,9 +529,7 @@ impl RenderResult {
/// ```
pub fn print(&mut self, text: impl Into<String>) {
let text = text.into();
- if self.immediate_output {
- print!("{text}");
- }
+ self.emit(&text, Stdout);
self.append_to_buffer(text, Stdout);
}
@@ -393,9 +547,7 @@ impl RenderResult {
/// ```
pub fn println(&mut self, text: impl Into<String>) {
let text = text.into();
- if self.immediate_output {
- println!("{text}");
- }
+ self.emit(&format!("{text}\n"), Stdout);
self.append_line_to_buffer(text, Stdout);
}
@@ -413,9 +565,7 @@ impl RenderResult {
/// ```
pub fn eprint(&mut self, text: impl Into<String>) {
let text = text.into();
- if self.immediate_output {
- eprint!("{text}");
- }
+ self.emit(&text, Stderr);
self.append_to_buffer(text, Stderr);
}
@@ -433,9 +583,7 @@ impl RenderResult {
/// ```
pub fn eprintln(&mut self, text: impl Into<String>) {
let text = text.into();
- if self.immediate_output {
- eprintln!("{text}");
- }
+ self.emit(&format!("{text}\n"), Stderr);
self.append_line_to_buffer(text, Stderr);
}
@@ -538,7 +686,7 @@ impl RenderResult {
///
/// # Returns
///
- /// A new `RenderResult` with the same `immediate_output` flag and `exit_code`, but with
+ /// A new `RenderResult` with the same print hooks and `exit_code`, but with
/// trimmed text content.
///
/// # Examples
@@ -579,11 +727,23 @@ impl RenderResult {
Self {
render_buffer: buffer,
- immediate_output: self.immediate_output,
+ print_hook: self.print_hook,
exit_code: self.exit_code,
}
}
+ /// Emits `content` to every bound print hook, if any.
+ fn emit(&mut self, content: &str, mode: RenderResultMode) {
+ if let Some(hooks) = &mut self.print_hook {
+ for hook in hooks {
+ hook(RenderResultPrint {
+ content: content.to_string(),
+ mode,
+ });
+ }
+ }
+ }
+
/// Exits the process with the exit code stored in this `RenderResult`.
///
/// This method calls `std::process::exit()` with the `exit_code` value,
@@ -623,7 +783,9 @@ fn string_to_render_result(string: impl Into<String>, mode: RenderResultMode) ->
#[cfg(test)]
mod tests {
use super::*;
+ use std::cell::RefCell;
use std::io::Write as IoWrite;
+ use std::rc::Rc;
#[test]
fn default_creates_empty_text_with_exit_code_zero() {
@@ -749,4 +911,79 @@ mod tests {
assert_eq!(trimmed.render_buffer[0].1, RenderResultMode::Stderr);
assert_eq!(trimmed.to_string(), "error");
}
+
+ #[test]
+ fn print_hooks_receive_content_and_mode() {
+ let mut result = RenderResult::default();
+ let captured: Rc<RefCell<Vec<RenderResultPrint>>> = Rc::default();
+ let hook_captured = Rc::clone(&captured);
+ result.bind_print_hook(move |print| hook_captured.borrow_mut().push(print));
+
+ result.print("Hello");
+ result.eprintln("World");
+
+ assert_eq!(
+ captured.borrow()[0],
+ RenderResultPrint {
+ content: "Hello".to_string(),
+ mode: RenderResultMode::Stdout
+ }
+ );
+ assert_eq!(
+ captured.borrow()[1],
+ RenderResultPrint {
+ content: "World\n".to_string(),
+ mode: RenderResultMode::Stderr
+ }
+ );
+ assert_eq!(result.to_string(), "HelloWorld");
+ }
+
+ #[test]
+ fn immediate_output_binds_stdout_hook() {
+ let mut result = RenderResult::default();
+ assert!(result.print_hook.is_none());
+ result.immediate_output();
+ assert!(result.print_hook.is_some());
+ }
+
+ #[test]
+ fn append_other_emits_through_hooks_when_self_has_them() {
+ let mut dest = RenderResult::default();
+ let emitted: Rc<RefCell<Vec<String>>> = Rc::default();
+ let hook_emitted = Rc::clone(&emitted);
+ dest.bind_print_hook(move |print| hook_emitted.borrow_mut().push(print.content));
+
+ let mut src = RenderResult::default();
+ src.append_to_buffer("Hello", RenderResultMode::Stdout);
+ dest.append_other(src);
+
+ assert_eq!(emitted.borrow().as_slice(), ["Hello"]);
+ assert_eq!(dest.to_string(), "Hello");
+ }
+
+ #[test]
+ fn append_other_does_not_reemit_when_other_has_hooks() {
+ let mut dest = RenderResult::default();
+ let emitted: Rc<RefCell<Vec<String>>> = Rc::default();
+ let hook_emitted = Rc::clone(&emitted);
+ dest.bind_print_hook(move |print| hook_emitted.borrow_mut().push(print.content));
+
+ let mut src = RenderResult::default();
+ src.bind_print_hook(|_| {});
+ src.append_to_buffer("Hello", RenderResultMode::Stdout);
+ dest.append_other(src);
+
+ assert!(emitted.borrow().is_empty());
+ assert_eq!(dest.to_string(), "Hello");
+ }
+
+ #[test]
+ fn write_does_not_emit_through_hooks() {
+ let mut result = RenderResult::default();
+ result.bind_print_hook(|_| panic!("append_to_buffer must not emit"));
+
+ IoWrite::write(&mut result, b"Hello").unwrap();
+ assert_eq!(result.to_string(), "Hello");
+ }
}
diff --git a/mingling_core/src/utils.rs b/mingling_core/src/utils.rs
new file mode 100644
index 0000000..89fcd8f
--- /dev/null
+++ b/mingling_core/src/utils.rs
@@ -0,0 +1,3 @@
+mod splitter;
+
+pub use splitter::ArgumentSplitter;
diff --git a/mingling_core/src/program/repl_exec/splitter.rs b/mingling_core/src/utils/splitter.rs
index 312ad73..6b2ea11 100644
--- a/mingling_core/src/program/repl_exec/splitter.rs
+++ b/mingling_core/src/utils/splitter.rs
@@ -1,12 +1,56 @@
-// Doc Not Optimize
-/// Wraps `split_input` to work with owned `String` inputs.
-pub fn split_input_string(input: &str) -> Vec<String> {
- split_input(input)
+/// A trait for splitting strings into arguments, respecting quotes and escapes.
+///
+/// # Examples
+///
+/// ```
+/// use mingling_core::utils::ArgumentSplitter;
+///
+/// let args = "echo \"hello world\"".split_args();
+/// assert_eq!(args, vec!["echo", "hello world"]);
+/// ```
+pub trait ArgumentSplitter {
+ /// Splits the input string into a vector of argument strings.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::utils::ArgumentSplitter;
+ ///
+ /// let args = "a 'b c' d".split_args();
+ /// assert_eq!(args, vec!["a", "b c", "d"]);
+ /// ```
+ fn split_args(self) -> Vec<String>;
+}
+
+impl<S: AsRef<str>> ArgumentSplitter for S {
+ /// Splits the string into arguments, respecting single quotes, double
+ /// quotes, and backslash escaping.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::utils::ArgumentSplitter;
+ ///
+ /// let args = r#"cmd --flag "value with spaces""#.split_args();
+ /// assert_eq!(args, vec!["cmd", "--flag", "value with spaces"]);
+ /// ```
+ ///
+ /// Escaped characters are unescaped:
+ ///
+ /// ```
+ /// use mingling_core::utils::ArgumentSplitter;
+ ///
+ /// let args = r#"echo a\ b"#.split_args();
+ /// assert_eq!(args, vec!["echo", "a b"]);
+ /// ```
+ fn split_args(self) -> Vec<String> {
+ split_args(self.as_ref())
+ }
}
/// Splits a string input into arguments, respecting single quotes, double quotes,
/// and backslash escaping.
-pub fn split_input(input: &str) -> Vec<String> {
+fn split_args(input: &str) -> Vec<String> {
let mut result: Vec<String> = Vec::new();
let mut current = String::new();
let mut chars = input.chars();
@@ -63,72 +107,72 @@ pub fn split_input(input: &str) -> Vec<String> {
#[cfg(test)]
mod splitter_tests {
- use crate::program::repl_exec::splitter::split_input;
+ use crate::utils::splitter::split_args;
#[test]
fn test_split_with_double_quotes() {
let input = r#"a "b c" d"#;
- let result = split_input(input);
+ let result = split_args(input);
assert_eq!(result, vec!["a", "b c", "d"]);
}
#[test]
fn test_split_with_single_quotes() {
let input = "a 'b c' d";
- let result = split_input(input);
+ let result = split_args(input);
assert_eq!(result, vec!["a", "b c", "d"]);
}
#[test]
fn test_empty_input() {
- assert!(split_input("").is_empty());
+ assert!(split_args("").is_empty());
}
#[test]
fn test_no_quotes() {
- let result = split_input("hello world");
+ let result = split_args("hello world");
assert_eq!(result, vec!["hello", "world"]);
}
#[test]
fn test_double_quotes_at_edges() {
- let result = split_input(r#""hello world" foo"#);
+ let result = split_args(r#""hello world" foo"#);
assert_eq!(result, vec!["hello world", "foo"]);
}
#[test]
fn test_single_quotes_at_edges() {
- let result = split_input("'hello world' foo");
+ let result = split_args("'hello world' foo");
assert_eq!(result, vec!["hello world", "foo"]);
}
#[test]
fn test_multiple_double_quoted_parts() {
- let result = split_input(r#"a "b c" d "e f g""#);
+ let result = split_args(r#"a "b c" d "e f g""#);
assert_eq!(result, vec!["a", "b c", "d", "e f g"]);
}
#[test]
fn test_multiple_single_quoted_parts() {
- let result = split_input("a 'b c' d 'e f g'");
+ let result = split_args("a 'b c' d 'e f g'");
assert_eq!(result, vec!["a", "b c", "d", "e f g"]);
}
#[test]
fn test_backslash_escaped_space() {
- let result = split_input("a b\\ c d");
+ let result = split_args("a b\\ c d");
assert_eq!(result, vec!["a", "b c", "d"]);
}
#[test]
fn test_backslash_escaped_double_quote() {
- let result = split_input(r#"a b\"c d"#);
+ let result = split_args(r#"a b\"c d"#);
assert_eq!(result, vec!["a", r#"b"c"#, "d"]);
}
#[test]
fn test_backslash_escaped_single_quote() {
- let result = split_input("a b\\'c d");
+ let result = split_args("a b\\'c d");
assert_eq!(result, vec!["a", "b'c", "d"]);
}
}