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 { 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 { 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::().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 = 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)); } }