Graded, real Rust exemplars — read them in order. Each one is bundled, small, and idiomatic; copy its shape. The stdio protocol remains language-neutral, but Rust is the sole maintained SDK and implementation language for official plugins.
Plugins/spotify (~85 LOC + unit tests). The
smallest useful plugin: manifest commands + one on_command hook mapping
subcommands to a CLI via run_command, with the argv plan kept pure and
unit-tested. Command-only plugins are on-demand: the host spawns them at
first use, not at startup.Plugins/marks (~200 LOC).
Vim-style marks: flash marks set / flash marks jump, host.normal_mode_target and
host.activate and host.open host RPCs (the app_control and open
capabilities), JSON state persisted under share_dir(), and the SDK’s
running-app snapshot to verify a saved process ID before activation.Plugins/processes (~300 LOC). A
sources plugin building its complete row set and pushing it with
publish from on_start, refreshed by events alone (app launch/quit,
flashlight open), backed by the host’s process table — plus cadences
armed only while core:status.observed shows their segment. No readiness
dance: initialize replies immediately and the catalog lands when ready.Plugins/browsers
(~2,500 LOC). Browser tabs behind one per-bundle engine table: a shared
AppleScript skeleton with per-dialect phrases for Chromium-family browsers
and Safari, and for Firefox the host Accessibility broker
(host.ax_snapshot / host.ax_perform, the accessibility capability)
completed from a file the browser keeps on disk. Refreshes fan out per
browser and publish on change; manifest actions go through on_action
with the performed / unhandled / error trichotomy, what the plugin leaves
unhandled falls back to its action_bindings, and every row’s
navigation_url route restores through on_navigate.Plugins/answers
(~1270 LOC). Three synchronous, CPU-only answer engines (calculator,
colors, timezones) behind one evaluate via an ordered engine table;
the ECB snapshot loads from disk in on_start and refreshes in the
background through host.fetch (the network_fetch capability),
atomically replacing state; query.prefixes = ["="] routing sends =
input to the calculator engine alone.Protocol conformance lives in the SDK workspace:
Plugins/_flash_plugin_rust/protocol.json pins the wire constants, the
wire/runtime test suites pin the framing and lifecycle behaviour, and the
probe workspace member exercises them end to end over real stdio.
./Scripts/test-plugins.sh --lane all is the one-command gate.
No code needed: a manifest-only plugin (no exec) declares
action_bindings, and its entries win over the defaults conventions for the
apps it names. Say an app opens a tab with Cmd-Shift-N, closes one with a
two-chord sequence, has a menu item for a new window and no print at all:
{
"id": "example-app",
"name": "Example App",
"version": "0.1.0",
"description": "What Example App does for Flash's actions",
"action_bindings": {
"tab_new": { "com.example.app": "cmd+shift+n" },
"tab_close": { "com.example.app": ["cmd+k", "cmd+w"] },
"window_new": { "com.example.app": { "menu": ["File", "New Window"] } },
"app_print": { "com.example.app": false }
}
}
Drop it in a directory, list it as file:<path> in [plugins] third_party,
and t in that app now sends Cmd-Shift-N. A bundle-id entry beats any
plugin-wide "" entry; to cover a family of apps with "" instead, scope the
plugin with only_bundle_ids (or only_terminals) and, between plugins that
both match, raise priority. The inspector’s Mappings page (“How actions
resolve in
./Scripts/new-plugin.sh myplugin "My Plugin" "What it does" # Rust scaffold, zero-edit green
./Scripts/build-plugins.sh dev myplugin # per-id Rust hot build
tail -f ~/Library/Logs/Flash/flash.log # watcher restart + plugin logs
CARGO_TARGET_DIR=build/plugin-target cargo test --manifest-path Plugins/myplugin/Cargo.toml
Debugging is a first-class feature of the transport: run the plugin binary in
a terminal and type NDJSON at it — {"id":1,"method":"initialize","params":
{"protocol_version":1}} — no host required.
Contracts you must not break — the full statements live in
docs/plugin-protocol.md and AGENTS.md: publish complete replacement
catalogs, never deltas; on transient failure don’t publish (the host keeps
last-good) and publish an authoritative [] only when the source is truly
empty; keep evaluate synchronous and I/O-free; bound every refresh; reply
to initialize immediately — startup work belongs in on_start.