Skip to content

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.

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.

Controls gadget activation, deactivation, and settings change notifications. The host calls into this interface to manage the gadget’s lifetime.

interface lifecycle {
enable: func() -> result<_, string>;
disable: func();
on-setting-changed: func(key: string, value: string);
}
pub trait LifecycleGuest {
fn enable() -> Result<(), String>;
fn disable();
fn on_setting_changed(key: String, value: String);
}

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(())
}

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");
}

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));
}
}
_ => {}
}
}

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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()
})
}

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,
}
}

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().

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.

interface messaging {
handle-message: func(method: string, payload: string) -> result<string, string>;
}
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.

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}")),
}
}

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.

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.

interface tasks {
run-task: func(task-id: string) -> result<_, string>;
}
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}")),
}
}

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.