aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-17 03:24:51 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-17 03:25:24 +0800
commitb9f208deed7b8e012fbcd84202e2fb1d5eb8eeb9 (patch)
tree2e0f00651b0cff09a2f6bafd6ce35046532ffa81
parent8b2c209c2f111e0e208de5dde9df6c4bd3ea1052 (diff)
feat(picker2): complete Picker2 prototype
Picker2 replaces the original Picker1 with a two-phase (tag → pick) + mask-bitmap architecture, decoupling argument matching from type conversion via a composable matcher pipeline. Architecture - FlagMatcher — boolean flags (--verbose) - ArgMatcher — single flag+value pairs (--name Alice) - MultiArgMatcher — multi-value flag groups (--files a.txt b.txt) - PositionalMatcher — positional arguments, respects `--` - SingleMatcher — composite for Single-type Pickables Types - Flag (Active/Inactive) — semantic bool flag value - String + 14 numeric types via SinglePickable trait - Vec<T> — greedy multi-value (all SinglePickable types) - VecUntil<T> — bounded multi-value via BoundaryCheck trait - VecUntil<T>, pick_string, pick_numbers, pick_bool, pick_flag Style system - UNIX_STYLE (kebab), POWERSHELL_STYLE (Pascal), WINDOWS_STYLE (Pascal) - naming_case auto-conversion via just_fmt integration - Style-aware separator (= for Unix, : for PS/Windows) Infrastructure - internal_repeat! macro generates PickerPattern1..=32 - SinglePickable blanket impl → Pickable - MultiPickableWithBoundary trait with greedy/bounded variants - 151 integration tests - Docs updated for parser feature
-rw-r--r--.run/src/bin/doc.ps12
-rw-r--r--.run/src/bin/doc.sh2
-rw-r--r--.run/version-files.toml8
-rw-r--r--.vscode/settings.json8
-rw-r--r--.zed/settings.json10
-rw-r--r--Cargo.lock19
-rw-r--r--Cargo.toml2
-rw-r--r--docs/dev/pages/issues/add-picker2.md14
-rw-r--r--examples/example-argument-parse/Cargo.lock4
-rw-r--r--examples/example-async-support/Cargo.lock4
-rw-r--r--examples/example-basic/Cargo.lock4
-rw-r--r--examples/example-clap-binding/Cargo.lock4
-rw-r--r--examples/example-combine-pathf-dispatch-tree/Cargo.lock4
-rw-r--r--examples/example-completion/Cargo.lock12
-rw-r--r--examples/example-custom-pickable/Cargo.lock4
-rw-r--r--examples/example-dispatch-tree/Cargo.lock4
-rw-r--r--examples/example-enum-tag/Cargo.lock12
-rw-r--r--examples/example-error-handling/Cargo.lock4
-rw-r--r--examples/example-exitcode/Cargo.lock4
-rw-r--r--examples/example-help/Cargo.lock4
-rw-r--r--examples/example-hook/Cargo.lock4
-rw-r--r--examples/example-implicit-dispatcher/Cargo.lock4
-rw-r--r--examples/example-lazy-resources/Cargo.lock4
-rw-r--r--examples/example-outside-type/Cargo.lock4
-rw-r--r--examples/example-pack-err/Cargo.lock4
-rw-r--r--examples/example-panic-unwind/Cargo.lock4
-rw-r--r--examples/example-pathfinder/Cargo.lock4
-rw-r--r--examples/example-repl-basic/Cargo.lock12
-rw-r--r--examples/example-resources/Cargo.lock4
-rw-r--r--examples/example-setup/Cargo.lock4
-rw-r--r--examples/example-structural-renderer/Cargo.lock4
-rw-r--r--examples/example-unit-test/Cargo.lock4
-rw-r--r--examples/full-todolist/Cargo.lock4
-rw-r--r--mingling/Cargo.toml15
-rw-r--r--mingling/src/lib.rs41
-rw-r--r--mingling_core/tests/test-all/Cargo.lock12
-rw-r--r--mingling_core/tests/test-basic/Cargo.lock4
-rw-r--r--mingling_core/tests/test-comp/Cargo.lock12
-rw-r--r--mingling_core/tests/test-dispatch-tree/Cargo.lock4
-rw-r--r--mingling_core/tests/test-repl/Cargo.lock4
-rw-r--r--mingling_core/tests/test-structural-renderer/Cargo.lock4
-rw-r--r--mingling_pathf/test/Cargo.lock10
-rw-r--r--mingling_picker/Cargo.toml7
-rw-r--r--mingling_picker/README.md42
-rw-r--r--mingling_picker/src/arg.rs299
-rw-r--r--mingling_picker/src/builtin.rs4
-rw-r--r--mingling_picker/src/builtin/pick_bool.rs22
-rw-r--r--mingling_picker/src/builtin/pick_flag.rs21
-rw-r--r--mingling_picker/src/builtin/pick_numbers.rs80
-rw-r--r--mingling_picker/src/builtin/pick_string.rs11
-rw-r--r--mingling_picker/src/corebind.rs1
-rw-r--r--mingling_picker/src/infos.rs446
-rw-r--r--mingling_picker/src/lib.rs44
-rw-r--r--mingling_picker/src/parselib.rs144
-rw-r--r--mingling_picker/src/parselib/arg_matcher.rs142
-rw-r--r--mingling_picker/src/parselib/flag_matcher.rs82
-rw-r--r--mingling_picker/src/parselib/multi_arg_matcher.rs141
-rw-r--r--mingling_picker/src/parselib/pos_matcher.rs71
-rw-r--r--mingling_picker/src/parselib/single_matcher.rs59
-rw-r--r--mingling_picker/src/parselib/style.rs225
-rw-r--r--mingling_picker/src/parselib/utils.rs271
-rw-r--r--mingling_picker/src/pickable.rs91
-rw-r--r--mingling_picker/src/pickable/multi_pickable.rs77
-rw-r--r--mingling_picker/src/pickable/single_pickable.rs69
-rw-r--r--mingling_picker/src/picker.rs360
-rw-r--r--mingling_picker/src/picker/parse.rs177
-rw-r--r--mingling_picker/src/picker/patterns.rs199
-rw-r--r--mingling_picker/src/picker/result.rs56
-rw-r--r--mingling_picker/src/value.rs5
-rw-r--r--mingling_picker/src/value/flag.rs145
-rw-r--r--mingling_picker/src/value/vec_until.rs134
-rw-r--r--mingling_picker/test/Cargo.lock68
-rw-r--r--mingling_picker/test/Cargo.toml9
-rw-r--r--mingling_picker/test/src/lib.rs23
-rw-r--r--mingling_picker/test/src/test.rs10
-rw-r--r--mingling_picker/test/src/test/arg_matcher_test.rs289
-rw-r--r--mingling_picker/test/src/test/basic_test.rs223
-rw-r--r--mingling_picker/test/src/test/multi_arg_test.rs181
-rw-r--r--mingling_picker/test/src/test/multi_value_test.rs66
-rw-r--r--mingling_picker/test/src/test/pos_matcher_test.rs141
-rw-r--r--mingling_picker/test/src/test/priority_test.rs85
-rw-r--r--mingling_picker/test/src/test/route_test.rs200
-rw-r--r--mingling_picker/test/src/test/style_test.rs300
-rw-r--r--mingling_picker/test/src/test/value_flag_test.rs174
-rw-r--r--mingling_picker/test/src/test/value_string_test.rs171
-rw-r--r--mingling_picker_macros/Cargo.toml3
-rw-r--r--mingling_picker_macros/src/arg.rs205
-rw-r--r--mingling_picker_macros/src/internal_repeat.rs315
-rw-r--r--mingling_picker_macros/src/lib.rs29
89 files changed, 6119 insertions, 93 deletions
diff --git a/.run/src/bin/doc.ps1 b/.run/src/bin/doc.ps1
index 987f0de..fd7afaa 100644
--- a/.run/src/bin/doc.ps1
+++ b/.run/src/bin/doc.ps1
@@ -1 +1 @@
-cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open
+cargo doc --workspace --no-deps --features 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 5e8a311..2d59896 100644
--- a/.run/src/bin/doc.sh
+++ b/.run/src/bin/doc.sh
@@ -1,3 +1,3 @@
#!/bin/bash
-cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open
+cargo doc --workspace --no-deps --features core,macros,builds,structural_renderer,repl,comp,parser,picker,clap,extra_macros --open
diff --git a/.run/version-files.toml b/.run/version-files.toml
index ca104d2..f00ae4c 100644
--- a/.run/version-files.toml
+++ b/.run/version-files.toml
@@ -17,3 +17,11 @@ pattern = "version = \"{VER}\""
[[file]]
file = "./docs/res/guide.txt"
pattern = "mingling = \"{VER}\""
+
+[[file]]
+file = "./mingling_picker/README.md"
+pattern = "version = \"{VER}\""
+
+[[file]]
+file = "./mingling_picker/README.md"
+pattern = "mingling_picker = \"{VER}\""
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0aa6db4..7472e1c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,5 +1,11 @@
{
"rust-analyzer.check.command": "clippy",
"rust-analyzer.checkOnSave": true,
- "rust-analyzer.linkedProjects": [".run/Cargo.toml"],
+ "rust-analyzer.linkedProjects": [
+ ".run/Cargo.toml",
+ "mingling_picker/Cargo.toml",
+ "mingling_pathf/test/Cargo.toml",
+ "mingling_picker/test/Cargo.toml",
+ ],
+ "rust-analyzer.cargo.features": ["mingling_support"],
}
diff --git a/.zed/settings.json b/.zed/settings.json
index 983506d..12c901e 100644
--- a/.zed/settings.json
+++ b/.zed/settings.json
@@ -4,7 +4,15 @@
"initialization_options": {
"checkOnSave": true,
"check": { "command": "clippy" },
- "linkedProjects": [".run/Cargo.toml"],
+ "linkedProjects": [
+ ".run/Cargo.toml",
+ "mingling_picker/Cargo.toml",
+ "mingling_pathf/test/Cargo.toml",
+ "mingling_picker/test/Cargo.toml",
+ ],
+ "cargo": {
+ "features": ["mingling_support"],
+ },
},
},
},
diff --git a/Cargo.lock b/Cargo.lock
index 5ca9565..a283e0f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -343,12 +343,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
name = "just_template"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb"
dependencies = [
- "just_fmt",
+ "just_fmt 0.1.2",
"just_template_macros",
]
@@ -407,7 +413,6 @@ dependencies = [
"mingling_core",
"mingling_macros",
"mingling_picker",
- "mingling_picker_macros",
"serde",
"size",
"tokio",
@@ -420,7 +425,7 @@ dependencies = [
"chrono",
"colored",
"dirs",
- "just_fmt",
+ "just_fmt 0.1.2",
"mingling",
"serde",
"serde_json",
@@ -432,7 +437,7 @@ name = "mingling_core"
version = "0.3.0"
dependencies = [
"env_logger",
- "just_fmt",
+ "just_fmt 0.2.0",
"just_template",
"log",
"mingling_pathf",
@@ -447,7 +452,7 @@ dependencies = [
name = "mingling_macros"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"proc-macro2",
"quote",
"syn",
@@ -457,7 +462,7 @@ dependencies = [
name = "mingling_pathf"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"proc-macro2",
"syn",
]
@@ -466,7 +471,9 @@ dependencies = [
name = "mingling_picker"
version = "0.3.0"
dependencies = [
+ "just_fmt 0.2.0",
"mingling_core",
+ "mingling_picker_macros",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index b3c3496..90989fc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,7 +39,7 @@ mingling_pathf = { path = "mingling_pathf", default-features = false }
mingling_picker = { path = "mingling_picker", default-features = false }
mingling_picker_macros = { path = "mingling_picker_macros", default-features = false }
-just_fmt = "0.1.2"
+just_fmt = "0.2.0"
just_template = "0.2.0"
serde = { version = "1.0.228", features = ["derive"] }
diff --git a/docs/dev/pages/issues/add-picker2.md b/docs/dev/pages/issues/add-picker2.md
index 1f0f941..f5c3ca7 100644
--- a/docs/dev/pages/issues/add-picker2.md
+++ b/docs/dev/pages/issues/add-picker2.md
@@ -84,3 +84,17 @@ pub struct PickerResult<Tuple> {
pub remains_argument: Arguments,
}
```
+
+---
+
+## 🕘 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
+- [ ] Complete
diff --git a/examples/example-argument-parse/Cargo.lock b/examples/example-argument-parse/Cargo.lock
index 369add5..696dcb6 100644
--- a/examples/example-argument-parse/Cargo.lock
+++ b/examples/example-argument-parse/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-async-support/Cargo.lock b/examples/example-async-support/Cargo.lock
index e457c63..83ce343 100644
--- a/examples/example-async-support/Cargo.lock
+++ b/examples/example-async-support/Cargo.lock
@@ -12,9 +12,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-basic/Cargo.lock b/examples/example-basic/Cargo.lock
index ace5fca..90b8e4a 100644
--- a/examples/example-basic/Cargo.lock
+++ b/examples/example-basic/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-clap-binding/Cargo.lock b/examples/example-clap-binding/Cargo.lock
index b45bd86..b747c40 100644
--- a/examples/example-clap-binding/Cargo.lock
+++ b/examples/example-clap-binding/Cargo.lock
@@ -120,9 +120,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-combine-pathf-dispatch-tree/Cargo.lock b/examples/example-combine-pathf-dispatch-tree/Cargo.lock
index 0903c86..f91e03f 100644
--- a/examples/example-combine-pathf-dispatch-tree/Cargo.lock
+++ b/examples/example-combine-pathf-dispatch-tree/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-completion/Cargo.lock b/examples/example-completion/Cargo.lock
index cfca7b3..568bec5 100644
--- a/examples/example-completion/Cargo.lock
+++ b/examples/example-completion/Cargo.lock
@@ -16,12 +16,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
name = "just_template"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb"
dependencies = [
- "just_fmt",
+ "just_fmt 0.1.2",
"just_template_macros",
]
@@ -49,7 +55,7 @@ dependencies = [
name = "mingling_core"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"just_template",
]
@@ -57,7 +63,7 @@ dependencies = [
name = "mingling_macros"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"proc-macro2",
"quote",
"syn",
diff --git a/examples/example-custom-pickable/Cargo.lock b/examples/example-custom-pickable/Cargo.lock
index b773df1..cb2d3ac 100644
--- a/examples/example-custom-pickable/Cargo.lock
+++ b/examples/example-custom-pickable/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-dispatch-tree/Cargo.lock b/examples/example-dispatch-tree/Cargo.lock
index 7d8b260..5751ecd 100644
--- a/examples/example-dispatch-tree/Cargo.lock
+++ b/examples/example-dispatch-tree/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-enum-tag/Cargo.lock b/examples/example-enum-tag/Cargo.lock
index 333bd33..211719f 100644
--- a/examples/example-enum-tag/Cargo.lock
+++ b/examples/example-enum-tag/Cargo.lock
@@ -16,12 +16,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
name = "just_template"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb"
dependencies = [
- "just_fmt",
+ "just_fmt 0.1.2",
"just_template_macros",
]
@@ -49,7 +55,7 @@ dependencies = [
name = "mingling_core"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"just_template",
]
@@ -57,7 +63,7 @@ dependencies = [
name = "mingling_macros"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"proc-macro2",
"quote",
"syn",
diff --git a/examples/example-error-handling/Cargo.lock b/examples/example-error-handling/Cargo.lock
index 7407809..d139c14 100644
--- a/examples/example-error-handling/Cargo.lock
+++ b/examples/example-error-handling/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-exitcode/Cargo.lock b/examples/example-exitcode/Cargo.lock
index d54648e..df8b935 100644
--- a/examples/example-exitcode/Cargo.lock
+++ b/examples/example-exitcode/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-help/Cargo.lock b/examples/example-help/Cargo.lock
index dd3829c..7a88e8e 100644
--- a/examples/example-help/Cargo.lock
+++ b/examples/example-help/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-hook/Cargo.lock b/examples/example-hook/Cargo.lock
index 7801069..cce863a 100644
--- a/examples/example-hook/Cargo.lock
+++ b/examples/example-hook/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-implicit-dispatcher/Cargo.lock b/examples/example-implicit-dispatcher/Cargo.lock
index b5ab8db..150104b 100644
--- a/examples/example-implicit-dispatcher/Cargo.lock
+++ b/examples/example-implicit-dispatcher/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-lazy-resources/Cargo.lock b/examples/example-lazy-resources/Cargo.lock
index a5c8dc2..9ff79d2 100644
--- a/examples/example-lazy-resources/Cargo.lock
+++ b/examples/example-lazy-resources/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-outside-type/Cargo.lock b/examples/example-outside-type/Cargo.lock
index 6528afd..4c00bc8 100644
--- a/examples/example-outside-type/Cargo.lock
+++ b/examples/example-outside-type/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-pack-err/Cargo.lock b/examples/example-pack-err/Cargo.lock
index 7384680..26bad2b 100644
--- a/examples/example-pack-err/Cargo.lock
+++ b/examples/example-pack-err/Cargo.lock
@@ -18,9 +18,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "memchr"
diff --git a/examples/example-panic-unwind/Cargo.lock b/examples/example-panic-unwind/Cargo.lock
index be8d21a..aafe504 100644
--- a/examples/example-panic-unwind/Cargo.lock
+++ b/examples/example-panic-unwind/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-pathfinder/Cargo.lock b/examples/example-pathfinder/Cargo.lock
index 6dcee1d..3fe98bb 100644
--- a/examples/example-pathfinder/Cargo.lock
+++ b/examples/example-pathfinder/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-repl-basic/Cargo.lock b/examples/example-repl-basic/Cargo.lock
index 07cdd4b..d405eaa 100644
--- a/examples/example-repl-basic/Cargo.lock
+++ b/examples/example-repl-basic/Cargo.lock
@@ -6,7 +6,7 @@ version = 4
name = "example-repl-basic"
version = "0.1.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.1.2",
"mingling",
]
@@ -17,6 +17,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
[[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 = [
@@ -29,14 +35,14 @@ dependencies = [
name = "mingling_core"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
]
[[package]]
name = "mingling_macros"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"proc-macro2",
"quote",
"syn",
diff --git a/examples/example-resources/Cargo.lock b/examples/example-resources/Cargo.lock
index 1397cf4..68d2138 100644
--- a/examples/example-resources/Cargo.lock
+++ b/examples/example-resources/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-setup/Cargo.lock b/examples/example-setup/Cargo.lock
index c2ec3e8..c6b660b 100644
--- a/examples/example-setup/Cargo.lock
+++ b/examples/example-setup/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/example-structural-renderer/Cargo.lock b/examples/example-structural-renderer/Cargo.lock
index bacbd70..959c670 100644
--- a/examples/example-structural-renderer/Cargo.lock
+++ b/examples/example-structural-renderer/Cargo.lock
@@ -18,9 +18,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "memchr"
diff --git a/examples/example-unit-test/Cargo.lock b/examples/example-unit-test/Cargo.lock
index a3a8813..2be0d51 100644
--- a/examples/example-unit-test/Cargo.lock
+++ b/examples/example-unit-test/Cargo.lock
@@ -11,9 +11,9 @@ dependencies = [
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/examples/full-todolist/Cargo.lock b/examples/full-todolist/Cargo.lock
index 716e51a..f4f0000 100644
--- a/examples/full-todolist/Cargo.lock
+++ b/examples/full-todolist/Cargo.lock
@@ -19,9 +19,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "memchr"
diff --git a/mingling/Cargo.toml b/mingling/Cargo.toml
index 667cfa5..c3f9511 100644
--- a/mingling/Cargo.toml
+++ b/mingling/Cargo.toml
@@ -22,29 +22,35 @@ mingling = { path = ".", features = [
[package.metadata.docs.rs]
features = [
+ "core",
+ "macros",
"builds",
"structural_renderer",
"repl",
"comp",
"parser",
+ "picker",
"clap",
"extra_macros",
]
[features]
+core = ["dep:mingling_core", "mingling_core/default"]
+macros = ["dep:mingling_macros", "mingling_macros/default"]
+
nightly = ["mingling_core/nightly", "mingling_macros/nightly"]
debug = ["mingling_core/debug"]
async = ["mingling_core/async", "mingling_macros/async"]
builds = ["mingling_core/builds"]
-default = ["mingling_core/default", "mingling_macros/default"]
+default = ["core", "macros"]
clap = ["mingling_core/clap", "mingling_macros/clap"]
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:mingling_picker", "dep:mingling_picker_macros"]
+picker = ["dep:mingling_picker", "mingling_picker/mingling_support"]
pathf = ["mingling_core/pathf", "mingling_macros/pathf"]
structural_renderer = [
@@ -82,9 +88,8 @@ ron_serde_fmt = ["mingling_core/ron_serde_fmt"]
extra_macros = ["mingling_macros/extra_macros"]
[dependencies]
-mingling_core.workspace = true
-mingling_macros.workspace = true
+mingling_core = { workspace = true, optional = true }
+mingling_macros = { workspace = true, optional = true }
mingling_picker = { workspace = true, optional = true }
-mingling_picker_macros = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
size = { version = "0.5", optional = true }
diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs
index 76d73b6..ba87f85 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -69,10 +69,14 @@
//! # About Features
//! All features of `Mingling` are opt-in. To learn what each feature provides, see [Features](feature/index.html) or [Helpdoc](https://mingling-rs.github.io/mingling/docs/doc.html#/pages/other/features)
+#[cfg(feature = "core")]
mod example_docs;
// Re-export Core lib
+#[cfg(feature = "core")]
pub use mingling::*;
+
+#[cfg(feature = "core")]
pub use mingling_core as mingling;
/// `Mingling` argument parser (Built-in)
@@ -83,6 +87,10 @@ pub mod parser;
#[cfg(feature = "picker")]
pub mod picker {
pub use mingling_picker::*;
+
+ pub mod parselib {
+ pub use mingling_picker::parselib::*;
+ }
}
/// Re-export of all macros from `mingling_macros`.
@@ -97,6 +105,7 @@ pub mod picker {
///
/// <https://docs.rs/mingling_macros/latest/mingling_macros/>
#[allow(unused_imports)]
+#[cfg(feature = "macros")]
pub mod macros {
/// `#[chain]` - Used to generate a struct implementing the `Chain` trait via a method
pub use mingling_macros::chain;
@@ -170,13 +179,15 @@ pub mod macros {
pub use mingling_macros::suggest_enum;
/// New Parser provided by the `picker` feature
#[cfg(feature = "picker")]
- pub use mingling_macros::*;
+ pub use mingling_picker::macros::*;
}
/// derive macro `EnumTag`
+#[cfg(feature = "macros")]
pub use mingling_macros::EnumTag;
/// derive macro Groupped
+#[cfg(feature = "macros")]
pub use mingling_macros::Groupped;
/// derive macro `StructuralData` — marks a type as supporting structured output
@@ -184,10 +195,12 @@ pub use mingling_macros::Groupped;
pub use mingling_macros::StructuralData;
/// Example projects for `Mingling`, for learning how to use `Mingling`
+#[cfg(feature = "core")]
pub mod _mingling_examples {
pub use crate::example_docs::*;
}
+#[cfg(feature = "core")]
mod features;
/// Module for checking which features are enabled at compile time.
@@ -195,19 +208,23 @@ mod features;
/// Each constant re-exported from this module corresponds to a Cargo feature flag.
/// They can be used for conditional compilation or runtime branching based on
/// feature availability.
+#[cfg(feature = "core")]
pub mod feature {
include!("./features.rs");
}
+#[cfg(feature = "core")]
mod setups;
/// Setups provided by Mingling, which can extend command-line programs.
+#[cfg(feature = "core")]
pub mod setup {
pub use crate::setups::*;
pub use mingling_core::setup::*;
}
/// Mutable global resources provided within Mingling
+#[cfg(feature = "core")]
pub mod res;
/// The prelude module provides convenient re-exports of commonly used macros and traits.
@@ -223,34 +240,45 @@ pub mod res;
/// ```
pub mod prelude {
/// Re-export of the `Groupped` derive macro for grouping types.
+ #[cfg(feature = "core")]
pub use crate::Groupped;
/// Re-export of the `RenderResult` struct for outputting rendering result
+ #[cfg(feature = "core")]
pub use crate::RenderResult;
/// Re-export of the `chain` macro for defining a chain of commands.
+ #[cfg(feature = "macros")]
pub use crate::macros::chain;
/// Re-export of the `dispatcher` macro for routing commands.
+ #[cfg(feature = "macros")]
pub use crate::macros::dispatcher;
/// Re-export of the `empty_result` macro for creating an empty result value for early return.
- #[cfg(feature = "extra_macros")]
+ #[cfg(all(feature = "extra_macros", feature = "macros"))]
pub use crate::macros::empty_result;
/// Re-export of the `gen_program` macro for generating the program entry point.
+ #[cfg(feature = "macros")]
pub use crate::macros::gen_program;
/// Re-export of the `pack` macro for creating wrapper types.
+ #[cfg(feature = "macros")]
pub use crate::macros::pack;
/// Re-export of the `pack_err` macro for creating error types.
- #[cfg(feature = "extra_macros")]
+ #[cfg(all(feature = "extra_macros", feature = "macros"))]
pub use crate::macros::pack_err;
/// Re-export of the `renderer` macro for defining renderer functions.
+ #[cfg(feature = "macros")]
pub use crate::macros::renderer;
/// Like `pack_err!` but also marks the type for structured output
- #[cfg(all(feature = "structural_renderer", feature = "extra_macros"))]
+ #[cfg(all(
+ feature = "macros",
+ feature = "structural_renderer",
+ feature = "extra_macros"
+ ))]
pub use mingling_macros::pack_err_structural;
/// Like `pack!` but also marks the type for structured output
- #[cfg(feature = "structural_renderer")]
+ #[cfg(all(feature = "macros", feature = "structural_renderer"))]
pub use mingling_macros::pack_structural;
/// Re-export of the `completion` macro for generating completion entries.
- #[cfg(feature = "comp")]
+ #[cfg(all(feature = "macros", feature = "comp"))]
pub use crate::macros::completion;
/// Re-export of the `AsPicker` trait for picker functionality.
@@ -261,5 +289,6 @@ pub mod prelude {
pub use mingling_picker::prelude::*;
/// Used to enable the `writeln!` macro for `RenderResult`
+ #[cfg(feature = "core")]
pub use std::io::Write;
}
diff --git a/mingling_core/tests/test-all/Cargo.lock b/mingling_core/tests/test-all/Cargo.lock
index 49348cf..bb7b75b 100644
--- a/mingling_core/tests/test-all/Cargo.lock
+++ b/mingling_core/tests/test-all/Cargo.lock
@@ -68,12 +68,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
name = "just_template"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb"
dependencies = [
- "just_fmt",
+ "just_fmt 0.1.2",
"just_template_macros",
]
@@ -123,7 +129,7 @@ dependencies = [
name = "mingling_core"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"just_template",
"ron",
"serde",
@@ -136,7 +142,7 @@ dependencies = [
name = "mingling_macros"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"proc-macro2",
"quote",
"syn",
diff --git a/mingling_core/tests/test-basic/Cargo.lock b/mingling_core/tests/test-basic/Cargo.lock
index 7b14f0b..5029bbf 100644
--- a/mingling_core/tests/test-basic/Cargo.lock
+++ b/mingling_core/tests/test-basic/Cargo.lock
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/mingling_core/tests/test-comp/Cargo.lock b/mingling_core/tests/test-comp/Cargo.lock
index bf843cf..175b65e 100644
--- a/mingling_core/tests/test-comp/Cargo.lock
+++ b/mingling_core/tests/test-comp/Cargo.lock
@@ -9,12 +9,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
name = "just_template"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb"
dependencies = [
- "just_fmt",
+ "just_fmt 0.1.2",
"just_template_macros",
]
@@ -41,7 +47,7 @@ dependencies = [
name = "mingling_core"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"just_template",
]
@@ -49,7 +55,7 @@ dependencies = [
name = "mingling_macros"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"proc-macro2",
"quote",
"syn",
diff --git a/mingling_core/tests/test-dispatch-tree/Cargo.lock b/mingling_core/tests/test-dispatch-tree/Cargo.lock
index 2c3a382..39d82c5 100644
--- a/mingling_core/tests/test-dispatch-tree/Cargo.lock
+++ b/mingling_core/tests/test-dispatch-tree/Cargo.lock
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/mingling_core/tests/test-repl/Cargo.lock b/mingling_core/tests/test-repl/Cargo.lock
index 14384f1..2b990f2 100644
--- a/mingling_core/tests/test-repl/Cargo.lock
+++ b/mingling_core/tests/test-repl/Cargo.lock
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "mingling"
diff --git a/mingling_core/tests/test-structural-renderer/Cargo.lock b/mingling_core/tests/test-structural-renderer/Cargo.lock
index 1d3c192..bf5291d 100644
--- a/mingling_core/tests/test-structural-renderer/Cargo.lock
+++ b/mingling_core/tests/test-structural-renderer/Cargo.lock
@@ -41,9 +41,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "just_fmt"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
[[package]]
name = "memchr"
diff --git a/mingling_pathf/test/Cargo.lock b/mingling_pathf/test/Cargo.lock
index 8b567b5..1260662 100644
--- a/mingling_pathf/test/Cargo.lock
+++ b/mingling_pathf/test/Cargo.lock
@@ -9,10 +9,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
name = "mingling_pathf"
version = "0.3.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.2.0",
"proc-macro2",
"syn",
]
@@ -50,7 +56,7 @@ dependencies = [
name = "test-mingling-pathf"
version = "0.1.0"
dependencies = [
- "just_fmt",
+ "just_fmt 0.1.2",
"mingling_pathf",
]
diff --git a/mingling_picker/Cargo.toml b/mingling_picker/Cargo.toml
index f992373..ddab33c 100644
--- a/mingling_picker/Cargo.toml
+++ b/mingling_picker/Cargo.toml
@@ -8,5 +8,10 @@ authors = ["Weicao-CatilGrass"]
readme = "README.md"
description = "Mingling's lightweight argument parser"
+[features]
+mingling_support = ["dep:mingling_core", "mingling_picker_macros/mingling_support"]
+
[dependencies]
-mingling_core.workspace = true
+mingling_core = { workspace = true, optional = true }
+mingling_picker_macros.workspace = true
+just_fmt.workspace = true
diff --git a/mingling_picker/README.md b/mingling_picker/README.md
index e69de29..c83bff4 100644
--- a/mingling_picker/README.md
+++ b/mingling_picker/README.md
@@ -0,0 +1,42 @@
+# Mingling Picker
+
+A command-line argument parser for [Mingling](https://github.com/mingling-rs/mingling), enabled by the `mingling/picker` feature.
+
+```toml
+[dependencies.mingling]
+version = "0.3.0"
+features = [
+ "picker"
+]
+```
+
+Of course, you can also use it as a standalone crate by replacing `mingling::picker` with `mingling_picker`:
+
+```toml
+[dependencies]
+mingling_picker = "0.3.0"
+```
+
+## Chained Argument Parser
+
+Provides a clean chained-call API for declaring arguments to parse:
+
+```rust
+let args: Vec<&str> = vec!["--name", "Bob", "--age", "24"];
+let result = args
+ .pick(&flag![name: String])
+ .or(|| "Alice".to_string())
+ .pick(&flag![age: i32])
+ .or(|| 24)
+ .post(|num| num.clamp(0, 120))
+ .parse();
+let (name, age): (String, i32) = a.unwrap();
+```
+
+## Parsing Function Library
+
+Provides a pure function library `parselib` for analyzing the structure of command-line arguments.
+
+```rust
+use mingling::picker::parselib::*;
+```
diff --git a/mingling_picker/src/arg.rs b/mingling_picker/src/arg.rs
new file mode 100644
index 0000000..78ad539
--- /dev/null
+++ b/mingling_picker/src/arg.rs
@@ -0,0 +1,299 @@
+use crate::Pickable;
+use std::marker::PhantomData;
+
+/// Represents a constraint definition for a parameter selection.
+///
+/// This structure describes the constraints that a command-line parameter (Picker parameter item)
+/// should satisfy, including its full name list (with aliases), short name form, and whether it is
+/// positional.
+///
+/// # Field Descriptions
+///
+/// - `full`: Full name or alias list. For example, `["config", "cfg"]` means the parameter can be
+/// matched with either `--config` or `--cfg`. Must contain at least one non-empty string.
+///
+/// - `short`: Short name (single character). For example, `Some('c')` means it can be passed using
+/// the `-c` form. If set to `None`, the short name form is not supported.
+///
+/// - `positional`: Whether the parameter is positional (i.e., an argument without a flag).
+/// - `true`: The parameter is positional; it is matched by its position in the command line rather
+/// than by a `--name` or `-n` flag.
+/// - `false`: The parameter is a named (flag-based) parameter.
+///
+/// - `_type`: PhantomData to hold the type parameter.
+#[derive(Default, Clone, Copy)]
+pub struct PickerArg<'a, Type>
+where
+ Type: Pickable<'a>,
+{
+ /// Full name, may include variant names (aliases), e.g., `["config", "cfg"]`.
+ pub full: &'a [&'a str],
+
+ /// Short name, e.g., `'c'`.
+ pub short: Option<char>,
+
+ /// Whether the parameter is positional (no flag, matched by position).
+ pub positional: bool,
+
+ /// PhantomData to hold the type parameter.
+ pub internal_type: PhantomData<Type>,
+}
+
+impl<'a, Type> From<&'a PickerArg<'a, Type>> for PickerArg<'a, Type>
+where
+ Type: Pickable<'a>,
+{
+ fn from(value: &'a PickerArg<'a, Type>) -> Self {
+ PickerArg {
+ full: value.full,
+ short: value.short,
+ positional: value.positional,
+ internal_type: PhantomData,
+ }
+ }
+}
+
+impl<'a, Type> PickerArg<'a, Type>
+where
+ Type: Pickable<'a>,
+{
+ /// Creates a new `PickerArg` with the provided parameters.
+ pub fn new(full: &'a [&'a str], short: Option<char>, positional: bool) -> Self {
+ Self {
+ full,
+ short,
+ positional,
+ internal_type: PhantomData,
+ }
+ }
+
+ /// Returns the full name list (including aliases).
+ pub fn full(&self) -> &'a [&'a str] {
+ self.full
+ }
+
+ /// Returns the short name, if any.
+ pub fn short(&self) -> Option<char> {
+ self.short
+ }
+
+ /// Returns whether the parameter is positional.
+ ///
+ /// If `full` is empty or `short` is `None`, the parameter is considered positional
+ /// regardless of the stored value.
+ pub fn is_positional(&self) -> bool {
+ if self.full.is_empty() && self.short.is_none() {
+ true
+ } else {
+ self.positional
+ }
+ }
+
+ /// Sets the full name list.
+ pub fn set_full(&mut self, full: &'a [&'a str]) {
+ self.full = full;
+ }
+
+ /// Sets the short name.
+ pub fn set_short(&mut self, short: Option<char>) {
+ self.short = short;
+ }
+
+ /// Sets whether the parameter is positional.
+ pub fn set_positional(&mut self, positional: bool) {
+ self.positional = positional;
+ }
+
+ /// Sets the full name list and returns self.
+ pub fn with_full(mut self, full: &'a [&'a str]) -> Self {
+ self.full = full;
+ self
+ }
+
+ /// Clears the full name list (sets it to an empty slice) and returns self.
+ pub fn without_full(mut self) -> Self {
+ self.full = &[];
+ self
+ }
+
+ /// Sets the short name to the given character and returns self.
+ pub fn with_short(mut self, short: char) -> Self {
+ self.short = Some(short);
+ self
+ }
+
+ /// Clears the short name (sets it to None) and returns self.
+ pub fn without_short(mut self) -> Self {
+ self.short = None;
+ self
+ }
+
+ /// Sets whether the parameter is positional and returns self.
+ pub fn with_positional(mut self, positional: bool) -> Self {
+ self.positional = positional;
+ self
+ }
+}
+
+/// Describes the attribute (behavior) of a command-line parameter.
+///
+/// The ordering reflects parse priority (higher = parsed first):
+/// `PositionalMulti < Positional < Flag < Single < Multi`
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum PickerArgAttr {
+ /// Positional argument that accepts multiple values (e.g., multiple input files).
+ PositionalMulti,
+
+ /// Positional argument matched by its position (e.g., an input file).
+ #[default]
+ Positional,
+
+ /// Boolean flag with no associated value (e.g., `--verbose`).
+ Flag,
+
+ /// Accepts a single value (e.g., `--name Alice`).
+ Single,
+
+ /// Accepts multiple values (e.g., `--file a.txt --file b.txt`).
+ Multi,
+}
+
+impl PickerArgAttr {
+ /// Determines if the given `PickerArg` represents a positional parameter.
+ ///
+ /// If the flag is positional (determined by `flag.is_positional()`), returns
+ /// `PickerArgAttr::Positional`. Otherwise, invokes the `other` closure to
+ /// produce and return a `PickerArgAttr`.
+ ///
+ /// # Parameters
+ ///
+ /// - `flag`: A reference to the [`PickerArg`] to evaluate.
+ /// - `other`: A closure that returns a [`PickerArgAttr`] when the flag is
+ /// **not** positional.
+ #[inline(always)]
+ pub fn positional_or_else<'a, T>(
+ flag: &PickerArg<'a, T>,
+ other: fn() -> PickerArgAttr,
+ ) -> PickerArgAttr
+ where
+ T: Pickable<'a>,
+ {
+ if flag.is_positional() {
+ PickerArgAttr::Positional
+ } else {
+ other()
+ }
+ }
+
+ /// Determines if the given `PickerArg` represents a positional parameter and returns
+ /// `PickerArgAttr::Positional` if so. Otherwise, returns the provided `default` attribute.
+ ///
+ /// # Parameters
+ ///
+ /// - `flag`: A reference to the [`PickerArg`] to evaluate.
+ /// - `default`: The [`PickerArgAttr`] to return if the flag is not positional.
+ #[inline(always)]
+ pub fn positional_or<'a, T>(flag: &PickerArg<'a, T>, default: PickerArgAttr) -> PickerArgAttr
+ where
+ T: Pickable<'a>,
+ {
+ if flag.is_positional() {
+ PickerArgAttr::Positional
+ } else {
+ default
+ }
+ }
+
+ /// Determines if the given `PickerArg` represents a positional parameter and returns
+ /// `PickerArgAttr::Positional` if so. Otherwise, returns `PickerArgAttr::Single`.
+ ///
+ /// # Parameters
+ ///
+ /// - `flag`: A reference to the [`PickerArg`] to evaluate.
+ #[inline(always)]
+ pub fn positional_or_single<'a, T>(flag: &PickerArg<'a, T>) -> PickerArgAttr
+ where
+ T: Pickable<'a>,
+ {
+ if flag.is_positional() {
+ PickerArgAttr::Positional
+ } else {
+ PickerArgAttr::Single
+ }
+ }
+
+ /// Determines if the given `PickerArg` represents a positional parameter and returns
+ /// `PickerArgAttr::PositionalMulti` if so. Otherwise, returns `PickerArgAttr::Multi`.
+ ///
+ /// # Parameters
+ ///
+ /// - `flag`: A reference to the [`PickerArg`] to evaluate.
+ #[inline(always)]
+ pub fn positional_or_multi<'a, T>(flag: &PickerArg<'a, T>) -> PickerArgAttr
+ where
+ T: Pickable<'a>,
+ {
+ if flag.is_positional() {
+ PickerArgAttr::PositionalMulti
+ } else {
+ PickerArgAttr::Multi
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_picker_flag_attr_ordering() {
+ // Multi > Single > Flag > Positional > PositionalMulti
+ assert!(PickerArgAttr::Multi > PickerArgAttr::Single);
+ assert!(PickerArgAttr::Multi > PickerArgAttr::Flag);
+ assert!(PickerArgAttr::Multi > PickerArgAttr::Positional);
+ assert!(PickerArgAttr::Multi > PickerArgAttr::PositionalMulti);
+
+ assert!(PickerArgAttr::Single > PickerArgAttr::Flag);
+ assert!(PickerArgAttr::Single > PickerArgAttr::Positional);
+ assert!(PickerArgAttr::Single > PickerArgAttr::PositionalMulti);
+
+ assert!(PickerArgAttr::Flag > PickerArgAttr::Positional);
+ assert!(PickerArgAttr::Flag > PickerArgAttr::PositionalMulti);
+
+ assert!(PickerArgAttr::Positional > PickerArgAttr::PositionalMulti);
+
+ // PartialOrd
+ assert!(PickerArgAttr::Multi >= PickerArgAttr::Single);
+ assert!(PickerArgAttr::Single >= PickerArgAttr::Flag);
+ assert!(PickerArgAttr::Flag >= PickerArgAttr::Positional);
+ assert!(PickerArgAttr::Positional >= PickerArgAttr::PositionalMulti);
+
+ assert!(PickerArgAttr::PositionalMulti < PickerArgAttr::Positional);
+ assert!(PickerArgAttr::Positional < PickerArgAttr::Flag);
+ assert!(PickerArgAttr::Flag < PickerArgAttr::Single);
+ assert!(PickerArgAttr::Single < PickerArgAttr::Multi);
+ }
+
+ #[test]
+ fn test_picker_flag_attr_sorting() {
+ // Sort
+ let mut values = vec![
+ PickerArgAttr::Flag,
+ PickerArgAttr::Single,
+ PickerArgAttr::Positional,
+ PickerArgAttr::Multi,
+ PickerArgAttr::PositionalMulti,
+ ];
+ values.sort();
+ assert_eq!(
+ values,
+ vec![
+ PickerArgAttr::PositionalMulti,
+ PickerArgAttr::Positional,
+ PickerArgAttr::Flag,
+ PickerArgAttr::Single,
+ PickerArgAttr::Multi,
+ ]
+ );
+ }
+}
diff --git a/mingling_picker/src/builtin.rs b/mingling_picker/src/builtin.rs
new file mode 100644
index 0000000..e855b08
--- /dev/null
+++ b/mingling_picker/src/builtin.rs
@@ -0,0 +1,4 @@
+mod pick_bool;
+mod pick_flag;
+mod pick_numbers;
+mod pick_string;
diff --git a/mingling_picker/src/builtin/pick_bool.rs b/mingling_picker/src/builtin/pick_bool.rs
new file mode 100644
index 0000000..ccc4424
--- /dev/null
+++ b/mingling_picker/src/builtin/pick_bool.rs
@@ -0,0 +1,22 @@
+use crate::parselib::{FlagMatcher, Matcher};
+use crate::pickable_needed::*;
+
+impl<'a> Pickable<'a> for bool {
+ fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::Flag
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ FlagMatcher::match_all(ctx.into())
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ if raw_strs.is_empty() {
+ // No matching flag found — signal NotFound so the fallback chain
+ // (default → route) gets a chance to run.
+ PickerArgResult::NotFound
+ } else {
+ PickerArgResult::Parsed(true)
+ }
+ }
+}
diff --git a/mingling_picker/src/builtin/pick_flag.rs b/mingling_picker/src/builtin/pick_flag.rs
new file mode 100644
index 0000000..b642a9a
--- /dev/null
+++ b/mingling_picker/src/builtin/pick_flag.rs
@@ -0,0 +1,21 @@
+use crate::parselib::{FlagMatcher, Matcher};
+use crate::pickable_needed::*;
+use crate::value::Flag;
+
+impl<'a> Pickable<'a> for Flag {
+ fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::Flag
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ FlagMatcher::match_all(ctx.into())
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ if raw_strs.is_empty() {
+ PickerArgResult::Parsed(Flag::Inactive)
+ } else {
+ PickerArgResult::Parsed(Flag::Active)
+ }
+ }
+}
diff --git a/mingling_picker/src/builtin/pick_numbers.rs b/mingling_picker/src/builtin/pick_numbers.rs
new file mode 100644
index 0000000..a5ab0a9
--- /dev/null
+++ b/mingling_picker/src/builtin/pick_numbers.rs
@@ -0,0 +1,80 @@
+use crate::{BoundaryCheck, SinglePickable, pickable_needed::*};
+
+macro_rules! impl_single_pickable_num {
+ ($($t:ty),+ $(,)?) => {
+ $(impl SinglePickable for $t {
+ fn pick_single(str: Option<&str>) -> PickerArgResult<Self> {
+ match str {
+ Some(s) => s.parse::<$t>().map(PickerArgResult::Parsed).unwrap_or(PickerArgResult::NotFound),
+ None => PickerArgResult::NotFound,
+ }
+ }
+ })+
+ };
+}
+
+/// Returns `true` if `raw` looks like an integer (digits, optional `+`/`-` prefix,
+/// no decimal point or exponent).
+fn is_int_like(raw: &str) -> bool {
+ let s = raw.trim();
+ let bytes = s.as_bytes();
+ if bytes.is_empty() {
+ return false;
+ }
+ let mut i = 0;
+ if bytes[0] == b'-' || bytes[0] == b'+' {
+ i = 1;
+ }
+ if i >= bytes.len() {
+ return false;
+ }
+ for &b in &bytes[i..] {
+ if !b.is_ascii_digit() {
+ return false;
+ }
+ }
+ true
+}
+
+/// Returns `true` if `raw` looks like a float (contains `.`, `e`, or `E`).
+fn is_float_like(raw: &str) -> bool {
+ let s = raw.trim();
+ s.contains('.') || s.contains('e') || s.contains('E')
+}
+
+impl_single_pickable_num! {
+ i8, i16, i32, i64, i128, isize,
+ u8, u16, u32, u64, u128, usize,
+ f32, f64,
+}
+
+// Integer boundary: only accept strings that look like integers.
+// Float-like strings trigger a boundary.
+macro_rules! impl_boundary_check_int {
+ ($($t:ty),+ $(,)?) => {
+ $(impl BoundaryCheck for $t {
+ fn check_boundary(raw: &str) -> bool {
+ !is_int_like(raw)
+ }
+ })+
+ };
+}
+
+impl_boundary_check_int! {
+ i8, i16, i32, i64, i128, isize,
+ u8, u16, u32, u64, u128, usize,
+}
+
+// Float boundary: only accept strings that look like floats.
+// Integer-like strings trigger a boundary.
+impl BoundaryCheck for f32 {
+ fn check_boundary(raw: &str) -> bool {
+ !is_float_like(raw) || raw.parse::<f32>().is_err()
+ }
+}
+
+impl BoundaryCheck for f64 {
+ fn check_boundary(raw: &str) -> bool {
+ !is_float_like(raw) || raw.parse::<f64>().is_err()
+ }
+}
diff --git a/mingling_picker/src/builtin/pick_string.rs b/mingling_picker/src/builtin/pick_string.rs
new file mode 100644
index 0000000..c96f667
--- /dev/null
+++ b/mingling_picker/src/builtin/pick_string.rs
@@ -0,0 +1,11 @@
+use crate::PickerArgResult::NotFound;
+use crate::{SinglePickable, pickable_needed::*};
+
+impl SinglePickable for String {
+ fn pick_single(str: Option<&str>) -> PickerArgResult<Self> {
+ match str {
+ Some(str) => PickerArgResult::Parsed(str.to_string()),
+ None => NotFound,
+ }
+ }
+}
diff --git a/mingling_picker/src/corebind.rs b/mingling_picker/src/corebind.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/mingling_picker/src/corebind.rs
@@ -0,0 +1 @@
+
diff --git a/mingling_picker/src/infos.rs b/mingling_picker/src/infos.rs
new file mode 100644
index 0000000..d2a0fce
--- /dev/null
+++ b/mingling_picker/src/infos.rs
@@ -0,0 +1,446 @@
+use crate::{Pickable, PickerArg};
+
+/// Represents the result of parsing or looking up a value.
+///
+/// This enum is generic over the type being parsed. It models three possible outcomes:
+/// - [`Unparsed`](PickerArgResult::Unparsed): The value has not yet been parsed (default).
+/// - [`Parsed`](PickerArgResult::Parsed): The value was successfully parsed into `Type`.
+/// - [`NotFound`](PickerArgResult::NotFound): The requested value could not be found.
+#[derive(Default)]
+pub enum PickerArgResult<Type> {
+ /// The value has not yet been parsed (default).
+ #[default]
+ Unparsed,
+
+ /// The value was successfully parsed into `Type`.
+ Parsed(Type),
+
+ /// The requested value could not be found.
+ NotFound,
+}
+
+impl<Type, E> From<Result<Type, E>> for PickerArgResult<Type> {
+ /// Converts a `Result<Type, E>` into a `PickerArgResult<Type>`.
+ ///
+ /// - `Ok(value)` maps to [`Parsed(value)`](PickerArgResult::Parsed).
+ /// - `Err(_)` maps to [`NotFound`](PickerArgResult::NotFound).
+ fn from(result: Result<Type, E>) -> Self {
+ match result {
+ Ok(value) => PickerArgResult::Parsed(value),
+ Err(_) => PickerArgResult::NotFound,
+ }
+ }
+}
+
+impl<Type> From<Option<Type>> for PickerArgResult<Type> {
+ /// Converts an `Option<Type>` into a `PickerArgResult<Type>`.
+ ///
+ /// - `Some(value)` maps to [`Parsed(value)`](PickerArgResult::Parsed).
+ /// - `None` maps to [`NotFound`](PickerArgResult::NotFound).
+ fn from(option: Option<Type>) -> Self {
+ match option {
+ Some(value) => PickerArgResult::Parsed(value),
+ None => PickerArgResult::NotFound,
+ }
+ }
+}
+
+impl<Type> PickerArgResult<Type> {
+ /// Returns `true` if the result is [`Parsed`](PickerArgResult::Parsed).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert!(result.is_parsed());
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert!(!result.is_parsed());
+ /// ```
+ pub fn is_parsed(&self) -> bool {
+ matches!(self, PickerArgResult::Parsed(_))
+ }
+
+ /// Returns `true` if the result is [`Parsed`](PickerArgResult::Parsed) or [`NotFound`](PickerArgResult::NotFound).
+ /// i.e., the value exists (was either found or not yet parsed).
+ /// Typically indicates the value was "found" in some sense.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert!(result.is_found());
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert!(result.is_found());
+ /// ```
+ pub fn is_found(&self) -> bool {
+ matches!(self, PickerArgResult::Parsed(_) | PickerArgResult::NotFound)
+ }
+
+ /// Returns `true` if the result is [`Unparsed`](PickerArgResult::Unparsed) or [`NotFound`](PickerArgResult::NotFound).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Unparsed;
+ /// assert!(result.is_err());
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(10);
+ /// assert!(!result.is_err());
+ /// ```
+ pub fn is_err(&self) -> bool {
+ !matches!(self, PickerArgResult::Parsed(_))
+ }
+
+ /// Returns `Some(&Type)` if [`Parsed`](PickerArgResult::Parsed), otherwise `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.parsed(), Some(&42));
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert_eq!(result.parsed(), None);
+ /// ```
+ pub fn parsed(&self) -> Option<&Type> {
+ if let PickerArgResult::Parsed(value) = self {
+ Some(value)
+ } else {
+ None
+ }
+ }
+
+ /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or panics with a given message.
+ ///
+ /// # Panics
+ /// Panics if the value is not [`Parsed`](PickerArgResult::Parsed), with a message including the provided `msg`.
+ ///
+ /// # Examples
+ ///
+ /// ```should_panic
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// result.expect("expected a parsed value");
+ /// ```
+ pub fn expect(self, msg: &str) -> Type {
+ match self {
+ PickerArgResult::Parsed(value) => value,
+ _ => panic!("{}", msg),
+ }
+ }
+
+ /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or panics.
+ ///
+ /// # Panics
+ /// Panics if the value is not [`Parsed`](PickerArgResult::Parsed).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.unwrap(), 42);
+ /// ```
+ ///
+ /// ```should_panic
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// result.unwrap();
+ /// ```
+ pub fn unwrap(self) -> Type {
+ match self {
+ PickerArgResult::Parsed(value) => value,
+ PickerArgResult::Unparsed => {
+ panic!("called `PickerArgResult::unwrap()` on an `Unparsed` value")
+ }
+ PickerArgResult::NotFound => {
+ panic!("called `PickerArgResult::unwrap()` on a `NotFound` value")
+ }
+ }
+ }
+
+ /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or a provided `default`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.unwrap_or(0), 42);
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert_eq!(result.unwrap_or(0), 0);
+ /// ```
+ pub fn unwrap_or(self, default: Type) -> Type {
+ match self {
+ PickerArgResult::Parsed(value) => value,
+ _ => default,
+ }
+ }
+
+ /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or computes it from a closure.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.unwrap_or_else(|| 0), 42);
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert_eq!(result.unwrap_or_else(|| 0), 0);
+ /// ```
+ pub fn unwrap_or_else<F: FnOnce() -> Type>(self, f: F) -> Type {
+ match self {
+ PickerArgResult::Parsed(value) => value,
+ _ => f(),
+ }
+ }
+
+ /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or the default value of `Type`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.unwrap_or_default(), 42);
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert_eq!(result.unwrap_or_default(), 0);
+ /// ```
+ pub fn unwrap_or_default(self) -> Type
+ where
+ Type: Default,
+ {
+ match self {
+ PickerArgResult::Parsed(value) => value,
+ _ => Type::default(),
+ }
+ }
+
+ /// Converts `PickerArgResult<Type>` into `Option<Type>`.
+ ///
+ /// Returns `Some(Type)` if [`Parsed`](PickerArgResult::Parsed), otherwise `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.to_option(), Some(42));
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert_eq!(result.to_option(), None);
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Unparsed;
+ /// assert_eq!(result.to_option(), None);
+ /// ```
+ pub fn to_option(self) -> Option<Type> {
+ match self {
+ PickerArgResult::Parsed(value) => Some(value),
+ _ => None,
+ }
+ }
+}
+
+/// Represents metadata about a command-line argument or flag.
+///
+/// This struct stores all relevant information about a tag/argument that can be used
+/// for parsing command-line inputs. It includes the short form (e.g., `-n`), long form
+/// (e.g., `--name`), aliases, and various flags that control parsing behavior.
+pub struct PickerArgInfo<'a> {
+ /// The short form of the tag, e.g. `'n'` for `-n`.
+ pub short: Option<char>,
+ /// The long form of the tag, e.g. `"name"` for `--name`.
+ pub long: Option<&'a str>,
+ /// Alternative names for the tag, e.g. `["-N", "--nickname"]`.
+ pub alias: Option<Vec<&'a str>>,
+ /// Whether this tag is a positional argument (no `-` or `--` prefix).
+ pub positional: bool,
+ /// Whether this tag is optional or required.
+ pub optional: bool,
+ /// Whether this tag can accept multiple values.
+ pub multi: bool,
+ /// Whether this tag participates in parsing after a `--` separator.
+ pub is_flag: bool,
+}
+
+impl<'a, T> From<PickerArg<'a, T>> for PickerArgInfo<'a>
+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,
+ }
+ }
+}
+
+impl<'a, T: Pickable<'a>> From<&'a PickerArg<'a, T>> for PickerArgInfo<'a> {
+ fn from(value: &'a 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,
+ }
+ }
+}
+
+impl<'a> PickerArgInfo<'a> {
+ /// Create a new `PickerTag` with default values.
+ pub fn new() -> Self {
+ Self {
+ short: None,
+ long: None,
+ alias: None,
+ positional: false,
+ optional: false,
+ multi: false,
+ is_flag: false,
+ }
+ }
+
+ /// Set the short flag (e.g., `'n'` for `-n`).
+ pub fn with_short(mut self, short: char) -> Self {
+ self.short = Some(short);
+ self
+ }
+
+ /// Set the long flag (e.g., `"name"` for `--name`).
+ pub fn with_long(mut self, long: &'a str) -> Self {
+ self.long = Some(long);
+ self
+ }
+
+ /// Set aliases for the tag.
+ pub fn with_alias(mut self, alias: Vec<&'a str>) -> Self {
+ self.alias = Some(alias);
+ self
+ }
+
+ /// Mark the tag as positional.
+ pub fn with_positional(mut self, positional: bool) -> Self {
+ self.positional = positional;
+ self
+ }
+
+ /// Mark the tag as optional.
+ pub fn with_optional(mut self, optional: bool) -> Self {
+ self.optional = optional;
+ self
+ }
+
+ /// Mark the tag as multi-value.
+ pub fn with_multi(mut self, multi: bool) -> Self {
+ self.multi = multi;
+ self
+ }
+
+ /// Mark the tag as a flag that participates in parsing after `--`.
+ pub fn with_is_flag(mut self, is_flag: bool) -> Self {
+ self.is_flag = is_flag;
+ self
+ }
+
+ /// Set the short flag (e.g., `'n'` for `-n`).
+ pub fn set_short(&mut self, short: char) -> &mut Self {
+ self.short = Some(short);
+ self
+ }
+
+ /// Set the long flag (e.g., `"name"` for `--name`).
+ pub fn set_long(&mut self, long: &'a str) -> &mut Self {
+ self.long = Some(long);
+ self
+ }
+
+ /// Set aliases for the tag.
+ pub fn set_alias(&mut self, alias: Vec<&'a str>) -> &mut Self {
+ self.alias = Some(alias);
+ self
+ }
+
+ /// Set whether this tag is positional.
+ pub fn set_positional(&mut self, positional: bool) -> &mut Self {
+ self.positional = positional;
+ self
+ }
+
+ /// Set whether this tag is optional.
+ pub fn set_optional(&mut self, optional: bool) -> &mut Self {
+ self.optional = optional;
+ self
+ }
+
+ /// Set whether this tag accepts multiple values.
+ pub fn set_multi(&mut self, multi: bool) -> &mut Self {
+ self.multi = multi;
+ self
+ }
+
+ /// Set whether this tag participates in parsing after a `--` separator.
+ pub fn set_is_flag(&mut self, is_flag: bool) -> &mut Self {
+ self.is_flag = is_flag;
+ self
+ }
+}
+
+impl<'a> Default for PickerArgInfo<'a> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/mingling_picker/src/lib.rs b/mingling_picker/src/lib.rs
index 3c3251d..afc7aca 100644
--- a/mingling_picker/src/lib.rs
+++ b/mingling_picker/src/lib.rs
@@ -1 +1,43 @@
-pub mod prelude {}
+mod builtin;
+
+mod picker;
+pub use picker::*;
+
+mod pickable;
+pub use pickable::*;
+
+mod arg;
+pub use arg::*;
+
+mod infos;
+pub use infos::*;
+
+pub mod parselib;
+
+pub mod value;
+
+pub mod prelude {
+ pub use crate::IntoPicker;
+}
+
+pub mod macros {
+ pub use mingling_picker_macros::*;
+}
+
+/// Provides the types necessary for implementing the `Pickable` trait
+pub mod pickable_needed {
+ pub use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext};
+}
+
+/// Provides the types necessary for implementing the `Matcher` trait
+pub mod matcher_needed {
+ pub use crate::PickerArgInfo;
+ pub use crate::parselib::{MaskedArg, Matcher, ParserStyle};
+}
+
+#[cfg(feature = "mingling_support")]
+mod corebind;
+
+#[allow(unused_imports)]
+#[cfg(feature = "mingling_support")]
+pub use corebind::*;
diff --git a/mingling_picker/src/parselib.rs b/mingling_picker/src/parselib.rs
new file mode 100644
index 0000000..7fbd606
--- /dev/null
+++ b/mingling_picker/src/parselib.rs
@@ -0,0 +1,144 @@
+mod flag_matcher;
+pub use flag_matcher::*;
+
+mod arg_matcher;
+pub use arg_matcher::*;
+
+mod multi_arg_matcher;
+pub use multi_arg_matcher::*;
+
+mod pos_matcher;
+pub use pos_matcher::*;
+
+mod single_matcher;
+pub use single_matcher::*;
+
+mod style;
+pub use style::*;
+
+mod utils;
+pub use utils::*;
+
+use crate::{PickerArgInfo, PickerArgs};
+
+/// Represents a single argument with its original raw string and index.
+///
+/// This is used during pattern matching to provide context about
+/// which argument is being processed.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct MaskedArg<'a> {
+ /// The raw string value of the argument.
+ pub raw: &'a str,
+ /// The original index of the argument in the full argument list.
+ pub raw_idx: usize,
+}
+
+/// Trait for defining matching logic against masked arguments.
+///
+/// Implementors can define custom strategies for matching one or all
+/// arguments that pass through a mask filter.
+pub trait Matcher {
+ /// Called when only one match is needed.
+ ///
+ /// Returns the index of the first matched argument, or `None` if no match.
+ fn on_match_one(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Option<usize>;
+
+ /// Called when all matches are needed.
+ ///
+ /// Returns a vector of indices of all matched arguments.
+ fn on_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Vec<usize>;
+
+ /// Convenience method that builds masked arguments from `PickerArgs` and a mask,
+ /// then calls `on_match_one`.
+ fn match_one<'a>(ctx: MatcherContext<'a>) -> Option<usize> {
+ let masked_args = build_masked_args(ctx.args, ctx.mask);
+ Self::on_match_one(masked_args.as_slice(), ctx.style, ctx.arg_info)
+ }
+
+ /// Convenience method that builds masked arguments from `PickerArgs` and a mask,
+ /// then calls `on_match_all`.
+ fn match_all<'a>(ctx: MatcherContext<'a>) -> Vec<usize> {
+ let masked_args = build_masked_args(ctx.args, ctx.mask);
+ Self::on_match_all(masked_args.as_slice(), ctx.style, ctx.arg_info)
+ }
+}
+
+/// Context for matcher operations
+///
+/// This struct bundles together the key pieces of data needed during matching:
+/// - `args`: The full set of parsed arguments.
+/// - `mask`: A byte mask indicating which arguments are currently active (non-zero = active).
+/// - `style`: The parsing style configuration.
+pub struct MatcherContext<'a> {
+ /// The full set of parsed arguments.
+ pub args: &'a PickerArgs<'a>,
+
+ /// A byte mask where non-zero values indicate the argument at that position is active/should be matched.
+ pub mask: &'a [u8],
+
+ /// The parsing style configuration.
+ pub style: &'a ParserStyle<'a>,
+
+ /// Metadata about the command-line argument/flag being processed.
+ ///
+ /// Contains information such as short form (`-n`), long form (`--name`),
+ /// aliases, and parsing flags (positional, optional, multi, is_flag).
+ /// Used by matchers to make decisions based on argument characteristics.
+ pub arg_info: &'a PickerArgInfo<'a>,
+}
+
+impl<'a> From<&'a crate::TagPhaseContext<'a>> for MatcherContext<'a> {
+ fn from(ctx: &'a crate::TagPhaseContext<'a>) -> Self {
+ MatcherContext {
+ args: ctx.args,
+ mask: ctx.mask,
+ style: ParserStyle::global_style(),
+ arg_info: ctx.arg_info,
+ }
+ }
+}
+
+impl<'a> From<crate::TagPhaseContext<'a>> for MatcherContext<'a> {
+ fn from(ctx: crate::TagPhaseContext<'a>) -> Self {
+ MatcherContext {
+ args: ctx.args,
+ mask: ctx.mask,
+ style: ParserStyle::global_style(),
+ arg_info: ctx.arg_info,
+ }
+ }
+}
+
+#[inline(always)]
+fn is_masked(mask: &[u8], idx: usize) -> bool {
+ idx < mask.len() && mask[idx] != 0
+}
+
+#[inline(always)]
+fn build_masked_args<'a>(args: &'a PickerArgs, mask: &'a [u8]) -> Vec<MaskedArg<'a>> {
+ let mut cidx = 0;
+ args.iter()
+ .filter_map(|r| {
+ let idx = cidx;
+ cidx += 1;
+ // Include args where mask is 0 (available/not yet claimed).
+ // mask[i] = 0 means available; mask[i] != 0 means already claimed.
+ if !is_masked(mask, idx) {
+ Some(MaskedArg {
+ raw: r,
+ raw_idx: idx,
+ })
+ } else {
+ None
+ }
+ })
+ .collect()
+}
diff --git a/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs
new file mode 100644
index 0000000..38bb9cc
--- /dev/null
+++ b/mingling_picker/src/parselib/arg_matcher.rs
@@ -0,0 +1,142 @@
+use crate::{
+ matcher_needed::*,
+ parselib::{build_possible_flags, seek_end_of_options},
+};
+
+/// `ArgMatcher` is used for parameters that carry a single value.
+///
+/// It handles two scenarios:
+///
+/// **Named** — `--name Alice` or `--name=Alice`.
+/// Each flag occurrence consumes **one** following argument as its value,
+/// regardless of what it is (even if it looks like a flag).
+/// This ensures the mask correctly claims the value slot; validation is
+/// the `Pickable`'s responsibility.
+///
+/// **Positional** — no flag prefix, matched by position.
+///
+/// # Examples
+///
+/// | Input | `on_match_one` | `on_match_all` |
+/// |-------|----------------|----------------|
+/// | `--name Alice` | `[0, 1]` (via Pickable tag) | `[0, 1]` |
+/// | `--name=Alice` | `[0]` | `[0]` |
+/// | `--val a --val b` | `[0, 1]` | `[0, 1, 2, 3]` |
+///
+/// Args after `--` are ignored.
+pub struct ArgMatcher;
+
+impl ArgMatcher {
+ /// Check whether `raw` matches `flag_str`, optionally with an inline value
+ /// separated by the style's value separator (`=` for Unix, `:` for PowerShell).
+ #[inline(always)]
+ fn matches(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool {
+ let eq_match =
+ |r: &str, f: &str| r.len() > f.len() && r.as_bytes().get(f.len()) == Some(&(sep as u8));
+
+ if case_sensitive {
+ raw == flag_str || (raw.starts_with(flag_str) && eq_match(raw, flag_str))
+ } else {
+ raw.eq_ignore_ascii_case(flag_str)
+ || (raw.len() > flag_str.len()
+ && raw[..flag_str.len()].eq_ignore_ascii_case(flag_str)
+ && raw.as_bytes()[flag_str.len()] == sep as u8)
+ }
+ }
+
+ /// Check whether the argument contains its value inline via the style's
+ /// value separator (eq mode), so no extra mask slot is needed.
+ #[inline(always)]
+ fn is_inline_value(raw: &str, flag_str: &str, sep: char) -> bool {
+ raw.len() > flag_str.len() && raw.as_bytes().get(flag_str.len()) == Some(&(sep as u8))
+ }
+}
+
+impl Matcher for ArgMatcher {
+ fn on_match_one(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Option<usize> {
+ if arg_info.positional {
+ return args.first().map(|a| a.raw_idx);
+ }
+
+ let possible_flags = build_possible_flags(style, arg_info);
+ let end = seek_end_of_options(args, style);
+ let sep = style.value_separator;
+
+ for arg in args {
+ if end.is_some_and(|e| arg.raw_idx >= e) {
+ break;
+ }
+
+ let matched = possible_flags
+ .iter()
+ .any(|f| Self::matches(arg.raw, f, style.case_sensitive, sep));
+ if matched {
+ return Some(arg.raw_idx);
+ }
+ }
+
+ None
+ }
+
+ fn on_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Vec<usize> {
+ if arg_info.positional {
+ let end = seek_end_of_options(args, style);
+ return args
+ .iter()
+ .take_while(|a| end.is_none_or(|e| a.raw_idx < e))
+ .map(|a| a.raw_idx)
+ .collect();
+ }
+
+ let possible_flags = build_possible_flags(style, arg_info);
+ let end = seek_end_of_options(args, style);
+ let sep = style.value_separator;
+
+ let mut result = Vec::new();
+ let mut i = 0;
+ while i < args.len() {
+ if end.is_some_and(|e| args[i].raw_idx >= e) {
+ break;
+ }
+
+ let matched = possible_flags
+ .iter()
+ .any(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep));
+
+ if matched {
+ let flag_str = possible_flags
+ .iter()
+ .find(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep))
+ .expect("already matched");
+
+ result.push(args[i].raw_idx);
+
+ if !Self::is_inline_value(args[i].raw, flag_str, sep) {
+ if i + 1 < args.len()
+ // Don't consume `--` (end-of-options marker) as a value.
+ && end.is_none_or(|e| args[i + 1].raw_idx < e)
+ {
+ result.push(args[i + 1].raw_idx);
+ i += 2;
+ continue;
+ }
+ i += 1;
+ continue;
+ }
+ i += 1;
+ continue;
+ }
+ i += 1;
+ }
+
+ result
+ }
+}
diff --git a/mingling_picker/src/parselib/flag_matcher.rs b/mingling_picker/src/parselib/flag_matcher.rs
new file mode 100644
index 0000000..e93d35a
--- /dev/null
+++ b/mingling_picker/src/parselib/flag_matcher.rs
@@ -0,0 +1,82 @@
+use crate::{
+ matcher_needed::*,
+ parselib::{build_possible_flags, get_seeked_first, multi_seek_eq, seek_end_of_options},
+};
+
+/// `FlagMatcher` is used to match flags in command-line arguments.
+///
+/// Flags typically start with `-` or `--` (e.g., `-h`, `--help`),
+/// and do not carry additional values. This matcher is responsible for finding
+/// these flags in the argument list, taking into account that flags after `--`
+/// (end-of-options marker) should not be matched.
+pub struct FlagMatcher;
+
+impl Matcher for FlagMatcher {
+ fn on_match_one(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Option<usize> {
+ let possible_flags = build_possible_flags(style, arg_info);
+ let flag_refs: Vec<&str> = possible_flags.iter().map(|s| s.as_str()).collect();
+ let end_of_options = seek_end_of_options(args, style);
+
+ let result = get_seeked_first(multi_seek_eq(args, &flag_refs, style.case_sensitive));
+
+ match (end_of_options, result) {
+ (Some(end), Some(current)) if current > end => None,
+ _ => result,
+ }
+ }
+
+ fn on_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Vec<usize> {
+ let possible_flags = build_possible_flags(style, arg_info);
+ single_pass_match_all(args, style, &possible_flags)
+ }
+}
+
+/// Single-pass match: finds the `--` marker and matching flags in one iteration.
+fn single_pass_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ possible_flags: &[String],
+) -> Vec<usize> {
+ let flag_refs: Vec<&str> = possible_flags.iter().map(|s| s.as_str()).collect();
+ let eoo = style.end_of_options;
+ let case_sensitive = style.case_sensitive;
+
+ let mut end_pos: Option<usize> = None;
+ let mut matches: Vec<usize> = Vec::new();
+
+ for arg in args {
+ if end_pos.is_none() {
+ let is_eoo = if case_sensitive {
+ arg.raw == eoo
+ } else {
+ arg.raw.eq_ignore_ascii_case(eoo)
+ };
+ if is_eoo {
+ end_pos = Some(arg.raw_idx);
+ continue;
+ }
+ }
+
+ // Only match flags before the end-of-options marker.
+ if end_pos.is_none() {
+ let matched = if case_sensitive {
+ flag_refs.contains(&arg.raw)
+ } else {
+ flag_refs.iter().any(|s| arg.raw.eq_ignore_ascii_case(s))
+ };
+ if matched {
+ matches.push(arg.raw_idx);
+ }
+ }
+ }
+
+ matches
+}
diff --git a/mingling_picker/src/parselib/multi_arg_matcher.rs b/mingling_picker/src/parselib/multi_arg_matcher.rs
new file mode 100644
index 0000000..e8bdb71
--- /dev/null
+++ b/mingling_picker/src/parselib/multi_arg_matcher.rs
@@ -0,0 +1,141 @@
+use crate::{
+ matcher_needed::*,
+ parselib::{build_possible_flags, seek_end_of_options},
+};
+
+/// `MultiArgMatcher` matches a named flag and **all** consecutive arguments
+/// that follow it, stopping at the next flag, the `--` marker, or the end
+/// of the argument list.
+///
+/// This is the tag implementation for `Multi` and `GreedyMulti` types
+/// such as `Vec<String>` (`--files a.txt b.txt`).
+///
+/// # Behavior
+///
+/// | Input | `on_match_all` |
+/// |-------|----------------|
+/// | `--val a b --val d e` | `[0, 1, 2, 5, 6]` (two groups) |
+/// | `--val=1 2` | `[0, 1]` (eq mode + one extra value) |
+///
+/// Args after `--` are ignored.
+pub struct MultiArgMatcher;
+
+impl Matcher for MultiArgMatcher {
+ fn on_match_one(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Option<usize> {
+ if arg_info.positional {
+ return args.first().map(|a| a.raw_idx);
+ }
+
+ let possible_flags = build_possible_flags(style, arg_info);
+ let end = seek_end_of_options(args, style);
+ let sep = style.value_separator;
+
+ for arg in args {
+ if end.is_some_and(|e| arg.raw_idx >= e) {
+ break;
+ }
+ let matched = possible_flags
+ .iter()
+ .any(|f| Self::flag_match(arg.raw, f, style.case_sensitive, sep));
+ if matched {
+ return Some(arg.raw_idx);
+ }
+ }
+ None
+ }
+
+ fn on_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Vec<usize> {
+ if arg_info.positional {
+ let end = seek_end_of_options(args, style);
+ return args
+ .iter()
+ .take_while(|a| end.is_none_or(|e| a.raw_idx < e))
+ .map(|a| a.raw_idx)
+ .collect();
+ }
+
+ let possible_flags = build_possible_flags(style, arg_info);
+ let end = seek_end_of_options(args, style);
+ let sep = style.value_separator;
+ let is_flag = |raw: &str| {
+ raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix)
+ };
+ let is_our_flag = |raw: &str| {
+ possible_flags
+ .iter()
+ .any(|f| Self::flag_match(raw, f, style.case_sensitive, sep))
+ };
+
+ let mut result = Vec::new();
+ let mut i = 0;
+ while i < args.len() {
+ if end.is_some_and(|e| args[i].raw_idx >= e) {
+ break;
+ }
+
+ let matched = is_our_flag(args[i].raw);
+
+ if matched {
+ result.push(args[i].raw_idx);
+
+ if Self::is_eq_match(args[i].raw, &possible_flags, style.case_sensitive, sep) {
+ i += 1;
+ while i < args.len()
+ && end.is_none_or(|e| args[i].raw_idx < e)
+ && !is_flag(args[i].raw)
+ {
+ result.push(args[i].raw_idx);
+ i += 1;
+ }
+ continue;
+ }
+
+ i += 1;
+ while i < args.len()
+ && end.is_none_or(|e| args[i].raw_idx < e)
+ && !is_flag(args[i].raw)
+ {
+ result.push(args[i].raw_idx);
+ i += 1;
+ }
+ continue;
+ }
+ i += 1;
+ }
+
+ result
+ }
+}
+
+impl MultiArgMatcher {
+ #[inline(always)]
+ fn flag_match(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool {
+ let eq = |r: &str, f: &str| r.len() > f.len() && r.as_bytes().get(f.len()) == Some(&(sep as u8));
+
+ if case_sensitive {
+ raw == flag_str || (raw.starts_with(flag_str) && eq(raw, flag_str))
+ } else {
+ raw.eq_ignore_ascii_case(flag_str)
+ || (raw.len() > flag_str.len()
+ && raw[..flag_str.len()].eq_ignore_ascii_case(flag_str)
+ && raw.as_bytes()[flag_str.len()] == sep as u8)
+ }
+ }
+
+ #[inline(always)]
+ fn is_eq_match(raw: &str, flags: &[String], case_sensitive: bool, sep: char) -> bool {
+ flags.iter().any(|f| {
+ Self::flag_match(raw, f, case_sensitive, sep)
+ && raw.len() > f.len()
+ && raw.as_bytes().get(f.len()) == Some(&(sep as u8))
+ })
+ }
+}
diff --git a/mingling_picker/src/parselib/pos_matcher.rs b/mingling_picker/src/parselib/pos_matcher.rs
new file mode 100644
index 0000000..279e01e
--- /dev/null
+++ b/mingling_picker/src/parselib/pos_matcher.rs
@@ -0,0 +1,71 @@
+use crate::{matcher_needed::*, parselib::seek_end_of_options};
+
+/// `PositionalMatcher` matches positional arguments — values not associated
+/// with any named flag.
+///
+/// # Rules
+///
+/// * Before `--`: skips any argument that starts with the style's long or short
+/// prefix (those belong to named matchers).
+/// * After `--`: takes **everything** — the `--` marker signals that all
+/// remaining values are positional, even if they look like flags.
+/// * Runs at the lowest priority (see [`PickerArgAttr::Positional`](crate::PickerArgAttr::Positional)).
+pub struct PositionalMatcher;
+
+impl PositionalMatcher {
+ /// Check whether `raw` looks like a named flag (starts with a prefix).
+ #[inline(always)]
+ fn is_flag_like(raw: &str, style: &ParserStyle) -> bool {
+ raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix)
+ }
+}
+
+impl Matcher for PositionalMatcher {
+ fn on_match_one(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ _arg_info: &PickerArgInfo,
+ ) -> Option<usize> {
+ let end = seek_end_of_options(args, style);
+
+ for arg in args {
+ if end.is_some_and(|e| arg.raw_idx == e) {
+ // Hit `--`: everything from here on is positional,
+ // including the first arg after `--`.
+ continue;
+ }
+ if end.is_some_and(|e| arg.raw_idx > e) {
+ // After `--`: accept everything.
+ return Some(arg.raw_idx);
+ }
+ // Before `--`: skip flag-like args.
+ if !Self::is_flag_like(arg.raw, style) {
+ return Some(arg.raw_idx);
+ }
+ }
+
+ None
+ }
+
+ fn on_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ _arg_info: &PickerArgInfo,
+ ) -> Vec<usize> {
+ let end = seek_end_of_options(args, style);
+ let mut after_end = false;
+ let mut result = Vec::new();
+
+ for arg in args {
+ if end.is_some_and(|e| arg.raw_idx == e) {
+ after_end = true;
+ continue;
+ }
+ if after_end || !Self::is_flag_like(arg.raw, style) {
+ result.push(arg.raw_idx);
+ }
+ }
+
+ result
+ }
+}
diff --git a/mingling_picker/src/parselib/single_matcher.rs b/mingling_picker/src/parselib/single_matcher.rs
new file mode 100644
index 0000000..25c4741
--- /dev/null
+++ b/mingling_picker/src/parselib/single_matcher.rs
@@ -0,0 +1,59 @@
+use crate::TagPhaseContext;
+use crate::parselib::{ArgMatcher, Matcher, ParserStyle, PositionalMatcher};
+
+/// `SingleMatcher` is a composite matcher for single-value parameters.
+///
+/// It delegates to [`PositionalMatcher`] for positional args and
+/// [`ArgMatcher`] for named args, adding a guard: if a named flag
+/// captures only itself with no inline value (eq mode), the result
+/// is cleared so that [`Pickable::pick`](crate::Pickable::pick) receives `[]` → `NotFound`.
+///
+/// This is the standard tag implementation for all `Single`-type
+/// `Pickable` implementations (e.g., `String`, `i32`, `u64`).
+pub struct SingleMatcher;
+
+impl SingleMatcher {
+ /// Match a single positional value or a named flag+value pair.
+ ///
+ /// For named args, only complete pairs (flag + value) are kept.
+ /// Flag occurrences without a following value or inline separator
+ /// are dropped so they remain available for other matchers.
+ #[inline(always)]
+ pub fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ if ctx.arg_info.positional {
+ PositionalMatcher::match_one(ctx.into())
+ .map(|i| vec![i])
+ .unwrap_or_default()
+ } else {
+ let args = ctx.args;
+ let positions = ArgMatcher::match_all(ctx.into());
+ let sep = ParserStyle::global_style().value_separator;
+
+ // Walk pairs: [flag, value, flag, value, ...]
+ // Drop any flag that has no following value and no inline separator.
+ let mut i = 0;
+ let mut result = Vec::with_capacity(positions.len());
+ while i < positions.len() {
+ let flag_idx = positions[i];
+ if let Some(raw) = args.get(flag_idx)
+ && raw.contains(sep)
+ {
+ // Eq mode: value is inline, keep just the flag.
+ result.push(flag_idx);
+ i += 1;
+ continue;
+ }
+ if i + 1 < positions.len() {
+ // Pair: flag + value.
+ result.push(flag_idx);
+ result.push(positions[i + 1]);
+ i += 2;
+ } else {
+ // Flag without value — drop it.
+ i += 1;
+ }
+ }
+ result
+ }
+ }
+}
diff --git a/mingling_picker/src/parselib/style.rs b/mingling_picker/src/parselib/style.rs
new file mode 100644
index 0000000..4ea161f
--- /dev/null
+++ b/mingling_picker/src/parselib/style.rs
@@ -0,0 +1,225 @@
+use std::sync::OnceLock;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+use crate::parselib::ParserStyleNamingCase::{Kebab, Pascal};
+
+/// Defines the style of command-line argument parsing (prefixes, separators, etc.).
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct ParserStyle<'a> {
+ /// End-of-options marker (e.g., `--`)
+ pub end_of_options: &'a str,
+
+ /// Prefix for long options (e.g., `--` or `/`)
+ pub long_prefix: &'a str,
+
+ /// Prefix for short options (e.g., `-` or `/`)
+ pub short_prefix: &'a str,
+
+ /// Prefix for combined short flags (e.g., `-abc`)
+ pub combine_prefix: &'a str,
+
+ /// Separator between name and value (e.g., `=` or `:`)
+ pub value_separator: char,
+
+ /// Whether option names are case-sensitive
+ pub case_sensitive: bool,
+
+ /// Whether combining short flags is allowed (e.g., `-abc` for `-a -b -c`)
+ pub allow_combine: bool,
+
+ /// Naming case
+ pub naming_case: ParserStyleNamingCase,
+}
+
+impl<'a> ParserStyle<'a> {
+ /// Formats a flag (short or long) into a full command-line option string.
+ ///
+ /// This method takes any type that can be converted into a `FlagStr` and produces
+ /// a complete option string by prepending the appropriate prefix.
+ ///
+ /// # Examples
+ ///
+ /// ```ignore
+ /// use mingling_picker::parselib::{ParserStyle, FlagStr, UNIX_STYLE};
+ /// let style = &UNIX_STYLE;
+ ///
+ /// assert_eq!(style.flag_string('v'), "-v");
+ /// assert_eq!(style.flag_string("verbose"), "--verbose");
+ /// ```
+ ///
+ /// # Parameters
+ ///
+ /// * `flag` - A value that can be converted to `FlagStr`, either a `char` for short flags
+ /// or a `&str` for long flags.
+ ///
+ /// # Returns
+ ///
+ /// A `String` with the prefix and the flag name combined.
+ #[must_use]
+ #[inline(always)]
+ pub fn flag_string<F>(&self, flag: F) -> String
+ where
+ F: Into<FlagStr<'a>>,
+ {
+ match flag.into() {
+ FlagStr::Short(short) => format!("{}{}", self.short_prefix, short),
+ FlagStr::Long(long) => format!("{}{}", self.long_prefix, long),
+ }
+ }
+}
+
+/// Represents a flag name for command-line argument parsing.
+///
+/// This enum can hold either a short flag (a single character, e.g., `'v'` for `-v`)
+/// or a long flag (a string, e.g., `"verbose"` for `--verbose`).
+///
+/// # Examples
+///
+/// ```
+/// use mingling_picker::parselib::FlagStr;
+///
+/// let short: FlagStr = 'v'.into();
+/// let long: FlagStr = "verbose".into();
+/// ```
+pub enum FlagStr<'a> {
+ /// A short flag represented by a single character.
+ Short(char),
+ /// A long flag represented by a string slice.
+ Long(&'a str),
+}
+
+impl<'a> From<char> for FlagStr<'a> {
+ /// Converts a single character into a `FlagStr::Short`.
+ fn from(c: char) -> Self {
+ FlagStr::Short(c)
+ }
+}
+
+impl<'a> From<&'a str> for FlagStr<'a> {
+ /// Converts a string slice into a `FlagStr::Long`.
+ fn from(s: &'a str) -> Self {
+ FlagStr::Long(s)
+ }
+}
+
+impl<'a> From<&'a String> for FlagStr<'a> {
+ /// Converts a reference to a `String` into a `FlagStr::Long`.
+ fn from(s: &'a String) -> Self {
+ FlagStr::Long(s.as_str())
+ }
+}
+
+#[repr(u8)]
+#[derive(Default, Clone, Copy, PartialEq, Eq)]
+pub enum ParserStyleNamingCase {
+ /// snake_case format (e.g., `brew_coffee`)
+ #[default]
+ Snake,
+ /// camelCase format (e.g., `brewCoffee`)
+ Camel,
+ /// PascalCase format (e.g., `BrewCoffee`)
+ Pascal,
+ /// kebab-case format (e.g., `brew-coffee`)
+ Kebab,
+ /// dot.case format (e.g., `brew.coffee`)
+ Dot,
+ /// Title Case format (e.g., `Brew Coffee`)
+ Title,
+ /// lower case format (e.g., `brew coffee`)
+ Lower,
+ /// UPPER CASE format (e.g., `BREW COFFEE`)
+ Upper,
+}
+
+impl ParserStyleNamingCase {
+ /// Converts the input string `s` to the naming case represented by this variant.
+ ///
+ /// This method takes any type `S` that can be converted into a `String` and
+ /// produced from a `String`, applies the corresponding case transformation,
+ /// and returns the result.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::parselib::ParserStyleNamingCase;
+ ///
+ /// let camel = ParserStyleNamingCase::Camel;
+ /// assert_eq!(camel.convert("brew_coffee".to_string()), "brewCoffee");
+ ///
+ /// let kebab = ParserStyleNamingCase::Kebab;
+ /// assert_eq!(kebab.convert("BrewCoffee".to_string()), "brew-coffee");
+ /// ```
+ pub fn convert<S>(&self, s: S) -> S
+ where
+ S: Into<String> + From<String>,
+ {
+ match self {
+ ParserStyleNamingCase::Camel => just_fmt::camel_case!(s.into()).into(),
+ ParserStyleNamingCase::Pascal => just_fmt::pascal_case!(s.into()).into(),
+ ParserStyleNamingCase::Kebab => just_fmt::kebab_case!(s.into()).into(),
+ ParserStyleNamingCase::Snake => just_fmt::snake_case!(s.into()).into(),
+ ParserStyleNamingCase::Dot => just_fmt::dot_case!(s.into()).into(),
+ ParserStyleNamingCase::Title => just_fmt::title_case!(s.into()).into(),
+ ParserStyleNamingCase::Lower => just_fmt::lower_case!(s.into()).into(),
+ ParserStyleNamingCase::Upper => just_fmt::upper_case!(s.into()).into(),
+ }
+ }
+}
+
+/// Unix-like style (e.g., `--verbose`, `-v`, `--name=value`)
+pub const UNIX_STYLE: ParserStyle = ParserStyle {
+ end_of_options: "--",
+ long_prefix: "--",
+ short_prefix: "-",
+ combine_prefix: "-",
+ value_separator: '=',
+ case_sensitive: true,
+ allow_combine: true,
+ naming_case: Kebab,
+};
+
+/// PowerShell style (e.g., `-Verbose`, `-Name:value`)
+pub const POWERSHELL_STYLE: ParserStyle = ParserStyle {
+ end_of_options: "--",
+ long_prefix: "-",
+ short_prefix: "-",
+ combine_prefix: "-",
+ value_separator: ':',
+ case_sensitive: false,
+ allow_combine: false,
+ naming_case: Pascal,
+};
+
+/// Windows-style command-line (e.g., `/Verbose`, `/Name:value`)
+pub const WINDOWS_STYLE: ParserStyle = ParserStyle {
+ end_of_options: "--",
+ long_prefix: "/",
+ short_prefix: "/",
+ combine_prefix: "/",
+ value_separator: ':',
+ case_sensitive: false,
+ allow_combine: false,
+ naming_case: Pascal,
+};
+
+static GLOBAL_STYLE: OnceLock<ParserStyle<'static>> = OnceLock::new();
+static GLOBAL_STYLE_SET: AtomicBool = AtomicBool::new(false);
+
+impl<'a> ParserStyle<'a> {
+ /// Sets the global parser style.
+ ///
+ /// This function can only be called once. Subsequent calls will have no effect.
+ /// The style is stored as a static reference; the provided style must be a static
+ /// constant (e.g., `&'static ParserStyle`). Use the built-in constants like
+ /// `UNIX_STYLE`, `POWERSHELL_STYLE`, or `WINDOWS_STYLE`.
+ pub fn set_global_style(style: &'static ParserStyle<'static>) {
+ if !GLOBAL_STYLE_SET.load(Ordering::Acquire) && GLOBAL_STYLE.set(*style).is_ok() {
+ GLOBAL_STYLE_SET.store(true, Ordering::Release);
+ }
+ }
+
+ /// Returns the global parser style, falling back to `UNIX_STYLE` if not set.
+ pub fn global_style() -> &'static ParserStyle<'static> {
+ GLOBAL_STYLE.get().unwrap_or(&UNIX_STYLE)
+ }
+}
diff --git a/mingling_picker/src/parselib/utils.rs b/mingling_picker/src/parselib/utils.rs
new file mode 100644
index 0000000..726346c
--- /dev/null
+++ b/mingling_picker/src/parselib/utils.rs
@@ -0,0 +1,271 @@
+use crate::{
+ PickerArgInfo,
+ parselib::{MaskedArg, ParserStyle},
+};
+
+#[inline(always)]
+pub fn build_possible_flags(style: &ParserStyle, arg_info: &PickerArgInfo) -> Vec<String> {
+ let mut possible_flags = vec![];
+
+ if let Some(short) = arg_info.short {
+ possible_flags.push(style.flag_string(short));
+ }
+
+ if let Some(long) = arg_info.long {
+ let converted = style.naming_case.convert(long.to_string());
+ possible_flags.push(style.flag_string(&converted));
+ }
+
+ if let Some(aliases) = &arg_info.alias {
+ for alias in aliases {
+ let converted = style.naming_case.convert(alias.to_string());
+ possible_flags.push(style.flag_string(&converted));
+ }
+ }
+
+ possible_flags
+}
+
+/// Extract a single value from the raw strings tagged by [`SingleMatcher`](crate::parselib::SingleMatcher).
+///
+/// Returns `None` if no value is available (empty slice),
+/// the inline value after the style separator if present (eq mode),
+/// or the value directly (positional or flag-following).
+///
+/// This is the standard `pick` helper for all `Single`-type
+/// [`Pickable`](crate::Pickable) implementations.
+#[must_use]
+pub fn seek_single<'a>(raw_strs: &'a [&'a str]) -> Option<&'a str> {
+ match raw_strs.len() {
+ 0 => None,
+ 1 => {
+ let s = raw_strs[0];
+ let sep = ParserStyle::global_style().value_separator;
+ if let Some(pos) = s.rfind(sep) {
+ Some(&s[pos + 1..])
+ } else {
+ Some(s)
+ }
+ }
+ _ => Some(raw_strs[1]),
+ }
+}
+
+/// Seeks the index of the end-of-options marker (`--`) in the argument list.
+///
+/// This function searches for the standard end-of-options separator (`--`)
+/// in the given argument list, respecting the parser's style settings
+/// (e.g., case sensitivity). The end-of-options marker indicates that all
+/// subsequent arguments should be treated as positional arguments, not flags.
+#[must_use]
+pub fn seek_end_of_options(args: &[MaskedArg], style: &ParserStyle) -> Option<usize> {
+ args.iter()
+ .find(|arg| {
+ if style.case_sensitive {
+ arg.raw == style.end_of_options
+ } else {
+ arg.raw.eq_ignore_ascii_case(style.end_of_options)
+ }
+ })
+ .map(|arg| arg.raw_idx)
+}
+
+/// Seeks arguments in `args` that are exactly equal to the given `string`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn seek_eq(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ arg.raw == string
+ } else {
+ arg.raw.eq_ignore_ascii_case(string)
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that contain the given `string` as a substring.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn seek_contains(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ arg.raw.contains(string)
+ } else {
+ arg.raw.to_lowercase().contains(&string.to_lowercase())
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that start with the given `string`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn seek_start_with(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ arg.raw.starts_with(string)
+ } else {
+ arg.raw.to_lowercase().starts_with(&string.to_lowercase())
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that end with the given `string`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn seek_end_with(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ arg.raw.ends_with(string)
+ } else {
+ arg.raw.to_lowercase().ends_with(&string.to_lowercase())
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that are exactly equal to any of the given `strings`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn multi_seek_eq(args: &[MaskedArg], strings: &[&str], case_sensitive: bool) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ strings.contains(&arg.raw)
+ } else {
+ strings.iter().any(|s| arg.raw.eq_ignore_ascii_case(s))
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that contain any of the given `strings` as a substring.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn multi_seek_contains(
+ args: &[MaskedArg],
+ strings: &[&str],
+ case_sensitive: bool,
+) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ strings.iter().any(|s| arg.raw.contains(s))
+ } else {
+ let lower_raw = arg.raw.to_lowercase();
+ strings
+ .iter()
+ .any(|s| lower_raw.contains(&s.to_lowercase()))
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that start with any of the given `strings`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn multi_seek_start_with(
+ args: &[MaskedArg],
+ strings: &[&str],
+ case_sensitive: bool,
+) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ strings.iter().any(|s| arg.raw.starts_with(s))
+ } else {
+ let lower_raw = arg.raw.to_lowercase();
+ strings
+ .iter()
+ .any(|s| lower_raw.starts_with(&s.to_lowercase()))
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that end with any of the given `strings`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn multi_seek_end_with(
+ args: &[MaskedArg],
+ strings: &[&str],
+ case_sensitive: bool,
+) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ strings.iter().any(|s| arg.raw.ends_with(s))
+ } else {
+ let lower_raw = arg.raw.to_lowercase();
+ strings
+ .iter()
+ .any(|s| lower_raw.ends_with(&s.to_lowercase()))
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Converts a `&Vec<String>` into a `Vec<&str>` by borrowing each string's slice.
+///
+/// This is useful for converting owned `String` vectors into borrowed `&str` slices
+/// for functions that take `&[&str]` or similar parameters.
+#[must_use]
+#[inline(always)]
+#[doc(hidden)]
+pub fn vec_string_to_vec_str(input: &[String]) -> Vec<&str> {
+ input.iter().map(|s| s.as_str()).collect()
+}
+
+/// Converts a `&Vec<String>` into a `Vec<&str>` by borrowing each string's slice.
+///
+/// This is useful for converting owned `String` vectors into borrowed `&str` slices
+/// for functions that take `&[&str]` or similar parameters.
+#[macro_export]
+#[doc(hidden)]
+macro_rules! vec_string_slice {
+ ($v:expr) => {
+ $v.iter()
+ .map(|s| s.as_str())
+ .collect::<Vec<&str>>()
+ .as_slice()
+ };
+}
+
+/// Gets the first element from a vector of seek results, if any.
+///
+/// Returns `Some(index)` if the vector is non-empty, otherwise `None`.
+#[must_use]
+#[inline(always)]
+pub fn get_seeked_first(seeked: Vec<usize>) -> Option<usize> {
+ seeked.into_iter().next()
+}
diff --git a/mingling_picker/src/pickable.rs b/mingling_picker/src/pickable.rs
new file mode 100644
index 0000000..758ae9a
--- /dev/null
+++ b/mingling_picker/src/pickable.rs
@@ -0,0 +1,91 @@
+use crate::{PickerArg, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs};
+
+mod single_pickable;
+pub use single_pickable::*;
+
+mod multi_pickable;
+pub use multi_pickable::*;
+
+/// `Pickable` trait defines how to parse a type instance from command-line arguments.
+///
+/// This trait is the core abstraction of the `Picker` argument parsing system, dividing the
+/// parsing process into two phases:
+///
+/// 1. **Tag phase ([`Pickable::tag`])**: Determines which argument positions the `Pickable` needs to handle.
+/// 2. **Pick phase ([`Pickable::pick`])**: Converts the raw strings at the tagged positions into the actual type.
+///
+/// Types implementing this trait must also implement [`Default`], so that a default value
+/// can be used as a fallback when parsing fails.
+///
+/// # Type Parameters
+///
+/// * `'a` - Lifetime parameter, used to associate references in [`PickerArg`].
+pub trait Pickable<'a>
+where
+ Self: Sized,
+{
+ /// Returns the parse-order attribute of this flag.
+ ///
+ /// This attribute is used to inform the parser about the parse order
+ /// between different `Pickable` types.
+ /// See [`PickerArgAttr`] for specific ordering definitions.
+ ///
+ /// # Parameters
+ ///
+ /// * `flag` - The current flag instance, which contains a reference to `Self`.
+ ///
+ /// # Returns
+ ///
+ /// Returns a [`PickerArgAttr`] describing the parse-order attribute of this flag.
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr;
+
+ /// Tag phase: Determines which argument positions the `Pickable` needs to handle.
+ ///
+ /// This function receives a [`TagPhaseContext`] containing argument context information.
+ /// During this phase, the parser invokes each `Pickable` and collects the position indices
+ /// they return, in order to determine which arguments to parse later.
+ ///
+ /// # Parameters
+ ///
+ /// * `ctx` - The tag phase context, containing argument information, all parameters of the
+ /// current Picker, and an availability mask.
+ ///
+ /// # Returns
+ ///
+ /// Returns a `Vec<usize>` representing the indices of the arguments in the argument list
+ /// that this `Pickable` needs to handle.
+ fn tag(ctx: TagPhaseContext) -> Vec<usize>;
+
+ /// Pick phase: Converts the raw string arguments tagged during the `tag` phase into
+ /// the actual expected type.
+ ///
+ /// This function receives a slice of the raw strings that were tagged in the `tag` step
+ /// and converts them into an instance of `Self`.
+ ///
+ /// # Parameters
+ ///
+ /// * `raw_strs` - A slice of strings containing the raw argument values to parse.
+ ///
+ /// # Returns
+ ///
+ /// Returns [`PickerArgResult<Self>`], i.e., the `Self` instance on success, or an appropriate
+ /// error message on failure.
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self>;
+}
+
+/// Tag phase context, providing the necessary argument and state information for
+/// [`Pickable::tag`].
+pub struct TagPhaseContext<'a> {
+ /// Argument information describing the structure and metadata of the argument
+ /// to be parsed.
+ pub arg_info: &'a PickerArgInfo<'a>,
+
+ /// A read-only list of all arguments in the current [`Picker`](crate::Picker).
+ pub args: &'a PickerArgs<'a>,
+
+ /// Mask indicating which argument positions have already been claimed.
+ ///
+ /// For example, if the mask is `[0, 0, 1, 0]`, then the argument at index `2`
+ /// has already been tagged by another `Pickable`.
+ pub mask: &'a [u8],
+}
diff --git a/mingling_picker/src/pickable/multi_pickable.rs b/mingling_picker/src/pickable/multi_pickable.rs
new file mode 100644
index 0000000..0ab0508
--- /dev/null
+++ b/mingling_picker/src/pickable/multi_pickable.rs
@@ -0,0 +1,77 @@
+use crate::{
+ matcher_needed::Matcher,
+ parselib::{MultiArgMatcher, ParserStyle},
+ Pickable, PickerArg, PickerArgAttr, PickerArgResult,
+ SinglePickable, TagPhaseContext,
+};
+
+/// Boundary check for multi-value positional parameters.
+pub trait BoundaryCheck {
+ fn check_boundary(raw: &str) -> bool;
+}
+
+/// Trait for multi-value parameters.
+pub trait MultiPickableWithBoundary: Sized {
+ type Checker: BoundaryCheck;
+ fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self>;
+}
+
+/// Marker: unit type that always accepts — no boundary.
+pub struct NoBoundary;
+
+impl BoundaryCheck for NoBoundary {
+ #[inline(always)]
+ fn check_boundary(_raw: &str) -> bool {
+ false
+ }
+}
+
+/// `Vec<T>` is greedy — it takes everything with `NoBoundary`.
+impl<T: SinglePickable> MultiPickableWithBoundary for Vec<T> {
+ type Checker = NoBoundary;
+
+ fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self> {
+ let mut result = Vec::with_capacity(raw.len());
+ for s in &raw {
+ match T::pick_single(Some(s)) {
+ PickerArgResult::Parsed(v) => result.push(v),
+ PickerArgResult::NotFound => return PickerArgResult::NotFound,
+ PickerArgResult::Unparsed => {}
+ }
+ }
+ PickerArgResult::Parsed(result)
+ }
+}
+
+/// If the first raw string looks like a named flag (starts with the
+/// style's long or short prefix), strip it — it's the flag, not a value.
+fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] {
+ if let Some(first) = raw_strs.first() {
+ let style = ParserStyle::global_style();
+ if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) {
+ return &raw_strs[1..];
+ }
+ }
+ raw_strs
+}
+
+// Pickable impl for Vec<T>
+
+impl<'a, T> Pickable<'a> for Vec<T>
+where
+ T: SinglePickable,
+{
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_multi(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ MultiArgMatcher::match_all(ctx.into())
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ let strs = strip_flag(raw_strs);
+ let owned: Vec<String> = strs.iter().map(|&s| s.to_string()).collect();
+ <Vec<T> as MultiPickableWithBoundary>::pick_multi(owned)
+ }
+}
diff --git a/mingling_picker/src/pickable/single_pickable.rs b/mingling_picker/src/pickable/single_pickable.rs
new file mode 100644
index 0000000..8a5b3e6
--- /dev/null
+++ b/mingling_picker/src/pickable/single_pickable.rs
@@ -0,0 +1,69 @@
+use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext};
+
+/// `SinglePickable` trait defines how to parse a type from a single command-line argument.
+///
+/// This trait provides a simplified interface for types that consume exactly one argument value.
+/// It is automatically implemented by the blanket `impl` of [`Pickable`], so types implementing
+/// `SinglePickable` will work with the full `Pickable` argument parsing system.
+///
+/// Additionally, `Option<S>` where `S: SinglePickable` also implements [`Pickable`], allowing
+/// optional arguments to be parsed naturally.
+///
+/// # Type Parameters
+///
+/// * `Self` - The type to be parsed from a single argument string.
+pub trait SinglePickable
+where
+ Self: Sized,
+{
+ /// Parse a single optional string value into an instance of `Self`.
+ ///
+ /// # Parameters
+ ///
+ /// * `str` - An `Option<&str>` representing the raw argument value. If `None`,
+ /// it indicates that no argument value was provided (e.g., for flag-like arguments).
+ ///
+ /// # Returns
+ ///
+ /// Returns [`PickerArgResult<Self>`], i.e., the parsed `Self` instance on success,
+ /// or an appropriate error message on failure.
+ fn pick_single(str: Option<&str>) -> PickerArgResult<Self>;
+}
+
+impl<'a, S> Pickable<'a> for S
+where
+ S: SinglePickable,
+{
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_single(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ crate::parselib::SingleMatcher::tag(ctx)
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ Self::pick_single(crate::parselib::seek_single(raw_strs))
+ }
+}
+
+impl<'a, S> Pickable<'a> for Option<S>
+where
+ S: SinglePickable,
+{
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_single(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ crate::parselib::SingleMatcher::tag(ctx)
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ match S::pick(raw_strs) {
+ PickerArgResult::Unparsed => PickerArgResult::Unparsed,
+ PickerArgResult::Parsed(r) => PickerArgResult::Parsed(Some(r)),
+ PickerArgResult::NotFound => PickerArgResult::Parsed(None),
+ }
+ }
+}
diff --git a/mingling_picker/src/picker.rs b/mingling_picker/src/picker.rs
new file mode 100644
index 0000000..4efb532
--- /dev/null
+++ b/mingling_picker/src/picker.rs
@@ -0,0 +1,360 @@
+use std::{marker::PhantomData, ops::Index};
+
+mod parse;
+
+mod patterns;
+pub use patterns::*;
+
+mod result;
+pub use result::*;
+
+use crate::{Pickable, PickerArg, PickerArgResult};
+
+/// Picker, used to record all states of a parameter parsing
+///
+/// Includes the following:
+///
+/// - Basic arguments
+/// - Parsing states
+/// - Parsing results
+pub struct Picker<'a, Route = ()> {
+ route_phantom: PhantomData<Route>,
+
+ /// Internal arguments of Picker
+ args: PickerArgs<'a>,
+}
+
+impl<'a> Picker<'a> {
+ /// Creates a new `Picker` from the command-line arguments (excluding the program name).
+ ///
+ /// This is equivalent to calling `std::env::args().skip(1)`, which
+ /// collects all arguments passed to the program except the first one
+ /// (the executable path).
+ pub fn from_args() -> Picker<'a, ()> {
+ Self::from_args_skip(1)
+ }
+
+ /// Creates a new `Picker` from the command-line arguments, skipping the
+ /// first `skip` entries.
+ ///
+ /// This method is useful when you want more control over which arguments
+ /// are included. For example, pass `skip = 2` to skip both the program
+ /// name and the first argument.
+ pub fn from_args_skip(skip: usize) -> Picker<'a, ()> {
+ let args = std::env::args().skip(skip).collect::<Vec<String>>();
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Owned(args),
+ }
+ }
+
+ /// Changes the route (phantom type parameter) of the `Picker`.
+ ///
+ /// This method allows converting a `Picker` from one route type to another,
+ /// while preserving the same underlying arguments. The route type is typically
+ /// used to distinguish different parsing contexts or to carry compile-time
+ /// state information through the picking chain.
+ pub fn with_route<NewRoute>(self) -> Picker<'a, NewRoute>
+ where
+ Self: Sized,
+ {
+ Picker {
+ route_phantom: PhantomData,
+ args: self.args,
+ }
+ }
+}
+
+/// Internal arguments of Picker
+///
+/// - `Slice` - borrowed slice of string slices
+/// - `Vec` - owned vector of borrowed string slices
+/// - `Owned` - owned vector of owned strings
+pub enum PickerArgs<'a> {
+ /// Borrowed slice of string slices
+ Slice(&'a [&'a str]),
+ /// Owned vector of borrowed string slices
+ Vec(Vec<&'a str>),
+ /// Owned vector of owned strings
+ Owned(Vec<String>),
+}
+
+impl<'a> Default for PickerArgs<'a> {
+ fn default() -> Self {
+ Self::Vec(vec![])
+ }
+}
+
+impl<'a> PickerArgs<'a> {
+ /// Returns the number of arguments.
+ pub fn len(&self) -> usize {
+ match self {
+ PickerArgs::Slice(items) => items.len(),
+ PickerArgs::Vec(items) => items.len(),
+ PickerArgs::Owned(items) => items.len(),
+ }
+ }
+
+ /// Returns `true` if there are no arguments.
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Returns an iterator over the arguments, yielding `&str` values.
+ pub fn iter(&'a self) -> PickerIter<'a> {
+ match self {
+ PickerArgs::Slice(items) => PickerIter::Slice(items.iter()),
+ PickerArgs::Vec(items) => PickerIter::Vec(items.iter()),
+ PickerArgs::Owned(items) => PickerIter::Owned(items.iter()),
+ }
+ }
+
+ /// Returns a reference to the argument at `index`, if it exists.
+ pub fn get(&self, index: usize) -> Option<&str> {
+ match self {
+ PickerArgs::Slice(items) => items.get(index).copied(),
+ PickerArgs::Vec(items) => items.get(index).copied(),
+ PickerArgs::Owned(items) => items.get(index).map(|s| s.as_str()),
+ }
+ }
+}
+
+impl<'a> Index<usize> for PickerArgs<'a> {
+ type Output = str;
+
+ fn index(&self, index: usize) -> &Self::Output {
+ match self {
+ PickerArgs::Slice(items) => items[index],
+ PickerArgs::Vec(items) => items[index],
+ PickerArgs::Owned(items) => &items[index],
+ }
+ }
+}
+
+impl<'a> IntoIterator for &'a PickerArgs<'a> {
+ type Item = &'a str;
+ type IntoIter = PickerIter<'a>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ match self {
+ PickerArgs::Slice(items) => PickerIter::Slice(items.iter()),
+ PickerArgs::Vec(items) => PickerIter::Vec(items.iter()),
+ PickerArgs::Owned(items) => PickerIter::Owned(items.iter()),
+ }
+ }
+}
+
+impl<'a, Route> From<&'a [&'a str]> for Picker<'a, Route> {
+ fn from(value: &'a [&'a str]) -> Self {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Slice(value),
+ }
+ }
+}
+
+impl<'a, Route> From<Vec<&'a str>> for Picker<'a, Route> {
+ fn from(value: Vec<&'a str>) -> Self {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Vec(value),
+ }
+ }
+}
+
+impl<'a, Route> From<Vec<String>> for Picker<'a, Route> {
+ fn from(value: Vec<String>) -> Self {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Owned(value),
+ }
+ }
+}
+
+impl<'a, Route> Picker<'a, Route> {
+ /// Returns a reference to the internal `PickerArgs`.
+ pub fn args(&self) -> &PickerArgs<'a> {
+ &self.args
+ }
+
+ /// Returns a mutable reference to the internal `PickerArgs`.
+ pub fn args_mut(&mut self) -> &mut PickerArgs<'a> {
+ &mut self.args
+ }
+
+ /// Consumes `self` and returns the internal `PickerArgs`.
+ pub fn into_args(self) -> PickerArgs<'a> {
+ self.args
+ }
+
+ /// Returns the number of arguments.
+ pub fn len(&self) -> usize {
+ self.args.len()
+ }
+
+ /// Returns `true` if there are no arguments.
+ pub fn is_empty(&self) -> bool {
+ self.args.is_empty()
+ }
+
+ /// Returns an iterator over the arguments, yielding `&str` values.
+ pub fn iter(&'a self) -> PickerIter<'a> {
+ self.args.iter()
+ }
+}
+
+impl<'a, Route> Index<usize> for Picker<'a, Route> {
+ type Output = str;
+
+ fn index(&self, index: usize) -> &Self::Output {
+ &self.args[index]
+ }
+}
+
+impl<'a, Route> Index<usize> for &Picker<'a, Route> {
+ type Output = str;
+
+ fn index(&self, index: usize) -> &Self::Output {
+ &self.args[index]
+ }
+}
+
+impl<'a, Route> IntoIterator for &'a Picker<'a, Route> {
+ type Item = &'a str;
+ type IntoIter = PickerIter<'a>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.args.iter()
+ }
+}
+
+/// Iterator for `Picker` (and `PickerArgs`), yielding `&'a str` values.
+pub enum PickerIter<'a> {
+ /// Iterates over a borrowed slice (`&[&str]`)
+ Slice(std::slice::Iter<'a, &'a str>),
+ /// Iterates over an owned vector of borrowed string slices (`Vec<&str>`)
+ Vec(std::slice::Iter<'a, &'a str>),
+ /// Iterates over an owned vector of owned strings (`Vec<String>`)
+ Owned(std::slice::Iter<'a, String>),
+}
+
+impl<'a> Iterator for PickerIter<'a> {
+ type Item = &'a str;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ match self {
+ PickerIter::Slice(iter) => iter.next().copied(),
+ PickerIter::Vec(iter) => iter.next().copied(),
+ PickerIter::Owned(iter) => iter.next().map(|s| s.as_str()),
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ match self {
+ PickerIter::Slice(iter) => iter.size_hint(),
+ PickerIter::Vec(iter) => iter.size_hint(),
+ PickerIter::Owned(iter) => iter.size_hint(),
+ }
+ }
+}
+
+impl<'a> ExactSizeIterator for PickerIter<'a> {}
+
+/// Trait for converting types into a `Picker`
+///
+/// Implemented for:
+/// - `&[&str]` (borrowed slice)
+/// - `&[String]` (borrowed slice of owned strings)
+/// - `Vec<&str>` (owned vector of borrowed strings)
+/// - `Vec<String>` (owned vector of owned strings)
+pub trait IntoPicker<'a> {
+ /// Converts the value into a `Picker`
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::{IntoPicker, Picker};
+ ///
+ /// let args: Picker = (&["hello", "world"][..]).to_picker();
+ /// assert_eq!(args.len(), 2);
+ ///
+ /// let args: Picker = vec!["foo", "bar"].to_picker();
+ /// assert_eq!(args.len(), 2);
+ ///
+ /// let args: Picker = vec!["a".to_string(), "b".to_string()].to_picker();
+ /// assert_eq!(args.len(), 2);
+ /// ```
+ fn to_picker(self) -> Picker<'a, ()>;
+
+ /// Creates a `PickerPattern1` from the given arg for the `pick` method.
+ ///
+ /// This method converts the value into a `Picker` and starts a parameter
+ /// picking chain with one arg. The result is initially `Unparsed`.
+ fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, ()>
+ where
+ Self: Sized,
+ N: Pickable<'a> + Default + Sized,
+ {
+ PickerPattern1 {
+ args: self.to_picker().args,
+ arg_1: arg.into(),
+ result_1: PickerArgResult::Unparsed,
+ default_1: None,
+ route_1: None,
+ post_1: None,
+ error_route: None,
+ }
+ }
+
+ /// Converts the value into a `Picker` with a specified route type.
+ ///
+ /// This method allows changing the route (phantom type parameter) of the picker.
+ /// The route type is typically used to distinguish different parsing contexts or
+ /// to carry compile-time state information through the picking chain.
+ fn with_route<NewRoute>(self) -> Picker<'a, NewRoute>
+ where
+ Self: Sized,
+ {
+ Picker {
+ route_phantom: PhantomData,
+ args: self.to_picker().args,
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for &'a [&'a str] {
+ fn to_picker(self) -> Picker<'a, ()> {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Slice(self),
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for &'a [String] {
+ fn to_picker(self) -> Picker<'a, ()> {
+ let vec: Vec<&str> = self.iter().map(|s| s.as_str()).collect();
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Vec(vec),
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for Vec<&'a str> {
+ fn to_picker(self) -> Picker<'a, ()> {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Vec(self),
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for Vec<String> {
+ fn to_picker(self) -> Picker<'a, ()> {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Owned(self),
+ }
+ }
+}
diff --git a/mingling_picker/src/picker/parse.rs b/mingling_picker/src/picker/parse.rs
new file mode 100644
index 0000000..8f7d514
--- /dev/null
+++ b/mingling_picker/src/picker/parse.rs
@@ -0,0 +1,177 @@
+// --------------------------------------------------------------------------------------------
+// I have to say, the code generated by this `internal_repeat!` macro is really UGLY.
+//
+// But I have to admit, this is a **trade-off**. To achieve the syntax of `pick().pick().pick()`
+// while ensuring type safety, this is the best approach I could think of.
+//
+// P.S. If there's a better way, please let me know. Thanks!
+// --------------------------------------------------------------------------------------------
+//
+// Then, I must disable `clippy::type_complexity` — this guy is way too noisy.
+#![allow(clippy::type_complexity)]
+
+use crate::{Pickable, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs, TagPhaseContext};
+use mingling_picker_macros::internal_repeat;
+
+internal_repeat!(1..=32 => {
+ use crate::PickerPattern$;
+ use crate::PickerResult$;
+});
+
+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.
+ ///
+ /// # Panics
+ ///
+ /// Panics if a route was selected.
+ pub fn unwrap(self) -> ((T$,+)) {
+ let p = self.parse();
+ ((p.v$.unwrap(),+))
+ }
+
+ /// Returns the individual option values without checking the route.
+ pub fn unpack(self) -> ((Option<T$>,+)) {
+ let p = self.parse();
+ ((p.v$,+))
+ }
+
+ /// Converts to a `Result`, returning `Err(route)` if a route was selected,
+ /// or `Ok(values)` otherwise.
+ pub fn to_result(self) -> Result<((T$,+)), Route> {
+ let p = self.parse();
+ if let Some(r) = p.route {
+ return Err(r);
+ }
+ Ok(p.unwrap())
+ }
+
+ /// Converts to an `Option`, returning `None` if a route was selected,
+ /// or `Some(values)` otherwise.
+ pub fn to_option(self) -> Option<((T$,+))> {
+ let p = self.parse();
+ if p.route.is_some() {
+ return None;
+ }
+ Some(p.unwrap())
+ }
+ }
+});
+
+internal_repeat!(1..=32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ pub fn parse(mut self) -> PickerResult$<(T$,+), Route> {
+ // ArgInfos
+ let arg_infos: [PickerArgInfo; $] = [
+ (
+ PickerArgInfo::from(self.arg_$),
+ +)
+ ];
+
+ let mut bundle: [
+ (
+ // Arg Attr
+ PickerArgAttr,
+
+ // Tag Func
+ Box<dyn FnOnce(&PickerArgs<'a>, &[u8]) -> Vec<usize>>,
+
+ // Pick Func
+ Box<dyn FnOnce(&[&str], &mut Option<Route>)>,
+
+ // Index
+ usize
+ )
+ ; $] = [
+ (
+ (
+ // Arg Attr
+ T$::get_attr(self.arg_$),
+
+ // Tag Func
+ Box::new(|args, mask| {
+ let ctx = TagPhaseContext {
+ arg_info: &arg_infos[$-],
+ args,
+ mask
+ };
+ T$::tag(ctx)
+ }),
+
+ // Pick Func
+ Box::new(|args, error_route| {
+ self.result_$ = match T$::pick(args) {
+ PickerArgResult::Parsed(mut value) => {
+ // Postprocess
+ if let Some(post) = self.post_$ {
+ value = post(value);
+ }
+ PickerArgResult::Parsed(value)
+ },
+ other => {
+ if let Some(get_default) = self.default_$ {
+ let mut value = get_default();
+
+ // Postprocess
+ if let Some(post) = self.post_$ {
+ value = post(value);
+ }
+
+ PickerArgResult::Parsed(value)
+ } else {
+ if error_route.is_none() {
+ if let Some(get_route) = self.route_$ {
+ *error_route = Some(get_route());
+ }
+ }
+ other
+ }
+ },
+
+ }
+ }),
+
+ // Index
+ $
+ ),
+ +)
+ ];
+
+ // Sort by Bundle Ord (descending)
+ bundle.sort_by(|a, b| b.0.cmp(&a.0));
+
+ // Mask — size = number of args (not args), so use args length
+ let mut mask: Vec<u8> = vec![0u8; self.args.len()];
+
+ // Parsing
+ for (_, tag_func, pick_func, _idx) in bundle {
+
+ // Tag phase
+ let tagged = tag_func(&self.args, mask.as_slice());
+ let mut args_to_pick: Vec<&str> = vec![];
+
+ for i in tagged {
+ mask[i] = 1;
+
+ // Update args to pick
+ args_to_pick.push(self.args.get(i).unwrap_or_default());
+ }
+
+ // Pick phase
+ pick_func(args_to_pick.as_slice(), &mut self.error_route);
+ }
+
+ // Combine Result
+ let result: PickerResult$<(T$,+), Route> = PickerResult$ {
+ route: self.error_route,
+ (
+ v$: self.result_$.to_option(),
+ +)
+ };
+ result
+ }
+ }
+});
diff --git a/mingling_picker/src/picker/patterns.rs b/mingling_picker/src/picker/patterns.rs
new file mode 100644
index 0000000..3c6e73f
--- /dev/null
+++ b/mingling_picker/src/picker/patterns.rs
@@ -0,0 +1,199 @@
+use mingling_picker_macros::internal_repeat;
+
+use crate::{Pickable, Picker, PickerArg, PickerArgResult, PickerArgs};
+
+internal_repeat!(1..=32 => {
+ #[doc(hidden)]
+ pub struct PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ pub args: PickerArgs<'a>,
+ pub error_route: Option<Route>,
+ (
+ pub arg_$: &'a PickerArg<'a, T$>,
+ pub result_$: PickerArgResult<T$>,
+ pub default_$: Option<Box<dyn FnOnce() -> T$>>,
+ pub route_$: Option<Box<dyn FnOnce() -> Route>>,
+ pub post_$: Option<Box<dyn FnOnce(T$) -> T$>>,
+ +)
+ }
+});
+
+internal_repeat!(1..=32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ /// Sets a default value provider for this arg.
+ ///
+ /// If the arg is not provided by the user at runtime, the given closure will be
+ /// called to produce a default value. The closure is expected to return `T$`.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .or(|| 42);
+ /// ```
+ #[allow(clippy::type_complexity)]
+ pub fn or<F>(mut self, func: F) -> Self
+ where
+ F: FnMut() -> T$,
+ F: 'static,
+ {
+ self.default_$ = Some(Box::new(func));
+ self
+ }
+
+ /// Uses the default value for this arg's type if the arg is not provided.
+ ///
+ /// If the arg is not provided by the user at runtime, the default value for `T$`
+ /// (as defined by the `Default` trait) will be used.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .or_default();
+ /// ```
+ #[allow(clippy::type_complexity)]
+ pub fn or_default(mut self) -> Self
+ where
+ T$: Default,
+ {
+ self.default_$ = Some(Box::new(|| T$::default()));
+ self
+ }
+
+ /// Sets a route for when the arg is not provided.
+ ///
+ /// If the arg is not provided by the user at runtime, the given closure will be
+ /// called to produce a route value that will be returned early.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .or_route(|| Redirect::home());
+ /// ```
+ pub fn or_route<F>(mut self, func: F) -> Self
+ where
+ F: FnMut() -> Route,
+ F: 'static,
+ {
+ self.route_$ = Some(Box::new(func));
+ self
+ }
+
+
+ /// Resets the route for this picker pattern, allowing a different route type.
+ ///
+ /// This method converts the current `PickerPattern` into a new one with a different
+ /// route type `NewRoute`. All existing arg configurations, defaults, and post-
+ /// processing functions are preserved, but the `error_route` and individual
+ /// `route_$` fields are cleared (set to `None`).
+ ///
+ /// This is useful when you want to change the error/redirect route type mid-chain,
+ /// for example when composing patterns from different contexts that use different
+ /// route enums.
+ #[allow(clippy::type_complexity)]
+ pub fn with_route<NewRoute>(self) -> PickerPattern$<'a, (T$,+), NewRoute> {
+ PickerPattern$ {
+ args: self.args,
+ error_route: None,
+ (
+ arg_$: self.arg_$,
+ result_$: self.result_$,
+ default_$: self.default_$,
+ route_$: None,
+ post_$: self.post_$,
+ +)
+ }
+ }
+
+ /// Attaches a post-processing function to this arg.
+ ///
+ /// After the arg's value is parsed (or defaulted), the given closure will be
+ /// invoked with the parsed value and its return value will be used as the final
+ /// result. This allows transforming or validating the parsed value.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .post(|val| val * 2);
+ /// ```
+ #[allow(clippy::type_complexity)]
+ pub fn post<F>(mut self, func: F) -> Self
+ where
+ F: FnMut(T$) -> T$,
+ F: 'static,
+ {
+ self.post_$ = Some(Box::new(func));
+ self
+ }
+ }
+});
+
+internal_repeat!(1..32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ #[allow(clippy::type_complexity)]
+ /// Adds a new arg to the picking chain, returning a new `PickerPattern` with one more type parameter.
+ ///
+ /// This method extends the current picking pattern by appending an additional arg.
+ /// The previous args and their results are preserved as part of the new pattern.
+ /// The new arg's result is initially `Unparsed`.
+ pub fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern$+<'a, (T$,+), N, Route>
+ where
+ N: Pickable<'a>,
+ {
+ PickerPattern$+ {
+ // Args
+ args: self.args,
+ error_route: self.error_route,
+
+ // Current
+ arg_$+: arg.into(),
+ result_$+: PickerArgResult::Unparsed,
+ default_$+: None,
+ route_$+: None,
+ post_$+: None,
+
+ // Prev
+ (
+ arg_$: self.arg_$,
+ result_$: self.result_$,
+ default_$: self.default_$,
+ route_$: self.route_$,
+ post_$: self.post_$,
+ +)
+ }
+ }
+ }
+});
+
+impl<'a, Route> Picker<'a, Route> {
+ /// Creates a `PickerPattern1` from the given arg to start a picking chain.
+ ///
+ /// This method initiates a parameter picking chain with one arg.
+ /// The result is initially `Unparsed`.
+ pub fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, Route>
+ where
+ N: Pickable<'a>,
+ {
+ PickerPattern1 {
+ args: self.args,
+ error_route: None::<Route>,
+ arg_1: arg.into(),
+ result_1: PickerArgResult::Unparsed,
+ route_1: None,
+ default_1: None,
+ post_1: None,
+ }
+ }
+}
diff --git a/mingling_picker/src/picker/result.rs b/mingling_picker/src/picker/result.rs
new file mode 100644
index 0000000..9cd78ae
--- /dev/null
+++ b/mingling_picker/src/picker/result.rs
@@ -0,0 +1,56 @@
+#![allow(clippy::type_complexity)] // Aha, Type Gymnastics!
+
+use mingling_picker_macros::internal_repeat;
+
+internal_repeat!(1..=32 => {
+ #[doc(hidden)]
+ pub struct PickerResult$<(T$,+), Route> {
+ /// The route selected by the picker, if any.
+ /// If this is `Some`, the picker chose to follow a route instead of selecting values,
+ /// and all value fields (`v1`, `v2`, ...) will be `None`.
+ ///
+ /// Note: "route" here refers to an alternative path/choice, not a network route.
+ pub route: Option<Route>,
+
+ (
+ #[doc = concat!("The optional value for the ", $, "th type parameter.")]
+ pub v$: Option<T$>,
+ +)
+ }
+});
+
+internal_repeat!(1..=32 => {
+ impl<(T$,+), Route> PickerResult$<(T$,+), Route> {
+ /// Unwraps the result, panicking if a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `self.route` is `Some(...)`.
+ pub fn unwrap(self) -> ((T$,+)) {
+ ((self.v$.unwrap(),+))
+ }
+
+ /// Returns the individual option values without checking the route.
+ pub fn unpack(self) -> ((Option<T$>,+)) {
+ ((self.v$,+))
+ }
+
+ /// Converts to a `Result`, returning `Err(route)` if a route was selected,
+ /// or `Ok(values)` otherwise.
+ pub fn to_result(self) -> Result<((T$,+)), Route> {
+ if let Some(r) = self.route {
+ return Err(r);
+ }
+ Ok(self.unwrap())
+ }
+
+ /// Converts to an `Option`, returning `None` if a route was selected,
+ /// or `Some(values)` otherwise.
+ pub fn to_option(self) -> Option<((T$,+))> {
+ if let Some(_) = self.route {
+ return None;
+ }
+ Some(self.unwrap())
+ }
+ }
+});
diff --git a/mingling_picker/src/value.rs b/mingling_picker/src/value.rs
new file mode 100644
index 0000000..995aa00
--- /dev/null
+++ b/mingling_picker/src/value.rs
@@ -0,0 +1,5 @@
+mod flag;
+pub use flag::*;
+
+mod vec_until;
+pub use vec_until::*;
diff --git a/mingling_picker/src/value/flag.rs b/mingling_picker/src/value/flag.rs
new file mode 100644
index 0000000..ee0d6ee
--- /dev/null
+++ b/mingling_picker/src/value/flag.rs
@@ -0,0 +1,145 @@
+use std::{
+ fmt::{Debug, Display},
+ ops::{Deref, Not},
+};
+
+/// Parsed result of a boolean-style command-line flag.
+///
+/// `Flag` is a **value type** that can be declared in [`PickerArg`].
+/// When the user passes `--verbose` on the command line, the parsed result is `Flag::Active`;
+/// when the flag is absent, the result is `Flag::Inactive`.
+///
+/// # Why not just `bool`?
+///
+/// Unlike a raw `bool`, `Flag` carries **explicit semantics** about whether
+/// the flag was actually provided by the user (`Active`) or simply omitted
+/// (`Inactive`). This distinction matters when you want to distinguish
+/// "the user intentionally omitted the flag" from "the flag was processed but
+/// resolved to false" — the `Pickable` implementation for `Flag` always
+/// returns `Parsed(Flag::Inactive)` when no matching argument is found,
+/// rather than `NotFound`, making it always succeed with a meaningful default.
+///
+/// # Conversions
+///
+/// `Flag` interoperates seamlessly with `bool`: `Flag::Active` is `true`,
+/// `Flag::Inactive` is `false`. The [`Deref`] impl allows using a `Flag`
+/// directly in boolean contexts:
+///
+/// ```
+/// # use mingling_picker::value::Flag;
+/// let flag = Flag::Active;
+/// if *flag { /* runs */ }
+/// ```
+///
+/// [`PickerArg`]: crate::PickerArg
+#[derive(Default, Clone, Copy, PartialEq, Eq)]
+pub enum Flag {
+ /// The flag was **not** present on the command line.
+ ///
+ /// This is the default state, equivalent to `false`.
+ #[default]
+ Inactive,
+
+ /// The flag **was** present on the command line.
+ ///
+ /// Equivalent to `true`.
+ Active,
+}
+
+impl Debug for Flag {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Inactive => write!(f, "inactive"),
+ Self::Active => write!(f, "active"),
+ }
+ }
+}
+
+impl Display for Flag {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Inactive => write!(f, "inactive"),
+ Self::Active => write!(f, "active"),
+ }
+ }
+}
+
+impl Flag {
+ /// Converts this `Flag` into a `bool`.
+ ///
+ /// Returns `true` if the flag is [`Active`], `false` if [`Inactive`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use mingling_picker::value::Flag;
+ /// assert!(Flag::Active.bool());
+ /// assert!(!Flag::Inactive.bool());
+ /// ```
+ ///
+ /// [`Active`]: Flag::Active
+ /// [`Inactive`]: Flag::Inactive
+ #[must_use]
+ #[inline(always)]
+ pub fn bool(&self) -> bool {
+ *self == Flag::Active
+ }
+}
+
+impl PartialEq<bool> for Flag {
+ fn eq(&self, other: &bool) -> bool {
+ self.bool() == *other
+ }
+}
+
+/// Compares `bool` with `Flag` using `==`.
+impl PartialEq<Flag> for bool {
+ fn eq(&self, other: &Flag) -> bool {
+ *self == other.bool()
+ }
+}
+
+impl From<bool> for Flag {
+ fn from(value: bool) -> Self {
+ if value { Flag::Active } else { Flag::Inactive }
+ }
+}
+
+impl From<Flag> for bool {
+ fn from(val: Flag) -> Self {
+ val == Flag::Active
+ }
+}
+
+/// Allows `Flag` to be used in boolean contexts via `*flag`.
+///
+/// # Examples
+///
+/// ```
+/// # use mingling_picker::value::Flag;
+/// let flag = Flag::Active;
+/// if *flag {
+/// println!("flag is set");
+/// }
+/// ```
+impl Deref for Flag {
+ type Target = bool;
+
+ fn deref(&self) -> &bool {
+ match self {
+ Flag::Active => &true,
+ Flag::Inactive => &false,
+ }
+ }
+}
+
+impl Not for Flag {
+ type Output = Flag;
+
+ fn not(self) -> Flag {
+ match self {
+ Flag::Active => Flag::Inactive,
+ Flag::Inactive => Flag::Active,
+ }
+ }
+}
diff --git a/mingling_picker/src/value/vec_until.rs b/mingling_picker/src/value/vec_until.rs
new file mode 100644
index 0000000..1b79641
--- /dev/null
+++ b/mingling_picker/src/value/vec_until.rs
@@ -0,0 +1,134 @@
+use std::marker::PhantomData;
+use std::ops::{Deref, DerefMut};
+
+use crate::{
+ BoundaryCheck, MultiPickableWithBoundary, Pickable, PickerArg, PickerArgAttr, PickerArgResult,
+ SinglePickable, TagPhaseContext,
+ matcher_needed::Matcher,
+ parselib::{MultiArgMatcher, ParserStyle},
+};
+
+/// A `Vec`-like container that stops collecting when [`BoundaryCheck`]
+/// returns `true`.
+///
+/// This type exists to signal "I know what I'm doing with boundaries"
+/// at the type level (as opposed to `Vec<T>` which greedily takes
+/// everything).
+#[derive(Debug, Clone, PartialEq, Eq, Default)]
+pub struct VecUntil<T> {
+ pub(crate) inner: Vec<T>,
+ _marker: PhantomData<T>,
+}
+
+impl<T> VecUntil<T> {
+ pub fn into_inner(self) -> Vec<T> {
+ self.inner
+ }
+}
+
+impl<T> From<Vec<T>> for VecUntil<T> {
+ fn from(v: Vec<T>) -> Self {
+ VecUntil {
+ inner: v,
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl<T> From<VecUntil<T>> for Vec<T> {
+ fn from(v: VecUntil<T>) -> Self {
+ v.inner
+ }
+}
+
+impl<T> Deref for VecUntil<T> {
+ type Target = Vec<T>;
+ fn deref(&self) -> &Vec<T> {
+ &self.inner
+ }
+}
+
+impl<T> DerefMut for VecUntil<T> {
+ fn deref_mut(&mut self) -> &mut Vec<T> {
+ &mut self.inner
+ }
+}
+
+// MultiPickableWithBoundary impl
+
+impl<T> MultiPickableWithBoundary for VecUntil<T>
+where
+ T: SinglePickable + BoundaryCheck,
+{
+ type Checker = T;
+
+ fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self> {
+ let mut inner = Vec::with_capacity(raw.len());
+ for s in &raw {
+ match T::pick_single(Some(s)) {
+ PickerArgResult::Parsed(v) => inner.push(v),
+ PickerArgResult::NotFound => return PickerArgResult::NotFound,
+ PickerArgResult::Unparsed => {}
+ }
+ }
+ PickerArgResult::Parsed(VecUntil {
+ inner,
+ _marker: PhantomData,
+ })
+ }
+}
+
+// Pickable impl
+
+impl<'a, T> Pickable<'a> for VecUntil<T>
+where
+ T: SinglePickable + BoundaryCheck,
+{
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_multi(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ let args = ctx.args;
+ let is_positional = ctx.arg_info.positional;
+ let positions = MultiArgMatcher::match_all(ctx.into());
+ if positions.is_empty() {
+ return positions;
+ }
+
+ let start = if is_positional { 0 } else { 1 };
+ if start >= positions.len() {
+ return positions;
+ }
+
+ let mut cut = start;
+ for &idx in &positions[start..] {
+ if let Some(raw) = args.get(idx)
+ && T::check_boundary(raw)
+ {
+ break;
+ }
+ cut += 1;
+ }
+
+ positions[..cut].to_vec()
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ let strs = strip_flag(raw_strs);
+ let owned: Vec<String> = strs.iter().map(|&s| s.to_string()).collect();
+ <VecUntil<T> as MultiPickableWithBoundary>::pick_multi(owned)
+ }
+}
+
+/// If the first raw string looks like a named flag (starts with the
+/// style's long or short prefix), strip it — it's the flag, not a value.
+fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] {
+ if let Some(first) = raw_strs.first() {
+ let style = ParserStyle::global_style();
+ if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) {
+ return &raw_strs[1..];
+ }
+ }
+ raw_strs
+}
diff --git a/mingling_picker/test/Cargo.lock b/mingling_picker/test/Cargo.lock
new file mode 100644
index 0000000..0fef84b
--- /dev/null
+++ b/mingling_picker/test/Cargo.lock
@@ -0,0 +1,68 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
+name = "mingling_picker"
+version = "0.3.0"
+dependencies = [
+ "just_fmt",
+ "mingling_picker_macros",
+]
+
+[[package]]
+name = "mingling_picker_macros"
+version = "0.3.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+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 = "test-mingling-picker"
+version = "0.1.0"
+dependencies = [
+ "mingling_picker",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/mingling_picker/test/Cargo.toml b/mingling_picker/test/Cargo.toml
new file mode 100644
index 0000000..127546c
--- /dev/null
+++ b/mingling_picker/test/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "test-mingling-picker"
+version = "0.1.0"
+edition = "2024"
+
+[workspace]
+
+[dependencies]
+mingling_picker = { path = "../" }
diff --git a/mingling_picker/test/src/lib.rs b/mingling_picker/test/src/lib.rs
new file mode 100644
index 0000000..eac6cad
--- /dev/null
+++ b/mingling_picker/test/src/lib.rs
@@ -0,0 +1,23 @@
+// Using `assert_eq!(x, true)` is clearer than `assert!(x)` for expressing expected values
+//
+// BECAUSE `assert!` only checks if the boolean value is true,
+// while `assert_eq!` explicitly shows the expected value
+#![allow(clippy::bool_assert_comparison)]
+
+#[cfg(test)]
+mod test;
+
+use mingling_picker::parselib::MaskedArg;
+
+/// Create a single `MaskedArg` from a raw string and its original index.
+pub fn make_masked(raw: &str, idx: usize) -> MaskedArg<'_> {
+ MaskedArg { raw, raw_idx: idx }
+}
+
+/// Create a `Vec<MaskedArg>` from an array of `(raw, raw_idx)` pairs.
+pub fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec<MaskedArg<'a>> {
+ pairs
+ .iter()
+ .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx })
+ .collect()
+}
diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs
new file mode 100644
index 0000000..9c53514
--- /dev/null
+++ b/mingling_picker/test/src/test.rs
@@ -0,0 +1,10 @@
+mod arg_matcher_test;
+mod basic_test;
+mod multi_arg_test;
+mod multi_value_test;
+mod pos_matcher_test;
+mod priority_test;
+mod route_test;
+mod style_test;
+mod value_flag_test;
+mod value_string_test;
diff --git a/mingling_picker/test/src/test/arg_matcher_test.rs b/mingling_picker/test/src/test/arg_matcher_test.rs
new file mode 100644
index 0000000..7b37bc8
--- /dev/null
+++ b/mingling_picker/test/src/test/arg_matcher_test.rs
@@ -0,0 +1,289 @@
+use mingling_picker::PickerArgInfo;
+use mingling_picker::parselib::{ArgMatcher, Matcher, POWERSHELL_STYLE, UNIX_STYLE};
+
+use crate::make_args;
+
+// on_match_one — Named
+
+#[test]
+fn test_match_one_named_basic() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_match_one_named_eq_mode() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name=Alice", 0)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_match_one_named_no_match() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--other", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+#[test]
+fn test_match_one_named_short_flag() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ info.set_short('n');
+ let args = make_args(&[("-n", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_match_one_named_after_end_of_options() {
+ // Flags after `--` should not be matched.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--", 0), ("--name", 1), ("Alice", 2)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+// on_match_one — Positional
+
+#[test]
+fn test_match_one_positional_basic() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("file.txt", 0)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_match_one_positional_takes_first() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("a.txt", 0), ("b.txt", 1)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+// on_match_all — Named, single occurrence
+
+#[test]
+fn test_match_all_named_flag_plus_value() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_match_all_named_eq_mode() {
+ // --name=Alice: value is inline, only tag the flag position.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name=Alice", 0)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_match_all_named_no_value() {
+ // Flag at end with no following arg: only tag the flag.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name", 0)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_match_all_named_value_looks_like_flag() {
+ // The next arg looks like a flag — still tag it.
+ // Validation is the Pickable's responsibility.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name", 0), ("--other", 1)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+// on_match_all — Named, multiple occurrences (Single per flag)
+
+#[test]
+fn test_match_all_named_two_occurrences() {
+ // --name Alice --name Bob → each occurrence gets one value.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name", 0), ("Alice", 1), ("--name", 2), ("Bob", 3)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3]);
+}
+
+#[test]
+fn test_match_all_named_skips_non_matching_args() {
+ // --val a b --val d → only pairs (0,1) and (3,4); idx 2 ("b") left free.
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--val", 3), ("d", 4)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 3, 4]);
+}
+
+// on_match_all — Named, short flag
+
+#[test]
+fn test_match_all_named_short_flag() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ info.set_short('n');
+ let args = make_args(&[("-n", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+// on_match_all — Named, eq + non-eq mixed
+
+#[test]
+fn test_match_all_named_mixed_eq_and_regular() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name=Alice", 0), ("--name", 1), ("Bob", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2]);
+}
+
+// on_match_all — Named, case insensitive (PowerShell)
+
+#[test]
+fn test_match_all_named_powershell_case_insensitive() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("Name");
+ let args = make_args(&[("-name", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_all(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+// on_match_all — Positional
+
+#[test]
+fn test_match_all_positional_single() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("file.txt", 0)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_match_all_positional_multiple() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("a.txt", 0), ("b.txt", 1)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+// End-of-options marker (`--`)
+
+#[test]
+fn test_match_all_named_stops_at_end_of_options() {
+ // --name before `--` should match, --name after should not.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[
+ ("--name", 0),
+ ("Alice", 1),
+ ("--", 2),
+ ("--name", 3),
+ ("Bob", 4),
+ ]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_match_all_positional_stops_at_end_of_options() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("before", 0), ("--", 1), ("after", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+// Empty args
+
+#[test]
+fn test_match_one_empty() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = vec![];
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+#[test]
+fn test_match_all_empty() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = vec![];
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// Verify that -- itself is never matched as a flag
+
+#[test]
+fn test_match_all_end_of_options_not_matched() {
+ // `--` should neither match as a flag nor take a value.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--", 0)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// `--` should never be consumed as a value by a named flag.
+
+#[test]
+fn test_match_all_named_does_not_consume_end_marker() {
+ // `--flag` before `--`, but the next position IS `--`.
+ // Only tag the flag, don't consume the end-of-options marker.
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag");
+ let args = make_args(&[("--flag", 0), ("--", 1), ("value", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0], "should NOT consume -- as a value");
+}
+
+#[test]
+fn test_match_all_named_does_not_consume_end_marker_flag_before_end() {
+ // Simulates: --flag -- you where -- is end-of-options.
+ // The flag should be tagged but -- should NOT be consumed as its value.
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag");
+
+ let args = make_args(&[("--flag", 0), ("--", 1), ("you", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result,
+ vec![0],
+ "flag before --: only tag flag, leave -- for positional"
+ );
+}
+
+#[test]
+fn test_match_all_flag_after_end_has_value() {
+ // Flag after `--` should not match at all.
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag");
+ let args = make_args(&[("--", 0), ("--flag", 1), ("value", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
diff --git a/mingling_picker/test/src/test/basic_test.rs b/mingling_picker/test/src/test/basic_test.rs
new file mode 100644
index 0000000..78b154e
--- /dev/null
+++ b/mingling_picker/test/src/test/basic_test.rs
@@ -0,0 +1,223 @@
+use mingling_picker::{IntoPicker, macros::arg};
+
+// Basic bool flag — present / absent
+
+#[test]
+fn test_bool_flag_present() {
+ let args = vec!["--verbose"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+#[test]
+fn test_bool_flag_absent() {
+ let args: Vec<&str> = vec![];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, false);
+}
+
+// Short flag — '-v'
+
+#[test]
+fn test_bool_short_flag_present() {
+ let args = vec!["-v"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool, 'v'])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+// Multiple bool flags at once
+
+#[test]
+fn test_two_bool_flags_both_present() {
+ // UNIX_STYLE now uses Kebab naming: `flag_a` → "flag-a" → --flag-a
+ let args = vec!["--flag-a", "--flag-b"];
+ let (a, b) = args
+ .to_picker()
+ .pick(&arg![flag_a: bool])
+ .or_default()
+ .pick(&arg![flag_b: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, true);
+ assert_eq!(b, true);
+}
+
+#[test]
+fn test_two_bool_flags_one_present() {
+ let args = vec!["--flag-a"];
+ let (a, b) = args
+ .to_picker()
+ .pick(&arg![flag_a: bool])
+ .or_default()
+ .pick(&arg![flag_b: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, true);
+ assert_eq!(b, false);
+}
+
+#[test]
+fn test_two_bool_flags_neither_present() {
+ let args: Vec<&str> = vec![];
+ let (a, b) = args
+ .to_picker()
+ .pick(&arg![flag_a: bool])
+ .or_default()
+ .pick(&arg![flag_b: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, false);
+ assert_eq!(b, false);
+}
+
+// Mixed short and long flags
+
+#[test]
+fn test_short_and_long_flags() {
+ let args = vec!["-a", "--long-b"];
+ let (a, b) = args
+ .to_picker()
+ .pick(&arg![flag_a: bool, 'a'])
+ .or_default()
+ .pick(&arg![long_b: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, true);
+ assert_eq!(b, true);
+}
+
+// Flags after `--` (end-of-options marker) should not be parsed.
+
+#[test]
+fn test_flag_after_end_of_options() {
+ let args = vec!["--", "--verbose"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, false);
+}
+
+// Alias matching for bool flags
+
+#[test]
+fn test_bool_flag_with_alias() {
+ let args = vec!["--cfg"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![config: bool, "cfg"])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+#[test]
+fn test_bool_flag_primary_name() {
+ let args = vec!["--config"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![config: bool, "cfg"])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+// Short flag + alias for bool flag
+
+#[test]
+fn test_bool_flag_short_and_alias() {
+ let args = vec!["-v"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool, 'v', "cfg"])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+// Default values: .or() / .or_default()
+
+#[test]
+fn test_or_default_without_args() {
+ let args: Vec<&str> = vec![];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, false);
+}
+
+#[test]
+fn test_or_custom_default() {
+ let args: Vec<&str> = vec![];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or(|| true)
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+// to_result / to_option interface
+
+#[test]
+fn test_to_result_ok() {
+ let args = vec!["--verbose"];
+ let result = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .to_result();
+ assert_eq!(result, Ok(true));
+}
+
+#[test]
+fn test_to_option_some() {
+ let args = vec!["--verbose"];
+ let opt = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .to_option();
+ assert_eq!(opt, Some(true));
+}
+
+// Chain with_route passthrough
+
+#[test]
+fn test_with_route_chain() {
+ let args = vec!["--flag"];
+ let parsed = args
+ .with_route::<String>()
+ .pick(&arg![flag: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+// Unrelated flag should not match
+
+#[test]
+fn test_unrelated_flag_does_not_match() {
+ let args = vec!["--other"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, false);
+}
diff --git a/mingling_picker/test/src/test/multi_arg_test.rs b/mingling_picker/test/src/test/multi_arg_test.rs
new file mode 100644
index 0000000..377357e
--- /dev/null
+++ b/mingling_picker/test/src/test/multi_arg_test.rs
@@ -0,0 +1,181 @@
+use mingling_picker::parselib::{Matcher, MultiArgMatcher, UNIX_STYLE};
+use mingling_picker::PickerArgInfo;
+
+use crate::make_args;
+
+// on_match_one — basic
+
+#[test]
+fn test_multi_one_finds_first_flag() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--other", 0), ("--val", 1), ("a", 2), ("b", 3)]);
+ let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(1));
+}
+
+#[test]
+fn test_multi_one_no_match() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--other", 0)]);
+ let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+// on_match_all — named, basic multi-value
+
+#[test]
+fn test_multi_all_flag_takes_all_values_until_next_flag() {
+ // --val a b c → all three values belong to --val
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("c", 3)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3]);
+}
+
+#[test]
+fn test_multi_all_stops_at_next_flag() {
+ // --val a b --other c d → only a,b belong to --val
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--other", 3), ("c", 4)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2]);
+}
+
+#[test]
+fn test_multi_all_flag_no_values() {
+ // --val at end with no values → just the flag
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_multi_all_empty() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = vec![];
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// on_match_all — named, multiple occurrences of same flag
+
+#[test]
+fn test_multi_all_two_occurrences() {
+ // --val a b --val c d → two groups
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[
+ ("--val", 0), ("a", 1), ("b", 2),
+ ("--val", 3), ("c", 4), ("d", 5),
+ ]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3, 4, 5]);
+}
+
+#[test]
+fn test_multi_all_skips_non_matching_args() {
+ // --val a --other b --val c → only --val groups
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[
+ ("--val", 0), ("a", 1),
+ ("--other", 2), ("b", 3),
+ ("--val", 4), ("c", 5),
+ ]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 4, 5]);
+}
+
+// on_match_all — named, eq mode
+
+#[test]
+fn test_multi_all_eq_mode() {
+ // --val=a b → eq mode + one extra value
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val=a", 0), ("b", 1)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_multi_all_eq_mode_no_extra() {
+ // --val=a alone → just the eq flag
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val=a", 0)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_multi_all_mixed_eq_and_regular() {
+ // --val=a b --val c d
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[
+ ("--val=a", 0), ("b", 1), ("--val", 2), ("c", 3), ("d", 4),
+ ]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3, 4]);
+}
+
+// on_match_all — short flag
+
+#[test]
+fn test_multi_all_short_flag() {
+ let mut info = PickerArgInfo::new();
+ info.set_short('v');
+ info.set_long("val");
+ let args = make_args(&[("-v", 0), ("a", 1), ("b", 2)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2]);
+}
+
+// on_match_all — end-of-options marker
+
+#[test]
+fn test_multi_all_stops_at_end_of_options() {
+ // --val a -- b → stops before `--`
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("--", 2), ("b", 3)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_multi_all_flag_after_end_ignored() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--", 0), ("--val", 1), ("a", 2)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// on_match_all — positional
+
+#[test]
+fn test_multi_all_positional() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("a.txt", 0), ("b.txt", 1)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_multi_all_positional_stops_at_end_of_options() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("a.txt", 0), ("--", 1), ("b.txt", 2)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
diff --git a/mingling_picker/test/src/test/multi_value_test.rs b/mingling_picker/test/src/test/multi_value_test.rs
new file mode 100644
index 0000000..0f56517
--- /dev/null
+++ b/mingling_picker/test/src/test/multi_value_test.rs
@@ -0,0 +1,66 @@
+use mingling_picker::value::{Flag, VecUntil};
+use mingling_picker::{IntoPicker, macros::arg};
+
+#[test]
+fn test_vec_until_i16_named() {
+ let (nums, rest): (VecUntil<i16>, Flag) = vec!["--nums", "1", "2", "3", "abc"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .pick(&arg![rest: Flag])
+ .unwrap();
+ assert_eq!(*nums, vec![1i16, 2, 3]);
+ assert_eq!(rest, Flag::Inactive);
+}
+
+#[test]
+fn test_vec_until_i16_stops_at_non_number() {
+ let nums: VecUntil<i16> = vec!["--nums", "42", "abc", "100"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*nums, vec![42i16]);
+}
+
+#[test]
+fn test_vec_until_i16_empty() {
+ let nums: VecUntil<i16> = vec!["--nums"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert!(nums.is_empty());
+}
+
+#[test]
+fn test_vec_until_i16_stops_at_next_flag() {
+ let nums: VecUntil<i16> = vec!["--nums", "1", "2", "--other", "3"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*nums, vec![1i16, 2]);
+}
+
+#[test]
+fn test_vec_until_i16_stops_at_end_of_options() {
+ let nums: VecUntil<i16> = vec!["--nums", "1", "2", "--", "3"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*nums, vec![1i16, 2]);
+}
+
+// Two VecUntil<T> with different boundary behaviours
+
+#[test]
+fn test_vec_until_f64_and_i16_positional() {
+ // Both are positional VecUntil. f64 takes all valid floats,
+ // i16 takes what's left. But note: "1", "2", "3" are also valid
+ // f64 values, so f64's check_boundary never fires — it consumes
+ // everything, and i16 gets nothing.
+ let (floats, ints): (VecUntil<f64>, VecUntil<i16>) = vec!["1.5", "2.5", "3.5", "1", "2", "3"]
+ .to_picker()
+ .pick(&arg![VecUntil<f64>])
+ .pick(&arg![VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*floats, vec![1.5, 2.5, 3.5]);
+ assert_eq!(*ints, vec![1i16, 2, 3]);
+}
diff --git a/mingling_picker/test/src/test/pos_matcher_test.rs b/mingling_picker/test/src/test/pos_matcher_test.rs
new file mode 100644
index 0000000..b5319f5
--- /dev/null
+++ b/mingling_picker/test/src/test/pos_matcher_test.rs
@@ -0,0 +1,141 @@
+use mingling_picker::PickerArgInfo;
+use mingling_picker::parselib::{MaskedArg, Matcher, PositionalMatcher, UNIX_STYLE, WINDOWS_STYLE};
+
+fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec<MaskedArg<'a>> {
+ pairs
+ .iter()
+ .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx })
+ .collect()
+}
+
+// on_match_one — basic
+
+#[test]
+fn test_pos_one_takes_first_non_flag() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--verbose", 0), ("file.txt", 1)]);
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(1));
+}
+
+#[test]
+fn test_pos_one_takes_first_if_no_flag() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("file.txt", 0)]);
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_pos_one_all_flags_returns_none() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--verbose", 0), ("--name", 1)]);
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+#[test]
+fn test_pos_one_empty_returns_none() {
+ let info = PickerArgInfo::new();
+ let args = vec![];
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+// on_match_one — after `--`
+
+#[test]
+fn test_pos_one_after_end_takes_even_flag_like() {
+ // After `--`, accept everything including `--verbose`.
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--", 0), ("--verbose", 1), ("file.txt", 2)]);
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(1));
+}
+
+#[test]
+fn test_pos_one_only_end_returns_none() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--", 0)]);
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+// on_match_one — Windows style prefix
+
+#[test]
+fn test_pos_one_windows_skips_slash_prefix() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("/Verbose", 0), ("file.txt", 1)]);
+ let result = PositionalMatcher::on_match_one(&args, &WINDOWS_STYLE, &info);
+ assert_eq!(result, Some(1));
+}
+
+// on_match_all — basic
+
+#[test]
+fn test_pos_all_collects_non_flags() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--verbose", 0), ("a.txt", 1), ("--name", 2), ("b.txt", 3)]);
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![1, 3]);
+}
+
+#[test]
+fn test_pos_all_only_flags_returns_empty() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--a", 0), ("--b", 1)]);
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+#[test]
+fn test_pos_all_empty() {
+ let info = PickerArgInfo::new();
+ let args = vec![];
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// on_match_all — after `--`
+
+#[test]
+fn test_pos_all_after_end_accepts_everything() {
+ // After `--`, even `--verbose` is accepted as positional.
+ let info = PickerArgInfo::new();
+ let args = make_args(&[
+ ("--verbose", 0),
+ ("a.txt", 1),
+ ("--", 2),
+ ("--flag", 3),
+ ("b.txt", 4),
+ ]);
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![1, 3, 4]);
+}
+
+#[test]
+fn test_pos_all_only_after_end() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--", 0), ("arg1", 1), ("--arg2", 2)]);
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![1, 2]);
+}
+
+// on_match_all — mixed before/after `--`
+
+#[test]
+fn test_pos_all_mixed() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[
+ ("infile", 0),
+ ("--verbose", 1),
+ ("--", 2),
+ ("outfile", 3),
+ ("--extra", 4),
+ ]);
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ // Before `--`: "infile" (skip --verbose).
+ // After `--`: everything — "outfile", "--extra".
+ assert_eq!(result, vec![0, 3, 4]);
+}
diff --git a/mingling_picker/test/src/test/priority_test.rs b/mingling_picker/test/src/test/priority_test.rs
new file mode 100644
index 0000000..8179389
--- /dev/null
+++ b/mingling_picker/test/src/test/priority_test.rs
@@ -0,0 +1,85 @@
+use mingling_picker::value::Flag;
+use mingling_picker::{IntoPicker, macros::arg};
+
+// Same flag name, different Pickable types
+//
+// PickerArgAttr priority: Flag < Single
+// So Single and Multi should parse BEFORE Flag when sharing
+// the same flag name.
+
+#[test]
+fn test_single_takes_flag_when_sharing_name() {
+ // --name Alice: String consumes it, Flag sees nothing.
+ let (name, verbose): (String, Flag) = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String]) // Single, parsed first
+ .pick(&arg![name: Flag]) // Flag, parsed second, nothing left
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(verbose, Flag::Inactive);
+}
+
+#[test]
+fn test_flag_only_triggers_when_single_missing() {
+ // --verbose: only Flag matches, String gets default.
+ let (name, verbose): (String, Flag) = vec!["--verbose"]
+ .to_picker()
+ .pick(&arg![name: String]) // Single, no match → default ""
+ .or_default()
+ .pick(&arg![name: Flag]) // Flag, no --name in args → Inactive
+ .unwrap();
+ assert_eq!(name, "");
+ assert_eq!(verbose, Flag::Inactive);
+}
+
+#[test]
+fn test_flag_gets_leftovers_after_single_consumes_value() {
+ // --name Alice --name: String takes "--name Alice", Flag takes "--name".
+ let (name, verbose): (String, Flag) = vec!["--name", "Alice", "--name"]
+ .to_picker()
+ .pick(&arg![name: String]) // Single: tag [0, 1], consumes --name Alice
+ .pick(&arg![name: Flag]) // Flag: tags position 2 (--name)
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(
+ verbose,
+ Flag::Active,
+ "Flag should see --name at position 2 after String consumed positions 0-1"
+ );
+}
+
+#[test]
+fn test_short_flag_sharing_same_letter() {
+ // -n Alice: String takes it, Flag misses.
+ let (name, verbose): (String, Flag) = vec!["-n", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String, 'n']) // Single, parsed first
+ .pick(&arg![name: Flag, 'n']) // Flag, parsed second
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(verbose, Flag::Inactive);
+}
+
+#[test]
+fn test_flag_captures_remaining_after_single_partial_consume() {
+ // --name Alice --verbose: String takes --name Alice, Flag takes --verbose.
+ let (name, verbose): (String, Flag) = vec!["--name", "Alice", "--verbose"]
+ .to_picker()
+ .pick(&arg![name: String]) // Single: tag [0, 1]
+ .pick(&arg![verbose: Flag]) // Flag: tag [2]
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(verbose, Flag::Active);
+}
+
+#[test]
+fn test_single_skips_already_claimed_positions() {
+ // --verbose --name Alice: Flag takes --verbose, String takes --name Alice.
+ let (verbose, name): (Flag, String) = vec!["--verbose", "--name", "Alice"]
+ .to_picker()
+ .pick(&arg![verbose: Flag]) // Flag: tag [0]
+ .pick(&arg![name: String]) // Single: tag [1, 2]
+ .unwrap();
+ assert_eq!(verbose, Flag::Active);
+ assert_eq!(name, "Alice");
+}
diff --git a/mingling_picker/test/src/test/route_test.rs b/mingling_picker/test/src/test/route_test.rs
new file mode 100644
index 0000000..9261db1
--- /dev/null
+++ b/mingling_picker/test/src/test/route_test.rs
@@ -0,0 +1,200 @@
+use mingling_picker::{IntoPicker, macros::arg};
+
+// Route mechanism — or_route
+
+#[test]
+fn test_or_route_triggered_to_result() {
+ // flag not present and no default value → route triggered → Err
+ let args: Vec<&str> = vec![];
+ let result: Result<bool, &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "missing_verbose")
+ .to_result();
+ assert_eq!(result, Err("missing_verbose"));
+}
+
+#[test]
+fn test_or_route_not_triggered_when_flag_present() {
+ // flag present → route not triggered → Ok
+ let args = vec!["--verbose"];
+ let result: Result<bool, &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "missing_verbose")
+ .to_result();
+ assert_eq!(result, Ok(true));
+}
+
+#[test]
+fn test_or_default_priority_over_or_route() {
+ // or_default takes priority over or_route: even if the flag is absent,
+ // having a default prevents the route from being triggered
+ let args: Vec<&str> = vec![];
+ let result: Result<bool, &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .or_route(|| "should_not_reach")
+ .to_result();
+ assert_eq!(result, Ok(false));
+}
+
+#[test]
+fn test_or_route_to_option_returns_none() {
+ // When route is triggered, to_option returns None
+ let args: Vec<&str> = vec![];
+ let opt: Option<bool> = args
+ .with_route::<&'static str>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "missing")
+ .to_option();
+ assert_eq!(opt, None);
+}
+
+#[test]
+#[should_panic(expected = "called `Option::unwrap()` on a `None` value")]
+fn test_or_route_unwrap_panics() {
+ let args: Vec<&str> = vec![];
+ args.with_route::<&'static str>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "missing")
+ .unwrap();
+}
+
+#[test]
+fn test_or_route_with_string_route() {
+ // Route type is String
+ let args: Vec<&str> = vec![];
+ let result: Result<bool, String> = args
+ .with_route::<String>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "route_hit".to_string())
+ .to_result();
+ assert_eq!(result, Err("route_hit".to_string()));
+}
+
+#[test]
+fn test_or_route_with_custom_enum() {
+ // Route type is a custom enum
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ #[allow(dead_code)]
+ enum Redirect {
+ Help,
+ Version,
+ }
+
+ let args: Vec<&str> = vec![];
+ let result: Result<bool, Redirect> = args
+ .with_route::<Redirect>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| Redirect::Help)
+ .to_result();
+ assert_eq!(result, Err(Redirect::Help));
+}
+
+// Route mechanism — multiple flags
+
+#[test]
+fn test_two_flags_first_missing_triggers_route() {
+ // Both flags missing; first has or_route → Err("first");
+ // first route wins (first-checked, first-triggered)
+ let args: Vec<&str> = vec![];
+ let result: Result<(bool, bool), &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![flag_a: bool])
+ .or_route(|| "first_missing")
+ .pick(&arg![flag_b: bool])
+ .or_route(|| "second_missing")
+ .to_result();
+ assert_eq!(result, Err("first_missing"));
+}
+
+#[test]
+fn test_two_flags_second_missing_triggers_route() {
+ // First has or_default (no route triggered), second missing with or_route → Err("second")
+ let args: Vec<&str> = vec![];
+ let result: Result<(bool, bool), &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![flag_a: bool])
+ .or_default()
+ .pick(&arg![flag_b: bool])
+ .or_route(|| "second_missing")
+ .to_result();
+ assert_eq!(result, Err("second_missing"));
+}
+
+#[test]
+fn test_two_flags_both_present_route_not_triggered() {
+ // Both flags present → route not triggered → Ok((true, true))
+ let args = vec!["--flag-a", "--flag-b"];
+ let result: Result<(bool, bool), &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![flag_a: bool])
+ .or_route(|| "first_missing")
+ .pick(&arg![flag_b: bool])
+ .or_route(|| "second_missing")
+ .to_result();
+ assert_eq!(result, Ok((true, true)));
+}
+
+#[test]
+fn test_two_flags_first_missing_no_route_second_has_route() {
+ // First missing but has no or_route, second missing with or_route → Err("second")
+ // Note: the first has neither route nor default, so pick failure does not modify error_route
+ let args: Vec<&str> = vec![];
+ let result: Result<(bool, bool), &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![flag_a: bool])
+ // No or_default, no or_route either
+ .pick(&arg![flag_b: bool])
+ .or_route(|| "second_missing")
+ .to_result();
+ assert_eq!(result, Err("second_missing"));
+}
+
+#[test]
+fn test_two_flags_only_second_has_default() {
+ // First is missing with no default/route (v1=NotFound), second is missing but has or_default
+ // Use unpack to check both results
+ let args: Vec<&str> = vec![];
+ let (a, b) = args
+ .to_picker()
+ .pick(&arg![flag_a: bool])
+ .pick(&arg![flag_b: bool])
+ .or_default()
+ .unpack();
+ assert_eq!(a, None);
+ assert_eq!(b, Some(false));
+}
+
+// Route with with_route
+
+#[test]
+fn test_with_route_and_or_route() {
+ // with_route::&lt;String&gt;() sets the Route type + or_route triggers
+ let args: Vec<&str> = vec![];
+ let result: Result<bool, String> = args
+ .with_route::<String>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "redirected".to_string())
+ .to_result();
+ assert_eq!(result, Err("redirected".to_string()));
+}
+
+#[test]
+fn test_with_route_type_switch() {
+ // with_route switching Route type clears old route_$ and error_route
+ let args: Vec<&str> = vec![];
+ let result: Result<(bool, bool), i32> = args
+ .with_route::<String>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "string_route".to_string())
+ .with_route::<i32>()
+ .pick(&arg![other: bool])
+ .or_route(|| -1)
+ .to_result();
+ // The first flag is missing, but its or_route is cleared when with_route switches (route_$ = None)
+ // The second flag is missing and has or_route → Err(-1)
+ assert_eq!(result, Err(-1));
+}
diff --git a/mingling_picker/test/src/test/style_test.rs b/mingling_picker/test/src/test/style_test.rs
new file mode 100644
index 0000000..3c337b9
--- /dev/null
+++ b/mingling_picker/test/src/test/style_test.rs
@@ -0,0 +1,300 @@
+use mingling_picker::PickerArgInfo;
+use mingling_picker::parselib::{
+ FlagMatcher, Matcher, POWERSHELL_STYLE, ParserStyle, ParserStyleNamingCase, UNIX_STYLE,
+ WINDOWS_STYLE, build_possible_flags,
+};
+
+use crate::make_masked;
+
+// Style: formatting utilities
+
+#[test]
+fn test_unix_style_flag_string() {
+ assert_eq!(UNIX_STYLE.flag_string('v'), "-v");
+ assert_eq!(UNIX_STYLE.flag_string("verbose"), "--verbose");
+}
+
+#[test]
+fn test_windows_style_flag_string() {
+ assert_eq!(WINDOWS_STYLE.flag_string('v'), "/v");
+ assert_eq!(WINDOWS_STYLE.flag_string("verbose"), "/verbose");
+}
+
+#[test]
+fn test_powershell_style_flag_string() {
+ assert_eq!(POWERSHELL_STYLE.flag_string('v'), "-v");
+ assert_eq!(POWERSHELL_STYLE.flag_string("Verbose"), "-Verbose");
+}
+
+#[test]
+fn test_build_possible_flags_windows() {
+ // Build PickerArgInfo from a flag definition: `verbose: bool`
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+ let flags = build_possible_flags(&WINDOWS_STYLE, &info);
+ assert_eq!(flags, vec!["/Verbose"]);
+}
+
+#[test]
+fn test_build_possible_flags_with_short_and_alias() {
+ let mut info = PickerArgInfo::new();
+ info.set_short('n');
+ info.set_long("name");
+ info.set_alias(vec!["nickname"]);
+ let flags = build_possible_flags(&UNIX_STYLE, &info);
+ assert_eq!(flags, vec!["-n", "--name", "--nickname"]);
+}
+
+// Style: matching with different styles via Matcher trait
+
+#[test]
+fn test_windows_style_match() {
+ // Windows style: /verbose (case insensitive)
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![make_masked("/verbose", 0)];
+ let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_windows_style_match_case_insensitive() {
+ // Windows style is case-insensitive: /VERBOSE should match "verbose"
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![make_masked("/VERBOSE", 0)];
+ let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_windows_style_no_match_on_unrelated_flag() {
+ // Different flag should not match
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![make_masked("/output", 0)];
+ let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+#[test]
+fn test_powershell_style_match() {
+ // PowerShell style: -Verbose (case insensitive)
+ let mut info = PickerArgInfo::new();
+ info.set_long("Verbose");
+
+ let args = vec![make_masked("-Verbose", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_powershell_style_match_case_insensitive() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("Verbose");
+
+ let args = vec![make_masked("-VERBOSE", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_unix_style_case_sensitive_no_match() {
+ // UNIX style is case-sensitive: --VERBOSE should NOT match --verbose
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![make_masked("--VERBOSE", 0)];
+ let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+#[test]
+fn test_windows_style_match_all() {
+ // on_match_all should find all matching flags
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+ info.set_short('v');
+
+ let args = vec![
+ make_masked("/v", 0),
+ make_masked("/output", 1),
+ make_masked("/VERBOSE", 2),
+ ];
+ let result = FlagMatcher::on_match_all(&args, &WINDOWS_STYLE, &info);
+ assert_eq!(result, vec![0, 2]);
+}
+
+#[test]
+fn test_windows_style_match_after_end_of_options() {
+ // end_of_options is always "--" regardless of style
+ // Flags after -- should not match
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![
+ make_masked("/verbose", 0),
+ make_masked("--", 1),
+ make_masked("/verbose", 2),
+ ];
+ let result = FlagMatcher::on_match_all(&args, &WINDOWS_STYLE, &info);
+ // end_of_options is always "--" regardless of style
+ assert_eq!(result, vec![0]);
+}
+
+// Naming case conversion
+
+/// A Unix-like style with kebab-case naming.
+const KEBAB_STYLE: ParserStyle<'static> = ParserStyle {
+ naming_case: ParserStyleNamingCase::Kebab,
+ ..UNIX_STYLE
+};
+
+#[test]
+fn test_kebab_case_naming_for_multiword_flag() {
+ // flag name `flag_a` → Kebab → `flag-a` → `--flag-a`
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag_a");
+
+ let args = vec![make_masked("--flag-a", 0)];
+ let result = FlagMatcher::on_match_one(&args, &KEBAB_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "--flag-a should match flag_a via kebab-case conversion"
+ );
+}
+
+#[test]
+fn test_snake_case_should_not_match_as_long_flag() {
+ // With kebab naming, `--flag_a` (snake) should NOT match `flag_a`
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag_a");
+
+ let args = vec![make_masked("--flag_a", 0)];
+ let result = FlagMatcher::on_match_one(&args, &KEBAB_STYLE, &info);
+ assert_eq!(
+ result, None,
+ "--flag_a should NOT match in kebab-style context"
+ );
+}
+
+#[test]
+fn test_kebab_case_naming_for_unix_style() {
+ // UNIX_STYLE now uses Kebab case: `flag_a` → `flag-a` → `--flag-a`
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag_a");
+
+ let args = vec![make_masked("--flag-a", 0)];
+ let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "--flag-a should match with kebab-style UNIX_STYLE"
+ );
+}
+
+#[test]
+fn test_snake_case_rejected_by_unix_style() {
+ // UNIX_STYLE uses Kebab: `--flag_a` (snake) should NOT match
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag_a");
+
+ let args = vec![make_masked("--flag_a", 0)];
+ let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result, None,
+ "--flag_a should NOT match under kebab-style UNIX_STYLE"
+ );
+}
+
+#[test]
+fn test_powershell_pascal_case_naming() {
+ // POWERSHELL_STYLE uses Pascal case: `verbose` → `Verbose` → `-Verbose`
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![make_masked("-Verbose", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "-Verbose should match verbose via Pascal case"
+ );
+}
+
+#[test]
+fn test_flag_naming_kebab_matches_my_name() {
+ // UNIX_STYLE now uses Kebab: `my_name` → `my-name` → `--my-name`
+ let mut info = PickerArgInfo::new();
+ info.set_long("my_name");
+
+ let args = vec![make_masked("--my-name", 0)];
+ let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "--my-name should match via kebab conversion"
+ );
+}
+
+#[test]
+fn test_flag_naming_kebab_rejects_my_name_underscore() {
+ // Kebab style: `--my_name` (snake) should NOT match
+ let mut info = PickerArgInfo::new();
+ info.set_long("my_name");
+
+ let args = vec![make_masked("--my_name", 0)];
+ let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result, None,
+ "--my_name should NOT match under kebab-style UNIX_STYLE"
+ );
+}
+
+#[test]
+fn test_flag_naming_pascal_matches_my_name() {
+ // `my_name` under Pascal → `MyName` → `-MyName`
+ let mut info = PickerArgInfo::new();
+ info.set_long("my_name");
+
+ let args = vec![make_masked("-MyName", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "-MyName should match via Pascal conversion"
+ );
+}
+
+#[test]
+fn test_flag_naming_pascal_matches_lowercase() {
+ // PowerShell is case-insensitive: `-myname` should also match
+ let mut info = PickerArgInfo::new();
+ info.set_long("my_name");
+
+ let args = vec![make_masked("-myname", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "-myname (lowercase) should match via case-insensitive Pascal"
+ );
+}
+
+#[test]
+fn test_flag_naming_pascal_rejects_my_name_underscore() {
+ // Pascal style: `-my_name` (underscore) should NOT match
+ let mut info = PickerArgInfo::new();
+ info.set_long("my_name");
+
+ let args = vec![make_masked("-my_name", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(
+ result, None,
+ "-my_name should NOT match under Pascal-style naming"
+ );
+}
diff --git a/mingling_picker/test/src/test/value_flag_test.rs b/mingling_picker/test/src/test/value_flag_test.rs
new file mode 100644
index 0000000..d267a27
--- /dev/null
+++ b/mingling_picker/test/src/test/value_flag_test.rs
@@ -0,0 +1,174 @@
+use mingling_picker::value::Flag;
+use mingling_picker::{IntoPicker, macros::arg};
+
+// Basic Flag — present / absent
+
+#[test]
+fn test_flag_present() {
+ let flag: Flag = vec!["--verbose"]
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .unwrap();
+ assert_eq!(flag, Flag::Active);
+}
+
+#[test]
+fn test_flag_absent_returns_inactive() {
+ // Unlike bool, Flag::pick returns Parsed(Inactive) when no match is found,
+ // so or_default() is NOT required — unwrap() works directly.
+ let flag: Flag = Vec::<&str>::new()
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .unwrap();
+ assert_eq!(flag, Flag::Inactive);
+}
+
+// Short Flag
+
+#[test]
+fn test_flag_short_present() {
+ let flag: Flag = vec!["-v"]
+ .to_picker()
+ .pick(&arg![verbose: Flag, 'v'])
+ .unwrap();
+ assert_eq!(flag, Flag::Active);
+}
+
+// Multiple Flags
+
+#[test]
+fn test_two_flags_both_present() {
+ let (a, b): (Flag, Flag) = vec!["--flag-a", "--flag-b"]
+ .to_picker()
+ .pick(&arg![flag_a: Flag])
+ .pick(&arg![flag_b: Flag])
+ .unwrap();
+ assert_eq!(a, Flag::Active);
+ assert_eq!(b, Flag::Active);
+}
+
+#[test]
+fn test_two_flags_one_present() {
+ let (a, b): (Flag, Flag) = vec!["--flag-a"]
+ .to_picker()
+ .pick(&arg![flag_a: Flag])
+ .pick(&arg![flag_b: Flag])
+ .unwrap();
+ assert_eq!(a, Flag::Active);
+ assert_eq!(b, Flag::Inactive);
+}
+
+#[test]
+fn test_two_flags_neither_present() {
+ let (a, b): (Flag, Flag) = Vec::<&str>::new()
+ .to_picker()
+ .pick(&arg![flag_a: Flag])
+ .pick(&arg![flag_b: Flag])
+ .unwrap();
+ assert_eq!(a, Flag::Inactive);
+ assert_eq!(b, Flag::Inactive);
+}
+
+// After `--` (end-of-options)
+
+#[test]
+fn test_flag_after_end_of_options() {
+ let flag: Flag = vec!["--", "--verbose"]
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .unwrap();
+ assert_eq!(flag, Flag::Inactive);
+}
+
+// Alias
+
+#[test]
+fn test_flag_with_alias() {
+ let flag: Flag = vec!["--cfg"]
+ .to_picker()
+ .pick(&arg![config: Flag, "cfg"])
+ .unwrap();
+ assert_eq!(flag, Flag::Active);
+}
+
+#[test]
+fn test_flag_primary_name() {
+ let flag: Flag = vec!["--config"]
+ .to_picker()
+ .pick(&arg![config: Flag, "cfg"])
+ .unwrap();
+ assert_eq!(flag, Flag::Active);
+}
+
+// Unrelated flag should not match
+
+#[test]
+fn test_unrelated_flag_does_not_match() {
+ let flag: Flag = vec!["--other"]
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .unwrap();
+ assert_eq!(flag, Flag::Inactive);
+}
+
+// to_result / to_option
+
+#[test]
+fn test_flag_to_result() {
+ let result: Result<Flag, ()> = vec!["--verbose"]
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .to_result();
+ assert_eq!(result, Ok(Flag::Active));
+}
+
+#[test]
+fn test_flag_to_option() {
+ let opt: Option<Flag> = vec!["--verbose"]
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .to_option();
+ assert_eq!(opt, Some(Flag::Active));
+}
+
+// Bool conversions
+
+#[test]
+fn test_flag_converts_to_bool() {
+ let flag = Flag::Active;
+ assert!(bool::from(flag));
+
+ let flag = Flag::Inactive;
+ assert!(!bool::from(flag));
+}
+
+#[test]
+fn test_flag_from_bool() {
+ assert_eq!(Flag::from(true), Flag::Active);
+ assert_eq!(Flag::from(false), Flag::Inactive);
+}
+
+#[test]
+fn test_flag_deref_to_bool() {
+ let active = Flag::Active;
+ assert!(*active);
+
+ let inactive = Flag::Inactive;
+ assert!(!*inactive);
+}
+
+// Flag never triggers route (unlike bool)
+//
+// Flag::pick always returns Parsed, so the fallback chain
+// (default → route) is never entered.
+
+#[test]
+fn test_flag_absent_does_not_trigger_route() {
+ // Even without or_default / or_route, absent flag returns Inactive, not a route
+ let result: Result<Flag, &str> = Vec::<&str>::new()
+ .with_route::<&str>()
+ .pick(&arg![verbose: Flag])
+ .or_route(|| "should_not_fire")
+ .to_result();
+ assert_eq!(result, Ok(Flag::Inactive));
+}
diff --git a/mingling_picker/test/src/test/value_string_test.rs b/mingling_picker/test/src/test/value_string_test.rs
new file mode 100644
index 0000000..b1e5c0b
--- /dev/null
+++ b/mingling_picker/test/src/test/value_string_test.rs
@@ -0,0 +1,171 @@
+use mingling_picker::{IntoPicker, macros::arg};
+
+// Basic named String — present / absent
+
+#[test]
+fn test_string_named_present() {
+ let val: String = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "Alice");
+}
+
+#[test]
+fn test_string_named_absent_uses_default() {
+ let val: String = Vec::<&str>::new()
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// Named String — eq mode
+
+#[test]
+fn test_string_named_eq_mode() {
+ let val: String = vec!["--name=Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "Alice");
+}
+
+// Named String — short flag
+
+#[test]
+fn test_string_named_short_flag() {
+ let val: String = vec!["-n", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String, 'n'])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "Alice");
+}
+
+// Named String — no value after flag
+
+#[test]
+fn test_string_named_missing_value_triggers_default() {
+ // --name at end with no following arg → pick returns NotFound → or_default gives ""
+ let val: String = vec!["--name"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// Positional String
+
+#[test]
+fn test_string_positional() {
+ let val: String = vec!["file.txt"]
+ .to_picker()
+ .pick(&arg![String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "file.txt");
+}
+
+#[test]
+fn test_string_positional_takes_first() {
+ let val: String = vec!["first", "second"]
+ .to_picker()
+ .pick(&arg![String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "first");
+}
+
+// Multiple occurrences (Single only tags one occurrence per Parseable)
+
+#[test]
+fn test_string_two_named_flags() {
+ let (a, b): (String, String) = vec!["--name", "Alice", "--greeting", "Hello"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .pick(&arg![greeting: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, "Alice");
+ assert_eq!(b, "Hello");
+}
+
+// Mixed named + positional
+
+#[test]
+fn test_string_named_and_positional() {
+ let (name, file): (String, String) = vec!["--name", "Alice", "file.txt"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .pick(&arg![String])
+ .or_default()
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(file, "file.txt");
+}
+
+// After `--` (end-of-options)
+
+#[test]
+fn test_string_named_after_end_of_options() {
+ // Named arg after `--` should not be matched → default
+ let val: String = vec!["--", "--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// Unrelated flag should not match → default
+
+#[test]
+fn test_string_unrelated_flag() {
+ let val: String = vec!["--other", "value"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// to_result / to_option
+
+#[test]
+fn test_string_to_result() {
+ let result: Result<String, ()> = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .to_result();
+ assert_eq!(result, Ok("Alice".to_string()));
+}
+
+#[test]
+fn test_string_to_option() {
+ let opt: Option<String> = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .to_option();
+ assert_eq!(opt, Some("Alice".to_string()));
+}
+
+// Custom default via .or()
+
+#[test]
+fn test_string_custom_default() {
+ let val: String = Vec::<&str>::new()
+ .to_picker()
+ .pick(&arg![name: String])
+ .or(|| "default_name".to_string())
+ .unwrap();
+ assert_eq!(val, "default_name");
+}
diff --git a/mingling_picker_macros/Cargo.toml b/mingling_picker_macros/Cargo.toml
index 5bab6e5..58488ea 100644
--- a/mingling_picker_macros/Cargo.toml
+++ b/mingling_picker_macros/Cargo.toml
@@ -11,6 +11,9 @@ description = "Mingling's lightweight argument parser macros"
[lib]
proc-macro = true
+[features]
+mingling_support = []
+
[dependencies]
syn.workspace = true
quote.workspace = true
diff --git a/mingling_picker_macros/src/arg.rs b/mingling_picker_macros/src/arg.rs
new file mode 100644
index 0000000..70b27b0
--- /dev/null
+++ b/mingling_picker_macros/src/arg.rs
@@ -0,0 +1,205 @@
+use proc_macro::{TokenStream, TokenTree};
+use proc_macro2::TokenStream as TS2;
+use quote::quote;
+
+pub(crate) fn arg(input: TokenStream) -> TokenStream {
+ let tokens: Vec<TokenTree> = input.into_iter().collect();
+ let args = split_at_commas(&tokens);
+ if args.is_empty() {
+ return quote! { compile_error!("arg! flaguires at least one argument") }.into();
+ }
+
+ let first = &args[0];
+ let rest = &args[1..];
+
+ // Validate: at most one char literal
+ let char_count = rest.iter().filter(|a| is_char_literal(a)).count();
+ if char_count > 1 {
+ return quote! { compile_error!("arg! only supports at most one short name") }.into();
+ }
+
+ // Extract short char and string aliases
+ let short_char: Option<char> = rest
+ .iter()
+ .find(|a| is_char_literal(a))
+ .and_then(|a| extract_char(a));
+ let aliases: Vec<String> = rest
+ .iter()
+ .filter(|a| is_string_literal(a))
+ .filter_map(|a| extract_string(a))
+ .collect();
+
+ // Parse first argument
+ // arg![name: Type] → name, with explicit type
+ // arg![Type] → positional type, no name
+ let colon_pos = first
+ .iter()
+ .position(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == ':'));
+ let has_named_colon = colon_pos.is_some_and(|pos| pos > 0 && !is_double_colon(first, pos));
+
+ let first_slice = first.as_slice();
+ let (name, ty, is_named) = if has_named_colon {
+ let pos = colon_pos.unwrap();
+ // `name : Type`
+ (
+ Some(&first_slice[..pos]),
+ Some(&first_slice[pos + 1..]),
+ true,
+ )
+ } else {
+ // No `name:` prefix → the entire first argument is the type
+ (None, Some(first_slice), false)
+ };
+
+ // Build full names
+ let full_names: Vec<String> = match name {
+ Some(n) => {
+ let mut v = vec![join_idents(n)];
+ v.extend(aliases);
+ v
+ }
+ None => aliases,
+ };
+
+ let path: TS2 = {
+ let ty_ts: TS2 = ty
+ .map(|t| {
+ let ts: TokenStream = t.iter().cloned().collect();
+ TS2::from(ts)
+ })
+ .unwrap_or(TS2::new());
+
+ #[cfg(feature = "mingling_support")]
+ let import = quote! { ::mingling::picker::PickerArg };
+
+ #[cfg(not(feature = "mingling_support"))]
+ let import = quote! { ::mingling_picker::PickerArg };
+
+ if ty.is_some() {
+ quote! { #import::<#ty_ts> }
+ } else {
+ quote! { #import::<_> }
+ }
+ };
+
+ // full: &["name", "alias", ...] or &[]
+ let full_value: TS2 = if !full_names.is_empty() {
+ let strs: Vec<proc_macro2::Literal> = full_names
+ .iter()
+ .map(|s| proc_macro2::Literal::string(s))
+ .collect();
+ quote! { &[#(#strs),*] }
+ } else {
+ quote! { &[] }
+ };
+
+ // short: Some('c') or None
+ let short_value: TS2 = match short_char {
+ Some(c) => {
+ let lit = proc_macro2::Literal::character(c);
+ quote! { ::std::option::Option::Some(#lit) }
+ }
+ None => quote! { ::std::option::Option::None },
+ };
+
+ let positional_value: bool = !is_named;
+
+ let result = quote! {
+ #path {
+ full: #full_value,
+ short: #short_value,
+ positional: #positional_value,
+ internal_type: ::std::marker::PhantomData,
+ }
+ };
+
+ result.into()
+}
+
+fn split_at_commas(tokens: &[TokenTree]) -> Vec<Vec<TokenTree>> {
+ let mut result = vec![Vec::new()];
+ let mut depth = 0u32;
+ for t in tokens {
+ match t {
+ TokenTree::Group(_g) => {
+ depth += 1;
+ result.last_mut().unwrap().push(t.clone());
+ depth -= 1;
+ }
+ TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 => {
+ result.push(Vec::new());
+ }
+ _ => result.last_mut().unwrap().push(t.clone()),
+ }
+ }
+ result
+}
+
+fn is_double_colon(tokens: &[TokenTree], pos: usize) -> bool {
+ pos > 0 && matches!(&tokens[pos - 1], TokenTree::Punct(p) if p.as_char() == ':')
+}
+
+fn is_char_literal(tokens: &[TokenTree]) -> bool {
+ tokens.len() == 1
+ && matches!(&tokens[0], TokenTree::Literal(l) if {
+ let s = l.to_string();
+ s.starts_with('\'') && s.len() >= 3
+ })
+}
+
+fn is_string_literal(tokens: &[TokenTree]) -> bool {
+ tokens.len() == 1
+ && matches!(&tokens[0], TokenTree::Literal(l) if {
+ l.to_string().starts_with('"')
+ })
+}
+
+fn extract_char(tokens: &[TokenTree]) -> Option<char> {
+ match &tokens[0] {
+ TokenTree::Literal(l) => {
+ let s = l.to_string();
+ let cs: Vec<char> = s.chars().collect();
+ if cs.len() >= 3 && cs[0] == '\'' && cs[cs.len() - 1] == '\'' {
+ let inner: String = cs[1..cs.len() - 1].iter().collect();
+ // inner is the character between the quotes of a char literal.
+ // For a literal `'n'`, inner is `"n"` (the character n).
+ // For an escape `'\n'`, inner is the actual newline character.
+ // The catch-all handles both literal single chars and escape
+ // sequences (the escaped char IS the actual control character).
+ match inner.as_str() {
+ "\\\\" => Some('\\'),
+ "\\'" => Some('\''),
+ _ => inner.chars().next(),
+ }
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
+}
+
+fn extract_string(tokens: &[TokenTree]) -> Option<String> {
+ match &tokens[0] {
+ TokenTree::Literal(l) => {
+ let s = l.to_string();
+ let cs: Vec<char> = s.chars().collect();
+ if cs.len() >= 2 && cs[0] == '"' && cs[cs.len() - 1] == '"' {
+ Some(cs[1..cs.len() - 1].iter().collect())
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
+}
+
+fn join_idents(tokens: &[TokenTree]) -> String {
+ tokens
+ .iter()
+ .map(|t| match t {
+ TokenTree::Ident(id) => id.to_string(),
+ _ => String::new(),
+ })
+ .collect()
+}
diff --git a/mingling_picker_macros/src/internal_repeat.rs b/mingling_picker_macros/src/internal_repeat.rs
new file mode 100644
index 0000000..4b2242b
--- /dev/null
+++ b/mingling_picker_macros/src/internal_repeat.rs
@@ -0,0 +1,315 @@
+use proc_macro::{Delimiter, Group, Ident, Literal, TokenStream, TokenTree};
+
+pub(crate) fn internal_repeat(input: TokenStream) -> TokenStream {
+ let tokens: Vec<TokenTree> = input.into_iter().collect();
+ let (range_start, range_end, body_start) = parse_range(&tokens);
+
+ let mut body: Vec<TokenTree> = tokens[body_start..].to_vec();
+ if body.len() == 1
+ && let TokenTree::Group(g) = &body[0]
+ && g.delimiter() == Delimiter::Brace
+ {
+ body = g.stream().into_iter().collect();
+ }
+
+ let mut result = Vec::new();
+ for i in range_start..=range_end {
+ result.extend(expand_body(&body, i, range_start, range_end));
+ }
+ result.into_iter().collect()
+}
+
+/// Parse `start .. end =>` or `start ..= end =>` or `count =>` (backward compat).
+/// Returns `(start, end_inclusive, body_start_index)`.
+fn parse_range(tokens: &[TokenTree]) -> (usize, usize, usize) {
+ // Find => separator
+ let arrow_pos = tokens.windows(2).position(|w| {
+ matches!(&w[0], TokenTree::Punct(p) if p.as_char() == '=')
+ && matches!(&w[1], TokenTree::Punct(p) if p.as_char() == '>')
+ });
+
+ let (arrow_pos, body_start) = match arrow_pos {
+ Some(p) => (p, p + 2),
+ None => return (1, 12, 0), // fallback
+ };
+
+ let before: Vec<&TokenTree> = tokens[..arrow_pos].iter().collect();
+
+ // Try to find `..` or `..=` pattern
+ // `..` is two Punct('.') tokens
+ let dotdot = before.windows(2).position(|w| {
+ matches!(w[0], TokenTree::Punct(p) if p.as_char() == '.')
+ && matches!(w[1], TokenTree::Punct(p) if p.as_char() == '.')
+ });
+
+ if let Some(dd) = dotdot {
+ // Start value: tokens before `..`
+ let start = parse_usize_tokens(&before[..dd]);
+ let after_dd = &before[dd + 2..];
+
+ // Check for `..=` (inclusive range)
+ let (inclusive, end_tokens) = if after_dd
+ .first()
+ .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '='))
+ {
+ (true, &after_dd[1..])
+ } else {
+ (false, after_dd)
+ };
+
+ let end = parse_usize_tokens(end_tokens);
+
+ if inclusive {
+ (start, end, body_start)
+ } else {
+ // Exclusive end: if end >= start, iterate start..end, so end_inclusive = end - 1
+ if end > start {
+ (start, end - 1, body_start)
+ } else {
+ (1, 12, body_start) // fallback
+ }
+ }
+ } else {
+ // No `..` found — fallback to simple count
+ let count = parse_usize_tokens(&before);
+ (1, count, body_start)
+ }
+}
+
+/// Parse a sequence of tokens as a single usize value.
+fn parse_usize_tokens(tokens: &[&TokenTree]) -> usize {
+ let s: String = tokens
+ .iter()
+ .map(|t| match t {
+ TokenTree::Literal(l) => l.to_string(),
+ TokenTree::Ident(id) => id.to_string(),
+ _ => String::new(),
+ })
+ .collect::<Vec<_>>()
+ .join("")
+ .replace(' ', "");
+
+ s.parse().unwrap_or(12)
+}
+
+/// Walk tokens, replacing:
+/// `$` → current
+/// `^$` → max
+/// `$^` → min
+/// `$+` → current + 1 (clamped)
+/// `$-` → current - 1 (clamped)
+/// `ident$` → ident{current}
+/// and expanding `( … )+` / `( … ,)+` / `( … ;)+` groups.
+fn expand_body(tokens: &[TokenTree], current: usize, min: usize, max: usize) -> Vec<TokenTree> {
+ let mut out = Vec::new();
+ let mut i = 0;
+ while i < tokens.len() {
+ // Check for a parenthesized repetition group: ( ... ) sep? +
+ if let Some(exp) = try_expand_paren_group(tokens, i, current, min, max) {
+ let (items, consumed) = exp;
+ out.extend(items);
+ i += consumed;
+ continue;
+ }
+
+ // `^$` — max value
+ if let TokenTree::Punct(p) = &tokens[i]
+ && p.as_char() == '^'
+ && i + 1 < tokens.len()
+ && let TokenTree::Punct(p2) = &tokens[i + 1]
+ && p2.as_char() == '$'
+ {
+ out.push(TokenTree::Literal(Literal::usize_suffixed(max)));
+ i += 2;
+ continue;
+ }
+
+ // `ident$` / `ident$+` / `ident$-` / `ident$^`
+ // → {ident}{current} / {ident}{current+1} / {ident}{current-1} / {ident}{min}
+ if let TokenTree::Ident(id) = &tokens[i] {
+ if i + 1 < tokens.len()
+ && let TokenTree::Punct(p) = &tokens[i + 1]
+ && p.as_char() == '$'
+ {
+ // Check for ident$+ / ident$- / ident$^
+ if i + 2 < tokens.len() {
+ match &tokens[i + 2] {
+ TokenTree::Punct(p2) if p2.as_char() == '+' => {
+ // ident$+ → {ident}{current+1}
+ let name = format!("{}{}", id, current + 1);
+ out.push(TokenTree::Ident(Ident::new(&name, id.span())));
+ i += 3;
+ continue;
+ }
+ TokenTree::Punct(p2) if p2.as_char() == '-' => {
+ // ident$- → {ident}{current-1}
+ let val = current.saturating_sub(1);
+ let name = format!("{}{}", id, val);
+ out.push(TokenTree::Ident(Ident::new(&name, id.span())));
+ i += 3;
+ continue;
+ }
+ TokenTree::Punct(p2) if p2.as_char() == '^' => {
+ let name = format!("{}{}", id, min);
+ out.push(TokenTree::Ident(Ident::new(&name, id.span())));
+ i += 3;
+ continue;
+ }
+ _ => {}
+ }
+ }
+ // ident$ alone → {ident}{current}
+ let name = format!("{}{}", id, current);
+ out.push(TokenTree::Ident(Ident::new(&name, id.span())));
+ i += 2;
+ continue;
+ }
+ out.push(tokens[i].clone());
+ i += 1;
+ continue;
+ }
+
+ match &tokens[i] {
+ TokenTree::Punct(p) if p.as_char() == '$' => {
+ // lookahead for $^, $+, $-
+ if i + 1 < tokens.len() {
+ match &tokens[i + 1] {
+ TokenTree::Punct(p2) if p2.as_char() == '^' => {
+ // $^ → min
+ out.push(TokenTree::Literal(Literal::usize_suffixed(min)));
+ i += 2;
+ continue;
+ }
+ TokenTree::Punct(p2) if p2.as_char() == '+' => {
+ // $+ → current + 1
+ out.push(TokenTree::Literal(Literal::usize_suffixed(current + 1)));
+ i += 2;
+ continue;
+ }
+ TokenTree::Punct(p2) if p2.as_char() == '-' => {
+ // $- → current - 1
+ let val = current.saturating_sub(1);
+ out.push(TokenTree::Literal(Literal::usize_suffixed(val)));
+ i += 2;
+ continue;
+ }
+ _ => {}
+ }
+ }
+ // `$` alone → current
+ out.push(TokenTree::Literal(Literal::usize_suffixed(current)));
+ i += 1;
+ continue;
+ }
+ TokenTree::Group(g) => {
+ let inner = expand_body_vec(&g.stream(), current, min, max);
+ out.push(TokenTree::Group(Group::new(
+ g.delimiter(),
+ inner.into_iter().collect(),
+ )));
+ }
+ other => out.push(other.clone()),
+ }
+ i += 1;
+ }
+ out
+}
+
+fn expand_body_vec(stream: &TokenStream, current: usize, min: usize, max: usize) -> Vec<TokenTree> {
+ let v: Vec<TokenTree> = stream.clone().into_iter().collect();
+ expand_body(&v, current, min, max)
+}
+
+/// Try to expand a repetition group.
+///
+/// New syntax (repetition marker `+` is the LAST token INSIDE the parens):
+/// - `(group,+)` — repeat `*` times (current), `,` is the separator
+/// - `(group,+)` — repeat `*` times, `,` is the separator
+/// - `(group,+)` — repeat `*` times, `;` is the separator
+/// - `(group +)` — repeat `*` times, no separator
+/// - `(group,+)+` — repeat `*+1` times (with separator)
+/// - `(group,+)--` — repeat `*-1` times (with separator) [not yet used]
+///
+/// The `*` is the current counter value. An optional `+` or `-` immediately
+/// after the closing paren shifts the repeat count up or down by one.
+fn try_expand_paren_group(
+ tokens: &[TokenTree],
+ i: usize,
+ current: usize,
+ _min: usize,
+ _max: usize,
+) -> Option<(Vec<TokenTree>, usize)> {
+ let group = match tokens.get(i)? {
+ TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => g,
+ _ => return None,
+ };
+
+ let stream: Vec<TokenTree> = group.stream().into_iter().collect();
+
+ // Repetition syntax: the LAST token inside the parens MUST be `+`.
+ // `(content,+)` — repeat * times, `,` separator
+ // `(content;+)` — repeat * times, `;` separator
+ // `(content,+)` — repeat * times, no separator
+ // `(content,+)+` — repeat *+1 times
+ // `(content,+)-` — repeat *-1 times
+ // An optional `+` / `-` right after `)` shifts the count by ±1.
+ //
+ // Any `+` tokens inside `content` are treated as regular Rust syntax
+ // (trait bounds, etc.) — the repetition marker is ONLY the final `+`.
+
+ let last_is_plus = stream
+ .last()
+ .is_some_and(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '+'));
+ if !last_is_plus {
+ return None;
+ }
+
+ // Determine separator and inner content.
+ let (inner, sep_str): (Vec<TokenTree>, &str) = {
+ if stream.len() >= 2 {
+ let sep_idx = stream.len() - 2;
+ match &stream[sep_idx] {
+ TokenTree::Punct(p) if p.as_char() == ',' => {
+ // (...,content,+) — inner is everything before the last `,`
+ let content: Vec<TokenTree> = stream[..sep_idx].into();
+ (content, ",")
+ }
+ TokenTree::Punct(p) if p.as_char() == ';' => {
+ let content: Vec<TokenTree> = stream[..sep_idx].into();
+ (content, ";")
+ }
+ _ => {
+ // (content+) — no separator, the `+` is the only special token
+ let content: Vec<TokenTree> = stream[..stream.len() - 1].into();
+ (content, "")
+ }
+ }
+ } else {
+ // (+) — bare repetition marker, empty content
+ (vec![], "")
+ }
+ };
+
+ // Handle modifier after `)`
+ let rest = &tokens[i + 1..];
+ let modifier: isize = match rest.first() {
+ Some(TokenTree::Punct(p)) if p.as_char() == '+' => 1,
+ Some(TokenTree::Punct(p)) if p.as_char() == '-' => -1,
+ _ => 0,
+ };
+ let consumed = if modifier != 0 { 2 } else { 1 };
+ let repeat_count = (current as isize + modifier) as usize;
+
+ let mut out = Vec::new();
+ for n in 1..=repeat_count {
+ if n > 1 && !sep_str.is_empty() {
+ out.push(TokenTree::Punct(proc_macro::Punct::new(
+ sep_str.chars().next().unwrap(),
+ proc_macro::Spacing::Alone,
+ )));
+ }
+ out.extend(expand_body(&inner, n, 1, repeat_count));
+ }
+
+ Some((out, consumed))
+}
diff --git a/mingling_picker_macros/src/lib.rs b/mingling_picker_macros/src/lib.rs
index 8b13789..33bb67d 100644
--- a/mingling_picker_macros/src/lib.rs
+++ b/mingling_picker_macros/src/lib.rs
@@ -1 +1,30 @@
+use proc_macro::TokenStream;
+mod arg;
+mod internal_repeat;
+
+/// Core proc-macro: repeats a template body `count` times.
+///
+/// Internal call signature: `internal_repeat!(count => { template })`
+#[proc_macro]
+pub fn internal_repeat(input: TokenStream) -> TokenStream {
+ internal_repeat::internal_repeat(input)
+}
+
+/// Quick builder for `PickerArg`.
+///
+/// # Syntax
+///
+/// ```ignore
+/// use mingling_picker_macros::flag;
+///
+/// let basic = arg![name: String];
+/// let with_short_name = arg![name: String, 'n'];
+/// let with_short_alias = arg![name: String, 'n', "alias"];
+/// let positional = arg![String];
+/// let positional_with_name = arg![String, 'n', "alias"];
+/// ```
+#[proc_macro]
+pub fn arg(input: TokenStream) -> TokenStream {
+ arg::arg(input)
+}