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.
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
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(())}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, 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>;}SDK trait
Section titled “SDK trait”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>;}CatalogEntry
Section titled “CatalogEntry”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. |
ScoredEntry
Section titled “ScoredEntry”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(). |
SearchResponse
Section titled “SearchResponse”| 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.
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> |
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. |
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 return an empty Vec.
fn entries() -> Vec<CatalogEntry> { QUOTES.with(|q| { q.borrow() .iter() .map(|quote| quote.to_catalog_entry()) .collect() })}search()
Section titled “search()”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, }}execute()
Section titled “execute()”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 contextlet entry = ScoredEntry { data: Some(data::encode(&url)?), // ...};
// In execute(): retrieve contextfn 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), }}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 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}")), }}SDK helpers
Section titled “SDK helpers”| 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. |
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.