f2a58f3a91
apple / swift (push) Successful in 1m15s
apple / screenshots (push) Successful in 4m37s
windows-host / package (push) Successful in 8m57s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 58s
ci / bench (push) Successful in 5m51s
decky / build-publish (push) Successful in 26s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 43s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 16s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m5s
arch / build-publish (push) Successful in 16m51s
android / android (push) Successful in 17m6s
deb / build-publish (push) Successful in 17m13s
ci / rust (push) Successful in 18m41s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m31s
docker / deploy-docs (push) Successful in 10s
External game-library providers become first-class (RFC §8): a plugin
computes its desired title list and PUTs it — the host owns the diff.
- CustomEntry gains `provider` + `external_id` (API-set only; never on
manual entries). GameEntry surfaces `provider` for console attribution
and the new `GET /library?provider=` filter.
- PUT /api/v1/library/provider/{p}: atomic declarative reconcile keyed
on the provider's `external_id` — host ids stay stable across syncs,
orphans drop, manual entries and other providers are never touched,
an empty array clears the set. Validated: provider id [a-z0-9._-]
(`manual` reserved), unique non-empty external_ids.
- DELETE /api/v1/library/provider/{p}: clean uninstall, returns the
removed count.
- Ownership is unambiguous both ways: manual CRUD now returns 409 for a
provider-owned entry (MutateOutcome::ProviderOwned) instead of letting
an edit be silently clobbered at the next sync.
- library.changed now carries the mutating source (`manual` or the
provider id) — hooks and the SDK filter on it.
- Spec + SDK schemas regenerated; sdk/examples/provider-sync.ts is the
provider-plugin skeleton.
347 host tests green (pure reconcile: stable ids, orphan drop,
idempotence, bystanders untouched; name/payload validation; route 400s)
+ 11 SDK tests. Live-verified end to end THROUGH the SDK against a real
host: sync → filtered list → manual-delete 409 → re-sync with stable id
+ orphan drop → uninstall (removed=2), with three
library.changed(source=romm) events observed on the live stream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1685 lines
155 KiB
TypeScript
1685 lines
155 KiB
TypeScript
/**
|
||
* Generated by orval v8.22.0 🍺
|
||
* Do not edit manually.
|
||
* punktfunk management API
|
||
* Control-plane API for managing a punktfunk streaming host: host capabilities, runtime status, paired clients, the pairing PIN flow, and session control. Authentication: HTTP bearer token, enforced on every route except `/api/v1/health` when the host is started with a management token (mandatory for non-loopback binds).
|
||
* OpenAPI spec version: 0.12.0
|
||
*/
|
||
import {
|
||
Schema as S
|
||
} from 'effect';
|
||
|
||
/**
|
||
* @summary List paired clients
|
||
*/
|
||
export const ListPairedClientsResponseItem = S.Struct({
|
||
"fingerprint": S.String.annotations({ description: 'Lowercase hex SHA-256 of the client certificate DER — the client\'s stable id here.' }),
|
||
"not_after_unix": S.optional(S.NullOr(S.Number)).annotations({ description: 'Certificate validity end (unix seconds).' }),
|
||
"not_before_unix": S.optional(S.NullOr(S.Number)).annotations({ description: 'Certificate validity start (unix seconds).' }),
|
||
"subject": S.optional(S.NullOr(S.String)).annotations({ description: 'Certificate subject (e.g. `CN=NVIDIA GameStream Client`), if the DER parses.' })
|
||
}).annotations({ description: 'A paired (certificate-pinned) Moonlight client.' })
|
||
export const ListPairedClientsResponse = S.Array(ListPairedClientsResponseItem)
|
||
|
||
|
||
/**
|
||
* Removes the client's certificate from the pairing store. Caveat: the nvhttp TLS layer
|
||
* does not yet reject unlisted certificates (`gamestream/tls.rs` accepts any well-formed
|
||
* client cert — a planned hardening step), so until that lands this removes the client
|
||
* from the listing without severing its ability to reconnect.
|
||
* @summary Unpair a client
|
||
*/
|
||
export const UnpairClientParams = S.Struct({
|
||
"fingerprint": S.String.annotations({ description: 'Hex SHA-256 fingerprint of the client certificate DER (64 chars, case-insensitive)' })
|
||
})
|
||
|
||
|
||
/**
|
||
* Lists every backend the host knows how to drive, flags which are usable right now, and marks
|
||
* the one an unspecified (`Auto`) client request resolves to. Clients pass an `id` to their
|
||
* `--compositor` flag (or `PUNKTFUNK_COMPOSITOR_*` over the C ABI) to request it.
|
||
* @summary Available compositor backends
|
||
*/
|
||
export const ListCompositorsResponseItem = S.Struct({
|
||
"available": S.Boolean.annotations({ description: 'Usable on this host right now: the live session\'s own compositor, or gamescope wherever\nits binary is installed.' }),
|
||
"default": S.Boolean.annotations({ description: 'True for the backend an `Auto` (unspecified) request resolves to right now.' }),
|
||
"id": S.String.annotations({ description: 'Stable identifier (`\"kwin\"` | `\"wlroots\"` | `\"mutter\"` | `\"gamescope\"`) — pass this to a\nclient\'s `--compositor` flag.' }),
|
||
"label": S.String.annotations({ description: 'Human-readable label for UIs.' })
|
||
}).annotations({ description: 'A compositor backend the host can drive a virtual output on, and whether it\'s usable now.' })
|
||
export const ListCompositorsResponse = S.Array(ListCompositorsResponseItem)
|
||
|
||
|
||
/**
|
||
* Set the **manual** desktop arrangement — per-identity-slot `(x, y)` offsets so a multi-monitor
|
||
* group (§6A/§6B) comes back where the operator placed it. Persisted into the policy's layout block
|
||
* and switched to manual mode; applied from the next connect (a live group re-applies on its next
|
||
* acquire). Locks in the current effective behavior as explicit fields, so arranging displays never
|
||
* silently changes keep-alive/topology/conflict/identity. See `design/display-management.md` §6.2.
|
||
* @summary Arrange virtual displays
|
||
*/
|
||
export const SetDisplayLayoutBody = S.Struct({
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })).annotations({ description: '`{\"<identity_slot>\": {\"x\": …, \"y\": …}}` — where each arranged display\'s top-left sits.' })
|
||
}).annotations({ description: 'Request body for `setDisplayLayout`: per-identity-slot desktop offsets, keyed by the identity-slot\nid as a string (the same id `\/display\/state` reports as `identity_slot`).' })
|
||
|
||
export const setDisplayLayoutResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const setDisplayLayoutResponseCustomPresetsItemFieldsMaxDisplaysMin = 0;
|
||
|
||
export const setDisplayLayoutResponseEffectiveKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const setDisplayLayoutResponseEffectiveMaxDisplaysMin = 0;
|
||
|
||
export const setDisplayLayoutResponsePresetsItemFieldsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const setDisplayLayoutResponsePresetsItemFieldsMaxDisplaysMin = 0;
|
||
|
||
export const setDisplayLayoutResponseSettingsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const setDisplayLayoutResponseSettingsMaxDisplaysMin = 0;
|
||
|
||
export const setDisplayLayoutResponseSettingsVersionMin = 0;
|
||
|
||
|
||
|
||
export const SetDisplayLayoutResponse = S.Struct({
|
||
"configured": S.Boolean.annotations({ description: 'True once a `display-settings.json` exists (the console has configured this host).' }),
|
||
"custom_presets": S.Array(S.Struct({
|
||
"fields": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseCustomPresetsItemFieldsMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The six display-behavior axes this preset applies (the same shape a built-in preset expands to).' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator\'s \*full\* setup, so — unlike a built-in preset — applying\none does set this axis.' }),
|
||
"id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }),
|
||
"name": S.String.annotations({ description: 'User-facing name shown on the preset card; editable.' })
|
||
}).annotations({ description: 'A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are \*\*data\*\* — a catalog stored in\n`<config>\/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT \/display\/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).' })).annotations({ description: 'The operator\'s saved custom presets (`display-presets.json`) — named field-bundles rendered\nalongside the built-ins. Managed via `POST\/PUT\/DELETE \/display\/presets`; applied by writing a\n`Custom` policy carrying the preset\'s fields.' }),
|
||
"effective": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseEffectiveKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseEffectiveMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The effective (preset-expanded) policy currently in force.' }),
|
||
"enforced": S.Array(S.String).annotations({ description: 'Option names this build enforces right now. All five axes are now acted on (keep_alive +\ntopology since Stage 0-2, identity Stage 3, mode_conflict Stage 4, layout Stage 5) — the console\nreads this to know which controls are live vs. \"coming soon\" (per-backend nuance, e.g. layout\nposition apply being KWin-only, is reported per display in `\/display\/state`).' }),
|
||
"presets": S.Array(S.Struct({
|
||
"fields": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponsePresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponsePresetsItemFieldsMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The effective policy this preset expands to (the same fields a `custom` policy carries).' }),
|
||
"id": S.String.annotations({ description: 'The preset id (`default` | `gaming-rig` | `shared-desktop` | `hotdesk` | `workstation`).' }),
|
||
"summary": S.String.annotations({ description: 'One-line story shown next to the option.' })
|
||
}).annotations({ description: 'One preset\'s human-facing description + the fields it expands to, so the console can render a\npreset picker with an accurate \"what this does\" preview without hardcoding the expansion.' })).annotations({ description: 'Every named preset and what it expands to (for the picker\'s preview).' }),
|
||
"settings": S.Struct({
|
||
"ddc_power_off": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): command physical monitors\' panels off over DDC\/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan \/ DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC\/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched.' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a game-launching session is served (`design\/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`\/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched.' }),
|
||
"identity": S.optional(S.Literal('shared', 'per-client', 'per-client-mode')).annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.optional(S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseSettingsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' }))).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.optional(S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
})).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseSettingsMaxDisplaysMin))).annotations({ description: 'Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).' }),
|
||
"mode_conflict": S.optional(S.Literal('separate', 'steal', 'join', 'reject')).annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"pnp_disable_monitors": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): DISABLE physical monitors\' PnP device nodes for the stream\'s\nduration (persistently, so a standby monitor\/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan \/\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode\'s wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off.' }),
|
||
"preset": S.optional(S.Literal('custom', 'default', 'gaming-rig', 'shared-desktop', 'hotdesk', 'workstation')).annotations({ description: 'A named bundle of the fields below. `Custom` (the default) means the explicit fields rule; any\nother preset ignores the stored fields and expands to its own ([`DisplayPolicy::effective`]).' }),
|
||
"topology": S.optional(S.Literal('auto', 'extend', 'primary', 'exclusive')).annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }),
|
||
"version": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseSettingsVersionMin))).annotations({ description: 'Schema version (currently 1) — lets a future field addition migrate rather than reject.' })
|
||
}).annotations({ description: 'The stored policy (preset + custom fields), or the built-in default when unconfigured.' })
|
||
}).annotations({ description: 'Full display-management state for the console: the stored policy, every preset\'s expansion, the\nresolved effective policy, and which options this build actually enforces yet (Stage 0 wires\nkeep-alive linger + topology; the rest are stored but not yet acted on).' })
|
||
|
||
|
||
/**
|
||
* The operator's named field-bundles (`display-presets.json`). These also ride the
|
||
* `GET /display/settings` response (`custom_presets`), so the console rarely needs this directly.
|
||
* @summary List the saved custom presets
|
||
*/
|
||
export const listCustomPresetsResponseFieldsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const listCustomPresetsResponseFieldsMaxDisplaysMin = 0;
|
||
|
||
|
||
|
||
export const ListCustomPresetsResponseItem = S.Struct({
|
||
"fields": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(listCustomPresetsResponseFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(listCustomPresetsResponseFieldsMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The six display-behavior axes this preset applies (the same shape a built-in preset expands to).' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator\'s \*full\* setup, so — unlike a built-in preset — applying\none does set this axis.' }),
|
||
"id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }),
|
||
"name": S.String.annotations({ description: 'User-facing name shown on the preset card; editable.' })
|
||
}).annotations({ description: 'A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are \*\*data\*\* — a catalog stored in\n`<config>\/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT \/display\/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).' })
|
||
export const ListCustomPresetsResponse = S.Array(ListCustomPresetsResponseItem)
|
||
|
||
|
||
/**
|
||
* Stores a named bundle of the display-behavior axes (+ the game-session axis) the operator can
|
||
* apply later. The host assigns a stable id, returned in the body. Applying a preset is a
|
||
* `PUT /display/settings` with a `Custom` policy carrying its `fields` — no separate apply route.
|
||
* @summary Save a custom preset
|
||
*/
|
||
export const createCustomPresetBodyFieldsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const createCustomPresetBodyFieldsMaxDisplaysMin = 0;
|
||
|
||
|
||
|
||
export const CreateCustomPresetBody = S.Struct({
|
||
"fields": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(createCustomPresetBodyFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(createCustomPresetBodyFieldsMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The six resolved fields after preset expansion — what the lifecycle\/registry and the Stage-0 call\nsites read, and what the mgmt API echoes as the \"currently in force\" policy. Pure output of\n[`DisplayPolicy::effective`].' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a session that \*\*launches a game\*\* (a library id on the Hello \/ apps.json \/ Decky pin) is\nserved (`design\/gamemode-and-dedicated-sessions.md` §5.2). Orthogonal to the preset\/lifecycle axes\n— a top-level [`DisplayPolicy`] field, NOT part of [`EffectivePolicy`], so a preset never clobbers\nit. Linux-only in effect (a launching Windows session opens into the one desktop).' }),
|
||
"name": S.String
|
||
}).annotations({ description: 'Request body to create or replace a custom preset (no `id` — the host owns it).' })
|
||
|
||
|
||
/**
|
||
* @summary Update a custom preset
|
||
*/
|
||
export const UpdateCustomPresetParams = S.Struct({
|
||
"id": S.String.annotations({ description: 'The custom preset id' })
|
||
})
|
||
|
||
export const updateCustomPresetBodyFieldsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const updateCustomPresetBodyFieldsMaxDisplaysMin = 0;
|
||
|
||
|
||
|
||
export const UpdateCustomPresetBody = S.Struct({
|
||
"fields": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(updateCustomPresetBodyFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(updateCustomPresetBodyFieldsMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The six resolved fields after preset expansion — what the lifecycle\/registry and the Stage-0 call\nsites read, and what the mgmt API echoes as the \"currently in force\" policy. Pure output of\n[`DisplayPolicy::effective`].' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a session that \*\*launches a game\*\* (a library id on the Hello \/ apps.json \/ Decky pin) is\nserved (`design\/gamemode-and-dedicated-sessions.md` §5.2). Orthogonal to the preset\/lifecycle axes\n— a top-level [`DisplayPolicy`] field, NOT part of [`EffectivePolicy`], so a preset never clobbers\nit. Linux-only in effect (a launching Windows session opens into the one desktop).' }),
|
||
"name": S.String
|
||
}).annotations({ description: 'Request body to create or replace a custom preset (no `id` — the host owns it).' })
|
||
|
||
export const updateCustomPresetResponseFieldsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const updateCustomPresetResponseFieldsMaxDisplaysMin = 0;
|
||
|
||
|
||
|
||
export const UpdateCustomPresetResponse = S.Struct({
|
||
"fields": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(updateCustomPresetResponseFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(updateCustomPresetResponseFieldsMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The six display-behavior axes this preset applies (the same shape a built-in preset expands to).' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator\'s \*full\* setup, so — unlike a built-in preset — applying\none does set this axis.' }),
|
||
"id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }),
|
||
"name": S.String.annotations({ description: 'User-facing name shown on the preset card; editable.' })
|
||
}).annotations({ description: 'A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are \*\*data\*\* — a catalog stored in\n`<config>\/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT \/display\/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).' })
|
||
|
||
|
||
/**
|
||
* Removes it from the catalog. The active policy is untouched — if this preset was the one applied,
|
||
* the running behavior stays exactly as it was (the catalog and `display-settings.json` are decoupled).
|
||
* @summary Delete a custom preset
|
||
*/
|
||
export const DeleteCustomPresetParams = S.Struct({
|
||
"id": S.String.annotations({ description: 'The custom preset id' })
|
||
})
|
||
|
||
|
||
/**
|
||
* Tear down lingering/pinned displays now — so a physical-screen user gets their screen back
|
||
* without waiting out the linger. `slot` releases one; omit it to release all kept displays.
|
||
* Active (streaming) displays are never torn down here (that is session control).
|
||
* @summary Release kept virtual displays
|
||
*/
|
||
export const releaseDisplayBodySlotMin = 0;
|
||
|
||
|
||
|
||
export const ReleaseDisplayBody = S.Struct({
|
||
"slot": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(releaseDisplayBodySlotMin)))).annotations({ description: 'Slot to release (see `state`); omit to release \*\*all\*\* kept displays.' })
|
||
}).annotations({ description: 'Request body for `releaseDisplay`.' })
|
||
|
||
export const releaseDisplayResponseReleasedMin = 0;
|
||
|
||
|
||
|
||
export const ReleaseDisplayResponse = S.Struct({
|
||
"released": S.Number.pipe(S.greaterThanOrEqualTo(releaseDisplayResponseReleasedMin)).annotations({ description: 'Number of kept displays torn down.' })
|
||
}).annotations({ description: 'Result of a `\/display\/release`.' })
|
||
|
||
|
||
/**
|
||
* The stored virtual-display policy (lifecycle, topology, conflict handling, identity, layout),
|
||
* every preset's expansion, and which options this build enforces yet. See
|
||
* `design/display-management.md`.
|
||
* @summary Display-management policy
|
||
*/
|
||
export const getDisplaySettingsResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const getDisplaySettingsResponseCustomPresetsItemFieldsMaxDisplaysMin = 0;
|
||
|
||
export const getDisplaySettingsResponseEffectiveKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const getDisplaySettingsResponseEffectiveMaxDisplaysMin = 0;
|
||
|
||
export const getDisplaySettingsResponsePresetsItemFieldsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const getDisplaySettingsResponsePresetsItemFieldsMaxDisplaysMin = 0;
|
||
|
||
export const getDisplaySettingsResponseSettingsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const getDisplaySettingsResponseSettingsMaxDisplaysMin = 0;
|
||
|
||
export const getDisplaySettingsResponseSettingsVersionMin = 0;
|
||
|
||
|
||
|
||
export const GetDisplaySettingsResponse = S.Struct({
|
||
"configured": S.Boolean.annotations({ description: 'True once a `display-settings.json` exists (the console has configured this host).' }),
|
||
"custom_presets": S.Array(S.Struct({
|
||
"fields": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseCustomPresetsItemFieldsMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The six display-behavior axes this preset applies (the same shape a built-in preset expands to).' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator\'s \*full\* setup, so — unlike a built-in preset — applying\none does set this axis.' }),
|
||
"id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }),
|
||
"name": S.String.annotations({ description: 'User-facing name shown on the preset card; editable.' })
|
||
}).annotations({ description: 'A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are \*\*data\*\* — a catalog stored in\n`<config>\/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT \/display\/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).' })).annotations({ description: 'The operator\'s saved custom presets (`display-presets.json`) — named field-bundles rendered\nalongside the built-ins. Managed via `POST\/PUT\/DELETE \/display\/presets`; applied by writing a\n`Custom` policy carrying the preset\'s fields.' }),
|
||
"effective": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseEffectiveKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseEffectiveMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The effective (preset-expanded) policy currently in force.' }),
|
||
"enforced": S.Array(S.String).annotations({ description: 'Option names this build enforces right now. All five axes are now acted on (keep_alive +\ntopology since Stage 0-2, identity Stage 3, mode_conflict Stage 4, layout Stage 5) — the console\nreads this to know which controls are live vs. \"coming soon\" (per-backend nuance, e.g. layout\nposition apply being KWin-only, is reported per display in `\/display\/state`).' }),
|
||
"presets": S.Array(S.Struct({
|
||
"fields": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponsePresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponsePresetsItemFieldsMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The effective policy this preset expands to (the same fields a `custom` policy carries).' }),
|
||
"id": S.String.annotations({ description: 'The preset id (`default` | `gaming-rig` | `shared-desktop` | `hotdesk` | `workstation`).' }),
|
||
"summary": S.String.annotations({ description: 'One-line story shown next to the option.' })
|
||
}).annotations({ description: 'One preset\'s human-facing description + the fields it expands to, so the console can render a\npreset picker with an accurate \"what this does\" preview without hardcoding the expansion.' })).annotations({ description: 'Every named preset and what it expands to (for the picker\'s preview).' }),
|
||
"settings": S.Struct({
|
||
"ddc_power_off": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): command physical monitors\' panels off over DDC\/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan \/ DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC\/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched.' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a game-launching session is served (`design\/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`\/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched.' }),
|
||
"identity": S.optional(S.Literal('shared', 'per-client', 'per-client-mode')).annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.optional(S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseSettingsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' }))).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.optional(S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
})).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseSettingsMaxDisplaysMin))).annotations({ description: 'Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).' }),
|
||
"mode_conflict": S.optional(S.Literal('separate', 'steal', 'join', 'reject')).annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"pnp_disable_monitors": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): DISABLE physical monitors\' PnP device nodes for the stream\'s\nduration (persistently, so a standby monitor\/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan \/\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode\'s wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off.' }),
|
||
"preset": S.optional(S.Literal('custom', 'default', 'gaming-rig', 'shared-desktop', 'hotdesk', 'workstation')).annotations({ description: 'A named bundle of the fields below. `Custom` (the default) means the explicit fields rule; any\nother preset ignores the stored fields and expands to its own ([`DisplayPolicy::effective`]).' }),
|
||
"topology": S.optional(S.Literal('auto', 'extend', 'primary', 'exclusive')).annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }),
|
||
"version": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseSettingsVersionMin))).annotations({ description: 'Schema version (currently 1) — lets a future field addition migrate rather than reject.' })
|
||
}).annotations({ description: 'The stored policy (preset + custom fields), or the built-in default when unconfigured.' })
|
||
}).annotations({ description: 'Full display-management state for the console: the stored policy, every preset\'s expansion, the\nresolved effective policy, and which options this build actually enforces yet (Stage 0 wires\nkeep-alive linger + topology; the rest are stored but not yet acted on).' })
|
||
|
||
|
||
/**
|
||
* Persists a new policy (validated + clamped) and applies it from the next connect/teardown — a
|
||
* running session keeps the display it opened on. `keep_alive: forever` (the gaming-rig preset) is
|
||
* honored (the display is Pinned; free it via `POST /display/release`).
|
||
* @summary Set the display-management policy
|
||
*/
|
||
export const setDisplaySettingsBodyKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const setDisplaySettingsBodyMaxDisplaysMin = 0;
|
||
|
||
export const setDisplaySettingsBodyVersionMin = 0;
|
||
|
||
|
||
|
||
export const SetDisplaySettingsBody = S.Struct({
|
||
"ddc_power_off": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): command physical monitors\' panels off over DDC\/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan \/ DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC\/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched.' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a game-launching session is served (`design\/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`\/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched.' }),
|
||
"identity": S.optional(S.Literal('shared', 'per-client', 'per-client-mode')).annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.optional(S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsBodyKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' }))).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.optional(S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
})).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsBodyMaxDisplaysMin))).annotations({ description: 'Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).' }),
|
||
"mode_conflict": S.optional(S.Literal('separate', 'steal', 'join', 'reject')).annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"pnp_disable_monitors": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): DISABLE physical monitors\' PnP device nodes for the stream\'s\nduration (persistently, so a standby monitor\/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan \/\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode\'s wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off.' }),
|
||
"preset": S.optional(S.Literal('custom', 'default', 'gaming-rig', 'shared-desktop', 'hotdesk', 'workstation')).annotations({ description: 'A named bundle of the fields below. `Custom` (the default) means the explicit fields rule; any\nother preset ignores the stored fields and expands to its own ([`DisplayPolicy::effective`]).' }),
|
||
"topology": S.optional(S.Literal('auto', 'extend', 'primary', 'exclusive')).annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }),
|
||
"version": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsBodyVersionMin))).annotations({ description: 'Schema version (currently 1) — lets a future field addition migrate rather than reject.' })
|
||
}).annotations({ description: 'The user-facing display-management policy — what `display-settings.json` holds and what the mgmt\nAPI GETs\/PUTs. When [`preset`](Self::preset) is not [`Preset::Custom`] the explicit fields are\nignored (the console writes one or the other); [`effective`](Self::effective) resolves both to a\nsingle [`EffectivePolicy`].' })
|
||
|
||
export const setDisplaySettingsResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const setDisplaySettingsResponseCustomPresetsItemFieldsMaxDisplaysMin = 0;
|
||
|
||
export const setDisplaySettingsResponseEffectiveKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const setDisplaySettingsResponseEffectiveMaxDisplaysMin = 0;
|
||
|
||
export const setDisplaySettingsResponsePresetsItemFieldsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const setDisplaySettingsResponsePresetsItemFieldsMaxDisplaysMin = 0;
|
||
|
||
export const setDisplaySettingsResponseSettingsKeepAliveTwoSecondsMin = 0;
|
||
|
||
export const setDisplaySettingsResponseSettingsMaxDisplaysMin = 0;
|
||
|
||
export const setDisplaySettingsResponseSettingsVersionMin = 0;
|
||
|
||
|
||
|
||
export const SetDisplaySettingsResponse = S.Struct({
|
||
"configured": S.Boolean.annotations({ description: 'True once a `display-settings.json` exists (the console has configured this host).' }),
|
||
"custom_presets": S.Array(S.Struct({
|
||
"fields": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseCustomPresetsItemFieldsMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The six display-behavior axes this preset applies (the same shape a built-in preset expands to).' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator\'s \*full\* setup, so — unlike a built-in preset — applying\none does set this axis.' }),
|
||
"id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }),
|
||
"name": S.String.annotations({ description: 'User-facing name shown on the preset card; editable.' })
|
||
}).annotations({ description: 'A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are \*\*data\*\* — a catalog stored in\n`<config>\/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT \/display\/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).' })).annotations({ description: 'The operator\'s saved custom presets (`display-presets.json`) — named field-bundles rendered\nalongside the built-ins. Managed via `POST\/PUT\/DELETE \/display\/presets`; applied by writing a\n`Custom` policy carrying the preset\'s fields.' }),
|
||
"effective": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseEffectiveKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseEffectiveMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The effective (preset-expanded) policy currently in force.' }),
|
||
"enforced": S.Array(S.String).annotations({ description: 'Option names this build enforces right now. All five axes are now acted on (keep_alive +\ntopology since Stage 0-2, identity Stage 3, mode_conflict Stage 4, layout Stage 5) — the console\nreads this to know which controls are live vs. \"coming soon\" (per-backend nuance, e.g. layout\nposition apply being KWin-only, is reported per display in `\/display\/state`).' }),
|
||
"presets": S.Array(S.Struct({
|
||
"fields": S.Struct({
|
||
"identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponsePresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponsePresetsItemFieldsMaxDisplaysMin)),
|
||
"mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' })
|
||
}).annotations({ description: 'The effective policy this preset expands to (the same fields a `custom` policy carries).' }),
|
||
"id": S.String.annotations({ description: 'The preset id (`default` | `gaming-rig` | `shared-desktop` | `hotdesk` | `workstation`).' }),
|
||
"summary": S.String.annotations({ description: 'One-line story shown next to the option.' })
|
||
}).annotations({ description: 'One preset\'s human-facing description + the fields it expands to, so the console can render a\npreset picker with an accurate \"what this does\" preview without hardcoding the expansion.' })).annotations({ description: 'Every named preset and what it expands to (for the picker\'s preview).' }),
|
||
"settings": S.Struct({
|
||
"ddc_power_off": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): command physical monitors\' panels off over DDC\/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan \/ DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC\/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched.' }),
|
||
"game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a game-launching session is served (`design\/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`\/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched.' }),
|
||
"identity": S.optional(S.Literal('shared', 'per-client', 'per-client-mode')).annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }),
|
||
"keep_alive": S.optional(S.Union(S.Struct({
|
||
"mode": S.Literal('off')
|
||
}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({
|
||
"mode": S.Literal('duration'),
|
||
"seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseSettingsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' })
|
||
}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({
|
||
"mode": S.Literal('forever')
|
||
}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' }))).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }),
|
||
"layout": S.optional(S.Struct({
|
||
"mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }),
|
||
"positions": S.optional(S.Record({ key: S.String, value: S.Struct({
|
||
"x": S.Number,
|
||
"y": S.Number
|
||
}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) }))
|
||
})).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }),
|
||
"max_displays": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseSettingsMaxDisplaysMin))).annotations({ description: 'Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).' }),
|
||
"mode_conflict": S.optional(S.Literal('separate', 'steal', 'join', 'reject')).annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }),
|
||
"pnp_disable_monitors": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): DISABLE physical monitors\' PnP device nodes for the stream\'s\nduration (persistently, so a standby monitor\/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan \/\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode\'s wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off.' }),
|
||
"preset": S.optional(S.Literal('custom', 'default', 'gaming-rig', 'shared-desktop', 'hotdesk', 'workstation')).annotations({ description: 'A named bundle of the fields below. `Custom` (the default) means the explicit fields rule; any\nother preset ignores the stored fields and expands to its own ([`DisplayPolicy::effective`]).' }),
|
||
"topology": S.optional(S.Literal('auto', 'extend', 'primary', 'exclusive')).annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }),
|
||
"version": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseSettingsVersionMin))).annotations({ description: 'Schema version (currently 1) — lets a future field addition migrate rather than reject.' })
|
||
}).annotations({ description: 'The stored policy (preset + custom fields), or the built-in default when unconfigured.' })
|
||
}).annotations({ description: 'Full display-management state for the console: the stored policy, every preset\'s expansion, the\nresolved effective policy, and which options this build actually enforces yet (Stage 0 wires\nkeep-alive linger + topology; the rest are stored but not yet acted on).' })
|
||
|
||
|
||
/**
|
||
* The host's managed virtual displays right now — active (streaming), lingering (kept after
|
||
* disconnect, counting down to teardown), or pinned (kept indefinitely). See
|
||
* `design/display-management.md`.
|
||
* @summary Live virtual displays
|
||
*/
|
||
export const getDisplayStateResponseDisplaysItemDisplayIndexMin = 0;
|
||
|
||
export const getDisplayStateResponseDisplaysItemExpiresInMsMin = 0;
|
||
|
||
export const getDisplayStateResponseDisplaysItemGroupMin = 0;
|
||
|
||
export const getDisplayStateResponseDisplaysItemIdentitySlotMin = 0;
|
||
|
||
export const getDisplayStateResponseDisplaysItemSessionsMin = 0;
|
||
|
||
export const getDisplayStateResponseDisplaysItemSlotMin = 0;
|
||
|
||
|
||
|
||
export const GetDisplayStateResponse = S.Struct({
|
||
"displays": S.Array(S.Struct({
|
||
"backend": S.String.annotations({ description: 'Backend name (`pf-vdisplay`, `kwin`, …).' }),
|
||
"client": S.optional(S.NullOr(S.String)).annotations({ description: 'Short client label, when the owner tracks it.' }),
|
||
"display_index": S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemDisplayIndexMin)).annotations({ description: 'This display\'s ordinal within its group, in acquire order (0-based).' }),
|
||
"expires_in_ms": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemExpiresInMsMin)))).annotations({ description: 'Milliseconds until a lingering display is torn down (absent when active\/pinned).' }),
|
||
"group": S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemGroupMin)).annotations({ description: 'Display group (shared desktop) id — several displays with the same group form one desktop (§6A).' }),
|
||
"identity_slot": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemIdentitySlotMin)))).annotations({ description: 'Stable per-client identity slot keying persistent config + manual layout (absent = shared\/anonymous).' }),
|
||
"mode": S.String.annotations({ description: '`WIDTHxHEIGHT@HZ`.' }),
|
||
"sessions": S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemSessionsMin)).annotations({ description: 'Live sessions holding the display.' }),
|
||
"slot": S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemSlotMin)).annotations({ description: 'Stable-enough id for the `\/display\/release` `slot` argument.' }),
|
||
"state": S.String.annotations({ description: '`active` | `lingering` | `pinned`.' }),
|
||
"topology": S.String.annotations({ description: 'Effective topology for this display\'s group (`extend` | `primary` | `exclusive`).' }),
|
||
"x": S.Number.annotations({ description: 'Desktop-space top-left `x` (auto-row or the console\'s manual arrangement, §6.2).' }),
|
||
"y": S.Number.annotations({ description: 'Desktop-space top-left `y`.' })
|
||
}).annotations({ description: 'One live or kept virtual display.' }))
|
||
}).annotations({ description: 'The host\'s managed virtual displays right now.' })
|
||
|
||
|
||
/**
|
||
* Server-Sent Events stream of the host's lifecycle events: client connect/disconnect, session
|
||
* and stream start/end, pairing decisions, display create/release, library changes, host
|
||
* start/stop — both protocol planes. Frames carry `id:` = the event's monotonic `seq`,
|
||
* `event:` = its kind, and `data:` = the event JSON (schema-versioned, additive-only).
|
||
*
|
||
* Resume: standard `Last-Event-ID` (or `?since=`) replays from the in-memory ring; a consumer
|
||
* that fell off the ring receives an `event: dropped` frame first and should resync via the
|
||
* REST snapshots. Keep-alive comments are sent every 15 s.
|
||
* @summary Stream host lifecycle events (SSE)
|
||
*/
|
||
export const streamEventsQuerySinceMin = 0;
|
||
|
||
|
||
|
||
export const StreamEventsQueryParams = S.Struct({
|
||
"since": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(streamEventsQuerySinceMin))).annotations({ description: 'Resume cursor: only events with `seq` greater than this are sent (the ring keeps the newest ~1024). `Last-Event-ID` takes precedence.' }),
|
||
"kinds": S.optional(S.String).annotations({ description: 'Comma-separated server-side kind filter: exact kinds (`pairing.pending`) or `domain.\*` prefixes (`stream.\*`).' })
|
||
})
|
||
|
||
export const streamEventsHeaderLastEventIDMin = 0;
|
||
|
||
|
||
|
||
export const StreamEventsHeader = S.Struct({
|
||
"Last-Event-ID": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(streamEventsHeaderLastEventIDMin)))).annotations({ description: 'SSE auto-reconnect cursor — the `id:` of the last received frame.' })
|
||
})
|
||
|
||
|
||
/**
|
||
* Lists the host's hardware GPUs, the persisted auto/manual preference, the GPU the next session
|
||
* will use (and why), and the GPU live sessions encode on right now.
|
||
* @summary GPU inventory and selection
|
||
*/
|
||
export const listGpusResponseActiveTwoSessionsMin = 0;
|
||
|
||
export const listGpusResponseGpusItemVramMbMin = 0;
|
||
|
||
|
||
|
||
export const ListGpusResponse = S.Struct({
|
||
"active": S.optional(S.Union(S.Null, S.Struct({
|
||
"backend": S.String.annotations({ description: 'The encode backend in use (`nvenc` | `amf` | `qsv` | `vaapi` | `software`).' }),
|
||
"id": S.String.annotations({ description: 'Stable id matching an entry of `gpus` (empty for the CPU\/software encoder).' }),
|
||
"name": S.String,
|
||
"sessions": S.Number.pipe(S.greaterThanOrEqualTo(listGpusResponseActiveTwoSessionsMin)).annotations({ description: 'Number of live encode sessions on it.' }),
|
||
"vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' })
|
||
}).annotations({ description: 'The GPU live sessions use right now (absent while nothing is streaming).' }))),
|
||
"env_override": S.optional(S.NullOr(S.String)).annotations({ description: '`PUNKTFUNK_RENDER_ADAPTER` (the host.env pin), when set — it applies while `mode` is\n`auto`; a manual preference overrides it.' }),
|
||
"gpus": S.Array(S.Struct({
|
||
"id": S.String.annotations({ description: 'Stable identifier (`vendorid-deviceid-occurrence`, hex PCI ids) — pass to `setGpuPreference`.\nStable across reboots and driver updates, unlike an adapter index or LUID.' }),
|
||
"name": S.String.annotations({ description: 'Adapter\/marketing name.' }),
|
||
"vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' }),
|
||
"vram_mb": S.Number.pipe(S.greaterThanOrEqualTo(listGpusResponseGpusItemVramMbMin)).annotations({ description: 'Dedicated VRAM in MiB (0 where the platform doesn\'t expose it).' })
|
||
}).annotations({ description: 'One hardware GPU on the host (software\/WARP adapters are never listed).' })).annotations({ description: 'The host\'s hardware GPUs.' }),
|
||
"mode": S.String.annotations({ description: '`auto` or `manual`.' }),
|
||
"preferred_available": S.Boolean.annotations({ description: 'Whether the preferred GPU is currently present.' }),
|
||
"preferred_id": S.optional(S.NullOr(S.String)).annotations({ description: 'The manually preferred GPU\'s stable id, when one is stored (kept while `mode` is `auto` so\na console can offer returning to it). May reference a GPU that is currently absent.' }),
|
||
"preferred_name": S.optional(S.NullOr(S.String)).annotations({ description: 'The stored name of the preferred GPU (a usable label even when it is absent).' }),
|
||
"selected": S.optional(S.Union(S.Null, S.Struct({
|
||
"id": S.String,
|
||
"name": S.String,
|
||
"source": S.String.annotations({ description: 'Why this GPU was selected: `preference` (the manual choice), `env`\n(`PUNKTFUNK_RENDER_ADAPTER`), `auto` (max dedicated VRAM \/ platform default), or\n`preference_missing` (a manual choice is set but that GPU is absent — auto-selected\ninstead so the host keeps streaming).' }),
|
||
"vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' })
|
||
}).annotations({ description: 'The GPU the next session will use.' })))
|
||
}).annotations({ description: 'Full GPU-selection state for the console: inventory, the persisted preference, what the next\nsession will use, and what is in use right now.' })
|
||
|
||
|
||
/**
|
||
* `auto` restores automatic selection (`PUNKTFUNK_RENDER_ADAPTER` pin, else max dedicated VRAM);
|
||
* `manual` pins capture + encode to the given GPU. Persisted across restarts; applies to the
|
||
* **next** session (a running session keeps its GPU). If the preferred GPU is absent at session
|
||
* start the host falls back to automatic selection rather than failing.
|
||
* @summary Set the GPU preference
|
||
*/
|
||
export const SetGpuPreferenceBody = S.Struct({
|
||
"gpu_id": S.optional(S.NullOr(S.String)).annotations({ description: 'Required when `mode` is `manual`: the stable `id` of a currently listed GPU\n(see `listGpus`).' }),
|
||
"mode": S.String.annotations({ description: '`auto` (env pin, else max dedicated VRAM — the default) or `manual`.' })
|
||
}).annotations({ description: 'Request body for `setGpuPreference`.' })
|
||
|
||
export const setGpuPreferenceResponseActiveTwoSessionsMin = 0;
|
||
|
||
export const setGpuPreferenceResponseGpusItemVramMbMin = 0;
|
||
|
||
|
||
|
||
export const SetGpuPreferenceResponse = S.Struct({
|
||
"active": S.optional(S.Union(S.Null, S.Struct({
|
||
"backend": S.String.annotations({ description: 'The encode backend in use (`nvenc` | `amf` | `qsv` | `vaapi` | `software`).' }),
|
||
"id": S.String.annotations({ description: 'Stable id matching an entry of `gpus` (empty for the CPU\/software encoder).' }),
|
||
"name": S.String,
|
||
"sessions": S.Number.pipe(S.greaterThanOrEqualTo(setGpuPreferenceResponseActiveTwoSessionsMin)).annotations({ description: 'Number of live encode sessions on it.' }),
|
||
"vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' })
|
||
}).annotations({ description: 'The GPU live sessions use right now (absent while nothing is streaming).' }))),
|
||
"env_override": S.optional(S.NullOr(S.String)).annotations({ description: '`PUNKTFUNK_RENDER_ADAPTER` (the host.env pin), when set — it applies while `mode` is\n`auto`; a manual preference overrides it.' }),
|
||
"gpus": S.Array(S.Struct({
|
||
"id": S.String.annotations({ description: 'Stable identifier (`vendorid-deviceid-occurrence`, hex PCI ids) — pass to `setGpuPreference`.\nStable across reboots and driver updates, unlike an adapter index or LUID.' }),
|
||
"name": S.String.annotations({ description: 'Adapter\/marketing name.' }),
|
||
"vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' }),
|
||
"vram_mb": S.Number.pipe(S.greaterThanOrEqualTo(setGpuPreferenceResponseGpusItemVramMbMin)).annotations({ description: 'Dedicated VRAM in MiB (0 where the platform doesn\'t expose it).' })
|
||
}).annotations({ description: 'One hardware GPU on the host (software\/WARP adapters are never listed).' })).annotations({ description: 'The host\'s hardware GPUs.' }),
|
||
"mode": S.String.annotations({ description: '`auto` or `manual`.' }),
|
||
"preferred_available": S.Boolean.annotations({ description: 'Whether the preferred GPU is currently present.' }),
|
||
"preferred_id": S.optional(S.NullOr(S.String)).annotations({ description: 'The manually preferred GPU\'s stable id, when one is stored (kept while `mode` is `auto` so\na console can offer returning to it). May reference a GPU that is currently absent.' }),
|
||
"preferred_name": S.optional(S.NullOr(S.String)).annotations({ description: 'The stored name of the preferred GPU (a usable label even when it is absent).' }),
|
||
"selected": S.optional(S.Union(S.Null, S.Struct({
|
||
"id": S.String,
|
||
"name": S.String,
|
||
"source": S.String.annotations({ description: 'Why this GPU was selected: `preference` (the manual choice), `env`\n(`PUNKTFUNK_RENDER_ADAPTER`), `auto` (max dedicated VRAM \/ platform default), or\n`preference_missing` (a manual choice is set but that GPU is absent — auto-selected\ninstead so the host keeps streaming).' }),
|
||
"vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' })
|
||
}).annotations({ description: 'The GPU the next session will use.' })))
|
||
}).annotations({ description: 'Full GPU-selection state for the console: inventory, the persisted preference, what the next\nsession will use, and what is in use right now.' })
|
||
|
||
|
||
/**
|
||
* Always available without authentication.
|
||
* @summary Liveness probe
|
||
*/
|
||
export const getHealthResponseAbiVersionMin = 0;
|
||
|
||
|
||
|
||
export const GetHealthResponse = S.Struct({
|
||
"abi_version": S.Number.pipe(S.greaterThanOrEqualTo(getHealthResponseAbiVersionMin)).annotations({ description: '`punktfunk-core` C ABI version.' }),
|
||
"status": S.String.annotations({ description: 'Always `\"ok\"` when the host responds.' }),
|
||
"version": S.String.annotations({ description: '`punktfunk-host` crate version.' })
|
||
}).annotations({ description: 'Liveness + version probe.' })
|
||
|
||
|
||
/**
|
||
* The operator's `hooks.json`: commands and webhooks fired on host lifecycle events. Empty
|
||
* when unconfigured.
|
||
* @summary Get the hook configuration
|
||
*/
|
||
export const getHooksResponseHooksItemDebounceMsMin = 0;
|
||
|
||
export const getHooksResponseHooksItemTimeoutSMin = 0;
|
||
|
||
|
||
|
||
export const GetHooksResponse = S.Struct({
|
||
"hooks": S.optional(S.Array(S.Struct({
|
||
"debounce_ms": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(getHooksResponseHooksItemDebounceMsMin))).annotations({ description: 'Minimum interval between firings of this hook, in milliseconds. 0 = fire every time.' }),
|
||
"filter": S.optional(S.Union(S.Null, S.Struct({
|
||
"app": S.optional(S.NullOr(S.String)).annotations({ description: 'Launched app id\/title (`stream.\*` events).' }),
|
||
"client": S.optional(S.NullOr(S.String)).annotations({ description: 'Client\/device name (for `session.\*`: the short client label the Dashboard shows).' }),
|
||
"fingerprint": S.optional(S.NullOr(S.String)).annotations({ description: 'Certificate fingerprint (hex, case-insensitive).' }),
|
||
"plane": S.optional(S.Union(S.Null, S.Literal('native', 'gamestream').annotations({ description: 'Protocol plane (`native` \/ `gamestream`).' })))
|
||
}).annotations({ description: 'Exact-match constraints on the event\'s fields; every present field must match.' }))),
|
||
"hmac_secret_file": S.optional(S.NullOr(S.String)).annotations({ description: 'File holding the webhook HMAC secret (`X-Punktfunk-Signature: sha256=<hex>`). The file\nshould be operator-owned and private; a world-readable secret is warned about.' }),
|
||
"on": S.String.annotations({ description: 'Which events fire this hook: an exact kind (`stream.started`) or a `domain.\*` prefix\n(`pairing.\*`) — the same vocabulary as the SSE `?kinds=` filter.' }),
|
||
"run": S.optional(S.NullOr(S.String)).annotations({ description: 'Shell command to execute (detached, event JSON on stdin + `PF_EVENT_\*` env).' }),
|
||
"timeout_s": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(getHooksResponseHooksItemTimeoutSMin))).annotations({ description: 'Exec timeout in seconds (1–600, default 30); the process group is killed on expiry.' }),
|
||
"webhook": S.optional(S.NullOr(S.String)).annotations({ description: 'URL to POST the event JSON to.' })
|
||
}).annotations({ description: 'One hook: fire `run` and\/or `webhook` when an event matching `on` (+ `filter`) occurs.' })))
|
||
}).annotations({ description: 'The operator\'s hook configuration — the `hooks.json` document and the `\/api\/v1\/hooks` body.' })
|
||
|
||
|
||
/**
|
||
* Validates and persists a full `hooks.json` document (this is a whole-document PUT, not a
|
||
* patch). Applies from the next event — no restart. Hook commands run as the host user
|
||
* (interactive user session on Windows): treat this configuration as operator-privileged.
|
||
* @summary Replace the hook configuration
|
||
*/
|
||
export const setHooksBodyHooksItemDebounceMsMin = 0;
|
||
|
||
export const setHooksBodyHooksItemTimeoutSMin = 0;
|
||
|
||
|
||
|
||
export const SetHooksBody = S.Struct({
|
||
"hooks": S.optional(S.Array(S.Struct({
|
||
"debounce_ms": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setHooksBodyHooksItemDebounceMsMin))).annotations({ description: 'Minimum interval between firings of this hook, in milliseconds. 0 = fire every time.' }),
|
||
"filter": S.optional(S.Union(S.Null, S.Struct({
|
||
"app": S.optional(S.NullOr(S.String)).annotations({ description: 'Launched app id\/title (`stream.\*` events).' }),
|
||
"client": S.optional(S.NullOr(S.String)).annotations({ description: 'Client\/device name (for `session.\*`: the short client label the Dashboard shows).' }),
|
||
"fingerprint": S.optional(S.NullOr(S.String)).annotations({ description: 'Certificate fingerprint (hex, case-insensitive).' }),
|
||
"plane": S.optional(S.Union(S.Null, S.Literal('native', 'gamestream').annotations({ description: 'Protocol plane (`native` \/ `gamestream`).' })))
|
||
}).annotations({ description: 'Exact-match constraints on the event\'s fields; every present field must match.' }))),
|
||
"hmac_secret_file": S.optional(S.NullOr(S.String)).annotations({ description: 'File holding the webhook HMAC secret (`X-Punktfunk-Signature: sha256=<hex>`). The file\nshould be operator-owned and private; a world-readable secret is warned about.' }),
|
||
"on": S.String.annotations({ description: 'Which events fire this hook: an exact kind (`stream.started`) or a `domain.\*` prefix\n(`pairing.\*`) — the same vocabulary as the SSE `?kinds=` filter.' }),
|
||
"run": S.optional(S.NullOr(S.String)).annotations({ description: 'Shell command to execute (detached, event JSON on stdin + `PF_EVENT_\*` env).' }),
|
||
"timeout_s": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setHooksBodyHooksItemTimeoutSMin))).annotations({ description: 'Exec timeout in seconds (1–600, default 30); the process group is killed on expiry.' }),
|
||
"webhook": S.optional(S.NullOr(S.String)).annotations({ description: 'URL to POST the event JSON to.' })
|
||
}).annotations({ description: 'One hook: fire `run` and\/or `webhook` when an event matching `on` (+ `filter`) occurs.' })))
|
||
}).annotations({ description: 'The operator\'s hook configuration — the `hooks.json` document and the `\/api\/v1\/hooks` body.' })
|
||
|
||
export const setHooksResponseHooksItemDebounceMsMin = 0;
|
||
|
||
export const setHooksResponseHooksItemTimeoutSMin = 0;
|
||
|
||
|
||
|
||
export const SetHooksResponse = S.Struct({
|
||
"hooks": S.optional(S.Array(S.Struct({
|
||
"debounce_ms": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setHooksResponseHooksItemDebounceMsMin))).annotations({ description: 'Minimum interval between firings of this hook, in milliseconds. 0 = fire every time.' }),
|
||
"filter": S.optional(S.Union(S.Null, S.Struct({
|
||
"app": S.optional(S.NullOr(S.String)).annotations({ description: 'Launched app id\/title (`stream.\*` events).' }),
|
||
"client": S.optional(S.NullOr(S.String)).annotations({ description: 'Client\/device name (for `session.\*`: the short client label the Dashboard shows).' }),
|
||
"fingerprint": S.optional(S.NullOr(S.String)).annotations({ description: 'Certificate fingerprint (hex, case-insensitive).' }),
|
||
"plane": S.optional(S.Union(S.Null, S.Literal('native', 'gamestream').annotations({ description: 'Protocol plane (`native` \/ `gamestream`).' })))
|
||
}).annotations({ description: 'Exact-match constraints on the event\'s fields; every present field must match.' }))),
|
||
"hmac_secret_file": S.optional(S.NullOr(S.String)).annotations({ description: 'File holding the webhook HMAC secret (`X-Punktfunk-Signature: sha256=<hex>`). The file\nshould be operator-owned and private; a world-readable secret is warned about.' }),
|
||
"on": S.String.annotations({ description: 'Which events fire this hook: an exact kind (`stream.started`) or a `domain.\*` prefix\n(`pairing.\*`) — the same vocabulary as the SSE `?kinds=` filter.' }),
|
||
"run": S.optional(S.NullOr(S.String)).annotations({ description: 'Shell command to execute (detached, event JSON on stdin + `PF_EVENT_\*` env).' }),
|
||
"timeout_s": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setHooksResponseHooksItemTimeoutSMin))).annotations({ description: 'Exec timeout in seconds (1–600, default 30); the process group is killed on expiry.' }),
|
||
"webhook": S.optional(S.NullOr(S.String)).annotations({ description: 'URL to POST the event JSON to.' })
|
||
}).annotations({ description: 'One hook: fire `run` and\/or `webhook` when an event matching `on` (+ `filter`) occurs.' })))
|
||
}).annotations({ description: 'The operator\'s hook configuration — the `hooks.json` document and the `\/api\/v1\/hooks` body.' })
|
||
|
||
|
||
/**
|
||
* @summary Host identity and capabilities
|
||
*/
|
||
export const getHostInfoResponseAbiVersionMin = 0;
|
||
|
||
export const getHostInfoResponsePortsAudioMin = 0;
|
||
|
||
export const getHostInfoResponsePortsControlMin = 0;
|
||
|
||
export const getHostInfoResponsePortsHttpMin = 0;
|
||
|
||
export const getHostInfoResponsePortsHttpsMin = 0;
|
||
|
||
export const getHostInfoResponsePortsMgmtMin = 0;
|
||
|
||
export const getHostInfoResponsePortsRtspMin = 0;
|
||
|
||
export const getHostInfoResponsePortsVideoMin = 0;
|
||
|
||
|
||
|
||
export const GetHostInfoResponse = S.Struct({
|
||
"abi_version": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponseAbiVersionMin)).annotations({ description: '`punktfunk-core` C ABI version.' }),
|
||
"app_version": S.String.annotations({ description: 'GameStream host version advertised to Moonlight clients.' }),
|
||
"codecs": S.Array(S.Literal('h264', 'hevc', 'av1', 'pyrowave').annotations({ description: 'Video codec identifier. The wire token matches the codec\'s canonical name used across the\nstack (SDP\/GameStream advertisement, the stats-capture `CaptureMeta.codec`, and the encoder\'s\n[`Codec::label`]) — notably `H.265` serializes as `\"hevc\"`, not `\"h265\"`, so the same codec\nreads identically on every console page.' })).annotations({ description: 'Codecs the host can encode (NVENC).' }),
|
||
"gamestream": S.Boolean.annotations({ description: 'Whether the GameStream\/Moonlight-compat planes are running (`--gamestream`). `false` on the\nsecure default (native punktfunk\/1 only) — a console can hide Moonlight-only UI (e.g. the\nMoonlight PIN pairing card, which could never receive a PIN when this is `false`).' }),
|
||
"gfe_version": S.String.annotations({ description: 'GFE version advertised to Moonlight clients.' }),
|
||
"hostname": S.String,
|
||
"local_ip": S.String.annotations({ description: 'Best-effort primary LAN IP.' }),
|
||
"ports": S.Struct({
|
||
"audio": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsAudioMin)),
|
||
"control": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsControlMin)),
|
||
"http": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsHttpMin)).annotations({ description: 'nvhttp plain HTTP (serverinfo, pairing).' }),
|
||
"https": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsHttpsMin)).annotations({ description: 'nvhttp mutual-TLS HTTPS (post-pairing).' }),
|
||
"mgmt": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsMgmtMin)).annotations({ description: 'This management API.' }),
|
||
"rtsp": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsRtspMin)),
|
||
"video": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsVideoMin))
|
||
}).annotations({ description: 'Every port a client integration may need (Moonlight derives the stream ports from the\nHTTP base; a control pane should not have to).' }),
|
||
"uniqueid": S.String.annotations({ description: 'Stable per-host id (persisted across restarts), matched on pairing.' }),
|
||
"version": S.String.annotations({ description: '`punktfunk-host` crate version.' })
|
||
}).annotations({ description: 'Host identity and advertised capabilities (static for the life of the process).' })
|
||
|
||
|
||
/**
|
||
* Every installed-store title (Steam, read from the host's local files — no Steam API key)
|
||
* merged with the user's custom entries, sorted by title. Artwork fields are URLs the client
|
||
* fetches directly (the public Steam CDN for Steam titles). `?provider=` narrows to the
|
||
* entries a given external provider owns.
|
||
* @summary List the game library
|
||
*/
|
||
export const GetLibraryQueryParams = S.Struct({
|
||
"provider": S.optional(S.String).annotations({ description: 'Only entries owned by this external provider' })
|
||
})
|
||
|
||
export const GetLibraryResponseItem = S.Struct({
|
||
"art": S.Struct({
|
||
"header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }),
|
||
"hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }),
|
||
"logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }),
|
||
"portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' })
|
||
}).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }),
|
||
"id": S.String.annotations({ description: 'Stable, store-qualified id: `steam:<appid>` or `custom:<id>`.' }),
|
||
"launch": S.optional(S.Union(S.Null, S.Struct({
|
||
"kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }),
|
||
"value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' })
|
||
}).annotations({ description: 'How the host would launch it, when known.' }))),
|
||
"provider": S.optional(S.NullOr(S.String)).annotations({ description: 'The external provider owning this entry (custom-store entries synced by a provider\nplugin, RFC §8) — `None` for installed-store titles and manual custom entries. The\nconsole uses it for attribution; `GET \/library?provider=` filters on it.' }),
|
||
"store": S.String.annotations({ description: 'Which store surfaced it: `\"steam\"` or `\"custom\"`.' }),
|
||
"title": S.String
|
||
}).annotations({ description: 'One title in the unified library, regardless of which store it came from.' })
|
||
export const GetLibraryResponse = S.Array(GetLibraryResponseItem)
|
||
|
||
|
||
/**
|
||
* Resolves `kind` (`portrait` | `hero` | `logo` | `header`) for the given library id and streams
|
||
* the image bytes. For a Steam title, the host's own local Steam cache is tried first (exact —
|
||
* it's what the user's Steam client already shows for it), the public Steam CDN's flat URL
|
||
* convention as a fallback (newer titles' CDN assets can live at a per-asset-hash path the host
|
||
* can't predict, in which case this 404s and the client falls through to its next art candidate).
|
||
* Only Steam ids are backed today; any other store 404s.
|
||
* @summary Fetch one cover-art image for a library entry
|
||
*/
|
||
export const GetLibraryArtParams = S.Struct({
|
||
"id": S.String.annotations({ description: 'The store-qualified library id, e.g. `steam:570`' }),
|
||
"kind": S.String.annotations({ description: '`portrait` | `hero` | `logo` | `header`' })
|
||
})
|
||
|
||
|
||
/**
|
||
* Creates a user-curated title (e.g. a non-Steam game, an emulator, a ROM) with caller-supplied
|
||
* artwork URLs. The host assigns a stable id, returned in the body.
|
||
* @summary Add a custom library entry
|
||
*/
|
||
export const CreateCustomGameBody = S.Struct({
|
||
"art": S.optional(S.Struct({
|
||
"header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }),
|
||
"hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }),
|
||
"logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }),
|
||
"portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' })
|
||
})).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }),
|
||
"launch": S.optional(S.Union(S.Null, S.Struct({
|
||
"kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }),
|
||
"value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' })
|
||
}).annotations({ description: 'How the host would launch a title (consumed by the session launcher in a later step). Kept\nopen-ended so new stores slot in: `steam_appid` → `steam steam:\/\/rungameid\/<value>`;\n`command` → run `<value>` nested in a gamescope session.' }))),
|
||
"prep": S.optional(S.Array(S.Struct({
|
||
"do": S.String.annotations({ description: 'Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_\*` context).' }),
|
||
"undo": S.optional(S.NullOr(S.String)).annotations({ description: 'Command run after the session ends. Skipped when its `do` failed (it never took effect).' })
|
||
}).annotations({ description: 'One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n\*\*synchronously before the app launches\*\* (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).' }))).annotations({ description: 'Per-title prep\/undo steps — commands run as the host user; operator-privileged config.' }),
|
||
"title": S.String
|
||
}).annotations({ description: 'Request body to create or replace a custom entry (no `id` — the host owns it).' })
|
||
|
||
|
||
/**
|
||
* @summary Update a custom library entry
|
||
*/
|
||
export const UpdateCustomGameParams = S.Struct({
|
||
"id": S.String.annotations({ description: 'The custom entry id (without the `custom:` prefix)' })
|
||
})
|
||
|
||
export const UpdateCustomGameBody = S.Struct({
|
||
"art": S.optional(S.Struct({
|
||
"header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }),
|
||
"hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }),
|
||
"logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }),
|
||
"portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' })
|
||
})).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }),
|
||
"launch": S.optional(S.Union(S.Null, S.Struct({
|
||
"kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }),
|
||
"value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' })
|
||
}).annotations({ description: 'How the host would launch a title (consumed by the session launcher in a later step). Kept\nopen-ended so new stores slot in: `steam_appid` → `steam steam:\/\/rungameid\/<value>`;\n`command` → run `<value>` nested in a gamescope session.' }))),
|
||
"prep": S.optional(S.Array(S.Struct({
|
||
"do": S.String.annotations({ description: 'Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_\*` context).' }),
|
||
"undo": S.optional(S.NullOr(S.String)).annotations({ description: 'Command run after the session ends. Skipped when its `do` failed (it never took effect).' })
|
||
}).annotations({ description: 'One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n\*\*synchronously before the app launches\*\* (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).' }))).annotations({ description: 'Per-title prep\/undo steps — commands run as the host user; operator-privileged config.' }),
|
||
"title": S.String
|
||
}).annotations({ description: 'Request body to create or replace a custom entry (no `id` — the host owns it).' })
|
||
|
||
export const UpdateCustomGameResponse = S.Struct({
|
||
"art": S.optional(S.Struct({
|
||
"header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }),
|
||
"hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }),
|
||
"logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }),
|
||
"portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' })
|
||
})).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }),
|
||
"external_id": S.optional(S.NullOr(S.String)).annotations({ description: 'The provider\'s own stable key for this title — the reconcile diff key, so the\nhost-assigned `id` stays stable across reconciles. Present iff `provider` is.' }),
|
||
"id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }),
|
||
"launch": S.optional(S.Union(S.Null, S.Struct({
|
||
"kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }),
|
||
"value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' })
|
||
}).annotations({ description: 'How the host would launch a title (consumed by the session launcher in a later step). Kept\nopen-ended so new stores slot in: `steam_appid` → `steam steam:\/\/rungameid\/<value>`;\n`command` → run `<value>` nested in a gamescope session.' }))),
|
||
"prep": S.optional(S.Array(S.Struct({
|
||
"do": S.String.annotations({ description: 'Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_\*` context).' }),
|
||
"undo": S.optional(S.NullOr(S.String)).annotations({ description: 'Command run after the session ends. Skipped when its `do` failed (it never took effect).' })
|
||
}).annotations({ description: 'One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n\*\*synchronously before the app launches\*\* (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).' }))).annotations({ description: 'Per-title prep\/undo steps (RFC §6): each `do` runs before this title launches, each\n`undo` at session end in reverse order (see [`crate::hooks::run_prep`]).' }),
|
||
"provider": S.optional(S.NullOr(S.String)).annotations({ description: 'The external provider owning this entry (RFC §8), set ONLY by the provider reconcile\nAPI — `None` = a manual entry, which no provider operation ever touches, and which the\nmanual CRUD alone may edit (the converse holds too: manual CRUD refuses provider-owned\nentries, so ownership is never ambiguous).' }),
|
||
"title": S.String
|
||
}).annotations({ description: 'A user-added title, persisted in `~\/.config\/punktfunk\/library.json`. Same shape the API\nreturns and the web console edits.' })
|
||
|
||
|
||
/**
|
||
* @summary Delete a custom library entry
|
||
*/
|
||
export const DeleteCustomGameParams = S.Struct({
|
||
"id": S.String.annotations({ description: 'The custom entry id (without the `custom:` prefix)' })
|
||
})
|
||
|
||
|
||
/**
|
||
* Atomically replaces the full entry set owned by `{provider}` (RFC §8): the payload is the
|
||
* provider's desired list, keyed by its own stable `external_id` — the host diffs, keeps each
|
||
* surviving title's host id stable across reconciles, drops orphans, and never touches manual
|
||
* entries or other providers'. An empty array removes everything the provider owns. Emits
|
||
* `library.changed` with the provider as `source`.
|
||
* @summary Replace a provider's library entries (declarative reconcile)
|
||
*/
|
||
export const ReconcileProviderEntriesParams = S.Struct({
|
||
"provider": S.String.annotations({ description: 'The provider id ([a-z0-9._-], `manual` reserved)' })
|
||
})
|
||
|
||
export const ReconcileProviderEntriesBodyItem = S.Struct({
|
||
"art": S.optional(S.Struct({
|
||
"header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }),
|
||
"hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }),
|
||
"logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }),
|
||
"portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' })
|
||
})).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }),
|
||
"external_id": S.String.annotations({ description: 'The provider\'s stable id for this title (the reconcile diff key).' }),
|
||
"launch": S.optional(S.Union(S.Null, S.Struct({
|
||
"kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }),
|
||
"value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' })
|
||
}).annotations({ description: 'How the host would launch a title (consumed by the session launcher in a later step). Kept\nopen-ended so new stores slot in: `steam_appid` → `steam steam:\/\/rungameid\/<value>`;\n`command` → run `<value>` nested in a gamescope session.' }))),
|
||
"prep": S.optional(S.Array(S.Struct({
|
||
"do": S.String.annotations({ description: 'Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_\*` context).' }),
|
||
"undo": S.optional(S.NullOr(S.String)).annotations({ description: 'Command run after the session ends. Skipped when its `do` failed (it never took effect).' })
|
||
}).annotations({ description: 'One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n\*\*synchronously before the app launches\*\* (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).' }))).annotations({ description: 'Per-title prep\/undo steps — commands run as the host user; operator-privileged config.' }),
|
||
"title": S.String
|
||
}).annotations({ description: 'One title in a provider\'s declarative reconcile payload (RFC §8): [`CustomInput`] plus the\nprovider\'s required stable key.' })
|
||
export const ReconcileProviderEntriesBody = S.Array(ReconcileProviderEntriesBodyItem)
|
||
|
||
export const ReconcileProviderEntriesResponseItem = S.Struct({
|
||
"art": S.optional(S.Struct({
|
||
"header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }),
|
||
"hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }),
|
||
"logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }),
|
||
"portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' })
|
||
})).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }),
|
||
"external_id": S.optional(S.NullOr(S.String)).annotations({ description: 'The provider\'s own stable key for this title — the reconcile diff key, so the\nhost-assigned `id` stays stable across reconciles. Present iff `provider` is.' }),
|
||
"id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }),
|
||
"launch": S.optional(S.Union(S.Null, S.Struct({
|
||
"kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }),
|
||
"value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' })
|
||
}).annotations({ description: 'How the host would launch a title (consumed by the session launcher in a later step). Kept\nopen-ended so new stores slot in: `steam_appid` → `steam steam:\/\/rungameid\/<value>`;\n`command` → run `<value>` nested in a gamescope session.' }))),
|
||
"prep": S.optional(S.Array(S.Struct({
|
||
"do": S.String.annotations({ description: 'Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_\*` context).' }),
|
||
"undo": S.optional(S.NullOr(S.String)).annotations({ description: 'Command run after the session ends. Skipped when its `do` failed (it never took effect).' })
|
||
}).annotations({ description: 'One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n\*\*synchronously before the app launches\*\* (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).' }))).annotations({ description: 'Per-title prep\/undo steps (RFC §6): each `do` runs before this title launches, each\n`undo` at session end in reverse order (see [`crate::hooks::run_prep`]).' }),
|
||
"provider": S.optional(S.NullOr(S.String)).annotations({ description: 'The external provider owning this entry (RFC §8), set ONLY by the provider reconcile\nAPI — `None` = a manual entry, which no provider operation ever touches, and which the\nmanual CRUD alone may edit (the converse holds too: manual CRUD refuses provider-owned\nentries, so ownership is never ambiguous).' }),
|
||
"title": S.String
|
||
}).annotations({ description: 'A user-added title, persisted in `~\/.config\/punktfunk\/library.json`. Same shape the API\nreturns and the web console edits.' })
|
||
export const ReconcileProviderEntriesResponse = S.Array(ReconcileProviderEntriesResponseItem)
|
||
|
||
|
||
/**
|
||
* Deletes every entry owned by `{provider}` — the clean-uninstall path for a provider plugin
|
||
* (RFC §8). Emits `library.changed` when anything was removed.
|
||
* @summary Remove a provider's library entries
|
||
*/
|
||
export const DeleteProviderEntriesParams = S.Struct({
|
||
"provider": S.String.annotations({ description: 'The provider id' })
|
||
})
|
||
|
||
export const deleteProviderEntriesResponseRemovedMin = 0;
|
||
|
||
|
||
|
||
export const DeleteProviderEntriesResponse = S.Struct({
|
||
"removed": S.Number.pipe(S.greaterThanOrEqualTo(deleteProviderEntriesResponseRemovedMin)).annotations({ description: 'How many entries the provider owned (and were removed).' })
|
||
}).annotations({ description: 'The count envelope a provider uninstall returns.' })
|
||
|
||
|
||
/**
|
||
* Non-sensitive status (counts and booleans only — no PIN values, no fingerprints, no device
|
||
* names). Unauthenticated, but served to loopback peers only.
|
||
* @summary Local status summary for the tray icon
|
||
*/
|
||
export const getLocalSummaryResponseKeptDisplaysMin = 0;
|
||
|
||
export const getLocalSummaryResponseNativePairedClientsMin = 0;
|
||
|
||
export const getLocalSummaryResponsePairedClientsMin = 0;
|
||
|
||
export const getLocalSummaryResponsePendingApprovalsMin = 0;
|
||
|
||
export const getLocalSummaryResponseSessionTwoFpsMin = 0;
|
||
|
||
export const getLocalSummaryResponseSessionTwoHeightMin = 0;
|
||
|
||
export const getLocalSummaryResponseSessionTwoWidthMin = 0;
|
||
|
||
|
||
|
||
export const GetLocalSummaryResponse = S.Struct({
|
||
"audio_streaming": S.Boolean.annotations({ description: 'True while the audio stream thread is running.' }),
|
||
"conflicts": S.optional(S.Array(S.String)).annotations({ description: 'Other Moonlight-compatible hosts (Sunshine\/Apollo\/…) detected on this machine at startup —\nrunning one alongside Punktfunk is unsupported. Compact labels (e.g. `Sunshine (running)`);\nthe tray\/console surface them so the clash is visible before pairing silently fails.' }),
|
||
"kept_displays": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponseKeptDisplaysMin)).annotations({ description: 'Virtual displays being KEPT with no live session — lingering (keep-alive window) or pinned\n(`keep_alive: forever`). Non-zero means a display (and, exclusive, your physical monitors) is\nheld; the tray surfaces it + a one-click release. Active (in-use) displays are not counted.' }),
|
||
"native_paired_clients": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponseNativePairedClientsMin)).annotations({ description: 'Number of paired native (punktfunk\/1) devices.' }),
|
||
"paired_clients": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponsePairedClientsMin)).annotations({ description: 'Number of pinned (paired) GameStream client certificates.' }),
|
||
"pending_approvals": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponsePendingApprovalsMin)).annotations({ description: 'Native pairing knocks awaiting the operator\'s approval (count only).' }),
|
||
"pin_pending": S.Boolean.annotations({ description: 'True while a GameStream pairing handshake is parked waiting for the user\'s PIN.' }),
|
||
"session": S.optional(S.Union(S.Null, S.Struct({
|
||
"fps": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponseSessionTwoFpsMin)),
|
||
"height": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponseSessionTwoHeightMin)),
|
||
"width": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponseSessionTwoWidthMin))
|
||
}).annotations({ description: 'The active launch session (set by Moonlight\'s `\/launch`, cleared on cancel\/stop).' }))),
|
||
"version": S.String.annotations({ description: 'Host version (mirrors `\/health`).' }),
|
||
"video_streaming": S.Boolean.annotations({ description: 'True while the video stream thread is running.' })
|
||
}).annotations({ description: 'Non-sensitive host status for the local tray icon: counts and booleans only — no PIN values,\nno fingerprints, no device names. Served unauthenticated to LOOPBACK peers only (see\n`require_auth`): the bearer-token file is SYSTEM\/Administrators-DACL\'d on Windows, so the\nper-user tray process cannot authenticate — this narrow read-only route is its status source.' })
|
||
|
||
|
||
/**
|
||
* The host's recent log entries — an in-memory ring of the newest few thousand, captured at
|
||
* DEBUG and above regardless of `RUST_LOG`. Follow live by polling with `after` set to the last
|
||
* response's `next` cursor; a `dropped: true` means entries were evicted between polls (the ring
|
||
* wrapped). Bearer-only: logs can reference client identities and host paths, so this is part of
|
||
* the loopback-only admin surface, never the LAN-readable mTLS one.
|
||
* @summary Host logs
|
||
*/
|
||
export const logsGetQueryAfterMin = 0;
|
||
|
||
export const logsGetQueryLimitMin = 0;
|
||
|
||
|
||
|
||
export const LogsGetQueryParams = S.Struct({
|
||
"after": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(logsGetQueryAfterMin))).annotations({ description: 'Return entries with seq greater than this (omitted\/0 = oldest retained)' }),
|
||
"limit": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(logsGetQueryLimitMin))).annotations({ description: 'Max entries per response (default and cap 1000)' })
|
||
})
|
||
|
||
export const logsGetResponseEntriesItemSeqMin = 0;
|
||
|
||
export const logsGetResponseEntriesItemTsMsMin = 0;
|
||
|
||
export const logsGetResponseNextMin = 0;
|
||
|
||
|
||
|
||
export const LogsGetResponse = S.Struct({
|
||
"dropped": S.Boolean.annotations({ description: 'True when entries between `after` and the first returned one were already evicted.' }),
|
||
"entries": S.Array(S.Struct({
|
||
"level": S.String.annotations({ description: '`ERROR` | `WARN` | `INFO` | `DEBUG` | `TRACE`.' }),
|
||
"msg": S.String.annotations({ description: 'The formatted message, structured fields appended as `key=value`.' }),
|
||
"seq": S.Number.pipe(S.greaterThanOrEqualTo(logsGetResponseEntriesItemSeqMin)).annotations({ description: 'Monotonic sequence number (1-based) — pass the last one back as the `after` cursor.' }),
|
||
"target": S.String.annotations({ description: 'The emitting module path (tracing target).' }),
|
||
"ts_ms": S.Number.pipe(S.greaterThanOrEqualTo(logsGetResponseEntriesItemTsMsMin)).annotations({ description: 'Unix timestamp in milliseconds.' })
|
||
}).annotations({ description: 'One captured log event.' })),
|
||
"next": S.Number.pipe(S.greaterThanOrEqualTo(logsGetResponseNextMin)).annotations({ description: 'Cursor for the next poll (the last returned seq, or the request\'s `after` when empty).' })
|
||
}).annotations({ description: 'One poll\'s worth of log entries.' })
|
||
|
||
|
||
/**
|
||
* @summary List native paired clients
|
||
*/
|
||
export const ListNativeClientsResponseItem = S.Struct({
|
||
"fingerprint": S.String.annotations({ description: 'Hex SHA-256 of the client certificate — its stable id here.' }),
|
||
"name": S.String.annotations({ description: 'The name the client supplied when pairing.' })
|
||
}).annotations({ description: 'A paired native (punktfunk\/1) client.' })
|
||
export const ListNativeClientsResponse = S.Array(ListNativeClientsResponseItem)
|
||
|
||
|
||
/**
|
||
* Removes a punktfunk/1 client from the native trust store by fingerprint.
|
||
* @summary Unpair a native client
|
||
*/
|
||
export const UnpairNativeClientParams = S.Struct({
|
||
"fingerprint": S.String.annotations({ description: 'Hex SHA-256 of the client certificate (case-insensitive)' })
|
||
})
|
||
|
||
|
||
/**
|
||
* The native (punktfunk/1) pairing window. Poll while armed to show the PIN + countdown.
|
||
* `enabled: false` means this host runs GameStream only (no `--native`).
|
||
* @summary Native pairing status
|
||
*/
|
||
export const getNativePairingResponseExpiresInSecsMin = 0;
|
||
|
||
export const getNativePairingResponsePairedClientsMin = 0;
|
||
|
||
|
||
|
||
export const GetNativePairingResponse = S.Struct({
|
||
"armed": S.Boolean.annotations({ description: 'True while a pairing window is open.' }),
|
||
"enabled": S.Boolean.annotations({ description: 'Whether the native host is running (the unified host started with `--native`).' }),
|
||
"expires_in_secs": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(getNativePairingResponseExpiresInSecsMin)))).annotations({ description: 'Seconds left in the window (null = disarmed, or armed with no expiry via the CLI flag).' }),
|
||
"paired_clients": S.Number.pipe(S.greaterThanOrEqualTo(getNativePairingResponsePairedClientsMin)).annotations({ description: 'Number of paired native clients.' }),
|
||
"pin": S.optional(S.NullOr(S.String)).annotations({ description: 'The PIN to display while armed (null when disarmed).' })
|
||
}).annotations({ description: 'Native (punktfunk\/1) pairing status. Unlike GameStream, the \*\*host\*\* mints the PIN (the SPAKE2\nceremony needs it client-side first), so the console \*\*displays\*\* `pin` for the user to enter on\ntheir device — armed on demand for a short window.' })
|
||
|
||
|
||
/**
|
||
* Opens a pairing window and mints a fresh PIN to display. The user enters it on their device
|
||
* within `ttl_secs`; the device then appears in the native client list.
|
||
* @summary Arm native pairing
|
||
*/
|
||
export const armNativePairingBodyTtlSecsMin = 0;
|
||
|
||
|
||
|
||
export const ArmNativePairingBody = S.Struct({
|
||
"fingerprint": S.optional(S.NullOr(S.String)).annotations({ description: 'Optional: bind the window to ONE device fingerprint (hex SHA-256, e.g. from a pending knock).\nWhen set, only a pairing attempt from that fingerprint consumes the window — so an unpaired\nLAN peer can neither pair nor burn a window armed for a specific device (security-review #9).\nOmit for an unbound window (any device may use the PIN — trusted-LAN only).' }),
|
||
"ttl_secs": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(armNativePairingBodyTtlSecsMin)))).annotations({ description: 'Window length in seconds (default 120; clamped to 15–600).' })
|
||
}).annotations({ description: 'Arm-native-pairing request body.' })
|
||
|
||
export const armNativePairingResponseExpiresInSecsMin = 0;
|
||
|
||
export const armNativePairingResponsePairedClientsMin = 0;
|
||
|
||
|
||
|
||
export const ArmNativePairingResponse = S.Struct({
|
||
"armed": S.Boolean.annotations({ description: 'True while a pairing window is open.' }),
|
||
"enabled": S.Boolean.annotations({ description: 'Whether the native host is running (the unified host started with `--native`).' }),
|
||
"expires_in_secs": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(armNativePairingResponseExpiresInSecsMin)))).annotations({ description: 'Seconds left in the window (null = disarmed, or armed with no expiry via the CLI flag).' }),
|
||
"paired_clients": S.Number.pipe(S.greaterThanOrEqualTo(armNativePairingResponsePairedClientsMin)).annotations({ description: 'Number of paired native clients.' }),
|
||
"pin": S.optional(S.NullOr(S.String)).annotations({ description: 'The PIN to display while armed (null when disarmed).' })
|
||
}).annotations({ description: 'Native (punktfunk\/1) pairing status. Unlike GameStream, the \*\*host\*\* mints the PIN (the SPAKE2\nceremony needs it client-side first), so the console \*\*displays\*\* `pin` for the user to enter on\ntheir device — armed on demand for a short window.' })
|
||
|
||
|
||
/**
|
||
* Unpaired devices that tried to connect while the host requires pairing. Approve one to pair
|
||
* it without a PIN (delegated approval); entries expire after ~10 minutes.
|
||
* @summary List devices awaiting pairing approval
|
||
*/
|
||
export const listPendingDevicesResponseAgeSecsMin = 0;
|
||
|
||
export const listPendingDevicesResponseIdMin = 0;
|
||
|
||
|
||
|
||
export const ListPendingDevicesResponseItem = S.Struct({
|
||
"age_secs": S.Number.pipe(S.greaterThanOrEqualTo(listPendingDevicesResponseAgeSecsMin)).annotations({ description: 'Seconds since the device last knocked.' }),
|
||
"fingerprint": S.String.annotations({ description: 'Hex SHA-256 of the device\'s certificate — what approval pins.' }),
|
||
"id": S.Number.pipe(S.greaterThanOrEqualTo(listPendingDevicesResponseIdMin)).annotations({ description: 'Id to address approve\/deny (per-process; entries expire after ~10 minutes).' }),
|
||
"name": S.String.annotations({ description: 'Best-effort device label (the client\'s own name, else fingerprint-derived).' })
|
||
}).annotations({ description: 'An unpaired device that tried to connect while the host requires pairing — awaiting\n\*\*delegated approval\*\* (approve it here instead of fetching the host PIN out of band).' })
|
||
export const ListPendingDevicesResponse = S.Array(ListPendingDevicesResponseItem)
|
||
|
||
|
||
/**
|
||
* Pairs the device's certificate fingerprint — it can connect immediately (no PIN). Optionally
|
||
* relabel it via the body; send `{}` to keep the name it knocked with.
|
||
* @summary Approve a pending device
|
||
*/
|
||
export const approvePendingDevicePathIdMin = 0;
|
||
|
||
|
||
|
||
export const ApprovePendingDeviceParams = S.Struct({
|
||
"id": S.Number.pipe(S.greaterThanOrEqualTo(approvePendingDevicePathIdMin)).annotations({ description: 'Pending-request id from the pending list' })
|
||
})
|
||
|
||
export const ApprovePendingDeviceBody = S.Struct({
|
||
"name": S.optional(S.NullOr(S.String)).annotations({ description: 'Operator-chosen label for the device (defaults to the name it knocked with).' })
|
||
}).annotations({ description: 'Approve-pending-device request body. Send `{}` to keep the device\'s own name.' })
|
||
|
||
export const ApprovePendingDeviceResponse = S.Struct({
|
||
"fingerprint": S.String.annotations({ description: 'Hex SHA-256 of the client certificate — its stable id here.' }),
|
||
"name": S.String.annotations({ description: 'The name the client supplied when pairing.' })
|
||
}).annotations({ description: 'A paired native (punktfunk\/1) client.' })
|
||
|
||
|
||
/**
|
||
* Drops the request. Not a blocklist — the device's next attempt knocks again.
|
||
* @summary Deny a pending device
|
||
*/
|
||
export const denyPendingDevicePathIdMin = 0;
|
||
|
||
|
||
|
||
export const DenyPendingDeviceParams = S.Struct({
|
||
"id": S.Number.pipe(S.greaterThanOrEqualTo(denyPendingDevicePathIdMin)).annotations({ description: 'Pending-request id from the pending list' })
|
||
})
|
||
|
||
|
||
/**
|
||
* Poll this to know when to prompt the user for the PIN Moonlight displays.
|
||
* @summary Pairing-flow status
|
||
*/
|
||
export const GetPairingStatusResponse = S.Struct({
|
||
"pin_pending": S.Boolean.annotations({ description: 'True while a pairing handshake is parked waiting for the user\'s PIN.' })
|
||
}).annotations({ description: 'Pairing-flow status.' })
|
||
|
||
|
||
/**
|
||
* Delivers the PIN the Moonlight client is displaying, completing the out-of-band half
|
||
* of the pairing handshake.
|
||
* @summary Submit the pairing PIN
|
||
*/
|
||
export const SubmitPairingPinBody = S.Struct({
|
||
"pin": S.String.annotations({ description: '1–16 ASCII digits (Moonlight shows 4).' })
|
||
}).annotations({ description: 'The PIN Moonlight displays during pairing.' })
|
||
|
||
|
||
/**
|
||
* The full sample time-series of the capture currently recording, for live graphing. `404` when
|
||
* nothing is armed.
|
||
* @summary Live in-progress capture
|
||
*/
|
||
export const statsCaptureLiveResponseMetaDurationMsMin = 0;
|
||
|
||
export const statsCaptureLiveResponseMetaFpsMin = 0;
|
||
|
||
export const statsCaptureLiveResponseMetaHeightMin = 0;
|
||
|
||
export const statsCaptureLiveResponseMetaSampleCountMin = 0;
|
||
|
||
export const statsCaptureLiveResponseMetaStartedUnixMsMin = 0;
|
||
|
||
export const statsCaptureLiveResponseMetaWidthMin = 0;
|
||
|
||
export const statsCaptureLiveResponseSamplesItemBitrateKbpsMin = 0;
|
||
|
||
export const statsCaptureLiveResponseSamplesItemFecRecoveredMin = 0;
|
||
|
||
export const statsCaptureLiveResponseSamplesItemFramesDroppedMin = 0;
|
||
|
||
export const statsCaptureLiveResponseSamplesItemPacketsDroppedMin = 0;
|
||
|
||
export const statsCaptureLiveResponseSamplesItemSendDroppedMin = 0;
|
||
|
||
export const statsCaptureLiveResponseSamplesItemSessionIdMin = 0;
|
||
|
||
export const statsCaptureLiveResponseSamplesItemTMsMin = 0;
|
||
|
||
|
||
|
||
export const StatsCaptureLiveResponse = S.Struct({
|
||
"meta": S.Struct({
|
||
"client": S.String.annotations({ description: 'Short label \/ fingerprint prefix, or `\"\"` if unknown.' }),
|
||
"codec": S.String.annotations({ description: '`\"h264\" | \"hevc\" | \"av1\"`.' }),
|
||
"duration_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaDurationMsMin)),
|
||
"fps": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaFpsMin)),
|
||
"height": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaHeightMin)),
|
||
"id": S.String.annotations({ description: 'e.g. `\"2026-06-26T20-14-03Z_5120x1440\"` — also the filename stem.' }),
|
||
"kind": S.String.annotations({ description: '`\"native\" | \"gamestream\"`.' }),
|
||
"sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaSampleCountMin)),
|
||
"started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaStartedUnixMsMin)),
|
||
"width": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaWidthMin))
|
||
}).annotations({ description: 'Capture summary — the filename stem plus the negotiated mode\/codec\/client. Stored at the head\nof each on-disk recording and listed standalone (without the sample body) by\n[`StatsRecorder::list`].' }),
|
||
"samples": S.Array(S.Struct({
|
||
"bitrate_kbps": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemBitrateKbpsMin)).annotations({ description: 'Configured target bitrate.' }),
|
||
"fec_recovered": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemFecRecoveredMin)).annotations({ description: 'FEC shards recovered this window (delta).' }),
|
||
"fps": S.Number.annotations({ description: 'Genuine NEW frames\/s from the source.' }),
|
||
"frames_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemFramesDroppedMin)).annotations({ description: 'Frames dropped this window (delta).' }),
|
||
"mbps": S.Number.annotations({ description: 'Transmit goodput (Mb\/s).' }),
|
||
"packets_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemPacketsDroppedMin)).annotations({ description: 'Packets dropped this window (receiver-side \/ reassembler, where known).' }),
|
||
"repeat_fps": S.Number.annotations({ description: 'Re-encoded holds\/s (source-starvation indicator).' }),
|
||
"send_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemSendDroppedMin)).annotations({ description: 'Host send-buffer overflow \/ EAGAIN this window (delta).' }),
|
||
"session_id": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemSessionIdMin)).annotations({ description: 'Disambiguates concurrent sessions (usually constant).' }),
|
||
"stages": S.Array(S.Struct({
|
||
"name": S.String.annotations({ description: '`\"capture\" | \"submit\" | \"encode\" | \"packetize\" | \"send\"` (path-dependent).' }),
|
||
"p50_us": S.Number,
|
||
"p99_us": S.Number
|
||
}).annotations({ description: 'One pipeline stage\'s latency in an aggregation window (microseconds).' })).annotations({ description: 'Ordered pipeline stages for this path.' }),
|
||
"t_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemTMsMin)).annotations({ description: 'Milliseconds since capture start (monotonic; stamped by [`StatsRecorder::push_sample`]).' })
|
||
}).annotations({ description: 'One aggregated sample (~ every 2 s native, ~ every 1 s GameStream).' }))
|
||
}).annotations({ description: 'A full capture: summary + the sample time-series. The wire + on-disk shape.' })
|
||
|
||
|
||
/**
|
||
* Arms a new performance-stats capture. Idempotent: if a capture is already running this returns
|
||
* the current status unchanged. While armed, the streaming loops emit aggregated samples (~ every
|
||
* 1–2 s) into the in-progress capture, readable live via `GET /stats/capture/live`.
|
||
* @summary Start a stats capture
|
||
*/
|
||
export const statsCaptureStartResponseElapsedMsMin = 0;
|
||
|
||
export const statsCaptureStartResponseSampleCountMin = 0;
|
||
|
||
export const statsCaptureStartResponseStartedUnixMsMin = 0;
|
||
|
||
|
||
|
||
export const StatsCaptureStartResponse = S.Struct({
|
||
"armed": S.Boolean.annotations({ description: 'Capture currently running.' }),
|
||
"elapsed_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStartResponseElapsedMsMin)).annotations({ description: 'Host-measured elapsed time of the in-progress capture, in ms (`0` if idle). Computed from the\nhost\'s MONOTONIC clock, so a console can show elapsed time without subtracting `started_unix_ms`\nfrom its own (possibly skewed) wall clock.' }),
|
||
"kind": S.String.annotations({ description: 'Path of the in-progress capture (`\"\"` if idle).' }),
|
||
"sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStartResponseSampleCountMin)).annotations({ description: 'Samples in the in-progress capture.' }),
|
||
"started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStartResponseStartedUnixMsMin)).annotations({ description: 'Unix start time of the in-progress capture (`0` if idle).' })
|
||
}).annotations({ description: 'Snapshot of the in-progress capture for the management API.' })
|
||
|
||
|
||
/**
|
||
* Whether a capture is armed, its sample count, and start time. Poll this (e.g. every 2 s) to
|
||
* drive the capture-control UI.
|
||
* @summary Stats capture status
|
||
*/
|
||
export const statsCaptureStatusResponseElapsedMsMin = 0;
|
||
|
||
export const statsCaptureStatusResponseSampleCountMin = 0;
|
||
|
||
export const statsCaptureStatusResponseStartedUnixMsMin = 0;
|
||
|
||
|
||
|
||
export const StatsCaptureStatusResponse = S.Struct({
|
||
"armed": S.Boolean.annotations({ description: 'Capture currently running.' }),
|
||
"elapsed_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStatusResponseElapsedMsMin)).annotations({ description: 'Host-measured elapsed time of the in-progress capture, in ms (`0` if idle). Computed from the\nhost\'s MONOTONIC clock, so a console can show elapsed time without subtracting `started_unix_ms`\nfrom its own (possibly skewed) wall clock.' }),
|
||
"kind": S.String.annotations({ description: 'Path of the in-progress capture (`\"\"` if idle).' }),
|
||
"sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStatusResponseSampleCountMin)).annotations({ description: 'Samples in the in-progress capture.' }),
|
||
"started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStatusResponseStartedUnixMsMin)).annotations({ description: 'Unix start time of the in-progress capture (`0` if idle).' })
|
||
}).annotations({ description: 'Snapshot of the in-progress capture for the management API.' })
|
||
|
||
|
||
/**
|
||
* Disarms the in-progress capture and writes it to disk atomically, returning its summary. If
|
||
* nothing was recording, returns `204 No Content`.
|
||
* @summary Stop the stats capture
|
||
*/
|
||
export const statsCaptureStopResponseDurationMsMin = 0;
|
||
|
||
export const statsCaptureStopResponseFpsMin = 0;
|
||
|
||
export const statsCaptureStopResponseHeightMin = 0;
|
||
|
||
export const statsCaptureStopResponseSampleCountMin = 0;
|
||
|
||
export const statsCaptureStopResponseStartedUnixMsMin = 0;
|
||
|
||
export const statsCaptureStopResponseWidthMin = 0;
|
||
|
||
|
||
|
||
export const StatsCaptureStopResponse = S.Struct({
|
||
"client": S.String.annotations({ description: 'Short label \/ fingerprint prefix, or `\"\"` if unknown.' }),
|
||
"codec": S.String.annotations({ description: '`\"h264\" | \"hevc\" | \"av1\"`.' }),
|
||
"duration_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseDurationMsMin)),
|
||
"fps": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseFpsMin)),
|
||
"height": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseHeightMin)),
|
||
"id": S.String.annotations({ description: 'e.g. `\"2026-06-26T20-14-03Z_5120x1440\"` — also the filename stem.' }),
|
||
"kind": S.String.annotations({ description: '`\"native\" | \"gamestream\"`.' }),
|
||
"sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseSampleCountMin)),
|
||
"started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseStartedUnixMsMin)),
|
||
"width": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseWidthMin))
|
||
}).annotations({ description: 'Capture summary — the filename stem plus the negotiated mode\/codec\/client. Stored at the head\nof each on-disk recording and listed standalone (without the sample body) by\n[`StatsRecorder::list`].' })
|
||
|
||
|
||
/**
|
||
* Every saved capture's summary (the `meta` head only — not the sample body), newest first.
|
||
* @summary List saved recordings
|
||
*/
|
||
export const statsRecordingsListResponseDurationMsMin = 0;
|
||
|
||
export const statsRecordingsListResponseFpsMin = 0;
|
||
|
||
export const statsRecordingsListResponseHeightMin = 0;
|
||
|
||
export const statsRecordingsListResponseSampleCountMin = 0;
|
||
|
||
export const statsRecordingsListResponseStartedUnixMsMin = 0;
|
||
|
||
export const statsRecordingsListResponseWidthMin = 0;
|
||
|
||
|
||
|
||
export const StatsRecordingsListResponseItem = S.Struct({
|
||
"client": S.String.annotations({ description: 'Short label \/ fingerprint prefix, or `\"\"` if unknown.' }),
|
||
"codec": S.String.annotations({ description: '`\"h264\" | \"hevc\" | \"av1\"`.' }),
|
||
"duration_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseDurationMsMin)),
|
||
"fps": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseFpsMin)),
|
||
"height": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseHeightMin)),
|
||
"id": S.String.annotations({ description: 'e.g. `\"2026-06-26T20-14-03Z_5120x1440\"` — also the filename stem.' }),
|
||
"kind": S.String.annotations({ description: '`\"native\" | \"gamestream\"`.' }),
|
||
"sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseSampleCountMin)),
|
||
"started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseStartedUnixMsMin)),
|
||
"width": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseWidthMin))
|
||
}).annotations({ description: 'Capture summary — the filename stem plus the negotiated mode\/codec\/client. Stored at the head\nof each on-disk recording and listed standalone (without the sample body) by\n[`StatsRecorder::list`].' })
|
||
export const StatsRecordingsListResponse = S.Array(StatsRecordingsListResponseItem)
|
||
|
||
|
||
/**
|
||
* The full capture (meta + samples) for `id`, for graphing or download.
|
||
* @summary Get a saved recording
|
||
*/
|
||
export const StatsRecordingGetParams = S.Struct({
|
||
"id": S.String.annotations({ description: 'The recording id (its filename stem)' })
|
||
})
|
||
|
||
export const statsRecordingGetResponseMetaDurationMsMin = 0;
|
||
|
||
export const statsRecordingGetResponseMetaFpsMin = 0;
|
||
|
||
export const statsRecordingGetResponseMetaHeightMin = 0;
|
||
|
||
export const statsRecordingGetResponseMetaSampleCountMin = 0;
|
||
|
||
export const statsRecordingGetResponseMetaStartedUnixMsMin = 0;
|
||
|
||
export const statsRecordingGetResponseMetaWidthMin = 0;
|
||
|
||
export const statsRecordingGetResponseSamplesItemBitrateKbpsMin = 0;
|
||
|
||
export const statsRecordingGetResponseSamplesItemFecRecoveredMin = 0;
|
||
|
||
export const statsRecordingGetResponseSamplesItemFramesDroppedMin = 0;
|
||
|
||
export const statsRecordingGetResponseSamplesItemPacketsDroppedMin = 0;
|
||
|
||
export const statsRecordingGetResponseSamplesItemSendDroppedMin = 0;
|
||
|
||
export const statsRecordingGetResponseSamplesItemSessionIdMin = 0;
|
||
|
||
export const statsRecordingGetResponseSamplesItemTMsMin = 0;
|
||
|
||
|
||
|
||
export const StatsRecordingGetResponse = S.Struct({
|
||
"meta": S.Struct({
|
||
"client": S.String.annotations({ description: 'Short label \/ fingerprint prefix, or `\"\"` if unknown.' }),
|
||
"codec": S.String.annotations({ description: '`\"h264\" | \"hevc\" | \"av1\"`.' }),
|
||
"duration_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaDurationMsMin)),
|
||
"fps": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaFpsMin)),
|
||
"height": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaHeightMin)),
|
||
"id": S.String.annotations({ description: 'e.g. `\"2026-06-26T20-14-03Z_5120x1440\"` — also the filename stem.' }),
|
||
"kind": S.String.annotations({ description: '`\"native\" | \"gamestream\"`.' }),
|
||
"sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaSampleCountMin)),
|
||
"started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaStartedUnixMsMin)),
|
||
"width": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaWidthMin))
|
||
}).annotations({ description: 'Capture summary — the filename stem plus the negotiated mode\/codec\/client. Stored at the head\nof each on-disk recording and listed standalone (without the sample body) by\n[`StatsRecorder::list`].' }),
|
||
"samples": S.Array(S.Struct({
|
||
"bitrate_kbps": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemBitrateKbpsMin)).annotations({ description: 'Configured target bitrate.' }),
|
||
"fec_recovered": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemFecRecoveredMin)).annotations({ description: 'FEC shards recovered this window (delta).' }),
|
||
"fps": S.Number.annotations({ description: 'Genuine NEW frames\/s from the source.' }),
|
||
"frames_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemFramesDroppedMin)).annotations({ description: 'Frames dropped this window (delta).' }),
|
||
"mbps": S.Number.annotations({ description: 'Transmit goodput (Mb\/s).' }),
|
||
"packets_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemPacketsDroppedMin)).annotations({ description: 'Packets dropped this window (receiver-side \/ reassembler, where known).' }),
|
||
"repeat_fps": S.Number.annotations({ description: 'Re-encoded holds\/s (source-starvation indicator).' }),
|
||
"send_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemSendDroppedMin)).annotations({ description: 'Host send-buffer overflow \/ EAGAIN this window (delta).' }),
|
||
"session_id": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemSessionIdMin)).annotations({ description: 'Disambiguates concurrent sessions (usually constant).' }),
|
||
"stages": S.Array(S.Struct({
|
||
"name": S.String.annotations({ description: '`\"capture\" | \"submit\" | \"encode\" | \"packetize\" | \"send\"` (path-dependent).' }),
|
||
"p50_us": S.Number,
|
||
"p99_us": S.Number
|
||
}).annotations({ description: 'One pipeline stage\'s latency in an aggregation window (microseconds).' })).annotations({ description: 'Ordered pipeline stages for this path.' }),
|
||
"t_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemTMsMin)).annotations({ description: 'Milliseconds since capture start (monotonic; stamped by [`StatsRecorder::push_sample`]).' })
|
||
}).annotations({ description: 'One aggregated sample (~ every 2 s native, ~ every 1 s GameStream).' }))
|
||
}).annotations({ description: 'A full capture: summary + the sample time-series. The wire + on-disk shape.' })
|
||
|
||
|
||
/**
|
||
* Removes the recording `id` from disk. `404` if there is no such recording.
|
||
* @summary Delete a saved recording
|
||
*/
|
||
export const StatsRecordingDeleteParams = S.Struct({
|
||
"id": S.String.annotations({ description: 'The recording id (its filename stem)' })
|
||
})
|
||
|
||
|
||
/**
|
||
* @summary Live host status
|
||
*/
|
||
export const getStatusResponseActiveSessionsMin = 0;
|
||
|
||
export const getStatusResponsePairedClientsMin = 0;
|
||
|
||
export const getStatusResponseSessionTwoFpsMin = 0;
|
||
|
||
export const getStatusResponseSessionTwoHeightMin = 0;
|
||
|
||
export const getStatusResponseSessionTwoWidthMin = 0;
|
||
|
||
export const getStatusResponseStreamTwoBitrateKbpsMin = 0;
|
||
|
||
export const getStatusResponseStreamTwoFpsMin = 0;
|
||
|
||
export const getStatusResponseStreamTwoHeightMin = 0;
|
||
|
||
export const getStatusResponseStreamTwoMinFecMin = 0;
|
||
|
||
export const getStatusResponseStreamTwoPacketSizeMin = 0;
|
||
|
||
export const getStatusResponseStreamTwoWidthMin = 0;
|
||
|
||
|
||
|
||
export const GetStatusResponse = S.Struct({
|
||
"active_sessions": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseActiveSessionsMin)).annotations({ description: 'Number of live streaming sessions across BOTH planes (GameStream + native punktfunk\/1). The\nnative server admits concurrent sessions, so this can exceed 1; `session`\/`stream` below\ndescribe a single representative session for the detail card.' }),
|
||
"audio_streaming": S.Boolean.annotations({ description: 'True while the audio stream thread is running.' }),
|
||
"paired_clients": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponsePairedClientsMin)).annotations({ description: 'Number of pinned (paired) client certificates.' }),
|
||
"pin_pending": S.Boolean.annotations({ description: 'True while a pairing handshake is parked waiting for the user\'s PIN\n(submit it via `POST \/api\/v1\/pair\/pin`).' }),
|
||
"session": S.optional(S.Union(S.Null, S.Struct({
|
||
"fps": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseSessionTwoFpsMin)),
|
||
"height": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseSessionTwoHeightMin)),
|
||
"width": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseSessionTwoWidthMin))
|
||
}).annotations({ description: 'A representative active session. GameStream\'s launch (Moonlight `\/launch`) when present, else\nthe first live native session. `null` when nothing is streaming.' }))),
|
||
"stream": S.optional(S.Union(S.Null, S.Struct({
|
||
"bitrate_kbps": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoBitrateKbpsMin)),
|
||
"codec": S.Literal('h264', 'hevc', 'av1', 'pyrowave').annotations({ description: 'Video codec identifier. The wire token matches the codec\'s canonical name used across the\nstack (SDP\/GameStream advertisement, the stats-capture `CaptureMeta.codec`, and the encoder\'s\n[`Codec::label`]) — notably `H.265` serializes as `\"hevc\"`, not `\"h265\"`, so the same codec\nreads identically on every console page.' }),
|
||
"fps": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoFpsMin)),
|
||
"height": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoHeightMin)),
|
||
"min_fec": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoMinFecMin)).annotations({ description: 'Client\'s parity floor per FEC block (`minRequiredFecPackets`).' }),
|
||
"packet_size": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoPacketSizeMin)).annotations({ description: 'Video payload size per packet (bytes).' }),
|
||
"width": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoWidthMin))
|
||
}).annotations({ description: 'The active stream\'s parameters — RTSP-negotiated for GameStream, or the live native session\'s\nmode\/codec\/bitrate. `null` when nothing is streaming.' }))),
|
||
"video_streaming": S.Boolean.annotations({ description: 'True while the video stream thread is running.' })
|
||
}).annotations({ description: 'Live host status (changes as clients launch\/end sessions).' })
|