From 0fc038939a2f1575e3051268739b89a5d53d9a75 Mon Sep 17 00:00:00 2001 From: Lennard Kittner Date: Mon, 16 Mar 2026 13:40:56 +0100 Subject: [PATCH] Add dialog prompts and make udev checks linux only --- Cargo.lock | 114 ++++++++++++++++++++++++++++- Cargo.toml | 1 + src/bin/hyper_headset_cli.rs | 28 ++++--- src/lib.rs | 137 +++++++++++++++++++++++------------ src/main.rs | 21 +++++- 5 files changed, 238 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e498b80..8561107 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,7 +17,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -78,7 +78,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -103,6 +103,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.4" @@ -241,6 +247,37 @@ dependencies = [ "dbus", ] +[[package]] +name = "dialog" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736bab36d647d14c985725a57a4110a1182c6852104536cd42f1c97e96d29bf0" +dependencies = [ + "dirs", + "rpassword", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "dispatch2" version = "0.3.0" @@ -325,6 +362,17 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.5.0" @@ -345,7 +393,7 @@ name = "hidapi" version = "2.6.3" dependencies = [ "cc", - "cfg-if", + "cfg-if 1.0.4", "libc", "pkg-config", "windows-sys 0.48.0", @@ -356,6 +404,7 @@ name = "hyper_headset" version = "1.5.2" dependencies = [ "clap 4.5.58", + "dialog", "enigo", "hidapi", "ksni", @@ -369,6 +418,16 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "ksni" version = "0.2.2" @@ -396,6 +455,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -510,6 +578,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.12.3" @@ -539,6 +618,17 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "rpassword" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37473170aedbe66ffa3ad3726939ba677d83c646ad4fd99e5b4bc38712f45ec" +dependencies = [ + "kernel32-sys", + "libc", + "winapi 0.2.8", +] + [[package]] name = "rustix" version = "1.1.4" @@ -652,6 +742,18 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -662,6 +764,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index ed2bedf..61393b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,5 +11,6 @@ enigo = "0.6.1" hidapi = { path = "vendor/hidapi" } thistermination = "1.0.0" [target.'cfg(target_os = "linux")'.dependencies] +dialog = "0.3.0" ksni = "0.2.0" shell-escape = "0.1.5" diff --git a/src/bin/hyper_headset_cli.rs b/src/bin/hyper_headset_cli.rs index f95ba14..91771f9 100644 --- a/src/bin/hyper_headset_cli.rs +++ b/src/bin/hyper_headset_cli.rs @@ -1,19 +1,27 @@ -use std::{ - fs, - io::{self}, - time::Duration, -}; +use std::time::Duration; use clap::{Arg, Command}; -use hyper_headset::{ - check_rule, debug_println, devices::connect_compatible_device, prompt_user_for_udev_rule, - update_rule, RuleState, UDEV_RULES, UDEV_RULE_PATH_SYSTEM, UDEV_RULE_PATH_USER, -}; +use hyper_headset::devices::connect_compatible_device; const SHOW_ALL_OPTIONS: bool = false; fn main() { - prompt_user_for_udev_rule(); + #[cfg(target_os = "linux")] + { + use hyper_headset::act_as_askpass_handler; + use hyper_headset::prompt_user_for_udev_rule; + + if let Ok(name) = std::env::current_exe() { + if let Some(name) = name.to_str() { + if let Ok(askpass) = std::env::var("SUDO_ASKPASS") { + if name == askpass { + act_as_askpass_handler(); + } + } + } + } + prompt_user_for_udev_rule(); + } let mut device = match connect_compatible_device() { Ok(device) => device, Err(error) => { diff --git a/src/lib.rs b/src/lib.rs index 2ed3ce3..473909e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,8 @@ +#[cfg(target_os = "linux")] use std::{fs, io, process::Command, time::Duration}; +use dialog::{Choice, DialogBox}; + // #![warn(missing_docs)] pub mod devices; @@ -21,6 +24,7 @@ pub enum RuleState { RuleMatch(bool), } +#[cfg(target_os = "linux")] pub fn check_rule(path: &str, rules: &str) -> RuleState { let mut rule_state; @@ -39,34 +43,95 @@ pub fn check_rule(path: &str, rules: &str) -> RuleState { rule_state } +#[cfg(target_os = "linux")] +pub fn act_as_askpass_handler() -> ! { + let a = dialog::Password::new("Created rule at /usr/lib/udev/rules.d/99-HyperHeadset.rules") + .title("HyperHeadset") + .show() + .expect("Failed to open dialog"); + println!("{}", a.unwrap_or("".to_string())); + std::process::exit(0) +} + +#[cfg(target_os = "linux")] pub fn update_rule(path: &str, rules: &str) { - let status = Command::new("sudo") - .arg("sh") - .arg("-c") - .arg(format!( - "echo {} > {} && udevadm control --reload-rules && udevadm trigger", - shell_escape::escape(rules.into()), - shell_escape::escape(path.into()) - )) - .status(); + let status = if std::io::IsTerminal::is_terminal(&std::io::stdin()) { + Command::new("sudo") + .arg("sh") + .arg("-c") + .arg(format!( + "echo {} > {} && udevadm control --reload-rules && udevadm trigger", + shell_escape::escape(rules.into()), + shell_escape::escape(path.into()) + )) + .status() + } else { + Command::new("sudo") + .env("SUDO_ASKPASS", std::env::current_exe().unwrap()) + .arg("--askpass") + .arg("sh") + .arg("-c") + .arg(format!( + "echo {} > {} && udevadm control --reload-rules && udevadm trigger", + shell_escape::escape(rules.into()), + shell_escape::escape(path.into()) + )) + .status() + }; // a little delay so the rules are active before trying to connect std::thread::sleep(Duration::from_millis(500)); match status { Ok(exit_status) if exit_status.success() => { - println!("created rule at {path}.\nYou may need to replug your headset for the udev rules to take effect."); + show_message(&format!("created rule at {path}.\nYou may need to replug your headset for the udev rules to take effect.")); } Ok(e) => { - println!("Failed to create rule at {path}: {}", e); - println!("Your headset may not be recognized without the correct udev rules."); + show_message(&format!("Failed to create rule at {path}: {}.\nYour headset may not be recognized without the correct udev rules.", e)); } Err(e) => { - println!("Failed to create rule at {path}: {}", e); - println!("Your headset may not be recognized without the correct udev rules."); + show_message(&format!("Failed to create rule at {path}: {}\nYour headset may not be recognized without the correct udev rules.", e)); } } } +#[cfg(target_os = "linux")] +fn show_message(message: &str) { + if std::io::IsTerminal::is_terminal(&std::io::stdin()) { + println!("{message}"); + } else { + let _ = dialog::Message::new(message.to_string()) + .title("HyperHeadset") + .show(); + } +} + +#[cfg(target_os = "linux")] +fn handle_udev_rule_user_interaction(path: &str, ask_message: &str, decline_message: &str) { + if std::io::IsTerminal::is_terminal(&std::io::stdin()) { + print!("{ask_message} (y/N): "); + io::Write::flush(&mut io::stdout()).unwrap(); + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + if matches!(input.trim(), "y" | "Y") { + update_rule(path, UDEV_RULES); + } else { + println!("{decline_message}"); + } + } else if dialog::Question::new(ask_message.to_string()) + .title("HyperHeadset") + .show() + .unwrap_or(Choice::No) + == Choice::Yes + { + update_rule(path, UDEV_RULES); + } else { + let _ = dialog::Message::new(decline_message.to_string()) + .title("HyperHeadset") + .show(); + } +} + +#[cfg(target_os = "linux")] pub fn prompt_user_for_udev_rule() { let user_rule_state = check_rule(UDEV_RULE_PATH_USER, UDEV_RULES); let system_rule_state = check_rule(UDEV_RULE_PATH_SYSTEM, UDEV_RULES); @@ -77,45 +142,23 @@ pub fn prompt_user_for_udev_rule() { (_, RuleState::RuleMatch(true)) => (), (RuleState::RuleMatch(false), _) | (RuleState::RuleExists(true), _) => { - print!( - "Udev rules at {UDEV_RULE_PATH_USER} do not have the expected value. Do you want to recreate them? (y/N): " - ); - io::Write::flush(&mut io::stdout()).unwrap(); - let mut input = String::new(); - io::stdin().read_line(&mut input).unwrap(); - if matches!(input.trim(), "y" | "Y") { - update_rule(UDEV_RULE_PATH_USER, UDEV_RULES); - } else { - println!("Your headset may not be recognized without the correct udev rules."); - } + handle_udev_rule_user_interaction(UDEV_RULE_PATH_USER, + &format!("Udev rules at {UDEV_RULE_PATH_USER} do not have the expected value. Do you want to recreate them?"), + "Your headset may not be recognized without the correct udev rules."); } (RuleState::RuleExists(false), RuleState::RuleMatch(false)) | (RuleState::RuleExists(false), RuleState::RuleExists(true)) => { - print!( - "Udev rules at {UDEV_RULE_PATH_SYSTEM} do not have the expected value. Do you want to recreate them? (y/N): " - ); - io::Write::flush(&mut io::stdout()).unwrap(); - let mut input = String::new(); - io::stdin().read_line(&mut input).unwrap(); - if matches!(input.trim(), "y" | "Y") { - update_rule(UDEV_RULE_PATH_SYSTEM, UDEV_RULES); - } else { - println!("Your headset may not be recognized without the correct udev rules."); - } + handle_udev_rule_user_interaction(UDEV_RULE_PATH_SYSTEM, + &format!("Udev rules at {UDEV_RULE_PATH_SYSTEM} do not have the expected value. Do you want to recreate them?"), + "Your headset may not be recognized without the correct udev rules."); } (RuleState::RuleExists(false), RuleState::RuleExists(false)) => { - print!("No udev rules found. Do you want to create {UDEV_RULE_PATH_USER}? (y/N): "); - io::Write::flush(&mut io::stdout()).unwrap(); - let mut input = String::new(); - io::stdin().read_line(&mut input).unwrap(); - if matches!(input.trim(), "y" | "Y") { - update_rule(UDEV_RULE_PATH_USER, UDEV_RULES); - } else { - println!( - "Without udev rules your headset can only be accessed when running as root." - ); - } + handle_udev_rule_user_interaction( + UDEV_RULE_PATH_USER, + &format!("No udev rules found. Do you want to create {UDEV_RULE_PATH_USER}?"), + "Without udev rules your headset can only be accessed when running as root.", + ); } } } diff --git a/src/main.rs b/src/main.rs index 29de5b4..beb1954 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,11 +3,26 @@ use enigo::{Direction, Enigo, Key, Keyboard, Settings}; use std::time::Duration; mod status_tray; -use hyper_headset::{devices::connect_compatible_device, prompt_user_for_udev_rule}; +use hyper_headset::devices::connect_compatible_device; use status_tray::{StatusTray, TrayHandler}; fn main() { - prompt_user_for_udev_rule(); + #[cfg(target_os = "linux")] + { + use hyper_headset::act_as_askpass_handler; + use hyper_headset::prompt_user_for_udev_rule; + + if let Ok(name) = std::env::current_exe() { + if let Some(name) = name.to_str() { + if let Ok(askpass) = std::env::var("SUDO_ASKPASS") { + if name == askpass { + act_as_askpass_handler(); + } + } + } + } + prompt_user_for_udev_rule(); + } let matches = Command::new(env!("CARGO_PKG_NAME")) .version(env!("CARGO_PKG_VERSION")) .author(env!("CARGO_PKG_AUTHORS")) @@ -54,7 +69,7 @@ fn main() { // with the default refresh_interval the state is only actively queried every 3min // querying the device to frequently can lead to instability - let mute_state = device.get_device_state().muted.clone(); + let mute_state = device.get_device_state().muted; match if run_counter % 30 == 0 { device.active_refresh_state() } else {