Repository: pwr-Solaar/Solaar

Issue:
Improve Solaar rules in Wayland
**Information**
- Solaar version: `1.0.4.r54.g0427e56`
- Distribution: Arch GNU/Linux
- Kernel version: 5.8.x, 5.9.x
- Output of `solaar show`:

<details>

```
  1: Craft Advanced Keyboard
     Device path  : /dev/hidraw1
     WPID         : 4066
     Codename     : Craft
     Kind         : keyboard
     Protocol     : HID++ 4.5
     Polling rate : 8 ms (125Hz)
     Serial number: 02AFBAD2
     Model ID:      B35040660000
     Unit ID:       78CEC42D
        Bootloader: BOT 41.01.B0015
          Firmware: MPK 07.01.B0015
             Other:
             Other:
     The power switch is located on the edge of top right corner.
     Supports 39 HID++ 2.0 features:
         0: ROOT                   {0000}
         1: FEATURE SET            {0001}
         2: DEVICE FW VERSION      {0003}
            Firmware: Bootloader BOT 41.01.B0015 0000B6A2C54601
            Firmware: Firmware MPK 07.01.B0015 4066B6A2C54601
            Firmware: Other
            Firmware: Other
            Unit ID: 78CEC42D  Model ID: B35040660000  Transport IDs: {'btleid': 'B350', 'wpid': '4066'}
         3: DEVICE NAME            {0005}
            Name: Craft Advanced Keyboard
            Kind: keyboard
         4: WIRELESS DEVICE STATUS {1D4B}
         5: RESET                  {0020}
         6: DEVICE FRIENDLY NAME   {0007}
         7: BATTERY STATUS         {1000}
            Battery: 50%, discharging, next level 20%.
         8: CHANGE HOST            {1814}
            Change Host: 1:able
         9: HOSTS INFO             {1815}
            Host 0 (paired):
            Host 1 (paired): ARCADIA
            Host 2 (paired): whitepad
        10: BACKLIGHT2             {1982}
            Backlight: True
        11: REPROG CONTROLS V4     {1B04}
            Key/Button Diversion: {'209': 0, '210': 0, '211': 0, '199': 0, '200': 0, '224': 0, '225': 0, '110': 0, '226': 0, '227': 0, '228': 0, '229': 0, '230': 0, '231': 0, '232': 0, '233': 0, '10': 0, '191': 0, '234': 0, '111': 0, '236': 0, '235': 0}
        12: PERSISTENT REMAPPABLE ACTION {1C00}
        13: K375S FN INVERSION     {40A3}
            Swap Fx function: False
        14: ENCRYPTION             {4100}
        15: LOCK KEY STATE         {4220}
        16: KEYBOARD DISABLE KEYS  {4521}
            Disable keys: {'1': False, '2': False, '4': False, '8': False, '16': False}
        17: MULTIPLATFORM          {4531}
            Set OS: Windows
        18: CROWN                  {4600}
            Divert crown events: False
        19: DFUCONTROL SIGNED      {00C2}
        20: unknown:1803           {1803}   internal, hidden
        21: CONFIG DEVICE PROPS    {1806}   internal, hidden
        22: unknown:1813           {1813}   internal, hidden
        23: OOBSTATE               {1805}   internal, hidden
        24: unknown:1830           {1830}   internal, hidden
        25: unknown:1890           {1890}   internal, hidden
        26: unknown:1891           {1891}   internal, hidden
        27: unknown:1801           {1801}   internal, hidden
        28: unknown:18A1           {18A1}   internal, hidden
        29: unknown:9280           {9280}   internal, hidden
        30: unknown:1A20           {1A20}   internal, hidden
        31: unknown:1DF3           {1DF3}   internal, hidden
        32: unknown:1E00           {1E00}   hidden
        33: unknown:1EB0           {1EB0}   internal, hidden
        34: unknown:1861           {1861}   internal, hidden
        35: unknown:18B0           {18B0}   internal, hidden
        36: unknown:92C0           {92C0}   internal, hidden
        37: unknown:9203           {9203}   internal, hidden
        38: unknown:3615           {3615}   internal, hidden
     Has 24 reprogrammable keys:
         0: Host Switch Channel 1     , default: HostSwitch Channel 1        => HostSwitch Channel 1
             nonstandard, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
         1: Host Switch Channel 2     , default: HostSwitch Channel 2        => HostSwitch Channel 2
             nonstandard, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
         2: Host Switch Channel 3     , default: HostSwitch Channel 3        => HostSwitch Channel 3
             nonstandard, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
         3: Brightness Down           , default: Brightness Down             => Brightness Down
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:1, group:0, group mask:empty
             reporting: default
         4: Brightness Up             , default: Brightness Up               => Brightness Up
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:2, group:0, group mask:empty
             reporting: default
         5: Mission Control/Task View , default: Mission Control/Task View   => Mission Control/Task View
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:3, group:0, group mask:empty
             reporting: default
         6: Dashboard Launchpad/Action Center, default: Dashboard Launchpad/Action Center => Dashboard Launchpad/Action Center
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:4, group:0, group mask:empty
             reporting: default
         7: Show Desktop              , default: Show Desktop                => Show Desktop
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:5, group:0, group mask:empty
             reporting: default
         8: Backlight Down            , default: Backlight Down              => Backlight Down
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:6, group:0, group mask:empty
             reporting: default
         9: Backlight Up              , default: Backlight Up                => Backlight Up
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:7, group:0, group mask:empty
             reporting: default
        10: Previous Fn               , default: Previous                    => Previous
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:8, group:0, group mask:empty
             reporting: default
        11: Play/Pause Fn             , default: Play/Pause                  => Play/Pause
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:9, group:0, group mask:empty
             reporting: default
        12: Next Fn                   , default: Next                        => Next
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:10, group:0, group mask:empty
             reporting: default
        13: Mute Fn                   , default: Mute                        => Mute
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:11, group:0, group mask:empty
             reporting: default
        14: Volume Down Fn            , default: Volume Down                 => Volume Down
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:12, group:0, group mask:empty
             reporting: default
        15: Volume Up Fn              , default: Volume Up                   => Volume Up
             nonstandard, reprogrammable, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
        16: Calculator                , default: Calculator                  => Calculator
             nonstandard, reprogrammable, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
        17: Screen Capture/Print Screen, default: Screen Capture              => Screen Capture
             nonstandard, reprogrammable, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
        18: App Contextual Menu/Right Click, default: Right Click/App Contextual Menu => Right Click/App Contextual Menu
             nonstandard, reprogrammable, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
        19: Lock PC                   , default: WindowsLock                 => WindowsLock
             nonstandard, reprogrammable, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
        20: Left Arrow                , default: Keyboard Left Arrow         => Keyboard Left Arrow
             nonstandard, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
        21: Right Arrow               , default: Keyboard Right Arrow        => Keyboard Right Arrow
             nonstandard, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
        22: F Lock                    , default: Do Nothing One              => Do Nothing One
             is FN, pos:0, group:0, group mask:empty
             reporting: default
        23: unknown:0034              , default: Do Nothing One              => Do Nothing One
             nonstandard, pos:0, group:0, group mask:empty
             reporting: default
     Battery: 50%, discharging, next level 20%.
```
</details>
**Is your feature request related to a problem? Please describe.**

The newly introduced rule-based crown handling (#987, #988) does not appear to work in Wayland environments. As I understand, this is not a bug but a deliberately missing feature.

**Describe the solution you'd like**

It would be nice to have this working in Wayland environments.

**Describe alternatives you've considered**

I don't know, use X11 maybe.

**Additional context**

As I understand, input injection can happen via non-X11-dependent mechanisms, like pynput's uinput backend. What else does solaar need from the window system?


Discussion:
See Issue #1006.  Pynput complained when run under Wayland.   
What is needed is a way to inject input events in Wayland without root access.  And also a way to find out the name of the process that is receiving input events.  And either a way to find out which keyboard modifiers are active or some way to send a modified key event that does not change the keyboard modifiers.
Ah, I see. The title of that issue was not prominent enough, I guess.

> What is needed is a way to inject input events in Wayland without root access.

uinput _should_ work for that... on my system, `/dev/uinput` is assigned an ACL corresponding to the user of the active session, so it's doable in theory.

>  And also a way to find out the name of the process that is receiving input events. And either a way to find out which keyboard modifiers are active or some way to send a modified key event that does not change the keyboard modifiers.

This would be trickier for sure.
What is needed is someone who knows about Wayland and can help put together a solution.  

On my Fedora system there does not appear to be any ACLs set up for input devices.
> On my Fedora system there does not appear to be any ACLs set up for input devices.

TBH any system with functional logind will do that (and a default Fedora absolutely counts). Try `getfacl /dev/uinput` while your graphical session is active.

> What is needed is someone who knows about Wayland and can help put together a solution.

So I googled a bit. There actually is a protocol, wlr-foreign-toplevel-management, that does what Solaar wants. Unfortunately (for me) [GNOME does not implement it and likely will not](https://www.mail-archive.com/wayland-devel@lists.freedesktop.org/msg39978.html). There is also [said to be some GNOME-specific DBus API](https://stackoverflow.com/a/64030239) (edit: [an example of its usage](https://github.com/ActivityWatch/activitywatch/issues/92#issuecomment-642030180)), but whatever it will be, I doubt that you would want to support an exception just for GNOME.
No ACLs on /dev/uinput here.   Perhaps it is only done in Wayland?
```
$ grep uinput -r /etc/udev/rules.d /usr/lib/udev/rules.d 
/usr/lib/udev/rules.d/70-steam-input.rules:KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess", OPTIONS+="static_node=uinput"
```

Right. The udev rule that assigns ACLs to uinput node is not built-in as I thought, but is rather supplied by Steam on my system.

That said, nothing prevents Solaar from shipping a similar rule itself (the significant part is `TAG+="uaccess"`).
The help of someone who knows about Wayland and can program in Python would be very helpful.   There isn't too much that is needed - faking input (probably using uinput) and determining the active program are the major bits.
I wrote a little program to fake input using /dev/uinput.  It works for keyboard input but not for mouse buttons and I have no idea why.  If someone can figure it out it would be possible to do more Solaar rule processing under Wayland.

EDIT:  It turns out that you need to synchronize between mouse down and up to register a click, but not for keyboard keys.  The buggy behaviour is really for keyboards, not for mice.
If someone can find something saying how to determine the current program in Wayland that would help in getting Solaar rules running in Wayland.
If you have X11 (emulation?) running in Wayland then Solaar rules will be active.  They can't see the active program or fake input, but at least they can respond to notifications and run programs.
So as of yet this hasn't been implemented right? I can see Solaar intercepting my Mouse Gesture Button of Ctrl + Alt + Shift, but I can't give it any new inputs. I figure these are send over X and therefore not registered. I might check to see if it would run in a contained X client through `xwayland`.

But I guess a workaround would be to give the keyboard bindings a new function through a bindsym in Sway. 
The problem in Wayland is that it is difficult to emulate input.   
What should work is diverting keys and having them trigger rules that execute programs.
If this isn't working then provide output of `solaar -dd` as diverted keys are being pressed.  Also provide contents of `~/.config/solaar/rules.yaml` and `~/.config/solaar/config.json`.
```
solaar -dd            
14:21:34,514     INFO [MainThread] root: language en_US (UTF-8), translations path /usr/share/locale
14:21:34,694    DEBUG [MainThread] logitech_receiver.diversion: rule Key assuming action "pressed" for "Brightness Down"
14:21:34,695    DEBUG [MainThread] logitech_receiver.diversion: rule Key assuming action "pressed" for "Brightness Up"
14:21:34,697    DEBUG [MainThread] logitech_receiver.diversion: load rule: Rule(/home/wout/.config/solaar/rules.yaml)[Key: Mouse Gesture Button (pressed),  KeyPress: Control_L w]
14:21:34,697     INFO [MainThread] logitech_receiver.diversion: loaded 1 rules from /home/wout/.config/solaar/rules.yaml
14:21:34,699    DEBUG [MainThread] solaar.ui.tray: using AppIndicator3
14:21:34,712     INFO [MainThread] solaar.upower: connected to system dbus, watching for suspend/resume events
```
Rules

```
%YAML 1.3
---
- Key: [Mouse Gesture Button, pressed]
- KeyPress: [Control_L, w]
...
```

Lastly, the config:

```
{
  "4071:DA5B3453": {
    "_modelId": "B01E40710000",
    "_name": "Wireless Mouse MX Master",
    "_sensitive": {
      "mouse-gestures": false
    },
    "_serial": "DA5B3453",
    "_unitId": "E7B71F3A",
    "divert-keys": {
      "195": 1,
      "196": 0,
      "82": 0,
      "83": 0,
      "86": 0
    },
    "dpi": 1000,
    "dpi-sliding": 0,
    "gesture2-gestures": {
      "45": true,
      "46": true
    },
    "hires-smooth-invert": false,
    "hires-smooth-resolution": true,
    "mouse-gestures": "195",
    "reprogrammable-keys": {
      "195": 195,
      "196": 196,
      "80": 80,
      "81": 81,
      "82": 82,
      "83": 83,
      "86": 86
    },
    "smart-shift": 16
  },
  "_version": "1.1.0"
```

So you are saying that I should make the Mouse Gesture button to make a script, and in that script put a keybind for `Control_L` and `w`, as a workaround that is guaranteed to work?
It looks as if you had a running Solaar.  To get debug output you need to quit out of Solaar and then run `solaar -dd`.  This should produce a lot of output - the useful part will we what is output when you press the diverted button.

One  way to debug rules is to include an action to execute a command that pops up an on-screen notification, like `notify-send` with an argument like `rule X run`.
The KeyPress action uses XTest, which might not work in Wayland.
> One way to debug rules is to include an action to execute a command that pops up an on-screen notification, like `notify-send` with an argument like `rule X run`.

That's very helpful, thanks. I do think it Solaar works as it should, just that my use-case is limiting. The keypress might be not supported, but I could circumvent it also, by rebinding the Ctrl+Alt+Tab, which is the default for the mouse gesture button to Ctrl+w.

Will try the `solaar -dd` command now and list the output:


```
16:35:38,539    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: diverted controls pressed: 195, 0, 0, 0
16:35:38,540    ERROR [ReceiverListener:hidraw0] logitech_receiver.listener: processing Notification(11,1,0A,00,00C30000000000000000000000000000)
```

Then more error code info

```
Traceback (most recent call last):
  File "/usr/lib/python3.9/site-packages/logitech_receiver/listener.py", line 190, in run
    self._notifications_callback(n)
  File "/usr/lib/python3.9/site-packages/solaar/listener.py", line 262, in _notifications_handler
    _notifications.process(dev, n)
  File "/usr/lib/python3.9/site-packages/logitech_receiver/notifications.py", line 60, in process
    return _process_device_notification(device, status, notification)
  File "/usr/lib/python3.9/site-packages/logitech_receiver/notifications.py", line 195, in _process_device_notification
    return _process_feature_notification(device, status, n, feature)
  File "/usr/lib/python3.9/site-packages/logitech_receiver/notifications.py", line 438, in _process_feature_notification
    _diversion.process_notification(device, status, n, feature)
  File "/usr/lib/python3.9/site-packages/logitech_receiver/diversion.py", line 795, in process_notification
    rules.evaluate(feature, notification, device, status, True)
  File "/usr/lib/python3.9/site-packages/logitech_receiver/diversion.py", line 203, in evaluate
    result = component.evaluate(feature, notification, device, status, result)
  File "/usr/lib/python3.9/site-packages/logitech_receiver/diversion.py", line 203, in evaluate
    result = component.evaluate(feature, notification, device, status, result)
  File "/usr/lib/python3.9/site-packages/logitech_receiver/diversion.py", line 203, in evaluate
    result = component.evaluate(feature, notification, device, status, result)
  File "/usr/lib/python3.9/site-packages/logitech_receiver/diversion.py", line 324, in evaluate
    result = any(bool(s and s.startswith(self.process)) for s in x11_focus_prog())
TypeError: 'NoneType' object is not iterable
16:33:24,124    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 FFFF0000000000000000000000000000]
16:33:24,124    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
16:33:24,124    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=-1 dy=0
16:33:24,124    ERROR [ReceiverListener:hidraw0] logitech_receiver.listener: processing Notification(11,1,0A,10,FFFF0000000000000000000000000000)
```
Hmm.  That looks like a bit of a bug.  Solaar rules haven't been tested under XWayland.

It looks like you put in a Process condition in your rule.  You probably wanted to use an Execute action instead.
PR #1391 should fix the crash you experienced.

To download and use Solar from its GitHub repository
```
git clone https://github.com/pwr-Solaar/Solaar.git
cd Solaar
```
Run Solaar as bin/solaar from this directory.


To run PR #1391, first clone Solaar if you have not already done so and cd to the clone directory.  The first time you download the pull request, fetch it into a new branch and checkout that branch, as in:
```
git fetch origin pull/1391/head:pull_1391
git checkout pull_1391
```
To download a new version of the pull request, fetch it and then set your pull branch
to the new fetch, as in:
```
git checkout pull_1391
git fetch origin pull/1391/head
git reset --hard FETCH_HEAD
```

Running the above commands, and setting 'firefox' as an execute command. I can verify that working, but that might have been also the case before. The Keypresses of Ctrl+w are not working yet. I was sure to close solaar before I opening the command. I will have a play around, I am not the most well-versed when it comes to linux, so thanks for the initial pointers, also how to git into new repos.
It seems to hang a bit, but definitely gives different and no error like before. 

<details>

```
22:44:11,432    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data 00C30000000000000000000000000000
22:44:11,432    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: diverted controls pressed: 0xc3, 0x0, 0x0, 0x0
22:44:11,432     INFO [ReceiverListener:hidraw0] logitech_receiver.diversion: KeyPress action: ['Control_L', 'w'], modifiers 0 ['0x25', '0x19']
22:44:11,439    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[20 01 0200 0001D0FF00006E00000000]
22:44:11,447    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 0000FFFD000000000000000000000000]
22:44:11,448    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,448    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data 0000FFFD000000000000000000000000
22:44:11,448    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=0 dy=-3
22:44:11,455    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 0000FFFE000000000000000000000000]
22:44:11,455    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,456    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data 0000FFFE000000000000000000000000
22:44:11,456    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=0 dy=-2
22:44:11,463    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 0001FFFF000000000000000000000000]
22:44:11,464    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,464    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data 0001FFFF000000000000000000000000
22:44:11,464    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=1 dy=-1
22:44:11,471    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 0000FFFE000000000000000000000000]
22:44:11,471    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,471    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data 0000FFFE000000000000000000000000
22:44:11,472    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=0 dy=-2
22:44:11,479    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 0000FFFF000000000000000000000000]
22:44:11,480    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,480    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data 0000FFFF000000000000000000000000
22:44:11,480    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=0 dy=-1
22:44:11,487    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 0000FFFF000000000000000000000000]
22:44:11,487    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,488    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data 0000FFFF000000000000000000000000
22:44:11,488    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=0 dy=-1
22:44:11,495    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 0001FFFF000000000000000000000000]
22:44:11,496    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,496    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data 0001FFFF000000000000000000000000
22:44:11,496    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=1 dy=-1
22:44:11,617    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 0000FFFF000000000000000000000000]
22:44:11,618    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,618    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data 0000FFFF000000000000000000000000
22:44:11,618    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=0 dy=-1
22:44:11,681    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 0000FFFE000000000000000000000000]
22:44:11,682    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,682    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data 0000FFFE000000000000000000000000
22:44:11,682    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=0 dy=-2
22:44:11,689    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 FFFDFFFC000000000000000000000000]
22:44:11,689    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,690    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data FFFDFFFC000000000000000000000000
22:44:11,690    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=-3 dy=-4
22:44:11,697    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 FFFDFFFC000000000000000000000000]
22:44:11,698    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,698    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data FFFDFFFC000000000000000000000000
22:44:11,698    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=-3 dy=-4
22:44:11,705    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 FFFAFFFC000000000000000000000000]
22:44:11,706    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,706    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data FFFAFFFC000000000000000000000000
22:44:11,706    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=-6 dy=-4
22:44:11,713    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 FFF9FFFC000000000000000000000000]
22:44:11,714    DEBUG [ReceiverListener:hidraw0] logitech_receiver.settings: dpi: settings read 1000 from <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>
22:44:11,714    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: notification for feature REPROG CONTROLS V4, report 0, data FFF9FFFC000000000000000000000000
22:44:11,714    DEBUG [ReceiverListener:hidraw0] logitech_receiver.notifications: <Device(1,4071,Wireless Mouse MX Master,DA5B3453)>: rawXY dx=-7 dy=-4
22:44:11,721    DEBUG [ReceiverListener:hidraw0] logitech_receiver.base: (20) => r[11 01 0A10 FFFBFFFD000000000000000000000000]
```

</details.
The KeyPress action is unlikely to work in Wayland.   You should execute some command that has the desired effect, if this is possible.
If someone is willing to do the testing, it should be possible to get part of Solaar rules running under Wayland.  It might even be possible to get Solaar to optionally use /dev/uinput for simulating keypresses and mouse movement.  If someone is willing to do some testing please respond here.
I am up for some testing, I am using Sway under Wayland. I don't think it should matter which DE is being used, but I thought I should mention.
Excellent.  Modifications to the rule code that shouldn't crash in Wayland is in PR #1438.  There is no access to the current display process so Process and MouseProcess conditions do not work.  There is no way to simulate input so KeyPress, MouseClick, and MouseScroll actions do not work.  There is no way to determine the state of the modifier keys so Modifiers conditions do not work. Warnings should be emitted for any of these.
Please try out some rules, both bits that should work and bits that should not.  Run Solaar as `bin/solaar -d` and look at the warnings.  There should only be warnings for the above parts of rules and everything else should work.

To clone and use Solar from its GitHub repository
```
git clone https://github.com/pwr-Solaar/Solaar.git
cd Solaar
```
Run Solaar as bin/solaar from this directory.


To run PR #1438, first clone Solaar if you have not already done so and cd to the clone directory.  The first time you download the pull request, fetch it into a new branch and checkout that branch, as in:
```
git fetch origin pull/1438/head:pull_1438
git checkout pull_1438
```
To download a new version of the pull request, fetch it and then set your pull branch
to the new fetch, as in:
```
git checkout pull_1438
git fetch origin pull/1438/head
git reset --hard FETCH_HEAD
```

Hi!

I just got here trying some things out... not sure if my case fits in this issue. I'm using and MX Keys logitech keyboard in EndevourOS (Arch based) and gnome as DE running on ~~wayland~~ (tried both wayland and xorg/X11, problem happens in both).

Everything in the keyboard worked just plug&play but brightness fn keys. Weird thing is that, when trying to just bind those keys in gnome settings, it didn't seem to notice key presses.

Looking around got here and I installed Solaar. Just tried the `solaar -dd` thing and... totally weird: it logs out any key I press.... but those brightness fn keys. If I press them (f1 and f2) withouth pressing the fn key, they get logged, but if I keep the fn key pressed... nothing. **UPDATE**: keys controlling keyboard's ligth do not show up in this log either.

Any ideas? I'm up for testing too.

PS: In case this is relevant to you, this is the info shown in Solaar for my keyboard:

```
Ruta         : /dev/hidraw3
Índice       : 1
PID inalámbrico: 408A
Protocolo    : HID++ 4.5
Tasa de sondeo: 20 ms (50Hz)
Serial       : 21941FA6
  Gestor de arranque: BL1 08.00.B0011
  Firmware   : MPK 12.01.B0013
  Otros      : 
```
The brightness up/down keys (when they aren't just F1 and F2 keys) don't produce anything (at least when the keyboard is on its Windows settings).  You need to divert the keys so that they produce HID++ notifications that Solaar can react to.  Then Solaar can (and should, using a built-in rule) emit keypresses that should change the screen brightness (if your computer can change the brightness and is set up to do that).

Why the keyboard works this way is a mystery to me.  

The situation with the keyboard brightness keys is similar, but at least they do change the keyboard brightness.
Sorry I mixed up issues, I [just posted how I got it working in the original issue](https://github.com/pwr-Solaar/Solaar/issues/1374#issuecomment-1025771403) (more related to this as for title).

Thing is, although I divert those keys, they still do not increase/decrease brightness. It's like `XF86_MonBrightnessUp/Down` are doing nothing in gnome 41.3 (my current version).
Gnome uses Wayland, as far as I know, and Solaar rules use X11 features.  It may be that the X11 shim that can be used in Gnome doesn't handle this part of X11.
Umm... I'm not an expert at all in this, but I can choose in GDM to launch gnome on wayland or X11. I've checked on both about the system running and shows as expected in each case. In fact, it's working for deviating and mapping keys. Only thing is I'm not able to fire the event gnome is expecting (i.e: no OSD shown, not sync with settings, etc...).

Anyhow, I've only been able to match it using that _hack-ish_ thing linked above.
There should be some mechanism in GNOME to react to these keys.  I don't use GNOME so I don't know what it is.  In the XFCE Power Manager has a setting "Hande display brightness keys".  Maybe there is an equivalent in GNOME?
It should. I'm just not sure if I'm having some kind of bug due to the nvidia driver I'm using or something... anyway, I'll try with some live distros to see if it's something related to my current setup.
Solaar just detect my key presses at Wayland, if I enable "divert", but can't write characters afterwards.

I think only proper way to have it is a service that installed for the purpose. Because you need root access.
Here is the code that I think it will benefit much: 
https://github.com/dictation-toolbox/aenea/tree/4ef9ee309d1faa691e4109a425e928a5d83ea5a9/server/linux_wayland

As I understand, at first we need to find keyboard:
```
import evdev
def findKeyEvent():
	devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
	for device in devices:
		cap = device.capabilities();
		key=cap.get(evdev.ecodes.EV_KEY)
		if key is not None:
			try:
				#generate exception if not found
				key.index(evdev.ecodes.KEY_A)
				key.index(evdev.ecodes.KEY_B)
				key.index(evdev.ecodes.KEY_C)
				return device.path
			except:
				pass
	return None

##findKeyEvent() finds the keyboard input device that we can inject characters into it. Than we could inject keycodes via:

key=evdev.InputDevice( findKeyEvent() )
key.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_A ,0)
key.write(evdev.ecodes.EV_KEY, evdev.ecodes.KEY_A ,1)

##This writes "a" letter. That's all.
```

Indeed code has also Mouse controls in it. I believe this helps. This is my 2¢.
Thanks.  I have been thinking along similar lines.  Do you know whether this method works correctly if the user is using a Dvorak keyboard?
PR #1438 needs more testing from someone using Wayland (both with and without an X11 shim on top of it).
I test it and... I don't find any differences. Working on XWayland but still not print anything as input Wayland...


Please run `solaar -dd` and see what warnings are printed.  There should be warnings about some parts of rules not working instead of a warning that rules do not work under Wayland.  Rules conditions for notifications (keys down, etc.) should work as well as rule actions that execute programs and rule actions that make changes to logitech devices.

Rule conditions that should work:  Not, And, Or, Feature, Report, Key, Test.
Rule actions that should work: Set, Execute.


Yes, now got an warning as:
`04:09:57,975  WARNING [MainThread] logitech_receiver.diversion: rule KeyPress key name not currently available KeyPress:`
But, solaar keypresses was working on XWayland terminals. But now, it doesn't...
This looks like a bug.   Can you provide your rules file (~/.config/solaar/rules.yaml)?

Edit:  It's an oversight in the code so no need to provide your rules file.
@EUA KeyPress actions should be working again.  Modifier conditions should now work under Wayland (even non-X Wayland).
Yes keypresses now working on XWayland but still not working on wayland.
Getting keypresses to work on Wayland is going to require quite a bit more work.  The changes so far are some preparatory work only.  One problem is that keypresses still don't work right under all conditions even under X11 because it is hard to determine the state of the keyboard.

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
@@ -25,6 +25,7 @@
 from logging import getLogger
 from math import sqrt as _sqrt
 
+import evdev
 import keysyms.keysymdef as _keysymdef
 import psutil
 
@@ -51,16 +52,15 @@
 # Process condition depends on X11 from python-xlib, and is probably not possible at all in Wayland
 # MouseProcess condition depends on X11 from python-xlib, and is probably not possible at all in Wayland
 # Modifiers condition depends only on GDK
-# KeyPress action currently only works in X11, and is not currently available under Wayland
 # KeyPress action determines whether a keysym is a currently-down modifier using get_modifier_mapping from python-xlib;
 #   under Wayland no modifier keys are considered down so all modifier keys are pressed, potentially leading to problems
 # KeyPress action translates key names to keysysms using the local file described for GUI keyname determination
 # KeyPress action gets the current keyboard group using XkbGetState from libX11.so using ctypes definitions
 #   under Wayland the keyboard group is None resulting in using the first keyboard group
 # KeyPress action translates keysyms to keycodes using the GDK keymap
-# KeyPress action simulates keyboard input with X11 XTest from python-xlib
-# MouseScroll and MouseClick actions currently only work in X11, and are not currently available under Wayland
-# MouseScroll and MouseClick actions simulate mouse input with X11 XTest from python-xlib
+# KeyPress, MouseScroll, and MouseClick actions use XTest (under X11) or uinput.
+# For uinput to work the user must have write access for /dev/uinput.
+# To get this access run  sudo setfacl -m u:${user}:rw /dev/uinput
 #
 # Rule GUI keyname determination uses a local file generated
 #   from http://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
@@ -71,7 +71,6 @@
 
 try:
     import Xlib
-    from Xlib import X
     from Xlib.display import Display
     xdisplay = Display()
     modifier_keycodes = xdisplay.get_modifier_mapping()  # there should be a way to do this in Gdk
@@ -84,10 +83,10 @@
     # set up to get keyboard state using ctypes interface to libx11
     import ctypes
 
-    class X11Display(ctypes.Structure):
+    class XkbDisplay(ctypes.Structure):
         """ opaque struct """
 
-    class X11XkbStateRec(ctypes.Structure):
+    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),
@@ -95,13 +94,14 @@ class X11XkbStateRec(ctypes.Structure):
                     ('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(X11Display)
-    X11Lib.XkbGetState.argtypes = [ctypes.POINTER(X11Display), ctypes.c_uint, ctypes.POINTER(X11XkbStateRec)]
-    display = X11Lib.XOpenDisplay(None)
+    X11Lib.XOpenDisplay.restype = ctypes.POINTER(XkbDisplay)
+    X11Lib.XkbGetState.argtypes = [ctypes.POINTER(XkbDisplay), ctypes.c_uint, ctypes.POINTER(XkbStateRec)]
+    Xkbdisplay = X11Lib.XOpenDisplay(None)
 except Exception:
     _log.warn(
-        'X11 not available - rules cannot access current process or keyboard group and cannot simulate input. %s',
+        'X11 not available - rules cannot access current process, modifier keys, or keyboard group. %s',
         exc_info=_sys.exc_info()
     )
     modifier_keycodes = []
@@ -110,8 +110,8 @@ class X11XkbStateRec(ctypes.Structure):
 
 def kbdgroup():
     if x11:
-        state = X11XkbStateRec()
-        X11Lib.XkbGetState(display, 0x100, ctypes.pointer(state))  # 0x100 is core device FIXME
+        state = XkbStateRec()
+        X11Lib.XkbGetState(Xkbdisplay, XkbUseCoreKbd, ctypes.pointer(state))
         return state.group
     else:
         return None
@@ -163,6 +163,103 @@ 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:
+        try:
+            Xlib.ext.xtest.fake_input(displayt, event, code)
+            displayt.sync()
+            return True
+        except Exception as e:
+            displayt = None
+            _log.warn('xtest fake input failed: %s', e)
+
+
+def simulate_uinput(device, what, code, arg):
+    global ukeyboard, umouse
+    if device:
+        try:
+            device.write(what, code, arg)
+            device.syn()
+            return True
+        except Exception as e:
+            ukeyboard = umouse = None
+            _log.warn('uinput write key failed: %s', e)
+
+
+def simulate_key(code, event):  # X11 keycode and event
+    if 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):
+        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
+    return True
+
+
+def simulate_scroll(dx, dy):
+    if displayt:
+        success = True
+        if dx:
+            success = click(7 if dx > 0 else 6, count=abs(dx))
+        if dy and success:
+            success = click(4 if dy > 0 else 5, count=abs(dy))
+        if success:
+            return True
+    if umouse:
+        success = True
+        if dx:
+            success = simulate_uinput(umouse, 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)
+        if success:
+            return True
+    _log.warn('no way to simulate scrolling')
+
+
 TESTS = {
     'crown_right': lambda f, r, d: f == _F.CROWN and r == 0 and d[1] < 128 and d[1],
     'crown_left': lambda f, r, d: f == _F.CROWN and r == 0 and d[1] >= 128 and 256 - d[1],
@@ -192,11 +289,6 @@ def xy_direction(_x, _y):
 
 COMPONENTS = {}
 
-if x11:
-    displayt = Display()
-else:
-    displayt = None
-
 
 class RuleComponent:
     def compile(self, c):
@@ -654,9 +746,6 @@ def __init__(self, keys):
         if not all(self.key_symbols):
             _log.warn('rule KeyPress not sequence of key names %s', keys)
             self.key_symbols = []
-        if not x11:
-            _log.warn('rule KeyPress action only available in X11 %s', keys)
-            self.key_symbols = []
 
     def keysym_to_keycode(self, keysym, modifiers):  # maybe should take shift into account
         group = kbdgroup() or 0
@@ -666,7 +755,7 @@ def keysym_to_keycode(self, keysym, modifiers):  # maybe should take shift into
             return k.keycode
         else:
             for k in keycodes.keys:
-                if group == k.group:
+                if group is None or group == k.group:
                     return k.keycode
             _log.warn('rule KeyPress key symbol not currently available %s', self)
 
@@ -680,23 +769,21 @@ def needed(self, k, modifiers):
     def keyDown(self, keysyms, modifiers):
         for k in keysyms:
             keycode = self.keysym_to_keycode(k, modifiers)
-            if self.needed(keycode, modifiers) and x11 and keycode:
-                Xlib.ext.xtest.fake_input(displayt, X.KeyPress, keycode)
+            if keycode and self.needed(keycode, modifiers):
+                simulate_key(keycode, Xlib.X.KeyPress)
 
     def keyUp(self, keysyms, modifiers):
         for k in keysyms:
             keycode = self.keysym_to_keycode(k, modifiers)
-            if self.needed(keycode, modifiers) and x11 and keycode:
-                Xlib.ext.xtest.fake_input(displayt, X.KeyRelease, keycode)
+            if keycode and self.needed(keycode, modifiers):
+                simulate_key(keycode, Xlib.X.KeyRelease)
 
     def evaluate(self, feature, notification, device, status, last_result):
         current = gkeymap.get_modifier_state()
         if _log.isEnabledFor(_INFO):
-            _log.info('KeyPress action: %s, modifiers %s %s', self.key_symbols, current, [hex(k) for k in self.key_symbols])
+            _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)
-        if x11:
-            displayt.sync()
         return None
 
     def data(self):
@@ -711,28 +798,6 @@ def data(self):
 #    def evaluate(self, feature, notification, device, status, last_result):
 #        super().keyUp(self.keys, current_key_modifiers)
 
-buttons = {
-    'unknown': None,
-    'left': 1,
-    'middle': 2,
-    'right': 3,
-    'scroll_up': 4,
-    'scroll_down': 5,
-    'scroll_left': 6,
-    'scroll_right': 7
-}
-for i in range(8, 31):
-    buttons['button%d' % i] = i
-
-
-def click(button, count):
-    if x11:
-        for _ in range(count):
-            Xlib.ext.xtest.fake_input(displayt, Xlib.X.ButtonPress, button)
-            Xlib.ext.xtest.fake_input(displayt, Xlib.X.ButtonRelease, button)
-    else:
-        _log.warn('X11 not available - rules cannot simulate mouse clicks')
-
 
 class MouseScroll(Action):
     def __init__(self, amounts):
@@ -743,8 +808,6 @@ def __init__(self, amounts):
             _log.warn('rule MouseScroll argument not two numbers %s', amounts)
             amounts = [0, 0]
         self.amounts = amounts
-        if not x11:
-            _log.warn('X11 not available - rules cannot simulate mouse scrolling - %s', self)
 
     def __str__(self):
         return 'MouseScroll: ' + ' '.join([str(a) for a in self.amounts])
@@ -758,12 +821,7 @@ def evaluate(self, feature, notification, device, status, last_result):
         if _log.isEnabledFor(_INFO):
             _log.info('MouseScroll action: %s %s %s', self.amounts, last_result, amounts)
         dx, dy = amounts
-        if dx:
-            click(button=buttons['scroll_right'] if dx > 0 else buttons['scroll_left'], count=abs(dx))
-        if dy:
-            click(button=buttons['scroll_up'] if dy > 0 else buttons['scroll_down'], count=abs(dy))
-        if x11:
-            displayt.sync()
+        simulate_scroll(dx, dy)
         return None
 
     def data(self):
@@ -786,8 +844,6 @@ def __init__(self, args):
         except (ValueError, TypeError):
             _log.warn('rule MouseClick action: count %s should be an integer', count)
             self.count = 1
-        if not x11:
-            _log.warn('X11 not available - rules cannot simulate mouse clicks - %s', self)
 
     def __str__(self):
         return 'MouseClick: %s (%d)' % (self.button, self.count)
@@ -797,8 +853,6 @@ 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)
-        if x11:
-            displayt.sync()
         return None
 
     def data(self):
@@ -890,7 +944,7 @@ def data(self):
 }
 
 built_in_rules = Rule([])
-if True:  # x11
+if True:
     built_in_rules = Rule([
         {'Rule': [  # Implement problematic keys for Craft and MX Master
             {'Rule': [{'Key': ['Brightness Down', 'pressed']}, {'KeyPress': 'XF86_MonBrightnessDown'}]},
@@ -912,12 +966,6 @@ def data(self):
             {'Rule': [{'Test': 'crown_right_ratchet'}, {'KeyPress': 'XF86_AudioRaiseVolume'}]},
             {'Rule': [{'Test': 'crown_left_ratchet'}, {'KeyPress': 'XF86_AudioLowerVolume'}]}
         ]},
-        {'Rule': [  # Thumb wheel does horizontal movement, doubled if control key not pressed
-            {'Feature': 'THUMB WHEEL'},  # with control modifier on mouse scrolling sometimes does something different!
-            {'Rule': [{'Modifiers': 'Control'}, {'Test': 'thumb_wheel_up'}, {'MouseScroll': [-1, 0]}]},
-            {'Rule': [{'Modifiers': 'Control'}, {'Test': 'thumb_wheel_down'}, {'MouseScroll': [-1, 0]}]},
-            {'Rule': [{'Or': [{'Test': 'thumb_wheel_up'}, {'Test': 'thumb_wheel_down'}]}, {'MouseScroll': [-2, 0]}]}
-        ]}
     ])
 
 keys_down = []
@@ -928,8 +976,6 @@ def data(self):
 
 # process a notification
 def process_notification(device, status, notification, feature):
-    if False:  # not x11
-        return
     global keys_down, g_keys_down, m_keys_down, mr_key_down, key_down, key_up
     key_down, key_up = None, None
     # need to keep track of keys that are down to find a new key down
@@ -1046,5 +1092,4 @@ def _load_config_rule_file():
     rules = Rule([Rule(loaded_rules, source=_file_path), built_in_rules])
 
 
-if True:  # x11
-    _load_config_rule_file()
+_load_config_rule_file()


Test patch: