keybinds 1.3.4__tar.gz → 1.3.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {keybinds-1.3.4 → keybinds-1.3.5}/PKG-INFO +31 -16
  2. {keybinds-1.3.4 → keybinds-1.3.5}/README.md +30 -15
  3. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/__init__.py +4 -2
  4. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_backend.py +17 -7
  5. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_constants.py +9 -0
  6. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_mouse.py +118 -3
  7. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/types.py +25 -0
  8. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/winput/winput.py +3 -1
  9. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds.egg-info/PKG-INFO +31 -16
  10. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds.egg-info/SOURCES.txt +1 -0
  11. {keybinds-1.3.4 → keybinds-1.3.5}/pyproject.toml +1 -1
  12. keybinds-1.3.5/tests/test_mouse_wheel.py +177 -0
  13. {keybinds-1.3.4 → keybinds-1.3.5}/LICENSE +0 -0
  14. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_async.py +0 -0
  15. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_base_bind.py +0 -0
  16. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_bind_registry.py +0 -0
  17. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_dispatcher.py +0 -0
  18. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_hook.py +0 -0
  19. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_keyboard.py +0 -0
  20. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_parsing.py +0 -0
  21. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_state.py +0 -0
  22. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/_window.py +0 -0
  23. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/bind.py +0 -0
  24. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/decorators.py +0 -0
  25. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/diagnostics/__init__.py +0 -0
  26. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/diagnostics/analysis.py +0 -0
  27. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/diagnostics/core.py +0 -0
  28. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/diagnostics/models.py +0 -0
  29. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/diagnostics/reporting.py +0 -0
  30. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/diagnostics/tracing.py +0 -0
  31. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/logical/__init__.py +0 -0
  32. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/logical/abbreviation.py +0 -0
  33. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/logical/keyboard.py +0 -0
  34. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/logical/parsing.py +0 -0
  35. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/logical/translate.py +0 -0
  36. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/presets.py +0 -0
  37. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/simple.py +0 -0
  38. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/winput/LICENSE +0 -0
  39. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/winput/__init__.py +0 -0
  40. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds/winput/vk_codes.py +0 -0
  41. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds.egg-info/dependency_links.txt +0 -0
  42. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds.egg-info/requires.txt +0 -0
  43. {keybinds-1.3.4 → keybinds-1.3.5}/keybinds.egg-info/top_level.txt +0 -0
  44. {keybinds-1.3.4 → keybinds-1.3.5}/setup.cfg +0 -0
  45. {keybinds-1.3.4 → keybinds-1.3.5}/setup.py +0 -0
  46. {keybinds-1.3.4 → keybinds-1.3.5}/tests/test_bind_registry.py +0 -0
  47. {keybinds-1.3.4 → keybinds-1.3.5}/tests/test_bind_wait_and_is_pressed.py +0 -0
  48. {keybinds-1.3.4 → keybinds-1.3.5}/tests/test_decorators_and_simple.py +0 -0
  49. {keybinds-1.3.4 → keybinds-1.3.5}/tests/test_diagnostics.py +0 -0
  50. {keybinds-1.3.4 → keybinds-1.3.5}/tests/test_logical_parsing.py +0 -0
  51. {keybinds-1.3.4 → keybinds-1.3.5}/tests/test_parsing.py +0 -0
  52. {keybinds-1.3.4 → keybinds-1.3.5}/tests/test_runtime_bindings.py +0 -0
  53. {keybinds-1.3.4 → keybinds-1.3.5}/tests/test_runtime_edge_cases.py +0 -0
  54. {keybinds-1.3.4 → keybinds-1.3.5}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keybinds
3
- Version: 1.3.4
3
+ Version: 1.3.5
4
4
  Summary: Flexible global keyboard & mouse hotkeys for Windows
5
5
  Author: echolane
6
6
  License: MIT License
@@ -55,14 +55,14 @@ Flexible global keyboard & mouse hotkeys for Windows.
55
55
  ![Python](https://img.shields.io/badge/python-3.7%2B-blue)
56
56
  ![Platform](https://img.shields.io/badge/platform-windows-lightgrey)
57
57
  ![License](https://img.shields.io/badge/license-MIT-green)
58
- ![Status](https://img.shields.io/badge/status-maintenance-yellow)
58
+ ![Status](https://img.shields.io/badge/status-active-success)
59
59
  ![PyPI](https://img.shields.io/pypi/v/keybinds)
60
60
 
61
61
  **keybinds** is a Python library for global keyboard and mouse binds on Windows, with support for hotkeys, logical key binds, text matching, and text abbreviations.
62
62
 
63
63
  It supports:
64
64
  - global keyboard binds: single keys, chords (`ctrl+e`), and sequences (`g,k,i`)
65
- - global mouse binds: `left`, `right`, `middle`, `x1`, `x2`
65
+ - global mouse binds: `left`, `right`, `middle`, `x1`, `x2`, `wheel_up`, `wheel_down`
66
66
  - rich triggers: press, release, click, hold, repeat, double tap, and sequence
67
67
  - suppression and injected-input policies
68
68
  - sync and async callbacks
@@ -108,7 +108,7 @@ Stable decorator helpers:
108
108
 
109
109
  ```python
110
110
  import keybinds
111
- from keybinds.decorators import bind_key, bind_mouse
111
+ from keybinds import bind_key, bind_mouse
112
112
 
113
113
  @bind_key("ctrl+e")
114
114
  def inventory():
@@ -127,7 +127,7 @@ Decorators use the default hook automatically.
127
127
 
128
128
  ```python
129
129
  import keybinds
130
- from keybinds.decorators import bind_logical, bind_text, bind_abbreviation
130
+ from keybinds import bind_logical, bind_text, bind_abbreviation
131
131
 
132
132
  # Experimental API: may change in future releases.
133
133
 
@@ -227,7 +227,7 @@ hook.bind("ctrl+e", lambda: print("Pressed"))
227
227
  ### Release
228
228
 
229
229
  ```python
230
- from keybinds.types import BindConfig, Trigger
230
+ from keybinds import BindConfig, Trigger
231
231
 
232
232
  hook.bind(
233
233
  "ctrl+t",
@@ -239,7 +239,7 @@ hook.bind(
239
239
  ### Hold
240
240
 
241
241
  ```python
242
- from keybinds.types import BindConfig, Trigger, Timing
242
+ from keybinds import BindConfig, Trigger, Timing
243
243
 
244
244
  hook.bind(
245
245
  "h",
@@ -254,7 +254,7 @@ hook.bind(
254
254
  ### Repeat (auto-fire while held)
255
255
 
256
256
  ```python
257
- from keybinds.types import BindConfig, Trigger, Timing
257
+ from keybinds import BindConfig, Trigger, Timing
258
258
 
259
259
  hook.bind(
260
260
  "space",
@@ -269,7 +269,7 @@ hook.bind(
269
269
  ### Double tap
270
270
 
271
271
  ```python
272
- from keybinds.types import BindConfig, Trigger
272
+ from keybinds import BindConfig, Trigger
273
273
 
274
274
  hook.bind(
275
275
  "g",
@@ -281,7 +281,7 @@ hook.bind(
281
281
  ### Sequence
282
282
 
283
283
  ```python
284
- from keybinds.types import BindConfig, Trigger
284
+ from keybinds import BindConfig, Trigger
285
285
 
286
286
  hook.bind(
287
287
  "g,k,i",
@@ -295,7 +295,7 @@ hook.bind(
295
295
  ## Mouse Basics
296
296
 
297
297
  ```python
298
- from keybinds.types import MouseBindConfig, Trigger
298
+ from keybinds import MouseBindConfig, Trigger
299
299
 
300
300
  hook.bind_mouse(
301
301
  "middle",
@@ -312,12 +312,28 @@ Mouse buttons:
312
312
  - `x1`
313
313
  - `x2`
314
314
 
315
+ Mouse wheel (only `Trigger.ON_PRESS`):
316
+
317
+ - `wheel_up` / `wheel_down`
318
+ - `wheel_left` / `wheel_right` — horizontal (many mice never emit these)
319
+
320
+ Wheel units accumulate until `MouseBindConfig.wheel_delta` (default `120`, the Win32 notch). Smaller wheel steps add up until they reach `wheel_delta`. Scrolling the other way clears the leftover amount.
321
+
322
+ ```python
323
+ hook.bind_mouse("wheel_up", lambda: print("scroll up"))
324
+ hook.bind_mouse(
325
+ "wheel_down",
326
+ lambda: print("scroll down"),
327
+ config=MouseBindConfig(wheel_delta=40),
328
+ )
329
+ ```
330
+
315
331
  ---
316
332
 
317
333
  ## Suppression (block input from apps)
318
334
 
319
335
  ```python
320
- from keybinds.types import BindConfig, SuppressPolicy
336
+ from keybinds import BindConfig, SuppressPolicy
321
337
 
322
338
  hook.bind(
323
339
  "ctrl+r",
@@ -341,7 +357,7 @@ Policies:
341
357
  Control how injected input (macros, `SendInput`, automation tools) is handled:
342
358
 
343
359
  ```python
344
- from keybinds.types import BindConfig, InjectedPolicy
360
+ from keybinds import BindConfig, InjectedPolicy
345
361
 
346
362
  hook.bind(
347
363
  "f1",
@@ -361,7 +377,7 @@ Policies:
361
377
  ## Strict Chords
362
378
 
363
379
  ```python
364
- from keybinds.types import BindConfig, Constraints, ChordPolicy
380
+ from keybinds import BindConfig, Constraints, ChordPolicy
365
381
 
366
382
  hook.bind(
367
383
  "ctrl+shift+u",
@@ -397,8 +413,7 @@ Callbacks may be `async def`. If a callback returns an awaitable, **keybinds** s
397
413
 
398
414
  ```python
399
415
  import asyncio
400
- from keybinds import Hook
401
- from keybinds.decorators import bind_key
416
+ from keybinds import Hook, bind_key
402
417
 
403
418
  hook = Hook()
404
419
 
@@ -5,14 +5,14 @@ Flexible global keyboard & mouse hotkeys for Windows.
5
5
  ![Python](https://img.shields.io/badge/python-3.7%2B-blue)
6
6
  ![Platform](https://img.shields.io/badge/platform-windows-lightgrey)
7
7
  ![License](https://img.shields.io/badge/license-MIT-green)
8
- ![Status](https://img.shields.io/badge/status-maintenance-yellow)
8
+ ![Status](https://img.shields.io/badge/status-active-success)
9
9
  ![PyPI](https://img.shields.io/pypi/v/keybinds)
10
10
 
11
11
  **keybinds** is a Python library for global keyboard and mouse binds on Windows, with support for hotkeys, logical key binds, text matching, and text abbreviations.
12
12
 
13
13
  It supports:
14
14
  - global keyboard binds: single keys, chords (`ctrl+e`), and sequences (`g,k,i`)
15
- - global mouse binds: `left`, `right`, `middle`, `x1`, `x2`
15
+ - global mouse binds: `left`, `right`, `middle`, `x1`, `x2`, `wheel_up`, `wheel_down`
16
16
  - rich triggers: press, release, click, hold, repeat, double tap, and sequence
17
17
  - suppression and injected-input policies
18
18
  - sync and async callbacks
@@ -58,7 +58,7 @@ Stable decorator helpers:
58
58
 
59
59
  ```python
60
60
  import keybinds
61
- from keybinds.decorators import bind_key, bind_mouse
61
+ from keybinds import bind_key, bind_mouse
62
62
 
63
63
  @bind_key("ctrl+e")
64
64
  def inventory():
@@ -77,7 +77,7 @@ Decorators use the default hook automatically.
77
77
 
78
78
  ```python
79
79
  import keybinds
80
- from keybinds.decorators import bind_logical, bind_text, bind_abbreviation
80
+ from keybinds import bind_logical, bind_text, bind_abbreviation
81
81
 
82
82
  # Experimental API: may change in future releases.
83
83
 
@@ -177,7 +177,7 @@ hook.bind("ctrl+e", lambda: print("Pressed"))
177
177
  ### Release
178
178
 
179
179
  ```python
180
- from keybinds.types import BindConfig, Trigger
180
+ from keybinds import BindConfig, Trigger
181
181
 
182
182
  hook.bind(
183
183
  "ctrl+t",
@@ -189,7 +189,7 @@ hook.bind(
189
189
  ### Hold
190
190
 
191
191
  ```python
192
- from keybinds.types import BindConfig, Trigger, Timing
192
+ from keybinds import BindConfig, Trigger, Timing
193
193
 
194
194
  hook.bind(
195
195
  "h",
@@ -204,7 +204,7 @@ hook.bind(
204
204
  ### Repeat (auto-fire while held)
205
205
 
206
206
  ```python
207
- from keybinds.types import BindConfig, Trigger, Timing
207
+ from keybinds import BindConfig, Trigger, Timing
208
208
 
209
209
  hook.bind(
210
210
  "space",
@@ -219,7 +219,7 @@ hook.bind(
219
219
  ### Double tap
220
220
 
221
221
  ```python
222
- from keybinds.types import BindConfig, Trigger
222
+ from keybinds import BindConfig, Trigger
223
223
 
224
224
  hook.bind(
225
225
  "g",
@@ -231,7 +231,7 @@ hook.bind(
231
231
  ### Sequence
232
232
 
233
233
  ```python
234
- from keybinds.types import BindConfig, Trigger
234
+ from keybinds import BindConfig, Trigger
235
235
 
236
236
  hook.bind(
237
237
  "g,k,i",
@@ -245,7 +245,7 @@ hook.bind(
245
245
  ## Mouse Basics
246
246
 
247
247
  ```python
248
- from keybinds.types import MouseBindConfig, Trigger
248
+ from keybinds import MouseBindConfig, Trigger
249
249
 
250
250
  hook.bind_mouse(
251
251
  "middle",
@@ -262,12 +262,28 @@ Mouse buttons:
262
262
  - `x1`
263
263
  - `x2`
264
264
 
265
+ Mouse wheel (only `Trigger.ON_PRESS`):
266
+
267
+ - `wheel_up` / `wheel_down`
268
+ - `wheel_left` / `wheel_right` — horizontal (many mice never emit these)
269
+
270
+ Wheel units accumulate until `MouseBindConfig.wheel_delta` (default `120`, the Win32 notch). Smaller wheel steps add up until they reach `wheel_delta`. Scrolling the other way clears the leftover amount.
271
+
272
+ ```python
273
+ hook.bind_mouse("wheel_up", lambda: print("scroll up"))
274
+ hook.bind_mouse(
275
+ "wheel_down",
276
+ lambda: print("scroll down"),
277
+ config=MouseBindConfig(wheel_delta=40),
278
+ )
279
+ ```
280
+
265
281
  ---
266
282
 
267
283
  ## Suppression (block input from apps)
268
284
 
269
285
  ```python
270
- from keybinds.types import BindConfig, SuppressPolicy
286
+ from keybinds import BindConfig, SuppressPolicy
271
287
 
272
288
  hook.bind(
273
289
  "ctrl+r",
@@ -291,7 +307,7 @@ Policies:
291
307
  Control how injected input (macros, `SendInput`, automation tools) is handled:
292
308
 
293
309
  ```python
294
- from keybinds.types import BindConfig, InjectedPolicy
310
+ from keybinds import BindConfig, InjectedPolicy
295
311
 
296
312
  hook.bind(
297
313
  "f1",
@@ -311,7 +327,7 @@ Policies:
311
327
  ## Strict Chords
312
328
 
313
329
  ```python
314
- from keybinds.types import BindConfig, Constraints, ChordPolicy
330
+ from keybinds import BindConfig, Constraints, ChordPolicy
315
331
 
316
332
  hook.bind(
317
333
  "ctrl+shift+u",
@@ -347,8 +363,7 @@ Callbacks may be `async def`. If a callback returns an awaitable, **keybinds** s
347
363
 
348
364
  ```python
349
365
  import asyncio
350
- from keybinds import Hook
351
- from keybinds.decorators import bind_key
366
+ from keybinds import Hook, bind_key
352
367
 
353
368
  hook = Hook()
354
369
 
@@ -21,6 +21,8 @@ from .types import (
21
21
  Checks,
22
22
  BindConfig,
23
23
  MouseButton,
24
+ WHEEL_BUTTONS,
25
+ is_wheel_button,
24
26
  MouseBindConfig,
25
27
  LogicalConfig,
26
28
  Callback,
@@ -36,12 +38,12 @@ from ._backend import reinstall_hooks, rehook
36
38
 
37
39
  from ._constants import register_key_token
38
40
 
39
- __version__ = "1.3.4"
41
+ __version__ = "1.3.5"
40
42
 
41
43
  __all__ = [
42
44
  "Trigger","SuppressPolicy","ChordPolicy","OrderPolicy","FocusPolicy","InjectedPolicy","TextBoundaryPolicy","TextBackspacePolicy","OsKeyRepeatPolicy","ReplacementPolicy",
43
45
  "Timing","Constraints","Checks","BindConfig",
44
- "MouseButton","MouseBindConfig","Callback","Predicate","LogicalConfig",
46
+ "MouseButton","WHEEL_BUTTONS","is_wheel_button","MouseBindConfig","Callback","Predicate","LogicalConfig",
45
47
  "Bind","LogicalBind","TextAbbreviationBind","KeyboardBind","MouseBind","Hook","get_default_hook","set_default_hook","join","unbind",
46
48
  "bind_key","bind_logical","bind_text","add_abbreviation","bind_abbreviation","bind_mouse","presets","register_key_token","winput",
47
49
  "reinstall_hooks","rehook"
@@ -15,6 +15,8 @@ from ._constants import (
15
15
  WM_RBUTTONDOWN, WM_RBUTTONUP,
16
16
  WM_MBUTTONDOWN, WM_MBUTTONUP,
17
17
  WM_XBUTTONDOWN, WM_XBUTTONUP,
18
+ MOUSE_BUTTON_ACTIONS,
19
+ MOUSE_WHEEL_ACTIONS,
18
20
  )
19
21
  from .types import MouseButton
20
22
  from ._state import InputState
@@ -291,14 +293,22 @@ class _GlobalBackend:
291
293
  return flags
292
294
 
293
295
  def _on_mouse(self, event: winput.MouseEvent) -> int:
294
- # Hard filter: only handle button up/down (everything else is noise).
295
296
  act = event.action
296
- if act not in (
297
- WM_LBUTTONDOWN, WM_LBUTTONUP,
298
- WM_RBUTTONDOWN, WM_RBUTTONUP,
299
- WM_MBUTTONDOWN, WM_MBUTTONUP,
300
- WM_XBUTTONDOWN, WM_XBUTTONUP,
301
- ):
297
+
298
+ # Wheel is a pulse, not a pressed button. Dispatch without touching
299
+ # pressed_mouse so hold/is_pressed for real buttons stay correct.
300
+ if act in MOUSE_WHEEL_ACTIONS:
301
+ state = self.current_state_snapshot()
302
+ flags = winput.WP_CONTINUE
303
+ for h in self._alive_hooks():
304
+ try:
305
+ flags |= h._handle_mouse_event(event, state)
306
+ except Exception:
307
+ print_exc()
308
+ return flags
309
+
310
+ # Hard filter: only handle button up/down (move is noise).
311
+ if act not in MOUSE_BUTTON_ACTIONS:
302
312
  return winput.WP_CONTINUE
303
313
 
304
314
  injected = bool(getattr(event, "injected", False))
@@ -40,6 +40,15 @@ WM_SYSKEYUP = winput.WM_SYSKEYUP
40
40
  WM_MOUSEMOVE = getattr(winput, "WM_MOUSEMOVE", 0x0200)
41
41
  WM_MOUSEWHEEL = getattr(winput, "WM_MOUSEWHEEL", 0x020A)
42
42
  WM_MOUSEHWHEEL = getattr(winput, "WM_MOUSEHWHEEL", 0x020E)
43
+ WHEEL_DELTA = getattr(winput, "WHEEL_DELTA", 120)
44
+
45
+ MOUSE_BUTTON_ACTIONS = frozenset({
46
+ WM_LBUTTONDOWN, WM_LBUTTONUP,
47
+ WM_RBUTTONDOWN, WM_RBUTTONUP,
48
+ WM_MBUTTONDOWN, WM_MBUTTONUP,
49
+ WM_XBUTTONDOWN, WM_XBUTTONUP,
50
+ })
51
+ MOUSE_WHEEL_ACTIONS = frozenset({WM_MOUSEWHEEL, WM_MOUSEHWHEEL})
43
52
 
44
53
  KEYEVENTF_KEYUP = getattr(winput, "KEYEVENTF_KEYUP", 0x0002)
45
54
  KEYEVENTF_UNICODE = getattr(winput, "KEYEVENTF_UNICODE", 0x0004)
@@ -6,12 +6,22 @@ from typing import Callable, Optional, Union, Tuple
6
6
 
7
7
  from . import winput
8
8
 
9
- from .types import Callback, MouseBindConfig, MouseButton, SuppressPolicy, InjectedPolicy, Trigger
9
+ from .types import (
10
+ Callback,
11
+ MouseBindConfig,
12
+ MouseButton,
13
+ SuppressPolicy,
14
+ InjectedPolicy,
15
+ Trigger,
16
+ is_wheel_button,
17
+ )
10
18
  from ._constants import (
11
19
  WM_LBUTTONDOWN, WM_LBUTTONUP,
12
20
  WM_RBUTTONDOWN, WM_RBUTTONUP,
13
21
  WM_MBUTTONDOWN, WM_MBUTTONUP,
14
22
  WM_XBUTTONDOWN, WM_XBUTTONUP,
23
+ WM_MOUSEWHEEL, WM_MOUSEHWHEEL,
24
+ MOUSE_WHEEL_ACTIONS,
15
25
  )
16
26
  from .diagnostics import _DiagnosticsManager, _EventTrace
17
27
  from ._base_bind import BaseBind
@@ -22,7 +32,7 @@ def _normalize_mouse_button(btn: object) -> MouseButton:
22
32
  if isinstance(btn, MouseButton):
23
33
  return btn
24
34
  if isinstance(btn, str):
25
- s = btn.strip().lower()
35
+ s = btn.strip().lower().replace("-", "_")
26
36
  aliases = {
27
37
  "left": MouseButton.LEFT,
28
38
  "lmb": MouseButton.LEFT,
@@ -34,6 +44,20 @@ def _normalize_mouse_button(btn: object) -> MouseButton:
34
44
  "mouse4": MouseButton.X1,
35
45
  "x2": MouseButton.X2,
36
46
  "mouse5": MouseButton.X2,
47
+ "wheelup": MouseButton.WHEEL_UP,
48
+ "wheel_up": MouseButton.WHEEL_UP,
49
+ "scrollup": MouseButton.WHEEL_UP,
50
+ "scroll_up": MouseButton.WHEEL_UP,
51
+ "wheeldown": MouseButton.WHEEL_DOWN,
52
+ "wheel_down": MouseButton.WHEEL_DOWN,
53
+ "scrolldown": MouseButton.WHEEL_DOWN,
54
+ "scroll_down": MouseButton.WHEEL_DOWN,
55
+ "wheelleft": MouseButton.WHEEL_LEFT,
56
+ "wheel_left": MouseButton.WHEEL_LEFT,
57
+ "hwheel_left": MouseButton.WHEEL_LEFT,
58
+ "wheelright": MouseButton.WHEEL_RIGHT,
59
+ "wheel_right": MouseButton.WHEEL_RIGHT,
60
+ "hwheel_right": MouseButton.WHEEL_RIGHT,
37
61
  }
38
62
  if s in aliases:
39
63
  return aliases[s]
@@ -53,6 +77,11 @@ class MouseBind(BaseBind[winput.MouseEvent]):
53
77
  ) -> None:
54
78
  super().__init__(callback, config=config or MouseBindConfig(), hwnd=hwnd, dispatch=dispatch, diagnostics=diagnostics)
55
79
  self.button = _normalize_mouse_button(button)
80
+ if is_wheel_button(self.button) and self.config.trigger != Trigger.ON_PRESS:
81
+ raise ValueError(
82
+ f"mouse wheel bind {self.button.name.lower()!r} only supports "
83
+ f"Trigger.ON_PRESS, got {self.config.trigger!r}"
84
+ )
56
85
  self._set_diagnostics_identity(self.button.name.lower(), "mouse")
57
86
 
58
87
  self._down_ms: Optional[int] = None
@@ -60,6 +89,7 @@ class MouseBind(BaseBind[winput.MouseEvent]):
60
89
  self._tap_count: int = 0
61
90
  self._tap_last_ms: int = 0
62
91
  self._armed: bool = False
92
+ self._wheel_acc: int = 0
63
93
 
64
94
  def _checks_ok(self, event: winput.MouseEvent, state: InputState, trace: Optional[_EventTrace] = None) -> bool:
65
95
  return super()._checks_ok(event, state, trace=trace)
@@ -86,6 +116,34 @@ class MouseBind(BaseBind[winput.MouseEvent]):
86
116
  return which == 2
87
117
  return True
88
118
 
119
+ def _wheel_contribution(self, event: winput.MouseEvent) -> Optional[int]:
120
+ """Return signed units toward this bind, 0 on same-axis reversal, None if unrelated."""
121
+ act = event.action
122
+ if act not in MOUSE_WHEEL_ACTIONS:
123
+ return None
124
+ try:
125
+ delta = int(getattr(event, "additional_data", 0) or 0)
126
+ except Exception:
127
+ delta = 0
128
+ if delta == 0:
129
+ return None
130
+
131
+ if act == WM_MOUSEWHEEL:
132
+ if self.button == MouseButton.WHEEL_UP:
133
+ return delta if delta > 0 else 0
134
+ if self.button == MouseButton.WHEEL_DOWN:
135
+ return -delta if delta < 0 else 0
136
+ return None
137
+
138
+ if act == WM_MOUSEHWHEEL:
139
+ if self.button == MouseButton.WHEEL_RIGHT:
140
+ return delta if delta > 0 else 0
141
+ if self.button == MouseButton.WHEEL_LEFT:
142
+ return -delta if delta < 0 else 0
143
+ return None
144
+
145
+ return None
146
+
89
147
  def reset(self) -> None:
90
148
  self._down_ms = None
91
149
  self._press_suppress_up = False
@@ -93,6 +151,60 @@ class MouseBind(BaseBind[winput.MouseEvent]):
93
151
  self._tap_last_ms = 0
94
152
  self._hold_token += 1
95
153
  self._armed = False
154
+ self._wheel_acc = 0
155
+
156
+ def _handle_wheel(self, event: winput.MouseEvent, now_ms: int, trace: _EventTrace) -> int:
157
+ contrib = self._wheel_contribution(event)
158
+ if contrib is None:
159
+ return winput.WP_CONTINUE
160
+
161
+ if contrib == 0:
162
+ if self._wheel_acc:
163
+ trace.note("decision", "wheel_accumulator_reset", previous_acc=self._wheel_acc)
164
+ self._wheel_acc = 0
165
+ return winput.WP_CONTINUE
166
+
167
+ self._wheel_acc += contrib
168
+ quantum = int(self.config.wheel_delta) # type: ignore
169
+ # too lazy to fix this typing issue now
170
+
171
+ fired = 0
172
+ flags = winput.WP_CONTINUE
173
+ sup = self.config.suppress
174
+ trig_name = self.config.trigger.name.lower()
175
+
176
+ trace.note(
177
+ "decision",
178
+ "wheel_tick",
179
+ button=self.button.name.lower(),
180
+ delta=contrib,
181
+ accumulated=self._wheel_acc,
182
+ quantum=quantum,
183
+ )
184
+
185
+ while self._wheel_acc >= quantum:
186
+ if not self.can_fire_now(now_ms, trace=trace):
187
+ break
188
+ self._wheel_acc -= quantum
189
+ self._fires += 1
190
+ self._last_fire_ms = now_ms
191
+ self._fire(trace.fire(trigger=trig_name))
192
+ fired += 1
193
+
194
+ if fired:
195
+ trace.note("decision", "wheel_notches_fired", notches=fired, remainder=self._wheel_acc)
196
+
197
+ if sup == SuppressPolicy.ALWAYS:
198
+ flags |= winput.WP_DONT_PASS_INPUT_ON
199
+ trace.suppress("suppressed_always")
200
+ elif fired and sup == SuppressPolicy.WHEN_MATCHED:
201
+ flags |= winput.WP_DONT_PASS_INPUT_ON
202
+ trace.suppress("suppressed_when_matched", trigger=trig_name)
203
+ elif fired and sup in (SuppressPolicy.WHILE_ACTIVE, SuppressPolicy.WHILE_EVALUATING):
204
+ flags |= winput.WP_DONT_PASS_INPUT_ON
205
+ trace.suppress("suppressed_while_active" if sup == SuppressPolicy.WHILE_ACTIVE else "suppressed_while_evaluating")
206
+
207
+ return flags
96
208
 
97
209
  def _get_pressed_for_policy(self, state: InputState, *, inj: bool) -> set[MouseButton]:
98
210
  pol = self.config.injected
@@ -132,7 +244,7 @@ class MouseBind(BaseBind[winput.MouseEvent]):
132
244
  return [self.button] if self.is_pressed() else []
133
245
 
134
246
  def handle(self, event: winput.MouseEvent, state: InputState) -> int:
135
- # Mouse move/wheel is extremely frequent; this is called only after Hook filtered.
247
+ # Move is filtered in the backend. Wheel and button events reach here.
136
248
  with self._lock:
137
249
  trace = self._trace(event)
138
250
  now_ms = int(event.time)
@@ -153,6 +265,9 @@ class MouseBind(BaseBind[winput.MouseEvent]):
153
265
  trace.skip("injected_only_but_physical")
154
266
  return winput.WP_CONTINUE
155
267
 
268
+ if is_wheel_button(self.button):
269
+ return self._handle_wheel(event, now_ms, trace)
270
+
156
271
  down_act, up_act = self._actions_for_button()
157
272
  is_down = event.action == down_act
158
273
  is_up = event.action == up_act
@@ -203,6 +203,22 @@ class MouseButton(Enum):
203
203
  MIDDLE = auto()
204
204
  X1 = auto()
205
205
  X2 = auto()
206
+ WHEEL_UP = auto()
207
+ WHEEL_DOWN = auto()
208
+ WHEEL_LEFT = auto()
209
+ WHEEL_RIGHT = auto()
210
+
211
+
212
+ WHEEL_BUTTONS = frozenset({
213
+ MouseButton.WHEEL_UP,
214
+ MouseButton.WHEEL_DOWN,
215
+ MouseButton.WHEEL_LEFT,
216
+ MouseButton.WHEEL_RIGHT,
217
+ })
218
+
219
+
220
+ def is_wheel_button(button: MouseButton) -> bool:
221
+ return button in WHEEL_BUTTONS
206
222
 
207
223
 
208
224
  @dataclass(frozen=True)
@@ -216,9 +232,15 @@ class MouseBindConfig:
216
232
  timing: Timing = field(default_factory=Timing)
217
233
  constraints: Constraints = field(default_factory=Constraints)
218
234
  checks: Checks = field(default_factory=Checks)
235
+ # Units accumulated before one wheel callback. Win32 notch is 120.
236
+ wheel_delta: int = 120
219
237
 
220
238
  def __post_init__(self):
221
239
  object.__setattr__(self, "checks", Checks.coerce(self.checks))
240
+ delta = int(self.wheel_delta)
241
+ if delta < 1:
242
+ raise ValueError("wheel_delta must be >= 1")
243
+ object.__setattr__(self, "wheel_delta", delta)
222
244
 
223
245
  def soft_merge(self, patch: MouseBindConfig) -> MouseBindConfig:
224
246
  """Apply only non-default fields from `patch` (patch semantics)."""
@@ -328,6 +350,9 @@ def _merge_soft(lhs: TBind, rhs: TBind) -> TBind:
328
350
  if rhs.focus != default_bind.focus:
329
351
  out = replace(out, focus=rhs.focus)
330
352
 
353
+ if isinstance(out, MouseBindConfig) and rhs.wheel_delta != default_bind.wheel_delta:
354
+ out = replace(out, wheel_delta=rhs.wheel_delta)
355
+
331
356
  out = replace(out, timing=_merge_dc_soft(out.timing, rhs.timing, _DEFAULT_TIMING))
332
357
  out = replace(out, constraints=_merge_dc_soft(out.constraints, rhs.constraints, _DEFAULT_CONSTRAINTS))
333
358
  out = replace(out, checks=_merge_dc_soft(out.checks, rhs.checks, _DEFAULT_CHECKS))
@@ -352,7 +352,9 @@ def _LowLevelMouseProc(nCode : int, wParam : int, lParam : int, cbfunc : Callabl
352
352
  extra_data = GET_HWORD(ms.mouseData)
353
353
 
354
354
  elif wParam == WM_MOUSEWHEEL or wParam == WM_MOUSEHWHEEL: # used scrollwheel
355
- extra_data = GET_HWORD(ms.mouseData) // WHEEL_DELTA
355
+ # Raw signed HIWORD. WHEEL_DELTA (120) is the notch quantum at the bind layer;
356
+ # dividing here would drop high-res / touchpad packets smaller than 120.
357
+ extra_data = GET_HWORD(ms.mouseData)
356
358
 
357
359
  result = cbfunc(MouseEvent(
358
360
  (ms.pt.x, ms.pt.y),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keybinds
3
- Version: 1.3.4
3
+ Version: 1.3.5
4
4
  Summary: Flexible global keyboard & mouse hotkeys for Windows
5
5
  Author: echolane
6
6
  License: MIT License
@@ -55,14 +55,14 @@ Flexible global keyboard & mouse hotkeys for Windows.
55
55
  ![Python](https://img.shields.io/badge/python-3.7%2B-blue)
56
56
  ![Platform](https://img.shields.io/badge/platform-windows-lightgrey)
57
57
  ![License](https://img.shields.io/badge/license-MIT-green)
58
- ![Status](https://img.shields.io/badge/status-maintenance-yellow)
58
+ ![Status](https://img.shields.io/badge/status-active-success)
59
59
  ![PyPI](https://img.shields.io/pypi/v/keybinds)
60
60
 
61
61
  **keybinds** is a Python library for global keyboard and mouse binds on Windows, with support for hotkeys, logical key binds, text matching, and text abbreviations.
62
62
 
63
63
  It supports:
64
64
  - global keyboard binds: single keys, chords (`ctrl+e`), and sequences (`g,k,i`)
65
- - global mouse binds: `left`, `right`, `middle`, `x1`, `x2`
65
+ - global mouse binds: `left`, `right`, `middle`, `x1`, `x2`, `wheel_up`, `wheel_down`
66
66
  - rich triggers: press, release, click, hold, repeat, double tap, and sequence
67
67
  - suppression and injected-input policies
68
68
  - sync and async callbacks
@@ -108,7 +108,7 @@ Stable decorator helpers:
108
108
 
109
109
  ```python
110
110
  import keybinds
111
- from keybinds.decorators import bind_key, bind_mouse
111
+ from keybinds import bind_key, bind_mouse
112
112
 
113
113
  @bind_key("ctrl+e")
114
114
  def inventory():
@@ -127,7 +127,7 @@ Decorators use the default hook automatically.
127
127
 
128
128
  ```python
129
129
  import keybinds
130
- from keybinds.decorators import bind_logical, bind_text, bind_abbreviation
130
+ from keybinds import bind_logical, bind_text, bind_abbreviation
131
131
 
132
132
  # Experimental API: may change in future releases.
133
133
 
@@ -227,7 +227,7 @@ hook.bind("ctrl+e", lambda: print("Pressed"))
227
227
  ### Release
228
228
 
229
229
  ```python
230
- from keybinds.types import BindConfig, Trigger
230
+ from keybinds import BindConfig, Trigger
231
231
 
232
232
  hook.bind(
233
233
  "ctrl+t",
@@ -239,7 +239,7 @@ hook.bind(
239
239
  ### Hold
240
240
 
241
241
  ```python
242
- from keybinds.types import BindConfig, Trigger, Timing
242
+ from keybinds import BindConfig, Trigger, Timing
243
243
 
244
244
  hook.bind(
245
245
  "h",
@@ -254,7 +254,7 @@ hook.bind(
254
254
  ### Repeat (auto-fire while held)
255
255
 
256
256
  ```python
257
- from keybinds.types import BindConfig, Trigger, Timing
257
+ from keybinds import BindConfig, Trigger, Timing
258
258
 
259
259
  hook.bind(
260
260
  "space",
@@ -269,7 +269,7 @@ hook.bind(
269
269
  ### Double tap
270
270
 
271
271
  ```python
272
- from keybinds.types import BindConfig, Trigger
272
+ from keybinds import BindConfig, Trigger
273
273
 
274
274
  hook.bind(
275
275
  "g",
@@ -281,7 +281,7 @@ hook.bind(
281
281
  ### Sequence
282
282
 
283
283
  ```python
284
- from keybinds.types import BindConfig, Trigger
284
+ from keybinds import BindConfig, Trigger
285
285
 
286
286
  hook.bind(
287
287
  "g,k,i",
@@ -295,7 +295,7 @@ hook.bind(
295
295
  ## Mouse Basics
296
296
 
297
297
  ```python
298
- from keybinds.types import MouseBindConfig, Trigger
298
+ from keybinds import MouseBindConfig, Trigger
299
299
 
300
300
  hook.bind_mouse(
301
301
  "middle",
@@ -312,12 +312,28 @@ Mouse buttons:
312
312
  - `x1`
313
313
  - `x2`
314
314
 
315
+ Mouse wheel (only `Trigger.ON_PRESS`):
316
+
317
+ - `wheel_up` / `wheel_down`
318
+ - `wheel_left` / `wheel_right` — horizontal (many mice never emit these)
319
+
320
+ Wheel units accumulate until `MouseBindConfig.wheel_delta` (default `120`, the Win32 notch). Smaller wheel steps add up until they reach `wheel_delta`. Scrolling the other way clears the leftover amount.
321
+
322
+ ```python
323
+ hook.bind_mouse("wheel_up", lambda: print("scroll up"))
324
+ hook.bind_mouse(
325
+ "wheel_down",
326
+ lambda: print("scroll down"),
327
+ config=MouseBindConfig(wheel_delta=40),
328
+ )
329
+ ```
330
+
315
331
  ---
316
332
 
317
333
  ## Suppression (block input from apps)
318
334
 
319
335
  ```python
320
- from keybinds.types import BindConfig, SuppressPolicy
336
+ from keybinds import BindConfig, SuppressPolicy
321
337
 
322
338
  hook.bind(
323
339
  "ctrl+r",
@@ -341,7 +357,7 @@ Policies:
341
357
  Control how injected input (macros, `SendInput`, automation tools) is handled:
342
358
 
343
359
  ```python
344
- from keybinds.types import BindConfig, InjectedPolicy
360
+ from keybinds import BindConfig, InjectedPolicy
345
361
 
346
362
  hook.bind(
347
363
  "f1",
@@ -361,7 +377,7 @@ Policies:
361
377
  ## Strict Chords
362
378
 
363
379
  ```python
364
- from keybinds.types import BindConfig, Constraints, ChordPolicy
380
+ from keybinds import BindConfig, Constraints, ChordPolicy
365
381
 
366
382
  hook.bind(
367
383
  "ctrl+shift+u",
@@ -397,8 +413,7 @@ Callbacks may be `async def`. If a callback returns an awaitable, **keybinds** s
397
413
 
398
414
  ```python
399
415
  import asyncio
400
- from keybinds import Hook
401
- from keybinds.decorators import bind_key
416
+ from keybinds import Hook, bind_key
402
417
 
403
418
  hook = Hook()
404
419
 
@@ -45,6 +45,7 @@ tests/test_bind_wait_and_is_pressed.py
45
45
  tests/test_decorators_and_simple.py
46
46
  tests/test_diagnostics.py
47
47
  tests/test_logical_parsing.py
48
+ tests/test_mouse_wheel.py
48
49
  tests/test_parsing.py
49
50
  tests/test_runtime_bindings.py
50
51
  tests/test_runtime_edge_cases.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "keybinds"
7
- version = "1.3.4"
7
+ version = "1.3.5"
8
8
  description = "Flexible global keyboard & mouse hotkeys for Windows"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.7"
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+
6
+ def test_normalize_mouse_button_accepts_wheel_aliases(runtime_env):
7
+ normalize = runtime_env._mouse._normalize_mouse_button
8
+ MouseButton = runtime_env.types.MouseButton
9
+
10
+ assert normalize("wheel_up") is MouseButton.WHEEL_UP
11
+ assert normalize("scrollup") is MouseButton.WHEEL_UP
12
+ assert normalize("wheel-down") is MouseButton.WHEEL_DOWN
13
+ assert normalize("hwheel_left") is MouseButton.WHEEL_LEFT
14
+ assert normalize("hwheel_right") is MouseButton.WHEEL_RIGHT
15
+ assert normalize(MouseButton.WHEEL_UP) is MouseButton.WHEEL_UP
16
+
17
+ with pytest.raises(ValueError):
18
+ normalize("wheel")
19
+ with pytest.raises(ValueError):
20
+ normalize("tilt_left")
21
+
22
+
23
+ def test_wheel_bind_rejects_non_press_triggers(runtime_env):
24
+ MouseBindConfig = runtime_env.types.MouseBindConfig
25
+ Trigger = runtime_env.types.Trigger
26
+ hook = runtime_env.make_hook()
27
+
28
+ with pytest.raises(ValueError, match="ON_PRESS"):
29
+ hook.bind_mouse(
30
+ "wheel_up",
31
+ lambda: None,
32
+ config=MouseBindConfig(trigger=Trigger.ON_HOLD),
33
+ )
34
+
35
+
36
+ def test_wheel_delta_rejects_non_positive(runtime_env):
37
+ MouseBindConfig = runtime_env.types.MouseBindConfig
38
+ with pytest.raises(ValueError, match="wheel_delta"):
39
+ MouseBindConfig(wheel_delta=0)
40
+
41
+
42
+ def test_vertical_wheel_accumulates_to_configured_delta(runtime_env):
43
+ hook = runtime_env.make_hook()
44
+ driver = runtime_env.HookDriver(runtime_env, hook)
45
+ hits = []
46
+
47
+ bind = hook.bind_mouse("wheel_up", lambda: hits.append("up"))
48
+ hook.bind_mouse("wheel_down", lambda: hits.append("down"))
49
+
50
+ assert bind.config.wheel_delta == 120
51
+ assert driver.wheel(60) == runtime_env.winput.WP_CONTINUE
52
+ assert hits == []
53
+ assert bind._wheel_acc == 60
54
+
55
+ assert driver.wheel(60) == runtime_env.winput.WP_CONTINUE
56
+ assert hits == ["up"]
57
+ assert bind._wheel_acc == 0
58
+
59
+
60
+ def test_custom_wheel_delta_changes_quantum(runtime_env):
61
+ MouseBindConfig = runtime_env.types.MouseBindConfig
62
+ hook = runtime_env.make_hook()
63
+ driver = runtime_env.HookDriver(runtime_env, hook)
64
+ hits = []
65
+
66
+ hook.bind_mouse(
67
+ "wheel_up",
68
+ lambda: hits.append("up"),
69
+ config=MouseBindConfig(wheel_delta=40),
70
+ )
71
+
72
+ driver.wheel(40)
73
+ driver.wheel(80)
74
+ assert hits == ["up", "up", "up"]
75
+
76
+
77
+ def test_wheel_delta_soft_merge_overrides_default(runtime_env):
78
+ MouseBindConfig = runtime_env.types.MouseBindConfig
79
+ SuppressPolicy = runtime_env.types.SuppressPolicy
80
+
81
+ merged = MouseBindConfig(suppress=SuppressPolicy.WHEN_MATCHED).soft_merge(
82
+ MouseBindConfig(wheel_delta=30)
83
+ )
84
+ assert merged.wheel_delta == 30
85
+ assert merged.suppress is SuppressPolicy.WHEN_MATCHED
86
+
87
+
88
+ def test_vertical_wheel_fires_once_per_notch_including_multiples(runtime_env):
89
+ hook = runtime_env.make_hook()
90
+ driver = runtime_env.HookDriver(runtime_env, hook)
91
+ hits = []
92
+
93
+ hook.bind_mouse("wheel_down", lambda: hits.append("down"))
94
+
95
+ driver.wheel(-240)
96
+ assert hits == ["down", "down"]
97
+
98
+
99
+ def test_opposite_vertical_direction_resets_remainder(runtime_env):
100
+ hook = runtime_env.make_hook()
101
+ driver = runtime_env.HookDriver(runtime_env, hook)
102
+ hits = []
103
+
104
+ up = hook.bind_mouse("wheel_up", lambda: hits.append("up"))
105
+
106
+ driver.wheel(60)
107
+ assert up._wheel_acc == 60
108
+ driver.wheel(-30)
109
+ assert up._wheel_acc == 0
110
+ driver.wheel(60)
111
+ assert hits == []
112
+
113
+
114
+ def test_horizontal_wheel_is_independent_axis(runtime_env):
115
+ hook = runtime_env.make_hook()
116
+ driver = runtime_env.HookDriver(runtime_env, hook)
117
+ hits = []
118
+
119
+ hook.bind_mouse("wheel_left", lambda: hits.append("left"))
120
+ hook.bind_mouse("wheel_right", lambda: hits.append("right"))
121
+ hook.bind_mouse("wheel_up", lambda: hits.append("up"))
122
+
123
+ driver.wheel(-120, horizontal=True)
124
+ driver.wheel(120, horizontal=True)
125
+ driver.wheel(120)
126
+
127
+ assert hits == ["left", "right", "up"]
128
+
129
+
130
+ def test_wheel_when_matched_suppresses_only_after_a_notch(runtime_env):
131
+ MouseBindConfig = runtime_env.types.MouseBindConfig
132
+ SuppressPolicy = runtime_env.types.SuppressPolicy
133
+ hook = runtime_env.make_hook()
134
+ driver = runtime_env.HookDriver(runtime_env, hook)
135
+ hits = []
136
+
137
+ hook.bind_mouse(
138
+ "wheel_up",
139
+ lambda: hits.append("up"),
140
+ config=MouseBindConfig(suppress=SuppressPolicy.WHEN_MATCHED),
141
+ )
142
+
143
+ flags_partial = driver.wheel(40)
144
+ flags_full = driver.wheel(80)
145
+
146
+ assert flags_partial == runtime_env.winput.WP_CONTINUE
147
+ assert flags_full & runtime_env.winput.WP_DONT_PASS_INPUT_ON
148
+ assert hits == ["up"]
149
+
150
+
151
+ def test_wheel_is_never_pressed(runtime_env):
152
+ hook = runtime_env.make_hook()
153
+ driver = runtime_env.HookDriver(runtime_env, hook)
154
+ runtime_env.backend_singleton.current_state_snapshot = driver._state
155
+ bind = hook.bind_mouse("wheel_up", lambda: None)
156
+
157
+ driver.wheel(120)
158
+ assert bind.is_pressed() is False
159
+ assert bind.pressed_keys() == []
160
+
161
+
162
+ def test_wheel_respects_injected_ignore(runtime_env):
163
+ MouseBindConfig = runtime_env.types.MouseBindConfig
164
+ InjectedPolicy = runtime_env.types.InjectedPolicy
165
+ hook = runtime_env.make_hook()
166
+ driver = runtime_env.HookDriver(runtime_env, hook)
167
+ hits = []
168
+
169
+ hook.bind_mouse(
170
+ "wheel_up",
171
+ lambda: hits.append("up"),
172
+ config=MouseBindConfig(injected=InjectedPolicy.IGNORE),
173
+ )
174
+
175
+ driver.wheel(120, injected=True)
176
+ driver.wheel(120, injected=False)
177
+ assert hits == ["up"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes