From e7dcfd2fd556ac29576fa801f2702779757d0dbf Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 15 Apr 2026 01:57:14 -0300 Subject: [PATCH] feat(launcher): guide staging flow and capture modes --- client/src/launcher/device_test.rs | 52 +++--- client/src/launcher/ui.rs | 69 ++++++-- client/src/launcher/ui_components.rs | 68 +++++++- client/src/launcher/ui_runtime.rs | 235 ++++++++++++++++++++++++-- scripts/ci/hygiene_gate_baseline.json | 219 ++++++++++++------------ scripts/ci/quality_gate_baseline.json | 164 +++++++++--------- 6 files changed, 553 insertions(+), 254 deletions(-) diff --git a/client/src/launcher/device_test.rs b/client/src/launcher/device_test.rs index 08e16da..638b385 100644 --- a/client/src/launcher/device_test.rs +++ b/client/src/launcher/device_test.rs @@ -21,6 +21,7 @@ pub enum DeviceTestKind { Speaker, } +#[derive(Default)] pub struct DeviceTestController { camera: Option, selected_camera: Option, @@ -28,17 +29,6 @@ pub struct DeviceTestController { speaker: Option, } -impl Default for DeviceTestController { - fn default() -> Self { - Self { - camera: None, - selected_camera: None, - microphone: None, - speaker: None, - } - } -} - impl DeviceTestController { pub fn new() -> Self { Self::default() @@ -227,7 +217,9 @@ impl LocalCameraPreview { } self.set_status(match self.selected_device.as_deref() { - Some(camera) => format!("Selected {camera}. Click Start Preview to verify it here."), + Some(camera) => format!( + "Selected {camera}. Start Preview to confirm framing here before you launch the relay." + ), None => CAMERA_PREVIEW_IDLE.to_string(), }); Ok(()) @@ -255,7 +247,7 @@ impl LocalCameraPreview { let running = Arc::clone(&self.running); let token = generation.fetch_add(1, Ordering::AcqRel) + 1; running.store(true, Ordering::Release); - self.set_status(format!("Starting preview for {selected}...")); + self.set_status(format!("Starting local preview for {selected}...")); std::thread::spawn(move || { if let Err(err) = run_camera_preview_feed( @@ -266,12 +258,11 @@ impl LocalCameraPreview { status_text.clone(), generation.clone(), running.clone(), - ) { - if generation.load(Ordering::Acquire) == token { - running.store(false, Ordering::Release); - if let Ok(mut status) = status_text.lock() { - *status = format!("Camera preview failed: {err}"); - } + ) && generation.load(Ordering::Acquire) == token + { + running.store(false, Ordering::Release); + if let Ok(mut status) = status_text.lock() { + *status = format!("Camera preview failed: {err}"); } } }); @@ -285,7 +276,9 @@ impl LocalCameraPreview { *latest = None; } self.set_status(match self.selected_device.as_deref() { - Some(camera) => format!("Preview stopped. {camera} is still selected."), + Some(camera) => { + format!("Local preview stopped. {camera} stays selected for the next relay launch.") + } None => CAMERA_PREVIEW_IDLE.to_string(), }); } @@ -327,16 +320,15 @@ fn run_camera_preview_feed( .context("starting in-launcher camera preview pipeline")?; if let Ok(mut status) = status_text.lock() { - *status = format!("Previewing {selected} locally."); + *status = format!("Local preview live for {selected}."); } while running.load(Ordering::Acquire) && generation.load(Ordering::Acquire) == token { - if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(250)) { - if let Some(frame) = sample_to_frame(&sample) { - if let Ok(mut slot) = latest.lock() { - *slot = Some(frame); - } - } + if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(250)) + && let Some(frame) = sample_to_frame(&sample) + && let Ok(mut slot) = latest.lock() + { + *slot = Some(frame); } } @@ -363,9 +355,9 @@ fn build_camera_preview_pipeline(device: &str) -> Result<(gst::Pipeline, gst_app .expect("camera preview appsink"); appsink.set_caps(Some( &gst::Caps::builder("video/x-raw") - .field("format", &"RGBA") - .field("width", &CAMERA_PREVIEW_WIDTH) - .field("height", &CAMERA_PREVIEW_HEIGHT) + .field("format", "RGBA") + .field("width", CAMERA_PREVIEW_WIDTH) + .field("height", CAMERA_PREVIEW_HEIGHT) .build(), )); Ok((pipeline, appsink)) diff --git a/client/src/launcher/ui.rs b/client/src/launcher/ui.rs index 7541a0e..009ec2a 100644 --- a/client/src/launcher/ui.rs +++ b/client/src/launcher/ui.rs @@ -3,7 +3,7 @@ use anyhow::Result; #[cfg(not(coverage))] use { super::clipboard::send_clipboard_to_remote, - super::device_test::DeviceTestController, + super::device_test::{DeviceTestController, DeviceTestKind}, super::devices::DeviceCatalog, super::diagnostics::quality_probe_command, super::launcher_focus_signal_path, @@ -121,11 +121,18 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { let camera_combo_read = camera_combo.clone(); camera_combo.connect_changed(move |_| { let selected = selected_combo_value(&camera_combo_read); + let preview_was_running = + tests.borrow_mut().is_running(DeviceTestKind::Camera); state.borrow_mut().select_camera(selected.clone()); if let Err(err) = tests.borrow_mut().set_camera_selection(selected.as_deref()) { widgets .status_label .set_text(&format!("Camera preview update failed: {err}")); + } else if preview_was_running { + widgets.status_label.set_text(&format!( + "Local camera preview switched to {}.", + selected.as_deref().unwrap_or("auto") + )); } refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); refresh_test_buttons(&widgets, &mut tests.borrow_mut()); @@ -136,13 +143,20 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { let state = Rc::clone(&state); let widgets = widgets.clone(); let child_proc = Rc::clone(&child_proc); + let tests = Rc::clone(&tests); let microphone_combo = microphone_combo.clone(); let microphone_combo_read = microphone_combo.clone(); microphone_combo.connect_changed(move |_| { state .borrow_mut() .select_microphone(selected_combo_value(µphone_combo_read)); + if tests.borrow_mut().is_running(DeviceTestKind::Microphone) { + widgets.status_label.set_text( + "Microphone selection changed. Restart Monitor Mic to audition the new input.", + ); + } refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); + refresh_test_buttons(&widgets, &mut tests.borrow_mut()); }); } @@ -150,13 +164,23 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { let state = Rc::clone(&state); let widgets = widgets.clone(); let child_proc = Rc::clone(&child_proc); + let tests = Rc::clone(&tests); let speaker_combo = speaker_combo.clone(); let speaker_combo_read = speaker_combo.clone(); speaker_combo.connect_changed(move |_| { state .borrow_mut() .select_speaker(selected_combo_value(&speaker_combo_read)); + let speaker_running = tests.borrow_mut().is_running(DeviceTestKind::Speaker); + let microphone_running = + tests.borrow_mut().is_running(DeviceTestKind::Microphone); + if speaker_running || microphone_running { + widgets.status_label.set_text( + "Speaker selection changed. Restart the local audio tests to hear the new output.", + ); + } refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); + refresh_test_buttons(&widgets, &mut tests.borrow_mut()); }); } @@ -203,10 +227,23 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { Ok(child) => { *child_proc.borrow_mut() = Some(child); let _ = state.borrow_mut().start_remote(); - widgets_handle.status_label.set_text(&format!( - "Relay started. Inputs are routed to {}.", - routing_name(state.borrow().routing) - )); + let routing = routing_name(state.borrow().routing); + let power_mode = state.borrow().capture_power.mode.clone(); + let message = match power_mode.as_str() { + "forced-off" => format!( + "Relay started with inputs routed to {}, but capture is forced off. Return capture to Auto or Force On when you want remote video.", + routing + ), + "forced-on" => format!( + "Relay started with inputs routed to {}. Capture is being held awake for staging.", + routing + ), + _ => format!( + "Relay started with inputs routed to {}. Capture will wake automatically for previews and live session demand.", + routing + ), + }; + widgets_handle.status_label.set_text(&message); } Err(err) => { widgets_handle @@ -231,7 +268,9 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { stop_button.connect_clicked(move |_| { stop_child_process(&child_proc); let _ = state.borrow_mut().stop_remote(); - widgets_handle.status_label.set_text("Relay stopped."); + widgets_handle + .status_label + .set_text("Relay stopped. Local staging remains available."); refresh_launcher_ui(&widgets_handle, &state.borrow(), false); }); } @@ -351,8 +390,8 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { &widgets_handle, &mut tests.borrow_mut(), result, - "Camera preview started inside the launcher.", - "Camera preview stopped.", + "Camera preview started inside the launcher. This stays local until you start the relay.", + "Camera preview stopped. The selected camera stays staged for the next launch.", ); }); } @@ -374,7 +413,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { &widgets_handle, &mut tests.borrow_mut(), result, - "Microphone monitor started.", + "Microphone monitor started locally through the selected speaker.", "Microphone monitor stopped.", ); }); @@ -394,7 +433,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { &widgets_handle, &mut tests.borrow_mut(), result, - "Speaker test tone started.", + "Speaker tone started locally.", "Speaker test tone stopped.", ); }); @@ -547,7 +586,9 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { let child_running = reap_exited_child(&child_proc); if !child_running && state.borrow().remote_active { let _ = state.borrow_mut().stop_remote(); - widgets.status_label.set_text("Relay ended."); + widgets + .status_label + .set_text("Relay ended. Local staging remains available."); } let next_state_marker = path_marker(input_state_path.as_path()); @@ -593,9 +634,9 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> { let mode = power.mode.clone(); state.borrow_mut().set_capture_power(power); widgets.status_label.set_text(match mode.as_str() { - "forced-on" => "Capture feeds forced on.", - "forced-off" => "Capture feeds forced off.", - _ => "Capture feeds returned to automatic mode.", + "forced-on" => "Capture feeds forced on. Remote eyes stay awake even if previews or the relay stop.", + "forced-off" => "Capture feeds forced off. Remote eye previews and session video stay dark until you switch back.", + _ => "Capture feeds returned to automatic mode. Live previews and relay demand will wake them as needed.", }); } PowerMessage::Command(Err(err)) => { diff --git a/client/src/launcher/ui_components.rs b/client/src/launcher/ui_components.rs index c1b08fe..eea1452 100644 --- a/client/src/launcher/ui_components.rs +++ b/client/src/launcher/ui_components.rs @@ -39,6 +39,10 @@ pub struct LauncherWidgets { pub status_label: gtk::Label, pub summary: SummaryWidgets, pub power_detail: gtk::Label, + pub launch_plan_title: gtk::Label, + pub launch_plan_summary: gtk::Label, + pub launch_plan_detail: gtk::Label, + pub local_test_detail: gtk::Label, pub display_panes: [DisplayPaneWidgets; 2], pub start_button: gtk::Button, pub stop_button: gtk::Button, @@ -151,8 +155,14 @@ pub fn build_launcher_view( )); let start_button = gtk::Button::with_label("Start Relay"); start_button.add_css_class("suggested-action"); + start_button.set_tooltip_text(Some( + "Launch the relay using the staged devices and current input routing.", + )); let stop_button = gtk::Button::with_label("Stop Relay"); stop_button.add_css_class("destructive-action"); + stop_button.set_tooltip_text(Some( + "Stop the live relay session. Local staging and previews stay available.", + )); server_row.append(&server_entry); server_row.append(&start_button); server_row.append(&stop_button); @@ -225,7 +235,7 @@ pub fn build_launcher_view( let (devices_panel, devices_body) = build_panel("Device Staging"); let devices_intro = gtk::Label::new(Some( - "Lock in the exact local camera, microphone, and speaker you want before you launch the relay.", + "Choose the exact local camera, microphone, and speaker the next relay launch should inherit.", )); devices_intro.add_css_class("dim-label"); devices_intro.set_wrap(true); @@ -320,6 +330,37 @@ pub fn build_launcher_view( devices_body.append(&preview_shell); sidebar.append(&devices_panel); + let (plan_panel, plan_body) = build_panel("Launch Plan"); + let launch_plan_title = gtk::Label::new(Some("Stage locally, then start the relay.")); + launch_plan_title.add_css_class("title-4"); + launch_plan_title.set_halign(gtk::Align::Start); + launch_plan_title.set_wrap(true); + let launch_plan_summary = + gtk::Label::new(Some("Camera: auto\nMicrophone: auto\nSpeaker: auto")); + launch_plan_summary.add_css_class("launch-plan-summary"); + launch_plan_summary.set_halign(gtk::Align::Start); + launch_plan_summary.set_xalign(0.0); + launch_plan_summary.set_wrap(true); + let local_test_detail = gtk::Label::new(Some( + "Local checks are idle. Use Start Preview, Monitor Mic, or Play Tone before you launch.", + )); + local_test_detail.add_css_class("dim-label"); + local_test_detail.set_halign(gtk::Align::Start); + local_test_detail.set_xalign(0.0); + local_test_detail.set_wrap(true); + let launch_plan_detail = gtk::Label::new(Some( + "Automatic capture mode will wake the remote feeds when previews or the live relay ask for them.", + )); + launch_plan_detail.add_css_class("dim-label"); + launch_plan_detail.set_halign(gtk::Align::Start); + launch_plan_detail.set_xalign(0.0); + launch_plan_detail.set_wrap(true); + plan_body.append(&launch_plan_title); + plan_body.append(&launch_plan_summary); + plan_body.append(&local_test_detail); + plan_body.append(&launch_plan_detail); + sidebar.append(&plan_panel); + let (actions_panel, actions_body) = build_panel("Remote Actions"); let actions_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); let clipboard_button = gtk::Button::with_label("Send Clipboard"); @@ -336,11 +377,18 @@ pub fn build_launcher_view( sidebar.append(&actions_panel); let stage_header = gtk::Box::new(gtk::Orientation::Horizontal, 8); - let stage_title = gtk::Label::new(Some("Remote Eyes")); + let stage_title = gtk::Label::new(Some("Remote Eye Feeds")); stage_title.add_css_class("title-4"); stage_title.set_halign(gtk::Align::Start); stage_header.append(&stage_title); + let stage_note = gtk::Label::new(Some( + "These are the live server-side eye feeds. In Auto mode, open eye previews and active relay sessions count as capture demand.", + )); + stage_note.add_css_class("dim-label"); + stage_note.set_wrap(true); + stage_note.set_xalign(0.0); stage.append(&stage_header); + stage.append(&stage_note); let display_row = gtk::Box::new(gtk::Orientation::Horizontal, 16); display_row.set_hexpand(true); @@ -387,6 +435,10 @@ pub fn build_launcher_view( shortcut_value, }, power_detail, + launch_plan_title, + launch_plan_summary, + launch_plan_detail, + local_test_detail, display_panes: [left_pane.clone(), right_pane.clone()], start_button: start_button.clone(), stop_button: stop_button.clone(), @@ -482,6 +534,13 @@ pub fn install_css(window: >k::ApplicationWindow) { label.status-line { opacity: 0.88; } + label.launch-plan-summary { + font-family: monospace; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 14px; + padding: 12px; + } entry.server-entry { min-height: 38px; } @@ -489,6 +548,11 @@ pub fn install_css(window: >k::ApplicationWindow) { min-height: 36px; padding: 0 14px; } + button.pill-toggle-active { + background: rgba(91, 179, 162, 0.2); + border-color: rgba(91, 179, 162, 0.45); + font-weight: 700; + } "#, ); if let Some(display) = gtk::gdk::Display::default() { diff --git a/client/src/launcher/ui_runtime.rs b/client/src/launcher/ui_runtime.rs index 5a8a493..b33528e 100644 --- a/client/src/launcher/ui_runtime.rs +++ b/client/src/launcher/ui_runtime.rs @@ -52,7 +52,21 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi widgets .power_detail .set_text(&capture_power_detail(&state.capture_power)); + widgets + .launch_plan_title + .set_text(&launch_plan_title(state, child_running)); + widgets + .launch_plan_summary + .set_text(&launch_plan_summary(state)); + widgets + .launch_plan_detail + .set_text(&launch_plan_detail(state, child_running)); + widgets.start_button.set_label(if child_running { + "Relay Running" + } else { + "Start Relay" + }); widgets.start_button.set_sensitive(!child_running); widgets.stop_button.set_sensitive(child_running); widgets.clipboard_button.set_sensitive(child_running); @@ -71,6 +85,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi widgets.power_off_button.set_sensitive( power_available && !matches!(state.capture_power.mode.as_str(), "forced-off"), ); + sync_power_mode_button_styles(widgets, state.capture_power.mode.as_str()); for monitor_id in 0..2 { refresh_display_pane( @@ -81,27 +96,32 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi } pub fn refresh_test_buttons(widgets: &LauncherWidgets, tests: &mut DeviceTestController) { - widgets - .camera_test_button - .set_label(if tests.is_running(DeviceTestKind::Camera) { - "Stop Preview" - } else { - "Start Preview" - }); + let camera_running = tests.is_running(DeviceTestKind::Camera); + let microphone_running = tests.is_running(DeviceTestKind::Microphone); + let speaker_running = tests.is_running(DeviceTestKind::Speaker); + + widgets.camera_test_button.set_label(if camera_running { + "Stop Preview" + } else { + "Start Preview" + }); widgets .microphone_test_button - .set_label(if tests.is_running(DeviceTestKind::Microphone) { + .set_label(if microphone_running { "Stop Monitor" } else { "Monitor Mic" }); - widgets - .speaker_test_button - .set_label(if tests.is_running(DeviceTestKind::Speaker) { - "Stop Tone" - } else { - "Play Tone" - }); + widgets.speaker_test_button.set_label(if speaker_running { + "Stop Tone" + } else { + "Play Tone" + }); + widgets.local_test_detail.set_text(&local_test_detail( + camera_running, + microphone_running, + speaker_running, + )); } pub fn update_test_action_result( @@ -140,7 +160,7 @@ pub fn open_popout_window( let window = gtk::ApplicationWindow::builder() .application(app) - .title(&format!( + .title(format!( "Lesavka {}", widgets.display_panes[monitor_id].title )) @@ -310,6 +330,137 @@ pub fn capture_power_detail(power: &CapturePowerStatus) -> String { } } +/// Highlights the currently active capture mode so it reads like a segmented control. +fn sync_power_mode_button_styles(widgets: &LauncherWidgets, mode: &str) { + for button in [ + &widgets.power_auto_button, + &widgets.power_on_button, + &widgets.power_off_button, + ] { + button.remove_css_class("pill-toggle-active"); + } + match mode { + "forced-on" => widgets.power_on_button.add_css_class("pill-toggle-active"), + "forced-off" => widgets.power_off_button.add_css_class("pill-toggle-active"), + _ => widgets + .power_auto_button + .add_css_class("pill-toggle-active"), + } +} + +/// Summarizes the current staging state as a short operator-facing heading. +fn launch_plan_title(state: &LauncherState, child_running: bool) -> String { + if child_running || state.remote_active { + return match state.capture_power.mode.as_str() { + "forced-off" => "Relay live, but capture is intentionally dark.".to_string(), + "forced-on" => "Relay live with capture held awake.".to_string(), + _ => "Relay live with automatic capture management.".to_string(), + }; + } + + match state.capture_power.mode.as_str() { + "forced-off" => "Staging mode is holding capture off.".to_string(), + "forced-on" => "Capture is pre-warmed for staging.".to_string(), + _ => "Stage locally, then start the relay.".to_string(), + } +} + +/// Shows the exact devices the next relay launch will inherit. +fn launch_plan_summary(state: &LauncherState) -> String { + format!( + "Camera: {}\nMicrophone: {}\nSpeaker: {}", + selected_device_label(state.devices.camera.as_deref()), + selected_device_label(state.devices.microphone.as_deref()), + selected_device_label(state.devices.speaker.as_deref()) + ) +} + +/// Explains the consequence of the current capture and session state. +fn launch_plan_detail(state: &LauncherState, child_running: bool) -> String { + if child_running || state.remote_active { + return match state.capture_power.mode.as_str() { + "forced-off" => format!( + "Inputs are routed to {}. Return capture to Auto or Force On when you want the remote eyes and session video to wake up.", + capitalize(routing_name(state.routing)) + ), + "forced-on" => format!( + "Inputs are routed to {}. The relay host is keeping the capture feeds up even without preview demand.", + capitalize(routing_name(state.routing)) + ), + _ => format!( + "Inputs are routed to {}. Live eye previews and session demand will wake capture automatically as needed.", + capitalize(routing_name(state.routing)) + ), + }; + } + + if !state.capture_power.available { + return format!( + "Capture power status from {} is unavailable right now. You can still stage devices locally before you try the relay host again.", + state.capture_power.unit + ); + } + + match state.capture_power.mode.as_str() { + "forced-off" => { + "Remote eye previews and the next relay session will stay dark until you return to Auto or Force On." + .to_string() + } + "forced-on" => { + "The relay host is already holding capture awake, which is useful for preflight framing checks before the session starts." + .to_string() + } + _ => { + "Automatic capture mode wakes the remote feeds only while eye previews or the live relay session actually need them." + .to_string() + } + } +} + +/// Reports which local staging checks are active right now. +fn local_test_detail( + camera_running: bool, + microphone_running: bool, + speaker_running: bool, +) -> String { + let mut active = Vec::new(); + if camera_running { + active.push("camera preview"); + } + if microphone_running { + active.push("mic monitor"); + } + if speaker_running { + active.push("speaker tone"); + } + + if active.is_empty() { + "Local checks are idle. Use Start Preview, Monitor Mic, or Play Tone before you launch." + .to_string() + } else { + format!( + "Local checks running: {}. Stop them whenever staging is complete.", + active.join(", ") + ) + } +} + +/// Formats a selected device for the launch-plan summary. +fn selected_device_label(value: Option<&str>) -> String { + value + .map(compact_device_name) + .unwrap_or_else(|| "auto".to_string()) +} + +/// Prefer the basename for `/dev/...` entries while keeping Pulse names intact. +fn compact_device_name(value: &str) -> String { + let trimmed = value.trim(); + if trimmed.is_empty() { + return "auto".to_string(); + } + trimmed.rsplit('/').next().unwrap_or(trimmed).to_string() +} + pub fn capitalize(value: &str) -> String { let mut chars = value.chars(); match chars.next() { @@ -455,3 +606,55 @@ pub fn next_input_routing(routing: InputRouting) -> InputRouting { InputRouting::Local => InputRouting::Remote, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn launch_plan_summary_compacts_selected_devices() { + let mut state = LauncherState::new(); + state.select_camera(Some( + "/dev/v4l/by-id/usb-Logitech_C920-video-index0".to_string(), + )); + state.select_microphone(Some("alsa_input.usb-focusrite".to_string())); + state.select_speaker(Some("alsa_output.studio".to_string())); + + let summary = launch_plan_summary(&state); + assert!(summary.contains("usb-Logitech_C920-video-index0")); + assert!(summary.contains("alsa_input.usb-focusrite")); + assert!(summary.contains("alsa_output.studio")); + assert!(!summary.contains("/dev/v4l/by-id/")); + } + + #[test] + fn launch_plan_detail_calls_out_forced_off_sessions() { + let mut state = LauncherState::new(); + state.set_capture_power(CapturePowerStatus { + available: true, + enabled: false, + unit: "relay.service".to_string(), + detail: "inactive/dead".to_string(), + active_leases: 0, + mode: "forced-off".to_string(), + }); + state.start_remote(); + + let detail = launch_plan_detail(&state, true); + assert!(detail.contains("Return capture to Auto or Force On")); + } + + #[test] + fn local_test_detail_mentions_idle_and_running_modes() { + assert!(local_test_detail(false, false, false).contains("idle")); + let running = local_test_detail(true, true, false); + assert!(running.contains("camera preview")); + assert!(running.contains("mic monitor")); + } + + #[test] + fn compact_device_name_prefers_basename_when_available() { + assert_eq!(compact_device_name("/dev/video0"), "video0"); + assert_eq!(compact_device_name("alsa_input.usb"), "alsa_input.usb"); + } +} diff --git a/scripts/ci/hygiene_gate_baseline.json b/scripts/ci/hygiene_gate_baseline.json index 7871389..72c8e96 100644 --- a/scripts/ci/hygiene_gate_baseline.json +++ b/scripts/ci/hygiene_gate_baseline.json @@ -1,273 +1,272 @@ { "files": { "client/src/app.rs": { - "loc": 546, "clippy_warnings": 42, - "doc_debt": 10 + "doc_debt": 10, + "loc": 546 }, "client/src/app_support.rs": { - "loc": 128, + "clippy_warnings": 0, "doc_debt": 3, - "clippy_warnings": 0 + "loc": 128 }, "client/src/handshake.rs": { - "loc": 194, + "clippy_warnings": 0, "doc_debt": 3, - "clippy_warnings": 0 + "loc": 194 }, "client/src/input/camera.rs": { - "loc": 372, "clippy_warnings": 38, - "doc_debt": 7 + "doc_debt": 7, + "loc": 372 }, "client/src/input/inputs.rs": { - "loc": 673, "clippy_warnings": 42, - "doc_debt": 16 + "doc_debt": 16, + "loc": 673 }, "client/src/input/keyboard.rs": { - "loc": 580, "clippy_warnings": 24, - "doc_debt": 18 + "doc_debt": 18, + "loc": 580 }, "client/src/input/keymap.rs": { - "loc": 196, "clippy_warnings": 8, - "doc_debt": 0 + "doc_debt": 0, + "loc": 196 }, "client/src/input/microphone.rs": { - "loc": 166, "clippy_warnings": 17, - "doc_debt": 2 + "doc_debt": 2, + "loc": 166 }, "client/src/input/mod.rs": { - "loc": 8, + "clippy_warnings": 0, "doc_debt": 0, - "clippy_warnings": 0 + "loc": 8 }, "client/src/input/mouse.rs": { - "loc": 317, "clippy_warnings": 40, - "doc_debt": 8 + "doc_debt": 8, + "loc": 317 }, "client/src/launcher/clipboard.rs": { - "loc": 177, "clippy_warnings": 2, - "doc_debt": 1 + "doc_debt": 1, + "loc": 177 }, "client/src/launcher/device_test.rs": { - "loc": 454, - "clippy_warnings": 36, - "doc_debt": 20 + "clippy_warnings": 22, + "doc_debt": 20, + "loc": 446 }, "client/src/launcher/devices.rs": { - "loc": 158, "clippy_warnings": 6, - "doc_debt": 3 + "doc_debt": 3, + "loc": 158 }, "client/src/launcher/diagnostics.rs": { - "loc": 175, "clippy_warnings": 17, - "doc_debt": 3 + "doc_debt": 3, + "loc": 175 }, "client/src/launcher/mod.rs": { - "loc": 195, "clippy_warnings": 6, - "doc_debt": 4 + "doc_debt": 4, + "loc": 195 }, "client/src/launcher/power.rs": { - "loc": 69, + "clippy_warnings": 0, "doc_debt": 1, - "clippy_warnings": 0 + "loc": 69 }, "client/src/launcher/preview.rs": { - "loc": 293, "clippy_warnings": 20, - "doc_debt": 6 + "doc_debt": 6, + "loc": 293 }, "client/src/launcher/state.rs": { - "loc": 360, "clippy_warnings": 14, - "doc_debt": 15 + "doc_debt": 15, + "loc": 360 }, "client/src/launcher/ui.rs": { - "loc": 654, - "clippy_warnings": 4, - "doc_debt": 1 + "clippy_warnings": 10, + "doc_debt": 1, + "loc": 695 }, "client/src/launcher/ui_components.rs": { - "loc": 615, "clippy_warnings": 8, - "doc_debt": 4 + "doc_debt": 4, + "loc": 679 }, "client/src/launcher/ui_runtime.rs": { - "loc": 457, "clippy_warnings": 10, - "doc_debt": 18 + "doc_debt": 20, + "loc": 660 }, "client/src/layout.rs": { - "loc": 78, "clippy_warnings": 6, - "doc_debt": 0 + "doc_debt": 0, + "loc": 78 }, "client/src/lib.rs": { - "loc": 14, + "clippy_warnings": 0, "doc_debt": 0, - "clippy_warnings": 0 + "loc": 14 }, "client/src/main.rs": { - "loc": 96, "clippy_warnings": 2, - "doc_debt": 2 + "doc_debt": 2, + "loc": 96 }, "client/src/output/audio.rs": { - "loc": 195, "clippy_warnings": 37, - "doc_debt": 5 + "doc_debt": 5, + "loc": 195 }, "client/src/output/display.rs": { - "loc": 81, + "clippy_warnings": 0, "doc_debt": 0, - "clippy_warnings": 0 + "loc": 81 }, "client/src/output/layout.rs": { - "loc": 155, "clippy_warnings": 4, - "doc_debt": 2 + "doc_debt": 2, + "loc": 155 }, "client/src/output/mod.rs": { - "loc": 6, + "clippy_warnings": 0, "doc_debt": 0, - "clippy_warnings": 0 + "loc": 6 }, "client/src/output/video.rs": { - "loc": 547, "clippy_warnings": 36, - "doc_debt": 4 + "doc_debt": 4, + "loc": 547 }, "client/src/paste.rs": { - "loc": 46, "clippy_warnings": 2, - "doc_debt": 1 + "doc_debt": 1, + "loc": 46 }, "common/src/bin/cli.rs": { - "loc": 3, + "clippy_warnings": 0, "doc_debt": 0, - "clippy_warnings": 0 + "loc": 3 }, "common/src/cli.rs": { - "loc": 22, + "clippy_warnings": 0, "doc_debt": 0, - "clippy_warnings": 0 + "loc": 22 }, "common/src/hid.rs": { - "loc": 134, + "clippy_warnings": 0, "doc_debt": 2, - "clippy_warnings": 0 + "loc": 134 }, "common/src/lib.rs": { - "loc": 22, + "clippy_warnings": 0, "doc_debt": 0, - "clippy_warnings": 0 + "loc": 22 }, "common/src/paste.rs": { - "loc": 95, + "clippy_warnings": 0, "doc_debt": 2, - "clippy_warnings": 0 + "loc": 95 }, "server/src/audio.rs": { - "loc": 386, "clippy_warnings": 37, - "doc_debt": 7 + "doc_debt": 7, + "loc": 386 }, "server/src/bin/lesavka-uvc.real.inc": { - "clippy_warnings": 31, - "doc_debt": 0 + "clippy_warnings": 31 }, "server/src/bin/lesavka-uvc.rs": { - "loc": 710, + "clippy_warnings": 0, "doc_debt": 17, - "clippy_warnings": 0 + "loc": 710 }, "server/src/camera.rs": { - "loc": 392, "clippy_warnings": 12, - "doc_debt": 11 + "doc_debt": 11, + "loc": 392 }, "server/src/camera_runtime.rs": { - "loc": 200, "clippy_warnings": 10, - "doc_debt": 5 + "doc_debt": 5, + "loc": 200 }, "server/src/capture_power.rs": { - "loc": 338, "clippy_warnings": 10, - "doc_debt": 7 + "doc_debt": 7, + "loc": 338 }, "server/src/gadget.rs": { - "loc": 327, "clippy_warnings": 30, - "doc_debt": 7 + "doc_debt": 7, + "loc": 327 }, "server/src/handshake.rs": { - "loc": 40, "clippy_warnings": 2, - "doc_debt": 1 + "doc_debt": 1, + "loc": 40 }, "server/src/lib.rs": { - "loc": 14, + "clippy_warnings": 0, "doc_debt": 0, - "clippy_warnings": 0 + "loc": 14 }, "server/src/main.rs": { - "loc": 572, "clippy_warnings": 10, - "doc_debt": 13 + "doc_debt": 13, + "loc": 572 }, "server/src/paste.rs": { - "loc": 207, "clippy_warnings": 6, - "doc_debt": 3 + "doc_debt": 3, + "loc": 207 }, "server/src/runtime_support.rs": { - "loc": 387, "clippy_warnings": 14, - "doc_debt": 8 + "doc_debt": 8, + "loc": 387 }, "server/src/uvc_control/model.rs": { - "loc": 510, + "clippy_warnings": 0, "doc_debt": 11, - "clippy_warnings": 0 + "loc": 510 }, "server/src/uvc_control/protocol.rs": { - "loc": 403, + "clippy_warnings": 0, "doc_debt": 11, - "clippy_warnings": 0 + "loc": 403 }, "server/src/uvc_runtime.rs": { - "loc": 241, "clippy_warnings": 4, - "doc_debt": 5 + "doc_debt": 5, + "loc": 241 }, "server/src/video.rs": { - "loc": 344, "clippy_warnings": 25, - "doc_debt": 2 + "doc_debt": 2, + "loc": 343 }, "server/src/video_sinks.rs": { - "loc": 559, "clippy_warnings": 78, - "doc_debt": 11 + "doc_debt": 11, + "loc": 559 }, "server/src/video_support.rs": { - "loc": 236, "clippy_warnings": 8, - "doc_debt": 6 + "doc_debt": 6, + "loc": 236 }, "testing/src/lib.rs": { - "loc": 10, + "clippy_warnings": 0, "doc_debt": 0, - "clippy_warnings": 0 + "loc": 10 } } } diff --git a/scripts/ci/quality_gate_baseline.json b/scripts/ci/quality_gate_baseline.json index ddc7392..0789c64 100644 --- a/scripts/ci/quality_gate_baseline.json +++ b/scripts/ci/quality_gate_baseline.json @@ -1,168 +1,168 @@ { "files": { "client/src/app.rs": { - "loc": 546, - "line_percent": 95.1219512195122 + "line_percent": 95.1219512195122, + "loc": 546 }, "client/src/app_support.rs": { - "loc": 128, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 128 }, "client/src/handshake.rs": { - "loc": 194, - "line_percent": 96.15384615384616 + "line_percent": 96.15384615384616, + "loc": 194 }, "client/src/input/camera.rs": { - "loc": 372, - "line_percent": 98.42931937172776 + "line_percent": 98.42931937172776, + "loc": 372 }, "client/src/input/inputs.rs": { - "loc": 673, - "line_percent": 97.55102040816327 + "line_percent": 97.55102040816327, + "loc": 673 }, "client/src/input/keyboard.rs": { - "loc": 580, - "line_percent": 95.9409594095941 + "line_percent": 95.9409594095941, + "loc": 580 }, "client/src/input/keymap.rs": { - "loc": 196, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 196 }, "client/src/input/microphone.rs": { - "loc": 166, - "line_percent": 95.94594594594594 + "line_percent": 95.94594594594594, + "loc": 166 }, "client/src/input/mouse.rs": { - "loc": 317, - "line_percent": 97.32142857142857 + "line_percent": 97.32142857142857, + "loc": 317 }, "client/src/launcher/clipboard.rs": { - "loc": 177, - "line_percent": 98.0 + "line_percent": 98.0, + "loc": 177 }, "client/src/launcher/devices.rs": { - "loc": 158, - "line_percent": 98.13084112149532 + "line_percent": 98.13084112149532, + "loc": 158 }, "client/src/launcher/diagnostics.rs": { - "loc": 175, - "line_percent": 97.14285714285714 + "line_percent": 97.14285714285714, + "loc": 175 }, "client/src/launcher/mod.rs": { - "loc": 195, - "line_percent": 95.23809523809523 + "line_percent": 95.23809523809523, + "loc": 195 }, "client/src/launcher/state.rs": { - "loc": 360, - "line_percent": 98.29059829059828 + "line_percent": 98.29059829059828, + "loc": 360 }, "client/src/launcher/ui.rs": { - "loc": 654, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 695 }, "client/src/layout.rs": { - "loc": 78, - "line_percent": 97.72727272727273 + "line_percent": 97.72727272727273, + "loc": 78 }, "client/src/main.rs": { - "loc": 96, - "line_percent": 97.0873786407767 + "line_percent": 97.0873786407767, + "loc": 96 }, "client/src/output/audio.rs": { - "loc": 195, - "line_percent": 98.78048780487805 + "line_percent": 98.78048780487805, + "loc": 195 }, "client/src/output/display.rs": { - "loc": 81, - "line_percent": 97.61904761904762 + "line_percent": 97.61904761904762, + "loc": 81 }, "client/src/output/layout.rs": { - "loc": 155, - "line_percent": 98.9795918367347 + "line_percent": 98.9795918367347, + "loc": 155 }, "client/src/output/video.rs": { - "loc": 547, - "line_percent": 96.22641509433963 + "line_percent": 96.22641509433963, + "loc": 547 }, "client/src/paste.rs": { - "loc": 46, - "line_percent": 96.29629629629629 + "line_percent": 96.29629629629629, + "loc": 46 }, "common/src/bin/cli.rs": { - "loc": 3, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 3 }, "common/src/cli.rs": { - "loc": 22, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 22 }, "common/src/hid.rs": { - "loc": 134, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 134 }, "common/src/lib.rs": { - "loc": 22, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 22 }, "common/src/paste.rs": { - "loc": 95, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 95 }, "server/src/audio.rs": { - "loc": 386, - "line_percent": 98.9010989010989 + "line_percent": 100.0, + "loc": 386 }, "server/src/bin/lesavka-uvc.rs": { - "loc": 710, - "line_percent": 96.35535307517085 + "line_percent": 96.35535307517085, + "loc": 710 }, "server/src/camera.rs": { - "loc": 392, - "line_percent": 99.09909909909909 + "line_percent": 99.09909909909909, + "loc": 392 }, "server/src/camera_runtime.rs": { - "loc": 200, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 200 }, "server/src/capture_power.rs": { - "loc": 338, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 338 }, "server/src/gadget.rs": { - "loc": 327, - "line_percent": 96.875 + "line_percent": 96.875, + "loc": 327 }, "server/src/handshake.rs": { - "loc": 40, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 40 }, "server/src/main.rs": { - "loc": 572, - "line_percent": 95.33333333333334 + "line_percent": 95.33333333333334, + "loc": 572 }, "server/src/paste.rs": { - "loc": 207, - "line_percent": 97.12230215827337 + "line_percent": 97.12230215827337, + "loc": 207 }, "server/src/runtime_support.rs": { - "loc": 387, - "line_percent": 96.42857142857143 + "line_percent": 96.42857142857143, + "loc": 387 }, "server/src/uvc_runtime.rs": { - "loc": 241, - "line_percent": 97.14285714285714 + "line_percent": 97.14285714285714, + "loc": 241 }, "server/src/video.rs": { - "loc": 344, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 343 }, "server/src/video_sinks.rs": { - "loc": 559, - "line_percent": 100.0 + "line_percent": 100.0, + "loc": 559 }, "server/src/video_support.rs": { - "loc": 236, - "line_percent": 96.03174603174604 + "line_percent": 96.03174603174604, + "loc": 236 } } }