aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-30 18:05:05 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-30 18:05:05 +0800
commit13408e79b940e9a33ca593ed30d1b20c54e01234 (patch)
tree282549991a3f31791401ca2f3255b9318679d2e9
parent29867ab5c0b40378a33318d989c809f90fc7d3aa (diff)
feat(docs): add Chinese and English documentation for Mingling tutorialsunreleased
Add comprehensive documentation covering Declare a Dispatcher, Declare a Chain, Rendering Results, Multi-Command Program, Argument Parsing with Picker and Clap, Program Setup, Error Handling, Help Info, Resource System, Exit Code Control, Hook System, Testing, Completion, Structural Rendering, and Core Concepts
-rw-r--r--docs/_sidebar.md21
-rw-r--r--docs/_zh_CN/_sidebar.md21
-rw-r--r--docs/_zh_CN/pages/10-help.md69
-rw-r--r--docs/_zh_CN/pages/11-resource-system.md89
-rw-r--r--docs/_zh_CN/pages/12-exit-code.md70
-rw-r--r--docs/_zh_CN/pages/13-hook.md107
-rw-r--r--docs/_zh_CN/pages/14-testing.md129
-rw-r--r--docs/_zh_CN/pages/2-define-a-dispatcher.md103
-rw-r--r--docs/_zh_CN/pages/3-define-a-chain.md131
-rw-r--r--docs/_zh_CN/pages/4-render-result.md142
-rw-r--r--docs/_zh_CN/pages/5-multiple-commands.md109
-rw-r--r--docs/_zh_CN/pages/6-argument-parse-picker.md393
-rw-r--r--docs/_zh_CN/pages/7-argument-parse-clap.md87
-rw-r--r--docs/_zh_CN/pages/8-setup-and-resources.md89
-rw-r--r--docs/_zh_CN/pages/9-error-handling.md120
-rw-r--r--docs/_zh_CN/pages/advanced/.name1
-rw-r--r--docs/_zh_CN/pages/advanced/1-completion.md83
-rw-r--r--docs/_zh_CN/pages/advanced/2-structural-renderer.md120
-rw-r--r--docs/_zh_CN/pages/concepts/.name1
-rw-r--r--docs/_zh_CN/pages/concepts/1-the-pipeline.md129
-rw-r--r--docs/_zh_CN/pages/concepts/2-resource.md60
-rw-r--r--docs/_zh_CN/pages/concepts/3-any-output.md73
-rw-r--r--docs/_zh_CN/pages/concepts/4-program-collect.md52
-rw-r--r--docs/pages/1-getting-started.md18
-rw-r--r--docs/pages/10-help.md69
-rw-r--r--docs/pages/11-resource-system.md89
-rw-r--r--docs/pages/12-exit-code.md68
-rw-r--r--docs/pages/13-hook.md107
-rw-r--r--docs/pages/14-testing.md129
-rw-r--r--docs/pages/2-define-a-dispatcher.md103
-rw-r--r--docs/pages/3-define-a-chain.md131
-rw-r--r--docs/pages/4-render-result.md142
-rw-r--r--docs/pages/5-multiple-commands.md109
-rw-r--r--docs/pages/6-argument-parse-picker.md393
-rw-r--r--docs/pages/7-argument-parse-clap.md87
-rw-r--r--docs/pages/8-setup-and-resources.md89
-rw-r--r--docs/pages/9-error-handling.md120
-rw-r--r--docs/pages/advanced/.name1
-rw-r--r--docs/pages/advanced/1-completion.md83
-rw-r--r--docs/pages/advanced/2-structural-renderer.md120
-rw-r--r--docs/pages/concepts/.name1
-rw-r--r--docs/pages/concepts/1-the-pipeline.md129
-rw-r--r--docs/pages/concepts/2-resource.md60
-rw-r--r--docs/pages/concepts/3-any-output.md73
-rw-r--r--docs/pages/concepts/4-program-collect.md52
45 files changed, 4363 insertions, 9 deletions
diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index 47f46b8..84e36a2 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -1,5 +1,26 @@
- [Welcome!](README)
* [Getting Started](pages/1-getting-started)
+* [Declare a Dispatcher](pages/2-define-a-dispatcher)
+* [Declare a Chain](pages/3-define-a-chain)
+* [Rendering Results](pages/4-render-result)
+* [Multi-Command Program](pages/5-multiple-commands)
+* [Parsing Arguments with Picker](pages/6-argument-parse-picker)
+* [Parsing Arguments with Clap](pages/7-argument-parse-clap)
+* [Program Setup](pages/8-setup-and-resources)
+* [Error Handling](pages/9-error-handling)
+* [Help Info](pages/10-help)
+* [Using the Resource System](pages/11-resource-system)
+* [Exit Code Control](pages/12-exit-code)
+* [Hook System](pages/13-hook)
+* [Testing Your Program](pages/14-testing)
+* Advanced
+ * [Completion](pages/advanced/1-completion)
+ * [Structural Rendering](pages/advanced/2-structural-renderer)
+* Core Concepts
+ * [The Pipeline](pages/concepts/1-the-pipeline)
+ * [Resource System](pages/concepts/2-resource)
+ * [AnyOutput Mechanism](pages/concepts/3-any-output)
+ * [About ProgramCollect](pages/concepts/4-program-collect)
* Other
* [Features](pages/other/features)
* [Naming Conventions](pages/other/naming_rule)
diff --git a/docs/_zh_CN/_sidebar.md b/docs/_zh_CN/_sidebar.md
index 620b11c..80645c0 100644
--- a/docs/_zh_CN/_sidebar.md
+++ b/docs/_zh_CN/_sidebar.md
@@ -1,5 +1,26 @@
- [Welcome!](README)
* [起步](pages/1-getting-started)
+* [声明一个分发器](pages/2-define-a-dispatcher)
+* [声明一个链](pages/3-define-a-chain)
+* [将结果渲染](pages/4-render-result)
+* [多命令程序](pages/5-multiple-commands)
+* [使用 Picker 完成参数解析](pages/6-argument-parse-picker)
+* [使用 Clap 完成参数解析](pages/7-argument-parse-clap)
+* [程序装配](pages/8-setup-and-resources)
+* [错误处理](pages/9-error-handling)
+* [帮助信息](pages/10-help)
+* [使用资源系统](pages/11-resource-system)
+* [退出码控制](pages/12-exit-code)
+* [钩子系统](pages/13-hook)
+* [测试你的程序](pages/14-testing)
* 其他
* [特性](pages/other/features)
* [命名规范](pages/other/naming_rule)
+* 核心概念
+ * [基础管线](pages/concepts/1-the-pipeline)
+ * [资源系统](pages/concepts/2-resource)
+ * [任意输出机制](pages/concepts/3-any-output)
+ * [关于 ProgramCollect](pages/concepts/4-program-collect)
+* 进阶
+ * [补全](pages/advanced/1-completion)
+ * [结构化渲染](pages/advanced/2-structural-renderer)
diff --git a/docs/_zh_CN/pages/10-help.md b/docs/_zh_CN/pages/10-help.md
new file mode 100644
index 0000000..7b56557
--- /dev/null
+++ b/docs/_zh_CN/pages/10-help.md
@@ -0,0 +1,69 @@
+<h1 align="center">帮助信息</h1>
+<p align="center">
+ 为命令添加 --help 支持
+</p>
+
+没有帮助信息的 CLI 不是好 CLI。
+
+Mingling 里用 `#[help]` 宏给命令添加帮助文本。
+
+## 最简单的帮助
+
+直接给 Entry 写一个帮助函数:
+
+```rust
+@@@use mingling::macros::help;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+#[help]
+fn help_greet(_entry: EntryGreet) {
+ r_println!("Usage: greet [name]");
+ r_println!("Say hello to someone.");
+}
+```
+
+> [!NOTE]
+> 帮助函数里也用 `r_println!`,因为 `#[help]` 走的也是渲染流程 —— 它是被 `--help` 参数提前触发的短路渲染,不是独立于管线之外的逻辑。
+
+## 全局帮助
+
+你也可以为 `ErrorDispatcherNotFound` 写帮助,作为"根帮助":
+
+```rust
+@@@use mingling::macros::help;
+// 用户直接输入 --help 时触发
+#[help]
+fn help_root(entry: ErrorDispatcherNotFound) {
+ r_println!("Usage: my-cli <command>");
+ r_println!("Commands:");
+ r_println!(" greet Say hello");
+}
+```
+
+> [!TIP]
+> `ErrorDispatcherNotFound` 是 `gen_program!()` 自动生成的类型,代表"没有匹配到任何命令"的情况。为它写 `#[help]` 就是给程序的根命令加帮助。
+
+## 需要 Setup 配合
+
+要让 `--help` 正常工作,需要在 `main` 里加上 `BasicProgramSetup`:
+
+```rust
+@@@use mingling::macros::help;
+@@@use mingling::setup::BasicProgramSetup;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup);
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+`BasicProgramSetup` 内置了 `HelpFlagSetup`,它的作用仅仅是把 `program.user_context.help` 设为 `true`。
+
+真正把请求路由到 `#[help]` 函数的是 `gen_program!()` 生成的代码 —— 它在调度时检查这个标记,如果为 `true` 就走帮助渲染路径,不经过 Chain。
+
+不加 `BasicProgramSetup` 的话,`--help` 只是一个普通参数,会被当成 Entry 的输入传给 Chain。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/11-resource-system.md b/docs/_zh_CN/pages/11-resource-system.md
new file mode 100644
index 0000000..5c3cf90
--- /dev/null
+++ b/docs/_zh_CN/pages/11-resource-system.md
@@ -0,0 +1,89 @@
+<h1 align="center">使用资源系统</h1>
+<p align="center">
+ 手把手带你使用资源
+</p>
+
+资源是 Mingling 中管理全局状态的机制。任何实现了 `Default + Clone` 的类型都可以成为资源。
+
+## 定义一个资源
+
+```rust
+// 只要实现 Default + Clone,就可以作为资源使用
+#[derive(Default, Clone)]
+struct ResCurrentDir(String);
+
+// 注册到 Program
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_resource(ResCurrentDir(".".into()));
+ program.exec_and_exit();
+}
+```
+
+因为 `ResCurrentDir` 同时实现了 `Default` 和 `Clone`,框架会自动为它实现 `ResourceMarker` trait,无需手动 impl。
+
+## 注入并使用
+
+在 Chain 或 Renderer 中,只需在参数列表里声明你要的资源:
+
+```rust
+@@@#[derive(Default, Clone)]
+@@@struct ResCurrentDir(String);
+@@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir);
+@@@pack!(ResultPath = String);
+// 通过 &T 注入只读资源
+#[chain]
+fn handle_pwd(_args: EntryPrintWorkingDir, cwd: &ResCurrentDir) -> Next {
+ ResultPath::new(cwd.0.clone()).to_render()
+}
+
+#[renderer]
+fn render_path(result: ResultPath) {
+ r_println!("{}", *result);
+}
+```
+
+## 修改资源
+
+用 `&mut T` 注入可修改资源:
+
+```rust
+@@@#[derive(Default, Clone)]
+@@@struct ResVisitCount(u32);
+@@@dispatcher!("visit", CMDVisit => EntryVisit);
+@@@pack!(ResultDone = ());
+#[chain]
+fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next {
+ counter.0 += 1;
+ ResultDone::default()
+}
+
+#[renderer]
+fn render_done(_done: ResultDone, counter: &ResVisitCount) {
+ r_println!("visit count is : {}", counter.0);
+}
+```
+
+## 多个资源同用
+
+Chain 可以同时注入任意多个资源,框架按类型自动匹配:
+
+```rust
+@@@#[derive(Default, Clone)] struct ResConfig(String);
+@@@#[derive(Default, Clone)] struct ResCounter(u32);
+@@@dispatcher!("test", CMDTest => EntryTest);
+@@@pack!(ResultDone = ());
+// 同时注入只读 + 可修改
+#[chain]
+fn handle_test(_args: EntryTest, config: &ResConfig, counter: &mut ResCounter) -> Next {
+ println!("config: {}", config.0);
+ counter.0 += 1;
+ ResultDone::default().to_render()
+}
+```
+
+如果要深入了解 `ResourceMarker`、`LazyRes` 惰性加载等进阶内容,可以查看 [核心概念:资源系统](pages/concepts/2-resource)。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/12-exit-code.md b/docs/_zh_CN/pages/12-exit-code.md
new file mode 100644
index 0000000..7c55b60
--- /dev/null
+++ b/docs/_zh_CN/pages/12-exit-code.md
@@ -0,0 +1,70 @@
+<h1 align="center">退出码控制</h1>
+<p align="center">
+ 如何使用资源系统管理程序退出码
+</p>
+
+程序退出时给 shell 一个正确的退出码是 CLI 的基本素养
+
+。Mingling 提供了开箱即用的 `ExitCodeSetup`,配合 `ResExitCode` 资源,让退出码控制变得极其简单。
+
+## 启用 ExitCodeSetup
+
+```rust
+@@@use mingling::prelude::*;
+@@@use mingling::setup::ExitCodeSetup;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(ExitCodeSetup::default());
+@@@ program.exec_and_exit();
+}
+```
+
+`ExitCodeSetup` 做了两件事:
+
+1. 注册 `ResExitCode` 资源(默认值为 `0`)
+2. 注册一个 `finish` hook,在程序退出前读取 `ResExitCode` 的值作为最终退出码
+
+## 修改退出码
+
+在 Chain 或 Renderer 中通过 `ResExitCode` 注入来修改退出码:
+
+```rust
+@@@use mingling::res::ResExitCode;
+@@@use mingling::setup::ExitCodeSetup;
+@@@pack!(EntryCheck = Vec<String>);
+#[chain]
+fn handle_check(_args: EntryCheck, ec: &mut ResExitCode) {
+ // 检查失败的时候修改退出码资源
+ ec.exit_code = 1;
+}
+```
+
+> [!TIP]
+> `ResExitCode` 就是一个 `struct ResExitCode { pub exit_code: i32 }`。`&mut ResExitCode` 注入后直接改字段即可。
+
+## `Program` 的三种执行方式
+
+`Program` 提供了三种执行方式(不包括 `repl` 特性下的 `exec_repl`):
+
+| 方式 | 行为 |
+| ------------------------------- | -------------------------------------------------------------------------- |
+| `program.exec_and_exit()` | 执行并直接以退出码终止进程 |
+| `program.exec()` | 执行后返回 `i32` 退出码,由调用方决定怎么处理 |
+| `program.exec_without_render()` | 返回 `Result<RenderResult, ProgramExecuteError>`,可读取内部的 `exit_code` |
+
+```rust
+@@@use mingling::setup::ExitCodeSetup;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(ExitCodeSetup::default());
+
+ // 获取退出码自行处理
+ let exit_code = program.exec();
+ std::process::exit(exit_code);
+}
+@@@gen_program!();
+```
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/13-hook.md b/docs/_zh_CN/pages/13-hook.md
new file mode 100644
index 0000000..543ee55
--- /dev/null
+++ b/docs/_zh_CN/pages/13-hook.md
@@ -0,0 +1,107 @@
+<h1 align="center">钩子系统</h1>
+<p align="center">
+ 如何使用 ProgramHook 向程序内部插入行为
+</p>
+
+Hook 让你在管线的各个生命周期节点插入自定义逻辑 —— 在 dispatch 之前、chain 之后、render 前后、程序退出时 ……
+
+你可以把横切关注点(日志、鉴权、指标收集)写在 hook 里,而不是散落在各处的业务代码中。
+
+## 基本用法
+
+`ProgramHook` 采用 builder 模式构造:
+
+```rust
+@@@use mingling::hook::ProgramHook;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_hook(
+ ProgramHook::empty()
+ .on_pre_chain(|info| {
+ println!("before chain: {}", info.input);
+ })
+ .on_post_render(|info| {
+ println!("after render: {}", info.result);
+ }),
+ );
+ program.exec_and_exit();
+}
+```
+
+> [!TIP]
+> `ProgramHook::empty()` 创建一个空 hook,然后链式调用 `.on_*()` 方法注册你关心的生命周期节点。没有注册的节点不会执行。
+
+## 生命周期节点
+
+Hook 覆盖了管线的完整生命周期:
+
+| 阶段 | Hook | 触发时机 |
+| ------------ | ------------------ | ------------- |
+| **Dispatch** | `on_begin` | 执行开始 |
+| | `on_pre_dispatch` | Dispatch 之前 |
+| | `on_post_dispatch` | Dispatch 之后 |
+| **Chain** | `on_pre_chain` | Chain 执行前 |
+| | `on_post_chain` | Chain 执行后 |
+| **Render** | `on_pre_render` | Render 执行前 |
+| | `on_post_render` | Render 执行后 |
+| **Finish** | `on_finish` | 程序退出前 |
+
+每个 hook 回调接收对应的 `Hook*Info` 结构体,里面包含当前上下文的信息(输入类型、参数、渲染结果等)。
+
+## 实际例子:记录操作日志
+
+```rust
+@@@use mingling::prelude::*;
+@@@use mingling::hook::ProgramHook;
+@@@
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@
+@@@#[chain] fn handle_greet(args: EntryGreet) -> Next {
+@@@ ResultName::new(args.inner.first().cloned().unwrap_or_default()).to_render()
+@@@}
+@@@#[renderer] fn render_name(r: ResultName) { r_println!("Hello, {}!", *r); }
+fn main() {
+ let mut program = ThisProgram::new();
+
+ // 记录每次 chain 执行前后的信息
+ program.with_hook(
+ ProgramHook::empty()
+ .on_pre_chain(|info| {
+ eprintln!("[hook] executing chain for: {}", info.input);
+ })
+ .on_post_chain(|info| {
+ eprintln!("[hook] chain output: {}", info.output.member_id);
+ }),
+ );
+
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+运行效果:
+
+```text
+[hook] executing chain for: EntryGreet
+[hook] chain output: ResultName
+Hello, World!
+```
+
+## 通过 Hook 控制行为
+
+Hook 不仅用来 "看",还可以用 `ProgramControlUnit` 改变程序行为:
+
+| 变体 | 效果 |
+| -------------------------- | ----------------------------------------- |
+| `Continue` | 什么都不做,继续执行 |
+| `OverrideExitCode(i32)` | 覆盖退出码 |
+| `RouteToChain(AnyOutput)` | 将当前数据替换为新的,重新进入 Chain 循环 |
+| `RouteToRender(AnyOutput)` | 跳过后续 Chain,直接渲染 |
+
+> [!NOTE]
+> Hook 可以注册多个,按注册顺序执行。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/14-testing.md b/docs/_zh_CN/pages/14-testing.md
new file mode 100644
index 0000000..af9d077
--- /dev/null
+++ b/docs/_zh_CN/pages/14-testing.md
@@ -0,0 +1,129 @@
+<h1 align="center">测试你的程序</h1>
+<p align="center">
+ 为 Chain 和 Renderer 编写单元测试
+</p>
+
+管线模型附带的一个好处就是 **可测试性**。
+
+Chain 只是一个接收输入、返回输出的函数,Renderer 也只是接收输入、写入内容的函数 —— 没有全局状态的黑魔法,测试起来很直接。
+
+## 测试 Renderer
+
+Renderer 是最容易测试的——调用函数,断言返回结果:
+
+```rust
+@@@pack!(ResultName = String);
+// 返回 String 而不是 ()
+#[renderer]
+fn render_name(name: ResultName) -> String {
+ r_println!("Hello, {}!", *name);
+}
+
+#[test]
+fn test_render_name() {
+ let result = render_name(ResultName::new("Alice".to_string()));
+ assert_eq!(result, "Hello, Alice!\n");
+}
+```
+
+注意到 Renderer 的返回值改成了 `-> String`——`#[renderer]` 会把 `RenderResult` 自动转换成你指定的返回类型(默认是 `()`)。返回 `String` 后你就可以直接断言输出内容了。
+
+## 测试 Chain
+
+测试 Chain 稍微复杂一点,因为它的返回值是 `Next`(实际是 `impl Into<ChainProcess<ThisProgram>>`)。需要用框架提供的断言宏:
+
+```rust
+@@@use mingling::{assert_member_id, assert_render_result, unpack_chain_process};
+@@@dispatcher!("hello", CMDHello => EntryHello);
+@@@pack!(ResultName = String);
+@@@pack!(ErrorNoName = ());
+@@@#[chain]
+@@@fn handle_hello(args: EntryHello) -> Next {
+@@@ let name = args.inner.first().cloned().unwrap_or_default();
+@@@ if name.is_empty() {
+@@@ ErrorNoName::default().to_render()
+@@@ } else {
+@@@ ResultName::new(name).to_render()
+@@@ }
+@@@}
+#[test]
+fn test_handle_hello_with_name() {
+ let chain_process = handle_hello(EntryGreet::new(vec!["Alice".to_string()])).into();
+ // 断言这是一个渲染结果(不是继续 chain)
+ assert_render_result!(chain_process);
+ // 断言 member_id 是 ResultName
+ assert_member_id!(chain_process, ResultName);
+ // 解包出内部值
+ let result_name = unpack_chain_process!(chain_process, ResultName);
+ assert_eq!(result_name.inner, "Alice");
+}
+```
+
+三个测试宏的作用:
+
+| 宏 | 功能 |
+| ----------------------- | --------------------------------------------- |
+| `assert_render_result!` | 断言 Chain 返回的是渲染路径(而非继续 chain) |
+| `assert_member_id!` | 断言返回值的成员 ID 是某个类型 |
+| `unpack_chain_process!` | 从 ChainProcess 中解包出原始类型 |
+
+## 用 entry! 宏构造数据
+
+如果启用了 `extra_macros`,可以用 `entry!` 快速构造 Entry:
+
+```rust
+// Features: ["extra_macros"]
+
+@@@use mingling::{assert_member_id, unpack_chain_process};
+@@@use mingling::macros::entry;
+@@@dispatcher!("hello", CMDHello => EntryHello);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_hello(args: EntryHello) -> Next {
+@@@ let name = args.inner.first().cloned().unwrap_or_default();
+@@@ ResultName::new(name).to_render()
+@@@}
+#[test]
+fn test_with_entry_macro() {
+ // entry! 从字符串字面量构造 Entry
+ let entry = entry!("--name", "Alice");
+ let chain_process = handle_hello(entry).into();
+ let result_name = unpack_chain_process!(chain_process, ResultName);
+ assert_eq!(result_name.inner, "Alice");
+}
+```
+
+## 测试资源注入
+
+如果 Chain 使用了资源,测试时需要提供资源实例:
+
+```rust
+@@@use mingling::{assert_render_result, unpack_chain_process};
+@@@#[derive(Default, Clone)]
+@@@struct ResPrefix(String);
+@@@dispatcher!("hello", CMDHello => EntryHello);
+@@@pack!(ResultGreeting = String);
+@@@
+#[chain]
+fn handle_hello(args: EntryHello, prefix: &ResPrefix) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ ResultGreeting::new(format!("{}, {}", prefix.0, name)).to_render()
+}
+
+#[test]
+fn test_handle_with_resource() {
+ // 资源需要在测试中手动传入
+ let result = handle_hello(
+ EntryHello::new(vec!["World".to_string()]),
+ &ResPrefix("Hello".to_string()),
+ );
+ let greeting = unpack_chain_process!(result, ResultGreeting, ThisProgram);
+ assert_eq!(greeting.inner, "Hello, World");
+}
+```
+
+管线模型让测试变得简单:每个 Chain 和 Renderer 都是相对独立的函数,构造输入、断言输出即可。
+
+<p align="center" style="font-size: 0.85em; color: clear;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/2-define-a-dispatcher.md b/docs/_zh_CN/pages/2-define-a-dispatcher.md
new file mode 100644
index 0000000..0afd911
--- /dev/null
+++ b/docs/_zh_CN/pages/2-define-a-dispatcher.md
@@ -0,0 +1,103 @@
+<h1 align="center">声明一个分发器</h1>
+<p align="center">
+ 使用 dispatcher! 宏声明命令,并注册
+</p>
+
+Mingling 的管线从 Dispatcher 开始。
+
+它的工作很简单:**匹配用户输入的命令,把参数包装成一个 Entry 类型**。
+
+## `dispatcher!` 宏
+
+`dispatcher!` 宏会同时生成两个类型:
+
+| 生成物 | 用途 |
+| ----------- | ----------------------------------------------- |
+| `CMDType` | 分发器本身,需要注册到 Program |
+| `EntryType` | 入口类型,包裹 `Vec<String>`,作为 Chain 的输入 |
+
+写法是固定的三个部分:
+
+```rust
+dispatcher!("命令路径", 分发器类型 => 入口类型);
+```
+
+看一个具体的例子:
+
+```rust
+dispatcher!("greet", CMDGreet => EntryGreet);
+```
+
+> [!NOTE]
+> 命令名(`"greet"`)会自动转换为 kebab-case。即使你写 `"GreetUser"`,匹配时也会变成 `greet-user`。
+
+## 注册到 Program
+
+有了分发器之后,需要告诉 Program 它的存在:
+
+```rust
+@@@ dispatcher!("greet", CMDGreet => EntryGreet);
+@@@ fn main() {
+@@@ let mut program = ThisProgram::new();
+// 注册分发器
+program.with_dispatcher(CMDGreet);
+@@@ }
+@@@ gen_program!();
+```
+
+> [!TIP]
+> 如果命令多了,可以用 `with_dispatchers` 一次注册多个:`program.with_dispatchers((CMDGreet, CMDAdd, CMDRemoteRm))`。
+
+## 多级命令
+
+如果你的程序有层级结构——比如 `remote add`、`remote rm`——只需要在命令名里加点号分隔:
+
+```rust
+dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd);
+dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm);
+```
+
+用户在终端输入 `remote add` 时,Mingling 会依次匹配 `remote` 和 `add` 两个层级。
+
+## 入口类型 `EntryGreet`
+
+你可能会好奇 `EntryGreet` 里面到底有什么。它本质上就是一个包装了 `Vec<String>` 的结构体:
+
+```rust
+// 示意,dispatcher! 宏实际生成的代码
+pub struct EntryGreet {
+ pub inner: Vec<String>,
+}
+```
+
+用户在命令行输入 `greet Alice Bob`,`EntryGreet.inner` 就是 `vec!["Alice", "Bob"]`。
+
+> [!IMPORTANT]
+> Entry 的 `inner` 只包含 **匹配后剩余的参数**。
+>
+> 以 `remote add origin` 为例,`remote` 和 `add` 用于匹配命令路径,只有 `origin` 会进入 `EntryRemoteAdd.inner`。
+
+## 进阶:隐式声明
+
+以上是标准写法。如果你启用了 `extra_macros` 特性,还可以更简洁:
+
+```rust
+// Features: ["extra_macros"]
+// 省略 CMDType 和 EntryType,名字自动推导
+ dispatcher!("greet");
+// dispatcher!("greet", CMDGreet => EntryGreet);
+```
+
+这种写法会自动生成 `CMDGreet` 和 `EntryGreet`,效果跟显式声明完全一样。
+
+不过在教程阶段,我们继续用显式写法——更清晰,也不依赖额外特性。
+
+详见[特性列表](pages/other/features)。
+
+## 下一步
+
+接下来我们写一个 Chain 来接收 Entry,处理真正的业务逻辑。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/3-define-a-chain.md b/docs/_zh_CN/pages/3-define-a-chain.md
new file mode 100644
index 0000000..7ac5c60
--- /dev/null
+++ b/docs/_zh_CN/pages/3-define-a-chain.md
@@ -0,0 +1,131 @@
+<h1 align="center">声明一个链</h1>
+<p align="center">
+ 使用 chain 宏声明链,并承接 Entry 输入
+</p>
+
+上一节我们声明了 `dispatcher!("greet", CMDGreet => EntryGreet)`
+
+现在用户输入 `greet` 时会被匹配并包装成 `EntryGreet`。
+
+但拿到 Entry 之后呢?
+
+我们需要一个 Chain 来处理它。
+
+## `#[chain]` 宏
+
+`#[chain]` 用来标记一个处理函数,格式非常直接:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+pack!(ResultName = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ // args 就是用户输入经过匹配后剩下的参数
+ let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string());
+ // 把结果包装成 Next,告诉调度器下一步去哪
+ ResultName::new(name)
+}
+```
+
+注意到了吗?
+
+Chain 函数签名里写着它需要什么——`args: EntryGreet`
+
+然后用 `ResultName::new(name)` 返回一个新类型。
+
+这个返回的 `Next` 会展开成 `impl Into<ChainProcess<ThisProgram>>`。
+
+> [!TIP]
+> 想知道 `Into<ChainProcess<G>>` 是怎么工作的?
+>
+> 可以去 [任意输出机制](pages/concepts/3-any-output) 章节了解 `ChainProcess`。
+
+## `pack!` 宏
+
+你大概猜到了,`pack!(ResultName = String)` 定义了一个管线中传递的类型:
+
+```rust
+// pack!(ResultName = String) 大概生成了这样的代码
+
+#[derive(Groupped)]
+pub struct ResultName {
+ pub inner: String,
+}
+```
+
+你可以把它理解为一个 打了标签的 `String`。
+
+调度器通过这个标签来精确路由,确保数据不会混淆 —— 比如发给 `RenderGreet` 的数据不会被误传给 `RenderError`。
+
+> [!NOTE]
+> 与简单的类型别名 (`type`) 不同,`pack!` 会生成一个全新的类型,拥有独立的 `TypeId`。
+
+命名上推荐这样的习惯:
+
+| 角色 | 命名模式 | 示例 |
+| -------- | ---------------- | -------------------- |
+| 入口 | `Entry` + 命令名 | `EntryGreet` |
+| 中间状态 | `State` + 描述 | `StateParsedArgs` |
+| 最终结果 | `Result` + 描述 | `ResultGreetSomeone` |
+| 错误 | `Error` + 描述 | `ErrorUserNotFound` |
+
+详见 [命名规范](pages/other/naming_rule),不过现在你只需要记住:**用 `pack!` 给你的数据取一个有意义的名字**。
+
+## 从 Entry 中提取参数
+
+`EntryGreet` 的 `inner` 是一个 `Vec<String>`,你可以在 Chain 里自由地处理它:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ // 取第一个参数,没有就用默认值
+ let name = args
+ .inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+
+ ResultName::new(name)
+}
+```
+
+如果你启用了 `parser` 特性,还可以用 `Picker` 做更灵活的参数提取,不过那是后话了。
+
+## 组合起来
+
+现在把 Dispatcher 和 Chain 连在一起:
+
+```rust
+// 1. 声明命令
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+// 2. 声明管线中的数据类型
+pack!(ResultName = String);
+
+// 3. 处理逻辑
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+ ResultName::new(name)
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+gen_program!();
+```
+
+不过这段代码还没写完 —— 我们只有 Dispatcher 和 Chain,还差最后一步:**把结果渲染出来**。这就是下一篇要讲的 Renderer。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/4-render-result.md b/docs/_zh_CN/pages/4-render-result.md
new file mode 100644
index 0000000..9fac8a4
--- /dev/null
+++ b/docs/_zh_CN/pages/4-render-result.md
@@ -0,0 +1,142 @@
+<h1 align="center">将结果渲染</h1>
+<p align="center">
+ 使用 renderer 宏声明渲染器,将结果输出
+</p>
+
+现在,我们创建了 Dispatcher 和 Chain,也通过 `pack!` 产出了一个 Result 类型。最后一步:**把结果展示给用户**。
+
+## `#[renderer]` 宏
+
+跟 `#[chain]` 类似,`#[renderer]` 用于标记一个输出函数:
+
+```rust
+@@@pack!(ResultName = String);
+#[renderer]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
+}
+```
+
+Renderer 接收 Chain 产出的结果,然后用 `r_println!` 输出。这个 `r_println!` 跟我们平常用的 `println!` 有什么区别?
+
+## `r_println!` 和 `r_print!` 宏
+
+`r_println!` 和 `r_print!` 是 Mingling 提供的打印宏,它们把内容写入 `RenderResult` 而不是直接输出到终端。这样做的好处是:
+
+1. **RenderResult 持有退出码**——你可以设置程序以特定退出码结束
+2. **方便测试**——可以捕获渲染结果做断言
+3. **便于后处理**——你可以将结果捕获,统一进行文本后处理
+
+> [!TIP]
+> 如果只是简单打印,你可以先把它理解为 `println!` 的平替。用 `r_println!` 替换 `println!` 不会错。
+
+## 完整的可运行程序
+
+把三篇教程的内容合在一起,你的第一个 Mingling 程序就完整了:
+
+```rust
+// 1. 用 Dispatcher 声明命令
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+// 2. 用 pack! 声明结果数据
+pack!(ResultName = String);
+
+// 3. 用 Chain 处理逻辑
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+ ResultName::new(name)
+}
+
+// 4. 用 Renderer 输出结果
+#[renderer]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
+}
+
+// 5. 在 main 函数内装配程序并运行
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+// 6. 使用 gen_program! 生成完整程序
+gen_program!();
+```
+
+## 跑起来试试
+
+```bash
+~# cargo run -- greet Alice
+```
+
+```text
+Hello, Alice!
+```
+
+试试不给参数:
+
+```bash
+~# cargo run -- greet
+```
+
+```text
+Hello, World!
+```
+
+试试不存在的命令:
+
+```bash
+cargo run -- great
+```
+
+```text
+# 什么也没输出!
+```
+
+## 补上 Fallback
+
+`gen_program!()` 自动生成了一个 `ErrorDispatcherNotFound` 类型,包裹 `Vec<String>`——它存的是用户输入的那些没匹配到的命令。你只需要给它写一个 Renderer:
+
+```rust
+#[renderer]
+fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) {
+ if err.inner.is_empty() {
+ r_println!("Unknown command");
+ } else {
+ r_println!("Command not found: \"{}\"", err.inner.join(" "));
+ }
+}
+```
+
+加上之后,再试试不存在的命令:
+
+```bash
+cargo run -- great
+```
+
+```text
+Command not found: "great"
+```
+
+## 恭喜
+
+你完成了第一个完整的 Mingling 程序!来回顾一下学到的东西:
+
+| 概念 | 对应宏/函数 | 一句话 |
+| -------- | ---------------- | -------------------------- |
+| 声明命令 | `dispatcher!` | 告诉程序用户能输入什么 |
+| 处理逻辑 | `#[chain]` | 收到参数后做什么 |
+| 输出结果 | `#[renderer]` | 怎么把结果告诉用户 |
+| 类型包装 | `pack!` | 给你的数据取个有意义的名字 |
+| 程序入口 | `gen_program!()` | 自动生成管线的接线图 |
+
+在真实项目中你还会用到资源注入、hook、补全、REPL 等高级功能,不过核心骨架永远不变:**Dispatcher → Chain → Renderer**。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/5-multiple-commands.md b/docs/_zh_CN/pages/5-multiple-commands.md
new file mode 100644
index 0000000..4a9c72d
--- /dev/null
+++ b/docs/_zh_CN/pages/5-multiple-commands.md
@@ -0,0 +1,109 @@
+<h1 align="center">多命令程序</h1>
+<p align="center">
+ 在一个程序里添加多个命令
+</p>
+
+真实世界的 CLI 很少只有一个命令。这篇我们来扩展之前的 greet 程序,加上第二个命令,看看多命令的程序长什么样。
+
+## 添加第二个命令
+
+继续在同一个项目里操作:
+
+```rust
+// 声明两个命令
+dispatcher!("greet", CMDGreet => EntryGreet);
+dispatcher!("add", CMDAdd => EntryAdd);
+
+pack!(ResultGreeting = String);
+pack!(ResultSum = i32);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string());
+ ResultGreeting::new(name)
+}
+
+#[chain]
+fn handle_add(args: EntryAdd) -> Next {
+ let sum: i32 = args.inner.iter().filter_map(|s| s.parse::<i32>().ok()).sum();
+ ResultSum::new(sum)
+}
+
+#[renderer]
+fn render_greet(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
+}
+
+#[renderer]
+fn render_sum(result: ResultSum) {
+ r_println!("Sum: {}", *result);
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatchers((CMDGreet, CMDAdd));
+ program.exec_and_exit();
+}
+
+gen_program!();
+```
+
+两个命令共享同一个管线模型,但各走各的:
+
+```text
+> my-cli greet Alice
+Hello, Alice!
+> my-cli add 1 2 3
+Sum: 6
+```
+
+## 注册多个分发器
+
+注意到 `with_dispatchers` 了吗?当你需要注册多个分发器时,一次传一个元组就行:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@dispatcher!("add", CMDAdd => EntryAdd);
+@@@pack!(ResultGreeting = String);
+@@@pack!(ResultSum = i32);
+@@@#[chain] fn handle_greet(_args: EntryGreet) -> Next { ResultGreeting::new("ok".into()) }
+@@@#[renderer] fn render_greet(_greeting: ResultGreeting) { r_println!("hi"); }
+@@@#[chain] fn handle_add(_args: EntryAdd) -> Next { ResultSum::new(0) }
+@@@#[renderer] fn render_sum(_sum: ResultSum) { r_println!("sum"); }
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatchers((CMDGreet, CMDAdd));
+ program.exec_and_exit();
+}
+```
+
+等价于一个个注册,效果一样。
+
+> [!TIP]
+> 元组最多支持 7 个分发器。超过 7 个时链式调用 `with_dispatcher` 就行。
+
+## 子命令
+
+多层级的命令也是同理——每个点号分隔的层级都只是名字的一部分:
+
+```rust
+dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd);
+dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm);
+```
+
+每个子命令的 Entry、Chain、Renderer 完全独立,互不干扰。
+
+## 数据类型的独立性
+
+注意我们用了两个不同的 `pack!`:
+
+- `pack!(ResultGreeting = String)`
+- `pack!(ResultSum = i32)`
+
+它们都是独立的类型,`gen_program!()` 会给它们分配不同的枚举变体。
+
+调度器永远不会把 `ResultGreeting` 的数据送到 `render_sum` 去 —— **类型安全从命名那一刻就保证了**。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/6-argument-parse-picker.md b/docs/_zh_CN/pages/6-argument-parse-picker.md
new file mode 100644
index 0000000..9c7e028
--- /dev/null
+++ b/docs/_zh_CN/pages/6-argument-parse-picker.md
@@ -0,0 +1,393 @@
+<h1 align="center">使用 Picker 完成参数解析</h1>
+<p align="center">
+ 用 Picker 完成基本的参数解析
+</p>
+
+前面教程中我们都是手动从 `EntryGreet.inner`(`Vec<String>`)中提取参数。
+
+```rust
+@@@ fn main() {
+@@@ let args : Vec<String> = vec![];
+let name = args.first().cloned().unwrap_or_else(|| "World".to_string());
+@@@ }
+```
+
+但是,对于参数较多的场景,这个方案就不够用了:Mingling 提供了 `Picker` —— 通过链式调用来提取和转换参数。
+
+要启用 `Picker`,你需要修改 `Cargo.toml`
+
+```toml
+# Cargo.toml
+[dependencies.mingling]
+features = ["parser"]
+```
+
+好了,让我们看看 `Picker` 的写法:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let name = prev.pick_or((), "World").unpack();
+ ResultName::new(name)
+}
+```
+
+`AsPicker` 为所有可以转换为 `Vec<String>` 的类型实现了 `pick`、`pick_or`、`pick_or_route` 函数:它们可以语义化地从字符串列表中 **拾取 (Pick)** 参数,并转换为结构化数据。
+
+对于上述示例中的代码:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_greet_entry(prev: EntryGreet) -> Next {
+let name = prev.pick_or((), "World").unpack();
+@@@ResultName::new(name)
+@@@}
+```
+
+它的语义为:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_greet_entry(prev: EntryGreet) {
+@@@let name: String =
+ prev.pick_or((), "World").unpack();
+// ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~
+// | | | | |_ 解包为 String
+// | | | |__________ 默认值为 "World"
+// | | |______________ 取出第一个位置参数(不指定标志)
+// | |______________________ 拾取或使用默认
+// |___________________________ 从前一个输入中
+@@@}
+```
+
+## 解析标志参数
+
+若你的程序需要解析标志参数(例如 `greet --name Alice`),可以使用如下方式
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let name = prev.pick_or(["--name", "-n"], "World").unpack();
+ ResultName::new(name)
+}
+```
+
+同理,它的语义为:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_greet_entry(prev: EntryGreet) {
+@@@let name: String =
+ prev.pick_or(["--name", "-n"], "World").unpack();
+// ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~
+// | | | | |_ 解包为 String
+// | | | |__________ 默认值为 "World"
+// | | |____________________________ 取出 "--name" 或 "-n" 后面的参数
+// | |____________________________________ 拾取或使用默认
+// |_________________________________________ 从前一个输入中
+@@@}
+```
+
+## 关于 `.unpack()`
+
+你可能注意到了,`Picker` 在命令解析的最后,会执行一个 `.unpack()` 函数,它的作用是将前面解析出来的结果,转换为结构化信息。
+
+对于只拾取了一次的数据来说,`.unpack()` 会返回单个数据,而对于多次拾取,`Picker` 则会返回元组:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("test", CMDTest => EntryTest);
+@@@pack!(ResultInfo = (String, u8, u32));
+
+#[chain]
+fn handle_test_entry(prev: EntryTest) -> Next {
+ let (name, age, id) = prev
+ .pick::<String>(["--name", "-n"])
+ .pick::<u8>(["--age", "-a"])
+ .pick::<u32>(["--id", "-I"])
+ .unpack();
+
+ ResultInfo::new((name, age, id))
+}
+```
+
+> [!IMPORTANT]
+> `Picker` 对解析顺序极其敏感,特别是位置参数:因为它是顺序解析的。若你需要解析位置参数,请确保解析前已拾取并消费所有 **标志参数**。
+
+## 使用 `pick_or_route` 处理边界情况
+
+就像那句老话:"永远不要相信你的用户"。为了应对必要参数缺失、输入类型不匹配等错误情况,`pick_or_route` 能将执行链路由到专门的错误处理类型上。
+
+先来看一个简单示例
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@use mingling::macros::route;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@pack!(ErrorNoName = ());
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let pick_result = prev
+ .pick_or_route(["--name", "-n"], ErrorNoName::default())
+ .unpack();
+
+ // 使用 route! 宏展开 pick_result
+ let name = route!(pick_result);
+ ResultName::new(name).into()
+}
+
+#[renderer]
+fn render_no_name(_prev: ErrorNoName) {
+ r_println!("Error: No name provided.");
+}
+
+#[renderer]
+fn render_name(prev: ResultName) {
+ r_println!("Hello, {}!", *prev);
+}
+```
+
+若使用 `pick_or_route`,写法会变得相对复杂:因为 `.unpack()` 不再直接返回参数,而是 `Result<Value, Route>`。
+
+不过 **Mingling** 的 `extra_macros` 特性提供了简化展开的宏 `route!`,它不复杂,只是省略了一部分样板代码:
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@ pack!(ErrorFail = ());
+@@@ use mingling::macros::route;
+@@@ fn func() -> mingling::ChainProcess<ThisProgram> {
+@@@ let args: Vec<String> = vec![];
+@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack();
+let name = route!(pick_result);
+@@@ mingling::macros::empty_result!()
+@@@ }
+```
+
+它展开为:
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@ pack!(ErrorFail = ());
+@@@ fn func() -> mingling::ChainProcess<ThisProgram> {
+@@@ let args: Vec<String> = vec![];
+@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack();
+let name = match pick_result {
+ Ok(r) => r,
+ Err(e) => return e.to_chain(),
+};
+@@@ mingling::macros::empty_result!()
+@@@ }
+```
+
+## 提取值的后处理
+
+在您使用 `pick` 提取了用户输入后,可以使用 `after` 立刻处理该参数
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let name = prev
+ .pick_or(["--name", "-n"], "World")
+ // 在提取出 --name 后,立刻格式化
+ .after(|name: String| {
+ name.replace(['-', '_', '.'], " ")
+ .to_lowercase()
+ .trim()
+ .to_string()
+ })
+ .unpack();
+
+ ResultName::new(name)
+}
+```
+
+同样,你可以使用 `after_or_route` 来处理输入参数的格式错误
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@use mingling::macros::route;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@pack!(ErrorNameTooLong = usize);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let pick_result = prev
+ .pick_or(["--name", "-n"], "World")
+ .after_or_route(|name: &String| {
+ if name.len() < 32 {
+ Ok(name.clone())
+ } else {
+ Err(ErrorNameTooLong::new(name.len()))
+ }
+ })
+ .unpack();
+ let name = route!(pick_result);
+
+ ResultName::new(name).into()
+}
+
+#[renderer]
+fn render_name_too_long(prev: ErrorNameTooLong) {
+ let len = *prev;
+ r_println!("Error: name too long (length: {} > 32)", len);
+}
+
+#[renderer]
+fn render_name(prev: ResultName) {
+ r_println!("Hello, {}!", *prev);
+}
+```
+
+## 布尔值解析
+
+`Picker` 当然也可以解析布尔类型,但是布尔类型分为显式和隐式模式:
+
+| 模式 | 格式 |
+| ---- | ----------------------------------- |
+| 隐式 | `--confirmed` |
+| 显式 | `--confirm true` 或 `--confirm yes` |
+
+- 使用 `.pick::<bool>(flag)` 时,采用隐式解析:只要标志存在即为 `true`
+- 使用 `.pick::<Yes>(flag)` 或 `.pick::<True>(flag)` 时,采用显式解析
+
+一般来说使用隐式解析即可,但在处理重要的确认行为时,显式逻辑更符合语义。
+
+```rust
+// Features: ["parser"]
+@@@use mingling::parser::Yes;
+@@@dispatcher!("test", CMDTest => EntryTest);
+@@@pack!(ResultDone = ());
+
+#[chain]
+fn handle_entry(prev: EntryTest) -> Next {
+@@@ let prev1 = prev.clone();
+ let _confirmed: bool = prev.pick::<Yes>(()).unpack().is_yes();
+@@@ let prev = prev1;
+ let _confirm: bool = prev.pick::<bool>(["--confirm", "-C"]).unpack();
+ ResultDone::default().to_render()
+}
+```
+
+## 特殊用法:`usize` 解析
+
+**Mingling** 为 `usize` 提供了一个特殊的用法:解析类似 `25G`、`32mib` 等字样
+
+```rust
+// Features: ["parser"]
+
+#[test]
+fn parse_size() {
+ let vec = vec!["--size".to_string(), "25mib".to_string()];
+ let size: usize = vec.pick(["--size", "-S"]).unpack();
+ assert_eq!(size, 25 * 1024 * 1024);
+}
+```
+
+## 自定义可解析类型
+
+你可以使用 `Pickable` trait 使你的类型支持被 `Picker` 解析,这也是 `Picker` 拓展性的来源
+
+```rust
+// Features: ["parser"]
+@@@use mingling::parser::{Pickable, Argument};
+@@@use mingling::Flag;
+#[derive(Default)]
+pub struct Address {
+ ip: String,
+ port: u16,
+}
+
+impl Pickable for Address {
+ type Output = Self;
+ fn pick(args: &mut Argument, flag: Flag) -> Option<Self::Output> {
+ let raw = args.pick_argument(flag)?;
+ let parts: Vec<&str> = raw.split(':').collect();
+ let ip = parts.first()?.to_string();
+ let port: u16 = parts.get(1)?.parse().ok()?;
+ Some(Address { ip, port })
+ }
+}
+@@@dispatcher!("connect", CMDConnect => EntryConnect);
+@@@pack!(ResultConnected = Address);
+
+#[chain]
+fn handle_connect_entry(prev: EntryConnect) -> Next {
+ let address: Address = prev.pick("--addr").unpack();
+ ResultConnected::new(address)
+}
+
+#[renderer]
+fn render_connected(prev: ResultConnected) {
+ let addr = prev.inner;
+ r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port);
+}
+```
+
+执行效果如下:
+
+```text
+~# my-cli connect --addr 127.0.0.1:8080
+Connected: IP: 127.0.0.1 PORT: 8080
+```
+
+## 自动为枚举实现 Pickable
+
+要为枚举类型实现 `Pickable`,只需该枚举实现了 `EnumTag`,然后为其实现 `PickableEnum` 即可
+
+```rust
+// Features: ["parser"]
+@@@use mingling::parser::PickableEnum;
+@@@use mingling::EnumTag;
+#[derive(Debug, Default, EnumTag)]
+pub enum Fruits {
+ #[default]
+ Apple,
+ Banana,
+ Orange,
+}
+
+impl PickableEnum for Fruits {}
+@@@dispatcher!("eat", CMDEat => EntryEat);
+@@@pack!(ResultFruit = Fruits);
+
+#[chain]
+fn handle_eat_entry(prev: EntryEat) -> Next {
+ let fruit: Fruits = prev.pick("--fruit").unpack();
+ ResultFruit::new(fruit)
+}
+
+#[renderer]
+fn render_fruit(prev: ResultFruit) {
+ r_println!("Picked fruit: {:?}", *prev);
+}
+```
+
+以上便是 `Picker` 的所有用法。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/7-argument-parse-clap.md b/docs/_zh_CN/pages/7-argument-parse-clap.md
new file mode 100644
index 0000000..e15bebb
--- /dev/null
+++ b/docs/_zh_CN/pages/7-argument-parse-clap.md
@@ -0,0 +1,87 @@
+<h1 align="center">使用 Clap 完成参数解析</h1>
+<p align="center">
+ 用 clap 做更复杂的参数解析
+</p>
+
+Picker 适合轻量级的参数提取,但当参数数量多、有复杂的校验规则、或者需要自动生成 `--help` 时,可以接入 [clap](https://crates.io/crates/clap)。
+
+## 开启 clap 特性
+
+```toml
+[dependencies.mingling]
+features = ["clap"]
+
+[dependencies.clap]
+version = "4"
+features = ["derive", "color"]
+```
+
+## dispatcher_clap
+
+`#[dispatcher_clap]` 加在 `clap::Parser` 结构体上,自动生成 Dispatcher:
+
+```rust
+// Features: ["clap"]
+// Dependencies:
+// clap = "4"
+@@@ use mingling::macros::dispatcher_clap;
+#[derive(Default, clap::Parser, Groupped)]
+#[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)]
+pub struct EntryGreet {
+ #[clap(default_value = "World")]
+ name: String,
+ #[arg(short, long, default_value_t = 1)]
+ repeat: i32,
+}
+
+#[renderer]
+fn render_greet(greet: EntryGreet) {
+ let count = greet.repeat.max(0) as usize;
+ r_print!("Hello, ");
+ for _ in 0..count {
+ r_print!("{} ", greet.name);
+ }
+ r_println!("!");
+}
+
+#[renderer]
+fn render_greet_parse_failed(err: ErrorGreetParsed) {
+ r_println!("{}", *err);
+}
+```
+
+## 与 BasicProgramSetup 配合
+
+如果需要 `--help` 支持,在 main 中注册 `BasicProgramSetup` 并设置 clap 帮助的输出模式:
+
+```rust
+// Features: ["clap"]
+// Dependencies:
+// clap = "4"
+@@@use mingling::setup::BasicProgramSetup;
+@@@use mingling::macros::dispatcher_clap;
+@@@#[derive(Default, clap::Parser, Groupped)]
+@@@#[dispatcher_clap("greet", CMDGreet)]
+@@@pub struct EntryGreet {
+@@@ name: String,
+@@@}
+@@@#[renderer]
+@@@fn render_greet(greet: EntryGreet) {
+@@@ r_println!("Hello, {}!", greet.name);
+@@@}
+@@@
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup);
+ program.stdout_setting.clap_help_print_behaviour =
+ mingling::ClapHelpPrintBehaviour::WriteToRenderResult;
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+详见 [example-clap-binding](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-clap-binding)。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/8-setup-and-resources.md b/docs/_zh_CN/pages/8-setup-and-resources.md
new file mode 100644
index 0000000..d38e69d
--- /dev/null
+++ b/docs/_zh_CN/pages/8-setup-and-resources.md
@@ -0,0 +1,89 @@
+<h1 align="center">程序装配</h1>
+<p align="center">
+ 用 Setup 初始化你的程序
+</p>
+
+当程序启动时需要做一些初始化工作——比如解析全局参数、注册资源——你可以用 `#[program_setup]` 来组织这些逻辑。
+
+## 用 Setup 做初始化
+
+```rust
+// Features: ["extra_macros"]
+@@@use mingling::macros::program_setup;
+@@@use mingling::Program;
+#[program_setup]
+fn my_setup(program: &mut Program<ThisProgram>) {
+ // 从参数中提取全局标志
+ program.global_flag(["-v", "--verbose"], |program| {
+ program.stdout_setting.verbose = true;
+ });
+}
+@@@
+@@@fn main() {
+@@@ let mut program = ThisProgram::new();
+@@@ program.with_setup(MySetup);
+@@@ program.exec_and_exit();
+@@@}
+@@@gen_program!();
+```
+
+`#[program_setup]` 标记的函数接收 `&mut Program<ThisProgram>`,你可以在里面做任何初始化操作。
+
+在 `main` 里通过 `program.with_setup(...)` 注册即可使用。
+
+> [!NOTE]
+> `#[program_setup]` 需要 `extra_macros` 特性。没有此特性时,可以手动实现 `ProgramSetup` trait。
+
+## 提取全局参数
+
+Setup 里最常用的操作就是提取全局参数。Mingling 提供了几个辅助方法:
+
+```rust
+// Features: ["extra_macros"]
+@@@use mingling::macros::program_setup;
+@@@use mingling::Program;
+#[program_setup]
+fn my_setup(program: &mut Program<ThisProgram>) {
+ // 布尔标志
+ program.global_flag(["-v", "--verbose"], |program| {
+ program.stdout_setting.verbose = true;
+ });
+
+ // 带值的参数
+ program.global_argument("--name", |_program, value| {
+ // value 就是 "Alice"
+ let _ = value;
+ });
+}
+```
+
+> [!TIP]
+> `global_flag` 和 `global_argument` 会自动从 `program.args` 中移除已匹配的参数,这些参数不会进入管线。
+
+## 内置 Setup
+
+Mingling 提供了一些开箱即用的 Setup,覆盖了 CLI 程序最常见的需求:
+
+| Setup | 功能 |
+| --------------------------- | --------------------------------------------------------------------- |
+| `BasicProgramSetup` | 解析 `--help`/`-h`、`--quiet`/`-q`、`--confirm`/`-C` |
+| `DirectoryEnvironmentSetup` | 注册目录资源:当前目录、可执行目录、Home 目录、临时目录 |
+| `ExitCodeSetup` | 通过 `ResExitCode` 控制程序退出码 |
+| `StructuralRendererSetup` | 启用 `--json`、`--yaml` 等结构化输出(需 `structural_renderer` 特性) |
+
+用法就是在 `main` 里加一行:
+
+```rust
+@@@use mingling::setup::BasicProgramSetup;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup);
+ program.exec_and_exit();
+}
+```
+
+`BasicProgramSetup` 帮你处理了绝大多数 CLI 程序都需要的通用参数,省去自己手动解析的麻烦。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/9-error-handling.md b/docs/_zh_CN/pages/9-error-handling.md
new file mode 100644
index 0000000..a4d5b0a
--- /dev/null
+++ b/docs/_zh_CN/pages/9-error-handling.md
@@ -0,0 +1,120 @@
+<h1 align="center">错误处理</h1>
+<p align="center">
+ 将错误优雅地展示给用户
+</p>
+
+管线里不只有成功路径。当输入有误、资源找不到、操作失败时,你需要一个地方来处理这些"意外",而不是让程序 panic。
+
+## 两个路径:成功 vs 错误
+
+回顾管线模型,Chain 的返回值是 `Next`,它有两个去向:
+
+| 路由 | 含义 |
+| -------------- | ---------------------------- |
+| `.to_render()` | 出结果了,交给 Renderer 展示 |
+| `.to_chain()` | 还没处理完,交给下一个 Chain |
+
+错误类型的值也可以走任意一条路——你可以选择直接渲染错误信息,也可以交给下一个 Chain 尝试恢复。
+
+## 用独立类型区分错误
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+pack!(ResultGreeting = String);
+pack!(ErrorNameEmpty = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+
+ if name.is_empty() {
+ ErrorNameEmpty::new("name is required".to_string()).to_render()
+ } else {
+ ResultGreeting::new(name).to_render()
+ }
+}
+```
+
+然后各自写 Renderer:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultGreeting = String);
+@@@pack!(ErrorNameEmpty = String);
+@@@#[chain] fn handle_greet(args: EntryGreet) -> Next { ResultGreeting::new(args.inner.first().cloned().unwrap_or_default()).to_render() }
+
+#[renderer]
+fn render_greeting(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
+}
+
+#[renderer]
+fn render_error_name_empty(err: ErrorNameEmpty) {
+ r_println!("Error: {}", *err);
+}
+```
+
+两个 Renderer 各司其职,用户看到什么取决于 Chain 返回了什么。
+
+## 完整的例子
+
+```rust
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+pack!(ResultGreeting = String);
+pack!(ErrorNameEmpty = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ if name.is_empty() {
+ ErrorNameEmpty::new("name is required".to_string()).to_render()
+ } else {
+ ResultGreeting::new(name).to_render()
+ }
+}
+
+#[renderer]
+fn render_greeting(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
+}
+
+#[renderer]
+fn render_error_name_empty(err: ErrorNameEmpty) {
+ r_println!("Error: {}", *err);
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+gen_program!();
+```
+
+运行效果:
+
+```text
+~# my-cli greet Alice
+Hello, Alice!
+
+~# my-cli greet
+Error: name is required
+```
+
+## 关于 `pack_err!`
+
+如果你启用了 `extra_macros`,还可以用 `pack_err!` 快速声明带有自动 `name` 字段的错误类型:
+
+```rust
+// Features: ["extra_macros"]
+pack_err!(ErrorNotFound);
+// 生成: struct ErrorNotFound { pub name: String }
+```
+
+详见 [特性列表](pages/other/features)。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/advanced/.name b/docs/_zh_CN/pages/advanced/.name
new file mode 100644
index 0000000..eb92a2d
--- /dev/null
+++ b/docs/_zh_CN/pages/advanced/.name
@@ -0,0 +1 @@
+进阶
diff --git a/docs/_zh_CN/pages/advanced/1-completion.md b/docs/_zh_CN/pages/advanced/1-completion.md
new file mode 100644
index 0000000..3941404
--- /dev/null
+++ b/docs/_zh_CN/pages/advanced/1-completion.md
@@ -0,0 +1,83 @@
+<h1 align="center">补全</h1>
+<p align="center">
+ 使用 comp 特性实现完全动态的补全系统
+</p>
+
+Mingling 的补全是**完全动态**的——没有静态的补全文件,而是在运行时根据用户当前输入实时计算补全建议。
+
+## 开启 comp
+
+```toml
+# Cargo.toml
+[dependencies.mingling]
+features = ["comp"]
+
+[build-dependencies.mingling]
+features = [
+ "comp",
+ # 启用 `builds` 特性以提供构建期支持
+ "builds"
+]
+```
+
+## 工作原理
+
+当用户按下 `TAB` 时,补全脚本会调用程序的隐藏子命令 `__comp`,它会根据输入的 `ShellContext` 动态地查询最合适的建议。
+
+这个隐藏子命令由 `gen_program!()` 在启用 `comp` 特性时自动生成,对应的分发器是 `CMDCompletion`,你需要使用 `with_dispatcher` 添加到程序中。
+
+补全流程:
+
+1. 二次匹配用户当前输入的 `Dispatcher`
+2. 调用对应的 `#[completion]` 函数
+3. 函数返回 `Suggest`(文件补全或建议列表)
+4. 通知 Shell 将建议呈现出来
+
+## 定义补全
+
+用 `#[completion(EntryType)]` 为 Entry 定义补全逻辑:
+
+```rust
+// Features: ["comp"]
+@@@use mingling::prelude::*;
+@@@use mingling::{ShellContext, Suggest, SuggestItem};
+@@@use std::collections::BTreeSet;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+
+#[completion(EntryGreet)]
+fn complete_greet(ctx: &ShellContext) -> Suggest {
+ if ctx.previous_word == "greet" {
+ let mut items = BTreeSet::new();
+ items.insert(SuggestItem::new_with_desc("Alice".into(), "Likes to receive messages".into()));
+ items.insert(SuggestItem::new("World".into()));
+ Suggest::Suggest(items)
+ } else {
+ Suggest::FileCompletion
+ }
+}
+```
+
+`suggest!` 宏是更简洁的写法,效果相同:
+
+```rust
+// Features: ["comp"]
+@@@use mingling::macros::suggest;
+@@@fn example() {
+suggest! {
+ "Alice": "Likes to receive messages",
+ "World"
+};
+@@@}
+```
+
+`ShellContext` 包含用户当前的输入状态(`previous_word`、`current_word`、`all_words` 等),`Suggest` 有两种变体:`Suggest::Suggest(list)` 返回建议列表,`Suggest::FileCompletion` 交给 shell 自己做文件补全。
+
+## 生成补全脚本
+
+在 `build.rs` 中调用 `build_comp_scripts` 生成补全脚本(需要 `builds` + `comp` 特性)。
+
+详见 [example-completion](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-completion)。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/advanced/2-structural-renderer.md b/docs/_zh_CN/pages/advanced/2-structural-renderer.md
new file mode 100644
index 0000000..14164da
--- /dev/null
+++ b/docs/_zh_CN/pages/advanced/2-structural-renderer.md
@@ -0,0 +1,120 @@
+<h1 align="center">结构化渲染</h1>
+<p align="center">
+ 使用 structural_renderer 特性将结果渲染为序列化文本
+</p>
+
+启用 `structural_renderer` 后,你的程序可以通过 `--json`、`--yaml` 等参数将输出切换为结构化格式,方便与其他工具集成。
+
+## 开启特性
+
+```toml
+[dependencies.mingling]
+features = ["structural_renderer"]
+```
+
+`structural_renderer` 会自动启用 `json_serde_fmt`。
+
+如果需要更多格式,可以启用 `structural_renderer_full`(包含 JSON、YAML、TOML、RON)。
+
+> [!NOTE]
+> 若需要定制输出类型,可以查看 [特性](./pages/other/features)
+
+## 基本用法
+
+启用 `StructuralRendererSetup` 后,用 `pack_structural!` 替代 `pack!` 来声明支持结构化输出的类型:
+
+```rust
+// Features: ["structural_renderer"]
+// Dependencies:
+// serde = "1"
+@@@use mingling::setup::StructuralRendererSetup;
+@@@dispatcher!("render", CMDRender => EntryRender);
+
+// pack_structural! 等价于 pack! + StructuralData
+pack_structural!(ResultInfo = (String, i32));
+
+#[chain]
+fn handle_render(args: EntryRender) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
+ ResultInfo::new((name, age))
+}
+
+#[renderer]
+fn render_info(r: ResultInfo) {
+ r_println!("{:?}", *r);
+}
+```
+
+运行效果:
+
+```text
+~# my-cli render Bob 22
+("Bob", 22)
+
+~# my-cli render Bob 22 --json
+{"inner":["Bob",22]}
+```
+
+用户传入 `--json` 时,框架自动将渲染结果序列化为 JSON,无需修改业务代码。
+
+## 自定义输出结构
+
+`pack_structural!` 的默认输出包含 `inner` 字段。要完全控制输出结构,可以用 `#[derive(StructuralData, Serialize, Groupped)]` 手动定义类型:
+
+```rust
+// Features: ["structural_renderer"]
+// Dependencies:
+// serde = "1"
+@@@use mingling::prelude::*;
+@@@use mingling::setup::StructuralRendererSetup;
+@@@use mingling::StructuralData;
+@@@use serde::Serialize;
+@@@dispatcher!("render", CMDRender => EntryRender);
+
+#[derive(Serialize, StructuralData, Groupped)]
+struct Info {
+ name: String,
+ age: i32,
+}
+
+#[chain]
+fn handle_render(args: EntryRender) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
+ Info { name, age }.to_render()
+}
+
+#[renderer]
+fn render_info(info: Info) {
+ r_println!("{} is {} years old", info.name, info.age);
+}
+@@@
+@@@fn main() {
+@@@ let mut program = ThisProgram::new();
+@@@ program.with_setup(StructuralRendererSetup);
+@@@ program.with_dispatcher(CMDRender);
+@@@ program.exec();
+@@@}
+@@@gen_program!();
+```
+
+这时 `--json` 输出:
+
+```json
+{ "name": "Bob", "age": 22 }
+```
+
+## 注意事项
+
+- 支持格式:JSON、YAML、TOML、RON(取决于启用的特性)
+- `StructuralRendererSetup` 注册 `--json`、`--yaml`、`--toml`、`--ron` 等全局参数
+
+> [!NOTE]
+> 每个类型仍需一个**空的 Renderer**,否则该类型 **不被视为可渲染**
+
+详见 [example-structural-renderer](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-structural-renderer)。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/concepts/.name b/docs/_zh_CN/pages/concepts/.name
new file mode 100644
index 0000000..dfd4543
--- /dev/null
+++ b/docs/_zh_CN/pages/concepts/.name
@@ -0,0 +1 @@
+核心概念
diff --git a/docs/_zh_CN/pages/concepts/1-the-pipeline.md b/docs/_zh_CN/pages/concepts/1-the-pipeline.md
new file mode 100644
index 0000000..b0bb19d
--- /dev/null
+++ b/docs/_zh_CN/pages/concepts/1-the-pipeline.md
@@ -0,0 +1,129 @@
+<h1 align="center">基础管线</h1>
+<p align="center">
+ Mingling 的核心执行流程
+</p>
+
+Mingling 把命令的处理拆成三个独立的阶段:Dispatcher → Chain → Renderer。这篇讲实际的执行逻辑——从用户输入到最终输出,每一步发生了什么。
+
+## 完整流程
+
+```mermaid
+graph TD
+ A["program.exec_and_exit()"] --> B["Hook: pre_dispatch"]
+ B --> C["Dispatch<br/>匹配命令 → Entry"]
+ C --> D["Hook: post_dispatch"]
+ D --> E{"user_context.help?"}
+ E -->|"true"| F["render_help<br/>直接跳帮助渲染"]
+ E -->|"false"| G{"has_chain?"}
+ G -->|"有"| H["Hook: pre_chain"]
+ H --> I["do_chain<br/>执行业务逻辑"]
+ I --> J{"ChainProcess?"}
+ J -->|"Ok(any, Renderer)"| K["Hook: pre_render →<br/>render → post_render"]
+ J -->|"Ok(any, Chain)"| G
+ J -->|"Err"| L["finish"]
+ G -->|"无"| M{"has_renderer?"}
+ M -->|"有"| K
+ M -->|"无"| N["build_renderer_not_found"]
+ N --> G
+ K --> O["Hook: finish → 返回 RenderResult"]
+ L --> O
+ F --> O
+```
+
+## 阶段详解
+
+### 1. 分发
+
+`exec_with_args` 首先调用 `dispatch_args_dynamic` 或 `dispatch_args_trie`(取决于是否启用 `dispatch_tree` 特性),将用户输入的参数与注册的 Dispatcher 匹配。
+
+匹配规则是按空格分割的**前缀匹配**——优先匹配最长的那个。例如注册了 `remote.add` 和 `remote`,输入 `remote add origin` 会匹配 `remote.add`。
+
+```mermaid
+graph LR
+ Input["用户输入"] --> M{匹配 Dispatcher}
+ M -->|"匹配到"| E["调用 dispatcher.begin(args)<br/>返回包装好的 Entry"]
+ M -->|"未匹配"| NF["build_dispatcher_not_found<br/>生成 ErrorDispatcherNotFound"]
+```
+
+匹配成功后调用 `dispatcher.begin(args)`,返回 `ChainProcess::Ok((AnyOutput, _))`,即包装好用户输入参数的 Entry 类型。
+
+如果没有匹配到任何 Dispatcher,则生成 `ErrorDispatcherNotFound`(包裹完整的输入参数),后续可以被 Renderer 处理显示 "Command not found"。
+
+### 2. Help 短路
+
+在进入主循环前,检查 `program.user_context.help`。如果为 `true`(由 `BasicProgramSetup` 中的 `HelpFlagSetup` 在解析到 `--help` 时设置),直接调用 `render_help` 跳过整个管线。
+
+### 3. Chain 主循环
+
+这是核心调度逻辑。每次循环检查当前的 `AnyOutput`:
+
+1. **有 Chain 能处理** → 执行 `C::do_chain(current)`
+ - 返回 `(AnyOutput, Renderer)` → 退出循环,进入渲染
+ - 返回 `(AnyOutput, Chain)` → 继续循环,把结果交给下一个 Chain
+ - 返回 `Err` → 终止程序
+
+2. **没有 Chain,但有 Renderer** → 直接渲染
+
+3. **两者都没有** → 生成 `renderer_not_found`,再循环一次(因为刚生成的这个类型可能有 Renderer)
+
+```mermaid
+graph TD
+ Start["当前 AnyOutput"] --> C{"has_chain?"}
+ C -->|"是"| Chain["do_chain"]
+ Chain -->|"返回 (any, Chain)"| C
+ Chain -->|"返回 (any, Renderer)"| Render["render"]
+ Chain -->|"Err"| Exit["退出"]
+ C -->|"否"| R{"has_renderer?"}
+ R -->|"是"| Render
+ R -->|"否"| N["build_renderer_not_found<br/>再试一次"]
+ N --> C
+```
+
+### 4. Render
+
+渲染阶段调用 `C::render(any, &mut render_result)`,根据 `member_id` 找到对应的 `#[renderer]` 函数,将结果写入 `RenderResult`。如果启用了 `structural_renderer`,还会根据 `program.structural_renderer_name` 将结果序列化为 JSON/YAML 等格式。
+
+### 5. 退出
+
+设置 `exit_code`,触发 `finish` hook,返回 `RenderResult`。
+
+> [!TIP]
+> 这套运行时调度代码由 `gen_program!()` 生成的枚举和 `ProgramCollect` 实现来驱动。
+>
+> 编译期只生成了类型到 Chain / Renderer / Help / Completion 的映射关系,实际的匹配和路由都是在运行时完成的。
+
+## 这和直接函数调用的区别
+
+这套管线可以避免你写出如下的代码:
+
+```rust
+@@@ struct Config;
+@@@ impl Config { fn read() -> Self { Config } }
+@@@ fn main() {
+@@@ let json = true;
+// 读取配置
+let mut config = Config::read();
+
+// 执行操作
+let Ok(result) = operation(&mut config) else {
+ panic!("错误处理");
+};
+
+// 渲染结果
+if json {
+ print_json();
+} else {
+ println!("成功!");
+}
+@@@ }
+@@@ fn operation(config: &mut Config) -> Result<(),()> { Ok(()) }
+@@@ fn print_json() {}
+```
+
+Mingling 的管线把 **命令匹配**、**业务逻辑**、**输出展示** 拆成三个独立的位置,每个位置只负责一件事。
+
+更重要的是,管线经过 hook 和 `AnyOutput` 机制,允许横切关注点(日志、鉴权、退出码)无侵入地插入,不会污染你的业务代码。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/concepts/2-resource.md b/docs/_zh_CN/pages/concepts/2-resource.md
new file mode 100644
index 0000000..1052254
--- /dev/null
+++ b/docs/_zh_CN/pages/concepts/2-resource.md
@@ -0,0 +1,60 @@
+<h1 align="center">资源系统</h1>
+<p align="center">
+ Mingling 如何管理全局状态
+</p>
+
+命令行程序经常需要共享一些全局的东西——配置文件、数据库连接、计数器、当前工作目录。
+
+在普通 Rust 里你可能会用 `OnceCell` 或 `lazy_static`,在 Mingling 里有一套统一的机制:**资源系统**。
+
+## 什么是资源?
+
+资源就是在多个 Chain 和 Renderer 之间共享的数据。
+
+你只需要定义一个类型、注册到 Program,然后在函数签名里声明你需要它——剩下的注入和生命周期管理都由框架完成。
+
+## 核心机制:ResourceMarker
+
+任何同时实现了 `Default + Clone` 的类型都可以自动成为资源。框架会为它实现 `ResourceMarker` trait,使其具备:
+
+- **`res_clone()`** —— 当多个 Chain 同时访问时,框架可以通过 clone 来避免锁竞争
+- **`res_default()`** —— 资源未注册时提供兜底值
+
+如果你需要更精细的生命周期控制,可以使用 `LazyRes<T>`。它允许资源在第一次被访问时才初始化,并且可以在析构时执行回调(比如退出前保存状态到磁盘)。
+
+## 为什么不用全局变量?
+
+传统做法的静态变量是隐式依赖 —— 你看函数签名根本不知道它用了什么全局状态。而 Mingling 的资源注入让 **依赖显式化**:
+
+- 函数需要什么资源,参数列表就写什么
+- `&T` 表示只读访问,`&mut T` 表示可修改
+- 调用者一眼就能看出这个函数的副作用
+
+例如:
+
+```rust
+@@@ use mingling::res::ResExitCode;
+@@@ pack!(ErrorFileNotFound = ());
+#[chain]
+fn handle_error_file_not_found(
+ error: ErrorFileNotFound,
+ ec: &mut ResExitCode // 通过签名可以看出副作用!
+) {
+ ec.exit_code = 2; // 这里修改了退出码
+}
+```
+
+## 资源与 Setup 的关系
+
+资源通常通过两个途径注册到 Program:
+
+1. **直接注册** —— 在 `main` 中调用 `program.with_resource(...)`
+2. **通过 Setup** —— 使用 `DirectoryEnvironmentSetup` 等内置 Setup 批量注册(如 `ResCurrentDir`、`ResHomeDir`)
+
+Setup 是比资源更高层的抽象,一个 Setup 可以注册多个资源并做其他初始化工作。
+
+详见教程中的 [程序装配](./pages/8-setup-and-resources) 一章。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/concepts/3-any-output.md b/docs/_zh_CN/pages/concepts/3-any-output.md
new file mode 100644
index 0000000..9b820da
--- /dev/null
+++ b/docs/_zh_CN/pages/concepts/3-any-output.md
@@ -0,0 +1,73 @@
+<h1 align="center">任意输出机制</h1>
+<p align="center">
+ 关于 AnyOutput 和 ChainProcess 的运作模式
+</p>
+
+Dispatcher → Chain → Renderer 三阶段之间传递的数据是什么?
+
+Chain 的输出可能是一个成功结果、一个错误、或者还需要继续交给下一个 Chain——这些类型各不相同,管线如何在编译期不知道具体类型的情况下,把它们送到正确的地方?
+
+## AnyOutput:类型擦除 + 组标签
+
+Mingling 的解法是把**所有类型擦除到同一个包装里**,然后用一个**枚举标签**来区分它们:
+
+```
+AnyOutput<G>
+├── inner: Box<dyn Any + Send> ← 真正的数据,类型已被擦除
+├── type_id: TypeId ← 运行时类型 ID,用于安全 downcast
+└── member_id: G ← 枚举标签,标记"这是谁"
+```
+
+这里的 `G` 就是 `gen_program!()` 生成的程序枚举(也就是你熟知的 `ThisProgram`)。
+
+每个被 `pack!` 或 `#[derive(Groupped)]` 标记的类型都被分配到这个枚举的一个变体。
+
+## ChainProcess:数据 + 路由
+
+在 `AnyOutput` 的基础上,`ChainProcess<G>` 加了一个**路由信息**:
+
+```
+ChainProcess<G>
+├── Ok(AnyOutput<G>, NextProcess) ← 携带数据,告诉调度器下一步去哪
+│ ├── NextProcess::Chain ← "还没完,继续交给下一个 Chain"
+│ └── NextProcess::Renderer ← "出结果了,展示给用户"
+└── Err(ChainProcessError) ← "出错了,终止程序"
+```
+
+这就是为什么 Chain 函数返回的不是裸数据,而是 `ChainProcess`——它把 **"下一步去哪"** 和 **"数据"** 打包在一起。
+
+调度器根据 `NextProcess` 决定是继续循环还是退出渲染。
+
+## Groupped:谁是谁
+
+调度器如何知道 `AnyOutput` 里装的是 `ResultName` 还是 `ErrorUserBlocked`?答案是 `Groupped` trait:
+
+```
+trait Groupped<G> {
+ fn member_id() -> G;
+}
+```
+
+当你用 `pack!(ResultName = String)` 时,宏自动为 `ResultName` 实现 `Groupped`,`member_id()` 返回枚举中对应的变体。调度器一看 `member_id`,就去找对应的 Chain 或 Renderer。
+
+`to_chain()` 和 `to_render()` 本质上是 `AnyOutput` 的快捷方法,分别构造 `ChainProcess::Ok(any, Chain)` 和 `ChainProcess::Ok(any, Renderer)`。
+
+## 调度的执行
+
+在运行时,主循环的工作就是:
+
+1. 看当前 `AnyOutput` 的 `member_id`
+2. 查这个变体有没有对应的 Chain → 有就执行,拿到新的 `AnyOutput` 和 `NextProcess`
+3. 如果 `NextProcess` 是 `Chain` → 回到第 1 步
+4. 如果 `NextProcess` 是 `Renderer` → 退出循环,渲染
+
+这套机制保证了**类型安全**:`gen_program!()` 生成的调度代码在做 `restore`(从 `Box<dyn Any>` 还原为具体类型)时,一定是在匹配的 `member_id` 分支内做的,不可能把 `ResultName` 的数据当作 `ErrorUserBlocked` 来解包。
+
+> [!TIP]
+> 日常开发中你不需要手动操作 `AnyOutput` 或 `ChainProcess`。
+>
+> `pack!`、`#[chain]`、`#[renderer]` 这些宏帮你处理了所有的包装和解包。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/concepts/4-program-collect.md b/docs/_zh_CN/pages/concepts/4-program-collect.md
new file mode 100644
index 0000000..f5e6b8f
--- /dev/null
+++ b/docs/_zh_CN/pages/concepts/4-program-collect.md
@@ -0,0 +1,52 @@
+<h1 align="center">关于 ProgramCollect</h1>
+<p align="center">
+ 了解 gen_program!() 是如何生成程序的
+</p>
+
+每个 Mingling 程序最后都有一行 `gen_program!()`。它在背后做了三件事,把整个程序的骨架搭建出来。
+
+## gen_program!() 的三件事
+
+### 1. 生成枚举
+
+扫描当前模块中所有 `pack!`、`#[chain]`、`#[renderer]` 等宏标记的类型,为每个类型生成一个枚举变体。
+
+这个枚举就是 `AnyOutput<G>` 中 `G` 的类型 —— 调度器靠枚举变体来区分管线中传递的不同数据。
+
+### 2. 生成 ProgramCollect 实现
+
+`ProgramCollect` 是一个 trait,定义了 **"每个枚举变体对应什么类型、由谁处理"** 的映射关系:
+
+- **`do_chain`** —— 根据 `member_id` 调用对应的 `#[chain]` 函数,返回新的 `AnyOutput` 和 `NextProcess`
+- **`render`** —— 根据 `member_id` 调用对应的 `#[renderer]` 函数,写入 `RenderResult`
+- **`render_help`** —— 根据 `member_id` 调用对应的 `#[help]` 函数
+- **`has_chain` / `has_renderer`** —— 判断某个变体有没有对应的处理函数
+- **`build_dispatcher_not_found` / `build_renderer_not_found` / `build_empty_result`** —— 三个内置降级类型,处理边界情况
+
+这套映射在运行时通过枚举匹配来完成——编译期只生成了枚举和匹配分支,实际的函数调用发生在运行时。
+
+### 3. 生成 ThisProgram
+
+生成 `ThisProgram` 类型别名,指向 `Program<生成的枚举>`。这就是为什么在 `main` 中可以直接写 `ThisProgram::new()`——它就是你整个程序的完整类型。
+
+---
+
+## 关于 `pathf` 和 `dispatch_tree` 下的差异
+
+以上是默认行为,但在启用特定 feature 时会有变化:
+
+### 1. `dispatch_tree` 特性
+
+Dispatcher 的匹配不再使用 `Vec<Box<dyn Dispatcher>>` 做线性匹配,而是在编译期将子命令结构构建为前缀树(Trie)。
+
+匹配复杂度从 `O(n)` 降到 `O(k)` —— `k` 是输入长度,与命令数量无关。
+
+### 2. `pathf` 特性(Module Pathfinder)
+
+默认情况下所有宏标记的类型必须在同一模块才能被 `gen_program!()` 收集到
+
+启用 `pathf` 后,编译期自动扫描所有子模块,找到所有用宏标记的类型并生成完整的模块路径引用 —— 类型定义在深层子模块也无需手动 `use`。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/1-getting-started.md b/docs/pages/1-getting-started.md
index 5e746d3..c3527db 100644
--- a/docs/pages/1-getting-started.md
+++ b/docs/pages/1-getting-started.md
@@ -19,9 +19,9 @@ features = []
## Enable Features
-**Mingling** has all features disabled by default and does not provide an all-in-one feature like `full`.
+**Mingling** has all features disabled by default and does **not** provide an all-in-one feature like `full`.
-Some features will **directly affect the behavior of the entire lifecycle**, so you need to enable them as needed, for example:
+Some features **directly affect the entire lifecycle behavior**, so you need to enable them as needed, e.g.:
```toml
[dependencies.mingling]
@@ -33,11 +33,11 @@ features = [
```
> [!NOTE]
-> Visit [docs.rs](https://docs.rs/mingling/latest/mingling/feature/index.html) or [Features](pages/other/features) to learn about all available features.
+> Visit [docs.rs](https://docs.rs/mingling/latest/mingling/feature/index.html) or [Features](pages/other/features) to learn about all features.
## Write the Basic Entry Point
-Edit `src/main.rs` with the following code:
+Write the following code in `src/main.rs`:
```rust
use mingling::prelude::*;
@@ -52,13 +52,13 @@ gen_program!();
```
> [!IMPORTANT]
-> Almost all Rust code blocks in the documentation have been compiled through the CI pipeline and are guaranteed to be usable.
+> Almost all Rust code blocks in the docs have been compiled in CI and are guaranteed to work.
>
-> However, code blocks starting with `// NOT VERIFIED` have **not been verified**.
+> However, code blocks starting with `// NOT VERIFIED` are **not verified**.
>
-> Want to know which `*.md` files have been compiled? Check [`verified-docs.toml`](https://github.com/mingling-rs/mingling/blob/main/verified-docs.toml)
+> Want to know which `*.md` files are compiled? See [`verified-docs.toml`](https://github.com/mingling-rs/mingling/blob/main/verified-docs.toml).
-## Verify Compilation
+## Verify with Compilation
```plaintext
~# cargo check
@@ -66,4 +66,4 @@ gen_program!();
---
-Once everything is good, start building!
+Once everything is good, start writing something!
diff --git a/docs/pages/10-help.md b/docs/pages/10-help.md
new file mode 100644
index 0000000..2f3b74f
--- /dev/null
+++ b/docs/pages/10-help.md
@@ -0,0 +1,69 @@
+<h1 align="center">Help Info</h1>
+<p align="center">
+ Adding --help support to commands
+</p>
+
+A CLI without help info is not a good CLI.
+
+In Mingling, use the `#[help]` macro to add help text to commands.
+
+## Simplest Help
+
+Write a help function directly for an Entry:
+
+```rust
+@@@use mingling::macros::help;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+#[help]
+fn help_greet(_entry: EntryGreet) {
+ r_println!("Usage: greet [name]");
+ r_println!("Say hello to someone.");
+}
+```
+
+> [!NOTE]
+> Help functions also use `r_println!`, because `#[help]` follows the rendering pipeline — it's a short-circuit render triggered early by the `--help` flag, not logic outside the pipeline.
+
+## Global Help
+
+You can also write help for `ErrorDispatcherNotFound` as the "root help":
+
+```rust
+@@@use mingling::macros::help;
+// Triggered when user passes --help directly
+#[help]
+fn help_root(entry: ErrorDispatcherNotFound) {
+ r_println!("Usage: my-cli <command>");
+ r_println!("Commands:");
+ r_println!(" greet Say hello");
+}
+```
+
+> [!TIP]
+> `ErrorDispatcherNotFound` is a type generated by `gen_program!()`, representing "no matching command found." Writing `#[help]` for it adds help to the program's root command.
+
+## Requires Setup
+
+For `--help` to work properly, add `BasicProgramSetup` in `main`:
+
+```rust
+@@@use mingling::macros::help;
+@@@use mingling::setup::BasicProgramSetup;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup);
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+`BasicProgramSetup` includes `HelpFlagSetup`, which simply sets `program.user_context.help` to `true`.
+
+The actual routing to the `#[help]` function is handled by code generated via `gen_program!()` — it checks this flag during dispatch, and if `true`, goes through the help rendering path, bypassing the Chain.
+
+Without `BasicProgramSetup`, `--help` is treated as a normal argument and passed as input to the Entry's Chain.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/11-resource-system.md b/docs/pages/11-resource-system.md
new file mode 100644
index 0000000..b54938a
--- /dev/null
+++ b/docs/pages/11-resource-system.md
@@ -0,0 +1,89 @@
+<h1 align="center">Using the Resource System</h1>
+<p align="center">
+ A hands-on guide to resources
+</p>
+
+Resources are Mingling's mechanism for managing global state. Any type that implements `Default + Clone` can be a resource.
+
+## Define a Resource
+
+```rust
+// Any type implementing Default + Clone can be used as a resource
+#[derive(Default, Clone)]
+struct ResCurrentDir(String);
+
+// Register with the Program
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_resource(ResCurrentDir(".".into()));
+ program.exec_and_exit();
+}
+```
+
+Since `ResCurrentDir` implements both `Default` and `Clone`, the framework automatically implements `ResourceMarker` for it — no manual impl needed.
+
+## Inject & Use
+
+In a Chain or Renderer, simply declare the resource in the parameter list:
+
+```rust
+@@@#[derive(Default, Clone)]
+@@@struct ResCurrentDir(String);
+@@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir);
+@@@pack!(ResultPath = String);
+// Inject read-only resource via &T
+#[chain]
+fn handle_pwd(_args: EntryPrintWorkingDir, cwd: &ResCurrentDir) -> Next {
+ ResultPath::new(cwd.0.clone()).to_render()
+}
+
+#[renderer]
+fn render_path(result: ResultPath) {
+ r_println!("{}", *result);
+}
+```
+
+## Modify a Resource
+
+Use `&mut T` to inject a mutable resource:
+
+```rust
+@@@#[derive(Default, Clone)]
+@@@struct ResVisitCount(u32);
+@@@dispatcher!("visit", CMDVisit => EntryVisit);
+@@@pack!(ResultDone = ());
+#[chain]
+fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next {
+ counter.0 += 1;
+ ResultDone::default()
+}
+
+#[renderer]
+fn render_done(_done: ResultDone, counter: &ResVisitCount) {
+ r_println!("visit count is : {}", counter.0);
+}
+```
+
+## Use Multiple Resources
+
+A Chain can inject any number of resources at once — the framework matches them by type automatically:
+
+```rust
+@@@#[derive(Default, Clone)] struct ResConfig(String);
+@@@#[derive(Default, Clone)] struct ResCounter(u32);
+@@@dispatcher!("test", CMDTest => EntryTest);
+@@@pack!(ResultDone = ());
+// Inject both read-only and mutable resources
+#[chain]
+fn handle_test(_args: EntryTest, config: &ResConfig, counter: &mut ResCounter) -> Next {
+ println!("config: {}", config.0);
+ counter.0 += 1;
+ ResultDone::default().to_render()
+}
+```
+
+For deeper topics like `ResourceMarker`, lazy loading with `LazyRes`, etc., see [Core Concepts: Resource System](pages/concepts/2-resource).
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/12-exit-code.md b/docs/pages/12-exit-code.md
new file mode 100644
index 0000000..6828cde
--- /dev/null
+++ b/docs/pages/12-exit-code.md
@@ -0,0 +1,68 @@
+<h1 align="center">Exit Code Control</h1>
+<p align="center">
+ Managing program exit codes via the resource system
+</p>
+
+Providing the shell with a correct exit code when a program terminates is a basic CLI convention. Mingling offers a ready-to-use `ExitCodeSetup` that, together with the `ResExitCode` resource, makes exit code control incredibly simple.
+
+## Enabling ExitCodeSetup
+
+```rust
+@@@use mingling::prelude::*;
+@@@use mingling::setup::ExitCodeSetup;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(ExitCodeSetup::default());
+@@@ program.exec_and_exit();
+}
+```
+
+`ExitCodeSetup` does two things:
+
+1. Registers the `ResExitCode` resource (default value `0`)
+2. Registers a `finish` hook that reads the value of `ResExitCode` as the final exit code before the program exits
+
+## Modifying the Exit Code
+
+In a Chain or Renderer, inject `ResExitCode` to modify the exit code:
+
+```rust
+@@@use mingling::res::ResExitCode;
+@@@use mingling::setup::ExitCodeSetup;
+@@@pack!(EntryCheck = Vec<String>);
+#[chain]
+fn handle_check(_args: EntryCheck, ec: &mut ResExitCode) {
+ // Modify exit code when check fails
+ ec.exit_code = 1;
+}
+```
+
+> [!TIP]
+> `ResExitCode` is simply `struct ResExitCode { pub exit_code: i32 }`. Inject `&mut ResExitCode` and modify the field directly.
+
+## Three Execution Modes of `Program`
+
+`Program` provides three execution modes (excluding `exec_repl` under the `repl` feature):
+
+| Mode | Behavior |
+| ------------------------------- | ----------------------------------------------------------------------------------------- |
+| `program.exec_and_exit()` | Executes and terminates the process directly with the exit code |
+| `program.exec()` | Executes and returns an `i32` exit code, letting the caller decide handling |
+| `program.exec_without_render()` | Returns `Result<RenderResult, ProgramExecuteError>`, with internal `exit_code` accessible |
+
+```rust
+@@@use mingling::setup::ExitCodeSetup;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(ExitCodeSetup::default());
+
+ // Get exit code and handle it yourself
+ let exit_code = program.exec();
+ std::process::exit(exit_code);
+}
+@@@gen_program!();
+```
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/13-hook.md b/docs/pages/13-hook.md
new file mode 100644
index 0000000..0c5e460
--- /dev/null
+++ b/docs/pages/13-hook.md
@@ -0,0 +1,107 @@
+<h1 align="center">Hook System</h1>
+<p align="center">
+ How to insert custom behavior into a program using ProgramHook
+</p>
+
+Hooks let you insert custom logic at various lifecycle points in the pipeline — before dispatch, after chain, before/after render, on program exit …
+
+You can write cross-cutting concerns (logging, auth, metrics collection) in hooks instead of scattering them across business code.
+
+## Basic Usage
+
+`ProgramHook` uses a builder pattern:
+
+```rust
+@@@use mingling::hook::ProgramHook;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_hook(
+ ProgramHook::empty()
+ .on_pre_chain(|info| {
+ println!("before chain: {}", info.input);
+ })
+ .on_post_render(|info| {
+ println!("after render: {}", info.result);
+ }),
+ );
+ program.exec_and_exit();
+}
+```
+
+> [!TIP]
+> `ProgramHook::empty()` creates an empty hook, then chain-calls `.on_*()` methods to register the lifecycle nodes you care about. Unregistered nodes won't execute.
+
+## Lifecycle Nodes
+
+Hooks cover the full pipeline lifecycle:
+
+| Stage | Hook | Trigger Point |
+| ------------ | ------------------ | ------------------- |
+| **Dispatch** | `on_begin` | Execution start |
+| | `on_pre_dispatch` | Before dispatch |
+| | `on_post_dispatch` | After dispatch |
+| **Chain** | `on_pre_chain` | Before chain exec |
+| | `on_post_chain` | After chain exec |
+| **Render** | `on_pre_render` | Before render exec |
+| | `on_post_render` | After render exec |
+| **Finish** | `on_finish` | Before program exit |
+
+Each hook callback receives a corresponding `Hook*Info` struct containing context info (input type, params, render results, etc.).
+
+## Real Example: Logging Operations
+
+```rust
+@@@use mingling::prelude::*;
+@@@use mingling::hook::ProgramHook;
+@@@
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@
+@@@#[chain] fn handle_greet(args: EntryGreet) -> Next {
+@@@ ResultName::new(args.inner.first().cloned().unwrap_or_default()).to_render()
+@@@}
+@@@#[renderer] fn render_name(r: ResultName) { r_println!("Hello, {}!", *r); }
+fn main() {
+ let mut program = ThisProgram::new();
+
+ // Log info before and after each chain execution
+ program.with_hook(
+ ProgramHook::empty()
+ .on_pre_chain(|info| {
+ eprintln!("[hook] executing chain for: {}", info.input);
+ })
+ .on_post_chain(|info| {
+ eprintln!("[hook] chain output: {}", info.output.member_id);
+ }),
+ );
+
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+Run output:
+
+```text
+[hook] executing chain for: EntryGreet
+[hook] chain output: ResultName
+Hello, World!
+```
+
+## Controlling Behavior via Hooks
+
+Hooks aren't just for observation — you can use `ProgramControlUnit` to alter program behavior:
+
+| Variant | Effect |
+| -------------------------- | ----------------------------------------- |
+| `Continue` | Do nothing, continue execution |
+| `OverrideExitCode(i32)` | Override the exit code |
+| `RouteToChain(AnyOutput)` | Replace current data, re-enter Chain loop |
+| `RouteToRender(AnyOutput)` | Skip subsequent Chain, render directly |
+
+> [!NOTE]
+> Multiple hooks can be registered and execute in registration order.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/14-testing.md b/docs/pages/14-testing.md
new file mode 100644
index 0000000..789da96
--- /dev/null
+++ b/docs/pages/14-testing.md
@@ -0,0 +1,129 @@
+<h1 align="center">Testing Your Program</h1>
+<p align="center">
+ Writing unit tests for Chain and Renderer
+</p>
+
+A built-in benefit of the pipeline model is **testability**.
+
+A Chain is just a function that takes input and returns output; a Renderer is just a function that takes input and writes content — no global state magic, testing is straightforward.
+
+## Testing Renderer
+
+Renderer is the easiest to test — call the function, assert the result:
+
+```rust
+@@@pack!(ResultName = String);
+// Returns String instead of ()
+#[renderer]
+fn render_name(name: ResultName) -> String {
+ r_println!("Hello, {}!", *name);
+}
+
+#[test]
+fn test_render_name() {
+ let result = render_name(ResultName::new("Alice".to_string()));
+ assert_eq!(result, "Hello, Alice!\n");
+}
+```
+
+Note the Renderer's return type changed to `-> String` — `#[renderer]` auto-converts `RenderResult` to whatever return type you specify (default is `()`). By returning `String`, you can directly assert on the output content.
+
+## Testing Chain
+
+Testing a Chain is slightly more complex because its return value is `Next` (actually `impl Into<ChainProcess<ThisProgram>>`). You'll need the assertion macros provided by the framework:
+
+```rust
+@@@use mingling::{assert_member_id, assert_render_result, unpack_chain_process};
+@@@dispatcher!("hello", CMDHello => EntryHello);
+@@@pack!(ResultName = String);
+@@@pack!(ErrorNoName = ());
+@@@#[chain]
+@@@fn handle_hello(args: EntryHello) -> Next {
+@@@ let name = args.inner.first().cloned().unwrap_or_default();
+@@@ if name.is_empty() {
+@@@ ErrorNoName::default().to_render()
+@@@ } else {
+@@@ ResultName::new(name).to_render()
+@@@ }
+@@@}
+#[test]
+fn test_handle_hello_with_name() {
+ let chain_process = handle_hello(EntryGreet::new(vec!["Alice".to_string()])).into();
+ // Asserts this is a render result (not continuing the chain)
+ assert_render_result!(chain_process);
+ // Asserts member_id is ResultName
+ assert_member_id!(chain_process, ResultName);
+ // Unpacks the inner value
+ let result_name = unpack_chain_process!(chain_process, ResultName);
+ assert_eq!(result_name.inner, "Alice");
+}
+```
+
+What the three test macros do:
+
+| Macro | Function |
+| ----------------------- | ----------------------------------------------------------------- |
+| `assert_render_result!` | Asserts Chain returned the render path (not continuing the chain) |
+| `assert_member_id!` | Asserts the return value's member ID is a certain type |
+| `unpack_chain_process!` | Unpacks the original type from ChainProcess |
+
+## Constructing Data with the entry! Macro
+
+If `extra_macros` is enabled, you can use `entry!` to quickly construct an Entry:
+
+```rust
+// Features: ["extra_macros"]
+
+@@@use mingling::{assert_member_id, unpack_chain_process};
+@@@use mingling::macros::entry;
+@@@dispatcher!("hello", CMDHello => EntryHello);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_hello(args: EntryHello) -> Next {
+@@@ let name = args.inner.first().cloned().unwrap_or_default();
+@@@ ResultName::new(name).to_render()
+@@@}
+#[test]
+fn test_with_entry_macro() {
+ // entry! constructs an Entry from string literals
+ let entry = entry!("--name", "Alice");
+ let chain_process = handle_hello(entry).into();
+ let result_name = unpack_chain_process!(chain_process, ResultName);
+ assert_eq!(result_name.inner, "Alice");
+}
+```
+
+## Testing Resource Injection
+
+If a Chain uses resources, you need to provide resource instances in the test:
+
+```rust
+@@@use mingling::{assert_render_result, unpack_chain_process};
+@@@#[derive(Default, Clone)]
+@@@struct ResPrefix(String);
+@@@dispatcher!("hello", CMDHello => EntryHello);
+@@@pack!(ResultGreeting = String);
+@@@
+#[chain]
+fn handle_hello(args: EntryHello, prefix: &ResPrefix) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ ResultGreeting::new(format!("{}, {}", prefix.0, name)).to_render()
+}
+
+#[test]
+fn test_handle_with_resource() {
+ // Resources need to be passed manually in tests
+ let result = handle_hello(
+ EntryHello::new(vec!["World".to_string()]),
+ &ResPrefix("Hello".to_string()),
+ );
+ let greeting = unpack_chain_process!(result, ResultGreeting, ThisProgram);
+ assert_eq!(greeting.inner, "Hello, World");
+}
+```
+
+The pipeline model makes testing simple: each Chain and Renderer is a relatively independent function — construct input, assert output.
+
+<p align="center" style="font-size: 0.85em; color: clear;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/2-define-a-dispatcher.md b/docs/pages/2-define-a-dispatcher.md
new file mode 100644
index 0000000..804ad1b
--- /dev/null
+++ b/docs/pages/2-define-a-dispatcher.md
@@ -0,0 +1,103 @@
+<h1 align="center">Declare a Dispatcher</h1>
+<p align="center">
+ Use the <code>dispatcher!</code> macro to declare commands and register them
+</p>
+
+Mingling's pipeline starts with a Dispatcher.
+
+Its job is simple: **match the user's input command, wrap the arguments into an Entry type**.
+
+## The `dispatcher!` Macro
+
+The `dispatcher!` macro generates two types at once:
+
+| Generated type | Purpose |
+| -------------- | -------------------------------------------------------------- |
+| `CMDType` | The dispatcher itself, needs to be registered to Program |
+| `EntryType` | The entry type, wraps `Vec<String>`, serves as input for Chain |
+
+The syntax is a fixed three-part pattern:
+
+```rust
+dispatcher!("command path", DispatcherType => EntryType);
+```
+
+Here's a concrete example:
+
+```rust
+dispatcher!("greet", CMDGreet => EntryGreet);
+```
+
+> [!NOTE]
+> The command name (`"greet"`) is auto-converted to kebab-case. Even if you write `"GreetUser"`, matching will use `greet-user`.
+
+## Registering with Program
+
+Once you have a dispatcher, you need to tell Program about it:
+
+```rust
+@@@ dispatcher!("greet", CMDGreet => EntryGreet);
+@@@ fn main() {
+@@@ let mut program = ThisProgram::new();
+// Register the dispatcher
+program.with_dispatcher(CMDGreet);
+@@@ }
+@@@ gen_program!();
+```
+
+> [!TIP]
+> If you have many commands, use `with_dispatchers` to register multiple at once: `program.with_dispatchers((CMDGreet, CMDAdd, CMDRemoteRm))`.
+
+## Multi-level Commands
+
+If your program has a hierarchy — e.g., `remote add`, `remote rm` — just separate the command name with dots:
+
+```rust
+dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd);
+dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm);
+```
+
+When the user types `remote add` in the terminal, Mingling matches `remote` and `add` as two levels in sequence.
+
+## The Entry Type `EntryGreet`
+
+You might be curious about what's inside `EntryGreet`. It's essentially a struct wrapping `Vec<String>`:
+
+```rust
+// Illustration of code generated by the dispatcher! macro
+pub struct EntryGreet {
+ pub inner: Vec<String>,
+}
+```
+
+When the user types `greet Alice Bob` on the command line, `EntryGreet.inner` becomes `vec!["Alice", "Bob"]`.
+
+> [!IMPORTANT]
+> Entry's `inner` only contains **the remaining args after matching**.
+>
+> Take `remote add origin` as an example: `remote` and `add` are used for matching the command path, only `origin` goes into `EntryRemoteAdd.inner`.
+
+## Advanced: Implicit Declaration
+
+The above is the standard syntax. If you enable the `extra_macros` feature, you can be more concise:
+
+```rust
+// Features: ["extra_macros"]
+// Omit CMDType and EntryType, names are auto-derived
+ dispatcher!("greet");
+// dispatcher!("greet", CMDGreet => EntryGreet);
+```
+
+This syntax auto-generates `CMDGreet` and `EntryGreet`, with the same effect as the explicit declaration.
+
+But for the tutorial, we'll stick with explicit syntax — it's clearer and doesn't require extra features.
+
+See [Feature List](pages/other/features) for details.
+
+## Next Step
+
+Next we'll write a Chain to receive the Entry and handle the actual business logic.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/3-define-a-chain.md b/docs/pages/3-define-a-chain.md
new file mode 100644
index 0000000..c6d3f8e
--- /dev/null
+++ b/docs/pages/3-define-a-chain.md
@@ -0,0 +1,131 @@
+<h1 align="center">Declare a Chain</h1>
+<p align="center">
+ Use the <code>chain</code> macro to declare a chain and handle Entry input
+</p>
+
+In the previous section, we declared `dispatcher!("greet", CMDGreet => EntryGreet)`.
+
+Now when a user types `greet`, it gets matched and wrapped into `EntryGreet`.
+
+But what happens after we get the Entry?
+
+We need a Chain to process it.
+
+## The `#[chain]` Macro
+
+`#[chain]` marks a handler function. The format is straightforward:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+pack!(ResultName = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ // args contains the remaining params after matching user input
+ let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string());
+ // Wrap the result into Next, telling the dispatcher where to go next
+ ResultName::new(name)
+}
+```
+
+Notice anything?
+
+The Chain function signature declares what it needs — `args: EntryGreet`.
+
+Then it returns a newtype via `ResultName::new(name)`.
+
+This returned `Next` expands into `impl Into<ChainProcess<ThisProgram>>`.
+
+> [!TIP]
+> Wondering how `Into<ChainProcess<G>>` works?
+>
+> Check out the [Any Output Mechanism](pages/concepts/3-any-output) chapter to learn about `ChainProcess`.
+
+## The `pack!` Macro
+
+You've probably guessed it — `pack!(ResultName = String)` defines a type that flows through the pipeline:
+
+```rust
+// pack!(ResultName = String) generates code roughly like this
+
+#[derive(Groupped)]
+pub struct ResultName {
+ pub inner: String,
+}
+```
+
+Think of it as a **tagged** `String`.
+
+The dispatcher uses this tag for precise routing, ensuring data doesn't get mixed up — e.g., data sent to `RenderGreet` won't be misdelivered to `RenderError`.
+
+> [!NOTE]
+> Unlike a simple type alias (`type`), `pack!` generates a completely new type with its own `TypeId`.
+
+Here's a recommended naming convention:
+
+| Role | Naming Pattern | Example |
+| ------------ | ---------------------- | -------------------- |
+| Entry | `Entry` + command | `EntryGreet` |
+| Intermediate | `State` + description | `StateParsedArgs` |
+| Result | `Result` + description | `ResultGreetSomeone` |
+| Error | `Error` + description | `ErrorUserNotFound` |
+
+See [Naming Convention](pages/other/naming_rule) for details, but for now just remember: **use `pack!` to give your data a meaningful name**.
+
+## Extracting Params from Entry
+
+`EntryGreet`'s `inner` is a `Vec<String>`, which you can freely process inside a Chain:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ // Take the first param, or use a default
+ let name = args
+ .inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+
+ ResultName::new(name)
+}
+```
+
+If you enable the `parser` feature, you can also use `Picker` for more flexible param extraction — but that's a topic for later.
+
+## Putting It Together
+
+Now let's connect the Dispatcher and Chain:
+
+```rust
+// 1. Declare the command
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+// 2. Declare the pipeline data type
+pack!(ResultName = String);
+
+// 3. Processing logic
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+ ResultName::new(name)
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+gen_program!();
+```
+
+But this code isn't complete yet — we only have the Dispatcher and Chain. One last step remains: **rendering the result**. That's what the next chapter, Renderer, covers.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/4-render-result.md b/docs/pages/4-render-result.md
new file mode 100644
index 0000000..5093a52
--- /dev/null
+++ b/docs/pages/4-render-result.md
@@ -0,0 +1,142 @@
+<h1 align="center">Rendering Results</h1>
+<p align="center">
+ Use the <code>#[renderer]</code> macro to declare a renderer and output results
+</p>
+
+Now that we've created a Dispatcher and a Chain, and produced a Result type via `pack!`, there's one final step: **presenting the result to the user**.
+
+## The `#[renderer]` Macro
+
+Similar to `#[chain]`, `#[renderer]` marks an output function:
+
+```rust
+pack!(ResultName = String);
+#[renderer]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
+}
+```
+
+A Renderer takes the result produced by a Chain and outputs it using `r_println!`. What's the difference between `r_println!` and the usual `println!`?
+
+## The `r_println!` and `r_print!` Macros
+
+`r_println!` and `r_print!` are printing macros provided by Mingling. They write content into a `RenderResult` instead of printing directly to the terminal. This offers several benefits:
+
+1. **RenderResult holds an exit code** — you can make the program exit with a specific code
+2. **Easier testing** — you can capture rendered output and make assertions
+3. **Post-processing** — you can capture results and apply uniform text post-processing
+
+> [!TIP]
+> For simple printing, you can think of it as a drop-in replacement for `println!`. Using `r_println!` instead of `println!` is a safe choice.
+
+## A Complete Runnable Program
+
+Putting the content of all three tutorials together, here's your first complete Mingling program:
+
+```rust
+// 1. Declare a command with Dispatcher
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+// 2. Declare result data with pack!
+pack!(ResultName = String);
+
+// 3. Handle logic with Chain
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+ ResultName::new(name)
+}
+
+// 4. Output results with Renderer
+#[renderer]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
+}
+
+// 5. Assemble and run the program in main
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+// 6. Generate the complete program with gen_program!
+gen_program!();
+```
+
+## Try It Out
+
+```bash
+~# cargo run -- greet Alice
+```
+
+```text
+Hello, Alice!
+```
+
+Try without arguments:
+
+```bash
+~# cargo run -- greet
+```
+
+```text
+Hello, World!
+```
+
+Try an unknown command:
+
+```bash
+cargo run -- great
+```
+
+```text
+# No output!
+```
+
+## Add a Fallback
+
+`gen_program!()` auto-generates an `ErrorDispatcherNotFound` type that wraps `Vec<String>` — it holds the user's unmatched input. You just need to write a Renderer for it:
+
+```rust
+#[renderer]
+fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) {
+ if err.inner.is_empty() {
+ r_println!("Unknown command");
+ } else {
+ r_println!("Command not found: \"{}\"", err.inner.join(" "));
+ }
+}
+```
+
+With that added, try the unknown command again:
+
+```bash
+cargo run -- great
+```
+
+```text
+Command not found: "great"
+```
+
+## Congratulations
+
+You've completed your first full Mingling program! Here's a recap of what you've learned:
+
+| Concept | Macro/Function | In a Nutshell |
+| --------------- | ---------------- | ------------------------------------- |
+| Declare command | `dispatcher!` | Tell the program what users can input |
+| Handle logic | `#[chain]` | What to do with the arguments |
+| Output results | `#[renderer]` | How to present results to users |
+| Type wrapper | `pack!` | Give your data a meaningful name |
+| Program entry | `gen_program!()` | Auto-generate the pipeline wiring |
+
+In real projects you'll also use advanced features like resource injection, hooks, autocompletion, REPL, etc., but the core skeleton stays the same: **Dispatcher → Chain → Renderer**.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/5-multiple-commands.md b/docs/pages/5-multiple-commands.md
new file mode 100644
index 0000000..9ad7ef4
--- /dev/null
+++ b/docs/pages/5-multiple-commands.md
@@ -0,0 +1,109 @@
+<h1 align="center">Multi-Command Program</h1>
+<p align="center">
+ Adding multiple commands to a single program
+</p>
+
+Real-world CLIs rarely have just one command. Let's extend our previous greet program by adding a second command, and see what a multi-command program looks like.
+
+## Adding a Second Command
+
+Work in the same project:
+
+```rust
+// Declare two commands
+dispatcher!("greet", CMDGreet => EntryGreet);
+dispatcher!("add", CMDAdd => EntryAdd);
+
+pack!(ResultGreeting = String);
+pack!(ResultSum = i32);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string());
+ ResultGreeting::new(name)
+}
+
+#[chain]
+fn handle_add(args: EntryAdd) -> Next {
+ let sum: i32 = args.inner.iter().filter_map(|s| s.parse::<i32>().ok()).sum();
+ ResultSum::new(sum)
+}
+
+#[renderer]
+fn render_greet(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
+}
+
+#[renderer]
+fn render_sum(result: ResultSum) {
+ r_println!("Sum: {}", *result);
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatchers((CMDGreet, CMDAdd));
+ program.exec_and_exit();
+}
+
+gen_program!();
+```
+
+Both commands share the same pipeline model, but each has its own path:
+
+```text
+> my-cli greet Alice
+Hello, Alice!
+> my-cli add 1 2 3
+Sum: 6
+```
+
+## Registering Multiple Dispatchers
+
+Notice `with_dispatchers`? When you need to register multiple dispatchers, just pass them as a tuple:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@dispatcher!("add", CMDAdd => EntryAdd);
+@@@pack!(ResultGreeting = String);
+@@@pack!(ResultSum = i32);
+@@@#[chain] fn handle_greet(_args: EntryGreet) -> Next { ResultGreeting::new("ok".into()) }
+@@@#[renderer] fn render_greet(_greeting: ResultGreeting) { r_println!("hi"); }
+@@@#[chain] fn handle_add(_args: EntryAdd) -> Next { ResultSum::new(0) }
+@@@#[renderer] fn render_sum(_sum: ResultSum) { r_println!("sum"); }
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatchers((CMDGreet, CMDAdd));
+ program.exec_and_exit();
+}
+```
+
+This is equivalent to registering them one by one, same effect.
+
+> [!TIP]
+> The tuple supports up to 7 dispatchers. For more than 7, chain `with_dispatcher` calls instead.
+
+## Subcommands
+
+Multi-level commands work the same way—each dot-separated level is just part of the name:
+
+```rust
+dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd);
+dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm);
+```
+
+Each subcommand's Entry, Chain, and Renderer are completely independent and don't interfere.
+
+## Type Independence
+
+Notice we used two different `pack!` macros:
+
+- `pack!(ResultGreeting = String)`
+- `pack!(ResultSum = i32)`
+
+They are independent types, and `gen_program!()` assigns them different enum variants.
+
+The dispatcher will never route `ResultGreeting` data to `render_sum` — **type safety is guaranteed from the naming stage**.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/6-argument-parse-picker.md b/docs/pages/6-argument-parse-picker.md
new file mode 100644
index 0000000..7e91af8
--- /dev/null
+++ b/docs/pages/6-argument-parse-picker.md
@@ -0,0 +1,393 @@
+<h1 align="center">Parsing Arguments with Picker</h1>
+<p align="center">
+ Use Picker to perform basic argument parsing
+</p>
+
+In previous tutorials, we extracted args manually from `EntryGreet.inner` (`Vec<String>`).
+
+```rust
+@@@ fn main() {
+@@@ let args : Vec<String> = vec![];
+let name = args.first().cloned().unwrap_or_else(|| "World".to_string());
+@@@ }
+```
+
+But this approach doesn't scale well for many params. Mingling provides `Picker` — a chaining API to extract and convert args.
+
+To enable `Picker`, update your `Cargo.toml`:
+
+```toml
+# Cargo.toml
+[dependencies.mingling]
+features = ["parser"]
+```
+
+Now let's look at `Picker` in action:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let name = prev.pick_or((), "World").unpack();
+ ResultName::new(name)
+}
+```
+
+`AsPicker` implements `pick`, `pick_or`, and `pick_or_route` for any type that can convert to `Vec<String>`: they semantically **pick** args from a string list and convert them to structured data.
+
+Breaking down the example above:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_greet_entry(prev: EntryGreet) -> Next {
+let name = prev.pick_or((), "World").unpack();
+@@@ResultName::new(name)
+@@@}
+```
+
+Its semantics are:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_greet_entry(prev: EntryGreet) {
+@@@let name: String =
+ prev.pick_or((), "World").unpack();
+// ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~
+// | | | | |_ unpack to String
+// | | | |__________ default value "World"
+// | | |______________ pick the first positional arg (no flag)
+// | |______________________ pick or use default
+// |___________________________ from previous input
+@@@}
+```
+
+## Parsing Flag Args
+
+If your program needs to parse flag args (e.g., `greet --name Alice`), do this:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let name = prev.pick_or(["--name", "-n"], "World").unpack();
+ ResultName::new(name)
+}
+```
+
+Its semantics:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_greet_entry(prev: EntryGreet) {
+@@@let name: String =
+ prev.pick_or(["--name", "-n"], "World").unpack();
+// ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~
+// | | | | |_ unpack to String
+// | | | |__________ default value "World"
+// | | |____________________________ pick arg after "--name" or "-n"
+// | |____________________________________ pick or use default
+// |_________________________________________ from previous input
+@@@}
+```
+
+## About `.unpack()`
+
+You may have noticed that `Picker` calls `.unpack()` at the end of parsing. It converts the accumulated parse results into structured info.
+
+For a single pick, `.unpack()` returns a single value; for multiple picks, it returns a tuple:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("test", CMDTest => EntryTest);
+@@@pack!(ResultInfo = (String, u8, u32));
+
+#[chain]
+fn handle_test_entry(prev: EntryTest) -> Next {
+ let (name, age, id) = prev
+ .pick::<String>(["--name", "-n"])
+ .pick::<u8>(["--age", "-a"])
+ .pick::<u32>(["--id", "-I"])
+ .unpack();
+
+ ResultInfo::new((name, age, id))
+}
+```
+
+> [!IMPORTANT]
+> `Picker` is very sensitive to parse order, esp. for positional args (they're parsed sequentially). If you need to parse positional args, make sure all **flag args** have been picked and consumed first.
+
+## Handling Edge Cases with `pick_or_route`
+
+As the old saying goes: "Never trust your users." To handle missing required args, type mismatches, etc., `pick_or_route` routes the execution chain to a dedicated error-handling type.
+
+A simple example:
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@use mingling::macros::route;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@pack!(ErrorNoName = ());
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let pick_result = prev
+ .pick_or_route(["--name", "-n"], ErrorNoName::default())
+ .unpack();
+
+ // Use route! macro to unpack pick_result
+ let name = route!(pick_result);
+ ResultName::new(name).into()
+}
+
+#[renderer]
+fn render_no_name(_prev: ErrorNoName) {
+ r_println!("Error: No name provided.");
+}
+
+#[renderer]
+fn render_name(prev: ResultName) {
+ r_println!("Hello, {}!", *prev);
+}
+```
+
+With `pick_or_route`, the code gets a bit more complex: `.unpack()` no longer returns the value directly, but `Result<Value, Route>`.
+
+However, Mingling's `extra_macros` feature provides the `route!` macro to simplify unwrapping — it just omits some boilerplate:
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@ pack!(ErrorFail = ());
+@@@ use mingling::macros::route;
+@@@ fn func() -> mingling::ChainProcess<ThisProgram> {
+@@@ let args: Vec<String> = vec![];
+@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack();
+let name = route!(pick_result);
+@@@ mingling::macros::empty_result!()
+@@@ }
+```
+
+It expands to:
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@ pack!(ErrorFail = ());
+@@@ fn func() -> mingling::ChainProcess<ThisProgram> {
+@@@ let args: Vec<String> = vec![];
+@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack();
+let name = match pick_result {
+ Ok(r) => r,
+ Err(e) => return e.to_chain(),
+};
+@@@ mingling::macros::empty_result!()
+@@@ }
+```
+
+## Post-processing Extracted Values
+
+After picking user input, you can use `after` to process the value immediately:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let name = prev
+ .pick_or(["--name", "-n"], "World")
+ // Format immediately after extracting --name
+ .after(|name: String| {
+ name.replace(['-', '_', '.'], " ")
+ .to_lowercase()
+ .trim()
+ .to_string()
+ })
+ .unpack();
+
+ ResultName::new(name)
+}
+```
+
+Similarly, use `after_or_route` to handle format errors in input args:
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@use mingling::macros::route;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@pack!(ErrorNameTooLong = usize);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let pick_result = prev
+ .pick_or(["--name", "-n"], "World")
+ .after_or_route(|name: &String| {
+ if name.len() < 32 {
+ Ok(name.clone())
+ } else {
+ Err(ErrorNameTooLong::new(name.len()))
+ }
+ })
+ .unpack();
+ let name = route!(pick_result);
+
+ ResultName::new(name).into()
+}
+
+#[renderer]
+fn render_name_too_long(prev: ErrorNameTooLong) {
+ let len = *prev;
+ r_println!("Error: name too long (length: {} > 32)", len);
+}
+
+#[renderer]
+fn render_name(prev: ResultName) {
+ r_println!("Hello, {}!", *prev);
+}
+```
+
+## Boolean Parsing
+
+`Picker` can parse bools too, with two modes: implicit and explicit.
+
+| Mode | Format |
+| -------- | ----------------------------------- |
+| Implicit | `--confirmed` |
+| Explicit | `--confirm true` or `--confirm yes` |
+
+- Using `.pick::<bool>(flag)` → implicit: flag present means `true`
+- Using `.pick::<Yes>(flag)` or `.pick::<True>(flag)` → explicit
+
+Implicit is fine for most cases, but for important confirmations, explicit logic is more semantic.
+
+```rust
+// Features: ["parser"]
+@@@use mingling::parser::Yes;
+@@@dispatcher!("test", CMDTest => EntryTest);
+@@@pack!(ResultDone = ());
+
+#[chain]
+fn handle_entry(prev: EntryTest) -> Next {
+@@@ let prev1 = prev.clone();
+ let _confirmed: bool = prev.pick::<Yes>(()).unpack().is_yes();
+@@@ let prev = prev1;
+ let _confirm: bool = prev.pick::<bool>(["--confirm", "-C"]).unpack();
+ ResultDone::default().to_render()
+}
+```
+
+## Special Usage: `usize` Parsing
+
+Mingling provides a special `usize` feature: parsing strings like `25G`, `32mib`, etc.
+
+```rust
+// Features: ["parser"]
+
+#[test]
+fn parse_size() {
+ let vec = vec!["--size".to_string(), "25mib".to_string()];
+ let size: usize = vec.pick(["--size", "-S"]).unpack();
+ assert_eq!(size, 25 * 1024 * 1024);
+}
+```
+
+## Custom Parseable Types
+
+Implement the `Pickable` trait to make your type parseable by `Picker` — this is where Picker's extensibility comes from.
+
+```rust
+// Features: ["parser"]
+@@@use mingling::parser::{Pickable, Argument};
+@@@use mingling::Flag;
+#[derive(Default)]
+pub struct Address {
+ ip: String,
+ port: u16,
+}
+
+impl Pickable for Address {
+ type Output = Self;
+ fn pick(args: &mut Argument, flag: Flag) -> Option<Self::Output> {
+ let raw = args.pick_argument(flag)?;
+ let parts: Vec<&str> = raw.split(':').collect();
+ let ip = parts.first()?.to_string();
+ let port: u16 = parts.get(1)?.parse().ok()?;
+ Some(Address { ip, port })
+ }
+}
+@@@dispatcher!("connect", CMDConnect => EntryConnect);
+@@@pack!(ResultConnected = Address);
+
+#[chain]
+fn handle_connect_entry(prev: EntryConnect) -> Next {
+ let address: Address = prev.pick("--addr").unpack();
+ ResultConnected::new(address)
+}
+
+#[renderer]
+fn render_connected(prev: ResultConnected) {
+ let addr = prev.inner;
+ r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port);
+}
+```
+
+Output:
+
+```text
+~# my-cli connect --addr 127.0.0.1:8080
+Connected: IP: 127.0.0.1 PORT: 8080
+```
+
+## Auto-implementing Pickable for Enums
+
+To implement `Pickable` for an enum, just make it implement `EnumTag`, then implement `PickableEnum`:
+
+```rust
+// Features: ["parser"]
+@@@use mingling::parser::PickableEnum;
+@@@use mingling::EnumTag;
+#[derive(Debug, Default, EnumTag)]
+pub enum Fruits {
+ #[default]
+ Apple,
+ Banana,
+ Orange,
+}
+
+impl PickableEnum for Fruits {}
+@@@dispatcher!("eat", CMDEat => EntryEat);
+@@@pack!(ResultFruit = Fruits);
+
+#[chain]
+fn handle_eat_entry(prev: EntryEat) -> Next {
+ let fruit: Fruits = prev.pick("--fruit").unpack();
+ ResultFruit::new(fruit)
+}
+
+#[renderer]
+fn render_fruit(prev: ResultFruit) {
+ r_println!("Picked fruit: {:?}", *prev);
+}
+```
+
+That covers all the features of `Picker`.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/7-argument-parse-clap.md b/docs/pages/7-argument-parse-clap.md
new file mode 100644
index 0000000..e912c38
--- /dev/null
+++ b/docs/pages/7-argument-parse-clap.md
@@ -0,0 +1,87 @@
+<h1 align="center">Parsing Arguments with Clap</h1>
+<p align="center">
+ Use clap for more complex argument parsing
+</p>
+
+Picker is suitable for lightweight arg extraction, but when there are many args, complex validation rules, or you need auto-generated `--help`, you can use [clap](https://crates.io/crates/clap).
+
+## Enable clap feature
+
+```toml
+[dependencies.mingling]
+features = ["clap"]
+
+[dependencies.clap]
+version = "4"
+features = ["derive", "color"]
+```
+
+## dispatcher_clap
+
+Add `#[dispatcher_clap]` on a `clap::Parser` struct to auto-generate a Dispatcher:
+
+```rust
+// Features: ["clap"]
+// Dependencies:
+// clap = "4"
+@@@ use mingling::macros::dispatcher_clap;
+#[derive(Default, clap::Parser, Groupped)]
+#[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)]
+pub struct EntryGreet {
+ #[clap(default_value = "World")]
+ name: String,
+ #[arg(short, long, default_value_t = 1)]
+ repeat: i32,
+}
+
+#[renderer]
+fn render_greet(greet: EntryGreet) {
+ let count = greet.repeat.max(0) as usize;
+ r_print!("Hello, ");
+ for _ in 0..count {
+ r_print!("{} ", greet.name);
+ }
+ r_println!("!");
+}
+
+#[renderer]
+fn render_greet_parse_failed(err: ErrorGreetParsed) {
+ r_println!("{}", *err);
+}
+```
+
+## Working with BasicProgramSetup
+
+If you need `--help` support, register `BasicProgramSetup` in main and set the clap help output mode:
+
+```rust
+// Features: ["clap"]
+// Dependencies:
+// clap = "4"
+@@@use mingling::setup::BasicProgramSetup;
+@@@use mingling::macros::dispatcher_clap;
+@@@#[derive(Default, clap::Parser, Groupped)]
+@@@#[dispatcher_clap("greet", CMDGreet)]
+@@@pub struct EntryGreet {
+@@@ name: String,
+@@@}
+@@@#[renderer]
+@@@fn render_greet(greet: EntryGreet) {
+@@@ r_println!("Hello, {}!", greet.name);
+@@@}
+@@@
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup);
+ program.stdout_setting.clap_help_print_behaviour =
+ mingling::ClapHelpPrintBehaviour::WriteToRenderResult;
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+See [example-clap-binding](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-clap-binding) for more details.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/8-setup-and-resources.md b/docs/pages/8-setup-and-resources.md
new file mode 100644
index 0000000..3858e99
--- /dev/null
+++ b/docs/pages/8-setup-and-resources.md
@@ -0,0 +1,89 @@
+<h1 align="center">Program Setup</h1>
+<p align="center">
+ Initialize your program with Setup
+</p>
+
+When a program needs to do some init work at startup—like parsing global args or registering resources—you can organize that logic with `#[program_setup]`.
+
+## Initialize with Setup
+
+```rust
+// Features: ["extra_macros"]
+@@@use mingling::macros::program_setup;
+@@@use mingling::Program;
+#[program_setup]
+fn my_setup(program: &mut Program<ThisProgram>) {
+ // Extract global flag from args
+ program.global_flag(["-v", "--verbose"], |program| {
+ program.stdout_setting.verbose = true;
+ });
+}
+@@@
+@@@fn main() {
+@@@ let mut program = ThisProgram::new();
+@@@ program.with_setup(MySetup);
+@@@ program.exec_and_exit();
+@@@}
+@@@gen_program!();
+```
+
+A function annotated with `#[program_setup]` receives `&mut Program<ThisProgram>`, where you can do any init work.
+
+Register it in `main` via `program.with_setup(...)` to use it.
+
+> [!NOTE]
+> `#[program_setup]` requires the `extra_macros` feature. Without it, you can manually implement the `ProgramSetup` trait.
+
+## Extract Global Args
+
+The most common use of Setup is extracting global args. Mingling provides a few helper methods:
+
+```rust
+// Features: ["extra_macros"]
+@@@use mingling::macros::program_setup;
+@@@use mingling::Program;
+#[program_setup]
+fn my_setup(program: &mut Program<ThisProgram>) {
+ // Boolean flag
+ program.global_flag(["-v", "--verbose"], |program| {
+ program.stdout_setting.verbose = true;
+ });
+
+ // Flag with a value
+ program.global_argument("--name", |_program, value| {
+ // value is "Alice"
+ let _ = value;
+ });
+}
+```
+
+> [!TIP]
+> `global_flag` and `global_argument` automatically remove matched args from `program.args`, so they won't enter the pipeline.
+
+## Built-in Setup
+
+Mingling provides several ready-to-use Setups covering the most common CLI program needs:
+
+| Setup | Functionality |
+| --------------------------- | ------------------------------------------------------------------------------------------ |
+| `BasicProgramSetup` | Parses `--help`/`-h`, `--quiet`/`-q`, `--confirm`/`-C` |
+| `DirectoryEnvironmentSetup` | Registers directory resources: current dir, executable dir, home dir, temp dir |
+| `ExitCodeSetup` | Controls program exit code via `ResExitCode` |
+| `StructuralRendererSetup` | Enables `--json`, `--yaml` etc. structured output (requires `structural_renderer` feature) |
+
+Usage is just one line in `main`:
+
+```rust
+@@@use mingling::setup::BasicProgramSetup;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup);
+ program.exec_and_exit();
+}
+```
+
+`BasicProgramSetup` handles common params that most CLI programs need, saving you the trouble of manual parsing.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/9-error-handling.md b/docs/pages/9-error-handling.md
new file mode 100644
index 0000000..ec1f422
--- /dev/null
+++ b/docs/pages/9-error-handling.md
@@ -0,0 +1,120 @@
+<h1 align="center">Error Handling</h1>
+<p align="center">
+ Gracefully present errors to the user
+</p>
+
+A pipeline isn't just the happy path. When input is invalid, a resource isn't found, or an operation fails, you need a place to handle these "surprises" instead of letting the program panic.
+
+## Two Paths: Success vs. Error
+
+Recall the pipeline model: Chain's return value is `Next`, which has two destinations:
+
+| Route | Meaning |
+| -------------- | ------------------------------------------- |
+| `.to_render()` | Got a result, hand it to a Renderer to show |
+| `.to_chain()` | Not done yet, hand it to the next Chain |
+
+Error values can also take either path—you can render the error msg directly, or pass it to the next Chain for potential recovery.
+
+## Distinguish Errors with Dedicated Types
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+pack!(ResultGreeting = String);
+pack!(ErrorNameEmpty = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+
+ if name.is_empty() {
+ ErrorNameEmpty::new("name is required".to_string()).to_render()
+ } else {
+ ResultGreeting::new(name).to_render()
+ }
+}
+```
+
+Then write separate Renderers:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultGreeting = String);
+@@@pack!(ErrorNameEmpty = String);
+@@@#[chain] fn handle_greet(args: EntryGreet) -> Next { ResultGreeting::new(args.inner.first().cloned().unwrap_or_default()).to_render() }
+
+#[renderer]
+fn render_greeting(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
+}
+
+#[renderer]
+fn render_error_name_empty(err: ErrorNameEmpty) {
+ r_println!("Error: {}", *err);
+}
+```
+
+Each Renderer does its own job; what the user sees depends on what the Chain returned.
+
+## Complete Example
+
+```rust
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+pack!(ResultGreeting = String);
+pack!(ErrorNameEmpty = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ if name.is_empty() {
+ ErrorNameEmpty::new("name is required".to_string()).to_render()
+ } else {
+ ResultGreeting::new(name).to_render()
+ }
+}
+
+#[renderer]
+fn render_greeting(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
+}
+
+#[renderer]
+fn render_error_name_empty(err: ErrorNameEmpty) {
+ r_println!("Error: {}", *err);
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+gen_program!();
+```
+
+Output:
+
+```text
+~# my-cli greet Alice
+Hello, Alice!
+
+~# my-cli greet
+Error: name is required
+```
+
+## About `pack_err!`
+
+If you've enabled `extra_macros`, you can use `pack_err!` to quickly declare an error type with an auto-generated `name` field:
+
+```rust
+// Features: ["extra_macros"]
+pack_err!(ErrorNotFound);
+// Generates: struct ErrorNotFound { pub name: String }
+```
+
+See [Feature List](pages/other/features) for details.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/advanced/.name b/docs/pages/advanced/.name
new file mode 100644
index 0000000..a8a2c7e
--- /dev/null
+++ b/docs/pages/advanced/.name
@@ -0,0 +1 @@
+Advanced
diff --git a/docs/pages/advanced/1-completion.md b/docs/pages/advanced/1-completion.md
new file mode 100644
index 0000000..a90c3ce
--- /dev/null
+++ b/docs/pages/advanced/1-completion.md
@@ -0,0 +1,83 @@
+<h1 align="center">Completion</h1>
+<p align="center">
+ Fully dynamic completion system via the `comp` feature
+</p>
+
+Mingling's completion is **fully dynamic** — no static completion files, suggestions are computed at runtime based on the user's current input.
+
+## Enable `comp`
+
+```toml
+# Cargo.toml
+[dependencies.mingling]
+features = ["comp"]
+
+[build-dependencies.mingling]
+features = [
+ "comp",
+ # Enable `builds` for build-time support
+ "builds"
+]
+```
+
+## How it works
+
+When the user presses `TAB`, the completion script calls the program's hidden subcommand `__comp`, which dynamically queries the best suggestions based on the provided `ShellContext`.
+
+This hidden subcommand is auto-generated by `gen_program!()` when the `comp` feature is enabled. Its dispatcher is `CMDCompletion` — you need to add it to your program via `with_dispatcher`.
+
+Completion flow:
+
+1. Re-match the user's current input to a `Dispatcher`
+2. Call the corresponding `#[completion]` function
+3. The function returns a `Suggest` (file completion or a list of suggestions)
+4. Notify the shell to display the suggestions
+
+## Define completions
+
+Use `#[completion(EntryType)]` to define completion logic for an Entry:
+
+```rust
+// Features: ["comp"]
+@@@use mingling::prelude::*;
+@@@use mingling::{ShellContext, Suggest, SuggestItem};
+@@@use std::collections::BTreeSet;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+
+#[completion(EntryGreet)]
+fn complete_greet(ctx: &ShellContext) -> Suggest {
+ if ctx.previous_word == "greet" {
+ let mut items = BTreeSet::new();
+ items.insert(SuggestItem::new_with_desc("Alice".into(), "Likes to receive messages".into()));
+ items.insert(SuggestItem::new("World".into()));
+ Suggest::Suggest(items)
+ } else {
+ Suggest::FileCompletion
+ }
+}
+```
+
+The `suggest!` macro is a more concise way to write the same thing:
+
+```rust
+// Features: ["comp"]
+@@@use mingling::macros::suggest;
+@@@fn example() {
+suggest! {
+ "Alice": "Likes to receive messages",
+ "World"
+};
+@@@}
+```
+
+`ShellContext` holds the user's current input state (`previous_word`, `current_word`, `all_words`, etc.). `Suggest` has two variants: `Suggest::Suggest(list)` returns a suggestion list, `Suggest::FileCompletion` delegates file completion to the shell.
+
+## Generate completion scripts
+
+Call `build_comp_scripts` in `build.rs` to generate completion scripts (requires `builds` + `comp` features).
+
+See [example-completion](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-completion).
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/advanced/2-structural-renderer.md b/docs/pages/advanced/2-structural-renderer.md
new file mode 100644
index 0000000..09c86d1
--- /dev/null
+++ b/docs/pages/advanced/2-structural-renderer.md
@@ -0,0 +1,120 @@
+<h1 align="center">Structural Rendering</h1>
+<p align="center">
+ Use the <code>structural_renderer</code> feature to render output as serialized text
+</p>
+
+With `structural_renderer` enabled, your program can switch output to a structured format via `--json`, `--yaml`, etc., making it easy to integrate with other tools.
+
+## Enabling the Feature
+
+```toml
+[dependencies.mingling]
+features = ["structural_renderer"]
+```
+
+`structural_renderer` automatically enables `json_serde_fmt`.
+
+For more formats, enable `structural_renderer_full` (includes JSON, YAML, TOML, RON).
+
+> [!NOTE]
+> To customize output types, see [Features](./pages/other/features)
+
+## Basic Usage
+
+After enabling `StructuralRendererSetup`, use `pack_structural!` instead of `pack!` to declare types that support structured output:
+
+```rust
+// Features: ["structural_renderer"]
+// Dependencies:
+// serde = "1"
+@@@use mingling::setup::StructuralRendererSetup;
+@@@dispatcher!("render", CMDRender => EntryRender);
+
+// pack_structural! is equivalent to pack! + StructuralData
+pack_structural!(ResultInfo = (String, i32));
+
+#[chain]
+fn handle_render(args: EntryRender) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
+ ResultInfo::new((name, age))
+}
+
+#[renderer]
+fn render_info(r: ResultInfo) {
+ r_println!("{:?}", *r);
+}
+```
+
+Output:
+
+```text
+~# my-cli render Bob 22
+("Bob", 22)
+
+~# my-cli render Bob 22 --json
+{"inner":["Bob",22]}
+```
+
+When the user passes `--json`, the framework automatically serializes the render result as JSON — no business logic changes needed.
+
+## Customizing Output Structure
+
+The default output from `pack_structural!` includes an `inner` field. For full control over the output structure, define the type manually with `#[derive(StructuralData, Serialize, Groupped)]`:
+
+```rust
+// Features: ["structural_renderer"]
+// Dependencies:
+// serde = "1"
+@@@use mingling::prelude::*;
+@@@use mingling::setup::StructuralRendererSetup;
+@@@use mingling::StructuralData;
+@@@use serde::Serialize;
+@@@dispatcher!("render", CMDRender => EntryRender);
+
+#[derive(Serialize, StructuralData, Groupped)]
+struct Info {
+ name: String,
+ age: i32,
+}
+
+#[chain]
+fn handle_render(args: EntryRender) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
+ Info { name, age }.to_render()
+}
+
+#[renderer]
+fn render_info(info: Info) {
+ r_println!("{} is {} years old", info.name, info.age);
+}
+@@@
+@@@fn main() {
+@@@ let mut program = ThisProgram::new();
+@@@ program.with_setup(StructuralRendererSetup);
+@@@ program.with_dispatcher(CMDRender);
+@@@ program.exec();
+@@@}
+@@@gen_program!();
+```
+
+Now `--json` outputs:
+
+```json
+{ "name": "Bob", "age": 22 }
+```
+
+## Notes
+
+- Supported formats: JSON, YAML, TOML, RON (depends on enabled features)
+- `StructuralRendererSetup` registers global params like `--json`, `--yaml`, `--toml`, `--ron`
+
+> [!NOTE]
+> Each type still needs an **empty Renderer**, otherwise that type **is not considered renderable**
+
+See [example-structural-renderer](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-structural-renderer).
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/concepts/.name b/docs/pages/concepts/.name
new file mode 100644
index 0000000..efb7494
--- /dev/null
+++ b/docs/pages/concepts/.name
@@ -0,0 +1 @@
+Core Concepts
diff --git a/docs/pages/concepts/1-the-pipeline.md b/docs/pages/concepts/1-the-pipeline.md
new file mode 100644
index 0000000..e73379d
--- /dev/null
+++ b/docs/pages/concepts/1-the-pipeline.md
@@ -0,0 +1,129 @@
+<h1 align="center">The Pipeline</h1>
+<p align="center">
+ How Mingling executes commands, step by step
+</p>
+
+Mingling splits command handling into three independent phases: Dispatcher → Chain → Renderer. This doc covers the actual execution logic — what happens at each step from user input to final output.
+
+## Full Flow
+
+```mermaid
+graph TD
+ A["program.exec_and_exit()"] --> B["Hook: pre_dispatch"]
+ B --> C["Dispatch<br/>match cmd → Entry"]
+ C --> D["Hook: post_dispatch"]
+ D --> E{"user_context.help?"}
+ E -->|"true"| F["render_help<br/>skip to help rendering"]
+ E -->|"false"| G{"has_chain?"}
+ G -->|"yes"| H["Hook: pre_chain"]
+ H --> I["do_chain<br/>run business logic"]
+ I --> J{"ChainProcess?"}
+ J -->|"Ok(any, Renderer)"| K["Hook: pre_render →<br/>render → post_render"]
+ J -->|"Ok(any, Chain)"| G
+ J -->|"Err"| L["finish"]
+ G -->|"no"| M{"has_renderer?"}
+ M -->|"yes"| K
+ M -->|"no"| N["build_renderer_not_found"]
+ N --> G
+ K --> O["Hook: finish → return RenderResult"]
+ L --> O
+ F --> O
+```
+
+## Phase Breakdown
+
+### 1. Dispatch
+
+`exec_with_args` first calls `dispatch_args_dynamic` or `dispatch_args_trie` (depending on whether the `dispatch_tree` feature is enabled), matching user input against registered Dispatchers.
+
+The matching rule is **prefix matching** on space-separated tokens — the longest match wins. For example, if both `remote.add` and `remote` are registered, input `remote add origin` will match `remote.add`.
+
+```mermaid
+graph LR
+ Input["user input"] --> M{"match Dispatcher"}
+ M -->|"matched"| E["call dispatcher.begin(args)<br/>return wrapped Entry"]
+ M -->|"no match"| NF["build_dispatcher_not_found<br/>generate ErrorDispatcherNotFound"]
+```
+
+On a match, `dispatcher.begin(args)` is called, returning `ChainProcess::Ok((AnyOutput, _))` — the Entry type wrapping the user's input params.
+
+If no Dispatcher matches, `ErrorDispatcherNotFound` is generated (wrapping the full input), which a Renderer can later handle to display "Command not found".
+
+### 2. Help Shortcut
+
+Before entering the main loop, `program.user_context.help` is checked. If `true` (set by `HelpFlagSetup` in `BasicProgramSetup` when `--help` is parsed), `render_help` is called directly, skipping the entire pipeline.
+
+### 3. Chain Main Loop
+
+This is the core scheduling logic. Each iteration checks the current `AnyOutput`:
+
+1. **Has a Chain** → execute `C::do_chain(current)`
+ - Returns `(AnyOutput, Renderer)` → exit loop, go to rendering
+ - Returns `(AnyOutput, Chain)` → continue loop, pass result to next Chain
+ - Returns `Err` → terminate
+
+2. **No Chain, but has a Renderer** → render directly
+
+3. **Neither** → generate `renderer_not_found`, then loop again (the newly generated type might have a Renderer)
+
+```mermaid
+graph TD
+ Start["current AnyOutput"] --> C{"has_chain?"}
+ C -->|"yes"| Chain["do_chain"]
+ Chain -->|"returns (any, Chain)"| C
+ Chain -->|"returns (any, Renderer)"| Render["render"]
+ Chain -->|"Err"| Exit["exit"]
+ C -->|"no"| R{"has_renderer?"}
+ R -->|"yes"| Render
+ R -->|"no"| N["build_renderer_not_found<br/>try again"]
+ N --> C
+```
+
+### 4. Render
+
+The rendering phase calls `C::render(any, &mut render_result)`, which finds the matching `#[renderer]` function via `member_id` and writes the result into `RenderResult`. If `structural_renderer` is enabled, the result is also serialized to JSON/YAML (etc.) based on `program.structural_renderer_name`.
+
+### 5. Exit
+
+Sets `exit_code`, triggers the `finish` hook, and returns `RenderResult`.
+
+> [!TIP]
+> This runtime dispatch code is driven by the enums generated by `gen_program!()` and the `ProgramCollect` implementation.
+>
+> At compile time only the type-to-Chain / Renderer / Help / Completion mapping is generated; actual matching and routing happens at runtime.
+
+## How This Is Different from Direct Function Calls
+
+This pipeline helps avoid writing code like this:
+
+```rust
+@@@ struct Config;
+@@@ impl Config { fn read() -> Self { Config } }
+@@@ fn main() {
+@@@ let json = true;
+// read config
+let mut config = Config::read();
+
+// run operation
+let Ok(result) = operation(&mut config) else {
+ panic!("error handling");
+};
+
+// render result
+if json {
+ print_json();
+} else {
+ println!("success!");
+}
+@@@ }
+@@@ fn operation(config: &mut Config) -> Result<(),()> { Ok(()) }
+@@@ fn print_json() {}
+```
+
+Mingling's pipeline separates **cmd matching**, **business logic**, and **output rendering** into three independent slots, each responsible for one thing.
+
+More importantly, through hooks and the `AnyOutput` mechanism, the pipeline lets cross-cutting concerns (logging, auth, exit codes) be inserted non-invasively — no pollution of your business code.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/concepts/2-resource.md b/docs/pages/concepts/2-resource.md
new file mode 100644
index 0000000..ad7ee16
--- /dev/null
+++ b/docs/pages/concepts/2-resource.md
@@ -0,0 +1,60 @@
+<h1 align="center">Resource System</h1>
+<p align="center">
+ How Mingling Manages Global State
+</p>
+
+CLI programs often need to share global things—config files, database connections, counters, the current working directory.
+
+In vanilla Rust you might reach for `OnceCell` or `lazy_static`. In Mingling there's a unified mechanism: the **resource system**.
+
+## What is a Resource?
+
+A resource is data shared across multiple Chains and Renderers.
+
+You just define a type, register it with the Program, and declare it in your function signature—the framework handles injection and lifecycle management for you.
+
+## Core Mechanism: ResourceMarker
+
+Any type that implements both `Default + Clone` can automatically become a resource. The framework implements the `ResourceMarker` trait for it, giving it:
+
+- **`res_clone()`** — when multiple Chains access it concurrently, the framework can clone to avoid lock contention
+- **`res_default()`** — provides a fallback value when the resource hasn't been registered
+
+If you need finer lifecycle control, you can use `LazyRes<T>`. It lets the resource be initialized on first access and can run a callback on drop (e.g., saving state to disk before exit).
+
+## Why Not Global Variables?
+
+The traditional approach with statics creates implicit dependencies—you can't tell from the function signature what global state it uses. Mingling's resource injection makes **dependencies explicit**:
+
+- Whatever resources a function needs go in its parameter list
+- `&T` means read-only access, `&mut T` means mutable
+- Callers can see the function's side effects at a glance
+
+For example:
+
+```rust
+@@@ use mingling::res::ResExitCode;
+@@@ pack!(ErrorFileNotFound = ());
+#[chain]
+fn handle_error_file_not_found(
+ error: ErrorFileNotFound,
+ ec: &mut ResExitCode // the signature reveals the side effect!
+) {
+ ec.exit_code = 2; // modifying the exit code here
+}
+```
+
+## Resources and Setup
+
+Resources are typically registered with the Program in two ways:
+
+1. **Direct registration** — calling `program.with_resource(...)` in `main`
+2. **Via Setup** — using built-in Setups like `DirectoryEnvironmentSetup` to batch-register resources (e.g., `ResCurrentDir`, `ResHomeDir`)
+
+A Setup is a higher-level abstraction than a resource—one Setup can register multiple resources and do other initialization work.
+
+See the [Program Assembly](./pages/8-setup-and-resources) chapter in the tutorial for more details.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/concepts/3-any-output.md b/docs/pages/concepts/3-any-output.md
new file mode 100644
index 0000000..f780377
--- /dev/null
+++ b/docs/pages/concepts/3-any-output.md
@@ -0,0 +1,73 @@
+<h1 align="center">AnyOutput Mechanism</h1>
+<p align="center">
+ How AnyOutput and ChainProcess work
+</p>
+
+What data is passed between the Dispatcher → Chain → Renderer stages?
+
+A Chain's output could be a successful result, an error, or something that still needs to go to the next Chain—these types are all different. How does the pipeline route them to the right place without knowing the concrete types at compile time?
+
+## AnyOutput: Type Erasure + Group Tag
+
+Mingling's solution is to **erase all types into the same wrapper**, then use an **enum tag** to distinguish them:
+
+```
+AnyOutput<G>
+├── inner: Box<dyn Any + Send> ← the real data, type erased
+├── type_id: TypeId ← runtime type ID, for safe downcast
+└── member_id: G ← enum tag, marks "who this is"
+```
+
+Here `G` is the program enum generated by `gen_program!()` (i.e., `ThisProgram` as you know it).
+
+Each type annotated with `pack!` or `#[derive(Groupped)]` is assigned to one variant of this enum.
+
+## ChainProcess: Data + Routing
+
+On top of `AnyOutput`, `ChainProcess<G>` adds **routing info**:
+
+```
+ChainProcess<G>
+├── Ok(AnyOutput<G>, NextProcess) ← carries data, tells the dispatcher where to go next
+│ ├── NextProcess::Chain ← "not done yet, pass to the next Chain"
+│ └── NextProcess::Renderer ← "got a result, show it to the user"
+└── Err(ChainProcessError) ← "something went wrong, abort"
+```
+
+This is why a Chain function returns `ChainProcess` instead of raw data—it bundles **"where to go next"** and **"the data"** together.
+
+The dispatcher reads `NextProcess` to decide whether to continue the loop or exit to rendering.
+
+## Groupped: Who Is Who
+
+How does the dispatcher know whether an `AnyOutput` holds a `ResultName` or an `ErrorUserBlocked`? The answer is the `Groupped` trait:
+
+```
+trait Groupped<G> {
+ fn member_id() -> G;
+}
+```
+
+When you use `pack!(ResultName = String)`, the macro automatically implements `Groupped` for `ResultName`, and `member_id()` returns the corresponding enum variant. The dispatcher looks at `member_id` and finds the matching Chain or Renderer.
+
+`to_chain()` and `to_render()` are essentially convenience methods on `AnyOutput` that construct `ChainProcess::Ok(any, Chain)` and `ChainProcess::Ok(any, Renderer)` respectively.
+
+## How Dispatching Works
+
+At runtime, the main loop does this:
+
+1. Check the current `AnyOutput`'s `member_id`
+2. Look up whether this variant has a Chain → if yes, execute it, get a new `AnyOutput` and `NextProcess`
+3. If `NextProcess` is `Chain` → go back to step 1
+4. If `NextProcess` is `Renderer` → exit the loop, render
+
+This mechanism ensures **type safety**: the dispatch code generated by `gen_program!()` only does `restore` (converting from `Box<dyn Any>` back to a concrete type) inside the matching `member_id` branch, so it's impossible to unwrap a `ResultName`'s data as if it were `ErrorUserBlocked`.
+
+> [!TIP]
+> In day-to-day dev, you don't need to manually touch `AnyOutput` or `ChainProcess`.
+>
+> Macros like `pack!`, `#[chain]`, and `#[renderer]` handle all the wrapping and unwrapping for you.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/pages/concepts/4-program-collect.md b/docs/pages/concepts/4-program-collect.md
new file mode 100644
index 0000000..a24f115
--- /dev/null
+++ b/docs/pages/concepts/4-program-collect.md
@@ -0,0 +1,52 @@
+<h1 align="center">About ProgramCollect</h1>
+<p align="center">
+ Understand how gen_program!() builds a program
+</p>
+
+Every Mingling program ends with a `gen_program!()` call. Behind the scenes, it does three things to scaffold the entire program.
+
+## The three tasks of gen_program!()
+
+### 1. Generate an enum
+
+Scans the current module for all types marked with `pack!`, `#[chain]`, `#[renderer]` and similar macros, then generates an enum variant for each type.
+
+This enum is the type of `G` in `AnyOutput<G>` — the scheduler uses enum variants to distinguish different data flowing through the pipeline.
+
+### 2. Generate a ProgramCollect impl
+
+`ProgramCollect` is a trait that defines the mapping of **"which type each enum variant corresponds to and who handles it"**:
+
+- **`do_chain`** — calls the corresponding `#[chain]` function by `member_id`, returns a new `AnyOutput` and `NextProcess`
+- **`render`** — calls the corresponding `#[renderer]` function by `member_id`, writes to `RenderResult`
+- **`render_help`** — calls the corresponding `#[help]` function by `member_id`
+- **`has_chain` / `has_renderer`** — checks whether a variant has a corresponding handler
+- **`build_dispatcher_not_found` / `build_renderer_not_found` / `build_empty_result`** — three built-in fallback types for edge cases
+
+This mapping is resolved at runtime via enum matching — only the enum and match branches are generated at compile time; actual function calls happen at runtime.
+
+### 3. Generate ThisProgram
+
+Generates the `ThisProgram` type alias, pointing to `Program<GeneratedEnum>`. That's why you can write `ThisProgram::new()` directly in `main` — it's the complete type of your whole program.
+
+---
+
+## Differences under `pathf` and `dispatch_tree`
+
+The above describes the default behavior, which changes when specific features are enabled:
+
+### 1. `dispatch_tree` feature
+
+The Dispatcher no longer uses `Vec<Box<dyn Dispatcher>>` for linear matching. Instead, the subcommand structure is built as a prefix tree (Trie) at compile time.
+
+Matching complexity drops from `O(n)` to `O(k)` — where `k` is input length, independent of the number of commands.
+
+### 2. `pathf` feature (Module Pathfinder)
+
+By default, all macro-marked types must be in the same module for `gen_program!()` to collect them.
+
+With `pathf` enabled, the compiler automatically scans all sub-modules at compile time, finds all macro-marked types, and generates full module path references — types defined in deep sub-modules don't need a manual `use`.
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>