diff --git a/clients/decky/main.py b/clients/decky/main.py index fa0e8a43..b9c6a9b4 100644 --- a/clients/decky/main.py +++ b/clients/decky/main.py @@ -1044,20 +1044,36 @@ class Plugin: try: return json.loads(_settings_path().read_text()) except (OSError, json.JSONDecodeError): - # The client's own defaults (native display, host-default bitrate, auto pad). + # The client's own defaults (native display, host-default bitrate, auto pad, + # stats overlay at Normal — `Settings::default` is `show_stats: true`). return { "width": 0, "height": 0, "refresh_hz": 0, "render_scale": 1.0, "bitrate_kbps": 0, "codec": "auto", "gamepad": "auto", "gamepad_forwarding": True, "compositor": "auto", "inhibit_shortcuts": True, "mic_enabled": False, + "stats_verbosity": "normal", "show_stats": True, } async def set_settings(self, settings: dict) -> dict: - """Write the stream settings JSON the (sandboxed) client reads on launch.""" + """Write the stream settings JSON the (sandboxed) client reads on launch. + + MERGED onto whatever is on disk, never a wholesale replace: this file is shared with + the desktop client and the console's settings screen, and it holds far more keys than + this panel models (decoder, GPU, profiles, touch/mouse model…). The panel reads it once + when it mounts, so a straight write would post a snapshot that predates anything those + other editors stored in the meantime — silently reverting it. + """ try: d = _client_config_dir() d.mkdir(parents=True, exist_ok=True) - _settings_path().write_text(json.dumps(settings, indent=2)) + try: + on_disk = json.loads(_settings_path().read_text()) + if not isinstance(on_disk, dict): + on_disk = {} + except (OSError, json.JSONDecodeError): + on_disk = {} # no file yet (or an unreadable one): this write creates it + on_disk.update(settings) + _settings_path().write_text(json.dumps(on_disk, indent=2)) return {"ok": True} except OSError as exc: decky.logger.exception("could not write settings") diff --git a/clients/decky/src/backend.ts b/clients/decky/src/backend.ts index e464e2d7..27011722 100644 --- a/clients/decky/src/backend.ts +++ b/clients/decky/src/backend.ts @@ -122,6 +122,13 @@ export interface StreamSettings { // here would be a dead one. The desktop client's row still edits this same file. inhibit_shortcuts: boolean; mic_enabled: boolean; + // Stats-overlay tier: "off" | "compact" | "normal" | "detailed". Absent in a pre-tier file, + // which resolves through `show_stats` — read both the way the client's + // `Settings::stats_verbosity` does, and write both the way `set_stats_verbosity` does. + stats_verbosity?: string; + // The legacy on/off the tier supersedes; kept written in sync so a client that predates the + // tiers still honours an Off chosen here. + show_stats?: boolean; } export interface UpdateInfo { diff --git a/clients/decky/src/settings.tsx b/clients/decky/src/settings.tsx index 18edd919..97baf0f0 100644 --- a/clients/decky/src/settings.tsx +++ b/clients/decky/src/settings.tsx @@ -55,6 +55,15 @@ const COMPOSITOR_LABELS: Record = { mutter: "GNOME (Mutter)", gamescope: "gamescope", }; +// The stats-overlay tiers, in the cycle order every other client's picker uses +// (punktfunk_core `StatsVerbosity::ALL`). Stored lowercase — the enum is `rename_all`. +const STATS_TIERS = ["off", "compact", "normal", "detailed"]; +const STATS_LABELS: Record = { + off: "Off", + compact: "Compact", + normal: "Normal", + detailed: "Detailed", +}; export const SettingsSection: FC = () => { const [s, setS] = useState(null); @@ -74,6 +83,12 @@ export const SettingsSection: FC = () => { if (!s) return ; + // Mirrors `Settings::stats_verbosity`: an absent tier is a pre-tier store, which resolves + // through the legacy `show_stats` bool — and an absent bool is the client's serde default + // (true), so a file this plugin wrote before it carried either key reads as Normal, exactly + // as the stream sees it. + const statsTier = s.stats_verbosity ?? ((s.show_stats ?? true) ? "normal" : "off"); + const resIdx = Math.max( 0, RESOLUTIONS.findIndex(([w, h]) => w === s.width && h === s.height), @@ -206,6 +221,26 @@ export const SettingsSection: FC = () => { checked={s.mic_enabled} onChange={(v) => patch({ mic_enabled: v })} /> + + +
+ ({ data: t, label: STATS_LABELS[t] ?? t }))} + selectedOption={statsTier} + // Both keys, in sync — the same pairing `Settings::set_stats_verbosity` keeps, so a + // client too old for the tier still reads the on/off it understands. + onChange={(o) => { + const tier = o.data as string; + patch({ stats_verbosity: tier, show_stats: tier !== "off" }); + }} + /> +
+
+
); }; diff --git a/docs-site/content/docs/client-settings.md b/docs-site/content/docs/client-settings.md index 1b3401fd..7c5c7cf1 100644 --- a/docs-site/content/docs/client-settings.md +++ b/docs-site/content/docs/client-settings.md @@ -197,7 +197,8 @@ when you return to the host list. iPhone, iPad, Apple TV and Android have no equ superset of the one before. This setting only picks the tier a session *starts* at — you can cycle them live in-stream, with a shortcut that differs by platform. The Apple app additionally lets you choose which corner the overlay sits in (Top Left, Top Right, Bottom Left, Bottom Right). The Decky -plugin has no stats setting. The shortcuts, and every number in the overlay, are in +plugin has the tier picker too, in its Settings section. The shortcuts, and every number in the +overlay, are in [Understanding the stats overlay](/docs/stats). ## Settings that are facts about your device