From f6b10cebc07d9955f1f73f88e3d80195055f4bc6 Mon Sep 17 00:00:00 2001 From: jixiuf Date: Wed, 27 Mar 2024 14:18:01 +0800 Subject: [PATCH] Window matcher for modmap and keymap (wlroots client only) (#447) --- README.md | 8 +++++++ src/client/gnome_client.rs | 4 ++++ src/client/hypr_client.rs | 4 ++++ src/client/kde_client.rs | 4 ++++ src/client/mod.rs | 22 ++++++++++++++++++ src/client/null_client.rs | 3 +++ src/client/sway_client.rs | 4 ++++ src/client/wlroots_client.rs | 22 ++++++++++++++++++ src/client/x11_client.rs | 4 ++++ src/config/application.rs | 2 +- src/config/keymap.rs | 9 +++++--- src/config/modmap.rs | 5 +++-- src/event_handler.rs | 43 +++++++++++++++++++++++++++++++++--- src/tests.rs | 3 +++ 14 files changed, 128 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a28fb8a..422b5bc 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,10 @@ modmap: not: [Application, ...] # or only: [Application, ...] + window: # Optional (only wlroots clients supported) + not: [/regex of window title/, ...] + # or + only: [/regex of window title/, ...] device: # Optional not: [Device, ...] # or @@ -262,6 +266,10 @@ keymap: not: [Application, ...] # or only: [Application, ...] + window: # Optional (only wlroots clients supported) + not: [/regex of window title/, ...] + # or + only: [/regex of window title/, ...] device: # Optional not: [Device, ...] # or diff --git a/src/client/gnome_client.rs b/src/client/gnome_client.rs index ee0c3c7..ee33edc 100644 --- a/src/client/gnome_client.rs +++ b/src/client/gnome_client.rs @@ -24,6 +24,10 @@ impl Client for GnomeClient { self.connect(); self.current_application().is_some() } + fn current_window(&mut self) -> Option { + // TODO: not implemented + None + } fn current_application(&mut self) -> Option { self.connect(); diff --git a/src/client/hypr_client.rs b/src/client/hypr_client.rs index 89b31db..c1dee54 100644 --- a/src/client/hypr_client.rs +++ b/src/client/hypr_client.rs @@ -12,6 +12,10 @@ impl Client for HyprlandClient { fn supported(&mut self) -> bool { true } + fn current_window(&mut self) -> Option { + // TODO: not implemented + None + } fn current_application(&mut self) -> Option { if let Ok(win_opt) = HyprClient::get_active() { diff --git a/src/client/kde_client.rs b/src/client/kde_client.rs index 1b24d1e..c8ad04a 100644 --- a/src/client/kde_client.rs +++ b/src/client/kde_client.rs @@ -173,6 +173,10 @@ impl Client for KdeClient { } conn_res.is_ok() } + fn current_window(&mut self) -> Option { + // TODO: not implemented + None + } fn current_application(&mut self) -> Option { let aw = self.active_window.lock().unwrap(); diff --git a/src/client/mod.rs b/src/client/mod.rs index d3be591..7d84c39 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,6 +1,7 @@ pub trait Client { fn supported(&mut self) -> bool; fn current_application(&mut self) -> Option; + fn current_window(&mut self) -> Option; } pub struct WMClient { @@ -8,6 +9,7 @@ pub struct WMClient { client: Box, supported: Option, last_application: String, + last_window: String, } impl WMClient { @@ -17,8 +19,28 @@ impl WMClient { client, supported: None, last_application: String::new(), + last_window: String::new(), } } + pub fn current_window(&mut self) -> Option { + if self.supported.is_none() { + let supported = self.client.supported(); + self.supported = Some(supported); + println!("application-client: {} (supported: {})", self.name, supported); + } + if !self.supported.unwrap() { + return None; + } + + let result = self.client.current_window(); + if let Some(window) = &result { + if &self.last_window != window { + self.last_window = window.clone(); + println!("window: {}", window); + } + } + result + } pub fn current_application(&mut self) -> Option { if self.supported.is_none() { diff --git a/src/client/null_client.rs b/src/client/null_client.rs index 7a0d573..bcdfe21 100644 --- a/src/client/null_client.rs +++ b/src/client/null_client.rs @@ -6,6 +6,9 @@ impl Client for NullClient { fn supported(&mut self) -> bool { false } + fn current_window(&mut self) -> Option { + None + } fn current_application(&mut self) -> Option { None diff --git a/src/client/sway_client.rs b/src/client/sway_client.rs index e36385c..6251351 100644 --- a/src/client/sway_client.rs +++ b/src/client/sway_client.rs @@ -40,6 +40,10 @@ impl Client for SwayClient { self.connect(); self.connection.is_some() } + fn current_window(&mut self) -> Option { + // TODO: not implemented + None + } fn current_application(&mut self) -> Option { self.connect(); diff --git a/src/client/wlroots_client.rs b/src/client/wlroots_client.rs index 9a938a1..aaf5a9f 100644 --- a/src/client/wlroots_client.rs +++ b/src/client/wlroots_client.rs @@ -20,6 +20,7 @@ use crate::client::Client; struct State { active_window: Option, windows: HashMap, + titles: HashMap, } #[derive(Default)] @@ -59,6 +60,22 @@ impl Client for WlRootsClient { } } } + fn current_window(&mut self) -> Option { + let queue = self.queue.as_mut()?; + + if let Err(_) = queue.roundtrip(&mut self.state) { + // try to reconnect + if let Err(err) = self.connect() { + log::error!("{err}"); + return None; + } + + log::debug!("Reconnected to wayland"); + } + + let id = self.state.active_window.as_ref()?; + self.state.titles.get(id).cloned() + } fn current_application(&mut self) -> Option { let queue = self.queue.as_mut()?; @@ -102,6 +119,7 @@ impl Dispatch for State { ) { if let ManagerEvent::Toplevel { toplevel } = event { state.windows.insert(toplevel.id(), "".into()); + state.titles.insert(toplevel.id(), "".into()); } } @@ -123,8 +141,12 @@ impl Dispatch for State { HandleEvent::AppId { app_id } => { state.windows.insert(handle.id(), app_id); } + HandleEvent::Title { title } => { + state.titles.insert(handle.id(), title); + } HandleEvent::Closed => { state.windows.remove(&handle.id()); + state.titles.remove(&handle.id()); } HandleEvent::State { state: handle_state } => { let activated = HandleState::Activated as u8; diff --git a/src/client/x11_client.rs b/src/client/x11_client.rs index 285ab90..4f088c1 100644 --- a/src/client/x11_client.rs +++ b/src/client/x11_client.rs @@ -48,6 +48,10 @@ impl Client for X11Client { return self.connection.is_some(); // TODO: Test XGetInputFocus and focused_window > 0? } + fn current_window(&mut self) -> Option { + // TODO: not implemented + None + } fn current_application(&mut self) -> Option { self.connect(); diff --git a/src/config/application.rs b/src/config/application.rs index aa3bf9b..ce2a83e 100644 --- a/src/config/application.rs +++ b/src/config/application.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Deserializer}; // TODO: Use trait to allow only either `only` or `not` #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] -pub struct Application { +pub struct OnlyOrNot { #[serde(default, deserialize_with = "deserialize_matchers")] pub only: Option>, #[serde(default, deserialize_with = "deserialize_matchers")] diff --git a/src/config/keymap.rs b/src/config/keymap.rs index baf085a..47fec9b 100644 --- a/src/config/keymap.rs +++ b/src/config/keymap.rs @@ -1,5 +1,5 @@ use crate::config::application::deserialize_string_or_vec; -use crate::config::application::Application; +use crate::config::application::OnlyOrNot; use crate::config::key_press::KeyPress; use crate::config::keymap_action::{Actions, KeymapAction}; use evdev::Key; @@ -17,7 +17,8 @@ pub struct Keymap { pub name: String, #[serde(deserialize_with = "deserialize_remap")] pub remap: HashMap>, - pub application: Option, + pub application: Option, + pub window: Option, pub device: Option, #[serde(default, deserialize_with = "deserialize_string_or_vec")] pub mode: Option>, @@ -41,7 +42,8 @@ where pub struct KeymapEntry { pub actions: Vec, pub modifiers: Vec, - pub application: Option, + pub application: Option, + pub title: Option, pub device: Option, pub mode: Option>, pub exact_match: bool, @@ -65,6 +67,7 @@ pub fn build_keymap_table(keymaps: &Vec) -> HashMap, - pub application: Option, + pub application: Option, + pub window: Option, pub device: Option, } diff --git a/src/event_handler.rs b/src/event_handler.rs index 9fea985..7206e23 100644 --- a/src/event_handler.rs +++ b/src/event_handler.rs @@ -1,6 +1,6 @@ use crate::action::Action; use crate::client::WMClient; -use crate::config::application::Application; +use crate::config::application::OnlyOrNot; use crate::config::key_press::{KeyPress, Modifier}; use crate::config::keymap::{build_override_table, OverrideEntry}; use crate::config::keymap_action::KeymapAction; @@ -35,6 +35,7 @@ pub struct EventHandler { // Check the currently active application application_client: WMClient, application_cache: Option, + title_cache: Option, // State machine for multi-purpose keys multi_purpose_keys: HashMap, // Current nested remaps @@ -68,6 +69,7 @@ impl EventHandler { pressed_keys: HashMap::new(), application_client, application_cache: None, + title_cache: None, multi_purpose_keys: HashMap::new(), override_remaps: vec![], override_timeout_key: None, @@ -113,6 +115,7 @@ impl EventHandler { device: &InputDeviceInfo, ) -> Result> { self.application_cache = None; // expire cache + self.title_cache = None; // expire cache let key = Key::new(event.code()); debug!("=> {}: {:?}", event.value(), &key); @@ -329,7 +332,11 @@ impl EventHandler { // fallthrough on state discrepancy vec![(key, value)] } - ModmapAction::PressReleaseKey(PressReleaseKey { skip_key_event, press, release }) => { + ModmapAction::PressReleaseKey(PressReleaseKey { + skip_key_event, + press, + release, + }) => { // Just hook actions, and then emit the original event. We might want to // support reordering the key event and dispatched actions later. if value == PRESS || value == RELEASE { @@ -381,6 +388,11 @@ impl EventHandler { fn find_modmap(&mut self, config: &Config, key: &Key, device: &InputDeviceInfo) -> Option { for modmap in &config.modmap { if let Some(key_action) = modmap.remap.get(key) { + if let Some(window_matcher) = &modmap.window { + if !self.match_window(window_matcher) { + continue; + } + } if let Some(application_matcher) = &modmap.application { if !self.match_application(application_matcher) { continue; @@ -454,6 +466,12 @@ impl EventHandler { if (exact_match && extra_modifiers.len() > 0) || missing_modifiers.len() > 0 { continue; } + if let Some(window_matcher) = &entry.title { + if !self.match_window(window_matcher) { + continue; + } + } + if let Some(application_matcher) = &entry.application { if !self.match_application(application_matcher) { continue; @@ -619,8 +637,27 @@ impl EventHandler { Modifier::Key(key) => self.modifiers.contains(key), } } + fn match_window(&mut self, window_matcher: &OnlyOrNot) -> bool { + // Lazily fill the wm_class cache + if self.title_cache.is_none() { + match self.application_client.current_window() { + Some(title) => self.title_cache = Some(title), + None => self.title_cache = Some(String::new()), + } + } + + if let Some(title) = &self.title_cache { + if let Some(title_only) = &window_matcher.only { + return title_only.iter().any(|m| m.matches(title)); + } + if let Some(title_not) = &window_matcher.not { + return title_not.iter().all(|m| !m.matches(title)); + } + } + false + } - fn match_application(&mut self, application_matcher: &Application) -> bool { + fn match_application(&mut self, application_matcher: &OnlyOrNot) -> bool { // Lazily fill the wm_class cache if self.application_cache.is_none() { match self.application_client.current_application() { diff --git a/src/tests.rs b/src/tests.rs index b805ece..da1627a 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -23,6 +23,9 @@ impl Client for StaticClient { fn supported(&mut self) -> bool { true } + fn current_window(&mut self) -> Option { + None + } fn current_application(&mut self) -> Option { self.current_application.clone()