Repository: pwr-Solaar/Solaar

Issue:
MX Ergo: Remapped Mouse Buttons Can't Be Held Down
**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.8.  Upgrade to the latest version and see if your issue persists. -->
<!-- If you are not running the current version of Solaar, strongly consider upgrading to the newest version. -->
- Solaar version (`solaar --version` or `git describe --tags` if cloned from this repository): 1.1.8+dfsg-2
- Distribution: Ubuntu 23.04
- Kernel version (ex. `uname -srmo`): `Linux 6.2.0-27-generic x86_64 GNU/Linux`
- Output of `solaar show`:

<details>

```
solaar version 1.1.8+dfsg-2

Unifying Receiver
  Device path  : /dev/hidraw1
  USB id       : 046d:C52B
  Serial       : 0210947D
    Firmware   : 12.11.B0032
    Bootloader : 04.16
    Other      : AA.AA
  Has 1 paired device(s) out of a maximum of 6.
  Notifications: wireless, software present (0x000900)
  Device activity counters: 1=172

  1: MX Ergo Multi-Device Trackball 
     Device path  : /dev/hidraw2
     WPID         : 406F
     Codename     : MX Ergo
     Kind         : trackball
     Protocol     : HID++ 4.5
     Polling rate : 8 ms (125Hz)
     Serial number: 1F0B3C09
     Model ID:      B01D406F0000
     Unit ID:       A697ADA8
        Bootloader: BOT 49.00.B0001
          Firmware: MPM 06.03.B0022
             Other: 
     The power switch is located on the base.
     Supports 36 HID++ 2.0 features:
         0: ROOT                   {0000} V0     
         1: FEATURE SET            {0001} V0     
         2: DEVICE FW VERSION      {0003} V2     
            Firmware: Bootloader BOT 49.00.B0001 0000CDE885F3
            Firmware: Firmware MPM 06.03.B0022 406FCDE885F3
            Firmware: Other   
            Unit ID: A697ADA8  Model ID: B01D406F0000  Transport IDs: {'btleid': 'B01D', 'wpid': '406F'}
         3: DEVICE NAME            {0005} V0     
            Name: MX Ergo Multi-Device Trackball 
            Kind: trackball
         4: WIRELESS DEVICE STATUS {1D4B} V0     
         5: DEVICE FRIENDLY NAME   {0007} V0     
            Friendly Name: MX Ergo
         6: CONFIG CHANGE          {0020} V0     
         7: CRYPTO ID              {0021} V1     
         8: BATTERY STATUS         {1000} V1     
            Battery: 90%, discharging, next level 50%.
         9: LED CONTROL            {1300} V0     
        10: REPROG CONTROLS V4     {1B04} V3     
            Key/Button Actions (saved): {Left Button:Left Click, Right Button:Right Click, Middle Button:Mouse Middle Button, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, Left Tilt:Mouse Scroll Left Button , Right Tilt:Mouse Scroll Right Button, DPI Change:DPI Change}
            Key/Button Actions        : {Left Button:Left Click, Right Button:Right Click, Middle Button:Mouse Middle Button, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, Left Tilt:Mouse Scroll Left Button , Right Tilt:Mouse Scroll Right Button, DPI Change:DPI Change}
            Key/Button Diversion (saved): {Middle Button:Regular, Back Button:Regular, Forward Button:Regular, Left Tilt:Regular, Right Tilt:Regular, DPI Change:Diverted}
            Key/Button Diversion        : {Middle Button:Regular, Back Button:Regular, Forward Button:Regular, Left Tilt:Regular, Right Tilt:Regular, DPI Change:Diverted}
        11: PERSISTENT REMAPPABLE ACTION {1C00} V0     
            Persistent Key/Button Mapping        : {Left Button:Mouse Button Left, Right Button:Mouse Button Right, Middle Button:Mouse Button Middle, Back Button:F23, Forward Button:F24, Left Tilt:Horizontal Scroll Left, Right Tilt:Horizontal Scroll Right, DPI Change:Mouse Button DPI}
        12: POINTER AXIS ORIENTATION {2006} V0     
        13: POINTER SPEED          {2205} V0     
            Pointer Speed: 1.0
            Sensitivity (Pointer Speed) (saved): 256
            Sensitivity (Pointer Speed)        : 256
            Sensitivity Switching (saved): DPI Change
            Sensitivity Switching        : Off
        14: VERTICAL SCROLLING     {2100} V0     
            Roller type: standard
            Ratchet per turn: 18
            Scroll lines: 0
        15: DFUCONTROL SIGNED      {00C2} V0     
        16: DEVICE RESET           {1802} V0    internal, hidden 
        17: unknown:1803           {1803} V0    internal, hidden 
        18: CONFIG DEVICE PROPS    {1806} V0    internal, hidden 
        19: OOBSTATE               {1805} V0    internal, hidden 
        20: unknown:1813           {1813} V0    internal, hidden 
        21: CHANGE HOST            {1814} V1     
            Change Host        : 1:LessThanThree
        22: HOSTS INFO             {1815} V1     
            Host 0 (unpaired): LessThanThree
            Host 1 (unpaired): 
            Host 2 (unpaired): 
        23: unknown:1830           {1830} V0    internal, hidden 
        24: unknown:1861           {1861} V0    internal, hidden 
        25: unknown:1890           {1890} V0    internal, hidden 
        26: unknown:1891           {1891} V0    internal, hidden 
        27: unknown:18A1           {18A1} V0    internal, hidden 
        28: unknown:1DF3           {1DF3} V0    internal, hidden 
        29: unknown:1E00           {1E00} V0    hidden 
        30: unknown:1EB0           {1EB0} V0    internal, hidden 
        31: unknown:18B1           {18B1} V0    internal, hidden 
        32: unknown:1850           {1850} V0    internal, hidden 
        33: unknown:1F03           {1F03} V0    internal, hidden 
        34: unknown:18C0           {18C0} V0    internal, hidden 
        35: LOWRES WHEEL           {2130} V0     
            Wheel Reports: HID
            Scroll Wheel Diversion (saved): False
            Scroll Wheel Diversion        : False
     Has 9 reprogrammable keys:
         0: Left Button               , default: Left Click                  => Left Click                
             mse, reprogrammable, pos:0, group:1, group mask:g1
             reporting: default
         1: Right Button              , default: Right Click                 => Right Click               
             mse, reprogrammable, pos:0, group:1, group mask:g1
             reporting: default
         2: Middle Button             , default: Mouse Middle Button         => Mouse Middle Button       
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: default
         3: Back Button               , default: Mouse Back Button           => Mouse Back Button         
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: default
         4: Forward Button            , default: Mouse Forward Button        => Mouse Forward Button      
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: default
         5: DPI Change                , default: DPI Change                  => DPI Change                
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: diverted
         6: Left Tilt                 , default: Mouse Scroll Left Button    => Mouse Scroll Left Button  
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: default
         7: Right Tilt                , default: Mouse Scroll Right Button   => Mouse Scroll Right Button 
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: default
         8: Virtual Gesture Button    , default: Virtual Gesture Button      => Virtual Gesture Button    
             divertable, virtual, raw XY, force raw XY, pos:0, group:3, group mask:empty
             reporting: default
     Has 8 persistent remappable keys:
         0: Left Button                => Mouse Button: Mouse Button Left
         1: Right Button               => Mouse Button: Mouse Button Right
         2: Middle Button              => Mouse Button: Mouse Button Middle
         3: Back Button                => Key: F23 (remapped)
         4: Forward Button             => Key: F24 (remapped)
         5: DPI Change                 => Mouse Button: Mouse Button DPI
         6: Left Tilt                  => Horizontal Scroll: Horizontal Scroll Left
         7: Right Tilt                 => Horizontal Scroll: Horizontal Scroll Right
     Battery: 90%, discharging, next level 50%.

```
</details>

- Contents of `~/.config/solaar/config.yaml` (or `~/.config/solaar/config.json` if `~/.config/solaar/config.yaml` not present):

<details>

```
  GNU nano 7.2      /home/millie/.config/solaar/config.yaml                
- 1.1.8+dfsg-2
- _NAME: 'MX Ergo Multi-Device Trackball '
  _absent: [hi-res-scroll, hires-smooth-invert, hires-smooth-resolution, h>
    onboard_profiles, report_rate, dpi, backlight, backlight-timed, fn-swa>
    mr-key-led, multiplatform, gesture2-gestures, gesture2-divert, gesture>
  _battery: 4096
  _modelId: B01D406F0000
  _sensitive: {change-host: false, divert-keys: false, lowres-scroll-mode:>
    speed-change: false}
  _serial: 1F0B3C09
  _speed-change: 256
  _unitId: A697ADA8
  _wpid: 406F
  change-host: null
  divert-keys: {82: 0, 83: 0, 86: 0, 91: 0, 93: 0, 237: 1}
  lowres-scroll-mode: false
  persistent-remappable-keys: null
  pointer_speed: 256
  reprogrammable-keys: {80: 80, 81: 81, 82: 82, 83: 83, 86: 86, 91: 91, 93>
  speed-change: 237


```
</details>


- Errors or warrnings from Solaar:
<!-- Under normal operation Solaar keeps a log of warning and error messages in ~/.tmp
while it is running as a file starting with 'Solaar'.
If this file is not available or does not have useful information you can
run Solaar as `solaar -dd`, after killing any running Solaar processes to
have Solaar log informational, warning, and error messages to stdout. -->


<details>
2023-08-23 02:28:12,809,809     INFO [MainThread] root: language en_US (UTF-8), translations path None
2023-08-23 02:28:12,899,899     INFO [MainThread] logitech_receiver.diversion: GDK Keymap set up
2023-08-23 02:28:12,900,900    ERROR [MainThread] logitech_receiver.diversion: failed to load from /home/millie/.config/solaar/rules.yaml
expected '<document start>', but found '<stream end>'
  in "/home/millie/.config/solaar/rules.yaml", line 2, column 1
2023-08-23 02:28:12,911,911     INFO [MainThread] solaar.upower: connected to system dbus, watching for suspend/resume events
2023-08-23 02:28:12,937,937     INFO [MainThread] solaar.ui.notify: starting desktop notifications
2023-08-23 02:28:13,010,010     INFO [MainThread] solaar.listener: starting receiver listening threads
2023-08-23 02:28:13,013,013  WARNING [MainThread] hidapi.udev: Report Descriptor not processed for BID 0003 VID 0000046D PID 0000C52B: No module named 'hid_parser'
2023-08-23 02:28:13,013,013     INFO [MainThread] hidapi.udev: Found device BID 0003 VID 0000046D PID 0000C52B HID++ None None USB 2 2
2023-08-23 02:28:13,013,013     INFO [MainThread] solaar.listener: receiver event add DeviceInfo(path='/dev/hidraw1', bus_id=3, vendor_id='046D', product_id='C52B', interface=2, driver='logitech-djreceiver', manufacturer=None, product=None, serial='', release=None, isDevice=None, hidpp_short=None, hidpp_long=None)
2023-08-23 02:28:13,013,013     INFO [MainThread] logitech_receiver.base: New lock 14
2023-08-23 02:28:13,016,016     INFO [ReceiverListener:hidraw1] logitech_receiver.listener: started with <UnifyingReceiver(/dev/hidraw1,14)> (14)
2023-08-23 02:28:13,016,016     INFO [ReceiverListener:hidraw1] solaar.listener: <UnifyingReceiver(/dev/hidraw1,14)>: notifications listener has started (14)
2023-08-23 02:28:13,016,016     INFO [ReceiverListener:hidraw1] logitech_receiver.base: New lock <_ThreadedHandle(/dev/hidraw1)>
2023-08-23 02:28:13,016,016  WARNING [MainThread] hidapi.udev: Report Descriptor not processed for BID 0003 VID 00003434 PID 00000361: No module named 'hid_parser'
2023-08-23 02:28:13,017,017  WARNING [MainThread] hidapi.udev: Report Descriptor not processed for BID 0003 VID 00003434 PID 00000361: No module named 'hid_parser'
2023-08-23 02:28:13,017,017  WARNING [MainThread] hidapi.udev: Report Descriptor not processed for BID 0003 VID 00003434 PID 00000361: No module named 'hid_parser'
2023-08-23 02:28:13,017,017  WARNING [MainThread] hidapi.udev: Report Descriptor not processed for BID 0003 VID 0000D010 PID 00001601: No module named 'hid_parser'
2023-08-23 02:28:13,018,018  WARNING [MainThread] hidapi.udev: Report Descriptor not processed for BID 0003 VID 0000D010 PID 00001601: No module named 'hid_parser'
2023-08-23 02:28:13,018,018  WARNING [MainThread] hidapi.udev: Report Descriptor not processed for BID 0003 VID 0000D010 PID 00001601: No module named 'hid_parser'
2023-08-23 02:28:13,018,018  WARNING [MainThread] hidapi.udev: Report Descriptor not processed for BID 0003 VID 00000D8C PID 00000005: No module named 'hid_parser'
2023-08-23 02:28:13,027,027     INFO [ReceiverListener:hidraw1] logitech_receiver.receiver: <UnifyingReceiver(/dev/hidraw1,14)>: receiver notifications enabled => ('wireless', 'software present')
2023-08-23 02:28:13,033,033     INFO [ReceiverListener:hidraw1] solaar.listener: status_changed <UnifyingReceiver(/dev/hidraw1,14)>: present, No paired devices. (0) 
2023-08-23 02:28:13,034,034     INFO [ReceiverListener:hidraw1] solaar.listener: ignoring DJ pairing notification Notification(20,1,41,01,6F401E0000000000000000)
2023-08-23 02:28:13,034,034     INFO [ReceiverListener:hidraw1] solaar.listener: ignoring DJ pairing notification Notification(20,0,41,02,0000000000000000000000)
2023-08-23 02:28:13,040,040     INFO [ReceiverListener:hidraw1] logitech_receiver.receiver: <UnifyingReceiver(/dev/hidraw1,14)>: found new device 1 (406F)
2023-08-23 02:28:13,040,040     INFO [ReceiverListener:hidraw1] logitech_receiver.base: New lock 17
2023-08-23 02:28:13,040,040     INFO [ReceiverListener:hidraw1] solaar.listener: connection Notification(10,1,41,04,386F40) for <Device(1,406F,MX Ergo Multi-Device Trackball ,1F0B3C09)> (trackball)
2023-08-23 02:28:13,754,754     INFO [ReceiverListener:hidraw1] solaar.listener: status_changed <UnifyingReceiver(/dev/hidraw1,14)>: present, 1 paired device. (0) 
2023-08-23 02:28:13,874,874     INFO [ReceiverListener:hidraw1] logitech_receiver.device: <Device(1,406F,MX Ergo Multi-Device Trackball ,1F0B3C09)>: unitId A697ADA8 does not match serial 1F0B3C09
2023-08-23 02:28:13,914,914     INFO [ReceiverListener:hidraw1] solaar.listener: status_changed <Device(1,406F,MX Ergo Multi-Device Trackball ,1F0B3C09)>: paired online, {'LINK ENCRYPTED': True, 'BATTERY LEVEL': 90, 'BATTERY STATUS': NamedInt(0, 'discharging'), 'BATTERY NEXT LEVEL': 50, 'BATTERY VOLTAGE': None, 'BATTERY CHARGING': False, 'ERROR': None} (0) 
2023-08-23 02:28:14,852,852     INFO [ReceiverListener:hidraw1] logitech_receiver.status: <Device(1,406F,MX Ergo Multi-Device Trackball ,1F0B3C09)> pushing device settings [<Setting([feature:toggle] MX Ergo:lowres-scroll-mode=None)>, <Setting([feature:range] MX Ergo:pointer_speed=None)>, <Setting([feature:choice] MX Ergo:speed-change=None)>, <Setting([feature:map choice] MX Ergo:reprogrammable-keys=None)>, <Setting([feature:map choice] MX Ergo:persistent-remappable-keys=None)>, <Setting([feature:map choice] MX Ergo:divert-keys=None)>, <Setting([feature:choice] MX Ergo:change-host=None)>]
2023-08-23 02:28:15,254,254     INFO [ReceiverListener:hidraw1] solaar.listener: status_changed <Device(1,406F,MX Ergo Multi-Device Trackball ,1F0B3C09)>: paired online, {'LINK ENCRYPTED': True, 'BATTERY LEVEL': 90, 'BATTERY STATUS': NamedInt(0, 'discharging'), 'BATTERY NEXT LEVEL': 50, 'BATTERY VOLTAGE': None, 'BATTERY CHARGING': False, 'ERROR': None} (0) 
2023-08-23 02:28:19,916,916     INFO [MainThread] solaar.configuration: saved ['1.1.8+dfsg-2', {'_NAME': 'MX Ergo Multi-Device Trackball ', '_absent': ['hi-res-scroll', 'hires-smooth-invert', 'hires-smooth-resolution', 'hires-scroll-mode', 'scroll-ratchet', 'smart-shift', 'thumb-scroll-invert', 'thumb-scroll-mode', 'onboard_profiles', 'report_rate', 'dpi', 'backlight', 'backlight-timed', 'fn-swap', 'disable-keyboard-keys', 'crown-smooth', 'divert-crown', 'divert-gkeys', 'm-key-leds', 'mr-key-led', 'multiplatform', 'gesture2-gestures', 'gesture2-divert', 'gesture2-params', 'sidetone', 'equalizer'], '_battery': 4096, '_modelId': 'B01D406F0000', '_sensitive': {'change-host': False, 'divert-keys': False, 'lowres-scroll-mode': True, 'persistent-remappable-keys': True, 'pointer_speed': True, 'reprogrammable-keys': False, 'speed-change': False}, '_serial': '1F0B3C09', '_speed-change': 256, '_unitId': 'A697ADA8', '_wpid': '406F', 'change-host': None, 'divert-keys': {82: 0, 83: 0, 86: 0, 91: 0, 93: 0, 237: 1}, 'lowres-scroll-mode': False, 'persistent-remappable-keys': None, 'pointer_speed': 256, 'reprogrammable-keys': {80: 80, 81: 81, 82: 82, 83: 83, 86: 86, 91: 91, 93: 93, 237: 237}, 'speed-change': 237}] to /home/millie/.config/solaar/config.yaml
</details>

**Describe the bug**
When I  set the 'back' and 'forward' buttons on my MX Ergo to anything other than 'Mouse Button Back' and 'Mouse Button Forward' under 'Persistent Key/Button Mapping', it doesn't allow me to hold them down. Rather than pressing and holding, it immediately clicks and releases the moment I press the button. It only does this if I attempt to rebind them. 



**To Reproduce**
Steps to reproduce the behavior:
1. Go to 'Persistent Key/Button Mapping'
2. Set 'Forward Button' to any key you like (tested F24, F23, and GRAVE)
3. Set 'F24' or your chosen keybind to activate something such as push to talk
4. Hold the button and listen to the toggle quickly switch on and off, rather than staying on as it should (and does if not rebound)

**Screenshots**
[Screenshot of Solaar](https://cdn.discordapp.com/attachments/605677387581227028/1143800440018972763/image.png)




Discussion:
That's just how the mouse works.  All that Solaar is doing is activating a feature on the mouse.
Changing the behaviour would probably require getting Logitech to modify the firmware on the mouse.
I've remapped it on my Windows install without issue through Logitech Options +. Same keys, F23 and F24. Whatever the difference is, it's definitely a behavior the mouse is compatible with. Maybe it's on the software end with Options + rather than in the firmware? 

I guess that'd make it more of a feature request? 
It is unclear what feature the Logitech software is using and whether the Logitech software is doing something that Solaar cannot.   
Any idea how to get in touch with their devs? I tried reaching out through customer service to find an email or number or something, but after about half an hour of them trying to find a solution for me all I ended up with was a page about how they don't take unsolicited contributions, but that any unsolicited contributions offered to their representatives automatically belong to Logitech. 

So no luck on that end. 

Do you think it would be reasonable to put in a feature request for something like a 'press and hold' function for Solaar? Sort of similar to how with QMK or other keyboard mapping software you're able to set a button to press down a key combination and hold it or to release it.
I don't have any way to influence Logitech.

You could use Solaar rules, but you will have to wait for the solution to #2113, and live with the short delay introduced by using rules.

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
@@ -88,6 +88,8 @@
 _BUTTON_RELEASE = 2
 _BUTTON_PRESS = 3
 
+CLICK, DEPRESS, RELEASE = 'click', 'depress', 'release'
+
 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):
@@ -312,20 +314,36 @@ def simulate_key(code, event):  # X11 keycode but Solaar event code
 
 
 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
+    if isinstance(count, int):
+        for _ in range(count):
+            if not simulate_xtest(button[0], _BUTTON_PRESS):
+                return False
+            if not simulate_xtest(button[0], _BUTTON_RELEASE):
+                return False
+    else:
+        if count != RELEASE:
+            if not simulate_xtest(button[0], _BUTTON_PRESS):
+                return False
+        if count != DEPRESS:
+            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
+    if isinstance(count, int):
+        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
+    else:
+        if count != RELEASE:
+            if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 1):
+                return False
+        if count != DEPRESS:
+            if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 0):
+                return False
     return True
 
 
@@ -1040,6 +1058,29 @@ def data(self):
         return {'Device': self.devID}
 
 
+class Host(Condition):
+
+    def __init__(self, host, warn=True):
+        if not (isinstance(host, str)):
+            if warn:
+                _log.warn('rule Host Name argument not a string: %s', host)
+            self.host = ''
+        self.host = host
+
+    def __str__(self):
+        return 'Host: ' + str(self.host)
+
+    def evaluate(self, feature, notification, device, status, last_result):
+        if _log.isEnabledFor(_DEBUG):
+            _log.debug('evaluate condition: %s', self)
+        import socket
+        hostname = socket.getfqdn()
+        return hostname.startswith(self.host)
+
+    def data(self):
+        return {'Host': self.host}
+
+
 class Action(RuleComponent):
 
     def __init__(self, *args):
@@ -1050,7 +1091,6 @@ def evaluate(self, feature, notification, device, status, last_result):
 
 
 class KeyPress(Action):
-    CLICK, DEPRESS, RELEASE = 'click', 'depress', 'release'
 
     def __init__(self, args, warn=True):
         self.key_names, self.action = self.regularize_args(args)
@@ -1066,11 +1106,11 @@ def __init__(self, args, warn=True):
             self.key_symbols = []
 
     def regularize_args(self, args):
-        action = self.CLICK
+        action = CLICK
         if not isinstance(args, list):
             args = [args]
         keys = args
-        if len(args) == 2 and args[1] in [self.CLICK, self.DEPRESS, self.RELEASE]:
+        if len(args) == 2 and args[1] in [CLICK, DEPRESS, RELEASE]:
             keys = [args[0]] if isinstance(args[0], str) else args[0]
             action = args[1]
         return keys, action
@@ -1116,14 +1156,14 @@ def keyDown(self, keysyms, modifiers):
             (keycode, level) = self.keysym_to_keycode(k, modifiers)
             if keycode is None:
                 _log.warn('rule KeyPress key symbol not currently available %s', self)
-            elif self.action != self.CLICK or self.needed(keycode, modifiers):  # only check needed when clicking
+            elif self.action != CLICK or self.needed(keycode, modifiers):  # only check needed when clicking
                 self.mods(level, modifiers, _KEY_PRESS)
                 simulate_key(keycode, _KEY_PRESS)
 
     def keyUp(self, keysyms, modifiers):
         for k in keysyms:
             (keycode, level) = self.keysym_to_keycode(k, modifiers)
-            if keycode and (self.action != self.CLICK or self.needed(keycode, modifiers)):  # only check needed when clicking
+            if keycode and (self.action != CLICK or self.needed(keycode, modifiers)):  # only check needed when clicking
                 simulate_key(keycode, _KEY_RELEASE)
                 self.mods(level, modifiers, _KEY_RELEASE)
 
@@ -1132,9 +1172,9 @@ def evaluate(self, feature, notification, device, status, last_result):
             current = gkeymap.get_modifier_state()
             if _log.isEnabledFor(_INFO):
                 _log.info('KeyPress action: %s %s, group %s, modifiers %s', self.key_names, self.action, kbdgroup(), current)
-            if self.action != self.RELEASE:
+            if self.action != RELEASE:
                 self.keyDown(self.key_symbols, current)
-            if self.action != self.DEPRESS:
+            if self.action != DEPRESS:
                 self.keyUp(reversed(self.key_symbols), current)
             _time.sleep(0.01)
         else:
@@ -1202,9 +1242,11 @@ def __init__(self, args, warn=True):
         try:
             self.count = int(count)
         except (ValueError, TypeError):
-            if warn:
-                _log.warn('rule MouseClick action: count %s should be an integer', count)
-            self.count = 1
+            if count in [CLICK, DEPRESS, RELEASE]:
+                self.count = count
+            elif warn:
+                _log.warn('rule MouseClick action: argument %s should be an integer or CLICK, PRESS, or RELEASE', count)
+                self.count = 1
 
     def __str__(self):
         return 'MouseClick: %s (%d)' % (self.button, self.count)
@@ -1338,6 +1380,7 @@ def data(self):
     'MouseGesture': MouseGesture,
     'Active': Active,
     'Device': Device,
+    'Host': Host,
     'KeyPress': KeyPress,
     'MouseScroll': MouseScroll,
     'MouseClick': MouseClick,
diff --git a/lib/solaar/ui/diversion_rules.py b/lib/solaar/ui/diversion_rules.py
--- a/lib/solaar/ui/diversion_rules.py
+++ b/lib/solaar/ui/diversion_rules.py
@@ -29,9 +29,9 @@
 from gi.repository import Gdk, GObject, Gtk
 from logitech_receiver import diversion as _DIV
 from logitech_receiver.common import NamedInt, NamedInts, UnsortedNamedInts
+from logitech_receiver.diversion import CLICK, DEPRESS, RELEASE
 from logitech_receiver.diversion import XK_KEYS as _XK_KEYS
 from logitech_receiver.diversion import Key as _Key
-from logitech_receiver.diversion import KeyPress as _KeyPress
 from logitech_receiver.diversion import buttons as _buttons
 from logitech_receiver.hidpp20 import FEATURE as _ALL_FEATURES
 from logitech_receiver.settings import KIND as _SKIND
@@ -521,6 +521,7 @@ def _menu_insert(self, m, it, below=False):
                         (_('KeyIsDown'), _DIV.KeyIsDown, ''),
                         (_('Active'), _DIV.Active, ''),
                         (_('Device'), _DIV.Device, ''),
+                        (_('Host'), _DIV.Host, ''),
                         (_('Setting'), _DIV.Setting, [None, '', None]),
                         (_('Test'), _DIV.Test, next(iter(_DIV.TESTS))),
                         (_('Test bytes'), _DIV.TestBytes, [0, 1, 0]),
@@ -1769,13 +1770,13 @@ def create_widgets(self):
         self.add_btn.connect('clicked', self._clicked_add)
         self.widgets[self.add_btn] = (1, 1, 1, 1)
         self.action_clicked_radio = Gtk.RadioButton.new_with_label_from_widget(None, _('Click'))
-        self.action_clicked_radio.connect('toggled', self._on_update, _KeyPress.CLICK)
+        self.action_clicked_radio.connect('toggled', self._on_update, CLICK)
         self.widgets[self.action_clicked_radio] = (0, 3, 1, 1)
         self.action_pressed_radio = Gtk.RadioButton.new_with_label_from_widget(self.action_clicked_radio, _('Depress'))
-        self.action_pressed_radio.connect('toggled', self._on_update, _KeyPress.DEPRESS)
+        self.action_pressed_radio.connect('toggled', self._on_update, DEPRESS)
         self.widgets[self.action_pressed_radio] = (1, 3, 1, 1)
         self.action_released_radio = Gtk.RadioButton.new_with_label_from_widget(self.action_pressed_radio, _('Release'))
-        self.action_released_radio.connect('toggled', self._on_update, _KeyPress.RELEASE)
+        self.action_released_radio.connect('toggled', self._on_update, RELEASE)
         self.widgets[self.action_released_radio] = (2, 3, 1, 1)
 
     def _create_field(self):
@@ -1835,8 +1836,8 @@ def show(self, component, editable=True):
             self.del_btns[i].hide()
 
     def collect_value(self):
-        action = _KeyPress.CLICK if self.action_clicked_radio.get_active() else \
-                 _KeyPress.DEPRESS if self.action_pressed_radio.get_active() else _KeyPress.RELEASE
+        action = CLICK if self.action_clicked_radio.get_active() else \
+                 DEPRESS if self.action_pressed_radio.get_active() else RELEASE
         return [[f.get_text().strip() for f in self.fields if f.get_visible()], action]
 
     @classmethod
@@ -1845,8 +1846,7 @@ def left_label(cls, component):
 
     @classmethod
     def right_label(cls, component):
-        return ' + '.join(component.key_names
-                          ) + ('  (' + component.action + ')' if component.action != _KeyPress.CLICK else '')
+        return ' + '.join(component.key_names) + ('  (' + component.action + ')' if component.action != CLICK else '')
 
 
 class MouseScrollUI(ActionUI):
@@ -1910,6 +1910,7 @@ class MouseClickUI(ActionUI):
     MIN_VALUE = 1
     MAX_VALUE = 9
     BUTTONS = list(_buttons.keys())
+    ACTIONS = [CLICK, DEPRESS, RELEASE]
 
     def create_widgets(self):
         self.widgets = {}
@@ -1918,29 +1919,39 @@ def create_widgets(self):
         )
         self.widgets[self.label] = (0, 0, 4, 1)
         self.label_b = Gtk.Label(label=_('Button'), halign=Gtk.Align.END, valign=Gtk.Align.CENTER, hexpand=True)
-        self.label_c = Gtk.Label(label=_('Count'), halign=Gtk.Align.END, valign=Gtk.Align.CENTER, hexpand=True)
+        self.label_c = Gtk.Label(label=_('Count and Action'), halign=Gtk.Align.END, valign=Gtk.Align.CENTER, hexpand=True)
         self.field_b = CompletionEntry(self.BUTTONS)
         self.field_c = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1)
+        self.field_d = CompletionEntry(self.ACTIONS)
         for f in [self.field_b, self.field_c]:
             f.set_halign(Gtk.Align.CENTER)
             f.set_valign(Gtk.Align.START)
         self.field_b.connect('changed', self._on_update)
         self.field_c.connect('changed', self._on_update)
+        self.field_d.connect('changed', self._on_update)
         self.widgets[self.label_b] = (0, 1, 1, 1)
         self.widgets[self.field_b] = (1, 1, 1, 1)
         self.widgets[self.label_c] = (2, 1, 1, 1)
         self.widgets[self.field_c] = (3, 1, 1, 1)
+        self.widgets[self.field_d] = (4, 1, 1, 1)
 
     def show(self, component, editable):
         super().show(component, editable)
         with self.ignore_changes():
             self.field_b.set_text(component.button)
-            self.field_c.set_value(component.count)
+            if isinstance(component.count, int):
+                self.field_c.set_value(component.count)
+                self.field_d.set_text(CLICK)
+            else:
+                self.field_c.set_value(1)
+                self.field_d.set_text(component.count)
 
     def collect_value(self):
-        b, c = self.field_b.get_text(), int(self.field_c.get_value())
+        b, c, d = self.field_b.get_text(), int(self.field_c.get_value()), self.field_d.get_text()
         if b not in self.BUTTONS:
             b = 'unknown'
+        if d != CLICK:
+            c = d
         return [b, c]
 
     @classmethod
@@ -1949,7 +1960,7 @@ def left_label(cls, component):
 
     @classmethod
     def right_label(cls, component):
-        return f'{component.button} (x{component.count})'
+        return f'{component.button} ({"x" if isinstance(component.count, int) else ""}{component.count})'
 
 
 class ExecuteUI(ActionUI):
@@ -2252,13 +2263,44 @@ def left_label(cls, component):
 class DeviceUI(_DeviceUI, ConditionUI):
 
     CLASS = _DIV.Device
-    label_text = _('Device originated the current notification.')
+    label_text = _('Device that originated the current notification.')
 
     @classmethod
     def left_label(cls, component):
         return _('Device')
 
 
+class HostUI(ConditionUI):
+
+    CLASS = _DIV.Host
+
+    def create_widgets(self):
+        self.widgets = {}
+        self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
+        self.label.set_text(_('Name of host computer.'))
+        self.widgets[self.label] = (0, 0, 1, 1)
+        self.field = Gtk.Entry(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True)
+        self.field.set_size_request(600, 0)
+        self.field.connect('changed', self._on_update)
+        self.widgets[self.field] = (0, 1, 1, 1)
+
+    def show(self, component, editable):
+        super().show(component, editable)
+        with self.ignore_changes():
+            self.field.set_text(component.host)
+
+    def collect_value(self):
+        return self.field.get_text()
+
+    @classmethod
+    def left_label(cls, component):
+        return _('Host')
+
+    @classmethod
+    def right_label(cls, component):
+        return str(component.host)
+
+
 class _SettingWithValueUI:
 
     ALL_SETTINGS = _all_settings()
@@ -2624,6 +2666,7 @@ def _on_update(self, *_args):
     _DIV.MouseProcess: MouseProcessUI,
     _DIV.Active: ActiveUI,
     _DIV.Device: DeviceUI,
+    _DIV.Host: HostUI,
     _DIV.Feature: FeatureUI,
     _DIV.Report: ReportUI,
     _DIV.Modifiers: ModifiersUI,


Test patch: