diff options
| -rw-r--r-- | CHANGELOG.md | 162 | ||||
| -rw-r--r-- | examples/example-structural-renderer/Cargo.lock | 26 | ||||
| -rw-r--r-- | mingling/src/res.rs | 15 | ||||
| -rw-r--r-- | mingling/src/res/confirmer.rs | 268 | ||||
| -rw-r--r-- | mingling/src/res/osc94.rs | 264 | ||||
| -rw-r--r-- | mingling/src/res/osc94/state.rs | 74 | ||||
| -rw-r--r-- | mingling/src/setups.rs | 34 | ||||
| -rw-r--r-- | mingling/src/setups/confirmer.rs | 60 | ||||
| -rw-r--r-- | mingling/src/setups/osc94.rs | 89 | ||||
| -rw-r--r-- | mingling/src/setups/stdin_args.rs | 82 | ||||
| -rw-r--r-- | mingling_core/src/lib.rs | 3 | ||||
| -rw-r--r-- | mingling_core/src/program/exec.rs | 9 | ||||
| -rw-r--r-- | mingling_core/src/program/hook.rs | 31 | ||||
| -rw-r--r-- | mingling_core/src/program/hook/hook_info.rs | 5 | ||||
| -rw-r--r-- | mingling_core/src/program/once_exec.rs | 14 | ||||
| -rw-r--r-- | mingling_core/src/program/repl_exec.rs | 14 | ||||
| -rw-r--r-- | mingling_core/src/utils.rs | 3 | ||||
| -rw-r--r-- | mingling_core/src/utils/splitter.rs (renamed from mingling_core/src/program/repl_exec/splitter.rs) | 78 |
18 files changed, 1169 insertions, 62 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 713e7a5..9029c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -352,6 +352,133 @@ None `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")]`. @@ -462,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/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/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"]); } } |
