Repository: pwr-Solaar/Solaar

Issue:
diversion.py:403:x11_focus_prog:AttributeError: 'int' object has no attribute 'get_full_property'
**Information**
<!-- Make sure that your issue is not one of the known issues in the Solaar documentation at https://pwr-solaar.github.io/Solaar/ -->
<!-- Do not bother opening an issue for a version older than 1.1.0.  Update to the latest version and see if your issue persists. -->
- Solaar version (`solaar --version` or `git describe --tags` if cloned from this repository): `1.1.2`
- Distribution: `Fedora 35 and 36`
- Kernel version (ex. `uname -srmo`): `5.16.18-200.fc35.x86_64` and `5.17.1-300.fc36.x86_64`
- Output of `solaar show`: `N/A`, I'll add it once reporters provide it.

<details>

```

```
</details>

**Describe the bug**
I've got multiple users reporting that solaar is crashing after logging in to GNOME session on both Fedora 35 and 36.

**To Reproduce**
Steps to reproduce the behavior:
1. Install solaar-1.1.2
2. Log out and log in to GNOME session
3. solaar crashes with the following backtrace:

<details>

```
Traceback (most recent call last):
  File "/usr/lib/python3.10/site-packages/logitech_receiver/diversion.py", line 317, in evaluate
    result = component.evaluate(feature, notification, device, status, result)
  File "/usr/lib/python3.10/site-packages/logitech_receiver/diversion.py", line 317, in evaluate
    result = component.evaluate(feature, notification, device, status, result)
  File "/usr/lib/python3.10/site-packages/logitech_receiver/diversion.py", line 317, in evaluate
    result = component.evaluate(feature, notification, device, status, result)
  File "/usr/lib/python3.10/site-packages/logitech_receiver/diversion.py", line 443, in evaluate
    focus = x11_focus_prog() if x11 else None
  File "/usr/lib/python3.10/site-packages/logitech_receiver/diversion.py", line 403, in x11_focus_prog
    pid = window.get_full_property(NET_WM_PID, 0)
AttributeError: 'int' object has no attribute 'get_full_property'

Local variables in innermost frame:
pid: None
wm_class: None
window: 1
```

</details>

**Additional context**
Downstream bug reports:
* [Fedora 35 bug #2070753](https://bugzilla.redhat.com/show_bug.cgi?id=2070753)
* [Fedora 36 bug #2071252](https://bugzilla.redhat.com/show_bug.cgi?id=2071252)


Discussion:
Only the first bug report is public.
This appears to be a problem with the recent update to try to partly implement rules under Wayland.  But the code made the assumption that if the X11 libraries are available then it is possible to navigate through windows, which is not correct if Wayland is installed with X11 libraries being available.

An upgrade of rule processing in Wayland is almost done so expect a PR soon.  

In the meantime Solaar could be patched to remove the built-in rules by changing line 947 in diversion.py to 
```
if False:
```


Resolving patch:
diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py
--- a/lib/logitech_receiver/diversion.py
+++ b/lib/logitech_receiver/diversion.py
@@ -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 @@
 #   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
-    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
+    _x11 = None  # X11 might be available
+except Exception:
+    _x11 = False  # X11 is not available
 
-    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')
+xtest_available = True  # Xtest might be available
+xdisplay = None
+Xkbdisplay = None  # xkb might be avilable
+modifier_keycodes = []
+XkbUseCoreKbd = 0x100
 
-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
-
-if x11:
+
+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:
-        # 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)]
+        from Xlib.display import Display
+        xdisplay = Display()
+        modifier_keycodes = xdisplay.get_modifier_mapping()  # there should be a way to do this in Gdk
+        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 - some rule capabilities inoperable: %s', exc_info=_sys.exc_info())
+        _x11 = False
+        xtest_available = False
+    return _x11
+
+
+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 input')
+    _log.warn('no way to simulate key input')
 
 
-def click(button, count):
+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_xtest(button, Xlib.X.ButtonPress):
+        if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 1):
             return False
-        if not simulate_xtest(button, Xlib.X.ButtonRelease):
+        if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 0):
             return False
     return True
 
 
+def click(button, count):
+    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 @@ def data(self):
 
 
 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 @@ def __str__(self):
     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 @@ def data(self):
 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 @@ def __str__(self):
     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 @@ def keyDown(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.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 @@ def evaluate(self, feature, notification, device, status, last_result):
                 _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 @@ def evaluate(self, feature, notification, device, status, last_result):
             _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 @@ def evaluate(self, feature, notification, device, status, last_result):
             _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):
@@ -963,22 +1042,22 @@ def data(self):
             {'Rule': [{'Key': ['Brightness Down', 'pressed']}, {'KeyPress': 'XF86_MonBrightnessDown'}]},
             {'Rule': [{'Key': ['Brightness Up', 'pressed']}, {'KeyPress': 'XF86_MonBrightnessUp'}]},
         ]},
-        {'Rule': [  # In firefox, crown emits keys that move up and down if not pressed, rotate through tabs otherwise
-            {'Process': 'firefox'},
-            {'Rule': [{'Test': 'crown_pressed'}, {'Test': 'crown_right_ratchet'}, {'KeyPress': ['Control_R', 'Tab']}]},
-            {'Rule': [{'Test': 'crown_pressed'},
-                      {'Test': 'crown_left_ratchet'},
-                      {'KeyPress': ['Control_R', 'Shift_R', 'Tab']}]},
-            {'Rule': [{'Test': 'crown_right_ratchet'}, {'KeyPress': 'Down'}]},
-            {'Rule': [{'Test': 'crown_left_ratchet'}, {'KeyPress': 'Up'}]},
-        ]},
-        {'Rule': [  # Otherwise, crown movements emit keys that modify volume if not pressed, move between tracks otherwise
-            {'Feature': 'CROWN'}, {'Report': 0x0},
-            {'Rule': [{'Test': 'crown_pressed'}, {'Test': 'crown_right_ratchet'}, {'KeyPress': 'XF86_AudioNext'}]},
-            {'Rule': [{'Test': 'crown_pressed'}, {'Test': 'crown_left_ratchet'}, {'KeyPress': 'XF86_AudioPrev'}]},
-            {'Rule': [{'Test': 'crown_right_ratchet'}, {'KeyPress': 'XF86_AudioRaiseVolume'}]},
-            {'Rule': [{'Test': 'crown_left_ratchet'}, {'KeyPress': 'XF86_AudioLowerVolume'}]}
-        ]},
+        # {'Rule': [  # In firefox, crown emits keys that move up and down if not pressed, rotate through tabs otherwise
+        #     {'Process': 'firefox'},
+        #     {'Rule': [{'Test': 'crown_pressed'}, {'Test': 'crown_right_ratchet'}, {'KeyPress': ['Control_R', 'Tab']}]},
+        #     {'Rule': [{'Test': 'crown_pressed'},
+        #               {'Test': 'crown_left_ratchet'},
+        #               {'KeyPress': ['Control_R', 'Shift_R', 'Tab']}]},
+        #     {'Rule': [{'Test': 'crown_right_ratchet'}, {'KeyPress': 'Down'}]},
+        #     {'Rule': [{'Test': 'crown_left_ratchet'}, {'KeyPress': 'Up'}]},
+        # ]},
+        # {'Rule': [  # Otherwise, crown movements emit keys that modify volume if not pressed, move between tracks otherwise
+        #     {'Feature': 'CROWN'}, {'Report': 0x0},
+        #     {'Rule': [{'Test': 'crown_pressed'}, {'Test': 'crown_right_ratchet'}, {'KeyPress': 'XF86_AudioNext'}]},
+        #     {'Rule': [{'Test': 'crown_pressed'}, {'Test': 'crown_left_ratchet'}, {'KeyPress': 'XF86_AudioPrev'}]},
+        #     {'Rule': [{'Test': 'crown_right_ratchet'}, {'KeyPress': 'XF86_AudioRaiseVolume'}]},
+        #     {'Rule': [{'Test': 'crown_left_ratchet'}, {'KeyPress': 'XF86_AudioLowerVolume'}]}
+        # ]},
     ])
 
 keys_down = []


Test patch: