aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci-check-only.yml15
-rw-r--r--.github/workflows/ci.yml21
-rw-r--r--.gitignore3
-rw-r--r--.idea/.gitignore10
-rw-r--r--.idea/mingling.iml18
-rw-r--r--.idea/modules.xml8
-rw-r--r--.idea/runConfigurations/Check__All.xml26
-rw-r--r--.idea/runConfigurations/Check__Codes.xml26
-rw-r--r--.idea/runConfigurations/Check__Documents.xml26
-rw-r--r--.idea/runConfigurations/Refresh_All.xml27
-rw-r--r--.idea/runConfigurations/Run__Cargo_Format.xml23
-rw-r--r--.idea/runConfigurations/Run__Deploy_API_Docs.xml28
-rw-r--r--.idea/runConfigurations/Run__Fix_Documents_Codebox.xml28
-rw-r--r--.idea/runConfigurations/Run__Generate_Docsify_Sidebar.xml28
-rw-r--r--.idea/runConfigurations/Run__Package_All_Crates.xml28
-rw-r--r--.idea/runConfigurations/Run__Sync_Examples_To_Crates.xml28
-rw-r--r--.idea/runConfigurations/Run__Sync_Examples_To_Helpdoc.xml28
-rw-r--r--.idea/runConfigurations/Run__Sync_Feature_List.xml28
-rw-r--r--.idea/vcs.xml6
-rw-r--r--.run/.gitignore2
-rw-r--r--.run/src/bin/ci.rs20
-rw-r--r--.run/src/bin/cov-test.rs106
-rw-r--r--.run/src/bin/deploy-api-docs.rs35
-rw-r--r--.run/src/bin/doc.ps16
-rwxr-xr-x[-rw-r--r--].run/src/bin/doc.sh6
-rwxr-xr-x[-rw-r--r--].run/src/bin/http-page-preview.sh0
-rw-r--r--.run/src/bin/test-all-markdown-code.rs55
-rw-r--r--.run/src/bin/windows-folder-hide.ps1126
-rw-r--r--.run/src/lib.rs76
-rw-r--r--.vscode/settings.json11
-rw-r--r--.zed/settings.json11
-rw-r--r--CHANGELOG.md186
-rw-r--r--Cargo.lock343
-rw-r--r--Cargo.toml2
-rw-r--r--GETTING_STARTED.md732
-rw-r--r--README.md918
-rw-r--r--arg_picker/Cargo.toml3
-rw-r--r--arg_picker/src/arg.rs57
-rw-r--r--arg_picker/src/builtin.rs1
-rw-r--r--arg_picker/src/builtin/pick_picker_args.rs21
-rw-r--r--arg_picker/src/constants.rs16
-rw-r--r--arg_picker/src/infos.rs75
-rw-r--r--arg_picker/src/lib.rs21
-rw-r--r--arg_picker/src/parselib.rs23
-rw-r--r--arg_picker/src/pickable/multi_pickable.rs16
-rw-r--r--arg_picker/src/picker.rs108
-rw-r--r--arg_picker/src/picker/parse.rs8
-rw-r--r--arg_picker/src/picker/result.rs8
-rw-r--r--arg_picker/src/value/vec_until.rs1
-rw-r--r--arg_picker/test/src/test/route_test.rs4
-rw-r--r--arg_picker_macros/src/arg.rs11
-rw-r--r--arg_picker_macros/src/lib.rs1
-rw-r--r--docs/_zh_CN/pages/10-help.md26
-rw-r--r--docs/_zh_CN/pages/11-resource-system.md18
-rw-r--r--docs/_zh_CN/pages/14-testing.md2
-rw-r--r--docs/_zh_CN/pages/3-define-a-chain.md2
-rw-r--r--docs/_zh_CN/pages/4-render-result.md49
-rw-r--r--docs/_zh_CN/pages/5-multiple-commands.md17
-rw-r--r--docs/_zh_CN/pages/6-argument-parse-picker.md45
-rw-r--r--docs/_zh_CN/pages/7-argument-parse-clap.md34
-rw-r--r--docs/_zh_CN/pages/9-error-handling.md34
-rw-r--r--docs/_zh_CN/pages/advanced/2-structural-renderer.md22
-rw-r--r--docs/_zh_CN/pages/concepts/3-any-output.md10
-rw-r--r--docs/_zh_CN/pages/other/features.md18
-rw-r--r--docs/_zh_CN/pages/other/naming_rule.md19
-rw-r--r--docs/dev/pages/issues/add-picker2.md16
-rw-r--r--docs/dev/pages/issues/remove-r-print-macro.md12
-rw-r--r--docs/example-pages/examples.json15
-rw-r--r--docs/pages/10-help.md26
-rw-r--r--docs/pages/11-resource-system.md18
-rw-r--r--docs/pages/14-testing.md2
-rw-r--r--docs/pages/3-define-a-chain.md2
-rw-r--r--docs/pages/4-render-result.md53
-rw-r--r--docs/pages/5-multiple-commands.md17
-rw-r--r--docs/pages/6-argument-parse-picker.md116
-rw-r--r--docs/pages/7-argument-parse-clap.md34
-rw-r--r--docs/pages/9-error-handling.md34
-rw-r--r--docs/pages/advanced/2-structural-renderer.md22
-rw-r--r--docs/pages/concepts/3-any-output.md10
-rw-r--r--docs/pages/other/features.md18
-rw-r--r--docs/pages/other/naming_rule.md21
-rw-r--r--docs/res/icon3.pngbin0 -> 752608 bytes
-rw-r--r--docs/res/icon3_1024.pngbin0 -> 262830 bytes
-rw-r--r--examples/example-argument-picker/Cargo.lock94
-rw-r--r--examples/example-argument-picker/Cargo.toml12
-rw-r--r--examples/example-argument-picker/page.toml10
-rw-r--r--examples/example-argument-picker/src/main.rs225
-rw-r--r--examples/example-clap-binding/src/main.rs8
-rw-r--r--examples/example-custom-pickable/src/main.rs8
-rw-r--r--examples/example-enum-tag/src/main.rs4
-rw-r--r--examples/example-outside-type/src/main.rs2
-rw-r--r--examples/example-structural-renderer/src/main.rs14
-rw-r--r--examples/full-todolist/src/todolist.rs4
-rw-r--r--examples/test-examples.toml35
-rw-r--r--mingling/Cargo.toml7
-rw-r--r--mingling/src/constants.rs11
-rw-r--r--mingling/src/constants/exit_codes.rs14
-rw-r--r--mingling/src/constants/picker.rs115
-rw-r--r--mingling/src/example_docs.rs281
-rw-r--r--mingling/src/features.rs11
-rw-r--r--mingling/src/lib.md2
-rw-r--r--mingling/src/lib.rs57
-rw-r--r--mingling/src/parser/picker.rs4
-rw-r--r--mingling/src/picker.rs13
-rw-r--r--mingling/src/picker/entry_picker.rs (renamed from arg_picker/src/corebind/entry_picker.rs)25
-rw-r--r--mingling/src/picker/global.rs35
-rw-r--r--mingling/src/setups.rs7
-rw-r--r--mingling/src/setups/basic.rs26
-rw-r--r--mingling/src/setups/exit_code.rs11
-rw-r--r--mingling/src/setups/picker.rs7
-rw-r--r--mingling/src/setups/picker/basic.rs123
-rw-r--r--mingling/src/setups/picker/structural_renderer.rs76
-rw-r--r--mingling/src/setups/repl_basic.rs2
-rw-r--r--mingling/src/setups/structural_renderer.rs14
-rw-r--r--mingling_cli/Cargo.lock104
-rw-r--r--mingling_cli/Cargo.toml25
-rw-r--r--mingling_cli/src/main.rs3
-rw-r--r--mingling_core/Cargo.toml1
-rw-r--r--mingling_core/src/any.rs37
-rw-r--r--mingling_core/src/any/group.rs28
-rw-r--r--mingling_core/src/asset.rs3
-rw-r--r--mingling_core/src/asset/dispatcher.rs34
-rw-r--r--mingling_core/src/asset/routable.rs22
-rw-r--r--mingling_core/src/comp.rs2
-rw-r--r--mingling_core/src/comp/shell_ctx.rs34
-rw-r--r--mingling_core/src/comp/suggest.rs8
-rw-r--r--mingling_core/src/lib.rs7
-rw-r--r--mingling_core/src/program.rs47
-rw-r--r--mingling_core/src/program/collection.rs16
-rw-r--r--mingling_core/src/program/collection/mock.rs4
-rw-r--r--mingling_core/src/program/hook.rs4
-rw-r--r--mingling_core/src/program/setup.rs17
-rw-r--r--mingling_core/src/renderer/render_result.rs13
-rw-r--r--mingling_core/src/renderer/structural/error.rs1
-rw-r--r--mingling_macros/src/attr.rs9
-rw-r--r--mingling_macros/src/attr/chain.rs (renamed from mingling_macros/src/chain.rs)132
-rw-r--r--mingling_macros/src/attr/completion.rs (renamed from mingling_macros/src/completion.rs)41
-rw-r--r--mingling_macros/src/attr/dispatcher_clap.rs (renamed from mingling_macros/src/dispatcher_clap.rs)16
-rw-r--r--mingling_macros/src/attr/help.rs (renamed from mingling_macros/src/help.rs)86
-rw-r--r--mingling_macros/src/attr/program_setup.rs (renamed from mingling_macros/src/program_setup.rs)2
-rw-r--r--mingling_macros/src/attr/renderer.rs (renamed from mingling_macros/src/renderer.rs)59
-rw-r--r--mingling_macros/src/derive.rs2
-rw-r--r--mingling_macros/src/derive/enum_tag.rs (renamed from mingling_macros/src/enum_tag.rs)2
-rw-r--r--mingling_macros/src/derive/grouped.rs (renamed from mingling_macros/src/groupped.rs)12
-rw-r--r--mingling_macros/src/extensions.rs119
-rw-r--r--mingling_macros/src/extensions/buffer.rs95
-rw-r--r--mingling_macros/src/extensions/routeify.rs46
-rw-r--r--mingling_macros/src/func.rs13
-rw-r--r--mingling_macros/src/func/dispatcher.rs (renamed from mingling_macros/src/dispatcher.rs)10
-rw-r--r--mingling_macros/src/func/entry.rs (renamed from mingling_macros/src/entry.rs)2
-rw-r--r--mingling_macros/src/func/gen_program.rs628
-rw-r--r--mingling_macros/src/func/group.rs (renamed from mingling_macros/src/group_impl.rs)6
-rw-r--r--mingling_macros/src/func/node.rs (renamed from mingling_macros/src/node.rs)2
-rw-r--r--mingling_macros/src/func/pack.rs (renamed from mingling_macros/src/pack.rs)4
-rw-r--r--mingling_macros/src/func/pack_err.rs (renamed from mingling_macros/src/pack_err.rs)12
-rw-r--r--mingling_macros/src/func/r_print.rs62
-rw-r--r--mingling_macros/src/func/suggest.rs (renamed from mingling_macros/src/suggest.rs)4
-rw-r--r--mingling_macros/src/lib.rs987
-rw-r--r--mingling_macros/src/systems.rs5
-rw-r--r--mingling_macros/src/systems/dispatch_tree_gen.rs (renamed from mingling_macros/src/dispatch_tree_gen.rs)6
-rw-r--r--mingling_macros/src/systems/res_injection.rs (renamed from mingling_macros/src/res_injection.rs)0
-rw-r--r--mingling_macros/src/systems/structural_data.rs (renamed from mingling_macros/src/structural_data.rs)4
-rw-r--r--mingling_macros/src/utils.rs1
-rw-r--r--mingling_pathf/README.md2
-rw-r--r--mingling_pathf/src/error.rs16
-rw-r--r--mingling_pathf/src/lib.rs1
-rw-r--r--mingling_pathf/src/pattern_analyzer.rs4
-rw-r--r--mingling_pathf/src/patterns.rs4
-rw-r--r--mingling_pathf/src/patterns/dispatcher.rs8
-rw-r--r--mingling_pathf/src/patterns/dispatcher_clap.rs6
-rw-r--r--mingling_pathf/src/patterns/grouped_derive.rs (renamed from mingling_pathf/src/patterns/groupped_derive.rs)34
-rw-r--r--mingling_pathf/test/src/lib.rs4
-rw-r--r--mingling_pathf/test/src/test_files/test_grouped_derive.rs (renamed from mingling_pathf/test/src/test_files/test_groupped_derive.rs)16
-rw-r--r--mling/.gitignore2
-rw-r--r--mling/Cargo.toml41
l---------mling/LICENSE-APACHE1
l---------mling/LICENSE-MIT1
-rw-r--r--mling/README.md3
-rw-r--r--mling/build.rs63
-rw-r--r--mling/res/CHECKLIST.md110
-rw-r--r--mling/res/template_0.2/command/Cargo.toml.tmpl0
-rw-r--r--mling/res/template_0.2/command/src/main.rs1
-rw-r--r--mling/res/template_0.2/flat/Cargo.toml.tmpl0
-rw-r--r--mling/res/template_0.2/flat/src/main.rs0
-rw-r--r--mling/res/template_0.2/modularity/Cargo.toml.tmpl0
-rw-r--r--mling/res/template_0.2/modularity/src/main.rs0
-rw-r--r--mling/src/bin/cargo-mling.rs3
-rw-r--r--mling/src/bin/mling.rs3
-rw-r--r--mling/src/cargo_style.rs221
-rw-r--r--mling/src/cli.rs195
-rw-r--r--mling/src/display.rs384
-rw-r--r--mling/src/errors.rs2
-rw-r--r--mling/src/errors/io_error.rs224
-rw-r--r--mling/src/helps/mling_help.txt20
-rw-r--r--mling/src/lib.rs18
-rw-r--r--mling/src/pkg_mgr/installer.rs1
-rw-r--r--mling/src/pkg_mgr/mod.rs18
-rw-r--r--mling/src/proj_mgr/checklist_reader.rs294
-rw-r--r--mling/src/proj_mgr/generator.rs57
-rw-r--r--mling/src/proj_mgr/metadata.rs142
-rw-r--r--mling/src/proj_mgr/mod.rs30
-rw-r--r--mling/src/proj_mgr/show_binaries.rs78
-rw-r--r--mling/src/proj_mgr/show_directories.rs61
-rw-r--r--mling/src/res/current_dir.rs6
-rw-r--r--mling/src/res/manifest_path.rs13
-rw-r--r--mling/src/res/mod.rs5
-rw-r--r--verified-docs.toml1
207 files changed, 5539 insertions, 4714 deletions
diff --git a/.github/workflows/ci-check-only.yml b/.github/workflows/ci-check-only.yml
index 1179ed0..2cff3a6 100644
--- a/.github/workflows/ci-check-only.yml
+++ b/.github/workflows/ci-check-only.yml
@@ -20,9 +20,9 @@ jobs:
uses: actions/cache@v4
with:
path: .temp
- key: ${{ runner.os }}-mingling-temp-${{ hashFiles('Cargo.lock', 'dev_tools/Cargo.toml', 'verified-docs.toml', 'examples/test-examples.toml') }}-${{ hashFiles('dev_tools/src/**/*.rs') }}
+ key: ${{ runner.os }}-mingling-temp-codes-${{ hashFiles('.run/Cargo.toml', '.run/src/**/*.rs', 'Cargo.lock', 'verified-docs.toml', 'examples/test-examples.toml') }}
restore-keys: |
- ${{ runner.os }}-mingling-temp-
+ ${{ runner.os }}-mingling-temp-codes-
- run: cargo ci --test-codes
@@ -40,8 +40,15 @@ jobs:
uses: actions/cache@v4
with:
path: .temp
- key: ${{ runner.os }}-mingling-temp-${{ hashFiles('Cargo.lock', 'dev_tools/Cargo.toml', 'verified-docs.toml', 'examples/test-examples.toml') }}-${{ hashFiles('dev_tools/src/**/*.rs') }}
+ key: ${{ runner.os }}-mingling-temp-docs-${{ hashFiles('.run/Cargo.toml', '.run/src/**/*.rs', 'Cargo.lock', 'verified-docs.toml', 'examples/test-examples.toml') }}
restore-keys: |
- ${{ runner.os }}-mingling-temp-
+ ${{ runner.os }}-mingling-temp-docs-
- run: cargo ci --test-docs
+
+ - name: Upload docs test result on failure
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: docs-test-result-${{ matrix.os }}.md
+ path: .temp/DOCS-TEST-RESULT.md
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b81c1ef..0a6a9a6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,9 +22,9 @@ jobs:
uses: actions/cache@v4
with:
path: .temp
- key: ${{ runner.os }}-mingling-temp-${{ hashFiles('Cargo.lock', 'dev_tools/Cargo.toml', 'verified-docs.toml', 'examples/test-examples.toml') }}-${{ hashFiles('dev_tools/src/**/*.rs') }}
+ key: ${{ runner.os }}-mingling-temp-codes-${{ hashFiles('.run/Cargo.toml', '.run/src/**/*.rs', 'Cargo.lock', 'verified-docs.toml', 'examples/test-examples.toml') }}
restore-keys: |
- ${{ runner.os }}-mingling-temp-
+ ${{ runner.os }}-mingling-temp-codes-
- run: cargo ci --test-codes
@@ -42,12 +42,19 @@ jobs:
uses: actions/cache@v4
with:
path: .temp
- key: ${{ runner.os }}-mingling-temp-${{ hashFiles('Cargo.lock', 'dev_tools/Cargo.toml', 'verified-docs.toml', 'examples/test-examples.toml') }}-${{ hashFiles('dev_tools/src/**/*.rs') }}
+ key: ${{ runner.os }}-mingling-temp-docs-${{ hashFiles('.run/Cargo.toml', '.run/src/**/*.rs', 'Cargo.lock', 'verified-docs.toml', 'examples/test-examples.toml') }}
restore-keys: |
- ${{ runner.os }}-mingling-temp-
+ ${{ runner.os }}-mingling-temp-docs-
- run: cargo ci --test-docs
+ - name: Upload docs test result on failure
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: docs-test-result-${{ matrix.os }}.md
+ path: .temp/DOCS-TEST-RESULT.md
+
Move-Unreleased-Tag:
if: github.ref == 'refs/heads/main'
needs: [Check-Codes, Check-Docs]
@@ -78,6 +85,12 @@ jobs:
- name: Build API docs
run: cargo run --manifest-path .run/Cargo.toml --bin deploy-api-docs
+ - name: Install cargo-llvm-cov
+ run: cargo install cargo-llvm-cov
+
+ - name: Run cov-test
+ run: cargo run --manifest-path .run/Cargo.toml --bin cov-test
+
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
diff --git a/.gitignore b/.gitignore
index 274c6cf..63d40ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,9 @@
# Temp file for .run/src/bin/deploy-api-docs.rs
docs/api-docs/
+# Temp file for .run/src/bin/cov-test.rs
+docs/cov-test/
+
# Drafts
__*.md
__*/
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..30cf57e
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/mingling.iml b/.idea/mingling.iml
new file mode 100644
index 0000000..ca57e6c
--- /dev/null
+++ b/.idea/mingling.iml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="EMPTY_MODULE" version="4">
+ <component name="NewModuleRootManager">
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/arg_picker/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/arg_picker_macros/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/mingling/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/mingling_core/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/mingling_core/tests" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/mingling_macros/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/mingling_pathf/src" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/mling/src" isTestSource="false" />
+ <excludeFolder url="file://$MODULE_DIR$/target" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module> \ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..764f612
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectModuleManager">
+ <modules>
+ <module fileurl="file://$PROJECT_DIR$/.idea/mingling.iml" filepath="$PROJECT_DIR$/.idea/mingling.iml" />
+ </modules>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/runConfigurations/Check__All.xml b/.idea/runConfigurations/Check__All.xml
new file mode 100644
index 0000000..95af540
--- /dev/null
+++ b/.idea/runConfigurations/Check__All.xml
@@ -0,0 +1,26 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Check: All"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="CI"
+ >
+ <option
+ name="command"
+ value="run --manifest-path .run/Cargo.toml --bin ci --quiet -- -y"
+ />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2" />
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Check__Codes.xml b/.idea/runConfigurations/Check__Codes.xml
new file mode 100644
index 0000000..f117f8f
--- /dev/null
+++ b/.idea/runConfigurations/Check__Codes.xml
@@ -0,0 +1,26 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Check: Codes"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="CI"
+ >
+ <option
+ name="command"
+ value="run --manifest-path .run/Cargo.toml --bin ci --quiet -- --test-codes -y"
+ />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2" />
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Check__Documents.xml b/.idea/runConfigurations/Check__Documents.xml
new file mode 100644
index 0000000..b5a503b
--- /dev/null
+++ b/.idea/runConfigurations/Check__Documents.xml
@@ -0,0 +1,26 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Check: Documents"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="CI"
+ >
+ <option
+ name="command"
+ value="run --manifest-path .run/Cargo.toml --bin ci --quiet -- --test-docs -y"
+ />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2" />
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Refresh_All.xml b/.idea/runConfigurations/Refresh_All.xml
new file mode 100644
index 0000000..24be845
--- /dev/null
+++ b/.idea/runConfigurations/Refresh_All.xml
@@ -0,0 +1,27 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Refresh All"
+ type="CompoundRunConfigurationType"
+ >
+ <toRun name="Run: Cargo Format" type="CargoCommandRunConfiguration" />
+ <toRun
+ name="Run: Fix Documents Codebox"
+ type="CargoCommandRunConfiguration"
+ />
+ <toRun
+ name="Run: Generate Docsify Sidebar"
+ type="CargoCommandRunConfiguration"
+ />
+ <toRun
+ name="Run: Sync Examples To Crates"
+ type="CargoCommandRunConfiguration"
+ />
+ <toRun
+ name="Run: Sync Examples To Helpdoc"
+ type="CargoCommandRunConfiguration"
+ />
+ <toRun name="Run: Sync Feature List" type="CargoCommandRunConfiguration" />
+ <method v="2" />
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Run__Cargo_Format.xml b/.idea/runConfigurations/Run__Cargo_Format.xml
new file mode 100644
index 0000000..a99a91d
--- /dev/null
+++ b/.idea/runConfigurations/Run__Cargo_Format.xml
@@ -0,0 +1,23 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Run: Cargo Format"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="Tools"
+ >
+ <option name="command" value="fmt" />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2" />
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Run__Deploy_API_Docs.xml b/.idea/runConfigurations/Run__Deploy_API_Docs.xml
new file mode 100644
index 0000000..1a32a64
--- /dev/null
+++ b/.idea/runConfigurations/Run__Deploy_API_Docs.xml
@@ -0,0 +1,28 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Run: Deploy API Docs"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="Tools"
+ >
+ <option
+ name="command"
+ value="run --manifest-path .run/Cargo.toml --quiet --bin deploy-api-docs"
+ />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2">
+ <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
+ </method>
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Run__Fix_Documents_Codebox.xml b/.idea/runConfigurations/Run__Fix_Documents_Codebox.xml
new file mode 100644
index 0000000..0863ef1
--- /dev/null
+++ b/.idea/runConfigurations/Run__Fix_Documents_Codebox.xml
@@ -0,0 +1,28 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Run: Fix Documents Codebox"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="Tools"
+ >
+ <option
+ name="command"
+ value="run --manifest-path .run/Cargo.toml --quiet --bin docs-code-box-fix"
+ />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2">
+ <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
+ </method>
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Run__Generate_Docsify_Sidebar.xml b/.idea/runConfigurations/Run__Generate_Docsify_Sidebar.xml
new file mode 100644
index 0000000..a50a2dd
--- /dev/null
+++ b/.idea/runConfigurations/Run__Generate_Docsify_Sidebar.xml
@@ -0,0 +1,28 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Run: Generate Docsify Sidebar"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="Tools"
+ >
+ <option
+ name="command"
+ value="run --manifest-path .run/Cargo.toml --quiet --bin docsify-sidebar-gen"
+ />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2">
+ <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
+ </method>
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Run__Package_All_Crates.xml b/.idea/runConfigurations/Run__Package_All_Crates.xml
new file mode 100644
index 0000000..336c9b3
--- /dev/null
+++ b/.idea/runConfigurations/Run__Package_All_Crates.xml
@@ -0,0 +1,28 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Run: Package All Crates"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="Tools"
+ >
+ <option
+ name="command"
+ value="run --manifest-path .run/Cargo.toml --quiet --bin package-all"
+ />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2">
+ <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
+ </method>
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Run__Sync_Examples_To_Crates.xml b/.idea/runConfigurations/Run__Sync_Examples_To_Crates.xml
new file mode 100644
index 0000000..43db9bb
--- /dev/null
+++ b/.idea/runConfigurations/Run__Sync_Examples_To_Crates.xml
@@ -0,0 +1,28 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Run: Sync Examples To Crates"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="Tools"
+ >
+ <option
+ name="command"
+ value="run --manifest-path .run/Cargo.toml --quiet --bin refresh-docs"
+ />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2">
+ <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
+ </method>
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Run__Sync_Examples_To_Helpdoc.xml b/.idea/runConfigurations/Run__Sync_Examples_To_Helpdoc.xml
new file mode 100644
index 0000000..8e7faff
--- /dev/null
+++ b/.idea/runConfigurations/Run__Sync_Examples_To_Helpdoc.xml
@@ -0,0 +1,28 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Run: Sync Examples To Helpdoc"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="Tools"
+ >
+ <option
+ name="command"
+ value="run --manifest-path .run/Cargo.toml --quiet --bin sync-examples"
+ />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2">
+ <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
+ </method>
+ </configuration>
+</component>
diff --git a/.idea/runConfigurations/Run__Sync_Feature_List.xml b/.idea/runConfigurations/Run__Sync_Feature_List.xml
new file mode 100644
index 0000000..1e6ed8f
--- /dev/null
+++ b/.idea/runConfigurations/Run__Sync_Feature_List.xml
@@ -0,0 +1,28 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration
+ default="false"
+ name="Run: Sync Feature List"
+ type="CargoCommandRunConfiguration"
+ factoryName="Cargo Command"
+ folderName="Tools"
+ >
+ <option
+ name="command"
+ value="run --manifest-path .run/Cargo.toml --quiet --bin refresh-feature-mod"
+ />
+ <option name="workingDirectory" value="file://$PROJECT_DIR$" />
+ <envs />
+ <option name="emulateTerminal" value="true" />
+ <option name="channel" value="DEFAULT" />
+ <option name="requiredFeatures" value="true" />
+ <option name="allFeatures" value="false" />
+ <option name="withSudo" value="false" />
+ <option name="buildTarget" value="REMOTE" />
+ <option name="backtrace" value="SHORT" />
+ <option name="isRedirectInput" value="false" />
+ <option name="redirectInputPath" value="" />
+ <method v="2">
+ <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
+ </method>
+ </configuration>
+</component>
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="" vcs="Git" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.run/.gitignore b/.run/.gitignore
index 0dc39b0..40f407e 100644
--- a/.run/.gitignore
+++ b/.run/.gitignore
@@ -3,5 +3,3 @@
# If you want to add some Rust crates? Remove this line
# /Cargo.*
-
-last_check
diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs
index 2527ece..f3ae9e8 100644
--- a/.run/src/bin/ci.rs
+++ b/.run/src/bin/ci.rs
@@ -123,14 +123,26 @@ fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> {
}
if run_all || test_docs {
- println_cargo_style!("Phase: Test all examples");
- test_examples()?;
+ let mut exit_code = 0;
println_cargo_style!("Phase: Verify all *.md document code blocks are compilable");
- test_docs_code_blocks()?;
+ if let Err(code) = test_docs_code_blocks() {
+ exit_code = exit_code.max(code);
+ }
+
+ println_cargo_style!("Phase: Test all examples");
+ if let Err(code) = test_examples() {
+ exit_code = exit_code.max(code);
+ }
println_cargo_style!("Phase: Check all documentation is up to date");
- docs_refresh()?;
+ if let Err(code) = docs_refresh() {
+ exit_code = exit_code.max(code);
+ }
+
+ if exit_code != 0 {
+ return Err(exit_code);
+ }
}
run_cmd!("git add --renormalize .")?;
diff --git a/.run/src/bin/cov-test.rs b/.run/src/bin/cov-test.rs
new file mode 100644
index 0000000..a53f74c
--- /dev/null
+++ b/.run/src/bin/cov-test.rs
@@ -0,0 +1,106 @@
+use std::fs;
+use tools::{println_cargo_style, run_cmd};
+
+const OUTPUT_DIR: &str = "docs/cov-test";
+
+fn main() {
+ let repo_root = find_git_repo().expect("Failed to find git repository root");
+ let output_path = repo_root.join(OUTPUT_DIR);
+
+ // Read features from [package.metadata.docs.rs]
+ let features = tools::read_features().unwrap_or_else(|e| {
+ eprintln!("Error: {}", e);
+ std::process::exit(1);
+ });
+
+ let features_arg = features.join(",");
+
+ // Ensure output directory exists
+ std::fs::create_dir_all(&output_path).expect("Failed to create output directory");
+
+ let cmd = format!(
+ "cargo llvm-cov --html --output-dir \"{}\" --workspace --features \"{}\" --color always",
+ output_path.to_string_lossy(),
+ features_arg,
+ );
+
+ println_cargo_style!("Features: {}", features_arg);
+ println_cargo_style!("Coverage: {}", output_path.display());
+
+ println_cargo_style!("Running: cargo llvm-cov --html");
+ run_cmd!(&cmd).unwrap_or_else(|code| {
+ eprintln!("Error: cargo llvm-cov failed with exit code {}", code);
+ std::process::exit(code);
+ });
+
+ // Move files from <output_path>/html/ to <output_path>
+ let html_dir = output_path.join("html");
+ if html_dir.exists() && html_dir.is_dir() {
+ println_cargo_style!("Moving files from {}/html/ to {}/", OUTPUT_DIR, OUTPUT_DIR);
+
+ // Move each entry in html_dir up one level
+ for entry in fs::read_dir(&html_dir).expect("Failed to read html directory") {
+ let entry = entry.expect("Failed to read entry");
+ let entry_path = entry.path();
+ let file_name = entry
+ .file_name()
+ .to_str()
+ .expect("Invalid filename")
+ .to_owned();
+
+ let dest_path = output_path.join(&file_name);
+ // Remove existing file/directory at destination if any
+ if dest_path.exists() {
+ if dest_path.is_dir() {
+ fs::remove_dir_all(&dest_path).unwrap_or_else(|e| {
+ eprintln!(
+ "Warning: could not remove directory {}: {}",
+ dest_path.display(),
+ e
+ );
+ });
+ } else {
+ fs::remove_file(&dest_path).unwrap_or_else(|e| {
+ eprintln!(
+ "Warning: could not remove file {}: {}",
+ dest_path.display(),
+ e
+ );
+ });
+ }
+ }
+ fs::rename(&entry_path, &dest_path).unwrap_or_else(|e| {
+ eprintln!("Warning: could not move {}: {}", entry_path.display(), e);
+ });
+ }
+
+ // Remove the now-empty html directory
+ fs::remove_dir(&html_dir).unwrap_or_else(|e| {
+ eprintln!("Warning: could not remove html directory: {}", e);
+ });
+
+ println_cargo_style!("Files moved successfully.");
+ }
+
+ println_cargo_style!(
+ "Done: coverage report generated at {}/index.html",
+ OUTPUT_DIR
+ );
+}
+
+fn find_git_repo() -> Option<std::path::PathBuf> {
+ let mut current_dir = std::env::current_dir().ok()?;
+
+ loop {
+ let git_dir = current_dir.join(".git");
+ if git_dir.exists() && git_dir.is_dir() {
+ return Some(current_dir);
+ }
+
+ if !current_dir.pop() {
+ break;
+ }
+ }
+
+ None
+}
diff --git a/.run/src/bin/deploy-api-docs.rs b/.run/src/bin/deploy-api-docs.rs
index ea52886..7aa093c 100644
--- a/.run/src/bin/deploy-api-docs.rs
+++ b/.run/src/bin/deploy-api-docs.rs
@@ -2,43 +2,16 @@ use std::path::Path;
use tools::{println_cargo_style, run_cmd};
-const DOCS_RS_MANIFEST: &str = "mingling/Cargo.toml";
const OUTPUT_DIR: &str = "docs/api-docs";
fn main() {
let repo_root = find_git_repo().expect("Failed to find git repository root");
- let manifest_path = repo_root.join(DOCS_RS_MANIFEST);
- if !manifest_path.exists() {
- eprintln!("Error: Manifest not found at {}", manifest_path.display());
+ // Read features from [package.metadata.docs.rs]
+ let features = tools::read_features().unwrap_or_else(|e| {
+ eprintln!("Error: {}", e);
std::process::exit(1);
- }
-
- // Read and parse [package.metadata.docs.rs]
- let manifest_content =
- std::fs::read_to_string(&manifest_path).expect("Failed to read Cargo.toml");
- let cargo_toml: toml::Value = manifest_content
- .parse()
- .expect("Failed to parse Cargo.toml");
-
- let doc_features = cargo_toml
- .get("package")
- .and_then(|p| p.get("metadata"))
- .and_then(|m| m.get("docs"))
- .and_then(|d| d.get("rs"))
- .and_then(|rs| rs.get("features"))
- .and_then(|f| f.as_array())
- .unwrap_or_else(|| {
- eprintln!("Error: [package.metadata.docs.rs] or its 'features' key not found in mingling/Cargo.toml");
- std::process::exit(1);
- });
-
- let features: Vec<&str> = doc_features.iter().filter_map(|v| v.as_str()).collect();
-
- if features.is_empty() {
- eprintln!("Error: No features defined in [package.metadata.docs.rs]");
- std::process::exit(1);
- }
+ });
let features_arg = features.join(",");
diff --git a/.run/src/bin/doc.ps1 b/.run/src/bin/doc.ps1
index fd7afaa..0e55141 100644
--- a/.run/src/bin/doc.ps1
+++ b/.run/src/bin/doc.ps1
@@ -1 +1,5 @@
-cargo doc --workspace --no-deps --features core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros --open
+cargo doc `
+ --manifest-path mingling/Cargo.toml `
+ --no-deps `
+ --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros `
+ --open
diff --git a/.run/src/bin/doc.sh b/.run/src/bin/doc.sh
index 2d59896..014ea44 100644..100755
--- a/.run/src/bin/doc.sh
+++ b/.run/src/bin/doc.sh
@@ -1,3 +1,7 @@
#!/bin/bash
-cargo doc --workspace --no-deps --features core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros --open
+cargo doc \
+ --manifest-path mingling/Cargo.toml \
+ --no-deps \
+ --features docs_rs,core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros \
+ --open
diff --git a/.run/src/bin/http-page-preview.sh b/.run/src/bin/http-page-preview.sh
index bed4b1c..bed4b1c 100644..100755
--- a/.run/src/bin/http-page-preview.sh
+++ b/.run/src/bin/http-page-preview.sh
diff --git a/.run/src/bin/test-all-markdown-code.rs b/.run/src/bin/test-all-markdown-code.rs
index 280fca7..1c0c9e2 100644
--- a/.run/src/bin/test-all-markdown-code.rs
+++ b/.run/src/bin/test-all-markdown-code.rs
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use colored::Colorize;
use indicatif::ProgressBar;
use tools::verify::{
- build_block, compute_block_hash, generate_cargo_toml, generate_main_rs, generate_build_rs,
+ build_block, compute_block_hash, generate_build_rs, generate_cargo_toml, generate_main_rs,
is_block_testable, parse_code_blocks, write_summary_report,
};
use tools::{eprintln_cargo_style, println_cargo_style};
@@ -13,16 +13,7 @@ use tools::{eprintln_cargo_style, println_cargo_style};
/// Config from verified-docs.toml
#[derive(serde::Deserialize)]
struct Config {
- verified: VerifiedPaths,
-}
-
-#[derive(serde::Deserialize)]
-struct VerifiedPaths {
- readme: String,
- #[serde(default)]
- documents_en_us: Option<String>,
- #[serde(default)]
- documents_zh_cn: Option<String>,
+ verified: HashMap<String, String>,
}
#[tokio::main]
@@ -64,31 +55,28 @@ async fn main() {
};
// Collect all markdown files from config
+ // Keys are used as labels; values are either single file paths or directory globs
let mut files: Vec<(String, PathBuf)> = Vec::new();
- // README
- let readme_path = PathBuf::from(&config.verified.readme);
- if readme_path.exists() {
- files.push(("README".to_string(), readme_path));
- }
-
- // English docs
- if let Some(pattern) = &config.verified.documents_en_us {
- let base = pattern.trim_end_matches("/**").trim_end_matches('*');
- let dir = PathBuf::from(base);
- if dir.exists() && dir.is_dir() {
- collect_md_files(&dir, &mut files, "en");
+ for (key, value) in &config.verified {
+ let candidate = PathBuf::from(value);
+ if candidate.is_dir() {
+ // Directory — walk it for all .md files, using the key as label
+ collect_md_files(&candidate, &mut files, key);
+ } else if candidate.exists() && candidate.is_file() {
+ // Single file
+ files.push((key.to_string(), candidate));
+ } else if candidate.extension().is_none() {
+ // No extension — treat as a glob like "docs/pages/**", walk the base dir instead
+ let base = PathBuf::from(value.trim_end_matches("/**").trim_end_matches('*'));
+ if base.is_dir() {
+ collect_md_files(&base, &mut files, key);
+ }
}
}
- // Chinese docs
- if let Some(pattern) = &config.verified.documents_zh_cn {
- let base = pattern.trim_end_matches("/**").trim_end_matches('*');
- let dir = PathBuf::from(base);
- if dir.exists() && dir.is_dir() {
- collect_md_files(&dir, &mut files, "zh_CN");
- }
- }
+ // Sort for deterministic ordering
+ files.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
// If a single file was specified, filter the list to only that file
if let Some(ref target) = single_file {
@@ -201,10 +189,7 @@ async fn main() {
bar.inc(1);
} else {
bar.inc(1);
- bar.println(format!(
- " {} {block_label}",
- "failed".bold().bright_red()
- ));
+ bar.println(format!(" {} {block_label}", "failed".bold().bright_red()));
bar.println(format!(" {block_label} FAILED:\n{err}"));
}
group_results.push((block.source_file.clone(), block.line, ok, err));
diff --git a/.run/src/bin/windows-folder-hide.ps1 b/.run/src/bin/windows-folder-hide.ps1
index 0ab2632..ff53202 100644
--- a/.run/src/bin/windows-folder-hide.ps1
+++ b/.run/src/bin/windows-folder-hide.ps1
@@ -1,43 +1,115 @@
-# Check `last_check`
+$skipDirs = @('.git', '.temp', 'target', 'node_modules', '.pnpm')
+$selfPath = (Get-Item -LiteralPath $MyInvocation.MyCommand.Path).Directory.FullName
-$lastCheckFile = Join-Path $PSScriptRoot "last_check"
-$currentTime = Get-Date
-$timeThreshold = 10
+function Test-InSkipDir {
+ param(
+ [object]$Item
+ )
+ $path = if ($Item -is [string]) {
+ $Item
+ } elseif ($Item.PSPath) {
+ $Item.PSPath -replace '^.*::', ''
+ } else {
+ $Item.FullName
+ }
+
+ $parts = $path.Split([System.IO.Path]::DirectorySeparatorChar)
+ for ($i = 0; $i -lt $parts.Length - 1; $i++) {
+ if ($parts[$i] -in $skipDirs) {
+ return $true
+ }
+ }
+ return $false
+}
-if (Test-Path $lastCheckFile) {
- $lastCheckTime = Get-Content $lastCheckFile | Get-Date
- $timeDiff = ($currentTime - $lastCheckTime).TotalMinutes
+function Invoke-UnhideRecursive {
+ param([string]$Path)
+ Get-ChildItem -LiteralPath $Path -Force | ForEach-Object {
+ if ($_.PSIsContainer) {
+ if ($_.Name -in $skipDirs) {
+ if ($_.Attributes -band [System.IO.FileAttributes]::Hidden) {
+ Write-Host " -> unhiding skip directory (self only): `"$($_.FullName)`""
+ $_.Attributes = $_.Attributes -bxor [System.IO.FileAttributes]::Hidden
+ }
+ return
+ }
+ Invoke-UnhideRecursive $_.FullName
+ } else {
+ if ($_.Attributes -band [System.IO.FileAttributes]::Hidden) {
+ Write-Host " -> unhiding: `"$($_.FullName)`""
+ $_.Attributes = $_.Attributes -bxor [System.IO.FileAttributes]::Hidden
+ }
+ }
+ }
+}
- if ($timeDiff -lt $timeThreshold) {
- exit
+function Test-GitPathSkippable {
+ param([string]$GitPath)
+ $parts = $GitPath.Split(@('/', '\'))
+ for ($i = 0; $i -lt $parts.Length - 1; $i++) {
+ if ($parts[$i] -in $skipDirs) {
+ return $true
+ }
}
+ return $false
}
-$currentTime.ToString() | Out-File -FilePath $lastCheckFile -Force
+Write-Host "Step 1: Unhiding all files and directories (skipping $($skipDirs -join ', '))..."
-# Hide Files
+Invoke-UnhideRecursive -Path (Get-Location).Path
-Set-Location -Path (Join-Path $PSScriptRoot "..\..")
+Write-Host "Step 2: Hiding git-ignored items..."
-Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object {
- $_.FullName -notmatch '\\.temp\\' -and $_.FullName -notmatch '\\.git\\'
+git ls-files --others --ignored --exclude-standard | Where-Object {
+ -not (Test-GitPathSkippable $_)
} | ForEach-Object {
- attrib -h $_.FullName 2>&1 | Out-Null
+ $itemPath = $_
+ Write-Host "... checking: `"$itemPath`""
+ $item = Get-Item $_ -Force -ErrorAction SilentlyContinue
+ if (-not $item) { return }
+
+ if ($item.FullName -eq $selfPath) { return }
+
+ if (Test-InSkipDir $item) {
+ Write-Host " -> skipping (inside skip directory)"
+ return
+ }
+
+ if ($item.PSIsContainer) {
+ if (-not ($item.Attributes -band [System.IO.FileAttributes]::Hidden)) {
+ Write-Host " -> hiding directory (non-recursive)"
+ $item.Attributes = $item.Attributes -bor [System.IO.FileAttributes]::Hidden
+ }
+ } else {
+ if (-not ($item.Attributes -band [System.IO.FileAttributes]::Hidden)) {
+ Write-Host " -> hiding"
+ $item.Attributes = $item.Attributes -bor [System.IO.FileAttributes]::Hidden
+ }
+ }
}
-Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object {
- $_.Name -match '^\..*' -and $_.FullName -notmatch '\\\.\.$' -and $_.FullName -notmatch '\\\.$'
-} | ForEach-Object {
- attrib +h $_.FullName 2>&1 | Out-Null
+Write-Host "Step 3: Hiding dot-prefixed items..."
+Get-ChildItem -Path . -Force -Directory | Where-Object { $_.Name -match '^\.' } | ForEach-Object {
+ Write-Host "... checking: `"$($_.FullName)`""
+ if (Test-InSkipDir $_) {
+ Write-Host " -> skipping (inside skip directory)"
+ return
+ }
+ if (-not ($_.Attributes -band [System.IO.FileAttributes]::Hidden)) {
+ Write-Host " -> hiding directory"
+ $_.Attributes = $_.Attributes -bor [System.IO.FileAttributes]::Hidden
+ }
}
-if (Get-Command git -ErrorAction SilentlyContinue) {
- git status --ignored --short | ForEach-Object {
- if ($_ -match '^!!\s+(.+)$') {
- $ignoredPath = $matches[1]
- if ($ignoredPath -notmatch '\.lnk$' -and (Test-Path $ignoredPath)) {
- attrib +h $ignoredPath 2>&1 | Out-Null
- }
- }
+Get-ChildItem -Path . -Force -File | Where-Object { $_.Name -match '^\.' } | ForEach-Object {
+ if ($_.FullName -eq $selfPath) { return }
+ Write-Host "... checking: `"$($_.FullName)`""
+ if (Test-InSkipDir $_) {
+ Write-Host " -> skipping (inside skip directory)"
+ return
+ }
+ if (-not ($_.Attributes -band [System.IO.FileAttributes]::Hidden)) {
+ Write-Host " -> hiding file"
+ $_.Attributes = $_.Attributes -bor [System.IO.FileAttributes]::Hidden
}
}
diff --git a/.run/src/lib.rs b/.run/src/lib.rs
index d52c7fd..cff606a 100644
--- a/.run/src/lib.rs
+++ b/.run/src/lib.rs
@@ -334,6 +334,82 @@ pub fn run_cmd_with_progress(phase: &str, label: &str, cmd: String) -> Result<()
}
}
+/// Read `[package.metadata.docs.rs].features` from `mingling/Cargo.toml`.
+///
+/// Finds the git repository root, reads `mingling/Cargo.toml`, parses it as TOML,
+/// and extracts the feature list under `[package.metadata.docs.rs].features`.
+///
+/// # Errors
+///
+/// Returns `std::io::Error` if:
+/// - The git repository root cannot be found.
+/// - The manifest file cannot be read.
+/// - The TOML cannot be parsed.
+/// - The `[package.metadata.docs.rs].features` key is missing or empty.
+pub fn read_features() -> Result<Vec<String>, std::io::Error> {
+ // Find git repo root
+ let mut current_dir = std::env::current_dir()?;
+ let repo_root = loop {
+ let git_dir = current_dir.join(".git");
+ if git_dir.exists() && git_dir.is_dir() {
+ break Some(current_dir);
+ }
+ if !current_dir.pop() {
+ break None;
+ }
+ };
+ let repo_root = repo_root.ok_or_else(|| {
+ std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ "Failed to find git repository root",
+ )
+ })?;
+
+ let manifest_path = repo_root.join("mingling/Cargo.toml");
+ if !manifest_path.exists() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ format!("Manifest not found at {}", manifest_path.display()),
+ ));
+ }
+
+ let manifest_content = std::fs::read_to_string(&manifest_path)?;
+ let cargo_toml: toml::Value = manifest_content.parse().map_err(|e| {
+ std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ format!("Failed to parse Cargo.toml: {}", e),
+ )
+ })?;
+
+ let doc_features = cargo_toml
+ .get("package")
+ .and_then(|p| p.get("metadata"))
+ .and_then(|m| m.get("docs"))
+ .and_then(|d| d.get("rs"))
+ .and_then(|rs| rs.get("features"))
+ .and_then(|f| f.as_array())
+ .ok_or_else(|| {
+ std::io::Error::new(
+ std::io::ErrorKind::NotFound,
+ "[package.metadata.docs.rs] or its 'features' key not found in mingling/Cargo.toml",
+ )
+ })?;
+
+ let features: Vec<String> = doc_features
+ .iter()
+ .filter_map(|v| v.as_str().map(String::from))
+ .collect();
+
+ if features.is_empty() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "No features defined in [package.metadata.docs.rs]",
+ ));
+ }
+
+ Ok(features)
+}
+
#[must_use]
pub fn cargo_tomls() -> Vec<std::path::PathBuf> {
let mut cargo_tomls = Vec::new();
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 389a476..11145a1 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,8 +4,17 @@
"rust-analyzer.linkedProjects": [
".run/Cargo.toml",
"mingling_pathf/test/Cargo.toml",
+ "mingling_cli/Cargo.toml",
"arg_picker/Cargo.toml",
"arg_picker/test/Cargo.toml"
],
- "rust-analyzer.cargo.features": ["mingling_support"]
+ "rust-analyzer.cargo.features": [
+ "structural_renderer",
+ "mingling_support",
+ "all_serde_fmt",
+ "docs_rs",
+ "picker",
+ "parser",
+ "comp"
+ ]
}
diff --git a/.zed/settings.json b/.zed/settings.json
index 103cc28..3fa2cba 100644
--- a/.zed/settings.json
+++ b/.zed/settings.json
@@ -7,11 +7,20 @@
"linkedProjects": [
".run/Cargo.toml",
"mingling_pathf/test/Cargo.toml",
+ "mingling_cli/Cargo.toml",
"arg_picker/Cargo.toml",
"arg_picker/test/Cargo.toml"
],
"cargo": {
- "features": ["mingling_support"]
+ "features": [
+ "structural_renderer",
+ "mingling_support",
+ "all_serde_fmt",
+ "docs_rs",
+ "picker",
+ "parser",
+ "comp"
+ ]
}
}
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb0cc85..b90536e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -58,7 +58,36 @@ None
#### Optimizations:
-None
+1. **[`macros`]** Updated `route!` macro to use `Routable` trait instead of `Grouped` trait for error conversion, making the semantics clearer. The `route!` macro now calls `::mingling::Routable::to_chain(e)` on the error branch instead of `::mingling::Grouped::to_chain(e)`.
+
+ Additionally, `ChainProcess<ThisProgram>` now implements `Routable<ThisProgram>`, allowing `route!` to work with `Result<Ok, ChainProcess<ThisProgram>>` patterns — where the error side is already a `ChainProcess` value that should be routed directly:
+
+ When `ChainProcess` implements `Routable`, `to_chain()` re-routes the inner `AnyOutput` to the chain pipeline (preserving the existing `NextProcess::Chain`/`Renderer` flag), while `to_render()` re-routes it to the render pipeline. This enables seamless propagation of already-routed chain process values through the `route!` macro without double-wrapping.
+
+ The `Routable` trait is defined in `mingling_core::asset::routable` and provides unified routing capabilities (`to_chain` / `to_render`) for any type that can be dispatched into the program's pipeline. A blanket implementation is provided for all `T: Grouped<C> + Send`, ensuring backward compatibility — existing types that implement `Grouped` automatically implement `Routable`.
+
+2. **[`macros`]** Restructured the `mingling_macros` crate's internal module hierarchy. The previously flat module structure has been reorganized into a logical directory-based layout:
+
+ - **`attr/`** — Attribute macro implementations (e.g., `#[chain]`, `#[renderer]`, `#[help]`, `#[completion]`, `#[dispatcher_clap]`, `#[program_setup]`)
+ - **`derive/`** — Derive macro implementations (e.g., `#[derive(Grouped)]`, `#[derive(EnumTag)]`)
+ - **`func/`** — Function-like macro implementations (e.g., `pack!`, `group!`, `dispatcher!`, `suggest!`, `entry!`, `node!`, `gen_program!` and its sub-macros)
+ - **`systems/`** — Cross-cutting systems (e.g., resource injection, dispatch tree generation, structural data derive support)
+ - **`extensions/`** — Extension point mechanism for attribute macros (unchanged)
+ - **`utils.rs`** — Shared utility module for future common helpers
+
+ All public API items (`#[proc_macro]`, `#[proc_macro_derive]`, `#[proc_macro_attribute]`) remain at the crate root (`lib.rs`) with identical signatures. Internal function visibility has been tightened from `pub fn` to `pub(crate) fn` for all module-internal functions that were previously publicly accessible only within the crate.
+
+ _No migration is required for downstream code — all macros are re-exported with the same names and signatures as before._
+
+3. **[`macros`]** Refactored the code generation strategy for `#[chain]`, `#[renderer]`, `#[help]`, and `#[completion]` attribute macros. Instead of inlining the user's function body directly into the generated trait implementation, these macros now **preserve the original user function as a standalone item** and generate trait method implementations that **call the original function by name**, injecting resources from the application context.
+
+ This approach provides several benefits:
+
+ - **Better debugging and error messages** — The user's function exists as a named, callable item. Stack traces, profiler output, and error messages reference the user's function name (e.g., `handle_greet`) rather than an anonymous closure or inlined block, making debugging more intuitive.
+
+ - **Clearer macro expansion** — The generated code maintains a clear separation between the original user function and the trait glue code, reducing the cognitive load when inspecting macro-expanded output.
+
+ _No migration is required for downstream code — the behavior of all four macros is unchanged. This is purely an internal code generation refactoring._
#### Features:
@@ -141,24 +170,139 @@ None
}
```
+6. **[`macros:help`]** Removed the restriction that `#[help]` functions must return `::mingling::RenderResult`. The `#[help]` macro now accepts any return type (including no return type), and automatically converts the return value to `RenderResult` via `Into::into`.
+
+ - Functions returning `RenderResult` work as before.
+ - Functions returning other types (e.g., `String`, `i32`, `()`) are converted via `Into<RenderResult>`.
+ - Functions with no return type (`-> ()` or omitted) return `()` which is converted to an empty `RenderResult` via `From<()>`.
+
+ This makes `#[help]` consistent with the `#[renderer]` macro's ergonomic return type handling introduced in item 5 above.
+
+ ```rust
+ #[help]
+ fn help_greeting(prev: EntryGreeting) -> String {
+ format!("Displaying help for greeting: {}", *prev)
+ }
+
+ #[help]
+ fn help_void(prev: EntryVoid) {
+ // side effects only, returns empty RenderResult
+ }
+ ```
+
+7. **[`setups`]** Refactored `BasicProgramSetup`, `HelpFlagSetup`, `QuietFlagSetup`, `ConfirmFlagSetup`, `StructuralRendererSetup`, and `StructuralRendererSimpleSetup` into the `picker` subsystem under `mingling::setups::picker`. These setups now use the `arg_picker` (`picker`) chained argument parsing API internally instead of directly manipulating global arguments.
+
+ - The `BasicProgramSetup`, `HelpFlagSetup`, `QuietFlagSetup`, and `ConfirmFlagSetup` structs now use `PickerArg<Flag>` and chained `.pick()` calls to detect flags from the argument list, replacing the previous `global_argument`-based approach.
+ - The `StructuralRendererSetup` struct now uses `PickerArg<Flag>` constants (e.g., `JSON_FLAG`, `YAML_FLAG`) and chained `.pick()` calls to detect format-specifying flags, replacing the previous `global_argument` approach.
+ - The `StructuralRendererSimpleSetup` struct still uses the legacy `global_argument("--renderer", ...)` approach, preserving backward compatibility with the `--renderer <FORMAT>` syntax.
+ - New `PickerArg<Flag>` constants have been added in `mingling::setups::picker::consts`: `HELP_FLAG`, `QUIET_FLAG`, `CONFIRM_FLAG`, `JSON_FLAG`, `JSON_PRETTY_FLAG`, `YAML_FLAG`, `TOML_FLAG`, `RON_FLAG`, and `RON_PRETTY_FLAG`. The format-specific flags are feature-gated behind their respective `json_serde_fmt`, `yaml_serde_fmt`, `toml_serde_fmt`, and `ron_serde_fmt` features.
+ - The module structure is:
+ - `mingling::setups::picker` — re-exports all picker-based setup types
+ - `mingling::setups::picker::basic` — `BasicProgramSetup`, `HelpFlagSetup`, `QuietFlagSetup`, `ConfirmFlagSetup`
+ - `mingling::setups::picker::consts` — reusable `PickerArg<Flag>` constants
+ - `mingling::setups::picker::structural_renderer` — `StructuralRendererSetup`, `StructuralRendererSimpleSetup`
+ - All setup types remain available from `mingling::setups::*` as before — this is purely an internal refactoring; no public API surface changes.
+
+ The `picker` feature must be enabled for these refactored setups to be available. When the feature is disabled, the original implementations (using `global_argument`) remain in effect.
+
+8. **[`core`]** Added `get_args_mut()`, `take_args()`, and `replace_args()` methods to `Program` for more flexible argument manipulation:
+
+ - **`get_args_mut(&mut self) -> &mut [String]`** — Returns a mutable reference to the program's command-line arguments, allowing in-place modification of individual arguments.
+ - **`take_args(&mut self) -> Vec<String>`** — Takes ownership of the program's command-line arguments, replacing them with an empty `Vec`. Useful for transferring arguments to another context or processing them with ownership.
+ - **`replace_args(&mut self, args: Vec<String>) -> Vec<String>`** — Replaces the program's command-line arguments with a new set and returns the old ones. Enables swapping argument sets during program execution.
+
+ These methods complement the existing read-only `get_args(&self)` method, providing full control over argument mutation and ownership.
+
+9. **[`macros:chain`]** Relaxed the `#[chain]` return type validation. Previously, `#[chain]` functions were restricted to returning `Next`, `ChainProcess<ThisProgram>`, `()`, or omitting the return type. Now, any return type is accepted, and the generated `proc` function performs an explicit `Into<ChainProcess<ThisProgram>>` conversion using a fully-qualified turbofish based on the user-declared return type.
+
+ This means `#[chain]` functions can now return any pack type directly, without needing an explicit `.into()` call in the function body:
+
+ ```rust
+ // Before — required explicit .into()
+ #[chain]
+ fn handle_greet(args: EntryGreet) -> Next {
+ let name = /* ... */;
+ ResultGreeting::new(name).into()
+ }
+
+ // After — return any pack type directly
+ #[chain]
+ fn handle_greet(args: EntryGreet) -> ResultGreeting {
+ let name = /* ... */;
+ ResultGreeting::new(name)
+ }
+ ```
+
+ The generated `proc` function now wraps the body result in `<UserReturnType as Into<ChainProcess<ThisProgram>>>::into(...)`, which:
+ - Works for `Next` / `ChainProcess` via the identity `From<T> for T` implementation.
+ - Works for any pack type (`ResultGreeting`, etc.) via the `.into()` conversion generated by `pack!` / `#[derive(Grouped)]`.
+ - Works for `()` via the `From<()>` implementation on `ChainProcess`.
+
+ The return type validation has been removed entirely — any valid Rust return type is accepted. If the type does not implement `Into<ChainProcess<ThisProgram>>`, a standard Rust compilation error will be produced at the call site.
+
+10. **[`macros`]** **[`extensions`]** Added the `extensions` module to `mingling_macros`, providing an extension point mechanism for attribute macros (`#[chain]`, `#[renderer]`, `#[help]`, `#[completion]`). The extension system allows identifiers in the attribute argument to be extracted and processed before the main macro logic runs.
+
+ Each attribute macro now attempts to redispatch through `extensions::try_redispatch_simple()` (or `try_redispatch_completion` for `#[completion]`) before executing its standard logic. If extension identifiers are detected, the call is re-routed so that extensions are applied via additional `#[...]` attributes stacked on top of the inner core attribute. New extensions can be added without modifying the attribute macros themselves — only the `extensions` module needs to be updated to register new identifiers.
+
+ This system is designed for future extensibility: as new cross-cutting concerns (e.g., logging, metrics, validation) are identified, they can be added as simple extension identifiers without touching the core macro logic.
+
+11. **[`extensions`]** **[`macros`]** Added the `#[routeify]` extension attribute macro that transforms `expr?` into `route!(expr)`, enabling concise error routing in chain functions using the `?` operator syntax.
+
+ The `#[routeify]` macro can be used:
+ - **Standalone** — as a direct attribute: `#[routeify] fn handle(...) { ... }`
+ - **As an extension** — via the extension point system: `#[chain(routeify)] fn handle(...) { ... }`
+
+ When used as a `#[chain]` extension, the `routeify` identifier is detected by the extension point mechanism, stripped from the `#[chain]` attribute arguments, and `#[routeify]` is applied as an outer attribute on top of `#[chain]`. The re-dispatch token stream now correctly generates `#[#exts]*` (i.e., `#[routeify] #[chain]`) instead of the previous bare `#exts` — fixing a bug where extension identifiers were emitted without the `#[...]` attribute delimiter, producing invalid token streams.
+
+ ```rust
+ use mingling::macros::routeify;
+
+ #[chain(routeify)]
+ fn handle_calc(args: EntryCalculate) -> Next {
+ let a = args.pick(&arg![f32]).to_result()?;
+ let op = args.pick(&arg![Operator]).to_result()?;
+ StateCalculate { number_a: a, operator: op, ... }.to_chain()
+ }
+ ```
+
+ The `#[routeify]` macro is feature-gated behind `extra_macros` and re-exported as `mingling::macros::routeify`.
+
+ Internal changes:
+ - Added `mingling_macros/src/extensions/routeify.rs` with `routeify_impl` implementation.
+ - Updated `try_redispatch_simple` and `try_redispatch_completion` to emit `#[#exts]` instead of bare `#exts`, ensuring proper attribute syntax in the re-dispatched token stream.
+ - Registered `#[proc_macro_attribute] pub fn routeify` in `mingling_macros/src/lib.rs`.
+
#### **BREAKING CHANGES** (API CHANGES):
-1. **[`macros:renderer`]** **[`macros:help`]** Removed `r_println!` and `r_print!` macros. The `#[renderer]` and `#[help]` macros no longer implicitly inject an internal `RenderResult` variable or provide `r_println!` / `r_print!` macros.
+1. **[`macros:renderer`]** **[`macros:help`]** Removed `r_println!` and `r_print!` macros from being implicitly injected by `#[renderer]` and `#[help]` macros. These macros still exist, but must now be used **explicitly** — either with an explicit buffer argument, or via the `#[buffer]` extension attribute that re-enables implicit buffer injection.
- Renderers and help functions must now explicitly create and return a `RenderResult`:
+ **Option A — Explicit buffer:** Pass a `RenderResult` variable as the first argument:
```rust
+ use mingling::macros::r_println;
use mingling::prelude::*;
#[renderer]
fn render_greeting(greeting: ResultGreeting) -> RenderResult {
let mut result = RenderResult::new();
- writeln!(result, "Hello, {}!", *greeting).ok();
+ r_println!(result, "Hello, {}!", *greeting);
result
}
```
- All examples, docs, and test cases across the repository have been updated to use the new pattern: creating a `RenderResult` with `RenderResult::new()` or `RenderResult::default()`, writing with `write!`/`writeln!` from `std::io::Write`, and returning the result.
+ **Option B — Implicit buffer via `#[buffer]` extension:** Use `#[renderer(buffer)]` to re-enable the old implicit injection behavior, where a `RenderResult` buffer is automatically created and `r_println!`/`r_print!` write to it without an explicit argument. This requires the function to return `()` (unit); the expanded function will return `RenderResult`:
+
+ ```rust
+ use mingling::macros::r_println;
+
+ #[renderer(buffer)]
+ fn render_greeting(greeting: ResultGreeting) {
+ r_println!("Hello, {}!", *greeting);
+ // Returns RenderResult implicitly
+ }
+ ```
+
+ The `#[buffer]` extension is also available standalone as `#[buffer]` for use outside of `#[renderer]`/`#[help]` functions.
2. **[`macros:chain`]** The `#[chain]` macro's return type requirement has been relaxed. Previously, chain functions were required to return `Next` or `()` (with `()` auto-converting to `ResultEmpty`). Now, chain functions can also return `ChainProcess<ThisProgram>` directly, or omit the return type entirely (which defaults to `()` → `ResultEmpty`).
@@ -191,6 +335,38 @@ None
All examples, docs, and test cases across the repository have been updated to use `.into()` where packed types are returned from chain functions.
+3. **[`core`]** **[`ExitCodeSetup`]** Updated `ExitCodeSetup` to only override the exit code when `ResExitCode` has been modified (i.e., `exit_code != 0`). Previously, it unconditionally overrode the exit code, which could interfere with exit codes set by other hooks or the program's default exit flow. The `on_finish` hook now returns `ProgramControlUnit::OverrideExitCode(...)` only when the exit code is non-zero, and `ProgramControls::Empty` otherwise. The import of `ProgramControls` has been added accordingly.
+
+4. **[`core`]** **[`macros`]** Renamed `Groupped` (typo) to `Grouped`. All references to the trait, derive macro, module files, and related types have been corrected throughout the codebase:
+
+ - Trait: `Groupped<Group>` → `Grouped<Group>`
+ - Derive macro: `#[derive(Groupped)]` → `#[derive(Grouped)]`
+ - Serialize variant: `GrouppedSerialize` → `GroupedSerialize`
+ - Source files: `groupped.rs` → `grouped.rs`
+ - Pattern matcher: `GrouppedDerivePattern` → `GroupedDerivePattern`
+ - All `use` imports, type annotations, and trait bound references updated accordingly.
+
+ This is a pure rename — no behavioral changes. All functionality remains identical. Downstream code using the old `Groupped` name must migrate to `Grouped`.
+
+5. **[`core`]** **[`macros`]** Removed `to_chain()` and `to_render()` default methods from the `Grouped` trait. These methods are now exclusively provided by the `Routable` trait. All code that previously called `to_chain()` or `to_render()` via `Grouped` must now call them via `Routable`:
+
+ ```rust
+ // Before (via Grouped — removed)
+ use mingling::Grouped;
+ my_value.to_chain();
+
+ // After (via Routable)
+ use mingling::Routable;
+ my_value.to_chain();
+ ```
+
+ - The `Routable` trait is re-exported in `mingling::prelude` alongside `Grouped`.
+ - The blanket implementation `impl<T: Grouped<C> + Send> Routable<C> for T` remains, so all types that implement `Grouped` still have `to_chain()` and `to_render()` — they just need to import `Routable` instead of relying on `Grouped` for those methods.
+ - Internal macro-generated code (in `#[chain]`, `#[renderer]`, `#[dispatcher_clap]`, `gen_program!`, `empty_result!`, etc.) has been updated to reference `::mingling::Routable::<C>::to_chain(...)` / `::mingling::Routable::<C>::to_render(...)` instead of `::mingling::Grouped::<C>::to_chain(...)` / `::mingling::Grouped::<C>::to_render(...)`.
+ - Downstream crates using `mingling` macros are automatically migrated — the macro output now references `Routable`. Only manual `.to_chain()` / `.to_render()` calls in user code need updating (add `use mingling::Routable;`).
+
+ _No behavioral changes — this is a pure API migration from `Grouped` to `Routable` for routing methods._
+
---
## Release 0.2.2 (2026-07-10)
diff --git a/Cargo.lock b/Cargo.lock
index 4c0682e..5db81a8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -12,15 +12,6 @@ dependencies = [
]
[[package]]
-name = "android_system_properties"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
-dependencies = [
- "libc",
-]
-
-[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -76,7 +67,6 @@ version = "0.1.0"
dependencies = [
"arg-picker-macros",
"just_fmt 0.2.0",
- "mingling_core",
]
[[package]]
@@ -89,12 +79,6 @@ dependencies = [
]
[[package]]
-name = "autocfg"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
-
-[[package]]
name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -104,89 +88,24 @@ dependencies = [
]
[[package]]
-name = "bumpalo"
-version = "3.20.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
-
-[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
-name = "cc"
-version = "1.2.63"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
-dependencies = [
- "find-msvc-tools",
- "shlex",
-]
-
-[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
-name = "chrono"
-version = "0.4.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
-dependencies = [
- "iana-time-zone",
- "js-sys",
- "num-traits",
- "wasm-bindgen",
- "windows-link",
-]
-
-[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
-name = "colored"
-version = "3.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
-dependencies = [
- "windows-sys",
-]
-
-[[package]]
-name = "core-foundation-sys"
-version = "0.8.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
-
-[[package]]
-name = "dirs"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
-dependencies = [
- "dirs-sys",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
-dependencies = [
- "libc",
- "option-ext",
- "redox_users",
- "windows-sys",
-]
-
-[[package]]
name = "env_filter"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -226,77 +145,12 @@ dependencies = [
]
[[package]]
-name = "find-msvc-tools"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
-
-[[package]]
-name = "futures-core"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
-
-[[package]]
-name = "futures-task"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
-
-[[package]]
-name = "futures-util"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
-dependencies = [
- "futures-core",
- "futures-task",
- "pin-project-lite",
- "slab",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi",
-]
-
-[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
-name = "iana-time-zone"
-version = "0.1.65"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
-dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "log",
- "wasm-bindgen",
- "windows-core",
-]
-
-[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
-dependencies = [
- "cc",
-]
-
-[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -343,18 +197,6 @@ dependencies = [
]
[[package]]
-name = "js-sys"
-version = "0.3.99"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
-dependencies = [
- "cfg-if",
- "futures-util",
- "once_cell",
- "wasm-bindgen",
-]
-
-[[package]]
name = "just_fmt"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -394,15 +236,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
-name = "libredox"
-version = "0.1.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
-dependencies = [
- "libc",
-]
-
-[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -437,20 +270,6 @@ dependencies = [
]
[[package]]
-name = "mingling-cli"
-version = "0.3.0"
-dependencies = [
- "chrono",
- "colored",
- "dirs",
- "just_fmt 0.1.2",
- "mingling",
- "serde",
- "serde_json",
- "toml",
-]
-
-[[package]]
name = "mingling_core"
version = "0.3.0"
dependencies = [
@@ -497,15 +316,6 @@ dependencies = [
]
[[package]]
-name = "num-traits"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -518,12 +328,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
-name = "option-ext"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
-
-[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -595,17 +399,6 @@ dependencies = [
]
[[package]]
-name = "redox_users"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
-dependencies = [
- "getrandom",
- "libredox",
- "thiserror",
-]
-
-[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -649,12 +442,6 @@ dependencies = [
]
[[package]]
-name = "rustversion"
-version = "1.0.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
-
-[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -732,12 +519,6 @@ dependencies = [
]
[[package]]
-name = "shlex"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
-
-[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -754,12 +535,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
[[package]]
-name = "slab"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
-
-[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -787,26 +562,6 @@ dependencies = [
]
[[package]]
-name = "thiserror"
-version = "2.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "2.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
name = "tokio"
version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -904,110 +659,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
-name = "wasm-bindgen"
-version = "0.2.122"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
-dependencies = [
- "cfg-if",
- "once_cell",
- "rustversion",
- "wasm-bindgen-macro",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.122"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
-dependencies = [
- "quote",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.122"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
-dependencies = [
- "bumpalo",
- "proc-macro2",
- "quote",
- "syn",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.122"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "windows-core"
-version = "0.62.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
-dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-link",
- "windows-result",
- "windows-strings",
-]
-
-[[package]]
-name = "windows-implement"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "windows-interface"
-version = "0.59.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
-name = "windows-result"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-strings"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 99523c7..8baadbb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,7 +14,7 @@ members = [
"arg_picker_macros",
# Scaffolding tool
- "mling"
+ # "mling" (deprecated)
]
exclude = [
diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md
new file mode 100644
index 0000000..1aad8d5
--- /dev/null
+++ b/GETTING_STARTED.md
@@ -0,0 +1,732 @@
+# Writing with Mingling
+
+## The Big Picture
+
+Mingling organizes your CLI program into three distinct phases:
+
+```
+Input → [Dispatcher] → Entry → [Chain(s)] → Result → [Renderer] → Output
+```
+
+**Step 1: Input** — The user's raw arguments flow in.
+**Step 2: Dispatch** — The **Dispatcher** receives them and wraps them into an **Entry** type.
+**Step 3: Chain** — The Entry is handed to a **Chain** function for processing.
+**Step 4: Render** — The **Renderer** receives the result and writes it to the terminal.
+
+> [!NOTE]
+> A Chain can produce a **State** type, passed to the next Chain for further processing,
+>
+> or it can produce a **Result** type, handed to the Renderer for output.
+
+Every step in this pipeline is a **pure function** annotated with an attribute macro, and the framework recognizes them as long as they meet the requirements.
+
+---
+
+## 1. Defining Commands — `dispatcher!`
+
+The entry point for every subcommand is the `dispatcher!` macro. It generates two structs for you: a **Dispatcher** (used to register the command with the program) and an **Entry** (a wrapper around `Vec<String>` that holds the raw arguments).
+
+```rust
+use mingling::prelude::*;
+
+// command.name Dispatcher EntryType
+// │ │ │
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+// Nested subcommand: `remote add`
+dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd);
+```
+
+Then in `main()`, register the dispatcher with the program:
+
+```rust
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+Mingling also supports an abbreviated form (with the `extra_macros` feature):
+
+```rust
+// Features: ["extra_macros"]
+
+// Auto-generates CMDGreet / EntryGreet from "greet"
+dispatcher!("greet");
+```
+
+---
+
+## 2. The Chain — "#[chain]" — Where Logic Lives
+
+The `#[chain]` attribute turns a plain function into an execution step. Think of it as "the logic that transforms one typed value into another."
+
+```rust
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+pack!(ResultGreeting = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let greeting = args
+ .inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+ ResultGreeting::new(greeting).into()
+}
+```
+
+Key points:
+
+- The return type is `Next` — a type alias for `ChainProcess<ThisProgram>`.
+- You chain results by calling `.to_chain()` on any `pack!`-ed type.
+- You can have **multiple chain functions** for the same command, each transforming the data further.
+- With the `async` feature, chain functions can be `async fn`.
+
+---
+
+## 3. The Renderer — "#[renderer]" — How Output Works
+
+The `#[renderer]` attribute turns a function into an output handler. It receives the final result of a chain and returns a `RenderResult`.
+
+```rust
+use mingling::macros::pack;
+use mingling::prelude::*;
+use std::io::Write;
+
+pack!(ResultGreeting = String);
+
+#[renderer]
+fn render_greeting(greeting: ResultGreeting) -> RenderResult {
+ let mut result = RenderResult::new();
+ writeln!(result, "Hello, {}!", *greeting).ok();
+ result
+}
+```
+
+Inside a renderer, create a `RenderResult`, write to it using `write!` / `writeln!` (from [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html)), and return it. The output is captured in the buffer and flushed by the framework at the end of the pipeline.
+
+You can write renderers for **any type** in your program, including error types:
+
+```rust
+use mingling::prelude::*;
+use std::io::Write;
+
+#[renderer]
+fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult {
+ let mut result = RenderResult::new();
+ writeln!(result, "Command not found: [{}]", err.join(" ")).ok();
+ result
+}
+```
+
+---
+
+## 4. Argument Parsing — Picker
+
+Mingling provides a **Picker** for argument extraction. You can use `pick()` or `pick_or()` on an entry to tag typed values, then parse them into a tuple type all at once.
+
+```rust
+// Features: ["picker"]
+dispatcher!("greet", CMDGreet => EntryGreet);
+pack!(ResultGreeting = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let (name, count) = args
+ .pick(&arg![String]) // positional argument: first string
+ .pick_or(&arg![repeat: u8, 'r'], || 1) // optional flag with default value
+ .unwrap();
+ ResultGreeting::new(format!("{} x{}", name, count)).into()
+}
+```
+
+For more details on the Picker, see [arg-picker](https://github.com/mingling-rs/mingling/tree/main/arg_picker)
+
+---
+
+## 5. The Help System — "#[help]"
+
+Help is just another attribute macro. When the user passes `--help` or `-h`, the program skips the normal chain/render pipeline and routes directly to your `#[help]` function.
+
+Enable it by adding `BasicProgramSetup`:
+
+```rust
+use mingling::{macros::help, prelude::*, setup::BasicProgramSetup};
+use std::io::Write;
+
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+#[help]
+fn help_greet(_prev: EntryGreet) -> RenderResult {
+ let mut result = RenderResult::new();
+ writeln!(result, "Usage: greet <NAME>").ok();
+ writeln!(result, "Greets the user with the given name.").ok();
+ result
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup); // enables --help / -h
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+gen_program!();
+```
+
+The flow is:
+
+- User types `greet --help`
+- `BasicProgramSetup` sets `program.user_context.help = true`
+- The dispatcher sees this flag and routes to the `#[help]` function instead of the `#[chain]`
+
+---
+
+## 6. Completion — "#[completion]" — Dynamic Shell Completions
+
+With the `comp` feature, Mingling provides a fully dynamic completion system. You write a function that returns `Suggest` based on the current shell context, and Mingling generates the completion scripts for bash, zsh, fish, and pwsh.
+
+```rust
+// Features: ["comp", "extra_macros"]
+
+use mingling::{macros::suggest, ShellContext, Suggest};
+
+dispatcher!("greet", CMDGreet => EntryGreet);
+pack!(ResultName = (u8, String));
+
+#[completion(EntryGreet)]
+fn complete_greet(ctx: &ShellContext) -> Suggest {
+ // Suggest positional arguments
+ if ctx.previous_word == "greet" {
+ return suggest! {
+ "Alice": "Likes to receive messages",
+ "Bob": "Likes to pass messages",
+ "World"
+ };
+ }
+
+ // Suggest flag arguments
+ if ctx.typing_argument() {
+ return suggest! {
+ "-r": "Number of repetitions",
+ "--repeat": "Number of repetitions",
+ }
+ .strip_typed_argument(ctx);
+ }
+
+ suggest!() // no suggestions
+}
+```
+
+You also need to register the built-in completion dispatcher:
+
+```rust
+// Features: ["comp"]
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(crate::CMDCompletion);
+ program.exec_and_exit();
+}
+```
+
+In your `build.rs`, generate the shell scripts:
+
+```rust
+// BUILD TIME
+// Features: ["comp", "builds"]
+mingling::build::build_comp_scripts(env!("CARGO_PKG_NAME")).unwrap();
+```
+
+For enum-based completions, use `suggest_enum!`:
+
+```rust
+// Features: ["comp", "extra_macros"]
+
+use mingling::{ShellContext, Suggest};
+use mingling::macros::suggest_enum;
+use mingling::EnumTag;
+
+dispatcher!("lang.select", CMDLang => EntryLang);
+
+#[derive(EnumTag)]
+pub enum ProgrammingLanguages {
+ Rust,
+ Python,
+ JavaScript,
+}
+
+#[completion(EntryLang)]
+fn complete_lang(_: &ShellContext) -> Suggest {
+ suggest_enum!(ProgrammingLanguages)
+}
+```
+
+---
+
+## 7. Error Handling
+
+Mingling doesn't use `?` operator propagation. Instead, errors are just **alternative results** that flow through the same chain/render pipeline. Create error types with `pack!` and route to them with `.to_render()`:
+
+```rust
+use mingling::macros::pack;
+use mingling::prelude::*;
+use std::io::Write;
+
+dispatcher!("hello", CMDHello => EntryHello);
+pack!(ResultName = String);
+pack!(ErrorNoNameProvided = ());
+pack!(ErrorNameTooLong = u16);
+
+#[chain]
+fn handle(args: EntryHello) -> Next {
+ let Some(name) = args.inner.first().cloned() else {
+ return ErrorNoNameProvided::default().to_render(); // ← early return to error renderer
+ };
+
+ if name.len() > 10 {
+ return ErrorNameTooLong::new(name.len() as u16).to_render();
+ }
+
+ ResultName::new(name).to_render() // ← success path
+}
+
+#[renderer]
+fn render_no_name(_: ErrorNoNameProvided) -> RenderResult {
+ let mut result = RenderResult::new();
+ writeln!(result, "No name provided").ok();
+ result
+}
+
+#[renderer]
+fn render_too_long(len: ErrorNameTooLong) -> RenderResult {
+ let mut result = RenderResult::new();
+ writeln!(result, "Name too long: {} > 10", *len).ok();
+ result
+}
+```
+
+Two built-in fallback types are always available:
+
+- `ErrorDispatcherNotFound` — rendered when no dispatcher matches the input
+- `ErrorRendererNotFound` — rendered when no renderer is found for a result type
+
+---
+
+## 8. Resource Injection
+
+Chain and renderer functions can accept **additional parameters** for the program's global state. Resources are singleton values registered with `program.with_resource(...)`.
+
+```rust
+// Features: ["picker", "extra_macros"]
+
+use std::path::PathBuf;
+
+dispatcher!("current", CMDCurrent => EntryCurrent);
+dispatcher!("cd", CMDCd => EntryCd);
+
+#[derive(Default, Clone)]
+struct ResCurrentDir {
+ current_dir: PathBuf,
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_resource(ResCurrentDir {
+ current_dir: std::env::current_dir().unwrap(),
+ });
+ program.with_dispatcher(CMDCurrent);
+ program.with_dispatcher(CMDCd);
+ program.exec_and_exit();
+}
+
+// Read-only access (shared reference):
+#[chain]
+fn show_current(_prev: EntryCurrent, current_dir: &ResCurrentDir) -> Next {
+ println!("Current: {}", current_dir.current_dir.display());
+ empty_result!()
+}
+
+// Mutable access:
+#[chain]
+fn change_dir(prev: EntryCd, current_dir: &mut ResCurrentDir) -> Next {
+ let path = prev.pick_or_default(&arg![String]).unwrap();
+ current_dir.current_dir = current_dir.current_dir.join(path);
+ empty_result!()
+}
+```
+
+Resources can also be injected into `#[renderer]`:
+
+```rust
+use mingling::prelude::*;
+use std::io::Write;
+
+dispatcher!("current", CMDCurrent => EntryCurrent);
+
+#[derive(Default, Clone)]
+struct ResCurrentDir {
+ current_dir: std::path::PathBuf,
+}
+
+#[renderer]
+fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) -> RenderResult {
+ let mut result = RenderResult::new();
+ writeln!(result, "Current directory: {}", current_dir.current_dir.display()).ok();
+ result
+}
+```
+
+---
+
+## 9. Dispatch Tree — Compile-Time Command Trie
+
+As your program grows to dozens or hundreds of subcommands, linear dispatcher lookup becomes slow. Enable the `dispatch_tree` feature to convert the command structure into a **prefix tree (Trie)** at compile time.
+
+```rust
+// Features: ["dispatch_tree"]
+
+dispatcher!("cmd1", CMD1 => Entry1);
+dispatcher!("cmd2.sub1", CMD2Sub1 => Entry2Sub1);
+dispatcher!("cmd2.sub2", CMD2Sub2 => Entry2Sub2);
+dispatcher!("cmd3.sub1.leaf1", CMD3Sub1Leaf1 => Entry3Sub1Leaf1);
+dispatcher!("cmd3.sub1.leaf2", CMD3Sub1Leaf2 => Entry3Sub1Leaf2);
+// ... dozens more
+
+fn main() {
+ let program = ThisProgram::new();
+ // No more with_dispatcher calls — it's all compile-time!
+ program.exec_and_exit();
+}
+```
+
+With `dispatch_tree` enabled:
+
+- Dispatchers are auto-collected at compile time
+- `Program` no longer stores a dispatcher list
+- `program.with_dispatcher(...)` is not compiled
+- Lookup is **O(n)** where _n_ is input length, not number of commands
+
+---
+
+## 10. Clap Binding — Using Clap's Parser
+
+If you prefer clap's powerful argument parsing, use `#[dispatcher_clap]`. It generates a dispatcher from a `clap::Parser` struct.
+
+```rust
+// Features: ["clap"]
+// Dependencies:
+// clap = "4"
+
+use mingling::macros::dispatcher_clap;
+use mingling::prelude::*;
+use std::io::Write;
+
+#[derive(Default, clap::Parser, Grouped)]
+#[dispatcher_clap(
+ "greet", CMDGreet,
+ help = true, // auto-generate #[help] from clap
+ error = ErrorGreetParsed, // capture parse errors as a renderable type
+)]
+pub struct EntryGreet {
+ #[clap(default_value = "World")]
+ name: String,
+
+ #[arg(short, long, default_value_t = 1)]
+ repeat: i32,
+}
+
+#[renderer]
+fn render_greet(greet: EntryGreet) -> RenderResult {
+ let mut result = RenderResult::new();
+ write!(result, "Hello, ").ok();
+ for _ in 0..greet.repeat { write!(result, "{}", greet.name).ok(); }
+ writeln!(result, "!").ok();
+ result
+}
+
+#[renderer]
+fn render_parse_error(err: ErrorGreetParsed) -> RenderResult {
+ let mut result = RenderResult::new();
+ writeln!(result, "{}", *err).ok();
+ result
+}
+```
+
+You can control how clap help is displayed:
+
+```rust
+// Features: ["clap"]
+
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.stdout_setting.clap_help_print_behaviour =
+ mingling::ClapHelpPrintBehaviour::WriteToRenderResult;
+ // or: PrintDirectly — writes clap help straight to stdout
+ program.exec_and_exit();
+}
+```
+
+---
+
+## 11. REPL Mode
+
+With the `repl` feature, turn your CLI into an interactive shell with one method call:
+
+```rust
+// Features: ["repl"]
+
+fn main() {
+ ThisProgram::new().exec_repl();
+}
+```
+
+Mingling provides built-in REPL setups:
+
+```rust
+// Features: ["repl", "extra_macros"]
+
+use mingling::{
+ res::ResREPL,
+ setup::{BasicREPLReadlineSetup, BasicREPLOutputSetup, BasicREPLPromptSetup},
+};
+
+dispatcher!("cd", CMDCd => EntryCd);
+dispatcher!("exit", CMDExit => EntryExit);
+
+fn main() {
+ let mut program = ThisProgram::new();
+
+ program.with_dispatcher(CMDCd);
+ program.with_dispatcher(CMDExit);
+
+ // Enable line reading from stdin
+ program.with_setup(BasicREPLReadlineSetup);
+
+ // Enable output flushing after each render
+ program.with_setup(BasicREPLOutputSetup);
+
+ // Custom prompt
+ program.with_setup(BasicREPLPromptSetup::func(|| "> ".to_string()));
+
+ program.exec_repl(); // ← interactive loop
+}
+
+// Exit the REPL via the ResREPL resource:
+#[chain]
+fn handle_exit(_prev: EntryExit, repl: &mut ResREPL) {
+ repl.exit = true;
+}
+```
+
+---
+
+## 12. Hooks — Observing the Pipeline
+
+Mingling provides a `ProgramHook` system for observing every stage of the execution pipeline. Useful for debugging, logging, or telemetry.
+
+```rust
+use mingling::{
+ hook::{ProgramControlUnit, ProgramHook},
+};
+
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+fn main() {
+ let mut program = ThisProgram::new();
+
+ program.with_hook(
+ ProgramHook::<ThisProgram>::empty()
+ .on_begin::<_, ()>(|_| println!("[DEBUG] Program is begin"))
+ .on_pre_dispatch(|info| println!("[DEBUG] Pre dispatch: {}", info.arguments.join(" ")))
+ .on_post_dispatch(|info| println!("[DEBUG] Post dispatch: {}", info.entry))
+ .on_pre_chain(|info| {
+ println!("[DEBUG] Pre chain: {}", info.input);
+ })
+ .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id))
+ .on_finish(|_| {
+ println!("[DEBUG] Loop end");
+ ProgramControlUnit::OverrideExitCode(0) // Override exit code
+ })
+ .on_pre_render(|info| println!("[DEBUG] Pre render: {}", info.input))
+ .on_post_render(|_| println!("[DEBUG] Post render")),
+ );
+
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+---
+
+## 13. Structural Renderer — Structured Output (JSON/YAML)
+
+With the `structural_renderer` feature, users can add `--json` or `--yaml` flags to get structured output instead of human-readable text.
+
+```rust
+// Features: ["structural_renderer", "picker"]
+// Dependencies:
+// serde = "1"
+
+use mingling::prelude::*;
+use mingling::macros::buffer;
+use mingling::setup::picker::StructuralRendererSetup;
+use mingling::Grouped;
+use mingling::StructuralData;
+use serde::Serialize;
+use std::io::Write;
+
+dispatcher!("render", CMDRender => EntryRender);
+
+#[derive(Default, StructuralData, Serialize, Grouped)]
+struct ResultInfo {
+ name: String,
+ age: i32,
+}
+
+#[chain]
+fn render_info(args: EntryRender) -> Next {
+ let (name, age) = args
+ .pick_or_default(&arg![String])
+ .pick_or_default(&arg![i32])
+ .unwrap();
+ ResultInfo { name, age }.to_chain()
+}
+
+#[renderer(buffer)]
+fn render_info_result(info: ResultInfo) {
+ r_println!("{} is {} years old", info.name, info.age);
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(StructuralRendererSetup); // enables --json / --yaml
+ program.with_dispatcher(CMDRender);
+ let _ = program.exec();
+}
+```
+
+Then users can do:
+
+```bash
+$ myapp render Bob 22
+Bob is 22 years old
+
+$ myapp render Bob 22 --json
+{"name":"Bob","age":22}
+
+$ myapp render Bob 22 --yaml
+name: Bob
+age: 22
+```
+
+---
+
+## 14. Async Support
+
+Enable the `async` feature to use `async fn` inside `#[chain]`:
+
+```rust
+// Features: ["async", "picker"]
+// Dependencies:
+// tokio = { version = "1", features = ["full"] }
+
+use std::io::Write;
+use std::time::Duration;
+
+dispatcher!("download", CMDDownload => EntryDownload);
+pack!(ResultDownloaded = String);
+
+#[chain]
+pub async fn handle_download(args: EntryDownload) -> Next {
+ let file = args.pick_or_default(&arg![String]).unwrap();
+ download_file(file).await.into()
+}
+
+async fn download_file(name: String) -> ResultDownloaded {
+ tokio::time::sleep(Duration::from_secs(1)).await;
+ ResultDownloaded::new(name)
+}
+
+#[renderer]
+fn render_downloaded(result: ResultDownloaded) -> RenderResult {
+ let mut r = RenderResult::new();
+ writeln!(r, "\"{}\" downloaded.", *result).ok();
+ r
+}
+```
+
+> [!NOTE]
+>
+> `#[renderer]` functions cannot be async. When `async` is enabled, `program.exec_and_exit().await` returns a Future.
+
+---
+
+## 15. Wrapping Up — `gen_program!()`
+
+At the very end of your crate root (main.rs / lib.rs), call `gen_program!()` to generate the `ThisProgram` struct, the `Next` type alias, and all internal plumbing.
+
+```rust
+use mingling::macros::gen_program;
+
+gen_program!();
+```
+
+It must be placed **after** all your `dispatcher!`, `pack!`, `#[chain]`, `#[renderer]`, and `#[help]` declarations.
+
+---
+
+## Putting It All Together
+
+Here's a complete, runnable program:
+
+```rust
+use mingling::macros::pack;
+use mingling::prelude::*;
+use std::io::Write;
+
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+pack!(ResultGreeting = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let greeting = args
+ .inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+ ResultGreeting::new(greeting).into()
+}
+
+#[renderer]
+fn render_greeting(greeting: ResultGreeting) -> RenderResult {
+ let mut result = RenderResult::new();
+ writeln!(result, "Hello, {}!", *greeting).ok();
+ result
+}
+
+gen_program!();
+```
+
+```bash
+$ myapp greet
+Hello, World!
+
+$ myapp greet Alice
+Hello, Alice!
+```
diff --git a/README.md b/README.md
index f67e6d4..753413d 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
<p align="center">
<a href="https://github.com/mingling-rs/mingling">
- <img alt="Mingling" src="https://github.com/mingling-rs/mingling/raw/main/docs/res/icon2.png" width="50%">
+ <img alt="Mingling" src="https://github.com/mingling-rs/mingling/raw/main/docs/res/icon3.png" width="30%">
</a>
</p>
<h1 align="center">Mìng Lìng - 命令</h1>
@@ -21,878 +21,126 @@
<img src="https://img.shields.io/github/actions/workflow/status/mingling-rs/mingling/ci.yml">
</p>
-> [!WARNING]
->
-> **Note**: Mingling is still under active development, and its API may change. Feel free to try it out and give us feedback!
-> **Hint**: This note will be removed in version `0.5.0`
+## What is Mingling?
-<h1 align="center">
- What is Mingling?
-</h1>
+[`Mingling`](https://github.com/mingling-rs/mingling) is a **state-driven and data-driven** CLI workflow orchestration framework built in Rust.
-[`Mingling`](https://github.com/mingling-rs/mingling) is a **proc-macro and type-system based** Rust CLI framework, suitable for developing complex command-line programs with numerous subcommands.
+💡 Its name comes from the Chinese pinyin **"Mìng Lìng"**, which means **"command"**.
-> Its name comes from the Chinese Pinyin **"Mìng Lìng"**, meaning **"Command"**.
+## WARNING
-### Mingling's Core Capabilities
+Mingling is currently usable at a basic level, but it is still under active development, so many APIs are not yet mature. Any changes to the public API will be documented in detail in the [Changelog](https://github.com/mingling-rs/mingling/blob/main/CHANGELOG.md).
-1. **Separation of Concerns, Clear Logic**: Mingling decouples logic by responsibility, helping you organize your CLI program more clearly.
- See example: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-basic/src/main.rs)
-2. **"All Logic is Functions"**: Execution logic, rendering logic, completion logic, help logic — everything is a function. Just attach the corresponding attribute macro to bind them to your program.
-3. **Fully Dynamic Completion System**: With the `comp` feature, you can flexibly implement dynamic completion logic for any subcommand.
- See examples: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-completion/src/main.rs)
-4. **Lightning-Fast Subcommand Dispatch**: With the `dispatch_tree` feature, Mingling hardens the subcommand structure into a prefix tree at **compile time**, enabling blazing-fast subcommand lookup.
- See examples: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-dispatch-tree/src/main.rs)
-5. **Lightweight Dependencies, On-Demand Importing**: Minimal core dependencies keep builds fast; enhanced features are imported on demand through fine-grained feature flags.
-6. **Structured Output**: Enabling the `structural_renderer` feature adds support for flags like `--json` and `--yaml`, providing structured output capabilities.
- See examples: [Example](https://github.com/mingling-rs/mingling/blob/main/examples/example-structural-renderer/src/main.rs)
+Additionally, the project is currently developed by me alone ([Weicao-CatilGrass](https://github.com/Weicao-CatilGrass)). If you are interested in this project, I warmly welcome your [contributions](https://github.com/mingling-rs/mingling/blob/main/CONTRIBUTING.md). You can reach me directly via [Github Issue](https://github.com/mingling-rs/mingling/issues).
----
+## About Mingling's Design
-**💡 To learn more, check out the following links:**
-
-- 📖 [Mainpage](https://mingling-rs.github.io/mingling/)
-- 📖 [Examples](https://mingling-rs.github.io/mingling/docs/examples.html)
-- 📖 [docs.rs](https://docs.rs/mingling/latest/mingling/)
-- 📖 [Helpdoc](https://mingling-rs.github.io/mingling/docs/doc.html#/) Or [帮助文档](https://mingling-rs.github.io/mingling/docs/_zh_CN/index.html#/)
-- 🔍 [Devdoc](https://mingling-rs.github.io/mingling/docs/dev/)
-
-<h1 align="center">
- Getting Started
-</h1>
-
-Add Mingling to your `Cargo.toml`:
-
-```toml
-[dependencies.mingling]
-version = "0.3.0"
-features = []
-```
-
-Or use the github version
-
-```toml
-[dependencies.mingling]
-git = "https://github.com/mingling-rs/mingling.git"
-tag = "unreleased"
-features = []
-```
-
-Or use the [template project](https://github.com/mingling-rs/mingling-template):
-
-```bash
-cargo generate --git mingling-rs/mingling-template
-```
-
-<h1 align="center">
- Writing with Mingling
-</h1>
-
-### The Big Picture
-
-Mingling organizes your CLI program into three distinct phases:
-
-```
-User Input → [Dispatcher] → Entry → [Chain(s)] → Result → [Renderer] → Output
-```
-
-**Step1: Input** — The user's raw arguments flow in.
-**Step2: Dispatch** — A **Dispatcher** picks them up and wraps them into an **Entry** type.
-**Step3: Chain** — The entry is handed off to a **Chain** function, which processes it.
-**Step4: Render** — A **Renderer** takes that result and writes it to the terminal.
-
-> [!NOTE]
-> A Chain can produce a **State** type to be passed to the next Chain for further processing,
->
-> or it can produce a **Result** type to be handed off to a Renderer.
-
-Everything in this pipeline is a **plain Rust function** with an attribute macro on top.
-
-You never need to manually implement traits or construct boilerplate.
-
----
-
-### 1. Defining Commands — `dispatcher!`
-
-The entry point for every subcommand is the `dispatcher!` macro. It generates two structs for you: a **Dispatcher** (used to register the command with the program) and an **Entry** (a wrapper around `Vec<String>` that holds the raw arguments).
-
-```rust
-use mingling::prelude::*;
-
-// "command.name" Dispatcher EntryType
-// │ │ │
-dispatcher!("greet", CMDGreet => EntryGreet);
-
-// Nested subcommand: `remote add`
-dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd);
-```
-
-Then in `main()`, register the dispatcher with the program:
-
-```rust
-dispatcher!("greet", CMDGreet => EntryGreet);
-
-fn main() {
- let mut program = ThisProgram::new();
- program.with_dispatcher(CMDGreet);
- program.exec_and_exit();
-}
-```
-
-Mingling also supports an abbreviated form (with the `extra_macros` feature):
-
-```rust
-// Features: ["extra_macros"]
-
-// Auto-generates CMDGreet / EntryGreet from "greet"
-dispatcher!("greet");
-```
-
----
-
-### 2. The Chain — "#[chain]" — Where Logic Lives
-
-The `#[chain]` attribute turns a plain function into an execution step. Think of it as "the logic that transforms one typed value into another."
-
-```rust
-dispatcher!("greet", CMDGreet => EntryGreet);
-
-pack!(ResultGreeting = String);
-
-#[chain]
-fn handle_greet(args: EntryGreet) -> Next {
- let greeting = args
- .inner
- .first()
- .cloned()
- .unwrap_or_else(|| "World".to_string());
- ResultGreeting::new(greeting).into()
-}
-```
-
-Key points:
-
-- The return type is `Next` — a type alias for `ChainProcess<ThisProgram>`.
-- You chain results by calling `.to_chain()` on any `pack!`-ed type.
-- You can have **multiple chain functions** for the same command, each transforming the data further.
-- With the `async` feature, chain functions can be `async fn`.
-
----
-
-### 3. The Renderer — "#[renderer]" — How Output Works
-
-The `#[renderer]` attribute turns a function into an output handler. It receives the final result of a chain and returns a `RenderResult`.
-
-```rust
-use mingling::macros::pack;
-use mingling::prelude::*;
-use std::io::Write;
-
-pack!(ResultGreeting = String);
-
-#[renderer]
-fn render_greeting(greeting: ResultGreeting) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "Hello, {}!", *greeting).ok();
- result
-}
-```
-
-Inside a renderer, create a `RenderResult`, write to it using `write!` / `writeln!` (from [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html)), and return it. The output is captured in the buffer and flushed by the framework at the end of the pipeline.
-
-You can write renderers for **any type** in your program, including error types:
-
-```rust
-use mingling::prelude::*;
-use std::io::Write;
-
-#[renderer]
-fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "Command not found: [{}]", err.join(" ")).ok();
- result
-}
-```
-
----
-
-### 4. Parsing Arguments — The Picker
-
-Mingling provides a **Picker** for zero-cost argument extraction. You use `pick()` or `pick_or()` on an entry to extract typed values, then `unpack()` to get the final tuple.
-
-```rust
-// Features: ["parser"]
-
-use mingling::parser::Picker;
-
-dispatcher!("greet", CMDGreet => EntryGreet);
-pack!(ResultGreeting = String);
-
-#[chain]
-fn handle_greet(args: EntryGreet) -> Next {
- let (name, count) = Picker::new(args.inner)
- .pick::<String>(()) // positional: first string
- .pick_or::<u8>(["-r", "--repeat"], 1) // optional flag with default
- .unpack();
- ResultGreeting::new(format!("{} x{}", name, count)).into()
-}
-```
-
-With the `parser` feature, the `AsPicker` trait provides a shorthand directly on entries:
-
-```rust
-// Features: ["parser"]
-
-dispatcher!("greet", CMDGreet => EntryGreet);
-pack!(ResultGreeting = String);
-
-#[chain]
-fn handle(args: EntryGreet) -> Next {
- let (name, count) = args
- .pick::<Option<String>>(())
- .pick_or::<u8>(["-r", "--repeat"], 1)
- .unpack();
- ResultGreeting::new(format!("{} x{}", name.unwrap_or_default(), count)).into()
-}
-```
-
-For enums, derive `EnumTag` and implement `PickableEnum` to parse enum variants from strings:
-
-```rust
-// Features: ["parser", "extra_macros"]
-
-use mingling::{EnumTag, Groupped};
-use mingling::parser::PickableEnum;
-
-dispatcher!("lang.select", CMDLang => EntryLang);
-
-#[derive(Debug, Default, EnumTag, Groupped)]
-pub enum Language {
- #[default]
- Rust,
- #[enum_rename("C++")]
- CPlusPlus,
-}
-
-impl PickableEnum for Language {}
-
-#[chain]
-fn handle(args: EntryLang) -> Next {
- let lang: Language = args.pick(()).unpack();
- lang.into()
-}
-```
-
----
-
-### 5. The Help System — "#[help]"
-
-Help is just another attribute macro. When the user passes `--help` or `-h`, the program skips the normal chain/render pipeline and routes directly to your `#[help]` function.
-
-Enable it by adding `BasicProgramSetup`:
-
-```rust
-use mingling::{macros::help, prelude::*, setup::BasicProgramSetup};
-use std::io::Write;
-
-dispatcher!("greet", CMDGreet => EntryGreet);
-
-#[help]
-fn help_greet(_prev: EntryGreet) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "Usage: greet <NAME>").ok();
- writeln!(result, "Greets the user with the given name.").ok();
- result
-}
-
-fn main() {
- let mut program = ThisProgram::new();
- program.with_setup(BasicProgramSetup); // enables --help / -h
- program.with_dispatcher(CMDGreet);
- program.exec_and_exit();
-}
-
-gen_program!();
-```
-
-The flow is:
-
-- User types `greet --help`
-- `BasicProgramSetup` sets `program.user_context.help = true`
-- The dispatcher sees this flag and routes to the `#[help]` function instead of the `#[chain]`
-
----
-
-### 6. Completion — "#[completion]" — Dynamic Shell Completions
-
-With the `comp` feature, Mingling provides a fully dynamic completion system. You write a function that returns `Suggest` based on the current shell context, and Mingling generates the completion scripts for bash, zsh, fish, and pwsh.
-
-```rust
-// Features: ["comp", "extra_macros"]
-
-use mingling::{macros::suggest, ShellContext, Suggest};
-
-dispatcher!("greet", CMDGreet => EntryGreet);
-pack!(ResultName = (u8, String));
-
-#[completion(EntryGreet)]
-fn complete_greet(ctx: &ShellContext) -> Suggest {
- // Suggest positional arguments
- if ctx.previous_word == "greet" {
- return suggest! {
- "Alice": "Likes to receive messages",
- "Bob": "Likes to pass messages",
- "World"
- };
- }
-
- // Suggest flag arguments
- if ctx.typing_argument() {
- return suggest! {
- "-r": "Number of repetitions",
- "--repeat": "Number of repetitions",
- }
- .strip_typed_argument(ctx);
- }
-
- suggest!() // no suggestions
-}
-```
-
-You also need to register the built-in completion dispatcher:
-
-```rust
-// Features: ["comp"]
-
-fn main() {
- let mut program = ThisProgram::new();
- program.with_dispatcher(crate::CMDCompletion);
- program.exec_and_exit();
-}
-```
-
-In your `build.rs`, generate the shell scripts:
-
-```rust
-// BUILD TIME
-// Features: ["comp", "builds"]
-mingling::build::build_comp_scripts(env!("CARGO_PKG_NAME")).unwrap();
-```
-
-For enum-based completions, use `suggest_enum!`:
-
-```rust
-// Features: ["comp", "extra_macros"]
-
-use mingling::{ShellContext, Suggest};
-use mingling::macros::suggest_enum;
-use mingling::EnumTag;
-
-dispatcher!("lang.select", CMDLang => EntryLang);
-
-#[derive(EnumTag)]
-pub enum ProgrammingLanguages {
- Rust,
- Python,
- JavaScript,
-}
-
-#[completion(EntryLang)]
-fn complete_lang(_: &ShellContext) -> Suggest {
- suggest_enum!(ProgrammingLanguages)
-}
-```
-
----
-
-### 7. Error Handling
-
-Mingling doesn't use `?` operator propagation. Instead, errors are just **alternative results** that flow through the same chain/render pipeline. Create error types with `pack!` and route to them with `.to_render()`:
+Mingling abstracts the behavior of a program's lifecycle into three phases: **Dispatch**, **Execution**, and **Rendering**. Each phase is connected by types — the output of the current phase becomes the input of the next phase. For example:
```rust
-use mingling::macros::pack;
-use mingling::prelude::*;
-use std::io::Write;
-
-dispatcher!("hello", CMDHello => EntryHello);
-pack!(ResultName = String);
-pack!(ErrorNoNameProvided = ());
-pack!(ErrorNameTooLong = u16);
-
-#[chain]
-fn handle(args: EntryHello) -> Next {
- let Some(name) = args.inner.first().cloned() else {
- return ErrorNoNameProvided::default().to_render(); // ← early return to error renderer
- };
-
- if name.len() > 10 {
- return ErrorNameTooLong::new(name.len() as u16).to_render();
- }
-
- ResultName::new(name).to_render() // ← success path
-}
-
-#[renderer]
-fn render_no_name(_: ErrorNoNameProvided) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "No name provided").ok();
- result
-}
-
-#[renderer]
-fn render_too_long(len: ErrorNameTooLong) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "Name too long: {} > 10", *len).ok();
- result
-}
-```
-
-Two built-in fallback types are always available:
-
-- `ErrorDispatcherNotFound` — rendered when no dispatcher matches the input
-- `ErrorRendererNotFound` — rendered when no renderer is found for a result type
-
----
-
-### 8. Resource Injection
-
-Chain and renderer functions can accept **additional parameters** for the program's global state. Resources are singleton values registered with `program.with_resource(...)`.
-
-```rust
-// Features: ["parser", "extra_macros"]
-
-use std::path::PathBuf;
-
dispatcher!("current", CMDCurrent => EntryCurrent);
-dispatcher!("cd", CMDCd => EntryCd);
+pack!(StateNext = ());
-#[derive(Default, Clone)]
-struct ResCurrentDir {
- current_dir: PathBuf,
-}
-
-fn main() {
- let mut program = ThisProgram::new();
- program.with_resource(ResCurrentDir {
- current_dir: std::env::current_dir().unwrap(),
- });
- program.with_dispatcher(CMDCurrent);
- program.with_dispatcher(CMDCd);
- program.exec_and_exit();
-}
-
-// Read-only access (shared reference):
#[chain]
-fn show_current(_prev: EntryCurrent, current_dir: &ResCurrentDir) -> Next {
- println!("Current: {}", current_dir.current_dir.display());
- empty_result!()
-}
-
-// Mutable access:
-#[chain]
-fn change_dir(prev: EntryCd, current_dir: &mut ResCurrentDir) -> Next {
- let path: String = prev.pick(()).unpack();
- current_dir.current_dir = current_dir.current_dir.join(path);
- empty_result!()
-}
-```
-
-Resources can also be injected into `#[renderer]`:
-
-```rust
-use mingling::prelude::*;
-use std::io::Write;
-
-dispatcher!("current", CMDCurrent => EntryCurrent);
-
-#[derive(Default, Clone)]
-struct ResCurrentDir {
- current_dir: std::path::PathBuf,
-}
-
-#[renderer]
-fn render_current(_: EntryCurrent, current_dir: &ResCurrentDir) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "Current directory: {}", current_dir.current_dir.display()).ok();
- result
-}
-```
-
----
-
-### 9. Dispatch Tree — Compile-Time Command Trie
-
-As your program grows to dozens or hundreds of subcommands, linear dispatcher lookup becomes slow. Enable the `dispatch_tree` feature to convert the command structure into a **prefix tree (Trie)** at compile time.
-
-```rust
-// Features: ["dispatch_tree"]
-
-dispatcher!("cmd1", CMD1 => Entry1);
-dispatcher!("cmd2.sub1", CMD2Sub1 => Entry2Sub1);
-dispatcher!("cmd2.sub2", CMD2Sub2 => Entry2Sub2);
-dispatcher!("cmd3.sub1.leaf1", CMD3Sub1Leaf1 => Entry3Sub1Leaf1);
-dispatcher!("cmd3.sub1.leaf2", CMD3Sub1Leaf2 => Entry3Sub1Leaf2);
-// ... dozens more
-
-fn main() {
- let program = ThisProgram::new();
- // No more with_dispatcher calls — it's all compile-time!
- program.exec_and_exit();
+fn handle_current(_: EntryCurrent) -> StateNext {
+ // 1. The first phase outputs the StateNext value
+ StateNext::default() // ^^^^^^^^^
+} // |
+ // |
+ // 2. The second phase takes StateNext as input
+#[chain] // |
+fn handle_state_next(_: StateNext) {
+ todo!()
}
```
-With `dispatch_tree` enabled:
-
-- Dispatchers are auto-collected at compile time
-- `Program` no longer stores a dispatcher list
-- `program.with_dispatcher(...)` is not compiled
-- Lookup is **O(n)** where _n_ is input length, not number of commands
-
----
+See? `handle_current` and `handle_state_next` have no direct connection!
-### 10. Clap Binding — Using Clap's Parser
+They are bridged by `StateNext` and automatically linked by the framework.
-If you prefer clap's powerful argument parsing, use `#[dispatcher_clap]`. It generates a dispatcher from a `clap::Parser` struct.
+You can use this approach to separate computation from result rendering, like this:
```rust
-// Features: ["clap"]
-// Dependencies:
-// clap = "4"
-
-use mingling::macros::dispatcher_clap;
+// Features: ["picker"]
+use mingling::macros::buffer;
use mingling::prelude::*;
-use std::io::Write;
-
-#[derive(Default, clap::Parser, Groupped)]
-#[dispatcher_clap(
- "greet", CMDGreet,
- help = true, // auto-generate #[help] from clap
- error = ErrorGreetParsed, // capture parse errors as a renderable type
-)]
-pub struct EntryGreet {
- #[clap(default_value = "World")]
- name: String,
-
- #[arg(short, long, default_value_t = 1)]
- repeat: i32,
-}
-
-#[renderer]
-fn render_greet(greet: EntryGreet) -> RenderResult {
- let mut result = RenderResult::new();
- write!(result, "Hello, ").ok();
- for _ in 0..greet.repeat { write!(result, "{}", greet.name).ok(); }
- writeln!(result, "!").ok();
- result
-}
-
-#[renderer]
-fn render_parse_error(err: ErrorGreetParsed) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "{}", *err).ok();
- result
-}
-```
-
-You can control how clap help is displayed:
-
-```rust
-// Features: ["clap"]
-
-dispatcher!("greet", CMDGreet => EntryGreet);
-
-fn main() {
- let mut program = ThisProgram::new();
- program.with_dispatcher(CMDGreet);
- program.stdout_setting.clap_help_print_behaviour =
- mingling::ClapHelpPrintBehaviour::WriteToRenderResult;
- // or: PrintDirectly — writes clap help straight to stdout
- program.exec_and_exit();
-}
-```
-
----
-
-### 11. REPL Mode
-
-With the `repl` feature, turn your CLI into an interactive shell with one method call:
-
-```rust
-// Features: ["repl"]
-
-fn main() {
- ThisProgram::new().exec_repl();
-}
-```
-
-Mingling provides built-in REPL setups:
-
-```rust
-// Features: ["repl", "extra_macros"]
-
-use mingling::{
- res::ResREPL,
- setup::{BasicREPLReadlineSetup, BasicREPLOutputSetup, BasicREPLPromptSetup},
-};
-dispatcher!("cd", CMDCd => EntryCd);
-dispatcher!("exit", CMDExit => EntryExit);
+dispatcher!("calc", CMDCalculate => EntryCalculate);
+pack!(StateSumNumbers = Vec<i32>);
+pack!(ResultNumber = i32);
-fn main() {
- let mut program = ThisProgram::new();
-
- program.with_dispatcher(CMDCd);
- program.with_dispatcher(CMDExit);
-
- // Enable line reading from stdin
- program.with_setup(BasicREPLReadlineSetup);
-
- // Enable output flushing after each render
- program.with_setup(BasicREPLOutputSetup);
-
- // Custom prompt
- program.with_setup(BasicREPLPromptSetup::func(|| "> ".to_string()));
-
- program.exec_repl(); // ← interactive loop
-}
-
-// Exit the REPL via the ResREPL resource:
+// Entry: parse arguments and pass state to the calculation step
#[chain]
-fn handle_exit(_prev: EntryExit, repl: &mut ResREPL) {
- repl.exit = true;
-}
-```
-
----
-
-### 12. Hooks — Observing the Pipeline
-
-Mingling provides a `ProgramHook` system for observing every stage of the execution pipeline. Useful for debugging, logging, or telemetry.
-
-```rust
-use mingling::{
- hook::{ProgramControlUnit, ProgramHook},
-};
-
-dispatcher!("greet", CMDGreet => EntryGreet);
-
-fn main() {
- let mut program = ThisProgram::new();
-
- program.with_hook(
- ProgramHook::<ThisProgram>::empty()
- .on_begin::<_, ()>(|_| println!("[DEBUG] Program is begin"))
- .on_pre_dispatch(|info| println!("[DEBUG] Pre dispatch: {}", info.arguments.join(" ")))
- .on_post_dispatch(|info| println!("[DEBUG] Post dispatch: {}", info.entry))
- .on_pre_chain(|info| {
- println!("[DEBUG] Pre chain: {}", info.input);
- })
- .on_post_chain(|info| println!("[DEBUG] Post chain: {}", info.output.member_id))
- .on_finish(|_| {
- println!("[DEBUG] Loop end");
- ProgramControlUnit::OverrideExitCode(0) // Override exit code
- })
- .on_pre_render(|info| println!("[DEBUG] Pre render: {}", info.input))
- .on_post_render(|_| println!("[DEBUG] Post render")),
- );
-
- program.with_dispatcher(CMDGreet);
- program.exec_and_exit();
-}
-```
-
----
-
-### 13. Structural Renderer — Structured Output (JSON/YAML)
-
-With the `structural_renderer` feature, users can add `--json` or `--yaml` flags to get structured output instead of human-readable text.
-
-```rust
-// Features: ["structural_renderer", "parser"]
-// Dependencies:
-// serde = "1"
-
-use mingling::{prelude::*, setup::StructuralRendererSetup};
-use mingling::Groupped;
-use mingling::StructuralData;
-use serde::Serialize;
-use std::io::Write;
-
-dispatcher!("render", CMDRender => EntryRender);
-
-#[derive(Default, StructuralData, Serialize, Groupped)]
-struct ResultInfo {
- name: String,
- age: i32,
+fn handle_calc(args: EntryCalculate) -> StateSumNumbers {
+ let numbers = args.pick(&arg![Vec<i32>]).unwrap();
+ StateSumNumbers::new(numbers)
}
+// Calculate: pass the result to the rendering step
#[chain]
-fn render_info(args: EntryRender) -> Next {
- let (name, age) = args.pick::<String>(()).pick::<i32>(()).unpack();
- ResultInfo { name, age }.to_chain()
-}
-
-#[renderer]
-fn render_info_result(info: ResultInfo) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "{} is {} years old", info.name, info.age).ok();
- result
+fn handle_state_sum_numbers(sum: StateSumNumbers) -> ResultNumber {
+ let numbers = sum.inner;
+ let total: i32 = numbers.iter().sum();
+ ResultNumber::new(total)
}
-fn main() {
- let mut program = ThisProgram::new();
- program.with_setup(StructuralRendererSetup); // enables --json / --yaml
- program.with_dispatcher(CMDRender);
- let _ = program.exec();
+// Renderer: return the render result and let the framework handle output
+#[renderer(buffer)]
+fn render_number(number: ResultNumber) {
+ r_println!("Number: {}", *number);
}
```
-Then users can do:
+Although this may make your program slightly more verbose, each step is a **pure function**, making it extremely easy to test!
-```bash
-$ myapp render Bob 22
-Bob is 22 years old
+## Getting Started
-$ myapp render Bob 22 --json
-{"name":"Bob","age":22}
+Add Mingling to your `Cargo.toml`:
-$ myapp render Bob 22 --yaml
-name: Bob
-age: 22
+```toml
+[dependencies.mingling]
+version = "0.3.0"
+features = []
```
----
-
-### 14. Async Support
-
-Enable the `async` feature to use `async fn` inside `#[chain]`:
-
-```rust
-// Features: ["async", "parser"]
-// Dependencies:
-// tokio = { version = "1", features = ["full"] }
-
-use std::io::Write;
-use std::time::Duration;
-
-dispatcher!("download", CMDDownload => EntryDownload);
-pack!(ResultDownloaded = String);
-
-#[chain]
-pub async fn handle_download(args: EntryDownload) -> Next {
- let file = args.pick(()).unpack();
- download_file(file).await.into()
-}
-
-async fn download_file(name: String) -> ResultDownloaded {
- tokio::time::sleep(Duration::from_secs(1)).await;
- ResultDownloaded::new(name)
-}
+Or use the github version
-#[renderer]
-fn render_downloaded(result: ResultDownloaded) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "\"{}\" downloaded.", *result).ok();
- r
-}
+```toml
+[dependencies.mingling]
+git = "https://github.com/mingling-rs/mingling.git"
+tag = "unreleased"
+features = []
```
> [!NOTE]
->
-> `#[renderer]` functions cannot be async. When `async` is enabled, `program.exec_and_exit().await` returns a Future.
-
----
-
-### 15. Wrapping Up — `gen_program!()`
-
-At the very end of your crate root (main.rs / lib.rs), call `gen_program!()` to generate the `ThisProgram` struct, the `Next` type alias, and all internal plumbing.
-
-```rust
-use mingling::macros::gen_program;
-
-gen_program!();
-```
-
-It must be placed **after** all your `dispatcher!`, `pack!`, `#[chain]`, `#[renderer]`, and `#[help]` declarations.
-
----
-
-### Putting It All Together
-
-Here's a complete, runnable program:
-
-```rust
-use mingling::macros::pack;
-use mingling::prelude::*;
-use std::io::Write;
-
-dispatcher!("greet", CMDGreet => EntryGreet);
-
-fn main() {
- let mut program = ThisProgram::new();
- program.with_dispatcher(CMDGreet);
- program.exec_and_exit();
-}
+> To learn more, check out [Writing with Mingling](https://github.com/mingling-rs/mingling/blob/main/GETTING_STARTED.md)
-pack!(ResultGreeting = String);
-
-#[chain]
-fn handle_greet(args: EntryGreet) -> Next {
- let greeting = args
- .inner
- .first()
- .cloned()
- .unwrap_or_else(|| "World".to_string());
- ResultGreeting::new(greeting).into()
-}
-
-#[renderer]
-fn render_greeting(greeting: ResultGreeting) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "Hello, {}!", *greeting).ok();
- result
-}
-
-gen_program!();
-```
-
-```bash
-$ myapp greet
-Hello, World!
-
-$ myapp greet Alice
-Hello, Alice!
-```
-
-<h1 align="center">
- 🗺️ Roadmap 🗺️
-</h1>
+## Roadmap
- [x] Milestone.1 "MVP" 🎉
- - [x] [[0.1.4](https://docs.rs/mingling/0.1.4/mingling/)] [`core`] [`structural_renderer`] **Mingling** can render data into serializable formats via `--json` and `--yaml` flags
- - [x] [[0.1.5](https://docs.rs/mingling/0.1.5/mingling/)] [`core`] [`comp`] **Mingling** can dynamically invoke itself to provide completions for shells like `bash`, `zsh`, `fish`, and `pwsh`
- - [x] [[0.1.6](https://docs.rs/mingling/0.1.6/mingling/)] [`core`] [`comp`] **Mingling** can gather more context for smarter completions
- - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`clap`] Provides a **Clap** compatibility layer, allowing **Mingling** to reuse its powerful parsing capabilities
- - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`core`] **Mingling** can intercept `-h` or `--help` flags to display custom help text for each subcommand
- - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`mling`] Provides a basic scaffolding tool (`mling`) for rapid development and debugging
- - [x] [[0.1.8](https://docs.rs/mingling/0.1.8/mingling/)] [`core`] [`dispatch_tree`] Converts the subcommand list into a prefix tree to improve command matching speed
- - [x] [[0.1.9](https://docs.rs/mingling/0.1.9/mingling/)] [`core`] [`dev_toolkits`] Provides debugging interfaces for developers to capture invocation information when issues arise (`InvokeStackDisplay`) (indirectly implemented via `ProgramHook`)
- - [x] [[0.1.9](https://docs.rs/mingling/0.1.9/mingling/)] [`core`] [`repl`] Provides REPL capability (`program.exec_repl();`)
- - [x] [[0.2.0](https://docs.rs/mingling/0.2.0/mingling/)] Complete documentation, tests, and examples
+ - [x] [[0.1.4](https://docs.rs/mingling/0.1.4/mingling/)] [`core`] [`structural_renderer`] **Mingling** can render data into serializable formats via `--json` and `--yaml` flags
+ - [x] [[0.1.5](https://docs.rs/mingling/0.1.5/mingling/)] [`core`] [`comp`] **Mingling** can dynamically invoke itself to provide completions for shells like `bash`, `zsh`, `fish`, and `pwsh`
+ - [x] [[0.1.6](https://docs.rs/mingling/0.1.6/mingling/)] [`core`] [`comp`] **Mingling** can gather more context for smarter completions
+ - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`clap`] Provides a **Clap** compatibility layer, allowing **Mingling** to reuse its powerful parsing capabilities
+ - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`core`] **Mingling** can intercept `-h` or `--help` flags to display custom help text for each subcommand
+ - [x] [[0.1.7](https://docs.rs/mingling/0.1.7/mingling/)] [`mling`] Provides a basic scaffolding tool (`mling`) for rapid development and debugging
+ - [x] [[0.1.8](https://docs.rs/mingling/0.1.8/mingling/)] [`core`] [`dispatch_tree`] Converts the subcommand list into a prefix tree to improve command matching speed
+ - [x] [[0.1.9](https://docs.rs/mingling/0.1.9/mingling/)] [`core`] [`dev_toolkits`] Provides debugging interfaces for developers to capture invocation information when issues arise (`InvokeStackDisplay`) (indirectly implemented via `ProgramHook`)
+ - [x] [[0.1.9](https://docs.rs/mingling/0.1.9/mingling/)] [`core`] [`repl`] Provides REPL capability (`program.exec_repl();`)
+ - [x] [[0.2.0](https://docs.rs/mingling/0.2.0/mingling/)] Complete documentation, tests, and examples
- [ ] Milestone.2 "More Comfortable Dev and User Experience"
- - [ ] [`mling` / `mingling-cli`]
- - [ ] **Mingling** Linter
- - [ ] **Mingling** Project Generator
- - [ ] **Mingling** Program Installer & Manager (For development)
- - [ ] Helpdoc Editor
- - [ ] [`picker`] A more efficient and intelligent argument parser
- - [x] [`macros`] Remove `r_print!` / `r_println!` macros
+ - [ ] [`mling` / `mingling-cli`]
+ - [ ] **Mingling** Linter
+ - [ ] **Mingling** Project Generator
+ - [ ] **Mingling** Program Installer & Manager (For development)
+ - [ ] Helpdoc Editor
+ - [ ] [`picker`] A more efficient and intelligent argument parser
+ - [x] [`macros`] ~~Remove r_print! / r_println! macros~~ (see below)
+ - [x] [`macros`] Make implicit modifications to functions explicit
- [ ] Milestone.3 "Unplanned"
- - [ ] ...
+ - [ ] ...
-<h1 align="center">
- 🚫 Unplanned Features 🚫
-</h1>
+## Unplanned Features
While Mingling has several common CLI features that are **NOT PLANNED** to be directly included in the framework.
This is because the Rust ecosystem already has excellent and mature crates to handle these issues, and Mingling's design is intended to be used in combination with them.
@@ -902,10 +150,20 @@ This is because the Rust ecosystem already has excellent and mature crates to ha
- **Progress Bars**: To display progress indicators, the [`indicatif`](https://crates.io/crates/indicatif) crate is the standard choice.
- **TUI**: To build full-screen interactive terminal applications, it is recommended to use a framework like [`ratatui`](https://crates.io/crates/ratatui) (formerly `tui-rs`).
-<h1 align="center">
- 📄 Open Source License 📄
-</h1>
+## License
This project is licensed under the MIT License.
See [LICENSE-MIT](LICENSE-MIT) or [LICENSE-APACHE](LICENSE-APACHE) file for details.
+
+## Learn More
+
+**To learn more, check out the following links:**
+
+- 📦 Repo - [Github](https://github.com/mingling-rs/mingling) | [Gitee](https://gitee.com/mingling-rs/mingling) | [Origin](https://catilgrass.cn/mingling.git)
+- 🚪 Mainpage - [Github](https://mingling-rs.github.io/mingling/) | [crates.io](https://crates.io/crates/mingling)
+- 💡 Examples - [Github](https://mingling-rs.github.io/mingling/docs/examples.html)
+- 📖 Help Doc - [EN](https://mingling-rs.github.io/mingling/docs/doc.html#/) | [中文](https://mingling-rs.github.io/mingling/docs/_zh_CN/index.html#/)
+- 📖 API Doc - [docs.rs](https://docs.rs/mingling/latest/mingling/) | [latest](https://mingling-rs.github.io/mingling/docs/api-docs/mingling/)
+- 📖 Coverage Test - [LLVM Coverage](https://mingling-rs.github.io/mingling/docs/cov-test/)
+- 📖 Dev Doc - [Github](https://mingling-rs.github.io/mingling/docs/dev/)
diff --git a/arg_picker/Cargo.toml b/arg_picker/Cargo.toml
index f7672c1..d87a844 100644
--- a/arg_picker/Cargo.toml
+++ b/arg_picker/Cargo.toml
@@ -9,9 +9,8 @@ readme = "README.md"
description = "A lightweight, type-safe CLI argument parser"
[features]
-mingling_support = ["dep:mingling_core", "arg-picker-macros/mingling_support"]
+mingling_support = ["arg-picker-macros/mingling_support"]
[dependencies]
-mingling_core = { workspace = true, optional = true }
arg-picker-macros.workspace = true
just_fmt.workspace = true
diff --git a/arg_picker/src/arg.rs b/arg_picker/src/arg.rs
index 78ad539..4fa21b5 100644
--- a/arg_picker/src/arg.rs
+++ b/arg_picker/src/arg.rs
@@ -1,4 +1,4 @@
-use crate::Pickable;
+use crate::{Pickable, PickerArgInfo};
use std::marker::PhantomData;
/// Represents a constraint definition for a parameter selection.
@@ -133,14 +133,61 @@ where
self.positional = positional;
self
}
+
+ /// Converts this `PickerArg` into a `PickerArgInfo` value.
+ ///
+ /// This is a convenience method equivalent to calling `PickerArgInfo::from(self)`.
+ pub fn into_info(self) -> PickerArgInfo<'a> {
+ let value = self;
+ let (long, alias) = match value.full.len() {
+ 0 => (None, None),
+ _ => {
+ let long = Some(value.full[0]);
+ let alias = if value.full.len() > 1 {
+ Some(value.full[1..].to_vec())
+ } else {
+ None
+ };
+ (long, alias)
+ }
+ };
+
+ PickerArgInfo {
+ short: value.short,
+ long,
+ alias,
+ positional: value.positional,
+ optional: false,
+ multi: false,
+ is_flag: false,
+ }
+ }
}
/// Describes the attribute (behavior) of a command-line parameter.
///
/// The ordering reflects parse priority (higher = parsed first):
-/// `PositionalMulti < Positional < Flag < Single < Multi`
+/// `Postprocess < Final < PositionalMulti < Positional < Flag < Single < Multi < Begin < Preprocess`
+///
+/// # Variants
+///
+/// - `Postprocess` — Reserved lowest priority, used only in special cases.
+/// - `Final` — Reserved post-processing priority, used only in special cases.
+/// - `PositionalMulti` — Positional argument that accepts multiple values (e.g., multiple input files).
+/// - `Positional` — Positional argument matched by its position (e.g., an input file).
+/// - `Flag` — Boolean flag with no associated value (e.g., `--verbose`).
+/// - `Single` — Accepts a single value (e.g., `--name Alice`).
+/// - `Multi` — Accepts multiple values (e.g., `--file a.txt --file b.txt`).
+/// - `Begin` — Reserved pre-processing priority, used only in special cases.
+/// - `Preprocess` — Reserved highest priority, used only in special cases.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PickerArgAttr {
+ /// Reserved lowest priority, used only in special cases.
+ Postprocess,
+
+ /// Reserved post-processing priority, used only in special cases.
+ Final,
+
/// Positional argument that accepts multiple values (e.g., multiple input files).
PositionalMulti,
@@ -156,6 +203,12 @@ pub enum PickerArgAttr {
/// Accepts multiple values (e.g., `--file a.txt --file b.txt`).
Multi,
+
+ /// Reserved pre-processing priority, used only in special cases.
+ Begin,
+
+ /// Reserved highest priority, used only in special cases.
+ Preprocess,
}
impl PickerArgAttr {
diff --git a/arg_picker/src/builtin.rs b/arg_picker/src/builtin.rs
index e855b08..1c698ba 100644
--- a/arg_picker/src/builtin.rs
+++ b/arg_picker/src/builtin.rs
@@ -1,4 +1,5 @@
mod pick_bool;
mod pick_flag;
mod pick_numbers;
+mod pick_picker_args;
mod pick_string;
diff --git a/arg_picker/src/builtin/pick_picker_args.rs b/arg_picker/src/builtin/pick_picker_args.rs
new file mode 100644
index 0000000..419cbc8
--- /dev/null
+++ b/arg_picker/src/builtin/pick_picker_args.rs
@@ -0,0 +1,21 @@
+use crate::{PickerArgResult::Parsed, PickerArgs, parselib::build_masked_args, pickable_needed::*};
+
+impl<'a> Pickable<'a> for PickerArgs<'a> {
+ fn get_attr(_flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ // Use the lowest priority attribute
+ PickerArgAttr::Postprocess
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ // Collect all remaining raw index values
+ build_masked_args(ctx.args, ctx.mask)
+ .iter()
+ .map(|m| m.raw_idx)
+ .collect()
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ let remains: Vec<String> = raw_strs.iter().map(|s| s.to_string()).collect();
+ Parsed(PickerArgs::Owned(remains))
+ }
+}
diff --git a/arg_picker/src/constants.rs b/arg_picker/src/constants.rs
new file mode 100644
index 0000000..39250f8
--- /dev/null
+++ b/arg_picker/src/constants.rs
@@ -0,0 +1,16 @@
+use std::marker::PhantomData;
+
+use crate::{PickerArg, PickerArgs};
+
+/// Remaining positional arguments (anything not consumed as an option).
+/// - `full`: `[]` (empty — not triggered by any `--` prefix).
+/// - `short`: (none)
+/// - `positional`: `false` (this is a meta‑argument that collects everything left).
+/// This constant is used internally to access any leftover arguments after
+/// all defined flags/options have been processed.
+pub const REMAINS: PickerArg<PickerArgs> = PickerArg::<PickerArgs> {
+ full: &[],
+ short: None,
+ positional: false,
+ internal_type: PhantomData,
+};
diff --git a/arg_picker/src/infos.rs b/arg_picker/src/infos.rs
index 074539a..c17b6a2 100644
--- a/arg_picker/src/infos.rs
+++ b/arg_picker/src/infos.rs
@@ -1,4 +1,4 @@
-use crate::{Pickable, PickerArg};
+use crate::{Pickable, PickerArg, parselib::ParserStyle};
/// Represents the result of parsing or looking up a value.
///
@@ -288,28 +288,7 @@ where
T: Pickable<'a>,
{
fn from(value: PickerArg<'a, T>) -> Self {
- let (long, alias) = match value.full.len() {
- 0 => (None, None),
- _ => {
- let long = Some(value.full[0]);
- let alias = if value.full.len() > 1 {
- Some(value.full[1..].to_vec())
- } else {
- None
- };
- (long, alias)
- }
- };
-
- Self {
- short: value.short,
- long,
- alias,
- positional: value.positional,
- optional: false,
- multi: false,
- is_flag: false,
- }
+ value.into_info()
}
}
@@ -437,6 +416,56 @@ impl<'a> PickerArgInfo<'a> {
self.is_flag = is_flag;
self
}
+
+ /// Returns the short flag string, e.g. `-n` for short `n`.
+ ///
+ /// Uses [`ParserStyle::global_style()`] to format the flag.
+ ///
+ /// # Returns
+ ///
+ /// - `Some(String)` if `self.short` is set.
+ /// - `None` if `self.short` is `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use arg_picker::PickerArgInfo;
+ ///
+ /// let info = PickerArgInfo::new().with_short('n');
+ /// assert_eq!(info.short_flag(), Some("-n".to_string()));
+ ///
+ /// let info = PickerArgInfo::new();
+ /// assert_eq!(info.short_flag(), None);
+ /// ```
+ pub fn short_flag(&self) -> Option<String> {
+ let short = self.short?;
+ Some(ParserStyle::global_style().flag_string(short))
+ }
+
+ /// Returns the long flag string, e.g. `--name` for long `"name"`.
+ ///
+ /// Uses [`ParserStyle::global_style()`] to format the flag.
+ ///
+ /// # Returns
+ ///
+ /// - `Some(String)` if `self.long` is set.
+ /// - `None` if `self.long` is `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use arg_picker::PickerArgInfo;
+ ///
+ /// let info = PickerArgInfo::new().with_long("name");
+ /// assert_eq!(info.long_flag(), Some("--name".to_string()));
+ ///
+ /// let info = PickerArgInfo::new();
+ /// assert_eq!(info.long_flag(), None);
+ /// ```
+ pub fn long_flag(&self) -> Option<String> {
+ let long = self.long?;
+ Some(ParserStyle::global_style().flag_string(long))
+ }
}
impl<'a> Default for PickerArgInfo<'a> {
diff --git a/arg_picker/src/lib.rs b/arg_picker/src/lib.rs
index 21a0d35..d292826 100644
--- a/arg_picker/src/lib.rs
+++ b/arg_picker/src/lib.rs
@@ -1,4 +1,5 @@
#![doc = include_str!("../README.md")]
+#![deny(missing_docs)]
mod builtin;
@@ -29,13 +30,8 @@ pub mod value;
/// use arg_picker::prelude::*;
/// ```
pub mod prelude {
- pub use crate::macros::arg;
-
- #[cfg(not(feature = "mingling_support"))]
pub use crate::IntoPicker;
-
- #[cfg(feature = "mingling_support")]
- pub use crate::corebind::EntryPicker;
+ pub use crate::macros::arg;
}
/// Re-export of the `arg_picker_macros` crate
@@ -54,9 +50,12 @@ pub mod matcher_needed {
pub use crate::parselib::{MaskedArg, Matcher, ParserStyle};
}
-#[cfg(feature = "mingling_support")]
-mod corebind;
+mod constants;
-#[allow(unused_imports)]
-#[cfg(feature = "mingling_support")]
-pub use corebind::*;
+/// Re-export of constants used by `arg-picker`.
+///
+/// This module provides access to various constants defined internally, such as
+/// default values, configuration limits, and other static parameters.
+pub mod consts {
+ pub use crate::constants::*;
+}
diff --git a/arg_picker/src/parselib.rs b/arg_picker/src/parselib.rs
index 7fbd606..0fcd583 100644
--- a/arg_picker/src/parselib.rs
+++ b/arg_picker/src/parselib.rs
@@ -117,13 +117,32 @@ impl<'a> From<crate::TagPhaseContext<'a>> for MatcherContext<'a> {
}
}
+/// Checks whether the argument at index `idx` is already claimed (masked).
+///
+/// Returns `true` if `idx` is within the mask bounds and the mask value is non-zero,
+/// indicating the argument has been claimed by a previous matcher.
+///
+/// # Arguments
+///
+/// * `mask` - A byte slice where non-zero values indicate claimed arguments.
+/// * `idx` - The index to check in the mask.
#[inline(always)]
-fn is_masked(mask: &[u8], idx: usize) -> bool {
+pub fn is_masked(mask: &[u8], idx: usize) -> bool {
idx < mask.len() && mask[idx] != 0
}
+/// Builds a vector of [`MaskedArg`] from the given `PickerArgs` and mask.
+///
+/// Only arguments whose mask entry is `0` (i.e., available/not yet claimed) are included.
+/// Each resulting [`MaskedArg`] retains its original raw string and its index in the full
+/// argument list for later reference.
+///
+/// # Arguments
+///
+/// * `args` - The full set of parsed arguments.
+/// * `mask` - A byte slice where `0` means available and non-zero means already claimed.
#[inline(always)]
-fn build_masked_args<'a>(args: &'a PickerArgs, mask: &'a [u8]) -> Vec<MaskedArg<'a>> {
+pub fn build_masked_args<'a>(args: &'a PickerArgs, mask: &'a [u8]) -> Vec<MaskedArg<'a>> {
let mut cidx = 0;
args.iter()
.filter_map(|r| {
diff --git a/arg_picker/src/pickable/multi_pickable.rs b/arg_picker/src/pickable/multi_pickable.rs
index 84a8068..404c23b 100644
--- a/arg_picker/src/pickable/multi_pickable.rs
+++ b/arg_picker/src/pickable/multi_pickable.rs
@@ -5,13 +5,29 @@ use crate::{
};
/// Boundary check for multi-value positional parameters.
+///
+/// Determines whether a raw string marks the end of a multi-value
+/// parameter's input range.
pub trait BoundaryCheck {
+ /// Returns `true` if `raw` indicates a boundary (i.e., the start of
+ /// a new parameter), stopping greedy collection.
fn check_boundary(raw: &str) -> bool;
}
/// Trait for multi-value parameters.
+///
+/// Implementors define how a sequence of raw strings is converted into
+/// a single value, with an associated [`BoundaryCheck`] to control where
+/// collection stops.
pub trait MultiPickableWithBoundary: Sized {
+ /// The boundary checker type that determines when to stop consuming
+ /// positional arguments.
type Checker: BoundaryCheck;
+
+ /// Parse and collect multiple raw string values into `Self`.
+ ///
+ /// The caller should stop passing additional items once the
+ /// associated [`Checker`](Self::Checker) signals a boundary.
fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self>;
}
diff --git a/arg_picker/src/picker.rs b/arg_picker/src/picker.rs
index ecab648..f722fb5 100644
--- a/arg_picker/src/picker.rs
+++ b/arg_picker/src/picker.rs
@@ -73,6 +73,26 @@ pub enum PickerArgs<'a> {
Owned(Vec<String>),
}
+impl<'a> From<PickerArgs<'a>> for Vec<String> {
+ fn from(value: PickerArgs<'a>) -> Self {
+ match value {
+ PickerArgs::Slice(items) => items.iter().map(|s| s.to_string()).collect(),
+ PickerArgs::Vec(items) => items.into_iter().map(|s| s.to_string()).collect(),
+ PickerArgs::Owned(items) => items,
+ }
+ }
+}
+
+impl<'a> From<&'a PickerArgs<'a>> for Vec<&'a str> {
+ fn from(value: &'a PickerArgs<'a>) -> Self {
+ match value {
+ PickerArgs::Slice(items) => items.to_vec(),
+ PickerArgs::Vec(items) => items.clone(),
+ PickerArgs::Owned(items) => items.iter().map(|s| s.as_str()).collect(),
+ }
+ }
+}
+
impl<'a> Default for PickerArgs<'a> {
fn default() -> Self {
Self::Vec(vec![])
@@ -138,6 +158,15 @@ impl<'a> IntoIterator for &'a PickerArgs<'a> {
}
}
+impl<'a, Route> From<PickerArgs<'a>> for Picker<'a, Route> {
+ fn from(args: PickerArgs<'a>) -> Self {
+ Picker {
+ route_phantom: PhantomData,
+ args,
+ }
+ }
+}
+
impl<'a, Route> From<&'a [&'a str]> for Picker<'a, Route> {
fn from(value: &'a [&'a str]) -> Self {
Picker {
@@ -340,11 +369,83 @@ pub trait IntoPicker<'a> {
fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, ()>
where
Self: Sized,
- N: Pickable<'a> + Default + Sized,
+ N: Pickable<'a> + Sized,
{
Picker::build_pattern1(self.to_picker().args, arg.into(), None::<()>)
}
+ /// Starts building a picker pattern with the first argument, using a default value provider.
+ ///
+ /// This is a shorthand for calling `.pick(arg).or(func)`.
+ ///
+ /// # Type Parameters
+ ///
+ /// * `Next` — The nominal type of the first argument; must implement [`Pickable`].
+ ///
+ /// # Parameters
+ ///
+ /// * `arg` — The argument definition, typically obtained from [`crate::macros::arg`].
+ /// * `func` — A closure that provides a default value if the arg is not provided by the user.
+ fn pick_or<Next, F>(
+ self,
+ arg: impl Into<&'a PickerArg<'a, Next>>,
+ func: F,
+ ) -> PickerPattern1<'a, Next, ()>
+ where
+ Self: Sized,
+ Next: Pickable<'a> + Sized,
+ F: FnMut() -> Next + 'static,
+ {
+ self.pick(arg).or(func)
+ }
+
+ /// Starts building a picker pattern with the first argument, using a default value.
+ ///
+ /// This is a shorthand for calling `.pick(arg).or_default()`.
+ ///
+ /// # Type Parameters
+ ///
+ /// * `Next` — The nominal type of the first argument; must implement [`Pickable`] and [`Default`].
+ ///
+ /// # Parameters
+ ///
+ /// * `arg` — The argument definition, typically obtained from [`crate::macros::arg`].
+ fn pick_or_default<Next>(
+ self,
+ arg: impl Into<&'a PickerArg<'a, Next>>,
+ ) -> PickerPattern1<'a, Next, ()>
+ where
+ Self: Sized,
+ Next: Pickable<'a> + Default + Sized,
+ {
+ self.pick(arg).or_default()
+ }
+
+ /// Starts building a picker pattern with the first argument, using a route if the arg is not provided.
+ ///
+ /// This is a shorthand for calling `.pick(arg).or_route(func)`.
+ ///
+ /// # Type Parameters
+ ///
+ /// * `Next` — The nominal type of the first argument; must implement [`Pickable`].
+ ///
+ /// # Parameters
+ ///
+ /// * `arg` — The argument definition, typically obtained from [`crate::macros::arg`].
+ /// * `func` — A closure that produces a route value if the arg is not provided by the user.
+ fn pick_or_route<Next, F, Route>(
+ self,
+ arg: impl Into<&'a PickerArg<'a, Next>>,
+ func: F,
+ ) -> PickerPattern1<'a, Next, Route>
+ where
+ Self: Sized,
+ Next: Pickable<'a> + Sized,
+ F: FnMut() -> Route + 'static,
+ {
+ self.pick(arg).with_route::<Route>().or_route(func)
+ }
+
/// Converts the value into a `Picker` with a specified route type.
///
/// This method allows changing the route (phantom type parameter) of the picker.
@@ -408,10 +509,9 @@ impl<'a> IntoPicker<'a> for Vec<String> {
}
}
-// Private helper: shared construction logic for `PickerPattern1`.
-// Both `Picker::pick` and `IntoPicker::pick` delegate to this.
impl<'a, Route> Picker<'a, Route> {
- pub(crate) fn build_pattern1<N>(
+ /// Build the PickerPattern via Arguments
+ pub fn build_pattern1<N>(
args: PickerArgs<'a>,
arg: &'a PickerArg<'a, N>,
error_route: Option<Route>,
diff --git a/arg_picker/src/picker/parse.rs b/arg_picker/src/picker/parse.rs
index 366028b..9db5bd9 100644
--- a/arg_picker/src/picker/parse.rs
+++ b/arg_picker/src/picker/parse.rs
@@ -21,14 +21,16 @@ internal_repeat!(1..=32 => {
internal_repeat!(1..=32 => {
impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
where (T$: Pickable<'a>,+) {
- /// Unwraps the result, panicking if a route was selected.
+ /// Unwraps the result, panicking if a route was selected or a required
+ /// value is missing.
///
/// # Panics
///
- /// Panics if a route was selected.
+ /// Panics if a route was selected, or if a required argument was not
+ /// provided by the user.
pub fn unwrap(self) -> ((T$,+)) {
let p = self.parse();
- ((p.v$.unwrap(),+))
+ ((p.v$.expect(concat!("missing required argument at position ", $)),+))
}
/// Returns the individual option values without checking the route.
diff --git a/arg_picker/src/picker/result.rs b/arg_picker/src/picker/result.rs
index 83ca2cd..2099825 100644
--- a/arg_picker/src/picker/result.rs
+++ b/arg_picker/src/picker/result.rs
@@ -21,13 +21,15 @@ internal_repeat!(1..=32 => {
internal_repeat!(1..=32 => {
impl<(T$,+), Route> PickerResult$<(T$,+), Route> {
- /// Unwraps the result, panicking if a route was selected.
+ /// Unwraps the result, panicking if a route was selected or a required
+ /// value is missing.
///
/// # Panics
///
- /// Panics if `self.route` is `Some(...)`.
+ /// Panics if `self.route` is `Some(...)`, or if a required argument was
+ /// not provided by the user.
pub fn unwrap(self) -> ((T$,+)) {
- ((self.v$.unwrap(),+))
+ ((self.v$.expect(concat!("missing required argument at position ", $)),+))
}
/// Returns the individual option values without checking the route.
diff --git a/arg_picker/src/value/vec_until.rs b/arg_picker/src/value/vec_until.rs
index 1b79641..04d87ce 100644
--- a/arg_picker/src/value/vec_until.rs
+++ b/arg_picker/src/value/vec_until.rs
@@ -21,6 +21,7 @@ pub struct VecUntil<T> {
}
impl<T> VecUntil<T> {
+ /// Consumes `self` and returns the underlying [`Vec<T>`].
pub fn into_inner(self) -> Vec<T> {
self.inner
}
diff --git a/arg_picker/test/src/test/route_test.rs b/arg_picker/test/src/test/route_test.rs
index 7594c6e..c9cd5ab 100644
--- a/arg_picker/test/src/test/route_test.rs
+++ b/arg_picker/test/src/test/route_test.rs
@@ -1,4 +1,4 @@
-use arg_picker::{macros::arg, IntoPicker};
+use arg_picker::{IntoPicker, macros::arg};
// Route mechanism — or_route
@@ -53,7 +53,7 @@ fn test_or_route_to_option_returns_none() {
}
#[test]
-#[should_panic(expected = "called `Option::unwrap()` on a `None` value")]
+#[should_panic(expected = "missing required argument")]
fn test_or_route_unwrap_panics() {
let args: Vec<&str> = vec![];
args.with_route::<&'static str>()
diff --git a/arg_picker_macros/src/arg.rs b/arg_picker_macros/src/arg.rs
index 55860c4..20731b1 100644
--- a/arg_picker_macros/src/arg.rs
+++ b/arg_picker_macros/src/arg.rs
@@ -119,6 +119,7 @@ pub(crate) fn arg(input: TokenStream) -> TokenStream {
fn split_at_commas(tokens: &[TokenTree]) -> Vec<Vec<TokenTree>> {
let mut result = vec![Vec::new()];
let mut depth = 0u32;
+ let mut angle_depth = 0u32;
for t in tokens {
match t {
TokenTree::Group(_g) => {
@@ -126,7 +127,15 @@ fn split_at_commas(tokens: &[TokenTree]) -> Vec<Vec<TokenTree>> {
result.last_mut().unwrap().push(t.clone());
depth -= 1;
}
- TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 => {
+ TokenTree::Punct(p) if p.as_char() == '<' => {
+ angle_depth += 1;
+ result.last_mut().unwrap().push(t.clone());
+ }
+ TokenTree::Punct(p) if p.as_char() == '>' && angle_depth > 0 => {
+ angle_depth -= 1;
+ result.last_mut().unwrap().push(t.clone());
+ }
+ TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 && angle_depth == 0 => {
result.push(Vec::new());
}
_ => result.last_mut().unwrap().push(t.clone()),
diff --git a/arg_picker_macros/src/lib.rs b/arg_picker_macros/src/lib.rs
index 18cce84..0132e66 100644
--- a/arg_picker_macros/src/lib.rs
+++ b/arg_picker_macros/src/lib.rs
@@ -1,4 +1,5 @@
#![doc = include_str!("../README.md")]
+#![deny(missing_docs)]
use proc_macro::TokenStream;
diff --git a/docs/_zh_CN/pages/10-help.md b/docs/_zh_CN/pages/10-help.md
index 8cee2aa..1c4e410 100644
--- a/docs/_zh_CN/pages/10-help.md
+++ b/docs/_zh_CN/pages/10-help.md
@@ -13,18 +13,17 @@ Mingling 里用 `#[help]` 宏给命令添加帮助文本。
```rust
@@@use mingling::macros::help;
+@@@use mingling::macros::buffer;
@@@dispatcher!("greet", CMDGreet => EntryGreet);
-#[help]
-fn help_greet(_entry: EntryGreet) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Usage: greet [name]").ok();
- writeln!(r, "Say hello to someone.").ok();
- r
+#[help(buffer)]
+fn help_greet(_entry: EntryGreet) {
+ r_println!("Usage: greet [name]");
+ r_println!("Say hello to someone.");
}
```
> [!NOTE]
-> 帮助函数同样通过 `writeln!` 向 `RenderResult` 写入内容,因为 `#[help]` 遵循渲染管线 —— 它是由 `--help` 标志提前触发的短路渲染,而不是管线之外的逻辑。
+> 帮助函数同样通过 `r_println!` 向 `RenderResult` 写入内容,因为 `#[help]` 遵循渲染管线 —— 它是由 `--help` 标志提前触发的短路渲染,而不是管线之外的逻辑。
## 全局帮助
@@ -32,14 +31,13 @@ fn help_greet(_entry: EntryGreet) -> RenderResult {
```rust
@@@use mingling::macros::help;
+@@@use mingling::macros::buffer;
// 用户直接输入 --help 时触发
-#[help]
-fn help_root(entry: ErrorDispatcherNotFound) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Usage: my-cli <command>").ok();
- writeln!(r, "Commands:").ok();
- writeln!(r, " greet Say hello").ok();
- r
+#[help(buffer)]
+fn help_root(entry: ErrorDispatcherNotFound) {
+ r_println!("Usage: my-cli <command>");
+ r_println!("Commands:");
+ r_println!(" greet Say hello");
}
```
diff --git a/docs/_zh_CN/pages/11-resource-system.md b/docs/_zh_CN/pages/11-resource-system.md
index a8aab4d..b127a3a 100644
--- a/docs/_zh_CN/pages/11-resource-system.md
+++ b/docs/_zh_CN/pages/11-resource-system.md
@@ -27,6 +27,7 @@ fn main() {
在 Chain 或 Renderer 中,只需在参数列表里声明你要的资源:
```rust
+@@@use mingling::macros::buffer;
@@@#[derive(Default, Clone)]
@@@struct ResCurrentDir(String);
@@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir);
@@ -37,11 +38,9 @@ fn handle_pwd(_args: EntryPrintWorkingDir, cwd: &ResCurrentDir) -> Next {
ResultPath::new(cwd.0.clone()).to_render()
}
-#[renderer]
-fn render_path(result: ResultPath) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "{}", *result).ok();
- r
+#[renderer(buffer)]
+fn render_path(result: ResultPath) {
+ r_println!("{}", *result);
}
```
@@ -50,6 +49,7 @@ fn render_path(result: ResultPath) -> RenderResult {
用 `&mut T` 注入可修改资源:
```rust
+@@@use mingling::macros::buffer;
@@@#[derive(Default, Clone)]
@@@struct ResVisitCount(u32);
@@@dispatcher!("visit", CMDVisit => EntryVisit);
@@ -60,11 +60,9 @@ fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next {
ResultDone::default().into()
}
-#[renderer]
-fn render_done(_done: ResultDone, counter: &ResVisitCount) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "visit count is : {}", counter.0).ok();
- r
+#[renderer(buffer)]
+fn render_done(_done: ResultDone, counter: &ResVisitCount) {
+ r_println!("visit count is : {}", counter.0);
}
```
diff --git a/docs/_zh_CN/pages/14-testing.md b/docs/_zh_CN/pages/14-testing.md
index 621d43e..031be59 100644
--- a/docs/_zh_CN/pages/14-testing.md
+++ b/docs/_zh_CN/pages/14-testing.md
@@ -16,7 +16,7 @@ Renderer 是最容易测试的——调用函数,断言返回结果:
#[renderer]
fn render_greet(result: ResultName) -> RenderResult {
let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *result).ok();
+ r_println!(r, "Hello, {}!", *result);
r
}
diff --git a/docs/_zh_CN/pages/3-define-a-chain.md b/docs/_zh_CN/pages/3-define-a-chain.md
index b2850c5..f845d11 100644
--- a/docs/_zh_CN/pages/3-define-a-chain.md
+++ b/docs/_zh_CN/pages/3-define-a-chain.md
@@ -48,7 +48,7 @@ Chain 函数签名里写着它需要什么——`args: EntryGreet`
```rust
// pack!(ResultName = String) 大概生成了这样的代码
-#[derive(Groupped)]
+#[derive(Grouped)]
pub struct ResultName {
pub inner: String,
}
diff --git a/docs/_zh_CN/pages/4-render-result.md b/docs/_zh_CN/pages/4-render-result.md
index 81c5102..8b29fca 100644
--- a/docs/_zh_CN/pages/4-render-result.md
+++ b/docs/_zh_CN/pages/4-render-result.md
@@ -10,18 +10,31 @@
跟 `#[chain]` 类似,`#[renderer]` 用于标记一个输出函数:
```rust
-use std::io::Write;
+@@@use mingling::macros::buffer;
+@@@pack!(ResultName = String);
+#[renderer(buffer)]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
+}
+```
+
+Renderer 接收 Chain 产出的结果,然后返回一个 `RenderResult`。在函数内部,创建 `RenderResult`,用 `r_print!` / `r_println!` 写入内容,最后返回它。
+
+## `buffer` 拓展
+
+若您觉得显式创建并返回 `RenderResult` 过于繁琐,可以使用 `#[renderer(buffer)]` 在原始函数内植入一个缓冲区。
+
+```rust
+use mingling::macros::buffer;
@@@pack!(ResultName = String);
-#[renderer]
-fn render_name(name: ResultName) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "Hello, {}!", *name).ok();
- result
+#[renderer(buffer)]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
}
```
-Renderer 接收 Chain 产出的结果,然后返回一个 `RenderResult`。在函数内部,创建 `RenderResult`,用 `write!` / `writeln!`(来自 [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html))写入内容,最后返回它。
+这样,你的 renderer 函数就获得了更简洁的语法,但也引入了一个隐含机制:它会向函数内注入一个名为 `__render_result_buffer` 的可变 `RenderResult`。当 `r_print!` 宏没有显式指定 `RenderResult` 时,它便会按照约定向该缓冲区追加输出。
## `RenderResult` 类型
@@ -36,7 +49,7 @@ Renderer 接收 Chain 产出的结果,然后返回一个 `RenderResult`。在
把三篇教程的内容合在一起,你的第一个 Mingling 程序就完整了:
```rust
-use std::io::Write;
+use mingling::macros::buffer;
// 1. 用 Dispatcher 声明命令
dispatcher!("greet", CMDGreet => EntryGreet);
@@ -55,11 +68,9 @@ fn handle_greet(args: EntryGreet) -> Next {
}
// 4. 用 Renderer 输出结果
-#[renderer]
-fn render_name(name: ResultName) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "Hello, {}!", *name).ok();
- result
+#[renderer(buffer)]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
}
// 5. 在 main 函数内装配程序并运行
@@ -108,17 +119,15 @@ cargo run -- great
`gen_program!()` 自动生成了一个 `ErrorDispatcherNotFound` 类型,包裹 `Vec<String>`——它存的是用户输入的那些没匹配到的命令。你只需要给它写一个 Renderer:
```rust
-use std::io::Write;
+use mingling::macros::buffer;
-#[renderer]
-fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult {
- let mut result = RenderResult::new();
+#[renderer(buffer)]
+fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) {
if err.inner.is_empty() {
- writeln!(result, "Unknown command").ok();
+ r_println!("Unknown command");
} else {
- writeln!(result, "Command not found: \"{}\"", err.inner.join(" ")).ok();
+ r_println!("Command not found: \"{}\"", err.inner.join(" "));
}
- result
}
```
diff --git a/docs/_zh_CN/pages/5-multiple-commands.md b/docs/_zh_CN/pages/5-multiple-commands.md
index 38ce2cf..ff6596d 100644
--- a/docs/_zh_CN/pages/5-multiple-commands.md
+++ b/docs/_zh_CN/pages/5-multiple-commands.md
@@ -10,6 +10,7 @@
继续在同一个项目里操作:
```rust
+@@@use mingling::macros::buffer;
// 声明两个命令
dispatcher!("greet", CMDGreet => EntryGreet);
dispatcher!("add", CMDAdd => EntryAdd);
@@ -29,18 +30,14 @@ fn handle_add(args: EntryAdd) -> Next {
ResultSum::new(sum).into()
}
-#[renderer]
-fn render_greet(result: ResultGreeting) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *result).ok();
- r
+#[renderer(buffer)]
+fn render_greet(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
}
-#[renderer]
-fn render_sum(result: ResultSum) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Sum: {}", *result).ok();
- r
+#[renderer(buffer)]
+fn render_sum(result: ResultSum) {
+ r_println!("Sum: {}", *result);
}
fn main() {
diff --git a/docs/_zh_CN/pages/6-argument-parse-picker.md b/docs/_zh_CN/pages/6-argument-parse-picker.md
index 4bf162c..f0609ed 100644
--- a/docs/_zh_CN/pages/6-argument-parse-picker.md
+++ b/docs/_zh_CN/pages/6-argument-parse-picker.md
@@ -139,6 +139,7 @@ fn handle_test_entry(prev: EntryTest) -> Next {
```rust
// Features: ["parser", "extra_macros"]
+@@@use mingling::macros::buffer;
@@@use mingling::macros::route;
@@@dispatcher!("greet", CMDGreet => EntryGreet);
@@@pack!(ResultName = String);
@@ -155,11 +156,9 @@ fn handle_greet_entry(prev: EntryGreet) -> Next {
ResultName::new(name).into()
}
-#[renderer]
-fn render_greet(result: ResultName) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *result).ok();
- r
+#[renderer(buffer)]
+fn render_greet(result: ResultName) {
+ r_println!("Hello, {}!", *result);
}
```
@@ -225,6 +224,7 @@ fn handle_greet_entry(prev: EntryGreet) -> Next {
```rust
// Features: ["parser", "extra_macros"]
+@@@use mingling::macros::buffer;
@@@use mingling::macros::route;
@@@dispatcher!("greet", CMDGreet => EntryGreet);
@@@pack!(ResultName = String);
@@ -247,19 +247,15 @@ fn handle_greet_entry(prev: EntryGreet) -> Next {
ResultName::new(name).into()
}
-#[renderer]
-fn render_name_too_long(prev: ErrorNameTooLong) -> RenderResult {
- let mut r = RenderResult::new();
+#[renderer(buffer)]
+fn render_name_too_long(prev: ErrorNameTooLong) {
let len = *prev;
- writeln!(r, "Error: name too long (length: {} > 32)", len).ok();
- r
+ r_println!("Error: name too long (length: {} > 32)", len);
}
-#[renderer]
-fn render_name(prev: ResultName) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *prev).ok();
- r
+#[renderer(buffer)]
+fn render_name(prev: ResultName) {
+ r_println!("Hello, {}!", *prev);
}
```
@@ -314,6 +310,7 @@ fn parse_size() {
```rust
// Features: ["parser"]
+@@@use mingling::macros::buffer;
@@@use mingling::parser::{Pickable, Argument};
@@@use mingling::Flag;
#[derive(Default, Clone)]
@@ -341,11 +338,9 @@ fn handle_connect_entry(prev: EntryConnect) -> Next {
ResultConnected::new(address).into()
}
-#[renderer]
-fn render_connected(addr: ResultConnected) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Connected: IP: {} PORT: {}", addr.ip, addr.port).ok();
- r
+#[renderer(buffer)]
+fn render_connected(addr: ResultConnected) {
+ r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port);
}
```
@@ -362,6 +357,7 @@ Connected: IP: 127.0.0.1 PORT: 8080
```rust
// Features: ["parser"]
+@@@use mingling::macros::buffer;
@@@use mingling::parser::PickableEnum;
@@@use mingling::EnumTag;
#[derive(Debug, Default, EnumTag)]
@@ -382,11 +378,9 @@ fn handle_eat_entry(prev: EntryEat) -> Next {
ResultFruit::new(fruit).into()
}
-#[renderer]
-fn render_fruit(prev: ResultFruit) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Picked fruit: {:?}", *prev).ok();
- r
+#[renderer(buffer)]
+fn render_fruit(prev: ResultFruit) {
+ r_println!("Picked fruit: {:?}", *prev);
}
```
@@ -395,3 +389,4 @@ fn render_fruit(prev: ResultFruit) -> RenderResult {
<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
index 7dce301..fda93f3 100644
--- a/docs/_zh_CN/pages/7-argument-parse-clap.md
+++ b/docs/_zh_CN/pages/7-argument-parse-clap.md
@@ -25,7 +25,8 @@ features = ["derive", "color"]
// Dependencies:
// clap = "4"
@@@ use mingling::macros::dispatcher_clap;
-#[derive(Default, clap::Parser, Groupped)]
+@@@ use mingling::macros::buffer;
+#[derive(Default, clap::Parser, Grouped)]
#[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)]
pub struct EntryGreet {
#[clap(default_value = "World")]
@@ -34,23 +35,19 @@ pub struct EntryGreet {
repeat: i32,
}
-#[renderer]
-fn render_greet(greet: EntryGreet) -> RenderResult {
- let mut r = RenderResult::new();
+#[renderer(buffer)]
+fn render_greet(greet: EntryGreet) {
let count = greet.repeat.max(0) as usize;
- write!(r, "Hello, ").ok();
+ r_print!("Hello, ");
for _ in 0..count {
- write!(r, "{} ", greet.name).ok();
+ r_print!("{} ", greet.name);
}
- writeln!(r, "!").ok();
- r
+ r_println!("!");
}
-#[renderer]
-fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "{}", *err).ok();
- r
+#[renderer(buffer)]
+fn render_greet_parse_failed(err: ErrorGreetParsed) {
+ r_println!("{}", *err);
}
```
@@ -62,18 +59,17 @@ fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult {
// Features: ["clap"]
// Dependencies:
// clap = "4"
+@@@use mingling::macros::buffer;
@@@use mingling::setup::BasicProgramSetup;
@@@use mingling::macros::dispatcher_clap;
-@@@#[derive(Default, clap::Parser, Groupped)]
+@@@#[derive(Default, clap::Parser, Grouped)]
@@@#[dispatcher_clap("greet", CMDGreet)]
@@@pub struct EntryGreet {
@@@ name: String,
@@@}
-@@@#[renderer]
-@@@fn render_greet(greet: EntryGreet) -> RenderResult {
-@@@ let mut r = RenderResult::new();
-@@@ write!(r, "Hello, {}!", greet.name).ok();
-@@@ r
+@@@#[renderer(buffer)]
+@@@fn render_greet(greet: EntryGreet) {
+@@@ r_println!("Hello, {}!", greet.name);
@@@}
fn main() {
let mut program = ThisProgram::new();
diff --git a/docs/_zh_CN/pages/9-error-handling.md b/docs/_zh_CN/pages/9-error-handling.md
index 07c82da..4ce61ab 100644
--- a/docs/_zh_CN/pages/9-error-handling.md
+++ b/docs/_zh_CN/pages/9-error-handling.md
@@ -38,23 +38,20 @@ fn handle_greet(args: EntryGreet) -> Next {
然后各自写 Renderer:
```rust
+@@@use mingling::macros::buffer;
@@@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_greet(result: ResultGreeting) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *result).ok();
- r
+#[renderer(buffer)]
+fn render_greet(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
}
-#[renderer]
-fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Error: {}", *err).ok();
- r
+#[renderer(buffer)]
+fn render_error_name_empty(err: ErrorNameEmpty) {
+ r_println!("Error: {}", *err);
}
```
@@ -63,6 +60,7 @@ fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult {
## 完整的例子
```rust
+@@@use mingling::macros::buffer;
dispatcher!("greet", CMDGreet => EntryGreet);
pack!(ResultGreeting = String);
@@ -78,18 +76,14 @@ fn handle_greet(args: EntryGreet) -> Next {
}
}
-#[renderer]
-fn render_greet(result: ResultGreeting) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *result).ok();
- r
+#[renderer(buffer)]
+fn render_greet(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
}
-#[renderer]
-fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Error: {}", *err).ok();
- r
+#[renderer(buffer)]
+fn render_error_name_empty(err: ErrorNameEmpty) {
+ r_println!("Error: {}", *err);
}
fn main() {
diff --git a/docs/_zh_CN/pages/advanced/2-structural-renderer.md b/docs/_zh_CN/pages/advanced/2-structural-renderer.md
index 6b6b0f9..a498dac 100644
--- a/docs/_zh_CN/pages/advanced/2-structural-renderer.md
+++ b/docs/_zh_CN/pages/advanced/2-structural-renderer.md
@@ -27,6 +27,7 @@ features = ["structural_renderer"]
// Features: ["structural_renderer"]
// Dependencies:
// serde = "1"
+@@@use mingling::macros::buffer;
@@@use mingling::setup::StructuralRendererSetup;
@@@dispatcher!("render", CMDRender => EntryRender);
@@ -40,11 +41,9 @@ fn handle_render(args: EntryRender) -> Next {
ResultInfo::new((name, age)).into()
}
-#[renderer]
-fn render_info(r: ResultInfo) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "{:?}", *r).ok();
- result
+#[renderer(buffer)]
+fn render_info(r: ResultInfo) {
+ r_println!("{:?}", *r);
}
```
@@ -62,19 +61,20 @@ fn render_info(r: ResultInfo) -> RenderResult {
## 自定义输出结构
-`pack_structural!` 的默认输出包含 `inner` 字段。要完全控制输出结构,可以用 `#[derive(StructuralData, Serialize, Groupped)]` 手动定义类型:
+`pack_structural!` 的默认输出包含 `inner` 字段。要完全控制输出结构,可以用 `#[derive(StructuralData, Serialize, Grouped)]` 手动定义类型:
```rust
// Features: ["structural_renderer"]
// Dependencies:
// serde = "1"
+@@@use mingling::macros::buffer;
@@@use mingling::prelude::*;
@@@use mingling::setup::StructuralRendererSetup;
@@@use mingling::StructuralData;
@@@use serde::Serialize;
@@@dispatcher!("render", CMDRender => EntryRender);
-#[derive(Serialize, StructuralData, Groupped)]
+#[derive(Serialize, StructuralData, Grouped)]
struct Info {
name: String,
age: i32,
@@ -87,11 +87,9 @@ fn handle_render(args: EntryRender) -> Next {
Info { name, age }.to_render()
}
-#[renderer]
-fn render_info(info: Info) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "{} is {} years old", info.name, info.age).ok();
- r
+#[renderer(buffer)]
+fn render_info(info: Info) {
+ r_println!("{} is {} years old", info.name, info.age);
}
@@@
@@@fn main() {
diff --git a/docs/_zh_CN/pages/concepts/3-any-output.md b/docs/_zh_CN/pages/concepts/3-any-output.md
index 9b820da..118e856 100644
--- a/docs/_zh_CN/pages/concepts/3-any-output.md
+++ b/docs/_zh_CN/pages/concepts/3-any-output.md
@@ -20,7 +20,7 @@ AnyOutput<G>
这里的 `G` 就是 `gen_program!()` 生成的程序枚举(也就是你熟知的 `ThisProgram`)。
-每个被 `pack!` 或 `#[derive(Groupped)]` 标记的类型都被分配到这个枚举的一个变体。
+每个被 `pack!` 或 `#[derive(Grouped)]` 标记的类型都被分配到这个枚举的一个变体。
## ChainProcess:数据 + 路由
@@ -38,17 +38,17 @@ ChainProcess<G>
调度器根据 `NextProcess` 决定是继续循环还是退出渲染。
-## Groupped:谁是谁
+## Grouped:谁是谁
-调度器如何知道 `AnyOutput` 里装的是 `ResultName` 还是 `ErrorUserBlocked`?答案是 `Groupped` trait:
+调度器如何知道 `AnyOutput` 里装的是 `ResultName` 还是 `ErrorUserBlocked`?答案是 `Grouped` trait:
```
-trait Groupped<G> {
+trait Grouped<G> {
fn member_id() -> G;
}
```
-当你用 `pack!(ResultName = String)` 时,宏自动为 `ResultName` 实现 `Groupped`,`member_id()` 返回枚举中对应的变体。调度器一看 `member_id`,就去找对应的 Chain 或 Renderer。
+当你用 `pack!(ResultName = String)` 时,宏自动为 `ResultName` 实现 `Grouped`,`member_id()` 返回枚举中对应的变体。调度器一看 `member_id`,就去找对应的 Chain 或 Renderer。
`to_chain()` 和 `to_render()` 本质上是 `AnyOutput` 的快捷方法,分别构造 `ChainProcess::Ok(any, Chain)` 和 `ChainProcess::Ok(any, Renderer)`。
diff --git a/docs/_zh_CN/pages/other/features.md b/docs/_zh_CN/pages/other/features.md
index d92e597..8bd386c 100644
--- a/docs/_zh_CN/pages/other/features.md
+++ b/docs/_zh_CN/pages/other/features.md
@@ -171,7 +171,7 @@ fn handle_hello(args: EntryHello) {}
### `group!`
将外部类型注册为程序组成员,无需修改原始类型的定义。
-类型名会直接作为枚举变体,与 `pack!` 或 `#[derive(Groupped)]` 一致。
+类型名会直接作为枚举变体,与 `pack!` 或 `#[derive(Grouped)]` 一致。
```rust
// Features: ["extra_macros"]
@@ -274,12 +274,24 @@ analyze_and_build_type_mapping().unwrap();
**介绍:**
-启用解析器模块,提供文本解析和语法分析功能。
+启用参数解析器模块,提供参数解析功能。
-开启后可以使用 `Picker` 进行零成本的参数提取,支持 `pick()` 和 `pick_or()` 等方法。
+开启后可以使用 `Picker` 进行简易的参数提取,支持 `pick()` 和 `pick_or()` 等方法。
详见 [示例](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-argument-parse)
+## 特性 `picker`
+
+**介绍:**
+
+引入依赖 `arg-picker`,为 Mingling 提供更高级的参数解析能力。
+
+它可以与 `parser`、`clap` 特性共存,但建议不要和 `parser` 特性同时启用,因为两者的 API 极为相似。
+
+`picker` 是独立于 Mingling 的参数解析器,不依赖 `mingling_core` 的内置参数提取 API。
+
+详见 [示例](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-argument-picker)
+
## 特性 `repl`
**介绍:**
diff --git a/docs/_zh_CN/pages/other/naming_rule.md b/docs/_zh_CN/pages/other/naming_rule.md
index 5f2ac34..6e65ee4 100644
--- a/docs/_zh_CN/pages/other/naming_rule.md
+++ b/docs/_zh_CN/pages/other/naming_rule.md
@@ -96,7 +96,7 @@ Result + 内容
| `ResultGreetSomeone` | 问候结果 |
| `ResultFruitList` | 水果列表结果 |
-结果结构体期望被 Renderer 消费,内部结构应该为了渲染美观而设计。一般用 `#[derive(Groupped)]` 代替 `pack!()` 包装,以获得更灵活的字段控制。
+结果结构体期望被 Renderer 消费,内部结构应该为了渲染美观而设计。一般用 `#[derive(Grouped)]` 代替 `pack!()` 包装,以获得更灵活的字段控制。
### 错误
@@ -166,6 +166,7 @@ fn handle_remote_add(args: EntryRemoteAdd, cwd: &ResCurrentDir, db: &mut ResData
## 完整示例
```rust
+@@@use mingling::macros::buffer;
@@@ #[derive(Default, Clone)]
@@@ struct ResDatabase { }
@@@ impl ResDatabase { fn has_remote(&self, remote: &String) -> bool { true } }
@@ -192,19 +193,15 @@ fn handle_state_operation_remotes(state: StateOperationRemotes, db: &ResDatabase
}
// 结果渲染
-#[renderer]
-fn render_remote_added(result: ResultRemoteAdded) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Remote added: {}", result.inner).ok();
- r
+#[renderer(buffer)]
+fn render_remote_added(result: ResultRemoteAdded) {
+ r_println!("Remote added: {}", result.inner);
}
// 错误渲染
-#[renderer]
-fn render_error_repository_not_found(err: ErrorRepositoryNotFound) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Error: remote '{}' not found", err.inner).ok();
- r
+#[renderer(buffer)]
+fn render_error_repository_not_found(err: ErrorRepositoryNotFound) {
+ r_println!("Error: remote '{}' not found", err.inner);
}
```
diff --git a/docs/dev/pages/issues/add-picker2.md b/docs/dev/pages/issues/add-picker2.md
index f5c3ca7..a2b5a10 100644
--- a/docs/dev/pages/issues/add-picker2.md
+++ b/docs/dev/pages/issues/add-picker2.md
@@ -40,7 +40,7 @@ fn handle_hello(args: EntryHello) {
```rust
#[chain]
fn handle_hello(args: EntryHello) -> Next {
- // requires impl Groupped<_>
+ // requires impl Grouped<_>
// |
let parsed = args // vvvvvvvvvvvvvvvvvvv
.pick_route::<String, _>(positional!(), ErrorNoNameProvided)
@@ -90,11 +90,11 @@ pub struct PickerResult<Tuple> {
## 🕘 Progress
- [x] In Progress
- - [x] Added `Picker` struct and related call chain
- - [x] Added `parselib` providing parsing logic
- - [x] Added `Pickable` for extensibility
- - [ ] Comprehensive testing!
- - [ ] Improve documentation
- - [ ] Add examples
- - [ ] Update README
+ - [x] Added `Picker` struct and related call chain
+ - [x] Added `parselib` providing parsing logic
+ - [x] Added `Pickable` for extensibility
+ - [x] Comprehensive testing!
+ - [ ] Improve documentation
+ - [x] Add examples
+ - [ ] Update README
- [ ] Complete
diff --git a/docs/dev/pages/issues/remove-r-print-macro.md b/docs/dev/pages/issues/remove-r-print-macro.md
index d6f6bff..43d2740 100644
--- a/docs/dev/pages/issues/remove-r-print-macro.md
+++ b/docs/dev/pages/issues/remove-r-print-macro.md
@@ -89,9 +89,9 @@ As for rendering in logic functions like `#[chain]`, that should be handled by a
## 🕘 Progress
- [x] In Progress
- - [x] Remove `r_println!` and `r_print!` macros
- - [x] Modify `#[renderer]` and `#[help]` macros, remove implicit injection
- - [x] Provide **no-return-value mode** and **RenderResult return value mode** for `#[renderer]` and `#[help]` macros
- - [ ] Add new simplified syntax
- - [x] Update documentation and test cases, ensure **all pass**
-- [ ] Complete
+ - [x] Remove `r_println!` and `r_print!` macros
+ - [x] Modify `#[renderer]` and `#[help]` macros, remove implicit injection
+ - [x] Provide **no-return-value mode** and **RenderResult return value mode** for `#[renderer]` and `#[help]` macros
+ - [x] Add new simplified syntax
+ - [x] Update documentation and test cases, ensure **all pass**
+- [x] Complete
diff --git a/docs/example-pages/examples.json b/docs/example-pages/examples.json
index a539011..2a07365 100644
--- a/docs/example-pages/examples.json
+++ b/docs/example-pages/examples.json
@@ -31,6 +31,21 @@
]
},
{
+ "id": "example-argument-picker",
+ "name": "Argument Picker",
+ "icon": "📋",
+ "category": "parsing",
+ "desc": "Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line.\n",
+ "tags": [
+ "arg-picker",
+ "SinglePickable"
+ ],
+ "files": [
+ "src/main.rs",
+ "Cargo.toml"
+ ]
+ },
+ {
"id": "example-async-support",
"name": "Async Support",
"icon": "⚡",
diff --git a/docs/pages/10-help.md b/docs/pages/10-help.md
index c17f410..1e3ea78 100644
--- a/docs/pages/10-help.md
+++ b/docs/pages/10-help.md
@@ -13,18 +13,17 @@ Write a help function directly for an Entry:
```rust
@@@use mingling::macros::help;
+@@@use mingling::macros::buffer;
@@@dispatcher!("greet", CMDGreet => EntryGreet);
-#[help]
-fn help_greet(entry: EntryGreet) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Usage: greet [name]").ok();
- writeln!(r, "Say hello to someone.").ok();
- r
+#[help(buffer)]
+fn help_greet(_entry: EntryGreet) {
+ r_println!("Usage: greet [name]");
+ r_println!("Say hello to someone.");
}
```
> [!NOTE]
-> Help functions also use `writeln!` into a `RenderResult`, because `#[help]` follows the rendering pipeline — it's a short-circuit render triggered early by the `--help` flag, not logic outside the pipeline.
+> Help functions also use `r_println!` into a `RenderResult`, 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
@@ -32,14 +31,13 @@ You can also write help for `ErrorDispatcherNotFound` as the "root help":
```rust
@@@use mingling::macros::help;
+@@@use mingling::macros::buffer;
// Triggered when user passes --help directly
-#[help]
-fn help_root(entry: ErrorDispatcherNotFound) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Usage: my-cli <command>").ok();
- writeln!(r, "Commands:").ok();
- writeln!(r, " greet Say hello").ok();
- r
+#[help(buffer)]
+fn help_root(entry: ErrorDispatcherNotFound) {
+ r_println!("Usage: my-cli <command>");
+ r_println!("Commands:");
+ r_println!(" greet Say hello");
}
```
diff --git a/docs/pages/11-resource-system.md b/docs/pages/11-resource-system.md
index 9491212..0e2bd19 100644
--- a/docs/pages/11-resource-system.md
+++ b/docs/pages/11-resource-system.md
@@ -27,6 +27,7 @@ Since `ResCurrentDir` implements both `Default` and `Clone`, the framework autom
In a Chain or Renderer, simply declare the resource in the parameter list:
```rust
+@@@use mingling::macros::buffer;
@@@#[derive(Default, Clone)]
@@@struct ResCurrentDir(String);
@@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir);
@@ -37,11 +38,9 @@ fn handle_pwd(_args: EntryPrintWorkingDir, cwd: &ResCurrentDir) -> Next {
ResultPath::new(cwd.0.clone()).to_render()
}
-#[renderer]
-fn render_path(result: ResultPath) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "{}", *result).ok();
- r
+#[renderer(buffer)]
+fn render_path(result: ResultPath) {
+ r_println!("{}", *result);
}
```
@@ -50,6 +49,7 @@ fn render_path(result: ResultPath) -> RenderResult {
Use `&mut T` to inject a mutable resource:
```rust
+@@@use mingling::macros::buffer;
@@@#[derive(Default, Clone)]
@@@struct ResVisitCount(u32);
@@@dispatcher!("visit", CMDVisit => EntryVisit);
@@ -60,11 +60,9 @@ fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next {
ResultDone::default().into()
}
-#[renderer]
-fn render_done(_done: ResultDone, counter: &ResVisitCount) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "visit count is : {}", counter.0).ok();
- r
+#[renderer(buffer)]
+fn render_done(_done: ResultDone, counter: &ResVisitCount) {
+ r_println!("visit count is : {}", counter.0);
}
```
diff --git a/docs/pages/14-testing.md b/docs/pages/14-testing.md
index fa196b0..65fedc9 100644
--- a/docs/pages/14-testing.md
+++ b/docs/pages/14-testing.md
@@ -16,7 +16,7 @@ Renderer is the easiest to test — call the function, assert the result:
#[renderer]
fn render_greet(result: ResultName) -> RenderResult {
let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *result).ok();
+ r_println!(r, "Hello, {}!", *result);
r
}
diff --git a/docs/pages/3-define-a-chain.md b/docs/pages/3-define-a-chain.md
index b2822bc..450522b 100644
--- a/docs/pages/3-define-a-chain.md
+++ b/docs/pages/3-define-a-chain.md
@@ -48,7 +48,7 @@ You've probably guessed it — `pack!(ResultName = String)` defines a type that
```rust
// pack!(ResultName = String) generates code roughly like this
-#[derive(Groupped)]
+#[derive(Grouped)]
pub struct ResultName {
pub inner: String,
}
diff --git a/docs/pages/4-render-result.md b/docs/pages/4-render-result.md
index ca1e563..9e72d09 100644
--- a/docs/pages/4-render-result.md
+++ b/docs/pages/4-render-result.md
@@ -7,21 +7,34 @@ Now we've created a Dispatcher and a Chain, and produced a Result type via `pack
## The `#[renderer]` Macro
-Similar to `#[chain]`, `#[renderer]` marks an output function:
+Similar to `#[chain]`, `#[renderer]` marks a function that produces output:
```rust
-use std::io::Write;
+@@@use mingling::macros::buffer;
+@@@pack!(ResultName = String);
+#[renderer(buffer)]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
+}
+```
-pack!(ResultName = String);
-#[renderer]
-fn render_name(name: ResultName) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "Hello, {}!", *name).ok();
- result
+A Renderer receives the result produced by a Chain and returns a `RenderResult`. Inside the function, you create a `RenderResult`, write content with `r_print!` / `r_println!`, and finally return it.
+
+## The `buffer` Extension
+
+If you find explicitly creating and returning a `RenderResult` too verbose, you can use `#[renderer(buffer)]` to inject a buffer directly into your function.
+
+```rust
+use mingling::macros::buffer;
+
+@@@pack!(ResultName = String);
+#[renderer(buffer)]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
}
```
-A Renderer takes the result produced by a Chain and returns a `RenderResult`. Inside the function, create a `RenderResult`, write content using `write!` / `writeln!` (from [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html)), and return it.
+This gives your renderer function a more concise syntax, but also introduces an implicit mechanism: it injects a mutable `RenderResult` variable named `__render_result_buffer` into the function. When the `r_print!` macro is used without an explicit `RenderResult`, it will append output to that buffer by convention.
## The `RenderResult` Type
@@ -36,7 +49,7 @@ A Renderer takes the result produced by a Chain and returns a `RenderResult`. In
Putting all three tutorials together, here's your first complete Mingling program:
```rust
-use std::io::Write;
+use mingling::macros::buffer;
// 1. Declare commands with a Dispatcher
dispatcher!("greet", CMDGreet => EntryGreet);
@@ -55,11 +68,9 @@ fn handle_greet(args: EntryGreet) -> Next {
}
// 4. Output results with a Renderer
-#[renderer]
-fn render_name(name: ResultName) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "Hello, {}!", *name).ok();
- result
+#[renderer(buffer)]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
}
// 5. Assemble and run the program in main
@@ -108,17 +119,15 @@ cargo run -- great
`gen_program!()` auto-generates an `ErrorDispatcherNotFound` type wrapping `Vec<String>`—it holds the user input that didn't match any command. You just need to write a Renderer for it:
```rust
-use std::io::Write;
+use mingling::macros::buffer;
-#[renderer]
-fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) -> RenderResult {
- let mut result = RenderResult::new();
+#[renderer(buffer)]
+fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) {
if err.inner.is_empty() {
- writeln!(result, "Unknown command").ok();
+ r_println!("Unknown command");
} else {
- writeln!(result, "Command not found: \"{}\"", err.inner.join(" ")).ok();
+ r_println!("Command not found: \"{}\"", err.inner.join(" "));
}
- result
}
```
diff --git a/docs/pages/5-multiple-commands.md b/docs/pages/5-multiple-commands.md
index d9a335a..0b71e49 100644
--- a/docs/pages/5-multiple-commands.md
+++ b/docs/pages/5-multiple-commands.md
@@ -10,6 +10,7 @@ Real-world CLIs rarely have just one command. Let's extend our previous greet pr
Work in the same project:
```rust
+@@@use mingling::macros::buffer;
// Declare two commands
dispatcher!("greet", CMDGreet => EntryGreet);
dispatcher!("add", CMDAdd => EntryAdd);
@@ -29,18 +30,14 @@ fn handle_add(args: EntryAdd) -> Next {
ResultSum::new(sum).into()
}
-#[renderer]
-fn render_greet(result: ResultGreeting) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *result).ok();
- r
+#[renderer(buffer)]
+fn render_greet(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
}
-#[renderer]
-fn render_sum(result: ResultSum) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Sum: {}", *result).ok();
- r
+#[renderer(buffer)]
+fn render_sum(result: ResultSum) {
+ r_println!("Sum: {}", *result);
}
fn main() {
diff --git a/docs/pages/6-argument-parse-picker.md b/docs/pages/6-argument-parse-picker.md
index 398cd3c..01f1c37 100644
--- a/docs/pages/6-argument-parse-picker.md
+++ b/docs/pages/6-argument-parse-picker.md
@@ -3,7 +3,7 @@
Use Picker to perform basic argument parsing
</p>
-In previous tutorials, we extracted args manually from `EntryGreet.inner` (`Vec<String>`).
+In previous tutorials, we manually extracted parameters from `EntryGreet.inner` (`Vec<String>`).
```rust
@@@ fn main() {
@@ -12,7 +12,7 @@ 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.
+But this approach doesn't scale well when there are many params. Mingling provides `Picker` — a chained API for extracting and transforming params.
To enable `Picker`, update your `Cargo.toml`:
@@ -22,7 +22,7 @@ To enable `Picker`, update your `Cargo.toml`:
features = ["parser"]
```
-Now let's look at `Picker` in action:
+Now let's see how `Picker` is written:
```rust
// Features: ["parser"]
@@ -36,9 +36,9 @@ fn handle_greet_entry(prev: EntryGreet) -> Next {
}
```
-`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.
+`AsPicker` implements `pick`, `pick_or`, and `pick_or_route` for all types convertible to `Vec<String>`. These functions semantically **pick** params from the string list and convert them into structured data.
-Breaking down the example above:
+For the code above:
```rust
// Features: ["parser"]
@@ -62,17 +62,17 @@ Its semantics are:
@@@let name: String =
prev.pick_or((), "World").unpack();
// ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~
-// | | | | |_ unpack to String
+// | | | | |_ unpack as String
// | | | |__________ default value "World"
// | | |______________ pick the first positional arg (no flag)
// | |______________________ pick or use default
-// |___________________________ from previous input
+// |___________________________ from the previous input
@@@}
```
-## Parsing Flag Args
+## Parsing Flag Arguments
-If your program needs to parse flag args (e.g., `greet --name Alice`), do this:
+If your program needs to parse flag arguments (e.g. `greet --name Alice`), do this:
```rust
// Features: ["parser"]
@@ -97,19 +97,19 @@ Its semantics:
@@@let name: String =
prev.pick_or(["--name", "-n"], "World").unpack();
// ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~
-// | | | | |_ unpack to String
+// | | | | |_ unpack as String
// | | | |__________ default value "World"
-// | | |____________________________ pick arg after "--name" or "-n"
+// | | |____________________________ pick the value after "--name" or "-n"
// | |____________________________________ pick or use default
-// |_________________________________________ from previous input
+// |_________________________________________ from the 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.
+You may have noticed that `Picker` calls `.unpack()` at the end of parsing. It converts the collected results into structured info.
-For a single pick, `.unpack()` returns a single value; for multiple picks, it returns a tuple:
+For a single pick, `.unpack()` returns the value directly; for multiple picks, it returns a tuple:
```rust
// Features: ["parser"]
@@ -129,16 +129,17 @@ fn handle_test_entry(prev: EntryTest) -> Next {
```
> [!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.
+> `Picker` is sensitive to parse order, especially for positional args — it parses sequentially. If you need to parse positional args, make sure all **flag arguments** are 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.
+As the saying goes: "never trust your users." To handle missing required params, type mismatches, etc., `pick_or_route` routes the chain to a dedicated error handler.
-A simple example:
+Here's a simple example:
```rust
// Features: ["parser", "extra_macros"]
+@@@use mingling::macros::buffer;
@@@use mingling::macros::route;
@@@dispatcher!("greet", CMDGreet => EntryGreet);
@@@pack!(ResultName = String);
@@ -150,29 +151,20 @@ fn handle_greet_entry(prev: EntryGreet) -> Next {
.pick_or_route(["--name", "-n"], ErrorNoName::default())
.unpack();
- // Use route! macro to unpack pick_result
+ // Use route! macro to expand pick_result
let name = route!(pick_result);
ResultName::new(name).into()
}
-#[renderer]
-fn render_no_name(_prev: ErrorNoName) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Error: No name provided.").ok();
- r
-}
-
-#[renderer]
-fn render_name(prev: ResultName) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *prev).ok();
- r
+#[renderer(buffer)]
+fn render_greet(result: ResultName) {
+ r_println!("Hello, {}!", *result);
}
```
-With `pick_or_route`, the code gets a bit more complex: `.unpack()` no longer returns the value directly, but `Result<Value, Route>`.
+With `pick_or_route`, the code becomes more involved: `.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:
+However, **Mingling**'s `extra_macros` feature provides the `route!` macro for simplified expansion. It's not complex — it just reduces boilerplate:
```rust
// Features: ["parser", "extra_macros"]
@@ -204,7 +196,7 @@ let name = match pick_result {
## Post-processing Extracted Values
-After picking user input, you can use `after` to process the value immediately:
+After picking user input with `pick`, you can use `after` to process it immediately:
```rust
// Features: ["parser"]
@@ -215,7 +207,7 @@ After picking user input, you can use `after` to process the value immediately:
fn handle_greet_entry(prev: EntryGreet) -> Next {
let name = prev
.pick_or(["--name", "-n"], "World")
- // Format immediately after extracting --name
+ // Format immediately after picking --name
.after(|name: String| {
name.replace(['-', '_', '.'], " ")
.to_lowercase()
@@ -228,10 +220,11 @@ fn handle_greet_entry(prev: EntryGreet) -> Next {
}
```
-Similarly, use `after_or_route` to handle format errors in input args:
+Similarly, you can use `after_or_route` to handle input format errors:
```rust
// Features: ["parser", "extra_macros"]
+@@@use mingling::macros::buffer;
@@@use mingling::macros::route;
@@@dispatcher!("greet", CMDGreet => EntryGreet);
@@@pack!(ResultName = String);
@@ -254,35 +247,31 @@ fn handle_greet_entry(prev: EntryGreet) -> Next {
ResultName::new(name).into()
}
-#[renderer]
-fn render_name_too_long(prev: ErrorNameTooLong) -> RenderResult {
- let mut r = RenderResult::new();
+#[renderer(buffer)]
+fn render_name_too_long(prev: ErrorNameTooLong) {
let len = *prev;
- writeln!(r, "Error: name too long (length: {} > 32)", len).ok();
- r
+ r_println!("Error: name too long (length: {} > 32)", len);
}
-#[renderer]
-fn render_name(prev: ResultName) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *prev).ok();
- r
+#[renderer(buffer)]
+fn render_name(prev: ResultName) {
+ r_println!("Hello, {}!", *prev);
}
```
## Boolean Parsing
-`Picker` can parse bools too, with two modes: implicit and explicit.
+`Picker` can also parse booleans, in two modes:
| 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
+- `.pick::<bool>(flag)` uses implicit mode: the flag being present means `true`
+- `.pick::<Yes>(flag)` or `.pick::<True>(flag)` uses explicit mode
-Implicit is fine for most cases, but for important confirmations, explicit logic is more semantic.
+Implicit mode is generally sufficient, but for important confirmations, explicit logic is more idiomatic.
```rust
// Features: ["parser"]
@@ -302,7 +291,7 @@ fn handle_entry(prev: EntryTest) -> Next {
## Special Usage: `usize` Parsing
-Mingling provides a special `usize` feature: parsing strings like `25G`, `32mib`, etc.
+**Mingling** provides a special `usize` feature: parsing strings like `25G`, `32mib`, etc.
```rust
// Features: ["parser"]
@@ -315,12 +304,13 @@ fn parse_size() {
}
```
-## Custom Parseable Types
+## Custom Pickable Types
-Implement the `Pickable` trait to make your type parseable by `Picker` — this is where Picker's extensibility comes from.
+You can make your types pickable by `Picker` using the `Pickable` trait — this is where `Picker`'s extensibility comes from.
```rust
// Features: ["parser"]
+@@@use mingling::macros::buffer;
@@@use mingling::parser::{Pickable, Argument};
@@@use mingling::Flag;
#[derive(Default, Clone)]
@@ -348,12 +338,9 @@ fn handle_connect_entry(prev: EntryConnect) -> Next {
ResultConnected::new(address).into()
}
-#[renderer]
-fn render_connected(prev: ResultConnected) -> RenderResult {
- let mut r = RenderResult::new();
- let addr = prev.inner;
- writeln!(r, "Connected: IP: {} PORT: {}", addr.ip, addr.port).ok();
- r
+#[renderer(buffer)]
+fn render_connected(addr: ResultConnected) {
+ r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port);
}
```
@@ -366,10 +353,11 @@ 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`:
+To make an enum `Pickable`, just implement `EnumTag` on it, then implement `PickableEnum`:
```rust
// Features: ["parser"]
+@@@use mingling::macros::buffer;
@@@use mingling::parser::PickableEnum;
@@@use mingling::EnumTag;
#[derive(Debug, Default, EnumTag)]
@@ -390,15 +378,13 @@ fn handle_eat_entry(prev: EntryEat) -> Next {
ResultFruit::new(fruit).into()
}
-#[renderer]
-fn render_fruit(prev: ResultFruit) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Picked fruit: {:?}", *prev).ok();
- r
+#[renderer(buffer)]
+fn render_fruit(prev: ResultFruit) {
+ r_println!("Picked fruit: {:?}", *prev);
}
```
-That covers all the features of `Picker`.
+That covers all the usages of `Picker`.
<p align="center" style="font-size: 0.85em; color: gray;">
Written by @Weicao-CatilGrass
diff --git a/docs/pages/7-argument-parse-clap.md b/docs/pages/7-argument-parse-clap.md
index b11e00e..dcebd65 100644
--- a/docs/pages/7-argument-parse-clap.md
+++ b/docs/pages/7-argument-parse-clap.md
@@ -25,7 +25,8 @@ Add `#[dispatcher_clap]` on a `clap::Parser` struct to auto-generate a Dispatche
// Dependencies:
// clap = "4"
@@@ use mingling::macros::dispatcher_clap;
-#[derive(Default, clap::Parser, Groupped)]
+@@@ use mingling::macros::buffer;
+#[derive(Default, clap::Parser, Grouped)]
#[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)]
pub struct EntryGreet {
#[clap(default_value = "World")]
@@ -34,23 +35,19 @@ pub struct EntryGreet {
repeat: i32,
}
-#[renderer]
-fn render_greet(greet: EntryGreet) -> RenderResult {
- let mut r = RenderResult::new();
+#[renderer(buffer)]
+fn render_greet(greet: EntryGreet) {
let count = greet.repeat.max(0) as usize;
- write!(r, "Hello, ").ok();
+ r_print!("Hello, ");
for _ in 0..count {
- write!(r, "{} ", greet.name).ok();
+ r_print!("{} ", greet.name);
}
- writeln!(r, "!").ok();
- r
+ r_println!("!");
}
-#[renderer]
-fn render_greet_parse_failed(err: ErrorGreetParsed) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "{}", *err).ok();
- r
+#[renderer(buffer)]
+fn render_greet_parse_failed(err: ErrorGreetParsed) {
+ r_println!("{}", *err);
}
```
@@ -62,18 +59,17 @@ If you need `--help` support, register `BasicProgramSetup` in main and set the c
// Features: ["clap"]
// Dependencies:
// clap = "4"
+@@@use mingling::macros::buffer;
@@@use mingling::setup::BasicProgramSetup;
@@@use mingling::macros::dispatcher_clap;
-@@@#[derive(Default, clap::Parser, Groupped)]
+@@@#[derive(Default, clap::Parser, Grouped)]
@@@#[dispatcher_clap("greet", CMDGreet)]
@@@pub struct EntryGreet {
@@@ name: String,
@@@}
-@@@#[renderer]
-@@@fn render_greet(greet: EntryGreet) -> RenderResult {
-@@@ let mut r = RenderResult::new();
-@@@ write!(r, "Hello, {}!", greet.name).ok();
-@@@ r
+@@@#[renderer(buffer)]
+@@@fn render_greet(greet: EntryGreet) {
+@@@ r_println!("Hello, {}!", greet.name);
@@@}
fn main() {
let mut program = ThisProgram::new();
diff --git a/docs/pages/9-error-handling.md b/docs/pages/9-error-handling.md
index 3e004f2..f6e05e3 100644
--- a/docs/pages/9-error-handling.md
+++ b/docs/pages/9-error-handling.md
@@ -38,23 +38,20 @@ fn handle_greet(args: EntryGreet) -> Next {
Then write separate Renderers:
```rust
+@@@use mingling::macros::buffer;
@@@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) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *result).ok();
- r
+#[renderer(buffer)]
+fn render_greet(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
}
-#[renderer]
-fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Error: {}", *err).ok();
- r
+#[renderer(buffer)]
+fn render_error_name_empty(err: ErrorNameEmpty) {
+ r_println!("Error: {}", *err);
}
```
@@ -63,6 +60,7 @@ Each Renderer does its own job; what the user sees depends on what the Chain ret
## Complete Example
```rust
+@@@use mingling::macros::buffer;
dispatcher!("greet", CMDGreet => EntryGreet);
pack!(ResultGreeting = String);
@@ -78,18 +76,14 @@ fn handle_greet(args: EntryGreet) -> Next {
}
}
-#[renderer]
-fn render_greeting(result: ResultGreeting) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Hello, {}!", *result).ok();
- r
+#[renderer(buffer)]
+fn render_greet(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
}
-#[renderer]
-fn render_error_name_empty(err: ErrorNameEmpty) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Error: {}", *err).ok();
- r
+#[renderer(buffer)]
+fn render_error_name_empty(err: ErrorNameEmpty) {
+ r_println!("Error: {}", *err);
}
fn main() {
diff --git a/docs/pages/advanced/2-structural-renderer.md b/docs/pages/advanced/2-structural-renderer.md
index fab7530..b54af22 100644
--- a/docs/pages/advanced/2-structural-renderer.md
+++ b/docs/pages/advanced/2-structural-renderer.md
@@ -27,6 +27,7 @@ After enabling `StructuralRendererSetup`, use `pack_structural!` instead of `pac
// Features: ["structural_renderer"]
// Dependencies:
// serde = "1"
+@@@use mingling::macros::buffer;
@@@use mingling::setup::StructuralRendererSetup;
@@@dispatcher!("render", CMDRender => EntryRender);
@@ -40,11 +41,9 @@ fn handle_render(args: EntryRender) -> Next {
ResultInfo::new((name, age)).into()
}
-#[renderer]
-fn render_info(r: ResultInfo) -> RenderResult {
- let mut result = RenderResult::new();
- writeln!(result, "{:?}", *r).ok();
- result
+#[renderer(buffer)]
+fn render_info(r: ResultInfo) {
+ r_println!("{:?}", *r);
}
```
@@ -62,19 +61,20 @@ When the user passes `--json`, the framework automatically serializes the render
## 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)]`:
+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, Grouped)]`:
```rust
// Features: ["structural_renderer"]
// Dependencies:
// serde = "1"
+@@@use mingling::macros::buffer;
@@@use mingling::prelude::*;
@@@use mingling::setup::StructuralRendererSetup;
@@@use mingling::StructuralData;
@@@use serde::Serialize;
@@@dispatcher!("render", CMDRender => EntryRender);
-#[derive(Serialize, StructuralData, Groupped)]
+#[derive(Serialize, StructuralData, Grouped)]
struct Info {
name: String,
age: i32,
@@ -87,11 +87,9 @@ fn handle_render(args: EntryRender) -> Next {
Info { name, age }.to_render()
}
-#[renderer]
-fn render_info(info: Info) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "{} is {} years old", info.name, info.age).ok();
- r
+#[renderer(buffer)]
+fn render_info(info: Info) {
+ r_println!("{} is {} years old", info.name, info.age);
}
@@@
@@@fn main() {
diff --git a/docs/pages/concepts/3-any-output.md b/docs/pages/concepts/3-any-output.md
index f780377..f02805f 100644
--- a/docs/pages/concepts/3-any-output.md
+++ b/docs/pages/concepts/3-any-output.md
@@ -20,7 +20,7 @@ AnyOutput<G>
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.
+Each type annotated with `pack!` or `#[derive(Grouped)]` is assigned to one variant of this enum.
## ChainProcess: Data + Routing
@@ -38,17 +38,17 @@ This is why a Chain function returns `ChainProcess` instead of raw data—it bun
The dispatcher reads `NextProcess` to decide whether to continue the loop or exit to rendering.
-## Groupped: Who Is Who
+## Grouped: Who Is Who
-How does the dispatcher know whether an `AnyOutput` holds a `ResultName` or an `ErrorUserBlocked`? The answer is the `Groupped` trait:
+How does the dispatcher know whether an `AnyOutput` holds a `ResultName` or an `ErrorUserBlocked`? The answer is the `Grouped` trait:
```
-trait Groupped<G> {
+trait Grouped<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.
+When you use `pack!(ResultName = String)`, the macro automatically implements `Grouped` 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.
diff --git a/docs/pages/other/features.md b/docs/pages/other/features.md
index 325bb75..7994d4c 100644
--- a/docs/pages/other/features.md
+++ b/docs/pages/other/features.md
@@ -171,7 +171,7 @@ fn handle_hello(args: EntryHello) {}
### `group!`
Registers an external type as a member of the program group without modifying its definition.
-The type's simple name is used as the enum variant, just like `pack!` or `#[derive(Groupped)]`.
+The type's simple name is used as the enum variant, just like `pack!` or `#[derive(Grouped)]`.
```rust
// Features: ["extra_macros"]
@@ -274,12 +274,24 @@ See [example](https://mingling-rs.github.io/mingling/docs/example-viewer.html?na
**Description:**
-Enables the parser module, providing text parsing and analysis capabilities.
+Enables the argument parser module, providing argument parsing functionality.
-When enabled, you can use `Picker` for zero-cost argument extraction, supporting methods like `pick()` and `pick_or()`.
+When enabled, you can use `Picker` for simple argument extraction, supporting methods like `pick()` and `pick_or()`.
See [example](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-argument-parse)
+## Feature `picker`
+
+**Description:**
+
+Introduces the `arg-picker` dependency, providing more advanced argument parsing capabilities for Mingling.
+
+It can coexist with the `parser` and `clap` features, but it is recommended not to enable it alongside the `parser` feature, as their APIs are very similar.
+
+`picker` is an argument parser independent of Mingling and does not rely on the built-in argument extraction API of `mingling_core`.
+
+See [example](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-argument-picker)
+
## Feature `repl`
**Description:**
diff --git a/docs/pages/other/naming_rule.md b/docs/pages/other/naming_rule.md
index 2ede61f..1175ae0 100644
--- a/docs/pages/other/naming_rule.md
+++ b/docs/pages/other/naming_rule.md
@@ -96,7 +96,7 @@ Result + Content
| `ResultGreetSomeone` | Greeting result |
| `ResultFruitList` | Fruit list result |
-Result structs are expected to be consumed by the Renderer, and their internal structure should be designed for rendering aesthetics. Generally use `#[derive(Groupped)]` instead of `pack!()` wrapping for more flexible field control.
+Result structs are expected to be consumed by the Renderer, and their internal structure should be designed for rendering aesthetics. Generally use `#[derive(Grouped)]` instead of `pack!()` wrapping for more flexible field control.
### Error
@@ -166,6 +166,7 @@ fn handle_remote_add(args: EntryRemoteAdd, cwd: &ResCurrentDir, db: &mut ResData
## Complete Example
```rust
+@@@use mingling::macros::buffer;
@@@ #[derive(Default, Clone)]
@@@ struct ResDatabase { }
@@@ impl ResDatabase { fn has_remote(&self, remote: &String) -> bool { true } }
@@ -192,21 +193,17 @@ fn handle_state_operation_remotes(state: StateOperationRemotes, db: &ResDatabase
}
// Result rendering
-#[renderer]
-fn render_remote_added(result: ResultRemoteAdded) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Remote added: {}", result.inner).ok();
- r
+
+#[renderer(buffer)]
+fn render_remote_added(result: ResultRemoteAdded) {
+ r_println!("Remote added: {}", result.inner);
}
// Error rendering
-#[renderer]
-fn render_error_repository_not_found(err: ErrorRepositoryNotFound) -> RenderResult {
- let mut r = RenderResult::new();
- writeln!(r, "Error: remote '{}' not found", err.inner).ok();
- r
+#[renderer(buffer)]
+fn render_error_repository_not_found(err: ErrorRepositoryNotFound) {
+ r_println!("Error: remote '{}' not found", err.inner);
}
-
```
<p align="center" style="font-size: 0.85em; color: gray;">
diff --git a/docs/res/icon3.png b/docs/res/icon3.png
new file mode 100644
index 0000000..04ea5e9
--- /dev/null
+++ b/docs/res/icon3.png
Binary files differ
diff --git a/docs/res/icon3_1024.png b/docs/res/icon3_1024.png
new file mode 100644
index 0000000..868f8ad
--- /dev/null
+++ b/docs/res/icon3_1024.png
Binary files differ
diff --git a/examples/example-argument-picker/Cargo.lock b/examples/example-argument-picker/Cargo.lock
new file mode 100644
index 0000000..9a9baa4
--- /dev/null
+++ b/examples/example-argument-picker/Cargo.lock
@@ -0,0 +1,94 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "arg-picker"
+version = "0.1.0"
+dependencies = [
+ "arg-picker-macros",
+ "just_fmt",
+]
+
+[[package]]
+name = "arg-picker-macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "example-argument-picker"
+version = "0.1.0"
+dependencies = [
+ "mingling",
+]
+
+[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
+name = "mingling"
+version = "0.3.0"
+dependencies = [
+ "arg-picker",
+ "mingling_core",
+ "mingling_macros",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/examples/example-argument-picker/Cargo.toml b/examples/example-argument-picker/Cargo.toml
new file mode 100644
index 0000000..686b95b
--- /dev/null
+++ b/examples/example-argument-picker/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "example-argument-picker"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies.mingling]
+path = "../../mingling"
+
+# Enable `picker` features
+features = ["picker", "extra_macros"]
+
+[workspace]
diff --git a/examples/example-argument-picker/page.toml b/examples/example-argument-picker/page.toml
new file mode 100644
index 0000000..563bccc
--- /dev/null
+++ b/examples/example-argument-picker/page.toml
@@ -0,0 +1,10 @@
+[example]
+id = "example-argument-picker"
+name = "Argument Picker"
+icon = "📋"
+category = "parsing"
+desc = """
+Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line.
+"""
+tags = ["arg-picker", "SinglePickable"]
+files = ["src/main.rs", "Cargo.toml"]
diff --git a/examples/example-argument-picker/src/main.rs b/examples/example-argument-picker/src/main.rs
new file mode 100644
index 0000000..7fcc5db
--- /dev/null
+++ b/examples/example-argument-picker/src/main.rs
@@ -0,0 +1,225 @@
+//! Example Argument Picker
+//!
+//! > Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line.
+//!
+//! Run:
+//! ```bash
+//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 + 1
+//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 7 * 7
+//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc
+//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1
+//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 +
+//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3
+//! cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3 --round
+//! ```
+//!
+//! Output:
+//! ```plaintext
+//! Result: 2
+//! Result: 49
+//! Error: First number (number_a) was not provided.
+//! Error: Operator was not provided.
+//! Error: Second number (number_b) was not provided.
+//! Result: 1.3333334
+//! Result: 1
+//! ```
+
+use mingling::{
+ consts::REMAINS,
+ macros::route,
+ picker::{
+ IntoPicker, PickerArgResult, SinglePickable,
+ parselib::{ParserStyle, UNIX_STYLE},
+ value::Flag,
+ },
+ prelude::*,
+};
+
+// --------- IMPORTANT ---------
+// Use picker::BasicProgramSetup instead of the original BasicProgramSetup
+// It uses arg-picker to rewrite the logic of the original BasicProgramSetup
+use mingling::setup::picker::BasicProgramSetup;
+
+// --------- IMPORTANT ---------
+
+dispatcher!("calc", CMDCalculate => EntryCalculate);
+
+pack_err!(ErrorNumberANotProvided);
+pack_err!(ErrorNumberBNotProvided);
+pack_err!(ErrorNumberOperatorNotProvided);
+pack_err!(ErrorDivisionByZero);
+
+pack!(StateAdd = (f32, f32));
+pack!(StateSubtract = (f32, f32));
+pack!(StateMultiply = (f32, f32));
+pack!(StateDivide = (f32, f32));
+
+pack!(ResultNumber = f32);
+
+#[derive(Grouped)]
+struct StateCalculate {
+ number_a: f32,
+ operator: Operator,
+ number_b: f32,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+enum Operator {
+ Plus,
+ Dash,
+ Slash,
+ Star,
+}
+
+// --------- IMPORTANT ---------
+// Define SinglePickable for type Operator
+// This allows the type to be picked as an argument
+impl SinglePickable for Operator {
+ fn pick_single(str: Option<&str>) -> PickerArgResult<Self> {
+ let Some(str) = str else {
+ return PickerArgResult::NotFound;
+ };
+ let op = match str.chars().next() {
+ Some('+') => Operator::Plus,
+ Some('-') => Operator::Dash,
+ Some('*') => Operator::Star,
+ Some('/') => Operator::Slash,
+ _ => return PickerArgResult::NotFound,
+ };
+ PickerArgResult::Parsed(op)
+ }
+}
+// --------- IMPORTANT ---------
+
+#[derive(Default, Clone)]
+struct ResNumberDisplaySetting {
+ round: bool,
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+
+ // Use ParserStyle to manage the arg-picker theme
+ ParserStyle::set_global_style(&UNIX_STYLE);
+
+ // Enable picker::BasicProgramSetup
+ program.with_setup(BasicProgramSetup);
+
+ // --------- IMPORTANT ---------
+ // Pre-process global arguments before executing commands
+ let (round, args) = program
+ .take_args()
+ // Use arg![round: Flag] to indicate the `--round` | `-R` flag
+ // |
+ // vvvvvvvvvvvvvvvv
+ .pick(&arg![round: Flag, 'R'])
+ // Use REMAINS to extract remaining arguments
+ // |
+ // vvvvvvvv
+ .pick(&REMAINS)
+ // Since Flag and REMAINS will not fail to parse,
+ // we can safely unwrap here
+ .unwrap();
+ program.replace_args(args.into());
+
+ program.with_resource(ResNumberDisplaySetting { round: *round });
+ // --------- IMPORTANT ---------
+
+ program.with_dispatcher(CMDCalculate);
+ program.exec_and_exit();
+}
+
+#[chain]
+fn handle_calc(args: EntryCalculate) -> Next {
+ // --------- IMPORTANT ---------
+ let (number_a, operator, number_b) = route!(
+ // Use the arg! macro to define a positional argument of type f32
+ // |
+ // vvvvvvvvvv
+ args.pick_or_route(&arg![f32], || ErrorNumberANotProvided::default().to_chain())
+ .pick_or_route(&arg![Operator], || {
+ ErrorNumberOperatorNotProvided::default().to_chain()
+ }) // Returns a routable type when not found or fails to parse
+ // |
+ // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+ .pick_or_route(&arg![f32], || ErrorNumberBNotProvided::default().to_chain())
+ // Use `to_result` to parse arguments
+ // and convert to Result<(Tuple, ...), Route> type
+ .to_result()
+ );
+ // --------- IMPORTANT ---------
+
+ if operator == Operator::Slash && number_b == 0. {
+ return ErrorDivisionByZero::default().to_chain();
+ }
+
+ StateCalculate {
+ number_a,
+ operator,
+ number_b,
+ }
+ .to_chain()
+}
+
+#[chain]
+fn handle_state_calculate(state: StateCalculate) -> Next {
+ match (state.operator, state.number_a, state.number_b) {
+ (Operator::Plus, a, b) => StateAdd::new((a, b)).to_chain(),
+ (Operator::Dash, a, b) => StateSubtract::new((a, b)).to_chain(),
+ (Operator::Slash, a, b) => StateDivide::new((a, b)).to_chain(),
+ (Operator::Star, a, b) => StateMultiply::new((a, b)).to_chain(),
+ }
+}
+
+#[chain]
+fn handle_state_add(state_add: StateAdd) -> ResultNumber {
+ let (a, b) = state_add.inner;
+ ResultNumber::new(a + b)
+}
+
+#[chain]
+fn handle_state_subtract(state_subtract: StateSubtract) -> ResultNumber {
+ let (a, b) = state_subtract.inner;
+ ResultNumber::new(a - b)
+}
+
+#[chain]
+fn handle_state_multiply(state_multiply: StateMultiply) -> ResultNumber {
+ let (a, b) = state_multiply.inner;
+ ResultNumber::new(a * b)
+}
+
+#[chain]
+fn handle_state_divide(state_divide: StateDivide) -> ResultNumber {
+ let (a, b) = state_divide.inner;
+ ResultNumber::new(a / b)
+}
+
+#[renderer]
+fn render_result_number(result: ResultNumber, setting: &ResNumberDisplaySetting) -> String {
+ let round = setting.round;
+ let result = if round { result.round() } else { result.inner };
+ format!("Result: {}", result)
+}
+
+#[renderer]
+fn render_error_division_by_zero(_: ErrorDivisionByZero) -> String {
+ "Error: Division by zero is not allowed!".to_string()
+}
+
+#[renderer]
+fn render_error_number_a_not_provided(_: ErrorNumberANotProvided) -> String {
+ "Error: First number (number_a) was not provided.".to_string()
+}
+
+#[renderer]
+fn render_error_number_b_not_provided(_: ErrorNumberBNotProvided) -> String {
+ "Error: Second number (number_b) was not provided.".to_string()
+}
+
+#[renderer]
+fn render_error_number_operator_not_provided(_: ErrorNumberOperatorNotProvided) -> String {
+ "Error: Operator was not provided.".to_string()
+}
+
+gen_program!();
diff --git a/examples/example-clap-binding/src/main.rs b/examples/example-clap-binding/src/main.rs
index d99f8d1..0f839c8 100644
--- a/examples/example-clap-binding/src/main.rs
+++ b/examples/example-clap-binding/src/main.rs
@@ -38,7 +38,7 @@
//! For more information, try '--help'.
//! ```
-use mingling::{Groupped, macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup};
+use mingling::{Grouped, macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup};
use std::io::Write;
fn main() {
@@ -67,10 +67,10 @@ fn main() {
// Implement Clap Parser, and bind to Dispatcher
// _______________________________ Default trait, provides fallback on parse failure
// / ______________________ clap::Parser, parsing logic implemented by Clap
-// | / ________ Implement mingling::Groupped
+// | / ________ Implement mingling::Grouped
// | | / to ensure Mingling can recognize the type
-// vvvvvvv vvvvvvvvvvvv vvvvvvvv
-#[derive(Default, clap::Parser, Groupped)]
+// vvvvvvv vvvvvvvvvvvv vvvvvvv
+#[derive(Default, clap::Parser, Grouped)]
#[dispatcher_clap(
"greet", CMDGreet, // Bind EntryGreet to "greet" command
help = true, // Generate clap help for EntryGreet
diff --git a/examples/example-custom-pickable/src/main.rs b/examples/example-custom-pickable/src/main.rs
index d203815..b163278 100644
--- a/examples/example-custom-pickable/src/main.rs
+++ b/examples/example-custom-pickable/src/main.rs
@@ -14,15 +14,15 @@
//! Failed to parse address
//! ```
-use mingling::{macros::route, parser::Pickable, prelude::*, Groupped};
+use mingling::{macros::route, parser::Pickable, prelude::*, Grouped};
use std::io::Write;
// Define types that can be recognized by Mingling
// ________________________ `Pickable` trait needs to implement Default
-// / ________ The Groupped derive macro registers an ID for this type
+// / ________ The Grouped derive macro registers an ID for this type
// | / Mingling uses this ID to identify the type
-// vvvvvvv vvvvvvvv
-#[derive(Debug, Default, Clone, Groupped)]
+// vvvvvvv vvvvvvv
+#[derive(Debug, Default, Clone, Grouped)]
pub struct Address {
pub ip: [u8; 4],
pub port: u16,
diff --git a/examples/example-enum-tag/src/main.rs b/examples/example-enum-tag/src/main.rs
index 91c5358..b57511d 100644
--- a/examples/example-enum-tag/src/main.rs
+++ b/examples/example-enum-tag/src/main.rs
@@ -16,7 +16,7 @@
//! ```
use mingling::{
- macros::suggest_enum, parser::PickableEnum, prelude::*, EnumTag, Groupped, ShellContext,
+ macros::suggest_enum, parser::PickableEnum, prelude::*, EnumTag, Grouped, ShellContext,
Suggest,
};
use std::io::Write;
@@ -25,7 +25,7 @@ use std::io::Write;
// ________ adds metadata to the enum, enabling it to:
// / 1. Be used by the `suggest_enum!(Enum)` macro under the `comp` feature for autocompletion
// vvvvvvv 2. Implement the `PickableEnum` trait
-#[derive(Debug, Default, EnumTag, Groupped)]
+#[derive(Debug, Default, EnumTag, Grouped)]
pub enum ProgrammingLanguages {
#[enum_desc("An efficient and flexible compiled language widely used for system programming")]
C,
diff --git a/examples/example-outside-type/src/main.rs b/examples/example-outside-type/src/main.rs
index 925878a..3159d19 100644
--- a/examples/example-outside-type/src/main.rs
+++ b/examples/example-outside-type/src/main.rs
@@ -71,7 +71,7 @@ fn render_number(num: ParsedNumber) -> RenderResult {
/// Renderer for parse errors — using the outside `ParseIntError` type.
///
/// The `ParseIntError` type is registered via `group!` above, so it implements
-/// `Groupped<ThisProgram>` and can be used directly in a `#[renderer]` function.
+/// `Grouped<ThisProgram>` and can be used directly in a `#[renderer]` function.
#[renderer]
fn render_parse_error(err: ParseIntError) -> RenderResult {
let mut render_result = RenderResult::new();
diff --git a/examples/example-structural-renderer/src/main.rs b/examples/example-structural-renderer/src/main.rs
index 13c4c84..070e75d 100644
--- a/examples/example-structural-renderer/src/main.rs
+++ b/examples/example-structural-renderer/src/main.rs
@@ -18,7 +18,7 @@
//! ```
use mingling::prelude::*;
-use mingling::{Groupped, StructuralData, parser::Picker, setup::StructuralRendererSetup};
+use mingling::{parser::Picker, setup::StructuralRendererSetup, Grouped, StructuralData};
use serde::Serialize;
use std::io::Write;
@@ -35,12 +35,12 @@ fn main() {
// --------- IMPORTANT ---------
// For beautiful output structure, do not use `pack!` to wrap the types that need to be output.
// Instead, manually implement
-// __________________________________ Mark as structured data so it can be rendered
-// / ____________________ Implement serde::Serialize
-// | / _________ Implement mingling::Groupped
-// | | / to ensure Mingling can recognize the type
-// vvvvvvvvvvvv vvvvvvvvv vvvvvvvv
-#[derive(StructuralData, Serialize, Groupped)]
+// ____________________________________ Mark as structured data so it can be rendered
+// / ____________________ Implement serde::Serialize
+// | / _________ Implement mingling::Grouped
+// | | / to ensure Mingling can recognize the type
+// vvvvvvvvvvvv vvvvvvvvv vvvvvvv
+#[derive(StructuralData, Serialize, Grouped)]
struct Info {
#[serde(rename = "member_name")]
name: String,
diff --git a/examples/full-todolist/src/todolist.rs b/examples/full-todolist/src/todolist.rs
index 71338c3..d3582e3 100644
--- a/examples/full-todolist/src/todolist.rs
+++ b/examples/full-todolist/src/todolist.rs
@@ -1,13 +1,13 @@
//! Data structures, read and write logic for the todo list
-use mingling::{Groupped, RenderResult, macros::renderer};
+use mingling::{Grouped, RenderResult, macros::renderer};
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::{env::current_dir, path::PathBuf};
use crate::ResProgramFlags;
-#[derive(Debug, Serialize, Deserialize, Clone, Default, Groupped)]
+#[derive(Debug, Serialize, Deserialize, Clone, Default, Grouped)]
pub struct ResTodoList {
pub items: Vec<Todo>,
}
diff --git a/examples/test-examples.toml b/examples/test-examples.toml
index d2fe602..e450919 100644
--- a/examples/test-examples.toml
+++ b/examples/test-examples.toml
@@ -277,3 +277,38 @@ expect.result = "Hello, Alice!"
command = "hello"
expect.exit-code = 0
expect.result = "Hello, World!"
+
+[[test.example-argument-picker]]
+command = "calc 1 + 1"
+expect.exit-code = 0
+expect.result = "Result: 2"
+
+[[test.example-argument-picker]]
+command = "calc 7 * 7"
+expect.exit-code = 0
+expect.result = "Result: 49"
+
+[[test.example-argument-picker]]
+command = "calc"
+expect.exit-code = 0
+expect.result = "Error: First number (number_a) was not provided."
+
+[[test.example-argument-picker]]
+command = "calc 1"
+expect.exit-code = 0
+expect.result = "Error: Operator was not provided."
+
+[[test.example-argument-picker]]
+command = "calc 1 +"
+expect.exit-code = 0
+expect.result = "Error: Second number (number_b) was not provided."
+
+[[test.example-argument-picker]]
+command = "calc 4 / 3"
+expect.exit-code = 0
+expect.result = "Result: 1.3333334"
+
+[[test.example-argument-picker]]
+command = "calc 4 / 3 --round"
+expect.exit-code = 0
+expect.result = "Result: 1"
diff --git a/mingling/Cargo.toml b/mingling/Cargo.toml
index adf900f..351384f 100644
--- a/mingling/Cargo.toml
+++ b/mingling/Cargo.toml
@@ -21,7 +21,9 @@ mingling = { path = ".", features = [
] }
[package.metadata.docs.rs]
+rustdoc-args = ["--generate-link-to-definition"]
features = [
+ "docs_rs",
"core",
"macros",
"builds",
@@ -50,7 +52,7 @@ dispatch_tree = ["mingling_core/dispatch_tree", "mingling_macros/dispatch_tree"]
repl = ["mingling_core/repl", "mingling_macros/repl"]
comp = ["mingling_core/comp", "mingling_macros/comp"]
parser = ["dep:size"]
-picker = ["dep:arg-picker", "arg-picker/mingling_support"]
+picker = ["mingling_core/picker", "dep:arg-picker", "arg-picker/mingling_support"]
pathf = ["mingling_core/pathf", "mingling_macros/pathf"]
structural_renderer = [
@@ -87,6 +89,9 @@ ron_serde_fmt = ["mingling_core/ron_serde_fmt"]
extra_macros = ["mingling_macros/extra_macros"]
+# Section only shown in docs.rs
+docs_rs = []
+
[dependencies]
mingling_core = { workspace = true, optional = true }
mingling_macros = { workspace = true, optional = true }
diff --git a/mingling/src/constants.rs b/mingling/src/constants.rs
new file mode 100644
index 0000000..75f1c73
--- /dev/null
+++ b/mingling/src/constants.rs
@@ -0,0 +1,11 @@
+#[cfg(feature = "picker")]
+mod picker;
+
+#[cfg(feature = "picker")]
+pub use picker::*;
+
+#[cfg(feature = "picker")]
+pub use arg_picker::consts::*;
+
+mod exit_codes;
+pub use exit_codes::*;
diff --git a/mingling/src/constants/exit_codes.rs b/mingling/src/constants/exit_codes.rs
new file mode 100644
index 0000000..2359f42
--- /dev/null
+++ b/mingling/src/constants/exit_codes.rs
@@ -0,0 +1,14 @@
+/// Exit code indicating successful command execution.
+pub const EXIT_SUCCESS: i32 = 0;
+
+/// Exit code for general errors.
+pub const EXIT_GENERAL_ERR: i32 = 1;
+
+/// Exit code for incorrect command usage (or invalid arguments).
+pub const EXIT_USAGE_ERR: i32 = 2;
+
+/// Exit code indicating permission denied (or the command is not executable).
+pub const EXIT_PERM_DENIED: i32 = 126;
+
+/// Exit code for command not found (or PATH error).
+pub const EXIT_CMD_NOT_FOUND: i32 = 127;
diff --git a/mingling/src/constants/picker.rs b/mingling/src/constants/picker.rs
new file mode 100644
index 0000000..d98dcab
--- /dev/null
+++ b/mingling/src/constants/picker.rs
@@ -0,0 +1,115 @@
+use arg_picker::{PickerArg, value::Flag};
+use std::marker::PhantomData;
+
+/// Help flag: display usage information.
+/// - `full`: `["help"]`
+/// - `short`: `'h'`
+pub const HELP_FLAG: PickerArg<Flag> = PickerArg::<Flag> {
+ full: &["help"],
+ short: Some('h'),
+ positional: false,
+ internal_type: PhantomData,
+};
+
+/// Quiet flag: suppress output.
+/// - `full`: `["quiet"]`
+/// - `short`: `'q'`
+pub const QUIET_FLAG: PickerArg<Flag> = PickerArg::<Flag> {
+ full: &["quiet"],
+ short: Some('q'),
+ positional: false,
+ internal_type: PhantomData,
+};
+
+/// Confirm flag: require user confirmation before proceeding.
+/// - `full`: `["confirm"]`
+/// - `short`: `'C'`
+pub const CONFIRM_FLAG: PickerArg<Flag> = PickerArg::<Flag> {
+ full: &["confirm"],
+ short: Some('C'),
+ positional: false,
+ internal_type: PhantomData,
+};
+
+/// Renderer flag: explicitly specify the Structural Renderer argument.
+/// - `full`: `["renderer"]`
+/// - `short`: (none)
+#[cfg(feature = "structural_renderer")]
+pub const RENDERER_ARG: PickerArg<String> = PickerArg::<String> {
+ full: &["renderer"],
+ short: None,
+ positional: false,
+ internal_type: PhantomData,
+};
+
+/// JSON flag: enable JSON output format.
+/// - `full`: `["json"]`
+/// - `short`: (none)
+/// Available only when the `json_serde_fmt` feature is enabled.
+#[cfg(feature = "json_serde_fmt")]
+pub const JSON_FLAG: PickerArg<Flag> = PickerArg::<Flag> {
+ full: &["json"],
+ short: None,
+ positional: false,
+ internal_type: PhantomData,
+};
+
+/// JSON pretty flag: enable pretty-printed JSON output format.
+/// - `full`: `["json_pretty"]`
+/// - `short`: (none)
+/// Available only when the `json_serde_fmt` feature is enabled.
+#[cfg(feature = "json_serde_fmt")]
+pub const JSON_PRETTY_FLAG: PickerArg<Flag> = PickerArg::<Flag> {
+ full: &["json_pretty"],
+ short: None,
+ positional: false,
+ internal_type: PhantomData,
+};
+
+/// YAML flag: enable YAML output format.
+/// - `full`: `["yaml"]`
+/// - `short`: (none)
+/// Available only when the `yaml_serde_fmt` feature is enabled.
+#[cfg(feature = "yaml_serde_fmt")]
+pub const YAML_FLAG: PickerArg<Flag> = PickerArg::<Flag> {
+ full: &["yaml"],
+ short: None,
+ positional: false,
+ internal_type: PhantomData,
+};
+
+/// TOML flag: enable TOML output format.
+/// - `full`: `["toml"]`
+/// - `short`: (none)
+/// Available only when the `toml_serde_fmt` feature is enabled.
+#[cfg(feature = "toml_serde_fmt")]
+pub const TOML_FLAG: PickerArg<Flag> = PickerArg::<Flag> {
+ full: &["toml"],
+ short: None,
+ positional: false,
+ internal_type: PhantomData,
+};
+
+/// RON flag: enable RON output format.
+/// - `full`: `["ron"]`
+/// - `short`: (none)
+/// Available only when the `ron_serde_fmt` feature is enabled.
+#[cfg(feature = "ron_serde_fmt")]
+pub const RON_FLAG: PickerArg<Flag> = PickerArg::<Flag> {
+ full: &["ron"],
+ short: None,
+ positional: false,
+ internal_type: PhantomData,
+};
+
+/// RON pretty flag: enable pretty-printed RON output format.
+/// - `full`: `["ron_pretty"]`
+/// - `short`: (none)
+/// Available only when the `ron_serde_fmt` feature is enabled.
+#[cfg(feature = "ron_serde_fmt")]
+pub const RON_PRETTY_FLAG: PickerArg<Flag> = PickerArg::<Flag> {
+ full: &["ron_pretty"],
+ short: None,
+ positional: false,
+ internal_type: PhantomData,
+};
diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs
index c4f59c9..bdefb5a 100644
--- a/mingling/src/example_docs.rs
+++ b/mingling/src/example_docs.rs
@@ -124,6 +124,251 @@
/// }
/// ```
pub mod example_argument_parse {}
+/// Example Argument Picker
+///
+/// > Demonstrates how to use Mingling's `picker` feature and `Picker` to extract typed arguments from the command line.
+///
+/// Run:
+/// ```bash
+/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 + 1
+/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 7 * 7
+/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc
+/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1
+/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 1 +
+/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3
+/// cargo run --manifest-path examples/example-argument-picker/Cargo.toml --quiet -- calc 4 / 3 --round
+/// ```
+///
+/// Output:
+/// ```plaintext
+/// Result: 2
+/// Result: 49
+/// Error: First number (number_a) was not provided.
+/// Error: Operator was not provided.
+/// Error: Second number (number_b) was not provided.
+/// Result: 1.3333334
+/// Result: 1
+/// ```
+///
+/// Source code (./Cargo.toml)
+/// ```toml
+/// [package]
+/// name = "example-argument-picker"
+/// version = "0.1.0"
+/// edition = "2024"
+///
+/// [dependencies.mingling]
+/// path = "../../mingling"
+///
+/// # Enable `picker` features
+/// features = ["picker", "extra_macros"]
+///
+/// [workspace]
+/// ```
+///
+/// Source code (./src/main.rs)
+/// ```ignore
+/// use mingling::{
+/// consts::REMAINS,
+/// macros::route,
+/// picker::{
+/// IntoPicker, PickerArgResult, SinglePickable,
+/// parselib::{ParserStyle, UNIX_STYLE},
+/// value::Flag,
+/// },
+/// prelude::*,
+/// };
+///
+/// // --------- IMPORTANT ---------
+/// // Use picker::BasicProgramSetup instead of the original BasicProgramSetup
+/// // It uses arg-picker to rewrite the logic of the original BasicProgramSetup
+/// use mingling::setup::picker::BasicProgramSetup;
+///
+/// // --------- IMPORTANT ---------
+///
+/// dispatcher!("calc", CMDCalculate => EntryCalculate);
+///
+/// pack_err!(ErrorNumberANotProvided);
+/// pack_err!(ErrorNumberBNotProvided);
+/// pack_err!(ErrorNumberOperatorNotProvided);
+/// pack_err!(ErrorDivisionByZero);
+///
+/// pack!(StateAdd = (f32, f32));
+/// pack!(StateSubtract = (f32, f32));
+/// pack!(StateMultiply = (f32, f32));
+/// pack!(StateDivide = (f32, f32));
+///
+/// pack!(ResultNumber = f32);
+///
+/// #[derive(Grouped)]
+/// struct StateCalculate {
+/// number_a: f32,
+/// operator: Operator,
+/// number_b: f32,
+/// }
+///
+/// #[derive(Debug, PartialEq, Eq)]
+/// enum Operator {
+/// Plus,
+/// Dash,
+/// Slash,
+/// Star,
+/// }
+///
+/// // --------- IMPORTANT ---------
+/// // Define SinglePickable for type Operator
+/// // This allows the type to be picked as an argument
+/// impl SinglePickable for Operator {
+/// fn pick_single(str: Option<&str>) -> PickerArgResult<Self> {
+/// let Some(str) = str else {
+/// return PickerArgResult::NotFound;
+/// };
+/// let op = match str.chars().next() {
+/// Some('+') => Operator::Plus,
+/// Some('-') => Operator::Dash,
+/// Some('*') => Operator::Star,
+/// Some('/') => Operator::Slash,
+/// _ => return PickerArgResult::NotFound,
+/// };
+/// PickerArgResult::Parsed(op)
+/// }
+/// }
+/// // --------- IMPORTANT ---------
+///
+/// #[derive(Default, Clone)]
+/// struct ResNumberDisplaySetting {
+/// round: bool,
+/// }
+///
+/// fn main() {
+/// let mut program = ThisProgram::new();
+///
+/// // Use ParserStyle to manage the arg-picker theme
+/// ParserStyle::set_global_style(&UNIX_STYLE);
+///
+/// // Enable picker::BasicProgramSetup
+/// program.with_setup(BasicProgramSetup);
+///
+/// // --------- IMPORTANT ---------
+/// // Pre-process global arguments before executing commands
+/// let (round, args) = program
+/// .take_args()
+/// // Use arg![round: Flag] to indicate the `--round` | `-R` flag
+/// // |
+/// // vvvvvvvvvvvvvvvv
+/// .pick(&arg![round: Flag, 'R'])
+/// // Use REMAINS to extract remaining arguments
+/// // |
+/// // vvvvvvvv
+/// .pick(&REMAINS)
+/// // Since Flag and REMAINS will not fail to parse,
+/// // we can safely unwrap here
+/// .unwrap();
+/// program.replace_args(args.into());
+///
+/// program.with_resource(ResNumberDisplaySetting { round: *round });
+/// // --------- IMPORTANT ---------
+///
+/// program.with_dispatcher(CMDCalculate);
+/// program.exec_and_exit();
+/// }
+///
+/// #[chain]
+/// fn handle_calc(args: EntryCalculate) -> Next {
+/// // --------- IMPORTANT ---------
+/// let (number_a, operator, number_b) = route!(
+/// // Use the arg! macro to define a positional argument of type f32
+/// // |
+/// // vvvvvvvvvv
+/// args.pick_or_route(&arg![f32], || ErrorNumberANotProvided::default().to_chain())
+/// .pick_or_route(&arg![Operator], || {
+/// ErrorNumberOperatorNotProvided::default().to_chain()
+/// }) // Returns a routable type when not found or fails to parse
+/// // |
+/// // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+/// .pick_or_route(&arg![f32], || ErrorNumberBNotProvided::default().to_chain())
+/// // Use `to_result` to parse arguments
+/// // and convert to Result<(Tuple, ...), Route> type
+/// .to_result()
+/// );
+/// // --------- IMPORTANT ---------
+///
+/// if operator == Operator::Slash && number_b == 0. {
+/// return ErrorDivisionByZero::default().to_chain();
+/// }
+///
+/// StateCalculate {
+/// number_a,
+/// operator,
+/// number_b,
+/// }
+/// .to_chain()
+/// }
+///
+/// #[chain]
+/// fn handle_state_calculate(state: StateCalculate) -> Next {
+/// match (state.operator, state.number_a, state.number_b) {
+/// (Operator::Plus, a, b) => StateAdd::new((a, b)).to_chain(),
+/// (Operator::Dash, a, b) => StateSubtract::new((a, b)).to_chain(),
+/// (Operator::Slash, a, b) => StateDivide::new((a, b)).to_chain(),
+/// (Operator::Star, a, b) => StateMultiply::new((a, b)).to_chain(),
+/// }
+/// }
+///
+/// #[chain]
+/// fn handle_state_add(state_add: StateAdd) -> ResultNumber {
+/// let (a, b) = state_add.inner;
+/// ResultNumber::new(a + b)
+/// }
+///
+/// #[chain]
+/// fn handle_state_subtract(state_subtract: StateSubtract) -> ResultNumber {
+/// let (a, b) = state_subtract.inner;
+/// ResultNumber::new(a - b)
+/// }
+///
+/// #[chain]
+/// fn handle_state_multiply(state_multiply: StateMultiply) -> ResultNumber {
+/// let (a, b) = state_multiply.inner;
+/// ResultNumber::new(a * b)
+/// }
+///
+/// #[chain]
+/// fn handle_state_divide(state_divide: StateDivide) -> ResultNumber {
+/// let (a, b) = state_divide.inner;
+/// ResultNumber::new(a / b)
+/// }
+///
+/// #[renderer]
+/// fn render_result_number(result: ResultNumber, setting: &ResNumberDisplaySetting) -> String {
+/// let round = setting.round;
+/// let result = if round { result.round() } else { result.inner };
+/// format!("Result: {}", result)
+/// }
+///
+/// #[renderer]
+/// fn render_error_division_by_zero(_: ErrorDivisionByZero) -> String {
+/// "Error: Division by zero is not allowed!".to_string()
+/// }
+///
+/// #[renderer]
+/// fn render_error_number_a_not_provided(_: ErrorNumberANotProvided) -> String {
+/// "Error: First number (number_a) was not provided.".to_string()
+/// }
+///
+/// #[renderer]
+/// fn render_error_number_b_not_provided(_: ErrorNumberBNotProvided) -> String {
+/// "Error: Second number (number_b) was not provided.".to_string()
+/// }
+///
+/// #[renderer]
+/// fn render_error_number_operator_not_provided(_: ErrorNumberOperatorNotProvided) -> String {
+/// "Error: Operator was not provided.".to_string()
+/// }
+///
+/// gen_program!();
+/// ```
+pub mod example_argument_picker {}
/// Example Async Runtime Support
///
/// > This example shows how to drive an async runtime using the `async` feature
@@ -378,7 +623,7 @@ pub mod example_basic {}
///
/// Source code (./src/main.rs)
/// ```ignore
-/// use mingling::{Groupped, macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup};
+/// use mingling::{Grouped, macros::dispatcher_clap, prelude::*, setup::BasicProgramSetup};
/// use std::io::Write;
///
/// fn main() {
@@ -407,10 +652,10 @@ pub mod example_basic {}
/// // Implement Clap Parser, and bind to Dispatcher
/// // _______________________________ Default trait, provides fallback on parse failure
/// // / ______________________ clap::Parser, parsing logic implemented by Clap
-/// // | / ________ Implement mingling::Groupped
+/// // | / ________ Implement mingling::Grouped
/// // | | / to ensure Mingling can recognize the type
-/// // vvvvvvv vvvvvvvvvvvv vvvvvvvv
-/// #[derive(Default, clap::Parser, Groupped)]
+/// // vvvvvvv vvvvvvvvvvvv vvvvvvv
+/// #[derive(Default, clap::Parser, Grouped)]
/// #[dispatcher_clap(
/// "greet", CMDGreet, // Bind EntryGreet to "greet" command
/// help = true, // Generate clap help for EntryGreet
@@ -724,15 +969,15 @@ pub mod example_completion {}
///
/// Source code (./src/main.rs)
/// ```ignore
-/// use mingling::{macros::route, parser::Pickable, prelude::*, Groupped};
+/// use mingling::{macros::route, parser::Pickable, prelude::*, Grouped};
/// use std::io::Write;
///
/// // Define types that can be recognized by Mingling
/// // ________________________ `Pickable` trait needs to implement Default
-/// // / ________ The Groupped derive macro registers an ID for this type
+/// // / ________ The Grouped derive macro registers an ID for this type
/// // | / Mingling uses this ID to identify the type
-/// // vvvvvvv vvvvvvvv
-/// #[derive(Debug, Default, Clone, Groupped)]
+/// // vvvvvvv vvvvvvv
+/// #[derive(Debug, Default, Clone, Grouped)]
/// pub struct Address {
/// pub ip: [u8; 4],
/// pub port: u16,
@@ -963,7 +1208,7 @@ pub mod example_dispatch_tree {}
/// Source code (./src/main.rs)
/// ```ignore
/// use mingling::{
-/// macros::suggest_enum, parser::PickableEnum, prelude::*, EnumTag, Groupped, ShellContext,
+/// macros::suggest_enum, parser::PickableEnum, prelude::*, EnumTag, Grouped, ShellContext,
/// Suggest,
/// };
/// use std::io::Write;
@@ -972,7 +1217,7 @@ pub mod example_dispatch_tree {}
/// // ________ adds metadata to the enum, enabling it to:
/// // / 1. Be used by the `suggest_enum!(Enum)` macro under the `comp` feature for autocompletion
/// // vvvvvvv 2. Implement the `PickableEnum` trait
-/// #[derive(Debug, Default, EnumTag, Groupped)]
+/// #[derive(Debug, Default, EnumTag, Grouped)]
/// pub enum ProgrammingLanguages {
/// #[enum_desc("An efficient and flexible compiled language widely used for system programming")]
/// C,
@@ -1691,7 +1936,7 @@ pub mod example_lazy_resources {}
/// /// Renderer for parse errors — using the outside `ParseIntError` type.
/// ///
/// /// The `ParseIntError` type is registered via `group!` above, so it implements
-/// /// `Groupped<ThisProgram>` and can be used directly in a `#[renderer]` function.
+/// /// `Grouped<ThisProgram>` and can be used directly in a `#[renderer]` function.
/// #[renderer]
/// fn render_parse_error(err: ParseIntError) -> RenderResult {
/// let mut render_result = RenderResult::new();
@@ -2436,7 +2681,7 @@ pub mod example_setup {}
/// Source code (./src/main.rs)
/// ```ignore
/// use mingling::prelude::*;
-/// use mingling::{Groupped, StructuralData, parser::Picker, setup::StructuralRendererSetup};
+/// use mingling::{parser::Picker, setup::StructuralRendererSetup, Grouped, StructuralData};
/// use serde::Serialize;
/// use std::io::Write;
///
@@ -2453,12 +2698,12 @@ pub mod example_setup {}
/// // --------- IMPORTANT ---------
/// // For beautiful output structure, do not use `pack!` to wrap the types that need to be output.
/// // Instead, manually implement
-/// // __________________________________ Mark as structured data so it can be rendered
-/// // / ____________________ Implement serde::Serialize
-/// // | / _________ Implement mingling::Groupped
-/// // | | / to ensure Mingling can recognize the type
-/// // vvvvvvvvvvvv vvvvvvvvv vvvvvvvv
-/// #[derive(StructuralData, Serialize, Groupped)]
+/// // ____________________________________ Mark as structured data so it can be rendered
+/// // / ____________________ Implement serde::Serialize
+/// // | / _________ Implement mingling::Grouped
+/// // | | / to ensure Mingling can recognize the type
+/// // vvvvvvvvvvvv vvvvvvvvv vvvvvvv
+/// #[derive(StructuralData, Serialize, Grouped)]
/// struct Info {
/// #[serde(rename = "member_name")]
/// name: String,
diff --git a/mingling/src/features.rs b/mingling/src/features.rs
index cb474ae..78d6226 100644
--- a/mingling/src/features.rs
+++ b/mingling/src/features.rs
@@ -97,6 +97,17 @@ pub const MINGLING_DISPATCH_TREE: bool = false;
#[cfg(feature = "dispatch_tree")]
#[allow(unused)]
pub const MINGLING_DISPATCH_TREE: bool = true;
+/// Whether the `docs_rs` feature is enabled
+/// Current: `disabled`
+#[cfg(not(feature = "docs_rs"))]
+#[allow(unused)]
+pub const MINGLING_DOCS_RS: bool = false;
+
+/// Whether the `docs_rs` feature is enabled
+/// Current: `enabled`
+#[cfg(feature = "docs_rs")]
+#[allow(unused)]
+pub const MINGLING_DOCS_RS: bool = true;
/// Whether the `extra_macros` feature is enabled
/// Current: `disabled`
#[cfg(not(feature = "extra_macros"))]
diff --git a/mingling/src/lib.md b/mingling/src/lib.md
index 03fa61d..4d20a8b 100644
--- a/mingling/src/lib.md
+++ b/mingling/src/lib.md
@@ -76,7 +76,7 @@ Command not found: [great]
## Examples
`Mingling` provides detailed usage examples for your reference.
-See [Examples](_mingling_examples/index.html) or [Helpdoc](https://mingling-rs.github.io/mingling/docs/examples.html)
+See [Examples](EXAMPLES/index.html) or [Helpdoc](https://mingling-rs.github.io/mingling/docs/examples.html)
## About Features
diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs
index 718d282..f5362d5 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -1,3 +1,8 @@
+#![doc(html_logo_url = "https://github.com/mingling-rs/mingling/raw/main/docs/res/icon3.png")]
+#![doc(
+ html_favicon_url = "https://github.com/mingling-rs/mingling/raw/main/docs/res/favicon_small.png"
+)]
+#![deny(missing_docs)]
#![doc = include_str!("lib.md")]
#[cfg(feature = "core")]
@@ -16,12 +21,13 @@ pub mod parser;
/// `Mingling` argument parser (Picker2)
#[cfg(feature = "picker")]
-pub mod picker {
- pub use arg_picker::*;
+pub mod picker;
- pub mod parselib {
- pub use arg_picker::parselib::*;
- }
+mod constants;
+
+/// Constants used throughout the Mingling framework.
+pub mod consts {
+ pub use crate::constants::*;
}
/// Re-export of all macros from `mingling_macros`.
@@ -41,6 +47,8 @@ pub mod macros {
/// New Parser provided by the `picker` feature
#[cfg(feature = "picker")]
pub use arg_picker::macros::*;
+ /// `#[buffer]` - Wraps a unit-returning function to produce a `RenderResult`.
+ pub use mingling_macros::buffer;
/// `#[chain]` - Used to generate a struct implementing the `Chain` trait via a method
pub use mingling_macros::chain;
/// `#[completion(EntryType)]` - Used to generate completion entry
@@ -90,6 +98,12 @@ pub mod macros {
/// `#[program_setup]` - Used to generate program setup
#[cfg(feature = "extra_macros")]
pub use mingling_macros::program_setup;
+ /// `r_print!` - Prints text to a `RenderResult` buffer (without newline).
+ /// See the macro documentation for implicit vs. explicit buffer usage.
+ pub use mingling_macros::r_print;
+ /// `r_println!` - Prints text to a `RenderResult` buffer (with newline).
+ /// See the macro documentation for implicit vs. explicit buffer usage.
+ pub use mingling_macros::r_println;
#[doc(hidden)]
pub use mingling_macros::register_chain;
#[doc(hidden)]
@@ -105,6 +119,10 @@ pub mod macros {
/// `route! { /* ... */ }` - Used to generate a route that either returns a successful result or early returns an error.
#[cfg(feature = "extra_macros")]
pub use mingling_macros::route;
+ /// `#[routeify]` - An extension attribute macro that transforms `expr?` into `route!(expr)`.
+ /// Can be used standalone or as a chain/renderer extension: `#[chain(routeify, ...)]`.
+ #[cfg(feature = "extra_macros")]
+ pub use mingling_macros::routeify;
/// `suggest! { "hello", "bye" }` - Used to generate suggestions
#[cfg(feature = "comp")]
pub use mingling_macros::suggest;
@@ -117,17 +135,18 @@ pub mod macros {
#[cfg(feature = "macros")]
pub use mingling_macros::EnumTag;
-/// derive macro Groupped
+/// derive macro Grouped
#[cfg(feature = "macros")]
-pub use mingling_macros::Groupped;
+pub use mingling_macros::Grouped;
/// derive macro `StructuralData` — marks a type as supporting structured output
#[cfg(feature = "structural_renderer")]
pub use mingling_macros::StructuralData;
/// Example projects for `Mingling`, for learning how to use `Mingling`
-#[cfg(feature = "core")]
-pub mod _mingling_examples {
+#[cfg(all(feature = "core", feature = "docs_rs"))]
+#[allow(nonstandard_style)]
+pub mod EXAMPLES {
pub use crate::example_docs::*;
}
@@ -170,12 +189,15 @@ pub mod res;
/// use mingling::prelude::*;
/// ```
pub mod prelude {
- /// Re-export of the `Groupped` derive macro for grouping types.
+ /// Re-export of the `Grouped` trait
#[cfg(feature = "core")]
- pub use crate::Groupped;
+ pub use crate::Grouped;
/// Re-export of the `RenderResult` struct for outputting rendering result
#[cfg(feature = "core")]
pub use crate::RenderResult;
+ /// Re-export of the `Routable` trait
+ #[cfg(feature = "core")]
+ pub use crate::Routable;
/// Re-export of the `chain` macro for defining a chain of commands.
#[cfg(feature = "macros")]
pub use crate::macros::chain;
@@ -207,6 +229,12 @@ pub mod prelude {
/// Like `pack!` but also marks the type for structured output
#[cfg(all(feature = "macros", feature = "structural_renderer"))]
pub use mingling_macros::pack_structural;
+ /// `r_print!` - Prints text to a `RenderResult` buffer (without newline).
+ /// See the macro documentation for implicit vs. explicit buffer usage.
+ pub use mingling_macros::r_print;
+ /// `r_println!` - Prints text to a `RenderResult` buffer (with newline).
+ /// See the macro documentation for implicit vs. explicit buffer usage.
+ pub use mingling_macros::r_println;
/// Re-export of the `completion` macro for generating completion entries.
#[cfg(all(feature = "macros", feature = "comp"))]
@@ -217,9 +245,8 @@ pub mod prelude {
pub use crate::parser::AsPicker;
#[cfg(feature = "picker")]
- pub use arg_picker::prelude::*;
+ pub use arg_picker::prelude::arg;
- /// Used to enable the `writeln!` macro for `RenderResult`
- #[cfg(feature = "core")]
- pub use std::io::Write;
+ #[cfg(feature = "picker")]
+ pub use crate::picker::EntryPicker;
}
diff --git a/mingling/src/parser/picker.rs b/mingling/src/parser/picker.rs
index 601cd3f..ca4561c 100644
--- a/mingling/src/parser/picker.rs
+++ b/mingling/src/parser/picker.rs
@@ -743,6 +743,10 @@ where
}
}
+/// Trait for types that can be converted into a `Picker` to extract values from command-line arguments.
+///
+/// This trait provides a convenient way to convert a value (such as `Vec<String>`, `&[String]`, etc.)
+/// into a `Picker` and immediately start extracting values associated with specific flags.
pub trait AsPicker
where
Self: Into<Vec<String>>,
diff --git a/mingling/src/picker.rs b/mingling/src/picker.rs
new file mode 100644
index 0000000..f370656
--- /dev/null
+++ b/mingling/src/picker.rs
@@ -0,0 +1,13 @@
+/// Provides the specific parsing logic for command-line arguments and common utilities,
+/// as well as customization of command-line argument styles.
+pub mod parselib {
+ pub use arg_picker::parselib::*;
+}
+
+pub use arg_picker::*;
+
+mod entry_picker;
+pub use entry_picker::*;
+
+mod global;
+pub use global::*;
diff --git a/arg_picker/src/corebind/entry_picker.rs b/mingling/src/picker/entry_picker.rs
index d543f98..d9fb37c 100644
--- a/arg_picker/src/corebind/entry_picker.rs
+++ b/mingling/src/picker/entry_picker.rs
@@ -1,10 +1,8 @@
-use std::marker::PhantomData;
+use mingling_core::{ChainProcess, Grouped, ProgramCollect};
-use mingling_core::{ChainProcess, Groupped, ProgramCollect};
+use crate::{picker::Pickable, picker::Picker, picker::PickerArg, picker::PickerPattern1};
-use crate::{Pickable, Picker, PickerArg, PickerArgs, PickerPattern1};
-
-/// Trait for converting Mingling entry types (types that implement `Groupped<R>` and `Into<Vec<String>>`)
+/// Trait for converting Mingling entry types (types that implement `Grouped<R>` and `Into<Vec<String>>`)
/// into [`Picker`] instances for a given route type `R`.
///
/// This trait provides a bridge between entry definitions created with the `mingling` framework
@@ -14,7 +12,7 @@ use crate::{Pickable, Picker, PickerArg, PickerArgs, PickerPattern1};
///
/// * `'a` — The lifetime of the picker and its references to argument definitions.
/// * `This` — The program type used for dispatching runtime routes; must implement [`ProgramCollect`].
-/// * `Route` — The route type used for dispatching; must implement [`Groupped<This>`].
+/// * `Route` — The route type used for dispatching; must implement [`Grouped<This>`].
pub trait EntryPicker<'a, This> {
/// Converts `self` into a [`Picker`] for the given route type `Route`.
fn to_picker(self) -> Picker<'a, ChainProcess<This>>;
@@ -37,10 +35,10 @@ pub trait EntryPicker<'a, This> {
) -> PickerPattern1<'a, Next, ChainProcess<This>>
where
Self: Sized,
- Next: Pickable<'a> + Default + Sized,
+ Next: Pickable<'a> + Sized,
{
let picker = Self::to_picker(self);
- Picker::build_pattern1(picker.args, arg.into(), None)
+ Picker::build_pattern1(picker.into_args(), arg.into(), None)
}
/// Starts building a picker pattern with the first argument, using a default value provider.
@@ -62,7 +60,7 @@ pub trait EntryPicker<'a, This> {
) -> PickerPattern1<'a, Next, ChainProcess<This>>
where
Self: Sized,
- Next: Pickable<'a> + Default + Sized,
+ Next: Pickable<'a> + Sized,
F: FnMut() -> Next + 'static,
{
self.pick(arg).or(func)
@@ -109,7 +107,7 @@ pub trait EntryPicker<'a, This> {
) -> PickerPattern1<'a, Next, ChainProcess<This>>
where
Self: Sized,
- Next: Pickable<'a> + Default + Sized,
+ Next: Pickable<'a> + Sized,
F: FnMut() -> ChainProcess<This> + 'static,
{
self.pick(arg).or_route(func)
@@ -119,13 +117,10 @@ pub trait EntryPicker<'a, This> {
impl<'a, This, Bind> EntryPicker<'a, This> for Bind
where
This: ProgramCollect<Enum = This>,
- Bind: Groupped<This> + Into<Vec<String>>,
+ Bind: Grouped<This> + Into<Vec<String>>,
{
fn to_picker(self) -> Picker<'a, ChainProcess<This>> {
let args = self.into();
- Picker {
- route_phantom: PhantomData,
- args: PickerArgs::Owned(args),
- }
+ Picker::from(args)
}
}
diff --git a/mingling/src/picker/global.rs b/mingling/src/picker/global.rs
new file mode 100644
index 0000000..c203524
--- /dev/null
+++ b/mingling/src/picker/global.rs
@@ -0,0 +1,35 @@
+use arg_picker::{IntoPicker, Pickable, PickerArg, value::Flag};
+use mingling_core::{Program, ProgramCollect};
+
+use crate::consts::REMAINS;
+
+/// Picks a global flag from the program's arguments.
+///
+/// This function takes ownership of the program's current arguments, picks the specified `flag`
+/// from them, and then returns the remaining arguments back to the program. It returns the
+/// boolean value of the flag.
+pub fn pick_global_flag<C>(program: &mut Program<C>, flag: &PickerArg<Flag>) -> bool
+where
+ C: ProgramCollect<Enum = C>,
+{
+ let args = program.take_args();
+ let (flag, args) = args.pick(flag).pick(&REMAINS).unwrap();
+ program.replace_args(args.into());
+ *flag
+}
+
+/// Picks a global argument from the program's arguments.
+///
+/// This function takes ownership of the program's current arguments, picks the specified `arg`
+/// from them, and then returns the remaining arguments back to the program. It returns the
+/// picked argument value, or `None` if the argument was not present.
+pub fn pick_global_argument<C, A>(program: &mut Program<C>, arg: &PickerArg<A>) -> Option<A>
+where
+ A: for<'a> Pickable<'a> + Default,
+ C: ProgramCollect<Enum = C>,
+{
+ let args = program.take_args();
+ let (arg, remains) = args.pick(arg).pick(&REMAINS).unpack();
+ program.replace_args(remains.unwrap().into());
+ arg
+}
diff --git a/mingling/src/setups.rs b/mingling/src/setups.rs
index e572cf5..6fd728a 100644
--- a/mingling/src/setups.rs
+++ b/mingling/src/setups.rs
@@ -7,6 +7,13 @@ pub use dirs::*;
mod exit_code;
pub use exit_code::*;
+/// Picker's `ProgramSetup` variant.
+///
+/// Internally does not use its own argument parsing,
+/// but relies on `arg_picker`'s argument parsing capability.
+#[cfg(feature = "picker")]
+pub mod picker;
+
#[cfg(feature = "structural_renderer")]
mod structural_renderer;
diff --git a/mingling/src/setups/basic.rs b/mingling/src/setups/basic.rs
index 6a69733..e081c3e 100644
--- a/mingling/src/setups/basic.rs
+++ b/mingling/src/setups/basic.rs
@@ -1,3 +1,5 @@
+#![allow(deprecated)]
+
use mingling_core::{Flag, Program, ProgramCollect, setup::ProgramSetup};
/// Performs basic program initialization:
@@ -5,6 +7,12 @@ use mingling_core::{Flag, Program, ProgramCollect, setup::ProgramSetup};
/// - Collects `--quiet` flag to control message rendering
/// - Collects `--help` flag to enable help mode
/// - Collects `--confirm` flag to skip user confirmation
+#[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::BasicProgramSetup` instead"
+ )
+)]
pub struct BasicProgramSetup;
impl<C> ProgramSetup<C> for BasicProgramSetup
@@ -21,6 +29,12 @@ where
/// Provides setup for parsing the user help flag
///
/// The default value is `--help / -h`
+#[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::HelpFlagSetup` instead"
+ )
+)]
pub struct HelpFlagSetup {
flag: Flag,
}
@@ -54,6 +68,12 @@ impl Default for HelpFlagSetup {
/// Provides setup for parsing the quiet flag
///
/// The default value is `--quiet / -q`
+#[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::QuietFlagSetup` instead"
+ )
+)]
pub struct QuietFlagSetup {
flag: Flag,
}
@@ -88,6 +108,12 @@ impl Default for QuietFlagSetup {
/// Provides setup for parsing the confirm flag
///
/// The default value is `--confirm / -C`
+#[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::ConfirmFlagSetup` instead"
+ )
+)]
pub struct ConfirmFlagSetup {
flag: Flag,
}
diff --git a/mingling/src/setups/exit_code.rs b/mingling/src/setups/exit_code.rs
index 025ed8a..6b8a1ef 100644
--- a/mingling/src/setups/exit_code.rs
+++ b/mingling/src/setups/exit_code.rs
@@ -2,7 +2,7 @@ use std::marker::PhantomData;
use mingling_core::{
ProgramCollect,
- hook::{ProgramControlUnit, ProgramHook},
+ hook::{ProgramControlUnit, ProgramControls, ProgramHook},
setup::ProgramSetup,
this,
};
@@ -39,7 +39,14 @@ where
// Insert hook to override exit code before program ends
program.with_hook(ProgramHook::empty().on_finish(|_| {
let this = this::<C>().res_or_default::<ResExitCode>();
- ProgramControlUnit::OverrideExitCode(this.exit_code)
+ let ec = this.exit_code;
+
+ // Only override when ResExitCode has been modified
+ if ec != 0 {
+ ProgramControlUnit::OverrideExitCode(this.exit_code).into()
+ } else {
+ ProgramControls::Empty
+ }
}));
}
}
diff --git a/mingling/src/setups/picker.rs b/mingling/src/setups/picker.rs
new file mode 100644
index 0000000..0b7bd33
--- /dev/null
+++ b/mingling/src/setups/picker.rs
@@ -0,0 +1,7 @@
+mod basic;
+pub use basic::*;
+
+#[cfg(feature = "structural_renderer")]
+mod structural_renderer;
+#[cfg(feature = "structural_renderer")]
+pub use structural_renderer::*;
diff --git a/mingling/src/setups/picker/basic.rs b/mingling/src/setups/picker/basic.rs
new file mode 100644
index 0000000..c9f82b3
--- /dev/null
+++ b/mingling/src/setups/picker/basic.rs
@@ -0,0 +1,123 @@
+use arg_picker::{PickerArg, value::Flag};
+use mingling_core::{Program, ProgramCollect, setup::ProgramSetup};
+
+use crate::{
+ consts::{CONFIRM_FLAG, HELP_FLAG, QUIET_FLAG},
+ picker::pick_global_flag,
+};
+
+/// Performs basic program initialization:
+///
+/// - Collects `--quiet` flag to control message rendering
+/// - Collects `--help` flag to enable help mode
+/// - Collects `--confirm` flag to skip user confirmation
+pub struct BasicProgramSetup;
+
+impl<C> ProgramSetup<C> for BasicProgramSetup
+where
+ C: ProgramCollect<Enum = C>,
+{
+ fn setup(self, program: &mut Program<C>) {
+ program.with_setup(HelpFlagSetup::default());
+ program.with_setup(QuietFlagSetup::default());
+ program.with_setup(ConfirmFlagSetup::default());
+ }
+}
+
+/// Provides setup for parsing the user help flag
+///
+/// The default value is `--help / -h`
+pub struct HelpFlagSetup<'a> {
+ flag: &'a PickerArg<'a, Flag>,
+}
+
+impl<'a> HelpFlagSetup<'a> {
+ /// Creates a new `HelpFlagSetup` with the given flag aliases.
+ pub fn new(flag: &'a PickerArg<Flag>) -> Self {
+ Self { flag }
+ }
+}
+
+impl<'a, C> ProgramSetup<C> for HelpFlagSetup<'a>
+where
+ C: ProgramCollect<Enum = C>,
+{
+ fn setup(self, program: &mut Program<C>) {
+ let help = pick_global_flag(program, self.flag);
+ if help {
+ program.user_context.help = true;
+ }
+ }
+}
+
+impl<'a> Default for HelpFlagSetup<'a> {
+ fn default() -> Self {
+ Self { flag: &HELP_FLAG }
+ }
+}
+
+/// Provides setup for parsing the quiet flag
+///
+/// The default value is `--quiet / -q`
+pub struct QuietFlagSetup<'a> {
+ flag: &'a PickerArg<'a, Flag>,
+}
+
+impl<'a> QuietFlagSetup<'a> {
+ /// Creates a new `QuietFlagSetup` with the given flag aliases.
+ pub fn new(flag: &'a PickerArg<Flag>) -> Self {
+ Self { flag }
+ }
+}
+
+impl<'a, C> ProgramSetup<C> for QuietFlagSetup<'a>
+where
+ C: ProgramCollect<Enum = C>,
+{
+ fn setup(self, program: &mut Program<C>) {
+ let help = pick_global_flag(program, self.flag);
+ if help {
+ program.stdout_setting.quiet = true;
+ }
+ }
+}
+
+impl<'a> Default for QuietFlagSetup<'a> {
+ fn default() -> Self {
+ Self { flag: &QUIET_FLAG }
+ }
+}
+
+/// Provides setup for parsing the confirm flag
+///
+/// The default value is `--confirm / -C`
+pub struct ConfirmFlagSetup<'a> {
+ flag: &'a PickerArg<'a, Flag>,
+}
+
+impl<'a> ConfirmFlagSetup<'a> {
+ /// Creates a new `ConfirmFlagSetup` with the given flag aliases.
+ pub fn new(flag: &'a PickerArg<Flag>) -> Self {
+ Self { flag }
+ }
+}
+
+impl<'a, C> ProgramSetup<C> for ConfirmFlagSetup<'a>
+where
+ C: ProgramCollect<Enum = C>,
+{
+ fn setup(self, program: &mut Program<C>) {
+ let help = pick_global_flag(program, self.flag);
+ if help {
+ program.user_context.confirm = true;
+ }
+ }
+}
+
+impl<'a> Default for ConfirmFlagSetup<'a> {
+ fn default() -> Self {
+ Self {
+ flag: &CONFIRM_FLAG,
+ }
+ }
+}
diff --git a/mingling/src/setups/picker/structural_renderer.rs b/mingling/src/setups/picker/structural_renderer.rs
new file mode 100644
index 0000000..1fa48fd
--- /dev/null
+++ b/mingling/src/setups/picker/structural_renderer.rs
@@ -0,0 +1,76 @@
+use mingling_core::{Program, ProgramCollect, setup::ProgramSetup};
+
+use crate::{
+ consts::RENDERER_ARG,
+ picker::{pick_global_argument, pick_global_flag},
+};
+
+/// Sets up the structural renderer for the program:
+///
+/// - Adds a `--renderer` global argument to specify the renderer type
+pub struct StructuralRendererSimpleSetup;
+
+impl<C> ProgramSetup<C> for StructuralRendererSimpleSetup
+where
+ C: ProgramCollect<Enum = C>,
+{
+ fn setup(self, program: &mut Program<C>) {
+ if let Some(renderer) = pick_global_argument(program, &RENDERER_ARG) {
+ program.structural_renderer_name = renderer.into();
+ }
+ }
+}
+
+/// Sets up the structural renderer for the program:
+///
+/// - Adds global flags to specify the renderer type:
+/// * `--json` for JSON output
+/// * `--json-pretty` for pretty-printed JSON output
+/// * `--yaml` for YAML output
+/// * `--toml` for TOML output
+/// * `--ron` for RON output
+/// * `--ron-pretty` for pretty-printed RON output
+///
+/// # Flag priority
+///
+/// If multiple flags are specified, the last matching flag in the following
+/// declaration order takes precedence:
+/// 1. `--json`
+/// 2. `--json-pretty`
+/// 3. `--yaml`
+/// 4. `--toml`
+/// 5. `--ron`
+/// 6. `--ron-pretty`
+pub struct StructuralRendererSetup;
+
+impl<C> ProgramSetup<C> for StructuralRendererSetup
+where
+ C: ProgramCollect<Enum = C>,
+{
+ fn setup(self, program: &mut Program<C>) {
+ #[cfg(feature = "json_serde_fmt")]
+ if pick_global_flag(program, &crate::consts::JSON_FLAG) {
+ program.structural_renderer_name = crate::StructuralRendererSetting::Json;
+ }
+ #[cfg(feature = "json_serde_fmt")]
+ if pick_global_flag(program, &crate::consts::JSON_PRETTY_FLAG) {
+ program.structural_renderer_name = crate::StructuralRendererSetting::JsonPretty;
+ }
+ #[cfg(feature = "yaml_serde_fmt")]
+ if pick_global_flag(program, &crate::consts::YAML_FLAG) {
+ program.structural_renderer_name = crate::StructuralRendererSetting::Yaml;
+ }
+ #[cfg(feature = "toml_serde_fmt")]
+ if pick_global_flag(program, &crate::consts::TOML_FLAG) {
+ program.structural_renderer_name = crate::StructuralRendererSetting::Toml;
+ }
+ #[cfg(feature = "ron_serde_fmt")]
+ if pick_global_flag(program, &crate::consts::RON_FLAG) {
+ program.structural_renderer_name = crate::StructuralRendererSetting::Ron;
+ }
+ #[cfg(feature = "ron_serde_fmt")]
+ if pick_global_flag(program, &crate::consts::RON_PRETTY_FLAG) {
+ program.structural_renderer_name = crate::StructuralRendererSetting::RonPretty;
+ }
+ }
+}
diff --git a/mingling/src/setups/repl_basic.rs b/mingling/src/setups/repl_basic.rs
index 71a38d2..cf04372 100644
--- a/mingling/src/setups/repl_basic.rs
+++ b/mingling/src/setups/repl_basic.rs
@@ -19,7 +19,9 @@ where
/// meaning only the last configured instance will take effect globally.
/// Do not configure multiple prompts with different values — only one will be used.
pub enum BasicREPLPromptSetup {
+ /// A static prompt string that is displayed before each REPL input.
Prompt(String),
+ /// A function that returns a dynamic prompt string each time the REPL reads input.
Func(fn() -> String),
}
diff --git a/mingling/src/setups/structural_renderer.rs b/mingling/src/setups/structural_renderer.rs
index af3ed91..0ab2347 100644
--- a/mingling/src/setups/structural_renderer.rs
+++ b/mingling/src/setups/structural_renderer.rs
@@ -1,8 +1,16 @@
+#![allow(deprecated)]
+
use mingling_core::{Program, ProgramCollect, setup::ProgramSetup};
/// Sets up the structural renderer for the program:
///
/// - Adds a `--renderer` global argument to specify the renderer type
+#[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::StructuralRendererSimpleSetup` instead"
+ )
+)]
pub struct StructuralRendererSimpleSetup;
impl<C> ProgramSetup<C> for StructuralRendererSimpleSetup
@@ -25,6 +33,12 @@ where
/// * `--toml` for TOML output
/// * `--ron` for RON output
/// * `--ron-pretty` for pretty-printed RON output
+#[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When the `picker` feature is enabled, you can use `mingling::setup::picker::StructuralRendererSetup` instead"
+ )
+)]
pub struct StructuralRendererSetup;
impl<C> ProgramSetup<C> for StructuralRendererSetup
diff --git a/mingling_cli/Cargo.lock b/mingling_cli/Cargo.lock
new file mode 100644
index 0000000..378c81b
--- /dev/null
+++ b/mingling_cli/Cargo.lock
@@ -0,0 +1,104 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "arg-picker"
+version = "0.1.0"
+dependencies = [
+ "arg-picker-macros",
+ "just_fmt",
+]
+
+[[package]]
+name = "arg-picker-macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
+name = "mingling"
+version = "0.3.0"
+dependencies = [
+ "arg-picker",
+ "mingling_core",
+ "mingling_macros",
+]
+
+[[package]]
+name = "mingling-cli"
+version = "0.3.0"
+dependencies = [
+ "mingling",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "mingling_pathf",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "mingling_pathf"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/mingling_cli/Cargo.toml b/mingling_cli/Cargo.toml
new file mode 100644
index 0000000..1cc45b6
--- /dev/null
+++ b/mingling_cli/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "mingling-cli"
+version = "0.3.0"
+edition = "2024"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/mingling-rs/mingling/tree/main/mingling_cli"
+authors = ["Weicao-CatilGrass"]
+readme = "README.md"
+
+description = "Mingling's scaffolding tool for generating, analyzing, and modifying Mingling projects"
+keywords = ["cli", "cli-framework", "command-line", "tools"]
+categories = ["command-line-interface"]
+
+[dependencies.mingling]
+path = "../mingling"
+features = [
+ "picker",
+ "pathf",
+ "dispatch_tree",
+ "extra_macros"
+]
+
+[dependencies]
+
+[workspace]
diff --git a/mingling_cli/src/main.rs b/mingling_cli/src/main.rs
new file mode 100644
index 0000000..e7a11a9
--- /dev/null
+++ b/mingling_cli/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+ println!("Hello, world!");
+}
diff --git a/mingling_core/Cargo.toml b/mingling_core/Cargo.toml
index 85afd55..14140c6 100644
--- a/mingling_core/Cargo.toml
+++ b/mingling_core/Cargo.toml
@@ -15,6 +15,7 @@ nightly = []
default = []
async = []
builds = []
+picker = []
dispatch_tree = []
structural_renderer = ["dep:serde"]
diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs
index 2680f43..ec29a1b 100644
--- a/mingling_core/src/any.rs
+++ b/mingling_core/src/any.rs
@@ -1,12 +1,12 @@
use crate::error::ChainProcessError;
-use crate::{Groupped, ProgramCollect};
+use crate::{Grouped, ProgramCollect};
#[doc(hidden)]
pub mod group;
/// Any type output
///
-/// Accepts any type that implements `Send + Groupped<G>`
+/// Accepts any type that implements `Send + Grouped<G>`
/// After being passed into `AnyOutput`, it will be converted to `Box<dyn Any + Send + 'static>`
///
/// Note:
@@ -17,15 +17,26 @@ pub mod group;
#[derive(Debug)]
pub struct AnyOutput<G> {
pub(crate) inner: Box<dyn std::any::Any + Send + 'static>,
+
+ /// The [`TypeId`] of the concrete type stored in `inner`.
+ ///
+ /// This is set during construction and used for type-checking
+ /// in downcast, restore, and is methods.
pub type_id: std::any::TypeId,
+
+ /// The variant identifier returned by [`Grouped::member_id`] for the
+ /// concrete type stored in `inner`.
+ ///
+ /// This is used by the scheduler to dispatch on the correct enum
+ /// variant when routing the output.
pub member_id: G,
}
impl<G> AnyOutput<G> {
- /// Create an `AnyOutput` from a `Send + Groupped<G>` type
+ /// Create an `AnyOutput` from a `Send + Grouped<G>` type
pub fn new<T>(value: T) -> Self
where
- T: Send + Groupped<G> + 'static,
+ T: Send + Grouped<G> + 'static,
{
Self {
inner: Box::new(value),
@@ -106,7 +117,9 @@ impl<G> std::ops::DerefMut for AnyOutput<G> {
/// - Returns `Ok((`[`AnyOutput`](./struct.AnyOutput.html)`, `[`NextProcess::Renderer`](./enum.NextProcess.html)`))` to render this type next and output to the terminal
/// - Returns `Err(`[`ChainProcessError`](./error/enum.ChainProcessError.html)`]` to terminate the program directly
pub enum ChainProcess<G> {
+ /// Indicates success, containing the output value and the next step to execute.
Ok((AnyOutput<G>, NextProcess)),
+ /// Indicates a processing failure, containing the error that occurred.
Err(ChainProcessError),
}
@@ -117,7 +130,9 @@ pub enum ChainProcess<G> {
#[derive(Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum NextProcess {
+ /// Continue execution to the next chain
Chain,
+ /// Send output to renderer and end execution
Renderer,
}
@@ -148,7 +163,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
- use crate::Groupped;
+ use crate::Grouped;
/// Mock enum for testing AnyOutput
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -175,7 +190,7 @@ mod tests {
value: i32,
}
- impl Groupped<MockGroup> for AlphaData {
+ impl Grouped<MockGroup> for AlphaData {
fn member_id() -> MockGroup {
MockGroup::Alpha
}
@@ -187,7 +202,7 @@ mod tests {
name: String,
}
- impl Groupped<MockGroup> for BetaData {
+ impl Grouped<MockGroup> for BetaData {
fn member_id() -> MockGroup {
MockGroup::Beta
}
@@ -198,7 +213,7 @@ mod tests {
#[cfg_attr(feature = "structural_renderer", derive(serde::Serialize))]
struct GammaData;
- impl Groupped<MockGroup> for GammaData {
+ impl Grouped<MockGroup> for GammaData {
fn member_id() -> MockGroup {
MockGroup::Gamma
}
@@ -354,7 +369,7 @@ mod tests {
x: i32,
}
- impl Groupped<MockGroup> for SerData {
+ impl Grouped<MockGroup> for SerData {
fn member_id() -> MockGroup {
MockGroup::Gamma
}
@@ -381,13 +396,13 @@ mod tests {
b: String,
}
- impl Groupped<MockGroup> for SerA {
+ impl Grouped<MockGroup> for SerA {
fn member_id() -> MockGroup {
MockGroup::Alpha
}
}
- impl Groupped<MockGroup> for SerB {
+ impl Grouped<MockGroup> for SerB {
fn member_id() -> MockGroup {
MockGroup::Beta
}
diff --git a/mingling_core/src/any/group.rs b/mingling_core/src/any/group.rs
index cb847d9..2813ad5 100644
--- a/mingling_core/src/any/group.rs
+++ b/mingling_core/src/any/group.rs
@@ -1,34 +1,28 @@
-use crate::{AnyOutput, ChainProcess};
+use crate::{AnyOutput, ChainProcess, ProgramCollect, Routable};
/// Used to mark a type with a unique enum ID, assisting dynamic dispatch
///
-/// **Note:** Unlike earlier versions, `Groupped` no longer requires `Serialize`
+/// **Note:** Unlike earlier versions, `Grouped` no longer requires `Serialize`
/// even when the `structural_renderer` feature is enabled. Structured output is
/// controlled separately via the \[`StructuralData`\] trait.
-pub trait Groupped<Group>
+pub trait Grouped<Group>
where
Self: Sized + 'static,
{
/// Returns the specific enum value representing its ID within that enum
fn member_id() -> Group;
+}
- /// Converts the grouped item into a `ChainProcess` directed to the chain route.
- ///
- /// This wraps the item into an `AnyOutput` and routes it to the chain processing pipeline.
- fn to_chain(self) -> ChainProcess<Group>
- where
- Self: Send,
- {
+impl<T, C> Routable<C> for T
+where
+ C: ProgramCollect<Enum = C>,
+ T: Grouped<C> + Send,
+{
+ fn to_chain(self) -> ChainProcess<C> {
AnyOutput::new(self).route_chain()
}
- /// Converts the grouped item into a `ChainProcess` directed to the render route.
- ///
- /// This wraps the item into an `AnyOutput` and routes it to the render processing pipeline.
- fn to_render(self) -> ChainProcess<Group>
- where
- Self: Send,
- {
+ fn to_render(self) -> ChainProcess<C> {
AnyOutput::new(self).route_renderer()
}
}
diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs
index b1fd617..9b9a5d4 100644
--- a/mingling_core/src/asset.rs
+++ b/mingling_core/src/asset.rs
@@ -21,3 +21,6 @@ pub mod node;
#[doc(hidden)]
pub mod renderer;
+
+#[doc(hidden)]
+pub mod routable;
diff --git a/mingling_core/src/asset/dispatcher.rs b/mingling_core/src/asset/dispatcher.rs
index 8f04955..01c9ccf 100644
--- a/mingling_core/src/asset/dispatcher.rs
+++ b/mingling_core/src/asset/dispatcher.rs
@@ -32,22 +32,46 @@ where
C: ProgramCollect<Enum = C>,
{
/// Adds a dispatcher to the program.
- #[cfg(not(feature = "dispatch_tree"))]
+ #[cfg_attr(
+ feature = "dispatch_tree",
+ deprecated(
+ note = "When the `dispatch_tree` feature is enabled, the `dispatcher` field no longer exists inside Program. All types are collected at compile time by the `gen_program!()` macro, so the `with_dispatcher` function is no longer needed"
+ )
+ )]
pub fn with_dispatcher<Disp>(&mut self, dispatcher: Disp)
where
Disp: Dispatcher<C> + Send + Sync + 'static,
{
- self.dispatcher.push(Box::new(dispatcher));
+ #[cfg(not(feature = "dispatch_tree"))]
+ {
+ self.dispatcher.push(Box::new(dispatcher));
+ }
+ #[cfg(feature = "dispatch_tree")]
+ {
+ let _ = dispatcher;
+ }
}
/// Add some dispatchers to the program.
- #[cfg(not(feature = "dispatch_tree"))]
+ #[cfg_attr(
+ feature = "dispatch_tree",
+ deprecated(
+ note = "When the `dispatch_tree` feature is enabled, the `dispatcher` field no longer exists inside Program. All types are collected at compile time by the `gen_program!()` macro, so the `with_dispatcher` function is no longer needed"
+ )
+ )]
pub fn with_dispatchers<D>(&mut self, dispatchers: D)
where
D: Into<Dispatchers<C>>,
{
- let dispatchers = dispatchers.into();
- self.dispatcher.extend(dispatchers.dispatcher);
+ #[cfg(not(feature = "dispatch_tree"))]
+ {
+ let dispatchers = dispatchers.into();
+ self.dispatcher.extend(dispatchers.dispatcher);
+ }
+ #[cfg(feature = "dispatch_tree")]
+ {
+ let _ = dispatchers;
+ }
}
}
diff --git a/mingling_core/src/asset/routable.rs b/mingling_core/src/asset/routable.rs
new file mode 100644
index 0000000..24b7bb1
--- /dev/null
+++ b/mingling_core/src/asset/routable.rs
@@ -0,0 +1,22 @@
+use crate::ChainProcess;
+
+/// Provides routing capabilities for converting an item into a `ChainProcess`
+/// directed to either the chain or render processing pipeline.
+///
+/// This trait enables items to be dispatched to different processing routes
+/// (chain or render) by wrapping them into an `AnyOutput` and routing them
+/// through the appropriate pipeline.
+pub trait Routable<Group>
+where
+ Self: Sized + 'static,
+{
+ /// Converts the routable item into a `ChainProcess` directed to the chain route.
+ ///
+ /// This wraps the item into an `AnyOutput` and routes it to the chain processing pipeline.
+ fn to_chain(self) -> ChainProcess<Group>;
+
+ /// Converts the routable item into a `ChainProcess` directed to the render route.
+ ///
+ /// This wraps the item into an `AnyOutput` and routes it to the render processing pipeline.
+ fn to_render(self) -> ChainProcess<Group>;
+}
diff --git a/mingling_core/src/comp.rs b/mingling_core/src/comp.rs
index f6fecd1..55e9952 100644
--- a/mingling_core/src/comp.rs
+++ b/mingling_core/src/comp.rs
@@ -96,7 +96,7 @@ impl CompletionHelper {
trace!("entry type: {}", any.member_id);
let dispatcher_not_found =
- <P::ErrorDispatcherNotFound as crate::Groupped<P>>::member_id();
+ <P::ErrorDispatcherNotFound as crate::Grouped<P>>::member_id();
if dispatcher_not_found == any.member_id {
debug!("dispatcher_not_found matched");
diff --git a/mingling_core/src/comp/shell_ctx.rs b/mingling_core/src/comp/shell_ctx.rs
index cfa4700..1fca325 100644
--- a/mingling_core/src/comp/shell_ctx.rs
+++ b/mingling_core/src/comp/shell_ctx.rs
@@ -1,3 +1,5 @@
+#![allow(deprecated)]
+
use std::collections::HashSet;
use crate::{Flag, ShellFlag, Suggest};
@@ -115,6 +117,13 @@ impl ShellContext {
/// // }
/// }
/// ```
+ #[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn filling_argument_first(&self, flag: impl Into<Flag>) -> bool {
let flag = flag.into();
if self.filling_argument(&flag) {
@@ -153,6 +162,13 @@ impl ShellContext {
/// // }
/// }
/// ```
+ #[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn filling_argument(&self, flag: impl Into<Flag>) -> bool {
for f in flag.into().iter() {
if self.previous_word == **f {
@@ -190,6 +206,12 @@ impl ShellContext {
/// }
/// ```
#[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn typing_argument(&self) -> bool {
#[cfg(target_os = "windows")]
{
@@ -208,6 +230,12 @@ impl ShellContext {
/// when the user has already typed certain flags. The method processes both
/// regular suggestion sets and file completion suggestions differently.
#[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn strip_typed_argument(&self, suggest: Suggest) -> Suggest {
let typed = Self::get_typed_arguments(self);
match suggest {
@@ -225,6 +253,12 @@ impl ShellContext {
/// which typically represent command-line flags or options. It returns a vector
/// containing these flag strings, converted to owned `String` values.
#[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn get_typed_arguments(&self) -> HashSet<String> {
self.all_words
.iter()
diff --git a/mingling_core/src/comp/suggest.rs b/mingling_core/src/comp/suggest.rs
index a81de64..99afc54 100644
--- a/mingling_core/src/comp/suggest.rs
+++ b/mingling_core/src/comp/suggest.rs
@@ -1,3 +1,5 @@
+#![allow(deprecated)]
+
use std::collections::BTreeSet;
use crate::ShellContext;
@@ -30,6 +32,12 @@ impl Suggest {
/// Filters out already typed flag arguments from suggestion results.
#[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn strip_typed_argument(self, ctx: &ShellContext) -> Self {
ctx.strip_typed_argument(self)
}
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
index 3c2cf9b..2cb24b9 100644
--- a/mingling_core/src/lib.rs
+++ b/mingling_core/src/lib.rs
@@ -8,6 +8,8 @@
//!
//! Recommended to import [mingling](https://crates.io/crates/mingling) to use its features.
+#![deny(missing_docs)]
+
mod any;
mod asset;
mod program;
@@ -36,6 +38,7 @@ pub use crate::asset::help::*;
pub use crate::asset::lazy_resource::*;
pub use crate::asset::node::*;
pub use crate::asset::renderer::*;
+pub use crate::asset::routable::*;
/// All error types of `Mingling`
pub mod error {
@@ -76,6 +79,10 @@ pub mod comp;
#[cfg(feature = "comp")]
pub use crate::comp::*;
+/// Module for setting up a `Mingling` program.
+///
+/// This module provides the [`ProgramSetup`] type, which allows users to configure
+/// and initialize the program's execution environment.
pub mod setup {
pub use crate::program::setup::ProgramSetup;
}
diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs
index 71d5290..11e1bbf 100644
--- a/mingling_core/src/program.rs
+++ b/mingling_core/src/program.rs
@@ -53,10 +53,30 @@ where
#[cfg(not(feature = "dispatch_tree"))]
pub(crate) dispatcher: Vec<Box<dyn Dispatcher<C> + Send + Sync>>,
+ /// Program stdout settings.
+ ///
+ /// This struct controls the program's output behavior, including whether
+ /// to output errors, render results, suppress panic messages, enable
+ /// verbose/quiet/debug modes, colored output, and progress indicators.
+ ///
+ /// # Convention-only fields
+ ///
+ /// Fields marked with convention only are not enforced by the framework;
+ /// they serve as a shared convention for applications to follow consistently.
pub stdout_setting: ProgramStdoutSetting,
+
+ /// User-defined context that can be accessed throughout the program.
+ ///
+ /// This field holds a user-defined context value that can be
+ /// accessed and modified at any point during program execution. It is
+ /// useful for passing shared state or configuration across different
+ /// parts of the program.
pub user_context: ProgramUserContext,
#[cfg(feature = "structural_renderer")]
+ /// Setting for the structural renderer.
+ ///
+ /// This field is only available when the `structural_renderer` feature is enabled.
pub structural_renderer_name: StructuralRendererSetting,
pub(crate) hooks: Vec<ProgramHook<C>>,
@@ -130,6 +150,33 @@ where
&self.args
}
+ /// Returns a mutable reference to the program's command-line arguments.
+ #[must_use]
+ pub fn get_args_mut(&mut self) -> &mut [String] {
+ &mut self.args
+ }
+
+ /// Takes ownership of the program's command-line arguments, replacing them with an empty Vec.
+ /// This is useful when you need to transfer the arguments to another context or process them
+ /// and then replace them later.
+ #[must_use]
+ pub fn take_args(&mut self) -> Vec<String> {
+ std::mem::take(&mut self.args)
+ }
+
+ /// Replaces the program's command-line arguments with a new set and returns the old ones.
+ ///
+ /// # Arguments
+ ///
+ /// * `args` - The new command-line arguments to set.
+ ///
+ /// # Returns
+ ///
+ /// The previous command-line arguments.
+ pub fn replace_args(&mut self, args: Vec<String>) -> Vec<String> {
+ std::mem::replace(&mut self.args, args)
+ }
+
/// Get all registered dispatcher names from the program
#[must_use]
pub fn get_nodes(
diff --git a/mingling_core/src/program/collection.rs b/mingling_core/src/program/collection.rs
index d5aab4b..14705ac 100644
--- a/mingling_core/src/program/collection.rs
+++ b/mingling_core/src/program/collection.rs
@@ -4,7 +4,7 @@ use std::pin::Pin;
#[cfg(feature = "dispatch_tree")]
use crate::Dispatcher;
-use crate::{AnyOutput, ChainProcess, Groupped, RenderResult};
+use crate::{AnyOutput, ChainProcess, Grouped, RenderResult};
#[cfg(feature = "structural_renderer")]
use crate::{StructuralRendererSetting, error::StructuralRendererSerializeError};
@@ -21,9 +21,17 @@ pub use mock::*;
pub trait ProgramCollect {
/// Enum type representing internal IDs for the program
type Enum;
- type ErrorDispatcherNotFound: Groupped<Self::Enum>;
- type ErrorRendererNotFound: Groupped<Self::Enum>;
- type ResultEmpty: Groupped<Self::Enum>;
+ /// Error type when a dispatcher is not found for the given member
+ type ErrorDispatcherNotFound: Grouped<Self::Enum>;
+
+ /// Error type when a renderer is not found for the given member
+ type ErrorRendererNotFound: Grouped<Self::Enum>;
+
+ /// Result type for an empty chain result
+ ///
+ /// When the `extra_macros` feature is enabled,
+ /// you can use the `empty_result!()` macro to create this
+ type ResultEmpty: Grouped<Self::Enum>;
/// Use a prefix tree to quickly match arguments and dispatch to an Entry
#[cfg(feature = "dispatch_tree")]
diff --git a/mingling_core/src/program/collection/mock.rs b/mingling_core/src/program/collection/mock.rs
index 9b2e7af..5847f10 100644
--- a/mingling_core/src/program/collection/mock.rs
+++ b/mingling_core/src/program/collection/mock.rs
@@ -4,7 +4,7 @@ use std::pin::Pin;
#[cfg(feature = "dispatch_tree")]
use crate::Dispatcher;
-use crate::{AnyOutput, ChainProcess, Groupped, ProgramCollect, RenderResult};
+use crate::{AnyOutput, ChainProcess, Grouped, ProgramCollect, RenderResult};
#[cfg(feature = "structural_renderer")]
use crate::{StructuralRendererSetting, error::StructuralRendererSerializeError};
@@ -23,7 +23,7 @@ pub enum MockProgramCollect {
Bar,
}
-impl Groupped<MockProgramCollect> for MockProgramCollect {
+impl Grouped<MockProgramCollect> for MockProgramCollect {
fn member_id() -> MockProgramCollect {
MockProgramCollect::Foo
}
diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs
index 56d8e0e..db1691b 100644
--- a/mingling_core/src/program/hook.rs
+++ b/mingling_core/src/program/hook.rs
@@ -678,7 +678,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
- use crate::Groupped;
+ use crate::Grouped;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -694,7 +694,7 @@ mod tests {
}
}
- impl Groupped<MockHookEnum> for MockHookEnum {
+ impl Grouped<MockHookEnum> for MockHookEnum {
fn member_id() -> MockHookEnum {
MockHookEnum::A
}
diff --git a/mingling_core/src/program/setup.rs b/mingling_core/src/program/setup.rs
index f248fb6..838c29a 100644
--- a/mingling_core/src/program/setup.rs
+++ b/mingling_core/src/program/setup.rs
@@ -1,9 +1,26 @@
use crate::{ProgramCollect, program::Program};
+/// Trait for defining initialization/setup logic for a `Program`.
+///
+/// Implementors can define custom setup behavior that will be executed
+/// when the program is initialized via [`Program::with_setup`].
+///
+/// # Type Parameters
+///
+/// * `C` - The program collect type, which must implement [`ProgramCollect`]
+/// and have `Enum = C` (i.e., it collects itself).
pub trait ProgramSetup<C>
where
C: ProgramCollect<Enum = C>,
{
+ /// Perform setup on the given program.
+ ///
+ /// This method consumes the setup instance (`self`) and is called once
+ /// during program initialization.
+ ///
+ /// # Arguments
+ ///
+ /// * `program` - A mutable reference to the [`Program`] being set up.
fn setup(self, program: &mut Program<C>);
}
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
index e57a5b9..2c60fa4 100644
--- a/mingling_core/src/renderer/render_result.rs
+++ b/mingling_core/src/renderer/render_result.rs
@@ -8,6 +8,11 @@ use std::{
#[derive(Default, Debug, PartialEq)]
pub struct RenderResult {
render_text: String,
+
+ /// The exit code to return from the rendering process.
+ ///
+ /// A value of `0` indicates success, while non-zero values indicate
+ /// various error conditions.
pub exit_code: i32,
}
@@ -132,8 +137,8 @@ impl RenderResult {
/// result.print(", world!");
/// assert_eq!(result.deref(), "Hello, world!");
/// ```
- pub fn print(&mut self, text: &str) {
- self.render_text.push_str(text);
+ pub fn print(&mut self, text: impl AsRef<str>) {
+ self.render_text.push_str(text.as_ref());
}
/// Appends the given text followed by a newline to the rendered content.
@@ -149,8 +154,8 @@ impl RenderResult {
/// result.println("Second line");
/// assert_eq!(result.deref(), "First line\nSecond line\n");
/// ```
- pub fn println(&mut self, text: &str) {
- self.render_text.push_str(text);
+ pub fn println(&mut self, text: impl AsRef<str>) {
+ self.render_text.push_str(text.as_ref());
self.render_text.push('\n');
}
diff --git a/mingling_core/src/renderer/structural/error.rs b/mingling_core/src/renderer/structural/error.rs
index a7fbc75..63ded81 100644
--- a/mingling_core/src/renderer/structural/error.rs
+++ b/mingling_core/src/renderer/structural/error.rs
@@ -9,6 +9,7 @@ pub struct StructuralRendererSerializeError {
}
impl StructuralRendererSerializeError {
+ /// Creates a new `StructuralRendererSerializeError` with the given error message.
#[must_use]
pub fn new(error: String) -> Self {
Self { error }
diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs
new file mode 100644
index 0000000..e4cd826
--- /dev/null
+++ b/mingling_macros/src/attr.rs
@@ -0,0 +1,9 @@
+pub(crate) mod chain;
+#[cfg(feature = "comp")]
+pub(crate) mod completion;
+#[cfg(feature = "clap")]
+pub(crate) mod dispatcher_clap;
+pub(crate) mod help;
+#[cfg(feature = "extra_macros")]
+pub(crate) mod program_setup;
+pub(crate) mod renderer;
diff --git a/mingling_macros/src/chain.rs b/mingling_macros/src/attr/chain.rs
index 5044227..dbd87dd 100644
--- a/mingling_macros/src/chain.rs
+++ b/mingling_macros/src/attr/chain.rs
@@ -20,65 +20,81 @@ fn is_unit_return_type(sig: &Signature) -> bool {
}
}
-/// Validates that the return type is `Next`, `ChainProcess<ThisProgram>`, `()`, or omitted.
+/// Validates that the return type is acceptable.
+/// Accepts `()`, `Next`, `ChainProcess<...>`, or any type that can
+/// be converted to `ChainProcess` via `.into()` (i.e. any pack type).
fn validate_return_type(sig: &Signature) -> Result<(), proc_macro2::TokenStream> {
// `()` or omitted is always valid
if is_unit_return_type(sig) {
return Ok(());
}
- match &sig.output {
- ReturnType::Type(_, ty) => match &**ty {
- Type::Path(type_path) => {
- let last_segment = type_path.path.segments.last().unwrap();
- let ident_str = last_segment.ident.to_string();
- if ident_str == "Next" || ident_str == "ChainProcess" {
- return Ok(());
- }
- Err(syn::Error::new(
- ty.span(),
- "Chain function must return `Next`, `ChainProcess<ThisProgram>`, `()`, or omit the return type",
- )
- .to_compile_error())
- }
- _ => Err(syn::Error::new(
- ty.span(),
- "Chain function must return `Next`, `ChainProcess<ThisProgram>`, `()`, or omit the return type",
- )
- .to_compile_error()),
- },
- ReturnType::Default => {
- Err(syn::Error::new(
- sig.span(),
- "Chain function must specify a return type (must be `Next`, `ChainProcess<ThisProgram>`, or `()`)",
- )
- .to_compile_error())
- }
- }
+ Ok(())
}
/// Builds the `proc` function implementation inside the generated `Chain` impl.
///
-/// The user's function body is inlined directly, and its result is converted
-/// via `.into()` to `ChainProcess<ProgramType>`.
-#[allow(unused_variables)]
+/// Instead of inlining the user's body, the trait method calls the original
+/// function by name, with resources injected from the application context.
fn generate_proc_fn(
+ fn_name: &Ident,
has_resources: bool,
resources: &[ResourceInjection],
program_type: &proc_macro2::TokenStream,
previous_type: &TypePath,
- prev_param: &Pat,
- fn_body_stmts: &[syn::Stmt],
is_async_fn: bool,
is_unit_return: bool,
+ origin_return_type: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type);
let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
+
+ // Build the call to the original function with resource arguments injected.
+ // The variable names come from the resource injection bindings
+ // (immut bindings are `let #name = …`, mut closures receive `|#name: &mut T|`),
+ // which match the original function's parameter names.
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ let fn_call = if has_resources {
+ let call = quote! { #fn_name(#fixed_prev, #(#resource_args),*) };
+ if is_async_fn {
+ quote! { #call.await }
+ } else {
+ call
+ }
+ } else {
+ let call = quote! { #fn_name(#fixed_prev) };
+ if is_async_fn {
+ quote! { #call.await }
+ } else {
+ call
+ }
+ };
+
+ // Convert the function call to a syn::Stmt so existing wrapping functions can use it
+ let fn_call_expr: syn::Expr = syn::parse_quote! { #fn_call };
+ let fn_call_stmt = syn::Stmt::Expr(fn_call_expr, None);
+
let wrapped_body = if is_async_fn && !mut_resources.is_empty() {
- wrap_body_with_mut_resources_async(fn_body_stmts, &mut_resources, program_type)
+ wrap_body_with_mut_resources_async(&[fn_call_stmt], &mut_resources, program_type)
} else {
- wrap_body_with_mut_resources(fn_body_stmts, &mut_resources, program_type, is_unit_return)
+ wrap_body_with_mut_resources(
+ &[fn_call_stmt],
+ &mut_resources,
+ program_type,
+ is_unit_return,
+ )
};
let proc_body = if is_unit_return {
@@ -86,13 +102,13 @@ fn generate_proc_fn(
quote! {
#(#immut_resource_stmts)*
#wrapped_body;
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>
::to_chain(crate::ResultEmpty)
}
} else {
quote! {
#wrapped_body;
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>
::to_chain(crate::ResultEmpty)
}
};
@@ -106,18 +122,18 @@ fn generate_proc_fn(
} else {
quote! { #wrapped_body }
};
- // Use a let-binding with explicit type annotation so that the inner
- // body's `.into()` calls resolve correctly (same as the original function).
quote! {
- let __chain_result: ::mingling::ChainProcess<#program_type> = { #body };
- __chain_result
+ let __chain_result = { #body };
+ <#origin_return_type as ::std::convert::Into<
+ ::mingling::ChainProcess<#program_type>
+ >>::into(__chain_result)
}
};
#[cfg(feature = "async")]
{
quote! {
- async fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> {
+ async fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> {
#proc_body
}
}
@@ -126,7 +142,7 @@ fn generate_proc_fn(
#[cfg(not(feature = "async"))]
{
quote! {
- fn proc(#prev_param: #previous_type) -> ::mingling::ChainProcess<#program_type> {
+ fn proc(#fixed_prev: #previous_type) -> ::mingling::ChainProcess<#program_type> {
#proc_body
}
}
@@ -177,7 +193,7 @@ fn reject_async(sig: &Signature) -> Result<(), proc_macro2::TokenStream> {
Ok(())
}
-pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Reject non-empty attribute arguments; #[chain] must be bare
if !attr.is_empty() {
return syn::Error::new(
@@ -211,13 +227,12 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
}
// Extract the previous type, parameter name, and resource injection params
- let (prev_param, previous_type, resources) = match extract_args_info(&input_fn.sig) {
+ let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) {
Ok(info) => info,
Err(e) => return e.to_compile_error().into(),
};
// Prepare building blocks
- let fn_body = &input_fn.block;
let mut fn_attrs = input_fn.attrs.clone();
fn_attrs.retain(|attr| !attr.path().is_ident("chain"));
let vis = &input_fn.vis;
@@ -234,26 +249,30 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Always use the default crate-defined program path
let program_type = crate::default_program_path();
+ // Extract the user's return type for the explicit Into turbofish
+ let origin_return_type = match &input_fn.sig.output {
+ ReturnType::Type(_, ty) => quote! { #ty },
+ ReturnType::Default => quote! { () },
+ };
+
// Generate the `proc` function for the Chain impl
let proc_fn = generate_proc_fn(
+ fn_name,
has_resources,
&resources,
&program_type,
&previous_type,
- &prev_param,
- &fn_body.stmts,
#[cfg(feature = "async")]
is_async_fn,
#[cfg(not(feature = "async"))]
false,
is_unit_return,
+ &origin_return_type,
);
- // Preserve the original function untouched, with dead_code allowed
- // since it may only be called through the Chain trait dispatch.
+ // Preserve the original function untouched
// Note: do NOT add `#vis` here — `input_fn` (ItemFn) already contains its own visibility.
let original_fn = quote! {
- #[allow(dead_code)]
#(#fn_attrs)*
#input_fn
};
@@ -275,7 +294,10 @@ pub fn chain_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
}
/// Builds a match arm for chain mapping
-pub fn build_chain_arm(struct_name: &Ident, previous_type: &TypePath) -> proc_macro2::TokenStream {
+pub(crate) fn build_chain_arm(
+ struct_name: &Ident,
+ previous_type: &TypePath,
+) -> proc_macro2::TokenStream {
let enum_variant = &previous_type.path.segments.last().unwrap().ident;
quote! {
#struct_name => #enum_variant,
@@ -283,14 +305,14 @@ pub fn build_chain_arm(struct_name: &Ident, previous_type: &TypePath) -> proc_ma
}
/// Builds a match arm for chain existence check
-pub fn build_chain_exist_arm(previous_type: &TypePath) -> proc_macro2::TokenStream {
+pub(crate) fn build_chain_exist_arm(previous_type: &TypePath) -> proc_macro2::TokenStream {
let enum_variant = &previous_type.path.segments.last().unwrap().ident;
quote! {
Self::#enum_variant => true,
}
}
-pub fn register_chain(input: TokenStream) -> TokenStream {
+pub(crate) fn register_chain(input: TokenStream) -> TokenStream {
// Parse the input as a comma-separated list of arguments
let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
diff --git a/mingling_macros/src/completion.rs b/mingling_macros/src/attr/completion.rs
index ae01462..3ced091 100644
--- a/mingling_macros/src/completion.rs
+++ b/mingling_macros/src/attr/completion.rs
@@ -5,7 +5,7 @@ use syn::spanned::Spanned;
use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, TypePath, parse_macro_input};
#[cfg(feature = "comp")]
-pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse the attribute arguments such as HelloEntry or crate::EntryFine from #[completion(crate::EntryFine)]
use crate::get_global_set;
let previous_type_path: TypePath = if attr.is_empty() {
@@ -47,11 +47,8 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Extract the first param pattern and type for the ctx parameter
let first_arg = &inputs[0];
- let (ctx_pat, _ctx_type) = match first_arg {
- FnArg::Typed(PatType { pat, ty, .. }) => {
- let param_pat = (**pat).clone();
- (param_pat, (**ty).clone())
- }
+ let _ctx_type = match first_arg {
+ FnArg::Typed(PatType { ty, .. }) => (**ty).clone(),
FnArg::Receiver(_) => {
return syn::Error::new(
first_arg.span(),
@@ -61,6 +58,7 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
.into();
}
};
+ let fixed_ctx: Pat = syn::parse_quote!(ctx);
// Extract resources from params 2 through N, skipping ctx
let resources = match extract_resources_from_args(sig, 1) {
@@ -70,7 +68,6 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Get the function body
let fn_body = &input_fn.block;
- let fn_body_stmts = &fn_body.stmts;
// Get function attributes excluding the completion attribute
let mut fn_attrs = input_fn.attrs.clone();
@@ -96,12 +93,26 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// Generate immutable resource bindings
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type);
- // Build the comp method body with resource injection
- // Use modify_res for mutable resources same pattern as renderer.rs
- let wrapped_body = if mut_resources.is_empty() {
- quote! { #(#fn_body_stmts)* }
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_ctx, #(#resource_args),*) }
+ } else {
+ quote! { #fn_name(#fixed_ctx) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if mut_resources.is_empty() {
+ fn_call
} else {
- let mut wrapped = quote! { #(#fn_body_stmts)* };
+ let mut wrapped = fn_call;
for res in mut_resources.iter().rev() {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
@@ -117,10 +128,10 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
let comp_body = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #wrapped_body
+ #inner_call
}
} else {
- quote! { #(#fn_body_stmts)* }
+ quote! { #inner_call }
};
// Generate the struct and implementation
@@ -135,7 +146,7 @@ pub fn completion_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
impl ::mingling::Completion for #struct_name {
type Previous = #previous_type_path;
- fn comp(#ctx_pat: &::mingling::ShellContext) #output {
+ fn comp(#fixed_ctx: &::mingling::ShellContext) #output {
#comp_body
}
}
diff --git a/mingling_macros/src/dispatcher_clap.rs b/mingling_macros/src/attr/dispatcher_clap.rs
index 0945e31..40f7d47 100644
--- a/mingling_macros/src/dispatcher_clap.rs
+++ b/mingling_macros/src/attr/dispatcher_clap.rs
@@ -93,7 +93,7 @@ impl Parse for DispatcherClapInput {
}
#[cfg(feature = "clap")]
-pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr_input = parse_macro_input!(attr as DispatcherClapInput);
let input_struct = parse_macro_input!(item as ItemStruct);
let struct_name = &input_struct.ident;
@@ -108,23 +108,23 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream
let begin_body = if let Some(ref error_struct) = options.error_struct {
quote! {
if ::mingling::this::<#program_path>().user_context.help {
- return #struct_name::default().to_chain();
+ return ::mingling::Routable::<#program_path>::to_chain(#struct_name::default());
}
match <#struct_name as ::clap::Parser>::try_parse_from(clap_args) {
- Ok(parsed) => parsed.to_chain(),
+ Ok(parsed) => ::mingling::Routable::<#program_path>::to_chain(parsed),
Err(e) => {
- return #error_struct::new(format!("{}", e.render().ansi())).to_render()
+ return ::mingling::Routable::<#program_path>::to_render(#error_struct::new(format!("{}", e.render().ansi())))
},
}
}
} else {
quote! {
if ::mingling::this::<#program_path>().user_context.help {
- return #struct_name::default().to_chain();
+ return ::mingling::Routable::<#program_path>::to_chain(#struct_name::default());
}
let parsed = <#struct_name as ::clap::Parser>::try_parse_from(clap_args)
.unwrap_or_else(|e| e.exit());
- parsed.to_chain()
+ ::mingling::Routable::<#program_path>::to_chain(parsed)
}
};
@@ -144,7 +144,7 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream
Some(quote! {
#[allow(non_snake_case)]
#[::mingling::macros::help]
- pub fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult {
+ pub(crate) fn #help_fn_name(_prev: #struct_name) -> ::mingling::RenderResult {
use std::io::Write;
use clap::ColorChoice;
@@ -189,7 +189,7 @@ pub fn dispatcher_clap_attr(attr: TokenStream, item: TokenStream) -> TokenStream
// Generate the dispatcher struct
#[doc(hidden)]
#[derive(Default)]
- pub struct #dispatcher_struct;
+ pub(crate) struct #dispatcher_struct;
impl ::mingling::Dispatcher<#program_path> for #dispatcher_struct {
fn node(&self) -> ::mingling::Node {
diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/attr/help.rs
index 1903d07..aa7bc88 100644
--- a/mingling_macros/src/help.rs
+++ b/mingling_macros/src/attr/help.rs
@@ -1,38 +1,20 @@
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::spanned::Spanned;
-use syn::{Ident, ItemFn, ReturnType, Type, TypePath, parse_macro_input};
+use syn::{Ident, ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input};
use crate::get_global_set;
use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
-/// Validates that the function returns `::mingling::RenderResult`.
-fn validate_render_result_return(sig: &syn::Signature) -> syn::Result<()> {
+/// Extracts the user's return type, returning `None` for no return type.
+fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream> {
match &sig.output {
- ReturnType::Type(_, ty) => match &**ty {
- Type::Path(type_path) => {
- let last_seg = type_path.path.segments.last().map(|s| s.ident.to_string());
- match last_seg.as_deref() {
- Some("RenderResult") => Ok(()),
- _ => Err(syn::Error::new(
- ty.span(),
- "Help function must return `RenderResult`",
- )),
- }
- }
- _ => Err(syn::Error::new(
- ty.span(),
- "Help function must return `RenderResult`",
- )),
- },
- ReturnType::Default => Err(syn::Error::new(
- sig.span(),
- "Help function must have a return type `-> RenderResult`",
- )),
+ ReturnType::Type(_, ty) => Some(quote! { #ty }),
+ ReturnType::Default => None,
}
}
-pub fn help_attr(item: TokenStream) -> TokenStream {
+pub(crate) fn help_attr(item: TokenStream) -> TokenStream {
// Parse the function item
let input_fn = parse_macro_input!(item as ItemFn);
@@ -43,16 +25,14 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
.into();
}
- // Extract the entry type, parameter name, and resource injection params
- let (prev_param, entry_type, resources) = match extract_args_info(&input_fn.sig) {
+ // Extract the entry type and resource injection params
+ let (_, entry_type, resources) = match extract_args_info(&input_fn.sig) {
Ok(info) => info,
Err(e) => return e.to_compile_error().into(),
};
- // Validate return type is RenderResult
- if let Err(e) = validate_render_result_return(&input_fn.sig) {
- return e.to_compile_error().into();
- }
+ // Determine the user's return type for preserving the original function
+ let user_return_type = extract_user_return_type(&input_fn.sig);
// Get the function body
let fn_body = &input_fn.block;
@@ -69,8 +49,8 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
let fn_name = &input_fn.sig.ident;
// Get original inputs to keep the original function
-
let original_inputs = input_fn.sig.inputs.clone();
+ let original_return_type = user_return_type.clone().unwrap_or(quote! { () });
// Generate internal name using snake_case
let internal_name = format!(
@@ -86,13 +66,31 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
// Generate immutable resource bindings
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), &program_type);
- // Build the render_help body with resource injection
- // Use modify_res for mutable resources same pattern as renderer.rs
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
- let wrapped_body = if mut_resources.is_empty() {
- quote! { #(#fn_body_stmts)* }
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_prev, #(#resource_args),*) }
} else {
- let mut wrapped = quote! { #(#fn_body_stmts)* };
+ quote! { #fn_name(#fixed_prev) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if mut_resources.is_empty() {
+ fn_call
+ } else {
+ let mut wrapped = fn_call;
for res in mut_resources.iter().rev() {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
@@ -108,10 +106,10 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
let help_render_body = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #wrapped_body
+ #inner_call
}
} else {
- quote! { #(#fn_body_stmts)* }
+ quote! { #inner_call }
};
// Register the help request mapping
@@ -148,18 +146,18 @@ pub fn help_attr(item: TokenStream) -> TokenStream {
impl ::mingling::HelpRequest for #struct_name {
type Entry = #entry_type;
- fn render_help(#prev_param: Self::Entry) -> ::mingling::RenderResult {
- #help_render_body
+ fn render_help(#fixed_prev: Self::Entry) -> ::mingling::RenderResult {
+ let __help_result = { #help_render_body };
+ ::std::convert::Into::into(__help_result)
}
}
::mingling::macros::register_help!(#entry_type, #struct_name);
// Keep the original function unchanged
-
#(#fn_attrs)*
- #vis fn #fn_name(#original_inputs) -> ::mingling::RenderResult {
- #fn_body
+ #vis fn #fn_name(#original_inputs) -> #original_return_type {
+ #(#fn_body_stmts)*
}
};
@@ -179,7 +177,7 @@ fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2::
}
}
-pub fn register_help(input: TokenStream) -> TokenStream {
+pub(crate) fn register_help(input: TokenStream) -> TokenStream {
// Parse the input as a comma-separated list of arguments
let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
diff --git a/mingling_macros/src/program_setup.rs b/mingling_macros/src/attr/program_setup.rs
index 7fd9d16..dee5a1c 100644
--- a/mingling_macros/src/program_setup.rs
+++ b/mingling_macros/src/attr/program_setup.rs
@@ -47,7 +47,7 @@ fn extract_return_type(sig: &Signature) -> syn::Result<()> {
}
}
-pub fn setup_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn setup_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// #[program_setup] takes no arguments; always use the default program path
let _ = attr;
let program_path = crate::default_program_path();
diff --git a/mingling_macros/src/renderer.rs b/mingling_macros/src/attr/renderer.rs
index d124ec9..828dc00 100644
--- a/mingling_macros/src/renderer.rs
+++ b/mingling_macros/src/attr/renderer.rs
@@ -1,7 +1,7 @@
use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::spanned::Spanned;
-use syn::{ItemFn, ReturnType, Signature, TypePath, parse_macro_input};
+use syn::{ItemFn, Pat, ReturnType, Signature, TypePath, parse_macro_input};
use crate::get_global_set;
use crate::res_injection::{extract_args_info, generate_immut_resource_bindings};
@@ -15,7 +15,7 @@ fn extract_user_return_type(sig: &Signature) -> Option<proc_macro2::TokenStream>
}
#[allow(clippy::too_many_lines)]
-pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
+pub(crate) fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
// #[renderer] takes no arguments; always use the default program path
let _ = attr;
let program_path = crate::default_program_path();
@@ -31,8 +31,8 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
.into();
}
- // Extract the previous type, parameter name, and resource injection params
- let (prev_param, previous_type, resources) = match extract_args_info(&input_fn.sig) {
+ // Extract the previous type and resource injection params
+ let (_, previous_type, resources) = match extract_args_info(&input_fn.sig) {
Ok(info) => info,
Err(e) => return e.to_compile_error().into(),
};
@@ -69,31 +69,53 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
let immut_resource_stmts = generate_immut_resource_bindings(resources.iter(), program_type);
let mut_resources: Vec<_> = resources.iter().filter(|r| r.is_mut).collect();
- let inner_body_with_resources = if has_mut_resources {
- let mut wrapped = quote! { #(#fn_body_stmts)* };
+ // Build the call to the original function with resource arguments injected
+ let resource_args: Vec<_> = resources
+ .iter()
+ .map(|res| {
+ let var_name = &res.var_name;
+ quote! { #var_name }
+ })
+ .collect();
+
+ // Use a fixed parameter name `prev` for the trait method, regardless of
+ // the user's original parameter name (which may be `_` and cannot be
+ // referenced in expression position).
+ let fixed_prev: Pat = syn::parse_quote!(prev);
+
+ let fn_call = if has_resources {
+ quote! { #fn_name(#fixed_prev, #(#resource_args),*) }
+ } else {
+ quote! { #fn_name(#fixed_prev) }
+ };
+
+ // Wrap the function call with modify_res for mutable resources
+ let inner_call = if has_mut_resources {
+ let mut wrapped = fn_call;
for res in mut_resources.iter().rev() {
let var_name = &res.var_name;
let inner_type = &res.inner_type;
wrapped = quote! {
- ::mingling::this::<#program_type>().modify_res(|#var_name: &mut #inner_type| {
+ ::mingling::this::<#program_type>()
+ .modify_res(|#var_name: &mut #inner_type| {
#wrapped
})
};
}
wrapped
} else {
- quote! { #(#fn_body_stmts)* }
+ fn_call
};
- // Build the Renderer::render body with resource injection
- // The user's body now directly creates and returns a RenderResult.
+ // Build the Renderer::render body with resource injection.
+ // The trait method injects resources and calls the original function.
let render_fn_body = if has_resources {
quote! {
#(#immut_resource_stmts)*
- #inner_body_with_resources
+ #inner_call
}
} else {
- quote! { #inner_body_with_resources }
+ quote! { #inner_call }
};
// The original function preserves the user's exact signature and body.
@@ -112,14 +134,13 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
impl ::mingling::Renderer for #struct_name {
type Previous = #previous_type;
- fn render(#prev_param: Self::Previous) -> ::mingling::RenderResult {
+ fn render(#fixed_prev: Self::Previous) -> ::mingling::RenderResult {
let __renderer_result = { #render_fn_body };
::std::convert::Into::into(__renderer_result)
}
}
// Keep the original function unchanged
- #[allow(dead_code)]
#(#fn_attrs)*
#vis fn #fn_name(#original_inputs) -> #original_return_type {
#(#fn_body_stmts)*
@@ -130,7 +151,7 @@ pub fn renderer_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
}
/// Builds the renderer entry for the global renderers list
-pub fn build_renderer_entry(
+pub(crate) fn build_renderer_entry(
struct_name: &syn::Ident,
previous_type: &TypePath,
) -> proc_macro2::TokenStream {
@@ -141,7 +162,7 @@ pub fn build_renderer_entry(
}
/// Builds the renderer existence check entry
-pub fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream {
+pub(crate) fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::TokenStream {
let enum_variant = &previous_type.path.segments.last().unwrap().ident;
quote! {
Self::#enum_variant => true,
@@ -150,7 +171,9 @@ pub fn build_renderer_exist_entry(previous_type: &TypePath) -> proc_macro2::Toke
/// Builds the structural renderer entry
#[cfg(feature = "structural_renderer")]
-pub fn build_structural_renderer_entry(previous_type: &TypePath) -> proc_macro2::TokenStream {
+pub(crate) fn build_structural_renderer_entry(
+ previous_type: &TypePath,
+) -> proc_macro2::TokenStream {
let enum_variant = &previous_type.path.segments.last().unwrap().ident;
quote! {
Self::#enum_variant => {
@@ -164,7 +187,7 @@ pub fn build_structural_renderer_entry(previous_type: &TypePath) -> proc_macro2:
}
}
-pub fn register_renderer(input: TokenStream) -> TokenStream {
+pub(crate) fn register_renderer(input: TokenStream) -> TokenStream {
// Parse the input as a comma-separated list of arguments
let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
diff --git a/mingling_macros/src/derive.rs b/mingling_macros/src/derive.rs
new file mode 100644
index 0000000..ffa405b
--- /dev/null
+++ b/mingling_macros/src/derive.rs
@@ -0,0 +1,2 @@
+pub(crate) mod enum_tag;
+pub(crate) mod grouped;
diff --git a/mingling_macros/src/enum_tag.rs b/mingling_macros/src/derive/enum_tag.rs
index 6277b69..a7f71f0 100644
--- a/mingling_macros/src/enum_tag.rs
+++ b/mingling_macros/src/derive/enum_tag.rs
@@ -4,7 +4,7 @@ use syn::{
Attribute, Data, DeriveInput, Error, Fields, Ident, LitStr, Result, Variant, parse_macro_input,
};
-pub fn derive_enum_tag(input: TokenStream) -> TokenStream {
+pub(crate) fn derive_enum_tag(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match derive_enum_tag_impl(input) {
diff --git a/mingling_macros/src/groupped.rs b/mingling_macros/src/derive/grouped.rs
index 8aee003..307aab6 100644
--- a/mingling_macros/src/groupped.rs
+++ b/mingling_macros/src/derive/grouped.rs
@@ -2,7 +2,7 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, Ident, parse_macro_input};
-pub fn derive_groupped(input: TokenStream) -> TokenStream {
+pub(crate) fn derive_grouped(input: TokenStream) -> TokenStream {
// Parse the input struct/enum
let input = parse_macro_input!(input as DeriveInput);
let struct_name = input.ident;
@@ -12,11 +12,11 @@ pub fn derive_groupped(input: TokenStream) -> TokenStream {
let any_output_convert_impls =
proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident));
- // Generate the Groupped trait implementation
+ // Generate the Grouped trait implementation
let expanded = quote! {
::mingling::macros::register_type!(#struct_name);
- impl ::mingling::Groupped<#group_ident> for #struct_name {
+ impl ::mingling::Grouped<#group_ident> for #struct_name {
fn member_id() -> #group_ident {
#group_ident::#struct_name
}
@@ -29,7 +29,7 @@ pub fn derive_groupped(input: TokenStream) -> TokenStream {
}
#[cfg(feature = "structural_renderer")]
-pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
+pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream {
// Parse the input struct/enum
let input_parsed = parse_macro_input!(input as DeriveInput);
let struct_name = input_parsed.ident.clone();
@@ -39,14 +39,14 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
let any_output_convert_impls =
proc_macro2::TokenStream::from(build_any_output_convert_impls(&struct_name, &group_ident));
- // Generate both Serialize and Groupped implementations
+ // Generate both Serialize and Grouped implementations
let expanded = quote! {
#[derive(serde::Serialize)]
#input_parsed
::mingling::macros::register_type!(#struct_name);
- impl ::mingling::Groupped<#group_ident> for #struct_name {
+ impl ::mingling::Grouped<#group_ident> for #struct_name {
fn member_id() -> #group_ident {
#group_ident::#struct_name
}
diff --git a/mingling_macros/src/extensions.rs b/mingling_macros/src/extensions.rs
new file mode 100644
index 0000000..022761b
--- /dev/null
+++ b/mingling_macros/src/extensions.rs
@@ -0,0 +1,119 @@
+//! Extension point mechanism for Mingling attribute macros.
+//!
+//! This module provides a way for attribute macros like `#[chain]`, `#[renderer]`,
+//! `#[help]`, and `#[completion]` to accept extension identifiers that are
+//! applied as outer attributes before the bare macro.
+
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{Ident, Token};
+
+/// Extension: `#[routeify]` — transforms `expr?` into `route!(expr)`.
+#[cfg(feature = "extra_macros")]
+pub(crate) mod routeify;
+
+/// Extension: `#[buffer]` — wraps a unit-returning function to return `RenderResult`.
+pub(crate) mod buffer;
+
+/// Parsed extensions from an attribute macro like `#[chain(routeify, other_ext)]`.
+pub(crate) struct Extensions {
+ pub(crate) exts: Vec<Ident>,
+}
+
+impl Parse for Extensions {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let mut exts = Vec::new();
+ while !input.is_empty() {
+ let ident: Ident = input.parse()?;
+ exts.push(ident);
+ if input.peek(Token![,]) {
+ let _ = input.parse::<Token![,]>();
+ }
+ }
+ Ok(Extensions { exts })
+ }
+}
+
+/// Parsed extensions for `#[completion(EntryType, routeify, ...)]`.
+#[cfg(feature = "comp")]
+pub(crate) struct CompletionExt {
+ pub(crate) entry_type: proc_macro2::TokenStream,
+ pub(crate) exts: Vec<Ident>,
+}
+
+#[cfg(feature = "comp")]
+impl Parse for CompletionExt {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let entry_type: proc_macro2::TokenStream = input.parse()?;
+ let mut exts = Vec::new();
+ while !input.is_empty() {
+ let _ = input.parse::<Token![,]>();
+ if input.is_empty() {
+ break;
+ }
+ let ident: Ident = input.parse()?;
+ exts.push(ident);
+ }
+ Ok(CompletionExt { entry_type, exts })
+ }
+}
+
+/// Generates a re-dispatch token stream for attribute macros that take **no fixed arguments**
+/// (chain, renderer, help).
+pub(crate) fn try_redispatch_simple(
+ attr: TokenStream,
+ item: &TokenStream,
+ bare_attr_name: &str,
+) -> Option<TokenStream> {
+ if attr.is_empty() {
+ return None;
+ }
+
+ let exts: Extensions = syn::parse(attr).ok()?;
+ if exts.exts.is_empty() {
+ return None;
+ }
+
+ let bare = Ident::new(bare_attr_name, proc_macro2::Span::call_site());
+ let exts = &exts.exts;
+ let item = proc_macro2::TokenStream::from(item.clone());
+
+ Some(
+ quote! {
+ #(#[#exts])*
+ #[#bare]
+ #item
+ }
+ .into(),
+ )
+}
+
+/// Generates a re-dispatch token stream for `#[completion(EntryType, ...)]`.
+#[cfg(feature = "comp")]
+pub(crate) fn try_redispatch_completion(
+ attr: TokenStream,
+ item: &TokenStream,
+) -> Option<TokenStream> {
+ if attr.is_empty() {
+ return None;
+ }
+
+ let parsed: CompletionExt = syn::parse(attr).ok()?;
+ if parsed.exts.is_empty() {
+ return None;
+ }
+
+ let entry_type = &parsed.entry_type;
+ let exts = &parsed.exts;
+ let item = proc_macro2::TokenStream::from(item.clone());
+
+ Some(
+ quote! {
+ #(#[#exts])*
+ #[::mingling::macros::completion(#entry_type)]
+ #item
+ }
+ .into(),
+ )
+}
diff --git a/mingling_macros/src/extensions/buffer.rs b/mingling_macros/src/extensions/buffer.rs
new file mode 100644
index 0000000..27eb612
--- /dev/null
+++ b/mingling_macros/src/extensions/buffer.rs
@@ -0,0 +1,95 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{ItemFn, ReturnType, Type, parse_macro_input};
+
+/// Checks whether the return type is unit `()`.
+fn is_unit_return_type(sig: &syn::Signature) -> bool {
+ match &sig.output {
+ ReturnType::Type(_, ty) => match &**ty {
+ Type::Tuple(tuple) => tuple.elems.is_empty(),
+ _ => false,
+ },
+ ReturnType::Default => true,
+ }
+}
+
+/// The `#[buffer]` attribute macro.
+///
+/// Wraps a unit-returning function to produce a `::mingling::RenderResult` by
+/// injecting a local `__render_result_buffer` variable. Inside the function
+/// body, the `r_print!` / `r_println!` macros can write into the buffer.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_println};
+///
+/// #[buffer]
+/// fn render_greeting(prev: Greeting) {
+/// r_println!("Hello, {}!", *prev);
+/// }
+/// ```
+///
+/// Expands to:
+///
+/// ```rust,ignore
+/// fn render_greeting(prev: Greeting) -> mingling::RenderResult {
+/// let mut __render_result_buffer = mingling::RenderResult::new();
+/// {
+/// r_println!("Hello, {}!", *prev);
+/// }
+/// __render_result_buffer
+/// }
+/// ```
+pub(crate) fn buffer_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // Reject non-empty attribute arguments; #[buffer] must be bare
+ if !attr.is_empty() {
+ return syn::Error::new(
+ attr.into_iter().next().unwrap().span().into(),
+ "#[buffer] does not accept arguments",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Parse the function item
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // Validate the function is not async
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Buffer function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ // Validate return type is unit
+ if !is_unit_return_type(&input_fn.sig) {
+ return syn::Error::new(
+ input_fn.sig.span(),
+ "#[buffer] function must not have a return value (must return `()`)",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Get function attributes (excluding the buffer attribute)
+ let mut fn_attrs = input_fn.attrs.clone();
+ fn_attrs.retain(|attr| !attr.path().is_ident("buffer"));
+
+ let vis = &input_fn.vis;
+ let fn_name = &input_fn.sig.ident;
+ let inputs = &input_fn.sig.inputs;
+ let fn_body = &input_fn.block;
+
+ let expanded = quote! {
+ #(#fn_attrs)*
+ #vis fn #fn_name(#inputs) -> ::mingling::RenderResult {
+ let mut __render_result_buffer = ::mingling::RenderResult::new();
+ #fn_body
+ __render_result_buffer
+ }
+ };
+
+ expanded.into()
+}
diff --git a/mingling_macros/src/extensions/routeify.rs b/mingling_macros/src/extensions/routeify.rs
new file mode 100644
index 0000000..7cded54
--- /dev/null
+++ b/mingling_macros/src/extensions/routeify.rs
@@ -0,0 +1,46 @@
+//! The `#[routeify]` extension — transforms `expr?` into `route!(expr)`.
+//!
+//! Designed as an extension for the Mingling attribute macro system, intended
+//! to be used with `#[chain(routeify)]` or standalone as `#[routeify]`.
+//!
+//! # How it works
+//!
+//! The macro parses the function AST and replaces every `Expr::Try` node with an
+//! equivalent `route!(expr)` invocation.
+
+use proc_macro::TokenStream;
+use quote::ToTokens;
+use syn::spanned::Spanned;
+use syn::visit_mut::VisitMut;
+use syn::{Expr, ItemFn, parse_macro_input};
+
+struct RouteifyTransform;
+
+impl VisitMut for RouteifyTransform {
+ fn visit_expr_mut(&mut self, expr: &mut Expr) {
+ syn::visit_mut::visit_expr_mut(self, expr);
+
+ if let Expr::Try(try_expr) = expr {
+ let inner = &*try_expr.expr;
+ let inner_tokens = inner.to_token_stream();
+
+ // Set the span of the generated `route` ident to the `?` token's span,
+ // so that rust-analyzer resolves the `?` position to the `route!` macro
+ // instead of the standard Try trait, showing the route macro's docs on hover.
+ let q_span = try_expr.question_token.span();
+ let route_ident = proc_macro2::Ident::new("route", q_span);
+
+ if let Ok(macro_expr) = syn::parse2::<Expr>(quote::quote! {
+ ::mingling::macros::#route_ident!(#inner_tokens)
+ }) {
+ *expr = macro_expr;
+ }
+ }
+ }
+}
+
+pub(crate) fn routeify_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ let mut input_fn = parse_macro_input!(item as ItemFn);
+ RouteifyTransform.visit_item_fn_mut(&mut input_fn);
+ input_fn.to_token_stream().into()
+}
diff --git a/mingling_macros/src/func.rs b/mingling_macros/src/func.rs
new file mode 100644
index 0000000..33bb094
--- /dev/null
+++ b/mingling_macros/src/func.rs
@@ -0,0 +1,13 @@
+pub(crate) mod dispatcher;
+#[cfg(feature = "extra_macros")]
+pub(crate) mod entry;
+pub(crate) mod gen_program;
+#[cfg(feature = "extra_macros")]
+pub(crate) mod group;
+pub(crate) mod node;
+pub(crate) mod pack;
+#[cfg(feature = "extra_macros")]
+pub(crate) mod pack_err;
+pub(crate) mod r_print;
+#[cfg(feature = "comp")]
+pub(crate) mod suggest;
diff --git a/mingling_macros/src/dispatcher.rs b/mingling_macros/src/func/dispatcher.rs
index 2a7c850..a61dd26 100644
--- a/mingling_macros/src/dispatcher.rs
+++ b/mingling_macros/src/func/dispatcher.rs
@@ -75,7 +75,7 @@ impl Parse for DispatcherChainInput {
// are nearly identical and could benefit from refactoring into common helper functions.
#[allow(clippy::too_many_lines)]
-pub fn dispatcher(input: TokenStream) -> TokenStream {
+pub(crate) fn dispatcher(input: TokenStream) -> TokenStream {
// Parse the input
let dispatcher_input = syn::parse_macro_input!(input as DispatcherChainInput);
@@ -134,8 +134,8 @@ pub fn dispatcher(input: TokenStream) -> TokenStream {
::mingling::macros::node!(#command_name_str)
}
fn begin(&self, args: Vec<String>) -> ::mingling::ChainProcess<#program_type> {
- use ::mingling::Groupped;
- #pack::new(args).to_chain()
+ use ::mingling::Grouped;
+ ::mingling::Routable::to_chain(#pack::new(args))
}
fn clone_dispatcher(&self) -> Box<dyn ::mingling::Dispatcher<#program_type>> {
Box::new(#command_struct)
@@ -209,7 +209,7 @@ impl Parse for RegisterDispatcherInput {
}
#[cfg(feature = "dispatch_tree")]
-pub fn register_dispatcher(input: TokenStream) -> TokenStream {
+pub(crate) fn register_dispatcher(input: TokenStream) -> TokenStream {
let RegisterDispatcherInput {
node_name,
dispatcher_type,
@@ -243,7 +243,7 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream {
}
#[cfg(not(feature = "dispatch_tree"))]
-pub fn register_dispatcher(_input: TokenStream) -> TokenStream {
+pub(crate) fn register_dispatcher(_input: TokenStream) -> TokenStream {
quote! {}.into()
}
diff --git a/mingling_macros/src/entry.rs b/mingling_macros/src/func/entry.rs
index 2ac5d6b..35209e5 100644
--- a/mingling_macros/src/entry.rs
+++ b/mingling_macros/src/func/entry.rs
@@ -39,7 +39,7 @@ fn parse_strings(input: &syn::parse::ParseBuffer) -> syn::Result<Vec<String>> {
Ok(strings)
}
-pub fn entry(input: TokenStream) -> TokenStream {
+pub(crate) fn entry(input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as EntryInput);
let strings = match &parsed {
diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs
new file mode 100644
index 0000000..de4da06
--- /dev/null
+++ b/mingling_macros/src/func/gen_program.rs
@@ -0,0 +1,628 @@
+use std::collections::HashMap;
+
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse_macro_input;
+
+use crate::CHAINS;
+use crate::CHAINS_EXIST;
+#[cfg(feature = "dispatch_tree")]
+use crate::COMPILE_TIME_DISPATCHERS;
+#[cfg(feature = "comp")]
+use crate::COMPLETIONS;
+use crate::HELP_REQUESTS;
+use crate::PACKED_TYPES;
+use crate::RENDERERS;
+use crate::RENDERERS_EXIST;
+#[cfg(feature = "structural_renderer")]
+use crate::STRUCTURAL_RENDERERS;
+use crate::attr::{chain, renderer};
+use crate::get_global_set;
+#[cfg(feature = "dispatch_tree")]
+use crate::systems::dispatch_tree_gen;
+
+#[cfg(feature = "async")]
+const ASYNC_ENABLED: bool = true;
+#[cfg(not(feature = "async"))]
+const ASYNC_ENABLED: bool = false;
+
+/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents.
+fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) {
+ let s = entry.to_string();
+ let arrow_idx = s
+ .find("=>")
+ .unwrap_or_else(|| panic!("Entry missing '=>': {s}"));
+ let struct_str = s[..arrow_idx].trim();
+ let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim();
+ let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site());
+ let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site());
+ (struct_ident, variant_ident)
+}
+
+/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`.
+/// Always compiled; returns None when pathf feature is not enabled,
+/// or when the file does not exist / fails to load.
+fn load_pathf_map() -> Option<std::collections::HashMap<String, String>> {
+ if !cfg!(feature = "pathf") {
+ return None;
+ }
+ let out_dir = std::env::var("OUT_DIR").ok()?;
+ let crate_name = std::env::var("CARGO_PKG_NAME").ok()?;
+ let path = std::path::Path::new(&out_dir)
+ .join(&crate_name)
+ .join("type_using.rs");
+ let content = std::fs::read_to_string(&path).ok()?;
+ Some(
+ content
+ .lines()
+ .filter_map(|line| {
+ let line = line.trim();
+ if let Some(rest) = line.strip_prefix("use ") {
+ let path = rest.strip_suffix(';').unwrap_or(rest);
+ if let Some((_mod, type_name)) = path.rsplit_once("::") {
+ return Some((type_name.to_string(), path.to_string()));
+ }
+ }
+ None
+ })
+ .collect(),
+ )
+}
+
+/// Resolves a type name to its full path token stream using the pathf mapping.
+pub(crate) fn resolve_type(
+ name: &str,
+ map: &std::collections::HashMap<String, String>,
+) -> proc_macro2::TokenStream {
+ if let Some(full_path) = map.get(name) {
+ syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| {
+ let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
+ quote! { #ident }
+ })
+ } else {
+ let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
+ quote! { #ident }
+ }
+}
+
+pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream {
+ #[cfg(feature = "comp")]
+ let comp_gen = quote! {
+ ::mingling::macros::program_comp_gen!();
+ };
+
+ #[cfg(not(feature = "comp"))]
+ let comp_gen = quote! {};
+
+ TokenStream::from(quote! {
+ /// Alias for the current program type `crate::ThisProgram`
+ pub type Next = ::mingling::ChainProcess<crate::ThisProgram>;
+
+ impl ::mingling::Routable<crate::ThisProgram> for ::mingling::ChainProcess<crate::ThisProgram>
+ {
+ fn to_chain(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Chain))
+ }
+ other => other,
+ }
+ }
+
+ fn to_render(self) -> ::mingling::ChainProcess<crate::ThisProgram> {
+ match self {
+ ::mingling::ChainProcess::Ok((any, _)) => {
+ ::mingling::ChainProcess::Ok((any, mingling::NextProcess::Renderer))
+ }
+ other => other,
+ }
+ }
+ }
+
+ #comp_gen
+ ::mingling::macros::program_fallback_gen!();
+ ::mingling::macros::program_final_gen!();
+ })
+}
+
+#[cfg(feature = "comp")]
+pub(crate) fn program_comp_gen_impl(_input: TokenStream) -> TokenStream {
+ #[cfg(feature = "async")]
+ let fn_exec_comp = quote! {
+ #[doc(hidden)]
+ #[::mingling::macros::chain]
+ pub async fn __exec_completion(prev: CompletionContext) -> Next {
+ use ::mingling::Grouped;
+
+ let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
+ match read_ctx {
+ Ok(ctx) => {
+ let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
+ ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest)))
+ }
+ Err(_) => std::process::exit(1),
+ }
+ }
+ };
+
+ #[cfg(not(feature = "async"))]
+ let fn_exec_comp = quote! {
+ #[doc(hidden)]
+ #[::mingling::macros::chain]
+ pub fn __exec_completion(prev: CompletionContext) -> Next {
+ use ::mingling::Grouped;
+
+ let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
+ match read_ctx {
+ Ok(ctx) => {
+ let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
+ ::mingling::Routable::<crate::ThisProgram>::to_render(crate::CompletionSuggest::new((ctx, suggest)))
+ }
+ Err(_) => std::process::exit(1),
+ }
+ }
+ };
+
+ #[cfg(feature = "dispatch_tree")]
+ let internal_dispatcher_comp = quote! {
+ use __internal_completion_mod::__internal_dispatcher_comp;
+ };
+
+ #[cfg(not(feature = "dispatch_tree"))]
+ let internal_dispatcher_comp = quote! {};
+
+ let comp_dispatcher = quote! {
+ #[doc(hidden)]
+ mod __internal_completion_mod {
+ use ::mingling::Grouped;
+ ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext);
+ ::mingling::macros::pack!(
+ CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest)
+ );
+ }
+ #internal_dispatcher_comp
+ use __internal_completion_mod::CompletionContext;
+ use __internal_completion_mod::CompletionSuggest;
+ pub use __internal_completion_mod::CMDCompletion;
+
+ #fn_exec_comp
+
+ ::mingling::macros::register_type!(CompletionContext);
+
+ #[allow(unused)]
+ #[doc(hidden)]
+ #[::mingling::macros::renderer]
+ pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult {
+ let result = ::mingling::RenderResult::default();
+ let (ctx, suggest) = prev.inner;
+ ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest);
+ result
+ }
+ };
+
+ TokenStream::from(comp_dispatcher)
+}
+
+pub(crate) fn register_type_impl(input: TokenStream) -> TokenStream {
+ let type_ident = parse_macro_input!(input as syn::Ident);
+ let entry_str = type_ident.to_string();
+
+ get_global_set(&PACKED_TYPES)
+ .lock()
+ .unwrap()
+ .insert(entry_str);
+
+ TokenStream::new()
+}
+
+pub(crate) fn register_chain_impl(input: TokenStream) -> TokenStream {
+ chain::register_chain(input)
+}
+
+pub(crate) fn register_renderer_impl(input: TokenStream) -> TokenStream {
+ renderer::register_renderer(input)
+}
+
+pub(crate) fn program_fallback_gen_impl(_input: TokenStream) -> TokenStream {
+ #[cfg(feature = "structural_renderer")]
+ let pack_empty = quote! {
+ #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Grouped, Default)]
+ pub struct ResultEmpty;
+ };
+
+ #[cfg(not(feature = "structural_renderer"))]
+ let pack_empty = quote! {
+ #[derive(::mingling::Grouped, Default)]
+ pub struct ResultEmpty;
+ };
+
+ let expanded = quote! {
+ ::mingling::macros::pack!(ErrorRendererNotFound = String);
+ ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>);
+ #pack_empty
+ };
+ TokenStream::from(expanded)
+}
+
+#[allow(clippy::too_many_lines)]
+pub(crate) fn program_final_gen_impl(_input: TokenStream) -> TokenStream {
+ let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site());
+
+ let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone();
+
+ let renderers = get_global_set(&RENDERERS).lock().unwrap().clone();
+ let chains = get_global_set(&CHAINS).lock().unwrap().clone();
+ let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone();
+ let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone();
+
+ #[cfg(feature = "structural_renderer")]
+ let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS)
+ .lock()
+ .unwrap()
+ .clone();
+
+ #[cfg(feature = "comp")]
+ let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone();
+
+ let packed_types: Vec<proc_macro2::TokenStream> = packed_types
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let chain_tokens: Vec<proc_macro2::TokenStream> = chains
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let pathf_map: Option<HashMap<String, String>> = load_pathf_map();
+
+ #[cfg(feature = "pathf")]
+ let pathf_hint: proc_macro2::TokenStream = if pathf_map.is_none() {
+ quote! {
+ compile_error!(
+"Cannot load type mapping computed by `pathf`.
+If not yet configured, execute `mingling::build::analyze_and_build_type_mapping()` in your `build.rs`.
+
+fn main() {
+ // Require features: [\"builds\", \"pathf\"]
+ mingling::build::analyze_and_build_type_mapping().unwrap();
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\__ Write to `build.rs`
+
+}
+ ");
+ }
+ } else {
+ quote! {}
+ };
+
+ #[cfg(not(feature = "pathf"))]
+ let pathf_hint: proc_macro2::TokenStream = quote! {};
+
+ let pathf_map: HashMap<String, String> = if cfg!(feature = "pathf") {
+ pathf_map.unwrap_or_default()
+ } else {
+ HashMap::new()
+ };
+
+ let pathf_uses: Vec<proc_macro2::TokenStream> = if cfg!(feature = "pathf") {
+ pathf_map
+ .values()
+ .map(|path| format!("use {};", path).parse().unwrap_or_default())
+ .collect()
+ } else {
+ Vec::new()
+ };
+
+ #[cfg(feature = "structural_renderer")]
+ let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ #[cfg(feature = "structural_renderer")]
+ let structural_render = quote! {
+ fn structural_render(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ setting: &::mingling::StructuralRendererSetting,
+ ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> {
+ #[allow(unused_imports)]
+ #(#pathf_uses)*
+ match any.member_id {
+ #(#structural_renderer_tokens)*
+ _ => {
+ // Non-structural types: render ResultEmpty (which implements
+ // StructuralData + Serialize) instead of producing nothing.
+ let mut r = ::mingling::RenderResult::default();
+ ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?;
+ Ok(r)
+ }
+ }
+ }
+ };
+
+ #[cfg(not(feature = "structural_renderer"))]
+ let structural_render = quote! {};
+
+ #[cfg(feature = "dispatch_tree")]
+ let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS)
+ .lock()
+ .unwrap()
+ .clone()
+ .iter()
+ .cloned()
+ .collect();
+
+ #[cfg(feature = "dispatch_tree")]
+ let dispatch_tree_nodes = {
+ let entries: Vec<(String, String, String)> = compile_time_dispatchers
+ .iter()
+ .filter_map(|entry| {
+ let parts: Vec<&str> = entry.split(':').collect();
+ if parts.len() == 3 {
+ Some((
+ parts[0].to_string(),
+ parts[1].to_string(),
+ parts[2].to_string(),
+ ))
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map);
+ let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map);
+
+ quote! {
+ #get_nodes_fn
+ #dispatch_trie_fn
+ }
+ };
+
+ #[cfg(not(feature = "dispatch_tree"))]
+ let dispatch_tree_nodes = quote! {};
+
+ #[cfg(feature = "comp")]
+ let completion_tokens: Vec<proc_macro2::TokenStream> = completions
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ #[cfg(feature = "comp")]
+ let comp = quote! {
+ fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest {
+ #[allow(unused_imports)]
+ #(#pathf_uses)*
+ match any.member_id {
+ #(#completion_tokens)*
+ _ => ::mingling::Suggest::FileCompletion,
+ }
+ }
+ };
+
+ #[cfg(not(feature = "comp"))]
+ let comp = quote! {};
+
+ // Build render function arms from stored entries
+ let render_fn =
+ if renderer_tokens.is_empty() {
+ quote! {
+ fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ ::mingling::RenderResult::default()
+ }
+ }
+ } else {
+ let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| {
+ let (struct_ident, variant_ident) = parse_entry_pair(entry);
+ let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
+ let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
+ quote! {
+ Self::#variant_ident => {
+ // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
+ // so downcasting to `#variant_ident` is safe.
+ let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
+ <#resolved_struct as ::mingling::Renderer>::render(value)
+ }
+ }
+ }).collect();
+ quote! {
+ fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ match any.member_id {
+ #(#render_arms)*
+ _ => ::mingling::RenderResult::default(),
+ }
+ }
+ }
+ };
+
+ // Build do_chain function (async and sync versions)
+ let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| {
+ let (struct_ident, variant_ident) = parse_entry_pair(entry);
+ let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
+ let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
+ quote! {
+ Self::#variant_ident => {
+ // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
+ // so downcasting to `#variant_ident` is safe.
+ let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
+ let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await };
+ ::std::boxed::Box::pin(fut)
+ }
+ }
+ }).collect();
+
+ let chain_arms_sync: Vec<_> = chain_tokens
+ .iter()
+ .map(|entry| {
+ let (struct_ident, variant_ident) = parse_entry_pair(entry);
+ let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
+ let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
+ quote! {
+ Self::#variant_ident => {
+ // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
+ // so downcasting to `#variant_ident` is safe.
+ let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
+ <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value)
+ }
+ }
+ })
+ .collect();
+
+ let do_chain_fn = if chain_tokens.is_empty() {
+ quote! {
+ fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> {
+ ::core::panic!("No chain found for type id")
+ }
+ }
+ } else if ASYNC_ENABLED {
+ quote! {
+ fn do_chain(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> {
+ match any.member_id {
+ #(#chain_arms_async)*
+ _ => ::core::panic!("No chain found for type id: {:?}", any.type_id),
+ }
+ }
+ }
+ } else {
+ quote! {
+ fn do_chain(
+ any: ::mingling::AnyOutput<Self::Enum>,
+ ) -> ::mingling::ChainProcess<Self::Enum> {
+ match any.member_id {
+ #(#chain_arms_sync)*
+ _ => ::core::panic!("No chain found for type id: {:?}", any.type_id),
+ }
+ }
+ }
+ };
+
+ let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS)
+ .lock()
+ .unwrap()
+ .clone()
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
+ let num_variants = packed_types.len();
+ let repr_type = if u8::try_from(num_variants).is_ok() {
+ quote! { u8 }
+ } else if u16::try_from(num_variants).is_ok() {
+ quote! { u16 }
+ } else if u32::try_from(num_variants).is_ok() {
+ quote! { u32 }
+ } else {
+ quote! { u128 }
+ };
+
+ let expanded = quote! {
+ #pathf_hint
+
+ #[derive(Debug, PartialEq, Eq, Clone)]
+ #[repr(#repr_type)]
+ #[allow(nonstandard_style)]
+ pub enum #name {
+ #(#packed_types),*
+ }
+
+ impl ::std::fmt::Display for #name {
+ fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+ match self {
+ #(#name::#packed_types => write!(f, stringify!(#packed_types)),)*
+ }
+ }
+ }
+
+ impl ::mingling::ProgramCollect for #name {
+ type Enum = #name;
+ type ErrorDispatcherNotFound = ErrorDispatcherNotFound;
+ type ErrorRendererNotFound = ErrorRendererNotFound;
+ type ResultEmpty = ResultEmpty;
+ fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string()))
+ }
+ fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args))
+ }
+ fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> {
+ ::mingling::AnyOutput::new(ResultEmpty)
+ }
+ #render_fn
+ #do_chain_fn
+ fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
+ #[allow(unused_imports)]
+ #(#pathf_uses)*
+ match any.member_id {
+ #(#help_tokens)*
+ _ => ::mingling::RenderResult::default(),
+ }
+ }
+ fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
+ match any.member_id {
+ #(#renderer_exist_tokens)*
+ _ => false
+ }
+ }
+ fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
+ match any.member_id {
+ #(#chain_exist_tokens)*
+ _ => false
+ }
+ }
+ #dispatch_tree_nodes
+ #structural_render
+ #comp
+ }
+
+ impl #name {
+ /// Creates a new `Program<#name>` instance with default configuration.
+ pub fn new() -> ::mingling::Program<#name> {
+ ::mingling::Program::new()
+ }
+
+ /// Returns a static reference to the global `Program<#name>` singleton.
+ pub fn this() -> &'static ::mingling::Program<#name> {
+ &::mingling::this::<#name>()
+ }
+ }
+ };
+
+ // Clear all global registries to prevent stale state in Rust Analyzer
+ get_global_set(&PACKED_TYPES).lock().unwrap().clear();
+ get_global_set(&CHAINS).lock().unwrap().clear();
+ get_global_set(&CHAINS_EXIST).lock().unwrap().clear();
+ get_global_set(&RENDERERS).lock().unwrap().clear();
+ get_global_set(&RENDERERS_EXIST).lock().unwrap().clear();
+ get_global_set(&HELP_REQUESTS).lock().unwrap().clear();
+ #[cfg(feature = "comp")]
+ get_global_set(&COMPLETIONS).lock().unwrap().clear();
+ #[cfg(feature = "dispatch_tree")]
+ get_global_set(&COMPILE_TIME_DISPATCHERS)
+ .lock()
+ .unwrap()
+ .clear();
+ #[cfg(feature = "structural_renderer")]
+ get_global_set(&STRUCTURAL_RENDERERS)
+ .lock()
+ .unwrap()
+ .clear();
+
+ TokenStream::from(expanded)
+}
diff --git a/mingling_macros/src/group_impl.rs b/mingling_macros/src/func/group.rs
index 59da9dd..b865913 100644
--- a/mingling_macros/src/group_impl.rs
+++ b/mingling_macros/src/func/group.rs
@@ -91,7 +91,7 @@ fn gen_type_use(type_path: &TypePath) -> proc_macro2::TokenStream {
}
}
-pub fn group_macro(input: TokenStream) -> TokenStream {
+pub(crate) fn group_macro(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as GroupInput);
let is_aliased = matches!(input, GroupInput::Aliased { .. });
@@ -124,7 +124,7 @@ pub fn group_macro(input: TokenStream) -> TokenStream {
quote! {}
};
- // Generate the module with the Groupped implementation
+ // Generate the module with the Grouped implementation
let expanded = quote! {
#alias_stmt
#[allow(non_camel_case_types)]
@@ -133,7 +133,7 @@ pub fn group_macro(input: TokenStream) -> TokenStream {
#type_use
#alias_use
- impl ::mingling::Groupped<__MinglingProgram> for #type_name {
+ impl ::mingling::Grouped<__MinglingProgram> for #type_name {
fn member_id() -> __MinglingProgram {
__MinglingProgram::#type_name
}
diff --git a/mingling_macros/src/node.rs b/mingling_macros/src/func/node.rs
index b3a61c6..1b944a1 100644
--- a/mingling_macros/src/node.rs
+++ b/mingling_macros/src/func/node.rs
@@ -18,7 +18,7 @@ impl Parse for NodeInput {
}
}
-pub fn node(input: TokenStream) -> TokenStream {
+pub(crate) fn node(input: TokenStream) -> TokenStream {
// Parse the input as a string literal
let input_parsed = syn::parse_macro_input!(input as NodeInput);
let path_str = input_parsed.path.value();
diff --git a/mingling_macros/src/pack.rs b/mingling_macros/src/func/pack.rs
index 7f05232..a1a7e6b 100644
--- a/mingling_macros/src/pack.rs
+++ b/mingling_macros/src/func/pack.rs
@@ -25,7 +25,7 @@ impl Parse for PackInput {
}
#[allow(clippy::too_many_lines)]
-pub fn pack(input: TokenStream) -> TokenStream {
+pub(crate) fn pack(input: TokenStream) -> TokenStream {
let pack_input = syn::parse_macro_input!(input as PackInput);
let group_name = crate::default_program_path();
@@ -138,7 +138,7 @@ pub fn pack(input: TokenStream) -> TokenStream {
}
}
- impl ::mingling::Groupped<#group_name> for #type_name {
+ impl ::mingling::Grouped<#group_name> for #type_name {
fn member_id() -> #group_name {
#group_name::#type_name
}
diff --git a/mingling_macros/src/pack_err.rs b/mingling_macros/src/func/pack_err.rs
index ba7cf17..36e550a 100644
--- a/mingling_macros/src/pack_err.rs
+++ b/mingling_macros/src/func/pack_err.rs
@@ -31,7 +31,7 @@ impl syn::parse::Parse for PackErrInput {
}
#[allow(clippy::too_many_lines)]
-pub fn pack_err(input: TokenStream) -> TokenStream {
+pub(crate) fn pack_err(input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as PackErrInput);
match parsed {
@@ -42,7 +42,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream {
// Note: No longer derives Serialize under structural_renderer.
// Use pack_err_structural for structured output support.
let derive = quote! {
- #[derive(::mingling::Groupped)]
+ #[derive(::mingling::Grouped)]
};
let expanded = quote! {
@@ -75,7 +75,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream {
// Note: No longer derives Serialize under structural_renderer.
// Use pack_err_structural for structured output support.
let derive = quote! {
- #[derive(::mingling::Groupped)]
+ #[derive(::mingling::Grouped)]
};
let expanded = quote! {
@@ -123,7 +123,7 @@ pub fn pack_err(input: TokenStream) -> TokenStream {
/// impl ::mingling::__private::StructuralData for ErrorNotFound {}
/// ```
#[cfg(feature = "structural_renderer")]
-pub fn pack_err_structural(input: TokenStream) -> TokenStream {
+pub(crate) fn pack_err_structural(input: TokenStream) -> TokenStream {
let parsed = parse_macro_input!(input as PackErrInput);
let type_name = match &parsed {
@@ -150,7 +150,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
let snake_name = snake_case!(&name_str);
let expanded = quote! {
- #[derive(::mingling::Groupped, ::serde::Serialize)]
+ #[derive(::mingling::Grouped, ::serde::Serialize)]
pub struct #type_name {
/// The snake_case name of this error, automatically set at compile time.
pub name: String,
@@ -179,7 +179,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
let snake_name = snake_case!(&name_str);
let expanded = quote! {
- #[derive(::mingling::Groupped, ::serde::Serialize)]
+ #[derive(::mingling::Grouped, ::serde::Serialize)]
pub struct #type_name {
/// The snake_case name of this error, automatically set at compile time.
pub name: String,
diff --git a/mingling_macros/src/func/r_print.rs b/mingling_macros/src/func/r_print.rs
new file mode 100644
index 0000000..e81b544
--- /dev/null
+++ b/mingling_macros/src/func/r_print.rs
@@ -0,0 +1,62 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{Ident, Token};
+
+/// Parsed input for `r_println!` and `r_print!`.
+///
+/// Two forms:
+/// - `(ident, format_args...)` — explicit buffer
+/// - `(format_args...)` — implicit `__render_result_buffer`
+enum PrintInput {
+ Explicit { dst: Ident, args: TokenStream2 },
+ Implicit { args: TokenStream2 },
+}
+
+impl Parse for PrintInput {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ // Peek: if the next token is an ident followed by a comma, it's the explicit form
+ if input.peek(Ident) && input.peek2(Token![,]) {
+ let dst: Ident = input.parse()?;
+ let _comma: Token![,] = input.parse()?;
+ let args: TokenStream2 = input.parse()?;
+ Ok(PrintInput::Explicit { dst, args })
+ } else {
+ let args: TokenStream2 = input.parse()?;
+ Ok(PrintInput::Implicit { args })
+ }
+ }
+}
+
+fn expand_print(input: TokenStream, method: &str) -> TokenStream {
+ let parsed: PrintInput = match syn::parse(input) {
+ Ok(p) => p,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let method_ident = Ident::new(method, proc_macro2::Span::call_site());
+
+ let expanded = match parsed {
+ PrintInput::Explicit { dst, args } => {
+ quote! {
+ #dst.#method_ident(format!(#args))
+ }
+ }
+ PrintInput::Implicit { args } => {
+ quote! {
+ __render_result_buffer.#method_ident(format!(#args))
+ }
+ }
+ };
+
+ expanded.into()
+}
+
+pub(crate) fn r_println(input: TokenStream) -> TokenStream {
+ expand_print(input, "println")
+}
+
+pub(crate) fn r_print(input: TokenStream) -> TokenStream {
+ expand_print(input, "print")
+}
diff --git a/mingling_macros/src/suggest.rs b/mingling_macros/src/func/suggest.rs
index d3ab446..0f2026f 100644
--- a/mingling_macros/src/suggest.rs
+++ b/mingling_macros/src/func/suggest.rs
@@ -35,7 +35,7 @@ impl Parse for SuggestItem {
}
#[cfg(feature = "comp")]
-pub fn suggest(input: TokenStream) -> TokenStream {
+pub(crate) fn suggest(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as SuggestInput);
let mut items = Vec::new();
@@ -71,7 +71,7 @@ pub fn suggest(input: TokenStream) -> TokenStream {
expanded.into()
}
-pub fn suggest_enum(input: TokenStream) -> TokenStream {
+pub(crate) fn suggest_enum(input: TokenStream) -> TokenStream {
let enum_type = parse_macro_input!(input as syn::Type);
let expanded = quote! {{
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index e9fc327..56ed999 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -13,7 +13,7 @@
//! ┌──────────────────────────────────────────────────────────────────┐
//! │ Phase 1: Declaration │
//! │ │
-//! │ dispatcher! pack! node! #[derive(Groupped)] │
+//! │ dispatcher! pack! node! #[derive(Grouped)] │
//! │ │ │ │ │ │
//! │ V V V V │
//! │ Declares Wraps a Builds Makes a type │
@@ -55,7 +55,7 @@
//! | `pack_err!` | Creates an error struct with automatic `name` field |
//! | `pack_err_structural!` | Like `pack_err!` but also derives `StructuralData` for structured output |
//! | `entry!` | Creates a packed entry from string literals |
-//! | [`#[derive(Groupped)]`](derive@Groupped) | Makes a type recognizable by the framework's type registry |
+//! | [`#[derive(Grouped)]`](derive@Grouped) | Makes a type recognizable by the framework's type registry |
//! | `#[derive(StructuralData)]` | Marks a type as eligible for structured output (JSON/YAML/etc.) |
//! | [`#[derive(EnumTag)]`](derive@EnumTag) | Adds enum variant metadata (name, description) |
//!
@@ -141,40 +141,50 @@
//! }
//! ```
-use proc_macro::TokenStream;
+#![deny(missing_docs)]
+
+#[cfg(feature = "extra_macros")]
use quote::quote;
+
+#[cfg(feature = "extra_macros")]
+use syn::parse_macro_input;
+
+use proc_macro::TokenStream;
use std::collections::BTreeSet;
use std::sync::Mutex;
use std::sync::OnceLock;
-use syn::parse_macro_input;
-mod chain;
+mod attr;
+mod derive;
+mod func;
+mod systems;
+
+mod extensions;
+mod utils;
+
+// Bring all sub-modules into scope at the old paths so that existing
+// references (e.g. `chain::chain_attr`, `renderer::renderer_attr`)
+// continue to work without any `use`-path changes.
#[cfg(feature = "comp")]
-mod completion;
-#[cfg(feature = "dispatch_tree")]
-mod dispatch_tree_gen;
-mod dispatcher;
+use attr::completion;
#[cfg(feature = "clap")]
-mod dispatcher_clap;
+use attr::dispatcher_clap;
#[cfg(feature = "extra_macros")]
-mod entry;
-mod enum_tag;
+use attr::program_setup;
+use attr::{chain, help, renderer};
+use derive::{enum_tag, grouped};
#[cfg(feature = "extra_macros")]
-mod group_impl;
-mod groupped;
-mod help;
-mod node;
-mod pack;
+use func::entry;
#[cfg(feature = "extra_macros")]
-mod pack_err;
+pub(crate) use func::group as group_impl;
#[cfg(feature = "extra_macros")]
-mod program_setup;
-mod renderer;
-mod res_injection;
-#[cfg(feature = "structural_renderer")]
-mod structural_data;
+use func::pack_err;
#[cfg(feature = "comp")]
-mod suggest;
+use func::suggest;
+use func::{dispatcher, node, pack};
+use systems::res_injection;
+#[cfg(feature = "structural_renderer")]
+pub(crate) use systems::structural_data;
pub(crate) fn default_program_path() -> proc_macro2::TokenStream {
quote::quote! { crate::ThisProgram }
@@ -262,44 +272,36 @@ fn entry_has_variant(entry: &str, variant_name: &str) -> bool {
entry.contains(&format!(":: {variant_name} =>"))
}
-/// Registers an outside-type as a member of a program group without modifying its definition.
+/// Registers an outside type (from `std` or other crates) as a type recognizable
+/// by the Mingling framework, without modifying the original type definition.
///
-/// This macro allows you to use outside-types from external crates (like `std::io::Error`)
-/// within the Mingling framework by generating a `Groupped` implementation and registering
-/// the type's simple name as an enum variant.
+/// This macro generates a newtype wrapper around the given type that implements
+/// `Grouped`, `Into<AnyOutput>`, `Into<ChainProcess>`, and the `Routable` trait,
+/// making the outside type usable in `#[chain]` and `#[renderer]` functions.
///
/// # Syntax
///
/// ```rust,ignore
-/// group!(std::io::Error);
+/// // Simple form — creates a wrapper named after the type's last segment:
/// group!(ParseIntError);
-/// ```
///
-/// The type is registered under the default program (`crate::ThisProgram`).
-///
-/// # How it works
-///
-/// The macro generates a module containing:
-/// - A `use` import for the program path and the outside-type
-/// - An `impl Groupped<Program>` for the outside-type
-/// - A `register_type!` call with the type's simple name
-///
-/// The type's simple name (e.g. `Error`) is used as the enum variant in the generated
-/// program enum, just like `#[derive(Groupped)]` or `pack!`.
+/// // Aliased form — creates a wrapper with a custom name:
+/// group!(ErrorIo = std::io::Error);
+/// ```
///
/// # Example
///
-/// ```rust,ignore
-/// use mingling::macros::group;
-///
-/// // Register std::io::Error as a group member
-/// group!(std::io::Error);
+/// See the full example in the crate documentation or run:
+/// ```bash
+/// cargo run --example example-outside-type -- parse 42
+/// cargo run --example example-outside-type -- parse hello
+/// cargo run --example example-outside-type -- error
/// ```
///
-/// After expansion, the type can be used in chains and renderers like any
-/// `#[derive(Groupped)]` type.
+/// # Requirements
///
-/// This macro is only available with the `extra_macros` feature.
+/// - The type must be accessible at the call site (imported or fully qualified).
+/// - The alias name (if provided) must not conflict with existing types.
#[cfg(feature = "extra_macros")]
#[proc_macro]
pub fn group(input: TokenStream) -> TokenStream {
@@ -402,7 +404,7 @@ pub fn node(input: TokenStream) -> TokenStream {
/// - `AsRef<String>`, `AsMut<String>`
/// - `Default` if `String: Default`
/// - `Into<AnyOutput<ThisProgram>>`, `Into<ChainProcess<ThisProgram>>`
-/// - Implements `Groupped<ThisProgram>` with `member_id()` returning the enum variant
+/// - Implements `Grouped<ThisProgram>` with `member_id()` returning the enum variant
///
/// The struct is also registered via `register_type!` so that `gen_program!`
/// can include it in the program enum.
@@ -438,7 +440,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream {
/// Creates an error struct with a `name: String` field and optional `info: Type` field.
///
-/// This macro provides a concise way to define error types that implement `Groupped`
+/// This macro provides a concise way to define error types that implement `Grouped`
/// and are registered for inclusion in the program enum.
///
/// The `name` field is automatically set to the snake_case version of the struct name
@@ -461,7 +463,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream {
/// For `pack_err!(ErrorNotFound)`:
///
/// ```rust,ignore
-/// #[derive(::mingling::Groupped)]
+/// #[derive(::mingling::Grouped)]
/// pub struct ErrorNotFound {
/// name: String,
/// }
@@ -478,7 +480,7 @@ pub fn pack_structural(input: TokenStream) -> TokenStream {
/// For `pack_err!(ErrorNotDir = PathBuf)`:
///
/// ```rust,ignore
-/// #[derive(::mingling::Groupped)]
+/// #[derive(::mingling::Grouped)]
/// pub struct ErrorNotDir {
/// name: String,
/// info: PathBuf,
@@ -521,20 +523,47 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
pack_err::pack_err_structural(input)
}
-/// Early-returns an error from a `Result`, converting the `Ok` branch to a
-/// `ChainProcess`.
+/// Early-returns the error from a `Result`, converting the `Ok` branch to the
+/// next chain process value.
///
/// This macro is equivalent to:
/// ```rust,ignore
/// match expr {
/// Ok(r) => r,
-/// Err(e) => return ::mingling::Groupped::to_chain(e),
+/// Err(e) => return ::mingling::Routable::to_chain(e),
/// }
/// ```
///
-/// It is useful inside chain functions where you have a `Result<SomeType, SomeType>`
-/// where both types implement `Groupped` and want to propagate the error case
-/// as an early return via `Groupped::to_chain()`.
+/// It is useful inside chain functions where you have a `Result<SuccessType, ErrorType>`
+/// where both types implement `Routable` and you want to propagate the error case
+/// as an early return via `Routable::to_chain()`.
+///
+/// The key difference from a simple `?` operator is that `route!` converts **both**
+/// the success and error types into the chain process — the `Ok` value is unwrapped
+/// directly, while the `Err` value is converted via `Routable::to_chain()` and
+/// returned early.
+///
+/// ## Interaction with `#[routeify]`
+///
+/// The [`#[routeify]`](attr.routeify.html) attribute macro automatically replaces
+/// every `expr?` inside a function with `route!(expr)`. This means you can use the
+/// familiar `?` syntax in chain functions instead of writing `route!(...)`
+/// explicitly:
+///
+/// ```rust,ignore
+/// use mingling::macros::chain;
+///
+/// #[chain(routeify)]
+/// fn process(prev: SomeEntry) -> Next {
+/// // `?` here expands to `route!(...)` → this macro → the match block
+/// let value = some_fallible_call()?;
+/// value.to_chain()
+/// }
+/// ```
+///
+/// Because `#[routeify]` maps the span of `?` to this macro, hovering over `?` in
+/// a `#[routeify]` function will display this documentation — explaining what
+/// the `?` actually expands to.
///
/// # Example
///
@@ -542,7 +571,7 @@ pub fn pack_err_structural(input: TokenStream) -> TokenStream {
/// use mingling::macros::{chain, route};
///
/// #[chain]
-/// fn process(prev: SomeEntry) -> ChainProcess<ThisProgram> {
+/// fn process(prev: SomeEntry) -> Next {
/// let value = route!(current_dir().map_err(|e| ErrorEntry::new(e.to_string_lossy().to_string())));
/// // value is the PathBuf from current_dir()
/// value.to_chain()
@@ -555,7 +584,7 @@ pub fn route(input: TokenStream) -> TokenStream {
let expanded = quote! {
match #expr {
Ok(r) => r,
- Err(e) => return ::mingling::Groupped::to_chain(e),
+ Err(e) => return ::mingling::Routable::to_chain(e),
}
};
TokenStream::from(expanded)
@@ -613,7 +642,7 @@ pub fn route(input: TokenStream) -> TokenStream {
#[proc_macro]
pub fn empty_result(_input: TokenStream) -> TokenStream {
let expanded = quote! {
- <crate::ResultEmpty as ::mingling::Groupped::<crate::ThisProgram>>::to_chain(crate::ResultEmpty)
+ <crate::ResultEmpty as ::mingling::Routable::<crate::ThisProgram>>::to_chain(crate::ResultEmpty)
};
TokenStream::from(expanded)
}
@@ -872,6 +901,11 @@ pub fn dispatcher(input: TokenStream) -> TokenStream {
/// - With the `async` feature, async functions are supported; without it, async functions are rejected.
#[proc_macro_attribute]
pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream {
+ // Extension point: if attr contains extension identifiers like `routeify`,
+ // re-dispatch as #[ext1] #[ext2] #[chain] fn ...
+ if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "chain") {
+ return redispatch;
+ }
chain::chain_attr(attr, item)
}
@@ -939,6 +973,9 @@ pub fn chain(attr: TokenStream, item: TokenStream) -> TokenStream {
/// ```
#[proc_macro_attribute]
pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream {
+ if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "renderer") {
+ return redispatch;
+ }
renderer::renderer_attr(attr, item)
}
@@ -993,6 +1030,9 @@ pub fn renderer(attr: TokenStream, item: TokenStream) -> TokenStream {
#[cfg(feature = "comp")]
#[proc_macro_attribute]
pub fn completion(attr: TokenStream, item: TokenStream) -> TokenStream {
+ if let Some(redispatch) = extensions::try_redispatch_completion(attr.clone(), &item) {
+ return redispatch;
+ }
completion::completion_attr(attr, item)
}
@@ -1235,7 +1275,7 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream {
///
/// - The function must have exactly one parameter (the entry type to provide help for).
/// - The parameter type must be a single-segment type path (e.g., `MyEntry`, not `other::MyEntry`).
-/// - The function must return `RenderResult`.
+/// - The function may return `RenderResult`, `()`, or any type that implements `Into<RenderResult>`.
/// - The function cannot be async.
///
/// # See also
@@ -1246,14 +1286,136 @@ pub fn register_dispatcher(input: TokenStream) -> TokenStream {
///
/// [`BasicProgramSetup`]: https://docs.rs/mingling/latest/mingling/setup/struct.BasicProgramSetup.html
#[proc_macro_attribute]
-pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
+pub fn help(attr: TokenStream, item: TokenStream) -> TokenStream {
+ if let Some(redispatch) = extensions::try_redispatch_simple(attr.clone(), &item, "help") {
+ return redispatch;
+ }
help::help_attr(item)
}
-/// Derive macro for automatically implementing the `Groupped` trait on a struct.
+/// Extension attribute macro that transforms `expr?` into `route!(expr)`.
+///
+/// Designed for use with `#[chain(routeify, ...)]` to enable concise error
+/// routing in chain functions using the `?` operator syntax.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// #[chain(routeify)]
+/// fn handle_calc(args: EntryCalculate) -> Next {
+/// let a = args.pick(&arg![f32]).to_result()?;
+/// let op = args.pick(&arg![Operator]).to_result()?;
+/// StateCalculate { number_a: a, operator: op, ... }.to_chain()
+/// }
+/// ```
+#[cfg(feature = "extra_macros")]
+#[proc_macro_attribute]
+pub fn routeify(attr: TokenStream, item: TokenStream) -> TokenStream {
+ extensions::routeify::routeify_impl(attr, item)
+}
+
+/// Wraps a unit-returning function to produce a `RenderResult`.
+///
+/// The `#[buffer]` attribute macro injects a local `__render_result_buffer`
+/// variable of type `::mingling::RenderResult` and changes the function's
+/// return type to `::mingling::RenderResult`. Inside the body, use the
+/// `r_print!` and `r_println!` macros to write into the buffer.
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_println};
+///
+/// #[buffer]
+/// fn render_my_type(prev: MyType) {
+/// r_println!("Value: {:?}", *prev);
+/// }
+/// ```
///
-/// The `#[derive(Groupped)]` macro:
-/// 1. Implements `Groupped<crate::ThisProgram>`.
+/// This expands to:
+///
+/// ```rust,ignore
+/// fn render_my_type(prev: MyType) -> mingling::RenderResult {
+/// let mut __render_result_buffer = mingling::RenderResult::new();
+/// {
+/// r_println!("Value: {:?}", *prev);
+/// }
+/// __render_result_buffer
+/// }
+/// ```
+///
+/// # Requirements
+///
+/// - The function must return `()` (unit).
+/// - The function cannot be async.
+#[proc_macro_attribute]
+pub fn buffer(attr: TokenStream, item: TokenStream) -> TokenStream {
+ extensions::buffer::buffer_impl(attr, item)
+}
+
+/// Prints text to a `RenderResult` buffer, with a trailing newline.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_println};
+///
+/// #[buffer]
+/// fn render() {
+/// r_println!("Hello, {}!", name);
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// Pass a `RenderResult` variable as the first argument:
+///
+/// ```rust,ignore
+/// use mingling::macros::r_println;
+/// use mingling::RenderResult;
+///
+/// let mut r = RenderResult::new();
+/// r_println!(r, "value: {}", 42);
+/// assert_eq!(&*r, "value: 42\n");
+/// ```
+#[proc_macro]
+pub fn r_println(input: TokenStream) -> TokenStream {
+ func::r_print::r_println(input)
+}
+
+/// Prints text to a `RenderResult` buffer, without a trailing newline.
+///
+/// # Implicit buffer (inside `#[buffer]` functions)
+///
+/// ```rust,ignore
+/// use mingling::macros::{buffer, r_print};
+///
+/// #[buffer]
+/// fn render() {
+/// r_print!("Hello, ");
+/// r_println!("world!");
+/// }
+/// ```
+///
+/// # Explicit buffer
+///
+/// ```rust,ignore
+/// use mingling::macros::r_print;
+/// use mingling::RenderResult;
+///
+/// let mut r = RenderResult::new();
+/// r_print!(r, "value: {}", 42);
+/// assert_eq!(&*r, "value: 42");
+/// ```
+#[proc_macro]
+pub fn r_print(input: TokenStream) -> TokenStream {
+ func::r_print::r_print(input)
+}
+
+/// Derive macro for automatically implementing the `Grouped` trait on a struct.
+///
+/// The `#[derive(Grouped)]` macro:
+/// 1. Implements `Grouped<crate::ThisProgram>`.
/// 2. Registers the type via `register_type!` so it's included in the program enum.
/// 3. Generates `Into<AnyOutput<Group>>` and `Into<ChainProcess<Group>>` conversions.
/// 4. Adds `to_chain()` and `to_render()` methods to the struct.
@@ -1261,7 +1423,7 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
/// # Syntax
///
/// ```rust,ignore
-/// #[derive(Groupped)]
+/// #[derive(Grouped)]
/// struct MyStruct {
/// // ...
/// }
@@ -1270,9 +1432,9 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
/// # Example
///
/// ```rust,ignore
-/// use mingling::macros::Groupped;
+/// use mingling::macros::Grouped;
///
-/// #[derive(Groupped)]
+/// #[derive(Grouped)]
/// struct Greeting {
/// name: String,
/// }
@@ -1280,9 +1442,9 @@ pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
///
/// This is equivalent to using `pack!` but works with custom structs that
/// have named fields. For simple wrappers, prefer `pack!`.
-#[proc_macro_derive(Groupped, attributes(group))]
-pub fn derive_groupped(input: TokenStream) -> TokenStream {
- groupped::derive_groupped(input)
+#[proc_macro_derive(Grouped, attributes(group))]
+pub fn derive_grouped(input: TokenStream) -> TokenStream {
+ grouped::derive_grouped(input)
}
/// Derive macro for automatically implementing the `EnumTag` trait on an enum
@@ -1351,18 +1513,18 @@ pub fn derive_structural_data(input: TokenStream) -> TokenStream {
structural_data::derive_structural_data(input)
}
-/// Derive macro for implementing both `Groupped` and `serde::Serialize` on a struct.
+/// Derive macro for implementing both `Grouped` and `serde::Serialize` on a struct.
///
/// **This macro is only available with the `structural_renderer` feature.**
///
-/// This is identical to `#[derive(Groupped)]` but also adds `#[derive(serde::Serialize)]`
+/// This is identical to `#[derive(Grouped)]` but also adds `#[derive(serde::Serialize)]`
/// to the struct, which is required for the structural renderer to serialize output
/// to formats like JSON, YAML, TOML, or RON.
///
/// # Syntax
///
/// ```rust,ignore
-/// #[derive(GrouppedSerialize)]
+/// #[derive(GroupedSerialize)]
/// struct Info {
/// name: String,
/// age: i32,
@@ -1372,19 +1534,19 @@ pub fn derive_structural_data(input: TokenStream) -> TokenStream {
/// # Example
///
/// ```rust,ignore
-/// use mingling::GrouppedSerialize;
+/// use mingling::GroupedSerialize;
/// use serde::Serialize;
///
-/// #[derive(GrouppedSerialize)]
+/// #[derive(GroupedSerialize)]
/// struct Info {
/// name: String,
/// age: i32,
/// }
/// ```
#[cfg(feature = "structural_renderer")]
-#[proc_macro_derive(GrouppedSerialize, attributes(group))]
-pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
- groupped::derive_groupped_serialize(input)
+#[proc_macro_derive(GroupedSerialize, attributes(group))]
+pub fn derive_grouped_serialize(input: TokenStream) -> TokenStream {
+ grouped::derive_grouped_serialize(input)
}
/// Generates the program enum and all collected types, chains, and renderers.
@@ -1436,119 +1598,42 @@ pub fn derive_groupped_serialize(input: TokenStream) -> TokenStream {
/// gen_program!();
/// ```
#[proc_macro]
-pub fn gen_program(_input: TokenStream) -> TokenStream {
- #[cfg(feature = "comp")]
- let comp_gen = quote! {
- ::mingling::macros::program_comp_gen!();
- };
-
- #[cfg(not(feature = "comp"))]
- let comp_gen = quote! {};
-
- TokenStream::from(quote! {
- pub type Next = ::mingling::ChainProcess<crate::ThisProgram>;
-
- #comp_gen
- ::mingling::macros::program_fallback_gen!();
- ::mingling::macros::program_final_gen!();
- })
+pub fn gen_program(input: TokenStream) -> TokenStream {
+ func::gen_program::gen_program_impl(input)
}
-/// Internal macro used by `gen_program!` to generate completion infrastructure.
+/// Internal macro used by `gen_program!` to generate the completion infrastructure for
+/// shell completion support.
///
/// **This macro is only available with the `comp` feature.**
///
-/// This is an internal macro and should not be called directly by user code.
-/// It generates a completion dispatcher, the `CompletionContext` type, and
-/// the execution/render logic for shell completion.
-///
-/// The generated module `__completion_gen` contains:
-/// - A `__comp` dispatcher that routes completion requests
-/// - A `__exec_completion` chain that processes `CompletionContext` into `CompletionSuggest`
-/// - A `__render_completion` renderer that outputs completion suggestions
-#[proc_macro]
+/// The `program_comp_gen!` macro generates:
+/// 1. A hidden `__internal_completion_mod` module containing:
+/// - A `CompletionContext` packed type (wrapping `ShellContext`), dispatched via `"__comp"`.
+/// - A `CompletionSuggest` packed type (wrapping `(ShellContext, Suggest)`).
+/// - An internal dispatcher (`CMDCompletion`) for the `"__comp"` command path.
+/// 2. An internal chain function `__exec_completion` that:
+/// - Reads a `ShellContext` from the packed `CompletionContext`.
+/// - Calls `CompletionHelper::exec_completion::<ThisProgram>(&ctx)` to generate suggestions.
+/// - Routes the result to the completion renderer via `CompletionSuggest`.
+/// 3. An internal renderer `__render_completion` that renders the suggestions via
+/// `CompletionHelper::render_suggest`.
+///
+/// When the `dispatch_tree` feature is enabled, it also imports the internal dispatcher
+/// from the generated module into the parent scope for trie-based dispatch.
+///
+/// This macro is called automatically by `gen_program!` and should not be called
+/// directly by user code.
#[cfg(feature = "comp")]
-pub fn program_comp_gen(_input: TokenStream) -> TokenStream {
- #[cfg(feature = "async")]
- let fn_exec_comp = quote! {
- #[doc(hidden)]
- #[::mingling::macros::chain]
- pub async fn __exec_completion(prev: CompletionContext) -> Next {
- use ::mingling::Groupped;
-
- let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
- match read_ctx {
- Ok(ctx) => {
- let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
- crate::CompletionSuggest::new((ctx, suggest)).to_render()
- }
- Err(_) => std::process::exit(1),
- }
- }
- };
-
- #[cfg(not(feature = "async"))]
- let fn_exec_comp = quote! {
- #[doc(hidden)]
- #[::mingling::macros::chain]
- pub fn __exec_completion(prev: CompletionContext) -> Next {
- use ::mingling::Groupped;
-
- let read_ctx = ::mingling::ShellContext::try_from(prev.inner);
- match read_ctx {
- Ok(ctx) => {
- let suggest = ::mingling::CompletionHelper::exec_completion::<crate::ThisProgram>(&ctx);
- crate::CompletionSuggest::new((ctx, suggest)).to_render()
- }
- Err(_) => std::process::exit(1),
- }
- }
- };
-
- #[cfg(feature = "dispatch_tree")]
- let internal_dispatcher_comp = quote! {
- use __internal_completion_mod::__internal_dispatcher_comp;
- };
-
- #[cfg(not(feature = "dispatch_tree"))]
- let internal_dispatcher_comp = quote! {};
-
- let comp_dispatcher = quote! {
- #[doc(hidden)]
- mod __internal_completion_mod {
- use ::mingling::Groupped;
- ::mingling::macros::dispatcher!("__comp", CMDCompletion => CompletionContext);
- ::mingling::macros::pack!(
- CompletionSuggest = (::mingling::ShellContext, ::mingling::Suggest)
- );
- }
- #internal_dispatcher_comp
- use __internal_completion_mod::CompletionContext;
- use __internal_completion_mod::CompletionSuggest;
- pub use __internal_completion_mod::CMDCompletion;
-
- #fn_exec_comp
-
- ::mingling::macros::register_type!(CompletionContext);
-
- #[allow(unused)]
- #[doc(hidden)]
- #[::mingling::macros::renderer]
- pub fn __render_completion(prev: CompletionSuggest) -> ::mingling::RenderResult {
- let result = ::mingling::RenderResult::default();
- let (ctx, suggest) = prev.inner;
- ::mingling::CompletionHelper::render_suggest::<crate::ThisProgram>(ctx, suggest);
- result
- }
- };
-
- TokenStream::from(comp_dispatcher)
+#[proc_macro]
+pub fn program_comp_gen(input: TokenStream) -> TokenStream {
+ func::gen_program::program_comp_gen_impl(input)
}
/// Registers a type into the global packed types registry for inclusion in
/// the program enum generated by `gen_program!`.
///
-/// This macro is called internally by `pack!` and `#[derive(Groupped)]`(`macro.derive_groupped.html`)
+/// This macro is called internally by `pack!` and `#[derive(Grouped)]`(`macro.derive_grouped.html`)
/// and is generally not needed in user code. However, it can be used for manual
/// registration if you are implementing custom type registration outside of
/// the standard macros.
@@ -1567,115 +1652,60 @@ pub fn program_comp_gen(_input: TokenStream) -> TokenStream {
/// Panics if the global `PACKED_TYPES` mutex is poisoned.
#[proc_macro]
pub fn register_type(input: TokenStream) -> TokenStream {
- let type_ident = parse_macro_input!(input as syn::Ident);
- let entry_str = type_ident.to_string();
-
- get_global_set(&PACKED_TYPES)
- .lock()
- .unwrap()
- .insert(entry_str);
-
- TokenStream::new()
+ func::gen_program::register_type_impl(input)
}
-/// Registers a chain mapping from a previous type to a chain struct.
+/// Registers a chain mapping function into the global chain registry.
///
-/// This macro is called internally by `#[chain]`(macro.chain.html) and is
-/// generally not needed in user code. It inserts entries into the global
-/// `CHAINS` and `CHAINS_EXIST` registries.
+/// This macro is called internally by `#[chain]` and is generally not needed
+/// in user code. Each call stores a string entry containing the source-to-target
+/// type mapping, which is later consumed by `gen_program!` to generate the
+/// `has_chain` and `do_chain` dispatch logic in `ProgramCollect`.
///
-/// # Syntax
+/// The entry string format is a match arm: the source variant maps to a call
+/// that converts the value into a `ChainProcess` via the destination type.
///
-/// ```rust,ignore
-/// register_chain!(PreviousType, ChainStruct);
-/// ```
+/// # Panics
///
-/// The `PreviousType` is the input type of the chain step, and `ChainStruct`
-/// is the generated struct that implements the `Chain` trait.
+/// Panics if the global `CHAINS` mutex is poisoned.
#[proc_macro]
pub fn register_chain(input: TokenStream) -> TokenStream {
- chain::register_chain(input)
+ func::gen_program::register_chain_impl(input)
}
-/// Registers a renderer mapping from a type to a renderer struct.
+/// Registers a renderer mapping function into the global renderer registry.
///
-/// This macro is called internally by `#[renderer]`(macro.renderer.html) and is
-/// generally not needed in user code. It inserts entries into the global
-/// `RENDERERS`, `RENDERERS_EXIST` and (with `structural_renderer` feature)
-/// `STRUCTURAL_RENDERERS` registries.
+/// This macro is called internally by `#[renderer]` and is generally not
+/// needed in user code. Each call stores a string entry containing the
+/// type-to-render mapping, which is later consumed by `gen_program!` to
+/// generate the `has_renderer` and `render` dispatch logic in `ProgramCollect`.
///
-/// # Syntax
+/// The entry string format is a match arm: the type variant maps to a call
+/// of the registered renderer function that produces a `RenderResult`.
///
-/// ```rust,ignore
-/// register_renderer!(PreviousType, RendererStruct);
-/// ```
+/// # Panics
///
-/// The `PreviousType` is the input type of the renderer, and `RendererStruct`
-/// is the generated struct that implements the `Renderer` trait.
+/// Panics if the global `RENDERERS` mutex is poisoned.
#[proc_macro]
pub fn register_renderer(input: TokenStream) -> TokenStream {
- renderer::register_renderer(input)
+ func::gen_program::register_renderer_impl(input)
}
-/// Internal macro used by `gen_program!` to generate fallback types.
-///
-/// This macro generates the fallback wrapper types that are essential
-/// for error handling in the Mingling pipeline:
-///
-/// - **`ErrorRendererNotFound`** — Wraps a `String` (the name of the missing renderer).
-/// Used when no matching renderer is found for a given output type.
-/// - **`ErrorDispatcherNotFound`** — Wraps `Vec<String>` (the unrecognized command args).
-/// Used when no matching dispatcher is found for user input.
-/// - **`ResultEmpty`** — Wraps `()` (the unit type).
-/// Used when the chain returns an empty result.
-///
-/// Users can (and should) write `#[renderer]` functions for these types
-/// to provide meaningful error messages.
+/// Internal macro used by `gen_program!` to generate the fallback types for
+/// error cases when no dispatcher or renderer is found.
///
/// This macro is called automatically by `gen_program!` and should not
/// be called directly by user code.
-///
-/// # Syntax
-///
-/// ```rust,ignore
-/// // Called internally by gen_program!:
-/// program_fallback_gen!();
-/// ```
-///
-/// # Generated code equivalent
-///
-/// ```rust,ignore
-/// pack!(ErrorRendererNotFound = String);
-/// pack!(ErrorDispatcherNotFound = Vec<String>);
-/// pack!(ResultEmpty = ());
-/// ```
#[proc_macro]
-pub fn program_fallback_gen(_input: TokenStream) -> TokenStream {
- #[cfg(feature = "structural_renderer")]
- let pack_empty = quote! {
- #[derive(::serde::Serialize, ::mingling::StructuralData, ::mingling::Groupped, Default)]
- pub struct ResultEmpty;
- };
-
- #[cfg(not(feature = "structural_renderer"))]
- let pack_empty = quote! {
- #[derive(::mingling::Groupped, Default)]
- pub struct ResultEmpty;
- };
-
- let expanded = quote! {
- ::mingling::macros::pack!(ErrorRendererNotFound = String);
- ::mingling::macros::pack!(ErrorDispatcherNotFound = Vec<String>);
- #pack_empty
- };
- TokenStream::from(expanded)
+pub fn program_fallback_gen(input: TokenStream) -> TokenStream {
+ func::gen_program::program_fallback_gen_impl(input)
}
/// Internal macro used by `gen_program!` to generate the final program enum
/// and its `ProgramCollect` implementation.
///
/// This is the core code generation macro that:
-/// 1. Collects all registered types (from `pack!`, `#[derive(Groupped)]`, etc.) and
+/// 1. Collects all registered types (from `pack!`, `#[derive(Grouped)]`, etc.) and
/// creates an enum with each type as a variant.
/// 2. Generates the `Display` implementation for the enum.
/// 3. Generates the `ProgramCollect` implementation that dispatches to all
@@ -1720,434 +1750,9 @@ pub fn program_fallback_gen(_input: TokenStream) -> TokenStream {
/// pub fn new() -> Program<MyProgram> { Program::new() }
/// }
/// ```
-///
-/// # Panics
-///
-// Feature detection: baked into the proc-macro binary at compile time
-#[cfg(feature = "async")]
-const ASYNC_ENABLED: bool = true;
-#[cfg(not(feature = "async"))]
-const ASYNC_ENABLED: bool = false;
-
-/// Parses an entry of the format `StructName => EnumVariant,` into a pair of idents.
-fn parse_entry_pair(entry: &proc_macro2::TokenStream) -> (proc_macro2::Ident, proc_macro2::Ident) {
- let s = entry.to_string();
- let arrow_idx = s
- .find("=>")
- .unwrap_or_else(|| panic!("Entry missing '=>': {s}"));
- let struct_str = s[..arrow_idx].trim();
- let variant_str = s[arrow_idx + 2..].trim().trim_end_matches(',').trim();
- let struct_ident = proc_macro2::Ident::new(struct_str, proc_macro2::Span::call_site());
- let variant_ident = proc_macro2::Ident::new(variant_str, proc_macro2::Span::call_site());
- (struct_ident, variant_ident)
-}
-
-/// Loads the pathf type mapping from `$OUT_DIR/{crate}/type_using.rs`.
-/// Always compiled; returns empty map when pathf feature is not enabled.
-fn load_pathf_map() -> std::collections::HashMap<String, String> {
- if !cfg!(feature = "pathf") {
- return std::collections::HashMap::new();
- }
- let out_dir = std::env::var("OUT_DIR").ok();
- let crate_name = std::env::var("CARGO_PKG_NAME").ok();
- match (out_dir, crate_name) {
- (Some(dir), Some(name)) => {
- let path = std::path::Path::new(&dir).join(&name).join("type_using.rs");
- match std::fs::read_to_string(&path) {
- Ok(content) => content
- .lines()
- .filter_map(|line| {
- let line = line.trim();
- if let Some(rest) = line.strip_prefix("use ") {
- let path = rest.strip_suffix(';').unwrap_or(rest);
- if let Some((_mod, type_name)) = path.rsplit_once("::") {
- return Some((type_name.to_string(), path.to_string()));
- }
- }
- None
- })
- .collect(),
- Err(_) => std::collections::HashMap::new(),
- }
- }
- _ => std::collections::HashMap::new(),
- }
-}
-
-/// Resolves a type name to its full path token stream using the pathf mapping.
-pub(crate) fn resolve_type(
- name: &str,
- map: &std::collections::HashMap<String, String>,
-) -> proc_macro2::TokenStream {
- if let Some(full_path) = map.get(name) {
- syn::parse_str::<proc_macro2::TokenStream>(full_path).unwrap_or_else(|_| {
- let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
- quote! { #ident }
- })
- } else {
- let ident = proc_macro2::Ident::new(name, proc_macro2::Span::call_site());
- quote! { #ident }
- }
-}
-
-/// Panics if any of the global registries (`PACKED_TYPES`, `RENDERERS`, `CHAINS`, etc.)
-/// are poisoned.
#[proc_macro]
-#[allow(clippy::too_many_lines)]
-pub fn program_final_gen(_input: TokenStream) -> TokenStream {
- let name = syn::Ident::new("ThisProgram", proc_macro2::Span::call_site());
-
- let packed_types = get_global_set(&PACKED_TYPES).lock().unwrap().clone();
-
- let renderers = get_global_set(&RENDERERS).lock().unwrap().clone();
- let chains = get_global_set(&CHAINS).lock().unwrap().clone();
- let renderer_exist = get_global_set(&RENDERERS_EXIST).lock().unwrap().clone();
- let chain_exist = get_global_set(&CHAINS_EXIST).lock().unwrap().clone();
-
- #[cfg(feature = "structural_renderer")]
- let structural_renderers = get_global_set(&STRUCTURAL_RENDERERS)
- .lock()
- .unwrap()
- .clone();
-
- #[cfg(feature = "comp")]
- let completions = get_global_set(&COMPLETIONS).lock().unwrap().clone();
-
- let packed_types: Vec<proc_macro2::TokenStream> = packed_types
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let renderer_tokens: Vec<proc_macro2::TokenStream> = renderers
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let chain_tokens: Vec<proc_macro2::TokenStream> = chains
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let renderer_exist_tokens: Vec<proc_macro2::TokenStream> = renderer_exist
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let chain_exist_tokens: Vec<proc_macro2::TokenStream> = chain_exist
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let pathf_map: std::collections::HashMap<String, String> = if cfg!(feature = "pathf") {
- load_pathf_map()
- } else {
- std::collections::HashMap::new()
- };
-
- let pathf_uses: Vec<proc_macro2::TokenStream> = if cfg!(feature = "pathf") {
- pathf_map
- .values()
- .map(|path| format!("use {};", path).parse().unwrap_or_default())
- .collect()
- } else {
- Vec::new()
- };
-
- #[cfg(feature = "structural_renderer")]
- let structural_renderer_tokens: Vec<proc_macro2::TokenStream> = structural_renderers
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- #[cfg(feature = "structural_renderer")]
- let structural_render = quote! {
- fn structural_render(
- any: ::mingling::AnyOutput<Self::Enum>,
- setting: &::mingling::StructuralRendererSetting,
- ) -> Result<::mingling::RenderResult, ::mingling::error::StructuralRendererSerializeError> {
- #[allow(unused_imports)]
- #(#pathf_uses)*
- match any.member_id {
- #(#structural_renderer_tokens)*
- _ => {
- // Non-structural types: render ResultEmpty (which implements
- // StructuralData + Serialize) instead of producing nothing.
- let mut r = ::mingling::RenderResult::default();
- ::mingling::StructuralRenderer::render(&ResultEmpty, setting, &mut r)?;
- Ok(r)
- }
- }
- }
- };
-
- #[cfg(not(feature = "structural_renderer"))]
- let structural_render = quote! {};
-
- #[cfg(feature = "dispatch_tree")]
- let compile_time_dispatchers: Vec<String> = get_global_set(&COMPILE_TIME_DISPATCHERS)
- .lock()
- .unwrap()
- .clone()
- .iter()
- .cloned()
- .collect();
-
- #[cfg(feature = "dispatch_tree")]
- let dispatch_tree_nodes = {
- let entries: Vec<(String, String, String)> = compile_time_dispatchers
- .iter()
- .filter_map(|entry| {
- let parts: Vec<&str> = entry.split(':').collect();
- if parts.len() == 3 {
- Some((
- parts[0].to_string(),
- parts[1].to_string(),
- parts[2].to_string(),
- ))
- } else {
- None
- }
- })
- .collect();
-
- let get_nodes_fn = dispatch_tree_gen::gen_get_nodes(&entries, &pathf_map);
- let dispatch_trie_fn = dispatch_tree_gen::gen_dispatch_args_trie(&entries, &pathf_map);
-
- quote! {
- #get_nodes_fn
- #dispatch_trie_fn
- }
- };
-
- #[cfg(not(feature = "dispatch_tree"))]
- let dispatch_tree_nodes = quote! {};
-
- #[cfg(feature = "comp")]
- let completion_tokens: Vec<proc_macro2::TokenStream> = completions
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- #[cfg(feature = "comp")]
- let comp = quote! {
- fn do_comp(any: &::mingling::AnyOutput<Self::Enum>, ctx: &::mingling::ShellContext) -> ::mingling::Suggest {
- #[allow(unused_imports)]
- #(#pathf_uses)*
- match any.member_id {
- #(#completion_tokens)*
- _ => ::mingling::Suggest::FileCompletion,
- }
- }
- };
-
- #[cfg(not(feature = "comp"))]
- let comp = quote! {};
-
- // Build render function arms from stored entries
- let render_fn =
- if renderer_tokens.is_empty() {
- quote! {
- fn render(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
- ::mingling::RenderResult::default()
- }
- }
- } else {
- let render_arms: Vec<_> = renderer_tokens.iter().map(|entry| {
- let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
- quote! {
- Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
- let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
- <#resolved_struct as ::mingling::Renderer>::render(value)
- }
- }
- }).collect();
- quote! {
- fn render(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
- match any.member_id {
- #(#render_arms)*
- _ => ::mingling::RenderResult::default(),
- }
- }
- }
- };
-
- // Build do_chain function (async and sync versions)
- let chain_arms_async: Vec<_> = chain_tokens.iter().map(|entry| {
- let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
- quote! {
- Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
- let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
- let fut = async { <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value).await };
- ::std::boxed::Box::pin(fut)
- }
- }
- }).collect();
-
- let chain_arms_sync: Vec<_> = chain_tokens
- .iter()
- .map(|entry| {
- let (struct_ident, variant_ident) = parse_entry_pair(entry);
- let downcast_ty = resolve_type(&variant_ident.to_string(), &pathf_map);
- let resolved_struct = resolve_type(&struct_ident.to_string(), &pathf_map);
- quote! {
- Self::#variant_ident => {
- // SAFETY: The `type_id` check ensures that `any` contains a value of type `#variant_ident`,
- // so downcasting to `#variant_ident` is safe.
- let value = unsafe { any.downcast::<#downcast_ty>().unwrap_unchecked() };
- <#resolved_struct as ::mingling::Chain<Self::Enum>>::proc(value)
- }
- }
- })
- .collect();
-
- let do_chain_fn = if chain_tokens.is_empty() {
- quote! {
- fn do_chain(_any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::ChainProcess<Self::Enum> {
- ::core::panic!("No chain found for type id")
- }
- }
- } else if ASYNC_ENABLED {
- quote! {
- fn do_chain(
- any: ::mingling::AnyOutput<Self::Enum>,
- ) -> ::std::pin::Pin<::std::boxed::Box<dyn ::std::future::Future<Output = ::mingling::ChainProcess<Self::Enum>> + ::std::marker::Send>> {
- match any.member_id {
- #(#chain_arms_async)*
- _ => ::core::panic!("No chain found for type id: {:?}", any.type_id),
- }
- }
- }
- } else {
- quote! {
- fn do_chain(
- any: ::mingling::AnyOutput<Self::Enum>,
- ) -> ::mingling::ChainProcess<Self::Enum> {
- match any.member_id {
- #(#chain_arms_sync)*
- _ => ::core::panic!("No chain found for type id: {:?}", any.type_id),
- }
- }
- }
- };
-
- let help_tokens: Vec<proc_macro2::TokenStream> = get_global_set(&HELP_REQUESTS)
- .lock()
- .unwrap()
- .clone()
- .iter()
- .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
- .collect();
-
- let num_variants = packed_types.len();
- let repr_type = if u8::try_from(num_variants).is_ok() {
- quote! { u8 }
- } else if u16::try_from(num_variants).is_ok() {
- quote! { u16 }
- } else if u32::try_from(num_variants).is_ok() {
- quote! { u32 }
- } else {
- quote! { u128 }
- };
-
- let expanded = quote! {
- #[derive(Debug, PartialEq, Eq, Clone)]
- #[repr(#repr_type)]
- #[allow(nonstandard_style)]
- pub enum #name {
- #(#packed_types),*
- }
-
- impl ::std::fmt::Display for #name {
- fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
- match self {
- #(#name::#packed_types => write!(f, stringify!(#packed_types)),)*
- }
- }
- }
-
- impl ::mingling::ProgramCollect for #name {
- type Enum = #name;
- type ErrorDispatcherNotFound = ErrorDispatcherNotFound;
- type ErrorRendererNotFound = ErrorRendererNotFound;
- type ResultEmpty = ResultEmpty;
- fn build_renderer_not_found(member_id: Self::Enum) -> ::mingling::AnyOutput<Self::Enum> {
- ::mingling::AnyOutput::new(ErrorRendererNotFound::new(member_id.to_string()))
- }
- fn build_dispatcher_not_found(args: Vec<String>) -> ::mingling::AnyOutput<Self::Enum> {
- ::mingling::AnyOutput::new(ErrorDispatcherNotFound::new(args))
- }
- fn build_empty_result() -> ::mingling::AnyOutput<Self::Enum> {
- ::mingling::AnyOutput::new(ResultEmpty)
- }
- #render_fn
- #do_chain_fn
- fn render_help(any: ::mingling::AnyOutput<Self::Enum>) -> ::mingling::RenderResult {
- #[allow(unused_imports)]
- #(#pathf_uses)*
- match any.member_id {
- #(#help_tokens)*
- _ => ::mingling::RenderResult::default(),
- }
- }
- fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
- match any.member_id {
- #(#renderer_exist_tokens)*
- _ => false
- }
- }
- fn has_chain(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
- match any.member_id {
- #(#chain_exist_tokens)*
- _ => false
- }
- }
- #dispatch_tree_nodes
- #structural_render
- #comp
- }
-
- impl #name {
- /// Creates a new `Program<#name>` instance with default configuration.
- pub fn new() -> ::mingling::Program<#name> {
- ::mingling::Program::new()
- }
-
- /// Returns a static reference to the global `Program<#name>` singleton.
- pub fn this() -> &'static ::mingling::Program<#name> {
- &::mingling::this::<#name>()
- }
- }
- };
-
- // Clear all global registries to prevent stale state in Rust Analyzer
- get_global_set(&PACKED_TYPES).lock().unwrap().clear();
- get_global_set(&CHAINS).lock().unwrap().clear();
- get_global_set(&CHAINS_EXIST).lock().unwrap().clear();
- get_global_set(&RENDERERS).lock().unwrap().clear();
- get_global_set(&RENDERERS_EXIST).lock().unwrap().clear();
- get_global_set(&HELP_REQUESTS).lock().unwrap().clear();
- #[cfg(feature = "comp")]
- get_global_set(&COMPLETIONS).lock().unwrap().clear();
- #[cfg(feature = "dispatch_tree")]
- get_global_set(&COMPILE_TIME_DISPATCHERS)
- .lock()
- .unwrap()
- .clear();
- #[cfg(feature = "structural_renderer")]
- get_global_set(&STRUCTURAL_RENDERERS)
- .lock()
- .unwrap()
- .clear();
-
- TokenStream::from(expanded)
+pub fn program_final_gen(input: TokenStream) -> TokenStream {
+ func::gen_program::program_final_gen_impl(input)
}
/// Builds a `Suggest` instance with inline suggestion items.
diff --git a/mingling_macros/src/systems.rs b/mingling_macros/src/systems.rs
new file mode 100644
index 0000000..53e58c5
--- /dev/null
+++ b/mingling_macros/src/systems.rs
@@ -0,0 +1,5 @@
+#[cfg(feature = "dispatch_tree")]
+pub(crate) mod dispatch_tree_gen;
+pub(crate) mod res_injection;
+#[cfg(feature = "structural_renderer")]
+pub(crate) mod structural_data;
diff --git a/mingling_macros/src/dispatch_tree_gen.rs b/mingling_macros/src/systems/dispatch_tree_gen.rs
index b66e2f4..7383421 100644
--- a/mingling_macros/src/dispatch_tree_gen.rs
+++ b/mingling_macros/src/systems/dispatch_tree_gen.rs
@@ -4,11 +4,11 @@ use just_fmt::snake_case;
use proc_macro2::TokenStream;
use quote::quote;
-use crate::resolve_type;
+use crate::func::gen_program::resolve_type;
/// Generate the `get_nodes()` function body for a ProgramCollect impl.
/// If `pathf_map` is non-empty, resolves internal dispatcher statics using full paths.
-pub fn gen_get_nodes(
+pub(crate) fn gen_get_nodes(
entries: &[(String, String, String)],
pathf_map: &HashMap<String, String>,
) -> TokenStream {
@@ -40,7 +40,7 @@ pub fn gen_get_nodes(
/// Single-node groups use `starts_with`; multi-node groups recurse with `nth()` match.
///
/// If `pathf_map` is non-empty, resolves dispatcher types using full paths.
-pub fn gen_dispatch_args_trie(
+pub(crate) fn gen_dispatch_args_trie(
entries: &[(String, String, String)],
pathf_map: &HashMap<String, String>,
) -> TokenStream {
diff --git a/mingling_macros/src/res_injection.rs b/mingling_macros/src/systems/res_injection.rs
index 606b9a6..606b9a6 100644
--- a/mingling_macros/src/res_injection.rs
+++ b/mingling_macros/src/systems/res_injection.rs
diff --git a/mingling_macros/src/structural_data.rs b/mingling_macros/src/systems/structural_data.rs
index 5350d7e..74bcf09 100644
--- a/mingling_macros/src/structural_data.rs
+++ b/mingling_macros/src/systems/structural_data.rs
@@ -176,7 +176,7 @@ pub(crate) fn pack_structural(input: TokenStream) -> TokenStream {
}
}
- impl ::mingling::Groupped<#program_path> for #type_name {
+ impl ::mingling::Grouped<#program_path> for #type_name {
fn member_id() -> #program_path {
#program_path::#type_name
}
@@ -297,7 +297,7 @@ pub(crate) fn group_structural(input: TokenStream) -> TokenStream {
#type_use
#alias_use
- impl ::mingling::Groupped<__MinglingProgram> for #type_name {
+ impl ::mingling::Grouped<__MinglingProgram> for #type_name {
fn member_id() -> __MinglingProgram {
__MinglingProgram::#type_name
}
diff --git a/mingling_macros/src/utils.rs b/mingling_macros/src/utils.rs
new file mode 100644
index 0000000..49d0e7a
--- /dev/null
+++ b/mingling_macros/src/utils.rs
@@ -0,0 +1 @@
+// Shared utilities for the macro crate.
diff --git a/mingling_pathf/README.md b/mingling_pathf/README.md
index 89d1811..9706f49 100644
--- a/mingling_pathf/README.md
+++ b/mingling_pathf/README.md
@@ -7,7 +7,7 @@
## Overview
-`mingling_pathf` provides the `pathf` feature for Mingling. It automatically analyzes the full module paths of all Mingling types in a crate at build-time (types defined via `pack!`, `#[derive(Groupped)]`, `#[chain]`, `#[renderer]`, etc.), and generates a mapping from type names to module paths for consumption by `gen_program!()` at compile-time.
+`mingling_pathf` provides the `pathf` feature for Mingling. It automatically analyzes the full module paths of all Mingling types in a crate at build-time (types defined via `pack!`, `#[derive(Grouped)]`, `#[chain]`, `#[renderer]`, etc.), and generates a mapping from type names to module paths for consumption by `gen_program!()` at compile-time.
When enabled, `gen_program!()` uses full module paths for type references in the generated dispatch code (e.g., `downcast::<myapp::sub::ResultMyName>()`), eliminating the need to `use` all types in the module where `gen_program!()` is called. This allows for a more flexible module organization without the constraint of centralized `use` statements.
diff --git a/mingling_pathf/src/error.rs b/mingling_pathf/src/error.rs
index 70a8f6e..d384901 100644
--- a/mingling_pathf/src/error.rs
+++ b/mingling_pathf/src/error.rs
@@ -22,7 +22,9 @@ pub enum MinglingPathfinderError {
/// `parent` is the directory containing the file that declared the module.
/// `module_name` is the name of the module that could not be found.
ModuleNotFound {
+ /// The directory containing the file that declared the module.
parent: PathBuf,
+ /// The name of the module that could not be found.
module_name: String,
},
@@ -30,7 +32,12 @@ pub enum MinglingPathfinderError {
///
/// `file` is the file containing the invalid attribute.
/// `path_attr` is the value of the `#[path]` attribute.
- PathPointsOutside { file: PathBuf, path_attr: String },
+ PathPointsOutside {
+ /// The file containing the invalid `#[path]` attribute.
+ file: PathBuf,
+ /// The value of the `#[path]` attribute that points outside the project.
+ path_attr: String,
+ },
/// No entry point file (`main.rs`, `lib.rs`, or any file under `bin/`) was found.
NoEntryPointFound,
@@ -39,7 +46,12 @@ pub enum MinglingPathfinderError {
///
/// `path` is the file that failed to parse.
/// `message` contains details from the parser.
- SynError { path: PathBuf, message: String },
+ SynError {
+ /// The file that failed to parse.
+ path: PathBuf,
+ /// Details from the parser about the parse failure.
+ message: String,
+ },
}
impl fmt::Display for MinglingPathfinderError {
diff --git a/mingling_pathf/src/lib.rs b/mingling_pathf/src/lib.rs
index 557ae45..69ff3ac 100644
--- a/mingling_pathf/src/lib.rs
+++ b/mingling_pathf/src/lib.rs
@@ -1,5 +1,6 @@
#![allow(clippy::needless_doctest_main)]
#![doc = include_str!("../README.md")]
+#![deny(missing_docs)]
pub mod config;
pub mod error;
diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs
index 3765971..31d6918 100644
--- a/mingling_pathf/src/pattern_analyzer.rs
+++ b/mingling_pathf/src/pattern_analyzer.rs
@@ -31,7 +31,7 @@ pub fn init_with_config(config: PathfinderConfig) -> PatternAnalyzer {
analyzer.add_pattern(BasicStructPattern);
analyzer.add_pattern(PackPattern);
analyzer.add_pattern(GroupPattern);
- analyzer.add_pattern(GrouppedDerivePattern);
+ analyzer.add_pattern(GroupedDerivePattern);
analyzer.add_pattern(ChainPattern);
analyzer.add_pattern(RendererPattern);
analyzer.add_pattern(HelpPattern);
@@ -100,10 +100,12 @@ pub struct PatternAnalyzer {
}
impl PatternAnalyzer {
+ /// Creates a new empty `PatternAnalyzer`.
pub fn new() -> Self {
Self::default()
}
+ /// Registers a new pattern for analysis.
pub fn add_pattern(&mut self, pattern: impl AnalyzePattern + 'static) {
self.patterns.push(Box::new(pattern));
}
diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs
index 9801e9b..9845b73 100644
--- a/mingling_pathf/src/patterns.rs
+++ b/mingling_pathf/src/patterns.rs
@@ -6,7 +6,7 @@ pub use completion::*;
pub use dispatcher::*;
pub use dispatcher_clap::*;
pub use group::*;
-pub use groupped_derive::*;
+pub use grouped_derive::*;
pub use help::*;
pub use pack::*;
pub use renderer::*;
@@ -17,7 +17,7 @@ mod completion;
mod dispatcher;
mod dispatcher_clap;
mod group;
-mod groupped_derive;
+mod grouped_derive;
mod help;
mod pack;
mod renderer;
diff --git a/mingling_pathf/src/patterns/dispatcher.rs b/mingling_pathf/src/patterns/dispatcher.rs
index 6796a2c..eaa53f0 100644
--- a/mingling_pathf/src/patterns/dispatcher.rs
+++ b/mingling_pathf/src/patterns/dispatcher.rs
@@ -20,10 +20,18 @@ use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
/// - `CMD*` — the dispatcher struct (always)
/// - `__internal_dispatcher_*` — dispatch tree static (when `use_dispatch_tree` is true)
pub struct DispatcherPattern {
+ /// Whether the dispatcher generates a dispatch tree static (`__internal_dispatcher_*`).
pub use_dispatch_tree: bool,
}
impl DispatcherPattern {
+ /// Creates a new `DispatcherPattern`.
+ ///
+ /// # Arguments
+ ///
+ /// * `use_dispatch_tree` — when `true`, the generated dispatcher also produces a
+ /// `__internal_dispatcher_*` static dispatch tree item. Set this based on whether
+ /// your macro invocation includes the `use_dispatch_tree` configuration.
pub fn new(use_dispatch_tree: bool) -> Self {
Self { use_dispatch_tree }
}
diff --git a/mingling_pathf/src/patterns/dispatcher_clap.rs b/mingling_pathf/src/patterns/dispatcher_clap.rs
index 1a86ad5..0a249b4 100644
--- a/mingling_pathf/src/patterns/dispatcher_clap.rs
+++ b/mingling_pathf/src/patterns/dispatcher_clap.rs
@@ -29,10 +29,16 @@ use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
/// - `#[dispatcher_clap("greet", CMDGreet, help = true)] struct EntryGreet { ... }`
/// - `#[dispatcher_clap("greet", CMDGreet, error = ErrorGreet, help = true)] struct EntryGreet { ... }`
pub struct DispatcherClapPattern {
+ /// Whether to include the `__internal_dispatcher_*` dispatch tree static in the analysis.
pub use_dispatch_tree: bool,
}
impl DispatcherClapPattern {
+ /// Creates a new `DispatcherClapPattern` with the given configuration.
+ ///
+ /// # Parameters
+ /// - `use_dispatch_tree`: When `true`, enables analysis of the `__internal_dispatcher_*`
+ /// static dispatch tree for each matched command.
pub fn new(use_dispatch_tree: bool) -> Self {
Self { use_dispatch_tree }
}
diff --git a/mingling_pathf/src/patterns/groupped_derive.rs b/mingling_pathf/src/patterns/grouped_derive.rs
index 91daaef..9522c1f 100644
--- a/mingling_pathf/src/patterns/groupped_derive.rs
+++ b/mingling_pathf/src/patterns/grouped_derive.rs
@@ -1,5 +1,5 @@
-//! The `GrouppedDerivePattern` matches structs, enums, and unions annotated with
-//! `#[derive(Groupped)]` or `#[derive(GrouppedSerialize)]` (or any combination
+//! The `GroupedDerivePattern` matches structs, enums, and unions annotated with
+//! `#[derive(Grouped)]` or `#[derive(GroupedSerialize)]` (or any combination
//! with other derives). It also recurses into `mod` items to find nested types.
//! This is used to track grouped items for code generation or analysis.
@@ -7,17 +7,17 @@ use syn::Item;
use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern};
-/// Matches `#[derive(Groupped)]` and `#[derive(GrouppedSerialize)]`.
+/// Matches `#[derive(Grouped)]` and `#[derive(GroupedSerialize)]`.
///
/// Covers the forms:
-/// - `#[derive(Groupped)] struct T { ... }`
-/// - `#[derive(Groupped, Serialize, ...)] struct T { ... }`
-/// - `#[derive(GrouppedSerialize)] struct T { ... }`
-pub struct GrouppedDerivePattern;
+/// - `#[derive(Grouped)] struct T { ... }`
+/// - `#[derive(Grouped, Serialize, ...)] struct T { ... }`
+/// - `#[derive(GroupedSerialize)] struct T { ... }`
+pub struct GroupedDerivePattern;
-impl AnalyzePattern for GrouppedDerivePattern {
+impl AnalyzePattern for GroupedDerivePattern {
fn contains(&self, content: &str) -> bool {
- content.contains("Groupped")
+ content.contains("Grouped")
}
fn analyze(&self, content: &str) -> Vec<AnalyzeItem> {
@@ -29,19 +29,19 @@ impl AnalyzePattern for GrouppedDerivePattern {
for item in &syntax.items {
match item {
- Item::Struct(s) if has_groupped_derive(&s.attrs) => {
+ Item::Struct(s) if has_grouped_derive(&s.attrs) => {
items.push(AnalyzeItem {
module: String::new(),
item_name: s.ident.to_string(),
});
}
- Item::Enum(e) if has_groupped_derive(&e.attrs) => {
+ Item::Enum(e) if has_grouped_derive(&e.attrs) => {
items.push(AnalyzeItem {
module: String::new(),
item_name: e.ident.to_string(),
});
}
- Item::Union(u) if has_groupped_derive(&u.attrs) => {
+ Item::Union(u) if has_grouped_derive(&u.attrs) => {
items.push(AnalyzeItem {
module: String::new(),
item_name: u.ident.to_string(),
@@ -51,13 +51,13 @@ impl AnalyzePattern for GrouppedDerivePattern {
if let Some((_, nested)) = &item_mod.content {
for n in nested {
match n {
- Item::Struct(s) if has_groupped_derive(&s.attrs) => {
+ Item::Struct(s) if has_grouped_derive(&s.attrs) => {
items.push(AnalyzeItem {
module: item_mod.ident.to_string(),
item_name: s.ident.to_string(),
});
}
- Item::Enum(e) if has_groupped_derive(&e.attrs) => {
+ Item::Enum(e) if has_grouped_derive(&e.attrs) => {
items.push(AnalyzeItem {
module: item_mod.ident.to_string(),
item_name: e.ident.to_string(),
@@ -76,10 +76,10 @@ impl AnalyzePattern for GrouppedDerivePattern {
}
}
-fn has_groupped_derive(attrs: &[syn::Attribute]) -> bool {
+fn has_grouped_derive(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
if attr.path().is_ident("derive") {
- // Correctly parse comma-separated paths in #[derive(Groupped, Debug, ...)]
+ // Correctly parse comma-separated paths in #[derive(Grouped, Debug, ...)]
attr.parse_args_with(|input: syn::parse::ParseStream| {
let paths =
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated(
@@ -87,7 +87,7 @@ fn has_groupped_derive(attrs: &[syn::Attribute]) -> bool {
)?;
Ok(paths.iter().any(|p| {
let name = p.segments.last().unwrap().ident.to_string();
- name == "Groupped" || name == "GrouppedSerialize"
+ name == "Grouped" || name == "GroupedSerialize"
}))
})
.unwrap_or(false)
diff --git a/mingling_pathf/test/src/lib.rs b/mingling_pathf/test/src/lib.rs
index 824cbbf..7e7cbd5 100644
--- a/mingling_pathf/test/src/lib.rs
+++ b/mingling_pathf/test/src/lib.rs
@@ -222,11 +222,11 @@ fn test_group_analyze() {
}
#[test]
-fn test_groupped_derive_analyze() {
+fn test_grouped_derive_analyze() {
let analyzer = mingling_pathf::pattern_analyzer::init();
let file = current_dir()
.unwrap()
- .join("src/test_files/test_groupped_derive.rs");
+ .join("src/test_files/test_grouped_derive.rs");
let r = analyzer.analyze_file(file).unwrap();
let required: Vec<&str> = vec![
diff --git a/mingling_pathf/test/src/test_files/test_groupped_derive.rs b/mingling_pathf/test/src/test_files/test_grouped_derive.rs
index 913587c..20055e9 100644
--- a/mingling_pathf/test/src/test_files/test_groupped_derive.rs
+++ b/mingling_pathf/test/src/test_files/test_grouped_derive.rs
@@ -1,42 +1,42 @@
-#[derive(Groupped)]
+#[derive(Grouped)]
struct Derived1 {
value: String,
}
-#[derive(Groupped, Debug, Clone)]
+#[derive(Grouped, Debug, Clone)]
struct Derived2 {
value: i32,
}
-#[derive(GrouppedSerialize)]
+#[derive(GroupedSerialize)]
struct Derived3 {
value: bool,
}
-#[derive(Groupped)]
+#[derive(Grouped)]
enum EnumDerived1 {
A,
B,
}
-#[derive(GrouppedSerialize)]
+#[derive(GroupedSerialize)]
enum EnumDerived2 {
X(String),
Y(i32),
}
pub mod sub {
- #[derive(Groupped)]
+ #[derive(Grouped)]
struct Derived1 {
value: String,
}
- #[derive(GrouppedSerialize)]
+ #[derive(GroupedSerialize)]
struct Derived3 {
value: bool,
}
- #[derive(Groupped)]
+ #[derive(Grouped)]
enum EnumDerived1 {
A,
}
diff --git a/mling/.gitignore b/mling/.gitignore
deleted file mode 100644
index b124b7c..0000000
--- a/mling/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-debug.rs
-version.txt
diff --git a/mling/Cargo.toml b/mling/Cargo.toml
deleted file mode 100644
index 8008463..0000000
--- a/mling/Cargo.toml
+++ /dev/null
@@ -1,41 +0,0 @@
-[package]
-name = "mingling-cli"
-version.workspace = true
-edition.workspace = true
-authors = ["Weicao-CatilGrass"]
-license.workspace = true
-readme = "README.md"
-repository.workspace = true
-description = "Mingling's scaffolding tool"
-keywords = ["cli", "scaffolding", "command-line", "framework"]
-categories = ["command-line-interface"]
-
-[[bin]]
-name = "cargo-mling"
-path = "src/bin/cargo-mling.rs"
-
-[[bin]]
-name = "mling"
-path = "src/bin/mling.rs"
-
-[dependencies]
-mingling = { path = "../mingling", features = [
- "parser",
- "comp",
- "structural_renderer",
- "extra_macros",
- "yaml_serde_fmt",
- "pathf"
-] }
-
-serde = { version = "1", features = ["derive"] }
-serde_json = "1"
-
-colored = "3.1.1"
-dirs = "6.0.0"
-just_fmt = "0.1.2"
-toml.workspace = true
-
-[build-dependencies]
-chrono = "0.4"
-mingling = { path = "../mingling", features = ["comp", "pathf", "builds"] }
diff --git a/mling/LICENSE-APACHE b/mling/LICENSE-APACHE
deleted file mode 120000
index 965b606..0000000
--- a/mling/LICENSE-APACHE
+++ /dev/null
@@ -1 +0,0 @@
-../LICENSE-APACHE \ No newline at end of file
diff --git a/mling/LICENSE-MIT b/mling/LICENSE-MIT
deleted file mode 120000
index 76219eb..0000000
--- a/mling/LICENSE-MIT
+++ /dev/null
@@ -1 +0,0 @@
-../LICENSE-MIT \ No newline at end of file
diff --git a/mling/README.md b/mling/README.md
deleted file mode 100644
index a4d0b1d..0000000
--- a/mling/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-This directory is the companion CLI tool for Mingling. After installation, it can be invoked via `cargo mling` or `mling`.
-
-It is currently under refactoring and **completely unusable**.
diff --git a/mling/build.rs b/mling/build.rs
deleted file mode 100644
index 9a7b503..0000000
--- a/mling/build.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-use std::path::Path;
-use std::process::Command;
-
-use mingling::build::analyze_and_build_type_mapping;
-use mingling::build::build_comp_scripts;
-
-fn main() {
- build_version_info();
- build_completion();
- analyze_and_build_type_mapping().unwrap();
-}
-
-fn build_version_info() {
- // Read version from CARGO_PKG_VERSION (inherited from workspace Cargo.toml)
- let version = env!("CARGO_PKG_VERSION");
-
- // Get git commit hash (first 9 characters)
- let commit_hash = Command::new("git")
- .args(["rev-parse", "--short=9", "HEAD"])
- .output()
- .ok()
- .and_then(|output| {
- if output.status.success() {
- String::from_utf8(output.stdout).ok()
- } else {
- None
- }
- })
- .map(|s| s.trim().to_string())
- .unwrap_or_else(|| "unknown".to_string());
-
- // Get date from git commit, fallback to current date
- let date = Command::new("git")
- .args(["log", "-1", "--format=%ad", "--date=format:%Y-%m-%d"])
- .output()
- .ok()
- .and_then(|output| {
- if output.status.success() {
- String::from_utf8(output.stdout).ok()
- } else {
- None
- }
- })
- .map(|s| s.trim().to_string())
- .unwrap_or_else(|| chrono::Local::now().format("%Y-%m-%d").to_string());
-
- let version_string = format!("mling {version} ({commit_hash} {date})");
-
- let out_dir = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
- .join("src")
- .join("helps")
- .join("version.txt");
-
- std::fs::write(&out_dir, version_string).expect("failed to write version.txt");
-
- println!("cargo::rerun-if-changed=../Cargo.toml");
- println!("cargo::rerun-if-changed=../Cargo.lock");
- println!("cargo::rerun-if-changed=.git/HEAD");
-}
-
-fn build_completion() {
- build_comp_scripts("mling").unwrap();
-}
diff --git a/mling/res/CHECKLIST.md b/mling/res/CHECKLIST.md
deleted file mode 100644
index bc7b8bc..0000000
--- a/mling/res/CHECKLIST.md
+++ /dev/null
@@ -1,110 +0,0 @@
-> [!NOTE]
->
-> You are using `mling` to create your `mingling` project. Before proceeding, please fill in your project information.
-
-## Question 1: Fill in your project information
-
-Use `snake-case` style to fill in the project name. It will serve as your `crate` name:
-
-```name
-my-cli
-```
-
-Describe your `program` in one sentence:
-
-```description
-This is a command-line program
-```
-
-## Question 2: Which Mingling version do you need?
-
-> [!TIP]
->
-> You must select **EXACTLY ONE** option
-
-- [x] `ver:0.2` Based on the `Mingling@0.2.x` template
-
-## Question 3: What is your project layout style?
-
-> [!TIP]
->
-> You must select **EXACTLY ONE** option
-
-- [x] `base:flat` Flat mode (Simple)
-
-```
-# The most basic mode
-./main.rs
-./foo.rs
-./bar.rs
-./globals.rs
-```
-
-- [ ] `base:modularity` Modular mode
-
-```
-├─ module1/ # Stores private concepts internally
-│ ├── mod.rs # Command registration
-│ ├── chains/ # Core logic
-│ └── renderers/ # Result rendering
-├─ module2/
-│ ├── mod.rs # Command registration
-│ ├── chains/ # Core logic
-│ └── renderers/ # Result rendering
-├─ global/ # Stores public concepts in the global directory
-│ ├── errors/ # Global Errors
-│ ├── setups/ # Setups
-│ └── resources/ # Global Resources
-└─ main.rs
-```
-
-- [ ] `base:command` Command mode
-
-```
-# Stores all commands
-├─ commands/ # Organizes subcommands by hierarchy
-│ ├── remote/add.rs # "remote add"
-│ ├── remote/rm.rs # "remote rm"
-│ ├── remote.rs # Not a command,
-│ │ but a local implementation for
-│ │ the remote series of commands
-│ ├── foo.rs # "foo"
-│ └── bar.rs # "bar"
-├─ errors/ # Global Errors
-├─ resources/ # Global Resources
-├─ setups/ # Setups
-└─ main.rs
-```
-
-## Question 4: What additional features do you need to enable?
-
-> [!TIP]
->
-> All of the following options are **OPTIONAL**
-
-**Pre:**
-
-- [x] `exec:git_init` Initialize this project as a git repository (requires `git` to be installed)
-
-**Features:**
-
-- [x] `feat:extra_macros` Extra macro support
-- [x] `feat:dispatch_tree` Compile-time dispatch tree (solidify all commands)
-- [ ] `feat:structural_renderer` Structural renderer (JSON/YAML) for feature output
-- [ ] `feat:comp` Dynamic completion system
-- [ ] `feat:clap` Clap support
-- [ ] `feat:parser` Mingling Picker parser support
-- [ ] `feat:async` Async support
-
-**Implementations:**
-
-- [ ] `impl:exit_code` Introduce exit code control
-- [ ] `impl:fallback` Fallback functionality when no command is given
-
----
-
-After filling in the above, execute the following command to create the project:
-
-```
-~# mling gen
-```
diff --git a/mling/res/template_0.2/command/Cargo.toml.tmpl b/mling/res/template_0.2/command/Cargo.toml.tmpl
deleted file mode 100644
index e69de29..0000000
--- a/mling/res/template_0.2/command/Cargo.toml.tmpl
+++ /dev/null
diff --git a/mling/res/template_0.2/command/src/main.rs b/mling/res/template_0.2/command/src/main.rs
deleted file mode 100644
index 8b13789..0000000
--- a/mling/res/template_0.2/command/src/main.rs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/mling/res/template_0.2/flat/Cargo.toml.tmpl b/mling/res/template_0.2/flat/Cargo.toml.tmpl
deleted file mode 100644
index e69de29..0000000
--- a/mling/res/template_0.2/flat/Cargo.toml.tmpl
+++ /dev/null
diff --git a/mling/res/template_0.2/flat/src/main.rs b/mling/res/template_0.2/flat/src/main.rs
deleted file mode 100644
index e69de29..0000000
--- a/mling/res/template_0.2/flat/src/main.rs
+++ /dev/null
diff --git a/mling/res/template_0.2/modularity/Cargo.toml.tmpl b/mling/res/template_0.2/modularity/Cargo.toml.tmpl
deleted file mode 100644
index e69de29..0000000
--- a/mling/res/template_0.2/modularity/Cargo.toml.tmpl
+++ /dev/null
diff --git a/mling/res/template_0.2/modularity/src/main.rs b/mling/res/template_0.2/modularity/src/main.rs
deleted file mode 100644
index e69de29..0000000
--- a/mling/res/template_0.2/modularity/src/main.rs
+++ /dev/null
diff --git a/mling/src/bin/cargo-mling.rs b/mling/src/bin/cargo-mling.rs
deleted file mode 100644
index 4c285c6..0000000
--- a/mling/src/bin/cargo-mling.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-fn main() {
- mingling_cli::cli::run();
-}
diff --git a/mling/src/bin/mling.rs b/mling/src/bin/mling.rs
deleted file mode 100644
index 4c285c6..0000000
--- a/mling/src/bin/mling.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-fn main() {
- mingling_cli::cli::run();
-}
diff --git a/mling/src/cargo_style.rs b/mling/src/cargo_style.rs
deleted file mode 100644
index c212094..0000000
--- a/mling/src/cargo_style.rs
+++ /dev/null
@@ -1,221 +0,0 @@
-use colored::Colorize;
-
-/// Formats a message in cargo-style format with a bold green prefix.
-///
-/// The message should be in the format `prefix: content`. The prefix will be
-/// bold green and right-padded to 12 characters. If there is no colon in the
-/// string, the entire string is printed as content with an empty prefix.
-///
-/// # Macros
-///
-/// - `format_cargo!("prefix: {}", arg)` — format-style invocation
-/// - `format_cargo!(expr)` — direct expression invocation
-///
-/// # Panics
-///
-/// Panics if the prefix (text before the first `:`) exceeds 12 characters.
-///
-/// # Examples
-///
-/// ```ignore
-/// format_cargo!("Compiling: hello.rs");
-/// // Output: " Compiling hello.rs" (green bold "Compiling" padded to 12)
-/// ```
-#[macro_export]
-macro_rules! format_cargo {
- ($fmt:literal, $($arg:tt)*) => {
- $crate::get_cargo_info_format(format!($fmt, $($arg)*))
- };
- ($cmd:expr) => {
- $crate::get_cargo_info_format($cmd)
- };
-}
-
-/// Formats an error message in cargo-style format with a bold red "error" prefix.
-///
-/// # Macros
-///
-/// - `eformat_cargo!("prefix: {}", arg)` — format-style invocation
-/// - `eformat_cargo!(expr)` — direct expression invocation
-///
-/// # Examples
-///
-/// ```ignore
-/// eformat_cargo!("failed to parse input");
-/// // Output: "error: failed to parse input" (red bold "error")
-/// ```
-#[macro_export]
-macro_rules! eformat_cargo {
- ($fmt:literal, $($arg:tt)*) => {
- $crate::get_cargo_error_format(format!($fmt, $($arg)*))
- };
- ($cmd:expr) => {
- $crate::get_cargo_error_format($cmd)
- };
-}
-
-/// Formats a help message in cargo-style format with a bright white "help" prefix.
-///
-/// # Macros
-///
-/// - `hformat_cargo!("prefix: {}", arg)` — format-style invocation
-/// - `hformat_cargo!(expr)` — direct expression invocation
-///
-/// # Examples
-///
-/// ```ignore
-/// hformat_cargo!("use --verbose for more info");
-/// // Output: "help: use --verbose for more info" (bright white "help")
-/// ```
-#[macro_export]
-macro_rules! hformat_cargo {
- ($fmt:literal, $($arg:tt)*) => {
- $crate::get_cargo_help_format(format!($fmt, $($arg)*))
- };
- ($cmd:expr) => {
- $crate::get_cargo_help_format($cmd)
- };
-}
-
-/// Print a message in cargo-style format with a bold green prefix.
-///
-/// # Macros
-///
-/// - `println_cargo!("prefix: {}", arg)` — format-style invocation
-/// - `println_cargo!(expr)` — direct expression invocation
-///
-/// # Examples
-///
-/// ```ignore
-/// println_cargo!("Compiling: hello.rs");
-/// ```
-#[macro_export]
-macro_rules! println_cargo {
- ($fmt:literal, $($arg:tt)*) => {
- println!("{}", $crate::get_cargo_info_format(format!($fmt, $($arg)*)))
- };
- ($cmd:expr) => {
- println!("{}", $crate::get_cargo_info_format($cmd))
- };
-}
-
-/// Print an error message in cargo-style format with a bold red "error" prefix.
-///
-/// # Macros
-///
-/// - `eprintln_cargo!("prefix: {}", arg)` — format-style invocation
-/// - `eprintln_cargo!(expr)` — direct expression invocation
-///
-/// # Examples
-///
-/// ```ignore
-/// eprintln_cargo!("failed to parse input");
-/// ```
-#[macro_export]
-macro_rules! eprintln_cargo {
- ($fmt:literal, $($arg:tt)*) => {
- eprintln!("{}", $crate::get_cargo_error_format(format!($fmt, $($arg)*)))
- };
- ($cmd:expr) => {
- eprintln!("{}", $crate::get_cargo_error_format($cmd))
- };
-}
-
-/// Print a help message in cargo-style format with a bright white "help" prefix.
-///
-/// # Macros
-///
-/// - `hprintln_cargo!("prefix: {}", arg)` — format-style invocation
-/// - `hprintln_cargo!(expr)` — direct expression invocation
-///
-/// # Examples
-///
-/// ```ignore
-/// hprintln_cargo!("use --verbose for more info");
-/// ```
-#[macro_export]
-macro_rules! hprintln_cargo {
- ($fmt:literal, $($arg:tt)*) => {
- println!("{}", $crate::get_cargo_help_format(format!($fmt, $($arg)*)))
- };
- ($cmd:expr) => {
- println!("{}", $crate::get_cargo_help_format($cmd))
- };
-}
-
-/// Format a message in cargo style format, with bold green prefix.
-///
-/// The input string is split at the first `:`. The part before the colon becomes
-/// the prefix (bold green, right-padded to 12 characters), and the part after
-/// becomes the content.
-///
-/// If no colon is found, the entire string is treated as content and no prefix
-/// is shown.
-///
-/// # Panics
-///
-/// Panics if the prefix (text before the first `:`) exceeds 12 characters.
-///
-/// # Examples
-///
-/// ```ignore
-/// get_cargo_info_format("Compiling: my_program.rs");
-/// // returns " Compiling my_program.rs"
-/// ```
-pub fn get_cargo_info_format(str: impl Into<String>) -> String {
- let s = str.into();
- let (prefix, content) = if let Some(pos) = s.find(':') {
- (
- s[..pos].trim().to_string(),
- s[pos + 1..].trim_start().to_string(),
- )
- } else {
- (String::new(), s.trim().to_string())
- };
-
- assert!(
- prefix.len() <= 12,
- "prefix length exceeds 12: '{}' has length {}",
- prefix,
- prefix.len()
- );
-
- let padding = " ".repeat(12 - prefix.len());
-
- format!(
- "{}{} {}",
- padding,
- prefix.bold().bright_green(),
- content.trim()
- )
-}
-
-/// Format an error message in cargo style format, with bold red "error" prefix.
-///
-/// The input string is printed as the error content, prefixed by a bold red
-/// `error:` label.
-///
-/// # Examples
-///
-/// ```ignore
-/// get_cargo_error_format("something went wrong");
-/// // returns "error: something went wrong"
-/// ```
-pub fn get_cargo_error_format(str: impl Into<String>) -> String {
- format!("{}: {}", "error".bold().bright_red(), str.into())
-}
-
-/// Format a help message in cargo style format, with bright white "help" prefix.
-///
-/// The input string is printed as the help content, prefixed by a bright white
-/// `help:` label (not bold).
-///
-/// # Examples
-///
-/// ```ignore
-/// get_cargo_help_format("use --verbose for more info");
-/// // returns "help: use --verbose for more info"
-/// ```
-pub fn get_cargo_help_format(str: impl Into<String>) -> String {
- format!("{}: {}", "help".bright_white(), str.into())
-}
diff --git a/mling/src/cli.rs b/mling/src/cli.rs
deleted file mode 100644
index 67d2ef0..0000000
--- a/mling/src/cli.rs
+++ /dev/null
@@ -1,195 +0,0 @@
-use crate::{
- CMDCompletion, ErrorDispatcherNotFound, Next, ThisProgram,
- display::markdown,
- eformat_cargo, eprintln_cargo, hformat_cargo,
- pkg_mgr::{CMDInstall, CMDListNamespace, CMDRemoveNamespace, PackageManagerSetup},
- proj_mgr::ProjectManagerSetup,
- res::{ResCurrentDir, ResManifestPath},
-};
-use colored::Colorize;
-use mingling::{
- Groupped, Program, RenderResult,
- hook::ProgramHook,
- macros::{chain, help, pack, program_setup, renderer},
- res::ResExitCode,
- setup::{ExitCodeSetup, HelpFlagSetup, QuietFlagSetup, StructuralRendererSetup},
-};
-use std::{env::current_dir, io::Write, path::PathBuf, process::exit, str::FromStr};
-
-pub fn run() {
- #[cfg(windows)]
- colored::control::set_virtual_terminal(true).unwrap();
-
- // Preprocess args to handle cargo-mling invocations
- let mut args: Vec<String> = std::env::args().collect();
- if args.first().is_some_and(|a| a.contains("cargo-mling")) {
- args[0] = "cargo-mling".to_string();
- }
- if args.get(1).is_some_and(|a| a == "mling") {
- args.remove(1);
- }
-
- // Build program with preprocessed args
- let mut program = Program::<ThisProgram>::new_with_args(args);
-
- // Intercept Version
- program.global_flag(["-V", "--version"], |_| {
- eprintln!(include_str!("helps/version.txt"));
- exit(0)
- });
-
- // Intercept Help
- program.with_hook(
- ProgramHook::empty().on_post_dispatch(|info| match info.entry {
- // When dispatcher is not found
- ThisProgram::ErrorDispatcherNotFound
- // And user requests Help
- if ThisProgram::this().user_context.help => {
- // Print help
- eprintln!("{}", markdown(include_str!("helps/mling_help.txt")));
- exit(0)
- }
- _ => {}
- }),
- );
-
- // Commands
- program.with_dispatcher(CMDCompletion);
-
- // Setups
- program.with_setup(HelpFlagSetup::new(["-h", "--help"]));
- program.with_setup(StructuralRendererSetup);
- program.with_setup(ExitCodeSetup::default());
-
- program.with_setup(StandardOutputSetup);
- program.with_setup(PackageManagerSetup);
- program.with_setup(ProjectManagerSetup);
-
- // Resources
- program.with_resource(ResCurrentDir {
- path: current_dir().unwrap(),
- });
-
- let manifest_path = program.pick_global_argument(["-P", "--manifest-path"]);
- program.with_resource(ResManifestPath {
- raw: manifest_path,
- resolved: None,
- });
-
- // Manifest Path Check
- if !program.is_completing() {
- program.with_hook(ProgramHook::empty().on_post_dispatch(|_| {
- let p = ThisProgram::this();
- p.modify_res(|manifest_path: &mut ResManifestPath| {
- manifest_path.resolved = Some(resolve_manifest_path(manifest_path.raw.clone()));
- });
- }));
- }
-
- // Execute
- let quiet = program.stdout_setting.quiet;
- let error_output = program.stdout_setting.error_output && !quiet;
- let render_output = program.stdout_setting.render_output && !quiet;
- let result = program.exec_without_render().unwrap();
- if !result.is_empty() {
- if result.exit_code == 0 && render_output {
- println!("{}", result.trim());
- } else if error_output {
- eprintln!("{}", result.trim());
- }
- }
- exit(result.exit_code);
-}
-
-#[program_setup]
-fn standard_output_setup(program: &mut Program<ThisProgram>) {
- program.with_setup(QuietFlagSetup::new("--silence"));
- program.global_flag(["--no-error"], |program| {
- program.stdout_setting.error_output = false;
- });
- program.global_flag(["--no-result"], |program| {
- program.stdout_setting.render_output = false;
- });
- program.global_flag(["--silence", "--quiet"], |program| {
- program.stdout_setting.quiet = true;
- });
-}
-
-fn resolve_manifest_path(provided: Option<String>) -> PathBuf {
- let p = ThisProgram::this();
- let disable_error_output =
- // --no-error
- !p.stdout_setting.error_output ||
- // or --quiet/--silence
- p.stdout_setting.quiet;
-
- if let Some(path) = provided {
- let p = PathBuf::from_str(&path).unwrap();
- if p.is_dir() {
- let candidate = p.join("Cargo.toml");
- if candidate.exists() {
- return candidate;
- }
- if !disable_error_output {
- eprintln_cargo!("`{}` is not a crate root", p.display());
- }
- exit(1);
- }
- return p;
- }
- // Walk up from current directory to find nearest Cargo.toml
- let mut dir = current_dir().unwrap();
- loop {
- let candidate = dir.join("Cargo.toml");
- if candidate.exists() {
- return candidate;
- }
- if !dir.pop() {
- // Reached filesystem root without finding Cargo.toml
- if !disable_error_output {
- eprintln_cargo!("`{}` is not a crate root", current_dir().unwrap().display());
- }
- exit(1);
- }
- }
-}
-
-pack!(ResultMlingHelp = ());
-pack!(ResultUnknownCommand = String);
-
-#[chain]
-pub fn handle_error_dispatcher_not_found(err: ErrorDispatcherNotFound) -> Next {
- if err.is_empty() {
- ResultMlingHelp::default().to_render()
- } else {
- ResultUnknownCommand::new(err.join(" ")).to_render()
- }
-}
-
-#[renderer]
-pub fn render_mling_help(_prev: ResultMlingHelp, ec: &mut ResExitCode) -> RenderResult {
- let mut result = RenderResult::default();
- writeln!(result, "{}", markdown(include_str!("helps/mling_help.txt"))).ok();
- ec.exit_code = 0;
- result
-}
-
-#[renderer]
-pub fn render_unknown_command(prev: ResultUnknownCommand, ec: &mut ResExitCode) -> RenderResult {
- let mut result = RenderResult::default();
- writeln!(
- result,
- "{}",
- eformat_cargo!("no such command: `{}`", prev.bright_yellow().bold())
- )
- .ok();
- writeln!(result).ok();
- writeln!(
- result,
- "{}",
- hformat_cargo!("view all commands with `cargo help mling`")
- )
- .ok();
- ec.exit_code = 101;
- result
-}
diff --git a/mling/src/display.rs b/mling/src/display.rs
deleted file mode 100644
index 313a054..0000000
--- a/mling/src/display.rs
+++ /dev/null
@@ -1,384 +0,0 @@
-use colored::Colorize;
-use std::collections::VecDeque;
-
-/// Trait for adding markdown formatting to strings
-pub trait Markdown {
- fn markdown(&self) -> String;
-}
-
-impl Markdown for &str {
- fn markdown(&self) -> String {
- markdown(self)
- }
-}
-
-impl Markdown for String {
- fn markdown(&self) -> String {
- markdown(self)
- }
-}
-
-/// Converts a string to colored/formatted text with ANSI escape codes.
-///
-/// Supported syntax:
-/// - Bold: `**text**`
-/// - Italic: `*text*`
-/// - Underline: `_text_`
-/// - Angle-bracketed content: `<text>` (displayed as cyan)
-/// - Inline code: `` `text` `` (displayed as green)
-/// - Color tags: `[[color_name]]` and `[[/]]` to close color
-/// - Escape characters: `\*`, `\<`, `\>`, `` \` ``, `\_` for literal characters
-/// - Headings: `# Heading 1`, `## Heading 2`, up to `###### Heading 6`
-/// - Blockquote: `> text` (displays a gray background marker at the beginning of the line)
-///
-/// Color tags support the following color names:
-/// Color tags support the following color names:
-///
-/// | Type | Color Names |
-/// |-------------------------|-----------------------------------------------------------------------------|
-/// | Standard colors | `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` |
-/// | Bright colors | `bright_black` |
-/// | | `bright_red` |
-/// | | `bright_green` |
-/// | | `bright_yellow` |
-/// | | `bright_blue` |
-/// | | `bright_magenta` |
-/// | | `bright_cyan` |
-/// | | `bright_white` |
-/// | Bright color shorthands | `b_black` |
-/// | | `b_red` |
-/// | | `b_green` |
-/// | | `b_yellow` |
-/// | | `b_blue` |
-/// | | `b_magenta` |
-/// | | `b_cyan` |
-/// | | `b_white` |
-/// | Gray colors | `gray`/`grey` |
-/// | | `bright_gray`/`bright_grey` |
-/// | | `b_gray`/`b_grey` |
-///
-/// Color tags can be nested, `[[/]]` will close the most recently opened color tag.
-///
-/// # Arguments
-/// * `text` - The text to format, can be any type that implements `AsRef<str>`
-///
-/// # Returns
-/// Returns a `String` containing ANSI escape codes that can display colored/formatted text in ANSI-supported terminals.
-///
-/// # Examples
-/// ```
-/// # use mingling_cli::display::markdown;
-/// let formatted = markdown("Hello **world**!");
-/// println!("{}", formatted);
-///
-/// let colored = markdown("[[red]]Red text[[/]] and normal text");
-/// println!("{}", colored);
-///
-/// let nested = markdown("[[blue]]Blue [[green]]Green[[/]] Blue[[/]] normal");
-/// println!("{}", nested);
-/// ```
-pub fn markdown(text: impl AsRef<str>) -> String {
- let text = text.as_ref();
- let lines: Vec<&str> = text.lines().collect();
- let mut result = String::new();
- let mut content_indent = 0;
-
- for line in lines {
- // Don't trim the line initially, we need to check if it starts with #
- let trimmed_line = line.trim();
- let mut line_result = String::new();
-
- // Check if line starts with # for heading
- // Check if the original line (not trimmed) starts with #
- if line.trim_start().starts_with('#') {
- let chars: Vec<char> = line.trim_start().chars().collect();
- let mut level = 0;
-
- // Count # characters at the beginning
- while level < chars.len() && level < 7 && chars[level] == '#' {
- level += 1;
- }
-
- // Cap level at 6
- let effective_level = if level > 6 { 6 } else { level };
-
- // Skip # characters and any whitespace after them
- let mut content_start = level;
- while content_start < chars.len() && chars[content_start].is_whitespace() {
- content_start += 1;
- }
-
- // Extract heading content
- let heading_content: String = if content_start < chars.len() {
- chars[content_start..].iter().collect()
- } else {
- String::new()
- };
-
- // Process the heading content with formatting
- let processed_content = process_line(&heading_content);
-
- // Format heading as white background, black text, bold
- // ANSI codes: \x1b[1m for bold, \x1b[47m for white background, \x1b[30m for black text
- let formatted_heading = format!("\x1b[1m\x1b[47m\x1b[30m {processed_content} \x1b[0m");
-
- // Add indentation to the heading line itself
- // Heading indentation = level - 1
- let heading_indent = if effective_level > 0 {
- effective_level - 1
- } else {
- 0
- };
- let indent = " ".repeat(heading_indent);
- line_result.push_str(&indent);
- line_result.push_str(&formatted_heading);
-
- // Update content indent level for subsequent content
- // Content after heading should be indented by effective_level
- content_indent = effective_level;
- } else if !trimmed_line.is_empty() {
- // Process regular line with existing formatting
- let processed_line = process_line_with_quote(trimmed_line);
-
- // Add indentation based on content_indent
- let indent = " ".repeat(content_indent);
- line_result.push_str(&indent);
- line_result.push_str(&processed_line);
- } else {
- line_result.push(' ');
- }
-
- if !line_result.is_empty() {
- result.push_str(&line_result);
- result.push('\n');
- }
- }
-
- // Remove trailing newline
- if result.ends_with('\n') {
- result.pop();
- }
-
- result
-}
-
-// Helper function to process a single line with existing formatting and handle > quotes
-fn process_line_with_quote(line: &str) -> String {
- let chars: Vec<char> = line.chars().collect();
-
- // Check if line starts with '>' and not escaped
- if !chars.is_empty() && chars[0] == '>' {
- // Check if it's escaped
- if chars.len() > 1 && chars[1] == '\\' {
- // It's \>, so treat as normal text starting from position 0
- return process_line(line);
- }
-
- // It's a regular > at the beginning, replace with gray background gray text space
- let gray_bg_space = "\x1b[48;5;242m\x1b[38;5;242m \x1b[0m";
- let rest_of_line = if chars.len() > 1 {
- chars[1..].iter().collect::<String>()
- } else {
- String::new()
- };
-
- // Process the rest of the line normally
- let processed_rest = process_line(&rest_of_line);
-
- // Combine the gray background space with the processed rest
- format!("{gray_bg_space}{processed_rest}")
- } else {
- // No > at the beginning, process normally
- process_line(line)
- }
-}
-
-// Helper function to process a single line with existing formatting
-fn process_line(line: &str) -> String {
- let mut result = String::new();
- let mut color_stack: VecDeque<String> = VecDeque::new();
-
- let chars: Vec<char> = line.chars().collect();
- let mut i = 0;
-
- while i < chars.len() {
- // Check for escape character \
- if chars[i] == '\\' && i + 1 < chars.len() {
- let escaped_char = chars[i + 1];
- // Only escape specific characters
- if matches!(escaped_char, '*' | '<' | '>' | '`' | '_') {
- let mut escaped_text = escaped_char.to_string();
- apply_color_stack(&mut escaped_text, &color_stack);
- result.push_str(&escaped_text);
- i += 2;
- continue;
- }
- }
-
- // Check for color tag start [[color]]
- if i + 1 < chars.len()
- && chars[i] == '['
- && chars[i + 1] == '['
- && let Some(end) = find_tag_end(&chars, i)
- {
- let tag_content: String = chars[i + 2..end].iter().collect();
-
- // Check if it's a closing tag [[/]]
- if tag_content == "/" {
- color_stack.pop_back();
- } else {
- // It's a color tag
- color_stack.push_back(tag_content.clone());
- }
- i = end + 2;
- continue;
- }
-
- // Check for bold **text**
- if i + 1 < chars.len()
- && chars[i] == '*'
- && chars[i + 1] == '*'
- && let Some(end) = find_matching(&chars, i + 2, "**")
- {
- let bold_text: String = chars[i + 2..end].iter().collect();
- let mut formatted_text = bold_text.bold().to_string();
- apply_color_stack(&mut formatted_text, &color_stack);
- result.push_str(&formatted_text);
- i = end + 2;
- continue;
- }
-
- // Check for italic *text*
- if chars[i] == '*'
- && let Some(end) = find_matching(&chars, i + 1, "*")
- {
- let italic_text: String = chars[i + 1..end].iter().collect();
- let mut formatted_text = italic_text.italic().to_string();
- apply_color_stack(&mut formatted_text, &color_stack);
- result.push_str(&formatted_text);
- i = end + 1;
- continue;
- }
-
- // Check for underline _text_
- if chars[i] == '_'
- && let Some(end) = find_matching(&chars, i + 1, "_")
- {
- let underline_text: String = chars[i + 1..end].iter().collect();
- let mut formatted_text = format!("\x1b[4m{underline_text}\x1b[0m");
- apply_color_stack(&mut formatted_text, &color_stack);
- result.push_str(&formatted_text);
- i = end + 1;
- continue;
- }
-
- // Check for angle-bracketed content <text>
- if chars[i] == '<'
- && let Some(end) = find_matching(&chars, i + 1, ">")
- {
- // Include the angle brackets in the output
- let angle_text: String = chars[i..=end].iter().collect();
- let mut formatted_text = angle_text.cyan().to_string();
- apply_color_stack(&mut formatted_text, &color_stack);
- result.push_str(&formatted_text);
- i = end + 1;
- continue;
- }
-
- // Check for inline code `text`
- if chars[i] == '`'
- && let Some(end) = find_matching(&chars, i + 1, "`")
- {
- // Include the backticks in the output
- let code_text: String = chars[i..=end].iter().collect();
- let mut formatted_text = code_text.green().to_string();
- apply_color_stack(&mut formatted_text, &color_stack);
- result.push_str(&formatted_text);
- i = end + 1;
- continue;
- }
-
- // Regular character
- let mut current_char = chars[i].to_string();
- apply_color_stack(&mut current_char, &color_stack);
- result.push_str(&current_char);
- i += 1;
- }
-
- result
-}
-
-// Helper function to find matching delimiter
-fn find_matching(chars: &[char], start: usize, delimiter: &str) -> Option<usize> {
- let delim_chars: Vec<char> = delimiter.chars().collect();
- let delim_len = delim_chars.len();
-
- let mut j = start;
- while j < chars.len() {
- if delim_len == 1 {
- if chars[j] == delim_chars[0] {
- return Some(j);
- }
- } else if j + 1 < chars.len()
- && chars[j] == delim_chars[0]
- && chars[j + 1] == delim_chars[1]
- {
- return Some(j);
- }
- j += 1;
- }
- None
-}
-
-// Helper function to find color tag end
-fn find_tag_end(chars: &[char], start: usize) -> Option<usize> {
- let mut j = start + 2;
- while j + 1 < chars.len() {
- if chars[j] == ']' && chars[j + 1] == ']' {
- return Some(j);
- }
- j += 1;
- }
- None
-}
-
-// Helper function to apply color stack to text
-fn apply_color_stack(text: &mut String, color_stack: &VecDeque<String>) {
- let mut result = text.clone();
- for color in color_stack.iter().rev() {
- result = apply_color(&result, color);
- }
- *text = result;
-}
-
-// Helper function to apply color to text
-fn apply_color(text: impl AsRef<str>, color_name: impl AsRef<str>) -> String {
- let text = text.as_ref();
- let color_name = color_name.as_ref();
- match color_name {
- // Normal colors
- "black" => text.black().to_string(),
- "red" => text.red().to_string(),
- "green" => text.green().to_string(),
- "yellow" => text.yellow().to_string(),
- "blue" => text.blue().to_string(),
- "magenta" => text.magenta().to_string(),
- "cyan" => text.cyan().to_string(),
- "white" | "b_white" | "bright_gray" | "bright_grey" | "b_gray" | "b_grey" => {
- text.white().to_string()
- }
-
- // Bright colors and their b_ short aliases
- "bright_black" | "b_black" | "gray" | "grey" => text.bright_black().to_string(),
- "bright_red" | "b_red" => text.bright_red().to_string(),
- "bright_green" | "b_green" => text.bright_green().to_string(),
- "bright_yellow" | "b_yellow" => text.bright_yellow().to_string(),
- "bright_blue" | "b_blue" => text.bright_blue().to_string(),
- "bright_magenta" | "b_magenta" => text.bright_magenta().to_string(),
- "bright_cyan" | "b_cyan" => text.bright_cyan().to_string(),
- "bright_white" => text.bright_white().to_string(),
-
- // Default to white if color not recognized
- _ => text.to_string(),
- }
-}
diff --git a/mling/src/errors.rs b/mling/src/errors.rs
deleted file mode 100644
index ece80ce..0000000
--- a/mling/src/errors.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-mod io_error;
-pub use io_error::*;
diff --git a/mling/src/errors/io_error.rs b/mling/src/errors/io_error.rs
deleted file mode 100644
index ac503cc..0000000
--- a/mling/src/errors/io_error.rs
+++ /dev/null
@@ -1,224 +0,0 @@
-use mingling::{
- RenderResult,
- macros::{group, renderer},
- res::ResExitCode,
-};
-use std::io::Write as _;
-
-use crate::eformat_cargo;
-
-group!(ErrorIo = std::io::Error);
-
-// Error code constants for each std::io::ErrorKind variant
-pub const EC_IO_ERR_NOT_FOUND: i32 = 1000;
-pub const EC_IO_ERR_PERMISSION_DENIED: i32 = 1001;
-pub const EC_IO_ERR_CONNECTION_REFUSED: i32 = 1002;
-pub const EC_IO_ERR_CONNECTION_RESET: i32 = 1003;
-pub const EC_IO_ERR_HOST_UNREACHABLE: i32 = 1004;
-pub const EC_IO_ERR_NETWORK_UNREACHABLE: i32 = 1005;
-pub const EC_IO_ERR_CONNECTION_ABORTED: i32 = 1006;
-pub const EC_IO_ERR_NOT_CONNECTED: i32 = 1007;
-pub const EC_IO_ERR_ADDR_IN_USE: i32 = 1008;
-pub const EC_IO_ERR_ADDR_NOT_AVAILABLE: i32 = 1009;
-pub const EC_IO_ERR_NETWORK_DOWN: i32 = 1010;
-pub const EC_IO_ERR_BROKEN_PIPE: i32 = 1011;
-pub const EC_IO_ERR_ALREADY_EXISTS: i32 = 1012;
-pub const EC_IO_ERR_WOULD_BLOCK: i32 = 1013;
-pub const EC_IO_ERR_NOT_A_DIRECTORY: i32 = 1014;
-pub const EC_IO_ERR_IS_A_DIRECTORY: i32 = 1015;
-pub const EC_IO_ERR_DIRECTORY_NOT_EMPTY: i32 = 1016;
-pub const EC_IO_ERR_READ_ONLY_FILESYSTEM: i32 = 1017;
-pub const EC_IO_ERR_STALE_NETWORK_FILE_HANDLE: i32 = 1018;
-pub const EC_IO_ERR_INVALID_INPUT: i32 = 1019;
-pub const EC_IO_ERR_INVALID_DATA: i32 = 1020;
-pub const EC_IO_ERR_TIMED_OUT: i32 = 1021;
-pub const EC_IO_ERR_WRITE_ZERO: i32 = 1022;
-pub const EC_IO_ERR_STORAGE_FULL: i32 = 1023;
-pub const EC_IO_ERR_NOT_SEEKABLE: i32 = 1024;
-pub const EC_IO_ERR_QUOTA_EXCEEDED: i32 = 1025;
-pub const EC_IO_ERR_FILE_TOO_LARGE: i32 = 1026;
-pub const EC_IO_ERR_RESOURCE_BUSY: i32 = 1027;
-pub const EC_IO_ERR_EXECUTABLE_FILE_BUSY: i32 = 1028;
-pub const EC_IO_ERR_DEADLOCK: i32 = 1029;
-pub const EC_IO_ERR_CROSSES_DEVICES: i32 = 1030;
-pub const EC_IO_ERR_TOO_MANY_LINKS: i32 = 1031;
-pub const EC_IO_ERR_INVALID_FILENAME: i32 = 1032;
-pub const EC_IO_ERR_ARGUMENT_LIST_TOO_LONG: i32 = 1033;
-pub const EC_IO_ERR_INTERRUPTED: i32 = 1034;
-pub const EC_IO_ERR_UNSUPPORTED: i32 = 1035;
-pub const EC_IO_ERR_UNEXPECTED_EOF: i32 = 1036;
-pub const EC_IO_ERR_OUT_OF_MEMORY: i32 = 1037;
-pub const EC_IO_ERR_OTHER: i32 = 1038;
-
-#[renderer]
-pub fn render_error_io(err: ErrorIo, ec: &mut ResExitCode) -> RenderResult {
- let mut result = RenderResult::default();
- match err.kind() {
- std::io::ErrorKind::NotFound => {
- writeln!(result, "{}", eformat_cargo!("file or directory not found")).ok();
- ec.exit_code = EC_IO_ERR_NOT_FOUND;
- }
- std::io::ErrorKind::PermissionDenied => {
- writeln!(result, "{}", eformat_cargo!("permission denied")).ok();
- ec.exit_code = EC_IO_ERR_PERMISSION_DENIED;
- }
- std::io::ErrorKind::ConnectionRefused => {
- writeln!(result, "{}", eformat_cargo!("connection refused")).ok();
- ec.exit_code = EC_IO_ERR_CONNECTION_REFUSED;
- }
- std::io::ErrorKind::ConnectionReset => {
- writeln!(result, "{}", eformat_cargo!("connection reset")).ok();
- ec.exit_code = EC_IO_ERR_CONNECTION_RESET;
- }
- std::io::ErrorKind::HostUnreachable => {
- writeln!(result, "{}", eformat_cargo!("host unreachable")).ok();
- ec.exit_code = EC_IO_ERR_HOST_UNREACHABLE;
- }
- std::io::ErrorKind::NetworkUnreachable => {
- writeln!(result, "{}", eformat_cargo!("network unreachable")).ok();
- ec.exit_code = EC_IO_ERR_NETWORK_UNREACHABLE;
- }
- std::io::ErrorKind::ConnectionAborted => {
- writeln!(result, "{}", eformat_cargo!("connection aborted")).ok();
- ec.exit_code = EC_IO_ERR_CONNECTION_ABORTED;
- }
- std::io::ErrorKind::NotConnected => {
- writeln!(result, "{}", eformat_cargo!("not connected")).ok();
- ec.exit_code = EC_IO_ERR_NOT_CONNECTED;
- }
- std::io::ErrorKind::AddrInUse => {
- writeln!(result, "{}", eformat_cargo!("address in use")).ok();
- ec.exit_code = EC_IO_ERR_ADDR_IN_USE;
- }
- std::io::ErrorKind::AddrNotAvailable => {
- writeln!(result, "{}", eformat_cargo!("address not available")).ok();
- ec.exit_code = EC_IO_ERR_ADDR_NOT_AVAILABLE;
- }
- std::io::ErrorKind::NetworkDown => {
- writeln!(result, "{}", eformat_cargo!("network down")).ok();
- ec.exit_code = EC_IO_ERR_NETWORK_DOWN;
- }
- std::io::ErrorKind::BrokenPipe => {
- writeln!(result, "{}", eformat_cargo!("broken pipe")).ok();
- ec.exit_code = EC_IO_ERR_BROKEN_PIPE;
- }
- std::io::ErrorKind::AlreadyExists => {
- writeln!(
- result,
- "{}",
- eformat_cargo!("file or directory already exists")
- )
- .ok();
- ec.exit_code = EC_IO_ERR_ALREADY_EXISTS;
- }
- std::io::ErrorKind::WouldBlock => {
- writeln!(result, "{}", eformat_cargo!("operation would block")).ok();
- ec.exit_code = EC_IO_ERR_WOULD_BLOCK;
- }
- std::io::ErrorKind::NotADirectory => {
- writeln!(result, "{}", eformat_cargo!("not a directory")).ok();
- ec.exit_code = EC_IO_ERR_NOT_A_DIRECTORY;
- }
- std::io::ErrorKind::IsADirectory => {
- writeln!(result, "{}", eformat_cargo!("is a directory")).ok();
- ec.exit_code = EC_IO_ERR_IS_A_DIRECTORY;
- }
- std::io::ErrorKind::DirectoryNotEmpty => {
- writeln!(result, "{}", eformat_cargo!("directory not empty")).ok();
- ec.exit_code = EC_IO_ERR_DIRECTORY_NOT_EMPTY;
- }
- std::io::ErrorKind::ReadOnlyFilesystem => {
- writeln!(result, "{}", eformat_cargo!("read-only filesystem")).ok();
- ec.exit_code = EC_IO_ERR_READ_ONLY_FILESYSTEM;
- }
- std::io::ErrorKind::StaleNetworkFileHandle => {
- writeln!(result, "{}", eformat_cargo!("stale network file handle")).ok();
- ec.exit_code = EC_IO_ERR_STALE_NETWORK_FILE_HANDLE;
- }
- std::io::ErrorKind::InvalidInput => {
- writeln!(result, "{}", eformat_cargo!("invalid input")).ok();
- ec.exit_code = EC_IO_ERR_INVALID_INPUT;
- }
- std::io::ErrorKind::InvalidData => {
- writeln!(result, "{}", eformat_cargo!("invalid data")).ok();
- ec.exit_code = EC_IO_ERR_INVALID_DATA;
- }
- std::io::ErrorKind::TimedOut => {
- writeln!(result, "{}", eformat_cargo!("timed out")).ok();
- ec.exit_code = EC_IO_ERR_TIMED_OUT;
- }
- std::io::ErrorKind::WriteZero => {
- writeln!(result, "{}", eformat_cargo!("write zero")).ok();
- ec.exit_code = EC_IO_ERR_WRITE_ZERO;
- }
- std::io::ErrorKind::StorageFull => {
- writeln!(result, "{}", eformat_cargo!("storage full")).ok();
- ec.exit_code = EC_IO_ERR_STORAGE_FULL;
- }
- std::io::ErrorKind::NotSeekable => {
- writeln!(result, "{}", eformat_cargo!("not seekable")).ok();
- ec.exit_code = EC_IO_ERR_NOT_SEEKABLE;
- }
- std::io::ErrorKind::QuotaExceeded => {
- writeln!(result, "{}", eformat_cargo!("quota exceeded")).ok();
- ec.exit_code = EC_IO_ERR_QUOTA_EXCEEDED;
- }
- std::io::ErrorKind::FileTooLarge => {
- writeln!(result, "{}", eformat_cargo!("file too large")).ok();
- ec.exit_code = EC_IO_ERR_FILE_TOO_LARGE;
- }
- std::io::ErrorKind::ResourceBusy => {
- writeln!(result, "{}", eformat_cargo!("resource busy")).ok();
- ec.exit_code = EC_IO_ERR_RESOURCE_BUSY;
- }
- std::io::ErrorKind::ExecutableFileBusy => {
- writeln!(result, "{}", eformat_cargo!("executable file busy")).ok();
- ec.exit_code = EC_IO_ERR_EXECUTABLE_FILE_BUSY;
- }
- std::io::ErrorKind::Deadlock => {
- writeln!(result, "{}", eformat_cargo!("deadlock")).ok();
- ec.exit_code = EC_IO_ERR_DEADLOCK;
- }
- std::io::ErrorKind::CrossesDevices => {
- writeln!(result, "{}", eformat_cargo!("crosses devices")).ok();
- ec.exit_code = EC_IO_ERR_CROSSES_DEVICES;
- }
- std::io::ErrorKind::TooManyLinks => {
- writeln!(result, "{}", eformat_cargo!("too many links")).ok();
- ec.exit_code = EC_IO_ERR_TOO_MANY_LINKS;
- }
- std::io::ErrorKind::InvalidFilename => {
- writeln!(result, "{}", eformat_cargo!("invalid filename")).ok();
- ec.exit_code = EC_IO_ERR_INVALID_FILENAME;
- }
- std::io::ErrorKind::ArgumentListTooLong => {
- writeln!(result, "{}", eformat_cargo!("argument list too long")).ok();
- ec.exit_code = EC_IO_ERR_ARGUMENT_LIST_TOO_LONG;
- }
- std::io::ErrorKind::Interrupted => {
- writeln!(result, "{}", eformat_cargo!("interrupted")).ok();
- ec.exit_code = EC_IO_ERR_INTERRUPTED;
- }
- std::io::ErrorKind::Unsupported => {
- writeln!(result, "{}", eformat_cargo!("unsupported")).ok();
- ec.exit_code = EC_IO_ERR_UNSUPPORTED;
- }
- std::io::ErrorKind::UnexpectedEof => {
- writeln!(result, "{}", eformat_cargo!("unexpected end of file")).ok();
- ec.exit_code = EC_IO_ERR_UNEXPECTED_EOF;
- }
- std::io::ErrorKind::OutOfMemory => {
- writeln!(result, "{}", eformat_cargo!("out of memory")).ok();
- ec.exit_code = EC_IO_ERR_OUT_OF_MEMORY;
- }
- std::io::ErrorKind::Other => {
- writeln!(result, "{}", eformat_cargo!(err.to_string())).ok();
- ec.exit_code = EC_IO_ERR_OTHER;
- }
- _ => {
- writeln!(result, "{}", eformat_cargo!(err.to_string())).ok();
- ec.exit_code = EC_IO_ERR_OTHER;
- }
- }
- result
-}
diff --git a/mling/src/helps/mling_help.txt b/mling/src/helps/mling_help.txt
deleted file mode 100644
index 73bfd4d..0000000
--- a/mling/src/helps/mling_help.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-Mingling's scaffolding tool
-
-[[b_green]]**Usage:**[[/]] [[b_cyan]]**cargo mling**[[/]] [[cyan]][COMMAND] [OPTIONS]...[[/]]
-
-[[b_green]]**Options:**[[/]]
-__ [[b_cyan]]**-V**[[/]], [[b_cyan]]**--version**[[/]] Print version info and exit
-__ [[b_cyan]]**-h**[[/]], [[b_cyan]]**--help**[[/]] Print this help message
-
-__ [[b_cyan]]**-P**[[/]], [[b_cyan]]**--manifest-path**[[/]] [[cyan]]<PATH>[[/]] Path to _Cargo.toml_
-
-__ [[b_cyan]]**--silence**[[/]], [[b_cyan]]**--quiet**[[/]] Suppress all output
-__ [[b_cyan]]**--no-error**[[/]] Suppress error output
-__ [[b_cyan]]**--no-result**[[/]] Suppress result output
-
-[[b_green]]**Commands:**[[/]]
-__ [[b_cyan]]**install**[[/]] Install current project into
-__ the **mling** package manager
-__ [[b_cyan]]**ls**[[/]], [[b_cyan]]**show**[[/]], [[b_cyan]]**add**[[/]], [[b_cyan]]**rm**[[/]] List, show, add, or remove something
-
-Run \`[[b_cyan]]**cargo help mling**[[/]]\` for more detailed information.
diff --git a/mling/src/lib.rs b/mling/src/lib.rs
deleted file mode 100644
index 1de1228..0000000
--- a/mling/src/lib.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-#![allow(unused_imports)]
-
-use mingling::{
- macros::{chain, gen_program, pack, renderer},
- res::ResExitCode,
-};
-
-mod cargo_style;
-pub use cargo_style::*;
-
-pub mod cli;
-pub mod display;
-pub mod errors;
-pub mod pkg_mgr;
-pub mod proj_mgr;
-pub mod res;
-
-gen_program!();
diff --git a/mling/src/pkg_mgr/installer.rs b/mling/src/pkg_mgr/installer.rs
deleted file mode 100644
index 8b13789..0000000
--- a/mling/src/pkg_mgr/installer.rs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/mling/src/pkg_mgr/mod.rs b/mling/src/pkg_mgr/mod.rs
deleted file mode 100644
index 682b433..0000000
--- a/mling/src/pkg_mgr/mod.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use crate::ThisProgram;
-use mingling::{
- Program,
- macros::{dispatcher, program_setup},
-};
-
-pub mod installer;
-
-dispatcher!("install");
-dispatcher!("ls.namespace", CMDListNamespace => EntryListNamespace);
-dispatcher!("rm.namespace", CMDRemoveNamespace => EntryRemoveNamespace);
-
-#[program_setup]
-pub fn package_manager_setup(p: &mut Program<ThisProgram>) {
- p.with_dispatcher(CMDInstall);
- p.with_dispatcher(CMDListNamespace);
- p.with_dispatcher(CMDRemoveNamespace);
-}
diff --git a/mling/src/proj_mgr/checklist_reader.rs b/mling/src/proj_mgr/checklist_reader.rs
deleted file mode 100644
index 558d303..0000000
--- a/mling/src/proj_mgr/checklist_reader.rs
+++ /dev/null
@@ -1,294 +0,0 @@
-use std::{collections::HashMap, io, path::Path};
-
-/// Reads and parses a `CHECKLIST.md` file, extracting both *values* (from
-/// fenced code blocks) and *toggles* (from checkbox lines).
-///
-/// # Format
-///
-/// - **Values**: fenced code blocks where the info string is the key and
-/// the first non-empty line is the value. Example:
-/// <code>\`\`\`name<br>my-cli<br>\`\`\`</code>
-/// - **Toggles**: checkbox lines like `- [x] \`ns:key\`` (checked) or
-/// `- [ ] \`ns:key\`` (unchecked).
-///
-/// Checked items are stored as `"namespace:key"`.
-///
-/// # Usage
-///
-/// ```rust,ignore
-/// let reader = CheckListReader::from(&Path::new("CHECKLIST.md")).unwrap();
-/// assert_eq!(reader.read_value("name"), Some("my-cli".into()));
-/// assert!(reader.read_toggle("ver:0.2"));
-/// assert!(!reader.read_toggle("feat:comp"));
-/// assert_eq!(reader.read_toggles("feat"), vec!["feat:async"]);
-/// ```
-pub struct CheckListReader {
- /// Values extracted from fenced code blocks: `key → value`.
- values: HashMap<String, String>,
-
- /// Checked toggles: `"namespace:key" → true` (only checked items are stored).
- toggles: HashMap<String, bool>,
-
- /// All toggles (checked or not): `"namespace:key" → checked`.
- all_toggles: HashMap<String, bool>,
-}
-
-impl CheckListReader {
- /// Parse a CHECKLIST.md from a file path in a single left-to-right pass
- pub fn from(path: &Path) -> Result<Self, io::Error> {
- let content = std::fs::read_to_string(path)?;
-
- let mut reader = Self {
- values: HashMap::new(),
- toggles: HashMap::new(),
- all_toggles: HashMap::new(),
- };
- reader.parse(&content);
- Ok(reader)
- }
-
- /// Parse CHECKLIST.md content in a single pass.
- fn parse(&mut self, content: &str) {
- let lines: Vec<&str> = content.lines().collect();
- let mut i = 0;
-
- while i < lines.len() {
- let line = lines[i];
-
- if let Some(key) = line.strip_prefix("```").map(|s| s.trim())
- && !key.is_empty()
- && !key.starts_with(' ')
- {
- let mut block_lines = Vec::new();
- i += 1;
- while i < lines.len() && !lines[i].trim_start().starts_with("```") {
- block_lines.push(lines[i]);
- i += 1;
- }
- let value = block_lines
- .into_iter()
- .find(|l| !l.trim().is_empty())
- .map(|l| l.trim().to_string());
- if let Some(val) = value {
- self.values.insert(key.to_string(), val);
- }
- continue;
- }
-
- if let Some(toggle_key) = Self::parse_toggle_line(line) {
- let is_checked = line.contains("[x]") || line.contains("[X]");
- self.all_toggles.insert(toggle_key.clone(), is_checked);
- if is_checked {
- self.toggles.insert(toggle_key, true);
- }
- }
-
- i += 1;
- }
- }
-
- /// Extract the `namespace:key` from a toggle line.
- ///
- /// Matches: `- [x] `namespace:key`` or `- [ ] `namespace:key``
- fn parse_toggle_line(line: &str) -> Option<String> {
- let line = line.trim();
- if !line.starts_with("- [") {
- return None;
- }
- // Find the backtick-enclosed key
- let start = line.find('`')?;
- let rest = &line[start + 1..];
- let end = rest.find('`')?;
- let key = rest[..end].trim();
- if key.is_empty() {
- return None;
- }
- Some(key.to_string())
- }
-
- /// Read a value by its key.
- ///
- /// Returns `Some(value)` if a fenced code block with that key was found,
- /// or `None` if the key does not exist.
- #[must_use]
- pub fn read_value(&self, key: &str) -> Option<String> {
- self.values.get(key).cloned()
- }
-
- /// Check if a toggle (`namespace:key`) is checked.
- ///
- /// Returns `true` if the checkbox line was `- [x]`, `false` if it was
- /// `- [ ]` or the key does not exist in the file.
- #[must_use]
- pub fn read_toggle(&self, key: &str) -> bool {
- self.toggles.contains_key(key)
- }
-
- /// Get all toggles under a given `namespace` that are checked.
- ///
- /// For example, `read_toggles("feat")` returns all checked keys starting
- /// with `"feat:"`, such as `["feat:comp", "feat:parser"]`.
- #[must_use]
- pub fn read_toggles(&self, namespace: &str) -> Vec<String> {
- let prefix = format!("{namespace}:");
- let mut keys: Vec<String> = self
- .toggles
- .keys()
- .filter(|k| k.starts_with(&prefix))
- .cloned()
- .collect();
- keys.sort();
- keys
- }
-
- /// Get ALL toggles (checked or not) under a namespace.
- ///
- /// Useful for iterating all available options.
- #[must_use]
- pub fn all_toggles(&self, namespace: &str) -> Vec<String> {
- let prefix = format!("{namespace}:");
- let mut keys: Vec<String> = self
- .all_toggles
- .keys()
- .filter(|k| k.starts_with(&prefix))
- .cloned()
- .collect();
- keys.sort();
- keys
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn sample_md() -> &'static str {
- r#"> Some intro
-
-## Question 1: What is your project name?
-
-```name
-my-cli
-```
-
-## Question 2: Which version?
-
-- [x] `ver:0.2`
-
-## Question 3: Features?
-
-- [ ] `feat:structural_renderer`
-- [x] `feat:comp`
-- [x] `feat:parser`
-- [ ] `feat:async`
-
-```other
-some value
-```
-"#
- }
-
- #[test]
- fn test_read_value() {
- let mut reader = CheckListReader {
- values: HashMap::new(),
- toggles: HashMap::new(),
- all_toggles: HashMap::new(),
- };
- reader.parse(sample_md());
- assert_eq!(reader.read_value("name"), Some("my-cli".into()));
- assert_eq!(reader.read_value("other"), Some("some value".into()));
- assert_eq!(reader.read_value("nonexistent"), None);
- }
-
- #[test]
- fn test_read_toggle() {
- let mut reader = CheckListReader {
- values: HashMap::new(),
- toggles: HashMap::new(),
- all_toggles: HashMap::new(),
- };
- reader.parse(sample_md());
- assert!(reader.read_toggle("ver:0.2"));
- assert!(reader.read_toggle("feat:comp"));
- assert!(reader.read_toggle("feat:parser"));
- assert!(!reader.read_toggle("feat:structural_renderer"));
- assert!(!reader.read_toggle("feat:async"));
- assert!(!reader.read_toggle("nonexistent:key"));
- }
-
- #[test]
- fn test_read_toggles() {
- let mut reader = CheckListReader {
- values: HashMap::new(),
- toggles: HashMap::new(),
- all_toggles: HashMap::new(),
- };
- reader.parse(sample_md());
- let feat_toggles = reader.read_toggles("feat");
- assert_eq!(feat_toggles, vec!["feat:comp", "feat:parser"]);
- }
-
- #[test]
- fn test_all_toggles() {
- let mut reader = CheckListReader {
- values: HashMap::new(),
- toggles: HashMap::new(),
- all_toggles: HashMap::new(),
- };
- reader.parse(sample_md());
- let all = reader.all_toggles("feat");
- assert_eq!(
- all,
- vec![
- "feat:async",
- "feat:comp",
- "feat:parser",
- "feat:structural_renderer",
- ]
- );
- }
-
- #[test]
- fn test_from_path() {
- // Write a temp CHECKLIST.md, read it, then clean up
- let dir = std::env::temp_dir().join("mling_checklist_test");
- std::fs::create_dir_all(&dir).unwrap();
- let path = dir.join("CHECKLIST.md");
- std::fs::write(&path, sample_md()).unwrap();
-
- let reader = CheckListReader::from(&path).unwrap();
- assert_eq!(reader.read_value("name"), Some("my-cli".into()));
- assert!(reader.read_toggle("ver:0.2"));
-
- let _ = std::fs::remove_dir_all(&dir);
- }
-
- #[test]
- fn test_empty_file() {
- let mut reader = CheckListReader {
- values: HashMap::new(),
- toggles: HashMap::new(),
- all_toggles: HashMap::new(),
- };
- reader.parse("");
- assert_eq!(reader.read_value("anything"), None);
- assert!(!reader.read_toggle("ns:key"));
- assert!(reader.read_toggles("ns").is_empty());
- }
-
- #[test]
- fn test_no_toggle_or_value_lines() {
- let md = "# Just a heading\n\nSome text\n\n```code\nstill not a value\n```\n";
- let mut reader = CheckListReader {
- values: HashMap::new(),
- toggles: HashMap::new(),
- all_toggles: HashMap::new(),
- };
- reader.parse(md);
- // "code" isn't a valid value key (it's used as a language tag, not a name/value key)
- // but our parser treats ANY ```key as a value block. This is correct behavior —
- // malformed CHECKLIST.md may produce unexpected values.
- assert_eq!(reader.read_value("code"), Some("still not a value".into()));
- }
-}
diff --git a/mling/src/proj_mgr/generator.rs b/mling/src/proj_mgr/generator.rs
deleted file mode 100644
index 4f965d9..0000000
--- a/mling/src/proj_mgr/generator.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use std::path::{self, PathBuf};
-
-use mingling::{
- Groupped, RenderResult,
- macros::{chain, pack, renderer, route},
-};
-use std::io::Write as _;
-
-use crate::{Next, proj_mgr::EntryGenerateProject, res::ResCurrentDir};
-
-pack!(StateGenerateProjectReady = PathBuf);
-pack!(ResultGenerateProjectChecklistCreated = PathBuf);
-
-pack!(StateGenerateProjectExecBegin = PathBuf);
-pack!(StateGenerateProjectExecuting = ());
-
-const CHECK_LIST_NAME: &str = "CHECKLIST.md";
-const CHECK_LIST_CONTENT: &str = include_str!("../../res/CHECKLIST.md");
-
-#[chain]
-pub fn handle_generate(_args: EntryGenerateProject, cwd: &ResCurrentDir) -> Next {
- let checklist_path = cwd.path.join(CHECK_LIST_NAME);
-
- if !checklist_path.exists() {
- StateGenerateProjectReady::new(checklist_path).to_chain()
- } else {
- StateGenerateProjectExecBegin::new(checklist_path).to_chain()
- }
-}
-
-#[chain]
-pub fn handle_state_gen_proj_ready(prev: StateGenerateProjectReady) -> Next {
- let path = prev.inner;
- route!(std::fs::write(&path, CHECK_LIST_CONTENT));
- ResultGenerateProjectChecklistCreated::new(path).to_render()
-}
-
-#[renderer]
-pub fn render_gen_proj_checklist_created(
- result: ResultGenerateProjectChecklistCreated,
-) -> RenderResult {
- let mut res = RenderResult::default();
- writeln!(
- res,
- "Successfully create {} at \"{}\"",
- CHECK_LIST_NAME,
- result.to_string_lossy()
- )
- .ok();
- writeln!(res).ok();
- writeln!(
- res,
- "Please fill in {CHECK_LIST_NAME} and run `mling gen` again to continue generating"
- )
- .ok();
- res
-}
diff --git a/mling/src/proj_mgr/metadata.rs b/mling/src/proj_mgr/metadata.rs
deleted file mode 100644
index 1ba24e1..0000000
--- a/mling/src/proj_mgr/metadata.rs
+++ /dev/null
@@ -1,142 +0,0 @@
-use serde::Deserialize;
-use serde::Serialize;
-
-use std::path::PathBuf;
-use std::process::Command;
-
-/// Read cargo metadata by running `cargo metadata` with the given manifest path.
-pub fn read_metadata(cargo_toml: &PathBuf) -> Result<CargoLockFile, std::io::Error> {
- let output = Command::new("cargo")
- .arg("metadata")
- .arg("--format-version")
- .arg("1")
- .arg("--manifest-path")
- .arg(cargo_toml)
- .output()?;
-
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- return Err(std::io::Error::other(format!(
- "cargo metadata failed: {}",
- stderr
- )));
- }
-
- let lock_file: CargoLockFile = serde_json::from_slice(&output.stdout)
- .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
-
- Ok(lock_file)
-}
-
-/// A cargo metadata lock file that serde can serialize and deserialize.
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct CargoLockFile {
- pub packages: Vec<Package>,
- #[serde(rename = "workspace_members")]
- pub workspace_members: Vec<String>,
- #[serde(rename = "workspace_default_members")]
- pub workspace_default_members: Vec<String>,
- pub resolve: Resolve,
- #[serde(rename = "target_directory")]
- pub target_directory: String,
- #[serde(rename = "build_directory")]
- pub build_directory: String,
- pub version: u64,
- #[serde(rename = "workspace_root")]
- pub workspace_root: String,
- pub metadata: Option<serde_json::Value>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Package {
- pub name: String,
- pub version: String,
- pub id: String,
- pub license: Option<String>,
- #[serde(rename = "license_file")]
- pub license_file: Option<String>,
- pub description: Option<String>,
- pub source: Option<String>,
- pub dependencies: Vec<Dependency>,
- pub targets: Vec<Target>,
- pub features: std::collections::BTreeMap<String, Vec<String>>,
- #[serde(rename = "manifest_path")]
- pub manifest_path: String,
- pub metadata: Option<serde_json::Value>,
- pub publish: Option<serde_json::Value>,
- pub authors: Vec<String>,
- pub categories: Vec<String>,
- pub keywords: Vec<String>,
- pub readme: Option<String>,
- pub repository: Option<String>,
- pub homepage: Option<String>,
- pub documentation: Option<String>,
- pub edition: String,
- pub links: Option<String>,
- #[serde(rename = "default_run")]
- pub default_run: Option<String>,
- #[serde(rename = "rust_version")]
- pub rust_version: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Dependency {
- pub name: String,
- pub source: Option<String>,
- pub req: String,
- pub kind: Option<String>,
- pub rename: Option<String>,
- pub optional: bool,
- #[serde(rename = "uses_default_features")]
- pub uses_default_features: bool,
- pub features: Vec<String>,
- pub target: Option<String>,
- pub registry: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub path: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Target {
- pub kind: Vec<String>,
- #[serde(rename = "crate_types")]
- pub crate_types: Vec<String>,
- pub name: String,
- #[serde(rename = "src_path")]
- pub src_path: String,
- pub edition: String,
- pub doc: bool,
- pub doctest: bool,
- pub test: bool,
- #[serde(skip_serializing_if = "Option::is_none")]
- #[serde(rename = "required-features")]
- pub required_features: Option<Vec<String>>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Resolve {
- pub nodes: Vec<ResolveNode>,
- pub root: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct ResolveNode {
- pub id: String,
- pub dependencies: Vec<String>,
- pub deps: Vec<DepInfo>,
- pub features: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct DepInfo {
- pub name: String,
- pub pkg: String,
- #[serde(rename = "dep_kinds")]
- pub dep_kinds: Vec<DepKind>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct DepKind {
- pub kind: Option<String>,
- pub target: Option<String>,
-}
diff --git a/mling/src/proj_mgr/mod.rs b/mling/src/proj_mgr/mod.rs
deleted file mode 100644
index e0a4216..0000000
--- a/mling/src/proj_mgr/mod.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-use crate::ThisProgram;
-use mingling::{
- Program,
- macros::{dispatcher, program_setup},
-};
-
-pub mod checklist_reader;
-pub mod generator;
-pub mod metadata;
-pub mod show_binaries;
-pub mod show_directories;
-
-dispatcher!("gen", CMDGenerateProject => EntryGenerateProject);
-
-dispatcher!("show.binaries");
-dispatcher!("show.workspace-dir",
- CMDShowWorkspaceDirectory => EntryShowWorkspaceDirectory
-);
-dispatcher!("show.target-dir",
- CMDShowTargetDirectories => EntryShowTargetDirectories
-);
-
-#[program_setup]
-pub fn project_manager_setup(p: &mut Program<ThisProgram>) {
- p.with_dispatcher(CMDGenerateProject);
-
- p.with_dispatcher(CMDShowBinaries);
- p.with_dispatcher(CMDShowWorkspaceDirectory);
- p.with_dispatcher(CMDShowTargetDirectories);
-}
diff --git a/mling/src/proj_mgr/show_binaries.rs b/mling/src/proj_mgr/show_binaries.rs
deleted file mode 100644
index 9d5caf0..0000000
--- a/mling/src/proj_mgr/show_binaries.rs
+++ /dev/null
@@ -1,78 +0,0 @@
-use std::path::PathBuf;
-
-use colored::Colorize;
-use mingling::{
- Groupped, RenderResult,
- macros::{chain, pack, renderer},
-};
-use serde::Serialize;
-use std::io::Write as _;
-
-use crate::{
- Next,
- proj_mgr::{
- EntryShowBinaries,
- metadata::{CargoLockFile, read_metadata},
- },
- res::ResManifestPath,
-};
-
-#[derive(Serialize, Groupped)]
-pub struct ResultBinaries {
- pub binaries: Vec<DataBinary>,
-}
-
-#[derive(Serialize)]
-pub struct DataBinary {
- pub name: String,
- pub path: PathBuf,
-}
-
-#[chain]
-pub fn handle_show_binaries(_args: EntryShowBinaries, manifest_path: &ResManifestPath) -> Next {
- let metadata = read_metadata(manifest_path.resolved()).unwrap();
- let CargoLockFile {
- packages,
- workspace_members,
- ..
- } = metadata;
-
- let binaries: Vec<DataBinary> = packages
- .into_iter()
- .filter(|pkg| workspace_members.contains(&pkg.id))
- .flat_map(|pkg| {
- pkg.targets
- .into_iter()
- .filter(|target| target.kind.iter().any(|k| k == "bin"))
- .map(move |target| DataBinary {
- name: target.name,
- path: PathBuf::from(pkg.manifest_path.clone())
- .parent()
- .unwrap_or(&PathBuf::from("."))
- .join("src")
- .join(&target.src_path),
- })
- })
- .collect();
-
- ResultBinaries { binaries }.to_render()
-}
-
-#[renderer]
-pub fn render_binaries(binaries: ResultBinaries) -> RenderResult {
- let mut result = RenderResult::default();
- writeln!(result, "{}", "Binaries:".bright_cyan().bold()).ok();
- if let Some(max_name_len) = binaries.binaries.iter().map(|b| b.name.len()).max() {
- for binary in &binaries.binaries {
- writeln!(
- result,
- " {:width$} `{}`",
- binary.name.bright_yellow().bold(),
- binary.path.display().to_string().italic(),
- width = max_name_len
- )
- .ok();
- }
- }
- result
-}
diff --git a/mling/src/proj_mgr/show_directories.rs b/mling/src/proj_mgr/show_directories.rs
deleted file mode 100644
index 7d7c074..0000000
--- a/mling/src/proj_mgr/show_directories.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-use colored::Colorize;
-use mingling::{
- Groupped, RenderResult,
- macros::{chain, pack, renderer},
-};
-use serde::Serialize;
-use std::io::Write as _;
-
-use crate::{
- Next,
- proj_mgr::{EntryShowTargetDirectories, EntryShowWorkspaceDirectory, metadata::read_metadata},
- res::ResManifestPath,
-};
-
-#[derive(Serialize, Groupped)]
-pub struct ResultWorkspaceDirectory {
- pub path: String,
-}
-
-#[derive(Serialize, Groupped)]
-pub struct ResultTargetDirectory {
- pub path: String,
-}
-
-#[chain]
-pub fn handle_show_workspace_directory(
- _args: EntryShowWorkspaceDirectory,
- manifest_path: &ResManifestPath,
-) -> Next {
- let metadata = read_metadata(manifest_path.resolved()).unwrap();
- ResultWorkspaceDirectory {
- path: metadata.workspace_root,
- }
- .to_render()
-}
-
-#[chain]
-pub fn handle_show_target_directory(
- _args: EntryShowTargetDirectories,
- manifest_path: &ResManifestPath,
-) -> Next {
- let metadata = read_metadata(manifest_path.resolved()).unwrap();
- ResultTargetDirectory {
- path: metadata.target_directory,
- }
- .to_render()
-}
-
-#[renderer]
-pub fn render_workspace_directory(prev: ResultWorkspaceDirectory) -> RenderResult {
- let mut result = RenderResult::default();
- writeln!(result, "{}", prev.path.bright_cyan().bold()).ok();
- result
-}
-
-#[renderer]
-pub fn render_target_directory(prev: ResultTargetDirectory) -> RenderResult {
- let mut result = RenderResult::default();
- writeln!(result, "{}", prev.path.bright_cyan().bold()).ok();
- result
-}
diff --git a/mling/src/res/current_dir.rs b/mling/src/res/current_dir.rs
deleted file mode 100644
index b928596..0000000
--- a/mling/src/res/current_dir.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-use std::path::PathBuf;
-
-#[derive(Default, Clone)]
-pub struct ResCurrentDir {
- pub path: PathBuf,
-}
diff --git a/mling/src/res/manifest_path.rs b/mling/src/res/manifest_path.rs
deleted file mode 100644
index 7f1686e..0000000
--- a/mling/src/res/manifest_path.rs
+++ /dev/null
@@ -1,13 +0,0 @@
-use std::path::PathBuf;
-
-#[derive(Default, Clone)]
-pub struct ResManifestPath {
- pub raw: Option<String>,
- pub resolved: Option<PathBuf>,
-}
-
-impl ResManifestPath {
- pub fn resolved(&self) -> &PathBuf {
- self.resolved.as_ref().unwrap()
- }
-}
diff --git a/mling/src/res/mod.rs b/mling/src/res/mod.rs
deleted file mode 100644
index caa843c..0000000
--- a/mling/src/res/mod.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-mod current_dir;
-pub use current_dir::*;
-
-mod manifest_path;
-pub use manifest_path::*;
diff --git a/verified-docs.toml b/verified-docs.toml
index d66967f..f3b1f85 100644
--- a/verified-docs.toml
+++ b/verified-docs.toml
@@ -3,5 +3,6 @@
[verified]
readme = "./README.md"
+getting_started = "./GETTING_STARTED.md"
documents_en_us = "./docs/pages/**"
documents_zh_cn = "./docs/_zh_CN/pages/**"