use anyhow::Result; #[cfg(not(coverage))] use { super::clipboard::send_clipboard_text_to_remote, super::device_test::{DeviceTestController, DeviceTestKind}, super::devices::DeviceCatalog, super::diagnostics::{PerformanceSample, quality_probe_command}, super::launcher_clipboard_control_path, super::launcher_focus_signal_path, super::power::{fetch_capture_power, reset_usb_gadget, set_capture_power_mode}, super::state::{ BreakoutSizePreset, CapturePowerStatus, CaptureSizePreset, DisplaySurface, FeedSourcePreset, InputRouting, LauncherState, }, super::ui_components::{build_launcher_view, sync_input_device_combo, sync_stage_device_combo}, super::ui_runtime::{ RelayChild, append_session_log, apply_popout_window_size, attach_child_log_streams, capture_swap_key, copy_session_log, dock_all_displays_to_preview, dock_display_to_preview, input_control_path, input_state_path, next_input_routing, open_diagnostics_popout, open_popout_window, open_session_log_popout, path_marker, present_popout_windows, read_input_routing_state, reap_exited_child, refresh_launcher_ui, refresh_test_buttons, routing_name, selected_combo_value, selected_server_addr, shutdown_launcher_runtime, spawn_client_process, stop_child_process, toggle_key_label, update_test_action_result, write_input_routing_request, }, crate::handshake::{HandshakeProbe, probe}, crate::output::display::enumerate_monitors, gtk::glib, gtk::prelude::*, lesavka_common::lesavka::CapturePowerCommand, lesavka_common::process_metrics::ProcessCpuSampler, std::cell::{Cell, RefCell}, std::collections::VecDeque, std::process::Command, std::rc::Rc, std::time::{Duration, Instant}, }; #[cfg(not(coverage))] enum PowerMessage { Refresh(std::result::Result), Command(std::result::Result), } #[cfg(not(coverage))] enum RelayMessage { Spawned(std::result::Result), } #[cfg(not(coverage))] enum CapsMessage { Refresh(HandshakeProbe), } #[cfg(not(coverage))] enum ClipboardMessage { Finished(std::result::Result), } #[cfg(not(coverage))] const NETWORK_TELEMETRY_WINDOW: Duration = Duration::from_secs(8); #[cfg(not(coverage))] #[derive(Default)] struct NetworkTelemetry { rtt_samples: VecDeque<(Instant, f32)>, failures: VecDeque, } #[cfg(not(coverage))] #[derive(Clone, Copy, Debug, Default)] struct NetworkSnapshot { rtt_ms: f32, probe_spread_ms: f32, probe_loss_pct: f32, } #[cfg(not(coverage))] impl NetworkTelemetry { fn record(&mut self, probe: &HandshakeProbe) { let now = Instant::now(); self.trim(now); if let Some(rtt_ms) = probe.rtt_ms { self.rtt_samples.push_back((now, rtt_ms)); } else { self.failures.push_back(now); } self.trim(now); } fn snapshot(&mut self) -> NetworkSnapshot { let now = Instant::now(); self.trim(now); let rtt_ms = self.rtt_samples.back().map(|(_, rtt)| *rtt).unwrap_or(0.0); let probe_spread_ms = network_spread_ms(&self.rtt_samples); let probe_count = self.rtt_samples.len() + self.failures.len(); let probe_loss_pct = if probe_count == 0 { 0.0 } else { self.failures.len() as f32 * 100.0 / probe_count as f32 }; NetworkSnapshot { rtt_ms, probe_spread_ms, probe_loss_pct, } } fn trim(&mut self, now: Instant) { while let Some((oldest, _)) = self.rtt_samples.front().copied() { if now.saturating_duration_since(oldest) > NETWORK_TELEMETRY_WINDOW { let _ = self.rtt_samples.pop_front(); } else { break; } } while let Some(oldest) = self.failures.front().copied() { if now.saturating_duration_since(oldest) > NETWORK_TELEMETRY_WINDOW { let _ = self.failures.pop_front(); } else { break; } } } } #[cfg(not(coverage))] fn retained_stage_selection(current: Option<&str>, values: &[String]) -> Option { current .filter(|selected| values.iter().any(|value| value == *selected)) .map(str::to_string) .or_else(|| values.first().cloned()) } #[cfg(not(coverage))] fn retained_input_selection(current: Option<&str>, values: &[String]) -> Option { current .filter(|selected| values.iter().any(|value| value == *selected)) .map(str::to_string) } #[cfg(not(coverage))] fn network_spread_ms(samples: &VecDeque<(Instant, f32)>) -> f32 { if samples.len() < 2 { return 0.0; } let mut values = samples.iter().map(|(_, value)| *value).collect::>(); values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); let median = values[values.len() / 2]; let mut deviations = values .into_iter() .map(|value| (value - median).abs()) .collect::>(); deviations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); deviations[deviations.len() / 2] } #[cfg(not(coverage))] fn request_capture_power_refresh( power_tx: std::sync::mpsc::Sender, server_addr: String, delay: Duration, ) { std::thread::spawn(move || { if !delay.is_zero() { std::thread::sleep(delay); } let result = fetch_capture_power(&server_addr).map_err(|err| err.to_string()); let _ = power_tx.send(PowerMessage::Refresh(result)); }); } #[cfg(not(coverage))] fn request_capture_power_command( power_tx: std::sync::mpsc::Sender, server_addr: String, command: CapturePowerCommand, ) { std::thread::spawn(move || { let result = set_capture_power_mode(&server_addr, command).map_err(|err| err.to_string()); let _ = power_tx.send(PowerMessage::Command(result)); }); } #[cfg(not(coverage))] fn request_handshake_caps( caps_tx: std::sync::mpsc::Sender, server_addr: String, delay: Duration, ) { std::thread::spawn(move || { if !delay.is_zero() { std::thread::sleep(delay); } let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build(); let probe = match runtime { Ok(runtime) => runtime.block_on(probe(&server_addr)), Err(_) => HandshakeProbe::default(), }; let _ = caps_tx.send(CapsMessage::Refresh(probe)); }); } #[cfg(not(coverage))] fn unavailable_capture_power(detail: String) -> CapturePowerStatus { CapturePowerStatus { available: false, enabled: false, unit: "relay.service".to_string(), detail, active_leases: 0, mode: "auto".to_string(), detected_devices: 0, } } #[cfg(not(coverage))] fn refresh_eye_feed_controls( widgets: &super::ui_components::LauncherWidgets, state: &LauncherState, ) { for monitor_id in 0..2 { super::ui_components::sync_feed_source_combo( &widgets.display_panes[monitor_id].feed_source_combo, state.feed_source_options(monitor_id), state.feed_source_preset(monitor_id), ); if state.feed_source_preset(monitor_id) != FeedSourcePreset::Off { let choice = state .display_capture_size_choice(monitor_id) .unwrap_or_else(|| state.capture_size_choice(monitor_id)); if state.feed_source_preset(monitor_id) == FeedSourcePreset::ThisEye { super::ui_components::sync_capture_resolution_combo( &widgets.display_panes[monitor_id].capture_resolution_combo, state.capture_size_options(), state.capture_size_preset(monitor_id), ); } else { super::ui_components::sync_capture_resolution_locked( &widgets.display_panes[monitor_id].capture_resolution_combo, state.capture_size_options(), choice.preset, ); } } else { super::ui_components::sync_capture_resolution_disabled( &widgets.display_panes[monitor_id].capture_resolution_combo, ); } super::ui_components::sync_breakout_size_combo( &widgets.display_panes[monitor_id].breakout_combo, state.breakout_size_options(monitor_id), state.breakout_size_preset(monitor_id), ); refresh_preview_frame_ratio(widgets, monitor_id, state); } } #[cfg(not(coverage))] fn refresh_preview_frame_ratio( widgets: &super::ui_components::LauncherWidgets, monitor_id: usize, state: &LauncherState, ) { let capture = state .display_capture_size_choice(monitor_id) .unwrap_or_else(|| state.capture_size_choice(monitor_id)); widgets.display_panes[monitor_id] .preview_frame .set_ratio(capture.preset.display_aspect_ratio()); } #[cfg(not(coverage))] fn eye_caps_changed(state: &LauncherState, caps: &crate::handshake::PeerCaps) -> bool { let next_width = caps.eye_width.unwrap_or(state.preview_source.width); let next_height = caps.eye_height.unwrap_or(state.preview_source.height); let next_fps = caps.eye_fps.unwrap_or(state.preview_source.fps); state.preview_source.width != next_width || state.preview_source.height != next_height || state.preview_source.fps != next_fps } #[cfg(not(coverage))] fn record_diagnostics_sample( widgets: &super::ui_components::LauncherWidgets, state: &LauncherState, preview: Option<&super::preview::LauncherPreview>, network: NetworkSnapshot, client_process_cpu_pct: f32, ) { let left_metrics = preview .and_then(|preview| { (state.feed_source_preset(0) != FeedSourcePreset::Off).then_some( preview.snapshot_metrics( 0, match state.display_surface(0) { DisplaySurface::Preview => super::preview::PreviewSurface::Inline, DisplaySurface::Window => super::preview::PreviewSurface::Window, }, ), ) }) .flatten() .unwrap_or_default(); let right_metrics = preview .and_then(|preview| { (state.feed_source_preset(1) != FeedSourcePreset::Off).then_some( preview.snapshot_metrics( 1, match state.display_surface(1) { DisplaySurface::Preview => super::preview::PreviewSurface::Inline, DisplaySurface::Window => super::preview::PreviewSurface::Window, }, ), ) }) .flatten() .unwrap_or_default(); widgets .diagnostics_log .borrow_mut() .record(PerformanceSample { rtt_ms: network.rtt_ms, probe_spread_ms: network.probe_spread_ms, input_latency_ms: network.rtt_ms * 0.5, probe_loss_pct: network.probe_loss_pct, client_process_cpu_pct, server_process_cpu_pct: left_metrics .server_process_cpu_pct .max(right_metrics.server_process_cpu_pct), video_loss_pct: left_metrics .packet_loss_pct .max(right_metrics.packet_loss_pct), left_receive_fps: left_metrics.receive_fps, left_present_fps: left_metrics.present_fps, left_server_fps: left_metrics.server_fps, left_stream_spread_ms: left_metrics.stream_spread_ms, left_packet_gap_peak_ms: left_metrics.packet_gap_peak_ms, left_present_gap_peak_ms: left_metrics.present_gap_peak_ms, left_queue_depth: left_metrics.queue_depth, left_queue_peak: left_metrics.queue_depth_peak, left_server_source_gap_peak_ms: left_metrics.server_source_gap_peak_ms, left_server_send_gap_peak_ms: left_metrics.server_send_gap_peak_ms, left_server_queue_peak: left_metrics.server_queue_peak, left_server_encoder_label: left_metrics.server_encoder_label.clone(), left_decoder_label: left_metrics.decoder_label.clone(), left_stream_caps_label: left_metrics.stream_caps_label.clone(), left_decoded_caps_label: left_metrics.decoded_caps_label.clone(), left_rendered_caps_label: left_metrics.rendered_caps_label.clone(), right_receive_fps: right_metrics.receive_fps, right_present_fps: right_metrics.present_fps, right_server_fps: right_metrics.server_fps, right_stream_spread_ms: right_metrics.stream_spread_ms, right_packet_gap_peak_ms: right_metrics.packet_gap_peak_ms, right_present_gap_peak_ms: right_metrics.present_gap_peak_ms, right_queue_depth: right_metrics.queue_depth, right_queue_peak: right_metrics.queue_depth_peak, right_server_source_gap_peak_ms: right_metrics.server_source_gap_peak_ms, right_server_send_gap_peak_ms: right_metrics.server_send_gap_peak_ms, right_server_queue_peak: right_metrics.server_queue_peak, right_server_encoder_label: right_metrics.server_encoder_label.clone(), right_decoder_label: right_metrics.decoder_label.clone(), right_stream_caps_label: right_metrics.stream_caps_label.clone(), right_decoded_caps_label: right_metrics.decoded_caps_label.clone(), right_rendered_caps_label: right_metrics.rendered_caps_label.clone(), dropped_frames: left_metrics .dropped_frames .saturating_add(right_metrics.dropped_frames), queue_depth: left_metrics.queue_depth.max(right_metrics.queue_depth), }); } #[cfg(not(coverage))] fn largest_monitor_size() -> (u32, u32) { let (width, height) = enumerate_monitors() .into_iter() .max_by_key(|monitor| { effective_monitor_width(&monitor) as u64 * effective_monitor_height(&monitor) as u64 }) .map(|monitor| { ( effective_monitor_width(&monitor), effective_monitor_height(&monitor), ) }) .unwrap_or((1920, 1080)); (width.max(2), height.max(2)) } #[cfg(not(coverage))] fn largest_monitor_physical_size() -> (u32, u32) { if let Some((width, height)) = probe_kscreen_display_size() { return (width, height); } normalize_breakout_limit(largest_monitor_size().0, largest_monitor_size().1) } #[cfg(not(coverage))] fn probe_kscreen_display_size() -> Option<(u32, u32)> { let output = Command::new("kscreen-doctor").arg("-o").output().ok()?; if !output.status.success() { return None; } let text = String::from_utf8(output.stdout).ok()?; let mut best = None; for line in text.lines() { if !line.contains("Modes:") { continue; } let active = line .split_whitespace() .find(|token| token.contains('*') && token.contains('x'))?; let dims = active .trim_matches(|ch: char| ch == '*' || ch == '!') .split('@') .next()?; let (width, height) = dims.split_once('x')?; let width = width.parse::().ok()?; let height = height.parse::().ok()?; if best .map(|(best_w, best_h)| width as u64 * height as u64 > best_w as u64 * best_h as u64) .unwrap_or(true) { best = Some((width, height)); } } best } #[cfg(not(coverage))] fn effective_monitor_width(monitor: &crate::output::display::MonitorInfo) -> u32 { let scale = monitor.scale_factor.max(1) as u32; (monitor.geometry.width().max(1) as u32).saturating_mul(scale) } #[cfg(not(coverage))] fn effective_monitor_height(monitor: &crate::output::display::MonitorInfo) -> u32 { let scale = monitor.scale_factor.max(1) as u32; (monitor.geometry.height().max(1) as u32).saturating_mul(scale) } #[cfg(not(coverage))] fn normalize_breakout_limit(width: u32, height: u32) -> (u32, u32) { const STANDARD_SIZES: &[(u32, u32)] = &[ (3840, 2160), (2560, 1440), (1920, 1080), (1600, 900), (1366, 768), (1280, 720), (960, 540), ]; STANDARD_SIZES .iter() .copied() .find(|(candidate_w, candidate_h)| *candidate_w <= width && *candidate_h <= height) .unwrap_or((width.max(2), height.max(2))) } #[cfg(not(coverage))] fn launcher_default_size(width: u32, height: u32) -> (i32, i32) { let max_width = width.saturating_sub(48).max(640) as i32; let max_height = height.saturating_sub(72).max(520) as i32; (1380.min(max_width), 860.min(max_height)) } #[cfg(not(coverage))] fn rebind_inline_preview( preview: &super::preview::LauncherPreview, widgets: &super::ui_components::LauncherWidgets, state: &LauncherState, monitor_id: usize, ) { if let Some(binding) = widgets.display_panes[monitor_id] .preview_binding .borrow_mut() .take() { binding.close(); } if state.feed_source_preset(monitor_id) == FeedSourcePreset::Off { widgets.display_panes[monitor_id] .picture .set_paintable(Option::<>k::gdk::Paintable>::None); widgets.display_panes[monitor_id] .stream_status .set_text("Feed disabled."); return; } let binding = preview.install_on_picture( monitor_id, super::preview::PreviewSurface::Inline, &widgets.display_panes[monitor_id].picture, &widgets.display_panes[monitor_id].stream_status, ); *widgets.display_panes[monitor_id] .preview_binding .borrow_mut() = binding; } #[cfg(not(coverage))] fn rebind_popout_preview( preview: &super::preview::LauncherPreview, popouts: &Rc; 2]>>, state: &LauncherState, monitor_id: usize, ) { let mut popouts = popouts.borrow_mut(); let Some(handle) = popouts.get_mut(monitor_id).and_then(|slot| slot.as_mut()) else { return; }; handle.binding.close(); if state.feed_source_preset(monitor_id) == FeedSourcePreset::Off { handle .picture .set_paintable(Option::<>k::gdk::Paintable>::None); handle.status_label.set_text("Feed disabled."); return; } if let Some(binding) = preview.install_on_picture( monitor_id, super::preview::PreviewSurface::Window, &handle.picture, &handle.status_label, ) { handle.binding = binding; } let capture = state .display_capture_size_choice(monitor_id) .unwrap_or_else(|| state.capture_size_choice(monitor_id)); handle .frame .set_ratio(capture.preset.display_aspect_ratio()); } #[cfg(not(coverage))] fn apply_preview_profiles(preview: &super::preview::LauncherPreview, state: &LauncherState) { for monitor_id in 0..2 { let enabled = state.feed_source_preset(monitor_id) != FeedSourcePreset::Off; preview.set_monitor_enabled(monitor_id, enabled); let capture = state .display_capture_size_choice(monitor_id) .unwrap_or_else(|| state.capture_size_choice(monitor_id)); let source_monitor_id = state .resolved_feed_monitor_id(monitor_id) .unwrap_or(monitor_id); let breakout = state.breakout_size_choice(monitor_id); preview.set_capture_profile( monitor_id, source_monitor_id, capture.width, capture.height, capture.fps, capture.max_bitrate_kbit, ); preview.set_breakout_profile(monitor_id, breakout.width, breakout.height); } } #[cfg(not(coverage))] fn sync_preview_profiles( preview: &super::preview::LauncherPreview, widgets: &super::ui_components::LauncherWidgets, popouts: &Rc; 2]>>, state: &LauncherState, ) { apply_preview_profiles(preview, state); for monitor_id in 0..2 { rebind_inline_preview(preview, widgets, state, monitor_id); rebind_popout_preview(preview, popouts, state, monitor_id); } } #[cfg(not(coverage))] fn disconnected_capture_note(mode: &str) -> &'static str { match mode { "forced-on" => "Relay disconnected. Capture is still forced on for staging.", "forced-off" => { "Relay disconnected. Capture stays intentionally dark until you return to Auto or Force On." } _ => { "Relay disconnected. The server will hold capture briefly, then let it return to standby." } } } /// Keeps remote eye previews tied to a live session while respecting forced-off staging. fn session_preview_active( state: &crate::launcher::state::LauncherState, child_running: bool, ) -> bool { (child_running || state.remote_active) && state.capture_power.mode != "forced-off" } #[cfg(not(coverage))] pub fn run_gui_launcher(server_addr: String) -> Result<()> { let app = gtk::Application::builder() .application_id("dev.lesavka.launcher") .build(); let catalog = Rc::new(DeviceCatalog::discover()); let state = Rc::new(RefCell::new(LauncherState::new())); state.borrow_mut().apply_catalog_defaults(&catalog); let child_proc = Rc::new(RefCell::new(None::)); let tests = Rc::new(RefCell::new(DeviceTestController::new())); let server_addr = Rc::new(server_addr); let focus_signal_path = Rc::new(launcher_focus_signal_path()); let clipboard_control_path = Rc::new(launcher_clipboard_control_path()); let input_control_path = Rc::new(input_control_path()); let input_state_path = Rc::new(input_state_path()); let _ = std::fs::remove_file(focus_signal_path.as_path()); let _ = std::fs::remove_file(clipboard_control_path.as_path()); let _ = std::fs::remove_file(input_control_path.as_path()); let _ = std::fs::remove_file(input_state_path.as_path()); { let child_proc = Rc::clone(&child_proc); let focus_signal_path = Rc::clone(&focus_signal_path); let clipboard_control_path = Rc::clone(&clipboard_control_path); let input_control_path = Rc::clone(&input_control_path); let input_state_path = Rc::clone(&input_state_path); let tests = Rc::clone(&tests); app.connect_shutdown(move |_| { stop_child_process(&child_proc); tests.borrow_mut().stop_all(); let _ = std::fs::remove_file(focus_signal_path.as_path()); let _ = std::fs::remove_file(clipboard_control_path.as_path()); let _ = std::fs::remove_file(input_control_path.as_path()); let _ = std::fs::remove_file(input_state_path.as_path()); }); } { let catalog = Rc::clone(&catalog); let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc); let tests = Rc::clone(&tests); let server_addr = Rc::clone(&server_addr); let focus_signal_path = Rc::clone(&focus_signal_path); let input_control_path = Rc::clone(&input_control_path); let input_state_path = Rc::clone(&input_state_path); app.connect_activate(move |app| { let (display_width, display_height) = largest_monitor_size(); let (physical_width, physical_height) = largest_monitor_physical_size(); { let mut state = state.borrow_mut(); state.set_breakout_display_size(display_width, display_height); state.set_breakout_limit_size(physical_width, physical_height); } let view = build_launcher_view(app, server_addr.as_ref(), &catalog, &state.borrow()); let window = view.window.clone(); let (launcher_width, launcher_height) = launcher_default_size(display_width, display_height); window.set_default_size(launcher_width, launcher_height); let server_entry = view.server_entry.clone(); let camera_combo = view.camera_combo.clone(); let microphone_combo = view.microphone_combo.clone(); let speaker_combo = view.speaker_combo.clone(); let keyboard_combo = view.keyboard_combo.clone(); let mouse_combo = view.mouse_combo.clone(); let widgets = view.widgets.clone(); let preview = view.preview.clone(); let popouts = Rc::clone(&view.popouts); let diagnostics_popout = Rc::clone(&view.diagnostics_popout); let log_popout = Rc::clone(&view.log_popout); let shutdown_cleaned = Rc::new(Cell::new(false)); { let shutdown_cleaned = Rc::clone(&shutdown_cleaned); let app = app.clone(); let child_proc = Rc::clone(&child_proc); let tests = Rc::clone(&tests); let preview = preview.clone(); let widgets = widgets.clone(); let popouts = Rc::clone(&popouts); let diagnostics_popout = Rc::clone(&diagnostics_popout); let log_popout = Rc::clone(&log_popout); window.connect_close_request(move |_| { if !shutdown_cleaned.replace(true) { shutdown_launcher_runtime( &child_proc, &tests, preview.as_deref(), &widgets, &popouts, &diagnostics_popout, &log_popout, ); } let app = app.clone(); glib::idle_add_local_once(move || { app.quit(); }); glib::Propagation::Stop }); } { let shutdown_cleaned = Rc::clone(&shutdown_cleaned); let child_proc = Rc::clone(&child_proc); let tests = Rc::clone(&tests); let preview = preview.clone(); let widgets = widgets.clone(); let popouts = Rc::clone(&popouts); let diagnostics_popout = Rc::clone(&diagnostics_popout); let log_popout = Rc::clone(&log_popout); app.connect_shutdown(move |_| { if !shutdown_cleaned.replace(true) { shutdown_launcher_runtime( &child_proc, &tests, preview.as_deref(), &widgets, &popouts, &diagnostics_popout, &log_popout, ); } }); } { let mut tests = tests.borrow_mut(); if let Err(err) = tests.bind_camera_preview( &view.device_stage.camera_preview, &view.device_stage.camera_status, ) { widgets .status_label .set_text(&format!("Camera preview setup failed: {err}")); } if let Err(err) = tests.set_camera_selection(state.borrow().devices.camera.as_deref()) { widgets .status_label .set_text(&format!("Camera staging setup failed: {err}")); } } refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); refresh_test_buttons(&widgets, &mut tests.borrow_mut()); let (power_tx, power_rx) = std::sync::mpsc::channel::(); let power_request_in_flight = Rc::new(Cell::new(false)); let (relay_tx, relay_rx) = std::sync::mpsc::channel::(); let relay_request_in_flight = Rc::new(Cell::new(false)); let (caps_tx, caps_rx) = std::sync::mpsc::channel::(); let caps_request_in_flight = Rc::new(Cell::new(false)); let diagnostics_network = Rc::new(RefCell::new(NetworkTelemetry::default())); let diagnostics_process = Rc::new(RefCell::new(ProcessCpuSampler::new())); let next_power_probe = Rc::new(Cell::new(Instant::now() + Duration::from_millis(500))); let next_diagnostics_probe = Rc::new(Cell::new(Instant::now() + Duration::from_millis(250))); let next_diagnostics_sample = Rc::new(Cell::new(Instant::now() + Duration::from_secs(1))); let (clipboard_tx, clipboard_rx) = std::sync::mpsc::channel::(); let (log_tx, log_rx) = std::sync::mpsc::channel::(); if let Some(preview) = preview.as_ref() { preview.set_log_sink(log_tx.clone()); sync_preview_profiles(preview, &widgets, &popouts, &state.borrow()); } { let state = Rc::clone(&state); let widgets = widgets.clone(); let child_proc = Rc::clone(&child_proc); let tests = Rc::clone(&tests); let camera_combo = camera_combo.clone(); 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()); }); } { let state = Rc::clone(&state); let widgets = widgets.clone(); let child_proc = Rc::clone(&child_proc); let keyboard_combo = keyboard_combo.clone(); let keyboard_combo_read = keyboard_combo.clone(); keyboard_combo.connect_changed(move |_| { let selected = selected_combo_value(&keyboard_combo_read); state.borrow_mut().select_keyboard(selected.clone()); let message = match selected.as_deref() { Some(path) => { format!("The next relay launch will listen only to keyboard {path}.") } None => "The next relay launch will listen to all keyboards.".to_string(), }; widgets.status_label.set_text(&message); refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); }); } { let state = Rc::clone(&state); let widgets = widgets.clone(); let child_proc = Rc::clone(&child_proc); let mouse_combo = mouse_combo.clone(); let mouse_combo_read = mouse_combo.clone(); mouse_combo.connect_changed(move |_| { let selected = selected_combo_value(&mouse_combo_read); state.borrow_mut().select_mouse(selected.clone()); let message = match selected.as_deref() { Some(path) => { format!("The next relay launch will listen only to pointer {path}.") } None => { "The next relay launch will listen to all pointer devices." .to_string() } }; widgets.status_label.set_text(&message); refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); }); } if let Some(preview) = preview.as_ref() { preview.set_session_active(false); } request_capture_power_refresh( power_tx.clone(), selected_server_addr(&server_entry, server_addr.as_ref()), Duration::ZERO, ); caps_request_in_flight.set(true); request_handshake_caps( caps_tx.clone(), selected_server_addr(&server_entry, server_addr.as_ref()), Duration::ZERO, ); { let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc); let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_entry_read = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let preview = preview.clone(); let power_tx = power_tx.clone(); let caps_tx = caps_tx.clone(); let caps_request_in_flight = Rc::clone(&caps_request_in_flight); server_entry.connect_changed(move |_| { let server_addr = selected_server_addr(&server_entry_read, server_addr_fallback.as_ref()); { let mut state = state.borrow_mut(); state.set_server_available(false); state.set_server_version(None); } if let Some(preview) = preview.as_ref() { preview.set_server_addr(server_addr.clone()); } refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); request_capture_power_refresh( power_tx.clone(), server_addr.clone(), Duration::from_millis(150), ); caps_request_in_flight.set(true); request_handshake_caps(caps_tx.clone(), server_addr, Duration::from_millis(150)); }); } for monitor_id in 0..2 { let state = Rc::clone(&state); let widgets = widgets.clone(); let popouts = Rc::clone(&popouts); let child_proc = Rc::clone(&child_proc); let preview = preview.clone(); let feed_source_combo = widgets.display_panes[monitor_id].feed_source_combo.clone(); feed_source_combo.connect_changed(move |combo| { let Some(active_id) = combo.active_id() else { return; }; let Some(preset) = FeedSourcePreset::from_id(active_id.as_str()) else { return; }; if state.borrow().feed_source_preset(monitor_id) == preset { return; } { let mut state = state.borrow_mut(); state.set_feed_source_preset(monitor_id, preset); } if let Some(preview) = preview.as_ref() { sync_preview_profiles(preview, &widgets, &popouts, &state.borrow()); } refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); }); } for monitor_id in 0..2 { let state = Rc::clone(&state); let widgets = widgets.clone(); let popouts = Rc::clone(&popouts); let child_proc = Rc::clone(&child_proc); let preview = preview.clone(); let resolution_combo = widgets.display_panes[monitor_id].capture_resolution_combo.clone(); resolution_combo.connect_changed(move |combo| { let Some(active_id) = combo.active_id() else { return; }; let Some(preset) = CaptureSizePreset::from_id(active_id.as_str()) else { return; }; if state.borrow().feed_source_preset(monitor_id) != FeedSourcePreset::ThisEye { return; } if state.borrow().capture_size_preset(monitor_id) == preset { return; } { let mut state = state.borrow_mut(); state.set_capture_size_preset(monitor_id, preset); } if let Some(preview) = preview.as_ref() { let choice = state .borrow() .display_capture_size_choice(monitor_id) .unwrap_or_else(|| state.borrow().capture_size_choice(monitor_id)); let source_monitor_id = state .borrow() .resolved_feed_monitor_id(monitor_id) .unwrap_or(monitor_id); preview.set_capture_profile( monitor_id, source_monitor_id, choice.width, choice.height, choice.fps, choice.max_bitrate_kbit, ); sync_preview_profiles(preview, &widgets, &popouts, &state.borrow()); } refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); }); } for monitor_id in 0..2 { let state = Rc::clone(&state); let widgets = widgets.clone(); let popouts = Rc::clone(&popouts); let child_proc = Rc::clone(&child_proc); let preview = preview.clone(); let breakout_combo = widgets.display_panes[monitor_id].breakout_combo.clone(); breakout_combo.connect_changed(move |combo| { let Some(active_id) = combo.active_id() else { return; }; let Some(preset) = BreakoutSizePreset::from_id(active_id.as_str()) else { return; }; if state.borrow().breakout_size_preset(monitor_id) == preset { return; } { let mut state = state.borrow_mut(); state.set_breakout_size_preset(monitor_id, preset); } let size = state.borrow().breakout_size_choice(monitor_id); if let Some(preview) = preview.as_ref() { preview.set_breakout_profile(monitor_id, size.width, size.height); } let popout_open = { popouts .borrow() .get(monitor_id) .and_then(|slot| slot.as_ref()) .is_some() }; if popout_open { if let Some(preview) = preview.as_ref() { rebind_popout_preview(preview, &popouts, &state.borrow(), monitor_id); } if let Some(handle) = popouts .borrow() .get(monitor_id) .and_then(|slot| slot.as_ref()) { let display_limit = state.borrow().breakout_display_size(); apply_popout_window_size(handle, size, display_limit); } } refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); }); } { 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()); }); } { 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()); }); } { let state = Rc::clone(&state); let widgets = widgets.clone(); let widgets_handle = widgets.clone(); let child_proc = Rc::clone(&child_proc); let tests = Rc::clone(&tests); let camera_combo = camera_combo.clone(); let microphone_combo = microphone_combo.clone(); let speaker_combo = speaker_combo.clone(); let keyboard_combo = keyboard_combo.clone(); let mouse_combo = mouse_combo.clone(); widgets.device_refresh_button.connect_clicked(move |_| { let catalog = DeviceCatalog::discover(); let ( selected_camera, selected_microphone, selected_speaker, selected_keyboard, selected_mouse, ) = { let state = state.borrow(); ( retained_stage_selection( state.devices.camera.as_deref(), &catalog.cameras, ), retained_stage_selection( state.devices.microphone.as_deref(), &catalog.microphones, ), retained_stage_selection( state.devices.speaker.as_deref(), &catalog.speakers, ), retained_input_selection( state.devices.keyboard.as_deref(), &catalog.keyboards, ), retained_input_selection(state.devices.mouse.as_deref(), &catalog.mice), ) }; { let mut state = state.borrow_mut(); state.select_camera(selected_camera); state.select_microphone(selected_microphone); state.select_speaker(selected_speaker); state.select_keyboard(selected_keyboard); state.select_mouse(selected_mouse); } let state_snapshot = state.borrow().clone(); sync_stage_device_combo( &camera_combo, &catalog.cameras, state_snapshot.devices.camera.as_deref(), ); sync_stage_device_combo( µphone_combo, &catalog.microphones, state_snapshot.devices.microphone.as_deref(), ); sync_stage_device_combo( &speaker_combo, &catalog.speakers, state_snapshot.devices.speaker.as_deref(), ); sync_input_device_combo( &keyboard_combo, &catalog.keyboards, state_snapshot.devices.keyboard.as_deref(), "all keyboards", ); sync_input_device_combo( &mouse_combo, &catalog.mice, state_snapshot.devices.mouse.as_deref(), "all mice", ); if let Err(err) = tests .borrow_mut() .set_camera_selection(state_snapshot.devices.camera.as_deref()) { widgets_handle .status_label .set_text(&format!("Device refresh succeeded, but the webcam test could not switch cleanly: {err}")); } else { widgets_handle.status_label.set_text( "Device staging refreshed. Newly attached devices are ready for local tests; reconnect the relay if you want the live session to use a new webcam, mic, or speaker.", ); } refresh_launcher_ui( &widgets_handle, &state.borrow(), child_proc.borrow().is_some(), ); refresh_test_buttons(&widgets_handle, &mut tests.borrow_mut()); }); } { let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc); let widgets = widgets.clone(); let server_entry = server_entry.clone(); let camera_combo = camera_combo.clone(); let microphone_combo = microphone_combo.clone(); let speaker_combo = speaker_combo.clone(); let input_control_path = Rc::clone(&input_control_path); let input_state_path = Rc::clone(&input_state_path); let server_addr_fallback = Rc::clone(&server_addr); let preview = preview.clone(); let power_tx = power_tx.clone(); let relay_tx = relay_tx.clone(); let relay_request_in_flight = Rc::clone(&relay_request_in_flight); let popouts = Rc::clone(&popouts); let window = window.clone(); let start_button = widgets.start_button.clone(); let widgets_handle = widgets.clone(); start_button.connect_clicked(move |_| { let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); if relay_request_in_flight.get() { return; } if child_proc.borrow().is_some() { stop_child_process(&child_proc); let power_mode = { let mut state = state.borrow_mut(); let _ = state.stop_remote(); state.capture_power.mode.clone() }; dock_all_displays_to_preview( &state, &child_proc, &popouts, &widgets_handle, ); window.present(); if let Some(preview) = preview.as_ref() { preview.set_server_addr(server_addr.clone()); preview.set_session_active(false); } if power_mode != "auto" { widgets_handle.status_label.set_text( "Relay disconnected. Returning capture to automatic mode so it can fall back after the disconnect grace.", ); request_capture_power_command( power_tx.clone(), server_addr.clone(), CapturePowerCommand::Auto, ); } else { widgets_handle .status_label .set_text(disconnected_capture_note(&power_mode)); } request_capture_power_refresh( power_tx.clone(), server_addr.clone(), Duration::from_millis(250), ); request_capture_power_refresh( power_tx.clone(), server_addr, Duration::from_secs(31), ); refresh_launcher_ui(&widgets_handle, &state.borrow(), false); return; } { let mut state = state.borrow_mut(); state.select_camera(selected_combo_value(&camera_combo)); state.select_microphone(selected_combo_value(µphone_combo)); state.select_speaker(selected_combo_value(&speaker_combo)); state.select_keyboard(selected_combo_value(&keyboard_combo)); state.select_mouse(selected_combo_value(&mouse_combo)); } let _ = std::fs::remove_file(input_control_path.as_path()); let _ = std::fs::remove_file(input_state_path.as_path()); let launch_state = state.borrow().clone(); let input_toggle_key = launch_state.swap_key.clone(); let input_control_path = input_control_path.as_ref().clone(); let input_state_path = input_state_path.as_ref().clone(); relay_request_in_flight.set(true); widgets_handle.status_label.set_text(&format!( "Connecting relay with {} as the swap key...", toggle_key_label(&input_toggle_key) )); refresh_launcher_ui( &widgets_handle, &state.borrow(), child_proc.borrow().is_some(), ); let relay_tx = relay_tx.clone(); std::thread::spawn(move || { let result = spawn_client_process( &server_addr, &launch_state, &input_toggle_key, input_control_path.as_path(), input_state_path.as_path(), ) .map_err(|err| err.to_string()); let _ = relay_tx.send(RelayMessage::Spawned(result)); }); }); } { let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc); let widgets = widgets.clone(); let input_control_path = Rc::clone(&input_control_path); let popouts = Rc::clone(&popouts); let window = window.clone(); let input_toggle_button = widgets.input_toggle_button.clone(); let widgets_handle = widgets.clone(); input_toggle_button.connect_clicked(move |_| { let next = next_input_routing(state.borrow().routing); let child_running = child_proc.borrow().is_some(); if child_running { if let Err(err) = write_input_routing_request(input_control_path.as_path(), next) { widgets_handle .status_label .set_text(&format!("Could not update live input target: {err}")); refresh_launcher_ui(&widgets_handle, &state.borrow(), true); return; } widgets_handle.status_label.set_text(&format!( "Input routing switched toward {}.", routing_name(next) )); } else { widgets_handle.status_label.set_text(&format!( "Relay will start with {} input ownership.", routing_name(next) )); } state.borrow_mut().set_routing(next); refresh_launcher_ui(&widgets_handle, &state.borrow(), child_running); if matches!(next, InputRouting::Remote) { present_popout_windows(&popouts); } else { window.present(); } }); } { let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc); let widgets = widgets.clone(); let swap_key_button = widgets.swap_key_button.clone(); swap_key_button.connect_clicked(move |_| { let token = state.borrow_mut().begin_swap_key_binding(); widgets .status_label .set_text("Press a single key within 3 seconds to make it the swap shortcut."); refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc); let widgets = widgets.clone(); glib::timeout_add_local_once(Duration::from_secs(3), move || { if state.borrow_mut().cancel_swap_key_binding(token) { widgets.status_label.set_text( "Swap-key capture timed out. The previous shortcut is still in place.", ); refresh_launcher_ui( &widgets, &state.borrow(), child_proc.borrow().is_some(), ); } }); }); } { let child_proc = Rc::clone(&child_proc); let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let clipboard_tx = clipboard_tx.clone(); widgets.clipboard_button.connect_clicked(move |_| { if child_proc.borrow().is_none() { widgets .status_label .set_text("Start the relay before sending clipboard text."); return; } let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); let Some(display) = gtk::gdk::Display::default() else { widgets .status_label .set_text("No desktop clipboard is available in this session."); return; }; widgets .status_label .set_text("Reading the local clipboard and packing a remote paste spell..."); let clipboard = display.clipboard(); let clipboard_tx = clipboard_tx.clone(); clipboard.read_text_async(None::<>k::gio::Cancellable>, move |result| { match result { Ok(Some(text)) => { let text = text.trim_end_matches(['\r', '\n']).to_string(); if text.is_empty() { let _ = clipboard_tx.send(ClipboardMessage::Finished(Err( "clipboard is empty".to_string(), ))); return; } let clipboard_tx = clipboard_tx.clone(); std::thread::spawn(move || { let result = send_clipboard_text_to_remote(&server_addr, &text) .map_err(|err| err.to_string()); let _ = clipboard_tx .send(ClipboardMessage::Finished(result)); }); } Ok(None) => { let _ = clipboard_tx.send(ClipboardMessage::Finished(Err( "clipboard is empty".to_string(), ))); } Err(err) => { let _ = clipboard_tx.send(ClipboardMessage::Finished(Err( format!("clipboard read failed: {err}"), ))); } } }); }); } { let widgets = widgets.clone(); widgets.probe_button.connect_clicked(move |_| { if let Some(display) = gtk::gdk::Display::default() { let clipboard = display.clipboard(); clipboard.set_text(quality_probe_command()); widgets .status_label .set_text("Quality probe command copied to the local clipboard."); } else { widgets .status_label .set_text("No desktop clipboard is available in this session."); } }); } { let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let widgets_for_click = widgets.clone(); widgets.usb_recover_button.connect_clicked(move |_| { let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets_for_click.status_label.set_text( "Requesting a forced USB gadget re-enumeration on the relay host...", ); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let result = reset_usb_gadget(&server_addr).map_err(|err| err.to_string()); let _ = tx.send(result); }); let widgets = widgets_for_click.clone(); glib::timeout_add_local(Duration::from_millis(100), move || { match rx.try_recv() { Ok(Ok(())) => { widgets.status_label.set_text( "USB gadget recovery requested. Give the host a few seconds to re-enumerate keyboard, mouse, webcam, and audio.", ); glib::ControlFlow::Break } Ok(Err(err)) => { widgets .status_label .set_text(&format!("USB gadget recovery failed: {err}")); glib::ControlFlow::Break } Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue, Err(std::sync::mpsc::TryRecvError::Disconnected) => { widgets.status_label.set_text( "USB gadget recovery ended unexpectedly before the relay answered.", ); glib::ControlFlow::Break } } }); }); } { let widgets = widgets.clone(); widgets.diagnostics_copy_button.connect_clicked(move |_| { if let Err(err) = copy_session_log(&widgets.diagnostics_buffer) { widgets .status_label .set_text(&format!("Could not copy the diagnostics report: {err}")); } else { widgets .status_label .set_text("Diagnostics report copied to the local clipboard."); } }); } { let app = app.clone(); let widgets = widgets.clone(); let diagnostics_popout = Rc::clone(&diagnostics_popout); widgets.diagnostics_popout_button.connect_clicked(move |_| { open_diagnostics_popout( &app, &diagnostics_popout, &widgets.diagnostics_popout_scroll, &widgets.diagnostics_buffer, ); widgets .status_label .set_text("Diagnostics report moved into its own window."); }); } { let widgets = widgets.clone(); widgets.console_copy_button.connect_clicked(move |_| { if let Err(err) = copy_session_log(&widgets.session_log_buffer) { widgets .status_label .set_text(&format!("Could not copy the session log: {err}")); } else { widgets .status_label .set_text("Session log copied to the local clipboard."); } }); } { let app = app.clone(); let widgets = widgets.clone(); let log_popout = Rc::clone(&log_popout); widgets.console_popout_button.connect_clicked(move |_| { open_session_log_popout(&app, &log_popout, &widgets.session_log_buffer); widgets .status_label .set_text("Session log moved into its own window."); }); } { let widgets = widgets.clone(); let tests = Rc::clone(&tests); let camera_combo = camera_combo.clone(); let camera_test_button = widgets.camera_test_button.clone(); let widgets_handle = widgets.clone(); camera_test_button.connect_clicked(move |_| { let selected = selected_combo_value(&camera_combo); let result = { let mut tests = tests.borrow_mut(); let _ = tests.set_camera_selection(selected.as_deref()); tests.toggle_camera() }; update_test_action_result( &widgets_handle, &mut tests.borrow_mut(), result, "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.", ); }); } { let widgets = widgets.clone(); let tests = Rc::clone(&tests); let microphone_combo = microphone_combo.clone(); let speaker_combo = speaker_combo.clone(); let microphone_test_button = widgets.microphone_test_button.clone(); let widgets_handle = widgets.clone(); microphone_test_button.connect_clicked(move |_| { let mic = selected_combo_value(µphone_combo); let sink = selected_combo_value(&speaker_combo); let result = tests .borrow_mut() .toggle_microphone(mic.as_deref(), sink.as_deref()); update_test_action_result( &widgets_handle, &mut tests.borrow_mut(), result, "Microphone monitor started locally through the selected speaker.", "Microphone monitor stopped.", ); }); } { let widgets = widgets.clone(); let tests = Rc::clone(&tests); let speaker_combo = speaker_combo.clone(); let microphone_replay_button = widgets.microphone_replay_button.clone(); let widgets_handle = widgets.clone(); microphone_replay_button.connect_clicked(move |_| { let result = tests .borrow_mut() .toggle_microphone_replay(selected_combo_value(&speaker_combo).as_deref()); update_test_action_result( &widgets_handle, &mut tests.borrow_mut(), result, "Replaying the latest local mic capture through the selected speaker.", "Mic replay stopped.", ); }); } { let widgets = widgets.clone(); let tests = Rc::clone(&tests); let speaker_combo = speaker_combo.clone(); let speaker_test_button = widgets.speaker_test_button.clone(); let widgets_handle = widgets.clone(); speaker_test_button.connect_clicked(move |_| { let result = tests .borrow_mut() .toggle_speaker(selected_combo_value(&speaker_combo).as_deref()); update_test_action_result( &widgets_handle, &mut tests.borrow_mut(), result, "Speaker tone started locally.", "Speaker test tone stopped.", ); }); } { let widgets = widgets.clone(); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let power_tx = power_tx.clone(); let power_request_in_flight = Rc::clone(&power_request_in_flight); let power_auto_button = widgets.power_auto_button.clone(); let widgets_handle = widgets.clone(); power_auto_button.connect_clicked(move |_| { if power_request_in_flight.replace(true) { return; } let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets_handle .status_label .set_text("Returning capture feeds to automatic mode..."); request_capture_power_command( power_tx.clone(), server_addr, CapturePowerCommand::Auto, ); }); } { let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let power_tx = power_tx.clone(); let power_request_in_flight = Rc::clone(&power_request_in_flight); let power_on_button = widgets.power_on_button.clone(); let widgets_handle = widgets.clone(); power_on_button.connect_clicked(move |_| { if power_request_in_flight.replace(true) { return; } let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets_handle .status_label .set_text("Forcing capture feeds on for staging..."); request_capture_power_command( power_tx.clone(), server_addr, CapturePowerCommand::ForceOn, ); }); } { let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let power_tx = power_tx.clone(); let power_request_in_flight = Rc::clone(&power_request_in_flight); let power_off_button = widgets.power_off_button.clone(); let widgets_handle = widgets.clone(); power_off_button.connect_clicked(move |_| { if power_request_in_flight.replace(true) { return; } let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); widgets_handle .status_label .set_text("Forcing capture feeds off for staging..."); request_capture_power_command( power_tx.clone(), server_addr, CapturePowerCommand::ForceOff, ); }); } for monitor_id in 0..2 { let app = app.clone(); let preview = preview.clone(); let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc); let popouts = Rc::clone(&popouts); let widgets = widgets.clone(); let action_button = widgets.display_panes[monitor_id].action_button.clone(); action_button.connect_clicked(move |_| { let Some(preview) = preview.as_ref() else { widgets .status_label .set_text("Preview is unavailable for breakout windows."); return; }; let surface = { let state = state.borrow(); state.display_surface(monitor_id) }; match surface { DisplaySurface::Preview => { open_popout_window( &app, preview, &state, &child_proc, &popouts, &widgets, monitor_id, ); widgets.status_label.set_text(&format!( "{} moved into its own window.", widgets.display_panes[monitor_id].title )); } DisplaySurface::Window => { dock_display_to_preview( &state, &child_proc, &popouts, &widgets, monitor_id, ); widgets.status_label.set_text(&format!( "{} returned to the launcher preview.", widgets.display_panes[monitor_id].title )); } } }); } { let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc); let widgets = widgets.clone(); let key_controller = gtk::EventControllerKey::new(); key_controller.connect_key_pressed(move |_, key, _, _| { if !state.borrow().swap_key_binding { return glib::Propagation::Proceed; } let Some(swap_key) = capture_swap_key(key) else { widgets.status_label.set_text( "That key is not a good swap shortcut. Try a letter, digit, function key, or navigation key.", ); refresh_launcher_ui( &widgets, &state.borrow(), child_proc.borrow().is_some(), ); return glib::Propagation::Stop; }; let relay_live = child_proc.borrow().is_some() || state.borrow().remote_active; { let mut state = state.borrow_mut(); state.complete_swap_key_binding(swap_key.clone()); } let status_message = if relay_live { format!( "Swap key set to {}. Disconnect and reconnect the relay to use it live.", toggle_key_label(&swap_key) ) } else { format!( "Swap key set to {}. The next relay launch will use it.", toggle_key_label(&swap_key) ) }; widgets.status_label.set_text(&status_message); refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some()); glib::Propagation::Stop }); window.add_controller(key_controller); } { let window = window.clone(); let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc); let widgets = widgets.clone(); let focus_signal_path = Rc::clone(&focus_signal_path); let input_state_path = Rc::clone(&input_state_path); let tests = Rc::clone(&tests); let server_entry = server_entry.clone(); let server_addr_fallback = Rc::clone(&server_addr); let last_focus_marker = Rc::new(RefCell::new(path_marker(focus_signal_path.as_path()))); let last_state_marker = Rc::new(RefCell::new(path_marker(input_state_path.as_path()))); let power_request_in_flight = Rc::clone(&power_request_in_flight); let relay_request_in_flight = Rc::clone(&relay_request_in_flight); let preview = preview.clone(); let power_tx = power_tx.clone(); let caps_tx = caps_tx.clone(); let caps_request_in_flight = Rc::clone(&caps_request_in_flight); let diagnostics_network = Rc::clone(&diagnostics_network); let diagnostics_process = Rc::clone(&diagnostics_process); let next_diagnostics_probe = Rc::clone(&next_diagnostics_probe); let next_diagnostics_sample = Rc::clone(&next_diagnostics_sample); let log_tx = log_tx.clone(); glib::timeout_add_local(Duration::from_millis(180), move || { let child_running = reap_exited_child(&child_proc); if !child_running && state.borrow().remote_active { let power_mode = { let mut state = state.borrow_mut(); let _ = state.stop_remote(); state.capture_power.mode.clone() }; dock_all_displays_to_preview(&state, &child_proc, &popouts, &widgets); window.present(); if let Some(preview) = preview.as_ref() { preview.set_session_active(false); } let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); if power_mode != "auto" { widgets.status_label.set_text( "Relay disconnected. Returning capture to automatic mode so it can fall back after the disconnect grace.", ); request_capture_power_command( power_tx.clone(), server_addr.clone(), CapturePowerCommand::Auto, ); } else { widgets .status_label .set_text(disconnected_capture_note(&power_mode)); } request_capture_power_refresh( power_tx.clone(), server_addr.clone(), Duration::from_millis(250), ); request_capture_power_refresh( power_tx.clone(), server_addr, Duration::from_secs(31), ); } let next_state_marker = path_marker(input_state_path.as_path()); let mut last_state = last_state_marker.borrow_mut(); if next_state_marker > *last_state { *last_state = next_state_marker; if let Some(routing) = read_input_routing_state(input_state_path.as_path()) { state.borrow_mut().set_routing(routing); refresh_launcher_ui(&widgets, &state.borrow(), child_running); if matches!(routing, InputRouting::Remote) { present_popout_windows(&popouts); } else { window.present(); } } } let next_focus_marker = path_marker(focus_signal_path.as_path()); let mut last_focus = last_focus_marker.borrow_mut(); if next_focus_marker > *last_focus { *last_focus = next_focus_marker; state.borrow_mut().set_routing(InputRouting::Local); refresh_launcher_ui(&widgets, &state.borrow(), child_running); widgets .status_label .set_text("Local control restored and the launcher is focused."); window.present(); } while let Ok(message) = relay_rx.try_recv() { relay_request_in_flight.set(false); match message { RelayMessage::Spawned(Ok(mut child)) => { attach_child_log_streams(&mut child, log_tx.clone()); *child_proc.borrow_mut() = Some(child); { let mut state = state.borrow_mut(); state.set_server_available(true); let _ = state.start_remote(); } let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); if let Some(preview) = preview.as_ref() { preview.set_server_addr(server_addr.clone()); preview.set_session_active(session_preview_active( &state.borrow(), child_proc.borrow().is_some(), )); } 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 connected 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 connected with inputs routed to {}. Capture is being held awake and the eye previews are coming online.", routing ), _ => format!( "Relay connected with inputs routed to {}. The eye previews will come up with the live session.", routing ), }; widgets.status_label.set_text(&message); if matches!(state.borrow().routing, InputRouting::Remote) { present_popout_windows(&popouts); } request_capture_power_refresh( power_tx.clone(), server_addr.clone(), Duration::from_millis(250), ); request_capture_power_refresh( power_tx.clone(), server_addr, Duration::from_millis(1250), ); } RelayMessage::Spawned(Err(err)) => { state.borrow_mut().set_server_available(false); if let Some(preview) = preview.as_ref() { preview.set_session_active(false); } widgets .status_label .set_text(&format!("Relay start failed: {err}")); } } } while let Ok(line) = log_rx.try_recv() { append_session_log(&widgets.session_log_buffer, &line); let mut end = widgets.session_log_buffer.end_iter(); widgets .session_log_view .scroll_to_iter(&mut end, 0.0, false, 0.0, 1.0); } while let Ok(message) = power_rx.try_recv() { power_request_in_flight.set(false); match message { PowerMessage::Refresh(Ok(power)) => { { let mut state = state.borrow_mut(); state.set_server_available(true); state.set_capture_power(power); } if let Some(preview) = preview.as_ref() { let preview_active = { let state = state.borrow(); session_preview_active( &state, child_proc.borrow().is_some(), ) }; preview.set_session_active(preview_active); } } PowerMessage::Refresh(Err(err)) => { { let mut state = state.borrow_mut(); state.set_server_available(false); state.set_capture_power(unavailable_capture_power(err)); } if let Some(preview) = preview.as_ref() { let preview_active = { let state = state.borrow(); session_preview_active( &state, child_proc.borrow().is_some(), ) }; preview.set_session_active(preview_active); } } PowerMessage::Command(Ok(power)) => { let mode = power.mode.clone(); { let mut state = state.borrow_mut(); state.set_server_available(true); state.set_capture_power(power); } if let Some(preview) = preview.as_ref() { let preview_active = { let state = state.borrow(); session_preview_active( &state, child_proc.borrow().is_some(), ) }; preview.set_session_active(preview_active); } widgets.status_label.set_text(match mode.as_str() { "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)) => { let mut state = state.borrow_mut(); state.set_server_available(false); state.set_capture_power(unavailable_capture_power(err.clone())); widgets .status_label .set_text(&format!("Capture power update failed: {err}")); } } } while let Ok(message) = caps_rx.try_recv() { caps_request_in_flight.set(false); match message { CapsMessage::Refresh(probe_result) => { diagnostics_network.borrow_mut().record(&probe_result); let caps = probe_result.caps; let rebind_preview = eye_caps_changed(&state.borrow(), &caps); { let mut state = state.borrow_mut(); if probe_result.reachable { state.set_server_available(true); } else if child_proc.borrow().is_none() { state.set_server_available(false); } state.set_server_version(caps.server_version.clone()); } if let (Some(width), Some(height)) = (caps.eye_width, caps.eye_height) { let fps = caps .eye_fps .unwrap_or(crate::launcher::state::PreviewSourceSize::default().fps); { let mut state = state.borrow_mut(); state.set_preview_source_profile(width, height, fps); } if rebind_preview && let Some(preview) = preview.as_ref() { sync_preview_profiles(preview, &widgets, &popouts, &state.borrow()); } refresh_eye_feed_controls(&widgets, &state.borrow()); } else { refresh_eye_feed_controls(&widgets, &state.borrow()); } } } } while let Ok(message) = clipboard_rx.try_recv() { match message { ClipboardMessage::Finished(Ok(detail)) => { widgets.status_label.set_text(&format!("✨ {detail}")); } ClipboardMessage::Finished(Err(err)) => { widgets .status_label .set_text(&format!("Clipboard send failed: {err}")); } } } let now = Instant::now(); let child_running = child_proc.borrow().is_some(); if now >= next_power_probe.get() && !power_request_in_flight.get() && (child_running || state.borrow().capture_power.enabled || state.borrow().remote_active) { power_request_in_flight.set(true); let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); request_capture_power_refresh(power_tx.clone(), server_addr, Duration::ZERO); next_power_probe.set(now + Duration::from_secs(2)); } if now >= next_diagnostics_probe.get() && !caps_request_in_flight.get() { caps_request_in_flight.set(true); let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); request_handshake_caps(caps_tx.clone(), server_addr, Duration::ZERO); next_diagnostics_probe.set(now + Duration::from_secs(2)); } if now >= next_diagnostics_sample.get() { let network = diagnostics_network.borrow_mut().snapshot(); let client_process_cpu_pct = diagnostics_process .borrow_mut() .sample_percent() .unwrap_or(0.0); record_diagnostics_sample( &widgets, &state.borrow(), preview.as_ref().map(|preview| preview.as_ref()), network, client_process_cpu_pct, ); next_diagnostics_sample.set(now + Duration::from_secs(1)); } refresh_launcher_ui(&widgets, &state.borrow(), child_running); refresh_test_buttons(&widgets, &mut tests.borrow_mut()); glib::ControlFlow::Continue }); } window.present(); }); } let _ = app.run_with_args::<&str>(&[]); Ok(()) } #[cfg(coverage)] pub fn run_gui_launcher(_server_addr: String) -> Result<()> { Ok(()) } #[cfg(all(test, not(coverage)))] mod tests { use super::apply_preview_profiles; use crate::launcher::preview::{LauncherPreview, PreviewSurface}; use crate::launcher::state::{CaptureSizePreset, FeedSourcePreset, LauncherState}; #[test] fn fresh_preview_bootstrap_is_overridden_by_launcher_state_profiles() { let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap(); let state = LauncherState::default(); let bootstrap = preview.profile_for_test(1, PreviewSurface::Inline).unwrap(); assert_eq!(bootstrap.0, 0); assert_eq!(bootstrap.3, 1920); assert_eq!(bootstrap.4, 1080); assert_eq!(bootstrap.5, 60); assert_eq!(bootstrap.6, 18_000); apply_preview_profiles(&preview, &state); let inline = preview.profile_for_test(1, PreviewSurface::Inline).unwrap(); assert_eq!(inline.0, 1); assert_eq!(inline.3, 1920); assert_eq!(inline.4, 1080); assert_eq!(inline.5, 60); assert_eq!(inline.6, 18_000); let window = preview.profile_for_test(1, PreviewSurface::Window).unwrap(); assert_eq!(window.0, 1); assert_eq!(window.3, 1920); assert_eq!(window.4, 1080); assert_eq!(window.5, 60); assert_eq!(window.6, 18_000); preview.shutdown_all(); } #[test] fn source_preview_profile_stays_honest_after_apply() { let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap(); let mut state = LauncherState::default(); state.set_capture_size_preset(1, CaptureSizePreset::P1080); apply_preview_profiles(&preview, &state); let inline = preview.profile_for_test(1, PreviewSurface::Inline).unwrap(); assert_eq!(inline.0, 1); assert_eq!(inline.3, 1920); assert_eq!(inline.4, 1080); assert_eq!(inline.5, 60); assert_eq!(inline.6, 18_000); preview.shutdown_all(); } #[test] fn mirrored_preview_profile_keeps_its_own_feed_id_but_uses_the_other_source() { let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap(); let mut state = LauncherState::default(); state.set_feed_source_preset(0, FeedSourcePreset::OtherEye); apply_preview_profiles(&preview, &state); let inline = preview.profile_for_test(0, PreviewSurface::Inline).unwrap(); let window = preview.profile_for_test(0, PreviewSurface::Window).unwrap(); assert_eq!(inline.0, 1); assert_eq!(window.0, 1); preview.shutdown_all(); } #[test] fn mirrored_preview_profile_inherits_the_source_eye_mode() { let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap(); let mut state = LauncherState::default(); state.set_feed_source_preset(0, FeedSourcePreset::OtherEye); state.set_capture_size_preset(1, CaptureSizePreset::P720); apply_preview_profiles(&preview, &state); let inline = preview.profile_for_test(0, PreviewSurface::Inline).unwrap(); let window = preview.profile_for_test(0, PreviewSurface::Window).unwrap(); assert_eq!(inline.0, 1); assert_eq!(window.0, 1); assert_eq!(inline.3, 1280); assert_eq!(inline.4, 720); assert_eq!(inline.5, 60); assert_eq!(inline.6, 12_000); assert_eq!(window.3, 1280); assert_eq!(window.4, 720); assert_eq!(window.5, 60); assert_eq!(window.6, 12_000); preview.shutdown_all(); } #[test] fn off_preview_profile_disables_both_surfaces_instead_of_leaving_idle_feeds_running() { let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap(); let mut state = LauncherState::default(); state.set_feed_source_preset(0, FeedSourcePreset::Off); apply_preview_profiles(&preview, &state); assert_eq!( preview.feed_disabled_for_test(0, PreviewSurface::Inline), Some(true) ); assert_eq!( preview.feed_disabled_for_test(0, PreviewSurface::Window), Some(true) ); assert_eq!( preview.feed_disabled_for_test(1, PreviewSurface::Inline), Some(false) ); preview.shutdown_all(); } } #[cfg(all(test, coverage))] mod tests { use super::{run_gui_launcher, session_preview_active}; use crate::launcher::state::{CapturePowerStatus, LauncherState}; #[test] fn coverage_stub_returns_ok() { assert!(run_gui_launcher("http://127.0.0.1:50051".to_string()).is_ok()); } #[test] fn session_preview_stays_idle_when_capture_is_forced_off() { let mut state = LauncherState::new(); state.start_remote(); state.set_capture_power(CapturePowerStatus { available: true, enabled: false, unit: "relay.service".to_string(), detail: "inactive/dead".to_string(), active_leases: 1, mode: "forced-off".to_string(), detected_devices: 0, }); assert!(!session_preview_active(&state, true)); } }