use anyhow::Result; #[cfg(not(coverage))] use { super::clipboard::send_clipboard_text_to_remote, super::device_test::{DeviceTestController, DeviceTestKind}, super::devices::DeviceCatalog, super::diagnostics::quality_probe_command, super::launcher_clipboard_control_path, super::launcher_focus_signal_path, super::power::{fetch_capture_power, set_capture_power_mode}, super::state::{ BreakoutSizePreset, CapturePowerStatus, CaptureSizePreset, DisplaySurface, InputRouting, LauncherState, }, super::ui_components::build_launcher_view, 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, spawn_client_process, stop_child_process, toggle_key_label, update_test_action_result, write_input_routing_request, }, crate::handshake::{PeerCaps, negotiate}, crate::output::display::enumerate_monitors, gtk::glib, gtk::prelude::*, lesavka_common::lesavka::CapturePowerCommand, std::cell::{Cell, RefCell}, std::process::Command, std::rc::Rc, std::time::Duration, }; #[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(PeerCaps), } #[cfg(not(coverage))] enum ClipboardMessage { Finished(std::result::Result), } #[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 caps = match runtime { Ok(runtime) => runtime.block_on(negotiate(&server_addr)), Err(_) => PeerCaps::default(), }; let _ = caps_tx.send(CapsMessage::Refresh(caps)); }); } #[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(), } } #[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_capture_size_combo( &widgets.display_panes[monitor_id].capture_combo, state.capture_size_options(), state.capture_size_preset(monitor_id), ); super::ui_components::sync_breakout_size_combo( &widgets.display_panes[monitor_id].breakout_combo, state.breakout_size_options(), state.breakout_size_preset(monitor_id), ); } } #[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 rebind_inline_preview( preview: &super::preview::LauncherPreview, widgets: &super::ui_components::LauncherWidgets, monitor_id: usize, ) { if let Some(binding) = widgets.display_panes[monitor_id] .preview_binding .borrow_mut() .take() { binding.close(); } 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]>>, 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 let Some(binding) = preview.install_on_picture( monitor_id, super::preview::PreviewSurface::Window, &handle.picture, &handle.status_label, ) { handle.binding = binding; } } #[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 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 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 (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()); } { 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, ); 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(); 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), ); 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 capture_combo = widgets.display_panes[monitor_id].capture_combo.clone(); capture_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().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().capture_size_choice(monitor_id); preview.set_capture_profile( monitor_id, choice.width, choice.height, choice.fps, choice.max_bitrate_kbit, ); rebind_inline_preview(preview, &widgets, monitor_id); rebind_popout_preview(preview, &popouts, monitor_id); } 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, 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 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(); 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_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 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() { match message { CapsMessage::Refresh(caps) => { { let mut state = state.borrow_mut(); 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(30); { let mut state = state.borrow_mut(); state.set_preview_source_profile(width, height, fps); } if let Some(preview) = preview.as_ref() { for monitor_id in 0..2 { let capture = state.borrow().capture_size_choice(monitor_id); let breakout = state.borrow().breakout_size_choice(monitor_id); preview.set_capture_profile( monitor_id, capture.width, capture.height, capture.fps, capture.max_bitrate_kbit, ); preview.set_breakout_profile( monitor_id, breakout.width, breakout.height, ); rebind_inline_preview(preview, &widgets, monitor_id); rebind_popout_preview(preview, &popouts, monitor_id); } } 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 child_running = child_proc.borrow().is_some(); 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, 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(), }); assert!(!session_preview_active(&state, true)); } }