diff --git a/clients/linux-session/src/browse.rs b/clients/linux-session/src/browse.rs index af3c3a85..5b926eda 100644 --- a/clients/linux-session/src/browse.rs +++ b/clients/linux-session/src/browse.rs @@ -66,36 +66,37 @@ pub fn run(target: &str) -> u8 { overlay: Some(Box::new(overlay)), }; - let result = pf_presenter::run_browse(opts, |action, gamepad, native, force_software, vulkan| { - match action { - OverlayAction::Launch { id, title } => { - // The carousel only renders for a paired host, so the pin exists; the - // guard keeps a logic slip from turning into a pinless connect. - let Some(pin) = pin else { - tracing::warn!("launch without a stored pin — refusing"); - return ActionOutcome::Handled; - }; - tracing::info!(%id, %title, "launching from the library"); - ActionOutcome::Start(Box::new(session_params( - &settings, - addr.clone(), - port, - pin, - identity.clone(), - Some(id), - gamepad, - native, - force_software, - vulkan, - ))) + let result = + pf_presenter::run_browse(opts, |action, gamepad, native, force_software, vulkan| { + match action { + OverlayAction::Launch { id, title } => { + // The carousel only renders for a paired host, so the pin exists; the + // guard keeps a logic slip from turning into a pinless connect. + let Some(pin) = pin else { + tracing::warn!("launch without a stored pin — refusing"); + return ActionOutcome::Handled; + }; + tracing::info!(%id, %title, "launching from the library"); + ActionOutcome::Start(Box::new(session_params( + &settings, + addr.clone(), + port, + pin, + identity.clone(), + Some(id), + gamepad, + native, + force_software, + vulkan, + ))) + } + OverlayAction::Retry => { + spawn_fetch(shared.clone(), addr.clone(), mgmt, identity.clone(), pin); + ActionOutcome::Handled + } + OverlayAction::Quit => ActionOutcome::Quit, } - OverlayAction::Retry => { - spawn_fetch(shared.clone(), addr.clone(), mgmt, identity.clone(), pin); - ActionOutcome::Handled - } - OverlayAction::Quit => ActionOutcome::Quit, - } - }); + }); match result { Ok(()) => 0, diff --git a/clients/linux-session/src/main.rs b/clients/linux-session/src/main.rs index d700503e..21fd1aca 100644 --- a/clients/linux-session/src/main.rs +++ b/clients/linux-session/src/main.rs @@ -217,7 +217,10 @@ mod session_main { // connects silently; an unknown host is REFUSED — there is no dialog here, and a // silent TOFU would defeat the pinning model. Pair via the desktop client. let known = trust::KnownHosts::load(); - let known_host = known.hosts.iter().find(|h| h.addr == addr && h.port == port); + let known_host = known + .hosts + .iter() + .find(|h| h.addr == addr && h.port == port); let pin = arg_value("--fp") .as_deref() .and_then(trust::parse_hex32) @@ -285,7 +288,10 @@ mod session_main { json_line("ended", &reason, None); 0 } - Ok(pf_presenter::Outcome::ConnectFailed { msg, trust_rejected }) => { + Ok(pf_presenter::Outcome::ConnectFailed { + msg, + trust_rejected, + }) => { json_line("error", &msg, Some(trust_rejected)); if trust_rejected { EXIT_TRUST_REJECTED @@ -299,7 +305,6 @@ mod session_main { } } } - } #[cfg(target_os = "linux")] diff --git a/clients/linux/src/app.rs b/clients/linux/src/app.rs index 33ebc540..c20fcc61 100644 --- a/clients/linux/src/app.rs +++ b/clients/linux/src/app.rs @@ -162,15 +162,16 @@ impl SimpleComponent for AppModel { } } - let hosts = HostsPage::builder() - .launch(settings.clone()) - .forward(sender.input_sender(), |out| match out { - HostsOutput::Connect(req) => AppMsg::Connect(req), - HostsOutput::WakeConnect(req) => AppMsg::WakeConnect(req), - HostsOutput::Pair(req) => AppMsg::Pair(req), - HostsOutput::SpeedTest(req) => AppMsg::SpeedTest(req), - HostsOutput::Library(req, mgmt) => AppMsg::OpenLibrary(req, mgmt), - }); + let hosts = + HostsPage::builder() + .launch(settings.clone()) + .forward(sender.input_sender(), |out| match out { + HostsOutput::Connect(req) => AppMsg::Connect(req), + HostsOutput::WakeConnect(req) => AppMsg::WakeConnect(req), + HostsOutput::Pair(req) => AppMsg::Pair(req), + HostsOutput::SpeedTest(req) => AppMsg::SpeedTest(req), + HostsOutput::Library(req, mgmt) => AppMsg::OpenLibrary(req, mgmt), + }); let nav = adw::NavigationView::new(); nav.add(hosts.widget()); @@ -250,7 +251,12 @@ impl SimpleComponent for AppModel { }); } else if known.find_by_addr(&req.addr, req.port).is_some() { self.toast("Host fingerprint changed — re-pair with a PIN to continue"); - crate::ui_trust::pin_dialog(&self.window, &sender, self.identity.clone(), req); + crate::ui_trust::pin_dialog( + &self.window, + &sender, + self.identity.clone(), + req, + ); } else if req.pair_optional { crate::ui_trust::tofu_dialog(&self.window, &sender, req); } else { @@ -265,7 +271,10 @@ impl SimpleComponent for AppModel { None => { // Manual entry: a known address connects on its stored pin; // an unknown one must pair — never silent TOFU. - match known.find_by_addr(&req.addr, req.port).map(|k| k.fp_hex.clone()) { + match known + .find_by_addr(&req.addr, req.port) + .map(|k| k.fp_hex.clone()) + { Some(fp_hex) => sender.input(AppMsg::StartSession { req, fp_hex, @@ -307,7 +316,8 @@ impl SimpleComponent for AppModel { return; } self.hosts.emit(HostsMsg::ClearError); - self.hosts.emit(HostsMsg::SetConnecting(Some(req.card_key()))); + self.hosts + .emit(HostsMsg::SetConnecting(Some(req.card_key()))); let fullscreen = self.settings.borrow().fullscreen_on_stream; if let Err(e) = spawn::spawn_session( sender.input_sender().clone(), @@ -361,7 +371,12 @@ impl SimpleComponent for AppModel { (_, Some((_, true)), _) if !tofu => { // The stored pin no longer matches (rotated cert or impostor). self.toast("Host fingerprint changed — re-pair with a PIN to continue"); - crate::ui_trust::pin_dialog(&self.window, &sender, self.identity.clone(), req); + crate::ui_trust::pin_dialog( + &self.window, + &sender, + self.identity.clone(), + req, + ); } (_, Some((msg, _)), _) => self .hosts @@ -380,10 +395,15 @@ impl SimpleComponent for AppModel { } AppMsg::ShowPreferences => { let hosts = self.hosts.sender().clone(); - crate::ui_settings::show(&self.window, self.settings.clone(), &self.gamepad, move || { - // The library toggle changes the saved cards' menu — re-render. - let _ = hosts.send(HostsMsg::Refresh); - }); + crate::ui_settings::show( + &self.window, + self.settings.clone(), + &self.gamepad, + move || { + // The library toggle changes the saved cards' menu — re-render. + let _ = hosts.send(HostsMsg::Refresh); + }, + ); } AppMsg::ShowShortcuts => shortcuts_window(&self.window).present(), AppMsg::ShowAbout => crate::ui_settings::show_about(&self.window), @@ -435,12 +455,12 @@ impl AppModel { }, CompositorPref::Auto, GamepadPref::Auto, - 0, // bitrate_kbps (host default) - 0, // video_caps: probe connect, nothing presents - 2, // audio_channels: stereo + 0, // bitrate_kbps (host default) + 0, // video_caps: probe connect, nothing presents + 2, // audio_channels: stereo crate::video::decodable_codecs(), // codecs (unused by the probe, but honest) - 0, // preferred_codec: no preference - None, // launch: probe connect, no game + 0, // preferred_codec: no preference + None, // launch: probe connect, no game pin, Some(identity), std::time::Duration::from_secs(15), @@ -530,8 +550,7 @@ pub fn run() -> glib::ExitCode { // Streams and the console library live in the session binary now — exec it, // forwarding the relevant argv (the Decky wrapper keeps working through the shell // until it's repointed). - if crate::cli::arg_value("--connect").is_some() || crate::cli::arg_value("--browse").is_some() - { + if crate::cli::arg_value("--connect").is_some() || crate::cli::arg_value("--browse").is_some() { return crate::cli::exec_session(); } diff --git a/clients/linux/src/cli.rs b/clients/linux/src/cli.rs index 12bb5674..a28ea3c6 100644 --- a/clients/linux/src/cli.rs +++ b/clients/linux/src/cli.rs @@ -273,7 +273,14 @@ pub fn run_shot(ctx: &ShotCtx, scene: &str) { // no-art placeholders, and one solid-color texture standing in for a poster. "library" | "08-library" => { let (games, art) = mock_library(); - crate::ui_library::open_mock(&ctx.nav, ctx.identity.clone(), sender, mock_req(), games, art); + crate::ui_library::open_mock( + &ctx.nav, + ctx.identity.clone(), + sender, + mock_req(), + games, + art, + ); } other => tracing::warn!("unknown PUNKTFUNK_SHOT_SCENE={other:?}; showing hosts only"), } diff --git a/clients/linux/src/spawn.rs b/clients/linux/src/spawn.rs index 517dbc06..11d2aeb8 100644 --- a/clients/linux/src/spawn.rs +++ b/clients/linux/src/spawn.rs @@ -115,10 +115,7 @@ pub fn spawn_session( let stdout = child.stdout.take().expect("piped stdout"); // Park the child where the cancel handle (and the reader, for the final reap) can // reach it. - let slot = opts - .cancel - .clone() - .unwrap_or_default(); + let slot = opts.cancel.clone().unwrap_or_default(); *slot.0.lock().unwrap() = Some(child); let persist_paired = opts.persist_paired; @@ -138,7 +135,10 @@ pub fn spawn_session( persist_paired, }); } - Some(ChildEvent::Error { msg, trust_rejected }) => { + Some(ChildEvent::Error { + msg, + trust_rejected, + }) => { error = Some((msg, trust_rejected)); } Some(ChildEvent::Ended(msg)) => ended = Some(msg), @@ -173,9 +173,15 @@ mod tests { #[test] fn parses_the_stdout_contract() { - assert!(matches!(parse_line("{\"ready\":true}"), Some(ChildEvent::Ready))); + assert!(matches!( + parse_line("{\"ready\":true}"), + Some(ChildEvent::Ready) + )); match parse_line("{\"error\":\"no route\",\"trust_rejected\":false}") { - Some(ChildEvent::Error { msg, trust_rejected }) => { + Some(ChildEvent::Error { + msg, + trust_rejected, + }) => { assert_eq!(msg, "no route"); assert!(!trust_rejected); } diff --git a/clients/linux/src/ui_hosts.rs b/clients/linux/src/ui_hosts.rs index 847c5134..222fb4b7 100644 --- a/clients/linux/src/ui_hosts.rs +++ b/clients/linux/src/ui_hosts.rs @@ -170,7 +170,9 @@ impl relm4::factory::FactoryComponent for HostCard { l }; match &self.kind { - CardKind::Saved { host: k, online, .. } => { + CardKind::Saved { + host: k, online, .. + } => { // Presence pip + spelled-out state, then the trust pill. let pip = gtk::Box::new(gtk::Orientation::Horizontal, 0); pip.add_css_class("pf-pip"); @@ -232,11 +234,17 @@ impl relm4::factory::FactoryComponent for HostCard { } { let req = req.clone(); - add("speed", Box::new(move || CardOutput::SpeedTest(req.clone()))); + add( + "speed", + Box::new(move || CardOutput::SpeedTest(req.clone())), + ); } { let req = req.clone(); - add("library", Box::new(move || CardOutput::Library(req.clone()))); + add( + "library", + Box::new(move || CardOutput::Library(req.clone())), + ); } { let (fp, name) = (k.fp_hex.clone(), k.name.clone()); @@ -344,7 +352,9 @@ struct PageWidgets { pub enum HostsMsg { /// A resolved mDNS advert (also the CI scenes' injection path). Advert(DiscoveredHost), - AdvertRemoved { fullname: String }, + AdvertRemoved { + fullname: String, + }, /// Reload the disk store and re-render (fresh pairings, renames, the library gate). Refresh, /// Mark the card matching `ConnectRequest::card_key` as connecting; `None` restores. @@ -453,7 +463,10 @@ impl SimpleComponent for HostsPage { content.append(&searching); content.append(discovered.widget()); - let clamp = adw::Clamp::builder().maximum_size(1100).child(&content).build(); + let clamp = adw::Clamp::builder() + .maximum_size(1100) + .child(&content) + .build(); let scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .child(&clamp) @@ -545,10 +558,7 @@ impl SimpleComponent for HostsPage { }; model.rebuild(); - ComponentParts { - model, - widgets: (), - } + ComponentParts { model, widgets: () } } fn update(&mut self, msg: HostsMsg, sender: ComponentSender) { @@ -606,8 +616,7 @@ impl HostsPage { // A saved host is ONLINE iff a live advert matches it (fingerprint, or address // when the advert carries no fp). let matches = |k: &KnownHost, a: &DiscoveredHost| { - (!a.fp_hex.is_empty() && a.fp_hex == k.fp_hex) - || (a.addr == k.addr && a.port == k.port) + (!a.fp_hex.is_empty() && a.fp_hex == k.fp_hex) || (a.addr == k.addr && a.port == k.port) }; let most_recent = known .hosts @@ -851,6 +860,10 @@ mod tests { flow.emit_by_name::<()>("child-activated", &[&child]); - assert_eq!(fired.get(), 1, "the per-card handler should fire exactly once"); + assert_eq!( + fired.get(), + 1, + "the per-card handler should fire exactly once" + ); } } diff --git a/clients/windows/src/input.rs b/clients/windows/src/input.rs index 1a20bd5e..d3df291a 100644 --- a/clients/windows/src/input.rs +++ b/clients/windows/src/input.rs @@ -277,7 +277,8 @@ fn toggle_fullscreen(hwnd: isize) { ..Default::default() }; let mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY); - if GetWindowPlacement(hwnd, &mut wp).is_ok() && GetMonitorInfoW(mon, &mut mi).as_bool() { + if GetWindowPlacement(hwnd, &mut wp).is_ok() && GetMonitorInfoW(mon, &mut mi).as_bool() + { *SAVED.lock().unwrap() = Some(wp); SetWindowLongPtrW(hwnd, GWL_STYLE, style & !overlapped); let r = mi.rcMonitor; diff --git a/crates/pf-client-core/src/session.rs b/crates/pf-client-core/src/session.rs index ffdd98bf..3bdb68c3 100644 --- a/crates/pf-client-core/src/session.rs +++ b/crates/pf-client-core/src/session.rs @@ -362,9 +362,7 @@ fn pump( // frame) measures true received→decode-complete at zero // pipeline cost. Software/VAAPI keep the synchronous stamp. let hw_fence = match &image { - DecodedImage::VkFrame(v) => { - Some((v.timeline_sem, v.decode_done_value)) - } + DecodedImage::VkFrame(v) => Some((v.timeline_sem, v.decode_done_value)), _ => None, }; let _ = frame_tx.force_send(DecodedFrame { diff --git a/crates/pf-client-core/src/video.rs b/crates/pf-client-core/src/video.rs index 6456b901..83881898 100644 --- a/crates/pf-client-core/src/video.rs +++ b/crates/pf-client-core/src/video.rs @@ -284,8 +284,10 @@ impl Decoder { } }, None if choice == "vulkan" => { - bail!("PUNKTFUNK_DECODER=vulkan but the presenter's device can't (missing \ - video extensions/queue) — see the presenter log") + bail!( + "PUNKTFUNK_DECODER=vulkan but the presenter's device can't (missing \ + video extensions/queue) — see the presenter log" + ) } None => {} } @@ -883,17 +885,15 @@ impl VulkanDecoder { store.f11.pNext = &mut store.f12 as *mut _ as *mut std::ffi::c_void; store.f12.pNext = &mut store.f13 as *mut _ as *mut std::ffi::c_void; - (*hwctx).get_proc_addr = - std::mem::transmute::( - vk.get_instance_proc_addr, - ); + (*hwctx).get_proc_addr = std::mem::transmute::( + vk.get_instance_proc_addr, + ); (*hwctx).inst = vk.instance as pf_ffvk::VkInstance; (*hwctx).phys_dev = vk.physical_device as pf_ffvk::VkPhysicalDevice; (*hwctx).act_dev = vk.device as pf_ffvk::VkDevice; (*hwctx).device_features.sType = pf_ffvk::VkStructureType_VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; - (*hwctx).device_features.pNext = - &mut store.f11 as *mut _ as *mut std::ffi::c_void; + (*hwctx).device_features.pNext = &mut store.f11 as *mut _ as *mut std::ffi::c_void; (*hwctx).enabled_inst_extensions = store.inst_ptrs.as_ptr(); (*hwctx).nb_enabled_inst_extensions = store.inst_ptrs.len() as i32; (*hwctx).enabled_dev_extensions = store.dev_ptrs.as_ptr(); @@ -950,10 +950,8 @@ impl VulkanDecoder { let gipa = (*hwctx) .get_proc_addr .expect("get_proc_addr was just set above"); - let gdpa: pf_ffvk::PFN_vkGetDeviceProcAddr = std::mem::transmute(gipa( - (*hwctx).inst, - c"vkGetDeviceProcAddr".as_ptr(), - )); + let gdpa: pf_ffvk::PFN_vkGetDeviceProcAddr = + std::mem::transmute(gipa((*hwctx).inst, c"vkGetDeviceProcAddr".as_ptr())); let wait_semaphores: pf_ffvk::PFN_vkWaitSemaphores = std::mem::transmute(gdpa .expect("vkGetDeviceProcAddr resolvable")( (*hwctx).act_dev, @@ -975,8 +973,8 @@ impl VulkanDecoder { (*ctx).get_format = Some(pick_vulkan); (*ctx).flags |= ffi::AV_CODEC_FLAG_LOW_DELAY as i32; (*ctx).thread_count = 1; // hwaccel: threads only add latency - // Same pool headroom rationale as VAAPI: the presenter pins the on-screen - // frame + the newest in flight past receive_frame. + // Same pool headroom rationale as VAAPI: the presenter pins the on-screen + // frame + the newest in flight past receive_frame. (*ctx).extra_hw_frames = 4; let r = ffi::avcodec_open2(ctx, codec, ptr::null_mut()); if r < 0 { diff --git a/crates/pf-console-ui/src/library.rs b/crates/pf-console-ui/src/library.rs index 2b66a6ce..655d68a8 100644 --- a/crates/pf-console-ui/src/library.rs +++ b/crates/pf-console-ui/src/library.rs @@ -172,10 +172,22 @@ fn mat_mul(a: &[f64; 16], b: &[f64; 16]) -> [f64; 16] { /// `meshColors`: dark-violet corners sink the frame, the edges carry mid-tone violets, and /// the four interior points hold the bright brand family (warm pools left, cool right). pub const MESH_COLORS: [(f64, f64, f64); 16] = [ - (0.075, 0.060, 0.160), (0.34, 0.27, 0.72), (0.30, 0.26, 0.74), (0.075, 0.060, 0.160), - (0.42, 0.20, 0.54), (0.49, 0.39, 0.95), (0.28, 0.31, 0.84), (0.16, 0.26, 0.64), - (0.45, 0.23, 0.60), (0.53, 0.31, 0.75), (0.35, 0.35, 0.91), (0.19, 0.28, 0.70), - (0.075, 0.060, 0.160), (0.22, 0.18, 0.54), (0.24, 0.20, 0.58), (0.075, 0.060, 0.160), + (0.075, 0.060, 0.160), + (0.34, 0.27, 0.72), + (0.30, 0.26, 0.74), + (0.075, 0.060, 0.160), + (0.42, 0.20, 0.54), + (0.49, 0.39, 0.95), + (0.28, 0.31, 0.84), + (0.16, 0.26, 0.64), + (0.45, 0.23, 0.60), + (0.53, 0.31, 0.75), + (0.35, 0.35, 0.91), + (0.19, 0.28, 0.70), + (0.075, 0.060, 0.160), + (0.22, 0.18, 0.54), + (0.24, 0.20, 0.58), + (0.075, 0.060, 0.160), ]; /// The four interior control points that wander; the 12 boundary points stay pinned to the @@ -414,7 +426,10 @@ mod tests { } assert!((pos - 3.0).abs() < 0.01, "{pos}"); let (p, v) = spring_advance(0.0, 0.0, 1.0, BUMP_K, BUMP_C, 0.05); - assert!(p.is_finite() && v.is_finite() && p > 0.0 && p < 2.0, "{p}/{v}"); + assert!( + p.is_finite() && v.is_finite() && p > 0.0 && p < 2.0, + "{p}/{v}" + ); } /// The focused card (angle 0, scale 1) maps its center to (cx, cy) exactly. @@ -435,7 +450,15 @@ mod tests { #[test] fn side_card_inner_edge_recedes() { let flat = card_matrix(900.0, 400.0, 0.0, 1.0, POSTER_W, POSTER_H, PERSPECTIVE); - let tilted = card_matrix(900.0, 400.0, -ROTATE_DEG, 1.0, POSTER_W, POSTER_H, PERSPECTIVE); + let tilted = card_matrix( + 900.0, + 400.0, + -ROTATE_DEG, + 1.0, + POSTER_W, + POSTER_H, + PERSPECTIVE, + ); let project = |m: &[f32; 16], x: f32, y: f32| { let px = m[0] * x + m[1] * y + m[3]; let pw = m[12] * x + m[13] * y + m[15]; diff --git a/crates/pf-console-ui/src/library_ui.rs b/crates/pf-console-ui/src/library_ui.rs index ecb706a7..b9388e80 100644 --- a/crates/pf-console-ui/src/library_ui.rs +++ b/crates/pf-console-ui/src/library_ui.rs @@ -5,15 +5,17 @@ //! restack hack is gone), and every state renders in-scene (gamescope maps no dialogs). use crate::library::{ - card_matrix, initials, mesh_sksl, spring_advance, step_cursor, store_label, - LibraryGame, LibraryPhase, LibraryShared, StepResult, BUMP_C, BUMP_K, BUMP_PX, FOCUS_GAP, - JUMP, PERSPECTIVE, POSTER_H, POSTER_W, RECEDE_DIM, RECEDE_SCALE, ROTATE_DEG, SIDE_SPACING, - SPRING_C, SPRING_K, VISIBLE_RANGE, + card_matrix, initials, mesh_sksl, spring_advance, step_cursor, store_label, LibraryGame, + LibraryPhase, LibraryShared, StepResult, BUMP_C, BUMP_K, BUMP_PX, FOCUS_GAP, JUMP, PERSPECTIVE, + POSTER_H, POSTER_W, RECEDE_DIM, RECEDE_SCALE, ROTATE_DEG, SIDE_SPACING, SPRING_C, SPRING_K, + VISIBLE_RANGE, }; use anyhow::{anyhow, Result}; use pf_client_core::gamepad::{MenuDir, MenuEvent, MenuPulse}; use pf_presenter::overlay::OverlayAction; -use skia_safe::textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextAlign, TextStyle}; +use skia_safe::textlayout::{ + FontCollection, ParagraphBuilder, ParagraphStyle, TextAlign, TextStyle, +}; use skia_safe::{ Canvas, Color4f, Data, Font, FontStyle, Image, Paint, Point, RRect, Rect, RuntimeEffect, Typeface, M44, @@ -80,11 +82,7 @@ impl LibraryUi { if self.shared.generation() != self.generation { let (phase, games, generation) = self.shared.snapshot(); let fresh_games = self.games.len() != games.len() - || self - .games - .iter() - .zip(&games) - .any(|(a, b)| a.id != b.id); + || self.games.iter().zip(&games).any(|(a, b)| a.id != b.id); self.phase = phase; self.games = games; self.generation = generation; @@ -233,14 +231,8 @@ impl LibraryUi { .map_or(1.0 / 60.0, |t| (now - t).as_secs_f64().clamp(0.0, 0.05)); if self.phase == LibraryPhase::Ready { let target = f64::from(self.cursor); - (self.anim_pos, self.anim_vel) = spring_advance( - self.anim_pos, - self.anim_vel, - target, - SPRING_K, - SPRING_C, - dt, - ); + (self.anim_pos, self.anim_vel) = + spring_advance(self.anim_pos, self.anim_vel, target, SPRING_K, SPRING_C, dt); if (target - self.anim_pos).abs() < 0.001 && self.anim_vel.abs() < 0.01 { self.anim_pos = target; self.anim_vel = 0.0; @@ -259,19 +251,75 @@ impl LibraryUi { LibraryPhase::Ready => self.draw_carousel(canvas, wf, hf, k, fonts), LibraryPhase::Loading => { self.draw_spinner(canvas, wf / 2.0, hf / 2.0 - 24.0 * k, 16.0 * k); - fonts.centered(canvas, "Loading library…", 14.0 * k, DIM_TEXT, wf / 2.0, hf / 2.0 + 16.0 * k, wf * 0.8); + fonts.centered( + canvas, + "Loading library…", + 14.0 * k, + DIM_TEXT, + wf / 2.0, + hf / 2.0 + 16.0 * k, + wf * 0.8, + ); } LibraryPhase::PairFirst => { - fonts.centered_bold(canvas, "Not paired with this host", 22.0 * k, WHITE, wf / 2.0, hf / 2.0 - 20.0 * k, wf * 0.8); - fonts.centered(canvas, "Pair from the Punktfunk plugin first.", 14.0 * k, DIM_TEXT, wf / 2.0, hf / 2.0 + 12.0 * k, wf * 0.8); + fonts.centered_bold( + canvas, + "Not paired with this host", + 22.0 * k, + WHITE, + wf / 2.0, + hf / 2.0 - 20.0 * k, + wf * 0.8, + ); + fonts.centered( + canvas, + "Pair from the Punktfunk plugin first.", + 14.0 * k, + DIM_TEXT, + wf / 2.0, + hf / 2.0 + 12.0 * k, + wf * 0.8, + ); } LibraryPhase::Empty => { - fonts.centered_bold(canvas, "No games found", 22.0 * k, WHITE, wf / 2.0, hf / 2.0 - 20.0 * k, wf * 0.8); - fonts.centered(canvas, "Install Steam titles or add custom entries in the host's web console.", 14.0 * k, DIM_TEXT, wf / 2.0, hf / 2.0 + 12.0 * k, wf * 0.8); + fonts.centered_bold( + canvas, + "No games found", + 22.0 * k, + WHITE, + wf / 2.0, + hf / 2.0 - 20.0 * k, + wf * 0.8, + ); + fonts.centered( + canvas, + "Install Steam titles or add custom entries in the host's web console.", + 14.0 * k, + DIM_TEXT, + wf / 2.0, + hf / 2.0 + 12.0 * k, + wf * 0.8, + ); } LibraryPhase::Error { title, body, .. } => { - fonts.centered_bold(canvas, &title, 22.0 * k, WHITE, wf / 2.0, hf / 2.0 - 32.0 * k, wf * 0.8); - fonts.centered(canvas, &body, 14.0 * k, DIM_TEXT, wf / 2.0, hf / 2.0 + 4.0 * k, (600.0 * k).min(wf * 0.85)); + fonts.centered_bold( + canvas, + &title, + 22.0 * k, + WHITE, + wf / 2.0, + hf / 2.0 - 32.0 * k, + wf * 0.8, + ); + fonts.centered( + canvas, + &body, + 14.0 * k, + DIM_TEXT, + wf / 2.0, + hf / 2.0 + 4.0 * k, + (600.0 * k).min(wf * 0.85), + ); } } @@ -346,17 +394,28 @@ impl LibraryUi { let sh = iw / card_aspect; Rect::from_xywh(0.0, (ih - sh) / 2.0, iw, sh) }; - canvas.draw_image_rect(img, Some((&src, skia_safe::canvas::SrcRectConstraint::Fast)), rect, &Paint::default()); + canvas.draw_image_rect( + img, + Some((&src, skia_safe::canvas::SrcRectConstraint::Fast)), + rect, + &Paint::default(), + ); } None => { // Solid face, not glass: the side cards OVERLAP (GTK CSS note). - canvas.draw_rect(rect, &Paint::new(Color4f::new(0.118, 0.118, 0.145, 1.0), None)); + canvas.draw_rect( + rect, + &Paint::new(Color4f::new(0.118, 0.118, 0.145, 1.0), None), + ); let mono = initials(&game.title); let font = fonts.sans_bold(38.0 * k); let tw = font.measure_str(&mono, None).0; canvas.draw_str( &mono, - Point::new((card_w as f32 - tw) / 2.0, card_h as f32 / 2.0 + 13.0 * k as f32), + Point::new( + (card_w as f32 - tw) / 2.0, + card_h as f32 / 2.0 + 13.0 * k as f32, + ), &font, &Paint::new(Color4f::new(1.0, 1.0, 1.0, 0.45), None), ); @@ -384,7 +443,10 @@ impl LibraryUi { if prox > 0.0 { canvas.draw_rect( rect, - &Paint::new(Color4f::new(0.0, 0.0, 0.0, (prox * RECEDE_DIM) as f32), None), + &Paint::new( + Color4f::new(0.0, 0.0, 0.0, (prox * RECEDE_DIM) as f32), + None, + ), ); } canvas.restore(); @@ -392,7 +454,15 @@ impl LibraryUi { // Detail block: focused title + store, centered between strip and hints. if let Some(g) = self.games.get(self.cursor as usize) { - fonts.centered_bold(canvas, &g.title, 27.0 * k, WHITE, w / 2.0, h - 96.0 * k, w * 0.8); + fonts.centered_bold( + canvas, + &g.title, + 27.0 * k, + WHITE, + w / 2.0, + h - 96.0 * k, + w * 0.8, + ); fonts.centered( canvas, &store_label(&g.store).to_uppercase(), @@ -451,7 +521,9 @@ impl LibraryUi { } else { match &self.phase { LibraryPhase::Ready => "A Play B Quit L1 / R1 Jump".to_string(), - LibraryPhase::Error { can_retry: true, .. } => "A Retry B Quit".to_string(), + LibraryPhase::Error { + can_retry: true, .. + } => "A Retry B Quit".to_string(), _ => "B Quit".to_string(), } }; diff --git a/crates/pf-console-ui/src/skia_overlay.rs b/crates/pf-console-ui/src/skia_overlay.rs index 634c48b5..95ce7424 100644 --- a/crates/pf-console-ui/src/skia_overlay.rs +++ b/crates/pf-console-ui/src/skia_overlay.rs @@ -120,11 +120,13 @@ impl Overlay for SkiaOverlay { skvk::GetProcOf::Instance(raw_instance, name) => entry .get_instance_proc_addr(avk::Instance::from_raw(raw_instance as _), name) .map_or(std::ptr::null(), |f| f as *const std::ffi::c_void), - skvk::GetProcOf::Device(raw_device, name) => (instance.fp_v1_0() - .get_device_proc_addr)( - avk::Device::from_raw(raw_device as _), name - ) - .map_or(std::ptr::null(), |f| f as *const std::ffi::c_void), + skvk::GetProcOf::Device(raw_device, name) => { + (instance.fp_v1_0().get_device_proc_addr)( + avk::Device::from_raw(raw_device as _), + name, + ) + .map_or(std::ptr::null(), |f| f as *const std::ffi::c_void) + } } } }; @@ -173,12 +175,8 @@ impl Overlay for SkiaOverlay { } = event { use sdl3::keyboard::Mod; - if !keymod.intersects(Mod::LCTRLMOD | Mod::RCTRLMOD | Mod::LALTMOD | Mod::RALTMOD) - { - return self - .library - .as_mut() - .is_some_and(|l| l.key(*sc, *repeat)); + if !keymod.intersects(Mod::LCTRLMOD | Mod::RCTRLMOD | Mod::LALTMOD | Mod::RALTMOD) { + return self.library.as_mut().is_some_and(|l| l.key(*sc, *repeat)); } } } @@ -231,10 +229,10 @@ impl Overlay for SkiaOverlay { let slot = slots[next].as_mut().expect("just ensured"); let lib = library.as_mut().expect("library_visible"); let fonts = fonts.as_mut().expect("init ran"); - fonts.chip_text = Some( - ctx.pad - .map_or("No controller — keyboard works too".to_string(), str::to_owned), - ); + fonts.chip_text = Some(ctx.pad.map_or( + "No controller — keyboard works too".to_string(), + str::to_owned, + )); lib.sync(); lib.render(slot.surface.canvas(), ctx.width, ctx.height, fonts); gpu.context.flush_surface_with_texture_state( @@ -269,15 +267,12 @@ impl Overlay for SkiaOverlay { }; if want == self.drawn { // Unchanged — hand the presenter the already-rendered image. - return Ok(self - .slots[self.current] - .as_ref() - .map(|s| OverlayFrame { - image: s.image, - view: s.view, - width: s.width, - height: s.height, - })); + return Ok(self.slots[self.current].as_ref().map(|s| OverlayFrame { + image: s.image, + view: s.view, + width: s.width, + height: s.height, + })); } // Render into the OTHER slot — the presenter may still be sampling the current @@ -335,10 +330,8 @@ impl SkiaOverlay { // alternates and the presenter waits its fence before each record). unsafe { gpu.device.destroy_image_view(old.view, None) }; } - let info = skia_safe::ImageInfo::new_n32_premul( - (width.max(1) as i32, height.max(1) as i32), - None, - ); + let info = + skia_safe::ImageInfo::new_n32_premul((width.max(1) as i32, height.max(1) as i32), None); let mut surface = gpu::surfaces::render_target( &mut gpu.context, gpu::Budgeted::Yes, @@ -350,12 +343,11 @@ impl SkiaOverlay { None, ) .context("Skia render-target surface")?; - let texture = - gpu::surfaces::get_backend_texture( - &mut surface, - skia_safe::surface::BackendHandleAccess::FlushRead, - ) - .context("surface backend texture")?; + let texture = gpu::surfaces::get_backend_texture( + &mut surface, + skia_safe::surface::BackendHandleAccess::FlushRead, + ) + .context("surface backend texture")?; let image_info = texture .vulkan_image_info() .context("backend texture is not Vulkan")?; @@ -412,10 +404,7 @@ fn draw_osd_panel(canvas: &Canvas, font: &Font, text: &str, x: f32, y: f32) { for (i, line) in lines.iter().enumerate() { canvas.draw_str( line, - Point::new( - x + pad_x, - y + pad_y - metrics.ascent + line_h * i as f32, - ), + Point::new(x + pad_x, y + pad_y - metrics.ascent + line_h * i as f32), font, &text_paint, ); diff --git a/crates/pf-presenter/src/csc.rs b/crates/pf-presenter/src/csc.rs index e454abb3..8832814f 100644 --- a/crates/pf-presenter/src/csc.rs +++ b/crates/pf-presenter/src/csc.rs @@ -326,11 +326,10 @@ pub(crate) fn build_fullscreen_pipeline( .dynamic_state(&dynamic_state) .layout(layout) .render_pass(render_pass); - let pipeline = unsafe { - device.create_graphics_pipelines(vk::PipelineCache::null(), &[info], None) - } - .map_err(|(_, e)| e) - .context("CSC pipeline"); + let pipeline = + unsafe { device.create_graphics_pipelines(vk::PipelineCache::null(), &[info], None) } + .map_err(|(_, e)| e) + .context("CSC pipeline"); unsafe { device.destroy_shader_module(vert_mod, None); device.destroy_shader_module(frag_mod, None); @@ -444,7 +443,10 @@ mod tests { core::array::from_fn(|r| (0..3).map(|c| mat[c * 3 + r] * v[c]).sum()); let ours = apply(&rows, yuv); for (a, b) in gl.iter().zip(ours) { - assert!((a - b).abs() < 1e-5, "{matrix}/{full}: gl {gl:?} rows {ours:?}"); + assert!( + (a - b).abs() < 1e-5, + "{matrix}/{full}: gl {gl:?} rows {ours:?}" + ); } } } diff --git a/crates/pf-presenter/src/run.rs b/crates/pf-presenter/src/run.rs index 98ccb669..cf91a380 100644 --- a/crates/pf-presenter/src/run.rs +++ b/crates/pf-presenter/src/run.rs @@ -16,8 +16,8 @@ use crate::overlay::{FrameCtx, Overlay, OverlayAction, OverlayFrame, SessionPhas use crate::vk::{FrameInput, Presenter}; use anyhow::{Context as _, Result}; use pf_client_core::gamepad::GamepadService; -use pf_client_core::video::VulkanDecodeDevice; use pf_client_core::session::{self, SessionEvent, SessionHandle, SessionParams, Stats}; +use pf_client_core::video::VulkanDecodeDevice; use pf_client_core::video::{DecodedFrame, DecodedImage}; use punktfunk_core::client::NativeClient; use punktfunk_core::config::Mode; @@ -379,8 +379,8 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result } // F11 or Alt+Enter (some keyboards' Fn layer sends a media key for // plain F11 — the Moonlight-standard alias always exists). - let alt_enter = sc == Scancode::Return - && keymod.intersects(Mod::LALTMOD | Mod::RALTMOD); + let alt_enter = + sc == Scancode::Return && keymod.intersects(Mod::LALTMOD | Mod::RALTMOD); if sc == Scancode::F11 || alt_enter { fullscreen = !fullscreen; tracing::debug!(fullscreen, "fullscreen toggle"); @@ -535,9 +535,15 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result println!("stats: {}", st.osd_text.replace('\n', " | ")); } } - SessionEvent::Failed { msg, trust_rejected } => match &mode { + SessionEvent::Failed { + msg, + trust_rejected, + } => match &mode { ModeCtl::Single(_) => { - break 'main Some(Outcome::ConnectFailed { msg, trust_rejected }) + break 'main Some(Outcome::ConnectFailed { + msg, + trust_rejected, + }) } ModeCtl::Browse(_) => { tracing::warn!(%msg, "connect failed — back to the library"); diff --git a/crates/pf-presenter/src/vk.rs b/crates/pf-presenter/src/vk.rs index a4c162de..0f7c5b9a 100644 --- a/crates/pf-presenter/src/vk.rs +++ b/crates/pf-presenter/src/vk.rs @@ -376,11 +376,11 @@ impl Presenter { .api_version(vk::API_VERSION_1_3); // HDR10 presentation needs the extended colorspaces at the INSTANCE level. let mut instance_extensions: Vec = instance_extensions.to_vec(); - let inst_available = unsafe { entry.enumerate_instance_extension_properties(None) } - .unwrap_or_default(); - let has_colorspace_ext = inst_available.iter().any(|e| { - e.extension_name_as_c_str() == Ok(c"VK_EXT_swapchain_colorspace") - }); + let inst_available = + unsafe { entry.enumerate_instance_extension_properties(None) }.unwrap_or_default(); + let has_colorspace_ext = inst_available + .iter() + .any(|e| e.extension_name_as_c_str() == Ok(c"VK_EXT_swapchain_colorspace")); if has_colorspace_ext { instance_extensions.push("VK_EXT_swapchain_colorspace".into()); } @@ -486,10 +486,8 @@ impl Presenter { ash::khr::video_decode_h265::NAME, c"VK_KHR_video_decode_av1", ]; - let codec_exts: Vec<&std::ffi::CStr> = VIDEO_CODECS - .into_iter() - .filter(|n| has(n)) - .collect(); + let codec_exts: Vec<&std::ffi::CStr> = + VIDEO_CODECS.into_iter().filter(|n| has(n)).collect(); let video_ok = dev_is_13 && features_ok && decode_family.is_some() @@ -603,8 +601,7 @@ impl Presenter { None }; - let (format, hdr10_format) = - pick_formats(&surface_i, pdev, surface, has_colorspace_ext)?; + let (format, hdr10_format) = pick_formats(&surface_i, pdev, surface, has_colorspace_ext)?; let present_mode = pick_present_mode(&surface_i, pdev, surface)?; tracing::info!(?format, ?hdr10_format, ?present_mode, "swapchain config"); let overlay_pipe = OverlayPipe::new(&device, format.format)?; @@ -730,8 +727,8 @@ impl Presenter { .present_mode(self.present_mode) .clipped(true) .old_swapchain(old); - let swapchain = unsafe { self.swap_d.create_swapchain(&info, None) } - .context("vkCreateSwapchainKHR")?; + let swapchain = + unsafe { self.swap_d.create_swapchain(&info, None) }.context("vkCreateSwapchainKHR")?; // The old swapchain (and everything tied to its images) is parked, not // destroyed: the presentation engine may still hold its final present's // semaphore wait, which no fence covers. It dies after the next present on @@ -746,8 +743,12 @@ impl Presenter { self.swapchain = swapchain; self.images = unsafe { self.swap_d.get_swapchain_images(swapchain) }?; self.extent = extent; - self.overlay_pipe - .rebuild_targets(&self.device, &self.images, self.format.format, extent)?; + self.overlay_pipe.rebuild_targets( + &self.device, + &self.images, + self.format.format, + extent, + )?; for _ in 0..self.images.len() { self.render_sems.push(unsafe { @@ -929,7 +930,8 @@ impl Presenter { tracing::info!(width = f.width, height = f.height, "video image (re)built"); } // Safe while nothing in flight references the set — the fence wait above. - self.csc.bind_planes(&self.device, f.luma_view, f.chroma_view); + self.csc + .bind_planes(&self.device, f.luma_view, f.chroma_view); } if let Some((f, views)) = &vk_frame { if self @@ -995,7 +997,13 @@ impl Presenter { height: v.height, }; let ten_bit = f.is_p010(); - self.record_csc(v.framebuffer, extent, f.color, if ten_bit { 10 } else { 8 }, ten_bit); + self.record_csc( + v.framebuffer, + extent, + f.color, + if ten_bit { 10 } else { 8 }, + ten_bit, + ); } // Vulkan-Video frame: the decoded image is already on THIS device. Read the @@ -1019,7 +1027,13 @@ impl Presenter { }; let ten_bit = f.vk_format == vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16.as_raw(); - self.record_csc(v.framebuffer, extent, f.color, if ten_bit { 10 } else { 8 }, ten_bit); + self.record_csc( + v.framebuffer, + extent, + f.color, + if ten_bit { 10 } else { 8 }, + ten_bit, + ); vk_sync = Some(sync); } @@ -1207,7 +1221,10 @@ impl Presenter { if let Some(sync) = vk_sync.take() { let ok = submitted.is_ok(); unlock_vkframe( - vk_frame.as_ref().map(|(f, _)| f).expect("vk_sync implies vk_frame"), + vk_frame + .as_ref() + .map(|(f, _)| f) + .expect("vk_sync implies vk_frame"), &sync, ok, self.qfi, @@ -1217,10 +1234,11 @@ impl Presenter { self.submitted = true; // The hw frame is on the GPU now — park it until the fence proves the reads // done (destroyed at the next present's fence wait, or in Drop). - self.retired_hw = hw_frame - .take() - .map(Retired::Dmabuf) - .or_else(|| vk_frame.take().map(|(frame, views)| Retired::Vk { frame, views })); + self.retired_hw = hw_frame.take().map(Retired::Dmabuf).or_else(|| { + vk_frame + .take() + .map(|(frame, views)| Retired::Vk { frame, views }) + }); let swapchains = [self.swapchain]; let indices = [index]; @@ -1242,7 +1260,6 @@ impl Presenter { } } - /// Record the NV12→RGBA CSC pass into the video image (framebuffer): fullscreen /// triangle, CICP-driven push-constant rows. Shared by the dmabuf and Vulkan-Video /// paths — only the plane views bound beforehand differ. @@ -1306,7 +1323,11 @@ impl Presenter { let rows = csc_rows(color, depth, msb_packed); // Mode 1 = PQ→SDR tonemap (a PQ stream without an HDR10 surface); mode 0 // passes the transfer through (SDR as-is, or PQ onto the HDR10 swapchain). - let mode = if color.is_pq() && !self.hdr_active { 1.0f32 } else { 0.0 }; + let mode = if color.is_pq() && !self.hdr_active { + 1.0f32 + } else { + 0.0 + }; let peak = std::env::var("PUNKTFUNK_TONEMAP_PEAK") .ok() .and_then(|v| v.parse::().ok()) @@ -1332,8 +1353,7 @@ impl Presenter { /// exact sampling contract (the frames pool was created MUTABLE_FORMAT for this). /// 8-bit NV12 (R8 + R8G8) and 10-bit P010/X6 (R10X6 + R10X6G10X6). fn vkframe_plane_views(&self, f: &VkVideoFrame) -> Result<[vk::ImageView; 2]> { - let (luma_fmt, chroma_fmt) = if f.vk_format - == vk::Format::G8_B8R8_2PLANE_420_UNORM.as_raw() + let (luma_fmt, chroma_fmt) = if f.vk_format == vk::Format::G8_B8R8_2PLANE_420_UNORM.as_raw() { (vk::Format::R8_UNORM, vk::Format::R8G8_UNORM) } else if f.vk_format == vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16.as_raw() { @@ -1348,9 +1368,10 @@ impl Presenter { ); }; // img[0] is creation-constant (only the sync fields need the frames lock). - let image = vk::Image::from_raw( - unsafe { (*(f.vkframe as *const pf_ffvk::AVVkFrame)).img[0] } as u64, - ); + let image = + vk::Image::from_raw( + unsafe { (*(f.vkframe as *const pf_ffvk::AVVkFrame)).img[0] } as u64, + ); let make = |aspect: vk::ImageAspectFlags, format: vk::Format| { unsafe { self.device.create_image_view( @@ -1612,10 +1633,9 @@ fn pick_device( let families = unsafe { instance.get_physical_device_queue_family_properties(pdev) }; for (i, f) in families.iter().enumerate() { let graphics = f.queue_flags.contains(vk::QueueFlags::GRAPHICS); - let present = unsafe { - surface_i.get_physical_device_surface_support(pdev, i as u32, surface) - } - .unwrap_or(false); + let present = + unsafe { surface_i.get_physical_device_surface_support(pdev, i as u32, surface) } + .unwrap_or(false); if graphics && present { return Ok((pdev, i as u32)); } @@ -1724,7 +1744,6 @@ fn subresource_range() -> vk::ImageSubresourceRange { .layer_count(1) } - /// The live sync state of an `AVVkFrame`, snapshotted under the frames lock. struct VkFrameSync { image: u64, @@ -1766,10 +1785,8 @@ fn unlock_vkframe(f: &VkVideoFrame, sync: &VkFrameSync, submitted: bool, graphic (*vkf).queue_family[0] = graphics_qf; } } - let unlock: unsafe extern "C" fn( - *mut pf_ffvk::AVHWFramesContext, - *mut pf_ffvk::AVVkFrame, - ) = std::mem::transmute(f.unlock_frame); + let unlock: unsafe extern "C" fn(*mut pf_ffvk::AVHWFramesContext, *mut pf_ffvk::AVVkFrame) = + std::mem::transmute(f.unlock_frame); unlock(f.frames_ctx as *mut pf_ffvk::AVHWFramesContext, vkf); } } diff --git a/crates/punktfunk-host/src/vdisplay.rs b/crates/punktfunk-host/src/vdisplay.rs index deed3c8f..232145e7 100644 --- a/crates/punktfunk-host/src/vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay.rs @@ -1180,6 +1180,9 @@ pub(crate) mod identity; #[path = "vdisplay/admission.rs"] pub(crate) mod admission; #[cfg(target_os = "linux")] +#[path = "vdisplay/linux/hyprland.rs"] +mod hyprland; +#[cfg(target_os = "linux")] #[path = "vdisplay/linux/kwin.rs"] mod kwin; #[cfg(target_os = "windows")] @@ -1194,9 +1197,6 @@ pub(crate) mod pf_vdisplay; #[cfg(target_os = "linux")] #[path = "vdisplay/linux/wlroots.rs"] mod wlroots; -#[cfg(target_os = "linux")] -#[path = "vdisplay/linux/hyprland.rs"] -mod hyprland; #[cfg(test)] mod tests { diff --git a/crates/punktfunk-host/src/vdisplay/linux/hyprland.rs b/crates/punktfunk-host/src/vdisplay/linux/hyprland.rs index 92c0a4cf..ed468522 100644 --- a/crates/punktfunk-host/src/vdisplay/linux/hyprland.rs +++ b/crates/punktfunk-host/src/vdisplay/linux/hyprland.rs @@ -146,8 +146,9 @@ impl VirtualDisplay for HyprlandDisplay { preflight_once(); let name = next_output_name(); - hyprctl_dispatch(&["output", "create", "headless", &name]) - .with_context(|| format!("hyprctl output create headless {name} (is hyprctl reachable?)"))?; + hyprctl_dispatch(&["output", "create", "headless", &name]).with_context(|| { + format!("hyprctl output create headless {name} (is hyprctl reachable?)") + })?; // Own the output from here on so any later error (or drop) removes it. let output = OutputGuard(name.clone()); wait_monitor_ready(&name, Duration::from_secs(5))