Skip to content

Manifest

Every gadget must contain a manifest.toml at its root. The file uses the TOML format. This is the main entry point the host reads when discovering a gadget. Before any WASM code runs, the host parses this file to learn what the gadget is, what capabilities it needs, where its WASM binary and frontend bundles are located, and how to configure its sandbox. A gadget without a valid manifest.toml is ignored during discovery.

The following example shows all available sections. Each section and its fields are explained in detail below.

manifest.toml
[gadget]
id = "my-gadget"
name = "My Gadget"
description = "Does something useful"
version = "0.1.0"
wasm = "my_gadget.wasm"
icon = "heroicons:sparkles"
prefixes = ["!mg"]
[settings]
enabledFeature = true
threshold = 42
[permissions]
clipboard = true
frecency = true
[permissions.opener]
schemes = ["https", "http"]
open-path = true
[permissions.http]
origins = ["https://api.example.com"]
[permissions.filesystem]
read = ["${xdg-config}/myapp/config.toml"]
[[permissions.command]]
binary = "my-tool"
argv = [
{ kind = "literal", value = "run" },
{ kind = "rest", constraint = { kind = "any-string" } },
]
[storage.sql]
migrations = ["migrations/001_init.sql"]
[[tasks]]
id = "cleanup"
schedule = "*/30 * * * *"
[frontend]
launcher-bundle = "frontend/dist/launcher.js"
launcher-css = "frontend/dist/launcher.css"
settings-bundle = "frontend/dist/settings.js"
settings-css = "frontend/dist/settings.css"
[frontend.views]
main = "MainView"
[frontend.inline-views]
preview = "PreviewInline"
[frontend.settings]
component = "MySettings"
[shortcuts]
quick-open = { label = "Quick Open", default = "CmdOrCtrl+Shift+G" }

The [gadget] section is the only required section. It tells the host who your gadget is, where to find its WASM binary, and how to display it in the settings UI. The id is particularly important: it determines the gadget’s settings namespace (gadgets.<id>.*), its storage directory, and how other parts of the system reference it. Choose it carefully, because changing it later means losing existing user settings and data.

All fields except prefixes are required.

Field Type Description
id string Unique identifier. Lowercase alphanumeric, hyphens, and dots only. No leading or trailing hyphen. Should follow reverse-domain convention (see below).
name string Human-readable display name shown in settings.
description string Short description shown in the settings header.
version string Version string (e.g. "0.1.0").
wasm string Path to the compiled WASM binary, relative to the gadget root.
icon string Either "heroicons:<name>" for a Heroicons icon, or a relative path to a bundled asset (e.g. "assets/icon.svg").
prefixes list of strings Search prefixes for exclusive query routing. Default: [].

The wasm field must match the compiled binary filename. Cargo converts hyphens to underscores in crate names, so a crate named my-gadget produces my_gadget.wasm.

Most gadgets have configurable options: retention periods, feature toggles, API tokens, display preferences. The [settings] section declares default values for these options. When the gadget is loaded for the first time, these defaults are written into the settings store. Once the user changes a value through the settings panel, the user’s value takes precedence and the manifest default is never applied again for that key.

Keys are arbitrary (camelCase by convention). Values can be any JSON-compatible type: booleans, integers, floats, strings.

manifest.toml
[settings]
heuristicEnabled = true
retentionDays = 30
greeting = "Hello!"

Defaults are seeded into the settings store on first load. Existing user values are never overwritten. The gadget reads settings at runtime through the settings host import and reacts to changes via on_setting_changed.

Gadgets run in a sandbox. By default, no host capability is accessible. The [permissions] section is where you declare what your gadget needs. If a capability is not listed here, the host blocks it at runtime with a permission error. This design lets users audit what a gadget can do before installing it.

When the host instantiates a gadget, it reads the [permissions] section and builds a compiled permission object for each declared capability. These objects are stored on the WASM instance and checked on every host import call. The enforcement flow is:

  1. The manifest declares what the gadget needs.
  2. The host parses and compiles the declarations at instantiation time (glob patterns are compiled, regexes are anchored, origins are normalized).
  3. Every host import call checks the compiled permission object before doing any work. If the capability was not declared at all, the call returns a permission-denied error immediately.
  4. For scoped permissions (HTTP, filesystem, command, opener), the host runs additional checks against the specific request (URL origin, file path, binary + arguments) using the compiled rules.

Some capabilities are simple on/off grants. The gadget either has access or it doesn’t, with no further scoping. All default to false.

Key Capability
clipboard Write text to the system clipboard via clipboard. Read access is intentionally not exposed.
frecency Read the gadget’s own frecency ranking data via frecency. The host records frecency automatically during execution; this flag only controls whether the gadget can query its own rankings.
website-metadata Look up website titles and favicons via website-metadata. The cache is shared across all gadgets.
settings Access the gadget’s own settings namespace via settings.
sql-storage Use a per-gadget SQLite database via sql-storage. Also requires a [storage.sql] section declaring migrations.
path-resolver Resolve substitution variables via path-resolver.

For example, a gadget that copies results to the clipboard, uses frecency for its browse mode, and enriches URL results with website titles and favicons would declare:

manifest.toml
[permissions]
clipboard = true
frecency = true
website-metadata = true

The [permissions.opener] section controls the opener host import, which lets gadgets open URLs in the browser, launch files with their default application, or reveal paths in the file manager. Each of these three operations has its own permission field, described below. At least one must be enabled.

Field Type Default Description
schemes list of strings [] URL schemes allowed for open_url (e.g. "https", "http", "mailto").
open-path bool false Allow opening filesystem paths with the OS default handler.
reveal-path bool false Allow revealing filesystem paths in the file manager.

A gadget that opens web links and reveals files in the file manager, but does not launch arbitrary paths, would declare:

manifest.toml
[permissions.opener]
schemes = ["https", "http"]
reveal-path = true

At call time, the host enforces each field independently:

  • open_url: the host parses the URL and compares its scheme (case-insensitive) against the schemes list. A scheme not in the list is blocked with a permission error.
  • open_path / reveal_path: pure boolean gates. If false, the call is rejected immediately regardless of which path is requested.

The [permissions.http] section controls the http host import, which lets gadgets make synchronous HTTP requests. Gadgets that fetch data from web APIs, download databases, or call remote services declare their allowed origins here.

Field Type Description
origins list of strings Allowed origins. Normalized at parse time. Non-empty. Use "*" for trust-all.

A gadget that fetches data from two endpoints would declare:

manifest.toml
[permissions.http]
origins = ["https://api.example.com", "https://cdn.example.com"]

At call time, the host parses the request URL and extracts its origin (scheme + host + non-default port). A request to https://api.example.com/v1/data produces the origin https://api.example.com, which must be in the list. Port normalization is automatic: https://example.com:443 and https://example.com are the same origin.

The [permissions.filesystem] section controls the filesystem host import, which lets gadgets read files from the host filesystem. Gadgets that need to read configuration files, data directories, or log files declare which paths they may access here.

Field Type Description
read list of strings Path patterns. Supports * (single segment) and ** (cross-segment) globs. Non-empty.

Patterns can use substitution variables like ${home} or ${xdg-config} to reference platform-specific directories without hardcoding paths. See Substitution variables for the complete list of available variables and where they are recognized.

A gadget that reads its own config file and a directory of JSON data files would declare:

manifest.toml
[permissions.filesystem]
read = [
"${xdg-config}/myapp/config.toml",
"/var/lib/myapp/data/*.json",
"${home}/.config/myapp/**",
]

At call time, the host performs three steps before allowing a read:

  1. Validation: rejects paths that are empty, relative, or contain .., ., //, or NUL bytes.
  2. Canonicalization: resolves all symlinks in the request path via std::fs::canonicalize, producing the real filesystem path.
  3. Matching: checks the real path against the compiled glob set built from the manifest patterns. A symlink inside an allowed directory that points outside the allow set is rejected because the resolved real path no longer matches.

The [[permissions.command]] section controls the command host import, which lets gadgets run system processes. This is the most fine-grained permission: you specify not just which binary, but what argument patterns are allowed per position. A gadget granted git log cannot run git push.

The double-bracket [[...]] syntax is TOML’s array of tables: each [[permissions.command]] block declares one rule, and you can repeat the block as many times as needed to allow different binaries or different invocation shapes of the same binary.

Field Type Required Description
binary string yes Executable name or absolute path.
argv list of constraints no Per-position argument rules. Default: [] (zero arguments only).
cwd string no Working directory. Supports substitution variables.
timeout-ms-max integer no Hard ceiling on per-call timeout in milliseconds.
max-output-bytes integer no Hard ceiling on captured stdout + stderr bytes.
max-stdin-bytes integer no Hard ceiling on stdin input bytes.

Each element in argv constrains one argument position using an inline table with a kind field:

Kind Fields Description
literal value Exact string match. Supports substitution variables.
enum values Argument must equal one of the listed strings.
glob pattern Argument must match the glob pattern.
regex pattern Argument must match the regex (anchored to the full string automatically).
path-under root Argument must canonicalize to a path under root. Symlinks are resolved before checking. Supports substitution variables.
any-string (none) Accepts any value unconditionally.
rest constraint Applies the inner constraint to all remaining positions. Must be the last element in the list.

A gadget that runs git log with arbitrary trailing arguments and mdfind with a fixed query would declare:

manifest.toml
[[permissions.command]]
binary = "git"
argv = [
{ kind = "literal", value = "log" },
{ kind = "rest", constraint = { kind = "any-string" } },
]
[[permissions.command]]
binary = "mdfind"
argv = [
{ kind = "literal", value = "kMDItemContentType == 'com.apple.application-bundle'" },
]

At call time, the host matches the binary name by exact string equality, then walks the argument list position by position against the compiled constraints. Without a trailing rest constraint, the argument count must match exactly. With rest, the prefix must match and every trailing argument must satisfy the inner constraint. If no rule matches, the call is rejected before the process is spawned.

If you declare multiple rules for the same binary, their argument patterns must be distinguishable. The host rejects the manifest at parse time if two rules could both accept the same call, because it would be unclear which rule should apply. For example, this is rejected because a call like git log would match both rules:

manifest.toml (invalid)
[[permissions.command]]
binary = "git"
argv = [{ kind = "any-string" }]
[[permissions.command]]
binary = "git"
argv = [{ kind = "literal", value = "log" }, { kind = "rest", constraint = { kind = "any-string" } }]

The fix is to make both rules distinguishable by using a literal first argument in each:

manifest.toml (valid)
[[permissions.command]]
binary = "git"
argv = [{ kind = "literal", value = "status" }]
[[permissions.command]]
binary = "git"
argv = [{ kind = "literal", value = "log" }, { kind = "rest", constraint = { kind = "any-string" } }]

Several permission fields support ${...} substitution variables that resolve to host-specific paths at parse time. This lets manifests reference directories like the user’s home or the gadget’s own data directory without hardcoding platform-specific paths.

Variable Resolves to
${gadget-data} The gadget’s host-managed state directory.
${gadget-archive} The gadget’s code root (archive or dev directory).
${home} The user’s home directory.
${xdg-config} XDG config directory (~/.config on Linux, ~/Library/Application Support on macOS).
${xdg-data} XDG data directory (~/.local/share on Linux, ~/Library/Application Support on macOS).

Recognized in: [permissions.filesystem] read patterns, [[permissions.command]] cwd, literal values, enum values, and path-under roots.

These variables are also available at runtime through the path-resolver host import, so your gadget code can construct the same paths the permission rules reference.

Gadgets that need persistent structured storage (clipboard history, calculation logs, cached API responses) can declare a SQLite database in the [storage.sql] section. Each gadget gets its own isolated database file managed entirely by the host. The gadget never touches the file directly; instead it uses the sql-storage host import to execute queries and statements.

Field Type Description
migrations list of strings Ordered list of SQL file paths relative to the gadget root.

A gadget that creates a table and later adds an index would declare:

manifest.toml
[storage.sql]
migrations = [
"migrations/001_init.sql",
"migrations/002_add_index.sql",
]

Migration files are standard SQL scripts. The host applies them in the order they appear in the migrations list before the gadget’s enable() runs, so the database is always at the latest schema when your code first accesses it. Naming files with a numeric prefix (001_, 002_) is recommended to keep the directory listing consistent with the manifest order, but the filenames themselves have no effect on execution order.

A typical initial migration creates the gadget’s tables:

migrations/001_init.sql
CREATE TABLE history (
id TEXT PRIMARY KEY,
expression TEXT NOT NULL,
result TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);

A later migration can add an index or a new column:

migrations/002_add_index.sql
CREATE INDEX idx_history_created_at ON history(created_at DESC);

The database file is stored in the gadget’s host-managed state directory, separate from the gadget’s code. See Packaging for the storage layout.

Some gadgets need to run work periodically without user interaction: cleaning up old database rows, refreshing a cached dataset, syncing with an external service. Since WASM gadgets cannot spawn their own threads or timers, the host provides a cron-based scheduler. Like [[permissions.command]], the double-bracket [[tasks]] syntax is TOML’s array of tables, so you can declare multiple tasks by repeating the block. Each entry declares a task ID and a schedule. The host invokes the gadget’s run_task export at the configured times. Task errors are logged but do not disable the gadget. Duplicate task IDs within a gadget are rejected at parse time.

Field Type Description
id string Unique task identifier passed to run_task.
schedule string 5-field POSIX cron expression (minute hour day month weekday).

A gadget with a cleanup task every 30 minutes and a sync task every 6 hours would declare:

manifest.toml
[[tasks]]
id = "retention-cleanup"
schedule = "*/30 * * * *"
[[tasks]]
id = "sync"
schedule = "0 */6 * * *"

Gadgets that render custom UI in the launcher or provide their own settings panel ship React frontend bundles alongside the WASM binary. This section tells the host where to find those bundles and which JavaScript exports map to which views.

All paths are relative to the gadget root. See the Frontend section for building these bundles.

Field Type Description
launcher-bundle string ES module for the launcher webview. Required if views, inline-views, or launcher-css are declared.
launcher-css string CSS file loaded alongside the launcher bundle.
settings-bundle string ES module for the settings webview. Required if [frontend.settings] or settings-css are declared.
settings-css string CSS file loaded alongside the settings bundle.

When your gadget returns a CustomUi search response, the view field in your ViewResponse tells the host which component to mount. The [frontend.views] table connects that name to an actual JavaScript export from launcher-bundle. The key is the view name you use in Rust, the value is the named export in your JavaScript bundle.

A gadget with a grid view and a detail view would declare:

manifest.toml
[frontend.views]
grid = "EmojiGrid"
detail = "EmojiDetail"

When your search() returns a CustomUi response with view set to "grid", the host looks up that name in this table and mounts the EmojiGrid component.

The [frontend.inline-views] table works the same way, but for InlineUi search responses. Inline views render a small component above the standard result list rather than replacing it entirely.

A gadget that shows a quick preview above the results would declare:

manifest.toml
[frontend.inline-views]
preview = "ResultPreview"

When your search() returns an InlineUi response with view set to "preview", the host looks up that name here and mounts the ResultPreview component above the result list.

The [frontend.settings] section registers the settings panel component. If your gadget has configurable options declared in [settings], you probably want a settings panel so users can change them. This section points the host to the React component exported from settings-bundle that renders your gadget’s settings UI. See Settings Panels for building these components.

Field Type Description
component string Named JavaScript export for the settings panel.

The component name must match a named export in the settings-bundle:

manifest.toml
[frontend.settings]
component = "MySettings"

Some gadgets benefit from global keyboard shortcuts that work even when the launcher is closed. The Clipboard Manager uses this to let users open the clipboard history with Cmd+Shift+V from anywhere. This section declares those shortcuts. The host registers them as system-wide hotkeys when the gadget is enabled, and users can change the key combination in settings.

Field Type Description
label string Description shown in settings.
default string Initial key combination (e.g. "CmdOrCtrl+Shift+V"). Users can change it in settings.

Each key in the table is a stable shortcut ID, and the value declares the label and default binding:

manifest.toml
[shortcuts]
open-clipboard = { label = "Open Clipboard History", default = "CmdOrCtrl+Shift+V" }