rules: use GDK to get current keyboard modifiers

This commit is contained in:
Peter F. Patel-Schneider 2022-02-04 10:38:31 -05:00
parent 3e2be09cb5
commit 4c43910ed7
1 changed files with 41 additions and 68 deletions

View File

@ -25,9 +25,9 @@ from logging import INFO as _INFO
from logging import getLogger from logging import getLogger
from math import sqrt as _sqrt from math import sqrt as _sqrt
import _thread
import psutil import psutil
from gi.repository import Gdk, GLib
from solaar.ui.config_panel import change_setting as _change_setting from solaar.ui.config_panel import change_setting as _change_setting
from yaml import add_representer as _yaml_add_representer from yaml import add_representer as _yaml_add_representer
from yaml import dump_all as _yaml_dump_all from yaml import dump_all as _yaml_dump_all
@ -45,52 +45,29 @@ del getLogger
# See docs/rules.md for documentation # See docs/rules.md for documentation
# #
# many of the rule features require X11 so turn rule processing off if X11 is not available
XK_KEYS = {} XK_KEYS = {}
try: try:
import Xlib import Xlib
from Xlib import X from Xlib import X
from Xlib.display import Display from Xlib.display import Display
from Xlib.ext import record
from Xlib.protocol import rq
from Xlib import XK as _XK from Xlib import XK as _XK
_XK.load_keysym_group('xf86') _XK.load_keysym_group('xf86')
XK_KEYS = vars(_XK) XK_KEYS = vars(_XK)
disp_prog = Display() xdisplay = Display()
modifier_keycodes = xdisplay.get_modifier_mapping() # there should be a way to do this in Gdk
x11 = True x11 = True
NET_ACTIVE_WINDOW = disp_prog.intern_atom('_NET_ACTIVE_WINDOW') NET_ACTIVE_WINDOW = xdisplay.intern_atom('_NET_ACTIVE_WINDOW')
NET_WM_PID = disp_prog.intern_atom('_NET_WM_PID') NET_WM_PID = xdisplay.intern_atom('_NET_WM_PID')
WM_CLASS = disp_prog.intern_atom('WM_CLASS') WM_CLASS = xdisplay.intern_atom('WM_CLASS')
except Exception: except Exception:
_log.warn( _log.warn(
'X11 not available - rules cannot access current process or modifier key state nor can they simulate input', 'X11 not available - rules cannot access current process or modifier key state nor can they simulate input',
# 'X11 not available - rules cannot access current process nor can they simulate input',
exc_info=_sys.exc_info() exc_info=_sys.exc_info()
) )
modifier_keycodes = []
x11 = False x11 = False
# determine current key modifiers - there must be a better way to do this
if x11:
display = Display()
try:
context = display.record_create_context(
0, [record.AllClients], [{
'core_requests': (0, 0),
'core_replies': (0, 0),
'ext_requests': (0, 0, 0, 0),
'ext_replies': (0, 0, 0, 0),
'delivered_events': (0, 0),
'device_events': (X.KeyPress, X.KeyRelease),
'errors': (0, 0),
'client_started': False,
'client_died': False,
}]
)
except Exception:
_log.warn('X11 xtest not available - rules cannot access modifier key state', exc_info=_sys.exc_info())
context = None
modifier_keycodes = display.get_modifier_mapping()
current_key_modifiers = 0
def modifier_code(keycode): def modifier_code(keycode):
if keycode == 0: if keycode == 0:
@ -100,22 +77,8 @@ def modifier_code(keycode):
return m return m
def key_press_handler(reply): gdisplay = Gdk.Display.get_default()
global current_key_modifiers gkeymap = Gdk.Keymap.get_for_display(gdisplay)
data = reply.data
while len(data):
event, data = rq.EventField(None).parse_binary_value(data, display.display, None, None)
if event.type == X.KeyPress:
mod = modifier_code(event.detail)
current_key_modifiers = event.state | 1 << mod if mod is not None else event.state
elif event.type == X.KeyRelease:
mod = modifier_code(event.detail)
current_key_modifiers = event.state & ~(1 << mod) if mod is not None else event.state
if x11 and context is not None:
_thread.start_new_thread(display.record_enable_context, (context, key_press_handler))
# display.record_free_context(context) when should this be done??
key_down = None key_down = None
key_up = None key_up = None
@ -295,7 +258,7 @@ class And(Condition):
def x11_focus_prog(): def x11_focus_prog():
pid = wm_class = None pid = wm_class = None
window = disp_prog.get_input_focus().focus window = xdisplay.get_input_focus().focus
while window: while window:
pid = window.get_full_property(NET_WM_PID, 0) pid = window.get_full_property(NET_WM_PID, 0)
wm_class = window.get_wm_class() wm_class = window.get_wm_class()
@ -311,7 +274,7 @@ def x11_focus_prog():
def x11_pointer_prog(): def x11_pointer_prog():
pid = wm_class = None pid = wm_class = None
window = disp_prog.screen().root.query_pointer().child window = xdisplay.screen().root.query_pointer().child
for window in reversed(window.query_tree().children): for window in reversed(window.query_tree().children):
pid = window.get_full_property(NET_WM_PID, 0) pid = window.get_full_property(NET_WM_PID, 0)
wm_class = window.get_wm_class() wm_class = window.get_wm_class()
@ -401,7 +364,12 @@ class Report(Condition):
return {'Report': self.report} return {'Report': self.report}
MODIFIERS = {'Shift': 0x01, 'Control': 0x04, 'Alt': 0x08, 'Super': 0x40} MODIFIERS = {
'Shift': int(Gdk.ModifierType.SHIFT_MASK),
'Control': int(Gdk.ModifierType.CONTROL_MASK),
'Alt': int(Gdk.ModifierType.MOD1_MASK),
'Super': int(Gdk.ModifierType.MOD4_MASK)
}
MODIFIER_MASK = MODIFIERS['Shift'] + MODIFIERS['Control'] + MODIFIERS['Alt'] + MODIFIERS['Super'] MODIFIER_MASK = MODIFIERS['Shift'] + MODIFIERS['Control'] + MODIFIERS['Alt'] + MODIFIERS['Super']
@ -416,16 +384,14 @@ class Modifiers(Condition):
self.modifiers.append(k) self.modifiers.append(k)
else: else:
_log.warn('unknown rule Modifier value: %s', k) _log.warn('unknown rule Modifier value: %s', k)
if not x11:
_log.warn('X11 not available - rules cannot access keyboard modifier state - %s', self)
def __str__(self): def __str__(self):
return 'Modifiers: ' + str(self.desired) return 'Modifiers: ' + str(self.desired)
def evaluate(self, feature, notification, device, status, last_result): def evaluate(self, feature, notification, device, status, last_result):
if not context: ## (_, _, _, current) = gdisplay.get_pointer() # get the current keyboard modifier
_log.warn('X11 xtest not available - rules cannot access modifier key state - %s', self) current = gkeymap.get_modifier_state() # get the current keyboard modifier
return self.desired == (current_key_modifiers & MODIFIER_MASK) return self.desired == (current & MODIFIER_MASK)
def data(self): def data(self):
return {'Modifiers': [str(m) for m in self.modifiers]} return {'Modifiers': [str(m) for m in self.modifiers]}
@ -584,37 +550,44 @@ class KeyPress(Action):
key_from_string = lambda s: (displayt.keysym_to_keycode(Xlib.XK.string_to_keysym(s))) key_from_string = lambda s: (displayt.keysym_to_keycode(Xlib.XK.string_to_keysym(s)))
self.keys = [isinstance(k, str) and key_from_string(k) for k in keys] self.keys = [isinstance(k, str) and key_from_string(k) for k in keys]
if not all(self.keys): if not all(self.keys):
_log.warn('rule KeyPress argument not sequence of key names %s', keys) _log.warn('rule KeyPress argument not sequence of current key names %s', keys)
self.keys = [] self.keys = []
else: else:
self.keys = [] self.keys = []
_log.warn('X11 not available - rules cannot simulate keyboard input - %s', self) _log.warn('X11 not available - rules cannot simulate keyboard input - %s', self)
def string_to_keycode(self, s, modifiers): # should take group and shift into account
keysym = Xlib.XK.string_to_keysym(s)
keycodes = gkeymap.get_entries_for_keyval(keysym)
for k in keycodes.keys:
return k.keycode
_log.warn('rule KeyPress key name not currently available %s', self)
def __str__(self): def __str__(self):
return 'KeyPress: ' + ' '.join(self.key_symbols) return 'KeyPress: ' + ' '.join(self.key_symbols)
def needed(self, k, current_key_modifiers): def needed(self, k, modifiers):
if not context:
_log.warn('X11 xtest not available - rules cannot access modifier key state - %s', self)
code = modifier_code(k) code = modifier_code(k)
return not (code and current_key_modifiers & (1 << code)) return not (code is not None and modifiers & (1 << code))
def keyDown(self, keys, modifiers): def keyDown(self, keys, modifiers):
for k in keys: for k in keys:
if self.needed(k, modifiers) and x11: keycode = self.string_to_keycode(k, modifiers)
Xlib.ext.xtest.fake_input(displayt, X.KeyPress, k) if self.needed(keycode, modifiers) and x11 and keycode:
Xlib.ext.xtest.fake_input(displayt, X.KeyPress, keycode)
def keyUp(self, keys, modifiers): def keyUp(self, keys, modifiers):
for k in keys: for k in keys:
if self.needed(k, modifiers) and x11: keycode = self.string_to_keycode(k, modifiers)
Xlib.ext.xtest.fake_input(displayt, X.KeyRelease, k) if self.needed(keycode, modifiers) and x11 and keycode:
Xlib.ext.xtest.fake_input(displayt, X.KeyRelease, keycode)
def evaluate(self, feature, notification, device, status, last_result): def evaluate(self, feature, notification, device, status, last_result):
current = current_key_modifiers current = gkeymap.get_modifier_state()
if _log.isEnabledFor(_INFO): if _log.isEnabledFor(_INFO):
_log.info('KeyPress action: %s, modifiers %s %s', self.key_symbols, current, [hex(k) for k in self.keys]) _log.info('KeyPress action: %s, modifiers %s %s', self.key_symbols, current, [hex(k) for k in self.keys])
self.keyDown(self.keys, current) self.keyDown(self.key_symbols, current)
self.keyUp(reversed(self.keys), current) self.keyUp(reversed(self.key_symbols), current)
if x11: if x11:
displayt.sync() displayt.sync()
return None return None
@ -889,7 +862,7 @@ def process_notification(device, status, notification, feature):
if mr_key_down and not new_mr_key_down: if mr_key_down and not new_mr_key_down:
key_up = _CONTROL['MR'] key_up = _CONTROL['MR']
mr_key_down = new_mr_key_down mr_key_down = new_mr_key_down
rules.evaluate(feature, notification, device, status, True) GLib.idle_add(rules.evaluate, feature, notification, device, status, True)
_XDG_CONFIG_HOME = _os.environ.get('XDG_CONFIG_HOME') or _path.expanduser(_path.join('~', '.config')) _XDG_CONFIG_HOME = _os.environ.get('XDG_CONFIG_HOME') or _path.expanduser(_path.join('~', '.config'))