2026-04-23 07:00:06 -03:00
|
|
|
#[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<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: >k::Entry, fallback: &str) -> String {
|
|
|
|
|
let current = entry.text();
|
|
|
|
|
let trimmed = current.trim();
|
|
|
|
|
if trimmed.is_empty() {
|
2026-04-30 11:38:16 -03:00
|
|
|
normalize_server_addr(fallback)
|
2026-04-23 07:00:06 -03:00
|
|
|
} else {
|
2026-04-30 11:38:16 -03:00
|
|
|
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("://") {
|
2026-04-23 07:00:06 -03:00
|
|
|
trimmed.to_string()
|
2026-04-30 11:38:16 -03:00
|
|
|
} else {
|
|
|
|
|
format!("https://{trimmed}")
|
2026-04-23 07:00:06 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 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))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 00:30:07 -03:00
|
|
|
pub fn uplink_telemetry_path() -> PathBuf {
|
|
|
|
|
std::env::var(UPLINK_TELEMETRY_ENV)
|
|
|
|
|
.map(PathBuf::from)
|
|
|
|
|
.unwrap_or_else(|_| PathBuf::from(DEFAULT_UPLINK_TELEMETRY_PATH))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 07:00:06 -03:00
|
|
|
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_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: >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<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,
|
|
|
|
|
}
|
|
|
|
|
}
|