Window matcher for modmap and keymap (wlroots client only) (#447)

pull/448/head
jixiuf 2 months ago committed by GitHub
parent 8dede9f0b1
commit f6b10cebc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -208,6 +208,10 @@ modmap:
not: [Application, ...] not: [Application, ...]
# or # or
only: [Application, ...] only: [Application, ...]
window: # Optional (only wlroots clients supported)
not: [/regex of window title/, ...]
# or
only: [/regex of window title/, ...]
device: # Optional device: # Optional
not: [Device, ...] not: [Device, ...]
# or # or
@ -262,6 +266,10 @@ keymap:
not: [Application, ...] not: [Application, ...]
# or # or
only: [Application, ...] only: [Application, ...]
window: # Optional (only wlroots clients supported)
not: [/regex of window title/, ...]
# or
only: [/regex of window title/, ...]
device: # Optional device: # Optional
not: [Device, ...] not: [Device, ...]
# or # or

@ -24,6 +24,10 @@ impl Client for GnomeClient {
self.connect(); self.connect();
self.current_application().is_some() self.current_application().is_some()
} }
fn current_window(&mut self) -> Option<String> {
// TODO: not implemented
None
}
fn current_application(&mut self) -> Option<String> { fn current_application(&mut self) -> Option<String> {
self.connect(); self.connect();

@ -12,6 +12,10 @@ impl Client for HyprlandClient {
fn supported(&mut self) -> bool { fn supported(&mut self) -> bool {
true true
} }
fn current_window(&mut self) -> Option<String> {
// TODO: not implemented
None
}
fn current_application(&mut self) -> Option<String> { fn current_application(&mut self) -> Option<String> {
if let Ok(win_opt) = HyprClient::get_active() { if let Ok(win_opt) = HyprClient::get_active() {

@ -173,6 +173,10 @@ impl Client for KdeClient {
} }
conn_res.is_ok() conn_res.is_ok()
} }
fn current_window(&mut self) -> Option<String> {
// TODO: not implemented
None
}
fn current_application(&mut self) -> Option<String> { fn current_application(&mut self) -> Option<String> {
let aw = self.active_window.lock().unwrap(); let aw = self.active_window.lock().unwrap();

@ -1,6 +1,7 @@
pub trait Client { pub trait Client {
fn supported(&mut self) -> bool; fn supported(&mut self) -> bool;
fn current_application(&mut self) -> Option<String>; fn current_application(&mut self) -> Option<String>;
fn current_window(&mut self) -> Option<String>;
} }
pub struct WMClient { pub struct WMClient {
@ -8,6 +9,7 @@ pub struct WMClient {
client: Box<dyn Client>, client: Box<dyn Client>,
supported: Option<bool>, supported: Option<bool>,
last_application: String, last_application: String,
last_window: String,
} }
impl WMClient { impl WMClient {
@ -17,8 +19,28 @@ impl WMClient {
client, client,
supported: None, supported: None,
last_application: String::new(), last_application: String::new(),
last_window: String::new(),
} }
} }
pub fn current_window(&mut self) -> Option<String> {
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<String> { pub fn current_application(&mut self) -> Option<String> {
if self.supported.is_none() { if self.supported.is_none() {

@ -6,6 +6,9 @@ impl Client for NullClient {
fn supported(&mut self) -> bool { fn supported(&mut self) -> bool {
false false
} }
fn current_window(&mut self) -> Option<String> {
None
}
fn current_application(&mut self) -> Option<String> { fn current_application(&mut self) -> Option<String> {
None None

@ -40,6 +40,10 @@ impl Client for SwayClient {
self.connect(); self.connect();
self.connection.is_some() self.connection.is_some()
} }
fn current_window(&mut self) -> Option<String> {
// TODO: not implemented
None
}
fn current_application(&mut self) -> Option<String> { fn current_application(&mut self) -> Option<String> {
self.connect(); self.connect();

@ -20,6 +20,7 @@ use crate::client::Client;
struct State { struct State {
active_window: Option<ObjectId>, active_window: Option<ObjectId>,
windows: HashMap<ObjectId, String>, windows: HashMap<ObjectId, String>,
titles: HashMap<ObjectId, String>,
} }
#[derive(Default)] #[derive(Default)]
@ -59,6 +60,22 @@ impl Client for WlRootsClient {
} }
} }
} }
fn current_window(&mut self) -> Option<String> {
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<String> { fn current_application(&mut self) -> Option<String> {
let queue = self.queue.as_mut()?; let queue = self.queue.as_mut()?;
@ -102,6 +119,7 @@ impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for State {
) { ) {
if let ManagerEvent::Toplevel { toplevel } = event { if let ManagerEvent::Toplevel { toplevel } = event {
state.windows.insert(toplevel.id(), "<unknown>".into()); state.windows.insert(toplevel.id(), "<unknown>".into());
state.titles.insert(toplevel.id(), "<unknown>".into());
} }
} }
@ -123,8 +141,12 @@ impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for State {
HandleEvent::AppId { app_id } => { HandleEvent::AppId { app_id } => {
state.windows.insert(handle.id(), app_id); state.windows.insert(handle.id(), app_id);
} }
HandleEvent::Title { title } => {
state.titles.insert(handle.id(), title);
}
HandleEvent::Closed => { HandleEvent::Closed => {
state.windows.remove(&handle.id()); state.windows.remove(&handle.id());
state.titles.remove(&handle.id());
} }
HandleEvent::State { state: handle_state } => { HandleEvent::State { state: handle_state } => {
let activated = HandleState::Activated as u8; let activated = HandleState::Activated as u8;

@ -48,6 +48,10 @@ impl Client for X11Client {
return self.connection.is_some(); return self.connection.is_some();
// TODO: Test XGetInputFocus and focused_window > 0? // TODO: Test XGetInputFocus and focused_window > 0?
} }
fn current_window(&mut self) -> Option<String> {
// TODO: not implemented
None
}
fn current_application(&mut self) -> Option<String> { fn current_application(&mut self) -> Option<String> {
self.connect(); self.connect();

@ -7,7 +7,7 @@ use serde::{Deserialize, Deserializer};
// TODO: Use trait to allow only either `only` or `not` // TODO: Use trait to allow only either `only` or `not`
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct Application { pub struct OnlyOrNot {
#[serde(default, deserialize_with = "deserialize_matchers")] #[serde(default, deserialize_with = "deserialize_matchers")]
pub only: Option<Vec<ApplicationMatcher>>, pub only: Option<Vec<ApplicationMatcher>>,
#[serde(default, deserialize_with = "deserialize_matchers")] #[serde(default, deserialize_with = "deserialize_matchers")]

@ -1,5 +1,5 @@
use crate::config::application::deserialize_string_or_vec; 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::key_press::KeyPress;
use crate::config::keymap_action::{Actions, KeymapAction}; use crate::config::keymap_action::{Actions, KeymapAction};
use evdev::Key; use evdev::Key;
@ -17,7 +17,8 @@ pub struct Keymap {
pub name: String, pub name: String,
#[serde(deserialize_with = "deserialize_remap")] #[serde(deserialize_with = "deserialize_remap")]
pub remap: HashMap<KeyPress, Vec<KeymapAction>>, pub remap: HashMap<KeyPress, Vec<KeymapAction>>,
pub application: Option<Application>, pub application: Option<OnlyOrNot>,
pub window: Option<OnlyOrNot>,
pub device: Option<Device>, pub device: Option<Device>,
#[serde(default, deserialize_with = "deserialize_string_or_vec")] #[serde(default, deserialize_with = "deserialize_string_or_vec")]
pub mode: Option<Vec<String>>, pub mode: Option<Vec<String>>,
@ -41,7 +42,8 @@ where
pub struct KeymapEntry { pub struct KeymapEntry {
pub actions: Vec<KeymapAction>, pub actions: Vec<KeymapAction>,
pub modifiers: Vec<Modifier>, pub modifiers: Vec<Modifier>,
pub application: Option<Application>, pub application: Option<OnlyOrNot>,
pub title: Option<OnlyOrNot>,
pub device: Option<Device>, pub device: Option<Device>,
pub mode: Option<Vec<String>>, pub mode: Option<Vec<String>>,
pub exact_match: bool, pub exact_match: bool,
@ -65,6 +67,7 @@ pub fn build_keymap_table(keymaps: &Vec<Keymap>) -> HashMap<Key, Vec<KeymapEntry
actions: actions.to_vec(), actions: actions.to_vec(),
modifiers: key_press.modifiers.clone(), modifiers: key_press.modifiers.clone(),
application: keymap.application.clone(), application: keymap.application.clone(),
title: keymap.window.clone(),
device: keymap.device.clone(), device: keymap.device.clone(),
mode: keymap.mode.clone(), mode: keymap.mode.clone(),
exact_match: keymap.exact_match, exact_match: keymap.exact_match,

@ -1,4 +1,4 @@
use crate::config::application::Application; use crate::config::application::OnlyOrNot;
use crate::config::key::deserialize_key; use crate::config::key::deserialize_key;
use crate::config::modmap_action::ModmapAction; use crate::config::modmap_action::ModmapAction;
use evdev::Key; use evdev::Key;
@ -14,7 +14,8 @@ pub struct Modmap {
pub name: String, pub name: String,
#[serde(deserialize_with = "deserialize_remap")] #[serde(deserialize_with = "deserialize_remap")]
pub remap: HashMap<Key, ModmapAction>, pub remap: HashMap<Key, ModmapAction>,
pub application: Option<Application>, pub application: Option<OnlyOrNot>,
pub window: Option<OnlyOrNot>,
pub device: Option<Device>, pub device: Option<Device>,
} }

@ -1,6 +1,6 @@
use crate::action::Action; use crate::action::Action;
use crate::client::WMClient; use crate::client::WMClient;
use crate::config::application::Application; use crate::config::application::OnlyOrNot;
use crate::config::key_press::{KeyPress, Modifier}; use crate::config::key_press::{KeyPress, Modifier};
use crate::config::keymap::{build_override_table, OverrideEntry}; use crate::config::keymap::{build_override_table, OverrideEntry};
use crate::config::keymap_action::KeymapAction; use crate::config::keymap_action::KeymapAction;
@ -35,6 +35,7 @@ pub struct EventHandler {
// Check the currently active application // Check the currently active application
application_client: WMClient, application_client: WMClient,
application_cache: Option<String>, application_cache: Option<String>,
title_cache: Option<String>,
// State machine for multi-purpose keys // State machine for multi-purpose keys
multi_purpose_keys: HashMap<Key, MultiPurposeKeyState>, multi_purpose_keys: HashMap<Key, MultiPurposeKeyState>,
// Current nested remaps // Current nested remaps
@ -68,6 +69,7 @@ impl EventHandler {
pressed_keys: HashMap::new(), pressed_keys: HashMap::new(),
application_client, application_client,
application_cache: None, application_cache: None,
title_cache: None,
multi_purpose_keys: HashMap::new(), multi_purpose_keys: HashMap::new(),
override_remaps: vec![], override_remaps: vec![],
override_timeout_key: None, override_timeout_key: None,
@ -113,6 +115,7 @@ impl EventHandler {
device: &InputDeviceInfo, device: &InputDeviceInfo,
) -> Result<bool, Box<dyn Error>> { ) -> Result<bool, Box<dyn Error>> {
self.application_cache = None; // expire cache self.application_cache = None; // expire cache
self.title_cache = None; // expire cache
let key = Key::new(event.code()); let key = Key::new(event.code());
debug!("=> {}: {:?}", event.value(), &key); debug!("=> {}: {:?}", event.value(), &key);
@ -329,7 +332,11 @@ impl EventHandler {
// fallthrough on state discrepancy // fallthrough on state discrepancy
vec![(key, value)] 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 // Just hook actions, and then emit the original event. We might want to
// support reordering the key event and dispatched actions later. // support reordering the key event and dispatched actions later.
if value == PRESS || value == RELEASE { if value == PRESS || value == RELEASE {
@ -381,6 +388,11 @@ impl EventHandler {
fn find_modmap(&mut self, config: &Config, key: &Key, device: &InputDeviceInfo) -> Option<ModmapAction> { fn find_modmap(&mut self, config: &Config, key: &Key, device: &InputDeviceInfo) -> Option<ModmapAction> {
for modmap in &config.modmap { for modmap in &config.modmap {
if let Some(key_action) = modmap.remap.get(key) { 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 let Some(application_matcher) = &modmap.application {
if !self.match_application(application_matcher) { if !self.match_application(application_matcher) {
continue; continue;
@ -454,6 +466,12 @@ impl EventHandler {
if (exact_match && extra_modifiers.len() > 0) || missing_modifiers.len() > 0 { if (exact_match && extra_modifiers.len() > 0) || missing_modifiers.len() > 0 {
continue; continue;
} }
if let Some(window_matcher) = &entry.title {
if !self.match_window(window_matcher) {
continue;
}
}
if let Some(application_matcher) = &entry.application { if let Some(application_matcher) = &entry.application {
if !self.match_application(application_matcher) { if !self.match_application(application_matcher) {
continue; continue;
@ -619,8 +637,27 @@ impl EventHandler {
Modifier::Key(key) => self.modifiers.contains(key), 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 // Lazily fill the wm_class cache
if self.application_cache.is_none() { if self.application_cache.is_none() {
match self.application_client.current_application() { match self.application_client.current_application() {

@ -23,6 +23,9 @@ impl Client for StaticClient {
fn supported(&mut self) -> bool { fn supported(&mut self) -> bool {
true true
} }
fn current_window(&mut self) -> Option<String> {
None
}
fn current_application(&mut self) -> Option<String> { fn current_application(&mut self) -> Option<String> {
self.current_application.clone() self.current_application.clone()

Loading…
Cancel
Save