ashlar-command
Everything is a command. An app declares its commands once in a Registry; menus, shortcuts, the
command palette, the CLI, the control port and MCP all run them through one chokepoint,
Registry::execute, and discover them through Dispatch::commands. Headless, builds for wasm32.
Declaring commands
use ;
static REGISTRY: = new;
cmd!/query! take the same arguments as existing cmd!/query! command tables, so those port
by search and replace. A command body returns the host's own error type (Host::Error, anything
Into<CommandError>; leave the id empty and the chokepoint fills it in), so bodies written against
an app's Result alias port unchanged.
The chokepoint
Registry::execute(&self, state: &mut S, id, params) runs, in order:
- lookup (hash index;
Unknown) - nesting limit (
MAX_DEPTH= 64) Host::before: may rewrite params (resolve names to ids, settle an ongoing gesture)- parameter validation (
BadParams, every problem in one message): the registry's policy (Registry::with_validation(Validation::Strict | TypedOnly | Off), defaultStrict), then theHost::validatehook (default: nothing). WithOff, a disabled command reportsDisabledwhatever its params, and the host validates where it wants (the hook, orRegistry::validate) Host::enabled_for(default: the spec's predicate; override to check targets named in params)- the command under
catch_unwind(a panic becomesPanicked, the app keeps running) Host::after(every run, ok or not, at every depth: sync persistence, log errors at depth 0…)- journal: mutating commands that succeeded, params passed through
Host::scrubfirst
A call refused in steps 1–5 goes to Host::rejected instead of after.
execute converts a body's error into CommandError. A host whose callers match on its own error
type uses Registry::execute_host (needs Host::Error: Clone): Err(ExecError::Host(e)) is the
body's error unchanged, Err(ExecError::Command(e)) the chokepoint's own refusal or a panic;
ExecError::into_command(id) gives what execute would have returned.
Where the execution state lives. The registry is immutable and usually a static. The mutable
part (current nesting depth and the journal) is a Journal the host owns and hands out through
Host::journal(). One registry therefore serves any number of documents, and commands can run other
commands (REGISTRY.execute(state, …) from inside a body) with the depth tracked per state. Every
hook gets depth (0 = called from outside), so a host can log or sync once per top-level call.
The journal is a capped ring (10,000 entries, the oldest 1,000 dropped at once). By default only
top-level calls are recorded; Journal::new().record_nested(true) records nested calls too.
len, entries, since, truncate, retain and pop let the host keep it in step with its
undo history. The chokepoint manages depth() itself; a host calls Journal::set_depth only to
run a command from one of its hooks as part of the current call (save the depth, set it one
higher, execute, restore).
Parameters
Params::Static(&[Param])/Params::Typed(Vec<ParamSpec>): unknown keys, missing required keys, types, integer/float ranges and enum values are checked;params_schemagives a JSON Schema (CommandInfo.params_schema, cached per registry).Params::Text("{…}"): legacy docs ({"name":str,"fps":f64=23.976,"mix":"A|B"?},{layers: [id|name], add?}). Keys the doc does not mention are refused; the schema lists the documented keys with types where the doc states them plainly (str,u32,"A|B",[…]).Registry::with_common_params(&["sequence"]): keys every command accepts;with_param_aliases(&[("layers", "layer")]): alternative spellings for legacy docs.
Discovery
registry.bind(&mut state) is a Bound implementing Dispatch: commands() returns
CommandInfo with enabled + disabled_reason, shortcuts in portable form and schemas;
execute() goes through the chokepoint.
Shortcuts (shortcut, keymap)
- Portable syntax
Mod+Shift+S(Mod= ⌘ on macOS, Ctrl elsewhere;Ctrl= the literal Control key). AcceptsCmd+Z,CmdOrCtrl+Z,Option+X, glyphs⌘⇧S, alternativesMod+Y / Mod+Shift+Z. Chord {mods, key}is UI-independent; the UI layer maps its key events onto it.chord.display(Platform::Mac)→⇧⌘S;Platform::Other→Ctrl+Shift+S.Keymap: defaults from the registry plus user overrides (serdeOverrides, read leniently),lookup(chord),keys_for(id),conflicts(),would_conflict(id, chord). Off macOSModandCtrlare one key, soMod+KandCtrl+Kconflict there and not on macOS.
Palette matching
fuzzy_match(query, candidate) -> Option<FuzzyMatch {score, positions}> (char positions for
highlighting; word starts, camelCase and runs score higher) and fuzzy_rank. Bounded: queries over
64 chars never match, candidates are cut at 512 chars.