330 lines
11 KiB
Rust

pub mod devices;
pub mod diagnostics;
pub mod state;
mod clipboard;
#[cfg(not(coverage))]
mod device_test;
#[cfg(not(coverage))]
mod power;
#[cfg(not(coverage))]
mod preview;
mod ui;
#[cfg(not(coverage))]
mod ui_components;
#[cfg(not(coverage))]
mod ui_runtime;
use std::{collections::BTreeMap, path::PathBuf};
use crate::app_support::DEFAULT_SERVER_ADDR;
use anyhow::Result;
pub use diagnostics::{DiagnosticsLog, PerformanceSample, SnapshotReport, quality_probe_command};
pub use state::{CapturePowerStatus, DeviceSelection, InputRouting, LauncherState, ViewMode};
pub const LAUNCHER_FOCUS_SIGNAL_ENV: &str = "LESAVKA_LAUNCHER_FOCUS_SIGNAL";
pub const DEFAULT_LAUNCHER_FOCUS_SIGNAL_PATH: &str = "/tmp/lesavka-launcher-focus.signal";
pub const LAUNCHER_CLIPBOARD_CONTROL_ENV: &str = "LESAVKA_LAUNCHER_CLIPBOARD_CONTROL";
pub const DEFAULT_LAUNCHER_CLIPBOARD_CONTROL_PATH: &str = "/tmp/lesavka-launcher-clipboard.control";
pub const REMOTE_INPUT_FAILSAFE_SECONDS_ENV: &str = "LESAVKA_INPUT_REMOTE_FAILSAFE_SECS";
pub const DEFAULT_REMOTE_INPUT_FAILSAFE_SECONDS: &str = "0";
pub fn maybe_run_launcher(args: &[String]) -> Result<bool> {
if should_run_launcher(args) {
let server_addr = resolve_server_addr(args);
ui::run_gui_launcher(server_addr)?;
return Ok(true);
}
Ok(false)
}
/// Decides when to present the GUI launcher instead of direct session startup.
fn should_run_launcher(args: &[String]) -> bool {
!args.iter().any(|arg| arg == "--no-launcher")
}
pub fn runtime_env_vars(state: &LauncherState) -> BTreeMap<String, String> {
let mut envs = BTreeMap::new();
envs.insert(
"LESAVKA_CAPTURE_REMOTE".to_string(),
state.routing.as_env().to_string(),
);
envs.insert(
"LESAVKA_VIEW_MODE".to_string(),
state.view_mode.as_env().to_string(),
);
envs.insert("LESAVKA_CLIPBOARD_DELAY_MS".to_string(), "18".to_string());
envs.insert(
REMOTE_INPUT_FAILSAFE_SECONDS_ENV.to_string(),
std::env::var(REMOTE_INPUT_FAILSAFE_SECONDS_ENV)
.ok()
.filter(|value| value.trim().parse::<u64>().is_ok())
.unwrap_or_else(|| DEFAULT_REMOTE_INPUT_FAILSAFE_SECONDS.to_string()),
);
if matches!(state.view_mode, ViewMode::Unified) {
envs.insert("LESAVKA_DISABLE_VIDEO_RENDER".to_string(), "1".to_string());
}
if let Some(camera) = state.devices.camera.as_ref() {
envs.insert("LESAVKA_CAM_SOURCE".to_string(), camera.clone());
} else {
envs.insert("LESAVKA_CAM_DISABLE".to_string(), "1".to_string());
}
if let Some(microphone) = state.devices.microphone.as_ref() {
envs.insert("LESAVKA_MIC_SOURCE".to_string(), microphone.clone());
} else {
envs.insert("LESAVKA_MIC_DISABLE".to_string(), "1".to_string());
}
if let Some(speaker) = state.devices.speaker.as_ref() {
envs.insert("LESAVKA_AUDIO_SINK".to_string(), speaker.clone());
}
if let Some(keyboard) = state.devices.keyboard.as_ref() {
envs.insert("LESAVKA_KEYBOARD_DEVICE".to_string(), keyboard.clone());
}
if let Some(mouse) = state.devices.mouse.as_ref() {
envs.insert("LESAVKA_MOUSE_DEVICE".to_string(), mouse.clone());
}
for key in [
"LESAVKA_PASTE_KEY",
"LESAVKA_PASTE_KEY_FILE",
"LESAVKA_PASTE_RPC",
"LESAVKA_PASTE_MAX",
"LESAVKA_PASTE_DELAY_MS",
"LESAVKA_CLIPBOARD_CMD",
"LESAVKA_CLIPBOARD_TIMEOUT_MS",
] {
if let Ok(value) = std::env::var(key)
&& !value.trim().is_empty()
{
envs.insert(key.to_string(), value);
}
}
envs
}
pub fn launcher_focus_signal_path() -> PathBuf {
std::env::var(LAUNCHER_FOCUS_SIGNAL_ENV)
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(DEFAULT_LAUNCHER_FOCUS_SIGNAL_PATH))
}
pub fn launcher_clipboard_control_path() -> PathBuf {
std::env::var(LAUNCHER_CLIPBOARD_CONTROL_ENV)
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(DEFAULT_LAUNCHER_CLIPBOARD_CONTROL_PATH))
}
fn resolve_server_addr(args: &[String]) -> String {
for window in args.windows(2) {
if window[0] == "--server" {
return window[1].clone();
}
}
args.iter()
.find(|arg| !arg.starts_with("--"))
.cloned()
.or_else(|| std::env::var("LESAVKA_SERVER_ADDR").ok())
.unwrap_or_else(|| DEFAULT_SERVER_ADDR.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_server_addr_prefers_explicit_server_flag() {
let args = vec![
"--launcher".to_string(),
"--server".to_string(),
"http://example:50051".to_string(),
"http://fallback:50051".to_string(),
];
assert_eq!(resolve_server_addr(&args), "http://example:50051");
}
#[test]
fn resolve_server_addr_uses_first_non_flag_or_default() {
let args = vec![
"--launcher".to_string(),
"http://from-arg:50051".to_string(),
];
assert_eq!(resolve_server_addr(&args), "http://from-arg:50051");
let args = vec!["--launcher".to_string()];
assert_eq!(resolve_server_addr(&args), DEFAULT_SERVER_ADDR);
}
#[test]
fn runtime_env_vars_emit_selected_controls() {
let mut state = LauncherState::new();
state.set_routing(InputRouting::Local);
state.set_view_mode(ViewMode::Unified);
state.select_camera(Some("/dev/video0".to_string()));
state.select_microphone(Some("alsa_input.test".to_string()));
state.select_speaker(Some("alsa_output.test".to_string()));
state.select_keyboard(Some("/dev/input/event10".to_string()));
state.select_mouse(Some("/dev/input/event11".to_string()));
let envs = runtime_env_vars(&state);
assert_eq!(envs.get("LESAVKA_CAPTURE_REMOTE"), Some(&"0".to_string()));
assert_eq!(envs.get("LESAVKA_VIEW_MODE"), Some(&"unified".to_string()));
assert!(!envs.contains_key("LESAVKA_AUDIO_DISABLE"));
assert!(!envs.contains_key("LESAVKA_MIC_DISABLE"));
assert_eq!(
envs.get("LESAVKA_CLIPBOARD_DELAY_MS"),
Some(&"18".to_string())
);
assert_eq!(
envs.get(REMOTE_INPUT_FAILSAFE_SECONDS_ENV),
Some(&DEFAULT_REMOTE_INPUT_FAILSAFE_SECONDS.to_string())
);
assert_eq!(
envs.get("LESAVKA_DISABLE_VIDEO_RENDER"),
Some(&"1".to_string())
);
assert_eq!(
envs.get("LESAVKA_CAM_SOURCE"),
Some(&"/dev/video0".to_string())
);
assert_eq!(
envs.get("LESAVKA_MIC_SOURCE"),
Some(&"alsa_input.test".to_string())
);
assert_eq!(
envs.get("LESAVKA_AUDIO_SINK"),
Some(&"alsa_output.test".to_string())
);
assert_eq!(
envs.get("LESAVKA_KEYBOARD_DEVICE"),
Some(&"/dev/input/event10".to_string())
);
assert_eq!(
envs.get("LESAVKA_MOUSE_DEVICE"),
Some(&"/dev/input/event11".to_string())
);
assert!(!envs.contains_key("LESAVKA_PASTE_KEY_FILE"));
}
#[test]
fn runtime_env_vars_passes_through_clipboard_transport_env() {
temp_env::with_vars(
[
("LESAVKA_PASTE_KEY_FILE", Some("/tmp/paste-key")),
("LESAVKA_PASTE_RPC", Some("1")),
("LESAVKA_CLIPBOARD_CMD", Some("cat /tmp/secret")),
],
|| {
let state = LauncherState::new();
let envs = runtime_env_vars(&state);
assert_eq!(
envs.get("LESAVKA_PASTE_KEY_FILE"),
Some(&"/tmp/paste-key".to_string())
);
assert_eq!(envs.get("LESAVKA_PASTE_RPC"), Some(&"1".to_string()));
assert_eq!(
envs.get("LESAVKA_CLIPBOARD_CMD"),
Some(&"cat /tmp/secret".to_string())
);
},
);
}
#[test]
fn runtime_env_vars_passes_through_remote_failsafe_launch_option() {
temp_env::with_var(REMOTE_INPUT_FAILSAFE_SECONDS_ENV, Some("60"), || {
let state = LauncherState::new();
let envs = runtime_env_vars(&state);
assert_eq!(
envs.get(REMOTE_INPUT_FAILSAFE_SECONDS_ENV),
Some(&"60".to_string())
);
});
}
#[test]
fn runtime_env_vars_keeps_remote_failsafe_disabled_for_invalid_launch_option() {
temp_env::with_var(REMOTE_INPUT_FAILSAFE_SECONDS_ENV, Some("later"), || {
let state = LauncherState::new();
let envs = runtime_env_vars(&state);
assert_eq!(
envs.get(REMOTE_INPUT_FAILSAFE_SECONDS_ENV),
Some(&DEFAULT_REMOTE_INPUT_FAILSAFE_SECONDS.to_string())
);
});
}
#[test]
fn runtime_env_vars_do_not_disable_breakout_video_windows() {
let mut state = LauncherState::new();
state.set_view_mode(ViewMode::Breakout);
let envs = runtime_env_vars(&state);
assert!(!envs.contains_key("LESAVKA_DISABLE_VIDEO_RENDER"));
}
#[test]
fn runtime_env_vars_leave_auto_audio_devices_unset() {
let mut state = LauncherState::new();
state.select_microphone(Some("auto".to_string()));
state.select_speaker(Some("auto".to_string()));
let envs = runtime_env_vars(&state);
assert!(!envs.contains_key("LESAVKA_MIC_SOURCE"));
assert!(!envs.contains_key("LESAVKA_AUDIO_SINK"));
}
#[test]
fn runtime_env_vars_disable_uplink_media_when_unstaged() {
let state = LauncherState::new();
let envs = runtime_env_vars(&state);
assert_eq!(envs.get("LESAVKA_CAM_DISABLE"), Some(&"1".to_string()));
assert_eq!(envs.get("LESAVKA_MIC_DISABLE"), Some(&"1".to_string()));
assert!(!envs.contains_key("LESAVKA_CAM_SOURCE"));
assert!(!envs.contains_key("LESAVKA_MIC_SOURCE"));
}
#[test]
fn maybe_run_launcher_returns_false_with_explicit_opt_out() {
let args = vec!["--no-launcher".to_string()];
assert!(!maybe_run_launcher(&args).expect("launcher check"));
}
#[test]
#[cfg(coverage)]
fn maybe_run_launcher_returns_true_with_launcher_flag() {
let args = vec!["--launcher".to_string()];
assert!(maybe_run_launcher(&args).expect("launcher should run"));
}
#[test]
#[cfg(coverage)]
fn maybe_run_launcher_defaults_to_launcher_for_empty_args() {
let args: Vec<String> = vec![];
assert!(maybe_run_launcher(&args).expect("launcher should run"));
}
#[test]
fn should_run_launcher_defaults_true_for_empty_args() {
assert!(should_run_launcher(&[]));
}
#[test]
fn should_run_launcher_honors_explicit_opt_out() {
let args = vec!["--no-launcher".to_string()];
assert!(!should_run_launcher(&args));
}
#[test]
fn should_run_launcher_includes_legacy_direct_server_args() {
let args = vec!["http://server:50051".to_string()];
assert!(should_run_launcher(&args));
}
#[test]
fn should_run_launcher_with_server_flag() {
let args = vec!["--server".to_string(), "http://server:50051".to_string()];
assert!(should_run_launcher(&args));
}
}