1539 lines
70 KiB
Rust

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<CapturePowerStatus, String>),
Command(std::result::Result<CapturePowerStatus, String>),
}
#[cfg(not(coverage))]
enum RelayMessage {
Spawned(std::result::Result<RelayChild, String>),
}
#[cfg(not(coverage))]
enum CapsMessage {
Refresh(PeerCaps),
}
#[cfg(not(coverage))]
enum ClipboardMessage {
Finished(std::result::Result<String, String>),
}
#[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 request_capture_power_command(
power_tx: std::sync::mpsc::Sender<PowerMessage>,
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<CapsMessage>,
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::<u32>().ok()?;
let height = height.parse::<u32>().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<RefCell<[Option<super::ui_components::PopoutWindowHandle>; 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::<RelayChild>));
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::<PowerMessage>();
let power_request_in_flight = Rc::new(Cell::new(false));
let (relay_tx, relay_rx) = std::sync::mpsc::channel::<RelayMessage>();
let relay_request_in_flight = Rc::new(Cell::new(false));
let (caps_tx, caps_rx) = std::sync::mpsc::channel::<CapsMessage>();
let (clipboard_tx, clipboard_rx) = std::sync::mpsc::channel::<ClipboardMessage>();
let (log_tx, log_rx) = std::sync::mpsc::channel::<String>();
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(&microphone_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(&microphone_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::<&gtk::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(&microphone_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));
}
}