diff --git a/Cargo.lock b/Cargo.lock index f400c8a..18cd97c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + [[package]] name = "proc-macro2" version = "1.0.34" @@ -220,6 +226,16 @@ dependencies = [ "tap", ] +[[package]] +name = "x11" +version = "2.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "xremap" version = "0.1.0" @@ -230,6 +246,7 @@ dependencies = [ "nix", "serde", "serde_yaml", + "x11", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2023fc5..a0c6d4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ lazy_static = "1.4.0" nix = "0.23.1" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" +x11 = { version = "2.19.1", features = ["xlib"] } diff --git a/example/config.yml b/example/config.yml index bb5c03a..a93df36 100644 --- a/example/config.yml +++ b/example/config.yml @@ -106,12 +106,12 @@ keymap: # workaround prefix key bug M-r: remap: - 3: C-M-3 - 0: C-M-0 + '3': C-M-3 + '0': C-M-0 - name: Chrome, Slack (modified from Default) wm_class: - not: [Google-chrome, Slack] + only: [Google-chrome, Slack] remap: # Emacs basic C-b: left @@ -185,10 +185,10 @@ keymap: wm_class: only: Nocturn remap: - Super-j: M-j, - Super-k: M-k, - Super-o: M-o, - Super-p: M-p, + Super-j: M-j + Super-k: M-k + Super-o: M-o + Super-p: M-p Super-Enter: Shift-Enter - name: Slack diff --git a/example/test.yml b/example/test.yml index 18d3142..fdef0d4 100644 --- a/example/test.yml +++ b/example/test.yml @@ -4,7 +4,7 @@ # a: b keymap: - name: Global + wm_class: + only: Slack remap: - C-i: - remap: - C-a: C-u + C-i: C-u diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..afc7592 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1 @@ +pub mod x11_client; diff --git a/src/client/x11_client.rs b/src/client/x11_client.rs new file mode 100644 index 0000000..8b6c064 --- /dev/null +++ b/src/client/x11_client.rs @@ -0,0 +1,116 @@ +pub struct X11Client { + // Both of them are lazily initialized + display: Option<*mut x11::xlib::Display>, + supported: Option, + last_wm_class: String, +} + +impl X11Client { + pub fn new() -> X11Client { + X11Client { + display: None, + supported: None, + last_wm_class: String::new(), + } + } + + pub fn supported(&mut self) -> bool { + match self.supported { + Some(supported) => supported, + None => { + let display = self.display(); + let mut focused_window = 0; + let mut focus_state = 0; + unsafe { x11::xlib::XGetInputFocus(display, &mut focused_window, &mut focus_state) }; + let supported = focused_window > 0; + self.supported = Some(supported); + supported + }, + } + } + + pub fn current_wm_class(&mut self) -> Option { + if !self.supported() { + return None + } + + let display = self.display(); + let mut focused_window = 0; + let mut focus_state = 0; + unsafe { x11::xlib::XGetInputFocus(display, &mut focused_window, &mut focus_state) }; + + let mut x_class_hint = x11::xlib::XClassHint { + res_name: std::ptr::null_mut(), + res_class: std::ptr::null_mut(), + }; + loop { + unsafe { + if x11::xlib::XGetClassHint(display, focused_window, &mut x_class_hint) == 1 { + break; + } + } + + let mut nchildren: u32 = 0; + let mut root: x11::xlib::Window = 0; + let mut parent: x11::xlib::Window = 0; + let mut children: *mut x11::xlib::Window = &mut 0; + unsafe { + if x11::xlib::XQueryTree( + display, + focused_window, + &mut root, + &mut parent, + &mut children, + &mut nchildren, + ) == 0 + { + break; + } + } + if !children.is_null() { + unsafe { + x11::xlib::XFree(children as *mut std::ffi::c_void); + } + } + + // The root client's parent is NULL. Avoid querying it to prevent SEGV on XGetClientHint. + if parent == 0 { + return None; + } + focused_window = parent; + } + + if !x_class_hint.res_name.is_null() { + unsafe { + x11::xlib::XFree(x_class_hint.res_name as *mut std::ffi::c_void); + } + } + + if !x_class_hint.res_class.is_null() { + let wm_class = unsafe { + // Note: This seems to free `x_class_hint.res_class`. So XFree on it would double-free it. + std::ffi::CString::from_raw(x_class_hint.res_class as *mut i8) + .into_string() + .unwrap() + }; + if &self.last_wm_class != &wm_class { + self.last_wm_class = wm_class.clone(); + println!("wm_class: {}", &wm_class); + } + Some(wm_class) + } else { + None + } + } + + fn display(&mut self) -> *mut x11::xlib::Display { + match self.display { + Some(display) => display, + None => { + let display = unsafe { x11::xlib::XOpenDisplay(std::ptr::null()) }; + self.display = Some(display); + display + } + } + } +} diff --git a/src/config/key_press.rs b/src/config/key_press.rs index 03cdcfc..1c452ea 100644 --- a/src/config/key_press.rs +++ b/src/config/key_press.rs @@ -1,7 +1,7 @@ use crate::config::key::parse_key; use evdev::Key; use serde::de; -use serde::de::Visitor; +use serde::de::{Visitor}; use serde::{Deserialize, Deserializer}; use std::error; use std::fmt::Formatter; diff --git a/src/event_handler.rs b/src/event_handler.rs index abf93d8..5a6c488 100644 --- a/src/event_handler.rs +++ b/src/event_handler.rs @@ -6,9 +6,11 @@ use evdev::{EventType, InputEvent, Key}; use lazy_static::lazy_static; use std::collections::HashMap; use std::error::Error; +use crate::client::x11_client::X11Client; pub struct EventHandler { device: VirtualDevice, + x11_client: X11Client, override_remap: Option>>, shift: bool, control: bool, @@ -20,6 +22,7 @@ impl EventHandler { pub fn new(device: VirtualDevice) -> EventHandler { EventHandler { device, + x11_client: X11Client::new(), override_remap: None, shift: false, control: false, @@ -69,6 +72,7 @@ impl EventHandler { if !is_pressed(value) { return None; } + let mut wm_class_cache: Option = None; let key_press = KeyPress { key: key.clone(), @@ -86,6 +90,28 @@ impl EventHandler { } for keymap in &config.keymap { if let Some(actions) = keymap.remap.get(&key_press) { + // Lazily check wm_class as needed + if let Some(wm_class_matcher) = &keymap.wm_class { + if let None = &wm_class_cache { + match self.x11_client.current_wm_class() { + Some(wm_class) => wm_class_cache = Some(wm_class), + None => wm_class_cache = Some(String::new()), + } + } + if let Some(wm_class) = &wm_class_cache { + if let Some(wm_class_only) = &wm_class_matcher.only { + if !wm_class_only.contains(wm_class) { + continue; + } + } + if let Some(wm_class_not) = &wm_class_matcher.not { + if wm_class_not.contains(wm_class) { + continue; + } + } + } + } + return Some(actions.iter().map(|a| a.clone()).collect()); } } diff --git a/src/main.rs b/src/main.rs index 906b179..689ef64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::process::exit; extern crate getopts; +mod client; mod config; mod event_handler; mod input;