#[cfg(test)] /// Prefer the basename for `/dev/...` entries while keeping Pulse names intact. fn compact_device_name(value: &str) -> String { let trimmed = value.trim(); if trimmed.is_empty() { return "auto".to_string(); } trimmed.rsplit('/').next().unwrap_or(trimmed).to_string() } pub fn capitalize(value: &str) -> String { let mut chars = value.chars(); match chars.next() { Some(first) => format!("{}{}", first.to_ascii_uppercase(), chars.as_str()), None => String::new(), } } pub fn selected_combo_value(combo: >k::ComboBoxText) -> Option { combo .active_id() .map(|value| value.to_string()) .or_else(|| combo.active_text().map(|value| value.to_string())) .and_then(|value| { let value = value.to_string(); let trimmed = value.trim(); if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("auto") || trimmed.eq_ignore_ascii_case("all") { None } else { Some(trimmed.to_string()) } }) } pub fn selected_server_addr(entry: >k::Entry, fallback: &str) -> String { let current = entry.text(); let trimmed = current.trim(); if trimmed.is_empty() { normalize_server_addr(fallback) } else { let normalized = normalize_server_addr(trimmed); if normalized != trimmed { entry.set_text(&normalized); } normalized } } pub fn normalize_server_addr(raw: &str) -> String { let trimmed = raw.trim(); if trimmed.is_empty() || trimmed.contains("://") { trimmed.to_string() } else { format!("https://{trimmed}") } } pub fn input_control_path() -> PathBuf { std::env::var(INPUT_CONTROL_ENV) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(DEFAULT_INPUT_CONTROL_PATH)) } pub fn input_state_path() -> PathBuf { std::env::var(INPUT_STATE_ENV) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(DEFAULT_INPUT_STATE_PATH)) } pub fn input_toggle_control_path() -> PathBuf { std::env::var(TOGGLE_KEY_CONTROL_ENV) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(DEFAULT_TOGGLE_KEY_CONTROL_PATH)) } pub fn audio_gain_control_path() -> PathBuf { std::env::var(AUDIO_GAIN_CONTROL_ENV) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(DEFAULT_AUDIO_GAIN_CONTROL_PATH)) } pub fn mic_gain_control_path() -> PathBuf { std::env::var(MIC_GAIN_CONTROL_ENV) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(DEFAULT_MIC_GAIN_CONTROL_PATH)) } pub fn media_control_path() -> PathBuf { std::env::var(MEDIA_CONTROL_ENV) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(DEFAULT_MEDIA_CONTROL_PATH)) } pub fn uplink_camera_preview_path() -> PathBuf { std::env::var(UPLINK_CAMERA_PREVIEW_ENV) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(DEFAULT_UPLINK_CAMERA_PREVIEW_PATH)) } pub fn uplink_mic_level_path() -> PathBuf { std::env::var(UPLINK_MIC_LEVEL_ENV) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(DEFAULT_UPLINK_MIC_LEVEL_PATH)) } pub fn uplink_telemetry_path() -> PathBuf { std::env::var(UPLINK_TELEMETRY_ENV) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(DEFAULT_UPLINK_TELEMETRY_PATH)) } pub fn write_input_routing_request(path: &Path, routing: InputRouting) -> Result<()> { std::fs::write( path, format!("{} {}\n", routing_name(routing), control_request_nonce()), )?; Ok(()) } pub fn write_audio_gain_request(path: &Path, gain_percent: u32) -> Result<()> { let gain = gain_percent.min(super::state::MAX_AUDIO_GAIN_PERCENT) as f64 / 100.0; std::fs::write(path, format!("{gain:.3} {}\n", control_request_nonce()))?; Ok(()) } pub fn write_mic_gain_request(path: &Path, gain_percent: u32) -> Result<()> { let gain = gain_percent.min(super::state::MAX_MIC_GAIN_PERCENT) as f64 / 100.0; std::fs::write(path, format!("{gain:.3} {}\n", control_request_nonce()))?; Ok(()) } pub fn write_media_control_request(path: &Path, state: &LauncherState) -> Result<()> { crate::live_media_control::write_media_control_request( path, crate::live_media_control::MediaControlState::new( state.channels.camera, state.channels.microphone, state.channels.audio, ), )?; Ok(()) } pub fn write_input_toggle_key_request(path: &Path, swap_key: &str) -> Result<()> { std::fs::write( path, format!("{} {}\n", swap_key.trim(), control_request_nonce()), )?; Ok(()) } pub fn read_input_routing_state(path: &Path) -> Option { let raw = std::fs::read_to_string(path).ok()?; match raw .split_ascii_whitespace() .next()? .to_ascii_lowercase() .as_str() { "local" => Some(InputRouting::Local), "remote" => Some(InputRouting::Remote), _ => None, } } fn control_request_nonce() -> u128 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_nanos()) .unwrap_or_default() } pub fn routing_name(routing: InputRouting) -> &'static str { match routing { InputRouting::Local => "local", InputRouting::Remote => "remote", } } pub fn path_marker(path: &Path) -> u128 { std::fs::metadata(path) .ok() .and_then(|meta| meta.modified().ok()) .and_then(|stamp| stamp.duration_since(std::time::UNIX_EPOCH).ok()) .map(|duration| duration.as_millis()) .unwrap_or_default() } pub fn set_combo_active_text(combo: >k::ComboBoxText, wanted: Option<&str>) { let wanted = wanted.unwrap_or("auto"); if combo.set_active_id(Some(wanted)) { return; } if combo.set_active_id(Some("auto")) { return; } let _ = combo.set_active_id(Some("all")); } pub fn toggle_key_label(raw: &str) -> String { match raw.trim().to_ascii_lowercase().as_str() { "" | "off" | "none" | "disabled" => "Disabled".to_string(), "scrolllock" | "scroll_lock" | "scroll-lock" => "Scroll Lock".to_string(), "sysrq" | "sysreq" | "prtsc" | "printscreen" | "print_screen" | "print-screen" => { "SysRq / PrtSc".to_string() } "pause" | "pausebreak" | "pause_break" | "pause-break" => "Pause".to_string(), "pageup" | "page_up" | "page-up" => "Page Up".to_string(), "pagedown" | "page_down" | "page-down" => "Page Down".to_string(), "capslock" | "caps_lock" | "caps-lock" => "Caps Lock".to_string(), "backspace" | "back_space" | "back-space" => "Backspace".to_string(), "space" | "spacebar" => "Space".to_string(), "escape" | "esc" => "Escape".to_string(), value if value.starts_with('f') && value.len() <= 3 && value[1..].chars().all(|ch| ch.is_ascii_digit()) => { value.to_ascii_uppercase() } value if value.len() == 1 => value.to_ascii_uppercase(), value => capitalize(&value.replace(['_', '-'], " ")), } } pub fn capture_swap_key(key: gtk::gdk::Key) -> Option { let normalized_name = key.name()?.to_string().to_ascii_lowercase(); match normalized_name.as_str() { "shift_l" | "shift_r" | "control_l" | "control_r" | "alt_l" | "alt_r" | "super_l" | "super_r" | "meta_l" | "meta_r" | "hyper_l" | "hyper_r" | "iso_level3_shift" | "multi_key" => None, "scroll_lock" => Some("scrolllock".to_string()), "sys_req" | "print" => Some("sysrq".to_string()), "pause" | "break" => Some("pause".to_string()), "page_up" => Some("pageup".to_string()), "page_down" => Some("pagedown".to_string()), "caps_lock" => Some("capslock".to_string()), "backspace" => Some("backspace".to_string()), "return" => Some("enter".to_string()), "space" => Some("space".to_string()), "escape" => Some("escape".to_string()), "kp_0" => Some("0".to_string()), "kp_1" => Some("1".to_string()), "kp_2" => Some("2".to_string()), "kp_3" => Some("3".to_string()), "kp_4" => Some("4".to_string()), "kp_5" => Some("5".to_string()), "kp_6" => Some("6".to_string()), "kp_7" => Some("7".to_string()), "kp_8" => Some("8".to_string()), "kp_9" => Some("9".to_string()), other if other.starts_with('f') && other.len() <= 3 && other[1..].chars().all(|ch| ch.is_ascii_digit()) => { Some(other.to_string()) } other if other.len() == 1 => { let ch = other.chars().next()?; if ch.is_ascii_alphanumeric() { Some(ch.to_ascii_lowercase().to_string()) } else { None } } "insert" | "delete" | "home" | "end" | "left" | "right" | "up" | "down" | "tab" => { Some(normalized_name) } _ => None, } }