317 lines
12 KiB
Rust
317 lines
12 KiB
Rust
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<bool>,
|
|
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);
|
|
}
|