aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/version-files.toml18
-rw-r--r--.github/workflows/ci-check-only.yml2
-rw-r--r--.github/workflows/ci.yml65
-rw-r--r--.run/src/bin/cov-test.rs8
-rw-r--r--CHANGELOG.md114
-rw-r--r--Cargo.lock10
-rw-r--r--Cargo.toml2
-rw-r--r--README.md9
-rw-r--r--dist/index.html1131
-rw-r--r--docs/_zh_CN/pages/1-getting-started.md4
-rw-r--r--docs/dev/_sidebar.md10
-rw-r--r--docs/dev/pages/issues/0.5.0-roadmap.md138
-rw-r--r--docs/dev/pages/issues/t0_generalize-repl-system.md36
-rw-r--r--docs/dev/pages/issues/t0_remove-pack-macros.md49
-rw-r--r--docs/dev/pages/issues/t0_remove-parser-feature.md36
-rw-r--r--docs/dev/pages/issues/t0_remove-with-dispatcher.md35
-rw-r--r--docs/dev/pages/issues/t1_completion-higher-level-abstractions.md38
-rw-r--r--docs/dev/pages/issues/t1_modify-dispatcher-syntax.md44
-rw-r--r--docs/dev/pages/issues/t1_move-structural-renderer.md38
-rw-r--r--docs/dev/pages/issues/t1_pathf-export-macro.md53
-rw-r--r--docs/dev/pages/issues/t2_automated-dispatch-tree-optimization.md57
-rw-r--r--docs/example-viewer.html263
-rw-r--r--docs/examples.html140
-rw-r--r--docs/pages/1-getting-started.md4
-rw-r--r--docs/res/guide.txt2
-rw-r--r--docs/scripts/display-parse.js269
-rw-r--r--examples/example-argument-parse/Cargo.lock6
-rw-r--r--examples/example-argument-picker/Cargo.lock6
-rw-r--r--examples/example-async-support/Cargo.lock6
-rw-r--r--examples/example-basic/Cargo.lock6
-rw-r--r--examples/example-clap-binding/Cargo.lock6
-rw-r--r--examples/example-combine-pathf-dispatch-tree/Cargo.lock8
-rw-r--r--examples/example-combine-pathf-metadata/Cargo.lock8
-rw-r--r--examples/example-command-macro/Cargo.lock6
-rw-r--r--examples/example-completion/Cargo.lock6
-rw-r--r--examples/example-custom-pickable/Cargo.lock6
-rw-r--r--examples/example-dispatch-tree/Cargo.lock6
-rw-r--r--examples/example-enum-tag/Cargo.lock6
-rw-r--r--examples/example-error-handling/Cargo.lock6
-rw-r--r--examples/example-exitcode/Cargo.lock6
-rw-r--r--examples/example-help/Cargo.lock6
-rw-r--r--examples/example-hook/Cargo.lock6
-rw-r--r--examples/example-implicit-dispatcher/Cargo.lock6
-rw-r--r--examples/example-lazy-resources/Cargo.lock6
-rw-r--r--examples/example-metadata/Cargo.lock6
-rw-r--r--examples/example-outside-type/Cargo.lock6
-rw-r--r--examples/example-pack-err/Cargo.lock6
-rw-r--r--examples/example-panic-unwind/Cargo.lock6
-rw-r--r--examples/example-pathfinder/Cargo.lock8
-rw-r--r--examples/example-repl-basic/Cargo.lock6
-rw-r--r--examples/example-resources/Cargo.lock6
-rw-r--r--examples/example-setup/Cargo.lock6
-rw-r--r--examples/example-structural-renderer/Cargo.lock6
-rw-r--r--examples/example-unit-test/Cargo.lock6
-rw-r--r--examples/full-todolist/Cargo.lock6
-rw-r--r--index.html1366
-rw-r--r--mingling/src/confirm.rs5
-rw-r--r--mingling/src/confirm/count.rs75
-rw-r--r--mingling/src/confirm/predicate.rs60
-rw-r--r--mingling/src/lib.rs6
-rw-r--r--mingling/src/osc94.rs5
-rw-r--r--mingling/src/osc94/guard.rs203
-rw-r--r--mingling/src/osc94/state.rs243
-rw-r--r--mingling/src/res.rs7
-rw-r--r--mingling/src/res/confirm.rs184
-rw-r--r--mingling/src/res/confirmer.rs268
-rw-r--r--mingling/src/res/osc94.rs69
-rw-r--r--mingling/src/setups.rs7
-rw-r--r--mingling/src/setups/confirm.rs (renamed from mingling/src/setups/confirmer.rs)22
-rw-r--r--mingling/src/setups/osc94.rs89
-rw-r--r--mingling_cli/.gitignore2
-rw-r--r--mingling_cli/Cargo.lock1193
-rw-r--r--mingling_cli/Cargo.toml7
-rw-r--r--mingling_cli/help/help.txt3
-rw-r--r--mingling_cli/pre/lint_registry.rs8
-rw-r--r--mingling_cli/src/bin/wrapper.rs115
-rw-r--r--mingling_cli/src/lib.rs28
-rw-r--r--mingling_cli/src/linter/registry.rs5
-rw-r--r--mingling_cli/src/pkg_mgr/cmd_install.rs86
-rw-r--r--mingling_cli/src/updater.rs1
-rw-r--r--mingling_cli/src/updater/cmd_update.rs266
-rw-r--r--mingling_core/tests/test-all/Cargo.lock6
-rw-r--r--mingling_core/tests/test-basic/Cargo.lock6
-rw-r--r--mingling_core/tests/test-comp/Cargo.lock6
-rw-r--r--mingling_core/tests/test-dispatch-tree/Cargo.lock6
-rw-r--r--mingling_core/tests/test-repl/Cargo.lock6
-rw-r--r--mingling_core/tests/test-structural-renderer/Cargo.lock6
-rw-r--r--mingling_pathf/test/Cargo.lock2
88 files changed, 6354 insertions, 762 deletions
diff --git a/.config/version-files.toml b/.config/version-files.toml
index 4694f05..30fda5e 100644
--- a/.config/version-files.toml
+++ b/.config/version-files.toml
@@ -11,10 +11,6 @@ file = "./README.md"
pattern = "version = \"{VER}\""
[[file]]
-file = "./mingling/README.md"
-pattern = "version = \"{VER}\""
-
-[[file]]
file = "./docs/_zh_CN/pages/1-getting-started.md"
pattern = "version = \"{VER}\""
@@ -27,5 +23,17 @@ file = "./docs/res/guide.txt"
pattern = "mingling = \"{VER}\""
[[file]]
-file = "./mingling_core/src/docs/build.md"
+file = "./index.html"
+pattern = "cargo add mingling@{VER}"
+
+[[file]]
+file = "./index.html"
pattern = "version = \"{VER}\""
+
+[[file]]
+file = "./index.html"
+pattern = "mling proj-init {VER}@basic"
+
+[[file]]
+file = "./dist/index.html"
+pattern = "mling proj-init {VER}@basic"
diff --git a/.github/workflows/ci-check-only.yml b/.github/workflows/ci-check-only.yml
index b92614a..42c2e35 100644
--- a/.github/workflows/ci-check-only.yml
+++ b/.github/workflows/ci-check-only.yml
@@ -24,7 +24,7 @@ jobs:
]
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo ci --check-${{ matrix.item }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 020bf97..4ef1bbb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,7 +26,7 @@ jobs:
]
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo ci --check-${{ matrix.item }}
@@ -38,12 +38,63 @@ jobs:
name: docs-test-result-${{ matrix.os }}.md
path: .temp/DOCS-TEST-RESULT.md
+ # Build the distributable packages in parallel with the checks, so that the
+ # `unreleased` tag and the Pages deployment always publish fresh artifacts.
+ Build:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions-rust-lang/setup-rust-toolchain@v1
+
+ - name: Compute package metadata
+ id: metadata
+ shell: bash
+ run: |
+ case "$RUNNER_OS" in
+ Windows) os="win" ;;
+ Linux) os="linux" ;;
+ macOS) os="mac" ;;
+ esac
+ echo "os=$os" >> "$GITHUB_OUTPUT"
+
+ - name: Install mling
+ shell: bash
+ run: |
+ if [ "$RUNNER_OS" = "Windows" ]; then
+ powershell -ExecutionPolicy Bypass -File .run/src/bin/install-mling.ps1
+ else
+ bash .run/src/bin/install-mling.sh
+ fi
+
+ - name: Package mling
+ shell: bash
+ run: |
+ mkdir -p dist
+ name="mling-${{ steps.metadata.outputs.os }}.tar.gz"
+ tar -czf "dist/$name" -C .temp/mling .
+ if command -v sha256sum >/dev/null 2>&1; then
+ (cd dist && sha256sum "$name" > "$name.sha256")
+ else
+ (cd dist && shasum -a 256 "$name" > "$name.sha256")
+ fi
+
+ - name: Upload mling package
+ uses: actions/upload-artifact@v4
+ with:
+ name: mling-${{ steps.metadata.outputs.os }}-pkg
+ path: dist/mling-*
+ if-no-files-found: error
+
Move-Unreleased-Tag:
if: github.ref == 'refs/heads/main'
- needs: [Check]
+ needs: [Check, Build]
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
- run: |
@@ -62,7 +113,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install nightly toolchain
@@ -80,6 +131,12 @@ jobs:
- name: Delete .temp directory before deployment
run: rm -rf .temp
+ - name: Download mling packages
+ uses: actions/download-artifact@v4
+ with:
+ path: dist
+ merge-multiple: true
+
- name: Setup Pages
uses: actions/configure-pages@v5
diff --git a/.run/src/bin/cov-test.rs b/.run/src/bin/cov-test.rs
index ff2c3fc..f62ff01 100644
--- a/.run/src/bin/cov-test.rs
+++ b/.run/src/bin/cov-test.rs
@@ -9,8 +9,12 @@
//!
//! - `--object <PATH>`: include arbitrary binaries in the report
//! (upstream issue taiki-e/cargo-llvm-cov#367)
-//! - `--include-tests-examples-benches`: stop filtering those source dirs
-//! (upstream issue taiki-e/cargo-llvm-cov#503)
+//! - `--include-examples`: stop filtering source files under the
+//! `examples` directory (upstream issue taiki-e/cargo-llvm-cov#503)
+//!
+//! The script itself does not use `--include-examples`; it passes
+//! `--no-default-ignore-filename-regex` and supplies its own filter so that
+//! `tests`/`benches` directories stay in the report too.
//!
//! Install it with:
//!
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e663276..449e3a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,7 +11,8 @@ Any contributor making changes to the project must record their changes in this
**- Milestone.1 "MVP" -**
- [Unreleased](#unreleased)
-- [Release 0.4.0 (Unreleased)](#release-040-unreleased)
+- [Release 0.5.0 (Unreleased)](#release-050-unreleased)
+- [Release 0.4.0 (2026-08-16)](#release-040-2026-08-16)
- [Release 0.3.0 (2026-07-27)](#release-030-2026-07-27)
- [Release 0.2.2 (2026-07-10)](#release-022-2026-07-10)
- [Release 0.2.1 (2026-07-01)](#release-021-2026-07-01)
@@ -53,7 +54,29 @@ None
## Contents
-### 0.4.0 (Unreleased)
+### 0.5.0 (Unreleased)
+
+#### Fixes:
+
+None
+
+#### Optimizations:
+
+None
+
+#### Features:
+
+None
+
+#### **BREAKING CHANGES** (API CHANGES):
+
+None
+
+---
+
+## Contents
+
+### 0.4.0 (2026-08-16)
#### Fixes:
@@ -377,7 +400,7 @@ None
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:
+14. **[`confirm`]** Added the `Confirmer` resource and its confirmation predicates:
- **`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.
@@ -387,22 +410,22 @@ None
- **`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)`.
+ - **`mingling::confirm::ConfirmerCount`** — 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).
+ - **`mingling::confirm::ConfirmerPredicate`** — 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.
+ - **`mingling::confirm::YesConfirm`** — 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.
+ - **`mingling::confirm::TrueConfirm`** — Accepts `"true"`/`"t"` as yes and `"false"`/`"f"` as no. Case-insensitive with leading/trailing whitespace trimming.
- Derives `Debug`, `Default`, `Clone`, `Copy`.
+ The `Confirmer` resource derives `Debug`, `Default`, `Clone`, `Copy`. The `confirm` module also houses `ConfirmerCount`, `ConfirmerPredicate`, `YesConfirm`, and `TrueConfirm` at `mingling::confirm::*`.
-15. **[`setups`]** Added the `ConfirmerSetup` and `StandardInputArgsSetup` program setups:
+15. **[`setups`]** Added the `ConfirmSetup` 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.
+ ### `ConfirmSetup`
+ - **`mingling::setup::ConfirmSetup`** — A `ProgramSetup` that registers a `ResConfirm` 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 `ResConfirm` as confirmed via `modify_res`, so all `ask`/`try_ask` calls return `true` without prompting.
- - Registered via `program.with_setup(ConfirmerSetup)`.
+ - Registered via `program.with_setup(ConfirmSetup)`.
- Applies uniformly to all subcommands of the entire program; it does not support per-command overrides.
### `StandardInputArgsSetup`
@@ -412,6 +435,73 @@ None
- Empty input produces no arguments.
- **Note:** the setup does **not** validate input — stdin content is treated as trusted arguments appended directly, so untrusted input can inject arbitrary arguments. It also has no per-subcommand granularity; if different subcommands need different stdin behavior, do not use this setup.
+16. **[`osc94`]** **[`setups:osc94`]** Added the `ResOSC94` resource and `OSC94Setup` for managing terminal `OSC 9;4` protocol status:
+
+ ### `ResOSC94` resource
+ - **`mingling::res::ResOSC94`** — 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.
+
+ - **`ResOSC94::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::osc94::OSC94Guard`** — A guard for modifying process state, obtained via [`ResOSC94::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::osc94::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 `ResOSC94` 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::ResOSC94, setup::OSC94Setup};
+
+ fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(OSC94Setup);
+ program.exec_and_exit();
+ }
+
+ #[command]
+ fn hello(osc: &ResOSC94) {
+ let mut guard = osc.get_mut();
+ guard.set_progress(0.5);
+ // ... do work ...
+ guard.set_warn_state();
+ // ... guard is dropped, state automatically restored to Clean
+ }
+ ```
+
#### **BREAKING CHANGES** (API CHANGES):
1. **[`macros`]** **[BREAKING]** Renamed the `extra_macros` feature to `extras`. All feature-gated macro re-exports in `mingling/src/lib.rs` (and throughout the codebase) have been updated from `#[cfg(feature = "extra_macros")]` to `#[cfg(feature = "extras")]`.
diff --git a/Cargo.lock b/Cargo.lock
index 0abcd25..74611d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -270,7 +270,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"arg-picker",
"mingling",
@@ -283,11 +283,11 @@ dependencies = [
[[package]]
name = "mingling-workspace"
-version = "0.4.0"
+version = "0.5.0"
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"env_logger",
"just_fmt 0.2.0",
@@ -305,7 +305,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"proc-macro2",
@@ -315,7 +315,7 @@ dependencies = [
[[package]]
name = "mingling_pathf"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"proc-macro2",
diff --git a/Cargo.toml b/Cargo.toml
index 259095c..6b0e6b3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -61,7 +61,7 @@ log = "0.4.33"
env_logger = "0.11.11"
[workspace.package]
-version = "0.4.0"
+version = "0.5.0"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/mingling-rs/mingling"
diff --git a/README.md b/README.md
index 4310891..480510c 100644
--- a/README.md
+++ b/README.md
@@ -99,7 +99,7 @@ Add Mingling to your `Cargo.toml`:
```toml
[dependencies.mingling]
-version = "0.4.0"
+version = "0.5.0"
features = []
```
@@ -112,8 +112,10 @@ tag = "unreleased"
features = []
```
-> [!NOTE]
-> To learn more, check out [Writing with Mingling](https://github.com/mingling-rs/mingling/blob/main/GETTING-STARTED.md)
+To learn more, check out [Writing with Mingling](https://github.com/mingling-rs/mingling/blob/main/GETTING-STARTED.md)
+
+> [!Note]
+> You can also use the `mling` scaffolding tool to build, check, and manage your project [Download](https://mingling-rs.github.io/mingling/dist) | [About](https://github.com/mingling-rs/mingling/tree/main/mingling_cli)
## Roadmap
@@ -160,6 +162,7 @@ See [LICENSE-MIT](LICENSE-MIT) or [LICENSE-APACHE](LICENSE-APACHE) file for deta
**To learn more, check out the following links:**
+- ⚡ Mingling CLI - [About Mling](https://mingling-rs.github.io/mingling/dist)
- 📦 Repo - [Github](https://github.com/mingling-rs/mingling) | [Gitee](https://gitee.com/mingling-rs/mingling) | [Origin](https://catilgrass.cn/mingling.git)
- 🚪 Mainpage - [Github](https://mingling-rs.github.io/mingling/) | [crates.io](https://crates.io/crates/mingling)
- 💡 Examples - [Github](https://mingling-rs.github.io/mingling/docs/examples.html)
diff --git a/dist/index.html b/dist/index.html
new file mode 100644
index 0000000..905e90a
--- /dev/null
+++ b/dist/index.html
@@ -0,0 +1,1131 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Mling CLI — Download</title>
+ <link
+ rel="icon"
+ type="image/png"
+ href="../docs/res/favicon_small.png"
+ />
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+ <link
+ href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap"
+ rel="stylesheet"
+ />
+ <link
+ rel="stylesheet"
+ href="../docs/scripts/highlight/github-dark.min.css"
+ />
+ <style>
+ *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ }
+
+ html {
+ scroll-behavior: smooth;
+ scroll-snap-type: y mandatory;
+ }
+
+ .hero,
+ .section {
+ scroll-snap-align: start;
+ scroll-snap-stop: always;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ body {
+ font-family: "Noto Serif SC", Georgia, "Times New Roman", serif;
+ background-color: #1a1410;
+ color: #e8ddd0;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ overflow-x: hidden;
+ }
+
+ a {
+ color: #d4a84b;
+ text-decoration: none;
+ transition: color 0.2s;
+ }
+ a:hover {
+ color: #e8c46a;
+ }
+
+ .container {
+ max-width: 1080px;
+ margin: 0 auto;
+ padding: 0 1.5rem;
+ }
+
+ nav {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.8rem 2rem;
+ background: rgba(26, 20, 16, 0.88);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border-bottom: 1px solid rgba(212, 168, 75, 0.12);
+ }
+
+ nav .logo {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 700;
+ font-size: 1.1rem;
+ color: #e8ddd0;
+ }
+
+ nav .logo img {
+ width: 28px;
+ height: 28px;
+ filter: brightness(0) invert(1);
+ }
+
+ nav .nav-links {
+ display: flex;
+ gap: 1.5rem;
+ align-items: center;
+ font-size: 0.9rem;
+ }
+
+ nav .nav-links a {
+ color: #9a8a7a;
+ transition: color 0.2s;
+ }
+
+ nav .nav-links a:hover {
+ color: #d4a84b;
+ }
+
+ nav .nav-links .btn-nav {
+ display: inline-block;
+ padding: 0.35rem 1rem;
+ border: 1px solid #d4a84b;
+ border-radius: 20px;
+ color: #d4a84b;
+ font-weight: 600;
+ font-size: 0.85rem;
+ transition:
+ background 0.2s,
+ color 0.2s;
+ }
+
+ nav .nav-links .btn-nav:hover {
+ background: #d4a84b;
+ color: #1a1410;
+ }
+
+ .hero {
+ padding: 5rem 1.5rem 3rem;
+ text-align: center;
+ position: relative;
+ }
+
+ .hero h1,
+ .hero .subtitle {
+ position: relative;
+ z-index: 1;
+ }
+
+ /* Tiled grid background (field grid) */
+ .hero::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background-image:
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ );
+ background-size:
+ 40px 40px,
+ 40px 40px,
+ 20px 20px,
+ 20px 20px;
+ background-position:
+ calc(50% - 540px + 20px) 0,
+ calc(50% - 540px) 0,
+ calc(50% - 540px + 10px) 0,
+ calc(50% - 540px + 10px) 0;
+ pointer-events: none;
+ z-index: 0;
+ }
+
+ .hero h1 {
+ font-size: 5.5rem;
+ font-weight: 700;
+ color: #e8ddd0;
+ margin-bottom: 0.75rem;
+ }
+
+ .hero .subtitle {
+ font-size: 1.1rem;
+ color: #9a8a7a;
+ max-width: 640px;
+ margin: 0 auto 2.5rem;
+ line-height: 1.8;
+ text-align: left;
+ }
+
+ .hero .subtitle .star {
+ color: #c43931;
+ margin-right: 0.5rem;
+ }
+
+ .hero .subtitle b {
+ color: #e8ddd0;
+ font-weight: 700;
+ }
+
+ .hero .subtitle code {
+ font-family: "JetBrains Mono", "Noto Serif SC", monospace;
+ font-size: 0.9em;
+ color: #d4a84b;
+ background: rgba(212, 168, 75, 0.08);
+ padding: 0.1rem 0.35rem;
+ border-radius: 2px;
+ }
+
+ /* Hero command carousel terminal */
+ .hero-term {
+ width: 520px;
+ max-width: 100%;
+ margin: 0 auto;
+ background: #0f0b08;
+ border: 1px solid #3a2e24;
+ border-radius: 2.5px;
+ padding: 0.65rem 1.1rem;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
+ text-align: left;
+ font-family: "JetBrains Mono", "Noto Serif SC", monospace;
+ font-size: 0.95rem;
+ line-height: 1.6;
+ white-space: nowrap;
+ overflow: hidden;
+ position: relative;
+ z-index: 1;
+ }
+
+ .hero-term pre {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ }
+
+ .hero-term code {
+ font-family: "JetBrains Mono", "Noto Serif SC", monospace;
+ background: transparent !important;
+ padding: 0 !important;
+ }
+
+ .term-prompt {
+ color: #6a5a4a;
+ }
+
+ .term-cmd {
+ color: #7ec699;
+ }
+
+ .term-cursor {
+ display: inline-block;
+ width: 0.6em;
+ height: 1.15em;
+ background: #d0d0d0;
+ vertical-align: text-bottom;
+ margin-left: 1px;
+ animation: termBlink 1s steps(1) infinite;
+ }
+
+ @keyframes termBlink {
+ 50% {
+ opacity: 0;
+ }
+ }
+
+ .download-row {
+ display: flex;
+ gap: 1.25rem;
+ justify-content: center;
+ flex-wrap: wrap;
+ }
+
+ .download-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.4rem;
+ min-width: 220px;
+ padding: 1.75rem 1.5rem;
+ background: #241c16;
+ border: 1px solid #3a2e24;
+ border-radius: 2px;
+ color: #e8ddd0;
+ transition:
+ border-color 0.25s,
+ transform 0.2s;
+ }
+
+ .download-card:hover {
+ border-color: #d4a84b;
+ color: #e8ddd0;
+ transform: translateY(-4px);
+ }
+
+ .download-card .platform {
+ width: 44px;
+ height: 44px;
+ margin-bottom: 0.25rem;
+ }
+
+ .download-card .platform.invert {
+ filter: brightness(0) invert(1);
+ }
+
+ .download-card .os-name {
+ font-size: 1.05rem;
+ font-weight: 700;
+ }
+
+ .download-card .file-name {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+ width: 100%;
+ padding: 0.55rem 0.9rem;
+ background: rgba(212, 168, 75, 0.1);
+ border: 1px solid rgba(212, 168, 75, 0.45);
+ border-radius: 2px;
+ color: #d4a84b;
+ font-family: "JetBrains Mono", monospace;
+ font-size: 0.78rem;
+ text-decoration: none;
+ transition:
+ background 0.2s,
+ color 0.2s,
+ border-color 0.2s;
+ }
+
+ .download-card .file-name:hover {
+ background: #d4a84b;
+ color: #1a1410;
+ border-color: #d4a84b;
+ }
+
+ .download-card .file-name .btn-arrow {
+ flex-shrink: 0;
+ font-size: 0.9rem;
+ }
+
+ .download-card .checksum {
+ font-size: 0.78rem;
+ color: #7a6a5a;
+ }
+
+ .download-card .checksum a {
+ color: #7a6a5a;
+ text-decoration: underline;
+ }
+
+ .download-card .checksum a:hover {
+ color: #d4a84b;
+ }
+
+ .checksum-note {
+ margin: 1.25rem auto 0;
+ width: fit-content;
+ font-size: 0.8rem;
+ color: #6a5a4a;
+ text-align: center;
+ }
+
+ .section {
+ padding: 3.5rem 1.5rem;
+ border-top: 1px solid #2a1e14;
+ }
+
+ .section.alt {
+ background-color: #241c16;
+ }
+
+ .section h2 {
+ font-size: 1.6rem;
+ font-weight: 700;
+ color: #e8ddd0;
+ margin-bottom: 1rem;
+ }
+
+ .section h2::before {
+ content: "✦";
+ color: #c43931;
+ margin-right: 0.5rem;
+ }
+
+ .section h2 span {
+ color: #c43931;
+ }
+
+ .section h3 {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: #e8ddd0;
+ margin: 0 0 0.75rem;
+ }
+
+ .section h3::before {
+ content: "✦";
+ color: #c43931;
+ margin-right: 0.5rem;
+ }
+
+ .section h3:not(:first-of-type) {
+ margin-top: 2rem;
+ }
+
+ .section p {
+ color: #9a8a7a;
+ margin-bottom: 1.25rem;
+ max-width: 720px;
+ }
+
+ /* Dashed lines along the container edges, excluding hero */
+ body > section:not(.hero) {
+ position: relative;
+ background-image:
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ );
+ background-size:
+ 40px 40px,
+ 40px 40px,
+ 20px 20px,
+ 20px 20px;
+ background-position:
+ calc(50% - 540px + 20px) 0,
+ calc(50% - 540px) 0,
+ calc(50% - 540px + 10px) 0,
+ calc(50% - 540px + 10px) 0;
+ }
+
+ body > section:not(.hero)::before,
+ body > section:not(.hero)::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: repeating-linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.25) 0,
+ rgba(212, 168, 75, 0.25) 4px,
+ transparent 4px,
+ transparent 9px
+ );
+ pointer-events: none;
+ z-index: 49;
+ }
+
+ body > section:not(.hero)::before {
+ left: max(0px, calc(50% - 540px));
+ }
+
+ body > section:not(.hero)::after {
+ right: max(0px, calc(50% - 540px));
+ }
+
+ /* Diamond markers at line intersections */
+ .line-diamond {
+ position: absolute;
+ top: -4px;
+ width: 8px;
+ height: 8px;
+ background: rgba(212, 168, 75, 0.25);
+ transform: rotate(45deg);
+ pointer-events: none;
+ z-index: 50;
+ }
+
+ .line-diamond.left {
+ left: calc(max(0px, calc(50% - 540px)) - 4px);
+ }
+
+ .line-diamond.right {
+ right: calc(max(0px, calc(50% - 540px)) - 4px);
+ }
+
+ /* Horizontal dashed dividers between pages */
+ body > section + section,
+ body > footer {
+ border-top: 1px solid rgba(212, 168, 75, 0.25);
+ border-image: repeating-linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.25) 0 4px,
+ transparent 4px 9px
+ )
+ 1;
+ }
+
+ .code-block {
+ background: #1a1410;
+ border: 1px solid #3a2e24;
+ border-radius: 2px;
+ padding: 1.5rem 1.5rem 1.5rem;
+ overflow-x: auto;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ margin-bottom: 1.5rem;
+ }
+
+ .code-block pre {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ position: relative;
+ }
+
+ .code-line-highlight {
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 28px;
+ background: rgba(255, 255, 255, 0.1);
+ border-left: 2px solid rgba(255, 255, 255, 0.45);
+ pointer-events: none;
+ opacity: 0;
+ transition:
+ opacity 0.15s ease,
+ top 0.08s ease;
+ z-index: 3;
+ }
+
+ .code-block code {
+ font-family: "JetBrains Mono", "Noto Serif SC", monospace;
+ font-variant-ligatures: contextual;
+ font-size: 0.85rem;
+ line-height: 1.65;
+ color: #c0b0a0;
+ background: transparent !important;
+ padding: 0 !important;
+ }
+
+ .code-block code .cm {
+ color: #6a5a4a;
+ }
+
+ /* Shell simulator for help output */
+ .shell-term {
+ background: #0f0b08;
+ border: 1px solid #3a2e24;
+ border-radius: 2.5px;
+ padding: 1.5rem 1.75rem;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
+ overflow-x: auto;
+ overflow-y: auto;
+ max-width: 960px;
+ max-height: 480px;
+ margin: 0 auto;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ }
+
+ .shell-term::-webkit-scrollbar {
+ display: none;
+ }
+
+ .shell-term pre {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ position: relative;
+ }
+
+ .shell-term code {
+ font-family: "JetBrains Mono", "Noto Serif SC", monospace;
+ font-size: 0.85rem;
+ line-height: 1.7;
+ color: #c0b0a0;
+ white-space: pre;
+ display: block;
+ background: transparent !important;
+ padding: 0 !important;
+ }
+
+ .shell-term b {
+ font-weight: 700;
+ color: #e8ddd0;
+ }
+
+ .shell-term i {
+ font-style: italic;
+ }
+
+ .shell-term u {
+ text-decoration: underline;
+ }
+
+ .shell-term .t-cyan {
+ color: #79c0ff;
+ }
+
+ .shell-term .t-green {
+ color: #7ee787;
+ }
+
+ .shell-term .t-heading {
+ background: #e8ddd0;
+ color: #1a1410;
+ font-weight: 700;
+ padding: 0 0.25rem;
+ }
+
+ .shell-term .t-quote {
+ background: #6a5a4a;
+ color: #6a5a4a;
+ }
+
+ footer {
+ padding: 2rem 1.5rem;
+ text-align: center;
+ color: #6a5a4a;
+ font-size: 0.85rem;
+ border-top: 1px solid #2a1e14;
+ }
+
+ footer a {
+ color: #7a6a5a;
+ }
+
+ footer a:hover {
+ color: #d4a84b;
+ }
+
+ @media (max-width: 768px) {
+ html {
+ scroll-snap-type: none;
+ }
+
+ .hero,
+ .section {
+ min-height: auto;
+ }
+
+ nav {
+ padding: 0.8rem 1rem;
+ }
+
+ nav .nav-links {
+ gap: 0.8rem;
+ }
+
+ .hero h1 {
+ font-size: 2rem;
+ }
+
+ .download-card {
+ min-width: 100%;
+ }
+ }
+
+ /* Cursor glow dot */
+ .cursor-dot {
+ position: fixed;
+ pointer-events: none;
+ border-radius: 50%;
+ width: 400px;
+ height: 400px;
+ background: radial-gradient(
+ circle,
+ rgba(212, 168, 75, 0.06) 0%,
+ rgba(212, 168, 75, 0.02) 45%,
+ transparent 70%
+ );
+ transform: translate(-50%, -50%);
+ z-index: 10000;
+ opacity: 0;
+ transition:
+ opacity 0.3s ease,
+ left 0.12s ease-out,
+ top 0.12s ease-out;
+ }
+
+ /* Ink drop effect */
+ .ink-drop {
+ position: fixed;
+ pointer-events: none;
+ border-radius: 50%;
+ background: radial-gradient(
+ circle,
+ rgba(212, 168, 75, 0.15) 0%,
+ rgba(196, 57, 49, 0.06) 50%,
+ transparent 100%
+ );
+ width: 0;
+ height: 0;
+ transform: translate(-50%, -50%);
+ animation: inkSpread 0.9s ease-out forwards;
+ z-index: 9999;
+ }
+
+ @keyframes inkSpread {
+ 0% {
+ width: 0;
+ height: 0;
+ opacity: 1;
+ }
+ 100% {
+ width: 500px;
+ height: 500px;
+ opacity: 0;
+ }
+ }
+ </style>
+ </head>
+
+ <body>
+ <nav>
+ <a href="../" class="logo">
+ <img
+ src="../docs/res/icon.png"
+ alt="Mingling icon"
+ width="28"
+ height="28"
+ />
+ Mìng Lìng
+ </a>
+ <div class="nav-links">
+ <a
+ href="https://github.com/mingling-rs/mingling/tree/main/mingling_cli"
+ target="_blank"
+ >GitHub</a
+ >
+ <a
+ href="https://crates.io/crates/mingling-cli"
+ target="_blank"
+ class="btn-nav"
+ >cargo install mingling-cli</a
+ >
+ </div>
+ </nav>
+
+ <section class="hero">
+ <h1>Mingling CLI</h1>
+ <p class="subtitle">
+ <span class="star">✦</span> <b>Scaffold</b> — generate projects
+ with <code>proj-init</code><br />
+ <span class="star">✦</span> <b>Manage</b> — install, enable, and
+ inspect packages<br />
+ <span class="star">✦</span> <b>Update</b> — <code>mling</code>
+ keeps itself fresh
+ </p>
+ <div class="hero-term">
+ <pre><code class="no-highlight term-hero"></code></pre>
+ </div>
+ </section>
+
+ <section class="section alt">
+ <div class="container">
+ <h2>How to use</h2>
+ <div class="shell-term">
+ <pre><code class="no-highlight" id="shell-help"></code></pre>
+ </div>
+ </div>
+ </section>
+
+ <section class="section">
+ <div class="container">
+ <h2>Download</h2>
+ <p>
+ Choose the package for your platform and download the latest
+ build.
+ </p>
+ <div class="download-row">
+ <div class="download-card">
+ <svg
+ class="platform invert"
+ viewBox="0 0 128 128"
+ xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M113.823 104.595c-1.795-1.478-3.629-2.921-5.308-4.525-1.87-1.785-3.045-3.944-2.789-6.678.147-1.573-.216-2.926-2.113-3.452.446-1.154.864-1.928 1.033-2.753.188-.92.178-1.887.204-2.834.264-9.96-3.334-18.691-8.663-26.835-2.454-3.748-5.017-7.429-7.633-11.066-4.092-5.688-5.559-12.078-5.633-18.981a47.564 47.564 0 00-1.081-9.475C80.527 11.956 77.291 7.233 71.422 4.7c-4.497-1.942-9.152-2.327-13.901-1.084-6.901 1.805-11.074 6.934-10.996 14.088.074 6.885.417 13.779.922 20.648.288 3.893-.312 7.252-2.895 10.34-2.484 2.969-4.706 6.172-6.858 9.397-1.229 1.844-2.317 3.853-3.077 5.931-2.07 5.663-3.973 11.373-7.276 16.5-1.224 1.9-1.363 4.026-.494 6.199.225.563.363 1.429.089 1.882-2.354 3.907-5.011 7.345-10.066 8.095-3.976.591-4.172 1.314-4.051 5.413.1 3.337.061 6.705-.28 10.021-.363 3.555.008 4.521 3.442 5.373 7.924 1.968 15.913 3.647 23.492 6.854 3.227 1.365 6.465.891 9.064-1.763 2.713-2.771 6.141-3.855 9.844-3.859 6.285-.005 12.572.298 18.86.369 1.702.02 2.679.653 3.364 2.199.84 1.893 2.26 3.284 4.445 3.526 4.193.462 8.013-.16 11.19-3.359 3.918-3.948 8.436-7.066 13.615-9.227 1.482-.619 2.878-1.592 4.103-2.648 2.231-1.922 2.113-3.146-.135-5zM62.426 24.12c.758-2.601 2.537-4.289 5.243-4.801 2.276-.43 4.203.688 5.639 3.246 1.546 2.758 2.054 5.64.734 8.658-1.083 2.474-1.591 2.707-4.123 1.868-.474-.157-.937-.343-1.777-.652.708-.594 1.154-1.035 1.664-1.382 1.134-.772 1.452-1.858 1.346-3.148-.139-1.694-1.471-3.194-2.837-3.175-1.225.017-2.262 1.167-2.4 2.915-.086 1.089.095 2.199.173 3.589-3.446-1.023-4.711-3.525-3.662-7.118zm-12.75-2.251c1.274-1.928 3.197-2.314 5.101-1.024 2.029 1.376 3.547 5.256 2.763 7.576-.285.844-1.127 1.5-1.716 2.241l-.604-.374c-.23-1.253-.276-2.585-.757-3.733-.304-.728-1.257-1.184-1.919-1.762-.622.739-1.693 1.443-1.757 2.228-.088 1.084.477 2.28.969 3.331.311.661 1.001 1.145 1.713 1.916l-1.922 1.51c-3.018-2.7-3.915-8.82-1.871-11.909zM87.34 86.075c-.203 2.604-.5 2.713-3.118 3.098-1.859.272-2.359.756-2.453 2.964a101.744 101.744 0 00-.012 7.753c.061 1.77-.537 3.158-1.755 4.393-6.764 6.856-14.845 10.105-24.512 8.926-4.17-.509-6.896-3.047-9.097-6.639.98-.363 1.705-.607 2.412-.894 3.122-1.27 3.706-3.955 1.213-6.277-1.884-1.757-3.986-3.283-6.007-4.892-1.954-1.555-3.934-3.078-5.891-4.629-1.668-1.323-2.305-3.028-2.345-5.188-.094-5.182.972-10.03 3.138-14.747 1.932-4.209 3.429-8.617 5.239-12.885.935-2.202 1.906-4.455 3.278-6.388 1.319-1.854 2.134-3.669 1.988-5.94-.084-1.276-.016-2.562-.016-3.843l.707-.352c1.141.985 2.302 1.949 3.423 2.959 4.045 3.646 7.892 3.813 12.319.67 1.888-1.341 3.93-2.47 5.927-3.652.497-.294 1.092-.423 1.934-.738 2.151 5.066 4.262 10.033 6.375 15 1.072 2.524 1.932 5.167 3.264 7.547 2.671 4.775 4.092 9.813 4.07 15.272-.012 2.83.137 5.67-.081 8.482z"
+ />
+ </svg>
+ <span class="os-name">Linux</span>
+ <a class="file-name" href="./mling-linux.tar.gz"
+ >DOWNLOAD<span class="btn-arrow">↓</span></a
+ >
+ <span class="checksum"
+ ><a href="./mling-linux.tar.gz.sha256"
+ >SHA256</a
+ ></span
+ >
+ </div>
+ <div class="download-card">
+ <svg
+ class="platform invert"
+ viewBox="0 0 128 128"
+ xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
+ >
+ <path
+ d="M126 1.637l-67 9.834v49.831l67-.534zM1.647 66.709l.003 42.404 50.791 6.983-.04-49.057zm56.82.68l.094 49.465 67.376 9.509.016-58.863zM1.61 19.297l.047 42.383 50.791-.289-.023-49.016z"
+ />
+ </svg>
+ <span class="os-name">Windows</span>
+ <a class="file-name" href="./mling-win.tar.gz"
+ >DOWNLOAD<span class="btn-arrow">↓</span></a
+ >
+ <span class="checksum"
+ ><a href="./mling-win.tar.gz.sha256"
+ >SHA256</a
+ ></span
+ >
+ </div>
+ <div class="download-card">
+ <svg
+ class="platform invert"
+ viewBox="0 0 128 128"
+ xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
+ >
+ <path
+ d="M97.905 67.885c.174 18.8 16.494 25.057 16.674 25.137-.138.44-2.607 8.916-8.597 17.669-5.178 7.568-10.553 15.108-19.018 15.266-8.318.152-10.993-4.934-20.504-4.934-9.508 0-12.479 4.776-20.354 5.086-8.172.31-14.395-8.185-19.616-15.724C15.822 94.961 7.669 66.8 18.616 47.791c5.438-9.44 15.158-15.417 25.707-15.571 8.024-.153 15.598 5.398 20.503 5.398 4.902 0 14.106-6.676 23.782-5.696 4.051.169 15.421 1.636 22.722 12.324-.587.365-13.566 7.921-13.425 23.639M82.272 21.719c4.338-5.251 7.258-12.563 6.462-19.836-6.254.251-13.816 4.167-18.301 9.416-4.02 4.647-7.54 12.087-6.591 19.216 6.971.54 14.091-3.542 18.43-8.796"
+ />
+ </svg>
+ <span class="os-name">Mac OS</span>
+ <a class="file-name" href="./mling-mac.tar.gz"
+ >DOWNLOAD<span class="btn-arrow">↓</span></a
+ >
+ <span class="checksum"
+ ><a href="./mling-mac.tar.gz.sha256"
+ >SHA256</a
+ ></span
+ >
+ </div>
+ </div>
+
+ <p class="checksum-note">
+ Packages are built on every push to
+ <a
+ href="https://github.com/mingling-rs/mingling"
+ target="_blank"
+ >main</a
+ >
+ and published here automatically.
+ </p>
+ </div>
+ </section>
+
+ <section class="section alt">
+ <div class="container">
+ <h3>Install</h3>
+ <p>
+ Extract the package and load the bundled environment script
+ — it adds <code>bin/</code> to <code>PATH</code> and enables
+ shell completion.
+ </p>
+ <div class="code-block">
+ <pre><code class="language-bash"><span class="cm"># Linux / macOS (zsh, bash, fish)</span>
+tar -xzf mling-linux.tar.gz
+source load_mling.sh
+
+<span class="cm"># Windows (PowerShell)</span>
+tar -xzf mling-win.tar.gz
+.\load_mling.ps1</code></pre>
+ </div>
+ <h3>Update</h3>
+ <p>
+ <code>mling</code> can update itself. It downloads from this
+ page by default, verifies the sha256 checksum, and applies
+ the new version on your next invocation.
+ </p>
+ <div class="code-block">
+ <pre><code class="language-bash"><span class="cm"># Update to the latest build</span>
+mling update
+
+<span class="cm"># Point it at a custom mirror if needed</span>
+mling cfg update-url https://mingling-rs.github.io/mingling/dist
+mling update</code></pre>
+ </div>
+ </div>
+ </section>
+
+ <footer>
+ <a href="../">Mìng Lìng</a> ·
+ <a href="https://github.com/mingling-rs/mingling" target="_blank"
+ >GitHub</a
+ >
+ ·
+ <a href="../docs/doc.html">Docs</a>
+ </footer>
+
+ <script src="../docs/scripts/display-parse.js"></script>
+ <script src="../docs/scripts/highlight/highlight.min.js"></script>
+ <script src="../docs/scripts/highlight/bash.min.js"></script>
+ <script>
+ hljs.highlightAll();
+ </script>
+
+ <script>
+ // Load and render help.txt with the same coloring logic as display.rs
+ (function () {
+ var el = document.getElementById("shell-help");
+ if (!el) return;
+ fetch("../mingling_cli/help/help.txt")
+ .then(function (r) {
+ if (!r.ok) throw new Error("HTTP " + r.status);
+ return r.text();
+ })
+ .then(function (text) {
+ el.innerHTML = window.MlingDisplay.parse(text);
+ })
+ .catch(function (err) {
+ el.textContent =
+ "Failed to load help.txt: " + err.message;
+ });
+ })();
+ </script>
+
+ <script>
+ // Highlight the hovered line in code blocks
+ (function () {
+ function attachLineHighlight(container) {
+ var pre =
+ container.tagName === "PRE"
+ ? container
+ : container.querySelector("pre");
+ var code = pre.querySelector("code");
+ var hl = document.createElement("div");
+ hl.className = "code-line-highlight";
+ pre.appendChild(hl);
+
+ var lineHeight = parseFloat(
+ getComputedStyle(code).lineHeight,
+ );
+ var padTop =
+ parseFloat(getComputedStyle(code).paddingTop) || 0;
+
+ pre.addEventListener("mousemove", function (e) {
+ var rect = pre.getBoundingClientRect();
+ var y = e.clientY - rect.top;
+ var index = Math.floor((y - padTop) / lineHeight);
+ var lineCount = code.innerText.split("\n").length;
+ if (index < 0) index = 0;
+ if (index >= lineCount) index = lineCount - 1;
+ hl.style.top = padTop + index * lineHeight + "px";
+ hl.style.height = lineHeight + "px";
+ hl.style.opacity = "1";
+ });
+
+ pre.addEventListener("mouseleave", function () {
+ hl.style.opacity = "0";
+ });
+ }
+
+ document
+ .querySelectorAll(".code-block, .shell-term")
+ .forEach(attachLineHighlight);
+ })();
+ </script>
+
+ <script>
+ // Hero command carousel typewriter
+ (function () {
+ var code = document.querySelector(".term-hero");
+ if (!code) return;
+
+ var commands = [
+ "mling metadata",
+ "mling lint",
+ "mling ra-lint",
+ "mling proj-init 0.5.0@basic",
+ "mling class-add subcommand calc",
+ "mling install",
+ "mling pkg-enable your-cli@0.1.0",
+ "mling pkg-show",
+ "mling cfg update-url <URL>",
+ "mling update",
+ "mling -h",
+ ];
+ var idx = 0;
+ var cursor = document.createElement("span");
+ cursor.className = "term-cursor";
+ code.appendChild(cursor);
+
+ function clear() {
+ while (code.firstChild && code.firstChild !== cursor) {
+ code.removeChild(code.firstChild);
+ }
+ }
+
+ function typeText(text, cls, speed, done) {
+ var i = 0;
+ (function step() {
+ if (cls) {
+ var span = document.createElement("span");
+ span.className = cls;
+ span.textContent = text[i];
+ code.insertBefore(span, cursor);
+ } else {
+ cursor.insertAdjacentText("beforebegin", text[i]);
+ }
+ i++;
+ if (i < text.length) {
+ setTimeout(step, speed);
+ } else {
+ done();
+ }
+ })();
+ }
+
+ function play() {
+ var cmd = commands[idx];
+ idx = (idx + 1) % commands.length;
+ clear();
+ typeText("~# ", "term-prompt", 60, function () {
+ typeText(cmd, "term-cmd", 55, function () {
+ setTimeout(play, 2500);
+ });
+ });
+ }
+
+ setTimeout(play, 800);
+ })();
+ </script>
+
+ <script>
+ // Insert diamond decorations at line intersections
+ (function () {
+ document
+ .querySelectorAll("body > section + section, body > footer")
+ .forEach(function (el) {
+ var left = document.createElement("span");
+ left.className = "line-diamond left";
+ var right = document.createElement("span");
+ right.className = "line-diamond right";
+ el.appendChild(left);
+ el.appendChild(right);
+ });
+ })();
+ </script>
+
+ <script>
+ // Persistent glow dot following the mouse
+ var cursorDot = document.createElement("div");
+ cursorDot.className = "cursor-dot";
+ document.body.appendChild(cursorDot);
+
+ document.addEventListener("mousemove", function (e) {
+ cursorDot.style.left = e.clientX + "px";
+ cursorDot.style.top = e.clientY + "px";
+ if (cursorDot.style.opacity !== "1") {
+ cursorDot.style.opacity = "1";
+ }
+ });
+
+ // Ink drop ripple on click
+ document.addEventListener("click", function (e) {
+ var drop = document.createElement("div");
+ drop.className = "ink-drop";
+ drop.style.left = e.clientX + "px";
+ drop.style.top = e.clientY + "px";
+ document.body.appendChild(drop);
+ setTimeout(function () {
+ drop.remove();
+ }, 1000);
+ });
+ </script>
+
+ <script>
+ // Disable full-page snap after the last section so the footer is reachable
+ (function () {
+ var html = document.documentElement;
+ var sections = document.querySelectorAll(
+ "body > section.hero, body > section.section",
+ );
+ if (!sections.length) return;
+ var last = sections[sections.length - 1];
+ var lastTop = last.offsetTop;
+ var disabled = false;
+
+ window.addEventListener(
+ "wheel",
+ function (e) {
+ if (
+ e.deltaY > 0 &&
+ window.scrollY >= lastTop - 5 &&
+ !disabled
+ ) {
+ html.style.scrollSnapType = "none";
+ disabled = true;
+ }
+ },
+ { passive: true },
+ );
+
+ window.addEventListener(
+ "scroll",
+ function () {
+ if (disabled && window.scrollY < lastTop) {
+ html.style.scrollSnapType = "";
+ disabled = false;
+ }
+ },
+ { passive: true },
+ );
+ })();
+ </script>
+ </body>
+</html>
diff --git a/docs/_zh_CN/pages/1-getting-started.md b/docs/_zh_CN/pages/1-getting-started.md
index 6aa6229..47fdda8 100644
--- a/docs/_zh_CN/pages/1-getting-started.md
+++ b/docs/_zh_CN/pages/1-getting-started.md
@@ -13,7 +13,7 @@ cd my-cli
```toml
[dependencies.mingling]
-version = "0.4.0"
+version = "0.5.0"
features = []
```
@@ -25,7 +25,7 @@ features = []
```toml
[dependencies.mingling]
-version = "0.4.0"
+version = "0.5.0"
features = [
"parser",
"comp",
diff --git a/docs/dev/_sidebar.md b/docs/dev/_sidebar.md
index 03a0edf..cfb482a 100644
--- a/docs/dev/_sidebar.md
+++ b/docs/dev/_sidebar.md
@@ -1,10 +1,18 @@
- [Welcome!](README)
* ❓ Issues
- * [The Mingling 0.5.0 Roadmap](pages/issues/0.5.0-roadmap)
* [[Solved] The Picker2 Arguments Parser](pages/issues/_add-picker2)
* [[Solved] Remove r_print! and r_println! Macros](pages/issues/_remove-r-print-macro)
* [[Solved] The Command Macro](pages/issues/_the-command-macro)
* [[Solved] The Mod Pathfinder](pages/issues/_the-mod-pathfinder)
+ * [[T0] Generalize the REPL System](pages/issues/t0_generalize-repl-system)
+ * [[T0] Remove the pack! Family of Macros](pages/issues/t0_remove-pack-macros)
+ * [[T0] Remove the parser Feature](pages/issues/t0_remove-parser-feature)
+ * [[T0] Remove with_dispatcher and with_dispatchers](pages/issues/t0_remove-with-dispatcher)
+ * [[T1] Higher-Level Abstractions for the Completion System](pages/issues/t1_completion-higher-level-abstractions)
+ * [[T1] Modify the dispatcher! Syntax](pages/issues/t1_modify-dispatcher-syntax)
+ * [[T1] Move structural_renderer from mingling_core to mingling](pages/issues/t1_move-structural-renderer)
+ * [[T1] The pathf_export Attribute Macro](pages/issues/t1_pathf-export-macro)
+ * [[T2] Automated dispatcher_tree Optimization Decisions](pages/issues/t2_automated-dispatch-tree-optimization)
* [The Next-Gen Mingling Pipeline](pages/issues/the-next-pipeline)
* [Some Situations Where You'd Be Like "Shit!"](pages/issues/the-shit-time)
* 💡 Abouts
diff --git a/docs/dev/pages/issues/0.5.0-roadmap.md b/docs/dev/pages/issues/0.5.0-roadmap.md
deleted file mode 100644
index 5238cae..0000000
--- a/docs/dev/pages/issues/0.5.0-roadmap.md
+++ /dev/null
@@ -1,138 +0,0 @@
-<h1 align="center">The Mingling 0.5.0 Roadmap</h1>
-
-Mingling 0.5.0 is going to be a significant release, planned as follows:
-
-1. **Breaking:** Remove the `pack!` macro:
-
-Since the very first version of Mingling, the `pack!` macro has been around.
-Its purpose has gradually narrowed from "creating a type and registering it
-to Mingling" to "creating a newtype that derives Grouped". In other words,
-the functionality of `pack!` is gradually being replaced by the `Grouped derive`.
-Furthermore, in 0.2.0, in order to accommodate `StructuralData derive`, Mingling
-introduced `pack_structural!` and `pack_err_structural!` variants all at once,
-which greatly increases the maintenance cost of the project.
-
-So I plan to introduce a Breaking Change in 0.5.0: remove the entire `pack!` family of macros.
-
-All future type creation will be done as follows:
-
-```rust
-// Before
-pack!(ResultNames = Vec<String>);
-
-// After
-#[derive(Grouped)]
-pub struct ResultNames {
- names: Vec<String>
-}
-```
-
-2. **Breaking:** Generalize the REPL system
-
-The current REPL is merely _usable_, but far from _user-friendly_. Mingling plans
-to remove the `repl` feature in 0.5 and by default expose more execution-related
-interfaces for the Program, so that users can extend functionality beyond the REPL
-by leveraging Mingling's execution model.
-
-3. **Breaking:** Remove the `parser` feature
-
-In 0.3.0, Mingling introduced the `picker` feature, which provides more powerful
-parameter parsing capabilities. At that point, the original `parser` feature became
-inadequate. Mingling plans to completely remove it, which will directly affect
-downstream users of the `parser` feature.
-
-4. **Breaking:** Remove `with_dispatcher` and `with_dispatchers`
-
-Mingling's commands must be registered through `with_dispatcher` in order to be usable
-when `dispatcher_tree` is disabled. This has always been a strange semantic: `chain`,
-`renderer`, `help`, `completion`, and `metadata` are all collected at compile time,
-so why is `Dispatcher` the exception?
-
-In fact, during my usage of `dispatcher` from 0.1.0 to 0.4.0, I have never encountered
-a scenario where **dynamic registration** was necessary. I consider it unnecessary.
-
-Therefore, I plan to make `Dispatcher` registration also compile-time collected in
-non-`dispatcher_tree` states starting from 0.5.0.
-
-5. **Breaking:** Modify the `dispatcher!` syntax
-
-After completing item #4, `dispatcher!` will be simplified, because the `CMD*` struct will no longer need to be created
-
-```rust
-// Before
-dispatcher!("command", CMDCommand => EntryCommand);
-
-// After
-dispatcher!("command", EntryCommand);
-
-// NOTE: The implicit mode is not affected
-```
-
-6. **Feature:** Higher-level abstractions for the completion system
-
-Mingling's completion system filled a number of behavioral gaps in 0.4 and fixed many
-edge cases. It's now time to introduce more powerful higher-level abstractions.
-
-First, this feature will add a set of utility functions to `ShellContext`, enabling a
-smarter description of user state, rather than simply relying on manually identifying
-user behavior through fields like `previous_word`.
-
-Additionally, when the `picker` feature introduced in 0.3.0 is enabled together with
-the `comp` feature, a module named `picker_comp` will be activated to enable more
-completion behaviors.
-
-7. **Feature:** Automated `dispatcher_tree` optimization decisions (under consideration)
-
-After completing item #4, this Feature becomes implementable: Mingling can automatically
-decide whether to use `dispatcher_tree` to optimize dispatch efficiency based on the current
-number and depth of registered commands, so users no longer need to manually enable the
-`dispatch_tree` feature.
-
-Conditions: `dispatch_tree` has an advantage in cases where command depth is too high and
-the number of commands is too large. However, if the number of commands is too small, the
-increased CPU prediction failure rate will inevitably make it less efficient than linear
-lookup; specifics need to be tuned during implementation.
-
-Additionally, the issue where `pathf` + `dispatch_tree` must be explicitly specified in
-`[build-dependencies]` will be resolved:
-
-```toml
-# Before
-[build-dependencies.mingling]
-version = "0.4.0"
-features = [ "build", "pathf", "dispatch_tree" ] # `dispatch_tree` must be explicitly specified for `pathf` to recognize it
-
-# After
-[build-dependencies.mingling]
-version = "0.4.0"
-features = [ "build", "pathf" ] # No `dispatch_tree` feature; `pathf` no longer needs to consider its branches
-```
-
-8. **Feature:** A new macro designed for `pathf`: `#[pathf_export(type::TypePath)]`
-
-`pathf` has been around since 0.2.0 and has worked well for a long time, with many edge cases resolved. However, it still lacks an escape hatch — "when certain indirect expansions cannot be recognized by `pathf`, how can we assist its inference?"
-
-I plan to introduce a new attribute macro to supplement `pathf`'s path inference.
-
-```rust
-#[macro_export]
-macro_rules! repack {
- ($name:ident) => {
- // Ignored! This section cannot be parsed by pathf.
- #[mingling::macros::pathf_ignore]
- #[derive(mingling::Grouped)]
- pub struct $name;
- };
-}
-
-// The expansion contains macros that need to be parsed by pathf
-#[pathf_export(MyType)] // Explicitly specified to assist pathf's inference
-repack!(MyType);
-```
-
-> [!Note]
-> Haha, hopefully we'll never have to use it.
-
-<p align="center" style="font-size: 0.85em; color: gray;">
- Written by @Weicao-CatilGrass
-</p>
diff --git a/docs/dev/pages/issues/t0_generalize-repl-system.md b/docs/dev/pages/issues/t0_generalize-repl-system.md
new file mode 100644
index 0000000..73c9541
--- /dev/null
+++ b/docs/dev/pages/issues/t0_generalize-repl-system.md
@@ -0,0 +1,36 @@
+<h1 align="center">[T0] Generalize the REPL System</h1>
+<p align="center">
+ Breaking: remove the <code>repl</code> feature and expose execution interfaces on the Program
+</p>
+
+> [!NOTE]
+>
+> This is a **Breaking Change** planned for Mingling 0.5.0.
+
+## Background
+
+The current REPL is merely _usable_, but far from _user-friendly_. The `repl` feature locks a specific interactive front-end into the framework, while the underlying execution model is what actually provides value.
+
+## Plan
+
+- Remove the `repl` feature in 0.5.0.
+- By default, expose more execution-related interfaces for the `Program`, so that users can extend functionality beyond the REPL by leveraging Mingling's execution model.
+
+The goal is to separate the execution model from any particular interactive front-end, letting users build their own REPL (or other execution drivers) on top of Mingling's public interfaces.
+
+## Tasks
+
+- [ ] Audit the current `repl` implementation and identify which behaviors belong to the execution model vs. the interactive front-end
+- [ ] Design and expose the execution-related interfaces on `Program` (e.g. per-input execution, result handling, exit semantics)
+- [ ] Remove the `repl` feature from `mingling`, `mingling_core`, and `mingling_macros`
+- [ ] Remove or migrate the built-in REPL front-end
+- [ ] Update examples and docs that enable `repl`
+
+## 🕘 Progress
+
+- [ ] In Progress
+- [ ] Complete
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/dev/pages/issues/t0_remove-pack-macros.md b/docs/dev/pages/issues/t0_remove-pack-macros.md
new file mode 100644
index 0000000..517646a
--- /dev/null
+++ b/docs/dev/pages/issues/t0_remove-pack-macros.md
@@ -0,0 +1,49 @@
+`<h1 align="center">[T0] Remove the pack! Family of Macros</h1>
+<p align="center">
+ Breaking: retire the entire <code>pack!</code> family in favor of the <code>Grouped</code> derive
+</p>
+
+> [!NOTE]
+>
+> This is a **Breaking Change** planned for Mingling 0.5.0.
+
+## Background
+
+Since the very first version of Mingling, the `pack!` macro has been around. Its purpose has gradually narrowed from "creating a type and registering it to Mingling" to "creating a newtype that derives Grouped". In other words, the functionality of `pack!` is gradually being replaced by the `Grouped derive`.
+
+Furthermore, in 0.2.0, in order to accommodate `StructuralData derive`, Mingling introduced `pack_structural!` and `pack_err_structural!` variants all at once, which greatly increases the maintenance cost of the project.
+
+## Plan
+
+Remove the entire `pack!` family of macros (`pack!`, `pack_structural!`, `pack_err_structural!`) as a Breaking Change in 0.5.0.
+
+All future type creation will be done as follows:
+
+```rust
+// Before
+pack!(ResultNames = Vec<String>);
+
+// After
+#[derive(Grouped)]
+pub struct ResultNames {
+ names: Vec<String>
+}
+```
+
+## Tasks
+
+- [ ] Identify all usages of `pack!` / `pack_structural!` / `pack_err_structural!` across the codebase, examples, and docs
+- [ ] Migrate internal usages to `#[derive(Grouped)]`
+- [ ] Remove the macro definitions and their re-exports
+- [ ] Update the docs / helpdoc examples that reference `pack!`
+- [ ] Update downstream feature docs (`structural_renderer` etc.) where `pack_structural!` was involved
+- [ ] Verify all tests pass
+
+## 🕘 Progress
+
+- [ ] In Progress
+- [ ] Complete
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/dev/pages/issues/t0_remove-parser-feature.md b/docs/dev/pages/issues/t0_remove-parser-feature.md
new file mode 100644
index 0000000..5c1cffd
--- /dev/null
+++ b/docs/dev/pages/issues/t0_remove-parser-feature.md
@@ -0,0 +1,36 @@
+<h1 align="center">[T0] Remove the parser Feature</h1>
+<p align="center">
+ Breaking: retire the legacy argument parsing in favor of <code>picker</code>
+</p>
+
+> [!NOTE]
+>
+> This is a **Breaking Change** planned for Mingling 0.5.0.
+
+## Background
+
+In 0.3.0, Mingling introduced the `picker` feature, which provides more powerful parameter parsing capabilities. At that point, the original `parser` feature became inadequate.
+
+The `parser` feature was a temporary argument parsing solution created in the early stages of the project. While it can handle basic argument parsing tasks, its functionality is incomplete and has many limitations (see the [Picker2 issue](_add-picker2) for the full list).
+
+## Plan
+
+Completely remove the `parser` feature in 0.5.0. This will directly affect downstream users of the `parser` feature — they must migrate to `picker` (or handle parsing manually).
+
+## Tasks
+
+- [ ] Identify all usages of the `parser` feature across the codebase, examples, and docs
+- [ ] Migrate internal usages (tests, examples, dev-dependencies) to `picker`
+- [ ] Remove the `parser` feature from `mingling` and its dependency (`size`)
+- [ ] Remove parser-related modules and public API
+- [ ] Update docs and helpdoc examples
+- [ ] Note the downstream migration path in the changelog
+
+## 🕘 Progress
+
+- [ ] In Progress
+- [ ] Complete
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/dev/pages/issues/t0_remove-with-dispatcher.md b/docs/dev/pages/issues/t0_remove-with-dispatcher.md
new file mode 100644
index 0000000..8779d74
--- /dev/null
+++ b/docs/dev/pages/issues/t0_remove-with-dispatcher.md
@@ -0,0 +1,35 @@
+<h1 align="center">[T0] Remove with_dispatcher and with_dispatchers</h1>
+<p align="center">
+ Breaking: make <code>Dispatcher</code> registration compile-time collected in all modes
+</p>
+
+> [!NOTE]
+>
+> This is a **Breaking Change** planned for Mingling 0.5.0.
+
+## Background
+
+Mingling's commands must be registered through `with_dispatcher` in order to be usable when `dispatcher_tree` is disabled. This has always been a strange semantic: `chain`, `renderer`, `help`, `completion`, and `metadata` are all collected at compile time, so why is `Dispatcher` the exception?
+
+In fact, during usage of `dispatcher` from 0.1.0 to 0.4.0, no scenario has ever been encountered where **dynamic registration** was necessary. It is considered unnecessary.
+
+## Plan
+
+Make `Dispatcher` registration also compile-time collected in non-`dispatcher_tree` states starting from 0.5.0, and remove `with_dispatcher` / `with_dispatchers` (and the related `#[program_setup]` registration path if it becomes obsolete).
+
+## Tasks
+
+- [ ] Design how dispatchers are collected at compile time when `dispatcher_tree` is disabled (consistent with how `chain` / `renderer` / `completion` / `metadata` are collected)
+- [ ] Remove `with_dispatcher` and `with_dispatchers` from the `Program` API
+- [ ] Update `gen_program!` and the macros so registration happens automatically
+- [ ] Migrate examples, tests, and docs that call `with_dispatcher` / `with_dispatchers`
+- [ ] Verify both `dispatcher_tree`-enabled and disabled modes behave identically
+
+## 🕘 Progress
+
+- [ ] In Progress
+- [ ] Complete
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/dev/pages/issues/t1_completion-higher-level-abstractions.md b/docs/dev/pages/issues/t1_completion-higher-level-abstractions.md
new file mode 100644
index 0000000..69adba0
--- /dev/null
+++ b/docs/dev/pages/issues/t1_completion-higher-level-abstractions.md
@@ -0,0 +1,38 @@
+<h1 align="center">[T1] Higher-Level Abstractions for the Completion System</h1>
+<p align="center">
+ Feature: smarter state descriptions in <code>ShellContext</code> and a <code>picker_comp</code> module
+</p>
+
+## Background
+
+Mingling's completion system filled a number of behavioral gaps in 0.4 and fixed many edge cases. It's now time to introduce more powerful higher-level abstractions.
+
+Currently, completion logic relies on manually identifying user behavior through fields like `previous_word`, which is fragile and requires every completion function to re-derive the user's intent.
+
+## Plan
+
+### 1. Utility functions on `ShellContext`
+
+Add a set of utility functions to `ShellContext`, enabling a smarter description of user state, rather than simply relying on manually identifying user behavior through fields like `previous_word`.
+
+### 2. `picker_comp` module
+
+Additionally, when the `picker` feature (introduced in 0.3.0) is enabled together with the `comp` feature, a module named `picker_comp` will be activated to enable more completion behaviors — e.g. completing picker-style flags (`--key=value`, multi-flag forms, etc.) using knowledge of the picker parsing model.
+
+## Tasks
+
+- [ ] Design the `ShellContext` utility API (state descriptions / high-level queries over the current input state)
+- [ ] Implement the utility functions and add tests
+- [ ] Implement the `picker_comp` module, gated on `picker` + `comp`
+- [ ] Add completion behaviors specific to picker argument formats
+- [ ] Update docs and examples
+- [ ] Verify existing 0.4 completion edge-case fixes are preserved
+
+## 🕘 Progress
+
+- [ ] In Progress
+- [ ] Complete
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/dev/pages/issues/t1_modify-dispatcher-syntax.md b/docs/dev/pages/issues/t1_modify-dispatcher-syntax.md
new file mode 100644
index 0000000..f1a9346
--- /dev/null
+++ b/docs/dev/pages/issues/t1_modify-dispatcher-syntax.md
@@ -0,0 +1,44 @@
+<h1 align="center">[T1] Modify the dispatcher! Syntax</h1>
+<p align="center">
+ Breaking: drop the <code>CMD*</code> struct from the explicit form of <code>dispatcher!</code>
+</p>
+
+> [!NOTE]
+>
+> This is a **Breaking Change** planned for Mingling 0.5.0, and it depends on [Remove with_dispatcher and with_dispatchers](t0_remove-with-dispatcher).
+
+## Background
+
+Once `Dispatcher` registration is compile-time collected (see [Remove with_dispatcher and with_dispatchers](t0_remove-with-dispatcher)), the `CMD*` struct becomes unnecessary — it only existed to give `with_dispatcher` something to register.
+
+## Plan
+
+Simplify the `dispatcher!` syntax: the explicit form no longer creates a `CMD*` struct, so only the entry type needs to be given.
+
+```rust
+// Before
+dispatcher!("command", CMDCommand => EntryCommand);
+
+// After
+dispatcher!("command", EntryCommand);
+
+// NOTE: The implicit mode is not affected
+```
+
+## Tasks
+
+- [ ] Update `dispatcher!` to accept the simplified explicit form (`"name", EntryType`)
+- [ ] Decide whether the old `CMDType => EntryType` form should error with a helpful message or be removed outright
+- [ ] Remove the generated `CMD*` struct machinery
+- [ ] Update `#[command]` macro internals that depend on `CMD*`
+- [ ] Migrate examples, tests, and docs
+- [ ] Keep the implicit mode (`dispatcher!("name")`) working unchanged
+
+## 🕘 Progress
+
+- [ ] In Progress
+- [ ] Complete
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/dev/pages/issues/t1_move-structural-renderer.md b/docs/dev/pages/issues/t1_move-structural-renderer.md
new file mode 100644
index 0000000..66da5c8
--- /dev/null
+++ b/docs/dev/pages/issues/t1_move-structural-renderer.md
@@ -0,0 +1,38 @@
+<h1 align="center">[T1] Move structural_renderer from mingling_core to mingling</h1>
+<p align="center">
+ Breaking: inject <code>StructuralRenderer</code> via Hook instead of hardcoding it into the core loop
+</p>
+
+> [!NOTE]
+>
+> This is a **Breaking Change** planned for Mingling 0.5.0.
+
+## Background
+
+Mingling's Hook system is now complete, so there's no longer a need to hardcode `StructuralRenderer` into the core loop.
+
+The plan is to remove it from `exec.rs` and instead inject the Hook implementation via `StructuralRendererSetup`.
+
+## Plan
+
+- Remove the hardcoded `StructuralRenderer` from the core execution loop (`exec.rs`).
+- Implement the renderer as a Hook and inject it via `StructuralRendererSetup`.
+- `mingling_core` no longer depends on the structural renderer; the wiring moves up to the `mingling` crate level.
+
+## Tasks
+
+- [ ] Audit where `StructuralRenderer` is hardcoded in `mingling_core` (`exec.rs` and related)
+- [ ] Design `StructuralRendererSetup` as a Hook implementation
+- [ ] Move / re-implement the renderer wiring in `mingling`
+- [ ] Clean up `mingling_core`'s `structural_renderer` feature and its serde deps (if no longer needed there)
+- [ ] Migrate examples and tests
+- [ ] Verify structural renderer output is unchanged
+
+## 🕘 Progress
+
+- [ ] In Progress
+- [ ] Complete
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/dev/pages/issues/t1_pathf-export-macro.md b/docs/dev/pages/issues/t1_pathf-export-macro.md
new file mode 100644
index 0000000..0a98485
--- /dev/null
+++ b/docs/dev/pages/issues/t1_pathf-export-macro.md
@@ -0,0 +1,53 @@
+<h1 align="center">[T1] The pathf_export Attribute Macro</h1>
+<p align="center">
+ Feature: an escape hatch for <code>pathf</code>'s path inference
+</p>
+
+## Background
+
+`pathf` has been around since 0.2.0 and has worked well for a long time, with many edge cases resolved. However, it still lacks an escape hatch — "when certain indirect expansions cannot be recognized by `pathf`, how can we assist its inference?"
+
+For example, when a Mingling type is created through a user-defined `macro_rules!` wrapper, `pathf` cannot see through the indirect expansion:
+
+```rust
+#[macro_export]
+macro_rules! repack {
+ ($name:ident) => {
+ // Ignored! This section cannot be parsed by pathf.
+ #[mingling::macros::pathf_ignore]
+ #[derive(mingling::Grouped)]
+ pub struct $name;
+ };
+}
+
+// The expansion contains macros that need to be parsed by pathf
+#[pathf_export(MyType)] // Explicitly specified to assist pathf's inference
+repack!(MyType);
+```
+
+> [!Note]
+> Haha, hopefully we'll never have to use it.
+
+## Plan
+
+Introduce a new attribute macro, `#[pathf_export(type::TypePath)]`, to supplement `pathf`'s path inference. When applied to an item whose expansion contains Mingling types that `pathf` cannot recognize, it explicitly records the resulting type paths so the build-time analyzer can pick them up.
+
+The example above also shows `#[mingling::macros::pathf_ignore]`, which marks an item to be skipped by `pathf` (used inside macro bodies that `pathf` otherwise cannot parse).
+
+## Tasks
+
+- [ ] Design the `pathf_export` syntax and semantics (attribute position, multiple type paths, interplay with `pathf_ignore`)
+- [ ] Implement `pathf_export` in `mingling_macros`
+- [ ] Implement `pathf_ignore` support in `mingling_macros`
+- [ ] Teach `mingling-pathf` to consume the exported mappings
+- [ ] Add tests covering indirect macro expansions
+- [ ] Update docs and examples
+
+## 🕘 Progress
+
+- [ ] In Progress
+- [ ] Complete
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/dev/pages/issues/t2_automated-dispatch-tree-optimization.md b/docs/dev/pages/issues/t2_automated-dispatch-tree-optimization.md
new file mode 100644
index 0000000..691fd1a
--- /dev/null
+++ b/docs/dev/pages/issues/t2_automated-dispatch-tree-optimization.md
@@ -0,0 +1,57 @@
+<h1 align="center">[T2] Automated dispatcher_tree Optimization Decisions</h1>
+<p align="center">
+ Feature: let Mingling decide when <code>dispatch_tree</code> pays off (under consideration)
+</p>
+
+> [!NOTE]
+>
+> This item is **under consideration**. It depends on [Remove with_dispatcher and with_dispatchers](t0_remove-with-dispatcher).
+
+## Background
+
+`dispatch_tree` provides a faster dispatch path, but it is not always a win. Currently users must manually enable the `dispatch_tree` feature and make the trade-off themselves.
+
+After dispatcher registration becomes compile-time collected (see [Remove with_dispatcher and with_dispatchers](t0_remove-with-dispatcher)), Mingling can know the full set and depth of registered commands at compile time — making automated decisions implementable.
+
+## Plan
+
+Mingling can automatically decide whether to use `dispatcher_tree` to optimize dispatch efficiency based on the current number and depth of registered commands, so users no longer need to manually enable the `dispatch_tree` feature.
+
+### Conditions
+
+`dispatch_tree` has an advantage in cases where command depth is too high and the number of commands is too large. However, if the number of commands is too small, the increased CPU prediction failure rate will inevitably make it less efficient than linear lookup; specifics need to be tuned during implementation.
+
+### Resolve the `pathf` + `dispatch_tree` build-dependency issue
+
+Additionally, the issue where `pathf` + `dispatch_tree` must be explicitly specified in `[build-dependencies]` will be resolved:
+
+```toml
+# Before
+[build-dependencies.mingling]
+version = "0.4.0"
+features = [ "build", "pathf", "dispatch_tree" ] # `dispatch_tree` must be explicitly specified for `pathf` to recognize it
+
+# After
+[build-dependencies.mingling]
+version = "0.4.0"
+features = [ "build", "pathf" ] # No `dispatch_tree` feature; `pathf` no longer needs to consider its branches
+```
+
+## Tasks
+
+- [ ] Collect statistics about registered commands (count, depth) at compile time
+- [ ] Benchmark / tune the threshold between linear lookup and `dispatch_tree`
+- [ ] Implement the automatic decision and wire it into dispatch code generation
+- [ ] Remove the manual `dispatch_tree` feature toggle (or keep it as an override?)
+- [ ] Refactor `pathf` so it no longer branches on `dispatch_tree`
+- [ ] Update examples, tests, and docs
+
+## 🕘 Progress
+
+- [ ] Under Consideration
+- [ ] In Progress
+- [ ] Complete
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/example-viewer.html b/docs/example-viewer.html
index dd51e95..0417c30 100644
--- a/docs/example-viewer.html
+++ b/docs/example-viewer.html
@@ -31,6 +31,117 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
+ background-image:
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ );
+ background-size:
+ 40px 40px,
+ 40px 40px,
+ 20px 20px,
+ 20px 20px;
+ background-position:
+ calc(50% - 480px + 20px) 0,
+ calc(50% - 480px) 0,
+ calc(50% - 480px + 10px) 0,
+ calc(50% - 480px + 10px) 0;
+ }
+
+ /* Dashed side lines along the viewer edges */
+ html::before,
+ html::after {
+ content: "";
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: repeating-linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.25) 0,
+ rgba(212, 168, 75, 0.25) 4px,
+ transparent 4px,
+ transparent 9px
+ );
+ z-index: 49;
+ pointer-events: none;
+ }
+
+ html::before {
+ left: max(0px, calc(50% - 480px));
+ }
+
+ html::after {
+ right: max(0px, calc(50% - 480px));
+ }
+
+ /* Cursor glow dot */
+ .cursor-dot {
+ position: fixed;
+ pointer-events: none;
+ border-radius: 50%;
+ width: 400px;
+ height: 400px;
+ background: radial-gradient(
+ circle,
+ rgba(212, 168, 75, 0.06) 0%,
+ rgba(212, 168, 75, 0.02) 45%,
+ transparent 70%
+ );
+ transform: translate(-50%, -50%);
+ z-index: 10000;
+ opacity: 0;
+ transition:
+ opacity 0.3s ease,
+ left 0.12s ease-out,
+ top 0.12s ease-out;
+ }
+
+ /* Ink drop effect */
+ .ink-drop {
+ position: fixed;
+ pointer-events: none;
+ border-radius: 50%;
+ background: radial-gradient(
+ circle,
+ rgba(212, 168, 75, 0.15) 0%,
+ rgba(196, 57, 49, 0.06) 50%,
+ transparent 100%
+ );
+ width: 0;
+ height: 0;
+ transform: translate(-50%, -50%);
+ animation: inkSpread 0.9s ease-out forwards;
+ z-index: 9999;
+ }
+
+ @keyframes inkSpread {
+ 0% {
+ width: 0;
+ height: 0;
+ opacity: 1;
+ }
+ 100% {
+ width: 500px;
+ height: 500px;
+ opacity: 0;
+ }
}
a {
color: #d4a84b;
@@ -83,6 +194,24 @@
color: #d4a84b;
}
+ nav .nav-links .btn-nav {
+ display: inline-block;
+ padding: 0.35rem 1rem;
+ border: 1px solid #d4a84b;
+ border-radius: 20px;
+ color: #d4a84b;
+ font-weight: 600;
+ font-size: 0.85rem;
+ transition:
+ background 0.2s,
+ color 0.2s;
+ }
+
+ nav .nav-links .btn-nav:hover {
+ background: #d4a84b;
+ color: #1a1410;
+ }
+
.viewer {
margin-top: 4rem;
padding: 1.5rem;
@@ -184,6 +313,45 @@
font-variant-ligatures: inherit;
}
+ /* Line numbers + hover highlight */
+ .code-frame pre {
+ position: relative;
+ }
+
+ .code-frame pre code {
+ counter-reset: line;
+ }
+
+ .code-frame pre code .line {
+ display: block;
+ counter-increment: line;
+ }
+
+ .code-frame pre code .line::before {
+ content: counter(line);
+ display: inline-block;
+ width: 2.5ch;
+ margin-right: 3ch;
+ text-align: right;
+ color: #6a5a4a;
+ user-select: none;
+ }
+
+ .code-line-highlight {
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 28px;
+ background: rgba(255, 255, 255, 0.1);
+ border-left: 2px solid rgba(255, 255, 255, 0.45);
+ pointer-events: none;
+ opacity: 0;
+ transition:
+ opacity 0.15s ease,
+ top 0.08s ease;
+ z-index: 3;
+ }
+
.doc-box {
background: #241c16;
border: 1px solid #3a2e24;
@@ -226,6 +394,13 @@
color: #e8ddd0;
}
+ /* Markdown code blocks: drop the hljs theme's fg/bg, keep the pre styling */
+ .doc-box pre code.hljs {
+ background: transparent !important;
+ color: #c0b0a0;
+ padding: 0 !important;
+ }
+
.loading {
text-align: center;
padding: 4rem 1rem;
@@ -282,6 +457,12 @@
target="_blank"
>GitHub</a
>
+ <a
+ href="https://crates.io/crates/mingling"
+ target="_blank"
+ class="btn-nav"
+ >cargo add mingling</a
+ >
</div>
</nav>
@@ -325,6 +506,33 @@
</p>
</footer>
+ <script>
+ // Persistent glow dot following the mouse
+ var cursorDot = document.createElement("div");
+ cursorDot.className = "cursor-dot";
+ document.body.appendChild(cursorDot);
+
+ document.addEventListener("mousemove", function (e) {
+ cursorDot.style.left = e.clientX + "px";
+ cursorDot.style.top = e.clientY + "px";
+ if (cursorDot.style.opacity !== "1") {
+ cursorDot.style.opacity = "1";
+ }
+ });
+
+ // Ink drop ripple on click
+ document.addEventListener("click", function (e) {
+ var drop = document.createElement("div");
+ drop.className = "ink-drop";
+ drop.style.left = e.clientX + "px";
+ drop.style.top = e.clientY + "px";
+ document.body.appendChild(drop);
+ setTimeout(function () {
+ drop.remove();
+ }, 1000);
+ });
+ </script>
+
<script src="scripts/highlight/highlight.min.js"></script>
<script src="scripts/highlight/rust.min.js"></script>
<script src="scripts/highlight/bash.min.js"></script>
@@ -390,10 +598,18 @@
i++;
}
i++; // skip closing ```
- var codeText = escapeHtml(codeLines.join("\n"));
+ var rawCode = codeLines.join("\n");
+ var highlighted;
+ if (lang && hljs.getLanguage(lang)) {
+ highlighted = hljs.highlight(rawCode, {
+ language: lang,
+ }).value;
+ } else {
+ highlighted = escapeHtml(rawCode);
+ }
html +=
- "<pre style=\"background:#1a1410;border:1px solid #3a2e24;border-radius:2px;padding:0.75rem;margin:0.5rem 0;overflow-x:auto;font-size:0.8rem;line-height:1.5;font-family:'JetBrains Mono','Noto Serif SC',monospace;font-variant-ligatures:contextual\"><code>" +
- codeText +
+ "<pre style=\"background:#1a1410;border:1px solid #3a2e24;border-radius:2px;padding:0.75rem;margin:0.5rem 0;overflow-x:auto;font-size:0.8rem;line-height:1.5;font-family:'JetBrains Mono','Noto Serif SC',monospace;font-variant-ligatures:contextual\"><code class=\"hljs\">" +
+ highlighted +
"</code></pre>";
continue;
}
@@ -594,6 +810,18 @@
delete codeContent.dataset.highlighted;
hljs.highlightElement(codeContent);
+ // Wrap each line with a .line span for numbering
+ codeContent.innerHTML = codeContent.innerHTML
+ .split("\n")
+ .map(function (line) {
+ return (
+ '<span class="line">' +
+ line +
+ "</span>"
+ );
+ })
+ .join("");
+
tabsEl
.querySelectorAll("button")
.forEach(function (b) {
@@ -627,6 +855,35 @@
tabsEl.appendChild(btn);
});
+ // Line highlight on hover
+ (function () {
+ var pre = codePre;
+ var code = codeContent;
+ var hl = document.createElement("div");
+ hl.className = "code-line-highlight";
+ pre.appendChild(hl);
+
+ var lineHeight = parseFloat(
+ getComputedStyle(code).lineHeight,
+ );
+
+ pre.addEventListener("mousemove", function (e) {
+ var rect = pre.getBoundingClientRect();
+ var y = e.clientY - rect.top;
+ var index = Math.floor(y / lineHeight);
+ var lineCount = code.innerText.split("\n").length;
+ if (index < 0) index = 0;
+ if (index >= lineCount) index = lineCount - 1;
+ hl.style.top = index * lineHeight + "px";
+ hl.style.height = lineHeight + "px";
+ hl.style.opacity = "1";
+ });
+
+ pre.addEventListener("mouseleave", function () {
+ hl.style.opacity = "0";
+ });
+ })();
+
// Load first file
if (files.length > 0) loadFile(files[0]);
}
diff --git a/docs/examples.html b/docs/examples.html
index a200000..9d2ae8c 100644
--- a/docs/examples.html
+++ b/docs/examples.html
@@ -33,6 +33,37 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
+ background-image:
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ );
+ background-size:
+ 40px 40px,
+ 40px 40px,
+ 20px 20px,
+ 20px 20px;
+ background-position:
+ calc(50% - 550px + 20px) 0,
+ calc(50% - 550px) 0,
+ calc(50% - 550px + 10px) 0,
+ calc(50% - 550px + 10px) 0;
}
a {
@@ -55,6 +86,86 @@
padding: 0 1.5rem;
}
+ /* Dashed side lines along the container edges */
+ html::before,
+ html::after {
+ content: "";
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: repeating-linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.25) 0,
+ rgba(212, 168, 75, 0.25) 4px,
+ transparent 4px,
+ transparent 9px
+ );
+ z-index: 49;
+ pointer-events: none;
+ }
+
+ html::before {
+ left: max(0px, calc(50% - 550px));
+ }
+
+ html::after {
+ right: max(0px, calc(50% - 550px));
+ }
+
+ /* Cursor glow dot */
+ .cursor-dot {
+ position: fixed;
+ pointer-events: none;
+ border-radius: 50%;
+ width: 400px;
+ height: 400px;
+ background: radial-gradient(
+ circle,
+ rgba(212, 168, 75, 0.06) 0%,
+ rgba(212, 168, 75, 0.02) 45%,
+ transparent 70%
+ );
+ transform: translate(-50%, -50%);
+ z-index: 10000;
+ opacity: 0;
+ transition:
+ opacity 0.3s ease,
+ left 0.12s ease-out,
+ top 0.12s ease-out;
+ }
+
+ /* Ink drop effect */
+ .ink-drop {
+ position: fixed;
+ pointer-events: none;
+ border-radius: 50%;
+ background: radial-gradient(
+ circle,
+ rgba(212, 168, 75, 0.15) 0%,
+ rgba(196, 57, 49, 0.06) 50%,
+ transparent 100%
+ );
+ width: 0;
+ height: 0;
+ transform: translate(-50%, -50%);
+ animation: inkSpread 0.9s ease-out forwards;
+ z-index: 9999;
+ }
+
+ @keyframes inkSpread {
+ 0% {
+ width: 0;
+ height: 0;
+ opacity: 1;
+ }
+ 100% {
+ width: 500px;
+ height: 500px;
+ opacity: 0;
+ }
+ }
+
/* ── Nav ── */
nav {
position: fixed;
@@ -107,7 +218,7 @@
display: inline-block;
padding: 0.35rem 1rem;
border: 1px solid #d4a84b;
- border-radius: 2px;
+ border-radius: 20px;
color: #d4a84b;
font-weight: 600;
font-size: 0.85rem;
@@ -486,5 +597,32 @@
.replace(/"/g, "&quot;");
}
</script>
+
+ <script>
+ // Persistent glow dot following the mouse
+ var cursorDot = document.createElement("div");
+ cursorDot.className = "cursor-dot";
+ document.body.appendChild(cursorDot);
+
+ document.addEventListener("mousemove", function (e) {
+ cursorDot.style.left = e.clientX + "px";
+ cursorDot.style.top = e.clientY + "px";
+ if (cursorDot.style.opacity !== "1") {
+ cursorDot.style.opacity = "1";
+ }
+ });
+
+ // Ink drop ripple on click
+ document.addEventListener("click", function (e) {
+ var drop = document.createElement("div");
+ drop.className = "ink-drop";
+ drop.style.left = e.clientX + "px";
+ drop.style.top = e.clientY + "px";
+ document.body.appendChild(drop);
+ setTimeout(function () {
+ drop.remove();
+ }, 1000);
+ });
+ </script>
</body>
</html>
diff --git a/docs/pages/1-getting-started.md b/docs/pages/1-getting-started.md
index 443ed74..0131ec1 100644
--- a/docs/pages/1-getting-started.md
+++ b/docs/pages/1-getting-started.md
@@ -13,7 +13,7 @@ Add the following to `Cargo.toml`:
```toml
[dependencies.mingling]
-version = "0.4.0"
+version = "0.5.0"
features = []
```
@@ -25,7 +25,7 @@ Some features **directly affect the entire lifecycle behavior**, so you need to
```toml
[dependencies.mingling]
-version = "0.4.0"
+version = "0.5.0"
features = [
"parser",
"comp",
diff --git a/docs/res/guide.txt b/docs/res/guide.txt
index 74f9c69..e84f893 100644
--- a/docs/res/guide.txt
+++ b/docs/res/guide.txt
@@ -5,5 +5,5 @@
│ > cargo add mingling │
│ │
│ Or add this to your Cargo.toml │
- │ > mingling = "0.4.0" │
+ │ > mingling = "0.5.0" │
└────────────────────────────────────┘
diff --git a/docs/scripts/display-parse.js b/docs/scripts/display-parse.js
new file mode 100644
index 0000000..ca7b391
--- /dev/null
+++ b/docs/scripts/display-parse.js
@@ -0,0 +1,269 @@
+/*!
+ * MlingDisplay — client-side port of mingling_cli/src/utils/display.rs
+ *
+ * Parses the same markdown-ish markup used by `display.rs` and returns HTML
+ * with the equivalent ANSI styling applied as CSS classes / inline colors.
+ *
+ * Supported syntax:
+ * - Bold: **text**
+ * - Italic: *text*
+ * - Underline: _text_
+ * - Cyan: <text>
+ * - Inline code: `text` (green)
+ * - Colors: [[color_name]] ... [[/]] (nested)
+ * - Escapes: \* \< \> \` \_
+ * - Headings: # Heading 1 ... ###### Heading 6
+ * - Blockquote: > text
+ *
+ * Exposes: window.MlingDisplay.parse(text) -> HTML string
+ */
+(function () {
+ "use strict";
+
+ function findMatch(chars, start, delimiter) {
+ var len = delimiter.length;
+ for (var j = start; j < chars.length; j++) {
+ if (len === 1) {
+ if (chars[j] === delimiter) return j;
+ } else if (
+ j + 1 < chars.length &&
+ chars[j] === delimiter[0] &&
+ chars[j + 1] === delimiter[1]
+ ) {
+ return j;
+ }
+ }
+ return -1;
+ }
+
+ var COLOR_MAP = {
+ black: "#000",
+ red: "#ff7b72",
+ green: "#7ee787",
+ yellow: "#d4a84b",
+ blue: "#79c0ff",
+ magenta: "#d2a8ff",
+ cyan: "#79c0ff",
+ white: "#e8ddd0",
+ b_white: "#e8ddd0",
+ bright_gray: "#6a5a4a",
+ bright_grey: "#6a5a4a",
+ b_gray: "#6a5a4a",
+ b_grey: "#6a5a4a",
+ bright_black: "#6a5a4a",
+ b_black: "#6a5a4a",
+ gray: "#6a5a4a",
+ grey: "#6a5a4a",
+ bright_red: "#ff7b72",
+ b_red: "#ff7b72",
+ bright_green: "#7ee787",
+ b_green: "#7ee787",
+ bright_yellow: "#d4a84b",
+ b_yellow: "#d4a84b",
+ bright_blue: "#79c0ff",
+ b_blue: "#79c0ff",
+ bright_magenta: "#d2a8ff",
+ b_magenta: "#d2a8ff",
+ bright_cyan: "#79c0ff",
+ b_cyan: "#79c0ff",
+ bright_white: "#e8ddd0",
+ };
+
+ function applyColorStack(text, stack) {
+ var wrapped = text;
+ for (var k = stack.length - 1; k >= 0; k--) {
+ var css = COLOR_MAP[stack[k]];
+ if (css) {
+ wrapped =
+ '<span style="color:' + css + '">' + wrapped + "</span>";
+ }
+ }
+ return wrapped;
+ }
+
+ function escapeHtml(s) {
+ return s
+ .replace(/&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;");
+ }
+
+ function processLine(line) {
+ var chars = Array.from(line);
+ var result = "";
+ var colorStack = [];
+ var i = 0;
+
+ while (i < chars.length) {
+ var c = chars[i];
+
+ // Escape sequences \\* \\< \\> \\` \\_
+ if (
+ c === "\\" &&
+ i + 1 < chars.length &&
+ ["*", "<", ">", "`", "_"].indexOf(chars[i + 1]) !== -1
+ ) {
+ result += escapeHtml(chars[i + 1]);
+ i += 2;
+ continue;
+ }
+
+ // Color tags [[color]] / [[/]]
+ if (c === "[" && chars[i + 1] === "[") {
+ var tagEnd = -1;
+ for (var t = i + 2; t + 1 < chars.length; t++) {
+ if (chars[t] === "]" && chars[t + 1] === "]") {
+ tagEnd = t;
+ break;
+ }
+ }
+ if (tagEnd !== -1) {
+ var tag = chars.slice(i + 2, tagEnd).join("");
+ if (tag === "/") colorStack.pop();
+ else colorStack.push(tag);
+ i = tagEnd + 2;
+ continue;
+ }
+ }
+
+ // **bold**
+ if (c === "*" && chars[i + 1] === "*") {
+ var bEnd = findMatch(chars, i + 2, "**");
+ if (bEnd !== -1) {
+ var inner = processLine(chars.slice(i + 2, bEnd).join(""));
+ result += applyColorStack(
+ "<b>" + inner + "</b>",
+ colorStack,
+ );
+ i = bEnd + 2;
+ continue;
+ }
+ }
+
+ // *italic*
+ if (c === "*") {
+ var iEnd = findMatch(chars, i + 1, "*");
+ if (iEnd !== -1) {
+ var italic = processLine(chars.slice(i + 1, iEnd).join(""));
+ result += applyColorStack(
+ "<i>" + italic + "</i>",
+ colorStack,
+ );
+ i = iEnd + 1;
+ continue;
+ }
+ }
+
+ // _underline_
+ if (c === "_") {
+ var uEnd = findMatch(chars, i + 1, "_");
+ if (uEnd !== -1) {
+ var uText = processLine(chars.slice(i + 1, uEnd).join(""));
+ result += applyColorStack(
+ "<u>" + uText + "</u>",
+ colorStack,
+ );
+ i = uEnd + 1;
+ continue;
+ }
+ }
+
+ // <angle> cyan
+ if (c === "<") {
+ var cEnd = findMatch(chars, i + 1, ">");
+ if (cEnd !== -1) {
+ var angle = chars.slice(i, cEnd + 1).join("");
+ result += applyColorStack(
+ '<span class="t-cyan">' + escapeHtml(angle) + "</span>",
+ colorStack,
+ );
+ i = cEnd + 1;
+ continue;
+ }
+ }
+
+ // `code` green
+ if (c === "`") {
+ var gEnd = findMatch(chars, i + 1, "`");
+ if (gEnd !== -1) {
+ var codeText = chars.slice(i, gEnd + 1).join("");
+ result += applyColorStack(
+ '<span class="t-green">' +
+ escapeHtml(codeText) +
+ "</span>",
+ colorStack,
+ );
+ i = gEnd + 1;
+ continue;
+ }
+ }
+
+ // Regular character
+ result += applyColorStack(escapeHtml(c), colorStack);
+ i += 1;
+ }
+ return result;
+ }
+
+ function processLineWithQuote(line) {
+ var chars = Array.from(line);
+ if (chars.length && chars[0] === ">") {
+ if (chars.length > 1 && chars[1] === "\\") {
+ return processLine(line);
+ }
+ var rest = chars.length > 1 ? chars.slice(1).join("") : "";
+ return '<span class="t-quote"> </span>' + processLine(rest);
+ }
+ return processLine(line);
+ }
+
+ /**
+ * Parse display.rs-flavored markup into HTML.
+ * @param {string} text - Raw markup text.
+ * @returns {string} HTML with t-cyan / t-green / t-heading / t-quote classes
+ * and inline colors for [[color]] tags.
+ */
+ function parse(text) {
+ var lines = String(text).split("\n");
+ var result = "";
+ var contentIndent = 0;
+ for (var n = 0; n < lines.length; n++) {
+ var line = lines[n];
+ var trimmed = line.trim();
+ var ls = line.trimStart();
+ var lineResult = "";
+
+ if (ls.startsWith("#")) {
+ var level = 0;
+ while (level < ls.length && level < 7 && ls[level] === "#") {
+ level++;
+ }
+ var effective = level > 6 ? 6 : level;
+ var start = level;
+ while (
+ start < ls.length &&
+ (ls[start] === " " || ls[start] === "\t")
+ ) {
+ start++;
+ }
+ var content = start < ls.length ? ls.slice(start) : "";
+ var heading =
+ '<span class="t-heading"> ' +
+ processLine(content) +
+ " </span>";
+ var indent = " ".repeat(effective > 0 ? effective - 1 : 0);
+ lineResult = indent + heading;
+ contentIndent = effective;
+ } else if (trimmed !== "") {
+ lineResult =
+ " ".repeat(contentIndent) + processLineWithQuote(trimmed);
+ } else {
+ lineResult = " ";
+ }
+ result += lineResult + "\n";
+ }
+ return result;
+ }
+
+ window.MlingDisplay = { parse: parse };
+})();
diff --git a/examples/example-argument-parse/Cargo.lock b/examples/example-argument-parse/Cargo.lock
index 8debd86..146e227 100644
--- a/examples/example-argument-parse/Cargo.lock
+++ b/examples/example-argument-parse/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -66,7 +66,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -74,7 +74,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-argument-picker/Cargo.lock b/examples/example-argument-picker/Cargo.lock
index 9977771..f7969b6 100644
--- a/examples/example-argument-picker/Cargo.lock
+++ b/examples/example-argument-picker/Cargo.lock
@@ -74,7 +74,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"arg-picker",
"mingling_core",
@@ -83,7 +83,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -91,7 +91,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-async-support/Cargo.lock b/examples/example-async-support/Cargo.lock
index 67cd6b6..a2751be 100644
--- a/examples/example-async-support/Cargo.lock
+++ b/examples/example-async-support/Cargo.lock
@@ -58,7 +58,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -67,7 +67,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -75,7 +75,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-basic/Cargo.lock b/examples/example-basic/Cargo.lock
index 6be449c..e38b91d 100644
--- a/examples/example-basic/Cargo.lock
+++ b/examples/example-basic/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-clap-binding/Cargo.lock b/examples/example-clap-binding/Cargo.lock
index 46d334e..1eb895f 100644
--- a/examples/example-clap-binding/Cargo.lock
+++ b/examples/example-clap-binding/Cargo.lock
@@ -166,7 +166,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -174,7 +174,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -182,7 +182,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-combine-pathf-dispatch-tree/Cargo.lock b/examples/example-combine-pathf-dispatch-tree/Cargo.lock
index 8560b73..9fdbf4f 100644
--- a/examples/example-combine-pathf-dispatch-tree/Cargo.lock
+++ b/examples/example-combine-pathf-dispatch-tree/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -74,7 +74,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
@@ -84,7 +84,7 @@ dependencies = [
[[package]]
name = "mingling_pathf"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-combine-pathf-metadata/Cargo.lock b/examples/example-combine-pathf-metadata/Cargo.lock
index 745167a..0e1782e 100644
--- a/examples/example-combine-pathf-metadata/Cargo.lock
+++ b/examples/example-combine-pathf-metadata/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -74,7 +74,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
@@ -84,7 +84,7 @@ dependencies = [
[[package]]
name = "mingling_pathf"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-command-macro/Cargo.lock b/examples/example-command-macro/Cargo.lock
index cd1babf..53d6f18 100644
--- a/examples/example-command-macro/Cargo.lock
+++ b/examples/example-command-macro/Cargo.lock
@@ -74,7 +74,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"arg-picker",
"mingling_core",
@@ -83,7 +83,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -91,7 +91,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-completion/Cargo.lock b/examples/example-completion/Cargo.lock
index dab3f37..97401e7 100644
--- a/examples/example-completion/Cargo.lock
+++ b/examples/example-completion/Cargo.lock
@@ -84,7 +84,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -93,7 +93,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"just_template",
@@ -102,7 +102,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"proc-macro2",
diff --git a/examples/example-custom-pickable/Cargo.lock b/examples/example-custom-pickable/Cargo.lock
index 11f417a..3aa6236 100644
--- a/examples/example-custom-pickable/Cargo.lock
+++ b/examples/example-custom-pickable/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -66,7 +66,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -74,7 +74,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-dispatch-tree/Cargo.lock b/examples/example-dispatch-tree/Cargo.lock
index 015d536..b80a18e 100644
--- a/examples/example-dispatch-tree/Cargo.lock
+++ b/examples/example-dispatch-tree/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-enum-tag/Cargo.lock b/examples/example-enum-tag/Cargo.lock
index b9de59b..868af79 100644
--- a/examples/example-enum-tag/Cargo.lock
+++ b/examples/example-enum-tag/Cargo.lock
@@ -84,7 +84,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -93,7 +93,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"just_template",
@@ -102,7 +102,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"proc-macro2",
diff --git a/examples/example-error-handling/Cargo.lock b/examples/example-error-handling/Cargo.lock
index f03ca4d..3110ae9 100644
--- a/examples/example-error-handling/Cargo.lock
+++ b/examples/example-error-handling/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-exitcode/Cargo.lock b/examples/example-exitcode/Cargo.lock
index 2924262..37170f2 100644
--- a/examples/example-exitcode/Cargo.lock
+++ b/examples/example-exitcode/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-help/Cargo.lock b/examples/example-help/Cargo.lock
index 5a482a5..9c39a41 100644
--- a/examples/example-help/Cargo.lock
+++ b/examples/example-help/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-hook/Cargo.lock b/examples/example-hook/Cargo.lock
index 588e6ef..e4d68ce 100644
--- a/examples/example-hook/Cargo.lock
+++ b/examples/example-hook/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-implicit-dispatcher/Cargo.lock b/examples/example-implicit-dispatcher/Cargo.lock
index f9f8838..9ca0f10 100644
--- a/examples/example-implicit-dispatcher/Cargo.lock
+++ b/examples/example-implicit-dispatcher/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-lazy-resources/Cargo.lock b/examples/example-lazy-resources/Cargo.lock
index 0786373..21cb251 100644
--- a/examples/example-lazy-resources/Cargo.lock
+++ b/examples/example-lazy-resources/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-metadata/Cargo.lock b/examples/example-metadata/Cargo.lock
index 8517b6a..bfa1d01 100644
--- a/examples/example-metadata/Cargo.lock
+++ b/examples/example-metadata/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-outside-type/Cargo.lock b/examples/example-outside-type/Cargo.lock
index a12fc80..49c8c6f 100644
--- a/examples/example-outside-type/Cargo.lock
+++ b/examples/example-outside-type/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-pack-err/Cargo.lock b/examples/example-pack-err/Cargo.lock
index 1bc9022..bcfbb41 100644
--- a/examples/example-pack-err/Cargo.lock
+++ b/examples/example-pack-err/Cargo.lock
@@ -64,7 +64,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -83,7 +83,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-panic-unwind/Cargo.lock b/examples/example-panic-unwind/Cargo.lock
index 6481eb4..ec5492d 100644
--- a/examples/example-panic-unwind/Cargo.lock
+++ b/examples/example-panic-unwind/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -66,7 +66,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -74,7 +74,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-pathfinder/Cargo.lock b/examples/example-pathfinder/Cargo.lock
index e00aa91..b93e443 100644
--- a/examples/example-pathfinder/Cargo.lock
+++ b/examples/example-pathfinder/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -74,7 +74,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
@@ -84,7 +84,7 @@ dependencies = [
[[package]]
name = "mingling_pathf"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-repl-basic/Cargo.lock b/examples/example-repl-basic/Cargo.lock
index d4d5bd9..e85083d 100644
--- a/examples/example-repl-basic/Cargo.lock
+++ b/examples/example-repl-basic/Cargo.lock
@@ -64,7 +64,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"might_be_async",
@@ -81,7 +81,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"proc-macro2",
diff --git a/examples/example-resources/Cargo.lock b/examples/example-resources/Cargo.lock
index a559bdb..9623e25 100644
--- a/examples/example-resources/Cargo.lock
+++ b/examples/example-resources/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -66,7 +66,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -74,7 +74,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-setup/Cargo.lock b/examples/example-setup/Cargo.lock
index 47d36e5..c8e0e34 100644
--- a/examples/example-setup/Cargo.lock
+++ b/examples/example-setup/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-structural-renderer/Cargo.lock b/examples/example-structural-renderer/Cargo.lock
index c3f24c7..ac4cbba 100644
--- a/examples/example-structural-renderer/Cargo.lock
+++ b/examples/example-structural-renderer/Cargo.lock
@@ -64,7 +64,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -74,7 +74,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -85,7 +85,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/example-unit-test/Cargo.lock b/examples/example-unit-test/Cargo.lock
index a1b4e59..455fa9d 100644
--- a/examples/example-unit-test/Cargo.lock
+++ b/examples/example-unit-test/Cargo.lock
@@ -57,7 +57,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -73,7 +73,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/examples/full-todolist/Cargo.lock b/examples/full-todolist/Cargo.lock
index 28d551b..4fc964d 100644
--- a/examples/full-todolist/Cargo.lock
+++ b/examples/full-todolist/Cargo.lock
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -75,7 +75,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -85,7 +85,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/index.html b/index.html
index 2f6aa4f..15a72d6 100644
--- a/index.html
+++ b/index.html
@@ -26,6 +26,15 @@
html {
scroll-behavior: smooth;
+ scroll-snap-type: y mandatory;
+ }
+
+ .hero,
+ .code-demo,
+ .code-demo-mirror {
+ scroll-snap-align: start;
+ scroll-snap-stop: always;
+ min-height: 100vh;
}
body {
@@ -38,6 +47,102 @@
overflow-x: hidden;
}
+ /* 田字格背景(hero 以下的内容区) */
+ body > section:not(.hero):not(.cta-banner) {
+ background-image:
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ );
+ background-size:
+ 40px 40px,
+ 40px 40px,
+ 20px 20px,
+ 20px 20px;
+ background-position:
+ calc(50% - 540px + 20px) 0,
+ calc(50% - 540px) 0,
+ calc(50% - 540px + 10px) 0,
+ calc(50% - 540px + 10px) 0;
+ }
+ body > section:not(.hero) {
+ position: relative;
+ }
+
+ body > section:not(.hero)::before,
+ body > section:not(.hero)::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: repeating-linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.25) 0,
+ rgba(212, 168, 75, 0.25) 4px,
+ transparent 4px,
+ transparent 9px
+ );
+ pointer-events: none;
+ z-index: 49;
+ }
+
+ body > section:not(.hero)::before {
+ left: max(0px, calc(50% - 540px));
+ }
+
+ body > section:not(.hero)::after {
+ right: max(0px, calc(50% - 540px));
+ }
+
+ /* Diamond markers at line intersections */
+ .line-diamond {
+ position: absolute;
+ top: -4px;
+ width: 8px;
+ height: 8px;
+ background: rgba(212, 168, 75, 0.25);
+ transform: rotate(45deg);
+ pointer-events: none;
+ z-index: 50;
+ }
+
+ .line-diamond.left {
+ left: calc(max(0px, calc(50% - 540px)) - 4px);
+ }
+
+ .line-diamond.right {
+ right: calc(max(0px, calc(50% - 540px)) - 4px);
+ }
+
+ /* Horizontal dashed dividers between pages (border-image, same style as vertical lines) */
+ body > section + section,
+ body > footer {
+ border-top: 1px solid rgba(212, 168, 75, 0.25);
+ border-image: repeating-linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.25) 0 4px,
+ transparent 4px 9px
+ )
+ 1;
+ }
+
a {
color: #d4a84b;
text-decoration: none;
@@ -55,7 +160,7 @@
.container {
max-width: 1080px;
margin: 0 auto;
- padding: 0 1.5rem;
+ position: relative;
}
.section-title {
@@ -149,7 +254,7 @@
.hero::before {
content: "";
position: absolute;
- top: 20%;
+ top: 26%;
left: 50%;
width: 600px;
height: 600px;
@@ -162,6 +267,46 @@
pointer-events: none;
}
+ /* Tiled grid background (field grid) */
+ .hero::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background-image:
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ );
+ background-size:
+ 40px 40px,
+ 40px 40px,
+ 20px 20px,
+ 20px 20px;
+ background-position:
+ calc(50% - 540px + 20px) 0,
+ calc(50% - 540px) 0,
+ calc(50% - 540px + 10px) 0,
+ calc(50% - 540px + 10px) 0;
+ pointer-events: none;
+ z-index: 0;
+ }
+
.hero-content {
display: flex;
flex-direction: column;
@@ -171,6 +316,7 @@
max-width: 820px;
position: relative;
z-index: 1;
+ margin-top: 3rem;
}
.hero h1 {
@@ -305,11 +451,11 @@
@keyframes watermarkIn {
0% {
opacity: 0;
- top: 30%;
+ top: 36%;
}
100% {
opacity: 0.3;
- top: 15%;
+ top: 21%;
}
}
@@ -345,14 +491,47 @@
.code-demo {
padding: 5rem 1.5rem;
- background: #241c16;
+ background-color: #241c16;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.code-demo .code-grid {
display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 3rem;
- align-items: center;
+ grid-template-columns: 60fr 40fr;
+ grid-template-rows: 6fr 4fr;
+ gap: 2rem;
+ align-items: stretch;
+ }
+
+ .code-demo .code-grid > .code-block {
+ grid-row: span 2;
+ }
+
+ .code-demo-mirror {
+ background-color: #1a1410;
+ padding-top: calc(5rem + 1.75rem);
+ }
+
+ .code-demo-mirror .code-grid {
+ grid-template-columns: 38fr 62fr;
+ grid-template-rows: 5fr 5fr;
+ grid-template-areas:
+ "text code"
+ "term code";
+ }
+
+ .code-demo-mirror .code-grid > .code-text {
+ grid-area: text;
+ }
+
+ .code-demo-mirror .code-grid > .terminal-block {
+ grid-area: term;
+ }
+
+ .code-demo-mirror .code-grid > .code-block {
+ grid-area: code;
}
.code-demo .code-text h2 {
@@ -372,11 +551,26 @@
margin-bottom: 1.5rem;
}
+ .code-demo .code-text .star {
+ color: #c43931;
+ margin-right: 0.5rem;
+ }
+
+ .code-demo .code-text p b {
+ color: #e8ddd0;
+ font-weight: 700;
+ }
+
+ .code-demo .code-text .hl-word {
+ color: #c43931;
+ font-weight: 700;
+ }
+
.code-demo .code-text .features-list {
list-style: none;
display: flex;
flex-direction: column;
- gap: 0.6rem;
+ gap: 0.25rem;
}
.code-demo .code-text .features-list li {
@@ -394,13 +588,127 @@
flex-shrink: 0;
}
+ .code-demo .example-link {
+ display: inline-block;
+ margin-top: 1.5rem;
+ padding: 0.5rem 1.25rem;
+ border: 1px solid #d4a84b;
+ border-radius: 2.5px;
+ color: #d4a84b;
+ font-size: 0.9rem;
+ font-weight: 600;
+ font-family: "Segoe UI", sans-serif;
+ transition:
+ background 0.2s,
+ color 0.2s;
+ }
+
+ .code-demo .example-link:hover {
+ background: #d4a84b;
+ color: #1a1410;
+ }
+
+ .terminal-block {
+ background: #000;
+ border: 1px solid #3a2e24;
+ border-radius: 2.5px;
+ padding: 1rem 1.25rem;
+ overflow: hidden;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
+ position: relative;
+ }
+
+ .terminal-block pre {
+ margin: 0;
+ font-family: "JetBrains Mono", "Noto Serif SC", monospace;
+ font-size: 1.33rem;
+ line-height: 1.6;
+ color: #d0d0d0;
+ white-space: pre;
+ position: relative;
+ }
+
+ .term-cursor {
+ display: inline-block;
+ width: 0.6em;
+ height: 1.15em;
+ background: #d0d0d0;
+ vertical-align: text-bottom;
+ margin-left: 1px;
+ animation: termBlink 1s steps(1) infinite;
+ }
+
+ .term-cmd {
+ color: #7ec699;
+ }
+
+ .term-arg {
+ color: #d4a84b;
+ }
+
+ .term-prompt {
+ color: #6a5a4a;
+ }
+
+ .term-out {
+ color: #d0d0d0;
+ }
+
+ @keyframes termBlink {
+ 50% {
+ opacity: 0;
+ }
+ }
+
+ .term-suggest {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(212, 168, 75, 0.07);
+ border-top: 1px solid rgba(212, 168, 75, 0.3);
+ padding: 0.4rem 1.25rem;
+ font-family: "JetBrains Mono", "Noto Serif SC", monospace;
+ font-size: 1.33rem;
+ line-height: 1.5;
+ white-space: pre;
+ display: none;
+ pointer-events: none;
+ }
+
+ .term-suggest .sug-cmd {
+ color: #7ec699;
+ }
+
+ .term-suggest .sug-arg {
+ color: #d4a84b;
+ }
+
+ .term-suggest .sug-desc {
+ color: #6a5a4a;
+ font-size: 0.9rem;
+ }
+
+ .term-suggest .sug-active {
+ background: rgba(212, 168, 75, 0.18);
+ border-radius: 2px;
+ padding: 0 0.35rem;
+ margin: 0 -0.35rem;
+ color: #fff;
+ }
+
+ .term-suggest .sug-active .sug-arg,
+ .term-suggest .sug-active .sug-cmd {
+ color: #ffd97a;
+ }
+
.code-block {
background: #241c16;
border: 1px solid #3a2e24;
- border-radius: 2px;
- padding: 2rem 1.5rem 1.5rem;
+ border-radius: 2.5px;
+ padding: 2.5rem 1.875rem 1.875rem;
overflow-x: auto;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
position: relative;
}
@@ -408,26 +716,44 @@
margin: 0;
padding: 0;
background: transparent;
+ min-height: 440px;
+ position: relative;
}
.code-block code {
font-family: "JetBrains Mono", "Noto Serif SC", monospace;
font-variant-ligatures: contextual;
- font-size: 0.85rem;
+ font-size: 1.0625rem;
line-height: 1.65;
background: transparent !important;
padding: 0 !important;
}
+ .code-line-highlight {
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 28px;
+ background: rgba(255, 255, 255, 0.1);
+ border-left: 2px solid rgba(255, 255, 255, 0.45);
+ pointer-events: none;
+ opacity: 0;
+ transition:
+ opacity 0.15s ease,
+ top 0.08s ease;
+ z-index: 3;
+ }
+
.code-block::before {
- content: "▲ example-basic";
+ content: "▲ main.rs";
position: absolute;
- top: 0.5rem;
- right: 1rem;
- font-size: 0.7rem;
+ top: 0.625rem;
+ right: 1.25rem;
+ font-size: 0.875rem;
color: #6a5a4a;
font-family: "Segoe UI", sans-serif;
letter-spacing: 0.5px;
+ z-index: 2;
}
.features {
@@ -482,13 +808,48 @@
.cta-banner {
padding: 4rem 1.5rem;
text-align: center;
- background: linear-gradient(
- 135deg,
- rgba(212, 168, 75, 0.06),
- transparent
- );
- border-top: 1px solid rgba(212, 168, 75, 0.1);
- border-bottom: 1px solid rgba(212, 168, 75, 0.1);
+ background-image:
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.03) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to right,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ to bottom,
+ rgba(212, 168, 75, 0.015) 1px,
+ transparent 1px
+ ),
+ linear-gradient(
+ 135deg,
+ rgba(212, 168, 75, 0.06),
+ transparent
+ );
+ background-size:
+ 40px 40px,
+ 40px 40px,
+ 20px 20px,
+ 20px 20px,
+ 100% 100%;
+ background-position:
+ calc(50% - 540px + 20px) 0,
+ calc(50% - 540px) 0,
+ calc(50% - 540px + 10px) 0,
+ calc(50% - 540px + 10px) 0,
+ 0 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
}
.cta-banner h2 {
@@ -498,6 +859,11 @@
margin-bottom: 0.75rem;
}
+ .cta-banner h2 .star {
+ color: #c43931;
+ margin-right: 0.5rem;
+ }
+
.cta-banner p {
color: #9a8a7a;
margin-bottom: 1.5rem;
@@ -524,12 +890,113 @@
margin-top: 0.5rem;
}
+ .feature-carousel {
+ position: relative;
+ max-width: 600px;
+ margin: 1.25rem auto 0;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ }
+
+ .carousel-viewport {
+ flex: 1;
+ overflow: hidden;
+ }
+
+ .carousel-track {
+ display: flex;
+ transition: transform 0.4s ease;
+ }
+
+ .carousel-track > .carousel-code {
+ flex: 0 0 100%;
+ margin: 0;
+ text-align: left;
+ background: #0f0b08;
+ border: 1px solid #3a2e24;
+ border-radius: 2.5px;
+ padding: 0;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
+ overflow: hidden;
+ position: relative;
+ box-sizing: border-box;
+ aspect-ratio: 16 / 9;
+ }
+
+ .carousel-track > .carousel-code code {
+ font-family: "JetBrains Mono", "Noto Serif SC", monospace;
+ font-size: 0.95rem;
+ line-height: 1.6;
+ background: transparent !important;
+ padding: 1.5rem !important;
+ margin: 0;
+ display: block;
+ border: none;
+ border-radius: 0;
+ }
+
+ .carousel-track > .carousel-code .code-line-highlight {
+ left: 1.5rem;
+ right: 1.5rem;
+ }
+
+ .carousel-arrow {
+ background: transparent;
+ border: 1px solid rgba(212, 168, 75, 0.4);
+ color: #d4a84b;
+ font-size: 1.5rem;
+ width: 2.4rem;
+ height: 2.4rem;
+ border-radius: 50%;
+ cursor: pointer;
+ flex-shrink: 0;
+ line-height: 1;
+ padding: 0;
+ transition:
+ background 0.2s,
+ color 0.2s;
+ }
+
+ .carousel-arrow:hover {
+ background: #d4a84b;
+ color: #1a1410;
+ }
+
footer {
- padding: 2rem 1.5rem;
+ padding: 3rem 1.5rem 2rem;
text-align: center;
color: #6a5a4a;
font-size: 0.85rem;
border-top: 1px solid #2a1e14;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 2rem;
+ }
+
+ .footer-links {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 0.6rem 1.5rem;
+ max-width: 960px;
+ width: 100%;
+ text-align: left;
+ }
+
+ .footer-links p {
+ margin: 0;
+ color: #7a6a5a;
+ line-height: 1.7;
+ }
+
+ .footer-links a {
+ color: #7a6a5a;
+ }
+
+ .footer-links a:hover {
+ color: #b0a090;
}
footer a {
@@ -558,6 +1025,28 @@
z-index: 9999;
}
+ /* ── Cursor glow dot ── */
+ .cursor-dot {
+ position: fixed;
+ pointer-events: none;
+ border-radius: 50%;
+ width: 400px;
+ height: 400px;
+ background: radial-gradient(
+ circle,
+ rgba(212, 168, 75, 0.06) 0%,
+ rgba(212, 168, 75, 0.02) 45%,
+ transparent 70%
+ );
+ transform: translate(-50%, -50%);
+ z-index: 10000;
+ opacity: 0;
+ transition:
+ opacity 0.3s ease,
+ left 0.12s ease-out,
+ top 0.12s ease-out;
+ }
+
@keyframes inkSpread {
0% {
width: 0;
@@ -572,10 +1061,24 @@
}
@media (max-width: 768px) {
+ html {
+ scroll-snap-type: none;
+ }
+
+ .hero,
+ .code-demo,
+ .code-demo-mirror {
+ min-height: auto;
+ }
+
nav {
padding: 0.6rem 1rem;
}
+ .footer-links {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
nav .nav-links {
gap: 1rem;
font-size: 0.8rem;
@@ -603,9 +1106,23 @@
gap: 2rem;
}
+ .code-demo .code-grid > .code-block {
+ grid-row: auto;
+ }
+
+ .code-demo-mirror .code-grid {
+ grid-template-columns: 1fr;
+ grid-template-rows: auto auto auto;
+ grid-template-areas:
+ "code"
+ "text"
+ "term";
+ }
+
.code-block {
font-size: 0.75rem;
padding: 1rem;
+ transform: none;
}
}
@@ -618,10 +1135,6 @@
flex-direction: column;
align-items: center;
}
-
- .features-grid {
- grid-template-columns: 1fr;
- }
}
</style>
</head>
@@ -662,7 +1175,7 @@
alt=""
style="
position: absolute;
- top: 30%;
+ top: 36%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
@@ -679,12 +1192,13 @@
<h1>#[Mìng&nbsp;Lìng]</h1>
<p class="slogan" id="typewriter"></p>
- <p class="desc">A <b>proc-macro based</b> Rust CLI framework</p>
+ <p class="desc">
+ A <b>state-driven and data-driven</b> CLI workflow
+ orchestration framework
+ </p>
<div class="cta-row">
- <a href="docs/doc.html" class="btn-primary">
- 📖 Read The Docs
- </a>
+ <a href="docs/doc.html" class="btn-primary"> Documents </a>
<a
href="https://github.com/mingling-rs/mingling"
target="_blank"
@@ -704,162 +1218,157 @@
<!-- Code Demo -->
<section class="code-demo">
<div class="container code-grid">
+ <div class="code-block">
+ <pre><code class="language-rust">
+#[derive(Grouped)]
+pub struct ResultHello(String);
+
+#[command]
+pub fn hello(args: Entry) -> ResultHello {
+ let name = args
+ .pick_or(&amp;arg![String], || "World".into())
+ .unwrap();
+ ResultHello(name)
+}
+
+#[renderer(buffer)]
+pub fn render_hello(hello: ResultHello) {
+ r_println!("Hello, {}", hello.0);
+}
+ </code></pre>
+ </div>
+
<div class="code-text">
<h2>
- Dispatch, Parse<br />
- Then <span>Render</span>
+ One Command,<br />
+ One <span>Function.</span>
</h2>
<p>
- A proc-macro &amp; type-system CLI framework for
- building complex command-line programs with many
- subcommands.
+ <span class="star">✦</span> <b>Command</b> is just a
+ <span class="hl-word">Function</span>,<br />
+ <span class="star">✦</span> <b>Renderer</b> is just a
+ <span class="hl-word">Function</span>,<br />
+ <span class="star">✦</span> <b>Mingling</b> wires them
+ together<br />
+ <span class="star">✦</span> with no builder boilerplate.
</p>
<ul class="features-list">
<li>
- Separation of concerns: parsing, logic, rendering
+ <code>#[command]</code> turns a function into a
+ subcommand
</li>
<li>
- Compile-time prefix-tree routing
- <a
- href="docs/example-viewer.html?name=example-dispatch-tree"
- >(optional)</a
- >
+ <code>#[renderer]</code> — output, decoupled from
+ logic
</li>
<li>
- Dynamic shell completion out of the box
- <a
- href="docs/example-viewer.html?name=example-completion"
- >(optional)</a
- >
+ <code>pick / pick_or</code> — typed args via
+ arg-picker
</li>
</ul>
</div>
+ <div class="terminal-block">
+ <pre><code class="no-highlight" id="term-output">~# <s></s><span class="term-cmd">my-cli</span> <span class="term-arg">hello</span><s></s>
+<span class="instant">Hello, World!</span>
+~# <s></s><span class="term-cmd">my-cli</span> <span class="term-arg">hello</span><s></s> <span class="term-arg">Mingling</span><s></s>
+<span class="instant">Hello, Mingling!</span></code></pre>
+ </div>
+ </div>
+ </section>
+
+ <!-- Code Demo (mirrored) -->
+ <section class="code-demo code-demo-mirror">
+ <div class="container code-grid">
<div class="code-block">
<pre><code class="language-rust">
-use mingling::prelude::*;
-
-dispatcher!("greet", CMDGreet => EntryGreet);
-
-fn main() {
- let mut program = ThisProgram::new();
- program.with_dispatcher(CMDGreet);
- program.exec_and_exit();
-}
-
-pack!(ResultGreeting = String);
-
-#[chain]
-fn handle_greet(args: EntryGreet) -> Next {
- let name =
- args.pick_or((), "World").unpack();
- ResultGreeting::new(name)
+#[command(entry = EntryHello)]
+pub fn hello() { /* ... */ }
+
+#[command]
+pub fn bye() { /* ... */ }
+
+#[completion(EntryHello)]
+pub fn complete_hello(ctx: &amp;ShellContext) -&gt; Suggest {
+ if ctx.previous_word == "hello" {
+ suggest! {
+ "Alice": "The Sender",
+ "Bob": "The Receiver",
+ }
+ } else {
+ suggest! {}
+ }
}
-
-#[renderer]
-fn render_greet(result: ResultGreeting) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *result).ok();
- r
-}
-
-gen_program!();
</code></pre>
</div>
- </div>
- </section>
-
- <!-- Features -->
- <section class="features">
- <div class="container">
- <h2 class="section-title">
- <span>Mìng&nbsp;Lìng</span> — Build Complex CLIs, For You!
- </h2>
-
- <div class="features-grid">
- <div class="feature-card">
- <span class="icon">🧩</span>
- <h3>Separation of Concerns</h3>
- <p>
- Decouple parsing, business logic, rendering, help
- text, and completion — each is just a function with
- the right attribute macro.
- </p>
- </div>
-
- <div class="feature-card">
- <span class="icon">⚡</span>
- <h3>Blazing Dispatch</h3>
- <p>
- With the <code>dispatch_tree</code> feature, your
- subcommand structure is hardened into a prefix tree
- at compile time. O(len) lookup.
- </p>
- </div>
- <div class="feature-card">
- <span class="icon">🔄</span>
- <h3>Dynamic Completion</h3>
- <p>
- Enable <code>comp</code> and get smart,
- context-aware shell completions for bash, zsh, fish,
- and pwsh — no manual registration.
- </p>
- </div>
-
- <div class="feature-card">
- <span class="icon">📦</span>
- <h3>Lightweight &amp; Modular</h3>
- <p>
- Minimal core dependencies. Pull in advanced features
- (REPL, structured output, Clap binding) only when
- you need them via feature flags.
- </p>
- </div>
-
- <div class="feature-card">
- <span class="icon">📤</span>
- <h3>Structured Output</h3>
- <p>
- Add <code>general_renderer</code> and your program
- gains <code>--json</code> /
- <code>--yaml</code> flags automatically. Great for
- scripting and pipe workflows.
- </p>
- </div>
+ <div class="code-text">
+ <h2>
+ Type, Tab,<br />
+ <span>Suggest.</span>
+ </h2>
+ <p>
+ <span class="star">✦</span> <b>hello</b> offers Alice
+ &amp; Bob,<br />
+ <span class="star">✦</span> <b>bye</b> gets a friendly
+ farewell,<br />
+ <span class="star">✦</span> <b>Mingling</b> completes by
+ context —<br />
+ <span class="star">✦</span> suggestions born at runtime.
+ </p>
+ <a
+ href="docs/example-viewer.html?name=example-completion"
+ class="example-link"
+ >View Full Example →</a
+ >
+ </div>
- <div class="feature-card">
- <span class="icon">🔁</span>
- <h3>REPL Mode</h3>
- <p>
- Call <code>program.exec_repl()</code> and your CLI
- becomes an interactive shell — perfect for debugging
- and exploration.
- </p>
- </div>
+ <div class="terminal-block">
+ <pre><code class="no-highlight term-sequence"></code></pre>
</div>
</div>
</section>
<!-- CTA Banner -->
<section class="cta-banner">
- <h2>Ready to command your CLI?</h2>
- <p>
- Add Mingling to your project in one line, or dive straight into
- the docs.
- </p>
- <code>cargo add mingling</code>
- <div class="cta-row" style="margin-top: 1.5rem">
- <a href="docs/doc.html" class="btn-primary">
- 📖 Read The Docs
- </a>
- <a
- href="https://github.com/mingling-rs/mingling"
- target="_blank"
- class="btn-secondary"
- >
- GitHub →
- </a>
+ <h2><span class="star">✦</span> Ready to command your CLI?</h2>
+ <p>You can choose a way to install</p>
+
+ <div class="feature-carousel">
+ <button class="carousel-arrow prev" aria-label="Previous">
+ ‹
+ </button>
+ <div class="carousel-viewport">
+ <div class="carousel-track">
+ <pre
+ class="carousel-code"
+ ><code class="language-bash"># Initialize your project
+cargo init
+
+# Add Mingling
+cargo add mingling@0.5.0</code></pre>
+ <pre
+ class="carousel-code"
+ ><code class="language-toml"># Add the Mingling dependency
+[dependencies.mingling]
+version = "0.5.0"
+features = [
+ "extras", # extra macro support
+ "picker", # argument picker
+]</code></pre>
+ <pre
+ class="carousel-code"
+ ><code class="language-bash"># Download and install mingling-cli
+# https://mingling-rs.github.io/mingling/dist/
+
+mling proj-init 0.5.0@basic
+nano checklist.toml # Edit
+
+# Run `proj-init` again to deploy
+mling proj-init</code></pre>
+ </div>
+ </div>
+ <button class="carousel-arrow next" aria-label="Next">›</button>
</div>
</section>
@@ -868,6 +1377,43 @@ gen_program!();
<script src="docs/scripts/highlight/rust.min.js"></script>
<script>
hljs.highlightAll();
+
+ // Highlight the hovered line (code blocks + terminal blocks)
+ function attachLineHighlight(container) {
+ var pre =
+ container.tagName === "PRE"
+ ? container
+ : container.querySelector("pre");
+ var code = pre.querySelector("code");
+ var hl = document.createElement("div");
+ hl.className = "code-line-highlight";
+ pre.appendChild(hl);
+
+ var lineHeight = parseFloat(getComputedStyle(code).lineHeight);
+ var padTop = parseFloat(getComputedStyle(code).paddingTop) || 0;
+
+ pre.addEventListener("mousemove", function (e) {
+ var rect = pre.getBoundingClientRect();
+ var y = e.clientY - rect.top;
+ var index = Math.floor((y - padTop) / lineHeight);
+ var lineCount = code.innerText.split("\n").length;
+ if (index < 0) index = 0;
+ if (index >= lineCount) index = lineCount - 1;
+ hl.style.top = padTop + index * lineHeight + "px";
+ hl.style.height = lineHeight + "px";
+ hl.style.opacity = "1";
+ });
+
+ pre.addEventListener("mouseleave", function () {
+ hl.style.opacity = "0";
+ });
+ }
+
+ document
+ .querySelectorAll(
+ ".code-block, .terminal-block, .carousel-code",
+ )
+ .forEach(attachLineHighlight);
</script>
<script>
@@ -899,6 +1445,19 @@ gen_program!();
</script>
<script>
+ // Persistent glow dot following the mouse
+ var cursorDot = document.createElement("div");
+ cursorDot.className = "cursor-dot";
+ document.body.appendChild(cursorDot);
+
+ document.addEventListener("mousemove", function (e) {
+ cursorDot.style.left = e.clientX + "px";
+ cursorDot.style.top = e.clientY + "px";
+ if (cursorDot.style.opacity !== "1") {
+ cursorDot.style.opacity = "1";
+ }
+ });
+
document.addEventListener("click", function (e) {
var drop = document.createElement("div");
drop.className = "ink-drop";
@@ -911,7 +1470,490 @@ gen_program!();
});
</script>
+ <script>
+ // Feature carousel (infinite loop via cloned first/last cards)
+ (function () {
+ var track = document.querySelector(
+ ".feature-carousel .carousel-track",
+ );
+ if (!track) return;
+ var cards = Array.prototype.slice.call(track.children);
+ var count = cards.length;
+ if (count < 2) return;
+
+ // Clone first and last cards
+ var firstClone = cards[0].cloneNode(true);
+ var lastClone = cards[count - 1].cloneNode(true);
+ track.appendChild(firstClone);
+ track.insertBefore(lastClone, cards[0]);
+
+ var total = count + 2;
+ var index = 1; // start from the first real card
+ var animating = false;
+ var prev = document.querySelector(".feature-carousel .prev");
+ var next = document.querySelector(".feature-carousel .next");
+
+ function setTransform(animate) {
+ track.style.transition = animate
+ ? "transform 0.4s ease"
+ : "none";
+ track.style.transform = "translateX(-" + index * 100 + "%)";
+ }
+
+ function update(animate) {
+ setTransform(animate);
+ if (animate) {
+ animating = true;
+ setTimeout(function () {
+ animating = false;
+ if (index === total - 1) {
+ index = 1;
+ setTransform(false);
+ } else if (index === 0) {
+ index = total - 2;
+ setTransform(false);
+ }
+ }, 400);
+ }
+ }
+
+ next.addEventListener("click", function () {
+ if (animating) return;
+ index = Math.min(total - 1, index + 1);
+ update(true);
+ });
+
+ prev.addEventListener("click", function () {
+ if (animating) return;
+ index = Math.max(0, index - 1);
+ update(true);
+ });
+
+ setTransform(false);
+ })();
+ </script>
+
+ <script>
+ // Disable full-page snap after the mirrored page so the CTA area is reachable
+ (function () {
+ var html = document.documentElement;
+ var mirror = document.querySelector(".code-demo-mirror");
+ if (!mirror) return;
+ var disabled = false;
+ var mirrorTop = mirror.offsetTop;
+
+ window.addEventListener(
+ "wheel",
+ function (e) {
+ // Scrolling down near the mirrored page: release the snap early
+ if (
+ e.deltaY > 0 &&
+ window.scrollY >= mirrorTop - 5 &&
+ !disabled
+ ) {
+ html.style.scrollSnapType = "none";
+ disabled = true;
+ }
+ },
+ { passive: true },
+ );
+
+ window.addEventListener(
+ "scroll",
+ function () {
+ // Restore the snap when back above the mirrored page
+ if (disabled && window.scrollY < mirrorTop) {
+ html.style.scrollSnapType = "";
+ disabled = false;
+ }
+ },
+ { passive: true },
+ );
+ })();
+ </script>
+
+ <script>
+ // Insert diamond decorations at the line intersections
+ (function () {
+ document
+ .querySelectorAll("body > section + section, body > footer")
+ .forEach(function (el) {
+ var left = document.createElement("span");
+ left.className = "line-diamond left";
+ var right = document.createElement("span");
+ right.className = "line-diamond right";
+ el.appendChild(left);
+ el.appendChild(right);
+ });
+ })();
+ </script>
+
+ <script>
+ // Terminal typewriter effect: <s></s> marks pauses, loops after a 5s pause
+ (function () {
+ function attachTypewriter(term) {
+ var steps = [];
+ var parser = document.createElement("div");
+ parser.innerHTML = term.innerHTML;
+
+ function walk(node, cls) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ var text = node.textContent;
+ for (var i = 0; i < text.length; i++) {
+ steps.push({
+ type: "char",
+ ch: text[i],
+ cls: cls,
+ });
+ }
+ } else if (node.nodeName === "S") {
+ steps.push({ type: "pause", ms: 500 });
+ } else if (node.nodeName === "SPAN") {
+ if (
+ (node.className || "").indexOf("instant") !== -1
+ ) {
+ steps.push({
+ type: "instant",
+ text: node.textContent,
+ });
+ } else {
+ var newCls = node.className || cls;
+ for (
+ var j = 0;
+ j < node.childNodes.length;
+ j++
+ ) {
+ walk(node.childNodes[j], newCls);
+ }
+ }
+ } else {
+ for (var k = 0; k < node.childNodes.length; k++) {
+ walk(node.childNodes[k], cls);
+ }
+ }
+ }
+ for (var k = 0; k < parser.childNodes.length; k++) {
+ walk(parser.childNodes[k], "");
+ }
+
+ var cursor = document.createElement("span");
+ cursor.className = "term-cursor";
+
+ var index = 0;
+
+ function tick() {
+ if (index >= steps.length) {
+ setTimeout(reset, 5000);
+ return;
+ }
+ var s = steps[index];
+ index++;
+ if (s.type === "pause") {
+ setTimeout(tick, s.ms);
+ return;
+ }
+ if (s.type === "instant") {
+ cursor.insertAdjacentText("beforebegin", s.text);
+ setTimeout(tick, 250);
+ return;
+ }
+ if (s.cls) {
+ var span = document.createElement("span");
+ span.className = s.cls;
+ span.textContent = s.ch;
+ term.insertBefore(span, cursor);
+ } else {
+ cursor.insertAdjacentText("beforebegin", s.ch);
+ }
+ setTimeout(tick, s.ch === "\n" ? 180 : 45);
+ }
+
+ function reset() {
+ while (term.firstChild && term.firstChild !== cursor) {
+ term.removeChild(term.firstChild);
+ }
+ index = 0;
+ setTimeout(tick, 400);
+ }
+
+ while (term.firstChild) {
+ term.removeChild(term.firstChild);
+ }
+ term.appendChild(cursor);
+ setTimeout(tick, 1200);
+ }
+
+ document
+ .querySelectorAll(
+ ".terminal-block code:not(.term-sequence)",
+ )
+ .forEach(attachTypewriter);
+ })();
+ </script>
+
+ <script>
+ // Terminal completion animation sequence engine (for term-sequence terminals)
+ (function () {
+ var sequence = [
+ {
+ type: "type",
+ text: "~# ",
+ speed: 45,
+ cls: "term-prompt",
+ },
+ {
+ type: "type",
+ text: "my-cli",
+ speed: 45,
+ cls: "term-cmd",
+ },
+ { type: "type", text: " ", speed: 45 },
+ { type: "wait", ms: 600 },
+ {
+ type: "suggest",
+ lines: [
+ { text: "hello", active: true },
+ { text: "bye" },
+ ],
+ ms: 1200,
+ },
+ { type: "instant", text: "hello", cls: "term-arg" },
+ { type: "instant", text: " " },
+ { type: "clear-suggest" },
+ { type: "wait", ms: 500 },
+ {
+ type: "suggest",
+ lines: [
+ { text: "Alice", desc: "The Sender", active: true },
+ { text: "Bob", desc: "The Receiver" },
+ ],
+ ms: 1200,
+ },
+ { type: "instant", text: "Alice", cls: "term-arg" },
+ { type: "clear-suggest" },
+ { type: "wait", ms: 600 },
+ { type: "enter" },
+ { type: "instant", text: "Hello, Alice!", cls: "term-out" },
+ { type: "wait", ms: 500 },
+ { type: "enter" },
+ {
+ type: "type",
+ text: "~# ",
+ speed: 45,
+ cls: "term-prompt",
+ },
+ {
+ type: "type",
+ text: "my-cli",
+ speed: 45,
+ cls: "term-cmd",
+ },
+ { type: "type", text: " ", speed: 45 },
+ { type: "wait", ms: 600 },
+ { type: "type", text: "b", speed: 350, cls: "term-arg" },
+ { type: "wait", ms: 500 },
+ {
+ type: "suggest",
+ lines: [{ text: "bye", active: true }],
+ ms: 800,
+ },
+ { type: "instant", text: "ye", cls: "term-arg" },
+ { type: "clear-suggest" },
+ { type: "wait", ms: 600 },
+ { type: "enter" },
+ { type: "instant", text: "Goodbye!", cls: "term-out" },
+ { type: "wait", ms: 500 },
+ { type: "enter" },
+ ];
+
+ function attachSequence(term) {
+ var wrap = term.closest(".terminal-block");
+ var suggestEl = document.createElement("div");
+ suggestEl.className = "term-suggest";
+ wrap.appendChild(suggestEl);
+
+ var cursor = document.createElement("span");
+ cursor.className = "term-cursor";
+
+ function setSuggest(lines) {
+ suggestEl.innerHTML = "";
+ lines.forEach(function (l) {
+ var row = document.createElement("div");
+ if (l.desc) {
+ row.innerHTML =
+ '<span class="sug-arg">' +
+ l.text +
+ '</span> <span class="sug-desc">-- ' +
+ l.desc +
+ "</span>";
+ } else {
+ row.innerHTML =
+ '<span class="sug-cmd">' +
+ l.text +
+ "</span>";
+ }
+ if (l.active) {
+ row.className = "sug-active";
+ }
+ suggestEl.appendChild(row);
+ });
+ suggestEl.style.display = "block";
+ }
+
+ function clearSuggest() {
+ suggestEl.style.display = "none";
+ suggestEl.innerHTML = "";
+ }
+
+ function typeText(text, speed, cls, done) {
+ if (text.length === 0) {
+ done();
+ return;
+ }
+ var i = 0;
+ (function step() {
+ if (cls) {
+ var span = document.createElement("span");
+ span.className = cls;
+ span.textContent = text[i];
+ term.insertBefore(span, cursor);
+ } else {
+ cursor.insertAdjacentText(
+ "beforebegin",
+ text[i],
+ );
+ }
+ i++;
+ if (i < text.length) {
+ setTimeout(step, speed);
+ } else {
+ done();
+ }
+ })();
+ }
+
+ function runSequence(idx) {
+ if (idx >= sequence.length) {
+ setTimeout(reset, 5000);
+ return;
+ }
+ var s = sequence[idx];
+ function next() {
+ runSequence(idx + 1);
+ }
+ switch (s.type) {
+ case "type":
+ typeText(s.text, s.speed || 45, s.cls, next);
+ break;
+ case "instant":
+ if (s.cls) {
+ var span = document.createElement("span");
+ span.className = s.cls;
+ span.textContent = s.text;
+ term.insertBefore(span, cursor);
+ } else {
+ cursor.insertAdjacentText(
+ "beforebegin",
+ s.text,
+ );
+ }
+ next();
+ break;
+ case "enter":
+ cursor.insertAdjacentText("beforebegin", "\n");
+ next();
+ break;
+ case "wait":
+ setTimeout(next, s.ms);
+ break;
+ case "suggest":
+ setSuggest(s.lines);
+ setTimeout(next, s.ms || 800);
+ break;
+ case "clear-suggest":
+ clearSuggest();
+ next();
+ break;
+ }
+ }
+
+ function reset() {
+ clearSuggest();
+ while (term.firstChild && term.firstChild !== cursor) {
+ term.removeChild(term.firstChild);
+ }
+ runSequence(0);
+ }
+
+ term.appendChild(cursor);
+ setTimeout(function () {
+ runSequence(0);
+ }, 1200);
+ }
+
+ document
+ .querySelectorAll(".terminal-block code.term-sequence")
+ .forEach(attachSequence);
+ })();
+ </script>
+
<footer>
+ <div class="footer-links">
+ <p>
+ <a href="dist" target="_blank">Mingling CLI</a>
+ </p>
+ <p>
+ <a
+ href="https://github.com/mingling-rs/mingling"
+ target="_blank"
+ >Repository (github.com)</a
+ >
+ </p>
+ <p>
+ <a
+ href="https://gitee.com/mingling-rs/mingling"
+ target="_blank"
+ >Repository (gitee.com)</a
+ >
+ </p>
+ <p>
+ <a href="https://catilgrass.cn/mingling.git" target="_blank"
+ >Repository (catilgrass.cn)</a
+ >
+ </p>
+ <p>
+ <a href="https://crates.io/crates/mingling" target="_blank"
+ >Library (crates.io)</a
+ >
+ </p>
+ <p><a href="docs/examples.html">Examples</a></p>
+ <p>
+ <a href="docs/doc.html#/" target="_blank">Help Doc</a>
+ </p>
+ <p>
+ <a href="docs/_zh_CN/index.html#/" target="_blank"
+ >帮助文档</a
+ >
+ </p>
+ <p>
+ <a
+ href="https://docs.rs/mingling/latest/mingling/"
+ target="_blank"
+ >API Doc (docs.rs)</a
+ >
+ </p>
+ <p>
+ <a href="docs/api-docs/mingling/" target="_blank"
+ >API Doc (latest)</a
+ >
+ </p>
+ <p>
+ <a href="docs/dev/" target="_blank">Dev Doc</a>
+ </p>
+ <p>
+ <a href="docs/cov-test/" target="_blank">Coverage Test</a>
+ </p>
+ </div>
<p>
Built With ❤️ by
<a href="https://github.com/Weicao-CatilGrass" target="_blank"
diff --git a/mingling/src/confirm.rs b/mingling/src/confirm.rs
new file mode 100644
index 0000000..45a12d8
--- /dev/null
+++ b/mingling/src/confirm.rs
@@ -0,0 +1,5 @@
+mod predicate;
+pub use predicate::*;
+
+mod count;
+pub use count::*;
diff --git a/mingling/src/confirm/count.rs b/mingling/src/confirm/count.rs
new file mode 100644
index 0000000..c9f9db7
--- /dev/null
+++ b/mingling/src/confirm/count.rs
@@ -0,0 +1,75 @@
+/// Specifies the maximum number of attempts for a confirmation prompt.
+///
+/// # Default Implementations
+///
+/// `ConfirmCount` implements the following traits by default:
+///
+/// - [`Debug`] — for formatted output and debugging.
+/// - [`Clone`] — to create a copy of the value.
+/// - [`Copy`] — since the enum holds no heap-allocated data, it can be trivially copied.
+/// - [`PartialEq`] — allows comparing two `ConfirmCount` values for equality.
+/// - [`Eq`] — provides full equality semantics (as opposed to just partial).
+/// - [`From<T>`] for all primitive integer types (`i8`–`i128`, `isize`, `u8`–`u128`, `usize`),
+/// allowing convenient conversion from a raw number.
+///
+/// # What the Numbers Mean
+///
+/// The numeric value passed to a `From` conversion represents the **maximum number of times**
+/// the confirmation prompt will be shown to the user. For example:
+///
+/// - `ConfirmCount::from(3)` → asks at most **3** times before giving up.
+/// - `ConfirmCount::from(1)` → asks exactly **1** time.
+/// - `ConfirmCount::from(0)` → interpreted as [`ConfirmCount::Loop`], meaning it will keep asking
+/// indefinitely until a valid answer is parsed.
+///
+/// # Examples
+///
+/// ```
+/// use mingling::confirm::ConfirmCount;
+///
+/// // Convert from a numeric value
+/// let count: ConfirmCount = 3.into();
+/// assert_eq!(count, ConfirmCount::Max(3));
+///
+/// // Zero means loop forever
+/// let loop_count: ConfirmCount = 0.into();
+/// assert_eq!(loop_count, ConfirmCount::Loop);
+///
+/// // Large values are capped at usize::MAX
+/// let big: ConfirmCount = i128::MAX.into();
+/// assert_eq!(big, ConfirmCount::Max(usize::MAX));
+///
+/// // From a usize directly
+/// let from_usize = ConfirmCount::from(5usize);
+/// assert_eq!(from_usize, ConfirmCount::Max(5));
+/// ```
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ConfirmCount {
+ /// 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_Confirm_count {
+ ($($t:ty),*) => {
+ $(
+ impl From<$t> for ConfirmCount {
+ fn from(n: $t) -> Self {
+ if n == 0 {
+ ConfirmCount::Loop
+ } else {
+ match usize::try_from(n) {
+ Ok(max) => ConfirmCount::Max(max),
+ Err(_) => ConfirmCount::Max(usize::MAX),
+ }
+ }
+ }
+ }
+ )*
+ };
+}
+
+impl_from_for_Confirm_count!(
+ i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
+);
diff --git a/mingling/src/confirm/predicate.rs b/mingling/src/confirm/predicate.rs
new file mode 100644
index 0000000..786a459
--- /dev/null
+++ b/mingling/src/confirm/predicate.rs
@@ -0,0 +1,60 @@
+/// 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 ConfirmPredicate {
+ /// 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 `ConfirmPredicate` 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::ResConfirm;
+/// use mingling::confirm::YesConfirm;
+///
+/// let confirm = ResConfirm::default();
+/// let confirmed = confirm.ask::<YesConfirm>("Continue? [y/n] ");
+/// ```
+pub struct YesConfirm;
+
+/// A `ConfirmPredicate` 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::ResConfirm;
+/// use mingling::confirm::TrueConfirm;
+///
+/// let confirm = ResConfirm::default();
+/// let confirmed = confirm.ask::<TrueConfirm>("Enable this feature? [true/false] ");
+/// ```
+pub struct TrueConfirm;
+
+impl ConfirmPredicate 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 ConfirmPredicate 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/lib.rs b/mingling/src/lib.rs
index 59ef7a6..9d38a2a 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -23,6 +23,9 @@ pub mod CRATE_ROOT {
#[cfg(feature = "core")]
pub mod metadata;
+/// Support for the `OSC 9;4` protocol
+pub mod osc94;
+
#[cfg(feature = "core")]
mod example_docs;
@@ -143,6 +146,9 @@ pub use mingling_macros::Grouped;
#[cfg(feature = "structural_renderer")]
pub use mingling_macros::StructuralData;
+/// Mingling's confirmation module, providing core support for Confirmer
+pub mod confirm;
+
#[doc = include_str!("docs/docsrs_examples.md")]
#[cfg(all(feature = "core", feature = "docs_rs"))]
#[allow(nonstandard_style)]
diff --git a/mingling/src/osc94.rs b/mingling/src/osc94.rs
new file mode 100644
index 0000000..7dabb4a
--- /dev/null
+++ b/mingling/src/osc94.rs
@@ -0,0 +1,5 @@
+mod guard;
+pub use guard::*;
+
+mod state;
+pub use state::*;
diff --git a/mingling/src/osc94/guard.rs b/mingling/src/osc94/guard.rs
new file mode 100644
index 0000000..d17f483
--- /dev/null
+++ b/mingling/src/osc94/guard.rs
@@ -0,0 +1,203 @@
+use crate::osc94::OSC94State;
+
+/// A guard for modifying process state.
+///
+/// Obtained via [`crate::res::ResOSC94::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 [`crate::res::ResOSC94`], and the state is automatically restored to Clean
+/// when the guard is dropped:
+///
+/// ```
+/// use mingling::res::ResOSC94;
+/// use mingling::osc94::OSC94Guard;
+///
+/// let osc94 = ResOSC94::default();
+/// {
+/// let mut guard: OSC94Guard = 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,
+ pub(crate) 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::ResOSC94;
+ /// use mingling::osc94::{OSC94Guard, OSC94State};
+ ///
+ /// let osc94 = ResOSC94::default();
+ /// let mut guard: OSC94Guard = 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::ResOSC94;
+ /// use mingling::osc94::{OSC94Guard, OSC94State};
+ ///
+ /// let osc94 = ResOSC94::default();
+ /// let mut guard: OSC94Guard = 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::ResOSC94;
+ /// use mingling::osc94::{OSC94Guard, OSC94State};
+ ///
+ /// let osc94 = ResOSC94::default();
+ /// let mut guard: OSC94Guard = 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::ResOSC94;
+ /// use mingling::osc94::{OSC94Guard, OSC94State};
+ ///
+ /// let osc94 = ResOSC94::default();
+ /// let mut guard: OSC94Guard = 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::ResOSC94;
+ /// use mingling::osc94::OSC94Guard;
+ ///
+ /// let osc94 = ResOSC94::default();
+ /// let mut guard: OSC94Guard = 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::ResOSC94;
+ /// use mingling::osc94::{OSC94Guard, OSC94State};
+ ///
+ /// let osc94 = ResOSC94::default();
+ /// let guard: OSC94Guard = 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::ResOSC94;
+ /// use mingling::osc94::OSC94Guard;
+ ///
+ /// let osc94 = ResOSC94::default();
+ /// let mut guard: OSC94Guard = osc94.get_mut();
+ /// guard.set_progress(0.25);
+ /// assert_eq!(guard.progress(), 0.25);
+ /// ```
+ #[must_use]
+ pub const fn progress(&self) -> f32 {
+ match self.msg {
+ OSC94State::Normal(progress) => progress,
+ _ => 0.0,
+ }
+ }
+}
+
+impl Drop for OSC94Guard {
+ fn drop(&mut self) {
+ OSC94State::Clean.send();
+ }
+}
diff --git a/mingling/src/osc94/state.rs b/mingling/src/osc94/state.rs
new file mode 100644
index 0000000..1fff7ac
--- /dev/null
+++ b/mingling/src/osc94/state.rs
@@ -0,0 +1,243 @@
+/// `OSC 9;4` protocol message
+///
+/// Used to send task progress notification messages to the terminal via ANSI escape sequences.
+///
+/// This protocol follows the [Windows Terminal Progress Bar Sequences](https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences) specification.
+/// The status codes (0-4) represent: clear progress, normal, error, indeterminate, and warning states, respectively.
+///
+/// # Examples
+///
+/// ```rust
+/// use mingling::osc94::OSC94State;
+///
+/// // Set progress to 50%
+/// let state = OSC94State::Normal(0.5);
+/// assert_eq!(state.state_code(), 1);
+/// assert_eq!(state.progress(), 50.0);
+///
+/// // Generate escape sequence string
+/// let seq = state.to_escape_sequence();
+/// assert_eq!(seq, "\x1b]9;4;1;50\x07");
+///
+/// // Convert to string (Display implementation)
+/// let s = format!("{state}");
+/// assert_eq!(s, "\x1b]9;4;1;50\x07");
+///
+/// // Convert via From
+/// let s2: String = state.into();
+/// assert_eq!(s2, "\x1b]9;4;1;50\x07");
+///
+/// // Error state
+/// let err = OSC94State::Error;
+/// assert_eq!(err.state_code(), 2);
+/// ```
+///
+/// # Use Cases
+///
+/// In command-line tools or scripts, the [`OSC94State::send`] method can be used to directly send progress notifications to the terminal.
+/// Supported terminals include: `Windows Terminal`, `kitty`, `iTerm2`, `WezTerm`, `foot`, etc.
+///
+/// ```
+/// use mingling::osc94::OSC94State;
+///
+/// // Send progress 100%
+/// OSC94State::Normal(1.0).send();
+/// // Send completion (clear) message
+/// OSC94State::Clean.send();
+/// ```
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum OSC94State {
+ /// Clear/hide progress
+ Clean,
+ /// Normal state, corresponding to status code `1`, carries a progress value (0.0 to 1.0)
+ Normal(f32),
+ /// Error state, corresponding to status code `2`
+ Error,
+ /// Indeterminate state, corresponding to status code `3`
+ Unknown,
+ /// Warning state, corresponding to status code `4`
+ Warn,
+}
+
+impl OSC94State {
+ /// Returns the status code for the `OSC 9;4` protocol.
+ ///
+ /// Status code meanings:
+ /// - `0`: Clear progress (`Clean`)
+ /// - `1`: Normal state (`Normal`)
+ /// - `2`: Error state (`Error`)
+ /// - `3`: Indeterminate state (`Unknown`)
+ /// - `4`: Warning state (`Warn`)
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::osc94::OSC94State;
+ ///
+ /// assert_eq!(OSC94State::Clean.state_code(), 0);
+ /// assert_eq!(OSC94State::Normal(0.5).state_code(), 1);
+ /// assert_eq!(OSC94State::Error.state_code(), 2);
+ /// assert_eq!(OSC94State::Unknown.state_code(), 3);
+ /// assert_eq!(OSC94State::Warn.state_code(), 4);
+ /// ```
+ ///
+ /// # Return Value
+ ///
+ /// Returns the corresponding status code (`u8` type), ranging from `0` to `4`.
+ #[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), used for the `Normal` state, clamped to a valid range.
+ ///
+ /// This function converts the progress value (between 0.0 and 1.0) stored in the `Normal` variant
+ /// into a percentage (0 to 100) and rounds it. For non-`Normal` states (such as `Clean`, `Error`,
+ /// `Unknown`, `Warn`), it returns a fixed `0.0`, because only the `Normal` state carries progress information.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::osc94::OSC94State;
+ ///
+ /// // Progress conversion in normal state
+ /// assert_eq!(OSC94State::Normal(0.5).progress(), 50.0);
+ /// assert_eq!(OSC94State::Normal(1.0).progress(), 100.0);
+ /// assert_eq!(OSC94State::Normal(0.0).progress(), 0.0);
+ ///
+ /// // Out-of-range values are clamped to 0-100
+ /// assert_eq!(OSC94State::Normal(1.5).progress(), 100.0);
+ /// assert_eq!(OSC94State::Normal(-0.5).progress(), 0.0);
+ ///
+ /// // Rounding behavior
+ /// assert_eq!(OSC94State::Normal(0.335).progress(), 34.0);
+ /// assert_eq!(OSC94State::Normal(0.999).progress(), 100.0);
+ ///
+ /// // Non-Normal states return 0.0
+ /// assert_eq!(OSC94State::Clean.progress(), 0.0);
+ /// assert_eq!(OSC94State::Error.progress(), 0.0);
+ /// assert_eq!(OSC94State::Unknown.progress(), 0.0);
+ /// assert_eq!(OSC94State::Warn.progress(), 0.0);
+ /// ```
+ ///
+ /// # Return Value
+ ///
+ /// Returns an `f32` progress percentage, ranging from `0.0` to `100.0` (inclusive).
+ /// For the `Normal` state, returns the rounded result of converting its progress value to a percentage;
+ /// for other states, always returns `0.0`.
+ #[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 to the corresponding `OSC 9;4` escape sequence string.
+ ///
+ /// This method generates an ANSI escape sequence conforming to the
+ /// [Windows Terminal Progress Bar Sequences](https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences)
+ /// protocol based on the current state, in the format `\x1b]9;4;{status_code};{progress}\x07`.
+ ///
+ /// Escape sequence format description:
+ /// - `\x1b]`: ESC character followed by `]`, marking the start of an OSC (Operating System Command) sequence.
+ /// - `9;4`: Indicates the `OSC 9;4` protocol (task progress notification).
+ /// - `{status_code}`: Task status, ranging from `0` (clear), `1` (normal), `2` (error), `3` (indeterminate), to `4` (warning).
+ /// - `{progress}`: Task progress percentage (0-100), only meaningful for the `Normal` state.
+ /// - `\x07`: BEL character, marking the end of the OSC sequence.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::osc94::OSC94State;
+ ///
+ /// // Clear progress
+ /// let clean = OSC94State::Clean;
+ /// assert_eq!(clean.to_escape_sequence(), "\x1b]9;4;0;0\x07");
+ ///
+ /// // Normal state, progress 50%
+ /// let normal = OSC94State::Normal(0.5);
+ /// assert_eq!(normal.to_escape_sequence(), "\x1b]9;4;1;50\x07");
+ ///
+ /// // Normal state, progress 100%
+ /// let complete = OSC94State::Normal(1.0);
+ /// assert_eq!(complete.to_escape_sequence(), "\x1b]9;4;1;100\x07");
+ ///
+ /// // Error state
+ /// let error = OSC94State::Error;
+ /// assert_eq!(error.to_escape_sequence(), "\x1b]9;4;2;0\x07");
+ ///
+ /// // Indeterminate state
+ /// let unknown = OSC94State::Unknown;
+ /// assert_eq!(unknown.to_escape_sequence(), "\x1b]9;4;3;0\x07");
+ ///
+ /// // Warning state
+ /// let warn = OSC94State::Warn;
+ /// assert_eq!(warn.to_escape_sequence(), "\x1b]9;4;4;0\x07");
+ /// ```
+ ///
+ /// # Return Value
+ ///
+ /// Returns a `String` containing an ANSI escape sequence conforming to the `OSC 9;4` protocol standard.
+ /// This string can be directly output to a terminal that supports this protocol (such as `Windows Terminal`,
+ /// `kitty`, `iTerm2`, `WezTerm`, `foot`, etc.) to display a task progress notification.
+ #[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.
+ ///
+ /// This method outputs the escape sequence of the current state to standard output and flushes the buffer,
+ /// allowing terminals that support the
+ /// [Windows Terminal Progress Bar Sequences](https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences)
+ /// protocol to display the corresponding task progress notification.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::osc94::OSC94State;
+ ///
+ /// // Send normal state, progress 50%
+ /// OSC94State::Normal(0.5).send();
+ ///
+ /// // Send error state
+ /// OSC94State::Error.send();
+ ///
+ /// // Send clear progress message
+ /// OSC94State::Clean.send();
+ /// ```
+ ///
+ /// # 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/res.rs b/mingling/src/res.rs
index ab524cd..0314e39 100644
--- a/mingling/src/res.rs
+++ b/mingling/src/res.rs
@@ -7,5 +7,8 @@ pub use dirs::*;
mod exit_code;
pub use exit_code::*;
-mod confirmer;
-pub use confirmer::*;
+mod confirm;
+pub use confirm::*;
+
+mod osc94;
+pub use osc94::*;
diff --git a/mingling/src/res/confirm.rs b/mingling/src/res/confirm.rs
new file mode 100644
index 0000000..e07e9a2
--- /dev/null
+++ b/mingling/src/res/confirm.rs
@@ -0,0 +1,184 @@
+use std::io::{BufRead, Write};
+
+use crate::confirm::{ConfirmCount, ConfirmPredicate};
+
+/// A confirm for interactive confirmation.
+///
+/// This structure caches the confirmed state to avoid repeated prompts.
+///
+/// Typically, `ResConfirm` is registered via `ConfirmSetup`, and then injected into functions
+/// through Mingling's resource injection system.
+///
+/// # Registration
+///
+/// Before use, the `ConfirmSetup` must be registered with the program:
+///
+/// ```
+/// # use mingling::MockProgramCollect as ThisProgram;
+/// use mingling::setup::ConfirmSetup;
+/// use mingling::Program;
+///
+/// let mut program = Program::<ThisProgram>::new();
+/// program.with_setup(ConfirmSetup);
+/// ```
+///
+/// # Examples
+///
+/// ```
+/// use mingling::res::ResConfirm;
+/// use mingling::confirm::YesConfirm;
+///
+/// // In actual use, obtain the registered Confirm through the resource injection system
+/// let confirm = ResConfirm::new_confirmed();
+/// assert!(confirm.ask::<YesConfirm>("Continue? [y/n] "));
+/// ```
+#[derive(Debug, Default, Clone, Copy)]
+pub struct ResConfirm {
+ pub(crate) confirmed: bool,
+}
+
+impl ResConfirm {
+ /// Creates a new `ResConfirm` instance.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::res::ResConfirm;
+ ///
+ /// let confirm = ResConfirm::new();
+ /// ```
+ #[must_use]
+ pub const fn new() -> Self {
+ Self { confirmed: false }
+ }
+
+ /// Creates a `Confirm` instance in the confirmed state.
+ ///
+ /// The returned `Confirm` will directly return `true` when calling [`ask`](ResConfirm::ask) or
+ /// [`try_ask`](ResConfirm::try_ask), without prompting the user.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::res::ResConfirm;
+ /// use mingling::confirm::YesConfirm;
+ ///
+ /// let confirm = ResConfirm::new_confirmed();
+ /// assert!(confirm.ask::<YesConfirm>("Continue? [y/n] "));
+ /// ```
+ #[must_use]
+ pub const fn new_confirmed() -> Self {
+ Self { confirmed: true }
+ }
+
+ /// Marks the Confirm as confirmed.
+ ///
+ /// After calling this method, subsequent calls to [`ask`](ResConfirm::ask) or
+ /// [`try_ask`](ResConfirm::try_ask) on this Confirm will directly return `true`
+ /// without prompting the user.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::res::ResConfirm;
+ /// use mingling::confirm::YesConfirm;
+ ///
+ /// let mut confirm = ResConfirm::new();
+ /// confirm.set_confirmed();
+ /// assert!(confirm.ask::<YesConfirm>("Continue? [y/n] "));
+ /// ```
+ pub const fn set_confirmed(&mut self) {
+ self.confirmed = true;
+ }
+
+ /// Asks the user a confirmation question, with at most one attempt.
+ ///
+ /// Returns `false` if the user provides an unrecognizable answer.
+ /// Returns `true` directly if already confirmed previously.
+ ///
+ /// # Parameters
+ ///
+ /// * `ask` - The prompt text to display to the user.
+ ///
+ /// # Returns
+ ///
+ /// Returns a boolean indicating whether the user confirmed. Returns `false` if the user's input
+ /// could not be parsed or the maximum number of attempts was reached.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::res::ResConfirm;
+ /// use mingling::confirm::YesConfirm;
+ ///
+ /// let confirm = ResConfirm::new_confirmed();
+ /// let confirmed = confirm.ask::<YesConfirm>("Delete this file? [y/n] ");
+ /// ```
+ pub fn ask<P: ConfirmPredicate>(&self, ask: impl AsRef<str>) -> bool {
+ self.try_ask::<P>(ask, ConfirmCount::Max(1))
+ .unwrap_or(false)
+ }
+
+ /// Asks the user a confirmation question, allowing a specified maximum number of attempts.
+ ///
+ /// # Parameters
+ ///
+ /// * `ask` - The prompt text to display to the user.
+ /// * `count` - The maximum number of attempts. Passing `0` means unlimited attempts (loop
+ /// indefinitely), passing a positive integer means at most that many attempts.
+ ///
+ /// # Returns
+ ///
+ /// Returns `Some(true)` for confirmation, `Some(false)` for rejection.
+ /// Returns `None` if the maximum number of attempts is reached without being able to parse
+ /// the user's input.
+ ///
+ /// # Panics
+ ///
+ /// This function panics when the standard error output (`stderr`) cannot be flushed or when
+ /// reading from standard input fails.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling::res::ResConfirm;
+ /// use mingling::confirm::YesConfirm;
+ ///
+ /// let confirm = ResConfirm::new_confirmed();
+ /// let confirmed = confirm.try_ask::<YesConfirm>("Confirm execution? [y/n] ", 3);
+ /// ```
+ pub fn try_ask<P: ConfirmPredicate>(
+ &self,
+ ask: impl AsRef<str>,
+ count: impl Into<ConfirmCount>,
+ ) -> Option<bool> {
+ if self.confirmed {
+ return Some(true);
+ }
+
+ let count = count.into();
+ let mut attempts = 0usize;
+
+ loop {
+ eprint!("{}", ask.as_ref());
+ std::io::stderr().flush().unwrap();
+
+ let stdin = std::io::stdin();
+ let mut input = String::new();
+ stdin.lock().read_line(&mut input).unwrap();
+ if let Some(result) = P::is_yes(&input) {
+ return Some(result);
+ }
+
+ attempts += 1;
+ match count {
+ ConfirmCount::Loop => {}
+ ConfirmCount::Max(max) => {
+ if attempts >= max {
+ return None;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/mingling/src/res/confirmer.rs b/mingling/src/res/confirmer.rs
deleted file mode 100644
index 900562b..0000000
--- a/mingling/src/res/confirmer.rs
+++ /dev/null
@@ -1,268 +0,0 @@
-use std::io::{BufRead, Write};
-
-/// A confirmer for interactive confirmation.
-///
-/// This structure caches the confirmed state to avoid repeated prompts.
-///
-/// Typically, `Confirmer` is registered via [`ConfirmerSetup`], and then injected into functions
-/// through Mingling's resource injection system.
-///
-/// # Registration
-///
-/// Before use, the [`ConfirmerSetup`] must be registered with the program:
-///
-/// ```
-/// # use mingling::MockProgramCollect as ThisProgram;
-/// use mingling::setup::ConfirmerSetup;
-/// use mingling::Program;
-///
-/// let mut program = Program::<ThisProgram>::new();
-/// program.with_setup(ConfirmerSetup);
-/// ```
-///
-/// # Examples
-///
-/// ```
-/// use mingling::res::{Confirmer, YesConfirm};
-///
-/// // In actual use, obtain the registered confirmer through the resource injection system
-/// let confirmer = Confirmer::new_confirmed();
-/// assert!(confirmer.ask::<YesConfirm>("Continue? [y/n] "));
-/// ```
-#[derive(Debug, Default, Clone, Copy)]
-pub struct Confirmer {
- pub(crate) confirmed: bool,
-}
-
-impl Confirmer {
- /// Creates a new `Confirmer` instance.
- ///
- /// # Examples
- ///
- /// ```
- /// use mingling::res::Confirmer;
- ///
- /// let confirmer = Confirmer::new();
- /// ```
- #[must_use]
- pub const fn new() -> Self {
- Self { confirmed: false }
- }
-
- /// Creates a `Confirmer` instance in the confirmed state.
- ///
- /// The returned `Confirmer` will directly return `true` when calling [`ask`](Confirmer::ask) or
- /// [`try_ask`](Confirmer::try_ask), without prompting the user.
- ///
- /// # Examples
- ///
- /// ```
- /// use mingling::res::{Confirmer, YesConfirm};
- ///
- /// let confirmer = Confirmer::new_confirmed();
- /// assert!(confirmer.ask::<YesConfirm>("Continue? [y/n] "));
- /// ```
- #[must_use]
- pub const fn new_confirmed() -> Self {
- Self { confirmed: true }
- }
-
- /// Marks the confirmer as confirmed.
- ///
- /// After calling this method, subsequent calls to [`ask`](Confirmer::ask) or
- /// [`try_ask`](Confirmer::try_ask) on this confirmer will directly return `true`
- /// without prompting the user.
- ///
- /// # Examples
- ///
- /// ```
- /// use mingling::res::{Confirmer, YesConfirm};
- ///
- /// let mut confirmer = Confirmer::new();
- /// confirmer.set_confirmed();
- /// assert!(confirmer.ask::<YesConfirm>("Continue? [y/n] "));
- /// ```
- pub const fn set_confirmed(&mut self) {
- self.confirmed = true;
- }
-
- /// Asks the user a confirmation question, with at most one attempt.
- ///
- /// Returns `false` if the user provides an unrecognizable answer.
- /// Returns `true` directly if already confirmed previously.
- ///
- /// # Parameters
- ///
- /// * `ask` - The prompt text to display to the user.
- ///
- /// # Returns
- ///
- /// Returns a boolean indicating whether the user confirmed. Returns `false` if the user's input
- /// could not be parsed or the maximum number of attempts was reached.
- ///
- /// # Examples
- ///
- /// ```
- /// use mingling::res::{Confirmer, YesConfirm};
- ///
- /// let confirmer = Confirmer::new_confirmed();
- /// let confirmed = confirmer.ask::<YesConfirm>("Delete this file? [y/n] ");
- /// ```
- pub fn ask<P: ConfirmerPredicate>(&self, ask: impl AsRef<str>) -> bool {
- self.try_ask::<P>(ask, ConfirmerCount::Max(1))
- .unwrap_or(false)
- }
-
- /// Asks the user a confirmation question, allowing a specified maximum number of attempts.
- ///
- /// # Parameters
- ///
- /// * `ask` - The prompt text to display to the user.
- /// * `count` - The maximum number of attempts. Passing `0` means unlimited attempts (loop
- /// indefinitely), passing a positive integer means at most that many attempts.
- ///
- /// # Returns
- ///
- /// Returns `Some(true)` for confirmation, `Some(false)` for rejection.
- /// Returns `None` if the maximum number of attempts is reached without being able to parse
- /// the user's input.
- ///
- /// # Panics
- ///
- /// This function panics when the standard error output (`stderr`) cannot be flushed or when
- /// reading from standard input fails.
- ///
- /// # Examples
- ///
- /// ```
- /// use mingling::res::{Confirmer, YesConfirm};
- ///
- /// let confirmer = Confirmer::new_confirmed();
- /// let confirmed = confirmer.try_ask::<YesConfirm>("Confirm execution? [y/n] ", 3);
- /// ```
- pub fn try_ask<P: ConfirmerPredicate>(
- &self,
- ask: impl AsRef<str>,
- count: impl Into<ConfirmerCount>,
- ) -> Option<bool> {
- if self.confirmed {
- return Some(true);
- }
-
- let count = count.into();
- let mut attempts = 0usize;
-
- loop {
- eprint!("{}", ask.as_ref());
- std::io::stderr().flush().unwrap();
-
- let stdin = std::io::stdin();
- let mut input = String::new();
- stdin.lock().read_line(&mut input).unwrap();
- if let Some(result) = P::is_yes(&input) {
- return Some(result);
- }
-
- attempts += 1;
- match count {
- ConfirmerCount::Loop => {}
- ConfirmerCount::Max(max) => {
- if attempts >= max {
- return None;
- }
- }
- }
- }
- }
-}
-
-/// Specifies the maximum number of attempts for a confirmation prompt.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ConfirmerCount {
- /// Loop indefinitely until the user gives a parseable answer.
- Loop,
- /// Ask at most the specified number of times.
- Max(usize),
-}
-
-macro_rules! impl_from_for_confirmer_count {
- ($($t:ty),*) => {
- $(
- impl From<$t> for ConfirmerCount {
- fn from(n: $t) -> Self {
- if n == 0 {
- ConfirmerCount::Loop
- } else {
- match usize::try_from(n) {
- Ok(max) => ConfirmerCount::Max(max),
- Err(_) => ConfirmerCount::Max(usize::MAX),
- }
- }
- }
- }
- )*
- };
-}
-
-impl_from_for_confirmer_count!(
- i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
-);
-
-/// Defines how to parse user confirmation input.
-///
-/// A type implementing this trait determines which user input strings are treated as "yes" or "no".
-pub trait ConfirmerPredicate {
- /// Parses the user's input string, returning whether it is "yes".
- ///
- /// Returns `Some(true)` for yes, `Some(false)` for no,
- /// and `None` if the input cannot be parsed (requiring re-entry).
- fn is_yes(str: &str) -> Option<bool>;
-}
-
-/// A `ConfirmerPredicate` implementation that accepts "y"/"yes" as yes and "n"/"no" as no.
-///
-/// Input comparison is case-insensitive and automatically trims leading/trailing whitespace.
-///
-/// # Examples
-///
-/// ```
-/// use mingling::res::{Confirmer, YesConfirm};
-///
-/// let confirmer = Confirmer::default();
-/// let confirmed = confirmer.ask::<YesConfirm>("Continue? [y/n] ");
-/// ```
-pub struct YesConfirm;
-
-/// A `ConfirmerPredicate` implementation that accepts "true"/"t" as yes and "false"/"f" as no.
-///
-/// Input comparison is case-insensitive and automatically trims leading/trailing whitespace.
-///
-/// # Examples
-///
-/// ```
-/// use mingling::res::{Confirmer, TrueConfirm};
-///
-/// let confirmer = Confirmer::default();
-/// let confirmed = confirmer.ask::<TrueConfirm>("Enable this feature? [true/false] ");
-/// ```
-pub struct TrueConfirm;
-
-impl ConfirmerPredicate for YesConfirm {
- fn is_yes(str: &str) -> Option<bool> {
- match str.trim().to_lowercase().as_str() {
- "y" | "yes" => Some(true),
- "n" | "no" => Some(false),
- _ => None,
- }
- }
-}
-
-impl ConfirmerPredicate for TrueConfirm {
- fn is_yes(str: &str) -> Option<bool> {
- match str.trim().to_lowercase().as_str() {
- "true" | "t" => Some(true),
- "false" | "f" => Some(false),
- _ => None,
- }
- }
-}
diff --git a/mingling/src/res/osc94.rs b/mingling/src/res/osc94.rs
new file mode 100644
index 0000000..ea8538d
--- /dev/null
+++ b/mingling/src/res/osc94.rs
@@ -0,0 +1,69 @@
+use crate::osc94::{OSC94Guard, OSC94State};
+
+/// Process `OSC 9;4` status.
+///
+/// Provides support for the `OSC 9;4` protocol. You can inject it into the execution flow
+/// through Mingling's resource injection system, and use it to control your process state.
+///
+/// Typically, `ResOSC94` is registered via `OSC94Setup`, and then injected into functions
+/// through Mingling's resource injection system.
+///
+/// # Registration
+///
+/// Before use, the `OSC94Setup` must be registered with the program:
+///
+/// ```
+/// # 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::ResOSC94;
+/// use mingling::osc94::OSC94State;
+///
+/// let osc94 = ResOSC94::default();
+/// let mut guard = osc94.get_mut();
+///
+/// guard.set_progress(0.5);
+/// assert_eq!(guard.state(), OSC94State::Normal(0.5));
+/// ```
+#[derive(Debug, Default, Clone, Copy)]
+pub struct ResOSC94 {
+ pub(crate) is_support: bool,
+}
+
+impl ResOSC94 {
+ /// 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::ResOSC94;
+ /// use mingling::osc94::OSC94State;
+ ///
+ /// let osc94 = ResOSC94::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,
+ }
+ }
+}
diff --git a/mingling/src/setups.rs b/mingling/src/setups.rs
index b2af90c..e1835e7 100644
--- a/mingling/src/setups.rs
+++ b/mingling/src/setups.rs
@@ -8,8 +8,8 @@ pub mod picker;
mod basic;
pub use basic::*;
-mod confirmer;
-pub use confirmer::*;
+mod confirm;
+pub use confirm::*;
mod dirs;
pub use dirs::*;
@@ -17,6 +17,9 @@ pub use dirs::*;
mod exit_code;
pub use exit_code::*;
+mod osc94;
+pub use osc94::*;
+
#[cfg(feature = "repl")]
mod repl_basic;
#[cfg(feature = "repl")]
diff --git a/mingling/src/setups/confirmer.rs b/mingling/src/setups/confirm.rs
index 1824745..46ead36 100644
--- a/mingling/src/setups/confirmer.rs
+++ b/mingling/src/setups/confirm.rs
@@ -2,12 +2,12 @@ use mingling_core::{
Program, ProgramCollect, config, hook::ProgramHook, setup::ProgramSetup, this,
};
-use crate::res::Confirmer;
+use crate::res::ResConfirm;
-/// Confirmer setup for managing confirmation state
+/// Confirm 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
+/// store. It registers a [`ResConfirm`] resource and sets up a hook that
/// checks the user's confirmation mode during program execution.
///
/// # Usage
@@ -19,18 +19,18 @@ use crate::res::Confirmer;
/// ```rust
/// # use mingling::MockProgramCollect as ThisProgram;
/// use mingling::Program;
-/// use mingling::setup::ConfirmerSetup;
+/// use mingling::setup::ConfirmSetup;
///
/// let mut program = Program::<ThisProgram>::new();
-/// program.with_setup(ConfirmerSetup);
+/// program.with_setup(ConfirmSetup);
/// ```
///
/// # Behavior
///
-/// - Registers a [`Confirmer`] resource that tracks confirmation state.
+/// - Registers a [`ResConfirm`] 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
+/// - If confirmation is skipped, the [`ResConfirm`] resource is updated
/// to record the confirmed state.
///
/// # Notes
@@ -38,20 +38,20 @@ use crate::res::Confirmer;
/// - 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;
+pub struct ConfirmSetup;
-impl<C> ProgramSetup<C> for ConfirmerSetup
+impl<C> ProgramSetup<C> for ConfirmSetup
where
C: ProgramCollect<Enum = C> + 'static,
{
fn setup(self, program: &mut Program<C>) {
- program.with_resource(Confirmer::new());
+ program.with_resource(ResConfirm::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| {
+ p.modify_res(|c: &mut ResConfirm| {
c.set_confirmed();
});
}
diff --git a/mingling/src/setups/osc94.rs b/mingling/src/setups/osc94.rs
new file mode 100644
index 0000000..b08312f
--- /dev/null
+++ b/mingling/src/setups/osc94.rs
@@ -0,0 +1,89 @@
+use mingling_core::{Program, ProgramCollect, setup::ProgramSetup};
+
+use crate::res::ResOSC94;
+
+/// `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 [`ResOSC94`] 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 [`ResOSC94`] 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(ResOSC94 {
+ 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_cli/.gitignore b/mingling_cli/.gitignore
index 6a6d575..8b13789 100644
--- a/mingling_cli/.gitignore
+++ b/mingling_cli/.gitignore
@@ -1 +1 @@
-registry.json
+
diff --git a/mingling_cli/Cargo.lock b/mingling_cli/Cargo.lock
index a5976a9..c738a0e 100644
--- a/mingling_cli/Cargo.lock
+++ b/mingling_cli/Cargo.lock
@@ -3,6 +3,12 @@
version = 4
[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
name = "annotate-snippets"
version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -37,6 +43,30 @@ dependencies = [
]
[[package]]
+name = "async-compression"
+version = "0.4.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8"
+dependencies = [
+ "compression-codecs",
+ "compression-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -52,6 +82,12 @@ dependencies = [
]
[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -92,21 +128,65 @@ dependencies = [
]
[[package]]
+name = "cc"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core",
+]
+
+[[package]]
name = "colored"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "compression-codecs"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
+dependencies = [
+ "compression-core",
+ "flate2",
+ "memchr",
]
[[package]]
+name = "compression-core"
+version = "0.4.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
+
+[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -116,6 +196,24 @@ dependencies = [
]
[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -219,7 +317,18 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
- "windows-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.2",
]
[[package]]
@@ -235,7 +344,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
]
[[package]]
@@ -245,6 +380,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -261,8 +444,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi",
+ "rand_core",
+ "wasm-bindgen",
]
[[package]]
@@ -272,12 +471,214 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -288,12 +689,29 @@ dependencies = [
]
[[package]]
+name = "ipnet"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
+
+[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
+name = "js-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "just_fmt"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -342,6 +760,18 @@ dependencies = [
]
[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -351,6 +781,18 @@ dependencies = [
]
[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -370,7 +812,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"arg-picker",
"mingling_core",
@@ -379,29 +821,32 @@ dependencies = [
[[package]]
name = "mingling-cli"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"annotate-snippets",
"cargo_metadata",
"colored",
"dirs",
+ "flate2",
"just_fmt 0.2.0",
"just_template",
"mingling",
"proc-macro2",
"quote",
+ "reqwest",
"semver",
"serde",
"serde_json",
"sha2",
"syn 3.0.2",
+ "tar",
"tokio",
"toml_edit 0.25.13+spec-1.1.0",
]
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"just_template",
@@ -411,7 +856,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"proc-macro2",
@@ -421,7 +866,7 @@ dependencies = [
[[package]]
name = "mingling_pathf"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"proc-macro2",
@@ -429,6 +874,16 @@ dependencies = [
]
[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
name = "mio"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -436,10 +891,16 @@ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
dependencies = [
"libc",
"wasi",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -469,12 +930,27 @@ dependencies = [
]
[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -484,6 +960,62 @@ dependencies = [
]
[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
+dependencies = [
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -493,6 +1025,38 @@ dependencies = [
]
[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -507,12 +1071,130 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
- "getrandom",
+ "getrandom 0.2.17",
"libredox",
"thiserror",
]
[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -581,17 +1263,35 @@ dependencies = [
]
[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"digest",
]
[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -602,6 +1302,18 @@ dependencies = [
]
[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
name = "smallvec"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -614,16 +1326,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -646,6 +1370,37 @@ dependencies = [
]
[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
name = "thiserror"
version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -666,6 +1421,31 @@ dependencies = [
]
[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
name = "tokio"
version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -679,7 +1459,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -694,6 +1474,29 @@ dependencies = [
]
[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -772,6 +1575,81 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "async-compression",
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -790,18 +1668,135 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
+name = "wasm-bindgen"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -809,6 +1804,15 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
@@ -817,6 +1821,70 @@ dependencies = [
]
[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -835,6 +1903,105 @@ dependencies = [
]
[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.2",
+]
+
+[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/mingling_cli/Cargo.toml b/mingling_cli/Cargo.toml
index 5f9778a..dfc686d 100644
--- a/mingling_cli/Cargo.toml
+++ b/mingling_cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "mingling-cli"
-version = "0.4.0"
+version = "0.5.0"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/mingling-rs/mingling/tree/main/mingling_cli"
@@ -68,6 +68,11 @@ just_template = "0.2.1"
# Template sources
sha2 = "0.10"
+# Self-update
+reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "gzip"] }
+tar = "0.4"
+flate2 = "1"
+
[build-dependencies]
# Configure & Serialization
serde = { version = "1.0.229", features = ["derive"] }
diff --git a/mingling_cli/help/help.txt b/mingling_cli/help/help.txt
index 04bb972..2f2e14b 100644
--- a/mingling_cli/help/help.txt
+++ b/mingling_cli/help/help.txt
@@ -36,3 +36,6 @@ __ cfg <KEY> <VALUE> *Edit the value of a configuration item*
__ cfg <KEY> *Print the value of the configuration item; print nothing if it does not exist*
__ cfg <KEY> --pair *Output in the form of `"key" = "value"`*
__ cfg *Print all configuration items, can be used with grep for searching*
+
+__ **MINGLING:**
+__ update *Update mling to the latest version*
diff --git a/mingling_cli/pre/lint_registry.rs b/mingling_cli/pre/lint_registry.rs
index e69592b..5bfd3fe 100644
--- a/mingling_cli/pre/lint_registry.rs
+++ b/mingling_cli/pre/lint_registry.rs
@@ -1,4 +1,4 @@
-use std::{fs, io::Error};
+use std::{fs, io::Error, path::Path};
use just_template::{Template, tmpl};
@@ -67,7 +67,8 @@ pub fn gen_mod_file() -> Result<(), Error> {
/// Generate lint metadata registry
///
/// Parses each `.rs` file in `src/lints/` (excluding mod.rs and _init.rs),
-/// extracts doc-comment metadata, and writes the result as JSON.
+/// extracts doc-comment metadata, and writes the result as `registry.json`
+/// in the cargo `OUT_DIR`.
pub fn gen_lint_registry() -> Result<(), Error> {
let root = std::env::current_dir()?;
let lints_dir = root.join("src").join("lints");
@@ -102,7 +103,8 @@ pub fn gen_lint_registry() -> Result<(), Error> {
});
let json = serde_json::json!({ "lints": lints });
- let out_path = root.join("registry.json");
+ let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is not set");
+ let out_path = Path::new(&out_dir).join("registry.json");
fs::write(&out_path, serde_json::to_string_pretty(&json).unwrap())?;
Ok(())
}
diff --git a/mingling_cli/src/bin/wrapper.rs b/mingling_cli/src/bin/wrapper.rs
index b1aecf6..a47b926 100644
--- a/mingling_cli/src/bin/wrapper.rs
+++ b/mingling_cli/src/bin/wrapper.rs
@@ -1,9 +1,16 @@
use std::env;
use std::ffi::OsString;
-use std::path::PathBuf;
+use std::fs;
+use std::io;
+use std::path::{Component, Path, PathBuf};
use std::process::{self, Command};
+use flate2::read::GzDecoder;
+use sha2::{Digest, Sha256};
+use tar::Archive;
+
fn main() {
+ apply_update_if_present();
exec();
}
@@ -45,3 +52,109 @@ fn exec() {
}
}
}
+
+/// The update package staged by `mling update` at `{data_dir}/mingling/update.tar.gz`.
+fn update_package_path() -> Option<PathBuf> {
+ dirs::data_dir().map(|data_dir| data_dir.join("mingling").join("update.tar.gz"))
+}
+
+/// Apply a staged update, if any: unpack it over the installation directory
+/// (never replacing the running wrapper itself), then remove the staged file.
+/// This runs before forwarding to `mingling-cli`, so the new version is loaded
+/// by this very invocation.
+fn apply_update_if_present() {
+ let Some(update_path) = update_package_path() else {
+ return;
+ };
+ if !update_path.is_file() {
+ return;
+ }
+ let Ok(current_exe) = env::current_exe() else {
+ return;
+ };
+
+ // The package mirrors the install layout: the wrapper lives at
+ // `<root>/bin/mling` and the archive root maps onto `<root>`, so entries
+ // like `bin/mingling-cli` replace the files next to this wrapper.
+ let Some(exe_dir) = current_exe.parent() else {
+ return;
+ };
+ let Some(install_root) = exe_dir.parent() else {
+ return;
+ };
+
+ match unpack_update(&update_path, &current_exe, install_root) {
+ Ok(()) => {
+ // Record the applied package's checksum so `mling update` can tell
+ // that this installation is already up to date.
+ if let Some(checksum_path) = last_update_checksum_path()
+ && let Ok(checksum) = sha256_file(&update_path)
+ {
+ let _ = fs::write(checksum_path, checksum);
+ }
+ let _ = fs::remove_file(update_path);
+ }
+ Err(e) => eprintln!("mling: failed to apply update: {e}"),
+ }
+}
+
+/// Extract `update.tar.gz` into `install_root`, skipping the running wrapper.
+fn unpack_update(
+ update_path: &Path,
+ current_exe: &Path,
+ install_root: &Path,
+) -> std::io::Result<()> {
+ let file = fs::File::open(update_path)?;
+ let mut archive = Archive::new(GzDecoder::new(file));
+
+ for entry in archive.entries()? {
+ let mut entry = entry?;
+ let entry_type = entry.header().entry_type();
+ let rel_path = sanitize_relative_path(&entry.path()?);
+ if rel_path.as_os_str().is_empty() {
+ continue;
+ }
+ let dest = install_root.join(rel_path);
+ // The running executable cannot (and must not) replace itself.
+ if dest == current_exe {
+ continue;
+ }
+ if entry_type.is_dir() {
+ fs::create_dir_all(dest)?;
+ continue;
+ }
+ if !entry_type.is_file() {
+ continue;
+ }
+ if let Some(parent) = dest.parent() {
+ fs::create_dir_all(parent)?;
+ }
+ entry.unpack(dest)?;
+ }
+ Ok(())
+}
+
+/// Keep only normal path components so entries cannot escape `install_root`.
+fn sanitize_relative_path(path: &Path) -> PathBuf {
+ let mut out = PathBuf::new();
+ for component in path.components() {
+ if let Component::Normal(part) = component {
+ out.push(part);
+ }
+ }
+ out
+}
+
+/// `{data_dir}/mingling/last-update.sha256`, where the wrapper records the
+/// checksum of the update it applied.
+fn last_update_checksum_path() -> Option<PathBuf> {
+ dirs::data_dir().map(|data_dir| data_dir.join("mingling").join("last-update.sha256"))
+}
+
+/// The sha256 hex digest of a file.
+fn sha256_file(path: &Path) -> io::Result<String> {
+ let mut file = fs::File::open(path)?;
+ let mut hasher = Sha256::new();
+ io::copy(&mut file, &mut hasher)?;
+ Ok(format!("{:x}", hasher.finalize()))
+}
diff --git a/mingling_cli/src/lib.rs b/mingling_cli/src/lib.rs
index fdd7636..902ab32 100644
--- a/mingling_cli/src/lib.rs
+++ b/mingling_cli/src/lib.rs
@@ -1,7 +1,7 @@
use mingling::{
- ShellContext, Suggest,
+ RenderResult, ShellContext, Suggest,
consts::HELP_FLAG,
- macros::{completion, gen_program, help, suggest},
+ macros::{completion, gen_program, help, renderer, suggest},
};
use crate::{
@@ -21,11 +21,12 @@ pub mod message;
pub mod metadata;
pub mod pkg_mgr;
pub mod proj_mgr;
+pub mod updater;
pub mod utils;
#[help]
pub fn help_global(_: EntryFallback) -> String {
- include_str!("../help/help.txt").parse_color_code()
+ format!("{}\n", include_str!("../help/help.txt").parse_color_code())
}
#[completion(EntryFallback)]
@@ -41,4 +42,25 @@ pub fn complete_global(_ctx: &ShellContext) -> Suggest {
}
}
+#[renderer]
+pub fn handle_fallback(args: EntryFallback) -> RenderResult {
+ let mut r = RenderResult::new();
+ let args = args.inner;
+ if !args.is_empty() {
+ eprintln_cargo!(
+ r,
+ "{}",
+ format!("Unknown command `{}`", args.join(" ")).parse_color_code()
+ )
+ } else {
+ hprintln_cargo!(
+ r,
+ "{}",
+ "Welcome to MinglingCLI, please use `mling -h` to see available commands"
+ .parse_color_code(),
+ );
+ }
+ r
+}
+
gen_program!();
diff --git a/mingling_cli/src/linter/registry.rs b/mingling_cli/src/linter/registry.rs
index 79f3225..b9f82fa 100644
--- a/mingling_cli/src/linter/registry.rs
+++ b/mingling_cli/src/linter/registry.rs
@@ -26,8 +26,9 @@ pub struct LintMetadata {
#[program_setup]
pub fn lint_registry_setup(p: &mut Program<ThisProgram>) {
p.with_resource(ResLintRegistry::lazy_init(|| {
- let registry: ResLintRegistry = serde_json::from_str(include_str!("../../registry.json"))
- .expect("failed to parse embedded registry.json");
+ let registry: ResLintRegistry =
+ serde_json::from_str(include_str!(concat!(env!("OUT_DIR"), "/registry.json")))
+ .expect("failed to parse embedded registry.json");
registry
}));
}
diff --git a/mingling_cli/src/pkg_mgr/cmd_install.rs b/mingling_cli/src/pkg_mgr/cmd_install.rs
index ddcef7d..731ba98 100644
--- a/mingling_cli/src/pkg_mgr/cmd_install.rs
+++ b/mingling_cli/src/pkg_mgr/cmd_install.rs
@@ -2,9 +2,10 @@ use std::{env, fs, io, path::PathBuf, process::Command};
use cargo_metadata::TargetKind;
use mingling::{
- Grouped, LazyRes, RenderResult, Routable,
- macros::{chain, command, metadata, pack_err, renderer, routeify},
+ Grouped, LazyRes, RenderResult, Routable, ShellContext, Suggest,
+ macros::{arg, chain, command, completion, metadata, pack_err, renderer, routeify, suggest},
metadata::Description,
+ picker::{EntryPicker, PickerArg, value::Flag},
};
use crate::{
@@ -16,6 +17,11 @@ use crate::{
pack_err!(ErrorBuildFailed = String);
pack_err!(ErrorBinaryNotFound = String);
+pack_err!(ErrorPkgEnableFailed = String);
+
+/// Flag: `--enable` — run `mling pkg-enable` after a successful install
+/// to enable the package being installed.
+pub static ARG_ENABLE: PickerArg<Flag> = arg![enable: Flag];
/// Resolved install paths, used by the build step.
#[derive(Debug, Default, Grouped)]
@@ -24,6 +30,7 @@ pub struct StateInstallBuild {
pub install_dir: PathBuf,
pub release_dir: PathBuf,
pub exe_suffix: &'static str,
+ pub enable: bool,
}
/// State after `cargo build --release`, used by the copy step.
@@ -33,6 +40,16 @@ pub struct StateInstallCopy {
pub release_dir: PathBuf,
pub exe_suffix: &'static str,
pub installed: Vec<PathBuf>,
+ pub enable: bool,
+}
+
+/// State after the copy step when `--enable` was given: run `mling pkg-enable`.
+#[derive(Debug, Default, Grouped)]
+pub struct StateInstallEnable {
+ pub install_dir: PathBuf,
+ pub installed: Vec<PathBuf>,
+ pub name: String,
+ pub version: String,
}
#[derive(Debug, Default, Grouped)]
@@ -49,7 +66,12 @@ pub fn desc_install() -> Description {
}
#[command(routeify)]
-pub fn install(packages_dir: &ResPackagesDir, metadata: &mut LazyRes<ResMetadata>) -> Next {
+pub fn install(
+ args: EntryInstall,
+ packages_dir: &ResPackagesDir,
+ metadata: &mut LazyRes<ResMetadata>,
+) -> Next {
+ let enable = args.pick(&ARG_ENABLE).to_result()?;
let metadata = metadata.get_ref().data();
let packages_dir = &packages_dir.path;
if packages_dir.as_os_str().is_empty() {
@@ -69,6 +91,7 @@ pub fn install(packages_dir: &ResPackagesDir, metadata: &mut LazyRes<ResMetadata
.join("release")
.into_std_path_buf(),
exe_suffix: env::consts::EXE_SUFFIX,
+ enable: enable.bool(),
}
.to_chain()
}
@@ -93,6 +116,7 @@ pub fn handle_state_install_build(state: StateInstallBuild) -> Next {
release_dir: state.release_dir,
exe_suffix: state.exe_suffix,
installed: vec![],
+ enable: state.enable,
}
.to_chain()
}
@@ -153,6 +177,45 @@ pub fn handle_state_install_copy(
}
}
+ if state.enable {
+ let root_package = metadata
+ .root_package()
+ .or_else(|| metadata.workspace_packages().first().copied())
+ .ok_or(ErrorRootPackageNotFound::default())?;
+ return StateInstallEnable {
+ install_dir: state.install_dir,
+ installed: state.installed,
+ name: root_package.name.to_string(),
+ version: root_package.version.to_string(),
+ }
+ .to_chain();
+ }
+
+ ResultInstall {
+ install_dir: state.install_dir,
+ installed: state.installed,
+ }
+ .to_chain()
+}
+
+/// Step 3 (optional): enable the installed package via `mling pkg-enable`
+/// when `--enable` was given.
+#[chain(routeify)]
+pub fn handle_state_install_enable(state: StateInstallEnable) -> Next {
+ let spec = format!("{}@{}", state.name, state.version);
+ let status = Command::new("mling")
+ .args(["pkg-enable", &spec])
+ .status()
+ .map_err(|e| {
+ ErrorPkgEnableFailed::new(format!("failed to run `mling pkg-enable {spec}`: {e}"))
+ })?;
+ if !status.success() {
+ return ErrorPkgEnableFailed::new(format!(
+ "`mling pkg-enable {spec}` failed with {status}"
+ ))
+ .to_chain();
+ }
+
ResultInstall {
install_dir: state.install_dir,
installed: state.installed,
@@ -183,3 +246,20 @@ pub fn render_error_binary_not_found(err: ErrorBinaryNotFound) -> RenderResult {
eprintln_cargo!(r, "binary not found: {}", err.info);
r
}
+
+#[renderer]
+pub fn render_error_pkg_enable_failed(err: ErrorPkgEnableFailed) -> RenderResult {
+ let mut r = RenderResult::new();
+ eprintln_cargo!(r, "{}", err.info);
+ r
+}
+
+#[completion(EntryInstall)]
+pub fn complete_install(ctx: &ShellContext) -> Suggest {
+ if ctx.previous_word != "install" {
+ return Suggest::FileCompletion;
+ }
+ suggest! {
+ ARG_ENABLE: "Enable the package after installing (runs `mling pkg-enable`)"
+ }
+}
diff --git a/mingling_cli/src/updater.rs b/mingling_cli/src/updater.rs
new file mode 100644
index 0000000..1c81fb3
--- /dev/null
+++ b/mingling_cli/src/updater.rs
@@ -0,0 +1 @@
+pub mod cmd_update;
diff --git a/mingling_cli/src/updater/cmd_update.rs b/mingling_cli/src/updater/cmd_update.rs
new file mode 100644
index 0000000..545361f
--- /dev/null
+++ b/mingling_cli/src/updater/cmd_update.rs
@@ -0,0 +1,266 @@
+use std::{fs, path::Path, path::PathBuf};
+
+use mingling::{
+ Grouped, LazyRes, RenderResult, Routable,
+ macros::{chain, command, metadata, renderer, routeify},
+ metadata::Description,
+};
+use sha2::{Digest, Sha256};
+
+use crate::{Next, config::ResMlingConfig, eprintln_cargo, println_cargo};
+
+/// Config key holding the base URL that hosts the mling release packages.
+const CONFIG_KEY_UPDATE_URL: &str = "update-url";
+
+/// Default update source, used when the config key is unset.
+const DEFAULT_UPDATE_URL: &str = "https://mingling-rs.github.io/mingling/dist";
+
+/// Name of the staged update package inside `{data_dir}/mingling`.
+const UPDATE_FILE_NAME: &str = "update.tar.gz";
+
+/// Records the sha256 of the last applied update, written by the wrapper.
+const LAST_UPDATE_FILE_NAME: &str = "last-update.sha256";
+
+/// The resolved download task: check the remote checksum and stage the package.
+#[derive(Debug, Default, Grouped)]
+pub struct StateUpdateDownload {
+ pub base_url: String,
+ pub update_path: PathBuf,
+}
+
+/// The latest package is already installed.
+#[derive(Debug, Default, Grouped)]
+pub struct ResultUpdateUpToDate;
+
+/// The latest package was downloaded, verified, and staged for the wrapper.
+#[derive(Debug, Default, Grouped)]
+pub struct ResultUpdateStaged {
+ pub update_path: PathBuf,
+}
+
+/// Errors produced by the download pipeline.
+#[derive(Debug, Grouped)]
+pub enum UpdateError {
+ /// The data directory could not be determined.
+ NoDataDirectory,
+ /// The configured update URL is not a valid `http(s)://` URL.
+ InvalidUrl(String),
+ /// A network request failed, or the remote responded with an error.
+ Network(String),
+ /// The downloaded package failed its sha256 verification.
+ ChecksumMismatch(String),
+ /// Writing the staged update package failed.
+ Io(String),
+}
+
+#[metadata(EntryUpdate)]
+pub fn desc_update() -> Description {
+ "Update mling to the latest version".into()
+}
+
+#[command(routeify)]
+pub fn update(config: &mut LazyRes<ResMlingConfig>) -> Next {
+ let config = config.get_ref();
+ let source = config.get_or(CONFIG_KEY_UPDATE_URL, DEFAULT_UPDATE_URL);
+ let Some(update_path) = update_package_path() else {
+ return UpdateError::NoDataDirectory.to_chain();
+ };
+ if !is_http_url(source) {
+ return UpdateError::InvalidUrl(source.to_string()).to_chain();
+ }
+ StateUpdateDownload {
+ base_url: source.to_string(),
+ update_path,
+ }
+ .to_chain()
+}
+
+/// Check the remote checksum against the installed version; if they differ,
+/// download the package, verify its checksum, and stage it at
+/// `{data_dir}/mingling/update.tar.gz`.
+#[chain(routeify)]
+pub async fn handle_state_update_download(state: StateUpdateDownload) -> Next {
+ match check_and_fetch(&state.base_url, &state.update_path).await {
+ Ok(FetchOutcome::UpToDate) => ResultUpdateUpToDate.to_chain(),
+ Ok(FetchOutcome::Staged) => ResultUpdateStaged {
+ update_path: state.update_path,
+ }
+ .to_chain(),
+ Err(e) => e.to_chain(),
+ }
+}
+
+#[renderer]
+pub fn render_result_update_up_to_date(_: ResultUpdateUpToDate) -> RenderResult {
+ let mut result = RenderResult::new();
+ println_cargo!(result, "mling is already up to date");
+ result
+}
+
+#[renderer]
+pub fn render_result_update_staged(r: ResultUpdateStaged) -> RenderResult {
+ let mut result = RenderResult::new();
+ println_cargo!(result, "Downloaded: {}", r.update_path.display());
+ println_cargo!(result, "Run `mling` again to apply the update");
+ result
+}
+
+#[renderer]
+pub fn render_error_update(err: UpdateError) -> RenderResult {
+ let mut result = RenderResult::new();
+ match err {
+ UpdateError::NoDataDirectory => {
+ eprintln_cargo!(result, "failed to determine the data directory");
+ }
+ UpdateError::InvalidUrl(source) => {
+ eprintln_cargo!(
+ result,
+ "invalid update URL `{}`, expected an `http(s)://` URL such as `https://mingling-rs.github.io/mingling/dist`",
+ source
+ );
+ }
+ UpdateError::Network(msg) | UpdateError::ChecksumMismatch(msg) | UpdateError::Io(msg) => {
+ eprintln_cargo!(result, "{}", msg);
+ }
+ }
+ result
+}
+
+/// `{data_dir}/mingling/update.tar.gz`, where the wrapper looks for staged updates.
+pub fn update_package_path() -> Option<PathBuf> {
+ dirs::data_dir().map(|dir| dir.join("mingling").join(UPDATE_FILE_NAME))
+}
+
+/// `{data_dir}/mingling/last-update.sha256`, the checksum of the last applied update.
+fn last_update_checksum_path() -> Option<PathBuf> {
+ dirs::data_dir().map(|dir| dir.join("mingling").join(LAST_UPDATE_FILE_NAME))
+}
+
+fn is_http_url(source: &str) -> bool {
+ let source = source.trim();
+ source.starts_with("http://") || source.starts_with("https://")
+}
+
+/// The platform suffix used by the package names (`mling-{os}.tar.gz`).
+fn update_os_name() -> &'static str {
+ if cfg!(windows) {
+ "win"
+ } else if cfg!(target_os = "linux") {
+ "linux"
+ } else if cfg!(target_os = "macos") {
+ "mac"
+ } else {
+ "unknown"
+ }
+}
+
+enum FetchOutcome {
+ UpToDate,
+ Staged,
+}
+
+/// Fetch `mling-{os}.tar.gz.sha256`, skip the download when the installed
+/// version already matches, then download and verify the package before
+/// staging it.
+async fn check_and_fetch(base_url: &str, update_path: &Path) -> Result<FetchOutcome, UpdateError> {
+ let os = update_os_name();
+ let base = base_url.trim_end_matches('/');
+ let checksum_url = format!("{base}/mling-{os}.tar.gz.sha256");
+ let package_url = format!("{base}/mling-{os}.tar.gz");
+
+ let client = reqwest::Client::builder()
+ .user_agent(format!("mling-updater/{}", env!("CARGO_PKG_VERSION")))
+ .build()
+ .map_err(|e| UpdateError::Network(format!("failed to build HTTP client: {e}")))?;
+
+ // 1. Fetch the remote checksum first.
+ let response = client.get(&checksum_url).send().await.map_err(|e| {
+ UpdateError::Network(format!(
+ "failed to fetch checksum from `{checksum_url}`: {e}"
+ ))
+ })?;
+ if !response.status().is_success() {
+ return Err(UpdateError::Network(format!(
+ "failed to fetch checksum from `{checksum_url}`: HTTP {}",
+ response.status()
+ )));
+ }
+ let checksum_text = response
+ .text()
+ .await
+ .map_err(|e| UpdateError::Network(format!("failed to read checksum: {e}")))?;
+ let remote_sha = parse_sha256(&checksum_text).ok_or_else(|| {
+ UpdateError::Network(format!("invalid checksum file at `{checksum_url}`"))
+ })?;
+
+ // 2. Skip the download when the installed version already matches.
+ if let Some(local_sha) = read_last_update_checksum()
+ && local_sha == remote_sha
+ {
+ return Ok(FetchOutcome::UpToDate);
+ }
+
+ // 3. Download the package.
+ let response =
+ client.get(&package_url).send().await.map_err(|e| {
+ UpdateError::Network(format!("failed to download `{package_url}`: {e}"))
+ })?;
+ if !response.status().is_success() {
+ return Err(UpdateError::Network(format!(
+ "failed to download `{package_url}`: HTTP {}",
+ response.status()
+ )));
+ }
+ let bytes = response
+ .bytes()
+ .await
+ .map_err(|e| UpdateError::Network(format!("failed to read package body: {e}")))?;
+
+ // 4. Verify the package before staging it.
+ let actual_sha = sha256_hex(&bytes);
+ if actual_sha != remote_sha {
+ return Err(UpdateError::ChecksumMismatch(format!(
+ "checksum mismatch for `{package_url}`: expected {remote_sha}, got {actual_sha}"
+ )));
+ }
+
+ // 5. Stage it for the wrapper.
+ write_update_package(&bytes, update_path)?;
+ Ok(FetchOutcome::Staged)
+}
+
+/// Parse the sha256 hex digest from a `sha256sum`-style line (`<hash> <file>`).
+fn parse_sha256(line: &str) -> Option<String> {
+ let hash = line.split_whitespace().next()?;
+ (hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit())).then(|| hash.to_string())
+}
+
+/// The checksum of the last update applied by the wrapper, if recorded.
+fn read_last_update_checksum() -> Option<String> {
+ let path = last_update_checksum_path()?;
+ let content = fs::read_to_string(path).ok()?;
+ let sha = content.trim();
+ (!sha.is_empty()).then(|| sha.to_string())
+}
+
+fn sha256_hex(bytes: &[u8]) -> String {
+ let mut hasher = Sha256::new();
+ hasher.update(bytes);
+ format!("{:x}", hasher.finalize())
+}
+
+/// Stage the update package at the wrapper's well-known location. The bytes are
+/// written to a temporary file first so a failed download never corrupts a
+/// previously staged update.
+fn write_update_package(package: &[u8], update_path: &Path) -> Result<(), UpdateError> {
+ let parent = update_path.parent().ok_or_else(|| {
+ UpdateError::Io(format!("no parent directory for {}", update_path.display()))
+ })?;
+ fs::create_dir_all(parent).map_err(|e| UpdateError::Io(e.to_string()))?;
+ let tmp_path = parent.join("update.tar.gz.tmp");
+ fs::write(&tmp_path, package).map_err(|e| UpdateError::Io(e.to_string()))?;
+ if update_path.exists() {
+ fs::remove_file(update_path).map_err(|e| UpdateError::Io(e.to_string()))?;
+ }
+ fs::rename(&tmp_path, update_path).map_err(|e| UpdateError::Io(e.to_string()))
+}
diff --git a/mingling_core/tests/test-all/Cargo.lock b/mingling_core/tests/test-all/Cargo.lock
index b57638b..07bc08e 100644
--- a/mingling_core/tests/test-all/Cargo.lock
+++ b/mingling_core/tests/test-all/Cargo.lock
@@ -129,7 +129,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -139,7 +139,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"just_template",
@@ -153,7 +153,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"proc-macro2",
diff --git a/mingling_core/tests/test-basic/Cargo.lock b/mingling_core/tests/test-basic/Cargo.lock
index a8f189f..d425069 100644
--- a/mingling_core/tests/test-basic/Cargo.lock
+++ b/mingling_core/tests/test-basic/Cargo.lock
@@ -50,7 +50,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -58,7 +58,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -66,7 +66,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/mingling_core/tests/test-comp/Cargo.lock b/mingling_core/tests/test-comp/Cargo.lock
index 5f4eeb3..1a37590 100644
--- a/mingling_core/tests/test-comp/Cargo.lock
+++ b/mingling_core/tests/test-comp/Cargo.lock
@@ -77,7 +77,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -85,7 +85,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"just_template",
@@ -94,7 +94,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"proc-macro2",
diff --git a/mingling_core/tests/test-dispatch-tree/Cargo.lock b/mingling_core/tests/test-dispatch-tree/Cargo.lock
index c6d33ae..245893d 100644
--- a/mingling_core/tests/test-dispatch-tree/Cargo.lock
+++ b/mingling_core/tests/test-dispatch-tree/Cargo.lock
@@ -50,7 +50,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -58,7 +58,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -66,7 +66,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/mingling_core/tests/test-repl/Cargo.lock b/mingling_core/tests/test-repl/Cargo.lock
index e359032..b55f5ba 100644
--- a/mingling_core/tests/test-repl/Cargo.lock
+++ b/mingling_core/tests/test-repl/Cargo.lock
@@ -50,7 +50,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -58,7 +58,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -66,7 +66,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/mingling_core/tests/test-structural-renderer/Cargo.lock b/mingling_core/tests/test-structural-renderer/Cargo.lock
index d54d299..fcf244d 100644
--- a/mingling_core/tests/test-structural-renderer/Cargo.lock
+++ b/mingling_core/tests/test-structural-renderer/Cargo.lock
@@ -65,7 +65,7 @@ dependencies = [
[[package]]
name = "mingling"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"mingling_core",
"mingling_macros",
@@ -75,7 +75,7 @@ dependencies = [
[[package]]
name = "mingling_core"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"might_be_async",
@@ -88,7 +88,7 @@ dependencies = [
[[package]]
name = "mingling_macros"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt",
"proc-macro2",
diff --git a/mingling_pathf/test/Cargo.lock b/mingling_pathf/test/Cargo.lock
index e7de689..58c7c9a 100644
--- a/mingling_pathf/test/Cargo.lock
+++ b/mingling_pathf/test/Cargo.lock
@@ -16,7 +16,7 @@ checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling_pathf"
-version = "0.4.0"
+version = "0.5.0"
dependencies = [
"just_fmt 0.2.0",
"proc-macro2",