Skip to content

Views

Views are React components that render inside the launcher when a gadget’s search() returns a CustomUi or InlineUi response. There are two kinds:

  • Custom views replace the entire result list with a gadget-controlled UI. The Emoji Picker’s grid and the Calculator’s history panel are custom views. Only available when the gadget is the exclusive responder (via prefix routing).
  • Inline views render above the result list while the results remain visible below. Only one gadget can claim the inline slot per search. The Calculator’s live result display is an inline view.

Custom view components receive:

interface GadgetViewProps {
results: SourcedEntry[];
data?: unknown;
query: string;
matchedPrefix: string;
}
Prop Description
results The ScoredEntry list from the ViewResponse. Empty for full-takeover views.
data The opaque JSON payload from ViewResponse.data, already parsed.
query The current search query with the matched prefix already stripped. While the search for text that no longer starts with matchedPrefix runs, it is still the query of this view’s own search.
matchedPrefix The prefix that triggered this view (e.g. "=" for the calculator).

Inline view components receive:

interface InlineViewProps {
data: unknown;
query: string;
matchedPrefix: string;
selected: boolean;
}
Prop Description
data The opaque JSON payload from ViewResponse.data, already parsed.
query The current search query with the matched prefix already stripped. While the search for text that no longer starts with matchedPrefix runs, it is still the query of this view’s own search.
matchedPrefix The prefix that triggered this view.
selected Whether the inline view currently has focus (index 0 in the result list).

Views access gadget state and launcher actions through hooks imported from @torchsnap/gadget-sdk/hooks. These hooks work because the host wraps every view in a GadgetContextProvider.

function useGadgetInfo(): { id: string; enabled: boolean }

Returns the gadget’s ID and enabled state. The ID matches the [gadget] id from the manifest.

function useGadgetRuntime(): {
sendMessage: GadgetSendMessage;
logger: Logger;
}

Provides the messaging interface for frontend-to-backend RPC and a logger for the gadget.

function useLauncher(): LauncherActions

Returns actions for controlling the launcher from within a view:

Action Description
goBack() Return to the normal result list, dismissing the custom view.
dismiss() Close the launcher entirely.
openSettings() Open the Settings window on this gadget’s section and close the launcher, like the OpenSettings post-action. When the window cannot open, the launcher stays open, shows the error in its footer, and the returned promise rejects.
onExecute(entryId, slot) Run the action in slot of an entry the view received in results. Returns a promise. See Running entry actions.
showError(message) Show a failure of the view’s own action in the launcher footer. See Reporting errors.
onFooterChange(state) Replace the launcher’s footer bar with custom content.
setDisplayQuery(query) Update the search bar text without triggering a new search.
mouseActiveRef A ref indicating whether the mouse is currently active (for keyboard/mouse interaction management).

A view that renders its own list of results runs an entry’s action with onExecute. slot is one of "primary", "secondary", "copy", "reveal", "delete" and "openSettings" (the ActionSlot type), matching the slots of the Rust SDK. The host passes the command in that slot to the gadget’s execute(), and the launcher closes if execute() returns Dismiss.

The Emoji Picker’s grid copies the selected emoji on Enter:

const { onExecute } = useLauncher();
const entry = results[selectedIndex];
if (entry) {
onExecute(entry.id, "copy");
}

onExecute only reaches entries from the view’s results. For an unknown entry ID, or a slot the entry leaves empty, nothing happens. A view that acts on its own state instead, such as a value it computed, calls its backend with sendMessage and then a launcher action such as dismiss().

When an action fails, the launcher stays open and shows one error line in place of the footer hints. The line goes away on the user’s next key press, action, query or selection change, or when the launcher hides.

onExecute and openSettings report their own failures. When execute() returns Err, the footer shows <gadget name> failed: <message> and the promise onExecute returned rejects with the message. The launcher handles that rejection, so a view can ignore the promise. Await it only to react to the outcome, such as resetting a busy state:

const { onExecute } = useLauncher();
setBusy(true);
onExecute(entry.id, "copy")
.catch(() => {}) // the footer already shows the error
.finally(() => setBusy(false));

For an action the view runs through sendMessage, pass the failure to showError(message). The launcher shows the message as given:

const { dismiss, showError } = useLauncher();
const { sendMessage } = useGadgetRuntime();
try {
await sendMessage("copy", { result });
} catch (error) {
showError(`Copying the result failed: ${String(error)}`);
return;
}
dismiss();

Report only failures of something the user just did. A background load that fails, such as a preview, belongs in the view itself or in the gadget’s log (useGadgetRuntime().logger).

By default, the launcher shows an action bar in the footer derived from the selected entry’s actions. A custom view can override this by calling onFooterChange(state) with a FooterState object describing the custom footer content. The host resets the footer when the view unmounts.

For inline views, the custom footer is only shown when the inline slot is selected (focused at index 0).

import { highlightText } from "@torchsnap/gadget-sdk/utils";
function highlightText(
text: string,
positions: readonly number[],
highlightClassName?: string,
): ReactNode[]

Renders text with highlighted characters at the given UTF-16 code-unit offsets. The positions array matches the titlePositions and subtitlePositions fields on SourcedEntry. The default highlight class is "text-accent font-semibold".

Useful when your custom view renders its own result list and needs to show the same match highlighting as the host’s built-in list.

import { useWindowedList } from "@torchsnap/gadget-sdk/hooks";
function useWindowedList(params: {
selectedIndex: number;
setSelectedIndex: (index: number) => void;
resultCount: number;
pageSize: number;
}): {
windowStart: number;
wheelRef: (el: HTMLDivElement | null) => void;
}

Manages a virtual scrolling window for custom views that render long lists. Returns the current window start index and a ref callback for the scroll container that handles mouse wheel events.

Custom views can register keyboard shortcuts that override or extend the launcher’s default bindings while the view is mounted:

import { useKeyBindings, LAYER } from "@torchsnap/gadget-sdk/keybindings";
useKeyBindings([
{
id: "clear-history",
layer: LAYER.COMPONENT,
handler: () => clearHistory(),
keybindings: [{ combo: { modifiers: ["Meta"], key: "Backspace" } }],
},
]);

key is the key name as the browser reports it in KeyboardEvent.key, such as "Backspace", "Enter" or "?". Letters match regardless of case, so "k" and "K" are the same key and CapsLock does not affect them. For letters, Shift counts as a modifier: { modifiers: ["Meta", "Shift"], key: "k" } fires on Cmd+Shift+K only, and { modifiers: ["Meta"], key: "k" } on Cmd+K only. Meta is Cmd on macOS and Ctrl on other platforms.

Bindings are organized in layers. Higher layers take precedence:

Layer Constant Used by
0 LAYER.APP Host application defaults
1 LAYER.VIEW View-level bindings
2 LAYER.COMPONENT Component-level overrides

Set active: false on a binding to make it consume the key event without executing the handler (useful for suppressing a host binding without replacing it).

Views are registered in the manifest’s [frontend] section. The keys are view names your search() uses in ViewResponse.view. The values are JavaScript named exports from the launcher bundle:

manifest.toml
[frontend]
launcher-bundle = "frontend/dist/launcher.js"
[frontend.views]
grid = "EmojiGrid"
detail = "EmojiDetail"
[frontend.inline-views]
result = "CalculatorInline"

Your launcher entry point must export the matching names:

frontend/src/launcher.tsx
export { EmojiGrid } from "./views/EmojiGrid";
export { EmojiDetail } from "./views/EmojiDetail";
export { CalculatorInline } from "./views/CalculatorInline";