lesavka/client/src/launcher/ui_runtime/status_refresh.rs

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);
}