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}, }; 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 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; set_status_light( &widgets.summary.relay_light, server_light_state(state, relay_live), ); widgets .summary .relay_value .set_text(&server_version_label(state)); 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))); set_status_light( &widgets.summary.gpio_light, gpio_light_state(&state.capture_power), ); widgets .summary .gpio_value .set_text(&gpio_power_label(&state.capture_power)); widgets .summary .shortcut_value .set_text(&toggle_key_label(&state.swap_key)); widgets .power_detail .set_text(&capture_power_detail(&state.capture_power)); 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); } widgets .start_button .set_label(if relay_live { "Disconnect" } else { "Connect" }); 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.probe_button.set_sensitive(true); widgets .usb_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.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 .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); }