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>
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<!-- apple — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaApple. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill="#000000"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 635 B |
@@ -0,0 +1,2 @@
|
||||
<!-- arch — from Simple Icons (CC0 1.0), via react-icons SiArchlinux. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M11.39.605C10.376 3.092 9.764 4.72 8.635 7.132c.693.734 1.543 1.589 2.923 2.554-1.484-.61-2.496-1.224-3.252-1.86C6.86 10.842 4.596 15.138 0 23.395c3.612-2.085 6.412-3.37 9.021-3.862a6.61 6.61 0 01-.171-1.547l.003-.115c.058-2.315 1.261-4.095 2.687-3.973 1.426.12 2.534 2.096 2.478 4.409a6.52 6.52 0 01-.146 1.243c2.58.505 5.352 1.787 8.914 3.844-.702-1.293-1.33-2.459-1.929-3.57-.943-.73-1.926-1.682-3.933-2.713 1.38.359 2.367.772 3.137 1.234-6.09-11.334-6.582-12.84-8.67-17.74zM22.898 21.36v-.623h-.234v-.084h.562v.084h-.234v.623h.331v-.707h.142l.167.5.034.107a2.26 2.26 0 01.038-.114l.17-.493H24v.707h-.091v-.593l-.206.593h-.084l-.205-.602v.602h-.091"/></svg>
|
||||
|
After Width: | Height: | Size: 836 B |
@@ -0,0 +1,2 @@
|
||||
<!-- debian — from Simple Icons (CC0 1.0), via react-icons SiDebian. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M13.88 12.685c-.4 0 .08.2.601.28.14-.1.27-.22.39-.33a3.001 3.001 0 01-.99.05m2.14-.53c.23-.33.4-.69.47-1.06-.06.27-.2.5-.33.73-.75.47-.07-.27 0-.56-.8 1.01-.11.6-.14.89m.781-2.05c.05-.721-.14-.501-.2-.221.07.04.13.5.2.22M12.38.31c.2.04.45.07.42.12.23-.05.28-.1-.43-.12m.43.12l-.15.03.14-.01V.43m6.633 9.944c.02.64-.2.95-.38 1.5l-.35.181c-.28.54.03.35-.17.78-.44.39-1.34 1.22-1.62 1.301-.201 0 .14-.25.19-.34-.591.4-.481.6-1.371.85l-.03-.06c-2.221 1.04-5.303-1.02-5.253-3.842-.03.17-.07.13-.12.2a3.551 3.552 0 012.001-3.501 3.361 3.362 0 013.732.48 3.341 3.342 0 00-2.721-1.3c-1.18.01-2.281.76-2.651 1.57-.6.38-.67 1.47-.93 1.661-.361 2.601.66 3.722 2.38 5.042.27.19.08.21.12.35a4.702 4.702 0 01-1.53-1.16c.23.33.47.66.8.91-.55-.18-1.27-1.3-1.48-1.35.93 1.66 3.78 2.921 5.261 2.3a6.203 6.203 0 01-2.33-.28c-.33-.16-.77-.51-.7-.57a5.802 5.803 0 005.902-.84c.44-.35.93-.94 1.07-.95-.2.32.04.16-.12.44.44-.72-.2-.3.46-1.24l.24.33c-.09-.6.74-1.321.66-2.262.19-.3.2.3 0 .97.29-.74.08-.85.15-1.46.08.2.18.42.23.63-.18-.7.2-1.2.28-1.6-.09-.05-.28.3-.32-.53 0-.37.1-.2.14-.28-.08-.05-.26-.32-.38-.861.08-.13.22.33.34.34-.08-.42-.2-.75-.2-1.08-.34-.68-.12.1-.4-.3-.34-1.091.3-.25.34-.74.54.77.84 1.96.981 2.46-.1-.6-.28-1.2-.49-1.76.16.07-.26-1.241.21-.37A7.823 7.824 0 0017.702 1.6c.18.17.42.39.33.42-.75-.45-.62-.48-.73-.67-.61-.25-.65.02-1.06 0C15.082.73 14.862.8 13.8.4l.05.23c-.77-.25-.9.1-1.73 0-.05-.04.27-.14.53-.18-.741.1-.701-.14-1.431.03.17-.13.36-.21.55-.32-.6.04-1.44.35-1.18.07C9.6.68 7.847 1.3 6.867 2.22L6.838 2c-.45.54-1.96 1.611-2.08 2.311l-.131.03c-.23.4-.38.85-.57 1.261-.3.52-.45.2-.4.28-.6 1.22-.9 2.251-1.16 3.102.18.27 0 1.65.07 2.76-.3 5.463 3.84 10.776 8.363 12.006.67.23 1.65.23 2.49.25-.99-.28-1.12-.15-2.08-.49-.7-.32-.85-.7-1.34-1.13l.2.35c-.971-.34-.57-.42-1.361-.67l.21-.27c-.31-.03-.83-.53-.97-.81l-.34.01c-.41-.501-.63-.871-.61-1.161l-.111.2c-.13-.21-1.52-1.901-.8-1.511-.13-.12-.31-.2-.5-.55l.14-.17c-.35-.44-.64-1.02-.62-1.2.2.24.32.3.45.33-.88-2.172-.93-.12-1.601-2.202l.15-.02c-.1-.16-.18-.34-.26-.51l.06-.6c-.63-.74-.18-3.102-.09-4.402.07-.54.53-1.1.88-1.981l-.21-.04c.4-.71 2.341-2.872 3.241-2.761.43-.55-.09 0-.18-.14.96-.991 1.26-.7 1.901-.88.7-.401-.6.16-.27-.151 1.2-.3.85-.7 2.421-.85.16.1-.39.14-.52.26 1-.49 3.151-.37 4.562.27 1.63.77 3.461 3.011 3.531 5.132l.08.02c-.04.85.13 1.821-.17 2.711l.2-.42M9.54 13.236l-.05.28c.26.35.47.73.8 1.01-.24-.47-.42-.66-.75-1.3m.62-.02c-.14-.15-.22-.34-.31-.52.08.32.26.6.43.88l-.12-.36m10.945-2.382l-.07.15c-.1.76-.34 1.511-.69 2.212.4-.73.65-1.541.75-2.362M12.45.12c.27-.1.66-.05.95-.12-.37.03-.74.05-1.1.1l.15.02M3.006 5.142c.07.57-.43.8.11.42.3-.66-.11-.18-.1-.42m-.64 2.661c.12-.39.15-.62.2-.84-.35.44-.17.53-.2.83"/></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- fedora — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaFedora. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="#000000"><path d="M225 32C101.3 31.7.8 131.7.4 255.4L0 425.7a53.6 53.6 0 0 0 53.6 53.9l170.2.4c123.7.3 224.3-99.7 224.6-223.4S348.7 32.3 225 32zm169.8 157.2L333 126.6c2.3-4.7 3.8-9.2 3.8-14.3v-1.6l55.2 56.1a101 101 0 0 1 2.8 22.4zM331 94.3a106.06 106.06 0 0 1 58.5 63.8l-54.3-54.6a26.48 26.48 0 0 0-4.2-9.2zM118.1 247.2a49.66 49.66 0 0 0-7.7 11.4l-8.5-8.5a85.78 85.78 0 0 1 16.2-2.9zM97 251.4l11.8 11.9-.9 8a34.74 34.74 0 0 0 2.4 12.5l-27-27.2a80.6 80.6 0 0 1 13.7-5.2zm-18.2 7.4l38.2 38.4a53.17 53.17 0 0 0-14.1 4.7L67.6 266a107 107 0 0 1 11.2-7.2zm-15.2 9.8l35.3 35.5a67.25 67.25 0 0 0-10.5 8.5L53.5 278a64.33 64.33 0 0 1 10.1-9.4zm-13.3 12.3l34.9 35a56.84 56.84 0 0 0-7.7 11.4l-35.8-35.9c2.8-3.8 5.7-7.2 8.6-10.5zm-11 14.3l36.4 36.6a48.29 48.29 0 0 0-3.6 15.2l-39.5-39.8a99.81 99.81 0 0 1 6.7-12zm-8.8 16.3l41.3 41.8a63.47 63.47 0 0 0 6.7 26.2L25.8 326c1.4-4.9 2.9-9.6 4.7-14.5zm-7.9 43l61.9 62.2a31.24 31.24 0 0 0-3.6 14.3v1.1l-55.4-55.7a88.27 88.27 0 0 1-2.9-21.9zm5.3 30.7l54.3 54.6a28.44 28.44 0 0 0 4.2 9.2 106.32 106.32 0 0 1-58.5-63.8zm-5.3-37a80.69 80.69 0 0 1 2.1-17l72.2 72.5a37.59 37.59 0 0 0-9.9 8.7zm253.3-51.8l-42.6-.1-.1 56c-.2 69.3-64.4 115.8-125.7 102.9-5.7 0-19.9-8.7-19.9-24.2a24.89 24.89 0 0 1 24.5-24.6c6.3 0 6.3 1.6 15.7 1.6a55.91 55.91 0 0 0 56.1-55.9l.1-47c0-4.5-4.5-9-8.9-9l-33.6-.1c-32.6-.1-32.5-49.4.1-49.3l42.6.1.1-56a105.18 105.18 0 0 1 105.6-105 86.35 86.35 0 0 1 20.2 2.3c11.2 1.8 19.9 11.9 19.9 24 0 15.5-14.9 27.8-30.3 23.9-27.4-5.9-65.9 14.4-66 54.9l-.1 47a8.94 8.94 0 0 0 8.9 9l33.6.1c32.5.2 32.4 49.5-.2 49.4zm23.5-.3a35.58 35.58 0 0 0 7.6-11.4l8.5 8.5a102 102 0 0 1-16.1 2.9zm21-4.2L308.6 280l.9-8.1a34.74 34.74 0 0 0-2.4-12.5l27 27.2a74.89 74.89 0 0 1-13.7 5.3zm18-7.4l-38-38.4c4.9-1.1 9.6-2.4 13.7-4.7l36.2 35.9c-3.8 2.5-7.9 5-11.9 7.2zm15.5-9.8l-35.3-35.5a61.06 61.06 0 0 0 10.5-8.5l34.9 35a124.56 124.56 0 0 1-10.1 9zm13.2-12.3l-34.9-35a63.18 63.18 0 0 0 7.7-11.4l35.8 35.9a130.28 130.28 0 0 1-8.6 10.5zm11-14.3l-36.4-36.6a48.29 48.29 0 0 0 3.6-15.2l39.5 39.8a87.72 87.72 0 0 1-6.7 12zm13.5-30.9a140.63 140.63 0 0 1-4.7 14.3L345.6 190a58.19 58.19 0 0 0-7.1-26.2zm1-5.6l-71.9-72.1a32 32 0 0 0 9.9-9.2l64.3 64.7a90.93 90.93 0 0 1-2.3 16.6z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- linux — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaLinux. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="#000000"><path d="M220.8 123.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5.2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4.2-.8.7-.6 1.1.3 1.3 2.3 1.1 3.4 1.7zm-21.9 1.7c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6.2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5.1-1.3.6-3.4 1.5-3.2 2.9.1 1 1.8 1.5 2.8 1.4zM420 403.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72C311.1 85.4 315.7.1 234.8 0 132.4-.2 158 103.4 156.9 135.2c-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9.6 7.9 1.2 11.8 1.2 8.1 2.5 15.7.8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1.6 2.3 1.4 4.6 2.5 6.7v.1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3.4-8.2-4.4-17.3-15.5-29.7zM223.7 87.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8zM183 75.8c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4.7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1zm-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3-1.4-4.9 0-9 4.2-12.3zm3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4v-.2c2.4-7.6.6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9zm-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7.8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4.6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1.8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7.4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2zm257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6.8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1.3-.2.7-.3 1-.5.8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6zM173.3 148.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- nixos — from Simple Icons (CC0 1.0), via react-icons SiNixos. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M7.352 1.592l-1.364.002L5.32 2.75l1.557 2.713-3.137-.008-1.32 2.34H14.11l-1.353-2.332-3.192-.006-2.214-3.865zm6.175 0l-2.687.025 5.846 10.127 1.341-2.34-1.59-2.765 2.24-3.85-.683-1.182h-1.336l-1.57 2.705-1.56-2.72zm6.887 4.195l-5.846 10.125 2.696-.008 1.601-2.76 4.453.016.682-1.183-.666-1.157-3.13-.008L21.778 8.1l-1.365-2.313zM9.432 8.086l-2.696.008-1.601 2.76-4.453-.016L0 12.02l.666 1.157 3.13.008-1.575 2.71 1.365 2.315L9.432 8.086zM7.33 12.25l-.006.01-.002-.004-1.342 2.34 1.59 2.765-2.24 3.85.684 1.182H7.35l.004-.006h.001l1.567-2.698 1.558 2.72 2.688-.026-.004-.006h.01L7.33 12.25zm2.55 3.93l1.354 2.332 3.192.006 2.215 3.865 1.363-.002.668-1.156-1.557-2.713 3.137.008 1.32-2.34H9.881Z"/></svg>
|
||||
|
After Width: | Height: | Size: 875 B |
@@ -0,0 +1,2 @@
|
||||
<!-- opensuse — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaSuse. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="#000000"><path d="M471.08 102.66s-.3 18.3-.3 20.3c-9.1-3-74.4-24.1-135.7-26.3-51.9-1.8-122.8-4.3-223 57.3-19.4 12.4-73.9 46.1-99.6 109.7C7 277-.12 307 7 335.06a111 111 0 0 0 16.5 35.7c17.4 25 46.6 41.6 78.1 44.4 44.4 3.9 78.1-16 90-53.3 8.2-25.8 0-63.6-31.5-82.9-25.6-15.7-53.3-12.1-69.2-1.6-13.9 9.2-21.8 23.5-21.6 39.2.3 27.8 24.3 42.6 41.5 42.6a49 49 0 0 0 15.8-2.7c6.5-1.8 13.3-6.5 13.3-14.9 0-12.1-11.6-14.8-16.8-13.9-2.9.5-4.5 2-11.8 2.4-2-.2-12-3.1-12-14V316c.2-12.3 13.2-18 25.5-16.9 32.3 2.8 47.7 40.7 28.5 65.7-18.3 23.7-76.6 23.2-99.7-20.4-26-49.2 12.7-111.2 87-98.4 33.2 5.7 83.6 35.5 102.4 104.3h45.9c-5.7-17.6-8.9-68.3 42.7-68.3 56.7 0 63.9 39.9 79.8 68.3H460c-12.8-18.3-21.7-38.7-18.9-55.8 5.6-33.8 39.7-18.4 82.4-17.4 66.5.4 102.1-27 103.1-28 3.7-3.1 6.5-15.8 7-17.7 1.3-5.1-3.2-2.4-3.2-2.4-8.7 5.2-30.5 15.2-50.9 15.6-25.3.5-76.2-25.4-81.6-28.2-.3-.4.1 1.2-11-25.5 88.4 58.3 118.3 40.5 145.2 21.7.8-.6 4.3-2.9 3.6-5.7-13.8-48.1-22.4-62.7-34.5-69.6-37-21.6-125-34.7-129.2-35.3.1-.1-.9-.3-.9.7zm60.4 72.8a37.54 37.54 0 0 1 38.9-36.3c33.4 1.2 48.8 42.3 24.4 65.2-24.2 22.7-64.4 4.6-63.3-28.9zm38.6-25.3a26.27 26.27 0 1 0 25.4 27.2 26.19 26.19 0 0 0-25.4-27.2zm4.3 28.8c-15.4 0-15.4-15.6 0-15.6s15.4 15.64 0 15.64z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- steam — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaSteam. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512" fill="#000000"><path d="M496 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5v1.2L176.6 279c-15.5-.9-30.7 3.4-43.5 12.1L0 236.1C10.2 108.4 117.1 8 247.6 8 384.8 8 496 119 496 256zM155.7 384.3l-30.5-12.6a52.79 52.79 0 0 0 27.2 25.8c26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3.1-40.3-5.4-13-15.5-23.2-28.5-28.6-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zm173.8-129.9c-34.4 0-62.4-28-62.4-62.3s28-62.3 62.4-62.3 62.4 28 62.4 62.3-27.9 62.3-62.4 62.3zm.1-15.6c25.9 0 46.9-21 46.9-46.8 0-25.9-21-46.8-46.9-46.8s-46.9 21-46.9 46.8c.1 25.8 21.1 46.8 46.9 46.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 932 B |
@@ -0,0 +1,2 @@
|
||||
<!-- ubuntu — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaUbuntu. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512" fill="#000000"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm52.7 93c8.8-15.2 28.3-20.5 43.5-11.7 15.3 8.8 20.5 28.3 11.7 43.6-8.8 15.2-28.3 20.5-43.5 11.7-15.3-8.9-20.5-28.4-11.7-43.6zM87.4 287.9c-17.6 0-31.9-14.3-31.9-31.9 0-17.6 14.3-31.9 31.9-31.9 17.6 0 31.9 14.3 31.9 31.9 0 17.6-14.3 31.9-31.9 31.9zm28.1 3.1c22.3-17.9 22.4-51.9 0-69.9 8.6-32.8 29.1-60.7 56.5-79.1l23.7 39.6c-51.5 36.3-51.5 112.5 0 148.8L172 370c-27.4-18.3-47.8-46.3-56.5-79zm228.7 131.7c-15.3 8.8-34.7 3.6-43.5-11.7-8.8-15.3-3.6-34.8 11.7-43.6 15.2-8.8 34.7-3.6 43.5 11.7 8.8 15.3 3.6 34.8-11.7 43.6zm.3-69.5c-26.7-10.3-56.1 6.6-60.5 35-5.2 1.4-48.9 14.3-96.7-9.4l22.5-40.3c57 26.5 123.4-11.7 128.9-74.4l46.1.7c-2.3 34.5-17.3 65.5-40.3 88.4zm-5.9-105.3c-5.4-62-71.3-101.2-128.9-74.4l-22.5-40.3c47.9-23.7 91.5-10.8 96.7-9.4 4.4 28.3 33.8 45.3 60.5 35 23.1 22.9 38 53.9 40.2 88.5l-46 .6z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- windows — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaWindows. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="#000000"><path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z"/></svg>
|
||||
|
After Width: | Height: | Size: 339 B |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- The shell's embedded icon assets: the host-card OS marks (derived from the
|
||||
assets/os-icons masters; see that directory's README for provenance/licensing).
|
||||
Registered under the hicolor-style layout IconTheme::add_resource_path expects. -->
|
||||
<gresources>
|
||||
<gresource prefix="/io/unom/Punktfunk">
|
||||
<file>icons/scalable/actions/pf-os-windows-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-apple-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-linux-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-steam-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-ubuntu-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-fedora-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-arch-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-debian-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-nixos-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-opensuse-symbolic.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<!-- apple — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaApple. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill="#000000"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 635 B |
@@ -0,0 +1,2 @@
|
||||
<!-- arch — from Simple Icons (CC0 1.0), via react-icons SiArchlinux. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M11.39.605C10.376 3.092 9.764 4.72 8.635 7.132c.693.734 1.543 1.589 2.923 2.554-1.484-.61-2.496-1.224-3.252-1.86C6.86 10.842 4.596 15.138 0 23.395c3.612-2.085 6.412-3.37 9.021-3.862a6.61 6.61 0 01-.171-1.547l.003-.115c.058-2.315 1.261-4.095 2.687-3.973 1.426.12 2.534 2.096 2.478 4.409a6.52 6.52 0 01-.146 1.243c2.58.505 5.352 1.787 8.914 3.844-.702-1.293-1.33-2.459-1.929-3.57-.943-.73-1.926-1.682-3.933-2.713 1.38.359 2.367.772 3.137 1.234-6.09-11.334-6.582-12.84-8.67-17.74zM22.898 21.36v-.623h-.234v-.084h.562v.084h-.234v.623h.331v-.707h.142l.167.5.034.107a2.26 2.26 0 01.038-.114l.17-.493H24v.707h-.091v-.593l-.206.593h-.084l-.205-.602v.602h-.091"/></svg>
|
||||
|
After Width: | Height: | Size: 836 B |
@@ -0,0 +1,2 @@
|
||||
<!-- debian — from Simple Icons (CC0 1.0), via react-icons SiDebian. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M13.88 12.685c-.4 0 .08.2.601.28.14-.1.27-.22.39-.33a3.001 3.001 0 01-.99.05m2.14-.53c.23-.33.4-.69.47-1.06-.06.27-.2.5-.33.73-.75.47-.07-.27 0-.56-.8 1.01-.11.6-.14.89m.781-2.05c.05-.721-.14-.501-.2-.221.07.04.13.5.2.22M12.38.31c.2.04.45.07.42.12.23-.05.28-.1-.43-.12m.43.12l-.15.03.14-.01V.43m6.633 9.944c.02.64-.2.95-.38 1.5l-.35.181c-.28.54.03.35-.17.78-.44.39-1.34 1.22-1.62 1.301-.201 0 .14-.25.19-.34-.591.4-.481.6-1.371.85l-.03-.06c-2.221 1.04-5.303-1.02-5.253-3.842-.03.17-.07.13-.12.2a3.551 3.552 0 012.001-3.501 3.361 3.362 0 013.732.48 3.341 3.342 0 00-2.721-1.3c-1.18.01-2.281.76-2.651 1.57-.6.38-.67 1.47-.93 1.661-.361 2.601.66 3.722 2.38 5.042.27.19.08.21.12.35a4.702 4.702 0 01-1.53-1.16c.23.33.47.66.8.91-.55-.18-1.27-1.3-1.48-1.35.93 1.66 3.78 2.921 5.261 2.3a6.203 6.203 0 01-2.33-.28c-.33-.16-.77-.51-.7-.57a5.802 5.803 0 005.902-.84c.44-.35.93-.94 1.07-.95-.2.32.04.16-.12.44.44-.72-.2-.3.46-1.24l.24.33c-.09-.6.74-1.321.66-2.262.19-.3.2.3 0 .97.29-.74.08-.85.15-1.46.08.2.18.42.23.63-.18-.7.2-1.2.28-1.6-.09-.05-.28.3-.32-.53 0-.37.1-.2.14-.28-.08-.05-.26-.32-.38-.861.08-.13.22.33.34.34-.08-.42-.2-.75-.2-1.08-.34-.68-.12.1-.4-.3-.34-1.091.3-.25.34-.74.54.77.84 1.96.981 2.46-.1-.6-.28-1.2-.49-1.76.16.07-.26-1.241.21-.37A7.823 7.824 0 0017.702 1.6c.18.17.42.39.33.42-.75-.45-.62-.48-.73-.67-.61-.25-.65.02-1.06 0C15.082.73 14.862.8 13.8.4l.05.23c-.77-.25-.9.1-1.73 0-.05-.04.27-.14.53-.18-.741.1-.701-.14-1.431.03.17-.13.36-.21.55-.32-.6.04-1.44.35-1.18.07C9.6.68 7.847 1.3 6.867 2.22L6.838 2c-.45.54-1.96 1.611-2.08 2.311l-.131.03c-.23.4-.38.85-.57 1.261-.3.52-.45.2-.4.28-.6 1.22-.9 2.251-1.16 3.102.18.27 0 1.65.07 2.76-.3 5.463 3.84 10.776 8.363 12.006.67.23 1.65.23 2.49.25-.99-.28-1.12-.15-2.08-.49-.7-.32-.85-.7-1.34-1.13l.2.35c-.971-.34-.57-.42-1.361-.67l.21-.27c-.31-.03-.83-.53-.97-.81l-.34.01c-.41-.501-.63-.871-.61-1.161l-.111.2c-.13-.21-1.52-1.901-.8-1.511-.13-.12-.31-.2-.5-.55l.14-.17c-.35-.44-.64-1.02-.62-1.2.2.24.32.3.45.33-.88-2.172-.93-.12-1.601-2.202l.15-.02c-.1-.16-.18-.34-.26-.51l.06-.6c-.63-.74-.18-3.102-.09-4.402.07-.54.53-1.1.88-1.981l-.21-.04c.4-.71 2.341-2.872 3.241-2.761.43-.55-.09 0-.18-.14.96-.991 1.26-.7 1.901-.88.7-.401-.6.16-.27-.151 1.2-.3.85-.7 2.421-.85.16.1-.39.14-.52.26 1-.49 3.151-.37 4.562.27 1.63.77 3.461 3.011 3.531 5.132l.08.02c-.04.85.13 1.821-.17 2.711l.2-.42M9.54 13.236l-.05.28c.26.35.47.73.8 1.01-.24-.47-.42-.66-.75-1.3m.62-.02c-.14-.15-.22-.34-.31-.52.08.32.26.6.43.88l-.12-.36m10.945-2.382l-.07.15c-.1.76-.34 1.511-.69 2.212.4-.73.65-1.541.75-2.362M12.45.12c.27-.1.66-.05.95-.12-.37.03-.74.05-1.1.1l.15.02M3.006 5.142c.07.57-.43.8.11.42.3-.66-.11-.18-.1-.42m-.64 2.661c.12-.39.15-.62.2-.84-.35.44-.17.53-.2.83"/></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- fedora — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaFedora. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="#000000"><path d="M225 32C101.3 31.7.8 131.7.4 255.4L0 425.7a53.6 53.6 0 0 0 53.6 53.9l170.2.4c123.7.3 224.3-99.7 224.6-223.4S348.7 32.3 225 32zm169.8 157.2L333 126.6c2.3-4.7 3.8-9.2 3.8-14.3v-1.6l55.2 56.1a101 101 0 0 1 2.8 22.4zM331 94.3a106.06 106.06 0 0 1 58.5 63.8l-54.3-54.6a26.48 26.48 0 0 0-4.2-9.2zM118.1 247.2a49.66 49.66 0 0 0-7.7 11.4l-8.5-8.5a85.78 85.78 0 0 1 16.2-2.9zM97 251.4l11.8 11.9-.9 8a34.74 34.74 0 0 0 2.4 12.5l-27-27.2a80.6 80.6 0 0 1 13.7-5.2zm-18.2 7.4l38.2 38.4a53.17 53.17 0 0 0-14.1 4.7L67.6 266a107 107 0 0 1 11.2-7.2zm-15.2 9.8l35.3 35.5a67.25 67.25 0 0 0-10.5 8.5L53.5 278a64.33 64.33 0 0 1 10.1-9.4zm-13.3 12.3l34.9 35a56.84 56.84 0 0 0-7.7 11.4l-35.8-35.9c2.8-3.8 5.7-7.2 8.6-10.5zm-11 14.3l36.4 36.6a48.29 48.29 0 0 0-3.6 15.2l-39.5-39.8a99.81 99.81 0 0 1 6.7-12zm-8.8 16.3l41.3 41.8a63.47 63.47 0 0 0 6.7 26.2L25.8 326c1.4-4.9 2.9-9.6 4.7-14.5zm-7.9 43l61.9 62.2a31.24 31.24 0 0 0-3.6 14.3v1.1l-55.4-55.7a88.27 88.27 0 0 1-2.9-21.9zm5.3 30.7l54.3 54.6a28.44 28.44 0 0 0 4.2 9.2 106.32 106.32 0 0 1-58.5-63.8zm-5.3-37a80.69 80.69 0 0 1 2.1-17l72.2 72.5a37.59 37.59 0 0 0-9.9 8.7zm253.3-51.8l-42.6-.1-.1 56c-.2 69.3-64.4 115.8-125.7 102.9-5.7 0-19.9-8.7-19.9-24.2a24.89 24.89 0 0 1 24.5-24.6c6.3 0 6.3 1.6 15.7 1.6a55.91 55.91 0 0 0 56.1-55.9l.1-47c0-4.5-4.5-9-8.9-9l-33.6-.1c-32.6-.1-32.5-49.4.1-49.3l42.6.1.1-56a105.18 105.18 0 0 1 105.6-105 86.35 86.35 0 0 1 20.2 2.3c11.2 1.8 19.9 11.9 19.9 24 0 15.5-14.9 27.8-30.3 23.9-27.4-5.9-65.9 14.4-66 54.9l-.1 47a8.94 8.94 0 0 0 8.9 9l33.6.1c32.5.2 32.4 49.5-.2 49.4zm23.5-.3a35.58 35.58 0 0 0 7.6-11.4l8.5 8.5a102 102 0 0 1-16.1 2.9zm21-4.2L308.6 280l.9-8.1a34.74 34.74 0 0 0-2.4-12.5l27 27.2a74.89 74.89 0 0 1-13.7 5.3zm18-7.4l-38-38.4c4.9-1.1 9.6-2.4 13.7-4.7l36.2 35.9c-3.8 2.5-7.9 5-11.9 7.2zm15.5-9.8l-35.3-35.5a61.06 61.06 0 0 0 10.5-8.5l34.9 35a124.56 124.56 0 0 1-10.1 9zm13.2-12.3l-34.9-35a63.18 63.18 0 0 0 7.7-11.4l35.8 35.9a130.28 130.28 0 0 1-8.6 10.5zm11-14.3l-36.4-36.6a48.29 48.29 0 0 0 3.6-15.2l39.5 39.8a87.72 87.72 0 0 1-6.7 12zm13.5-30.9a140.63 140.63 0 0 1-4.7 14.3L345.6 190a58.19 58.19 0 0 0-7.1-26.2zm1-5.6l-71.9-72.1a32 32 0 0 0 9.9-9.2l64.3 64.7a90.93 90.93 0 0 1-2.3 16.6z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- linux — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaLinux. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="#000000"><path d="M220.8 123.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5.2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4.2-.8.7-.6 1.1.3 1.3 2.3 1.1 3.4 1.7zm-21.9 1.7c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6.2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5.1-1.3.6-3.4 1.5-3.2 2.9.1 1 1.8 1.5 2.8 1.4zM420 403.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72C311.1 85.4 315.7.1 234.8 0 132.4-.2 158 103.4 156.9 135.2c-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9.6 7.9 1.2 11.8 1.2 8.1 2.5 15.7.8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1.6 2.3 1.4 4.6 2.5 6.7v.1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3.4-8.2-4.4-17.3-15.5-29.7zM223.7 87.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8zM183 75.8c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4.7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1zm-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3-1.4-4.9 0-9 4.2-12.3zm3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4v-.2c2.4-7.6.6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9zm-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7.8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4.6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1.8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7.4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2zm257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6.8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1.3-.2.7-.3 1-.5.8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6zM173.3 148.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- nixos — from Simple Icons (CC0 1.0), via react-icons SiNixos. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M7.352 1.592l-1.364.002L5.32 2.75l1.557 2.713-3.137-.008-1.32 2.34H14.11l-1.353-2.332-3.192-.006-2.214-3.865zm6.175 0l-2.687.025 5.846 10.127 1.341-2.34-1.59-2.765 2.24-3.85-.683-1.182h-1.336l-1.57 2.705-1.56-2.72zm6.887 4.195l-5.846 10.125 2.696-.008 1.601-2.76 4.453.016.682-1.183-.666-1.157-3.13-.008L21.778 8.1l-1.365-2.313zM9.432 8.086l-2.696.008-1.601 2.76-4.453-.016L0 12.02l.666 1.157 3.13.008-1.575 2.71 1.365 2.315L9.432 8.086zM7.33 12.25l-.006.01-.002-.004-1.342 2.34 1.59 2.765-2.24 3.85.684 1.182H7.35l.004-.006h.001l1.567-2.698 1.558 2.72 2.688-.026-.004-.006h.01L7.33 12.25zm2.55 3.93l1.354 2.332 3.192.006 2.215 3.865 1.363-.002.668-1.156-1.557-2.713 3.137.008 1.32-2.34H9.881Z"/></svg>
|
||||
|
After Width: | Height: | Size: 875 B |
@@ -0,0 +1,2 @@
|
||||
<!-- opensuse — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaSuse. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="#000000"><path d="M471.08 102.66s-.3 18.3-.3 20.3c-9.1-3-74.4-24.1-135.7-26.3-51.9-1.8-122.8-4.3-223 57.3-19.4 12.4-73.9 46.1-99.6 109.7C7 277-.12 307 7 335.06a111 111 0 0 0 16.5 35.7c17.4 25 46.6 41.6 78.1 44.4 44.4 3.9 78.1-16 90-53.3 8.2-25.8 0-63.6-31.5-82.9-25.6-15.7-53.3-12.1-69.2-1.6-13.9 9.2-21.8 23.5-21.6 39.2.3 27.8 24.3 42.6 41.5 42.6a49 49 0 0 0 15.8-2.7c6.5-1.8 13.3-6.5 13.3-14.9 0-12.1-11.6-14.8-16.8-13.9-2.9.5-4.5 2-11.8 2.4-2-.2-12-3.1-12-14V316c.2-12.3 13.2-18 25.5-16.9 32.3 2.8 47.7 40.7 28.5 65.7-18.3 23.7-76.6 23.2-99.7-20.4-26-49.2 12.7-111.2 87-98.4 33.2 5.7 83.6 35.5 102.4 104.3h45.9c-5.7-17.6-8.9-68.3 42.7-68.3 56.7 0 63.9 39.9 79.8 68.3H460c-12.8-18.3-21.7-38.7-18.9-55.8 5.6-33.8 39.7-18.4 82.4-17.4 66.5.4 102.1-27 103.1-28 3.7-3.1 6.5-15.8 7-17.7 1.3-5.1-3.2-2.4-3.2-2.4-8.7 5.2-30.5 15.2-50.9 15.6-25.3.5-76.2-25.4-81.6-28.2-.3-.4.1 1.2-11-25.5 88.4 58.3 118.3 40.5 145.2 21.7.8-.6 4.3-2.9 3.6-5.7-13.8-48.1-22.4-62.7-34.5-69.6-37-21.6-125-34.7-129.2-35.3.1-.1-.9-.3-.9.7zm60.4 72.8a37.54 37.54 0 0 1 38.9-36.3c33.4 1.2 48.8 42.3 24.4 65.2-24.2 22.7-64.4 4.6-63.3-28.9zm38.6-25.3a26.27 26.27 0 1 0 25.4 27.2 26.19 26.19 0 0 0-25.4-27.2zm4.3 28.8c-15.4 0-15.4-15.6 0-15.6s15.4 15.64 0 15.64z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- steam — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaSteam. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512" fill="#000000"><path d="M496 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5v1.2L176.6 279c-15.5-.9-30.7 3.4-43.5 12.1L0 236.1C10.2 108.4 117.1 8 247.6 8 384.8 8 496 119 496 256zM155.7 384.3l-30.5-12.6a52.79 52.79 0 0 0 27.2 25.8c26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3.1-40.3-5.4-13-15.5-23.2-28.5-28.6-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zm173.8-129.9c-34.4 0-62.4-28-62.4-62.3s28-62.3 62.4-62.3 62.4 28 62.4 62.3-27.9 62.3-62.4 62.3zm.1-15.6c25.9 0 46.9-21 46.9-46.8 0-25.9-21-46.8-46.9-46.8s-46.9 21-46.9 46.8c.1 25.8 21.1 46.8 46.9 46.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 932 B |
@@ -0,0 +1,2 @@
|
||||
<!-- ubuntu — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaUbuntu. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512" fill="#000000"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm52.7 93c8.8-15.2 28.3-20.5 43.5-11.7 15.3 8.8 20.5 28.3 11.7 43.6-8.8 15.2-28.3 20.5-43.5 11.7-15.3-8.9-20.5-28.4-11.7-43.6zM87.4 287.9c-17.6 0-31.9-14.3-31.9-31.9 0-17.6 14.3-31.9 31.9-31.9 17.6 0 31.9 14.3 31.9 31.9 0 17.6-14.3 31.9-31.9 31.9zm28.1 3.1c22.3-17.9 22.4-51.9 0-69.9 8.6-32.8 29.1-60.7 56.5-79.1l23.7 39.6c-51.5 36.3-51.5 112.5 0 148.8L172 370c-27.4-18.3-47.8-46.3-56.5-79zm228.7 131.7c-15.3 8.8-34.7 3.6-43.5-11.7-8.8-15.3-3.6-34.8 11.7-43.6 15.2-8.8 34.7-3.6 43.5 11.7 8.8 15.3 3.6 34.8-11.7 43.6zm.3-69.5c-26.7-10.3-56.1 6.6-60.5 35-5.2 1.4-48.9 14.3-96.7-9.4l22.5-40.3c57 26.5 123.4-11.7 128.9-74.4l46.1.7c-2.3 34.5-17.3 65.5-40.3 88.4zm-5.9-105.3c-5.4-62-71.3-101.2-128.9-74.4l-22.5-40.3c47.9-23.7 91.5-10.8 96.7-9.4 4.4 28.3 33.8 45.3 60.5 35 23.1 22.9 38 53.9 40.2 88.5l-46 .6z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,2 @@
|
||||
<!-- windows — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaWindows. See README.md. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="#000000"><path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z"/></svg>
|
||||
|
After Width: | Height: | Size: 339 B |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- The shell's embedded icon assets: the host-card OS marks (derived from the
|
||||
assets/os-icons masters; see that directory's README for provenance/licensing).
|
||||
Registered under the hicolor-style layout IconTheme::add_resource_path expects. -->
|
||||
<gresources>
|
||||
<gresource prefix="/io/unom/Punktfunk">
|
||||
<file>icons/scalable/actions/pf-os-windows-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-apple-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-linux-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-steam-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-ubuntu-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-fedora-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-arch-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-debian-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-nixos-symbolic.svg</file>
|
||||
<file>icons/scalable/actions/pf-os-opensuse-symbolic.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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-<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)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
|
After Width: | Height: | Size: 723 B |
|
After Width: | Height: | Size: 919 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 355 B |
@@ -167,9 +167,10 @@ pub(crate) struct Hover {
|
||||
pub(crate) set: AsyncSetState<Option<String>>,
|
||||
}
|
||||
|
||||
/// The status row at the bottom of a tile: presence dot + Online/Offline, plus the trust chip.
|
||||
fn status_row(online: Option<bool>, badge: &str, kind: Pill) -> Element {
|
||||
status_row_with(online, badge, kind, None)
|
||||
/// The status row at the bottom of a tile: the host's OS mark (when advertised), presence
|
||||
/// dot + Online/Offline, plus the trust chip.
|
||||
fn status_row(os: &str, online: Option<bool>, badge: &str, kind: Pill) -> Element {
|
||||
status_row_with(os, online, badge, kind, None)
|
||||
}
|
||||
|
||||
/// [`status_row`] plus the profile chip: what a plain click on THIS tile will use — its own
|
||||
@@ -177,12 +178,26 @@ fn status_row(online: Option<bool>, badge: &str, kind: Pill) -> Element {
|
||||
/// was deleted shows nothing and resolves as the defaults, which is what will happen on
|
||||
/// connect (design §6).
|
||||
fn status_row_with(
|
||||
os: &str,
|
||||
online: Option<bool>,
|
||||
badge: &str,
|
||||
kind: Pill,
|
||||
profile: Option<(&str, Option<String>)>,
|
||||
) -> Element {
|
||||
let mut items: Vec<Element> = Vec::new();
|
||||
// The OS mark leads the row; nothing at all for an older host that doesn't advertise
|
||||
// one, so those tiles render exactly as they always did. Raster at 16px from the
|
||||
// materialized cache (reactor has no vector element); the raw chain is the tooltip.
|
||||
if let Some(uri) = super::os_icons::uri(os) {
|
||||
items.push(
|
||||
Image::new_with_uri(uri)
|
||||
.width(16.0)
|
||||
.height(16.0)
|
||||
.tooltip(os)
|
||||
.vertical_alignment(VerticalAlignment::Center)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if let Some(online) = online {
|
||||
items.push(
|
||||
presence_dot(online)
|
||||
@@ -663,6 +678,12 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
||||
}) {
|
||||
crate::trust::learn_mac(&k.fp_hex, &k.addr, k.port, &a.mac);
|
||||
}
|
||||
// Same for its OS chain — the tile's mark then survives the host going offline.
|
||||
if let Some(a) = hosts.iter().find(|h| {
|
||||
(h.fp_hex == k.fp_hex || (h.addr == k.addr && h.port == k.port)) && !h.os.is_empty()
|
||||
}) {
|
||||
crate::trust::learn_os(&k.fp_hex, &k.addr, k.port, &a.os);
|
||||
}
|
||||
let can_wake = !online && !k.mac.is_empty();
|
||||
let menu = {
|
||||
let (svc, target) = (props.svc.clone(), target.clone());
|
||||
@@ -786,6 +807,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
||||
&k.name,
|
||||
&format!("{}:{}", k.addr, k.port),
|
||||
status_row_with(
|
||||
&k.os,
|
||||
Some(online),
|
||||
if k.paired { "Paired" } else { "Trusted" },
|
||||
if k.paired { Pill::Good } else { Pill::Info },
|
||||
@@ -829,6 +851,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
||||
&k.name,
|
||||
&format!("{}:{}", k.addr, k.port),
|
||||
status_row_with(
|
||||
&k.os,
|
||||
Some(online),
|
||||
if k.paired { "Paired" } else { "Trusted" },
|
||||
if k.paired { Pill::Good } else { Pill::Info },
|
||||
@@ -893,7 +916,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
||||
&hover,
|
||||
&h.name,
|
||||
&format!("{}:{}", h.addr, h.port),
|
||||
status_row(None, badge, kind),
|
||||
status_row(&h.os, None, badge, kind),
|
||||
None,
|
||||
Some(Box::new(move || initiate(&ctx2, target.clone(), &ss, &st))),
|
||||
));
|
||||
|
||||
@@ -25,6 +25,7 @@ mod help;
|
||||
mod hosts;
|
||||
mod library;
|
||||
mod licenses;
|
||||
mod os_icons;
|
||||
mod pair;
|
||||
mod settings;
|
||||
mod speed;
|
||||
@@ -152,6 +153,8 @@ pub struct AppCtx {
|
||||
}
|
||||
|
||||
pub fn run(identity: (String, String), gamepad: GamepadService) -> windows_reactor::Result<()> {
|
||||
// The host tiles' OS marks load as file:/// URIs — put the embedded PNGs on disk first.
|
||||
os_icons::install();
|
||||
let ctx = Arc::new(AppCtx {
|
||||
identity,
|
||||
settings: Mutex::new(Settings::load()),
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! The host tiles' OS marks. Reactor's `ImageSource` is `file:///`-URI raster only (no
|
||||
//! vector element, no icon font with brand glyphs), so the monochrome PNGs under
|
||||
//! `assets/os/` (mid-gray — legible on both WinUI themes; derived from the
|
||||
//! `assets/os-icons` masters, see that README for provenance/licensing) are embedded in
|
||||
//! the exe and materialized once into `%LOCALAPPDATA%\punktfunk\os-icons\` — the same
|
||||
//! disk-cache-to-URI pattern as the library's poster art.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Embedded PNG per icon token, most-generic set the chain walk can land on. A distro
|
||||
/// without its own mark (Bazzite, CachyOS, ...) degrades to its family's and finally Tux.
|
||||
const ICONS: &[(&str, &[u8])] = &[
|
||||
("windows", include_bytes!("../../assets/os/windows.png")),
|
||||
("apple", include_bytes!("../../assets/os/apple.png")),
|
||||
("linux", include_bytes!("../../assets/os/linux.png")),
|
||||
("steam", include_bytes!("../../assets/os/steam.png")),
|
||||
("ubuntu", include_bytes!("../../assets/os/ubuntu.png")),
|
||||
("fedora", include_bytes!("../../assets/os/fedora.png")),
|
||||
("arch", include_bytes!("../../assets/os/arch.png")),
|
||||
("debian", include_bytes!("../../assets/os/debian.png")),
|
||||
("nixos", include_bytes!("../../assets/os/nixos.png")),
|
||||
("opensuse", include_bytes!("../../assets/os/opensuse.png")),
|
||||
];
|
||||
|
||||
fn dir() -> Option<PathBuf> {
|
||||
let base = std::env::var_os("LOCALAPPDATA")?;
|
||||
Some(PathBuf::from(base).join("punktfunk").join("os-icons"))
|
||||
}
|
||||
|
||||
/// Materialize the embedded PNGs to disk (idempotent; size mismatch rewrites, so an icon
|
||||
/// refresh in a newer build lands). Called once at GUI startup, before any tile renders.
|
||||
pub fn install() {
|
||||
let Some(dir) = dir() else { return };
|
||||
if std::fs::create_dir_all(&dir).is_err() {
|
||||
return; // tiles just render without the mark
|
||||
}
|
||||
for (token, bytes) in ICONS {
|
||||
let p = dir.join(format!("{token}.png"));
|
||||
let fresh = std::fs::metadata(&p)
|
||||
.map(|m| m.len() != bytes.len() as u64)
|
||||
.unwrap_or(true);
|
||||
if fresh {
|
||||
let _ = std::fs::write(&p, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `file:///` URI of the mark for an OS-identity chain: walk most-specific-first
|
||||
/// (pf-client-core's shared order/aliases) and take the first token we ship art for.
|
||||
/// `None` (no image element at all) when the host doesn't advertise a chain or nothing
|
||||
/// in it is recognized — the tile then renders exactly as it did before the field.
|
||||
pub fn uri(chain: &str) -> Option<String> {
|
||||
static DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||
let dir = DIR.get_or_init(dir).as_ref()?;
|
||||
let token = pf_client_core::os::os_icon_tokens(chain)
|
||||
.into_iter()
|
||||
.find(|t| ICONS.iter().any(|(name, _)| name == t))?;
|
||||
let p = dir.join(format!("{token}.png"));
|
||||
p.exists()
|
||||
.then(|| format!("file:///{}", p.display().to_string().replace('\\', "/")))
|
||||
}
|
||||
@@ -18,6 +18,10 @@ pub struct DiscoveredHost {
|
||||
/// Wake-on-LAN MAC(s) from the mDNS `mac` TXT (comma-separated `aa:bb:cc:dd:ee:ff`), which the
|
||||
/// hosts page persists onto the matching saved host so it can wake it later. Empty if absent.
|
||||
pub mac: Vec<String>,
|
||||
/// The host's OS-identity chain from the mDNS `os` TXT (`windows` | `macos` |
|
||||
/// `linux[/<family>][/<id>]`), sanitized — drives the host tile's OS mark and is
|
||||
/// persisted like `mac`. Empty if absent (older host).
|
||||
pub os: String,
|
||||
}
|
||||
|
||||
/// Browse continuously for the app's lifetime. The thread exits when the receiver is
|
||||
@@ -71,6 +75,7 @@ pub fn browse() -> async_channel::Receiver<DiscoveredHost> {
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect(),
|
||||
os: pf_client_core::os::sanitize_os(&val("os")),
|
||||
};
|
||||
if tx.send_blocking(host).is_err() {
|
||||
break; // UI gone — stop browsing
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
//! still load via a serde alias in core.
|
||||
|
||||
pub use pf_client_core::trust::{
|
||||
hex, learn_mac, load_or_create_identity, pair_error_message, parse_hex32, KnownHost,
|
||||
hex, learn_mac, learn_os, load_or_create_identity, pair_error_message, parse_hex32, KnownHost,
|
||||
KnownHosts, Settings,
|
||||
};
|
||||
|
||||
@@ -25,6 +25,10 @@ pub struct DiscoveredHost {
|
||||
/// Wake-on-LAN MAC(s) from the mDNS `mac` TXT (comma-separated `aa:bb:cc:dd:ee:ff`), which the
|
||||
/// hosts page persists onto the matching saved host so it can wake it later. Empty if absent.
|
||||
pub mac: Vec<String>,
|
||||
/// The host's OS-identity chain from the mDNS `os` TXT (`windows` | `macos` |
|
||||
/// `linux[/<family>][/<id>]`), sanitized ([`crate::os::sanitize_os`]) — drives the host
|
||||
/// card's OS icon and is persisted like `mac`. Empty if absent (older host).
|
||||
pub os: String,
|
||||
}
|
||||
|
||||
/// One discovery update for the UI's advert map.
|
||||
@@ -96,6 +100,7 @@ pub fn browse() -> async_channel::Receiver<DiscoveryEvent> {
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect(),
|
||||
os: crate::os::sanitize_os(&val("os")),
|
||||
})
|
||||
}
|
||||
ServiceEvent::ServiceRemoved(_ty, fullname) => {
|
||||
|
||||
@@ -41,6 +41,9 @@ pub mod deeplink;
|
||||
// state machine every front-end drives, and the session spawn + stdout contract.
|
||||
#[cfg(any(target_os = "linux", windows))]
|
||||
pub mod orchestrate;
|
||||
// The host's OS-identity chain (mDNS `os=` TXT): sanitize + icon-walk order. Pure string
|
||||
// logic, built everywhere (the Apple/Android ports mirror it rather than link it).
|
||||
pub mod os;
|
||||
// Client settings profiles: the override catalog + the one connect-time resolver
|
||||
// (design/client-settings-profiles.md §4). Sits beside `trust`, which owns the host records
|
||||
// the bindings live on.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//! The client half of the host's OS-identity advertisement (the mDNS `os=` TXT record — see the
|
||||
//! host crate's `osinfo.rs` for the producer): sanitize the untrusted chain once, and turn it
|
||||
//! into the icon-lookup order every front-end walks.
|
||||
//!
|
||||
//! The chain is slash-separated, generic → specific (`windows`, `macos`,
|
||||
//! `linux[/<family>][/<id>]`, e.g. `linux/fedora/bazzite`). A UI resolves an icon by walking
|
||||
//! [`os_icon_tokens`] (most-specific-first, brand aliases applied) and taking the first token it
|
||||
//! has art for — so a client with no Bazzite mark lands on `fedora`, then generic `linux`, and an
|
||||
//! unknown chain simply falls through to the UI's fallback glyph. Kept UI-agnostic here so the
|
||||
//! GTK, Windows and console shells (and the Swift/Kotlin ports, held to the same rules) resolve
|
||||
//! identically.
|
||||
|
||||
/// Reduce a raw `os` TXT value to the trusted grammar: lowercase slash-separated tokens of
|
||||
/// `[a-z0-9._-]` (each capped at 32 chars, at most 5 of them). mDNS is unauthenticated input —
|
||||
/// anything outside the grammar is dropped, and a value that sanitizes to nothing becomes `""`
|
||||
/// (same rendering as an older host that doesn't advertise `os` at all).
|
||||
pub fn sanitize_os(raw: &str) -> String {
|
||||
let tokens: Vec<String> = raw
|
||||
.to_lowercase()
|
||||
.split('/')
|
||||
.map(|t| {
|
||||
t.chars()
|
||||
.filter(|c| {
|
||||
c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '.' | '_' | '-')
|
||||
})
|
||||
.take(32)
|
||||
.collect::<String>()
|
||||
})
|
||||
.filter(|t| !t.is_empty())
|
||||
.take(5)
|
||||
.collect();
|
||||
tokens.join("/")
|
||||
}
|
||||
|
||||
/// The icon-lookup order for a chain: sanitized tokens most-specific-first, with brand aliases
|
||||
/// applied (`macos` → `apple` art, `steamos` → `steam` art). A UI takes the first token it has
|
||||
/// art for; an empty result (empty/garbage chain) means "no OS icon", exactly like an older host
|
||||
/// that doesn't advertise one.
|
||||
pub fn os_icon_tokens(chain: &str) -> Vec<String> {
|
||||
sanitize_os(chain)
|
||||
.split('/')
|
||||
.rev()
|
||||
.filter(|t| !t.is_empty())
|
||||
.map(|t| match t {
|
||||
"macos" => "apple".to_string(),
|
||||
"steamos" => "steam".to_string(),
|
||||
t => t.to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sanitize_passes_well_formed_chains() {
|
||||
assert_eq!(sanitize_os("windows"), "windows");
|
||||
assert_eq!(sanitize_os("linux/fedora/bazzite"), "linux/fedora/bazzite");
|
||||
assert_eq!(
|
||||
sanitize_os("linux/opensuse/opensuse-tumbleweed"),
|
||||
"linux/opensuse/opensuse-tumbleweed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_folds_case_and_drops_junk() {
|
||||
assert_eq!(sanitize_os("Linux/Fedora"), "linux/fedora");
|
||||
assert_eq!(sanitize_os("linux/fe do ra!/§"), "linux/fedora");
|
||||
assert_eq!(sanitize_os("///"), "");
|
||||
assert_eq!(sanitize_os(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_caps_token_length_and_count() {
|
||||
let long = "x".repeat(80);
|
||||
assert_eq!(sanitize_os(&long), "x".repeat(32));
|
||||
assert_eq!(sanitize_os("a/b/c/d/e/f/g"), "a/b/c/d/e");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walk_is_most_specific_first() {
|
||||
assert_eq!(
|
||||
os_icon_tokens("linux/fedora/bazzite"),
|
||||
["bazzite", "fedora", "linux"]
|
||||
);
|
||||
assert_eq!(os_icon_tokens("windows"), ["windows"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walk_applies_brand_aliases() {
|
||||
assert_eq!(os_icon_tokens("macos"), ["apple"]);
|
||||
assert_eq!(
|
||||
os_icon_tokens("linux/arch/steamos"),
|
||||
["steam", "arch", "linux"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walk_of_nothing_is_empty() {
|
||||
assert!(os_icon_tokens("").is_empty());
|
||||
assert!(os_icon_tokens("!!!").is_empty());
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,11 @@ pub struct KnownHost {
|
||||
/// pre-existing stores load; empty until first learned.
|
||||
#[serde(default)]
|
||||
pub mac: Vec<String>,
|
||||
/// The host's OS-identity chain (`windows` | `macos` | `linux[/<family>][/<id>]`) learned
|
||||
/// from its mDNS `os` TXT while online, so the card's OS icon survives the host going to
|
||||
/// sleep. `default` (and elided when empty) so pre-existing stores load unchanged.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub os: String,
|
||||
/// Share this machine's clipboard with THIS host (design/clipboard-and-file-transfer.md
|
||||
/// §5.3 — the Apple client's `StoredHost.clipboardSync`). Per-host, not global: handing a
|
||||
/// host your clipboard is a trust decision about that host. Default off; the host must
|
||||
@@ -183,6 +188,7 @@ impl Default for KnownHost {
|
||||
paired: false,
|
||||
last_used: None,
|
||||
mac: Vec::new(),
|
||||
os: String::new(),
|
||||
clipboard_sync: false,
|
||||
profile_id: None,
|
||||
pinned_profiles: Vec::new(),
|
||||
@@ -290,6 +296,10 @@ impl KnownHosts {
|
||||
if !entry.mac.is_empty() {
|
||||
h.mac = entry.mac;
|
||||
}
|
||||
// Same rule for the learned OS chain: only an upsert that carries one moves it.
|
||||
if !entry.os.is_empty() {
|
||||
h.os = entry.os;
|
||||
}
|
||||
// Everything below is state the user set ON this record, which a refresh (a
|
||||
// reconnect, a re-pair, a rediscovery) never carries and therefore must never
|
||||
// clear: the per-host clipboard decision — which survives today only because this
|
||||
@@ -389,6 +399,28 @@ pub fn learn_mac(fp_hex: &str, addr: &str, port: u16, mac: &[String]) {
|
||||
let _ = known.save();
|
||||
}
|
||||
|
||||
/// Learn/refresh a saved host's OS-identity chain from its live advert (mDNS `os` TXT), matched
|
||||
/// like [`learn_mac`]: by fingerprint or address. No-op — and no disk write — when unchanged, so
|
||||
/// the hosts page can call it on every discovery tick without churning the store.
|
||||
pub fn learn_os(fp_hex: &str, addr: &str, port: u16, os: &str) {
|
||||
if os.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut known = KnownHosts::load();
|
||||
let Some(h) = known
|
||||
.hosts
|
||||
.iter_mut()
|
||||
.find(|h| (!fp_hex.is_empty() && h.fp_hex == fp_hex) || (h.addr == addr && h.port == port))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if h.os == os {
|
||||
return;
|
||||
}
|
||||
h.os = os.to_string();
|
||||
let _ = known.save();
|
||||
}
|
||||
|
||||
/// Re-key a saved host's address/port after it rediscovered on a new DHCP lease (matched by
|
||||
/// fingerprint). No-op — and no disk write — when unchanged. Called from the wake-and-wait flow when
|
||||
/// a woken host reappears on a different IP than the stored one, so this and future connects dial the
|
||||
@@ -1077,6 +1109,28 @@ mod tests {
|
||||
assert_eq!(h.last_used, None);
|
||||
assert_eq!(h.mac, vec!["aa:bb:cc:dd:ee:ff".to_string()]);
|
||||
assert!(parse_hex32(&h.fp_hex).is_some());
|
||||
// A store predating the `os` field loads with it empty, and serializes back without
|
||||
// the key (an older client reading the same file sees exactly what it wrote).
|
||||
assert_eq!(h.os, "");
|
||||
assert!(!serde_json::to_string(&k).unwrap().contains("\"os\""));
|
||||
}
|
||||
|
||||
/// The learned OS chain round-trips, and an absent key stays absent — the same
|
||||
/// back-compat contract as every late `KnownHost` field.
|
||||
#[test]
|
||||
fn known_hosts_os_chain_round_trips() {
|
||||
let k = KnownHosts {
|
||||
hosts: vec![KnownHost {
|
||||
name: "HTPC".into(),
|
||||
addr: "192.168.1.181".into(),
|
||||
port: 9777,
|
||||
os: "linux/fedora/bazzite".into(),
|
||||
..Default::default()
|
||||
}],
|
||||
};
|
||||
let text = serde_json::to_string(&k).unwrap();
|
||||
let back: KnownHosts = serde_json::from_str(&text).unwrap();
|
||||
assert_eq!(back.hosts[0].os, "linux/fedora/bazzite");
|
||||
}
|
||||
|
||||
/// A pre-profiles known-hosts file loads unchanged — no binding, no pins — and its
|
||||
@@ -1130,6 +1184,7 @@ mod tests {
|
||||
paired: true,
|
||||
last_used: Some(1000),
|
||||
mac: vec!["aa:bb:cc:dd:ee:ff".into()],
|
||||
os: "linux/fedora/bazzite".into(),
|
||||
clipboard_sync: true,
|
||||
profile_id: Some("aaaaaaaaaaaa".into()),
|
||||
pinned_profiles: vec!["bbbbbbbbbbbb".into()],
|
||||
@@ -1151,6 +1206,8 @@ mod tests {
|
||||
assert!(h.paired);
|
||||
assert_eq!(h.last_used, Some(1000));
|
||||
assert_eq!(h.mac, vec!["aa:bb:cc:dd:ee:ff".to_string()]);
|
||||
// The learned OS chain rides the same rule as `mac`: a carrier-less upsert keeps it.
|
||||
assert_eq!(h.os, "linux/fedora/bazzite");
|
||||
assert!(h.clipboard_sync);
|
||||
assert_eq!(h.profile_id.as_deref(), Some("aaaaaaaaaaaa"));
|
||||
assert_eq!(h.pinned_profiles, vec!["bbbbbbbbbbbb".to_string()]);
|
||||
|
||||
@@ -31,6 +31,10 @@ pub struct HostRow {
|
||||
pub can_wake: bool,
|
||||
/// Last successful connect (UNIX seconds) — the most-recent accent.
|
||||
pub last_used: Option<u64>,
|
||||
/// The host's OS-identity chain (live advert preferred, else the stored one), for a
|
||||
/// future tile OS glyph. Empty = unknown (older host). Plumbed now; drawing is a
|
||||
/// follow-up — the Skia glyph set doesn't exist yet.
|
||||
pub os: String,
|
||||
}
|
||||
|
||||
/// The pairing ceremony's observable state (one at a time — the ceremony is modal).
|
||||
@@ -179,6 +183,7 @@ mod tests {
|
||||
mgmt_port: 47990,
|
||||
can_wake: false,
|
||||
last_used: None,
|
||||
os: String::new(),
|
||||
};
|
||||
shared.set_hosts(vec![row.clone()]);
|
||||
let g1 = shared.hosts_gen();
|
||||
|
||||
@@ -483,6 +483,7 @@ mod tests {
|
||||
mgmt_port: 47990,
|
||||
can_wake,
|
||||
last_used: None,
|
||||
os: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -429,6 +429,7 @@ mod tests {
|
||||
mgmt_port: 47990,
|
||||
can_wake: false,
|
||||
last_used: None,
|
||||
os: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ fn hosts() -> Vec<HostRow> {
|
||||
mgmt_port: 47990,
|
||||
can_wake: false,
|
||||
last_used: None,
|
||||
os: String::new(),
|
||||
};
|
||||
vec![
|
||||
HostRow {
|
||||
|
||||