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, ...]
# 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

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

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

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

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

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

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

@ -20,6 +20,7 @@ use crate::client::Client;
struct State {
active_window: Option<ObjectId>,
windows: HashMap<ObjectId, String>,
titles: HashMap<ObjectId, String>,
}
#[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> {
let queue = self.queue.as_mut()?;
@ -102,6 +119,7 @@ impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for State {
) {
if let ManagerEvent::Toplevel { toplevel } = event {
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 } => {
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;

@ -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<String> {
// TODO: not implemented
None
}
fn current_application(&mut self) -> Option<String> {
self.connect();

@ -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<Vec<ApplicationMatcher>>,
#[serde(default, deserialize_with = "deserialize_matchers")]

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

@ -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<String>,
title_cache: Option<String>,
// State machine for multi-purpose keys
multi_purpose_keys: HashMap<Key, MultiPurposeKeyState>,
// 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<bool, Box<dyn Error>> {
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<ModmapAction> {
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() {

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

Loading…
Cancel
Save