From 721d16ff30906e0615b10af2aa9a7d09999c9608 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Tue, 26 May 2026 02:27:09 +0800 Subject: [PATCH 01/10] rules: MouseScroll: Do not consider bool as number While the current code path probably won't run into a mistaken scroll amount as True == 1, let's fix this as my radar beeped when I first saw this. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index e933f5ad..dc76df2a 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -289,6 +289,10 @@ def signed(bytes_: bytes) -> int: return int.from_bytes(bytes_, "big", signed=True) +def is_number(n) -> bool: + return not isinstance(n, bool) and isinstance(n, numbers.Number) + + def xy_direction(_x, _y): # normalize x and y m = math.sqrt((_x * _x) + (_y * _y)) @@ -1216,7 +1220,7 @@ class MouseScroll(Action): def __init__(self, amounts, warn=True): if len(amounts) == 1 and isinstance(amounts[0], list): amounts = amounts[0] - if not (len(amounts) == 2 and all([isinstance(a, numbers.Number) for a in amounts])): + if not (len(amounts) == 2 and all([is_number(a) for a in amounts])): if warn: logger.warning("rule MouseScroll argument not two numbers %s", amounts) amounts = [0, 0] @@ -1227,7 +1231,7 @@ class MouseScroll(Action): def evaluate(self, feature, notification: HIDPPNotification, device, last_result): amounts = self.amounts - if isinstance(last_result, numbers.Number): + if is_number(last_result): amounts = [math.floor(last_result * a) for a in self.amounts] if logger.isEnabledFor(logging.INFO): logger.info("MouseScroll action: %s %s %s", self.amounts, last_result, amounts) From c81d19aaf68381166dc1a6fcaee159b35ca65c10 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Tue, 26 May 2026 02:27:10 +0800 Subject: [PATCH 02/10] rules: MouseScroll: Round to zero for signed values Rounding to negative infinity makes no sense for the purpose here. While the current code path probably won't run into a mistaken scroll amount as no rule component returns a floating point number, let's fix this as my radar beeped too. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index dc76df2a..87dd727c 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -1232,7 +1232,7 @@ class MouseScroll(Action): def evaluate(self, feature, notification: HIDPPNotification, device, last_result): amounts = self.amounts if is_number(last_result): - amounts = [math.floor(last_result * a) for a in self.amounts] + amounts = [math.trunc(last_result * a) for a in self.amounts] if logger.isEnabledFor(logging.INFO): logger.info("MouseScroll action: %s %s %s", self.amounts, last_result, amounts) dx, dy = amounts From a6a861bfc0fca7ad29d960f35ffebd447408a07c Mon Sep 17 00:00:00 2001 From: Rongrong Date: Tue, 26 May 2026 02:27:10 +0800 Subject: [PATCH 03/10] rules: Test: Fix divergence between documentation and code Return True when the documentation says so to ensure that the result is not accidentally taken into MouseScroll's scroll amount multiplication. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 87dd727c..537b9755 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -418,10 +418,10 @@ TESTS = { "crown_left": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[1] >= 128 and 256 - d[1], False], "crown_right_ratchet": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[2] < 128 and d[2], False], "crown_left_ratchet": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[2] >= 128 and 256 - d[2], False], - "crown_tap": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[5] == 0x01 and d[5], False], - "crown_start_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x01 and d[6], False], - "crown_end_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x05 and d[6], False], - "crown_pressed": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and 0x01 <= d[6] <= 0x04 and d[6], False], + "crown_tap": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[5] == 0x01 and True, False], + "crown_start_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x01 and True, False], + "crown_end_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x05 and True, False], + "crown_pressed": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and 0x01 <= d[6] <= 0x04 and True, False], "thumb_wheel_up": [thumb_wheel_up, True], "thumb_wheel_down": [thumb_wheel_down, True], "lowres_wheel_up": [ From a499e8d67677368b395088b3e9bc4c935f572a3f Mon Sep 17 00:00:00 2001 From: Rongrong Date: Tue, 26 May 2026 02:27:10 +0800 Subject: [PATCH 04/10] rules: Test: Fix divergence between crown and wheel All wheel tests return signed rotation amounts so that they can share the same rule to multiply the rotation, as per the example rules in the documentation. However, this doesn't apply to crown tests, which makes no sense as they also return rotation amount. Hence, do the same for crown tests. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 537b9755..7f117c27 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -414,10 +414,22 @@ def charging(f, r, d, _a): TESTS = { - "crown_right": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[1] < 128 and d[1], False], - "crown_left": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[1] >= 128 and 256 - d[1], False], - "crown_right_ratchet": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[2] < 128 and d[2], False], - "crown_left_ratchet": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[2] >= 128 and 256 - d[2], False], + "crown_right": [ + lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and signed(d[1:2]) > 0 and signed(d[1:2]), + False, + ], + "crown_left": [ + lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and signed(d[1:2]) < 0 and signed(d[1:2]), + False, + ], + "crown_right_ratchet": [ + lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and signed(d[2:3]) > 0 and signed(d[2:3]), + False, + ], + "crown_left_ratchet": [ + lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and signed(d[2:3]) < 0 and signed(d[2:3]), + False, + ], "crown_tap": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[5] == 0x01 and True, False], "crown_start_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x01 and True, False], "crown_end_press": [lambda f, r, d, a: f == SupportedFeature.CROWN and r == 0 and d[6] == 0x05 and True, False], From 73ced3a7daab7e06d09440609a8b87aa7ba94a35 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Tue, 26 May 2026 02:27:10 +0800 Subject: [PATCH 05/10] rules: Fix last_result not passed among chained rules A previous refactor accidentally broke chained rules that rely on the result chaining, so fix it. With it fixed, some rule examples from docs/rules.md work properly again, yay! --- - Feature: THUMB WHEEL - Rule: [ Modifiers: Control, Test: thumb_wheel_up, MouseScroll: [-2, 0] ] - Rule: - Modifiers: Control - Test: thumb_wheel_down - MouseScroll: [-2, 0] - Rule: [ Or: [ Test: thumb_wheel_up, Test: thumb_wheel_down ], MouseScroll: [-1, 0] ] ... --- - Feature: LOWRES WHEEL - Rule: [ Or: [ Test: lowres_wheel_up, Test: lowres_wheel_down ], MouseScroll: [0, 2] ] ... Fixes: 46366b243012 ("Fix warnings from automatic code inspections") Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 7f117c27..06d91150 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -481,14 +481,13 @@ class RuleComponent: def _evaluate(components, feature, notification: HIDPPNotification, device, result) -> Any: - res = True for component in components: - res = component.evaluate(feature, notification, device, result) - if not isinstance(component, Action) and res is None: + result = component.evaluate(feature, notification, device, result) + if not isinstance(component, Action) and result is None: return None - if isinstance(component, Condition) and not res: - return res - return res + if isinstance(component, Condition) and not result: + return result + return result class Rule(RuleComponent): From 8b134c31bb0918877e66acbc344d3737fb61d8a7 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Tue, 26 May 2026 02:27:11 +0800 Subject: [PATCH 06/10] rules: uinput: Fix device leakage While randomly messing around with rule components (I plan to add some new ones), I found this bug by screwing things up (shh...) Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 06d91150..579d6f9c 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -16,6 +16,7 @@ from __future__ import annotations +import contextlib import ctypes import logging import math @@ -330,6 +331,9 @@ def simulate_uinput(what, code, arg): logger.debug("uinput simulated input %s %s %s", what, code, arg) return True except Exception as e: + with contextlib.suppress(Exception): + udevice.close() + udevice = None logger.warning("uinput write failed: %s", e) From 6249ec5d821f6f34ff0b8add78820948200d63b8 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Tue, 26 May 2026 02:27:11 +0800 Subject: [PATCH 07/10] rules: uinput: Do not register KEY_MAX As doing so makes no sense. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 579d6f9c..acc6ee77 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -241,7 +241,7 @@ if evdev: } # 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"] + key_events = [c for n, c in evdev.ecodes.ecodes.items() if n.startswith("KEY") and n not in {"KEY_CNT", "KEY_MAX"}] for _, evcode in buttons.values(): if evcode: key_events.append(evcode) From 0406452cc2f431f44ff13d5fc46ed177f952bc8e Mon Sep 17 00:00:00 2001 From: Rongrong Date: Mon, 25 May 2026 13:33:43 +0800 Subject: [PATCH 08/10] rules: uinput: Refactor into a class So that further improvements can be less painful. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 151 ++++++++++++++--------------- 1 file changed, 74 insertions(+), 77 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index acc6ee77..7060d6c7 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -130,8 +130,6 @@ NET_WM_PID = None WM_CLASS = None -udevice = None - key_down = None key_up = None @@ -256,17 +254,75 @@ else: devicecap = {} -def setup_uinput(): - global udevice - if udevice is not None: - return udevice - try: - udevice = evdev.uinput.UInput(events=devicecap, name="solaar-keyboard") - if logger.isEnabledFor(logging.INFO): - logger.info("uinput device set up") +class UInput: + def __init__(self): + self.udevice = None + + def setup(self): + if self.udevice is not None: + return self.udevice + try: + self.udevice = evdev.uinput.UInput(events=devicecap, name="solaar-keyboard") + if logger.isEnabledFor(logging.INFO): + logger.info("uinput device set up") + return True + except Exception as e: + logger.warning("cannot create uinput device: %s", e) + + def simulate(self, what, code, arg): + if self.setup(): + try: + self.udevice.write(what, code, arg) + self.udevice.syn() + if logger.isEnabledFor(logging.DEBUG): + logger.debug("uinput simulated input %s %s %s", what, code, arg) + return True + except Exception as e: + with contextlib.suppress(Exception): + self.udevice.close() + + self.udevice = None + logger.warning("uinput write failed: %s", e) + + def key(self, code, event): # X11 keycode but Solaar event code + if evdev and self.simulate(evdev.ecodes.EV_KEY, code - 8, event): + return True + logger.warning("no way to simulate key input") + + def _click(self, button, count): + if isinstance(count, int): + for _ in range(count): + if not self.simulate(evdev.ecodes.EV_KEY, button[1], 1): + return False + if not self.simulate(evdev.ecodes.EV_KEY, button[1], 0): + return False + else: + if count != RELEASE: + if not self.simulate(evdev.ecodes.EV_KEY, button[1], 1): + return False + if count != DEPRESS: + if not self.simulate(evdev.ecodes.EV_KEY, button[1], 0): + return False return True - except Exception as e: - logger.warning("cannot create uinput device: %s", e) + + def click(self, button, count): + if self._click(button, count): + return True + logger.warning("no way to simulate mouse click") + return False + + def scroll(self, dx, dy): + success = True + if dx: + success = self.simulate(evdev.ecodes.EV_REL, evdev.ecodes.REL_HWHEEL, dx) + if dy and success: + success = self.simulate(evdev.ecodes.EV_REL, evdev.ecodes.REL_WHEEL, dy) + if success: + return True + logger.warning("no way to simulate scrolling") + + +uinput = UInput() def kbdgroup(): @@ -321,65 +377,6 @@ def xy_direction(_x, _y): return "noop" -def simulate_uinput(what, code, arg): - global udevice - if setup_uinput(): - try: - udevice.write(what, code, arg) - udevice.syn() - if logger.isEnabledFor(logging.DEBUG): - logger.debug("uinput simulated input %s %s %s", what, code, arg) - return True - except Exception as e: - with contextlib.suppress(Exception): - udevice.close() - - udevice = None - logger.warning("uinput write failed: %s", e) - - -def simulate_key(code, event): # X11 keycode but Solaar event code - if evdev and simulate_uinput(evdev.ecodes.EV_KEY, code - 8, event): - return True - logger.warning("no way to simulate key input") - - -def click_uinput(button, count): - 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 - - -def click(button, count): - if click_uinput(button, count): - return True - logger.warning("no way to simulate mouse click") - return False - - -def simulate_scroll(dx, dy): - if setup_uinput(): - success = True - if dx: - success = simulate_uinput(evdev.ecodes.EV_REL, evdev.ecodes.REL_HWHEEL, dx) - if dy and success: - success = simulate_uinput(evdev.ecodes.EV_REL, evdev.ecodes.REL_WHEEL, dy) - if success: - return True - logger.warning("no way to simulate scrolling") - - def thumb_wheel_up(f, r, d, a): global thumb_wheel_displacement if f != SupportedFeature.THUMB_WHEEL or r != 0: @@ -1176,11 +1173,11 @@ class KeyPress(Action): if level == 2 or level == 3: (sk, _) = keysym_to_keycode(XK_KEYS.get("ISO_Level3_Shift", None), modifiers) if sk and self.needed(sk, modifiers): - simulate_key(sk, direction) + uinput.key(sk, direction) if level == 1 or level == 3: (sk, _) = keysym_to_keycode(XK_KEYS.get("Shift_L", None), modifiers) if sk and self.needed(sk, modifiers): - simulate_key(sk, direction) + uinput.key(sk, direction) def keyDown(self, keysyms_, modifiers): for k in keysyms_: @@ -1189,13 +1186,13 @@ class KeyPress(Action): logger.warning("rule KeyPress key symbol not currently available %s", self) 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) + uinput.key(keycode, _KEY_PRESS) def keyUp(self, keysyms_, modifiers): for k in keysyms_: (keycode, level) = keysym_to_keycode(k, modifiers) if keycode and (self.action != CLICK or self.needed(keycode, modifiers)): # only check needed when clicking - simulate_key(keycode, _KEY_RELEASE) + uinput.key(keycode, _KEY_RELEASE) self.mods(level, modifiers, _KEY_RELEASE) def evaluate(self, feature, notification: HIDPPNotification, device, last_result): @@ -1251,7 +1248,7 @@ class MouseScroll(Action): if logger.isEnabledFor(logging.INFO): logger.info("MouseScroll action: %s %s %s", self.amounts, last_result, amounts) dx, dy = amounts - simulate_scroll(dx, dy) + uinput.scroll(dx, dy) time.sleep(0.01) return None @@ -1290,7 +1287,7 @@ class MouseClick(Action): if logger.isEnabledFor(logging.INFO): logger.info(f"MouseClick action: {str(self.count)} {self.button}") if self.button and self.count: - click(buttons[self.button], self.count) + uinput.click(buttons[self.button], self.count) time.sleep(0.01) return None From 3f72efebc10b128985040bed40c2f2659ca1b996 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Mon, 25 May 2026 17:50:18 +0800 Subject: [PATCH 09/10] rules: uinput: Only sleep if two events are really close to each other Several sleep points exist to serialize keyboard and mouse events. However, these unconditional sleeps hurt the performance as they block the main thread. Postpone sleep until the next event, and skip it if enough time has elapsed. Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index 7060d6c7..a5d2d358 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -257,6 +257,22 @@ else: class UInput: def __init__(self): self.udevice = None + self.barrier_until = None + + def _wait_barrier(self): + if self.barrier_until is None: + return + + remainder = self.barrier_until - time.monotonic() + if remainder > 0: + if logger.isEnabledFor(logging.DEBUG): + logger.debug("wait %.3fs due to barrier", remainder) + time.sleep(remainder) + + self.barrier_until = None + + def barrier(self, margin: float): + self.barrier_until = time.monotonic() + margin def setup(self): if self.udevice is not None: @@ -285,8 +301,10 @@ class UInput: logger.warning("uinput write failed: %s", e) def key(self, code, event): # X11 keycode but Solaar event code - if evdev and self.simulate(evdev.ecodes.EV_KEY, code - 8, event): - return True + if evdev: + self._wait_barrier() + if self.simulate(evdev.ecodes.EV_KEY, code - 8, event): + return True logger.warning("no way to simulate key input") def _click(self, button, count): @@ -306,12 +324,14 @@ class UInput: return True def click(self, button, count): + self._wait_barrier() if self._click(button, count): return True logger.warning("no way to simulate mouse click") return False def scroll(self, dx, dy): + self._wait_barrier() success = True if dx: success = self.simulate(evdev.ecodes.EV_REL, evdev.ecodes.REL_HWHEEL, dx) @@ -1210,7 +1230,7 @@ class KeyPress(Action): self.keyDown(self.key_symbols, current) if self.action != DEPRESS: self.keyUp(reversed(self.key_symbols), current) - time.sleep(0.01) + uinput.barrier(0.01) else: logger.warning("no keymap so cannot determine which keycode to send") return None @@ -1249,7 +1269,7 @@ class MouseScroll(Action): logger.info("MouseScroll action: %s %s %s", self.amounts, last_result, amounts) dx, dy = amounts uinput.scroll(dx, dy) - time.sleep(0.01) + uinput.barrier(0.01) return None def data(self): @@ -1288,7 +1308,7 @@ class MouseClick(Action): logger.info(f"MouseClick action: {str(self.count)} {self.button}") if self.button and self.count: uinput.click(buttons[self.button], self.count) - time.sleep(0.01) + uinput.barrier(0.01) return None def data(self): From e2a32cee85fdc09d5e28d41cc6266591d5b3e415 Mon Sep 17 00:00:00 2001 From: Rongrong Date: Mon, 25 May 2026 18:21:01 +0800 Subject: [PATCH 10/10] rules: uinput: Skip barrier for events of the same type The barrier means to serialize keyboard and mouse events [1]. It makes no sense to sleep between events of the same type. Skip barrier for events of the same type, so that the diversion rules for some really frequent HID++ notifications will be happy as long as they don't specify both keyboard and mouse actions. For example, HIRES WHEEL in freespin mode can emit HID++ notifications at a frequency of 125Hz (the report rate), which is faster than 100Hz (the barrier), resulting in backlogged HID++ notifications. Link: https://github.com/pwr-Solaar/Solaar/issues/1020#issuecomment-1086875735 [1] Signed-off-by: Rongrong --- lib/logitech_receiver/diversion.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index a5d2d358..c9010316 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -18,6 +18,7 @@ from __future__ import annotations import contextlib import ctypes +import enum import logging import math import numbers @@ -255,23 +256,33 @@ else: class UInput: + class EventType(enum.Enum): + KEYBOARD = enum.auto() + MOUSE = enum.auto() + def __init__(self): self.udevice = None self.barrier_until = None + self.barrier_type = None - def _wait_barrier(self): + def _wait_barrier(self, event_type: EventType): if self.barrier_until is None: return + if event_type == self.barrier_type: + return + remainder = self.barrier_until - time.monotonic() if remainder > 0: if logger.isEnabledFor(logging.DEBUG): - logger.debug("wait %.3fs due to barrier", remainder) + logger.debug("wait %.3fs due to %s barrier", remainder, self.barrier_type) time.sleep(remainder) self.barrier_until = None + self.barrier_type = None - def barrier(self, margin: float): + def barrier(self, margin: float, event_type: EventType): + self.barrier_type = event_type self.barrier_until = time.monotonic() + margin def setup(self): @@ -302,7 +313,7 @@ class UInput: def key(self, code, event): # X11 keycode but Solaar event code if evdev: - self._wait_barrier() + self._wait_barrier(self.EventType.KEYBOARD) if self.simulate(evdev.ecodes.EV_KEY, code - 8, event): return True logger.warning("no way to simulate key input") @@ -324,14 +335,14 @@ class UInput: return True def click(self, button, count): - self._wait_barrier() + self._wait_barrier(self.EventType.MOUSE) if self._click(button, count): return True logger.warning("no way to simulate mouse click") return False def scroll(self, dx, dy): - self._wait_barrier() + self._wait_barrier(self.EventType.MOUSE) success = True if dx: success = self.simulate(evdev.ecodes.EV_REL, evdev.ecodes.REL_HWHEEL, dx) @@ -1230,7 +1241,7 @@ class KeyPress(Action): self.keyDown(self.key_symbols, current) if self.action != DEPRESS: self.keyUp(reversed(self.key_symbols), current) - uinput.barrier(0.01) + uinput.barrier(0.01, uinput.EventType.KEYBOARD) else: logger.warning("no keymap so cannot determine which keycode to send") return None @@ -1269,7 +1280,7 @@ class MouseScroll(Action): logger.info("MouseScroll action: %s %s %s", self.amounts, last_result, amounts) dx, dy = amounts uinput.scroll(dx, dy) - uinput.barrier(0.01) + uinput.barrier(0.01, uinput.EventType.MOUSE) return None def data(self): @@ -1308,7 +1319,7 @@ class MouseClick(Action): logger.info(f"MouseClick action: {str(self.count)} {self.button}") if self.button and self.count: uinput.click(buttons[self.button], self.count) - uinput.barrier(0.01) + uinput.barrier(0.01, uinput.EventType.MOUSE) return None def data(self):