use anyhow::Result; use gtk::{gdk, glib, prelude::*}; use std::{ cell::RefCell, io::{BufRead, BufReader}, path::{Path, PathBuf}, process::{Child, Command, Stdio}, rc::Rc, sync::mpsc::Sender, time::{SystemTime, UNIX_EPOCH}, }; use super::{ LAUNCHER_CLIPBOARD_CONTROL_ENV, LAUNCHER_FOCUS_SIGNAL_ENV, device_test::{DeviceTestController, DeviceTestKind}, diagnostics::{SnapshotReport, quality_probe_command}, launcher_clipboard_control_path, launcher_focus_signal_path, preview::{LauncherPreview, PreviewSurface}, runtime_env_vars, state::{BreakoutSizeChoice, CapturePowerStatus, DisplaySurface, InputRouting, LauncherState}, ui_components::{ ConsoleLogLevel, DisplayPaneWidgets, LauncherWidgets, PopoutWindowHandle, set_rail_button_label, }, }; pub const INPUT_CONTROL_ENV: &str = "LESAVKA_LAUNCHER_INPUT_CONTROL"; pub const INPUT_STATE_ENV: &str = "LESAVKA_LAUNCHER_INPUT_STATE"; pub const TOGGLE_KEY_CONTROL_ENV: &str = "LESAVKA_LAUNCHER_TOGGLE_KEY_CONTROL"; pub const AUDIO_GAIN_CONTROL_ENV: &str = "LESAVKA_AUDIO_GAIN_CONTROL"; pub const MIC_GAIN_CONTROL_ENV: &str = "LESAVKA_MIC_GAIN_CONTROL"; pub const UPLINK_CAMERA_PREVIEW_ENV: &str = "LESAVKA_UPLINK_CAMERA_PREVIEW"; pub const UPLINK_MIC_LEVEL_ENV: &str = "LESAVKA_UPLINK_MIC_LEVEL"; pub use crate::uplink_telemetry::{DEFAULT_UPLINK_TELEMETRY_PATH, UPLINK_TELEMETRY_ENV}; pub const DEFAULT_INPUT_CONTROL_PATH: &str = "/tmp/lesavka-launcher-input.control"; pub const DEFAULT_INPUT_STATE_PATH: &str = "/tmp/lesavka-launcher-input.state"; pub const DEFAULT_TOGGLE_KEY_CONTROL_PATH: &str = "/tmp/lesavka-launcher-toggle-key.control"; pub const DEFAULT_AUDIO_GAIN_CONTROL_PATH: &str = "/tmp/lesavka-audio-gain.control"; pub const DEFAULT_MIC_GAIN_CONTROL_PATH: &str = "/tmp/lesavka-mic-gain.control"; pub const DEFAULT_UPLINK_CAMERA_PREVIEW_PATH: &str = "/tmp/lesavka-uplink-camera-preview.rgba"; pub const DEFAULT_UPLINK_MIC_LEVEL_PATH: &str = "/tmp/lesavka-uplink-mic-level.value"; pub type RelayChild = Child; pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, child_running: bool) { let relay_live = child_running || state.remote_active; let server_label = server_version_label(state); set_status_light( &widgets.summary.relay_light, server_light_state(state, relay_live), ); widgets.summary.relay_value.set_text(&server_label); widgets .summary .relay_value .set_tooltip_text(Some(&server_label)); set_status_light( &widgets.summary.routing_light, StatusLightState::from_active(matches!(state.routing, InputRouting::Remote)), ); widgets .summary .routing_value .set_text(&capitalize(routing_name(state.routing))); let gpio_label = if state.server_available { gpio_power_label(&state.capture_power) } else { "Offline".to_string() }; set_status_light( &widgets.summary.gpio_light, if state.server_available { gpio_light_state(&state.capture_power) } else { StatusLightState::Idle }, ); widgets.summary.gpio_value.set_text(&gpio_label); widgets.summary.gpio_value.set_tooltip_text(Some(&gpio_label)); widgets .summary .shortcut_value .set_text(&toggle_key_label(&state.swap_key)); let (usb_state, usb_value) = recovery_usb_health(state); set_status_light(&widgets.summary.usb_light, usb_state); widgets.summary.usb_value.set_text(&usb_value); widgets.summary.usb_value.set_tooltip_text(Some(&usb_value)); let (uac_state, uac_value) = recovery_uac_health(state); set_status_light(&widgets.summary.uac_light, uac_state); widgets.summary.uac_value.set_text(&uac_value); widgets.summary.uac_value.set_tooltip_text(Some(&uac_value)); let (uvc_state, uvc_value) = recovery_uvc_health(state); set_status_light(&widgets.summary.uvc_light, uvc_state); widgets.summary.uvc_value.set_text(&uvc_value); widgets.summary.uvc_value.set_tooltip_text(Some(&uvc_value)); let power_detail = if state.server_available { capture_power_detail(&state.capture_power) } else { "relay host is offline; GPIO power state is unavailable".to_string() }; widgets.power_detail.set_text(&power_detail); if (widgets.audio_gain_scale.value() - state.audio_gain_percent as f64).abs() > f64::EPSILON { widgets .audio_gain_scale .set_value(state.audio_gain_percent as f64); } widgets.audio_gain_value.set_text(&state.audio_gain_label()); if (widgets.mic_gain_scale.value() - state.mic_gain_percent as f64).abs() > f64::EPSILON { widgets .mic_gain_scale .set_value(state.mic_gain_percent as f64); } widgets.mic_gain_value.set_text(&state.mic_gain_label()); if widgets.camera_channel_toggle.is_active() != state.channels.camera { widgets .camera_channel_toggle .set_active(state.channels.camera); } if widgets.microphone_channel_toggle.is_active() != state.channels.microphone { widgets .microphone_channel_toggle .set_active(state.channels.microphone); } if widgets.audio_channel_toggle.is_active() != state.channels.audio { widgets .audio_channel_toggle .set_active(state.channels.audio); } set_rail_button_label( &widgets.start_button, if relay_live { "Disconnect" } else { "Connect" }, ); if relay_live { widgets.start_button.remove_css_class("suggested-action"); widgets.start_button.add_css_class("destructive-action"); } else { widgets.start_button.remove_css_class("destructive-action"); widgets.start_button.add_css_class("suggested-action"); } widgets.start_button.set_sensitive(true); widgets.server_entry.set_sensitive(!relay_live); widgets.start_button.set_tooltip_text(Some(if relay_live { "Disconnect the relay session." } else { "Start relay and previews." })); widgets.clipboard_button.set_sensitive(relay_live); widgets .usb_recover_button .set_sensitive(state.server_available); widgets .uac_recover_button .set_sensitive(state.server_available); widgets .uvc_recover_button .set_sensitive(state.server_available); widgets.device_refresh_button.set_sensitive(!relay_live); widgets .camera_combo .set_sensitive(!relay_live && state.channels.camera); widgets.camera_quality_combo.set_sensitive( !relay_live && state.channels.camera && state.devices.camera.is_some() && state.camera_quality.is_some(), ); widgets .microphone_combo .set_sensitive(!relay_live && state.channels.microphone); widgets .speaker_combo .set_sensitive(!relay_live && state.channels.audio); widgets .audio_gain_scale .set_sensitive(!relay_live && state.channels.audio); widgets.keyboard_combo.set_sensitive(!relay_live); widgets.mouse_combo.set_sensitive(!relay_live); widgets.camera_channel_toggle.set_sensitive(!relay_live); widgets.microphone_channel_toggle.set_sensitive(!relay_live); widgets.audio_channel_toggle.set_sensitive(!relay_live); widgets .camera_test_button .set_sensitive(!relay_live && state.channels.camera); widgets .microphone_test_button .set_sensitive(!relay_live && state.channels.microphone); widgets .mic_gain_scale .set_sensitive(!relay_live && state.channels.microphone); widgets .speaker_test_button .set_sensitive(!relay_live && state.channels.audio); widgets.webcam_transport_combo.set_sensitive(false); widgets.input_toggle_button.set_label(match state.routing { InputRouting::Remote => "Route Local", InputRouting::Local => "Route Remote", }); widgets .input_toggle_button .set_tooltip_text(Some(match state.routing { InputRouting::Remote => "Route inputs back local.", InputRouting::Local => "Route inputs to remote.", })); widgets.swap_key_button.set_label("Set Swap Key"); widgets .swap_key_button .set_tooltip_text(Some(if state.swap_key_binding { "Press the new swap key." } else { "Set the swap shortcut." })); let power_available = state.capture_power.available; widgets .power_auto_button .set_sensitive(power_available && !matches!(state.capture_power.mode.as_str(), "auto")); widgets.power_on_button.set_sensitive( power_available && !matches!(state.capture_power.mode.as_str(), "forced-on"), ); widgets.power_off_button.set_sensitive( power_available && !matches!(state.capture_power.mode.as_str(), "forced-off"), ); sync_power_mode_button_styles(widgets, state.capture_power.mode.as_str()); for monitor_id in 0..2 { refresh_display_pane( &widgets.display_panes[monitor_id], state.display_surface(monitor_id), ); } refresh_diagnostics_report(widgets, state, child_running); } pub fn refresh_test_buttons(widgets: &LauncherWidgets, tests: &mut DeviceTestController) { let camera_running = tests.is_running(DeviceTestKind::Camera); let microphone_running = tests.is_running(DeviceTestKind::Microphone); let microphone_replay_running = tests.is_running(DeviceTestKind::MicrophoneReplay); let speaker_running = tests.is_running(DeviceTestKind::Speaker); widgets.camera_test_button.set_label(if camera_running { "Stop Preview" } else { "Start Preview" }); widgets .camera_preview_stack .set_visible_child_name(if camera_running { "live" } else { "idle" }); let camera_mirrored = tests.camera_preview_mirrored(); if widgets.camera_mirror_button.is_active() != camera_mirrored { widgets.camera_mirror_button.set_active(camera_mirrored); } widgets.camera_mirror_button.set_visible(camera_running); widgets.camera_mirror_button.set_sensitive(camera_running); if !camera_running { widgets.camera_mirror_revealer.set_reveal_child(false); } widgets .microphone_test_button .set_label(if microphone_running { "Stop Monitor" } else { "Monitor Mic" }); widgets .microphone_replay_button .set_label(if microphone_replay_running { "Stop" } else { "Replay" }); widgets .microphone_replay_button .set_sensitive(microphone_replay_running || tests.microphone_replay_ready()); widgets.speaker_test_button.set_label(if speaker_running { "Stop Tone" } else { "Play Tone" }); widgets.audio_check_detail.set_text(&local_test_detail( camera_running, microphone_running, speaker_running, microphone_replay_running, )); if microphone_running { let level = tests.microphone_level_fraction().clamp(0.0, 1.0); widgets.audio_check_meter.set_fraction(level); widgets .audio_check_meter .set_text(Some(&format!("Mic {:>3}%", (level * 100.0).round() as u32))); } else if speaker_running || microphone_replay_running { widgets.audio_check_meter.set_text(Some("Playback")); widgets.audio_check_meter.pulse(); } else { widgets.audio_check_meter.set_fraction(0.0); widgets.audio_check_meter.set_text(Some("Idle")); } } pub fn update_test_action_result( widgets: &LauncherWidgets, tests: &mut DeviceTestController, result: Result, start_msg: &str, stop_msg: &str, ) { match result { Ok(true) => widgets.status_label.set_text(start_msg), Ok(false) => widgets.status_label.set_text(stop_msg), Err(err) => widgets .status_label .set_text(&format!("Device test failed: {err}")), } refresh_test_buttons(widgets, tests); }