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.
Component contract
Section titled “Component contract”GadgetViewProps
Section titled “GadgetViewProps”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). |
InlineViewProps
Section titled “InlineViewProps”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). |
Context hooks
Section titled “Context hooks”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.
useGadgetInfo
Section titled “useGadgetInfo”function useGadgetInfo(): { id: string; enabled: boolean }Returns the gadget’s ID and enabled state. The ID matches the [gadget] id from
the manifest.
useGadgetRuntime
Section titled “useGadgetRuntime”function useGadgetRuntime(): { sendMessage: GadgetSendMessage; logger: Logger;}Provides the messaging interface for frontend-to-backend RPC and a logger for the gadget.
useLauncher
Section titled “useLauncher”function useLauncher(): LauncherActionsReturns 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). |
Running entry actions
Section titled “Running entry actions”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().
Reporting errors
Section titled “Reporting errors”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).
Footer management
Section titled “Footer management”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).
Utilities
Section titled “Utilities”highlightText
Section titled “highlightText”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.
useWindowedList
Section titled “useWindowedList”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.
Keybindings
Section titled “Keybindings”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).
Manifest registration
Section titled “Manifest registration”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:
[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:
export { EmojiGrid } from "./views/EmojiGrid";export { EmojiDetail } from "./views/EmojiDetail";export { CalculatorInline } from "./views/CalculatorInline";