veks-completion
Dynamic shell completion engine for Rust CLI tools, plus a companion argv parser, help renderer, and tap-cadence model.
Zero non-std dependencies. One CommandTree declaration drives tab
completion, argv parsing, and --help rendering — single source of
truth for every CLI surface.
What's in here
- Tab completion — context-aware candidates, value providers, per-flag aliases, dynamic option discovery.
- Multi-tap rotating tiers — single tap shows the primary commands; rapid follow-up taps reveal cumulative supersets in layer-ordered display.
- Argv parser —
parse_argv(tree, &argv) → ParsedCommandwalks the same tree the completer uses to produce a structured parse (path, flags, positionals). - Help rendering —
render_usage(node, path)formats a--helpblock from the tree. - Closed value sets —
ClosedValuesproduces both completion and validation from one declaration. - Subtree-aware completion — context-sensitive providers can
take over completion inside any subtree with a structured
PartialParse. - Free-form attachment —
Extrasslot lets embedders carry handler payloads, parser state, dispatch rules without forcing the crate to grow generics. - Directive sets — bundle every surface a flag appears in (CLI form, help, value set, repeatability, optional YAML mirror) into one declaration; expand a slice of them onto a node in one call.
Why not clap_complete?
Clap provides clap_complete for generating static shell completion
scripts. It works well for simple CLIs with a fixed command tree, but
breaks down for tools with dynamic commands, multi-level subgroups, or
context-sensitive value completion. This crate was built for the veks
CLI but is fully generic and reusable. The specific limitations it
addresses:
1. Dynamic pipeline commands
Veks has a pipeline command group whose subcommands are registered
at runtime from a CommandRegistry, not from clap's derive macros.
Clap's completion generator only sees the derive-defined tree.
2. Shorthand dispatch
veks run is shorthand for veks prepare run; veks config for
veks datasets config. These are hidden top-level subcommand aliases.
Clap either hides them entirely (no completions) or shows them
alongside the originals (duplicates). veks-completion uses tap-tier
levels: hidden shortcuts get level=2 and surface only on rapid
double-tap.
3. One-level-at-a-time completion with rotating tiers
Clap eagerly chains subcommands. veks-completion completes one level at a time. Within a level, rapid double-tap reveals the next tier as a cumulative superset, layered in display order so primary commands always appear first.
4. Context-sensitive value completion
Per-option ValueProvider functions run arbitrary logic. ClosedValues
turns a static value set into a provider with one declaration that's
also queryable by parsers for validation.
5. Mixed derive + dynamic tree
The completion engine walks the augmented clap::Command tree at
completion time, so it always reflects the actual command structure
regardless of how commands were registered.
How it works
Node is a single struct that carries everything a CLI-tree node can
have: subcommand children, flags (value-taking + boolean), value
providers, discovery metadata (category + level), help text, plus
hooks (subtree provider, free-form extras). A node with no children is
"leaf-shaped"; with children it's "group-shaped"; with both it's
hybrid (e.g. a group that itself accepts cross-cutting flags).
A CommandTree is a root node + globals. At startup an embedder builds
the tree once. At completion time the engine walks the tree following
the user's typed words and returns candidates.
The bash script the engine emits is minimal — it just calls back into the binary. Completions never get out of sync with the installed binary because the binary itself produces them.
Usage — tab completion
use ;
let tree = new
.command
.group;
// In main(), before argument parsing:
if handle_complete_env
// And register a `completions` subcommand that prints the bash
// activation snippet for `eval "$(myapp completions)"`.
Usage — argv parsing
The same CommandTree produces structured parses:
use ;
let parsed = parse_argv?;
assert_eq!;
assert_eq!;
assert_eq!; // boolean
assert_eq!;
parse_argv_lenient treats unknown flags as positionals — useful for
pass-through CLIs.
Usage — help rendering
use render_usage;
let leaf = leaf_with_flags
.with_help
.with_flag_help
.with_flag_help;
println!;
// → USAGE: myapp compute knn
//
// Compute KNN over base vectors
//
// FLAGS:
// --metric Distance metric: L2 / IP / COSINE
// --verbose Print per-step progress
Usage — closed value sets (completion + validation)
use ;
let metrics = Static;
assert!;
assert!;
assert_eq!;
let leaf = leaf
.with_value_provider;
Usage — directive sets
When you have many flags that share a vocabulary shape, declare them
once as a &[Directive] and apply onto any node:
use ;
const DIRS: & = &;
let leaf = apply_directives;
// → flags + flag-help + value-providers + validation, all wired.
Usage — multi-tap rotating tiers
Tag commands with with_level(n) to control which tap-tier reveals
them:
let tree = new
.command
.command
.command
.command
.command;
Behavior:
- Tap 1 (cold or after pause): layer 1 only —
run - Tap 2 (within 200 ms): cumulative layers 1+2 —
run,--inspector,--summary - Tap 3 (within 200 ms): cumulative layers 1+2+3 — all five. At max, the persistent state resets, so a fourth rapid tap cycles back to layer 1.
- Pause > 200 ms: any tap starts fresh at layer 1.
- Within each tap's result: sorted in layer order (layer 1
first, then layer 2, …); within a layer,
---flags last, alphabetical otherwise.
The cadence rule is also exposed as a pure function for embedders that want their own clock/state:
use ;
let = next_tap_state;
TAP_ADVANCE_MS is the public window constant (200 ms).
Built-in options for downstream adopters
When you build a CommandTree, two opt-in builders enable common
capabilities without writing the boilerplate yourself. Both are
fully additive — they walk the tree once at finalisation time:
with_auto_help
let tree = new
.command
.command
.with_auto_help;
Walks every node and adds --help (boolean) plus a default help line
("Show usage information for this command."). Idempotent — nodes that
already declare --help are left alone. Once attached, --help:
-
shows up in tab completion at every level,
-
is recognised by
parse_argvas a known boolean flag (so users can type--helpanywhere without "unknown flag" errors), and -
can be paired with
render_usage(node, path)in your handler to print the help block:let parsed = parse_argv?; if parsed.flags.contains_key
with_metricsql_at
use Arc;
use ;
let catalog: = new;
let tree = new
.command
.with_metricsql_at;
Attaches a built-in metricsql_provider SubtreeProvider at the
specified node path. Equivalent to manually walk_path_mut(...)-ing
to the node and calling with_subtree_provider(metricsql_provider( catalog)), but surfaces the intent at tree-construction time.
The MetricsQL provider understands:
| Cursor context | Suggestions |
|---|---|
| Top of expression / after a binop | metric names + function names |
Inside { (label-matcher block) |
label keys for the preceding metric |
After key= or key=~ inside {…} |
label values (in quoted form) |
Inside "…" after key= |
label values (bare; quote already open) |
Inside [ (range selector) |
time units (5m, 1h, etc.) |
After sum/avg/count… |
by / without |
After sum by/without |
label keys (any) |
After offset |
time-unit suggestions |
Site-specific data (metric names, label keys, label values) comes
from your MetricsqlCatalog impl. The built-in vocabulary
(functions, time units, operators, aggregation modifiers) is baked
in — see providers::METRICSQL_FUNCTIONS,
providers::METRICSQL_TIME_UNITS, etc.
Usage — context-sensitive subtree completion
When tab completion needs grammar-aware behavior in a subtree (e.g.
inside a query DSL), attach a SubtreeProvider:
use Arc;
use ;
let metrics_provider: SubtreeProvider = new;
let tree = new
.command;
The deepest matching subtree provider on the path takes over completion. This is the recommended replacement for the pre-walker hook pattern.
Usage — handler attachment
Embedders that want to dispatch to handlers from the same tree can
attach arbitrary payloads via Extras:
use ;
let leaf = leaf
.with_extras;
// Later, after parse_argv resolves `path`:
if let Some = node.extras
Extras wraps Arc<dyn Any + Send + Sync> — no generic on Node,
no hard dependency on tokio or any handler framework.
Architecture
veks-completion/ # This crate — generic completion + parse + help
src/lib.rs # Node, CommandTree, complete(), parse_argv(),
# render_usage(), ClosedValues, Directive, …
veks/src/cli/
dyncomp.rs # Walks clap::Command -> CommandTree;
# registers global ValueProviders.
mod.rs # Wires `eval "$(veks completions)"` snippet.
The completion crate has no dependency on clap, veks, or any pipeline
code. The clap-to-tree conversion lives in dyncomp.rs inside the
veks binary crate.
Examples
Two runnable examples in examples/ show how the pieces fit
together. Read them top-to-bottom — the rustdoc comments narrate
each step.
basic.rs — first-time setup
Builds a small CommandTree with a few subcommands and a value
provider, prints what completion would suggest at sample cursor
positions. The minimum viable adoption.
metricsql.rs — end-to-end coding scenario
A full integration that mirrors how you'd actually adopt veks-completion in a tool with grammar-aware completion needs. It shows the four steps of a real adoption:
- Implement the catalog — a small
InMemoryCatalogthat provides the site-specific data the MetricsQL provider needs (metric names, label keys, label values). - Build the tree — uses both built-in options:
with_auto_help()(uniform--helpeverywhere) andwith_metricsql_at(&["query"], catalog)(grammar-aware completion inside thequerysubcommand). - Wire the entry point —
handle_complete_envfor tab callbacks,print_bash_scriptfor the activation snippet,parse_argv+render_usagefor--helphandling, dispatch on the resolved subcommand path. - Drive the demo — runs ten realistic MetricsQL cursor
positions (top-of-expression, inside
{, afterkey=, inside open quote, inside[, afterby, afteroffset, etc.) and prints what the completer suggests at each.
# Build + register completions, then tab around interactively:
# Or skip the shell setup and watch the demo print its results:
The demo's output for each scenario shows the raw input, the cursor
byte position, and the candidates the engine would offer — the
quickest way to sanity-check what your MetricsqlCatalog impl
returns at every cursor position the provider knows about. Adapt
this scenario script into your own integration tests by swapping in
your real catalog.