From 37aaaf37770b10428e53b29123f6057492bbf716 Mon Sep 17 00:00:00 2001 From: Bora Alp Arat Date: Thu, 7 May 2026 02:01:50 +0300 Subject: [PATCH 1/4] Implement dispatch queue for event processing Added dispatch queue management for event handling. --- core/mouse_hook_base.py | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/core/mouse_hook_base.py b/core/mouse_hook_base.py index 37499af..e434df3 100644 --- a/core/mouse_hook_base.py +++ b/core/mouse_hook_base.py @@ -2,6 +2,7 @@ Shared mouse hook behavior used by platform implementations. """ +import queue import time try: @@ -41,6 +42,33 @@ def __init__(self): self._gesture_cooldown_until = 0.0 self._gesture_input_source = None self._connected_device = None + self._dispatch_queue = None + + def _init_dispatch_queue(self, maxsize=0): + """Initialize dispatch queue storage for subclasses with event threads.""" + self._dispatch_queue = queue.Queue(maxsize=max(0, int(maxsize))) + + def _enqueue_dispatch_event(self, event): + """Best-effort enqueue that bounds memory when queue has a max size.""" + q = self._dispatch_queue + if q is None: + return + if q.maxsize <= 0: + q.put(event) + return + try: + q.put_nowait(event) + return + except queue.Full: + pass + try: + q.get_nowait() + except queue.Empty: + pass + try: + q.put_nowait(event) + except queue.Full: + self._emit_debug(f"Dropped event due to full dispatch queue: {event.event_type}") def register(self, event_type, callback): self._callbacks.setdefault(event_type, []).append(callback) @@ -254,3 +282,24 @@ def _on_hid_connect(self): def _on_hid_disconnect(self): self._connected_device = None self._set_device_connected(False) + + def _on_hid_gesture_down(self): + self._dispatch(MouseEvent(MouseEvent.GESTURE_DOWN)) + + def _on_hid_gesture_up(self): + self._dispatch(MouseEvent(MouseEvent.GESTURE_UP)) + + def _on_hid_gesture_move(self, dx, dy): + self._accumulate_gesture_delta(dx, dy, "hid_rawxy") + + def _on_hid_mode_shift_down(self): + self._dispatch(MouseEvent(MouseEvent.MODE_SHIFT_DOWN)) + + def _on_hid_mode_shift_up(self): + self._dispatch(MouseEvent(MouseEvent.MODE_SHIFT_UP)) + + def _on_hid_dpi_switch_down(self): + self._dispatch(MouseEvent(MouseEvent.DPI_SWITCH_DOWN)) + + def _on_hid_dpi_switch_up(self): + self._dispatch(MouseEvent(MouseEvent.DPI_SWITCH_UP)) From b0872dc4c6beba03a2e43f338a6052661b7cea12 Mon Sep 17 00:00:00 2001 From: Bora Alp Arat Date: Thu, 7 May 2026 02:04:07 +0300 Subject: [PATCH 2/4] Refactor dispatch queue handling in mouse hook --- core/mouse_hook_macos.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/mouse_hook_macos.py b/core/mouse_hook_macos.py index e6d5e7a..2e971b7 100644 --- a/core/mouse_hook_macos.py +++ b/core/mouse_hook_macos.py @@ -63,7 +63,7 @@ def __init__(self): self._wake_observer = None self._session_resign_observer = None self._session_activate_observer = None - self._dispatch_queue = queue.Queue() + self._init_dispatch_queue(maxsize=512) self._dispatch_thread = None self._first_event_logged = False @@ -219,7 +219,7 @@ def _accumulate_gesture_delta(self, delta_x, delta_y, source): "dy": self._gesture_delta_y, } ) - self._dispatch_queue.put( + self._enqueue_dispatch_event( MouseEvent( gesture_event, { @@ -400,7 +400,7 @@ def _event_tap_callback(self, proxy, event_type, cg_event, refcon): mouse_event = MouseEvent(MouseEvent.HSCROLL_LEFT, abs(h_delta)) should_block = MouseEvent.HSCROLL_LEFT in self._blocked_events if mouse_event: - self._dispatch_queue.put(mouse_event) + self._enqueue_dispatch_event(mouse_event) mouse_event = None if should_block: return None @@ -409,7 +409,7 @@ def _event_tap_callback(self, proxy, event_type, cg_event, refcon): return None if mouse_event: - self._dispatch_queue.put(mouse_event) + self._enqueue_dispatch_event(mouse_event) if should_block: return None From e4ffd033dab11c8867e68cef1d9c0e6fcd98a94d Mon Sep 17 00:00:00 2001 From: Bora Alp Arat Date: Thu, 7 May 2026 02:05:26 +0300 Subject: [PATCH 3/4] Refactor dispatch queue handling in mouse hook --- core/mouse_hook_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/mouse_hook_windows.py b/core/mouse_hook_windows.py index 8c55079..efcee54 100644 --- a/core/mouse_hook_windows.py +++ b/core/mouse_hook_windows.py @@ -241,7 +241,7 @@ def __init__(self): self._startup_ok = False self._prev_raw_buttons = {} self._last_rehook_time = 0 - self._dispatch_queue = queue.Queue() + self._init_dispatch_queue(maxsize=512) self._dispatch_worker_thread = None def _accumulate_gesture_delta(self, delta_x, delta_y, source): @@ -466,7 +466,7 @@ def _low_level_handler_inner(self, nCode, wParam, lParam): ) if event: - self._dispatch_queue.put(event) + self._enqueue_dispatch_event(event) if should_block: return 1 From 6c4ecc86ef6dd6f4d74ae821a32f8e93b7065cdd Mon Sep 17 00:00:00 2001 From: Bora Alp Arat Date: Thu, 7 May 2026 02:06:30 +0300 Subject: [PATCH 4/4] Add tests for BaseMouseHook dispatch queue behavior Added tests for enqueueing events in BaseMouseHook to ensure the dispatch queue maintains its size and handles overflow correctly. --- tests/test_mouse_hook.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_mouse_hook.py b/tests/test_mouse_hook.py index 34a2fcc..0b375b4 100644 --- a/tests/test_mouse_hook.py +++ b/tests/test_mouse_hook.py @@ -1,4 +1,5 @@ import importlib +import queue import sys import unittest from types import SimpleNamespace @@ -83,6 +84,37 @@ def test_runtime_state_projects_hid_identity(self): self.assertIs(state.connected_device, device) + +class BaseMouseHookDispatchQueueTests(unittest.TestCase): + def test_enqueue_keeps_queue_bounded_and_drops_oldest(self): + hook = BaseMouseHook() + hook._init_dispatch_queue(maxsize=2) + + hook._enqueue_dispatch_event(SimpleNamespace(event_type="e1", raw_data=None)) + hook._enqueue_dispatch_event(SimpleNamespace(event_type="e2", raw_data=None)) + hook._enqueue_dispatch_event(SimpleNamespace(event_type="e3", raw_data=None)) + + self.assertEqual(hook._dispatch_queue.qsize(), 2) + first = hook._dispatch_queue.get_nowait() + second = hook._dispatch_queue.get_nowait() + self.assertEqual(first.event_type, "e2") + self.assertEqual(second.event_type, "e3") + + def test_enqueue_drops_and_emits_debug_when_still_full(self): + hook = BaseMouseHook() + hook._init_dispatch_queue(maxsize=1) + hook.debug_mode = True + hook.set_debug_callback(Mock()) + + hook._dispatch_queue.put_nowait(SimpleNamespace(event_type="old", raw_data=None)) + with patch.object(hook._dispatch_queue, "get_nowait", side_effect=queue.Empty): + with patch.object(hook._dispatch_queue, "put_nowait", side_effect=[queue.Full, queue.Full]): + hook._enqueue_dispatch_event(SimpleNamespace(event_type="new", raw_data=None)) + + hook._debug_callback.assert_called_once() + self.assertIn("Dropped event due to full dispatch queue", hook._debug_callback.call_args[0][0]) + + class LinuxMouseHookReconnectTests(unittest.TestCase): def _reload_for_linux(self): fake_evdev = SimpleNamespace(