feat(client): the desktop clients wear the host's OS mark
The client half of the host's new `os=` advert, shared once in pf-client-core: `sanitize_os` (mDNS is unauthenticated input — lowercase `[a-z0-9._-]` tokens, capped) and `os_icon_tokens`, the most-specific-first walk with the brand aliases (`macos`→apple, `steamos`→steam) every platform resolves through. `DiscoveredHost` carries the chain, `KnownHost` persists it (`serde(default)`, elided when empty — older stores load unchanged and older clients read back exactly what they wrote), `upsert` moves it only when carried, and `learn_os` mirrors `learn_mac` — no-op, no disk write when unchanged — so the mark survives the host going to sleep. GTK shells: the card's status row leads with a recolorable symbolic glyph. That needed real embedded assets — the shells had none — so `data/` gains the ten `pf-os-*-symbolic` SVGs compiled into a gresource (new build.rs, glib-build-tools) and registered on the icon theme at startup; the Adwaita theme then tints them like every other status glyph. WinUI shell: reactor renders raster-from-URI only, so the embedded mid-gray PNGs (legible on both themes) materialize once into %LOCALAPPDATA%\punktfunk\os-icons\ — the library's poster-art pattern — and the tile's status row leads with a 16px image. The couch UI plumbs `HostRow.os` (live advert preferred, else the store) for a Skia glyph that is a declared follow-up; `--list-hosts` / `hosts --json` emit the stored chain so the Decky plugin can read it. A host that advertises no `os` renders everywhere exactly as it did before the field existed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -179,6 +179,7 @@ impl SimpleComponent for AppModel {
|
||||
}
|
||||
};
|
||||
load_css();
|
||||
install_os_icons();
|
||||
// Screenshot scenes must capture settled frames: kill every GTK/libadwaita
|
||||
// animation (a headless session may starve the frame clock and leave a
|
||||
// transition frozen mid-flight in the capture).
|
||||
@@ -937,6 +938,19 @@ pub fn run() -> glib::ExitCode {
|
||||
glib::ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
/// Register the embedded gresource (built by build.rs from `data/`) and point the icon
|
||||
/// theme at it, so the host cards' `pf-os-*-symbolic` OS marks resolve — and recolor —
|
||||
/// like any themed icon.
|
||||
fn install_os_icons() {
|
||||
if let Err(e) = gio::resources_register_include!("punktfunk-client.gresource") {
|
||||
tracing::warn!("register gresource: {e} — host cards lose their OS marks");
|
||||
return;
|
||||
}
|
||||
if let Some(display) = gdk::Display::default() {
|
||||
gtk::IconTheme::for_display(&display).add_resource_path("/io/unom/Punktfunk/icons");
|
||||
}
|
||||
}
|
||||
|
||||
fn load_css() {
|
||||
let provider = gtk::CssProvider::new();
|
||||
provider.load_from_string(CSS);
|
||||
|
||||
@@ -283,6 +283,7 @@ pub fn headless_list_hosts() -> glib::ExitCode {
|
||||
"fp_hex": h.fp_hex,
|
||||
"paired": h.paired,
|
||||
"mac": h.mac,
|
||||
"os": h.os,
|
||||
"last_used": h.last_used,
|
||||
"online": online.as_ref().map(|v| serde_json::Value::Bool(v[i]))
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
@@ -508,6 +509,7 @@ pub fn run_shot(ctx: &ShotCtx, scene: &str) {
|
||||
pair: "required".to_string(),
|
||||
mgmt_port: None,
|
||||
mac: Vec::new(),
|
||||
os: "linux/arch/steamos".to_string(),
|
||||
};
|
||||
|
||||
// What the self-capture renders: the main window, except for scenes that open their
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// The UI-agnostic plumbing lives in `pf-client-core`, shared with the session binary.
|
||||
// Root re-exports keep every `crate::trust`-style path resolving unchanged.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use pf_client_core::{discovery, gamepad, library, trust, video, wol};
|
||||
pub use pf_client_core::{discovery, gamepad, library, os, trust, video, wol};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod app;
|
||||
|
||||
@@ -217,6 +217,15 @@ impl relm4::factory::FactoryComponent for HostCard {
|
||||
let status = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
status.set_halign(gtk::Align::Center);
|
||||
status.set_margin_top(4);
|
||||
// The host's OS mark leads the row; nothing at all for an older host that doesn't
|
||||
// advertise one, so those cards render exactly as they always did.
|
||||
let os_chain = match &self.kind {
|
||||
CardKind::Saved { host: k, .. } => k.os.as_str(),
|
||||
CardKind::Discovered(a) => a.os.as_str(),
|
||||
};
|
||||
if let Some(img) = os_icon_image(os_chain) {
|
||||
status.append(&img);
|
||||
}
|
||||
let pill = |text: &str, class: &str| {
|
||||
let l = gtk::Label::new(Some(text));
|
||||
l.add_css_class("pf-pill");
|
||||
@@ -573,6 +582,28 @@ const PROBE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(12);
|
||||
/// for this — without it every profile is the same grey, and telling them apart across a grid
|
||||
/// at a glance is the whole reason the chip exists. No colour set keeps the neutral pill, so
|
||||
/// the palette stays opt-in.
|
||||
/// The OS-icon tokens this shell ships symbolic art for (`data/icons/.../pf-os-<t>-symbolic.svg`,
|
||||
/// embedded via gresource). Chains walk most-specific-first, so a distro without its own mark
|
||||
/// (Bazzite, CachyOS, ...) lands on its family's and finally on plain Tux.
|
||||
const OS_ICON_TOKENS: &[&str] = &[
|
||||
"windows", "apple", "linux", "steam", "ubuntu", "fedora", "arch", "debian", "nixos", "opensuse",
|
||||
];
|
||||
|
||||
/// The card's OS glyph for an advertised chain, or `None` (no widget) when the host doesn't
|
||||
/// advertise one / nothing in the chain is recognized-and-drawable. Symbolic, so it recolors
|
||||
/// with the Adwaita theme like every other status glyph; the raw chain is the tooltip.
|
||||
fn os_icon_image(chain: &str) -> Option<gtk::Image> {
|
||||
let token = crate::os::os_icon_tokens(chain)
|
||||
.into_iter()
|
||||
.find(|t| OS_ICON_TOKENS.contains(&t.as_str()))?;
|
||||
let img = gtk::Image::from_icon_name(&format!("pf-os-{token}-symbolic"));
|
||||
img.set_pixel_size(14);
|
||||
img.add_css_class("dim-label");
|
||||
img.set_valign(gtk::Align::Center);
|
||||
img.set_tooltip_text(Some(chain));
|
||||
Some(img)
|
||||
}
|
||||
|
||||
fn profile_pill(p: &Profile) -> gtk::Widget {
|
||||
let label = gtk::Label::new(Some(&p.name));
|
||||
label.add_css_class("pf-pill");
|
||||
@@ -1036,6 +1067,14 @@ impl HostsPage {
|
||||
{
|
||||
crate::trust::learn_mac(&k.fp_hex, &k.addr, k.port, &a.mac);
|
||||
}
|
||||
// Same for its OS chain — the icon then survives the host going offline.
|
||||
if let Some(a) = self
|
||||
.adverts
|
||||
.values()
|
||||
.find(|a| matches(k, a) && !a.os.is_empty())
|
||||
{
|
||||
crate::trust::learn_os(&k.fp_hex, &k.addr, k.port, &a.os);
|
||||
}
|
||||
saved.push_back(HostCard {
|
||||
connecting: self.connecting.as_deref() == Some(k.fp_hex.as_str()),
|
||||
kind: CardKind::Saved {
|
||||
|
||||
Reference in New Issue
Block a user