rules: fix problems when X11 is not available
This commit is contained in:
parent
371027c690
commit
5aa02aa01d
|
@ -16,9 +16,11 @@
|
|||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import ctypes as _ctypes
|
||||
import os as _os
|
||||
import os.path as _path
|
||||
import sys as _sys
|
||||
import time as _time
|
||||
|
||||
from logging import DEBUG as _DEBUG
|
||||
from logging import INFO as _INFO
|
||||
|
@ -67,74 +69,153 @@ del getLogger
|
|||
# and http://cgit.freedesktop.org/xorg/proto/x11proto/plain/XF86keysym.h
|
||||
# because there does not seem to be a non-X11 file for this set of key names
|
||||
|
||||
# Setting up is complex because there are several systems that each provide partial facilities:
|
||||
# GDK - always available (when running with a window system) but only provides access to keymap
|
||||
# X11 - provides access to active process and process with window under mouse and current modifier keys
|
||||
# Xtest extension to X11 - provides input simulation, partly works under Wayland
|
||||
# Wayland - provides input simulation
|
||||
|
||||
XK_KEYS = _keysymdef.keysymdef
|
||||
|
||||
# Event codes - can't use Xlib.X codes because Xlib might not be available
|
||||
_KEY_RELEASE = 0
|
||||
_KEY_PRESS = 1
|
||||
_BUTTON_RELEASE = 2
|
||||
_BUTTON_PRESS = 3
|
||||
|
||||
gdisplay = Gdk.Display.get_default() # can be None if Solaar is run without a full window system
|
||||
gkeymap = Gdk.Keymap.get_for_display(gdisplay) if gdisplay else None
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info('GDK Keymap %sset up', '' if gkeymap else 'not ')
|
||||
|
||||
wayland = _os.getenv('WAYLAND_DISPLAY') # is this Wayland?
|
||||
if wayland:
|
||||
_log.warn('rules cannot access active process or modifier keys in Wayland')
|
||||
|
||||
try:
|
||||
import Xlib
|
||||
_x11 = None # X11 might be available
|
||||
except Exception:
|
||||
_x11 = False # X11 is not available
|
||||
|
||||
xtest_available = True # Xtest might be available
|
||||
xdisplay = None
|
||||
Xkbdisplay = None # xkb might be avilable
|
||||
modifier_keycodes = []
|
||||
XkbUseCoreKbd = 0x100
|
||||
|
||||
|
||||
class XkbDisplay(_ctypes.Structure):
|
||||
""" opaque struct """
|
||||
|
||||
|
||||
class XkbStateRec(_ctypes.Structure):
|
||||
_fields_ = [('group', _ctypes.c_ubyte), ('locked_group', _ctypes.c_ubyte), ('base_group', _ctypes.c_ushort),
|
||||
('latched_group', _ctypes.c_ushort), ('mods', _ctypes.c_ubyte), ('base_mods', _ctypes.c_ubyte),
|
||||
('latched_mods', _ctypes.c_ubyte), ('locked_mods', _ctypes.c_ubyte), ('compat_state', _ctypes.c_ubyte),
|
||||
('grab_mods', _ctypes.c_ubyte), ('compat_grab_mods', _ctypes.c_ubyte), ('lookup_mods', _ctypes.c_ubyte),
|
||||
('compat_lookup_mods', _ctypes.c_ubyte),
|
||||
('ptr_buttons', _ctypes.c_ushort)] # something strange is happening here but it is not being used
|
||||
|
||||
|
||||
def x11_setup():
|
||||
global _x11, xdisplay, modifier_keycodes, NET_ACTIVE_WINDOW, NET_WM_PID, WM_CLASS, xtest_available
|
||||
if _x11 is not None:
|
||||
return _x11
|
||||
try:
|
||||
from Xlib.display import Display
|
||||
xdisplay = Display()
|
||||
modifier_keycodes = xdisplay.get_modifier_mapping() # there should be a way to do this in Gdk
|
||||
x11 = True
|
||||
|
||||
NET_ACTIVE_WINDOW = xdisplay.intern_atom('_NET_ACTIVE_WINDOW')
|
||||
NET_WM_PID = xdisplay.intern_atom('_NET_WM_PID')
|
||||
WM_CLASS = xdisplay.intern_atom('WM_CLASS')
|
||||
|
||||
_x11 = True # X11 available
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info('X11 library loaded and display set up')
|
||||
except Exception:
|
||||
_log.warn(
|
||||
'X11 not available - rules cannot access current process, modifier keys, or keyboard group. %s',
|
||||
exc_info=_sys.exc_info()
|
||||
)
|
||||
modifier_keycodes = []
|
||||
x11 = False
|
||||
_log.warn('X11 not available - some rule capabilities inoperable: %s', exc_info=_sys.exc_info())
|
||||
_x11 = False
|
||||
xtest_available = False
|
||||
return _x11
|
||||
|
||||
if x11:
|
||||
try:
|
||||
# set up to get keyboard state using ctypes interface to libx11
|
||||
import ctypes
|
||||
|
||||
class XkbDisplay(ctypes.Structure):
|
||||
""" opaque struct """
|
||||
|
||||
class XkbStateRec(ctypes.Structure):
|
||||
_fields_ = [('group', ctypes.c_ubyte), ('locked_group', ctypes.c_ubyte), ('base_group', ctypes.c_ushort),
|
||||
('latched_group', ctypes.c_ushort), ('mods', ctypes.c_ubyte), ('base_mods', ctypes.c_ubyte),
|
||||
('latched_mods', ctypes.c_ubyte), ('locked_mods', ctypes.c_ubyte), ('compat_state', ctypes.c_ubyte),
|
||||
('grab_mods', ctypes.c_ubyte), ('compat_grab_mods', ctypes.c_ubyte), ('lookup_mods', ctypes.c_ubyte),
|
||||
('compat_lookup_mods', ctypes.c_ubyte),
|
||||
('ptr_buttons', ctypes.c_ushort)] # something strange is happening here but it is not being used
|
||||
|
||||
XkbUseCoreKbd = 0x100
|
||||
X11Lib = ctypes.cdll.LoadLibrary('libX11.so')
|
||||
X11Lib.XOpenDisplay.restype = ctypes.POINTER(XkbDisplay)
|
||||
X11Lib.XkbGetState.argtypes = [ctypes.POINTER(XkbDisplay), ctypes.c_uint, ctypes.POINTER(XkbStateRec)]
|
||||
def xkb_setup():
|
||||
global X11Lib, Xkbdisplay
|
||||
if Xkbdisplay is not None:
|
||||
return Xkbdisplay
|
||||
try: # set up to get keyboard state using ctypes interface to libx11
|
||||
X11Lib = _ctypes.cdll.LoadLibrary('libX11.so')
|
||||
X11Lib.XOpenDisplay.restype = _ctypes.POINTER(XkbDisplay)
|
||||
X11Lib.XkbGetState.argtypes = [_ctypes.POINTER(XkbDisplay), _ctypes.c_uint, _ctypes.POINTER(XkbStateRec)]
|
||||
Xkbdisplay = X11Lib.XOpenDisplay(None)
|
||||
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info('XKB display set up')
|
||||
except Exception:
|
||||
_log.warn('X11 library not available - rules cannot access keyboard group. %s', exc_info=_sys.exc_info())
|
||||
Xkbdisplay = None
|
||||
_log.warn('XKB display not available - rules cannot access keyboard group: %s', exc_info=_sys.exc_info())
|
||||
Xkbdisplay = False
|
||||
return Xkbdisplay
|
||||
|
||||
|
||||
buttons = {
|
||||
'unknown': (None, None),
|
||||
'left': (1, evdev.ecodes.ecodes['BTN_LEFT']),
|
||||
'middle': (2, evdev.ecodes.ecodes['BTN_MIDDLE']),
|
||||
'right': (3, evdev.ecodes.ecodes['BTN_RIGHT']),
|
||||
'scroll_up': (4, evdev.ecodes.ecodes['BTN_4']),
|
||||
'scroll_down': (5, evdev.ecodes.ecodes['BTN_5']),
|
||||
'scroll_left': (6, evdev.ecodes.ecodes['BTN_6']),
|
||||
'scroll_right': (7, evdev.ecodes.ecodes['BTN_7']),
|
||||
'button8': (8, evdev.ecodes.ecodes['BTN_8']),
|
||||
'button9': (9, evdev.ecodes.ecodes['BTN_9']),
|
||||
}
|
||||
|
||||
# uinput capability for keyboard keys, mouse buttons, and scrolling
|
||||
key_events = [c for n, c in evdev.ecodes.ecodes.items() if n.startswith('KEY') and n != 'KEY_CNT']
|
||||
for (_, evcode) in buttons.values():
|
||||
if evcode:
|
||||
key_events.append(evcode)
|
||||
devicecap = {
|
||||
evdev.ecodes.EV_KEY:
|
||||
key_events,
|
||||
evdev.ecodes.EV_REL:
|
||||
[evdev.ecodes.REL_WHEEL, evdev.ecodes.REL_HWHEEL, evdev.ecodes.REL_WHEEL_HI_RES, evdev.ecodes.REL_HWHEEL_HI_RES]
|
||||
}
|
||||
udevice = None
|
||||
|
||||
|
||||
def setup_uinput():
|
||||
global udevice
|
||||
if udevice is not None:
|
||||
return udevice
|
||||
try:
|
||||
udevice = evdev.uinput.UInput(events=devicecap, name='solaar-keyboard')
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info('uinput device set up')
|
||||
except Exception as e:
|
||||
_log.warn('cannot create uinput device: %s', e)
|
||||
|
||||
|
||||
if wayland: # wayland can't use xtest so may as well set up uinput now
|
||||
setup_uinput()
|
||||
|
||||
|
||||
def kbdgroup():
|
||||
if Xkbdisplay:
|
||||
if xkb_setup():
|
||||
state = XkbStateRec()
|
||||
X11Lib.XkbGetState(Xkbdisplay, XkbUseCoreKbd, ctypes.pointer(state))
|
||||
X11Lib.XkbGetState(Xkbdisplay, XkbUseCoreKbd, _ctypes.pointer(state))
|
||||
return state.group
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def modifier_code(keycode):
|
||||
if keycode == 0:
|
||||
if wayland or not x11_setup() or keycode == 0:
|
||||
return None
|
||||
for m in range(0, len(modifier_keycodes)):
|
||||
if keycode in modifier_keycodes[m]:
|
||||
return m
|
||||
|
||||
|
||||
gdisplay = Gdk.Display.get_default() # can be None if Solaar is run without a full window system
|
||||
gkeymap = Gdk.Keymap.get_for_display(gdisplay) if gdisplay else None
|
||||
|
||||
key_down = None
|
||||
key_up = None
|
||||
|
||||
|
@ -170,85 +251,75 @@ def xy_direction(_x, _y):
|
|||
return 'noop'
|
||||
|
||||
|
||||
buttons = {
|
||||
'unknown': (None, None),
|
||||
'left': (1, evdev.ecodes.ecodes['BTN_LEFT']),
|
||||
'middle': (2, evdev.ecodes.ecodes['BTN_MIDDLE']),
|
||||
'right': (3, evdev.ecodes.ecodes['BTN_RIGHT']),
|
||||
'scroll_up': (4, evdev.ecodes.ecodes['BTN_4']),
|
||||
'scroll_down': (5, evdev.ecodes.ecodes['BTN_5']),
|
||||
'scroll_left': (6, evdev.ecodes.ecodes['BTN_6']),
|
||||
'scroll_right': (7, evdev.ecodes.ecodes['BTN_7']),
|
||||
'button8': (8, evdev.ecodes.ecodes['BTN_8']),
|
||||
'button9': (9, evdev.ecodes.ecodes['BTN_9']),
|
||||
}
|
||||
mousecap = {
|
||||
evdev.ecodes.EV_KEY: [evcode for (_, evcode) in buttons.values() if evcode],
|
||||
evdev.ecodes.EV_REL:
|
||||
[evdev.ecodes.REL_WHEEL, evdev.ecodes.REL_HWHEEL, evdev.ecodes.REL_WHEEL_HI_RES, evdev.ecodes.REL_HWHEEL_HI_RES]
|
||||
}
|
||||
|
||||
if x11:
|
||||
displayt = Display()
|
||||
else:
|
||||
displayt = None
|
||||
try:
|
||||
ukeyboard = evdev.uinput.UInput()
|
||||
umouse = evdev.uinput.UInput(mousecap)
|
||||
except Exception as e:
|
||||
if not x11:
|
||||
_log.warn('cannot create uinput device: %s', e)
|
||||
else:
|
||||
_log.info('cannot create uinput device: %s', e)
|
||||
ukeyboard = None
|
||||
umouse = None
|
||||
|
||||
|
||||
def simulate_xtest(code, event):
|
||||
global displayt
|
||||
if displayt:
|
||||
global xtest_available
|
||||
if x11_setup() and xtest_available:
|
||||
try:
|
||||
Xlib.ext.xtest.fake_input(displayt, event, code)
|
||||
displayt.sync()
|
||||
event = (
|
||||
Xlib.X.KeyPress if event == _KEY_PRESS else Xlib.X.KeyRelease if event == _KEY_RELEASE else
|
||||
Xlib.X.ButtonPress if event == _BUTTON_PRESS else Xlib.X.ButtonRelease if event == _BUTTON_RELEASE else None
|
||||
)
|
||||
Xlib.ext.xtest.fake_input(xdisplay, event, code)
|
||||
xdisplay.sync()
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug('xtest simulated input %s %s %s', xdisplay, event, code)
|
||||
return True
|
||||
except Exception as e:
|
||||
displayt = None
|
||||
xtest_available = False
|
||||
_log.warn('xtest fake input failed: %s', e)
|
||||
|
||||
|
||||
def simulate_uinput(device, what, code, arg):
|
||||
global ukeyboard, umouse
|
||||
if device:
|
||||
def simulate_uinput(what, code, arg):
|
||||
global udevice
|
||||
if setup_uinput():
|
||||
try:
|
||||
device.write(what, code, arg)
|
||||
device.syn()
|
||||
udevice.write(what, code, arg)
|
||||
udevice.syn()
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug('uinput simulated input %s %s %s', what, code, arg)
|
||||
return True
|
||||
except Exception as e:
|
||||
ukeyboard = umouse = None
|
||||
_log.warn('uinput write key failed: %s', e)
|
||||
udevice = None
|
||||
_log.warn('uinput write failed: %s', e)
|
||||
|
||||
|
||||
def simulate_key(code, event): # X11 keycode and event
|
||||
if simulate_xtest(code, event):
|
||||
def simulate_key(code, event): # X11 keycode but Solaar event code
|
||||
if not wayland and simulate_xtest(code, event):
|
||||
return True
|
||||
direction = 1 if event == Xlib.X.KeyPress or event == Xlib.X.ButtonPress else 0
|
||||
device = ukeyboard if event == Xlib.X.KeyPress or event == Xlib.X.KeyRelease else umouse
|
||||
if simulate_uinput(device, evdev.ecodes.EV_KEY, code - 8, direction):
|
||||
if simulate_uinput(evdev.ecodes.EV_KEY, code - 8, event):
|
||||
return True
|
||||
_log.warn('no way to simulate key input')
|
||||
|
||||
|
||||
def click_xtest(button, count):
|
||||
for _ in range(count):
|
||||
if not simulate_xtest(button[0], _BUTTON_PRESS):
|
||||
return False
|
||||
if not simulate_xtest(button[0], _BUTTON_RELEASE):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def click_uinput(button, count):
|
||||
for _ in range(count):
|
||||
if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 1):
|
||||
return False
|
||||
if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 0):
|
||||
return False
|
||||
return True
|
||||
_log.warn('no way to simulate input')
|
||||
|
||||
|
||||
def click(button, count):
|
||||
for _ in range(count):
|
||||
if not simulate_xtest(button, Xlib.X.ButtonPress):
|
||||
return False
|
||||
if not simulate_xtest(button, Xlib.X.ButtonRelease):
|
||||
return False
|
||||
if not wayland and click_xtest(button, count):
|
||||
return True
|
||||
if click_uinput(button, count):
|
||||
return True
|
||||
_log.warn('no way to simulate mouse click')
|
||||
return False
|
||||
|
||||
|
||||
def simulate_scroll(dx, dy):
|
||||
if displayt:
|
||||
if not wayland and xtest_available:
|
||||
success = True
|
||||
if dx:
|
||||
success = click(7 if dx > 0 else 6, count=abs(dx))
|
||||
|
@ -256,12 +327,12 @@ def simulate_scroll(dx, dy):
|
|||
success = click(4 if dy > 0 else 5, count=abs(dy))
|
||||
if success:
|
||||
return True
|
||||
if umouse:
|
||||
if setup_uinput():
|
||||
success = True
|
||||
if dx:
|
||||
success = simulate_uinput(umouse, evdev.ecodes.EV_REL, evdev.ecodes.REL_HWHEEL, dx)
|
||||
success = simulate_uinput(evdev.ecodes.EV_REL, evdev.ecodes.REL_HWHEEL, dx)
|
||||
if dy and success:
|
||||
success = simulate_uinput(umouse, evdev.ecodes.EV_REL, evdev.ecodes.REL_WHEEL, dy)
|
||||
success = simulate_uinput(evdev.ecodes.EV_REL, evdev.ecodes.REL_WHEEL, dy)
|
||||
if success:
|
||||
return True
|
||||
_log.warn('no way to simulate scrolling')
|
||||
|
@ -404,6 +475,8 @@ class And(Condition):
|
|||
|
||||
|
||||
def x11_focus_prog():
|
||||
if not x11_setup():
|
||||
return None
|
||||
pid = wm_class = None
|
||||
window = xdisplay.get_input_focus().focus
|
||||
while window:
|
||||
|
@ -420,6 +493,8 @@ def x11_focus_prog():
|
|||
|
||||
|
||||
def x11_pointer_prog():
|
||||
if not x11_setup():
|
||||
return None
|
||||
pid = wm_class = None
|
||||
window = xdisplay.screen().root.query_pointer().child
|
||||
for child in reversed(window.query_tree().children):
|
||||
|
@ -434,8 +509,8 @@ def x11_pointer_prog():
|
|||
class Process(Condition):
|
||||
def __init__(self, process):
|
||||
self.process = process
|
||||
if not x11:
|
||||
_log.warn('X11 not available - rules cannot access current process - %s', self)
|
||||
if wayland or not x11_setup():
|
||||
_log.warn('rules can only access active process in X11 - %s', self)
|
||||
if not isinstance(process, str):
|
||||
_log.warn('rule Process argument not a string: %s', process)
|
||||
self.process = str(process)
|
||||
|
@ -446,7 +521,7 @@ class Process(Condition):
|
|||
def evaluate(self, feature, notification, device, status, last_result):
|
||||
if not isinstance(self.process, str):
|
||||
return False
|
||||
focus = x11_focus_prog() if x11 else None
|
||||
focus = x11_focus_prog()
|
||||
result = any(bool(s and s.startswith(self.process)) for s in focus) if focus else None
|
||||
return result
|
||||
|
||||
|
@ -457,8 +532,8 @@ class Process(Condition):
|
|||
class MouseProcess(Condition):
|
||||
def __init__(self, process):
|
||||
self.process = process
|
||||
if not x11:
|
||||
_log.warn('X11 not available - rules cannot access current mouse process - %s', self)
|
||||
if wayland or not x11_setup():
|
||||
_log.warn('rules cannot access active mouse process in X11 - %s', self)
|
||||
if not isinstance(process, str):
|
||||
_log.warn('rule MouseProcess argument not a string: %s', process)
|
||||
self.process = str(process)
|
||||
|
@ -469,7 +544,8 @@ class MouseProcess(Condition):
|
|||
def evaluate(self, feature, notification, device, status, last_result):
|
||||
if not isinstance(self.process, str):
|
||||
return False
|
||||
result = any(bool(s and s.startswith(self.process)) for s in x11_pointer_prog()) if x11 else None
|
||||
pointer_focus = x11_pointer_prog()
|
||||
result = any(bool(s and s.startswith(self.process)) for s in pointer_focus) if pointer_focus else None
|
||||
return result
|
||||
|
||||
def data(self):
|
||||
|
@ -780,13 +856,13 @@ class KeyPress(Action):
|
|||
for k in keysyms:
|
||||
keycode = self.keysym_to_keycode(k, modifiers)
|
||||
if keycode and self.needed(keycode, modifiers):
|
||||
simulate_key(keycode, Xlib.X.KeyPress)
|
||||
simulate_key(keycode, _KEY_PRESS)
|
||||
|
||||
def keyUp(self, keysyms, modifiers):
|
||||
for k in keysyms:
|
||||
keycode = self.keysym_to_keycode(k, modifiers)
|
||||
if keycode and self.needed(keycode, modifiers):
|
||||
simulate_key(keycode, Xlib.X.KeyRelease)
|
||||
simulate_key(keycode, _KEY_RELEASE)
|
||||
|
||||
def evaluate(self, feature, notification, device, status, last_result):
|
||||
if gkeymap:
|
||||
|
@ -795,6 +871,7 @@ class KeyPress(Action):
|
|||
_log.info('KeyPress action: %s, modifiers %s %s', self.key_names, current, [hex(k) for k in self.key_symbols])
|
||||
self.keyDown(self.key_symbols, current)
|
||||
self.keyUp(reversed(self.key_symbols), current)
|
||||
_time.sleep(0.01)
|
||||
else:
|
||||
_log.warn('no keymap so cannot determine which keycode to send')
|
||||
return None
|
||||
|
@ -835,6 +912,7 @@ class MouseScroll(Action):
|
|||
_log.info('MouseScroll action: %s %s %s', self.amounts, last_result, amounts)
|
||||
dx, dy = amounts
|
||||
simulate_scroll(dx, dy)
|
||||
_time.sleep(0.01)
|
||||
return None
|
||||
|
||||
def data(self):
|
||||
|
@ -866,6 +944,7 @@ class MouseClick(Action):
|
|||
_log.info('MouseClick action: %d %s' % (self.count, self.button))
|
||||
if self.button and self.count:
|
||||
click(buttons[self.button], self.count)
|
||||
_time.sleep(0.01)
|
||||
return None
|
||||
|
||||
def data(self):
|
||||
|
|
Loading…
Reference in New Issue