use anyhow::Result; #[cfg(not(coverage))] use { super::clipboard::send_clipboard_to_remote, super::device_test::DeviceTestController, super::devices::DeviceCatalog, super::diagnostics::quality_probe_command, super::launcher_focus_signal_path, super::power::{fetch_capture_power, set_capture_power_mode}, super::state::{CapturePowerStatus, DisplaySurface, InputRouting, LauncherState}, super::ui_components::build_launcher_view, super::ui_runtime::{ dock_display_to_preview, input_control_path, input_state_path, next_input_routing, open_popout_window, path_marker, read_input_routing_state, reap_exited_child, refresh_launcher_ui, refresh_test_buttons, routing_name, selected_combo_value, selected_server_addr, selected_toggle_key, spawn_client_process, stop_child_process, update_test_action_result, write_input_routing_request, }, gtk::glib, gtk::prelude::*, lesavka_common::lesavka::CapturePowerCommand, std::cell::{Cell, RefCell}, std::process::Child, std::rc::Rc, std::time::{Duration, Instant}, }; #[cfg(not(coverage))] enum PowerMessage { Poll(std::result::Result), Command(std::result::Result), } #[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 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(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 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(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 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 widgets = view.widgets.clone(); let preview = view.preview.clone(); let popouts = Rc::clone(&view.popouts); { 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 last_power_poll = Rc::new(RefCell::new(None::)); { 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); 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}")); } 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 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)); 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 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)); 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(); 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 start_button = widgets.start_button.clone(); let widgets_handle = widgets.clone(); start_button.connect_clicked(move |_| { if child_proc.borrow().is_some() { widgets_handle .status_label .set_text("Relay is already running."); refresh_launcher_ui(&widgets_handle, &state.borrow(), true); 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)); } let _ = std::fs::remove_file(input_control_path.as_path()); let _ = std::fs::remove_file(input_state_path.as_path()); let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); let launch_state = state.borrow().clone(); let input_toggle_key = selected_toggle_key(&widgets.toggle_key_combo); match spawn_client_process( &server_addr, &launch_state, &input_toggle_key, input_control_path.as_path(), input_state_path.as_path(), ) { Ok(child) => { *child_proc.borrow_mut() = Some(child); let _ = state.borrow_mut().start_remote(); widgets_handle.status_label.set_text(&format!( "Relay started. Inputs are routed to {}.", routing_name(state.borrow().routing) )); } Err(err) => { widgets_handle .status_label .set_text(&format!("Relay start failed: {err}")); } } refresh_launcher_ui( &widgets_handle, &state.borrow(), child_proc.borrow().is_some(), ); }); } { let state = Rc::clone(&state); let child_proc = Rc::clone(&child_proc); let widgets = widgets.clone(); let stop_button = widgets.stop_button.clone(); let widgets_handle = widgets.clone(); stop_button.connect_clicked(move |_| { stop_child_process(&child_proc); let _ = state.borrow_mut().stop_remote(); widgets_handle.status_label.set_text("Relay stopped."); refresh_launcher_ui(&widgets_handle, &state.borrow(), false); }); } { 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 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); }); } { 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); 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()); widgets .status_label .set_text("Sending clipboard to the remote target..."); let (result_tx, result_rx) = std::sync::mpsc::channel::(); std::thread::spawn(move || { let message = match send_clipboard_to_remote(&server_addr) { Ok(mode) => mode, Err(err) => format!("Clipboard send failed: {err}"), }; let _ = result_tx.send(message); }); let status_label = widgets.status_label.clone(); glib::timeout_add_local(Duration::from_millis(100), move || { match result_rx.try_recv() { Ok(message) => { status_label.set_text(&message); glib::ControlFlow::Break } Err(std::sync::mpsc::TryRecvError::Empty) => { glib::ControlFlow::Continue } Err(std::sync::mpsc::TryRecvError::Disconnected) => { status_label .set_text("Clipboard send failed: launcher worker exited."); glib::ControlFlow::Break } } }); }); } { 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 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.", "Camera preview stopped.", ); }); } { 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.", "Microphone monitor 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 test tone started.", "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..."); let tx = power_tx.clone(); std::thread::spawn(move || { let result = set_capture_power_mode(&server_addr, CapturePowerCommand::Auto) .map_err(|err| err.to_string()); let _ = tx.send(PowerMessage::Command(result)); }); }); } { 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..."); let tx = power_tx.clone(); std::thread::spawn(move || { let result = set_capture_power_mode(&server_addr, CapturePowerCommand::ForceOn) .map_err(|err| err.to_string()); let _ = tx.send(PowerMessage::Command(result)); }); }); } { 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..."); let tx = power_tx.clone(); std::thread::spawn(move || { let result = set_capture_power_mode(&server_addr, CapturePowerCommand::ForceOff) .map_err(|err| err.to_string()); let _ = tx.send(PowerMessage::Command(result)); }); }); } 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; }; match state.borrow().display_surface(monitor_id) { 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 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 last_power_poll = Rc::clone(&last_power_poll); 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 _ = state.borrow_mut().stop_remote(); widgets.status_label.set_text("Relay ended."); } 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); } } 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) = power_rx.try_recv() { power_request_in_flight.set(false); match message { PowerMessage::Poll(Ok(power)) => { state.borrow_mut().set_capture_power(power); } PowerMessage::Poll(Err(err)) => { state.borrow_mut().set_capture_power(CapturePowerStatus { available: false, enabled: false, unit: "relay.service".to_string(), detail: err, active_leases: 0, mode: "auto".to_string(), }); } PowerMessage::Command(Ok(power)) => { let mode = power.mode.clone(); state.borrow_mut().set_capture_power(power); widgets.status_label.set_text(match mode.as_str() { "forced-on" => "Capture feeds forced on.", "forced-off" => "Capture feeds forced off.", _ => "Capture feeds returned to automatic mode.", }); } PowerMessage::Command(Err(err)) => { widgets .status_label .set_text(&format!("Capture power update failed: {err}")); } } } let should_poll_power = !power_request_in_flight.get() && last_power_poll .borrow() .map(|stamp| stamp.elapsed() >= Duration::from_millis(1400)) .unwrap_or(true); if should_poll_power { *last_power_poll.borrow_mut() = Some(Instant::now()); power_request_in_flight.set(true); let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref()); let tx = power_tx.clone(); std::thread::spawn(move || { let result = fetch_capture_power(&server_addr).map_err(|err| err.to_string()); let _ = tx.send(PowerMessage::Poll(result)); }); } 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; #[test] fn coverage_stub_returns_ok() { assert!(run_gui_launcher("http://127.0.0.1:50051".to_string()).is_ok()); } }