2026-04-14 18:44:40 -03:00
|
|
|
use anyhow::Result;
|
2026-04-13 23:11:35 -03:00
|
|
|
|
|
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
use {
|
2026-04-14 18:44:40 -03:00
|
|
|
super::clipboard::send_clipboard_to_remote,
|
2026-04-15 01:57:14 -03:00
|
|
|
super::device_test::{DeviceTestController, DeviceTestKind},
|
2026-04-13 23:11:35 -03:00
|
|
|
super::devices::DeviceCatalog,
|
2026-04-14 13:09:25 -03:00
|
|
|
super::diagnostics::quality_probe_command,
|
2026-04-14 18:44:40 -03:00
|
|
|
super::launcher_focus_signal_path,
|
2026-04-15 01:20:51 -03:00
|
|
|
super::power::{fetch_capture_power, set_capture_power_mode},
|
2026-04-14 23:03:18 -03:00
|
|
|
super::state::{CapturePowerStatus, DisplaySurface, InputRouting, LauncherState},
|
|
|
|
|
super::ui_components::build_launcher_view,
|
|
|
|
|
super::ui_runtime::{
|
2026-04-15 04:44:06 -03:00
|
|
|
RelayChild, capture_swap_key, 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, spawn_client_process,
|
|
|
|
|
stop_child_process, toggle_key_label, update_test_action_result,
|
|
|
|
|
write_input_routing_request,
|
2026-04-14 23:03:18 -03:00
|
|
|
},
|
2026-04-14 18:44:40 -03:00
|
|
|
gtk::glib,
|
2026-04-14 20:05:26 -03:00
|
|
|
gtk::prelude::*,
|
2026-04-15 01:20:51 -03:00
|
|
|
lesavka_common::lesavka::CapturePowerCommand,
|
2026-04-14 23:03:18 -03:00
|
|
|
std::cell::{Cell, RefCell},
|
2026-04-13 23:11:35 -03:00
|
|
|
std::rc::Rc,
|
2026-04-15 04:11:47 -03:00
|
|
|
std::time::Duration,
|
2026-04-13 23:11:35 -03:00
|
|
|
};
|
|
|
|
|
|
2026-04-14 20:05:26 -03:00
|
|
|
#[cfg(not(coverage))]
|
2026-04-14 23:03:18 -03:00
|
|
|
enum PowerMessage {
|
2026-04-15 04:11:47 -03:00
|
|
|
Refresh(std::result::Result<CapturePowerStatus, String>),
|
2026-04-14 23:03:18 -03:00
|
|
|
Command(std::result::Result<CapturePowerStatus, String>),
|
2026-04-14 20:05:26 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 04:44:06 -03:00
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
enum RelayMessage {
|
|
|
|
|
Spawned(std::result::Result<RelayChild, String>),
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 04:11:47 -03:00
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
fn request_capture_power_refresh(
|
|
|
|
|
power_tx: std::sync::mpsc::Sender<PowerMessage>,
|
|
|
|
|
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 disconnected_capture_note(mode: &str) -> &'static str {
|
|
|
|
|
match mode {
|
|
|
|
|
"forced-on" => "Relay disconnected. Capture is still forced on for staging.",
|
2026-04-15 04:44:06 -03:00
|
|
|
"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."
|
|
|
|
|
}
|
2026-04-15 04:11:47 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 23:11:35 -03:00
|
|
|
#[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);
|
2026-04-15 04:22:25 -03:00
|
|
|
let child_proc = Rc::new(RefCell::new(None::<RelayChild>));
|
2026-04-14 23:03:18 -03:00
|
|
|
let tests = Rc::new(RefCell::new(DeviceTestController::new()));
|
2026-04-13 23:11:35 -03:00
|
|
|
let server_addr = Rc::new(server_addr);
|
2026-04-14 18:44:40 -03:00
|
|
|
let focus_signal_path = Rc::new(launcher_focus_signal_path());
|
2026-04-14 20:05:26 -03:00
|
|
|
let input_control_path = Rc::new(input_control_path());
|
|
|
|
|
let input_state_path = Rc::new(input_state_path());
|
2026-04-14 18:44:40 -03:00
|
|
|
let _ = std::fs::remove_file(focus_signal_path.as_path());
|
2026-04-14 20:05:26 -03:00
|
|
|
let _ = std::fs::remove_file(input_control_path.as_path());
|
|
|
|
|
let _ = std::fs::remove_file(input_state_path.as_path());
|
2026-04-13 23:11:35 -03:00
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let child_proc = Rc::clone(&child_proc);
|
2026-04-14 20:05:26 -03:00
|
|
|
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);
|
2026-04-14 23:03:18 -03:00
|
|
|
let tests = Rc::clone(&tests);
|
2026-04-13 23:11:35 -03:00
|
|
|
app.connect_shutdown(move |_| {
|
2026-04-14 20:05:26 -03:00
|
|
|
stop_child_process(&child_proc);
|
2026-04-14 23:03:18 -03:00
|
|
|
tests.borrow_mut().stop_all();
|
2026-04-14 20:05:26 -03:00
|
|
|
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());
|
2026-04-13 23:11:35 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let catalog = Rc::clone(&catalog);
|
|
|
|
|
let state = Rc::clone(&state);
|
|
|
|
|
let child_proc = Rc::clone(&child_proc);
|
2026-04-14 23:03:18 -03:00
|
|
|
let tests = Rc::clone(&tests);
|
2026-04-13 23:11:35 -03:00
|
|
|
let server_addr = Rc::clone(&server_addr);
|
2026-04-14 18:44:40 -03:00
|
|
|
let focus_signal_path = Rc::clone(&focus_signal_path);
|
2026-04-14 20:05:26 -03:00
|
|
|
let input_control_path = Rc::clone(&input_control_path);
|
|
|
|
|
let input_state_path = Rc::clone(&input_state_path);
|
2026-04-13 23:11:35 -03:00
|
|
|
|
|
|
|
|
app.connect_activate(move |app| {
|
2026-04-14 23:03:18 -03:00
|
|
|
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);
|
2026-04-14 20:05:26 -03:00
|
|
|
|
2026-04-15 01:20:51 -03:00
|
|
|
{
|
|
|
|
|
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}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 20:05:26 -03:00
|
|
|
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
|
2026-04-14 23:03:18 -03:00
|
|
|
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
|
|
|
|
|
|
|
|
|
let (power_tx, power_rx) = std::sync::mpsc::channel::<PowerMessage>();
|
|
|
|
|
let power_request_in_flight = Rc::new(Cell::new(false));
|
2026-04-15 04:44:06 -03:00
|
|
|
let (relay_tx, relay_rx) = std::sync::mpsc::channel::<RelayMessage>();
|
|
|
|
|
let relay_request_in_flight = Rc::new(Cell::new(false));
|
2026-04-14 18:44:40 -03:00
|
|
|
|
|
|
|
|
{
|
2026-04-14 23:03:18 -03:00
|
|
|
let state = Rc::clone(&state);
|
2026-04-14 20:05:26 -03:00
|
|
|
let widgets = widgets.clone();
|
2026-04-14 23:03:18 -03:00
|
|
|
let child_proc = Rc::clone(&child_proc);
|
2026-04-15 01:20:51 -03:00
|
|
|
let tests = Rc::clone(&tests);
|
2026-04-14 23:03:18 -03:00
|
|
|
let camera_combo = camera_combo.clone();
|
|
|
|
|
let camera_combo_read = camera_combo.clone();
|
|
|
|
|
camera_combo.connect_changed(move |_| {
|
2026-04-15 01:20:51 -03:00
|
|
|
let selected = selected_combo_value(&camera_combo_read);
|
2026-04-15 01:57:14 -03:00
|
|
|
let preview_was_running =
|
|
|
|
|
tests.borrow_mut().is_running(DeviceTestKind::Camera);
|
2026-04-15 01:20:51 -03:00
|
|
|
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}"));
|
2026-04-15 01:57:14 -03:00
|
|
|
} else if preview_was_running {
|
|
|
|
|
widgets.status_label.set_text(&format!(
|
|
|
|
|
"Local camera preview switched to {}.",
|
|
|
|
|
selected.as_deref().unwrap_or("auto")
|
|
|
|
|
));
|
2026-04-15 01:20:51 -03:00
|
|
|
}
|
2026-04-14 23:03:18 -03:00
|
|
|
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
|
2026-04-15 01:20:51 -03:00
|
|
|
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
2026-04-14 23:03:18 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 04:11:47 -03:00
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-14 23:03:18 -03:00
|
|
|
{
|
|
|
|
|
let state = Rc::clone(&state);
|
|
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let child_proc = Rc::clone(&child_proc);
|
2026-04-15 01:57:14 -03:00
|
|
|
let tests = Rc::clone(&tests);
|
2026-04-14 23:03:18 -03:00
|
|
|
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));
|
2026-04-15 01:57:14 -03:00
|
|
|
if tests.borrow_mut().is_running(DeviceTestKind::Microphone) {
|
|
|
|
|
widgets.status_label.set_text(
|
|
|
|
|
"Microphone selection changed. Restart Monitor Mic to audition the new input.",
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-14 23:03:18 -03:00
|
|
|
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
|
2026-04-15 01:57:14 -03:00
|
|
|
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
2026-04-14 23:03:18 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
2026-04-14 18:44:40 -03:00
|
|
|
let state = Rc::clone(&state);
|
2026-04-14 23:03:18 -03:00
|
|
|
let widgets = widgets.clone();
|
2026-04-14 20:05:26 -03:00
|
|
|
let child_proc = Rc::clone(&child_proc);
|
2026-04-15 01:57:14 -03:00
|
|
|
let tests = Rc::clone(&tests);
|
2026-04-14 23:03:18 -03:00
|
|
|
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));
|
2026-04-15 01:57:14 -03:00
|
|
|
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.",
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-14 20:05:26 -03:00
|
|
|
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
|
2026-04-15 01:57:14 -03:00
|
|
|
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
2026-04-14 18:44:40 -03:00
|
|
|
});
|
2026-04-14 14:38:03 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-13 23:11:35 -03:00
|
|
|
{
|
|
|
|
|
let state = Rc::clone(&state);
|
|
|
|
|
let child_proc = Rc::clone(&child_proc);
|
2026-04-14 20:05:26 -03:00
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let server_entry = server_entry.clone();
|
2026-04-13 23:11:35 -03:00
|
|
|
let camera_combo = camera_combo.clone();
|
|
|
|
|
let microphone_combo = microphone_combo.clone();
|
|
|
|
|
let speaker_combo = speaker_combo.clone();
|
2026-04-14 20:05:26 -03:00
|
|
|
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);
|
2026-04-15 04:11:47 -03:00
|
|
|
let preview = preview.clone();
|
|
|
|
|
let power_tx = power_tx.clone();
|
2026-04-15 04:44:06 -03:00
|
|
|
let relay_tx = relay_tx.clone();
|
|
|
|
|
let relay_request_in_flight = Rc::clone(&relay_request_in_flight);
|
2026-04-14 23:03:18 -03:00
|
|
|
let start_button = widgets.start_button.clone();
|
|
|
|
|
let widgets_handle = widgets.clone();
|
2026-04-13 23:11:35 -03:00
|
|
|
start_button.connect_clicked(move |_| {
|
2026-04-15 04:11:47 -03:00
|
|
|
let server_addr =
|
|
|
|
|
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
2026-04-15 04:44:06 -03:00
|
|
|
if relay_request_in_flight.get() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-14 20:05:26 -03:00
|
|
|
if child_proc.borrow().is_some() {
|
2026-04-15 04:11:47 -03:00
|
|
|
stop_child_process(&child_proc);
|
|
|
|
|
let power_mode = {
|
|
|
|
|
let mut state = state.borrow_mut();
|
|
|
|
|
let _ = state.stop_remote();
|
|
|
|
|
state.capture_power.mode.clone()
|
|
|
|
|
};
|
|
|
|
|
if let Some(preview) = preview.as_ref() {
|
|
|
|
|
preview.set_server_addr(server_addr.clone());
|
|
|
|
|
preview.set_session_active(false);
|
|
|
|
|
}
|
2026-04-14 23:03:18 -03:00
|
|
|
widgets_handle
|
|
|
|
|
.status_label
|
2026-04-15 04:11:47 -03:00
|
|
|
.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);
|
2026-04-14 20:05:26 -03:00
|
|
|
return;
|
|
|
|
|
}
|
2026-04-13 23:11:35 -03:00
|
|
|
{
|
|
|
|
|
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));
|
|
|
|
|
}
|
2026-04-14 20:05:26 -03:00
|
|
|
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();
|
2026-04-15 04:44:06 -03:00
|
|
|
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)
|
|
|
|
|
));
|
2026-04-14 23:03:18 -03:00
|
|
|
refresh_launcher_ui(
|
|
|
|
|
&widgets_handle,
|
|
|
|
|
&state.borrow(),
|
|
|
|
|
child_proc.borrow().is_some(),
|
|
|
|
|
);
|
2026-04-15 04:44:06 -03:00
|
|
|
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));
|
|
|
|
|
});
|
2026-04-13 23:11:35 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 04:02:39 -03:00
|
|
|
{
|
|
|
|
|
let state = Rc::clone(&state);
|
|
|
|
|
let child_proc = Rc::clone(&child_proc);
|
2026-04-14 20:05:26 -03:00
|
|
|
let widgets = widgets.clone();
|
|
|
|
|
let input_control_path = Rc::clone(&input_control_path);
|
2026-04-14 23:03:18 -03:00
|
|
|
let input_toggle_button = widgets.input_toggle_button.clone();
|
|
|
|
|
let widgets_handle = widgets.clone();
|
2026-04-14 20:05:26 -03:00
|
|
|
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)
|
|
|
|
|
{
|
2026-04-14 23:03:18 -03:00
|
|
|
widgets_handle
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text(&format!("Could not update live input target: {err}"));
|
|
|
|
|
refresh_launcher_ui(&widgets_handle, &state.borrow(), true);
|
2026-04-14 20:05:26 -03:00
|
|
|
return;
|
2026-04-14 04:02:39 -03:00
|
|
|
}
|
2026-04-14 23:03:18 -03:00
|
|
|
widgets_handle.status_label.set_text(&format!(
|
|
|
|
|
"Input routing switched toward {}.",
|
2026-04-14 20:05:26 -03:00
|
|
|
routing_name(next)
|
|
|
|
|
));
|
2026-04-14 04:02:39 -03:00
|
|
|
} else {
|
2026-04-14 23:03:18 -03:00
|
|
|
widgets_handle.status_label.set_text(&format!(
|
|
|
|
|
"Relay will start with {} input ownership.",
|
2026-04-14 20:05:26 -03:00
|
|
|
routing_name(next)
|
|
|
|
|
));
|
2026-04-14 04:02:39 -03:00
|
|
|
}
|
2026-04-14 20:05:26 -03:00
|
|
|
state.borrow_mut().set_routing(next);
|
2026-04-14 23:03:18 -03:00
|
|
|
refresh_launcher_ui(&widgets_handle, &state.borrow(), child_running);
|
2026-04-14 04:02:39 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 04:44:06 -03:00
|
|
|
{
|
|
|
|
|
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 |_| {
|
|
|
|
|
state.borrow_mut().begin_swap_key_binding();
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Press a single key now to make it the swap shortcut.");
|
|
|
|
|
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 23:11:35 -03:00
|
|
|
{
|
2026-04-14 13:09:25 -03:00
|
|
|
let child_proc = Rc::clone(&child_proc);
|
2026-04-14 20:05:26 -03:00
|
|
|
let widgets = widgets.clone();
|
2026-04-14 13:09:25 -03:00
|
|
|
let server_entry = server_entry.clone();
|
2026-04-14 20:05:26 -03:00
|
|
|
let server_addr_fallback = Rc::clone(&server_addr);
|
2026-04-14 23:03:18 -03:00
|
|
|
widgets.clipboard_button.connect_clicked(move |_| {
|
2026-04-14 13:09:25 -03:00
|
|
|
if child_proc.borrow().is_none() {
|
2026-04-14 23:03:18 -03:00
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Start the relay before sending clipboard text.");
|
2026-04-14 13:09:25 -03:00
|
|
|
return;
|
|
|
|
|
}
|
2026-04-14 20:05:26 -03:00
|
|
|
let server_addr =
|
|
|
|
|
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
2026-04-14 23:03:18 -03:00
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text("Sending clipboard to the remote target...");
|
2026-04-14 18:44:40 -03:00
|
|
|
let (result_tx, result_rx) = std::sync::mpsc::channel::<String>();
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-14 20:05:26 -03:00
|
|
|
let status_label = widgets.status_label.clone();
|
2026-04-14 18:44:40 -03:00
|
|
|
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) => {
|
2026-04-14 20:05:26 -03:00
|
|
|
status_label
|
2026-04-14 23:03:18 -03:00
|
|
|
.set_text("Clipboard send failed: launcher worker exited.");
|
2026-04-14 18:44:40 -03:00
|
|
|
glib::ControlFlow::Break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-13 23:11:35 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:03:18 -03:00
|
|
|
{
|
|
|
|
|
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 |_| {
|
2026-04-15 01:20:51 -03:00
|
|
|
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()
|
|
|
|
|
};
|
2026-04-14 23:03:18 -03:00
|
|
|
update_test_action_result(
|
|
|
|
|
&widgets_handle,
|
|
|
|
|
&mut tests.borrow_mut(),
|
|
|
|
|
result,
|
2026-04-15 01:57:14 -03:00
|
|
|
"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.",
|
2026-04-14 23:03:18 -03:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
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,
|
2026-04-15 01:57:14 -03:00
|
|
|
"Microphone monitor started locally through the selected speaker.",
|
2026-04-14 23:03:18 -03:00
|
|
|
"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,
|
2026-04-15 01:57:14 -03:00
|
|
|
"Speaker tone started locally.",
|
2026-04-14 23:03:18 -03:00
|
|
|
"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);
|
2026-04-15 01:20:51 -03:00
|
|
|
let power_auto_button = widgets.power_auto_button.clone();
|
|
|
|
|
let widgets_handle = widgets.clone();
|
|
|
|
|
power_auto_button.connect_clicked(move |_| {
|
2026-04-14 23:03:18 -03:00
|
|
|
if power_request_in_flight.replace(true) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let server_addr =
|
|
|
|
|
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
2026-04-15 01:20:51 -03:00
|
|
|
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));
|
2026-04-14 23:03:18 -03:00
|
|
|
});
|
2026-04-15 01:20:51 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
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...");
|
2026-04-14 23:03:18 -03:00
|
|
|
let tx = power_tx.clone();
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
let result =
|
2026-04-15 01:20:51 -03:00
|
|
|
set_capture_power_mode(&server_addr, CapturePowerCommand::ForceOff)
|
|
|
|
|
.map_err(|err| err.to_string());
|
2026-04-14 23:03:18 -03:00
|
|
|
let _ = tx.send(PowerMessage::Command(result));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 20:05:26 -03:00
|
|
|
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
|
2026-04-14 23:03:18 -03:00
|
|
|
.set_text("Preview is unavailable for breakout windows.");
|
2026-04-14 20:05:26 -03:00
|
|
|
return;
|
|
|
|
|
};
|
2026-04-14 23:03:18 -03:00
|
|
|
match state.borrow().display_surface(monitor_id) {
|
2026-04-14 20:05:26 -03:00
|
|
|
DisplaySurface::Preview => {
|
|
|
|
|
open_popout_window(
|
|
|
|
|
&app,
|
|
|
|
|
preview,
|
|
|
|
|
&state,
|
|
|
|
|
&child_proc,
|
|
|
|
|
&popouts,
|
|
|
|
|
&widgets,
|
|
|
|
|
monitor_id,
|
|
|
|
|
);
|
|
|
|
|
widgets.status_label.set_text(&format!(
|
2026-04-14 23:03:18 -03:00
|
|
|
"{} moved into its own window.",
|
2026-04-14 20:05:26 -03:00
|
|
|
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!(
|
2026-04-14 23:03:18 -03:00
|
|
|
"{} returned to the launcher preview.",
|
2026-04-14 20:05:26 -03:00
|
|
|
widgets.display_panes[monitor_id].title
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 04:44:06 -03:00
|
|
|
{
|
|
|
|
|
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.set_swap_key(swap_key.clone());
|
|
|
|
|
state.finish_swap_key_binding();
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 20:05:26 -03:00
|
|
|
{
|
|
|
|
|
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);
|
2026-04-14 23:03:18 -03:00
|
|
|
let tests = Rc::clone(&tests);
|
|
|
|
|
let server_entry = server_entry.clone();
|
|
|
|
|
let server_addr_fallback = Rc::clone(&server_addr);
|
2026-04-14 20:05:26 -03:00
|
|
|
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())));
|
2026-04-14 23:03:18 -03:00
|
|
|
let power_request_in_flight = Rc::clone(&power_request_in_flight);
|
2026-04-15 04:44:06 -03:00
|
|
|
let relay_request_in_flight = Rc::clone(&relay_request_in_flight);
|
2026-04-15 04:11:47 -03:00
|
|
|
let preview = preview.clone();
|
|
|
|
|
let power_tx = power_tx.clone();
|
2026-04-14 20:05:26 -03:00
|
|
|
glib::timeout_add_local(Duration::from_millis(180), move || {
|
|
|
|
|
let child_running = reap_exited_child(&child_proc);
|
|
|
|
|
if !child_running && state.borrow().remote_active {
|
2026-04-15 04:11:47 -03:00
|
|
|
let power_mode = {
|
|
|
|
|
let mut state = state.borrow_mut();
|
|
|
|
|
let _ = state.stop_remote();
|
|
|
|
|
state.capture_power.mode.clone()
|
|
|
|
|
};
|
|
|
|
|
if let Some(preview) = preview.as_ref() {
|
|
|
|
|
preview.set_session_active(false);
|
|
|
|
|
}
|
2026-04-15 01:57:14 -03:00
|
|
|
widgets
|
|
|
|
|
.status_label
|
2026-04-15 04:11:47 -03:00
|
|
|
.set_text(disconnected_capture_note(&power_mode));
|
|
|
|
|
let server_addr =
|
|
|
|
|
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
|
|
|
|
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),
|
|
|
|
|
);
|
2026-04-14 20:05:26 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-14 23:03:18 -03:00
|
|
|
.set_text("Local control restored and the launcher is focused.");
|
2026-04-14 20:05:26 -03:00
|
|
|
window.present();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 04:44:06 -03:00
|
|
|
while let Ok(message) = relay_rx.try_recv() {
|
|
|
|
|
relay_request_in_flight.set(false);
|
|
|
|
|
match message {
|
|
|
|
|
RelayMessage::Spawned(Ok(child)) => {
|
|
|
|
|
*child_proc.borrow_mut() = Some(child);
|
|
|
|
|
let _ = state.borrow_mut().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(true);
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
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)) => {
|
|
|
|
|
if let Some(preview) = preview.as_ref() {
|
|
|
|
|
preview.set_session_active(false);
|
|
|
|
|
}
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text(&format!("Relay start failed: {err}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:03:18 -03:00
|
|
|
while let Ok(message) = power_rx.try_recv() {
|
|
|
|
|
power_request_in_flight.set(false);
|
|
|
|
|
match message {
|
2026-04-15 04:11:47 -03:00
|
|
|
PowerMessage::Refresh(Ok(power)) => {
|
2026-04-14 23:03:18 -03:00
|
|
|
state.borrow_mut().set_capture_power(power);
|
|
|
|
|
}
|
2026-04-15 04:11:47 -03:00
|
|
|
PowerMessage::Refresh(Err(err)) => {
|
2026-04-14 23:03:18 -03:00
|
|
|
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)) => {
|
2026-04-15 01:20:51 -03:00
|
|
|
let mode = power.mode.clone();
|
2026-04-14 23:03:18 -03:00
|
|
|
state.borrow_mut().set_capture_power(power);
|
2026-04-15 01:20:51 -03:00
|
|
|
widgets.status_label.set_text(match mode.as_str() {
|
2026-04-15 01:57:14 -03:00
|
|
|
"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.",
|
2026-04-14 23:03:18 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
PowerMessage::Command(Err(err)) => {
|
|
|
|
|
widgets
|
|
|
|
|
.status_label
|
|
|
|
|
.set_text(&format!("Capture power update failed: {err}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 20:05:26 -03:00
|
|
|
refresh_launcher_ui(&widgets, &state.borrow(), child_running);
|
2026-04-14 23:03:18 -03:00
|
|
|
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
2026-04-14 20:05:26 -03:00
|
|
|
glib::ControlFlow::Continue
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 23:11:35 -03:00
|
|
|
window.present();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 08:16:57 -03:00
|
|
|
let _ = app.run_with_args::<&str>(&[]);
|
2026-04-13 23:11:35 -03:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|