66
77import threading
88from core .mouse_hook import MouseHook , MouseEvent
9- from core .key_simulator import execute_action
9+ from core .key_simulator import ACTIONS , execute_action
1010from core .config import (
1111 load_config , get_active_mappings , get_profile_for_app ,
12- BUTTON_TO_EVENTS , save_config ,
12+ BUTTON_TO_EVENTS , GESTURE_DIRECTION_BUTTONS , save_config ,
1313)
1414from core .app_detector import AppDetector
1515
@@ -31,8 +31,16 @@ def __init__(self):
3131 self ._profile_change_cb = None # UI callback
3232 self ._connection_change_cb = None # UI callback for device status
3333 self ._battery_read_cb = None # UI callback for battery level
34+ self ._dpi_read_cb = None # UI callback for current DPI
35+ self ._debug_cb = None # UI callback for debug messages
36+ self ._gesture_event_cb = None # UI callback for structured gesture events
37+ self ._debug_events_enabled = bool (
38+ self .cfg .get ("settings" , {}).get ("debug_mode" , False )
39+ )
3440 self ._battery_poll_stop = threading .Event ()
3541 self ._lock = threading .Lock ()
42+ self .hook .set_debug_callback (self ._emit_debug )
43+ self .hook .set_gesture_callback (self ._emit_gesture_event )
3644 self ._setup_hooks ()
3745 self .hook .set_connection_change_callback (self ._on_connection_change )
3846 # Apply persisted DPI setting
@@ -54,6 +62,16 @@ def _setup_hooks(self):
5462 settings = self .cfg .get ("settings" , {})
5563 self .hook .invert_vscroll = settings .get ("invert_vscroll" , False )
5664 self .hook .invert_hscroll = settings .get ("invert_hscroll" , False )
65+ self .hook .debug_mode = self ._debug_events_enabled
66+ self .hook .configure_gestures (
67+ enabled = any (mappings .get (key , "none" ) != "none"
68+ for key in GESTURE_DIRECTION_BUTTONS ),
69+ threshold = settings .get ("gesture_threshold" , 50 ),
70+ deadzone = settings .get ("gesture_deadzone" , 40 ),
71+ timeout_ms = settings .get ("gesture_timeout_ms" , 3000 ),
72+ cooldown_ms = settings .get ("gesture_cooldown_ms" , 500 ),
73+ )
74+ self ._emit_mapping_snapshot ("Hook mappings refreshed" , mappings )
5775
5876 for btn_key , action_id in mappings .items ():
5977 events = list (BUTTON_TO_EVENTS .get (btn_key , ()))
@@ -75,13 +93,28 @@ def _setup_hooks(self):
7593 def _make_handler (self , action_id ):
7694 def handler (event ):
7795 if self ._enabled :
96+ self ._emit_debug (
97+ f"Mapped { event .event_type } -> { action_id } "
98+ f"({ self ._action_label (action_id )} )"
99+ )
100+ if event .event_type .startswith ("gesture_" ):
101+ self ._emit_gesture_event ({
102+ "type" : "mapped" ,
103+ "event_name" : event .event_type ,
104+ "action_id" : action_id ,
105+ "action_label" : self ._action_label (action_id ),
106+ })
78107 execute_action (action_id )
79108 return handler
80109
81110 def _make_hscroll_handler (self , action_id ):
82111 def handler (event ):
83112 if not self ._enabled :
84113 return
114+ self ._emit_debug (
115+ f"Mapped { event .event_type } -> { action_id } "
116+ f"({ self ._action_label (action_id )} )"
117+ )
85118 execute_action (action_id )
86119 return handler
87120
@@ -103,6 +136,7 @@ def _switch_profile(self, profile_name: str):
103136 # Lightweight: just re-wire callbacks, keep hook + HID++ alive
104137 self .hook .reset_bindings ()
105138 self ._setup_hooks ()
139+ self ._emit_debug (f"Active profile -> { profile_name } " )
106140 # Notify UI (if connected)
107141 if self ._profile_change_cb :
108142 try :
@@ -114,35 +148,98 @@ def set_profile_change_callback(self, cb):
114148 """Register a callback ``cb(profile_name)`` invoked on auto-switch."""
115149 self ._profile_change_cb = cb
116150
151+ def set_debug_callback (self , cb ):
152+ """Register ``cb(message: str)`` invoked for debug events."""
153+ self ._debug_cb = cb
154+
155+ def set_gesture_event_callback (self , cb ):
156+ """Register ``cb(event: dict)`` invoked for structured gesture debug events."""
157+ self ._gesture_event_cb = cb
158+
159+ def set_debug_enabled (self , enabled ):
160+ enabled = bool (enabled )
161+ self .cfg .setdefault ("settings" , {})["debug_mode" ] = enabled
162+ self ._debug_events_enabled = enabled
163+ self .hook .debug_mode = enabled
164+ if enabled :
165+ self ._emit_debug (f"Debug enabled on profile { self ._current_profile } " )
166+ self ._emit_mapping_snapshot (
167+ "Current mappings" , get_active_mappings (self .cfg )
168+ )
169+
170+ def set_debug_events_enabled (self , enabled ):
171+ self ._debug_events_enabled = bool (enabled )
172+ self .hook .debug_mode = self ._debug_events_enabled
173+
174+ def _action_label (self , action_id ):
175+ return ACTIONS .get (action_id , {}).get ("label" , action_id )
176+
177+ def _emit_debug (self , message ):
178+ if not self ._debug_events_enabled :
179+ return
180+ if self ._debug_cb :
181+ try :
182+ self ._debug_cb (message )
183+ except Exception :
184+ pass
185+
186+ def _emit_gesture_event (self , event ):
187+ if not self ._debug_events_enabled :
188+ return
189+ if self ._gesture_event_cb :
190+ try :
191+ self ._gesture_event_cb (event )
192+ except Exception :
193+ pass
194+
195+ def _emit_mapping_snapshot (self , prefix , mappings ):
196+ if not self ._debug_events_enabled :
197+ return
198+ interesting = [
199+ "gesture" ,
200+ "gesture_left" ,
201+ "gesture_right" ,
202+ "gesture_up" ,
203+ "gesture_down" ,
204+ "xbutton1" ,
205+ "xbutton2" ,
206+ ]
207+ summary = ", " .join (f"{ key } ={ mappings .get (key , 'none' )} " for key in interesting )
208+ self ._emit_debug (f"{ prefix } : { summary } " )
209+
117210 def _on_connection_change (self , connected ):
211+ self ._battery_poll_stop .set ()
118212 if self ._connection_change_cb :
119213 try :
120214 self ._connection_change_cb (connected )
121215 except Exception :
122216 pass
123- self ._battery_poll_stop .set () # stop any existing poll loop
124217 if connected :
125218 self ._battery_poll_stop = threading .Event ()
126219 threading .Thread (
127- target = self ._battery_poll_loop , daemon = True , name = "BatteryPoll"
220+ target = self ._battery_poll_loop ,
221+ args = (self ._battery_poll_stop ,),
222+ daemon = True ,
223+ name = "BatteryPoll" ,
128224 ).start ()
129225
130- def _battery_poll_loop (self ):
131- """Read battery on connect then every 5 minutes while connected."""
132- import time
133- time .sleep (1 ) # brief settle after connect
134- stop = self ._battery_poll_stop
135- while not stop .is_set ():
226+ def _battery_poll_loop (self , stop_event ):
227+ """Read battery on connect and refresh it periodically until disconnected."""
228+ if stop_event .wait (1 ):
229+ return
230+ while not stop_event .is_set ():
136231 hg = self .hook ._hid_gesture
137232 if hg :
138233 level = hg .read_battery ()
234+ if stop_event .is_set ():
235+ return
139236 if level is not None and self ._battery_read_cb :
140237 try :
141238 self ._battery_read_cb (level )
142239 except Exception :
143240 pass
144- if stop .wait (300 ): # 5 minutes between polls; exits immediately if stopped
145- break
241+ if stop_event .wait (300 ):
242+ return
146243
147244 def set_battery_callback (self , cb ):
148245 """Register ``cb(level: int)`` invoked when battery level is read (0-100)."""
@@ -156,6 +253,10 @@ def set_connection_change_callback(self, cb):
156253 def device_connected (self ):
157254 return self .hook .device_connected
158255
256+ @property
257+ def enabled (self ):
258+ return self ._enabled
259+
159260 # ------------------------------------------------------------------
160261 # Public API
161262 # ------------------------------------------------------------------
@@ -180,15 +281,15 @@ def reload_mappings(self):
180281 self ._current_profile = self .cfg .get ("active_profile" , "default" )
181282 self .hook .reset_bindings ()
182283 self ._setup_hooks ()
284+ self ._emit_debug (f"reload_mappings profile={ self ._current_profile } " )
183285
184286 def set_enabled (self , enabled ):
185- self ._enabled = enabled
287+ self ._enabled = bool ( enabled )
186288
187289 def start (self ):
188290 self .hook .start ()
189291 self ._app_detector .start ()
190292 # Read current DPI from device on startup (don't overwrite it)
191- self ._dpi_read_cb = None # UI callback for initial DPI
192293 def _read_dpi ():
193294 import time
194295 time .sleep (3 ) # give HID++ time to connect
@@ -210,6 +311,6 @@ def set_dpi_read_callback(self, cb):
210311 self ._dpi_read_cb = cb
211312
212313 def stop (self ):
314+ self ._battery_poll_stop .set ()
213315 self ._app_detector .stop ()
214316 self .hook .stop ()
215-
0 commit comments