From 2ca3f729fc3abc102b49c46a44d178c51ee48ee9 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 29 Jul 2026 14:35:23 +0200 Subject: [PATCH] feat(client/decky): host rows wear the OS mark for free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit react-icons already ships every brand mark, so this client costs nothing: the avahi parser and the saved-hosts feed surface the new `os` chain (optional on SavedHost — the installed flatpak client may predate the field), the merge model threads it through with the live advert preferred, and the row label leads with the resolved mark via the same most-specific-first walk as every other client. A payload without `os` renders exactly as today. Co-Authored-By: Claude Fable 5 --- clients/decky/main.py | 3 +++ clients/decky/src/backend.ts | 4 +++ clients/decky/src/hooks.ts | 5 ++++ clients/decky/src/os-icon.tsx | 49 +++++++++++++++++++++++++++++++++++ clients/decky/src/page.tsx | 2 ++ 5 files changed, 63 insertions(+) create mode 100644 clients/decky/src/os-icon.tsx diff --git a/clients/decky/main.py b/clients/decky/main.py index f5464b08..d1edeb18 100644 --- a/clients/decky/main.py +++ b/clients/decky/main.py @@ -644,6 +644,9 @@ def _parse_avahi_browse(stdout: str) -> list[dict]: "proto": props.get("proto", ""), "id": props.get("id", ""), "mgmt": mgmt, + # OS-identity chain for the host row's icon (e.g. "linux/fedora/bazzite"); + # empty on an older host that doesn't advertise it. + "os": props.get("os", ""), } key = props.get("id") or f"{address}:{port}" existing = out.get(key) diff --git a/clients/decky/src/backend.ts b/clients/decky/src/backend.ts index 1b447b5b..515e9c9c 100644 --- a/clients/decky/src/backend.ts +++ b/clients/decky/src/backend.ts @@ -11,6 +11,7 @@ export interface Host { paired: boolean; // whether THIS device has already PIN-paired this host (by fingerprint) id: string; // the host's stable instance id (mDNS TXT `id`; "" when not advertised) mgmt: number; // management-API port (mDNS TXT `mgmt`; 0 = not advertised → default 47990) + os: string; // OS-identity chain (mDNS TXT `os`, e.g. "linux/fedora/bazzite"); "" on older hosts } // One title from a host's game library (the flatpak client's --library TSV, parsed by the @@ -66,6 +67,9 @@ export interface SavedHost { fp_hex: string; // host cert fingerprint (lowercase hex); "" for a not-yet-paired manual entry paired: boolean; mac: string[]; + // OS-identity chain learned by the desktop client; optional because the installed + // flatpak client may predate the field. + os?: string; last_used: number | null; online: boolean | null; } diff --git a/clients/decky/src/hooks.ts b/clients/decky/src/hooks.ts index be8167e1..5276a53c 100644 --- a/clients/decky/src/hooks.ts +++ b/clients/decky/src/hooks.ts @@ -107,6 +107,7 @@ export interface HostView { pairPolicy: string; // the advert's policy ("required"|"optional"), "" when not advertising mgmt: number; // advertised mgmt-API port (0 = not advertised → default) id: string; // advertised stable host id ("" when not advertising) + os: string; // OS-identity chain (live advert preferred, else the stored one); "" unknown } function advertMatchesSaved(a: Host, s: SavedHost): boolean { @@ -131,6 +132,7 @@ export function mergeHosts(saved: SavedHost[], discovered: Host[]): HostView[] { pairPolicy: advert?.pair ?? "", mgmt: advert?.mgmt ?? 0, id: advert?.id ?? "", + os: advert?.os || s.os || "", }; }); for (const a of discovered) { @@ -148,6 +150,7 @@ export function mergeHosts(saved: SavedHost[], discovered: Host[]): HostView[] { pairPolicy: a.pair, mgmt: a.mgmt, id: a.id, + os: a.os, }); } return views; @@ -174,6 +177,7 @@ export function toHost(v: HostView): Host { paired: v.paired, id: v.id, mgmt: v.mgmt, + os: v.os, }; } @@ -485,6 +489,7 @@ export function resolvePinHost( paired: !!pin.paired, id: pin.host_id, mgmt: pin.mgmt, + os: "", // pins don't store the chain; the icon is a hosts-tab affordance }, online: false, }; diff --git a/clients/decky/src/os-icon.tsx b/clients/decky/src/os-icon.tsx new file mode 100644 index 00000000..9ad34452 --- /dev/null +++ b/clients/decky/src/os-icon.tsx @@ -0,0 +1,49 @@ +// The host row's OS mark, resolved from the host's OS-identity chain (mDNS `os` TXT / +// `--list-hosts` `os`, e.g. "linux/fedora/bazzite"): walk the chain most-specific-first and +// take the first token react-icons has a brand mark for, so an unknown distro degrades to its +// family's mark and finally to Tux. Mirrors pf-client-core's `os_icon_tokens` (aliases +// macos→apple, steamos→steam); null when the chain is absent or entirely unknown — the row +// then renders exactly as it did before the field existed. +import { FC } from "react"; +import { + FaApple, + FaFedora, + FaLinux, + FaSteam, + FaSuse, + FaUbuntu, + FaWindows, +} from "react-icons/fa"; +import { SiArchlinux, SiDebian, SiNixos } from "react-icons/si"; +import { IconType } from "react-icons"; + +const OS_ICONS: Record = { + windows: FaWindows, + apple: FaApple, + macos: FaApple, + linux: FaLinux, + steam: FaSteam, + steamos: FaSteam, + ubuntu: FaUbuntu, + fedora: FaFedora, + opensuse: FaSuse, + arch: SiArchlinux, + debian: SiDebian, + nixos: SiNixos, +}; + +export function resolveOsIcon(os: string | undefined): IconType | null { + for (const token of (os ?? "").toLowerCase().split("/").reverse()) { + const icon = OS_ICONS[token]; + if (icon) { + return icon; + } + } + return null; +} + +/** The mark itself, or nothing — sized/colored by the surrounding text like the lock glyph. */ +export const OsMark: FC<{ os?: string }> = ({ os }) => { + const Icon = resolveOsIcon(os); + return Icon ? : null; +}; diff --git a/clients/decky/src/page.tsx b/clients/decky/src/page.tsx index 0dd67808..c2431906 100644 --- a/clients/decky/src/page.tsx +++ b/clients/decky/src/page.tsx @@ -30,6 +30,7 @@ import { } from "react-icons/fa"; import { UpdateInfo, forgetHost, killStream } from "./backend"; import { PluginErrorBoundary } from "./boundary"; +import { OsMark } from "./os-icon"; import { DOCS_URL, HostView, @@ -182,6 +183,7 @@ const HostRow: FC<{ + {pair ? : } {host.name}