feat(linux): game library browser; split app.rs into cli/launch/ui_trust
- library.rs + ui_library.rs: the host's unified game library over the management API (the Apple LibraryClient/LibraryView ported) — mTLS with the paired identity, host verified by its pinned cert fingerprint (ureq + rustls, unified with the workspace rustls 0.23); posters load async with monogram placeholders, and picking a title starts a session that asks the host to launch it (the library id rides the Hello). - app.rs (~800 lines lighter) splits into cli.rs (argv/headless pairing/--connect/screenshot scenes), launch.rs (mode resolve + session worker + event stream into the UI) and ui_trust.rs (TOFU / SPAKE2 PIN / delegated-approval dialogs); ui_hosts/ui_stream reworked around the split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! LAN host discovery: browse the host's mDNS advert (`_punktfunk._udp`, TXT keys
|
||||
//! `fp`/`pair`/`id` — see the host crate's `discovery.rs`) on a worker thread and stream
|
||||
//! results to the UI.
|
||||
//! results to the UI. Removal events are forwarded too, so the hosts page can drop stale
|
||||
//! cards and flip a saved host's online pip when its advert disappears.
|
||||
|
||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||
|
||||
@@ -8,6 +9,8 @@ use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||
pub struct DiscoveredHost {
|
||||
/// Stable row key: the advertised host id, falling back to the mDNS fullname.
|
||||
pub key: String,
|
||||
/// The mDNS service fullname — what a later `Removed` event identifies the advert by.
|
||||
pub fullname: String,
|
||||
pub name: String,
|
||||
pub addr: String,
|
||||
pub port: u16,
|
||||
@@ -15,11 +18,23 @@ pub struct DiscoveredHost {
|
||||
pub fp_hex: String,
|
||||
/// Pairing requirement: `"required"` or `"optional"`.
|
||||
pub pair: String,
|
||||
/// The management API's port (mDNS `mgmt` TXT) — where the game library is served.
|
||||
/// `None` when not advertised (older host / standalone `punktfunk1-host`); the
|
||||
/// library client then falls back to the well-known default.
|
||||
pub mgmt_port: Option<u16>,
|
||||
}
|
||||
|
||||
/// One discovery update for the UI's advert map.
|
||||
pub enum DiscoveryEvent {
|
||||
/// A host advert appeared or refreshed (new address, pairing flipped, …).
|
||||
Resolved(DiscoveredHost),
|
||||
/// The advert went away (host stopped / left the network).
|
||||
Removed { fullname: String },
|
||||
}
|
||||
|
||||
/// Browse continuously for the app's lifetime. The thread exits when the receiver is
|
||||
/// dropped (the send fails) or the daemon dies.
|
||||
pub fn browse() -> async_channel::Receiver<DiscoveredHost> {
|
||||
pub fn browse() -> async_channel::Receiver<DiscoveryEvent> {
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
std::thread::Builder::new()
|
||||
.name("punktfunk-mdns".into())
|
||||
@@ -39,34 +54,42 @@ pub fn browse() -> async_channel::Receiver<DiscoveredHost> {
|
||||
}
|
||||
};
|
||||
while let Ok(event) = receiver.recv() {
|
||||
if let ServiceEvent::ServiceResolved(info) = event {
|
||||
let props = info.get_properties();
|
||||
let val = |k: &str| props.get_property_val_str(k).unwrap_or("").to_string();
|
||||
let Some(addr) = info.get_addresses().iter().next().map(|a| a.to_string())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let id = val("id");
|
||||
let host = DiscoveredHost {
|
||||
key: if id.is_empty() {
|
||||
info.get_fullname().to_string()
|
||||
} else {
|
||||
id
|
||||
},
|
||||
name: info
|
||||
.get_fullname()
|
||||
.split('.')
|
||||
.next()
|
||||
.unwrap_or("?")
|
||||
.to_string(),
|
||||
addr,
|
||||
port: info.get_port(),
|
||||
fp_hex: val("fp"),
|
||||
pair: val("pair"),
|
||||
};
|
||||
if tx.send_blocking(host).is_err() {
|
||||
break; // UI gone — stop browsing
|
||||
let update = match event {
|
||||
ServiceEvent::ServiceResolved(info) => {
|
||||
let props = info.get_properties();
|
||||
let val = |k: &str| props.get_property_val_str(k).unwrap_or("").to_string();
|
||||
let Some(addr) = info.get_addresses().iter().next().map(|a| a.to_string())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let id = val("id");
|
||||
DiscoveryEvent::Resolved(DiscoveredHost {
|
||||
key: if id.is_empty() {
|
||||
info.get_fullname().to_string()
|
||||
} else {
|
||||
id
|
||||
},
|
||||
fullname: info.get_fullname().to_string(),
|
||||
name: info
|
||||
.get_fullname()
|
||||
.split('.')
|
||||
.next()
|
||||
.unwrap_or("?")
|
||||
.to_string(),
|
||||
addr,
|
||||
port: info.get_port(),
|
||||
fp_hex: val("fp"),
|
||||
pair: val("pair"),
|
||||
mgmt_port: val("mgmt").parse().ok(),
|
||||
})
|
||||
}
|
||||
ServiceEvent::ServiceRemoved(_ty, fullname) => {
|
||||
DiscoveryEvent::Removed { fullname }
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
if tx.send_blocking(update).is_err() {
|
||||
break; // UI gone — stop browsing
|
||||
}
|
||||
}
|
||||
let _ = daemon.shutdown();
|
||||
|
||||
Reference in New Issue
Block a user