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}