Files
SteamFramelyHomebrew--framely/docs/developer-guide/sdk.md
T

7.7 KiB
Raw Blame History

SDK API reference

简体中文

Development · Manifest · Lifecycle

Sources are sdk/src/index.tsx, sdk/src/lifecycle.ts and sdk/python/framely.py. Import page APIs from @framely/sdk; build copies the Python helper into payload/framely.py.

Register pages

import {registerPlugin, Section} from '@framely/sdk';
function QuickPage() {return <Section title="My plugin">Quick content</Section>;}
function MainWindow() {return <Section title="Main window">Details</Section>;}
registerPlugin({QuickPage, windows: {main: MainWindow}});

QuickPage is required. WindowPage is the default independent-window component; windows maps declared keys to different components. Missing keys fall back to WindowPage, then QuickPage. Avoid window key quick, which is used by the quick-page route. Pages run in host sandbox iframes, with the bridge installed before your bundle. Registration mounts React, base styles and scrollbars.

Host API: framely

API Parameters/result Purpose
framely.call<T>(method, params = {}) Promise<T> Call your backend; failures reject
framely.windows.open(key) Declared key; asynchronous result Open a declared window
framely.windows.close(key) Key; asynchronous result Close a window without disabling the backend
framely.notifications.send(notification) Notification; asynchronous result Send or replace by ID
framely.notifications.remove(id) Notification ID; asynchronous result Withdraw notification
framely.dependencies() Promise<DependencyStatus[]> Query declared dependency state
framely.language.get() Promise<{preference, language}> Language preference and effective language
framely.onEvent(callback) Unsubscribe function Receive forwarded events

These use the plugin bridge, not administrator APIs or other plugins' backends. Handle failures and prevent duplicate submissions while waiting.

try {
  const result = await framely.call<{text: string}>('settings.get');
  console.log(result.text);
} catch (error) {console.error(error);}

Notifications

await framely.notifications.send({
  id: 'task', title: 'Done', body: 'File processed', durationMs: 8000,
  actions: [{id: 'open', label: 'Open', icon: '↗'}],
});

id, title, body are required. Optional image accepts PNG/JPEG data URLs or HTTPS, up to a 1 MiB string. At most three actions, each with id, label, icon. durationMs is 1000–60000; omission defaults to 8000 ms. Notification/action IDs follow core ID rules; action IDs must be unique within a notification. Title is at most 160 bytes, body 4096, nonempty action label 80, and action icon 16. Each plugin may send at most ten notifications per ten seconds; the global queue permits 64 concurrent notifications. Updating an existing ID does not occupy another slot.

Buttons invoke backend notification.action with {id, action} and forward a page event of the same name. UI-only plugins can listen for the event. Reuse an ID to update; remove it to withdraw.

React hooks

Hook Result/behavior
useBackend<T>(method, params = {}) {data, error, loading, call}; requests are explicit through call(), which still throws on failure
usePluginEvent(callback) Automatically subscribes and unsubscribes on unmount
useDependencies() {items, error}; loads automatically and refreshes on dependencies.changed

Return the unsubscribe function from an effect:

React.useEffect(() => framely.onEvent(event => console.log(event)), []);

DependencyStatus includes id, required, constraint, version, enabled, matches, available, optional state.phase. Constraint is a range string or {version, source}; uninstalled versions are null. See relationships.

Language changes emit {type: "language.changed", data: {preference, language}}; dependency changes emit {type: "dependencies.changed"}. onEvent is typed as unknown, so narrow the type before reading fields. Python emit("progress", {...}) becomes {type: "progress", data: {...}}.

UI components

Component Main props
Section title, children
Button Standard React button props, including onClick, disabled
Toggle label, checked, onChange(boolean); optional description/disabled
Slider label, value, onChange(number); defaults min 0, max 100, step 1
TextField label, value, onChange(string); multiline/password/placeholder/disabled
Select label, {value,label}[] options, value/onChange/disabled
Tabs {id,label}[] tabs, value/onChange
Notice children; error styling with error=true

Select is single-valued by default. With multiple, value and callback use string arrays; emptyLabel and clearLabel customize labels. Menus render inside the page for off-screen CEF.

Native input controls automatically use the SteamVR keyboard on Frame, and standard interactive controls get hover feedback. Custom controls need suitable ARIA roles and may use data-framely-interactive. Pages do not independently control other views, controllers or system accounts.

Python backend helper

from framely import serve, emit

def dispatch(method, params):
    if method == 'ping':
        emit('progress', {'percent': 100})
        return {'ok': True}
    raise ValueError('Unknown method')

serve(dispatch)

serve(dispatch, lifecycle=None) reads newline-delimited requests and translates returns/exceptions into responses. emit(event, data) emits events forwarded as {type, data}; notification can send notifications. Log to stderr; stdout is protocol-only. See lifecycle callbacks.

TypeScript lifecycle helpers

Import registerLifecycle, LifecycleContext, LifecyclePhase, LifecycleCallbacks from @framely/sdk/lifecycle. Registration returns an asynchronous (method, context) dispatcher for reserved lifecycle methods. It does not provide stdin/stdout transport, launch a backend or grant page privileges.

Preview and privilege boundaries

Development preview simulates windows, notifications and some bridge operations. Test backend calls, real dependencies, VR inputs, haptics and runtime identity on the device. Pages cannot directly call management APIs or reserved framely.lifecycle.* methods. Runtime identity is configured in Manifest, not granted by the SDK.

Headset visibility and capture pause

framely.ui.getVisibility() returns { known, captureObscured, pageVisible, sequence, sessionId }. framely.ui.onVisibilityChanged(callback, onError?) subscribes and immediately queries the current snapshot; it returns an unsubscribe function. useVisibility() provides the same fields and error. Capture visibility combines the actual visibility of the quick panel, manager and every plugin window, excluding notifications and dock icons. Remote browser pages have pageVisible: false and cannot report headset visibility.

Declare backend.uiVisibilityEvents: true to receive the reserved JSON-line RPC framely.ui.visibility at startup and on changes, independently of mounted pages. Reply with the request id. The backend snapshot includes known, captureObscured, sequence and views; pageVisible is frontend-only. A native heartbeat missing for more than two seconds makes known false. Capture backends should pause on !known || captureObscured. Existing backends do not receive this RPC without opting in. Node/TS can use registerVisibility from @framely/sdk/visibility; Python supports serve(dispatch, visibility=callback).