feat(client/decky): host rows wear the OS mark for free
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 <noreply@anthropic.com>
This commit is contained in:
@@ -644,6 +644,9 @@ def _parse_avahi_browse(stdout: str) -> list[dict]:
|
|||||||
"proto": props.get("proto", ""),
|
"proto": props.get("proto", ""),
|
||||||
"id": props.get("id", ""),
|
"id": props.get("id", ""),
|
||||||
"mgmt": mgmt,
|
"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}"
|
key = props.get("id") or f"{address}:{port}"
|
||||||
existing = out.get(key)
|
existing = out.get(key)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface Host {
|
|||||||
paired: boolean; // whether THIS device has already PIN-paired this host (by fingerprint)
|
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)
|
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)
|
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
|
// 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
|
fp_hex: string; // host cert fingerprint (lowercase hex); "" for a not-yet-paired manual entry
|
||||||
paired: boolean;
|
paired: boolean;
|
||||||
mac: string[];
|
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;
|
last_used: number | null;
|
||||||
online: boolean | null;
|
online: boolean | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export interface HostView {
|
|||||||
pairPolicy: string; // the advert's policy ("required"|"optional"), "" when not advertising
|
pairPolicy: string; // the advert's policy ("required"|"optional"), "" when not advertising
|
||||||
mgmt: number; // advertised mgmt-API port (0 = not advertised → default)
|
mgmt: number; // advertised mgmt-API port (0 = not advertised → default)
|
||||||
id: string; // advertised stable host id ("" when not advertising)
|
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 {
|
function advertMatchesSaved(a: Host, s: SavedHost): boolean {
|
||||||
@@ -131,6 +132,7 @@ export function mergeHosts(saved: SavedHost[], discovered: Host[]): HostView[] {
|
|||||||
pairPolicy: advert?.pair ?? "",
|
pairPolicy: advert?.pair ?? "",
|
||||||
mgmt: advert?.mgmt ?? 0,
|
mgmt: advert?.mgmt ?? 0,
|
||||||
id: advert?.id ?? "",
|
id: advert?.id ?? "",
|
||||||
|
os: advert?.os || s.os || "",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
for (const a of discovered) {
|
for (const a of discovered) {
|
||||||
@@ -148,6 +150,7 @@ export function mergeHosts(saved: SavedHost[], discovered: Host[]): HostView[] {
|
|||||||
pairPolicy: a.pair,
|
pairPolicy: a.pair,
|
||||||
mgmt: a.mgmt,
|
mgmt: a.mgmt,
|
||||||
id: a.id,
|
id: a.id,
|
||||||
|
os: a.os,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return views;
|
return views;
|
||||||
@@ -174,6 +177,7 @@ export function toHost(v: HostView): Host {
|
|||||||
paired: v.paired,
|
paired: v.paired,
|
||||||
id: v.id,
|
id: v.id,
|
||||||
mgmt: v.mgmt,
|
mgmt: v.mgmt,
|
||||||
|
os: v.os,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,6 +489,7 @@ export function resolvePinHost(
|
|||||||
paired: !!pin.paired,
|
paired: !!pin.paired,
|
||||||
id: pin.host_id,
|
id: pin.host_id,
|
||||||
mgmt: pin.mgmt,
|
mgmt: pin.mgmt,
|
||||||
|
os: "", // pins don't store the chain; the icon is a hosts-tab affordance
|
||||||
},
|
},
|
||||||
online: false,
|
online: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<string, IconType> = {
|
||||||
|
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 ? <Icon /> : null;
|
||||||
|
};
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
import { UpdateInfo, forgetHost, killStream } from "./backend";
|
import { UpdateInfo, forgetHost, killStream } from "./backend";
|
||||||
import { PluginErrorBoundary } from "./boundary";
|
import { PluginErrorBoundary } from "./boundary";
|
||||||
|
import { OsMark } from "./os-icon";
|
||||||
import {
|
import {
|
||||||
DOCS_URL,
|
DOCS_URL,
|
||||||
HostView,
|
HostView,
|
||||||
@@ -182,6 +183,7 @@ const HostRow: FC<{
|
|||||||
<Field
|
<Field
|
||||||
label={
|
label={
|
||||||
<span style={{ display: "inline-flex", alignItems: "center", gap: "0.4em" }}>
|
<span style={{ display: "inline-flex", alignItems: "center", gap: "0.4em" }}>
|
||||||
|
<OsMark os={host.os} />
|
||||||
{pair ? <FaLock /> : <FaLockOpen />}
|
{pair ? <FaLock /> : <FaLockOpen />}
|
||||||
{host.name}
|
{host.name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user