Skip to content

Imports

This page is the exhaustive reference for every host-provided interface in the gadget world. Each section shows the WIT contract, SDK helpers where available, and a usage example. For background on the WIT notation, see the WIT Primer.

The types interface holds type definitions shared across both imports and exports. It is imported into the gadget world so that interfaces on both sides resolve to the same generated types.

interface types {
variant entry-icon {
hero-icon(string),
data-url(string),
asset-icon(string),
emoji(string),
}
variant action-id {
open, copy, reveal, open-with, delete, open-settings,
custom(string),
}
record action {
id: action-id,
label: string,
}
}
Type Description
EntryIcon Icon for a search result. HeroIcon takes a Heroicons name, DataUrl a base64-encoded inline image, AssetIcon a path relative to the gadget archive, and Emoji a Unicode emoji character.
ActionId Identifies an action on a search result. Well-known variants (Open, Copy, Reveal, OpenWith, Delete, OpenSettings) receive default keybindings and icons from the host. Custom(string) is the escape hatch for gadget-specific actions.
Action Pairs an ActionId with a display label. The first action in an entry’s list is the primary action, activated with Enter.

These types are available in the SDK prelude as EntryIcon, ActionId, and Action.

Structured log output with severity levels and timing spans. Log messages appear in the Torchsnap devtools panel.

interface logging {
enum log-level { trace, debug, info, warn, error }
type metadata-entry = tuple<string, string>;
log: func(level: log-level, message: string,
metadata: list<metadata-entry>, span: option<u64>);
span-start: func(name: string, parent: option<u64>,
metadata: list<metadata-entry>) -> u64;
span-end: func(span-id: u64, metadata: list<metadata-entry>);
}

The SDK re-exports the raw functions at logging::log, logging::span_start, and logging::span_end. On top of these, it provides convenience macros that cover the common case of logging a message with optional key-value metadata outside of any span:

Macro Level
log_trace!("msg", key => value, ...) Trace
log_debug!("msg", key => value, ...) Debug
log_info!("msg", key => value, ...) Info
log_warn!("msg", key => value, ...) Warn
log_error!("msg", key => value, ...) Error

The key => value pairs are optional and accept anything that implements ToString. The macros always pass None for the span argument. For span-scoped log entries, call logging::log(...) directly with a handle from span_start.

log_info!("Fetching results", "source" => "api", "limit" => 50);
let span = logging::span_start("api-call", None, &[]);
// ... do work ...
logging::span_end(span, &[("status".into(), "ok".into())]);

Spans can be nested by passing the parent handle to span_start:

let outer = logging::span_start("enable", None, &[]);
let inner = logging::span_start("load-data", Some(outer), &[]);
// ...
logging::span_end(inner, &[]);
logging::span_end(outer, &[]);

Read access to the gadget’s configuration values. Keys are scoped to the gadget’s namespace: the host strips the gadgets.<id>. prefix automatically, so a key stored as gadgets.my-gadget.theme is accessed as just "theme". Values are JSON-encoded strings.

interface settings {
get: func(key: string) -> option<string>;
}

The raw function returns the JSON string or None if the key has never been written (neither by a manifest [settings] default nor at runtime).

The SDK module at settings wraps the raw call with typed JSON parsing:

Function Description
settings::get<T>(key) Fetch and parse. Returns None for both unset keys and parse failures.
settings::get_or<T>(key, default) Like get, but returns default on None.
settings::get_or_else<T>(key, f) Like get_or, but calls f() lazily to produce the default.

T must implement serde::de::DeserializeOwned.

let theme: String = settings::get_or("theme", "dark".into());
let max_results: u32 = settings::get_or("max-results", 10);
let verbose: bool = settings::get_or("verbose", false);

Read files bundled inside the gadget archive. Useful for shipping static data (word lists, icon sets, templates, configuration files) alongside your WASM binary. Paths are relative to the gadget’s root directory.

interface assets {
variant assets-error {
invalid-path(string),
not-found,
io-error(string),
}
read: func(path: string) -> result<list<u8>, assets-error>;
exists: func(path: string) -> result<bool, assets-error>;
}
Function Description
read(path) Read the file at path and return its contents as bytes.
exists(path) Check whether a file exists at path.

The host validates that paths stay inside the gadget root. Absolute paths, .. segments, backslashes, NUL bytes, and Windows drive letters are rejected with InvalidPath.

let bytes = assets::read("data/wordlist.txt")
.map_err(|e| format!("read bundled wordlist: {e:?}"))?;
let text = String::from_utf8(bytes)
.map_err(|e| format!("invalid UTF-8: {e}"))?;

Detect the host operating system and CPU architecture at runtime. Useful for gadgets that need to adapt behavior or select platform-specific binaries for command execution.

interface platform {
variant os { macos, linux, windows, other(string) }
variant arch { x86-64, aarch64, other(string) }
current-os: func() -> os;
current-arch: func() -> arch;
}
use torchsnap_gadget_sdk::platform::{self, Os, Arch};
match platform::current_os() {
Os::Macos => { /* macOS-specific path */ }
Os::Linux => { /* Linux-specific path */ }
_ => { /* fallback */ }
}

Resolve substitution variables to their actual host paths at runtime. Useful when constructing file paths dynamically rather than hardcoding platform-specific locations.

interface path-resolver {
variant resolve-error {
unknown-variable(string),
unterminated(string),
}
resolve: func(template: string) -> result<string, resolve-error>;
}

The following variables are recognized:

Variable Resolves to
{"${gadget-data}"} The gadget’s host-managed state directory.
{"${gadget-archive}"} The gadget’s code root (archive or directory).
{"${home}"} The user’s home directory.
{"${xdg-config}"} The XDG config directory (or platform equivalent).
{"${xdg-data}"} The XDG data directory (or platform equivalent).

UnknownVariable is returned for unrecognized variable names. Unterminated is returned when ${ is not closed before the end of the template string.

let data_dir = path_resolver::resolve("${gadget-data}/cache")?;
let config = path_resolver::resolve("${xdg-config}/my-app/config.json")?;

Write-only access to the system clipboard. This is what most gadgets use in their execute() implementation to copy a result. Read access is intentionally not exposed for privacy reasons.

Requires permission clipboard = true in the manifest.

interface clipboard {
variant clipboard-error {
backend-failure(string),
}
write-text: func(text: string) -> result<_, clipboard-error>;
}
clipboard::write_text(&entry.title)
.map_err(|e| format!("copy to clipboard: {e:?}"))?;

Read-only access to the gadget’s own frecency ranking data. The host automatically records which results the user activates and applies frecency score bonuses to query results. This interface exists for gadgets that need to read that data back to drive their own UI. The Emoji Picker, for example, uses it to show recently used emoji when the search bar is empty.

Requires permission frecency = true in the manifest.

interface frecency {
record frecency-item {
item-id: string,
score: u32,
}
is-enabled: func() -> bool;
top-items: func(limit: u32) -> list<frecency-item>;
}
Function Description
is_enabled() Whether frecency tracking is active for this gadget.
top_items(limit) Return the top limit items ranked by frecency score, highest first.

The score field is an opaque relative ordering key. Treat it as a rank, not an absolute magnitude.

if frecency::is_enabled() {
let top = frecency::top_items(20);
for item in &top {
// item.item_id matches the id field from your CatalogEntry or ScoredEntry
}
}

Per-gadget isolated SQLite database. The host creates the database file and runs migrations declared in the manifest’s [storage.sql] section before the gadget’s enable() is called. By the time your code runs, the schema is ready.

Requires a [storage.sql] section in the manifest declaring the migration files.

interface sql-storage {
variant sql-value {
null,
integer(s64),
real(f64),
text(string),
blob(list<u8>),
}
resource sql-handle {
execute: func(sql: string, params: list<sql-value>) -> result<u64, string>;
query: func(sql: string, params: list<sql-value>) -> result<list<list<sql-value>>, string>;
}
connection: func() -> sql-handle;
}
Function Description
connection() Obtain a handle to the gadget’s database.
handle.execute(sql, params) Run INSERT, UPDATE, or DELETE. Returns the number of rows affected.
handle.query(sql, params) Run SELECT. Returns all matching rows, each as a list of SqlValue in column order.

The SDK module at sql_storage provides typed column access and convenience query functions on top of the raw interface:

Row wraps a raw Vec<SqlValue> with typed accessors:

Method Returns
row.integer(idx) Option<i64>
row.text(idx) Option<&str>
row.real(idx) Option<f64>
row.blob(idx) Option<&[u8]>
row.is_null(idx) bool (also true for out-of-bounds indices)

Query helpers:

Function Description
sql_storage::query_all(db, sql, params) Returns Result<Vec<Row>, String>.
sql_storage::query_one(db, sql, params) Returns Result<Option<Row>, String>.

SqlValue::from conversions are implemented for &str, String, i64, f64, and Vec<u8>, so you can write SqlValue::from("text") or SqlValue::from(42_i64) instead of the explicit variant constructors.

let db = sql_storage::connection();
db.execute(
"INSERT INTO history (expression, result, computed_at) VALUES (?, ?, ?)",
&[
SqlValue::from(expression),
SqlValue::from(result),
SqlValue::from(timestamp),
],
)?;
let rows = sql_storage::query_all(
&db,
"SELECT expression, result FROM history ORDER BY computed_at DESC LIMIT ?",
&[SqlValue::from(50_i64)],
)?;
for row in &rows {
let expr = row.text(0).unwrap_or("");
let result = row.text(1).unwrap_or("");
}

Synchronous HTTP client for making requests to external services. The interface supports all standard methods, custom headers, request bodies, timeouts, and response size limits.

Requires permission [permissions.http] in the manifest.

interface http {
variant http-method {
get, post, put, patch, delete, head,
other(string),
}
record http-request {
url: string,
method: http-method,
headers: list<tuple<string, string>>,
body: option<list<u8>>,
timeout-ms: option<u32>,
max-body-size: option<u64>,
insecure-tls: bool,
}
record http-response {
status: u16,
headers: list<tuple<string, string>>,
body: list<u8>,
}
variant http-error {
permission-denied(string),
connection-refused(string),
timeout,
dns-failed(string),
tls-failed(string),
invalid-url(string),
other(string),
}
fetch: func(request: http-request) -> result<http-response, http-error>;
}

fetch returns an HttpError only when no HTTP response was produced at all (DNS failure, connection refused, timeout, etc.). HTTP 4xx and 5xx status codes are returned as a normal HttpResponse with the corresponding status field.

The insecure_tls flag disables TLS certificate verification. This is intended for local development endpoints with self-signed certificates.

let response = http::fetch(&HttpRequest {
url: "https://api.example.com/data".into(),
method: HttpMethod::Get,
headers: vec![("Accept".into(), "application/json".into())],
body: None,
timeout_ms: Some(5000),
max_body_size: None,
insecure_tls: false,
})?;
let body = String::from_utf8(response.body)
.map_err(|e| format!("invalid UTF-8: {e}"))?;

Read-only access to files on the host filesystem. You can read file contents, check whether a file exists, and query metadata like size and modification time.

Requires permission [permissions.fs] in the manifest.

interface filesystem {
variant fs-error {
permission-denied(string),
invalid-path(string),
not-found,
io(string),
}
record file-metadata {
size: u64,
modified-unix-ms: u64,
is-symlink: bool,
}
read-file: func(path: string) -> result<list<u8>, fs-error>;
file-exists: func(path: string) -> bool;
metadata: func(path: string) -> result<file-metadata, fs-error>;
}
Function Description
read_file(path) Read the entire file and return its contents as bytes.
file_exists(path) Returns false for missing files, permission errors, and invalid paths alike.
metadata(path) Returns the file’s size, last-modified timestamp, and whether it is a symlink. is_symlink reflects the path before symlink resolution.

Paths must be absolute. The host rejects paths containing .., ., //, or NUL bytes with InvalidPath.

if filesystem::file_exists("/usr/local/bin/my-tool") {
let content = filesystem::read_file("/usr/local/bin/my-tool")?;
}
let meta = filesystem::metadata("/some/file.txt")?;
log_info!("File size", "bytes" => meta.size);

Open URLs in the default browser, launch files with their default application, or reveal files in the system file manager.

Requires permission [permissions.opener] in the manifest.

interface opener {
variant opener-error {
permission-denied(string),
invalid-url(string),
backend-failure(string),
}
open-url: func(url: string) -> result<_, opener-error>;
open-path: func(path: string) -> result<_, opener-error>;
reveal-path: func(path: string) -> result<_, opener-error>;
}
Function Permission required Description
open_url(url) URL scheme in schemes list Open the URL in the default handler for its scheme.
open_path(path) open-path = true Launch the file at path with its default application.
reveal_path(path) reveal-path = true Show the file in the system file manager (Finder, Nautilus, etc.).
opener::open_url("https://example.com")
.map_err(|e| format!("open URL: {e:?}"))?;
opener::reveal_path("/Users/me/Documents/report.pdf")
.map_err(|e| format!("reveal file: {e:?}"))?;

Run system processes and capture their output. Commands are executed directly without a shell wrapper, so shell features like pipes, globbing, or environment variable expansion are not available. Use the platform interface to determine which binaries are appropriate for the current OS.

Requires permission [[permissions.command]] in the manifest.

interface command {
record command-options {
args: list<string>,
cwd: option<string>,
env: list<tuple<string, string>>,
stdin: option<list<u8>>,
timeout-ms: option<u32>,
max-output-bytes: option<u64>,
}
record command-result {
exit-code: option<s32>,
signal: option<string>,
timed-out: bool,
stdout: list<u8>,
stderr: list<u8>,
}
variant command-error {
permission-denied(string),
spawn-failed(string),
timeout,
output-too-large(tuple<list<u8>, list<u8>>),
}
run: func(binary: string, options: command-options) -> result<command-result, command-error>;
}
Field Description
exit_code The process exit code. None if the process was killed by a signal.
signal The signal name (e.g. "TERM") if the process was killed.
timed_out true if the process exceeded timeout_ms.
OutputTooLarge Returned when output exceeds max_output_bytes. The error carries the captured output up to that point.

The SDK module at command provides a builder API on top of the raw run function:

use torchsnap_gadget_sdk::command;
let result = command::run("/usr/bin/git")
.args(["log", "--oneline", "-5"])
.cwd("/path/to/repo")
.timeout(std::time::Duration::from_secs(10))
.invoke()?;
let stdout = String::from_utf8(result.stdout)
.map_err(|e| format!("invalid UTF-8: {e}"))?;

Builder methods:

Method Description
.arg(value) Append a single argument.
.args(iter) Append multiple arguments.
.cwd(path) Set the working directory.
.env(key, value) Add an environment variable.
.stdin(bytes) Provide stdin data.
.timeout(duration) Set the execution timeout.
.max_output_bytes(max) Set the output size limit.
.invoke() Execute and return Result<CommandResult, CommandError>.

Look up website titles, descriptions, and favicons through a host-managed cache shared across all gadgets. Useful for gadgets that display rich metadata for URLs (bookmark managers, link aggregators, history browsers).

Requires permission website-metadata = true in the manifest.

interface website-metadata {
use types.{entry-icon};
variant lookup-mode { cached, blocking }
record cache-entry {
title: option<string>,
description: option<string>,
favicon: entry-icon,
}
variant lookup-result {
hit(cache-entry),
reachable-no-data,
unreachable,
pending,
}
variant website-metadata-error {
permission-denied(string),
invalid-domain(string),
}
lookup: func(domain: string, mode: lookup-mode) -> result<lookup-result, website-metadata-error>;
}

The mode parameter controls caching behavior:

Mode Behavior
Cached Return immediately from cache. If the domain is not cached, the host schedules a background fetch and returns Pending.
Blocking Wait for the fetch to complete if not cached. Never returns Pending.

Use Cached in search functions (called on every keystroke) to avoid blocking. Use Blocking only when the result is genuinely needed before proceeding (e.g., deciding whether to suppress a result for an unreachable domain).

InvalidDomain is returned when the input contains a scheme, port, path, whitespace, or other characters that are not part of a bare domain name.

The SDK module at website_metadata provides a simplified API that folds errors and the raw lookup result into a flat enum:

pub enum Metadata {
Found(CacheEntry),
NoData,
Unreachable,
Pending,
}
Function Description
website_metadata::lookup_cached(domain) Non-blocking lookup. May return Pending.
website_metadata::lookup_blocking(domain) Blocking lookup. Never returns Pending.
website_metadata::favicon_or(domain, fallback) Non-blocking cached lookup. Returns the favicon on cache hit, or fallback for all other outcomes.

Permission and validation errors are logged and folded into Unreachable.

// In a search function (non-blocking):
let icon = website_metadata::favicon_or(
&domain,
EntryIcon::HeroIcon("globe-alt".into()),
);
// When the result matters for logic (blocking):
match website_metadata::lookup_blocking(&domain) {
website_metadata::Metadata::Found(entry) => {
let title = entry.title.unwrap_or_default();
let favicon = entry.favicon;
}
website_metadata::Metadata::Unreachable => {
// Domain not reachable, suppress or show fallback
}
_ => {}
}