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. The SDK 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 disables the gadget immediately and logs the error message. No further calls are dispatched after a failed enable. 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, action-id, action};
record catalog-entry {
id: string,
title: string,
subtitle: option<string>,
icon: option<entry-icon>,
keywords: list<string>,
actions: list<action>,
}
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: list<action>,
data: option<string>,
}
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 }
entries: func() -> list<catalog-entry>;
search: func(query: string, matched-prefix: option<string>) -> search-response;
execute: func(entry: scored-entry, action-id: action-id) -> result<post-action, string>;
}
pub trait SearchGuest {
fn entries() -> Vec<CatalogEntry>;
fn search(query: String, matched_prefix: Option<String>) -> SearchResponse;
fn execute(entry: ScoredEntry, action_id: ActionId) -> Result<PostAction, String>;
}

Returned by entries() for host-side fuzzy matching (catalog mode).

Field Type Description
id String Unique identifier within this gadget. Passed back in execute().
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 Vec<Action> The first action is the primary one (Enter). Well-known ActionId variants receive default keybindings from the host.

Returned by search() for gadget-scored results (query mode). Also passed back into execute().

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 Vec<Action> Same semantics as CatalogEntry.
data Option<String> Opaque gadget-private payload, round-tripped through execute().
Variant Description
Nothing The gadget has nothing for this query.
Results(Vec<ScoredEntry>) Standard scored results merged into the host’s result list.
CustomUi(ViewResponse) Replaces the entire result list with the named frontend view.
InlineUi(ViewResponse) 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> 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.

Returns the gadget’s catalog entries for host-side fuzzy matching. Called on every keystroke. Gadgets that only use query mode return an empty Vec.

fn entries() -> Vec<CatalogEntry> {
QUOTES.with(|q| {
q.borrow()
.iter()
.map(|quote| quote.to_catalog_entry())
.collect()
})
}

Called on every keystroke with the raw query string. matched_prefix is Some("=") (or whichever prefix) when the gadget claimed a prefix and the user’s input starts with it. None otherwise. Gadgets that only use catalog mode return SearchResponse::Nothing.

fn search(query: String, matched_prefix: Option<String>) -> SearchResponse {
let input = match &matched_prefix {
Some(prefix) => &query[prefix.len()..],
None => &query,
};
match evaluate(input) {
Some(result) => SearchResponse::Results(vec![result.to_scored_entry()]),
None => SearchResponse::Nothing,
}
}

Runs when the user activates a result. Receives the full ScoredEntry (including the data field) and the ActionId of the triggered action.

The data field is the primary mechanism for passing context from search() to execute(). Use the SDK’s data::encode and data::decode helpers:

// In search(): attach context
let entry = ScoredEntry {
data: Some(data::encode(&url)?),
// ...
};
// In execute(): retrieve context
fn execute(entry: ScoredEntry, action_id: ActionId) -> Result<PostAction, String> {
match action_id {
ActionId::Open => {
let url: String = data::decode(
entry.data.as_deref().ok_or("missing data")?,
)?;
opener::open_url(&url)?;
Ok(PostAction::Dismiss)
}
ActionId::Copy => {
clipboard::write_text(&entry.title)?;
Ok(PostAction::Dismiss)
}
_ => Ok(PostAction::Nothing),
}
}

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 MessagingGuest {
fn handle_message(method: String, payload: String) -> Result<String, String>;
}

Both payload and the Ok return value are JSON-encoded strings. Err(String) propagates to the frontend as an error on the sendMessage promise.

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 entries = load_history_from_db()?;
messaging::to_response(&entries)
}
"clear_history" => {
clear_history_db()?;
messaging::to_response(&true)
}
other => Err(format!("unknown method: {other}")),
}
}
Function Description
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.