From 0a25b64091b546d70b22cee59bcae18bb6b69532 Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 15:43:08 +0100 Subject: [PATCH 01/14] chore: clean Cargo.toml --- Cargo.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2c6bbd4..93b4f7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,6 @@ license = "ISC" homepage = "https://github.com/cyrinux/push2talk" repository = "https://github.com/cyrinux/push2talk" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] clap = { version = "4.4.7", features = ["derive"] } directories-next = "2.0.0" From 48a58073ecf512cee008bf1ff5af707e79e05971 Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 20:30:00 +0100 Subject: [PATCH 02/14] feat: use input instead of rdev --- Cargo.lock | 255 ++++++++++++++-------------------------------------- Cargo.toml | 6 +- README.md | 2 +- src/main.rs | 250 +++++++++++++++++++++++---------------------------- 4 files changed, 183 insertions(+), 330 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57087e7..9014bb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,21 +77,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -138,83 +123,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" -[[package]] -name = "cocoa" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667fdc068627a2816b9ff831201dd9864249d6ee8d190b9532357f1fc0f61ea7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.3", - "core-graphics 0.21.0", - "foreign-types", - "libc", - "objc", -] - [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "core-foundation" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -dependencies = [ - "core-foundation-sys 0.7.0", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys 0.8.4", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "core-graphics" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.7.0", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a67c4378cf203eace8fb6567847eb641fd6ff933c1145a115c6ee820ebb978" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.3", - "foreign-types", - "libc", -] - [[package]] name = "directories-next" version = "2.0.0" @@ -255,16 +169,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "epoll" -version = "4.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74351c3392ea1ff6cd2628e0042d268ac2371cb613252ff383b6dfa50d22fa79" -dependencies = [ - "bitflags 2.4.1", - "libc", -] - [[package]] name = "errno" version = "0.3.5" @@ -275,44 +179,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "evdev-rs" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b92abc30d5fd1e4f6440dee4d626abc68f4a9b5014dc1de575901e23c2e02321" -dependencies = [ - "bitflags 1.3.2", - "evdev-sys", - "libc", - "log", -] - -[[package]] -name = "evdev-sys" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ead42b547b15d47089c1243d907bcf0eb94e457046d3b315a26ac9c9e9ea6d" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "fs2" version = "0.4.3" @@ -353,23 +219,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] -name = "inotify" +name = "input" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46dd0a94b393c730779ccfd2a872b67b1eb67be3fc33082e733bdb38b5fde4d4" +checksum = "e6e74cd82cedcd66db78742a8337bdc48f188c4d2c12742cbc5cd85113f0b059" dependencies = [ "bitflags 1.3.2", - "inotify-sys", + "input-sys", + "io-lifetimes", + "libc", + "log", + "udev", +] + +[[package]] +name = "input-sys" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f6c2a17e8aba7217660e32863af87b0febad811d4b8620ef76b386603fddc2" +dependencies = [ "libc", ] [[package]] -name = "inotify-sys" -version = "0.1.5" +name = "io-lifetimes" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ + "hermit-abi", "libc", + "windows-sys", ] [[package]] @@ -392,12 +272,6 @@ dependencies = [ "either", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.149" @@ -431,6 +305,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "linux-raw-sys" version = "0.4.10" @@ -443,21 +327,21 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memmap2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +dependencies = [ + "libc", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -478,15 +362,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - [[package]] name = "pkg-config" version = "0.3.27" @@ -512,19 +387,21 @@ dependencies = [ [[package]] name = "push2talk" -version = "1.0.2" +version = "1.1.0" dependencies = [ "clap", "directories-next", "env_logger", "fs2", + "input", "itertools", + "libc", "log", "pulsectl-rs", - "rdev", "signal-hook", "strum", "strum_macros", + "xkbcommon", ] [[package]] @@ -536,26 +413,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rdev" -version = "0.5.3" -source = "git+https://github.com/cyrinux/rdev?branch=main#962feee063a1147e2a08307b637c44d39c40410e" -dependencies = [ - "cocoa", - "core-foundation 0.7.0", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", - "epoll", - "evdev-rs", - "inotify", - "lazy_static", - "libc", - "strum", - "strum_macros", - "winapi", - "x11", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -719,6 +576,17 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "udev" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebdbbd670373442a12fe9ef7aeb53aec4147a5a27a00bbc3ab639f08f48191a" +dependencies = [ + "libc", + "libudev-sys", + "pkg-config", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -835,11 +703,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "x11" -version = "2.21.0" +name = "xkbcommon" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" dependencies = [ "libc", - "pkg-config", + "memmap2", + "xkeysym", ] + +[[package]] +name = "xkeysym" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" diff --git a/Cargo.toml b/Cargo.toml index 93b4f7e..76b3aaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "push2talk" -version = "1.0.2" +version = "1.1.0" edition = "2021" authors = ["Cyril Levis", "Maxim Baz"] categories = ["gui", "audio"] @@ -16,10 +16,12 @@ clap = { version = "4.4.7", features = ["derive"] } directories-next = "2.0.0" env_logger = "0.10.0" fs2 = "0.4.3" +input = "0.8.3" itertools = "0.11.0" +libc = "0.2.149" log = "0.4.20" pulsectl-rs = { git = "https://github.com/cyrinux/pulsectl-rs", branch="main"} -rdev = { git = "https://github.com/cyrinux/rdev", branch="main", default-features = false, features = ["evdev-rs", "unstable_grab"] } signal-hook = "0.3.17" strum = "0.25.0" strum_macros = "0.25.3" +xkbcommon = "0.7.0" diff --git a/README.md b/README.md index 6a8ab98..c451e25 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ sudo usermod -a -G input $USER ## 🎤 Usage -- To set keybind compose of one or two keys, use env var, eg: `env PUSH2TALK_KEYBIND="ControlLeft,KeyO" push2talk` or `env PUSH2TALK_KEYBIND="MetaRight" push2talk`. +- To set keybind compose of one or two keys, use env var, eg: `env PUSH2TALK_KEYBIND="Control_L,O" push2talk` or `env PUSH2TALK_KEYBIND="Super_R" push2talk`. - To get more log: `RUST_LOG=debug push2talk`. - To specify an unique source to manage, use the env var, eg: `env PUSH2TALK_SOURCE="OpenComm by Shokz" push2talk`. diff --git a/src/main.rs b/src/main.rs index 6ef7139..30318e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,19 @@ use clap::Parser; use directories_next::BaseDirs; use fs2::FileExt; +use input::event::keyboard::KeyState::*; +use input::event::keyboard::KeyboardEventTrait; +use input::{Libinput, LibinputInterface}; use itertools::Itertools; +use libc::{O_RDONLY, O_RDWR, O_WRONLY}; use log::{debug, info}; use pulsectl::controllers::types::DeviceInfo; use pulsectl::controllers::{DeviceControl, SourceController}; -use rdev::{grab, Event, EventType, Key}; use signal_hook::flag; use std::error::Error; -use std::fs::OpenOptions; +use std::fs::{File, OpenOptions}; +use std::os::unix::{fs::OpenOptionsExt, io::OwnedFd}; +use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::{ @@ -20,6 +25,24 @@ use std::{ }, thread, time, }; +use xkbcommon::xkb; +use xkbcommon::xkb::Keysym; + +struct MyLibinputInterface; +impl LibinputInterface for MyLibinputInterface { + fn open_restricted(&mut self, path: &Path, flags: i32) -> Result { + OpenOptions::new() + .custom_flags(flags) + .read((flags & O_RDONLY != 0) | (flags & O_RDWR != 0)) + .write((flags & O_WRONLY != 0) | (flags & O_RDWR != 0)) + .open(path) + .map(|file| file.into()) + .map_err(|err| err.raw_os_error().unwrap()) + } + fn close_restricted(&mut self, fd: OwnedFd) { + let _ = File::from(fd); + } +} #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -61,6 +84,27 @@ fn main() -> Result<(), Box> { // Initialize logging setup_logging(); + // Init libinput + let mut libinput_context = Libinput::new_with_udev(MyLibinputInterface); + libinput_context + .udev_assign_seat("seat0") + .expect("Can't connect to libinput on seat0"); + + // Create context + let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + + // Load keymap informations + let keymap = xkb::Keymap::new_from_names( + &xkb_context, + "", // rules + "", // model + "", // layout + "", // variant + None, // options + xkb::COMPILE_NO_FLAGS, + ) + .expect("Can't init keymap"); + // Parse and validate keybinding environment variable let keybind_parsed = parse_keybind()?; validate_keybind(&keybind_parsed)?; @@ -84,41 +128,65 @@ fn main() -> Result<(), Box> { let sig_pause = Arc::new(AtomicBool::new(false)); register_signal(&sig_pause)?; - // Define the callback for key events - let callback = move |event: Event| -> Option { - let check_keybind = |key: Key, pressed: bool| -> bool { - match key { - k if Some(k) == second_key => second_key_pressed.set(pressed), - k if k == first_key => first_key_pressed.set(pressed), - _ => {} - } - !first_key_pressed.get() || second_key.is_some() && !second_key_pressed.get() - }; - - let (key, pressed) = match event.event_type { - EventType::KeyPress(key) => (key, true), - EventType::KeyRelease(key) => (key, false), - _ => return Some(event), - }; - - let should_mute = check_keybind(key, pressed); - if should_mute != last_mute.get() { - info!("Toggle mute: {}", should_mute); - last_mute.set(should_mute); - set_sources(should_mute, &source).ok(); - } + // Create the state tracker + let xkb_state = xkb::State::new(&keymap); - Some(event) + // Check keybind closure + let check_keybind = |key: Keysym, pressed: bool| -> bool { + match key { + k if Some(k) == second_key => second_key_pressed.set(pressed), + k if k == first_key => first_key_pressed.set(pressed), + _ => {} + } + !first_key_pressed.get() || second_key.is_some() && !second_key_pressed.get() }; - // Pause for a moment before starting the main loop - thread::sleep(time::Duration::from_secs(1)); + // Main event loop, toggles state based on signals and key events + let mut is_running = true; // Start the application info!("Push2talk started"); - main_loop(callback, &sig_pause); + loop { + if sig_pause.swap(false, Ordering::Relaxed) { + is_running = !is_running; + info!("Receive SIGUSR1 signal, is running: {is_running}"); + } - Ok(()) + if !is_running { + thread::sleep(time::Duration::from_secs(1)); + continue; + } + + libinput_context.dispatch().unwrap(); + for event in libinput_context.by_ref() { + match event { + input::Event::Keyboard(key_event) => { + let keysym = get_keysym(&key_event, &xkb_state); + let pressed = check_pressed(&key_event); + let should_mute = check_keybind(keysym, pressed); + if should_mute != last_mute.get() { + info!("Toggle mute: {}", should_mute); + last_mute.set(should_mute); + set_sources(should_mute, &source).ok(); + } + } + _ => {} + } + } + } +} + +fn get_keysym(key_event: &input::event::KeyboardEvent, xkb_state: &xkb::State) -> Keysym { + let keycode = key_event.key() + 8; + // libinput's keycodes are offset by 8 from XKB keycodes + xkb_state.key_get_one_sym(keycode.into()) +} + +fn check_pressed(state: &input::event::KeyboardEvent) -> bool { + match state.key_state() { + Released => false, + Pressed => true, + } } fn list_devices() -> Result<(), Box> { @@ -153,19 +221,28 @@ fn setup_logging() { ); } -fn parse_keybind() -> Result, Box> { - env::var("PUSH2TALK_KEYBIND") - .unwrap_or("ControlLeft,Space".to_string()) +fn parse_keybind() -> Result, Box> { + let keybind = env::var("PUSH2TALK_KEYBIND") + .unwrap_or("Control_L,Space".to_string()) .split(',') - .map(|k| k.parse().map_err(|_| format!("Unknown key: {k}").into())) - .collect() + .map(|k| xkb::keysym_from_name(k, xkb::KEYSYM_CASE_INSENSITIVE)) + .collect::>(); + + if keybind + .iter() + .any(|k| *k == xkb::keysym_from_name("KEY_NoSymbol", xkb::KEYSYM_CASE_INSENSITIVE)) + { + return Err("Unknown key".into()); + } + + Ok(keybind) } fn parse_source() -> Option { env::var_os("PUSH2TALK_SOURCE").map(|v| v.into_string().unwrap_or_default()) } -fn validate_keybind(keybind: &[Key]) -> Result<(), Box> { +fn validate_keybind(keybind: &[Keysym]) -> Result<(), Box> { match keybind.len() { 1 | 2 => Ok(()), n => Err(format!("Expected 1 or 2 keys for PUSH2TALK_KEYBIND, got {n}").into()), @@ -179,29 +256,6 @@ fn register_signal(sig_pause: &Arc) -> Result<(), Box> { Ok(()) } -fn main_loop( - callback: impl Fn(Event) -> Option + 'static + Clone, - sig_pause: &Arc, -) { - // Main event loop, toggles state based on signals and key events - let mut is_running = true; - loop { - if sig_pause.swap(false, Ordering::Relaxed) { - is_running = !is_running; - info!("Receive SIGUSR1 signal, is running: {is_running}"); - } - - if !is_running { - thread::sleep(time::Duration::from_secs(1)); - continue; - } - - if grab(callback.clone()).is_err() { - thread::sleep(time::Duration::from_secs(1)); - } - } -} - fn set_sources(mute: bool, source: &Option) -> Result<(), Box> { let mut handler = SourceController::create()?; let sources = handler.list_devices()?; @@ -235,81 +289,3 @@ fn set_sources(mute: bool, source: &Option) -> Result<(), Box Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_keybind() { - std::env::set_var("PUSH2TALK_KEYBIND", "ShiftLeft,ShiftRight"); - let parsed_keys = parse_keybind().unwrap(); - assert_eq!(parsed_keys, vec![Key::ShiftLeft, Key::ShiftRight]); - } - - #[test] - fn test_validate_keybind_empty() { - assert!(validate_keybind(&[]).is_err()); - } - - #[test] - fn test_validate_keybind_too_many() { - assert!(validate_keybind(&[Key::ShiftLeft, Key::ShiftRight, Key::AltGr]).is_err()); - } - - #[test] - fn test_validate_keybind_single_key() { - assert!(validate_keybind(&[Key::ShiftLeft]).is_ok()); - } - - #[test] - fn test_validate_keybind_two_keys() { - assert!(validate_keybind(&[Key::ShiftLeft, Key::ShiftRight]).is_ok()); - } - - #[test] - fn test_parse_source_valid() { - std::env::set_var("PUSH2TALK_SOURCE", "SourceName"); - assert_eq!(parse_source(), Some("SourceName".to_string())); - } - - #[test] - fn test_parse_source_empty() { - std::env::remove_var("PUSH2TALK_SOURCE"); - assert_eq!(parse_source(), None); - } - - #[test] - fn test_register_signal_success() { - let flag = Arc::new(AtomicBool::new(false)); - assert!(register_signal(&flag).is_ok()); - } - - // #[test] - // fn test_set_sources_mute_true() { - // let mut handler = SourceController::create().unwrap(); - // let devices = handler.list_devices().unwrap(); - // let sources = devices - // .iter() - // .filter(|dev| dev.description.is_some()) - // .map(|dev| dev.description.clone().unwrap()) - // .collect::>(); - - // let source_option = sources.first().cloned(); - // assert!(set_sources(true, &source_option).is_ok()); - // } - - // #[test] - // fn test_set_sources_mute_false() { - // let mut handler = SourceController::create().unwrap(); - // let devices = handler.list_devices().unwrap(); - // let sources = devices - // .iter() - // .filter(|dev| dev.description.is_some()) - // .map(|dev| dev.description.clone().unwrap()) - // .collect::>(); - - // let source_option = sources.first().cloned(); - // assert!(set_sources(false, &source_option).is_ok()); - // } -} From 5fd3bc17a6735c6282d3d79f15163e8204c3964e Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 20:40:27 +0100 Subject: [PATCH 03/14] chore: shorter --- src/main.rs | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index 30318e6..e0da701 100644 --- a/src/main.rs +++ b/src/main.rs @@ -159,19 +159,26 @@ fn main() -> Result<(), Box> { libinput_context.dispatch().unwrap(); for event in libinput_context.by_ref() { - match event { - input::Event::Keyboard(key_event) => { - let keysym = get_keysym(&key_event, &xkb_state); - let pressed = check_pressed(&key_event); - let should_mute = check_keybind(keysym, pressed); - if should_mute != last_mute.get() { - info!("Toggle mute: {}", should_mute); - last_mute.set(should_mute); - set_sources(should_mute, &source).ok(); - } - } - _ => {} - } + handle_event(event, &xkb_state, &check_keybind, &last_mute, &source); + } + } +} + +fn handle_event( + event: input::Event, + xkb_state: &xkb::State, + check_keybind: &dyn Fn(Keysym, bool) -> bool, + last_mute: &Cell, + source: &Option, +) { + if let input::Event::Keyboard(key_event) = event { + let keysym = get_keysym(&key_event, xkb_state); + let pressed = check_pressed(&key_event); + let should_mute = check_keybind(keysym, pressed); + if should_mute != last_mute.get() { + info!("Toggle mute: {}", should_mute); + last_mute.set(should_mute); + set_sources(should_mute, source).ok(); } } } From 59c3ae05cab8bf68a9763d9ed1f8ea32bf78437e Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 20:53:34 +0100 Subject: [PATCH 04/14] feat: add log to trace to find key to use --- src/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main.rs b/src/main.rs index e0da701..6e8435f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -173,7 +173,12 @@ fn handle_event( ) { if let input::Event::Keyboard(key_event) = event { let keysym = get_keysym(&key_event, xkb_state); + let name = xkb::keysym_get_name(keysym); let pressed = check_pressed(&key_event); + log::trace!( + "Key {}: {name}", + if pressed { "pressed" } else { "released" } + ); let should_mute = check_keybind(keysym, pressed); if should_mute != last_mute.get() { info!("Toggle mute: {}", should_mute); From 9e201f82fe38035cd996d7c464822e27fa923a2f Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 20:56:54 +0100 Subject: [PATCH 05/14] chore: rename MyLibinputInterface to Push2TalkLibinput --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6e8435f..568bb41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,8 +28,8 @@ use std::{ use xkbcommon::xkb; use xkbcommon::xkb::Keysym; -struct MyLibinputInterface; -impl LibinputInterface for MyLibinputInterface { +struct Push2TalkLibinput; +impl LibinputInterface for Push2TalkLibinput { fn open_restricted(&mut self, path: &Path, flags: i32) -> Result { OpenOptions::new() .custom_flags(flags) @@ -85,7 +85,7 @@ fn main() -> Result<(), Box> { setup_logging(); // Init libinput - let mut libinput_context = Libinput::new_with_udev(MyLibinputInterface); + let mut libinput_context = Libinput::new_with_udev(Push2TalkLibinput); libinput_context .udev_assign_seat("seat0") .expect("Can't connect to libinput on seat0"); From da275f89d185097009a33fb11e91a6b1f5e540bf Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 20:57:02 +0100 Subject: [PATCH 06/14] chore: dont allocate name var if not trace log --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 568bb41..9d41893 100644 --- a/src/main.rs +++ b/src/main.rs @@ -173,11 +173,11 @@ fn handle_event( ) { if let input::Event::Keyboard(key_event) = event { let keysym = get_keysym(&key_event, xkb_state); - let name = xkb::keysym_get_name(keysym); let pressed = check_pressed(&key_event); log::trace!( - "Key {}: {name}", - if pressed { "pressed" } else { "released" } + "Key {}: {}", + if pressed { "pressed" } else { "released" }, + xkb::keysym_get_name(keysym) ); let should_mute = check_keybind(keysym, pressed); if should_mute != last_mute.get() { From 8cb6d5b97837700d48320a24bc14b919c75ac5a0 Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 20:59:39 +0100 Subject: [PATCH 07/14] chore: update doc --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c451e25..6ee4723 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ sudo usermod -a -G input $USER ## 🎤 Usage -- To set keybind compose of one or two keys, use env var, eg: `env PUSH2TALK_KEYBIND="Control_L,O" push2talk` or `env PUSH2TALK_KEYBIND="Super_R" push2talk`. - +- To get the code name of the keys you want to use, start in trace mode: `env RUST_LOG=trace push2talk`. +- To set keybind compose of one or two keys, use env var, eg: `env PUSH2TALK_KEYBIND="Control_L,Space" push2talk` or `env PUSH2TALK_KEYBIND="Super_R" push2talk`. - To get more log: `RUST_LOG=debug push2talk`. - To specify an unique source to manage, use the env var, eg: `env PUSH2TALK_SOURCE="OpenComm by Shokz" push2talk`. - There is also a systemd unit provided. `systemctl --user start push2talk.service` @@ -41,4 +41,4 @@ We welcome contributions! ## 💑 Thanks -Made with love by @cyrinux and @maximbaz. +Made with love by @cyrinux and @maximbaz.ush From 83402d229b64f678484d15f83824c141ceee23d5 Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 21:15:45 +0100 Subject: [PATCH 08/14] test: add some tests --- README.md | 2 +- src/main.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ee4723..dba9a44 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,4 @@ We welcome contributions! ## 💑 Thanks -Made with love by @cyrinux and @maximbaz.ush +Made with love by @cyrinux and @maximbaz. diff --git a/src/main.rs b/src/main.rs index 9d41893..1124882 100644 --- a/src/main.rs +++ b/src/main.rs @@ -301,3 +301,85 @@ fn set_sources(mute: bool, source: &Option) -> Result<(), Box Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_keybind_default() { + // Assuming default keybinds are Control_L and Space + let keybind = parse_keybind().unwrap(); + assert_eq!(keybind.len(), 2); + // Assuming default keybinds are Control_L and Space + assert_eq!( + keybind[0], + xkb::keysym_from_name("Control_L", xkb::KEYSYM_CASE_INSENSITIVE) + ); + assert_eq!( + keybind[1], + xkb::keysym_from_name("Space", xkb::KEYSYM_CASE_INSENSITIVE) + ); + } + + #[test] + fn test_parse_keybind_custom() { + std::env::set_var("PUSH2TALK_KEYBIND", "Shift_L,Alt_L"); + let keybind = parse_keybind().unwrap(); + assert_eq!(keybind.len(), 2); + assert_eq!( + keybind[0], + xkb::keysym_from_name("Shift_L", xkb::KEYSYM_CASE_INSENSITIVE) + ); + assert_eq!( + keybind[1], + xkb::keysym_from_name("Alt_L", xkb::KEYSYM_CASE_INSENSITIVE) + ); + std::env::remove_var("PUSH2TALK_KEYBIND"); + } + + #[test] + fn test_parse_keybind_invalid() { + std::env::set_var("PUSH2TALK_KEYBIND", "InvalidKey"); + assert!(parse_keybind().is_err()); + std::env::remove_var("PUSH2TALK_KEYBIND"); + } + + #[test] + fn test_validate_keybind_valid() { + let keybind = vec![ + xkb::keysym_from_name("Control_L", xkb::KEYSYM_CASE_INSENSITIVE), + xkb::keysym_from_name("Space", xkb::KEYSYM_CASE_INSENSITIVE), + ]; + assert!(validate_keybind(&keybind).is_ok()); + } + + #[test] + fn test_validate_keybind_invalid() { + let keybind = vec![ + xkb::keysym_from_name("Control_L", xkb::KEYSYM_CASE_INSENSITIVE), + xkb::keysym_from_name("Space", xkb::KEYSYM_CASE_INSENSITIVE), + xkb::keysym_from_name("Shift_R", xkb::KEYSYM_CASE_INSENSITIVE), + ]; + assert!(validate_keybind(&keybind).is_err()); + } + + #[test] + fn test_parse_source_valid() { + std::env::set_var("PUSH2TALK_SOURCE", "SourceName"); + assert_eq!(parse_source(), Some("SourceName".to_string())); + std::env::remove_var("PUSH2TALK_SOURCE"); + } + + #[test] + fn test_parse_source_empty() { + std::env::remove_var("PUSH2TALK_SOURCE"); + assert_eq!(parse_source(), None); + } + + #[test] + fn test_register_signal_success() { + let flag = Arc::new(AtomicBool::new(false)); + assert!(register_signal(&flag).is_ok()); + } +} From 0292843d298327dd01c2ceab60fbe0256ee5026b Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 21:19:54 +0100 Subject: [PATCH 09/14] chore: pimp mute log --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 1124882..71e083e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,7 +71,9 @@ fn main() -> Result<(), Box> { .args(["-SIGUSR1", "push2talk"]) .spawn() .expect("Can't pause push2talk"); + println!("Toggle pause."); + return Ok(()); } @@ -181,7 +183,7 @@ fn handle_event( ); let should_mute = check_keybind(keysym, pressed); if should_mute != last_mute.get() { - info!("Toggle mute: {}", should_mute); + info!("Toggle {}", if should_mute { "mute" } else { "unmute" }); last_mute.set(should_mute); set_sources(should_mute, source).ok(); } From f40380f1214a80cea6d834617dda1f84c7b70b12 Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 21:50:22 +0100 Subject: [PATCH 10/14] chore: more cleanup --- src/main.rs | 47 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/main.rs b/src/main.rs index 71e083e..5fc17c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,22 +90,15 @@ fn main() -> Result<(), Box> { let mut libinput_context = Libinput::new_with_udev(Push2TalkLibinput); libinput_context .udev_assign_seat("seat0") - .expect("Can't connect to libinput on seat0"); + .map_err(|e| format!("Can't connect to libinput on seat0: {e:?}"))?; // Create context let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); // Load keymap informations - let keymap = xkb::Keymap::new_from_names( - &xkb_context, - "", // rules - "", // model - "", // layout - "", // variant - None, // options - xkb::COMPILE_NO_FLAGS, - ) - .expect("Can't init keymap"); + let keymap = + xkb::Keymap::new_from_names(&xkb_context, "", "", "", "", None, xkb::COMPILE_NO_FLAGS) + .unwrap(); // Parse and validate keybinding environment variable let keybind_parsed = parse_keybind()?; @@ -133,6 +126,9 @@ fn main() -> Result<(), Box> { // Create the state tracker let xkb_state = xkb::State::new(&keymap); + // Main event loop, toggles state based on signals and key events + let mut is_running = true; + // Check keybind closure let check_keybind = |key: Keysym, pressed: bool| -> bool { match key { @@ -143,15 +139,15 @@ fn main() -> Result<(), Box> { !first_key_pressed.get() || second_key.is_some() && !second_key_pressed.get() }; - // Main event loop, toggles state based on signals and key events - let mut is_running = true; - // Start the application info!("Push2talk started"); loop { if sig_pause.swap(false, Ordering::Relaxed) { is_running = !is_running; - info!("Receive SIGUSR1 signal, is running: {is_running}"); + info!( + "Receive SIGUSR1 signal, {}", + if is_running { "resuming" } else { "pausing" } + ) } if !is_running { @@ -159,7 +155,7 @@ fn main() -> Result<(), Box> { continue; } - libinput_context.dispatch().unwrap(); + libinput_context.dispatch()?; for event in libinput_context.by_ref() { handle_event(event, &xkb_state, &check_keybind, &last_mute, &source); } @@ -191,8 +187,8 @@ fn handle_event( } fn get_keysym(key_event: &input::event::KeyboardEvent, xkb_state: &xkb::State) -> Keysym { - let keycode = key_event.key() + 8; // libinput's keycodes are offset by 8 from XKB keycodes + let keycode = key_event.key() + 8; xkb_state.key_get_one_sym(keycode.into()) } @@ -246,7 +242,7 @@ fn parse_keybind() -> Result, Box> { .iter() .any(|k| *k == xkb::keysym_from_name("KEY_NoSymbol", xkb::KEYSYM_CASE_INSENSITIVE)) { - return Err("Unknown key".into()); + return Err("Unable to parse keybind".into()); } Ok(keybind) @@ -311,6 +307,7 @@ mod tests { #[test] fn test_parse_keybind_default() { // Assuming default keybinds are Control_L and Space + std::env::remove_var("PUSH2TALK_KEYBIND"); let keybind = parse_keybind().unwrap(); assert_eq!(keybind.len(), 2); // Assuming default keybinds are Control_L and Space @@ -325,30 +322,30 @@ mod tests { } #[test] - fn test_parse_keybind_custom() { - std::env::set_var("PUSH2TALK_KEYBIND", "Shift_L,Alt_L"); + fn test_parse_keybind_with_2_valid_keys() { + std::env::set_var("PUSH2TALK_KEYBIND", "Control_L,O"); let keybind = parse_keybind().unwrap(); assert_eq!(keybind.len(), 2); assert_eq!( keybind[0], - xkb::keysym_from_name("Shift_L", xkb::KEYSYM_CASE_INSENSITIVE) + xkb::keysym_from_name("Control_L", xkb::KEYSYM_CASE_INSENSITIVE) ); assert_eq!( keybind[1], - xkb::keysym_from_name("Alt_L", xkb::KEYSYM_CASE_INSENSITIVE) + xkb::keysym_from_name("O", xkb::KEYSYM_CASE_INSENSITIVE) ); std::env::remove_var("PUSH2TALK_KEYBIND"); } #[test] - fn test_parse_keybind_invalid() { + fn test_parse_keybind_with_invalid_key() { std::env::set_var("PUSH2TALK_KEYBIND", "InvalidKey"); assert!(parse_keybind().is_err()); std::env::remove_var("PUSH2TALK_KEYBIND"); } #[test] - fn test_validate_keybind_valid() { + fn test_validate_keybind_with_2_keys_is_valid() { let keybind = vec![ xkb::keysym_from_name("Control_L", xkb::KEYSYM_CASE_INSENSITIVE), xkb::keysym_from_name("Space", xkb::KEYSYM_CASE_INSENSITIVE), @@ -357,7 +354,7 @@ mod tests { } #[test] - fn test_validate_keybind_invalid() { + fn test_validate_keybind_with_3_keys_is_invalid() { let keybind = vec![ xkb::keysym_from_name("Control_L", xkb::KEYSYM_CASE_INSENSITIVE), xkb::keysym_from_name("Space", xkb::KEYSYM_CASE_INSENSITIVE), From 1fa190392a33c607096af3d4637a10e08d0636d4 Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 21:57:25 +0100 Subject: [PATCH 11/14] chore: reordering and remove more unwrap/expect --- src/main.rs | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5fc17c7..087d5e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,19 @@ use std::{ use xkbcommon::xkb; use xkbcommon::xkb::Keysym; +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// List sources devices + #[arg(short, long)] + list_devices: bool, + /// Toggle pause + #[arg(short, long)] + toggle_pause: bool, +} + struct Push2TalkLibinput; + impl LibinputInterface for Push2TalkLibinput { fn open_restricted(&mut self, path: &Path, flags: i32) -> Result { OpenOptions::new() @@ -44,17 +56,6 @@ impl LibinputInterface for Push2TalkLibinput { } } -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - /// List sources devices - #[arg(short, long)] - list_devices: bool, - /// Toggle pause - #[arg(short, long)] - toggle_pause: bool, -} - fn main() -> Result<(), Box> { // Initialize cli let cli = Cli::parse(); @@ -69,8 +70,7 @@ fn main() -> Result<(), Box> { if cli.toggle_pause { Command::new("killall") .args(["-SIGUSR1", "push2talk"]) - .spawn() - .expect("Can't pause push2talk"); + .spawn()?; println!("Toggle pause."); @@ -95,11 +95,6 @@ fn main() -> Result<(), Box> { // Create context let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); - // Load keymap informations - let keymap = - xkb::Keymap::new_from_names(&xkb_context, "", "", "", "", None, xkb::COMPILE_NO_FLAGS) - .unwrap(); - // Parse and validate keybinding environment variable let keybind_parsed = parse_keybind()?; validate_keybind(&keybind_parsed)?; @@ -123,12 +118,17 @@ fn main() -> Result<(), Box> { let sig_pause = Arc::new(AtomicBool::new(false)); register_signal(&sig_pause)?; - // Create the state tracker - let xkb_state = xkb::State::new(&keymap); - // Main event loop, toggles state based on signals and key events let mut is_running = true; + // Load keymap informations + let keymap = + xkb::Keymap::new_from_names(&xkb_context, "", "", "", "", None, xkb::COMPILE_NO_FLAGS) + .unwrap(); + + // Create the state tracker + let xkb_state = xkb::State::new(&keymap); + // Check keybind closure let check_keybind = |key: Keysym, pressed: bool| -> bool { match key { From 81616d493ea5d5b70a2852aa3a86b15e3c0100be Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 22:04:49 +0100 Subject: [PATCH 12/14] chore: more reorder --- src/main.rs | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main.rs b/src/main.rs index 087d5e5..5fc17c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,19 +28,7 @@ use std::{ use xkbcommon::xkb; use xkbcommon::xkb::Keysym; -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - /// List sources devices - #[arg(short, long)] - list_devices: bool, - /// Toggle pause - #[arg(short, long)] - toggle_pause: bool, -} - struct Push2TalkLibinput; - impl LibinputInterface for Push2TalkLibinput { fn open_restricted(&mut self, path: &Path, flags: i32) -> Result { OpenOptions::new() @@ -56,6 +44,17 @@ impl LibinputInterface for Push2TalkLibinput { } } +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// List sources devices + #[arg(short, long)] + list_devices: bool, + /// Toggle pause + #[arg(short, long)] + toggle_pause: bool, +} + fn main() -> Result<(), Box> { // Initialize cli let cli = Cli::parse(); @@ -70,7 +69,8 @@ fn main() -> Result<(), Box> { if cli.toggle_pause { Command::new("killall") .args(["-SIGUSR1", "push2talk"]) - .spawn()?; + .spawn() + .expect("Can't pause push2talk"); println!("Toggle pause."); @@ -95,6 +95,11 @@ fn main() -> Result<(), Box> { // Create context let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + // Load keymap informations + let keymap = + xkb::Keymap::new_from_names(&xkb_context, "", "", "", "", None, xkb::COMPILE_NO_FLAGS) + .unwrap(); + // Parse and validate keybinding environment variable let keybind_parsed = parse_keybind()?; validate_keybind(&keybind_parsed)?; @@ -118,17 +123,12 @@ fn main() -> Result<(), Box> { let sig_pause = Arc::new(AtomicBool::new(false)); register_signal(&sig_pause)?; - // Main event loop, toggles state based on signals and key events - let mut is_running = true; - - // Load keymap informations - let keymap = - xkb::Keymap::new_from_names(&xkb_context, "", "", "", "", None, xkb::COMPILE_NO_FLAGS) - .unwrap(); - // Create the state tracker let xkb_state = xkb::State::new(&keymap); + // Main event loop, toggles state based on signals and key events + let mut is_running = true; + // Check keybind closure let check_keybind = |key: Keysym, pressed: bool| -> bool { match key { From 96e59d7f036f011adae34e6eb2a5a596337d89ef Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 22:08:21 +0100 Subject: [PATCH 13/14] chore: make clippy happy --- src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5fc17c7..f81846f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use input::event::keyboard::KeyState::*; use input::event::keyboard::KeyboardEventTrait; use input::{Libinput, LibinputInterface}; use itertools::Itertools; -use libc::{O_RDONLY, O_RDWR, O_WRONLY}; +use libc::{O_RDWR, O_WRONLY}; use log::{debug, info}; use pulsectl::controllers::types::DeviceInfo; use pulsectl::controllers::{DeviceControl, SourceController}; @@ -33,7 +33,7 @@ impl LibinputInterface for Push2TalkLibinput { fn open_restricted(&mut self, path: &Path, flags: i32) -> Result { OpenOptions::new() .custom_flags(flags) - .read((flags & O_RDONLY != 0) | (flags & O_RDWR != 0)) + .read(true) .write((flags & O_WRONLY != 0) | (flags & O_RDWR != 0)) .open(path) .map(|file| file.into()) From b47f4d6db1f3344cf01bb9bc084a2dac80840480 Mon Sep 17 00:00:00 2001 From: Cyril LEVIS Date: Sun, 29 Oct 2023 22:21:23 +0100 Subject: [PATCH 14/14] fix: github action build --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 78172ed..5e19203 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install dependencies - run: sudo apt-get update && sudo apt-get install -y libxi-dev libxtst-dev libpulse-dev + run: sudo apt-get update && sudo apt-get install -y libxi-dev libudev-dev libxtst-dev libpulse-dev libxkbcommon-dev libinput-dev - name: Build run: cargo build --verbose - name: Run tests