//! `punktfunk-client` — the native Windows punktfunk/1 client. //! //! Pure Rust: `NativeClient` linked as a crate (no C ABI, like the GTK Linux client) · SDL3 //! gamepads · a **WinUI 3** shell (windows-reactor). Streaming (decode + present + audio) runs in //! the spawned `punktfunk-session` Vulkan binary; the shell owns host selection, trust and //! pairing. The trust surface mirrors the //! other native clients: persistent identity, trust-on-first-use, SPAKE2 PIN pairing — all in-app //! (host list, settings, pairing). Streaming runs in the spawned `punktfunk-session` binary; //! `--headless --speed-test` keeps a decode-less CLI measurement path. //! //! Usage: //! punktfunk-client (open the WinUI 3 window: host list, settings, pairing) //! punktfunk-client --discover (list punktfunk hosts on the LAN) //! punktfunk-client --headless --speed-test --connect host[:port] //! (measure the path: probe burst → goodput / loss / recommended bitrate) // Link as a GUI (windows) subsystem binary so the default windowed launch (MSIX / double-click) // does NOT pop a console window. The CLI paths (--headless/--discover) reattach to the launching // terminal's console at startup (see main), so their output is still visible when run from a shell. #![cfg_attr(windows, windows_subsystem = "windows")] #[cfg(windows)] mod app; #[cfg(windows)] mod discovery; #[cfg(windows)] mod gpu; #[cfg(windows)] mod probe; #[cfg(windows)] mod shell_window; #[cfg(windows)] mod spawn; #[cfg(windows)] mod trust; #[cfg(windows)] mod wol; #[cfg(windows)] fn main() { // With #![windows_subsystem = "windows"] the process starts with no console, so the GUI/MSIX // launch is window-free. AttachConsole only binds to an ALREADY-EXISTING parent console (it // never creates one), so when launched from a terminal — `--headless`/`--discover` — stdout and // the tracing writer below land in that terminal; from Explorer/MSIX it's a harmless no-op. unsafe { use windows::Win32::System::Console::{AttachConsole, ATTACH_PARENT_PROCESS}; let _ = AttachConsole(ATTACH_PARENT_PROCESS); } set_app_user_model_id(); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), ) .init(); let args: Vec = std::env::args().collect(); let flag = |name: &str| args.iter().any(|a| a == name); if flag("--discover") { discover_and_print(); return; } let identity = match trust::load_or_create_identity() { Ok(i) => i, Err(e) => { eprintln!("client identity: {e:#}"); std::process::exit(1); } }; if flag("--headless") { run_headless_cli(&args, identity); return; } // Windowed (default): the WinUI 3 app owns host selection, settings, and pairing. // Framework-dependent deployment: initialize the Windows App SDK runtime before any WinUI // call (build.rs stages the bootstrap DLL via windows-reactor-setup). if let Err(e) = windows_reactor::bootstrap() { tracing::error!(error = %e, "Windows App SDK bootstrap failed"); std::process::exit(1); } // The shared SDL gamepad service (pf-client-core). The shell only enumerates pads (Settings // list) and persists the pin; the spawned punktfunk-session runs the SAME service and does the // actual forwarding — so, unlike the old shell fork, we never `attach()` here. Idle it stays // hands-off the hardware (id-getter metadata, no device open, Valve HIDAPI drivers off). let gamepad = pf_client_core::gamepad::GamepadService::start(); if let Err(e) = app::run(identity, gamepad) { tracing::error!(error = %e, "WinUI app failed"); std::process::exit(1); } } /// Tag unpackaged (dev) runs with the explicit AppUserModelID that `pf-presenter`'s /// session window also adopts, so the shell and the stream window group as ONE taskbar /// app across the shell⇄session visibility handoff. MSIX runs already carry the package /// identity — overriding it would detach the window from the Start-menu pin, so packaged /// processes are left alone. Must run before any window exists. #[cfg(windows)] fn set_app_user_model_id() { use windows::Win32::Foundation::APPMODEL_ERROR_NO_PACKAGE; use windows::Win32::Storage::Packaging::Appx::GetCurrentPackageFullName; use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; unsafe { let mut len: u32 = 0; // No buffer: just probe whether the process has package identity. if GetCurrentPackageFullName(&mut len, None) != APPMODEL_ERROR_NO_PACKAGE { return; // packaged (or indeterminate) — leave the identity alone } // Must stay in sync with pf-presenter's win32.rs, or the windows stop grouping. let _ = SetCurrentProcessExplicitAppUserModelID(windows::core::w!("unom.punktfunk.client")); } } /// `--headless --speed-test --connect host[:port]`: measure the path over the real data plane and /// print the outcome — the Windows analogue of `punktfunk-probe`. The former in-process /// frame-count connect path went with the legacy builtin stream; real streaming is windowed-only. #[cfg(windows)] fn run_headless_cli(args: &[String], identity: (String, String)) { let arg = |name: &str| -> Option { args.iter() .position(|a| a == name) .and_then(|i| args.get(i + 1)) .cloned() }; let flag = |name: &str| args.iter().any(|a| a == name); let Some(target) = arg("--connect") else { eprintln!("--headless requires --connect host[:port]"); std::process::exit(2); }; let (host, port) = match target.rsplit_once(':') { Some((a, p)) => (a.to_string(), p.parse().unwrap_or(9777)), None => (target.clone(), 9777u16), }; // Speed test: measure the path over the real data plane, print the outcome, exit. The saved // fingerprint for this address (if any) pins the connect, like the GUI's per-host test. if flag("--speed-test") { let fp = trust::KnownHosts::load() .find_by_addr(&host, port) .map(|k| k.fp_hex.clone()); match probe::run_speed_probe(&host, port, fp.as_deref(), identity) { Ok(r) => { let mbps = f64::from(r.throughput_kbps) / 1000.0; let recommended = f64::from(r.throughput_kbps / 10 * 7) / 1000.0; println!( "{mbps:.0} Mbit/s measured · {:.1} % loss · recommended bitrate {recommended:.0} Mbit/s (--bitrate {:.0})", r.loss_pct, recommended ); } Err(e) => { eprintln!("speed test failed: {e}"); std::process::exit(1); } } return; } // Only --speed-test remains headless: real streaming runs in the windowed app's spawned // punktfunk-session binary, which the deleted in-process frame-count path was replaced by. eprintln!("--headless supports only --speed-test now \u{2014} run the windowed app to stream"); std::process::exit(2); } /// `--discover`: browse the LAN for punktfunk hosts (mDNS) and print them, then exit. #[cfg(windows)] fn discover_and_print() { use std::time::{Duration, Instant}; println!("Browsing the LAN for punktfunk hosts (~5 s)…"); let rx = discovery::browse(); let deadline = Instant::now() + Duration::from_secs(5); let mut seen = std::collections::HashSet::new(); while Instant::now() < deadline { while let Ok(h) = rx.try_recv() { if seen.insert(h.key.clone()) { println!( " {} {}:{} pair={} fp={}", h.name, h.addr, h.port, if h.pair.is_empty() { "optional" } else { &h.pair }, if h.fp_hex.is_empty() { "-" } else { &h.fp_hex }, ); } } std::thread::sleep(Duration::from_millis(100)); } if seen.is_empty() { println!(" (none found — is a host running with --native / punktfunk1-host?)"); } } /// WinUI 3 / Direct3D11 / WASAPI / SDL3 are Windows turf; this stub keeps `cargo build /// --workspace` green on Linux/macOS (the other native clients live in /// clients/linux and clients/apple). #[cfg(not(windows))] fn main() { eprintln!( "punktfunk-client-windows is Windows-only — the Linux client lives in \ clients/linux, the macOS client in clients/apple" ); std::process::exit(2); }