diff --git a/Cargo.lock b/Cargo.lock
index 6a517e2c..2219fceb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1585,6 +1585,12 @@ dependencies = [
"smallvec",
]
+[[package]]
+name = "glib-build-tools"
+version = "0.22.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9871f38b67853c358b8190f77b9f878eb27d933a950f1045b244c4559a9f5f0"
+
[[package]]
name = "glib-macros"
version = "0.22.6"
@@ -3313,6 +3319,7 @@ version = "0.21.0"
dependencies = [
"anyhow",
"async-channel",
+ "glib-build-tools",
"gtk4",
"libadwaita",
"pf-client-core",
@@ -3328,6 +3335,7 @@ name = "punktfunk-client-session"
version = "0.21.0"
dependencies = [
"anyhow",
+ "glib-build-tools",
"pf-client-core",
"pf-console-ui",
"pf-presenter",
diff --git a/clients/cli/src/main.rs b/clients/cli/src/main.rs
index 6e7eb3a7..fc664cc9 100644
--- a/clients/cli/src/main.rs
+++ b/clients/cli/src/main.rs
@@ -243,6 +243,7 @@ punktfunk:// link takes. Exit codes: 0 ok, 2 connect, 3 trust, 4 renderer, 5 not
"fp_hex": h.fp_hex,
"paired": h.paired,
"mac": h.mac,
+ "os": h.os,
"last_used": h.last_used,
"clipboard_sync": h.clipboard_sync,
"profile": h.profile_id.as_ref()
diff --git a/clients/linux/Cargo.toml b/clients/linux/Cargo.toml
index 0a2128d7..1aae33b7 100644
--- a/clients/linux/Cargo.toml
+++ b/clients/linux/Cargo.toml
@@ -33,5 +33,10 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
relm4 = { version = "0.11", features = ["libadwaita"] }
+# Compiles data/ (the OS-mark symbolic icons) into the embedded gresource (build.rs) —
+# needs `glib-compile-resources`, which ships with the GTK dev stack this crate needs anyway.
+[target.'cfg(target_os = "linux")'.build-dependencies]
+glib-build-tools = "0.22"
+
[lints]
workspace = true
diff --git a/clients/linux/build.rs b/clients/linux/build.rs
new file mode 100644
index 00000000..36b8a1ab
--- /dev/null
+++ b/clients/linux/build.rs
@@ -0,0 +1,15 @@
+//! Compile the shell's embedded assets (`data/` — the host-card OS-mark symbolic icons)
+//! into a gresource bundle, registered at startup via `gio::resources_register_include!`.
+
+fn main() {
+ // Host cfg gate mirrors this crate's `#[cfg(target_os = "linux")]` modules: on any other
+ // host the crate compiles to an empty stub and `glib-compile-resources` may not exist.
+ #[cfg(target_os = "linux")]
+ if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") {
+ glib_build_tools::compile_resources(
+ &["data"],
+ "data/resources.gresource.xml",
+ "punktfunk-client.gresource",
+ );
+ }
+}
diff --git a/clients/linux/data/icons/scalable/actions/pf-os-apple-symbolic.svg b/clients/linux/data/icons/scalable/actions/pf-os-apple-symbolic.svg
new file mode 100644
index 00000000..2af51e1a
--- /dev/null
+++ b/clients/linux/data/icons/scalable/actions/pf-os-apple-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/linux/data/icons/scalable/actions/pf-os-arch-symbolic.svg b/clients/linux/data/icons/scalable/actions/pf-os-arch-symbolic.svg
new file mode 100644
index 00000000..458911f1
--- /dev/null
+++ b/clients/linux/data/icons/scalable/actions/pf-os-arch-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/linux/data/icons/scalable/actions/pf-os-debian-symbolic.svg b/clients/linux/data/icons/scalable/actions/pf-os-debian-symbolic.svg
new file mode 100644
index 00000000..9b82e206
--- /dev/null
+++ b/clients/linux/data/icons/scalable/actions/pf-os-debian-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/linux/data/icons/scalable/actions/pf-os-fedora-symbolic.svg b/clients/linux/data/icons/scalable/actions/pf-os-fedora-symbolic.svg
new file mode 100644
index 00000000..428abe6a
--- /dev/null
+++ b/clients/linux/data/icons/scalable/actions/pf-os-fedora-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/linux/data/icons/scalable/actions/pf-os-linux-symbolic.svg b/clients/linux/data/icons/scalable/actions/pf-os-linux-symbolic.svg
new file mode 100644
index 00000000..0f806c24
--- /dev/null
+++ b/clients/linux/data/icons/scalable/actions/pf-os-linux-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/linux/data/icons/scalable/actions/pf-os-nixos-symbolic.svg b/clients/linux/data/icons/scalable/actions/pf-os-nixos-symbolic.svg
new file mode 100644
index 00000000..cc34d519
--- /dev/null
+++ b/clients/linux/data/icons/scalable/actions/pf-os-nixos-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/linux/data/icons/scalable/actions/pf-os-opensuse-symbolic.svg b/clients/linux/data/icons/scalable/actions/pf-os-opensuse-symbolic.svg
new file mode 100644
index 00000000..e52617ec
--- /dev/null
+++ b/clients/linux/data/icons/scalable/actions/pf-os-opensuse-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/linux/data/icons/scalable/actions/pf-os-steam-symbolic.svg b/clients/linux/data/icons/scalable/actions/pf-os-steam-symbolic.svg
new file mode 100644
index 00000000..db35d17b
--- /dev/null
+++ b/clients/linux/data/icons/scalable/actions/pf-os-steam-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/linux/data/icons/scalable/actions/pf-os-ubuntu-symbolic.svg b/clients/linux/data/icons/scalable/actions/pf-os-ubuntu-symbolic.svg
new file mode 100644
index 00000000..7bb9ce4f
--- /dev/null
+++ b/clients/linux/data/icons/scalable/actions/pf-os-ubuntu-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/linux/data/icons/scalable/actions/pf-os-windows-symbolic.svg b/clients/linux/data/icons/scalable/actions/pf-os-windows-symbolic.svg
new file mode 100644
index 00000000..f7f972bf
--- /dev/null
+++ b/clients/linux/data/icons/scalable/actions/pf-os-windows-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/linux/data/resources.gresource.xml b/clients/linux/data/resources.gresource.xml
new file mode 100644
index 00000000..660e71e4
--- /dev/null
+++ b/clients/linux/data/resources.gresource.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ icons/scalable/actions/pf-os-windows-symbolic.svg
+ icons/scalable/actions/pf-os-apple-symbolic.svg
+ icons/scalable/actions/pf-os-linux-symbolic.svg
+ icons/scalable/actions/pf-os-steam-symbolic.svg
+ icons/scalable/actions/pf-os-ubuntu-symbolic.svg
+ icons/scalable/actions/pf-os-fedora-symbolic.svg
+ icons/scalable/actions/pf-os-arch-symbolic.svg
+ icons/scalable/actions/pf-os-debian-symbolic.svg
+ icons/scalable/actions/pf-os-nixos-symbolic.svg
+ icons/scalable/actions/pf-os-opensuse-symbolic.svg
+
+
diff --git a/clients/linux/src/app.rs b/clients/linux/src/app.rs
index e1c3f5e1..dd877a55 100644
--- a/clients/linux/src/app.rs
+++ b/clients/linux/src/app.rs
@@ -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);
diff --git a/clients/linux/src/cli.rs b/clients/linux/src/cli.rs
index 7eea44d1..0c7feeff 100644
--- a/clients/linux/src/cli.rs
+++ b/clients/linux/src/cli.rs
@@ -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
diff --git a/clients/linux/src/main.rs b/clients/linux/src/main.rs
index 5c8d37b0..f5f04a89 100644
--- a/clients/linux/src/main.rs
+++ b/clients/linux/src/main.rs
@@ -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;
diff --git a/clients/linux/src/ui_hosts.rs b/clients/linux/src/ui_hosts.rs
index 597c6e93..ac538172 100644
--- a/clients/linux/src/ui_hosts.rs
+++ b/clients/linux/src/ui_hosts.rs
@@ -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--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 {
+ 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 {
diff --git a/clients/session/Cargo.toml b/clients/session/Cargo.toml
index 1d8567c8..2f19a15f 100644
--- a/clients/session/Cargo.toml
+++ b/clients/session/Cargo.toml
@@ -68,5 +68,10 @@ serde_json = "1"
[target.'cfg(windows)'.build-dependencies]
winresource = "0.1"
+# Compiles data/ (the OS-mark symbolic icons) into the embedded gresource (build.rs) —
+# needs `glib-compile-resources`, which ships with the GTK dev stack this crate needs anyway.
+[target.'cfg(target_os = "linux")'.build-dependencies]
+glib-build-tools = "0.22"
+
[lints]
workspace = true
diff --git a/clients/session/build.rs b/clients/session/build.rs
index 17c1d63c..1bd2e46c 100644
--- a/clients/session/build.rs
+++ b/clients/session/build.rs
@@ -4,6 +4,18 @@
//! icon is the generic default).
fn main() {
+ // Linux: compile the shell's embedded assets (`data/` — the host-card OS-mark symbolic
+ // icons) into a gresource bundle, registered at startup via
+ // `gio::resources_register_include!`. Host-gated like the Windows leg below.
+ #[cfg(target_os = "linux")]
+ if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") {
+ glib_build_tools::compile_resources(
+ &["data"],
+ "data/resources.gresource.xml",
+ "punktfunk-client.gresource",
+ );
+ }
+
// cfg(windows) is the HOST (skips Linux/macOS builds of this cross-platform binary);
// CARGO_CFG_WINDOWS is the TARGET (x64 and cross-compiled ARM64 both pass).
#[cfg(windows)]
diff --git a/clients/session/data/icons/scalable/actions/pf-os-apple-symbolic.svg b/clients/session/data/icons/scalable/actions/pf-os-apple-symbolic.svg
new file mode 100644
index 00000000..2af51e1a
--- /dev/null
+++ b/clients/session/data/icons/scalable/actions/pf-os-apple-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/session/data/icons/scalable/actions/pf-os-arch-symbolic.svg b/clients/session/data/icons/scalable/actions/pf-os-arch-symbolic.svg
new file mode 100644
index 00000000..458911f1
--- /dev/null
+++ b/clients/session/data/icons/scalable/actions/pf-os-arch-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/session/data/icons/scalable/actions/pf-os-debian-symbolic.svg b/clients/session/data/icons/scalable/actions/pf-os-debian-symbolic.svg
new file mode 100644
index 00000000..9b82e206
--- /dev/null
+++ b/clients/session/data/icons/scalable/actions/pf-os-debian-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/session/data/icons/scalable/actions/pf-os-fedora-symbolic.svg b/clients/session/data/icons/scalable/actions/pf-os-fedora-symbolic.svg
new file mode 100644
index 00000000..428abe6a
--- /dev/null
+++ b/clients/session/data/icons/scalable/actions/pf-os-fedora-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/session/data/icons/scalable/actions/pf-os-linux-symbolic.svg b/clients/session/data/icons/scalable/actions/pf-os-linux-symbolic.svg
new file mode 100644
index 00000000..0f806c24
--- /dev/null
+++ b/clients/session/data/icons/scalable/actions/pf-os-linux-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/session/data/icons/scalable/actions/pf-os-nixos-symbolic.svg b/clients/session/data/icons/scalable/actions/pf-os-nixos-symbolic.svg
new file mode 100644
index 00000000..cc34d519
--- /dev/null
+++ b/clients/session/data/icons/scalable/actions/pf-os-nixos-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/session/data/icons/scalable/actions/pf-os-opensuse-symbolic.svg b/clients/session/data/icons/scalable/actions/pf-os-opensuse-symbolic.svg
new file mode 100644
index 00000000..e52617ec
--- /dev/null
+++ b/clients/session/data/icons/scalable/actions/pf-os-opensuse-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/session/data/icons/scalable/actions/pf-os-steam-symbolic.svg b/clients/session/data/icons/scalable/actions/pf-os-steam-symbolic.svg
new file mode 100644
index 00000000..db35d17b
--- /dev/null
+++ b/clients/session/data/icons/scalable/actions/pf-os-steam-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/session/data/icons/scalable/actions/pf-os-ubuntu-symbolic.svg b/clients/session/data/icons/scalable/actions/pf-os-ubuntu-symbolic.svg
new file mode 100644
index 00000000..7bb9ce4f
--- /dev/null
+++ b/clients/session/data/icons/scalable/actions/pf-os-ubuntu-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/session/data/icons/scalable/actions/pf-os-windows-symbolic.svg b/clients/session/data/icons/scalable/actions/pf-os-windows-symbolic.svg
new file mode 100644
index 00000000..f7f972bf
--- /dev/null
+++ b/clients/session/data/icons/scalable/actions/pf-os-windows-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/clients/session/data/resources.gresource.xml b/clients/session/data/resources.gresource.xml
new file mode 100644
index 00000000..660e71e4
--- /dev/null
+++ b/clients/session/data/resources.gresource.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ icons/scalable/actions/pf-os-windows-symbolic.svg
+ icons/scalable/actions/pf-os-apple-symbolic.svg
+ icons/scalable/actions/pf-os-linux-symbolic.svg
+ icons/scalable/actions/pf-os-steam-symbolic.svg
+ icons/scalable/actions/pf-os-ubuntu-symbolic.svg
+ icons/scalable/actions/pf-os-fedora-symbolic.svg
+ icons/scalable/actions/pf-os-arch-symbolic.svg
+ icons/scalable/actions/pf-os-debian-symbolic.svg
+ icons/scalable/actions/pf-os-nixos-symbolic.svg
+ icons/scalable/actions/pf-os-opensuse-symbolic.svg
+
+
diff --git a/clients/session/src/app.rs b/clients/session/src/app.rs
index e1c3f5e1..dd877a55 100644
--- a/clients/session/src/app.rs
+++ b/clients/session/src/app.rs
@@ -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);
diff --git a/clients/session/src/cli.rs b/clients/session/src/cli.rs
index 89889fa5..4946aa56 100644
--- a/clients/session/src/cli.rs
+++ b/clients/session/src/cli.rs
@@ -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
diff --git a/clients/session/src/console.rs b/clients/session/src/console.rs
index 0c24fc32..51aea1a2 100644
--- a/clients/session/src/console.rs
+++ b/clients/session/src/console.rs
@@ -78,6 +78,7 @@ pub fn run(target: Option<&str>) -> u8 {
.unwrap_or(library::DEFAULT_MGMT_PORT),
can_wake: false,
last_used: k.and_then(|h| h.last_used),
+ os: k.map(|h| h.os.clone()).unwrap_or_default(),
};
let label = row.name.clone();
if k.is_none() {
@@ -295,6 +296,7 @@ fn fake_host_row() -> HostRow {
mgmt_port: library::DEFAULT_MGMT_PORT,
can_wake: false,
last_used: None,
+ os: "linux/arch/steamos".into(),
}
}
@@ -570,6 +572,10 @@ impl ServiceState {
.unwrap_or(library::DEFAULT_MGMT_PORT),
can_wake: !online && !h.mac.is_empty(),
last_used: h.last_used,
+ os: advert
+ .filter(|d| !d.os.is_empty())
+ .map(|d| d.os.clone())
+ .unwrap_or_else(|| h.os.clone()),
}
})
.collect();
@@ -600,6 +606,7 @@ impl ServiceState {
mgmt_port: d.mgmt_port.unwrap_or(library::DEFAULT_MGMT_PORT),
can_wake: false,
last_used: None,
+ os: d.os.clone(),
})
.collect();
extra.sort_by(|a, b| a.name.cmp(&b.name));
diff --git a/clients/session/src/main.rs b/clients/session/src/main.rs
index 5c8d37b0..f5f04a89 100644
--- a/clients/session/src/main.rs
+++ b/clients/session/src/main.rs
@@ -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;
diff --git a/clients/session/src/ui_hosts.rs b/clients/session/src/ui_hosts.rs
index 4a425f51..6ac134ff 100644
--- a/clients/session/src/ui_hosts.rs
+++ b/clients/session/src/ui_hosts.rs
@@ -216,6 +216,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");
@@ -579,6 +588,28 @@ impl relm4::factory::FactoryComponent for HostCard {
/// How long each saved-host reachability probe waits, and how often the sweep runs. The pip
/// reads `advertising OR probed-reachable`, so a host reached only over a routed network
/// (Tailscale/VPN) — which never appears on mDNS — still shows Online.
+/// The OS-icon tokens this shell ships symbolic art for (`data/icons/.../pf-os--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 {
+ 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)
+}
+
const PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(2500);
const PROBE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(12);
@@ -1015,6 +1046,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 {
diff --git a/clients/windows/assets/os/apple.png b/clients/windows/assets/os/apple.png
new file mode 100644
index 00000000..f0ab8f07
Binary files /dev/null and b/clients/windows/assets/os/apple.png differ
diff --git a/clients/windows/assets/os/arch.png b/clients/windows/assets/os/arch.png
new file mode 100644
index 00000000..f656b3b4
Binary files /dev/null and b/clients/windows/assets/os/arch.png differ
diff --git a/clients/windows/assets/os/debian.png b/clients/windows/assets/os/debian.png
new file mode 100644
index 00000000..6781b555
Binary files /dev/null and b/clients/windows/assets/os/debian.png differ
diff --git a/clients/windows/assets/os/fedora.png b/clients/windows/assets/os/fedora.png
new file mode 100644
index 00000000..9871c2c9
Binary files /dev/null and b/clients/windows/assets/os/fedora.png differ
diff --git a/clients/windows/assets/os/linux.png b/clients/windows/assets/os/linux.png
new file mode 100644
index 00000000..6bbd7b0b
Binary files /dev/null and b/clients/windows/assets/os/linux.png differ
diff --git a/clients/windows/assets/os/nixos.png b/clients/windows/assets/os/nixos.png
new file mode 100644
index 00000000..3d81fbe3
Binary files /dev/null and b/clients/windows/assets/os/nixos.png differ
diff --git a/clients/windows/assets/os/opensuse.png b/clients/windows/assets/os/opensuse.png
new file mode 100644
index 00000000..5709ea5c
Binary files /dev/null and b/clients/windows/assets/os/opensuse.png differ
diff --git a/clients/windows/assets/os/steam.png b/clients/windows/assets/os/steam.png
new file mode 100644
index 00000000..523baaba
Binary files /dev/null and b/clients/windows/assets/os/steam.png differ
diff --git a/clients/windows/assets/os/ubuntu.png b/clients/windows/assets/os/ubuntu.png
new file mode 100644
index 00000000..c3823a17
Binary files /dev/null and b/clients/windows/assets/os/ubuntu.png differ
diff --git a/clients/windows/assets/os/windows.png b/clients/windows/assets/os/windows.png
new file mode 100644
index 00000000..3da00a82
Binary files /dev/null and b/clients/windows/assets/os/windows.png differ
diff --git a/clients/windows/src/app/hosts.rs b/clients/windows/src/app/hosts.rs
index 452ffc75..6ec2a6ea 100644
--- a/clients/windows/src/app/hosts.rs
+++ b/clients/windows/src/app/hosts.rs
@@ -167,9 +167,10 @@ pub(crate) struct Hover {
pub(crate) set: AsyncSetState