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

276 lines
9.0 KiB
Rust

#[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: &gtk::ComboBoxText) -> Option<String> {
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: &gtk::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<InputRouting> {
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: &gtk::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<String> {
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,
}
}