Exports
This page is the exhaustive reference for every guest-implemented interface in
the gadget world. Each section shows the WIT contract, the SDK trait, and
usage examples. For background on the WIT notation, see the
WIT Primer.
Every gadget must implement all four export interfaces. For search and
messaging, the SDK provides the typed Search and Messaging traits, which
implement the generated guest traits for you. It also provides
impl_noop_messaging! and impl_noop_tasks! macros for interfaces your gadget
doesn’t use. Registration is handled by the define_gadget! macro:
use torchsnap_gadget_sdk::prelude::*;
struct MyGadget;define_gadget!(MyGadget);define_gadget! generates the WASM component FFI shims that wire every guest
export to the trait implementations on your struct.
Global state
Section titled “Global state”As WASM execution is single-threaded, you can safely use global state without
synchronization primitives. In Rust, the idiomatic pattern is thread_local!
with Cell, RefCell, or OnceCell for interior mutability:
use std::cell::{Cell, RefCell, OnceCell};
thread_local! { static CONFIG_CACHE: OnceCell<Config> = const { OnceCell::new() }; static RESULT_LIMIT: Cell<u32> = const { Cell::new(50) };}State initialized in enable() is available in all subsequent calls
(entries(), search(), execute(), on_setting_changed(), etc.) for the
lifetime of the gadget instance.
Lifecycle
Section titled “Lifecycle”Controls gadget activation, deactivation, and settings change notifications. The host calls into this interface to manage the gadget’s lifetime.
WIT contract
Section titled “WIT contract”interface lifecycle { enable: func() -> result<_, string>; disable: func(); on-setting-changed: func(key: string, value: string);}SDK trait
Section titled “SDK trait”pub trait LifecycleGuest { fn enable() -> Result<(), String>; fn disable(); fn on_setting_changed(key: String, value: String);}enable()
Section titled “enable()”Runs when the gadget is activated: at startup if the gadget is enabled in settings, or when the user toggles it on. This is where you initialize state, open database connections, seed caches from settings, or run first-launch data imports.
Returning Err(string) signals a fatal initialization failure. The host
deactivates the gadget and logs the error. No further calls reach the gadget
until it is started again. Settings show the gadget as failed to start in a
red box, with your string exactly as you returned it as the reason, so write
it for the user: say what is missing and what fixes it. If enable() traps
instead, Settings show the trap’s root cause. The user’s switch stays on, and
the Retry button in that box calls enable() again, as does turning the
gadget off and on. Each start begins with fresh memory, but anything you wrote
to storage before returning the error is still there.
Only return an error for conditions that genuinely prevent the gadget from functioning. Non-fatal issues (like a network fetch failing on first launch) should be handled internally.
fn enable() -> Result<(), String> { let db = sql_storage::connection(); let count = load_initial_data(&db)?; log_info!("Gadget enabled", "entries" => count); Ok(())}disable()
Section titled “disable()”Runs when the gadget is deactivated. Clean up any state here. The host drops the WASM instance after this call, so all memory is reclaimed regardless, but explicit cleanup lets you flush pending work or log a clean shutdown.
fn disable() { log_info!("Gadget disabled");}on_setting_changed()
Section titled “on_setting_changed()”Fires whenever a setting in the gadget’s namespace changes. key is relative
(the gadgets.<id>. prefix is stripped), and value is a JSON-encoded string
using the same encoding as
settings::get.
fn on_setting_changed(key: String, value: String) { match key.as_str() { "max-results" => { if let Ok(limit) = serde_json::from_str::<u32>(&value) { RESULT_LIMIT.with(|c| c.set(limit)); } } _ => {} }}Search
Section titled “Search”Controls how the gadget provides results and what happens when the user activates one. This is the most substantial export interface. The Search page covers the concepts (catalog mode, query mode, prefix routing, custom views) in detail. This section is the type and function reference.
WIT contract
Section titled “WIT contract”interface search { use types.{entry-icon, entry-actions};
record catalog-entry { id: string, title: string, subtitle: option<string>, icon: option<entry-icon>, keywords: list<string>, actions: entry-actions, }
record scored-entry { id: string, title: string, subtitle: option<string>, icon: option<entry-icon>, score: u32, title-highlight-positions: list<u32>, subtitle-highlight-positions: list<u32>, actions: entry-actions, }
record view-response { view: string, data: option<string>, results: list<scored-entry>, }
variant search-response { nothing, results(list<scored-entry>), custom-ui(view-response), inline-ui(view-response), }
enum post-action { nothing, dismiss, keep-open, open-settings }
entries: func() -> list<catalog-entry>; search: func(query: string, matched-prefix: option<string>) -> search-response; execute: func(command: string) -> result<post-action, string>;}entry-actions comes from the shared
types interface.
SDK trait
Section titled “SDK trait”pub trait Search { type Command: Serialize + DeserializeOwned;
fn entries() -> Vec<CatalogEntry<Self::Command>> { Vec::new() } fn search(query: String, matched_prefix: Option<String>) -> SearchResponse<Self::Command>; fn execute(command: Self::Command) -> Result<PostAction, String>;}Command is your gadget’s own type for what an action does (see
Commands). The SDK implements the generated
SearchGuest trait for every type that implements Search. That
implementation encodes the commands of your entries to JSON on the way out,
and decodes the command string back into Self::Command before it calls your
execute().
You can implement SearchGuest by hand instead and work with the raw WIT types
and command strings. The prelude does not export it. Import it as
torchsnap_gadget_sdk::SearchGuest, and the generated entry types from
torchsnap_gadget_sdk::wit_search. A type that implements Search cannot also
implement SearchGuest.
The entry, action, and response types below are the SDK’s own. They are
generic over the command type C, and the prelude exports them.
CatalogEntry
Section titled “CatalogEntry”Returned by entries() for host-side fuzzy matching
(catalog mode).
| Field | Type | Description |
|---|---|---|
id |
String |
Unique identifier within this gadget. The host records which entries the user picks by this ID. |
title |
String |
Primary display text. The host highlights match positions. |
subtitle |
Option<String> |
Secondary text shown below the title. |
icon |
Option<EntryIcon> |
Result icon. See shared types. |
keywords |
Vec<String> |
Additional match targets. Keyword matches score the entry but are not highlighted in the title. |
actions |
Actions<C> |
The entry’s actions, one optional action per slot. See Actions. |
ScoredEntry
Section titled “ScoredEntry”Returned by search() for gadget-scored results
(query mode).
| Field | Type | Description |
|---|---|---|
id |
String |
Unique identifier within this gadget. |
title |
String |
Primary display text. |
subtitle |
Option<String> |
Secondary text. |
icon |
Option<EntryIcon> |
Result icon. |
score |
u32 |
Sort ranking. Higher scores appear first. |
title_highlight_positions |
Vec<u32> |
UTF-16 code unit offsets to highlight in the title. |
subtitle_highlight_positions |
Vec<u32> |
UTF-16 code unit offsets to highlight in the subtitle. |
actions |
Actions<C> |
Same semantics as CatalogEntry. |
Actions
Section titled “Actions”The actions of an entry, one optional field per slot. The slot decides the key that runs the action.
| Field | Type | Key |
|---|---|---|
primary |
Option<Action<C>> |
Enter, or a click on the entry |
secondary |
Option<Action<C>> |
Cmd+Enter |
copy |
Option<Action<C>> |
Cmd+C |
reveal |
Option<Action<C>> |
Cmd+Shift+R |
delete |
Option<Action<C>> |
Cmd+Backspace |
open_settings |
Option<Action<C>> |
Cmd+, |
Cmd is Ctrl on platforms other than macOS.
| Method | Description |
|---|---|
Actions::new() |
No actions. Same as Actions::default(). |
.primary(label, command) |
Fill primary with a labeled action. |
.secondary(label, command) |
Fill secondary with a labeled action. |
.copy(action), .reveal(action), .delete(action), .open_settings(action) |
Fill that slot with action. |
.iter() |
The filled slots as (Slot, &Action<C>) pairs, in the order of the fields above. |
A second builder call for the same slot replaces the first action.
Action
Section titled “Action”| Field | Type | Description |
|---|---|---|
label |
Option<String> |
What the launcher shows. None shows the slot’s default label, such as “Copy”. primary and secondary have no default and show the entry’s title. |
command |
C |
What running the action does. execute() receives it when the user runs the action. |
Action::new(command) creates an action without a label, and
Action::labeled(label, command) one with a label.
SearchResponse
Section titled “SearchResponse”| Variant | Description |
|---|---|
Nothing |
The gadget has nothing for this query. |
Results(Vec<ScoredEntry<C>>) |
Standard scored results merged into the host’s result list. |
CustomUi(ViewResponse<C>) |
Replaces the entire result list with the named frontend view. |
InlineUi(ViewResponse<C>) |
Rendered above the result list. Only one gadget can claim the inline slot per search. |
Returning Results(vec![]) is semantically different from Nothing: it tells
the host the gadget participated and explicitly produced zero results, which
clears any stale entries from a previous keystroke.
ViewResponse
Section titled “ViewResponse”| Field | Type | Description |
|---|---|---|
view |
String |
Name of the frontend component registered in the manifest. |
data |
Option<String> |
JSON-encoded payload forwarded to the React component. |
results |
Vec<ScoredEntry<C>> |
Entries to display in the result list alongside the view. Empty for full-takeover custom UIs. |
PostAction
Section titled “PostAction”Returned by execute() to control launcher behavior after activation.
| Variant | Effect |
|---|---|
Nothing |
Leave the launcher open, state unchanged. |
Dismiss |
Close the launcher and reset its state. |
KeepOpen |
Leave the launcher open. Functionally identical to Nothing; exists as a semantic signal for multi-select workflows. |
OpenSettings |
Close the launcher and open the Settings window on this gadget’s section. A gadget without a settings section of its own gets the Gadgets page. |
entries()
Section titled “entries()”Returns the gadget’s catalog entries for host-side fuzzy matching. Called on
every keystroke. Gadgets that only use query mode can leave it out, as the
default implementation returns an empty Vec.
fn entries() -> Vec<CatalogEntry<Command>> { QUOTES.with(|q| { q.borrow() .iter() .map(|quote| quote.to_catalog_entry()) .collect() })}search()
Section titled “search()”Called on every keystroke. matched_prefix is Some("=") (or whichever
prefix) when the gadget claimed a
prefix and the user’s input starts with
it. None otherwise. When a prefix matched, the host has already stripped it
from query before calling search(), so query is ready to evaluate as is.
Gadgets that only use catalog mode return SearchResponse::Nothing.
fn search(query: String, matched_prefix: Option<String>) -> SearchResponse<Command> { match evaluate(&query) { Some(result) => SearchResponse::Results(vec![result.to_scored_entry()]), None => SearchResponse::Nothing, }}execute()
Section titled “execute()”Runs when the user runs an action. Receives the command of that action,
decoded into your Command type. The entry and the slot are not passed. Put
everything the action needs into the command.
A gadget whose entries open or copy a URL, and whose “API token missing” entry opens its settings:
fn execute(command: Command) -> Result<PostAction, String> { match command { Command::OpenUrl(url) => { opener::open_url(&url).map_err(|e| format!("open URL: {e}"))?; Ok(PostAction::Dismiss) } Command::CopyUrl(url) => { clipboard::write_text(&url).map_err(|e| format!("copy URL to clipboard: {e}"))?; Ok(PostAction::Dismiss) } Command::OpenSettings => Ok(PostAction::OpenSettings), }}If the command string does not decode into Command, the SDK returns an Err
without calling your execute().
Messaging
Section titled “Messaging”Custom RPC between the gadget’s frontend (React views and settings panels) and its WASM backend. This interface is optional: only gadgets with a frontend that needs to communicate with the backend implement it.
WIT contract
Section titled “WIT contract”interface messaging { handle-message: func(method: string, payload: string) -> result<string, string>;}SDK trait
Section titled “SDK trait”pub trait Messaging { type Request: DeserializeOwned;
fn handle(request: Self::Request) -> Result<serde_json::Value, String>;}Request lists every method your frontend calls. The SDK deserializes each
call from an object with a method and a payload key, so an enum with
serde’s adjacent tagging maps each method name to a variant and the payload to
the variant’s fields:
#[derive(Deserialize)]#[serde(tag = "method", content = "payload", rename_all = "snake_case")]enum Request { GetHistory { page: u32 }, ClearHistory,}
impl Messaging for MyGadget { type Request = Request;
fn handle(request: Request) -> Result<serde_json::Value, String> { match request { Request::GetHistory { page } => { let entries = load_history_page(page)?; serde_json::to_value(&entries).map_err(|e| format!("serialize history: {e}")) } Request::ClearHistory => { clear_history_db()?; Ok(serde_json::Value::Bool(true)) } } }}Methods without arguments are unit variants such as ClearHistory. They
accept an empty object or null as payload.
The SDK implements the generated MessagingGuest trait for every type that
implements Messaging, and serializes the Ok value as the response.
Err(String) propagates to the frontend as an error on the sendMessage
promise. An unknown method or a payload that does not fit its variant rejects
the promise as well, before handle runs.
Implementing MessagingGuest by hand
Section titled “Implementing MessagingGuest by hand”A gadget can implement the generated trait directly instead. The prelude does
not export it. Import it as torchsnap_gadget_sdk::MessagingGuest:
pub trait MessagingGuest { fn handle_message(method: String, payload: String) -> Result<String, String>;}Both payload and the Ok return value are JSON-encoded strings. The
method parameter lets a single gadget expose multiple operations. The
standard pattern is a match on method names:
fn handle_message(method: String, payload: String) -> Result<String, String> { match method.as_str() { "get_history" => { let request: PageRequest = messaging::parse_payload(&payload)?; let entries = load_history_page(request.page)?; messaging::to_response(&entries) } "clear_history" => { clear_history_db()?; messaging::to_response(&true) } other => Err(format!("unknown method: {other}")), }}SDK helpers
Section titled “SDK helpers”The messaging module is not in the prelude. Import it with
use torchsnap_gadget_sdk::messaging;.
| Function | Description |
|---|---|
messaging::decode_request<R>(method, payload) |
Decode a call into R the way Messaging does. Public so your tests can check your Request enum against the payloads your frontend sends. |
messaging::parse_payload<T>(payload) |
Parse the JSON-encoded payload into T. |
messaging::to_response<T>(value) |
Serialize value as a JSON-encoded response string. |
Noop macro
Section titled “Noop macro”Gadgets that don’t use messaging provide a stub with impl_noop_messaging!:
impl_noop_messaging!(MyGadget);This generates an implementation that returns
Err("gadget does not handle messages: <method>") for any call.
Scheduled background work via cron expressions declared in the
manifest. The host invokes the
gadget’s run_task export at the configured times. Task errors are logged but
do not disable the gadget.
WIT contract
Section titled “WIT contract”interface tasks { run-task: func(task-id: string) -> result<_, string>;}SDK trait
Section titled “SDK trait”pub trait TasksGuest { fn run_task(task_id: String) -> Result<(), String>;}task_id matches the id field from the manifest’s [[tasks]] entry. The
standard pattern dispatches on the ID:
fn run_task(task_id: String) -> Result<(), String> { match task_id.as_str() { "refresh-data" => fetch_and_update_cache(), "prune-history" => delete_entries_older_than_days(30), _ => Err(format!("unknown task: {task_id}")), }}Noop macro
Section titled “Noop macro”Gadgets that don’t use scheduled tasks provide a stub with impl_noop_tasks!:
impl_noop_tasks!(MyGadget);This generates an implementation that returns
Err("gadget declares no scheduled tasks: <task_id>") for any call.