diff --git a/Cargo.lock b/Cargo.lock index 03eeb24..ac49343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Simple-Wayland-HotKey-Daemon" -version = "1.0.0" -dependencies = [ - "clap", - "env_logger", - "evdev", - "log", - "nix", - "sysinfo", -] - [[package]] name = "aho-corasick" version = "0.7.18" @@ -58,6 +46,12 @@ dependencies = [ "wyz", ] +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + [[package]] name = "cc" version = "1.0.72" @@ -72,19 +66,28 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.0.7" +version = "3.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12e8611f9ae4e068fa3e56931fded356ff745e70987ff76924a6e0ab1c8ef2e3" +checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b" dependencies = [ "atty", "bitflags", + "clap_lex", "indexmap", - "os_str_bytes", "strsim", "termcolor", "textwrap", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -143,9 +146,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "env_logger" -version = "0.8.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" dependencies = [ "atty", "humantime", @@ -156,13 +159,16 @@ dependencies = [ [[package]] name = "evdev" -version = "0.11.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21eef104bd659ef808f1f84bed9a924e1aebcdd066845b377cd3b52cc497bb9f" +checksum = "c8afc805e5e0c306722a9517cd479a92692319e81b50354b31db150ecd89a6b4" dependencies = [ "bitvec", + "futures-core", "libc", "nix", + "thiserror", + "tokio", ] [[package]] @@ -171,6 +177,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures-core" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" + [[package]] name = "hashbrown" version = "0.11.2" @@ -202,6 +214,21 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + [[package]] name = "lazy_static" version = "1.4.0" @@ -210,9 +237,18 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.112" +version = "0.2.131" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c3b4822ccebfa39c02fc03d1534441b22ead323fa0f48bb7ddd8e6ba076a40" + +[[package]] +name = "lock_api" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +dependencies = [ + "scopeguard", +] [[package]] name = "log" @@ -238,6 +274,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + [[package]] name = "nix" version = "0.23.1" @@ -281,8 +329,52 @@ name = "os_str_bytes" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "memchr", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", ] [[package]] @@ -316,6 +408,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.5.4" @@ -333,23 +434,122 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "serde" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553" + +[[package]] +name = "serde_json" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "signal-hook-tokio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e" +dependencies = [ + "futures-core", + "libc", + "signal-hook", + "tokio", +] + +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "sohkd" +version = "1.2.1" +dependencies = [ + "clap", + "env_logger", + "evdev", + "itertools", + "log", + "nix", + "serde_json", + "signal-hook", + "signal-hook-tokio", + "sysinfo", + "tokio", + "tokio-stream", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sysinfo" -version = "0.23.0" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e757000a4bed2b1be9be65a3f418b9696adf30bb419214c73997422de73a591" +checksum = "3977ec2e0520829be45c8a2df70db2bf364714d8a748316a10c3c35d4d2b01c9" dependencies = [ "cfg-if", "core-foundation-sys", @@ -377,9 +577,83 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.14.2" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" @@ -412,6 +686,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "wyz" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 6794269..aceddcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] -name = "Simple-Odilia-HotKey-Daemon" -version = "1.0.1" +description = "Swhkd clone for Odilia" edition = "2021" +name = "sohkd" +version = "1.2.1" authors = [ "Shinyzenith \n", "Angelo Fallaria \n", @@ -10,23 +11,16 @@ authors = [ ] [dependencies] -clap = "3.0.4" -env_logger = "0.8.4" -#rdev = { version = "^0.5.1", features = ["unstable_grab"] } -rdev = { git = "https://github.com/TTWNO/rdev", features = ["unstable_grab"] } -log = "0.4.0" +clap = "3.1.6" +env_logger = "0.9.0" +evdev = { version = "^0.11.6", features = ["tokio"] } +itertools = "0.10.3" +log = "0.4.14" nix = "0.23.1" -sysinfo = "0.23.0" -once_cell = "^1.9.0" - -[[bin]] -name = "sohkd" -path = "src/daemon.rs" - -[[bin]] -name = "sohks" -path = "src/server.rs" - -[[bin]] -name = "sohkctl" -path = "src/ctrl.rs" +signal-hook = "0.3.13" +signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } +sysinfo = "0.23.5" +tokio = { version = "1.17.0", features = ["full"] } +tokio-stream = "0.1.8" +#odilia-common = { path = "../common" } +serde_json = "1.0" diff --git a/src/config.rs b/src/config.rs index 6bab8e2..e6c060b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,11 @@ +use itertools::Itertools; use std::collections::HashMap; use std::fs::File; use std::io::Read; -use std::{fmt, path}; +use std::{ + fmt, + path::{Path, PathBuf}, +}; #[derive(Debug)] pub enum Error { @@ -13,9 +17,9 @@ pub enum Error { #[derive(Debug, PartialEq)] pub enum ParseError { // u32 is the line number where an error occured - UnknownSymbol(u32), - InvalidModifier(u32), - InvalidKeysym(u32), + UnknownSymbol(PathBuf, u32), + InvalidModifier(PathBuf, u32), + InvalidKeysym(PathBuf, u32), } impl From for Error { @@ -34,177 +38,406 @@ impl fmt::Display for Error { Error::ConfigNotFound => "Config file not found.".fmt(f), Error::Io(io_err) => format!("I/O Error while parsing config file: {}", io_err).fmt(f), - Error::InvalidConfig(parse_err) => match parse_err { - ParseError::UnknownSymbol(line_nr) => { - format!("Unknown symbol at line {}.", line_nr).fmt(f) - } - ParseError::InvalidKeysym(line_nr) => { - format!("Invalid keysym at line {}.", line_nr).fmt(f) + ParseError::UnknownSymbol(path, line_nr) => format!( + "Error parsing config file {:?}. Unknown symbol at line {}.", + path, line_nr + ) + .fmt(f), + ParseError::InvalidKeysym(path, line_nr) => format!( + "Error parsing config file {:?}. Invalid keysym at line {}.", + path, line_nr + ) + .fmt(f), + ParseError::InvalidModifier(path, line_nr) => format!( + "Error parsing config file {:?}. Invalid modifier at line {}.", + path, line_nr + ) + .fmt(f), + }, + } + } +} + +pub const IMPORT_STATEMENT: &str = "include"; +pub const UNBIND_STATEMENT: &str = "ignore"; +pub const MODE_STATEMENT: &str = "mode"; +pub const MODE_END_STATEMENT: &str = "endmode"; +pub const MODE_ENTER_STATEMENT: &str = "@enter"; +pub const MODE_ESCAPE_STATEMENT: &str = "@escape"; +pub const ODILIA_SEND_STATEMENT: &str = "@odilia"; + +#[derive(Debug, PartialEq, Clone)] +pub struct Config { + pub path: PathBuf, + pub contents: String, + pub imports: Vec, +} + +pub fn load_file_contents(path: &Path) -> Result { + let mut file = File::open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + Ok(contents) +} + +impl Config { + pub fn get_imports(contents: &str) -> Result, Error> { + let mut imports = Vec::new(); + for line in contents.lines() { + if line.split(' ').next().unwrap() == IMPORT_STATEMENT { + if let Some(import_path) = line.split(' ').nth(1) { + imports.push(Path::new(import_path).to_path_buf()); } - ParseError::InvalidModifier(line_nr) => { - format!("Invalid modifier at line {}.", line_nr).fmt(f) + } + } + Ok(imports) + } + + pub fn new(path: &Path) -> Result { + let contents = load_file_contents(path)?; + let imports = Self::get_imports(&contents)?; + Ok(Config { path: path.to_path_buf(), contents, imports }) + } + + pub fn load_to_configs(&self) -> Result, Error> { + let mut configs = Vec::new(); + for import in &self.imports { + configs.push(Self::new(import)?) + } + Ok(configs) + } + + pub fn load_and_merge(config: Self) -> Result, Error> { + let mut configs = vec![config]; + let mut prev_count = 0; + let mut current_count = configs.len(); + while prev_count != current_count { + prev_count = configs.len(); + for config in configs.clone() { + for import in Self::load_to_configs(&config)? { + if !configs.contains(&import) { + configs.push(import); + } } - }, + } + current_count = configs.len(); } + Ok(configs) } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Hotkey { - pub mode: Option, - pub keysym: rdev::Key, +pub fn load(path: &Path) -> Result, Error> { + let config_self = Config::new(path)?; + let mut configs: Vec = Config::load_and_merge(config_self.clone())?; + configs.remove(0); + configs.push(config_self); + let mut modes: Vec = vec![Mode::default()]; + for config in configs { + let mut output = parse_contents(path.to_path_buf(), config.contents)?; + for hotkey in output[0].hotkeys.drain(..) { + modes[0].hotkeys.retain(|hk| hk.keybinding != hotkey.keybinding); + modes[0].hotkeys.push(hotkey); + } + for unbind in output[0].unbinds.drain(..) { + modes[0].hotkeys.retain(|hk| hk.keybinding != unbind); + } + output.remove(0); + for mut mode in output { + mode.hotkeys.retain(|x| !mode.unbinds.contains(&x.keybinding)); + modes.push(mode); + } + } + Ok(modes) +} + +#[derive(Debug, Clone)] +pub struct KeyBinding { + pub keysym: Option, pub modifiers: Vec, + pub send: bool, + pub on_release: bool, +} + +impl PartialEq for KeyBinding { + fn eq(&self, other: &Self) -> bool { + self.keysym == other.keysym + && self.modifiers.iter().all(|modifier| other.modifiers.contains(modifier)) + && self.modifiers.len() == other.modifiers.len() + && self.send == other.send + && self.on_release == other.on_release + } +} + +pub trait Prefix { + fn send(self) -> Self; + fn on_release(self) -> Self; +} + +pub trait Value { + fn keysym(&self) -> Option; + fn modifiers(&self) -> Vec; + fn is_send(&self) -> bool; + fn is_on_release(&self) -> bool; +} + +impl KeyBinding { + pub fn new(keysym: Option, modifiers: Vec) -> Self { + KeyBinding { keysym, modifiers, send: false, on_release: false } + } + pub fn on_release(mut self) -> Self { + self.on_release = true; + self + } +} + +impl Prefix for KeyBinding { + fn send(mut self) -> Self { + self.send = true; + self + } + fn on_release(mut self) -> Self { + self.on_release = true; + self + } +} + +impl Value for KeyBinding { + fn keysym(&self) -> Option { + self.keysym + } + fn modifiers(&self) -> Vec { + self.clone().modifiers + } + fn is_send(&self) -> bool { + self.send + } + fn is_on_release(&self) -> bool { + self.on_release + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Hotkey { + pub keybinding: KeyBinding, pub command: String, - pub consume: bool, } -#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq)] -// TODO: make the commented-out modifiers available +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] pub enum Modifier { Super, - // Hyper, - // Meta, Alt, Control, Shift, - // ModeSwitch, - // Lock, - // Mod1, - // Mod2, - // Mod3, - // Mod4, - // Mod5, + CapsLock, + Any, } impl Hotkey { - pub fn new(mode: Option, keysym: rdev::Key, modifiers: Vec, command: String, consume: bool) -> Self { - Hotkey { mode, keysym, modifiers, command, consume } + pub fn from_keybinding(keybinding: KeyBinding, command: String) -> Self { + Hotkey { keybinding, command } + } + #[cfg(test)] + pub fn new(keysym: Option, modifiers: Vec, command: String) -> Self { + Hotkey { keybinding: KeyBinding::new(keysym, modifiers), command } } } -pub fn load(path: path::PathBuf) -> Result, Error> { - let file_contents = load_file_contents(path)?; - parse_contents(file_contents) +impl Prefix for Hotkey { + fn send(mut self) -> Self { + self.keybinding.send = true; + self + } + fn on_release(mut self) -> Self { + self.keybinding.on_release = true; + self + } } -fn load_file_contents(path: path::PathBuf) -> Result { - let mut file = File::open(path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - Ok(contents) +impl Value for &Hotkey { + fn keysym(&self) -> Option { + self.keybinding.keysym + } + fn modifiers(&self) -> Vec { + self.keybinding.clone().modifiers + } + fn is_send(&self) -> bool { + self.keybinding.send + } + fn is_on_release(&self) -> bool { + self.keybinding.on_release + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Mode { + pub name: String, + pub hotkeys: Vec, + pub unbinds: Vec, +} + +impl Mode { + pub fn new(name: String) -> Self { + Mode { name, hotkeys: Vec::new(), unbinds: Vec::new() } + } + + pub fn default() -> Self { + Self::new("normal".to_string()) + } } -fn parse_contents(contents: String) -> Result, Error> { - let key_to_evdev_key: HashMap<&str, rdev::Key> = HashMap::from([ - ("q", rdev::Key::KeyQ), - ("w", rdev::Key::KeyW), - ("e", rdev::Key::KeyE), - ("r", rdev::Key::KeyR), - ("t", rdev::Key::KeyT), - ("y", rdev::Key::KeyY), - ("u", rdev::Key::KeyU), - ("i", rdev::Key::KeyI), - ("o", rdev::Key::KeyO), - ("p", rdev::Key::KeyP), - ("a", rdev::Key::KeyA), - ("s", rdev::Key::KeyS), - ("d", rdev::Key::KeyD), - ("f", rdev::Key::KeyF), - ("g", rdev::Key::KeyG), - ("h", rdev::Key::KeyH), - ("j", rdev::Key::KeyJ), - ("k", rdev::Key::KeyK), - ("l", rdev::Key::KeyL), - ("z", rdev::Key::KeyZ), - ("x", rdev::Key::KeyX), - ("c", rdev::Key::KeyC), - ("v", rdev::Key::KeyV), - ("b", rdev::Key::KeyB), - ("n", rdev::Key::KeyN), - ("m", rdev::Key::KeyM), - ("1", rdev::Key::Num1), - ("2", rdev::Key::Num2), - ("3", rdev::Key::Num3), - ("4", rdev::Key::Num4), - ("5", rdev::Key::Num5), - ("6", rdev::Key::Num6), - ("7", rdev::Key::Num7), - ("8", rdev::Key::Num8), - ("9", rdev::Key::Num9), - ("0", rdev::Key::Num0), - ("escape", rdev::Key::Escape), - ("delete", rdev::Key::Delete), - ("backspace", rdev::Key::Backspace), - ("return", rdev::Key::Return), - ("enter", rdev::Key::Return), - ("tab", rdev::Key::Tab), - ("space", rdev::Key::Space), - ("minus", rdev::Key::Minus), - ("-", rdev::Key::Minus), - ("equal", rdev::Key::Equal), - ("=", rdev::Key::Equal), - ("grave", rdev::Key::BackQuote), - ("`", rdev::Key::BackQuote), - ("print", rdev::Key::PrintScreen), - /* - ("volumeup", rdev::Key::KEY_VOLUMEUP), - ("xf86audioraisevolume", rdev::Key::KEY_VOLUMEUP), - ("volumedown", rdev::Key::KEY_VOLUMEDOWN), - ("xf86audiolowervolume", rdev::Key::KEY_VOLUMEDOWN), - ("mute", rdev::Key::KEY_MUTE), - ("xf86audiomute", rdev::Key::KEY_MUTE), - ("brightnessup", rdev::Key::KEY_BRIGHTNESSUP), - ("brightnessdown", rdev::Key::KEY_BRIGHTNESSDOWN), - TODO: find ways to accept xf86 keys - */ - (",", rdev::Key::Comma), - ("comma", rdev::Key::Comma), - (".", rdev::Key::Dot), - ("dot", rdev::Key::Dot), - ("/", rdev::Key::Slash), - ("slash", rdev::Key::Slash), - ("backslash", rdev::Key::IntlBackslash), - ("leftbrace", rdev::Key::LeftBracket), - ("[", rdev::Key::LeftBracket), - ("rightbrace", rdev::Key::RightBracket), - ("]", rdev::Key::RightBracket), - (";", rdev::Key::SemiColon), - ("semicolon", rdev::Key::SemiColon), - ("'", rdev::Key::Quote), - ("apostrophe", rdev::Key::Quote), - ("left", rdev::Key::LeftArrow), - ("right", rdev::Key::RightArrow), - ("up", rdev::Key::UpArrow), - ("down", rdev::Key::DownArrow), - ("f1", rdev::Key::F1), - ("f2", rdev::Key::F2), - ("f3", rdev::Key::F3), - ("f4", rdev::Key::F4), - ("f5", rdev::Key::F5), - ("f6", rdev::Key::F6), - ("f7", rdev::Key::F7), - ("f8", rdev::Key::F8), - ("f9", rdev::Key::F9), - ("f10", rdev::Key::F10), - ("f11", rdev::Key::F11), - ("f12", rdev::Key::F12), -/* - ("f13", rdev::Key::F13), - ("f14", rdev::Key::F14), - ("f15", rdev::Key::F15), - ("f16", rdev::Key::F16), - ("f17", rdev::Key::F17), - ("f18", rdev::Key::F18), - ("f19", rdev::Key::F19), - ("f20", rdev::Key::F20), - ("f21", rdev::Key::F21), - ("f22", rdev::Key::F22), - ("f23", rdev::Key::F23), - ("f24", rdev::Key::F24), -*/ +pub fn parse_contents(path: PathBuf, contents: String) -> Result, Error> { + // Don't forget to update valid key list on the man page if you do change this list. + let key_to_evdev_key: HashMap<&str, evdev::Key> = HashMap::from([ + ("q", evdev::Key::KEY_Q), + ("w", evdev::Key::KEY_W), + ("e", evdev::Key::KEY_E), + ("r", evdev::Key::KEY_R), + ("t", evdev::Key::KEY_T), + ("y", evdev::Key::KEY_Y), + ("u", evdev::Key::KEY_U), + ("i", evdev::Key::KEY_I), + ("o", evdev::Key::KEY_O), + ("p", evdev::Key::KEY_P), + ("a", evdev::Key::KEY_A), + ("s", evdev::Key::KEY_S), + ("d", evdev::Key::KEY_D), + ("f", evdev::Key::KEY_F), + ("g", evdev::Key::KEY_G), + ("h", evdev::Key::KEY_H), + ("j", evdev::Key::KEY_J), + ("k", evdev::Key::KEY_K), + ("l", evdev::Key::KEY_L), + ("z", evdev::Key::KEY_Z), + ("x", evdev::Key::KEY_X), + ("c", evdev::Key::KEY_C), + ("v", evdev::Key::KEY_V), + ("b", evdev::Key::KEY_B), + ("n", evdev::Key::KEY_N), + ("m", evdev::Key::KEY_M), + ("1", evdev::Key::KEY_1), + ("2", evdev::Key::KEY_2), + ("3", evdev::Key::KEY_3), + ("4", evdev::Key::KEY_4), + ("5", evdev::Key::KEY_5), + ("6", evdev::Key::KEY_6), + ("7", evdev::Key::KEY_7), + ("8", evdev::Key::KEY_8), + ("9", evdev::Key::KEY_9), + ("0", evdev::Key::KEY_0), + ("escape", evdev::Key::KEY_ESC), + ("backspace", evdev::Key::KEY_BACKSPACE), + //("capslock", evdev::Key::KEY_CAPSLOCK), + ("return", evdev::Key::KEY_ENTER), + ("enter", evdev::Key::KEY_ENTER), + ("tab", evdev::Key::KEY_TAB), + ("space", evdev::Key::KEY_SPACE), + ("plus", evdev::Key::KEY_KPPLUS), // Shouldn't this be kpplus? + ("kp0", evdev::Key::KEY_KP0), + ("kp1", evdev::Key::KEY_KP1), + ("kp2", evdev::Key::KEY_KP2), + ("kp3", evdev::Key::KEY_KP3), + ("kp4", evdev::Key::KEY_KP4), + ("kp5", evdev::Key::KEY_KP5), + ("kp6", evdev::Key::KEY_KP6), + ("kp7", evdev::Key::KEY_KP7), + ("kp8", evdev::Key::KEY_KP8), + ("kp9", evdev::Key::KEY_KP9), + ("kpasterisk", evdev::Key::KEY_KPASTERISK), + ("kpcomma", evdev::Key::KEY_KPCOMMA), + ("kpdot", evdev::Key::KEY_KPDOT), + ("kpenter", evdev::Key::KEY_KPENTER), + ("kpequal", evdev::Key::KEY_KPEQUAL), + ("kpjpcomma", evdev::Key::KEY_KPJPCOMMA), + ("kpleftparen", evdev::Key::KEY_KPLEFTPAREN), + ("kpminus", evdev::Key::KEY_KPMINUS), + ("kpplusminus", evdev::Key::KEY_KPPLUSMINUS), + ("kprightparen", evdev::Key::KEY_KPRIGHTPAREN), + ("minus", evdev::Key::KEY_MINUS), + ("-", evdev::Key::KEY_MINUS), + ("equal", evdev::Key::KEY_EQUAL), + ("=", evdev::Key::KEY_EQUAL), + ("grave", evdev::Key::KEY_GRAVE), + ("`", evdev::Key::KEY_GRAVE), + ("print", evdev::Key::KEY_SYSRQ), + ("volumeup", evdev::Key::KEY_VOLUMEUP), + ("xf86audioraisevolume", evdev::Key::KEY_VOLUMEUP), + ("volumedown", evdev::Key::KEY_VOLUMEDOWN), + ("xf86audiolowervolume", evdev::Key::KEY_VOLUMEDOWN), + ("mute", evdev::Key::KEY_MUTE), + ("xf86audiomute", evdev::Key::KEY_MUTE), + ("brightnessup", evdev::Key::KEY_BRIGHTNESSUP), + ("xf86monbrightnessup", evdev::Key::KEY_BRIGHTNESSUP), + ("brightnessdown", evdev::Key::KEY_BRIGHTNESSDOWN), + ("xf86audiomedia", evdev::Key::KEY_MEDIA), + ("xf86audiomicmute", evdev::Key::KEY_MICMUTE), + ("micmute", evdev::Key::KEY_MICMUTE), + ("xf86audionext", evdev::Key::KEY_NEXTSONG), + ("xf86audioplay", evdev::Key::KEY_PLAYPAUSE), + ("xf86audioprev", evdev::Key::KEY_PREVIOUSSONG), + ("xf86audiostop", evdev::Key::KEY_STOP), + ("xf86monbrightnessdown", evdev::Key::KEY_BRIGHTNESSDOWN), + (",", evdev::Key::KEY_COMMA), + ("comma", evdev::Key::KEY_COMMA), + (".", evdev::Key::KEY_DOT), + ("dot", evdev::Key::KEY_DOT), + ("period", evdev::Key::KEY_DOT), + ("/", evdev::Key::KEY_SLASH), + ("question", evdev::Key::KEY_QUESTION), + ("slash", evdev::Key::KEY_SLASH), + ("backslash", evdev::Key::KEY_BACKSLASH), + ("leftbrace", evdev::Key::KEY_LEFTBRACE), + ("[", evdev::Key::KEY_LEFTBRACE), + ("bracketleft", evdev::Key::KEY_LEFTBRACE), + ("rightbrace", evdev::Key::KEY_RIGHTBRACE), + ("]", evdev::Key::KEY_RIGHTBRACE), + ("bracketright", evdev::Key::KEY_RIGHTBRACE), + (";", evdev::Key::KEY_SEMICOLON), + ("scroll_lock", evdev::Key::KEY_SCROLLLOCK), + ("semicolon", evdev::Key::KEY_SEMICOLON), + ("'", evdev::Key::KEY_APOSTROPHE), + ("apostrophe", evdev::Key::KEY_APOSTROPHE), + ("left", evdev::Key::KEY_LEFT), + ("right", evdev::Key::KEY_RIGHT), + ("up", evdev::Key::KEY_UP), + ("down", evdev::Key::KEY_DOWN), + ("pause", evdev::Key::KEY_PAUSE), + ("home", evdev::Key::KEY_HOME), + ("delete", evdev::Key::KEY_DELETE), + ("insert", evdev::Key::KEY_INSERT), + ("end", evdev::Key::KEY_END), + ("pause", evdev::Key::KEY_PAUSE), + ("prior", evdev::Key::KEY_PAGEDOWN), + ("next", evdev::Key::KEY_PAGEUP), + ("pagedown", evdev::Key::KEY_PAGEDOWN), + ("pageup", evdev::Key::KEY_PAGEUP), + ("f1", evdev::Key::KEY_F1), + ("f2", evdev::Key::KEY_F2), + ("f3", evdev::Key::KEY_F3), + ("f4", evdev::Key::KEY_F4), + ("f5", evdev::Key::KEY_F5), + ("f6", evdev::Key::KEY_F6), + ("f7", evdev::Key::KEY_F7), + ("f8", evdev::Key::KEY_F8), + ("f9", evdev::Key::KEY_F9), + ("f10", evdev::Key::KEY_F10), + ("f11", evdev::Key::KEY_F11), + ("f12", evdev::Key::KEY_F12), + ("f13", evdev::Key::KEY_F13), + ("f14", evdev::Key::KEY_F14), + ("f15", evdev::Key::KEY_F15), + ("f16", evdev::Key::KEY_F16), + ("f17", evdev::Key::KEY_F17), + ("f18", evdev::Key::KEY_F18), + ("f19", evdev::Key::KEY_F19), + ("f20", evdev::Key::KEY_F20), + ("f21", evdev::Key::KEY_F21), + ("f22", evdev::Key::KEY_F22), + ("f23", evdev::Key::KEY_F23), + ("f24", evdev::Key::KEY_F24), ]); + // Don't forget to update modifier list on the man page if you do change this list. let mod_to_mod_enum: HashMap<&str, Modifier> = HashMap::from([ ("ctrl", Modifier::Control), ("control", Modifier::Control), @@ -213,20 +446,33 @@ fn parse_contents(contents: String) -> Result, Error> { ("alt", Modifier::Alt), ("mod1", Modifier::Alt), ("shift", Modifier::Shift), + ("capslock", Modifier::CapsLock), + ("any", Modifier::Any), ]); let lines: Vec<&str> = contents.split('\n').collect(); + let mut modes: Vec = vec![Mode::default()]; + let mut current_mode: usize = 0; // Go through each line, ignore comments and empty lines, mark lines starting with whitespace // as commands, and mark the other lines as keysyms. Mark means storing a line's type and the // line number in a vector. let mut lines_with_types: Vec<(&str, u32)> = Vec::new(); for (line_number, line) in lines.iter().enumerate() { - if line.trim().starts_with('#') || line.trim().is_empty() { + if line.trim().starts_with('#') + || line.split(' ').next().unwrap() == IMPORT_STATEMENT + || line.trim().is_empty() + { continue; } if line.starts_with(' ') || line.starts_with('\t') { lines_with_types.push(("command", line_number as u32)); + } else if line.starts_with(UNBIND_STATEMENT) { + lines_with_types.push(("unbind", line_number as u32)); + } else if line.starts_with(MODE_STATEMENT) { + lines_with_types.push(("modestart", line_number as u32)); + } else if line.starts_with(MODE_END_STATEMENT) { + lines_with_types.push(("modeend", line_number as u32)); } else { lines_with_types.push(("keysym", line_number as u32)); } @@ -234,41 +480,78 @@ fn parse_contents(contents: String) -> Result, Error> { // Edge case: return a blank vector if no lines detected if lines_with_types.is_empty() { - return Ok(vec![]); + return Ok(modes); } - // Go through lines_with_types, and add the next line over and over until the current line no - // longer ends with backslash. (Only if the lines have the same type) let mut actual_lines: Vec<(&str, u32, String)> = Vec::new(); - let mut current_line_type = lines_with_types[0].0; - let mut current_line_number = lines_with_types[0].1; - let mut current_line_string = String::new(); - for (line_type, line_number) in lines_with_types { - if line_type != current_line_type { - current_line_type = line_type; - current_line_number = line_number; - current_line_string = String::new(); + + if contents.contains('\\') { + // Go through lines_with_types, and add the next line over and over until the current line no + // longer ends with backslash. (Only if the lines have the same type) + let mut current_line_type = lines_with_types[0].0; + let mut current_line_number = lines_with_types[0].1; + let mut current_line_string = String::new(); + let mut continue_backslash; + + for (line_type, line_number) in lines_with_types { + if line_type != current_line_type { + current_line_type = line_type; + current_line_number = line_number; + current_line_string = String::new(); + } + + let line_to_add = lines[line_number as usize].trim(); + continue_backslash = line_to_add.ends_with('\\'); + + let line_to_add = line_to_add.strip_suffix('\\').unwrap_or(line_to_add); + + current_line_string.push_str(line_to_add); + + if !continue_backslash { + actual_lines.push((current_line_type, current_line_number, current_line_string)); + current_line_type = line_type; + current_line_number = line_number; + current_line_string = String::new(); + } } - current_line_string.push_str(lines[line_number as usize].trim()); - if !current_line_string.ends_with('\\') { + } else { + for (line_type, line_number) in lines_with_types { actual_lines.push(( - current_line_type, - current_line_number, - current_line_string.replace("\\", ""), + line_type, + line_number, + lines[line_number as usize].trim().to_string(), )); - current_line_type = line_type; - current_line_number = line_number; - current_line_string = String::new(); } } - let mut hotkeys: Vec = Vec::new(); + drop(lines); for (i, item) in actual_lines.iter().enumerate() { let line_type = item.0; let line_number = item.1; let line = &item.2; + if line_type == "unbind" { + let to_unbind = line.trim_start_matches(UNBIND_STATEMENT).trim(); + modes[current_mode].unbinds.push(parse_keybind( + path.clone(), + to_unbind, + line_number + 1, + &key_to_evdev_key, + &mod_to_mod_enum, + )?); + } + + if line_type == "modestart" { + let modename = line.split(' ').nth(1).unwrap(); + modes.push(Mode::new(modename.to_string())); + current_mode = modes.len() - 1; + } + + if line_type == "modeend" { + current_mode = 0; + } + if line_type != "keysym" { continue; } @@ -286,1063 +569,231 @@ fn parse_contents(contents: String) -> Result, Error> { let extracted_keys = extract_curly_brace(line); let extracted_commands = extract_curly_brace(&next_line.2); - 'hotkey_parse: for (key, command) in extracted_keys.iter().zip(extracted_commands.iter()) { - println!("{} {}", key, command); - let (mode, keysym, modifiers, consume) = - parse_keybind(key, line_number + 1, &key_to_evdev_key, &mod_to_mod_enum)?; - let hotkey = Hotkey { mode, keysym, modifiers, command: command.to_string(), consume }; - - // Ignore duplicate hotkeys - for i in hotkeys.iter() { - if i.keysym == hotkey.keysym && i.modifiers == hotkey.modifiers { - continue 'hotkey_parse; - } - } - - hotkeys.push(hotkey); + for (key, command) in extracted_keys.iter().zip(extracted_commands.iter()) { + let keybinding = parse_keybind( + path.clone(), + key, + line_number + 1, + &key_to_evdev_key, + &mod_to_mod_enum, + )?; + let hotkey = Hotkey::from_keybinding(keybinding, command.to_string()); + + // Override latter + modes[current_mode].hotkeys.retain(|h| h.keybinding != hotkey.keybinding); + modes[current_mode].hotkeys.push(hotkey); } } - Ok(hotkeys) + + Ok(modes) } // We need to get the reference to key_to_evdev_key // and mod_to_mod enum instead of recreating them // after each function call because it's too expensive fn parse_keybind( + path: PathBuf, line: &str, line_nr: u32, - key_to_evdev_key: &HashMap<&str, rdev::Key>, + key_to_evdev_key: &HashMap<&str, evdev::Key>, mod_to_mod_enum: &HashMap<&str, Modifier>, -) -> Result<(Option, rdev::Key, Vec, bool), Error> { +) -> Result { let line = line.split('#').next().unwrap(); let tokens: Vec = line.split('+').map(|s| s.trim().to_lowercase()).filter(|s| s != "_").collect(); - let last_token = tokens.last().unwrap().trim(); - // Check if each token is valid - for token in &tokens { - if token.starts_with("[") && token.ends_with("]") { - continue; - } - if token.to_string() == "!" { continue; } - if key_to_evdev_key.contains_key(token.as_str()) { - // Can't have a key that's like a modifier - if token != last_token { - return Err(Error::InvalidConfig(ParseError::InvalidModifier(line_nr))); - } - } else if mod_to_mod_enum.contains_key(token.as_str()) { - // Can't have a modifier that's like a modifier - if token == last_token { - return Err(Error::InvalidConfig(ParseError::InvalidKeysym(line_nr))); - } - } else { - return Err(Error::InvalidConfig(ParseError::UnknownSymbol(line_nr))); + let mut tokens_new = Vec::new(); + for mut token in tokens { + while token.trim().starts_with('_') { + token = token.trim().strip_prefix('_').unwrap().to_string(); } + tokens_new.push(token.trim().to_string()); } - // Translate keypress into evdev key - let keysym = key_to_evdev_key.get(last_token).unwrap(); - let consume = if tokens - .iter() - .filter(|s| s.to_string() == "!") - .map(|s| s.to_string()) - .collect::>() - .len() == 1 { true } else { false }; - log::trace!("Consume: {}", consume); - let mode = match tokens - .iter() - .filter(|s| s.starts_with("[") && s.ends_with("]")) - .map(|s| s.replace(&['[', ']'][..], "")) - .collect::>() - .get(0) { - Some(z) => Some(z.to_string()), - None => { - log::trace!("No mode specified"); - None - } - }; - let mut mod_index = if mode.is_some() { 1 } else { 0 }; - // make sure to push index forward - if consume { - mod_index += 1; - } - let modifiers: Vec = tokens[mod_index..(tokens.len() - 1)] - .iter() - .map(|token| *mod_to_mod_enum.get(token.as_str()).unwrap()) - .collect(); + let last_token = tokens_new.last().unwrap().trim(); - Ok((mode, *keysym, modifiers, consume)) -} + // Check if last_token is prefixed with @ or ~ or even both. + // If prefixed @, on_release = true; if prefixed ~, send = true + let send = last_token.starts_with('~') || last_token.starts_with("@~"); + let on_release = last_token.starts_with('@') || last_token.starts_with("~@"); -fn extract_curly_brace(line: &str) -> Vec { - if !line.is_ascii() { - return vec![line.to_string()]; - } - let mut output: Vec = Vec::new(); - - let index_open_brace = line.chars().position(|c| c == '{'); - let index_close_brace = line.chars().position(|c| c == '}'); - - if index_open_brace.is_none() || index_close_brace.is_none() { - return vec![line.to_string()]; - } - - let start_index = index_open_brace.unwrap(); - let end_index = index_close_brace.unwrap(); - - // There are no expansions to build if } is earlier than { - if start_index >= end_index { - return vec![line.to_string()]; - } - - let str_before_braces = line[..start_index].to_string(); - let str_after_braces = line[end_index + 1..].to_string(); - - let comma_separated_items = line[start_index + 1..end_index].split(','); - - for item in comma_separated_items { - let mut push_one_item = || { - output.push(format!("{}{}{}", str_before_braces, item.trim(), str_after_braces)); - }; - - if !item.contains('-') { - push_one_item(); - continue; - } - - // Parse dash ranges like {1-5} and {a-f} - - let mut range = item.split('-').map(|s| s.trim()); - let begin_char: &str; - let end_char: &str; - - if let Some(b) = range.next() { - begin_char = b; + // Delete the @ and ~ in the last token + fn strip_at(token: &str) -> &str { + if token.starts_with('@') { + let token = token.strip_prefix('@').unwrap(); + strip_tilde(token) + } else if token.starts_with('~') { + strip_tilde(token) } else { - push_one_item(); - continue; + token } + } - if let Some(e) = range.next() { - end_char = e; + fn strip_tilde(token: &str) -> &str { + if token.starts_with('~') { + let token = token.strip_prefix('~').unwrap(); + strip_at(token) + } else if token.starts_with('@') { + strip_at(token) } else { - push_one_item(); - continue; + token } - - // Do not accept range values that are longer than one char - // Example invalid: {ef-p} {3-56} - // Beginning of the range cannot be greater than end - // Example invalid: {9-4} {3-2} - if begin_char.len() != 1 || end_char.len() != 1 || begin_char > end_char { - push_one_item(); - continue; - } - - // In sohkd we will parse the full range using ASCII values. - - let begin_ascii_val = begin_char.parse::().unwrap() as u8; - let end_ascii_val = end_char.parse::().unwrap() as u8; - - for ascii_number in begin_ascii_val..=end_ascii_val { - output - .push(format!("{}{}{}", str_before_braces, ascii_number as char, str_after_braces)); - } - } - output -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use std::io::Write; - - // Implement a struct for a path used in tests - // so that the test file will be automatically removed - // no matter how the test goes - struct TestPath { - path: path::PathBuf, } - impl TestPath { - fn new(path: &str) -> Self { - TestPath { path: path::PathBuf::from(path) } - } - - // Create a path method for a more succinct way - // to deal with borrowing the path value - fn path(&self) -> path::PathBuf { - self.path.clone() - } - } + let last_token = strip_at(last_token); - impl Drop for TestPath { - fn drop(self: &mut TestPath) { - if self.path.exists() { - fs::remove_file(self.path()).unwrap(); + // Check if each token is valid + for token in &tokens_new { + let token = strip_at(token); + if key_to_evdev_key.contains_key(token) { + // Can't have a keysym that's like a modifier + if token != last_token { + return Err(Error::InvalidConfig(ParseError::InvalidModifier(path, line_nr))); } + } else if mod_to_mod_enum.contains_key(token) { + // Can't have a modifier that's like a keysym + /*if token == last_token { + return Err(Error::InvalidConfig(ParseError::InvalidKeysym(path, line_nr))); + }*/ + } else { + return Err(Error::InvalidConfig(ParseError::UnknownSymbol(path, line_nr))); } } - // Wrapper for config tests - fn eval_config_test(contents: &str, expected_hotkeys: Vec) -> std::io::Result<()> { - let result = parse_contents(contents.to_string()); - - let mut expected_hotkeys_mut = expected_hotkeys; - - if result.is_err() { - panic!("Expected Ok config, found Err {:?}", result.unwrap_err()); - } - - let actual_hotkeys = result.unwrap(); - - assert_eq!(actual_hotkeys.len(), expected_hotkeys_mut.len()); - - // Go through each actual hotkey, and pop a corresponding - // hotkey from the expected hotkeys - // to make sure that order does not matter - for hotkey in actual_hotkeys { - if let Some(index) = expected_hotkeys_mut.iter().position(|key| { - key.keysym == hotkey.keysym - && key.command == hotkey.command - && key.modifiers == hotkey.modifiers - }) { - expected_hotkeys_mut.remove(index); - } else { - panic!( - "unexpected hotkey {:#?} found in result\nExpected result:\n{:#?}", - hotkey, expected_hotkeys_mut - ); - } - } + // Translate keypress into evdev key + let keysym = match key_to_evdev_key.get(last_token) { + Some(k) => Some(*k), + _ => None + }; - if !expected_hotkeys_mut.is_empty() { - panic!( - "Some hotkeys were not returned by the actual result:\n{:#?}", - expected_hotkeys_mut - ); - } + let modifiers: Vec = tokens_new + .iter() + .filter_map(|token| match mod_to_mod_enum.get(token.as_str()) { + Some(m) => Some(*m), + _ => None, + }) + .collect(); - Ok(()) + let mut keybinding = KeyBinding::new(keysym, modifiers); + if send { + keybinding = keybinding.send(); } - - // Wrapper for the many error tests - fn eval_invalid_config_test( - contents: &str, - parse_error_type: ParseError, - ) -> std::io::Result<()> { - let result = parse_contents(contents.to_string()); - - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Check if the Error type is InvalidConfig - let result = match result { - Error::InvalidConfig(parse_err) => parse_err, - _ => panic!(), - }; - - // Check the ParseError enum type - if result != parse_error_type { - panic!("ParseError: Expected `{:?}`, found `{:?}`", parse_error_type, result); - } - - Ok(()) + if on_release { + keybinding = keybinding.on_release(); } + Ok(keybinding) +} - #[test] - fn test_nonexistent_file() { - let path = path::PathBuf::from(r"This File Doesn't Exist"); - - let result = load_file_contents(path); - - assert!(result.is_err()); +pub fn extract_curly_brace(line: &str) -> Vec { + if !line.contains('{') || !line.contains('}') || !line.is_ascii() { + return vec![line.to_string()]; + } - match result.unwrap_err() { - Error::ConfigNotFound => {} - _ => { - panic!("Error type for nonexistent file is wrong."); + // go through each character in the line and mark the position of each { and } + // if a { is not followed by a }, return the line as is + let mut brace_positions: Vec = Vec::new(); + let mut flag = false; + for (i, c) in line.chars().enumerate() { + if c == '{' { + if flag { + return vec![line.to_string()]; } + brace_positions.push(i); + flag = true; + } else if c == '}' { + if !flag { + return vec![line.to_string()]; + } + brace_positions.push(i); + flag = false; } } - #[test] - fn test_existing_file() -> std::io::Result<()> { - let setup = TestPath::new("/tmp/sohkd-test-file1"); - // Build a dummy file in /tmp - let mut f = File::create(setup.path())?; - f.write_all( - b" -x - dmenu_run - -q - bspc node -q", - )?; - - let result = load_file_contents(setup.path()); - assert!(result.is_ok()); - Ok(()) - } - - #[test] - fn test_basic_keybind() -> std::io::Result<()> { - let contents = " -r - alacritty - "; - - eval_config_test( - contents, - vec![Hotkey::new("".to_string(), evdev::Key::KEY_R, vec![], String::from("alacritty"))], - ) - } - - #[test] - fn test_multiple_keybinds() -> std::io::Result<()> { - let contents = " -r - alacritty - -w - kitty - -t - /bin/firefox - "; - - let hotkey_1 = Hotkey::new("".to_string(), evdev::Key::KEY_R, vec![], String::from("alacritty")); - let hotkey_2 = Hotkey::new("".to_string(), evdev::Key::KEY_W, vec![], String::from("kitty")); - let hotkey_3 = Hotkey::new("".to_string(), evdev::Key::KEY_T, vec![], String::from("/bin/firefox")); - - eval_config_test(contents, vec![hotkey_1, hotkey_2, hotkey_3]) - } - - #[test] - fn test_comments() -> std::io::Result<()> { - let contents = " -r - alacritty - -w - kitty - -#t - #/bin/firefox - "; - - let expected_keybinds = vec![ - Hotkey::new("".to_string(), evdev::Key::KEY_R, vec![], String::from("alacritty")), - Hotkey::new("".to_string(), evdev::Key::KEY_W, vec![], String::from("kitty")), - ]; - - eval_config_test(contents, expected_keybinds) - } - - #[test] - fn test_multiple_keypress() -> std::io::Result<()> { - let contents = " -super + 5 - alacritty - "; - - let expected_keybinds = - vec![Hotkey::new("".to_string(), evdev::Key::KEY_5, vec![Modifier::Super], String::from("alacritty"))]; - - eval_config_test(contents, expected_keybinds) - } - - #[test] - fn test_keysym_instead_of_modifier() -> std::io::Result<()> { - let contents = " -shift + k + m - notify-send 'Hello world!' - "; - - eval_invalid_config_test(contents, ParseError::InvalidModifier(2)) - } - - #[test] - fn test_modifier_instead_of_keysym() -> std::io::Result<()> { - let contents = " -shift + k + alt - notify-send 'Hello world!' - "; - - eval_invalid_config_test(contents, ParseError::InvalidModifier(2)) - } - - #[test] - fn test_unfinished_plus_sign() -> std::io::Result<()> { - let contents = " - - -shift + alt + - notify-send 'Hello world!' - "; - - eval_invalid_config_test(contents, ParseError::UnknownSymbol(4)) - } - - #[test] - fn test_plus_sign_at_start() -> std::io::Result<()> { - let contents = " -+ shift + k - notify-send 'Hello world!' - "; - - eval_invalid_config_test(contents, ParseError::UnknownSymbol(2)) - } - - #[test] - fn test_common_modifiers() -> std::io::Result<()> { - let contents = " -shift + k - notify-send 'Hello world!' - -control + 5 - notify-send 'Hello world!' - -alt + 2 - notify-send 'Hello world!' - -super + z - notify-send 'Hello world!' - "; - - let expected_hotkeys = vec![ - Hotkey::new( - "".to_string(), - evdev::Key::KEY_K, - vec![Modifier::Shift], - "notify-send 'Hello world!'".to_string(), - ), - Hotkey::new( - "".to_string(), - evdev::Key::KEY_5, - vec![Modifier::Control], - "notify-send 'Hello world!'".to_string(), - ), - Hotkey::new( - "".to_string(), - evdev::Key::KEY_2, - vec![Modifier::Alt], - "notify-send 'Hello world!'".to_string(), - ), - Hotkey::new( - "".to_string(), - evdev::Key::KEY_Z, - vec![Modifier::Super], - "notify-send 'Hello world!'".to_string(), - ), - ]; - - eval_config_test(contents, expected_hotkeys) - } - - #[test] - fn test_command_with_many_spaces() -> std::io::Result<()> { - let contents = " -p - xbacklight -inc 10 -fps 30 -time 200 - "; - - let expected_keybinds = vec![Hotkey::new( - evdev::Key::KEY_P, - vec![], - String::from("xbacklight -inc 10 -fps 30 -time 200"), - )]; - - eval_config_test(contents, expected_keybinds) - } - - #[test] - fn test_invalid_keybinding() -> std::io::Result<()> { - let contents = " -p - xbacklight -inc 10 -fps 30 -time 200 - -pesto - xterm - "; - - eval_invalid_config_test(contents, ParseError::UnknownSymbol(5)) - } - - #[test] - // keysyms not followed by command should be ignored - fn test_no_command() -> std::io::Result<()> { - let contents = " -k - xbacklight -inc 10 -fps 30 -time 200 - -w - - "; - - eval_config_test( - contents, - vec![Hotkey::new( - evdev::Key::KEY_K, - vec![], - "xbacklight -inc 10 -fps 30 -time 200".to_string(), - )], - ) - } + // now we have a list of positions of { and } + // we should extract the items between each pair of braces and store them in a vector + let mut items: Vec = Vec::new(); + let mut remaining_line: Vec = Vec::new(); + let mut start_index = 0; + for i in brace_positions.chunks(2) { + items.push(line[i[0] + 1..i[1]].to_string()); + remaining_line.push(line[start_index..i[0]].to_string()); + start_index = i[1] + 1; + } + + // now we have a list of items between each pair of braces + // we should extract the items between each comma and store them in a vector + let mut tokens_vec: Vec> = Vec::new(); + for item in items { + // Edge case: escape periods + // example: + // ``` + // super + {\,, .} + // riverctl focus-output {previous, next} + // ``` + let item = item.replace("\\,", "comma"); + + let items: Vec = item.split(',').map(|s| s.trim().to_string()).collect(); + tokens_vec.push(handle_ranges(items)); + } + + fn handle_ranges(items: Vec) -> Vec { + let mut output: Vec = Vec::new(); + for item in items { + if !item.contains('-') { + output.push(item); + continue; + } + let mut range = item.split('-').map(|s| s.trim()); - #[test] - fn test_real_config_snippet() -> std::io::Result<()> { - let contents = " -# reloads sxhkd configuration: -super + Escape - pkill -USR1 -x sxhkd ; sxhkd & - -# Launch Terminal -super + Return - alacritty -t \"Terminal\" -e \"$HOME/.config/sxhkd/new_tmux_terminal.sh\" - -# terminal emulator (no tmux) -super + shift + Return - alacritty -t \"Terminal\" - -# terminal emulator (new tmux session) -alt + Return - alacritty -t \"Terminal\" -e \"tmux\" - -ctrl + 0 - play-song.sh - -super + minus - play-song.sh album - "; - - let expected_result: Vec = vec![ - Hotkey::new( - evdev::Key::KEY_ESC, - vec![Modifier::Super], - String::from("pkill -USR1 -x sxhkd ; sxhkd &"), - ), - Hotkey::new( - evdev::Key::KEY_ENTER, - vec![Modifier::Super], - String::from( - "alacritty -t \"Terminal\" -e \"$HOME/.config/sxhkd/new_tmux_terminal.sh\"", - ), - ), - Hotkey::new( - evdev::Key::KEY_ENTER, - vec![Modifier::Super, Modifier::Shift], - String::from("alacritty -t \"Terminal\""), - ), - Hotkey::new( - evdev::Key::KEY_ENTER, - vec![Modifier::Alt], - String::from("alacritty -t \"Terminal\" -e \"tmux\""), - ), - Hotkey::new(evdev::Key::KEY_0, vec![Modifier::Control], String::from("play-song.sh")), - Hotkey::new( - evdev::Key::KEY_MINUS, - vec![Modifier::Super], - String::from("play-song.sh album"), - ), - ]; - - eval_config_test(contents, expected_result) - } + let begin_char: &str = if let Some(b) = range.next() { + b + } else { + output.push(item); + continue; + }; - #[test] - fn test_multiline_command() -> std::io::Result<()> { - let contents = " -k - mpc ls | dmenu | \\ - sed -i 's/foo/bar/g' - "; - - let expected_keybind = Hotkey::new( - evdev::Key::KEY_K, - vec![], - String::from("mpc ls | dmenu | sed -i 's/foo/bar/g'"), - ); - - eval_config_test(contents, vec![expected_keybind]) - } + let end_char: &str = if let Some(e) = range.next() { + e + } else { + output.push(item); + continue; + }; + + // Do not accept range values that are longer than one char + // Example invalid: {ef-p} {3-56} + // Beginning of the range cannot be greater than end + // Example invalid: {9-4} {3-2} + if begin_char.len() != 1 || end_char.len() != 1 || begin_char > end_char { + output.push(item); + continue; + } - #[test] - fn test_commented_out_keybind() -> std::io::Result<()> { - let contents = " -#w - gimp - "; + // In sohkd we will parse the full range using ASCII values. - eval_config_test(contents, vec![]) - } + let begin_ascii_val = begin_char.parse::().unwrap() as u8; + let end_ascii_val = end_char.parse::().unwrap() as u8; - // TODO: Write these tests as needed. - - #[test] - fn test_all_alphanumeric() -> std::io::Result<()> { - let symbols: [&str; 36] = [ - "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", - "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", - "8", "9", - ]; - let keysyms: [evdev::Key; 36] = [ - evdev::Key::KEY_A, - evdev::Key::KEY_B, - evdev::Key::KEY_C, - evdev::Key::KEY_D, - evdev::Key::KEY_E, - evdev::Key::KEY_F, - evdev::Key::KEY_G, - evdev::Key::KEY_H, - evdev::Key::KEY_I, - evdev::Key::KEY_J, - evdev::Key::KEY_K, - evdev::Key::KEY_L, - evdev::Key::KEY_M, - evdev::Key::KEY_N, - evdev::Key::KEY_O, - evdev::Key::KEY_P, - evdev::Key::KEY_Q, - evdev::Key::KEY_R, - evdev::Key::KEY_S, - evdev::Key::KEY_T, - evdev::Key::KEY_U, - evdev::Key::KEY_V, - evdev::Key::KEY_W, - evdev::Key::KEY_X, - evdev::Key::KEY_Y, - evdev::Key::KEY_Z, - evdev::Key::KEY_0, - evdev::Key::KEY_1, - evdev::Key::KEY_2, - evdev::Key::KEY_3, - evdev::Key::KEY_4, - evdev::Key::KEY_5, - evdev::Key::KEY_6, - evdev::Key::KEY_7, - evdev::Key::KEY_8, - evdev::Key::KEY_9, - ]; - - let mut contents = String::new(); - for symbol in &symbols { - contents.push_str(&format!("{}\n st\n", symbol)); + for ascii_number in begin_ascii_val..=end_ascii_val { + output.push((ascii_number as char).to_string()); + } } - let contents = &contents; - - let expected_result: Vec = - keysyms.iter().map(|keysym| Hotkey::new(*keysym, vec![], "st".to_string())).collect(); - - eval_config_test(contents, expected_result) + output } - #[test] - fn test_homerow_special_keys_top() -> std::io::Result<()> { - let symbols: [&str; 7] = - ["Escape", "BackSpace", "Return", "Tab", "minus", "equal", "grave"]; - - let keysyms: [evdev::Key; 7] = [ - evdev::Key::KEY_ESC, - evdev::Key::KEY_BACKSPACE, - evdev::Key::KEY_ENTER, - evdev::Key::KEY_TAB, - evdev::Key::KEY_MINUS, - evdev::Key::KEY_EQUAL, - evdev::Key::KEY_GRAVE, - ]; - - let mut contents = String::new(); - for symbol in &symbols { - contents.push_str(&format!("{}\n st\n", symbol)); + // now write the tokens back to the line and output a vector + let mut output: Vec = Vec::new(); + // generate a cartesian product iterator for all the vectors in tokens_vec + let cartesian_product_iter = tokens_vec.iter().multi_cartesian_product(); + for tokens in cartesian_product_iter.collect_vec() { + let mut line_to_push = String::new(); + for i in 0..remaining_line.len() { + line_to_push.push_str(&remaining_line[i]); + line_to_push.push_str(tokens[i]); } - let contents = &contents; - - let expected_result: Vec = - keysyms.iter().map(|keysym| Hotkey::new(*keysym, vec![], "st".to_string())).collect(); - - eval_config_test(contents, expected_result) - } - - #[test] - fn test_case_insensitive() -> std::io::Result<()> { - let contents = " -Super + SHIFT + alt + a - st -ReTurn - ts - "; - eval_config_test( - contents, - vec![ - Hotkey::new( - evdev::Key::KEY_A, - vec![Modifier::Super, Modifier::Shift, Modifier::Alt], - "st".to_string(), - ), - Hotkey::new(evdev::Key::KEY_ENTER, vec![], "ts".to_string()), - ], - ) - } - - #[test] - fn test_duplicate_hotkeys() -> std::io::Result<()> { - let contents = " -super + a - st -suPer + A - ts -b - st -B - ts -"; - eval_config_test( - contents, - vec![ - Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "st".to_string()), - Hotkey::new(evdev::Key::KEY_B, vec![], "st".to_string()), - ], - ) - } - - #[test] - fn test_inline_comment() -> std::io::Result<()> { - let contents = " -super + a #comment and comment super - st -super + shift + b - ts #this comment should be handled by shell -" - .to_string(); - eval_config_test( - &contents, - vec![ - Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "st".to_string()), - Hotkey::new( - evdev::Key::KEY_B, - vec![Modifier::Super, Modifier::Shift], - "ts #this comment should be handled by shell".to_string(), - ), - ], - ) - } - - #[test] - fn test_blank_config() -> std::io::Result<()> { - let contents = ""; - - eval_config_test(contents, vec![]) - } - - #[test] - fn test_blank_config_with_whitespace() -> std::io::Result<()> { - let contents = " - - - "; - - eval_config_test(contents, vec![]) - } - - #[test] - fn test_extract_curly_brace() -> std::io::Result<()> { - let keybind_with_curly_brace = "super + {a,b,c}"; - assert_eq!( - extract_curly_brace(keybind_with_curly_brace), - vec!["super + a", "super + b", "super + c",] - ); - let command_with_curly_brace = "bspc node -p {west,south,north,west}"; - assert_eq!( - extract_curly_brace(command_with_curly_brace), - vec![ - "bspc node -p west", - "bspc node -p south", - "bspc node -p north", - "bspc node -p west", - ] - ); - let wrong_format = "super + }a, b, c{"; - assert_eq!(extract_curly_brace(wrong_format), vec![wrong_format]); - let single_sym = "super + {a}"; - assert_eq!(extract_curly_brace(single_sym), vec!["super + a"]); - Ok(()) - } - - #[test] - fn test_curly_brace() -> std::io::Result<()> { - let contents = " -super + {a,b,c} - {firefox, brave, chrome}"; - eval_config_test( - contents, - vec![ - Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), - Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), - Hotkey::new(evdev::Key::KEY_C, vec![Modifier::Super], "chrome".to_string()), - ], - ) - } - - #[test] - fn test_curly_brace_less_commands() -> std::io::Result<()> { - let contents = " -super + {a,b,c} - {firefox, brave}"; - eval_config_test( - contents, - vec![ - Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), - Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), - ], - ) - } - - #[test] - fn test_curly_brace_less_keysyms() -> std::io::Result<()> { - let contents = " -super + {a, b} - {firefox, brave, chrome}"; - eval_config_test( - contents, - vec![ - Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), - Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), - ], - ) - } - - #[test] - fn test_range_syntax() -> std::io::Result<()> { - let contents = " -super + {1-9,0} - bspc desktop -f '{1-9,0}'"; - eval_config_test( - contents, - vec![ - Hotkey::new( - evdev::Key::KEY_1, - vec![Modifier::Super], - "bspc desktop -f '1'".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_2, - vec![Modifier::Super], - "bspc desktop -f '2'".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_3, - vec![Modifier::Super], - "bspc desktop -f '3'".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_4, - vec![Modifier::Super], - "bspc desktop -f '4'".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_5, - vec![Modifier::Super], - "bspc desktop -f '5'".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_6, - vec![Modifier::Super], - "bspc desktop -f '6'".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_7, - vec![Modifier::Super], - "bspc desktop -f '7'".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_8, - vec![Modifier::Super], - "bspc desktop -f '8'".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_9, - vec![Modifier::Super], - "bspc desktop -f '9'".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_0, - vec![Modifier::Super], - "bspc desktop -f '0'".to_string(), - ), - ], - ) - } - - #[test] - fn test_range_syntax_ascii_character() -> std::io::Result<()> { - let contents = " -super + {a-c} - {firefox, brave, chrome}"; - eval_config_test( - contents, - vec![ - Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), - Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), - Hotkey::new(evdev::Key::KEY_C, vec![Modifier::Super], "chrome".to_string()), - ], - ) - } - - #[test] - fn test_range_syntax_not_ascii() -> std::io::Result<()> { - let contents = " -super + {a-是} - {firefox, brave} - "; - eval_invalid_config_test(contents, ParseError::UnknownSymbol(2)) - } - - #[test] - fn test_range_syntax_invalid_range() -> std::io::Result<()> { - let contents = " -super + {bc-ad} - {firefox, brave} - "; - eval_invalid_config_test(contents, ParseError::UnknownSymbol(2)) - } - - #[test] - fn test_ranger_syntax_not_full_range() -> std::io::Result<()> { - let contents = " -super + {a-} - {firefox, brave}"; - eval_invalid_config_test(contents, ParseError::UnknownSymbol(2)) - } - - #[test] - fn test_none() -> std::io::Result<()> { - let contents = " -super + {_, shift} + b - {firefox, brave}"; - eval_config_test( - contents, - vec![ - Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "firefox".to_string()), - Hotkey::new( - evdev::Key::KEY_B, - vec![Modifier::Super, Modifier::Shift], - "brave".to_string(), - ), - ], - ) - } - - #[test] - #[ignore] - // TODO: handle multiple ranges - fn test_multiple_ranges() -> std::io::Result<()> { - let contents = " -super + {shift,alt} + {c,d} - {librewolf, firefox} {--sync, --help} - "; - - eval_config_test( - contents, - vec![ - Hotkey::new( - evdev::Key::KEY_C, - vec![Modifier::Super, Modifier::Shift], - "librewolf --sync".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_D, - vec![Modifier::Super, Modifier::Shift], - "librewolf --help".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_C, - vec![Modifier::Super, Modifier::Alt], - "firefox --sync".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_D, - vec![Modifier::Super, Modifier::Alt], - "firefox --help".to_string(), - ), - ], - ) - } - - #[test] - #[ignore] - fn test_multiple_ranges_numbers() -> std::io::Result<()> { - let contents = " -{control,super} + {1-3} - {notify-send, echo} {hello,how,are} - "; - - eval_config_test( - contents, - vec![ - Hotkey::new( - evdev::Key::KEY_1, - vec![Modifier::Control], - "notify-send hello".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_2, - vec![Modifier::Control], - "notify-send how".to_string(), - ), - Hotkey::new( - evdev::Key::KEY_3, - vec![Modifier::Control], - "notify-send are".to_string(), - ), - Hotkey::new(evdev::Key::KEY_1, vec![Modifier::Super], "echo hello".to_string()), - Hotkey::new(evdev::Key::KEY_2, vec![Modifier::Super], "echo how".to_string()), - Hotkey::new(evdev::Key::KEY_3, vec![Modifier::Super], "echo are".to_string()), - ], - ) - } -} - -#[cfg(test)] -mod display_test { - use super::*; - use std::io; - - #[test] - fn test_display_config_not_found_error() { - let error = Error::ConfigNotFound; - - assert_eq!(format!("{}", error), "Config file not found."); - } - - #[test] - fn test_display_io_error() { - let error = Error::Io(io::Error::from(io::ErrorKind::UnexpectedEof)); - - if !format!("{}", error).contains("unexpected end of file") { - panic!("Error message was '{}", error); + if brace_positions[brace_positions.len() - 1] < line.len() - 1 { + line_to_push.push_str(&line[brace_positions[brace_positions.len() - 1] + 1..]); } + output.push(line_to_push); } - - #[test] - fn test_display_unknown_symbol_error() { - let error = Error::InvalidConfig(ParseError::UnknownSymbol(10)); - - assert_eq!(format!("{}", error), "Unknown symbol at line 10."); - } - - #[test] - fn test_display_invalid_modifier_error() { - let error = Error::InvalidConfig(ParseError::InvalidModifier(25)); - - assert_eq!(format!("{}", error), "Invalid modifier at line 25."); - } - - #[test] - fn test_invalid_keysm_error() { - let error = Error::InvalidConfig(ParseError::InvalidKeysym(7)); - - assert_eq!(format!("{}", error), "Invalid keysym at line 7."); - } + output } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d8eb905 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,540 @@ +use crate::config::Value; +use serde_json; +use clap::{arg, Command}; +use evdev::{AttributeSet, Device, InputEventKind, Key}; +use nix::{ + sys::stat::{umask, Mode}, + unistd::{Group, Uid}, +}; +use signal_hook::consts::signal::*; +use signal_hook_tokio::Signals; +use std::{ + collections::{HashMap, HashSet}, + env, + error::Error, + fs, + fs::Permissions, + io::prelude::*, + os::unix::{fs::PermissionsExt, net::UnixStream}, + path::{Path, PathBuf}, + process::{exit, id}, +}; +use sysinfo::{ProcessExt, System, SystemExt}; +use tokio::select; +use tokio::time::Duration; +use tokio::time::{sleep, Instant}; +use tokio_stream::{StreamExt, StreamMap}; + +mod config; +mod perms; +mod uinput; + +#[cfg(test)] +mod tests; + +struct KeyboardState { + state_modifiers: HashSet, + state_keysyms: AttributeSet, +} + +impl KeyboardState { + fn new() -> KeyboardState { + KeyboardState { state_modifiers: HashSet::new(), state_keysyms: AttributeSet::new() } + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = set_command_line_args().get_matches(); + env::set_var("RUST_LOG", "sohkd=warn"); + + if args.is_present("debug") { + env::set_var("RUST_LOG", "sohkd=trace"); + } + + env_logger::init(); + log::trace!("Logger initialized."); + + let invoking_uid = match env::var("PKEXEC_UID") { + Ok(uid) => { + let uid = uid.parse::().unwrap(); + log::trace!("Invoking UID: {}", uid); + uid + } + Err(_) => { + log::error!("Failed to launch sohkd!!!"); + log::error!("Make sure to launch the binary with pkexec."); + exit(1); + } + }; + + setup_sohkd(invoking_uid); + + let load_config = || { + // Drop privileges to the invoking user. + perms::drop_privileges(invoking_uid); + + let config_file_path: PathBuf = if args.is_present("config") { + Path::new(args.value_of("config").unwrap()).to_path_buf() + } else { + fetch_xdg_config_path() + }; + + log::debug!("Using config file path: {:#?}", config_file_path); + + let modes = match config::load(&config_file_path) { + Err(e) => { + log::error!("Config Error: {}", e); + exit(1) + } + Ok(out) => out, + }; + + modes + }; + + let mut modes = load_config(); + let mut mode_stack: Vec = vec![0]; + + macro_rules! send_command { + ($hotkey: expr, $socket_path: expr) => { + log::info!("Hotkey pressed: {:#?}", $hotkey); + let command = $hotkey.command; + let mut commands_to_send = String::new(); + if command.contains('@') { + let commands = command.split("&&").map(|s| s.trim()).collect::>(); + for cmd in commands { + match cmd.split(' ').next().unwrap() { + config::MODE_ENTER_STATEMENT => { + let enter_mode = cmd.split(' ').nth(1).unwrap(); + for (i, mode) in modes.iter().enumerate() { + if mode.name == enter_mode { + mode_stack.push(i); + break; + } + } + log::info!( + "Entering mode: {}", + modes[mode_stack[mode_stack.len() - 1]].name + ); + } + config::MODE_ESCAPE_STATEMENT => { + mode_stack.pop(); + } + config::ODILIA_SEND_STATEMENT => { + log::debug!("Odilia event statement matched"); + } + _ => commands_to_send.push_str(format!("{cmd} &&").as_str()), + } + } + } else { + commands_to_send = command; + } + if commands_to_send.ends_with("&& ") { + commands_to_send = commands_to_send.strip_suffix("&& ").unwrap().to_string(); + } + if let Err(e) = socket_write(&commands_to_send, $socket_path.to_path_buf()) { + log::error!("Failed to send command to swhks through IPC."); + log::error!("Please make sure that swhks is running."); + log::error!("Err: {:#?}", e) + } + }; + } + + // Escalate back to the root user after reading the config file. + perms::raise_privileges(); + + let keyboard_devices: Vec = { + if let Some(arg_devices) = args.values_of("device") { + // for device in arg_devices { + // let device_path = Path::new(device); + // if let Ok(device_to_use) = Device::open(device_path) { + // log::info!("Using device: {}", device_to_use.name().unwrap_or(device)); + // keyboard_devices.push(device_to_use); + // } + // } + let arg_devices = arg_devices.collect::>(); + evdev::enumerate() + .filter(|(_, device)| arg_devices.contains(&device.name().unwrap_or(""))) + .map(|(_, device)| device) + .collect() + } else { + log::trace!("Attempting to find all keyboard file descriptors."); + evdev::enumerate().filter(check_device_is_keyboard).map(|(_, device)| device).collect() + } + }; + + if keyboard_devices.is_empty() { + log::error!("No valid keyboard device was detected!"); + exit(1); + } + + log::debug!("{} Keyboard device(s) detected.", keyboard_devices.len()); + + let mut uinput_device = match uinput::create_uinput_device() { + Ok(dev) => dev, + Err(e) => { + log::error!("Err: {:#?}", e); + exit(1); + } + }; + + let modifiers_map: HashMap = HashMap::from([ + (Key::KEY_LEFTMETA, config::Modifier::Super), + (Key::KEY_RIGHTMETA, config::Modifier::Super), + (Key::KEY_LEFTALT, config::Modifier::Alt), + (Key::KEY_RIGHTALT, config::Modifier::Alt), + (Key::KEY_LEFTCTRL, config::Modifier::Control), + (Key::KEY_RIGHTCTRL, config::Modifier::Control), + (Key::KEY_LEFTSHIFT, config::Modifier::Shift), + (Key::KEY_RIGHTSHIFT, config::Modifier::Shift), + (Key::KEY_CAPSLOCK, config::Modifier::CapsLock), + ]); + + let repeat_cooldown_duration: u64 = if args.is_present("cooldown") { + args.value_of("cooldown").unwrap().parse::().unwrap() + } else { + 250 + }; + + let mut signals = Signals::new(&[ + SIGUSR1, SIGUSR2, SIGHUP, SIGABRT, SIGBUS, SIGCHLD, SIGCONT, SIGINT, SIGPIPE, SIGQUIT, + SIGSYS, SIGTERM, SIGTRAP, SIGTSTP, SIGVTALRM, SIGXCPU, SIGXFSZ, + ])?; + + let mut execution_is_paused = false; + let mut last_hotkey: Option = None; + let mut pending_release: bool = false; + let mut keyboard_states: Vec = Vec::new(); + let mut keyboard_stream_map = StreamMap::new(); + + for (i, mut device) in keyboard_devices.into_iter().enumerate() { + let _ = device.grab(); + keyboard_stream_map.insert(i, device.into_event_stream()?); + keyboard_states.push(KeyboardState::new()); + } + + // The initial sleep duration is never read because last_hotkey is initialized to None + let hotkey_repeat_timer = sleep(Duration::from_millis(0)); + tokio::pin!(hotkey_repeat_timer); + + // The socket we're sending the commands to. + let socket_file_path = fetch_xdg_runtime_socket_path(); + loop { + select! { + _ = &mut hotkey_repeat_timer, if &last_hotkey.is_some() => { + let hotkey = last_hotkey.clone().unwrap(); + if hotkey.keybinding.on_release { + continue; + } + send_command!(hotkey.clone(), &socket_file_path); + hotkey_repeat_timer.as_mut().reset(Instant::now() + Duration::from_millis(repeat_cooldown_duration)); + } + + Some(signal) = signals.next() => { + match signal { + SIGUSR1 => { + execution_is_paused = true; + for (_, mut device) in evdev::enumerate().filter(check_device_is_keyboard) { + let _ = device.ungrab(); + } + } + + SIGUSR2 => { + execution_is_paused = false; + for (_, mut device) in evdev::enumerate().filter(check_device_is_keyboard) { + let _ = device.grab(); + } + } + + SIGHUP => { + modes = load_config(); + mode_stack = vec![0]; + } + + SIGINT => { + for (_, mut device) in evdev::enumerate().filter(check_device_is_keyboard) { + let _ = device.ungrab(); + } + log::warn!("Received SIGINT signal, exiting..."); + exit(1); + } + + _ => { + for (_, mut device) in evdev::enumerate().filter(check_device_is_keyboard) { + let _ = device.ungrab(); + } + + log::warn!("Received signal: {:#?}", signal); + log::warn!("Exiting..."); + exit(1); + } + } + } + + Some((i, Ok(command))) = keyboard_stream_map.next() => { + let keyboard_state = &mut keyboard_states[i]; + + let key = match command.kind() { + InputEventKind::Key(keycode) => keycode, + _ => continue + }; + + match command.value() { + // Key press + 1 => { + if let Some(modifier) = modifiers_map.get(&key) { + keyboard_state.state_modifiers.insert(*modifier); + } else { + keyboard_state.state_keysyms.insert(key); + } + } + + // Key release + 0 => { + if last_hotkey.is_some() && pending_release { + pending_release = false; + send_command!(last_hotkey.clone().unwrap(), &socket_file_path); + last_hotkey = None; + } + if let Some(modifier) = modifiers_map.get(&key) { + if let Some(hotkey) = &last_hotkey { + if hotkey.modifiers().contains(modifier) { + last_hotkey = None; + } + } + keyboard_state.state_modifiers.remove(modifier); + } else if keyboard_state.state_keysyms.contains(key) { + if let Some(hotkey) = &last_hotkey { + if hotkey.keysym().is_some() && key == hotkey.keysym().unwrap() { + last_hotkey = None; + } + } + keyboard_state.state_keysyms.remove(key); + } + } + + _ => {} + } + + let possible_hotkeys: Vec<&config::Hotkey> = modes[mode_stack[mode_stack.len() - 1]].hotkeys.iter() + .filter(|hotkey| hotkey.modifiers().len() == keyboard_state.state_modifiers.len()) + .collect(); + + let command_in_hotkeys = modes[mode_stack[mode_stack.len() - 1]].hotkeys.iter().any(|hotkey| { + ((hotkey.keysym().is_some() && + hotkey.keysym().unwrap().code() == command.code()) || + hotkey.keysym().is_none() && keyboard_state.state_keysyms.iter().count() == 0) && + (!keyboard_state.state_modifiers.is_empty() && hotkey.modifiers().contains(&config::Modifier::Any) || keyboard_state.state_modifiers + .iter() + .all(|x| hotkey.modifiers().contains(x)) && + keyboard_state.state_modifiers.len() == hotkey.modifiers().len()) + && !hotkey.is_send() + }); + + // Don't emit command to virtual device if it's from a valid hotkey + // TODO: this will make sure that individual capslock keys send without any other modifiers or keys pressed will ALWAYS be consumed. This should be an option. + if !command_in_hotkeys && !(keyboard_state.state_keysyms.iter().count() == 0 && keyboard_state.state_modifiers.len() == 1 && keyboard_state.state_modifiers.iter().all(|&m| m == config::Modifier::CapsLock)) { + uinput_device.emit(&[command]).unwrap(); + } + + if execution_is_paused || possible_hotkeys.is_empty() || last_hotkey.is_some() { + continue; + } + + log::debug!("state_modifiers: {:#?}", keyboard_state.state_modifiers); + log::debug!("state_keysyms: {:#?}", keyboard_state.state_keysyms); + log::debug!("hotkey: {:#?}", possible_hotkeys); + + for hotkey in possible_hotkeys { + // this should check if state_modifiers and hotkey.modifiers have the same elements + if (!keyboard_state.state_modifiers.is_empty() && hotkey.modifiers().contains(&config::Modifier::Any) || keyboard_state.state_modifiers.iter().all(|x| hotkey.modifiers().contains(x)) + && keyboard_state.state_modifiers.len() == hotkey.modifiers().len()) + && ((hotkey.keysym().is_some() + && keyboard_state.state_keysyms.contains(hotkey.keysym().unwrap())) + || (hotkey.keysym().is_none() + && keyboard_state.state_keysyms.iter().count() == 0 /* no keys are pressed that are not modiiers */)) + { + last_hotkey = Some(hotkey.clone()); + if pending_release { break; } + if hotkey.is_on_release() { + pending_release = true; + break; + } + send_command!(hotkey.clone(), &socket_file_path); + hotkey_repeat_timer.as_mut().reset(Instant::now() + Duration::from_millis(repeat_cooldown_duration)); + break; + } + } + } + } + } +} + +fn socket_write(command: &str, socket_path: PathBuf) -> Result<(), Box> { + let mut stream = UnixStream::connect(socket_path)?; + stream.write_all(command.as_bytes())?; + Ok(()) +} + +pub fn check_input_group() -> Result<(), Box> { + if !Uid::current().is_root() { + let groups = nix::unistd::getgroups(); + for (_, groups) in groups.iter().enumerate() { + for group in groups { + let group = Group::from_gid(*group); + if group.unwrap().unwrap().name == "input" { + log::error!("Note: INVOKING USER IS IN INPUT GROUP!!!!"); + log::error!("THIS IS A HUGE SECURITY RISK!!!!"); + } + } + } + log::error!("Consider using `pkexec sohkd ...`"); + exit(1); + } else { + log::warn!("Running sohkd as root!"); + Ok(()) + } +} + +pub fn check_device_is_keyboard(tup: &(PathBuf, Device)) -> bool { + let device = &tup.1; + if device.supported_keys().map_or(false, |keys| keys.contains(Key::KEY_ENTER)) { + if device.name() == Some("sohkd virtual output") { + return false; + } + log::debug!("Keyboard: {}", device.name().unwrap(),); + true + } else { + log::trace!("Other: {}", device.name().unwrap(),); + false + } +} + +pub fn set_command_line_args() -> Command<'static> { + let app = Command::new("sohkd") + .version(env!("CARGO_PKG_VERSION")) + .author(env!("CARGO_PKG_AUTHORS")) + .about("Simple Wayland HotKey Daemon") + .arg( + arg!(-c --config ) + .required(false) + .takes_value(true) + .help("Set a custom config file path."), + ) + .arg( + arg!(-C --cooldown ) + .required(false) + .takes_value(true) + .help("Set a custom repeat cooldown duration. Default is 250ms."), + ) + .arg(arg!(-d - -debug).required(false).help("Enable debug mode.")) + .arg( + arg!(-D --device ) + .required(false) + .takes_value(true) + .multiple_occurrences(true) + .help( + "Specific keyboard devices to use. Seperate multiple devices with semicolon.", + ), + ); + app +} + +pub fn fetch_xdg_config_path() -> PathBuf { + let config_file_path: PathBuf = match env::var("XDG_CONFIG_HOME") { + Ok(val) => { + log::debug!("XDG_CONFIG_HOME exists: {:#?}", val); + Path::new(&val).join("sohkd/sohkdrc") + } + Err(_) => { + log::error!("XDG_CONFIG_HOME has not been set."); + Path::new("/etc/sohkd/sohkdrc").to_path_buf() + } + }; + config_file_path +} + +pub fn fetch_xdg_runtime_socket_path() -> PathBuf { + match env::var("XDG_RUNTIME_DIR") { + Ok(val) => { + log::debug!("XDG_RUNTIME_DIR exists: {:#?}", val); + Path::new(&val).join("sohkd.sock") + } + Err(_) => { + log::error!("XDG_RUNTIME_DIR has not been set."); + Path::new(&format!("/run/user/{}/sohkd.sock", env::var("PKEXEC_UID").unwrap())) + .to_path_buf() + } + } +} + +pub fn setup_sohkd(invoking_uid: u32) { + // Set a sane process umask. + log::trace!("Setting process umask."); + umask(Mode::S_IWGRP | Mode::S_IWOTH); + + // Get the runtime path and create it if needed. + let runtime_path: String = match env::var("XDG_RUNTIME_DIR") { + Ok(runtime_path) => { + log::debug!("XDG_RUNTIME_DIR exists: {:#?}", runtime_path); + Path::new(&runtime_path).join("sohkd").to_str().unwrap().to_owned() + } + Err(_) => { + log::error!("XDG_RUNTIME_DIR has not been set."); + String::from("/run/sohkd/") + } + }; + if !Path::new(&runtime_path).exists() { + match fs::create_dir_all(Path::new(&runtime_path)) { + Ok(_) => { + log::debug!("Created runtime directory."); + match fs::set_permissions(Path::new(&runtime_path), Permissions::from_mode(0o600)) { + Ok(_) => log::debug!("Set runtime directory to readonly."), + Err(e) => log::error!("Failed to set runtime directory to readonly: {}", e), + } + } + Err(e) => log::error!("Failed to create runtime directory: {}", e), + } + } + + // Get the PID file path for instance tracking. + let pidfile: String = format!("{}sohkd_{}.pid", runtime_path, invoking_uid); + if Path::new(&pidfile).exists() { + log::trace!("Reading {} file and checking for running instances.", pidfile); + let sohkd_pid = match fs::read_to_string(&pidfile) { + Ok(sohkd_pid) => sohkd_pid, + Err(e) => { + log::error!("Unable to read {} to check all running instances", e); + exit(1); + } + }; + log::debug!("Previous PID: {}", sohkd_pid); + + // Check if sohkd is already running! + let mut sys = System::new_all(); + sys.refresh_all(); + for (pid, process) in sys.processes() { + if pid.to_string() == sohkd_pid && process.exe() == env::current_exe().unwrap() { + log::error!("Swhkd is already running!"); + log::error!("pid of existing sohkd process: {}", pid.to_string()); + log::error!("To close the existing sohkd process, run `sudo killall sohkd`"); + exit(1); + } + } + } + + // Write to the pid file. + match fs::write(&pidfile, id().to_string()) { + Ok(_) => {} + Err(e) => { + log::error!("Unable to write to {}: {}", pidfile, e); + exit(1); + } + } + + // Check if the user is in input group. + if check_input_group().is_err() { + exit(1); + } +} diff --git a/src/perms.rs b/src/perms.rs new file mode 100644 index 0000000..cd5a80b --- /dev/null +++ b/src/perms.rs @@ -0,0 +1,52 @@ +use nix::unistd::{Gid, Uid, User}; +use std::process::exit; + +pub fn drop_privileges(user_uid: u32) { + let user_uid = Uid::from_raw(user_uid); + let user = User::from_uid(user_uid).unwrap().unwrap(); + + set_initgroups(&user, user_uid.as_raw()); + set_egid(user_uid.as_raw()); + set_euid(user_uid.as_raw()); +} + +pub fn raise_privileges() { + let root_user = User::from_uid(Uid::from_raw(0)).unwrap().unwrap(); + + set_egid(0); + set_euid(0); + set_initgroups(&root_user, 0); +} + +fn set_initgroups(user: &nix::unistd::User, gid: u32) { + let gid = Gid::from_raw(gid); + match nix::unistd::initgroups(&user.gecos, gid) { + Ok(_) => log::debug!("Setting initgroups..."), + Err(e) => { + log::error!("Failed to set init groups: {:#?}", e); + exit(1); + } + } +} + +fn set_egid(gid: u32) { + let gid = Gid::from_raw(gid); + match nix::unistd::setegid(gid) { + Ok(_) => log::debug!("Setting EGID..."), + Err(e) => { + log::error!("Failed to set EGID: {:#?}", e); + exit(1); + } + } +} + +fn set_euid(uid: u32) { + let uid = Uid::from_raw(uid); + match nix::unistd::seteuid(uid) { + Ok(_) => log::debug!("Setting EUID..."), + Err(e) => { + log::error!("Failed to set EUID: {:#?}", e); + exit(1); + } + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..7c4fd3b --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,1240 @@ +mod test_config { + use crate::config::{ + extract_curly_brace, load, load_file_contents, parse_contents, Error, Hotkey, Modifier, + ParseError, Prefix, + }; + use std::fs; + use std::io::Write; + use std::{fs::File, path::PathBuf}; + + // Implement a struct for a path used in tests + // so that the test file will be automatically removed + // no matter how the test goes + struct TestPath { + path: PathBuf, + } + + impl TestPath { + fn new(path: &str) -> Self { + TestPath { path: PathBuf::from(path) } + } + + // Create a path method for a more succinct way + // to deal with borrowing the path value + fn path(&self) -> PathBuf { + self.path.clone() + } + } + + impl Drop for TestPath { + fn drop(self: &mut TestPath) { + if self.path.exists() { + fs::remove_file(self.path()).unwrap(); + } + } + } + + // Wrapper for config tests + fn eval_config_test(contents: &str, expected_hotkeys: Vec) -> std::io::Result<()> { + let result = parse_contents(PathBuf::new(), contents.to_string()); + + let mut expected_hotkeys_mut = expected_hotkeys; + + if result.is_err() { + panic!("Expected Ok config, found Err {:?}", result.unwrap_err()); + } + + let result = &result.unwrap()[0]; + let actual_hotkeys = &result.hotkeys; + + assert_eq!(actual_hotkeys.len(), expected_hotkeys_mut.len()); + + // Go through each actual hotkey, and pop a corresponding + // hotkey from the expected hotkeys + // to make sure that order does not matter + for hotkey in actual_hotkeys { + if let Some(index) = expected_hotkeys_mut.iter().position(|key| { + key.keybinding == hotkey.keybinding && key.command == hotkey.command + }) { + expected_hotkeys_mut.remove(index); + } else { + panic!( + "unexpected hotkey {:#?} found in result\nExpected result:\n{:#?}", + hotkey, expected_hotkeys_mut + ); + } + } + + if !expected_hotkeys_mut.is_empty() { + panic!( + "Some hotkeys were not returned by the actual result:\n{:#?}", + expected_hotkeys_mut + ); + } + + Ok(()) + } + + // Wrapper for the many error tests + fn eval_invalid_config_test( + contents: &str, + parse_error_type: ParseError, + ) -> std::io::Result<()> { + let result = parse_contents(PathBuf::new(), contents.to_string()); + + assert!(result.is_err()); + let result = result.unwrap_err(); + + // Check if the Error type is InvalidConfig + let result = match result { + Error::InvalidConfig(parse_err) => parse_err, + _ => panic!(), + }; + + // Check the ParseError enum type + if result != parse_error_type { + panic!("ParseError: Expected `{:?}`, found `{:?}`", parse_error_type, result); + } + + Ok(()) + } + + #[test] + fn test_nonexistent_file() { + let path = PathBuf::from(r"This File Doesn't Exist"); + + let result = load_file_contents(&path); + + assert!(result.is_err()); + + match result.unwrap_err() { + Error::ConfigNotFound => {} + _ => { + panic!("Error type for nonexistent file is wrong."); + } + } + } + + #[test] + fn test_existing_file() -> std::io::Result<()> { + let setup = TestPath::new("/tmp/sohkd-test-file1"); + // Build a dummy file in /tmp + let mut f = File::create(setup.path())?; + f.write_all( + b" +x + dmenu_run + +q + bspc node -q", + )?; + + let result = load_file_contents(&setup.path()); + assert!(result.is_ok()); + Ok(()) + } + + #[test] + fn test_load_multiple_config() -> std::io::Result<()> { + let setup = TestPath::new("/tmp/sohkd-test-file2"); + let mut f = File::create(setup.path())?; + f.write_all( + b" +include /tmp/sohkd-test-file3 +super + b + firefox", + )?; + + let setup2 = TestPath::new("/tmp/sohkd-test-file3"); + let mut f2 = File::create(setup2.path())?; + f2.write_all( + b" +super + c + hello", + )?; + + let hotkeys = &load(&setup.path()).unwrap()[0].hotkeys; + assert_eq!( + *hotkeys, + vec!( + Hotkey::new(evdev::Key::KEY_C, vec![Modifier::Super], String::from("hello")), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], String::from("firefox")) + ) + ); + Ok(()) + } + + #[test] + fn test_relative_import() -> std::io::Result<()> { + let setup = TestPath::new("/tmp/sohkd-relative-file1"); + let mut f = File::create(setup.path())?; + f.write_all( + b" +include sohkd-relative-file2 +super + b + firefox", + )?; + + let setup2 = TestPath::new("sohkd-relative-file2"); + let mut f2 = File::create(setup2.path())?; + f2.write_all( + b" +super + c + hello", + )?; + + let hotkeys = &load(&setup.path()).unwrap()[0].hotkeys; + assert_eq!( + *hotkeys, + vec!( + Hotkey::new(evdev::Key::KEY_C, vec![Modifier::Super], String::from("hello")), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], String::from("firefox")) + ) + ); + Ok(()) + } + + #[test] + fn test_more_multiple_configs() -> std::io::Result<()> { + let setup = TestPath::new("/tmp/sohkd-test-file4"); + let mut f = File::create(setup.path())?; + f.write_all( + b" +a + a", + )?; + + let setup2 = TestPath::new("/tmp/sohkd-test-file5"); + let mut f2 = File::create(setup2.path())?; + f2.write_all( + b" +include /tmp/sohkd-test-file4 +b + b", + )?; + let setup3 = TestPath::new("/tmp/sohkd-test-file6"); + let mut f3 = File::create(setup3.path())?; + f3.write_all( + b" +include /tmp/sohkd-test-file4 +include /tmp/sohkd-test-file5 +include /tmp/sohkd-test-file6 +include /tmp/sohkd-test-file7 +c + c", + )?; + let setup4 = TestPath::new("/tmp/sohkd-test-file7"); + let mut f4 = File::create(setup4.path())?; + f4.write_all( + b" +include /tmp/sohkd-test-file6 +d + d", + )?; + + let hotkeys = &load(&setup4.path()).unwrap()[0].hotkeys; + assert_eq!( + *hotkeys, + vec!( + Hotkey::new(evdev::Key::KEY_C, vec![], String::from("c")), + Hotkey::new(evdev::Key::KEY_A, vec![], String::from("a")), + Hotkey::new(evdev::Key::KEY_B, vec![], String::from("b")), + Hotkey::new(evdev::Key::KEY_D, vec![], String::from("d")), + ) + ); + Ok(()) + } + #[test] + fn test_include_and_unbind() -> std::io::Result<()> { + let setup = TestPath::new("/tmp/sohkd-test-file8"); + let mut f = File::create(setup.path())?; + f.write_all( + b" +include /tmp/sohkd-test-file9 +super + b + firefox +ignore super + d", + )?; + + let setup2 = TestPath::new("/tmp/sohkd-test-file9"); + let mut f2 = File::create(setup2.path())?; + f2.write_all( + b" +super + c + hello +super + d + world", + )?; + + let hotkeys = &load(&setup.path()).unwrap()[0].hotkeys; + assert_eq!( + *hotkeys, + vec!( + Hotkey::new(evdev::Key::KEY_C, vec![Modifier::Super], String::from("hello")), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], String::from("firefox")) + ) + ); + Ok(()) + } + + #[test] + fn test_basic_keybind() -> std::io::Result<()> { + let contents = " +r + alacritty + "; + + eval_config_test( + contents, + vec![Hotkey::new(evdev::Key::KEY_R, vec![], String::from("alacritty"))], + ) + } + + #[test] + fn test_multiple_keybinds() -> std::io::Result<()> { + let contents = " +r + alacritty + +w + kitty + +t + /bin/firefox + "; + + let hotkey_1 = Hotkey::new(evdev::Key::KEY_R, vec![], String::from("alacritty")); + let hotkey_2 = Hotkey::new(evdev::Key::KEY_W, vec![], String::from("kitty")); + let hotkey_3 = Hotkey::new(evdev::Key::KEY_T, vec![], String::from("/bin/firefox")); + + eval_config_test(contents, vec![hotkey_1, hotkey_2, hotkey_3]) + } + + #[test] + fn test_comments() -> std::io::Result<()> { + let contents = " +r + alacritty + +w + kitty + +#t + #/bin/firefox + "; + + let expected_keybinds = vec![ + Hotkey::new(evdev::Key::KEY_R, vec![], String::from("alacritty")), + Hotkey::new(evdev::Key::KEY_W, vec![], String::from("kitty")), + ]; + + eval_config_test(contents, expected_keybinds) + } + + #[test] + fn test_multiple_keypress() -> std::io::Result<()> { + let contents = " +super + 5 + alacritty + "; + + let expected_keybinds = + vec![Hotkey::new(evdev::Key::KEY_5, vec![Modifier::Super], String::from("alacritty"))]; + + eval_config_test(contents, expected_keybinds) + } + + #[test] + fn test_keysym_instead_of_modifier() -> std::io::Result<()> { + let contents = " +shift + k + m + notify-send 'Hello world!' + "; + + eval_invalid_config_test(contents, ParseError::InvalidModifier(PathBuf::new(), 2)) + } + + #[test] + fn test_modifier_instead_of_keysym() -> std::io::Result<()> { + let contents = " +shift + k + alt + notify-send 'Hello world!' + "; + + eval_invalid_config_test(contents, ParseError::InvalidModifier(PathBuf::new(), 2)) + } + + #[test] + fn test_unfinished_plus_sign() -> std::io::Result<()> { + let contents = " + + +shift + alt + + notify-send 'Hello world!' + "; + + eval_invalid_config_test(contents, ParseError::UnknownSymbol(PathBuf::new(), 4)) + } + + #[test] + fn test_plus_sign_at_start() -> std::io::Result<()> { + let contents = " ++ shift + k + notify-send 'Hello world!' + "; + + eval_invalid_config_test(contents, ParseError::UnknownSymbol(PathBuf::new(), 2)) + } + + #[test] + fn test_common_modifiers() -> std::io::Result<()> { + let contents = " +shift + k + notify-send 'Hello world!' + +control + 5 + notify-send 'Hello world!' + +alt + 2 + notify-send 'Hello world!' + +super + z + notify-send 'Hello world!' + "; + + let expected_hotkeys = vec![ + Hotkey::new( + evdev::Key::KEY_K, + vec![Modifier::Shift], + "notify-send 'Hello world!'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_5, + vec![Modifier::Control], + "notify-send 'Hello world!'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_2, + vec![Modifier::Alt], + "notify-send 'Hello world!'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_Z, + vec![Modifier::Super], + "notify-send 'Hello world!'".to_string(), + ), + ]; + + eval_config_test(contents, expected_hotkeys) + } + + #[test] + fn test_command_with_many_spaces() -> std::io::Result<()> { + let contents = " +p + xbacklight -inc 10 -fps 30 -time 200 + "; + + let expected_keybinds = vec![Hotkey::new( + evdev::Key::KEY_P, + vec![], + String::from("xbacklight -inc 10 -fps 30 -time 200"), + )]; + + eval_config_test(contents, expected_keybinds) + } + + #[test] + fn test_invalid_keybinding() -> std::io::Result<()> { + let contents = " +p + xbacklight -inc 10 -fps 30 -time 200 + +pesto + xterm + "; + + eval_invalid_config_test(contents, ParseError::UnknownSymbol(PathBuf::new(), 5)) + } + + #[test] + // keysyms not followed by command should be ignored + fn test_no_command() -> std::io::Result<()> { + let contents = " +k + xbacklight -inc 10 -fps 30 -time 200 + +w + + "; + + eval_config_test( + contents, + vec![Hotkey::new( + evdev::Key::KEY_K, + vec![], + "xbacklight -inc 10 -fps 30 -time 200".to_string(), + )], + ) + } + + #[test] + fn test_real_config_snippet() -> std::io::Result<()> { + let contents = " +# reloads sxhkd configuration: +super + Escape + pkill -USR1 -x sxhkd ; sxhkd & + +# Launch Terminal +super + Return + alacritty -t \"Terminal\" -e \"$HOME/.config/sxhkd/new_tmux_terminal.sh\" + +# terminal emulator (no tmux) +super + shift + Return + alacritty -t \"Terminal\" + +# terminal emulator (new tmux session) +alt + Return + alacritty -t \"Terminal\" -e \"tmux\" + +ctrl + 0 + play-song.sh + +super + minus + play-song.sh album + "; + + let expected_result: Vec = vec![ + Hotkey::new( + evdev::Key::KEY_ESC, + vec![Modifier::Super], + String::from("pkill -USR1 -x sxhkd ; sxhkd &"), + ), + Hotkey::new( + evdev::Key::KEY_ENTER, + vec![Modifier::Super], + String::from( + "alacritty -t \"Terminal\" -e \"$HOME/.config/sxhkd/new_tmux_terminal.sh\"", + ), + ), + Hotkey::new( + evdev::Key::KEY_ENTER, + vec![Modifier::Super, Modifier::Shift], + String::from("alacritty -t \"Terminal\""), + ), + Hotkey::new( + evdev::Key::KEY_ENTER, + vec![Modifier::Alt], + String::from("alacritty -t \"Terminal\" -e \"tmux\""), + ), + Hotkey::new(evdev::Key::KEY_0, vec![Modifier::Control], String::from("play-song.sh")), + Hotkey::new( + evdev::Key::KEY_MINUS, + vec![Modifier::Super], + String::from("play-song.sh album"), + ), + ]; + + eval_config_test(contents, expected_result) + } + + #[test] + fn test_multiline_command() -> std::io::Result<()> { + let contents = " +k + mpc ls | dmenu | \\ + sed -i 's/foo/bar/g' + "; + + let expected_keybind = Hotkey::new( + evdev::Key::KEY_K, + vec![], + String::from("mpc ls | dmenu | sed -i 's/foo/bar/g'"), + ); + + eval_config_test(contents, vec![expected_keybind]) + } + + #[test] + fn test_commented_out_keybind() -> std::io::Result<()> { + let contents = " +#w + gimp + "; + + eval_config_test(contents, vec![]) + } + + // TODO: Write these tests as needed. + + #[test] + fn test_all_alphanumeric() -> std::io::Result<()> { + let symbols: [&str; 36] = [ + "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", + "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", + "8", "9", + ]; + let keysyms: [evdev::Key; 36] = [ + evdev::Key::KEY_A, + evdev::Key::KEY_B, + evdev::Key::KEY_C, + evdev::Key::KEY_D, + evdev::Key::KEY_E, + evdev::Key::KEY_F, + evdev::Key::KEY_G, + evdev::Key::KEY_H, + evdev::Key::KEY_I, + evdev::Key::KEY_J, + evdev::Key::KEY_K, + evdev::Key::KEY_L, + evdev::Key::KEY_M, + evdev::Key::KEY_N, + evdev::Key::KEY_O, + evdev::Key::KEY_P, + evdev::Key::KEY_Q, + evdev::Key::KEY_R, + evdev::Key::KEY_S, + evdev::Key::KEY_T, + evdev::Key::KEY_U, + evdev::Key::KEY_V, + evdev::Key::KEY_W, + evdev::Key::KEY_X, + evdev::Key::KEY_Y, + evdev::Key::KEY_Z, + evdev::Key::KEY_0, + evdev::Key::KEY_1, + evdev::Key::KEY_2, + evdev::Key::KEY_3, + evdev::Key::KEY_4, + evdev::Key::KEY_5, + evdev::Key::KEY_6, + evdev::Key::KEY_7, + evdev::Key::KEY_8, + evdev::Key::KEY_9, + ]; + + let mut contents = String::new(); + for symbol in &symbols { + contents.push_str(&format!("{}\n st\n", symbol)); + } + let contents = &contents; + + let expected_result: Vec = + keysyms.iter().map(|keysym| Hotkey::new(*keysym, vec![], "st".to_string())).collect(); + + eval_config_test(contents, expected_result) + } + + #[test] + fn test_homerow_special_keys_top() -> std::io::Result<()> { + let symbols: [&str; 7] = + ["Escape", "BackSpace", "Return", "Tab", "minus", "equal", "grave"]; + + let keysyms: [evdev::Key; 7] = [ + evdev::Key::KEY_ESC, + evdev::Key::KEY_BACKSPACE, + evdev::Key::KEY_ENTER, + evdev::Key::KEY_TAB, + evdev::Key::KEY_MINUS, + evdev::Key::KEY_EQUAL, + evdev::Key::KEY_GRAVE, + ]; + + let mut contents = String::new(); + for symbol in &symbols { + contents.push_str(&format!("{}\n st\n", symbol)); + } + let contents = &contents; + + let expected_result: Vec = + keysyms.iter().map(|keysym| Hotkey::new(*keysym, vec![], "st".to_string())).collect(); + + eval_config_test(contents, expected_result) + } + + #[test] + fn test_case_insensitive() -> std::io::Result<()> { + let contents = " +Super + SHIFT + alt + a + st +ReTurn + ts + "; + eval_config_test( + contents, + vec![ + Hotkey::new( + evdev::Key::KEY_A, + vec![Modifier::Super, Modifier::Shift, Modifier::Alt], + "st".to_string(), + ), + Hotkey::new(evdev::Key::KEY_ENTER, vec![], "ts".to_string()), + ], + ) + } + + #[test] + fn test_duplicate_hotkeys() -> std::io::Result<()> { + let contents = " +super + shift + a + st +shift + suPer + A + ts +b + st +B + ts +"; + eval_config_test( + contents, + vec![ + Hotkey::new( + evdev::Key::KEY_A, + vec![Modifier::Super, Modifier::Shift], + "ts".to_string(), + ), + Hotkey::new(evdev::Key::KEY_B, vec![], "ts".to_string()), + ], + ) + } + + #[test] + fn test_inline_comment() -> std::io::Result<()> { + let contents = " +super + a #comment and comment super + st +super + shift + b + ts #this comment should be handled by shell +" + .to_string(); + eval_config_test( + &contents, + vec![ + Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "st".to_string()), + Hotkey::new( + evdev::Key::KEY_B, + vec![Modifier::Super, Modifier::Shift], + "ts #this comment should be handled by shell".to_string(), + ), + ], + ) + } + + #[test] + fn test_blank_config() -> std::io::Result<()> { + let contents = ""; + + eval_config_test(contents, vec![]) + } + + #[test] + fn test_blank_config_with_whitespace() -> std::io::Result<()> { + let contents = " + + + "; + + eval_config_test(contents, vec![]) + } + + #[test] + fn test_extract_curly_brace() -> std::io::Result<()> { + let keybind_with_curly_brace = "super + {a,b,c}"; + assert_eq!( + extract_curly_brace(keybind_with_curly_brace), + vec!["super + a", "super + b", "super + c",] + ); + let command_with_curly_brace = "bspc node -p {west,south,north,west}"; + assert_eq!( + extract_curly_brace(command_with_curly_brace), + vec![ + "bspc node -p west", + "bspc node -p south", + "bspc node -p north", + "bspc node -p west", + ] + ); + let wrong_format = "super + }a, b, c{"; + assert_eq!(extract_curly_brace(wrong_format), vec![wrong_format]); + let single_sym = "super + {a}"; + assert_eq!(extract_curly_brace(single_sym), vec!["super + a"]); + Ok(()) + } + + #[test] + fn test_curly_brace() -> std::io::Result<()> { + let contents = " +super + {a,b,c} + {firefox, brave, chrome}"; + eval_config_test( + contents, + vec![ + Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), + Hotkey::new(evdev::Key::KEY_C, vec![Modifier::Super], "chrome".to_string()), + ], + ) + } + + #[test] + fn test_curly_brace_less_commands() -> std::io::Result<()> { + let contents = " +super + {a,b,c} + {firefox, brave}"; + eval_config_test( + contents, + vec![ + Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), + ], + ) + } + + #[test] + fn test_curly_brace_less_keysyms() -> std::io::Result<()> { + let contents = " +super + {a, b} + {firefox, brave, chrome}"; + eval_config_test( + contents, + vec![ + Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), + ], + ) + } + + #[test] + fn test_range_syntax() -> std::io::Result<()> { + let contents = " +super + {1-9,0} + bspc desktop -f '{1-9,0}'"; + eval_config_test( + contents, + vec![ + Hotkey::new( + evdev::Key::KEY_1, + vec![Modifier::Super], + "bspc desktop -f '1'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_2, + vec![Modifier::Super], + "bspc desktop -f '2'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_3, + vec![Modifier::Super], + "bspc desktop -f '3'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_4, + vec![Modifier::Super], + "bspc desktop -f '4'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_5, + vec![Modifier::Super], + "bspc desktop -f '5'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_6, + vec![Modifier::Super], + "bspc desktop -f '6'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_7, + vec![Modifier::Super], + "bspc desktop -f '7'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_8, + vec![Modifier::Super], + "bspc desktop -f '8'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_9, + vec![Modifier::Super], + "bspc desktop -f '9'".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_0, + vec![Modifier::Super], + "bspc desktop -f '0'".to_string(), + ), + ], + ) + } + + #[test] + fn test_range_syntax_ascii_character() -> std::io::Result<()> { + let contents = " +super + {a-c} + {firefox, brave, chrome}"; + eval_config_test( + contents, + vec![ + Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "firefox".to_string()), + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "brave".to_string()), + Hotkey::new(evdev::Key::KEY_C, vec![Modifier::Super], "chrome".to_string()), + ], + ) + } + + #[test] + fn test_range_syntax_not_ascii() -> std::io::Result<()> { + let contents = " +super + {a-是} + {firefox, brave} + "; + eval_invalid_config_test(contents, ParseError::UnknownSymbol(PathBuf::new(), 2)) + } + + #[test] + fn test_range_syntax_invalid_range() -> std::io::Result<()> { + let contents = " +super + {bc-ad} + {firefox, brave} + "; + eval_invalid_config_test(contents, ParseError::UnknownSymbol(PathBuf::new(), 2)) + } + + #[test] + fn test_ranger_syntax_not_full_range() -> std::io::Result<()> { + let contents = " +super + {a-} + {firefox, brave}"; + eval_invalid_config_test(contents, ParseError::UnknownSymbol(PathBuf::new(), 2)) + } + + #[test] + fn test_none() -> std::io::Result<()> { + let contents = " +super + {_, shift} + b + {firefox, brave}"; + eval_config_test( + contents, + vec![ + Hotkey::new(evdev::Key::KEY_B, vec![Modifier::Super], "firefox".to_string()), + Hotkey::new( + evdev::Key::KEY_B, + vec![Modifier::Super, Modifier::Shift], + "brave".to_string(), + ), + ], + ) + } + + #[test] + fn test_multiple_ranges() -> std::io::Result<()> { + let contents = " +super + {shift,alt} + {c,d} + {librewolf, firefox} {--sync, --help} + "; + + eval_config_test( + contents, + vec![ + Hotkey::new( + evdev::Key::KEY_C, + vec![Modifier::Super, Modifier::Shift], + "librewolf --sync".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_D, + vec![Modifier::Super, Modifier::Shift], + "librewolf --help".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_C, + vec![Modifier::Super, Modifier::Alt], + "firefox --sync".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_D, + vec![Modifier::Super, Modifier::Alt], + "firefox --help".to_string(), + ), + ], + ) + } + + #[test] + fn test_multiple_ranges_numbers() -> std::io::Result<()> { + let contents = " +{control,super} + {1-3} + {notify-send, echo} {hello,how,are} + "; + + eval_config_test( + contents, + vec![ + Hotkey::new( + evdev::Key::KEY_1, + vec![Modifier::Control], + "notify-send hello".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_2, + vec![Modifier::Control], + "notify-send how".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_3, + vec![Modifier::Control], + "notify-send are".to_string(), + ), + Hotkey::new(evdev::Key::KEY_1, vec![Modifier::Super], "echo hello".to_string()), + Hotkey::new(evdev::Key::KEY_2, vec![Modifier::Super], "echo how".to_string()), + Hotkey::new(evdev::Key::KEY_3, vec![Modifier::Super], "echo are".to_string()), + ], + ) + } + + #[test] + fn test_bspwm_multiple_curly_brace() -> std::io::Result<()> { + let contents = " +super + {_,shift + }{h,j,k,l} + bspc node -{f,s} {west,south,north,east}"; + + eval_config_test( + contents, + vec![ + Hotkey::new( + evdev::Key::KEY_H, + vec![Modifier::Super], + "bspc node -f west".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_J, + vec![Modifier::Super], + "bspc node -f south".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_K, + vec![Modifier::Super], + "bspc node -f north".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_L, + vec![Modifier::Super], + "bspc node -f east".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_H, + vec![Modifier::Super, Modifier::Shift], + "bspc node -s west".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_J, + vec![Modifier::Super, Modifier::Shift], + "bspc node -s south".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_K, + vec![Modifier::Super, Modifier::Shift], + "bspc node -s north".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_L, + vec![Modifier::Super, Modifier::Shift], + "bspc node -s east".to_string(), + ), + ], + ) + } + + #[test] + fn test_longer_multiple_curly_brace() -> std::io::Result<()> { + let contents = " +super + {_, ctrl +} {_, shift +} {1-2} + riverctl {set, toggle}-{focused, view}-tags {1-2}"; + eval_config_test( + contents, + vec![ + Hotkey::new( + evdev::Key::KEY_1, + vec![Modifier::Super], + "riverctl set-focused-tags 1".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_2, + vec![Modifier::Super], + "riverctl set-focused-tags 2".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_1, + vec![Modifier::Super, Modifier::Control], + "riverctl toggle-focused-tags 1".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_2, + vec![Modifier::Super, Modifier::Control], + "riverctl toggle-focused-tags 2".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_1, + vec![Modifier::Super, Modifier::Shift], + "riverctl set-view-tags 1".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_2, + vec![Modifier::Super, Modifier::Shift], + "riverctl set-view-tags 2".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_1, + vec![Modifier::Super, Modifier::Control, Modifier::Shift], + "riverctl toggle-view-tags 1".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_2, + vec![Modifier::Super, Modifier::Control, Modifier::Shift], + "riverctl toggle-view-tags 2".to_string(), + ), + ], + ) + } + + #[test] + fn test_period_binding() -> std::io::Result<()> { + let contents = " +super + {comma, period} + riverctl focus-output {previous, next}"; + + eval_config_test( + contents, + vec![ + Hotkey::new( + evdev::Key::KEY_COMMA, + vec![Modifier::Super], + "riverctl focus-output previous".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_DOT, + vec![Modifier::Super], + "riverctl focus-output next".to_string(), + ), + ], + ) + } + + #[test] + fn test_period_escape_binding() -> std::io::Result<()> { + let contents = " +super + {\\,, .} + riverctl focus-output {previous, next}"; + + eval_config_test( + contents, + vec![ + Hotkey::new( + evdev::Key::KEY_COMMA, + vec![Modifier::Super], + "riverctl focus-output previous".to_string(), + ), + Hotkey::new( + evdev::Key::KEY_DOT, + vec![Modifier::Super], + "riverctl focus-output next".to_string(), + ), + ], + ) + } + + #[test] + fn test_prefix() -> std::io::Result<()> { + let contents = " +super + @1 + 1 +super + ~2 + 2 +super + ~@3 + 3 +super + @~4 + 4"; + + eval_config_test( + contents, + vec![ + Hotkey::new(evdev::Key::KEY_1, vec![Modifier::Super], "1".to_string()).on_release(), + Hotkey::new(evdev::Key::KEY_2, vec![Modifier::Super], "2".to_string()).send(), + Hotkey::new(evdev::Key::KEY_3, vec![Modifier::Super], "3".to_string()) + .on_release() + .send(), + Hotkey::new(evdev::Key::KEY_4, vec![Modifier::Super], "4".to_string()) + .on_release() + .send(), + ], + ) + } + + #[test] + fn test_override() -> std::io::Result<()> { + let contents = " +super + a + 1 +super + a + 2"; + eval_config_test( + contents, + vec![Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Super], "2".to_string())], + ) + } + + #[test] + fn test_any_modifier() -> std::io::Result<()> { + let contents = " +any + a + 1"; + eval_config_test( + contents, + vec![Hotkey::new(evdev::Key::KEY_A, vec![Modifier::Any], "1".to_string())], + ) + } +} + +mod test_config_display { + use crate::config::{Error, ParseError}; + use std::io; + use std::path::PathBuf; + + #[test] + fn test_display_io_error() { + let error = Error::Io(io::Error::from(io::ErrorKind::UnexpectedEof)); + + if !format!("{}", error).contains("unexpected end of file") { + panic!("Error message was '{}", error); + } + } + + #[test] + fn test_display_unknown_symbol_error() { + let error = Error::InvalidConfig(ParseError::UnknownSymbol(PathBuf::new(), 10)); + + assert_eq!( + format!("{}", error), + "Error parsing config file \"\". Unknown symbol at line 10." + ); + } + + #[test] + fn test_display_invalid_modifier_error() { + let error = Error::InvalidConfig(ParseError::InvalidModifier(PathBuf::new(), 25)); + + assert_eq!( + format!("{}", error), + "Error parsing config file \"\". Invalid modifier at line 25." + ); + } + + #[test] + fn test_invalid_keysm_error() { + let error = Error::InvalidConfig(ParseError::InvalidKeysym(PathBuf::new(), 7)); + + assert_eq!( + format!("{}", error), + "Error parsing config file \"\". Invalid keysym at line 7." + ); + } +} diff --git a/src/uinput.rs b/src/uinput.rs new file mode 100644 index 0000000..d248fab --- /dev/null +++ b/src/uinput.rs @@ -0,0 +1,569 @@ +use evdev::{ + uinput::{VirtualDevice, VirtualDeviceBuilder}, + AttributeSet, Key, +}; + +pub fn create_uinput_device() -> Result> { + let mut keys = AttributeSet::::new(); + for key in get_all_keys() { + keys.insert(key); + } + let device = VirtualDeviceBuilder::new()? + .name("sohkd virtual output") + .with_keys(&keys)? + .build() + .unwrap(); + Ok(device) +} +pub fn get_all_keys() -> Vec { + return vec![ + evdev::Key::KEY_RESERVED, + evdev::Key::KEY_ESC, + evdev::Key::KEY_1, + evdev::Key::KEY_2, + evdev::Key::KEY_3, + evdev::Key::KEY_4, + evdev::Key::KEY_5, + evdev::Key::KEY_6, + evdev::Key::KEY_7, + evdev::Key::KEY_8, + evdev::Key::KEY_9, + evdev::Key::KEY_0, + evdev::Key::KEY_MINUS, + evdev::Key::KEY_EQUAL, + evdev::Key::KEY_BACKSPACE, + evdev::Key::KEY_TAB, + evdev::Key::KEY_Q, + evdev::Key::KEY_W, + evdev::Key::KEY_E, + evdev::Key::KEY_R, + evdev::Key::KEY_T, + evdev::Key::KEY_Y, + evdev::Key::KEY_U, + evdev::Key::KEY_I, + evdev::Key::KEY_O, + evdev::Key::KEY_P, + evdev::Key::KEY_LEFTBRACE, + evdev::Key::KEY_RIGHTBRACE, + evdev::Key::KEY_ENTER, + evdev::Key::KEY_LEFTCTRL, + evdev::Key::KEY_A, + evdev::Key::KEY_S, + evdev::Key::KEY_D, + evdev::Key::KEY_F, + evdev::Key::KEY_G, + evdev::Key::KEY_H, + evdev::Key::KEY_J, + evdev::Key::KEY_K, + evdev::Key::KEY_L, + evdev::Key::KEY_SEMICOLON, + evdev::Key::KEY_APOSTROPHE, + evdev::Key::KEY_GRAVE, + evdev::Key::KEY_LEFTSHIFT, + evdev::Key::KEY_BACKSLASH, + evdev::Key::KEY_Z, + evdev::Key::KEY_X, + evdev::Key::KEY_C, + evdev::Key::KEY_V, + evdev::Key::KEY_B, + evdev::Key::KEY_N, + evdev::Key::KEY_M, + evdev::Key::KEY_COMMA, + evdev::Key::KEY_DOT, + evdev::Key::KEY_SLASH, + evdev::Key::KEY_RIGHTSHIFT, + evdev::Key::KEY_KPASTERISK, + evdev::Key::KEY_LEFTALT, + evdev::Key::KEY_SPACE, + evdev::Key::KEY_CAPSLOCK, + evdev::Key::KEY_F1, + evdev::Key::KEY_F2, + evdev::Key::KEY_F3, + evdev::Key::KEY_F4, + evdev::Key::KEY_F5, + evdev::Key::KEY_F6, + evdev::Key::KEY_F7, + evdev::Key::KEY_F8, + evdev::Key::KEY_F9, + evdev::Key::KEY_F10, + evdev::Key::KEY_NUMLOCK, + evdev::Key::KEY_SCROLLLOCK, + evdev::Key::KEY_KP7, + evdev::Key::KEY_KP8, + evdev::Key::KEY_KP9, + evdev::Key::KEY_KPMINUS, + evdev::Key::KEY_KP4, + evdev::Key::KEY_KP5, + evdev::Key::KEY_KP6, + evdev::Key::KEY_KPPLUS, + evdev::Key::KEY_KP1, + evdev::Key::KEY_KP2, + evdev::Key::KEY_KP3, + evdev::Key::KEY_KP0, + evdev::Key::KEY_KPDOT, + evdev::Key::KEY_ZENKAKUHANKAKU, + evdev::Key::KEY_102ND, + evdev::Key::KEY_F11, + evdev::Key::KEY_F12, + evdev::Key::KEY_RO, + evdev::Key::KEY_KATAKANA, + evdev::Key::KEY_HIRAGANA, + evdev::Key::KEY_HENKAN, + evdev::Key::KEY_KATAKANAHIRAGANA, + evdev::Key::KEY_MUHENKAN, + evdev::Key::KEY_KPJPCOMMA, + evdev::Key::KEY_KPENTER, + evdev::Key::KEY_RIGHTCTRL, + evdev::Key::KEY_KPSLASH, + evdev::Key::KEY_SYSRQ, + evdev::Key::KEY_RIGHTALT, + evdev::Key::KEY_LINEFEED, + evdev::Key::KEY_HOME, + evdev::Key::KEY_UP, + evdev::Key::KEY_PAGEUP, + evdev::Key::KEY_LEFT, + evdev::Key::KEY_RIGHT, + evdev::Key::KEY_END, + evdev::Key::KEY_DOWN, + evdev::Key::KEY_PAGEDOWN, + evdev::Key::KEY_INSERT, + evdev::Key::KEY_DELETE, + evdev::Key::KEY_MACRO, + evdev::Key::KEY_MUTE, + evdev::Key::KEY_VOLUMEDOWN, + evdev::Key::KEY_VOLUMEUP, + evdev::Key::KEY_POWER, + evdev::Key::KEY_KPEQUAL, + evdev::Key::KEY_KPPLUSMINUS, + evdev::Key::KEY_PAUSE, + evdev::Key::KEY_SCALE, + evdev::Key::KEY_KPCOMMA, + evdev::Key::KEY_HANGEUL, + evdev::Key::KEY_HANJA, + evdev::Key::KEY_YEN, + evdev::Key::KEY_LEFTMETA, + evdev::Key::KEY_RIGHTMETA, + evdev::Key::KEY_COMPOSE, + evdev::Key::KEY_STOP, + evdev::Key::KEY_AGAIN, + evdev::Key::KEY_PROPS, + evdev::Key::KEY_UNDO, + evdev::Key::KEY_FRONT, + evdev::Key::KEY_COPY, + evdev::Key::KEY_OPEN, + evdev::Key::KEY_PASTE, + evdev::Key::KEY_FIND, + evdev::Key::KEY_CUT, + evdev::Key::KEY_HELP, + evdev::Key::KEY_MENU, + evdev::Key::KEY_CALC, + evdev::Key::KEY_SETUP, + evdev::Key::KEY_SLEEP, + evdev::Key::KEY_WAKEUP, + evdev::Key::KEY_FILE, + evdev::Key::KEY_SENDFILE, + evdev::Key::KEY_DELETEFILE, + evdev::Key::KEY_XFER, + evdev::Key::KEY_PROG1, + evdev::Key::KEY_PROG2, + evdev::Key::KEY_WWW, + evdev::Key::KEY_MSDOS, + evdev::Key::KEY_COFFEE, + evdev::Key::KEY_DIRECTION, + evdev::Key::KEY_ROTATE_DISPLAY, + evdev::Key::KEY_CYCLEWINDOWS, + evdev::Key::KEY_MAIL, + evdev::Key::KEY_BOOKMARKS, + evdev::Key::KEY_COMPUTER, + evdev::Key::KEY_BACK, + evdev::Key::KEY_FORWARD, + evdev::Key::KEY_CLOSECD, + evdev::Key::KEY_EJECTCD, + evdev::Key::KEY_EJECTCLOSECD, + evdev::Key::KEY_NEXTSONG, + evdev::Key::KEY_PLAYPAUSE, + evdev::Key::KEY_PREVIOUSSONG, + evdev::Key::KEY_STOPCD, + evdev::Key::KEY_RECORD, + evdev::Key::KEY_REWIND, + evdev::Key::KEY_PHONE, + evdev::Key::KEY_ISO, + evdev::Key::KEY_CONFIG, + evdev::Key::KEY_HOMEPAGE, + evdev::Key::KEY_REFRESH, + evdev::Key::KEY_EXIT, + evdev::Key::KEY_MOVE, + evdev::Key::KEY_EDIT, + evdev::Key::KEY_SCROLLUP, + evdev::Key::KEY_SCROLLDOWN, + evdev::Key::KEY_KPLEFTPAREN, + evdev::Key::KEY_KPRIGHTPAREN, + evdev::Key::KEY_NEW, + evdev::Key::KEY_REDO, + evdev::Key::KEY_F13, + evdev::Key::KEY_F14, + evdev::Key::KEY_F15, + evdev::Key::KEY_F16, + evdev::Key::KEY_F17, + evdev::Key::KEY_F18, + evdev::Key::KEY_F19, + evdev::Key::KEY_F20, + evdev::Key::KEY_F21, + evdev::Key::KEY_F22, + evdev::Key::KEY_F23, + evdev::Key::KEY_F24, + evdev::Key::KEY_PLAYCD, + evdev::Key::KEY_PAUSECD, + evdev::Key::KEY_PROG3, + evdev::Key::KEY_PROG4, + evdev::Key::KEY_DASHBOARD, + evdev::Key::KEY_SUSPEND, + evdev::Key::KEY_CLOSE, + evdev::Key::KEY_PLAY, + evdev::Key::KEY_FASTFORWARD, + evdev::Key::KEY_BASSBOOST, + evdev::Key::KEY_PRINT, + evdev::Key::KEY_HP, + evdev::Key::KEY_CAMERA, + evdev::Key::KEY_SOUND, + evdev::Key::KEY_QUESTION, + evdev::Key::KEY_EMAIL, + evdev::Key::KEY_CHAT, + evdev::Key::KEY_SEARCH, + evdev::Key::KEY_CONNECT, + evdev::Key::KEY_FINANCE, + evdev::Key::KEY_SPORT, + evdev::Key::KEY_SHOP, + evdev::Key::KEY_ALTERASE, + evdev::Key::KEY_CANCEL, + evdev::Key::KEY_BRIGHTNESSDOWN, + evdev::Key::KEY_BRIGHTNESSUP, + evdev::Key::KEY_MEDIA, + evdev::Key::KEY_SWITCHVIDEOMODE, + evdev::Key::KEY_KBDILLUMTOGGLE, + evdev::Key::KEY_KBDILLUMDOWN, + evdev::Key::KEY_KBDILLUMUP, + evdev::Key::KEY_SEND, + evdev::Key::KEY_REPLY, + evdev::Key::KEY_FORWARDMAIL, + evdev::Key::KEY_SAVE, + evdev::Key::KEY_DOCUMENTS, + evdev::Key::KEY_BATTERY, + evdev::Key::KEY_BLUETOOTH, + evdev::Key::KEY_WLAN, + evdev::Key::KEY_UWB, + evdev::Key::KEY_UNKNOWN, + evdev::Key::KEY_VIDEO_NEXT, + evdev::Key::KEY_VIDEO_PREV, + evdev::Key::KEY_BRIGHTNESS_CYCLE, + evdev::Key::KEY_BRIGHTNESS_AUTO, + evdev::Key::KEY_DISPLAY_OFF, + evdev::Key::KEY_WWAN, + evdev::Key::KEY_RFKILL, + evdev::Key::KEY_MICMUTE, + evdev::Key::BTN_0, + evdev::Key::BTN_1, + evdev::Key::BTN_2, + evdev::Key::BTN_3, + evdev::Key::BTN_4, + evdev::Key::BTN_5, + evdev::Key::BTN_6, + evdev::Key::BTN_7, + evdev::Key::BTN_8, + evdev::Key::BTN_9, + evdev::Key::BTN_LEFT, + evdev::Key::BTN_RIGHT, + evdev::Key::BTN_MIDDLE, + evdev::Key::BTN_SIDE, + evdev::Key::BTN_EXTRA, + evdev::Key::BTN_FORWARD, + evdev::Key::BTN_BACK, + evdev::Key::BTN_TASK, + evdev::Key::BTN_TRIGGER, + evdev::Key::BTN_THUMB, + evdev::Key::BTN_THUMB2, + evdev::Key::BTN_TOP, + evdev::Key::BTN_TOP2, + evdev::Key::BTN_PINKIE, + evdev::Key::BTN_BASE, + evdev::Key::BTN_BASE2, + evdev::Key::BTN_BASE3, + evdev::Key::BTN_BASE4, + evdev::Key::BTN_BASE5, + evdev::Key::BTN_BASE6, + evdev::Key::BTN_DEAD, + evdev::Key::BTN_SOUTH, + evdev::Key::BTN_EAST, + evdev::Key::BTN_C, + evdev::Key::BTN_NORTH, + evdev::Key::BTN_WEST, + evdev::Key::BTN_Z, + evdev::Key::BTN_TL, + evdev::Key::BTN_TR, + evdev::Key::BTN_TL2, + evdev::Key::BTN_TR2, + evdev::Key::BTN_SELECT, + evdev::Key::BTN_START, + evdev::Key::BTN_MODE, + evdev::Key::BTN_THUMBL, + evdev::Key::BTN_THUMBR, + evdev::Key::BTN_TOOL_PEN, + evdev::Key::BTN_TOOL_RUBBER, + evdev::Key::BTN_TOOL_BRUSH, + evdev::Key::BTN_TOOL_PENCIL, + evdev::Key::BTN_TOOL_AIRBRUSH, + evdev::Key::BTN_TOOL_FINGER, + evdev::Key::BTN_TOOL_MOUSE, + evdev::Key::BTN_TOOL_LENS, + evdev::Key::BTN_TOOL_QUINTTAP, + evdev::Key::BTN_TOUCH, + evdev::Key::BTN_STYLUS, + evdev::Key::BTN_STYLUS2, + evdev::Key::BTN_TOOL_DOUBLETAP, + evdev::Key::BTN_TOOL_TRIPLETAP, + evdev::Key::BTN_TOOL_QUADTAP, + evdev::Key::BTN_GEAR_DOWN, + evdev::Key::BTN_GEAR_UP, + evdev::Key::KEY_OK, + evdev::Key::KEY_SELECT, + evdev::Key::KEY_GOTO, + evdev::Key::KEY_CLEAR, + evdev::Key::KEY_POWER2, + evdev::Key::KEY_OPTION, + evdev::Key::KEY_INFO, + evdev::Key::KEY_TIME, + evdev::Key::KEY_VENDOR, + evdev::Key::KEY_ARCHIVE, + evdev::Key::KEY_PROGRAM, + evdev::Key::KEY_CHANNEL, + evdev::Key::KEY_FAVORITES, + evdev::Key::KEY_EPG, + evdev::Key::KEY_PVR, + evdev::Key::KEY_MHP, + evdev::Key::KEY_LANGUAGE, + evdev::Key::KEY_TITLE, + evdev::Key::KEY_SUBTITLE, + evdev::Key::KEY_ANGLE, + evdev::Key::KEY_ZOOM, + evdev::Key::KEY_FULL_SCREEN, + evdev::Key::KEY_MODE, + evdev::Key::KEY_KEYBOARD, + evdev::Key::KEY_SCREEN, + evdev::Key::KEY_PC, + evdev::Key::KEY_TV, + evdev::Key::KEY_TV2, + evdev::Key::KEY_VCR, + evdev::Key::KEY_VCR2, + evdev::Key::KEY_SAT, + evdev::Key::KEY_SAT2, + evdev::Key::KEY_CD, + evdev::Key::KEY_TAPE, + evdev::Key::KEY_RADIO, + evdev::Key::KEY_TUNER, + evdev::Key::KEY_PLAYER, + evdev::Key::KEY_TEXT, + evdev::Key::KEY_DVD, + evdev::Key::KEY_AUX, + evdev::Key::KEY_MP3, + evdev::Key::KEY_AUDIO, + evdev::Key::KEY_VIDEO, + evdev::Key::KEY_DIRECTORY, + evdev::Key::KEY_LIST, + evdev::Key::KEY_MEMO, + evdev::Key::KEY_CALENDAR, + evdev::Key::KEY_RED, + evdev::Key::KEY_GREEN, + evdev::Key::KEY_YELLOW, + evdev::Key::KEY_BLUE, + evdev::Key::KEY_CHANNELUP, + evdev::Key::KEY_CHANNELDOWN, + evdev::Key::KEY_FIRST, + evdev::Key::KEY_LAST, + evdev::Key::KEY_AB, + evdev::Key::KEY_NEXT, + evdev::Key::KEY_RESTART, + evdev::Key::KEY_SLOW, + evdev::Key::KEY_SHUFFLE, + evdev::Key::KEY_BREAK, + evdev::Key::KEY_PREVIOUS, + evdev::Key::KEY_DIGITS, + evdev::Key::KEY_TEEN, + evdev::Key::KEY_TWEN, + evdev::Key::KEY_VIDEOPHONE, + evdev::Key::KEY_GAMES, + evdev::Key::KEY_ZOOMIN, + evdev::Key::KEY_ZOOMOUT, + evdev::Key::KEY_ZOOMRESET, + evdev::Key::KEY_WORDPROCESSOR, + evdev::Key::KEY_EDITOR, + evdev::Key::KEY_SPREADSHEET, + evdev::Key::KEY_GRAPHICSEDITOR, + evdev::Key::KEY_PRESENTATION, + evdev::Key::KEY_DATABASE, + evdev::Key::KEY_NEWS, + evdev::Key::KEY_VOICEMAIL, + evdev::Key::KEY_ADDRESSBOOK, + evdev::Key::KEY_MESSENGER, + evdev::Key::KEY_DISPLAYTOGGLE, + evdev::Key::KEY_SPELLCHECK, + evdev::Key::KEY_LOGOFF, + evdev::Key::KEY_DOLLAR, + evdev::Key::KEY_EURO, + evdev::Key::KEY_FRAMEBACK, + evdev::Key::KEY_FRAMEFORWARD, + evdev::Key::KEY_CONTEXT_MENU, + evdev::Key::KEY_MEDIA_REPEAT, + evdev::Key::KEY_10CHANNELSUP, + evdev::Key::KEY_10CHANNELSDOWN, + evdev::Key::KEY_IMAGES, + evdev::Key::KEY_DEL_EOL, + evdev::Key::KEY_DEL_EOS, + evdev::Key::KEY_INS_LINE, + evdev::Key::KEY_DEL_LINE, + evdev::Key::KEY_FN, + evdev::Key::KEY_FN_ESC, + evdev::Key::KEY_FN_F1, + evdev::Key::KEY_FN_F2, + evdev::Key::KEY_FN_F3, + evdev::Key::KEY_FN_F4, + evdev::Key::KEY_FN_F5, + evdev::Key::KEY_FN_F6, + evdev::Key::KEY_FN_F7, + evdev::Key::KEY_FN_F8, + evdev::Key::KEY_FN_F9, + evdev::Key::KEY_FN_F10, + evdev::Key::KEY_FN_F11, + evdev::Key::KEY_FN_F12, + evdev::Key::KEY_FN_1, + evdev::Key::KEY_FN_2, + evdev::Key::KEY_FN_D, + evdev::Key::KEY_FN_E, + evdev::Key::KEY_FN_F, + evdev::Key::KEY_FN_S, + evdev::Key::KEY_FN_B, + evdev::Key::KEY_BRL_DOT1, + evdev::Key::KEY_BRL_DOT2, + evdev::Key::KEY_BRL_DOT3, + evdev::Key::KEY_BRL_DOT4, + evdev::Key::KEY_BRL_DOT5, + evdev::Key::KEY_BRL_DOT6, + evdev::Key::KEY_BRL_DOT7, + evdev::Key::KEY_BRL_DOT8, + evdev::Key::KEY_BRL_DOT9, + evdev::Key::KEY_BRL_DOT10, + evdev::Key::KEY_NUMERIC_0, + evdev::Key::KEY_NUMERIC_1, + evdev::Key::KEY_NUMERIC_2, + evdev::Key::KEY_NUMERIC_3, + evdev::Key::KEY_NUMERIC_4, + evdev::Key::KEY_NUMERIC_5, + evdev::Key::KEY_NUMERIC_6, + evdev::Key::KEY_NUMERIC_7, + evdev::Key::KEY_NUMERIC_8, + evdev::Key::KEY_NUMERIC_9, + evdev::Key::KEY_NUMERIC_STAR, + evdev::Key::KEY_NUMERIC_POUND, + evdev::Key::KEY_NUMERIC_A, + evdev::Key::KEY_NUMERIC_B, + evdev::Key::KEY_NUMERIC_C, + evdev::Key::KEY_NUMERIC_D, + evdev::Key::KEY_CAMERA_FOCUS, + evdev::Key::KEY_WPS_BUTTON, + evdev::Key::KEY_TOUCHPAD_TOGGLE, + evdev::Key::KEY_TOUCHPAD_ON, + evdev::Key::KEY_TOUCHPAD_OFF, + evdev::Key::KEY_CAMERA_ZOOMIN, + evdev::Key::KEY_CAMERA_ZOOMOUT, + evdev::Key::KEY_CAMERA_UP, + evdev::Key::KEY_CAMERA_DOWN, + evdev::Key::KEY_CAMERA_LEFT, + evdev::Key::KEY_CAMERA_RIGHT, + evdev::Key::KEY_ATTENDANT_ON, + evdev::Key::KEY_ATTENDANT_OFF, + evdev::Key::KEY_ATTENDANT_TOGGLE, + evdev::Key::KEY_LIGHTS_TOGGLE, + evdev::Key::BTN_DPAD_UP, + evdev::Key::BTN_DPAD_DOWN, + evdev::Key::BTN_DPAD_LEFT, + evdev::Key::BTN_DPAD_RIGHT, + evdev::Key::KEY_ALS_TOGGLE, + evdev::Key::KEY_BUTTONCONFIG, + evdev::Key::KEY_TASKMANAGER, + evdev::Key::KEY_JOURNAL, + evdev::Key::KEY_CONTROLPANEL, + evdev::Key::KEY_APPSELECT, + evdev::Key::KEY_SCREENSAVER, + evdev::Key::KEY_VOICECOMMAND, + evdev::Key::KEY_ASSISTANT, + evdev::Key::KEY_KBD_LAYOUT_NEXT, + evdev::Key::KEY_BRIGHTNESS_MIN, + evdev::Key::KEY_BRIGHTNESS_MAX, + evdev::Key::KEY_KBDINPUTASSIST_PREV, + evdev::Key::KEY_KBDINPUTASSIST_NEXT, + evdev::Key::KEY_KBDINPUTASSIST_PREVGROUP, + evdev::Key::KEY_KBDINPUTASSIST_NEXTGROUP, + evdev::Key::KEY_KBDINPUTASSIST_ACCEPT, + evdev::Key::KEY_KBDINPUTASSIST_CANCEL, + evdev::Key::KEY_RIGHT_UP, + evdev::Key::KEY_RIGHT_DOWN, + evdev::Key::KEY_LEFT_UP, + evdev::Key::KEY_LEFT_DOWN, + evdev::Key::KEY_ROOT_MENU, + evdev::Key::KEY_MEDIA_TOP_MENU, + evdev::Key::KEY_NUMERIC_11, + evdev::Key::KEY_NUMERIC_12, + evdev::Key::KEY_AUDIO_DESC, + evdev::Key::KEY_3D_MODE, + evdev::Key::KEY_NEXT_FAVORITE, + evdev::Key::KEY_STOP_RECORD, + evdev::Key::KEY_PAUSE_RECORD, + evdev::Key::KEY_VOD, + evdev::Key::KEY_UNMUTE, + evdev::Key::KEY_FASTREVERSE, + evdev::Key::KEY_SLOWREVERSE, + evdev::Key::KEY_DATA, + evdev::Key::KEY_ONSCREEN_KEYBOARD, + evdev::Key::KEY_PRIVACY_SCREEN_TOGGLE, + evdev::Key::KEY_SELECTIVE_SCREENSHOT, + evdev::Key::BTN_TRIGGER_HAPPY1, + evdev::Key::BTN_TRIGGER_HAPPY2, + evdev::Key::BTN_TRIGGER_HAPPY3, + evdev::Key::BTN_TRIGGER_HAPPY4, + evdev::Key::BTN_TRIGGER_HAPPY5, + evdev::Key::BTN_TRIGGER_HAPPY6, + evdev::Key::BTN_TRIGGER_HAPPY7, + evdev::Key::BTN_TRIGGER_HAPPY8, + evdev::Key::BTN_TRIGGER_HAPPY9, + evdev::Key::BTN_TRIGGER_HAPPY10, + evdev::Key::BTN_TRIGGER_HAPPY11, + evdev::Key::BTN_TRIGGER_HAPPY12, + evdev::Key::BTN_TRIGGER_HAPPY13, + evdev::Key::BTN_TRIGGER_HAPPY14, + evdev::Key::BTN_TRIGGER_HAPPY15, + evdev::Key::BTN_TRIGGER_HAPPY16, + evdev::Key::BTN_TRIGGER_HAPPY17, + evdev::Key::BTN_TRIGGER_HAPPY18, + evdev::Key::BTN_TRIGGER_HAPPY19, + evdev::Key::BTN_TRIGGER_HAPPY20, + evdev::Key::BTN_TRIGGER_HAPPY21, + evdev::Key::BTN_TRIGGER_HAPPY22, + evdev::Key::BTN_TRIGGER_HAPPY23, + evdev::Key::BTN_TRIGGER_HAPPY24, + evdev::Key::BTN_TRIGGER_HAPPY25, + evdev::Key::BTN_TRIGGER_HAPPY26, + evdev::Key::BTN_TRIGGER_HAPPY27, + evdev::Key::BTN_TRIGGER_HAPPY28, + evdev::Key::BTN_TRIGGER_HAPPY29, + evdev::Key::BTN_TRIGGER_HAPPY30, + evdev::Key::BTN_TRIGGER_HAPPY31, + evdev::Key::BTN_TRIGGER_HAPPY32, + evdev::Key::BTN_TRIGGER_HAPPY33, + evdev::Key::BTN_TRIGGER_HAPPY34, + evdev::Key::BTN_TRIGGER_HAPPY35, + evdev::Key::BTN_TRIGGER_HAPPY36, + evdev::Key::BTN_TRIGGER_HAPPY37, + evdev::Key::BTN_TRIGGER_HAPPY38, + evdev::Key::BTN_TRIGGER_HAPPY39, + evdev::Key::BTN_TRIGGER_HAPPY40, + ]; +}