Files
punktfunk/sdk/src/gen/schemas.ts
T
enricobuehler 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
feat(host/library): external provider API — declarative reconcile (M4)
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>
2026-07-17 00:33:42 +02:00

1685 lines
155 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 (1600, 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 (1600, 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 (1600, 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 15600).' })
}).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: '116 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
* 12 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).' })