pub mod devices; pub mod diagnostics; pub mod state; mod ui; use std::collections::BTreeMap; use anyhow::Result; pub use diagnostics::{DiagnosticsLog, PerformanceSample, SnapshotReport, quality_probe_command}; pub use state::{DeviceSelection, InputRouting, LauncherState, ViewMode}; pub fn maybe_run_launcher(args: &[String]) -> Result { if args.iter().any(|arg| arg == "--launcher") { let server_addr = resolve_server_addr(args); ui::run_gui_launcher(server_addr)?; return Ok(true); } Ok(false) } 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(), ); if let Some(camera) = state.devices.camera.as_ref() { envs.insert("LESAVKA_CAM_SOURCE".to_string(), camera.clone()); } if let Some(microphone) = state.devices.microphone.as_ref() { envs.insert("LESAVKA_MIC_SOURCE".to_string(), microphone.clone()); } if let Some(speaker) = state.devices.speaker.as_ref() { envs.insert("LESAVKA_AUDIO_SINK".to_string(), speaker.clone()); } envs } 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(|| "http://127.0.0.1:50051".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!(resolve_server_addr(&args).starts_with("http://")); } #[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())); 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_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()) ); } #[test] fn maybe_run_launcher_returns_false_without_launcher_flag() { let args = vec!["http://server:50051".to_string()]; assert!(!maybe_run_launcher(&args).expect("launcher check")); } }