2342 lines
104 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::{PerformanceSample, quality_probe_command},
super::launcher_clipboard_control_path,
super::launcher_focus_signal_path,
super::power::{fetch_capture_power, reset_usb_gadget, set_capture_power_mode},
super::state::{
BreakoutSizePreset, CapturePowerStatus, CaptureSizePreset, DisplaySurface,
FeedSourcePreset, InputRouting, LauncherState, MAX_AUDIO_GAIN_PERCENT,
},
super::ui_components::{build_launcher_view, sync_input_device_combo, sync_stage_device_combo},
super::ui_runtime::{
RelayChild, append_session_log, apply_popout_window_size, attach_child_log_streams,
audio_gain_control_path, capture_swap_key, copy_plain_text, copy_session_log,
dock_all_displays_to_preview, dock_display_to_preview, input_control_path,
input_state_path, input_toggle_control_path, next_input_routing, open_diagnostics_popout,
open_popout_window, open_session_log_popout, path_marker, present_popout_windows,
read_input_routing_state, reap_exited_child, refresh_launcher_ui, refresh_test_buttons,
routing_name, selected_combo_value, selected_server_addr, shutdown_launcher_runtime,
spawn_client_process, stop_child_process, toggle_key_label, update_test_action_result,
write_audio_gain_request, write_input_routing_request, write_input_toggle_key_request,
},
crate::handshake::{HandshakeProbe, probe},
crate::output::display::enumerate_monitors,
gtk::glib,
gtk::prelude::*,
lesavka_common::lesavka::CapturePowerCommand,
lesavka_common::process_metrics::ProcessCpuSampler,
std::cell::{Cell, RefCell},
std::collections::VecDeque,
std::process::Command,
std::rc::Rc,
std::time::{Duration, Instant},
};
#[cfg(not(coverage))]
enum PowerMessage {
Refresh(std::result::Result<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(HandshakeProbe),
}
#[cfg(not(coverage))]
enum ClipboardMessage {
Finished(std::result::Result<String, String>),
}
#[cfg(not(coverage))]
const NETWORK_TELEMETRY_WINDOW: Duration = Duration::from_secs(8);
#[cfg(not(coverage))]
fn usb_audio_kernel_support_missing() -> bool {
Command::new("modinfo")
.arg("snd_usb_audio")
.status()
.map(|status| !status.success())
.unwrap_or(true)
}
#[cfg(not(coverage))]
#[derive(Default)]
struct NetworkTelemetry {
rtt_samples: VecDeque<(Instant, f32)>,
failures: VecDeque<Instant>,
}
#[cfg(not(coverage))]
#[derive(Clone, Copy, Debug, Default)]
struct NetworkSnapshot {
rtt_ms: f32,
probe_spread_ms: f32,
probe_loss_pct: f32,
}
#[cfg(not(coverage))]
impl NetworkTelemetry {
fn record(&mut self, probe: &HandshakeProbe) {
let now = Instant::now();
self.trim(now);
if let Some(rtt_ms) = probe.rtt_ms {
self.rtt_samples.push_back((now, rtt_ms));
} else {
self.failures.push_back(now);
}
self.trim(now);
}
fn snapshot(&mut self) -> NetworkSnapshot {
let now = Instant::now();
self.trim(now);
let rtt_ms = self.rtt_samples.back().map(|(_, rtt)| *rtt).unwrap_or(0.0);
let probe_spread_ms = network_spread_ms(&self.rtt_samples);
let probe_count = self.rtt_samples.len() + self.failures.len();
let probe_loss_pct = if probe_count == 0 {
0.0
} else {
self.failures.len() as f32 * 100.0 / probe_count as f32
};
NetworkSnapshot {
rtt_ms,
probe_spread_ms,
probe_loss_pct,
}
}
fn trim(&mut self, now: Instant) {
while let Some((oldest, _)) = self.rtt_samples.front().copied() {
if now.saturating_duration_since(oldest) > NETWORK_TELEMETRY_WINDOW {
let _ = self.rtt_samples.pop_front();
} else {
break;
}
}
while let Some(oldest) = self.failures.front().copied() {
if now.saturating_duration_since(oldest) > NETWORK_TELEMETRY_WINDOW {
let _ = self.failures.pop_front();
} else {
break;
}
}
}
}
#[cfg(not(coverage))]
fn retained_stage_selection(current: Option<&str>, values: &[String]) -> Option<String> {
current
.filter(|selected| values.iter().any(|value| value == *selected))
.map(str::to_string)
.or_else(|| values.first().cloned())
}
#[cfg(not(coverage))]
fn retained_input_selection(current: Option<&str>, values: &[String]) -> Option<String> {
current
.filter(|selected| values.iter().any(|value| value == *selected))
.map(str::to_string)
}
#[cfg(not(coverage))]
fn network_spread_ms(samples: &VecDeque<(Instant, f32)>) -> f32 {
if samples.len() < 2 {
return 0.0;
}
let mut values = samples.iter().map(|(_, value)| *value).collect::<Vec<_>>();
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = values[values.len() / 2];
let mut deviations = values
.into_iter()
.map(|value| (value - median).abs())
.collect::<Vec<_>>();
deviations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
deviations[deviations.len() / 2]
}
#[cfg(not(coverage))]
fn request_capture_power_refresh(
power_tx: std::sync::mpsc::Sender<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 probe = match runtime {
Ok(runtime) => runtime.block_on(probe(&server_addr)),
Err(_) => HandshakeProbe::default(),
};
let _ = caps_tx.send(CapsMessage::Refresh(probe));
});
}
#[cfg(not(coverage))]
fn unavailable_capture_power(detail: String) -> CapturePowerStatus {
CapturePowerStatus {
available: false,
enabled: false,
unit: "relay.service".to_string(),
detail,
active_leases: 0,
mode: "auto".to_string(),
detected_devices: 0,
}
}
#[cfg(not(coverage))]
fn refresh_eye_feed_controls(
widgets: &super::ui_components::LauncherWidgets,
state: &LauncherState,
) {
for monitor_id in 0..2 {
super::ui_components::sync_feed_source_combo(
&widgets.display_panes[monitor_id].feed_source_combo,
state.feed_source_options(monitor_id),
state.feed_source_preset(monitor_id),
);
if state.feed_source_preset(monitor_id) != FeedSourcePreset::Off {
let choice = state
.display_capture_size_choice(monitor_id)
.unwrap_or_else(|| state.capture_size_choice(monitor_id));
if state.feed_source_preset(monitor_id) == FeedSourcePreset::ThisEye {
super::ui_components::sync_capture_resolution_combo(
&widgets.display_panes[monitor_id].capture_resolution_combo,
state.capture_size_options(),
state.capture_size_preset(monitor_id),
);
} else {
super::ui_components::sync_capture_resolution_locked(
&widgets.display_panes[monitor_id].capture_resolution_combo,
state.capture_size_options(),
choice.preset,
);
}
} else {
super::ui_components::sync_capture_resolution_disabled(
&widgets.display_panes[monitor_id].capture_resolution_combo,
);
}
super::ui_components::sync_breakout_size_combo(
&widgets.display_panes[monitor_id].breakout_combo,
state.breakout_size_options(monitor_id),
state.breakout_size_preset(monitor_id),
);
refresh_preview_frame_ratio(widgets, monitor_id, state);
}
}
#[cfg(not(coverage))]
fn refresh_preview_frame_ratio(
widgets: &super::ui_components::LauncherWidgets,
monitor_id: usize,
state: &LauncherState,
) {
let capture = state
.display_capture_size_choice(monitor_id)
.unwrap_or_else(|| state.capture_size_choice(monitor_id));
widgets.display_panes[monitor_id]
.preview_frame
.set_ratio(capture.preset.display_aspect_ratio());
}
#[cfg(not(coverage))]
fn eye_caps_changed(state: &LauncherState, caps: &crate::handshake::PeerCaps) -> bool {
let next_width = caps.eye_width.unwrap_or(state.preview_source.width);
let next_height = caps.eye_height.unwrap_or(state.preview_source.height);
let next_fps = caps.eye_fps.unwrap_or(state.preview_source.fps);
state.preview_source.width != next_width
|| state.preview_source.height != next_height
|| state.preview_source.fps != next_fps
}
#[cfg(not(coverage))]
fn record_diagnostics_sample(
widgets: &super::ui_components::LauncherWidgets,
state: &LauncherState,
preview: Option<&super::preview::LauncherPreview>,
network: NetworkSnapshot,
client_process_cpu_pct: f32,
) {
let left_metrics = preview
.and_then(|preview| {
(state.feed_source_preset(0) != FeedSourcePreset::Off).then_some(
preview.snapshot_metrics(
0,
match state.display_surface(0) {
DisplaySurface::Preview => super::preview::PreviewSurface::Inline,
DisplaySurface::Window => super::preview::PreviewSurface::Window,
},
),
)
})
.flatten()
.unwrap_or_default();
let right_metrics = preview
.and_then(|preview| {
(state.feed_source_preset(1) != FeedSourcePreset::Off).then_some(
preview.snapshot_metrics(
1,
match state.display_surface(1) {
DisplaySurface::Preview => super::preview::PreviewSurface::Inline,
DisplaySurface::Window => super::preview::PreviewSurface::Window,
},
),
)
})
.flatten()
.unwrap_or_default();
widgets
.diagnostics_log
.borrow_mut()
.record(PerformanceSample {
rtt_ms: network.rtt_ms,
probe_spread_ms: network.probe_spread_ms,
input_latency_ms: network.rtt_ms * 0.5,
probe_loss_pct: network.probe_loss_pct,
client_process_cpu_pct,
server_process_cpu_pct: left_metrics
.server_process_cpu_pct
.max(right_metrics.server_process_cpu_pct),
video_loss_pct: left_metrics
.packet_loss_pct
.max(right_metrics.packet_loss_pct),
left_receive_fps: left_metrics.receive_fps,
left_present_fps: left_metrics.present_fps,
left_server_fps: left_metrics.server_fps,
left_stream_spread_ms: left_metrics.stream_spread_ms,
left_packet_gap_peak_ms: left_metrics.packet_gap_peak_ms,
left_present_gap_peak_ms: left_metrics.present_gap_peak_ms,
left_queue_depth: left_metrics.queue_depth,
left_queue_peak: left_metrics.queue_depth_peak,
left_server_source_gap_peak_ms: left_metrics.server_source_gap_peak_ms,
left_server_send_gap_peak_ms: left_metrics.server_send_gap_peak_ms,
left_server_queue_peak: left_metrics.server_queue_peak,
left_server_encoder_label: left_metrics.server_encoder_label.clone(),
left_decoder_label: left_metrics.decoder_label.clone(),
left_stream_caps_label: left_metrics.stream_caps_label.clone(),
left_decoded_caps_label: left_metrics.decoded_caps_label.clone(),
left_rendered_caps_label: left_metrics.rendered_caps_label.clone(),
right_receive_fps: right_metrics.receive_fps,
right_present_fps: right_metrics.present_fps,
right_server_fps: right_metrics.server_fps,
right_stream_spread_ms: right_metrics.stream_spread_ms,
right_packet_gap_peak_ms: right_metrics.packet_gap_peak_ms,
right_present_gap_peak_ms: right_metrics.present_gap_peak_ms,
right_queue_depth: right_metrics.queue_depth,
right_queue_peak: right_metrics.queue_depth_peak,
right_server_source_gap_peak_ms: right_metrics.server_source_gap_peak_ms,
right_server_send_gap_peak_ms: right_metrics.server_send_gap_peak_ms,
right_server_queue_peak: right_metrics.server_queue_peak,
right_server_encoder_label: right_metrics.server_encoder_label.clone(),
right_decoder_label: right_metrics.decoder_label.clone(),
right_stream_caps_label: right_metrics.stream_caps_label.clone(),
right_decoded_caps_label: right_metrics.decoded_caps_label.clone(),
right_rendered_caps_label: right_metrics.rendered_caps_label.clone(),
dropped_frames: left_metrics
.dropped_frames
.saturating_add(right_metrics.dropped_frames),
queue_depth: left_metrics.queue_depth.max(right_metrics.queue_depth),
});
}
#[cfg(not(coverage))]
fn largest_monitor_size() -> (u32, u32) {
let (width, height) = enumerate_monitors()
.into_iter()
.max_by_key(|monitor| {
effective_monitor_width(&monitor) as u64 * effective_monitor_height(&monitor) as u64
})
.map(|monitor| {
(
effective_monitor_width(&monitor),
effective_monitor_height(&monitor),
)
})
.unwrap_or((1920, 1080));
(width.max(2), height.max(2))
}
#[cfg(not(coverage))]
fn largest_monitor_physical_size() -> (u32, u32) {
if let Some((width, height)) = probe_kscreen_display_size() {
return (width, height);
}
normalize_breakout_limit(largest_monitor_size().0, largest_monitor_size().1)
}
#[cfg(not(coverage))]
fn probe_kscreen_display_size() -> Option<(u32, u32)> {
let output = Command::new("kscreen-doctor").arg("-o").output().ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8(output.stdout).ok()?;
let mut best = None;
for line in text.lines() {
if !line.contains("Modes:") {
continue;
}
let active = line
.split_whitespace()
.find(|token| token.contains('*') && token.contains('x'))?;
let dims = active
.trim_matches(|ch: char| ch == '*' || ch == '!')
.split('@')
.next()?;
let (width, height) = dims.split_once('x')?;
let width = width.parse::<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 launcher_default_size(width: u32, height: u32) -> (i32, i32) {
let max_width = width.saturating_sub(48).max(640) as i32;
let max_height = height.saturating_sub(72).max(520) as i32;
(1380.min(max_width), 860.min(max_height))
}
#[cfg(not(coverage))]
fn rebind_inline_preview(
preview: &super::preview::LauncherPreview,
widgets: &super::ui_components::LauncherWidgets,
state: &LauncherState,
monitor_id: usize,
) {
if let Some(binding) = widgets.display_panes[monitor_id]
.preview_binding
.borrow_mut()
.take()
{
binding.close();
}
if state.feed_source_preset(monitor_id) == FeedSourcePreset::Off {
widgets.display_panes[monitor_id]
.picture
.set_paintable(Option::<&gtk::gdk::Paintable>::None);
widgets.display_panes[monitor_id]
.stream_status
.set_text("Feed disabled.");
return;
}
let binding = preview.install_on_picture(
monitor_id,
super::preview::PreviewSurface::Inline,
&widgets.display_panes[monitor_id].picture,
&widgets.display_panes[monitor_id].stream_status,
);
*widgets.display_panes[monitor_id]
.preview_binding
.borrow_mut() = binding;
}
#[cfg(not(coverage))]
fn rebind_popout_preview(
preview: &super::preview::LauncherPreview,
popouts: &Rc<RefCell<[Option<super::ui_components::PopoutWindowHandle>; 2]>>,
state: &LauncherState,
monitor_id: usize,
) {
let mut popouts = popouts.borrow_mut();
let Some(handle) = popouts.get_mut(monitor_id).and_then(|slot| slot.as_mut()) else {
return;
};
handle.binding.close();
if state.feed_source_preset(monitor_id) == FeedSourcePreset::Off {
handle
.picture
.set_paintable(Option::<&gtk::gdk::Paintable>::None);
handle.status_label.set_text("Feed disabled.");
return;
}
if let Some(binding) = preview.install_on_picture(
monitor_id,
super::preview::PreviewSurface::Window,
&handle.picture,
&handle.status_label,
) {
handle.binding = binding;
}
let capture = state
.display_capture_size_choice(monitor_id)
.unwrap_or_else(|| state.capture_size_choice(monitor_id));
handle
.frame
.set_ratio(capture.preset.display_aspect_ratio());
}
#[cfg(not(coverage))]
fn apply_preview_profiles(preview: &super::preview::LauncherPreview, state: &LauncherState) {
for monitor_id in 0..2 {
let enabled = state.feed_source_preset(monitor_id) != FeedSourcePreset::Off;
preview.set_monitor_enabled(monitor_id, enabled);
let capture = state
.display_capture_size_choice(monitor_id)
.unwrap_or_else(|| state.capture_size_choice(monitor_id));
let source_monitor_id = state
.resolved_feed_monitor_id(monitor_id)
.unwrap_or(monitor_id);
let breakout = state.breakout_size_choice(monitor_id);
preview.set_capture_profile(
monitor_id,
source_monitor_id,
capture.width,
capture.height,
capture.fps,
capture.max_bitrate_kbit,
);
preview.set_breakout_profile(monitor_id, breakout.width, breakout.height);
}
}
#[cfg(not(coverage))]
fn sync_preview_profiles(
preview: &super::preview::LauncherPreview,
widgets: &super::ui_components::LauncherWidgets,
popouts: &Rc<RefCell<[Option<super::ui_components::PopoutWindowHandle>; 2]>>,
state: &LauncherState,
) {
apply_preview_profiles(preview, state);
for monitor_id in 0..2 {
rebind_inline_preview(preview, widgets, state, monitor_id);
rebind_popout_preview(preview, popouts, state, monitor_id);
}
}
#[cfg(not(coverage))]
fn disconnected_capture_note(mode: &str) -> &'static str {
match mode {
"forced-on" => "Relay disconnected. Capture is still forced on for staging.",
"forced-off" => {
"Relay disconnected. Capture stays intentionally dark until you return to Auto or Force On."
}
_ => {
"Relay disconnected. The server will hold capture briefly, then let it return to standby."
}
}
}
/// Keeps remote eye previews tied to a live session while respecting forced-off staging.
fn session_preview_active(
state: &crate::launcher::state::LauncherState,
child_running: bool,
) -> bool {
(child_running || state.remote_active) && state.capture_power.mode != "forced-off"
}
#[cfg(not(coverage))]
pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let app = gtk::Application::builder()
.application_id("dev.lesavka.launcher")
.build();
let catalog = Rc::new(DeviceCatalog::discover());
let state = Rc::new(RefCell::new(LauncherState::new()));
state.borrow_mut().apply_catalog_defaults(&catalog);
let child_proc = Rc::new(RefCell::new(None::<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 input_toggle_control_path = Rc::new(input_toggle_control_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 _ = std::fs::remove_file(input_toggle_control_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 input_toggle_control_path = Rc::clone(&input_toggle_control_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 _ = std::fs::remove_file(input_toggle_control_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);
let input_toggle_control_path = Rc::clone(&input_toggle_control_path);
app.connect_activate(move |app| {
let (display_width, display_height) = largest_monitor_size();
let (physical_width, physical_height) = largest_monitor_physical_size();
{
let mut state = state.borrow_mut();
state.set_breakout_display_size(display_width, display_height);
state.set_breakout_limit_size(physical_width, physical_height);
}
let view = build_launcher_view(app, server_addr.as_ref(), &catalog, &state.borrow());
let window = view.window.clone();
let (launcher_width, launcher_height) = launcher_default_size(display_width, display_height);
window.set_default_size(launcher_width, launcher_height);
let server_entry = view.server_entry.clone();
let camera_combo = view.camera_combo.clone();
let microphone_combo = view.microphone_combo.clone();
let speaker_combo = view.speaker_combo.clone();
let keyboard_combo = view.keyboard_combo.clone();
let mouse_combo = view.mouse_combo.clone();
let widgets = view.widgets.clone();
let preview = view.preview.clone();
let popouts = Rc::clone(&view.popouts);
let diagnostics_popout = Rc::clone(&view.diagnostics_popout);
let log_popout = Rc::clone(&view.log_popout);
let shutdown_cleaned = Rc::new(Cell::new(false));
{
let shutdown_cleaned = Rc::clone(&shutdown_cleaned);
let app = app.clone();
let child_proc = Rc::clone(&child_proc);
let tests = Rc::clone(&tests);
let preview = preview.clone();
let widgets = widgets.clone();
let popouts = Rc::clone(&popouts);
let diagnostics_popout = Rc::clone(&diagnostics_popout);
let log_popout = Rc::clone(&log_popout);
window.connect_close_request(move |_| {
if !shutdown_cleaned.replace(true) {
shutdown_launcher_runtime(
&child_proc,
&tests,
preview.as_deref(),
&widgets,
&popouts,
&diagnostics_popout,
&log_popout,
);
}
let app = app.clone();
glib::idle_add_local_once(move || {
app.quit();
});
glib::Propagation::Stop
});
}
{
let shutdown_cleaned = Rc::clone(&shutdown_cleaned);
let child_proc = Rc::clone(&child_proc);
let tests = Rc::clone(&tests);
let preview = preview.clone();
let widgets = widgets.clone();
let popouts = Rc::clone(&popouts);
let diagnostics_popout = Rc::clone(&diagnostics_popout);
let log_popout = Rc::clone(&log_popout);
app.connect_shutdown(move |_| {
if !shutdown_cleaned.replace(true) {
shutdown_launcher_runtime(
&child_proc,
&tests,
preview.as_deref(),
&widgets,
&popouts,
&diagnostics_popout,
&log_popout,
);
}
});
}
{
let mut tests = tests.borrow_mut();
if let Err(err) = tests.bind_camera_preview(
&view.device_stage.camera_preview,
&view.device_stage.camera_status,
) {
widgets
.status_label
.set_text(&format!("Camera preview setup failed: {err}"));
}
if let Err(err) =
tests.set_camera_selection(state.borrow().devices.camera.as_deref())
{
widgets
.status_label
.set_text(&format!("Camera staging setup failed: {err}"));
}
}
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
let (power_tx, power_rx) = std::sync::mpsc::channel::<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 caps_request_in_flight = Rc::new(Cell::new(false));
let diagnostics_network = Rc::new(RefCell::new(NetworkTelemetry::default()));
let diagnostics_process = Rc::new(RefCell::new(ProcessCpuSampler::new()));
let next_power_probe =
Rc::new(Cell::new(Instant::now() + Duration::from_millis(500)));
let next_diagnostics_probe =
Rc::new(Cell::new(Instant::now() + Duration::from_millis(250)));
let next_diagnostics_sample =
Rc::new(Cell::new(Instant::now() + Duration::from_secs(1)));
let preview_session_active = Rc::new(Cell::new(false));
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());
sync_preview_profiles(preview, &widgets, &popouts, &state.borrow());
}
{
let state = Rc::clone(&state);
let widgets = widgets.clone();
let child_proc = Rc::clone(&child_proc);
let tests = Rc::clone(&tests);
let camera_combo = camera_combo.clone();
let camera_combo_read = camera_combo.clone();
camera_combo.connect_changed(move |_| {
let selected = selected_combo_value(&camera_combo_read);
let preview_was_running =
tests.borrow_mut().is_running(DeviceTestKind::Camera);
state.borrow_mut().select_camera(selected.clone());
if let Err(err) = tests.borrow_mut().set_camera_selection(selected.as_deref()) {
widgets
.status_label
.set_text(&format!("Camera preview update failed: {err}"));
} else if preview_was_running {
widgets.status_label.set_text(&format!(
"Local camera preview switched to {}.",
selected.as_deref().unwrap_or("auto")
));
}
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
});
}
{
let state = Rc::clone(&state);
let widgets = widgets.clone();
let child_proc = Rc::clone(&child_proc);
let keyboard_combo = keyboard_combo.clone();
let keyboard_combo_read = keyboard_combo.clone();
keyboard_combo.connect_changed(move |_| {
let selected = selected_combo_value(&keyboard_combo_read);
state.borrow_mut().select_keyboard(selected.clone());
let message = match selected.as_deref() {
Some(path) => {
format!("The next relay launch will listen only to keyboard {path}.")
}
None => "The next relay launch will listen to all keyboards.".to_string(),
};
widgets.status_label.set_text(&message);
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
});
}
{
let state = Rc::clone(&state);
let widgets = widgets.clone();
let child_proc = Rc::clone(&child_proc);
let mouse_combo = mouse_combo.clone();
let mouse_combo_read = mouse_combo.clone();
mouse_combo.connect_changed(move |_| {
let selected = selected_combo_value(&mouse_combo_read);
state.borrow_mut().select_mouse(selected.clone());
let message = match selected.as_deref() {
Some(path) => {
format!("The next relay launch will listen only to pointer {path}.")
}
None => {
"The next relay launch will listen to all pointer devices."
.to_string()
}
};
widgets.status_label.set_text(&message);
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
});
}
if let Some(preview) = preview.as_ref() {
preview.set_session_active(false);
}
request_capture_power_refresh(
power_tx.clone(),
selected_server_addr(&server_entry, server_addr.as_ref()),
Duration::ZERO,
);
caps_request_in_flight.set(true);
request_handshake_caps(
caps_tx.clone(),
selected_server_addr(&server_entry, server_addr.as_ref()),
Duration::ZERO,
);
{
let state = Rc::clone(&state);
let child_proc = Rc::clone(&child_proc);
let widgets = widgets.clone();
let server_entry = server_entry.clone();
let server_entry_read = server_entry.clone();
let server_addr_fallback = Rc::clone(&server_addr);
let preview = preview.clone();
let power_tx = power_tx.clone();
let caps_tx = caps_tx.clone();
let caps_request_in_flight = Rc::clone(&caps_request_in_flight);
server_entry.connect_changed(move |_| {
let server_addr =
selected_server_addr(&server_entry_read, server_addr_fallback.as_ref());
{
let mut state = state.borrow_mut();
state.set_server_available(false);
state.set_server_version(None);
}
if let Some(preview) = preview.as_ref() {
preview.set_server_addr(server_addr.clone());
}
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
request_capture_power_refresh(
power_tx.clone(),
server_addr.clone(),
Duration::from_millis(150),
);
caps_request_in_flight.set(true);
request_handshake_caps(caps_tx.clone(), server_addr, Duration::from_millis(150));
});
}
for monitor_id in 0..2 {
let state = Rc::clone(&state);
let widgets = widgets.clone();
let popouts = Rc::clone(&popouts);
let child_proc = Rc::clone(&child_proc);
let preview = preview.clone();
let feed_source_combo = widgets.display_panes[monitor_id].feed_source_combo.clone();
feed_source_combo.connect_changed(move |combo| {
let Some(active_id) = combo.active_id() else {
return;
};
let Some(preset) = FeedSourcePreset::from_id(active_id.as_str()) else {
return;
};
if state.borrow().feed_source_preset(monitor_id) == preset {
return;
}
{
let mut state = state.borrow_mut();
state.set_feed_source_preset(monitor_id, preset);
}
if let Some(preview) = preview.as_ref() {
sync_preview_profiles(preview, &widgets, &popouts, &state.borrow());
}
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
});
}
for monitor_id in 0..2 {
let state = Rc::clone(&state);
let widgets = widgets.clone();
let popouts = Rc::clone(&popouts);
let child_proc = Rc::clone(&child_proc);
let preview = preview.clone();
let resolution_combo =
widgets.display_panes[monitor_id].capture_resolution_combo.clone();
resolution_combo.connect_changed(move |combo| {
let Some(active_id) = combo.active_id() else {
return;
};
let Some(preset) = CaptureSizePreset::from_id(active_id.as_str()) else {
return;
};
if state.borrow().feed_source_preset(monitor_id) != FeedSourcePreset::ThisEye {
return;
}
if state.borrow().capture_size_preset(monitor_id) == preset {
return;
}
{
let mut state = state.borrow_mut();
state.set_capture_size_preset(monitor_id, preset);
}
if let Some(preview) = preview.as_ref() {
let choice = state
.borrow()
.display_capture_size_choice(monitor_id)
.unwrap_or_else(|| state.borrow().capture_size_choice(monitor_id));
let source_monitor_id = state
.borrow()
.resolved_feed_monitor_id(monitor_id)
.unwrap_or(monitor_id);
preview.set_capture_profile(
monitor_id,
source_monitor_id,
choice.width,
choice.height,
choice.fps,
choice.max_bitrate_kbit,
);
sync_preview_profiles(preview, &widgets, &popouts, &state.borrow());
}
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
});
}
for monitor_id in 0..2 {
let state = Rc::clone(&state);
let widgets = widgets.clone();
let popouts = Rc::clone(&popouts);
let child_proc = Rc::clone(&child_proc);
let preview = preview.clone();
let breakout_combo = widgets.display_panes[monitor_id].breakout_combo.clone();
breakout_combo.connect_changed(move |combo| {
let Some(active_id) = combo.active_id() else {
return;
};
let Some(preset) = BreakoutSizePreset::from_id(active_id.as_str()) else {
return;
};
if state.borrow().breakout_size_preset(monitor_id) == preset {
return;
}
{
let mut state = state.borrow_mut();
state.set_breakout_size_preset(monitor_id, preset);
}
let size = state.borrow().breakout_size_choice(monitor_id);
if let Some(preview) = preview.as_ref() {
preview.set_breakout_profile(monitor_id, size.width, size.height);
}
let popout_open = {
popouts
.borrow()
.get(monitor_id)
.and_then(|slot| slot.as_ref())
.is_some()
};
if popout_open {
if let Some(preview) = preview.as_ref() {
rebind_popout_preview(preview, &popouts, &state.borrow(), monitor_id);
}
if let Some(handle) = popouts
.borrow()
.get(monitor_id)
.and_then(|slot| slot.as_ref())
{
let display_limit = state.borrow().breakout_display_size();
apply_popout_window_size(handle, size, display_limit);
}
}
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
});
}
{
let state = Rc::clone(&state);
let widgets = widgets.clone();
let child_proc = Rc::clone(&child_proc);
let tests = Rc::clone(&tests);
let microphone_combo = microphone_combo.clone();
let microphone_combo_read = microphone_combo.clone();
microphone_combo.connect_changed(move |_| {
state
.borrow_mut()
.select_microphone(selected_combo_value(&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 widgets = widgets.clone();
let child_proc = Rc::clone(&child_proc);
let audio_gain_scale = widgets.audio_gain_scale.clone();
audio_gain_scale.connect_value_changed(move |scale| {
let percent = scale
.value()
.round()
.clamp(0.0, MAX_AUDIO_GAIN_PERCENT as f64)
as u32;
let label = {
let mut state = state.borrow_mut();
if state.audio_gain_percent == percent {
return;
}
state.set_audio_gain_percent(percent);
state.audio_gain_label()
};
widgets.audio_gain_value.set_text(&label);
if child_proc.borrow().is_some() {
let path = audio_gain_control_path();
match write_audio_gain_request(&path, percent) {
Ok(()) => widgets
.status_label
.set_text(&format!("Remote audio gain set to {label}.")),
Err(err) => widgets.status_label.set_text(&format!(
"Remote audio gain set to {label} for the next relay launch, but live gain control could not be written: {err}"
)),
}
} else {
widgets.status_label.set_text(&format!(
"Remote audio gain set to {label} for the next relay launch."
));
}
});
}
{
let state = Rc::clone(&state);
let widgets = widgets.clone();
let widgets_handle = widgets.clone();
let child_proc = Rc::clone(&child_proc);
let tests = Rc::clone(&tests);
let camera_combo = camera_combo.clone();
let microphone_combo = microphone_combo.clone();
let speaker_combo = speaker_combo.clone();
let keyboard_combo = keyboard_combo.clone();
let mouse_combo = mouse_combo.clone();
widgets.device_refresh_button.connect_clicked(move |_| {
let catalog = DeviceCatalog::discover();
let (
selected_camera,
selected_microphone,
selected_speaker,
selected_keyboard,
selected_mouse,
) = {
let state = state.borrow();
(
retained_stage_selection(
state.devices.camera.as_deref(),
&catalog.cameras,
),
retained_stage_selection(
state.devices.microphone.as_deref(),
&catalog.microphones,
),
retained_stage_selection(
state.devices.speaker.as_deref(),
&catalog.speakers,
),
retained_input_selection(
state.devices.keyboard.as_deref(),
&catalog.keyboards,
),
retained_input_selection(state.devices.mouse.as_deref(), &catalog.mice),
)
};
{
let mut state = state.borrow_mut();
state.select_camera(selected_camera);
state.select_microphone(selected_microphone);
state.select_speaker(selected_speaker);
state.select_keyboard(selected_keyboard);
state.select_mouse(selected_mouse);
}
let state_snapshot = state.borrow().clone();
sync_stage_device_combo(
&camera_combo,
&catalog.cameras,
state_snapshot.devices.camera.as_deref(),
);
sync_stage_device_combo(
&microphone_combo,
&catalog.microphones,
state_snapshot.devices.microphone.as_deref(),
);
sync_stage_device_combo(
&speaker_combo,
&catalog.speakers,
state_snapshot.devices.speaker.as_deref(),
);
sync_input_device_combo(
&keyboard_combo,
&catalog.keyboards,
state_snapshot.devices.keyboard.as_deref(),
"all keyboards",
);
sync_input_device_combo(
&mouse_combo,
&catalog.mice,
state_snapshot.devices.mouse.as_deref(),
"all mice",
);
if let Err(err) = tests
.borrow_mut()
.set_camera_selection(state_snapshot.devices.camera.as_deref())
{
widgets_handle
.status_label
.set_text(&format!("Device refresh succeeded, but the webcam test could not switch cleanly: {err}"));
} else {
let message = if usb_audio_kernel_support_missing() {
"Device staging refreshed. USB audio devices may still stay invisible until the host boots a kernel with snd_usb_audio available; reconnect the relay if you want the live session to use a new webcam, mic, or speaker."
} else {
"Device staging refreshed. Newly attached devices are ready for local tests; all-keyboard/all-mouse relay sessions will pick up new input devices automatically."
};
widgets_handle.status_label.set_text(message);
}
refresh_launcher_ui(
&widgets_handle,
&state.borrow(),
child_proc.borrow().is_some(),
);
refresh_test_buttons(&widgets_handle, &mut tests.borrow_mut());
});
}
{
let state = Rc::clone(&state);
let child_proc = Rc::clone(&child_proc);
let widgets = widgets.clone();
let server_entry = server_entry.clone();
let camera_combo = camera_combo.clone();
let microphone_combo = microphone_combo.clone();
let speaker_combo = speaker_combo.clone();
let input_control_path = Rc::clone(&input_control_path);
let input_state_path = Rc::clone(&input_state_path);
let input_toggle_control_path = Rc::clone(&input_toggle_control_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 _ = std::fs::remove_file(input_toggle_control_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();
let input_toggle_control_path = input_toggle_control_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(),
input_toggle_control_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();
let server_entry = server_entry.clone();
let server_addr_fallback = Rc::clone(&server_addr);
let widgets_for_click = widgets.clone();
widgets.usb_recover_button.connect_clicked(move |_| {
let server_addr =
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
widgets_for_click.status_label.set_text(
"Requesting a forced USB gadget re-enumeration on the relay host...",
);
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let result =
reset_usb_gadget(&server_addr).map_err(|err| format!("{err:#}"));
let _ = tx.send(result);
});
let widgets = widgets_for_click.clone();
glib::timeout_add_local(Duration::from_millis(100), move || {
match rx.try_recv() {
Ok(Ok(())) => {
widgets.status_label.set_text(
"USB gadget recovery requested. Give the host a few seconds to re-enumerate keyboard, mouse, webcam, and audio.",
);
glib::ControlFlow::Break
}
Ok(Err(err)) => {
widgets
.status_label
.set_text(&format!("USB gadget recovery failed: {err}"));
glib::ControlFlow::Break
}
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
widgets.status_label.set_text(
"USB gadget recovery ended unexpectedly before the relay answered.",
);
glib::ControlFlow::Break
}
}
});
});
}
{
let widgets = widgets.clone();
widgets.diagnostics_copy_button.connect_clicked(move |_| {
if let Err(err) = copy_plain_text(&widgets.diagnostics_rendered_text.borrow()) {
widgets
.status_label
.set_text(&format!("Could not copy the diagnostics report: {err}"));
} else {
widgets
.status_label
.set_text("Diagnostics report copied to the local clipboard.");
}
});
}
{
let app = app.clone();
let widgets = widgets.clone();
let diagnostics_popout = Rc::clone(&diagnostics_popout);
widgets.diagnostics_popout_button.connect_clicked(move |_| {
open_diagnostics_popout(
&app,
&diagnostics_popout,
&widgets.diagnostics_popout_label,
&widgets.diagnostics_popout_scroll,
&widgets.diagnostics_rendered_text,
);
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 input_toggle_control_path = Rc::clone(&input_toggle_control_path);
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 {
match write_input_toggle_key_request(
input_toggle_control_path.as_path(),
&swap_key,
) {
Ok(()) => format!(
"Swap key set to {} and applied to the live relay.",
toggle_key_label(&swap_key)
),
Err(err) => format!(
"Swap key set to {}, but Lesavka could not push it live: {err}",
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 power_request_in_flight = Rc::clone(&power_request_in_flight);
let relay_request_in_flight = Rc::clone(&relay_request_in_flight);
let preview = preview.clone();
let power_tx = power_tx.clone();
let caps_tx = caps_tx.clone();
let caps_request_in_flight = Rc::clone(&caps_request_in_flight);
let diagnostics_network = Rc::clone(&diagnostics_network);
let diagnostics_process = Rc::clone(&diagnostics_process);
let next_diagnostics_probe = Rc::clone(&next_diagnostics_probe);
let next_diagnostics_sample = Rc::clone(&next_diagnostics_sample);
let preview_session_active = Rc::clone(&preview_session_active);
let log_tx = log_tx.clone();
glib::timeout_add_local(Duration::from_millis(180), move || {
let child_running = reap_exited_child(&child_proc);
if let Some(preview) = preview.as_ref() {
let desired_preview_active = {
let state_snapshot = state.borrow();
session_preview_active(&state_snapshot, child_running)
};
if preview_session_active.get() != desired_preview_active {
preview.set_session_active(desired_preview_active);
preview_session_active.set(desired_preview_active);
}
}
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),
);
}
if child_running
&& let Some(routing) = read_input_routing_state(input_state_path.as_path())
&& routing != state.borrow().routing
{
state.borrow_mut().set_routing(routing);
refresh_launcher_ui(&widgets, &state.borrow(), child_running);
if matches!(routing, InputRouting::Remote) {
present_popout_windows(&popouts);
} else {
window.present();
}
}
let next_focus_marker = path_marker(focus_signal_path.as_path());
let mut last_focus = last_focus_marker.borrow_mut();
if next_focus_marker > *last_focus {
*last_focus = next_focus_marker;
state.borrow_mut().set_routing(InputRouting::Local);
refresh_launcher_ui(&widgets, &state.borrow(), child_running);
widgets
.status_label
.set_text("Local control restored and the launcher is focused.");
window.present();
}
while let Ok(message) = relay_rx.try_recv() {
relay_request_in_flight.set(false);
match message {
RelayMessage::Spawned(Ok(mut child)) => {
attach_child_log_streams(&mut child, log_tx.clone());
*child_proc.borrow_mut() = Some(child);
{
let mut state = state.borrow_mut();
state.set_server_available(true);
let _ = state.start_remote();
}
let server_addr =
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
if let Some(preview) = preview.as_ref() {
preview.set_server_addr(server_addr.clone());
preview.set_session_active(session_preview_active(
&state.borrow(),
child_proc.borrow().is_some(),
));
}
let routing = routing_name(state.borrow().routing);
let power_mode = state.borrow().capture_power.mode.clone();
let message = match power_mode.as_str() {
"forced-off" => format!(
"Relay connected with inputs routed to {}, but capture is forced off. Return capture to Auto or Force On when you want remote video.",
routing
),
"forced-on" => format!(
"Relay connected with inputs routed to {}. Capture is being held awake and the eye previews are coming online.",
routing
),
_ => format!(
"Relay connected with inputs routed to {}. The eye previews will come up with the live session.",
routing
),
};
widgets.status_label.set_text(&message);
if matches!(state.borrow().routing, InputRouting::Remote) {
present_popout_windows(&popouts);
}
request_capture_power_refresh(
power_tx.clone(),
server_addr.clone(),
Duration::from_millis(250),
);
request_capture_power_refresh(
power_tx.clone(),
server_addr,
Duration::from_millis(1250),
);
}
RelayMessage::Spawned(Err(err)) => {
state.borrow_mut().set_server_available(false);
if let Some(preview) = preview.as_ref() {
preview.set_session_active(false);
}
widgets
.status_label
.set_text(&format!("Relay start failed: {err}"));
}
}
}
while let Ok(line) = log_rx.try_recv() {
append_session_log(&widgets.session_log_buffer, &line);
let mut end = widgets.session_log_buffer.end_iter();
widgets
.session_log_view
.scroll_to_iter(&mut end, 0.0, false, 0.0, 1.0);
}
while let Ok(message) = power_rx.try_recv() {
power_request_in_flight.set(false);
match message {
PowerMessage::Refresh(Ok(power)) => {
{
let mut state = state.borrow_mut();
state.set_server_available(true);
state.set_capture_power(power);
}
if let Some(preview) = preview.as_ref() {
let preview_active = {
let state = state.borrow();
session_preview_active(
&state,
child_proc.borrow().is_some(),
)
};
preview.set_session_active(preview_active);
}
}
PowerMessage::Refresh(Err(err)) => {
{
let mut state = state.borrow_mut();
state.set_server_available(false);
state.set_capture_power(unavailable_capture_power(err));
}
if let Some(preview) = preview.as_ref() {
let preview_active = {
let state = state.borrow();
session_preview_active(
&state,
child_proc.borrow().is_some(),
)
};
preview.set_session_active(preview_active);
}
}
PowerMessage::Command(Ok(power)) => {
let mode = power.mode.clone();
{
let mut state = state.borrow_mut();
state.set_server_available(true);
state.set_capture_power(power);
}
if let Some(preview) = preview.as_ref() {
let preview_active = {
let state = state.borrow();
session_preview_active(
&state,
child_proc.borrow().is_some(),
)
};
preview.set_session_active(preview_active);
}
widgets.status_label.set_text(match mode.as_str() {
"forced-on" => "Capture feeds forced on. Remote eyes stay awake even if previews or the relay stop.",
"forced-off" => "Capture feeds forced off. Remote eye previews and session video stay dark until you switch back.",
_ => "Capture feeds returned to automatic mode. Live previews and relay demand will wake them as needed.",
});
}
PowerMessage::Command(Err(err)) => {
let mut state = state.borrow_mut();
state.set_server_available(false);
state.set_capture_power(unavailable_capture_power(err.clone()));
widgets
.status_label
.set_text(&format!("Capture power update failed: {err}"));
}
}
}
while let Ok(message) = caps_rx.try_recv() {
caps_request_in_flight.set(false);
match message {
CapsMessage::Refresh(probe_result) => {
diagnostics_network.borrow_mut().record(&probe_result);
let caps = probe_result.caps;
let rebind_preview = eye_caps_changed(&state.borrow(), &caps);
{
let mut state = state.borrow_mut();
if probe_result.reachable {
state.set_server_available(true);
} else if child_proc.borrow().is_none() {
state.set_server_available(false);
}
state.set_server_version(caps.server_version.clone());
}
if let (Some(width), Some(height)) =
(caps.eye_width, caps.eye_height)
{
let fps = caps
.eye_fps
.unwrap_or(crate::launcher::state::PreviewSourceSize::default().fps);
{
let mut state = state.borrow_mut();
state.set_preview_source_profile(width, height, fps);
}
if rebind_preview && let Some(preview) = preview.as_ref() {
sync_preview_profiles(preview, &widgets, &popouts, &state.borrow());
}
refresh_eye_feed_controls(&widgets, &state.borrow());
} else {
refresh_eye_feed_controls(&widgets, &state.borrow());
}
}
}
}
while let Ok(message) = clipboard_rx.try_recv() {
match message {
ClipboardMessage::Finished(Ok(detail)) => {
widgets.status_label.set_text(&format!("{detail}"));
}
ClipboardMessage::Finished(Err(err)) => {
widgets
.status_label
.set_text(&format!("Clipboard send failed: {err}"));
}
}
}
let now = Instant::now();
let child_running = child_proc.borrow().is_some();
if now >= next_power_probe.get()
&& !power_request_in_flight.get()
&& (child_running
|| state.borrow().capture_power.enabled
|| state.borrow().remote_active)
{
power_request_in_flight.set(true);
let server_addr =
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
request_capture_power_refresh(power_tx.clone(), server_addr, Duration::ZERO);
next_power_probe.set(now + Duration::from_secs(2));
}
if now >= next_diagnostics_probe.get() && !caps_request_in_flight.get() {
caps_request_in_flight.set(true);
let server_addr =
selected_server_addr(&server_entry, server_addr_fallback.as_ref());
request_handshake_caps(caps_tx.clone(), server_addr, Duration::ZERO);
next_diagnostics_probe.set(now + Duration::from_secs(2));
}
if now >= next_diagnostics_sample.get() {
let network = diagnostics_network.borrow_mut().snapshot();
let client_process_cpu_pct = diagnostics_process
.borrow_mut()
.sample_percent()
.unwrap_or(0.0);
record_diagnostics_sample(
&widgets,
&state.borrow(),
preview.as_ref().map(|preview| preview.as_ref()),
network,
client_process_cpu_pct,
);
next_diagnostics_sample.set(now + Duration::from_secs(1));
}
refresh_launcher_ui(&widgets, &state.borrow(), child_running);
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
glib::ControlFlow::Continue
});
}
window.present();
});
}
let _ = app.run_with_args::<&str>(&[]);
Ok(())
}
#[cfg(coverage)]
pub fn run_gui_launcher(_server_addr: String) -> Result<()> {
Ok(())
}
#[cfg(all(test, not(coverage)))]
mod tests {
use super::apply_preview_profiles;
use crate::launcher::preview::{LauncherPreview, PreviewSurface};
use crate::launcher::state::{CaptureSizePreset, FeedSourcePreset, LauncherState};
#[test]
fn fresh_preview_bootstrap_is_overridden_by_launcher_state_profiles() {
let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap();
let state = LauncherState::default();
let bootstrap = preview.profile_for_test(1, PreviewSurface::Inline).unwrap();
assert_eq!(bootstrap.0, 0);
assert_eq!(bootstrap.3, 1920);
assert_eq!(bootstrap.4, 1080);
assert_eq!(bootstrap.5, 60);
assert_eq!(bootstrap.6, 18_000);
apply_preview_profiles(&preview, &state);
let inline = preview.profile_for_test(1, PreviewSurface::Inline).unwrap();
assert_eq!(inline.0, 1);
assert_eq!(inline.3, 1920);
assert_eq!(inline.4, 1080);
assert_eq!(inline.5, 60);
assert_eq!(inline.6, 18_000);
let window = preview.profile_for_test(1, PreviewSurface::Window).unwrap();
assert_eq!(window.0, 1);
assert_eq!(window.3, 1920);
assert_eq!(window.4, 1080);
assert_eq!(window.5, 60);
assert_eq!(window.6, 18_000);
preview.shutdown_all();
}
#[test]
fn source_preview_profile_stays_honest_after_apply() {
let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap();
let mut state = LauncherState::default();
state.set_capture_size_preset(1, CaptureSizePreset::P1080);
apply_preview_profiles(&preview, &state);
let inline = preview.profile_for_test(1, PreviewSurface::Inline).unwrap();
assert_eq!(inline.0, 1);
assert_eq!(inline.3, 1920);
assert_eq!(inline.4, 1080);
assert_eq!(inline.5, 60);
assert_eq!(inline.6, 18_000);
preview.shutdown_all();
}
#[test]
fn mirrored_preview_profile_keeps_its_own_feed_id_but_uses_the_other_source() {
let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap();
let mut state = LauncherState::default();
state.set_feed_source_preset(0, FeedSourcePreset::OtherEye);
apply_preview_profiles(&preview, &state);
let inline = preview.profile_for_test(0, PreviewSurface::Inline).unwrap();
let window = preview.profile_for_test(0, PreviewSurface::Window).unwrap();
assert_eq!(inline.0, 1);
assert_eq!(window.0, 1);
preview.shutdown_all();
}
#[test]
fn mirrored_preview_profile_inherits_the_source_eye_mode() {
let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap();
let mut state = LauncherState::default();
state.set_feed_source_preset(0, FeedSourcePreset::OtherEye);
state.set_capture_size_preset(1, CaptureSizePreset::P720);
apply_preview_profiles(&preview, &state);
let inline = preview.profile_for_test(0, PreviewSurface::Inline).unwrap();
let window = preview.profile_for_test(0, PreviewSurface::Window).unwrap();
assert_eq!(inline.0, 1);
assert_eq!(window.0, 1);
assert_eq!(inline.3, 1280);
assert_eq!(inline.4, 720);
assert_eq!(inline.5, 60);
assert_eq!(inline.6, 12_000);
assert_eq!(window.3, 1280);
assert_eq!(window.4, 720);
assert_eq!(window.5, 60);
assert_eq!(window.6, 12_000);
preview.shutdown_all();
}
#[test]
fn off_preview_profile_disables_both_surfaces_instead_of_leaving_idle_feeds_running() {
let preview = LauncherPreview::new("http://127.0.0.1:1".to_string()).unwrap();
let mut state = LauncherState::default();
state.set_feed_source_preset(0, FeedSourcePreset::Off);
apply_preview_profiles(&preview, &state);
assert_eq!(
preview.feed_disabled_for_test(0, PreviewSurface::Inline),
Some(true)
);
assert_eq!(
preview.feed_disabled_for_test(0, PreviewSurface::Window),
Some(true)
);
assert_eq!(
preview.feed_disabled_for_test(1, PreviewSurface::Inline),
Some(false)
);
preview.shutdown_all();
}
}
#[cfg(all(test, coverage))]
mod tests {
use super::{run_gui_launcher, session_preview_active};
use crate::launcher::state::{CapturePowerStatus, LauncherState};
#[test]
fn coverage_stub_returns_ok() {
assert!(run_gui_launcher("http://127.0.0.1:50051".to_string()).is_ok());
}
#[test]
fn session_preview_stays_idle_when_capture_is_forced_off() {
let mut state = LauncherState::new();
state.start_remote();
state.set_capture_power(CapturePowerStatus {
available: true,
enabled: false,
unit: "relay.service".to_string(),
detail: "inactive/dead".to_string(),
active_leases: 1,
mode: "forced-off".to_string(),
detected_devices: 0,
});
assert!(!session_preview_active(&state, true));
}
}