keybinds 1.3.2__tar.gz → 1.3.4__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 (53) hide show
  1. {keybinds-1.3.2 → keybinds-1.3.4}/PKG-INFO +13 -6
  2. {keybinds-1.3.2 → keybinds-1.3.4}/README.md +12 -5
  3. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/__init__.py +48 -48
  4. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_backend.py +60 -20
  5. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_base_bind.py +196 -183
  6. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_keyboard.py +119 -19
  7. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_mouse.py +18 -1
  8. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/logical/abbreviation.py +352 -337
  9. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/logical/keyboard.py +179 -25
  10. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/winput/winput.py +12 -6
  11. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds.egg-info/PKG-INFO +13 -6
  12. {keybinds-1.3.2 → keybinds-1.3.4}/pyproject.toml +1 -1
  13. {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_bind_wait_and_is_pressed.py +41 -0
  14. {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_runtime_edge_cases.py +118 -0
  15. {keybinds-1.3.2 → keybinds-1.3.4}/LICENSE +0 -0
  16. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_async.py +0 -0
  17. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_bind_registry.py +0 -0
  18. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_constants.py +0 -0
  19. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_dispatcher.py +0 -0
  20. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_hook.py +0 -0
  21. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_parsing.py +0 -0
  22. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_state.py +0 -0
  23. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_window.py +0 -0
  24. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/bind.py +0 -0
  25. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/decorators.py +0 -0
  26. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/__init__.py +0 -0
  27. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/analysis.py +0 -0
  28. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/core.py +0 -0
  29. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/models.py +0 -0
  30. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/reporting.py +0 -0
  31. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/tracing.py +0 -0
  32. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/logical/__init__.py +0 -0
  33. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/logical/parsing.py +0 -0
  34. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/logical/translate.py +0 -0
  35. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/presets.py +0 -0
  36. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/simple.py +0 -0
  37. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/types.py +0 -0
  38. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/winput/LICENSE +0 -0
  39. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/winput/__init__.py +0 -0
  40. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/winput/vk_codes.py +0 -0
  41. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds.egg-info/SOURCES.txt +0 -0
  42. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds.egg-info/dependency_links.txt +0 -0
  43. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds.egg-info/requires.txt +0 -0
  44. {keybinds-1.3.2 → keybinds-1.3.4}/keybinds.egg-info/top_level.txt +0 -0
  45. {keybinds-1.3.2 → keybinds-1.3.4}/setup.cfg +0 -0
  46. {keybinds-1.3.2 → keybinds-1.3.4}/setup.py +0 -0
  47. {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_bind_registry.py +0 -0
  48. {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_decorators_and_simple.py +0 -0
  49. {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_diagnostics.py +0 -0
  50. {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_logical_parsing.py +0 -0
  51. {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_parsing.py +0 -0
  52. {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_runtime_bindings.py +0 -0
  53. {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keybinds
3
- Version: 1.3.2
3
+ Version: 1.3.4
4
4
  Summary: Flexible global keyboard & mouse hotkeys for Windows
5
5
  Author: echolane
6
6
  License: MIT License
@@ -55,7 +55,7 @@ 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-active-success)
58
+ ![Status](https://img.shields.io/badge/status-maintenance-yellow)
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.
@@ -194,18 +194,25 @@ Top-level helpers use the default hook. If you use multiple hooks, prefer `hook.
194
194
 
195
195
  ## Bind state and waiting
196
196
 
197
- All bind objects expose two small runtime helpers:
197
+ All bind objects expose small runtime helpers:
198
198
 
199
199
  ```python
200
200
  if bind.is_pressed():
201
- print("bind is currently active")
201
+ print("full chord is currently held")
202
+
203
+ if bind.any_pressed():
204
+ print("at least one key/button of this bind is held")
205
+
206
+ print(bind.pressed_keys()) # held VKs for this bind, or [button] for mouse
202
207
 
203
208
  bind.wait() # wait until the bind fires
204
209
  bind.wait(0.5) # wait up to 0.5s, returns True/False
205
210
  ```
206
211
 
207
- `is_pressed()` checks whether the bind is currently pressed.
208
- `wait(timeout=None)` blocks until the bind fires and returns `True`, or returns `False` on timeout.
212
+ - `is_pressed()` — the full chord is held.
213
+ - `any_pressed()` — any key/button of this bind is held.
214
+ - `pressed_keys()` — those keys (VKs, or the mouse button).
215
+ - `wait(timeout=None)` — block until the bind fires (`False` on timeout).
209
216
 
210
217
  ---
211
218
 
@@ -5,7 +5,7 @@ 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-active-success)
8
+ ![Status](https://img.shields.io/badge/status-maintenance-yellow)
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.
@@ -144,18 +144,25 @@ Top-level helpers use the default hook. If you use multiple hooks, prefer `hook.
144
144
 
145
145
  ## Bind state and waiting
146
146
 
147
- All bind objects expose two small runtime helpers:
147
+ All bind objects expose small runtime helpers:
148
148
 
149
149
  ```python
150
150
  if bind.is_pressed():
151
- print("bind is currently active")
151
+ print("full chord is currently held")
152
+
153
+ if bind.any_pressed():
154
+ print("at least one key/button of this bind is held")
155
+
156
+ print(bind.pressed_keys()) # held VKs for this bind, or [button] for mouse
152
157
 
153
158
  bind.wait() # wait until the bind fires
154
159
  bind.wait(0.5) # wait up to 0.5s, returns True/False
155
160
  ```
156
161
 
157
- `is_pressed()` checks whether the bind is currently pressed.
158
- `wait(timeout=None)` blocks until the bind fires and returns `True`, or returns `False` on timeout.
162
+ - `is_pressed()` — the full chord is held.
163
+ - `any_pressed()` — any key/button of this bind is held.
164
+ - `pressed_keys()` — those keys (VKs, or the mouse button).
165
+ - `wait(timeout=None)` — block until the bind fires (`False` on timeout).
159
166
 
160
167
  ---
161
168
 
@@ -1,48 +1,48 @@
1
- import sys
2
-
3
- if sys.platform != "win32":
4
- raise RuntimeError("keybinds works on Windows only")
5
-
6
- from . import winput
7
-
8
- from .types import (
9
- Trigger,
10
- SuppressPolicy,
11
- ChordPolicy,
12
- OrderPolicy,
13
- FocusPolicy,
14
- InjectedPolicy,
15
- TextBoundaryPolicy,
16
- TextBackspacePolicy,
17
- OsKeyRepeatPolicy,
18
- ReplacementPolicy,
19
- Timing,
20
- Constraints,
21
- Checks,
22
- BindConfig,
23
- MouseButton,
24
- MouseBindConfig,
25
- LogicalConfig,
26
- Callback,
27
- Predicate,
28
- )
29
-
30
-
31
- from .bind import Bind, LogicalBind, TextAbbreviationBind, KeyboardBind, MouseBind, Hook, get_default_hook, set_default_hook, join, unbind
32
- from .decorators import bind_key, bind_logical, bind_text, add_abbreviation, bind_abbreviation, bind_mouse
33
- from . import presets
34
-
35
- from ._backend import reinstall_hooks, rehook
36
-
37
- from ._constants import register_key_token
38
-
39
- __version__ = "1.3.2"
40
-
41
- __all__ = [
42
- "Trigger","SuppressPolicy","ChordPolicy","OrderPolicy","FocusPolicy","InjectedPolicy","TextBoundaryPolicy","TextBackspacePolicy","OsKeyRepeatPolicy","ReplacementPolicy",
43
- "Timing","Constraints","Checks","BindConfig",
44
- "MouseButton","MouseBindConfig","Callback","Predicate","LogicalConfig",
45
- "Bind","LogicalBind","TextAbbreviationBind","KeyboardBind","MouseBind","Hook","get_default_hook","set_default_hook","join","unbind",
46
- "bind_key","bind_logical","bind_text","add_abbreviation","bind_abbreviation","bind_mouse","presets","register_key_token","winput",
47
- "reinstall_hooks","rehook"
48
- ]
1
+ import sys
2
+
3
+ if sys.platform != "win32":
4
+ raise RuntimeError("keybinds works on Windows only")
5
+
6
+ from . import winput
7
+
8
+ from .types import (
9
+ Trigger,
10
+ SuppressPolicy,
11
+ ChordPolicy,
12
+ OrderPolicy,
13
+ FocusPolicy,
14
+ InjectedPolicy,
15
+ TextBoundaryPolicy,
16
+ TextBackspacePolicy,
17
+ OsKeyRepeatPolicy,
18
+ ReplacementPolicy,
19
+ Timing,
20
+ Constraints,
21
+ Checks,
22
+ BindConfig,
23
+ MouseButton,
24
+ MouseBindConfig,
25
+ LogicalConfig,
26
+ Callback,
27
+ Predicate,
28
+ )
29
+
30
+
31
+ from .bind import Bind, LogicalBind, TextAbbreviationBind, KeyboardBind, MouseBind, Hook, get_default_hook, set_default_hook, join, unbind
32
+ from .decorators import bind_key, bind_logical, bind_text, add_abbreviation, bind_abbreviation, bind_mouse
33
+ from . import presets
34
+
35
+ from ._backend import reinstall_hooks, rehook
36
+
37
+ from ._constants import register_key_token
38
+
39
+ __version__ = "1.3.4"
40
+
41
+ __all__ = [
42
+ "Trigger","SuppressPolicy","ChordPolicy","OrderPolicy","FocusPolicy","InjectedPolicy","TextBoundaryPolicy","TextBackspacePolicy","OsKeyRepeatPolicy","ReplacementPolicy",
43
+ "Timing","Constraints","Checks","BindConfig",
44
+ "MouseButton","MouseBindConfig","Callback","Predicate","LogicalConfig",
45
+ "Bind","LogicalBind","TextAbbreviationBind","KeyboardBind","MouseBind","Hook","get_default_hook","set_default_hook","join","unbind",
46
+ "bind_key","bind_logical","bind_text","add_abbreviation","bind_abbreviation","bind_mouse","presets","register_key_token","winput",
47
+ "reinstall_hooks","rehook"
48
+ ]
@@ -1,6 +1,7 @@
1
1
  # keybinds/_backend.py
2
2
  from __future__ import annotations
3
3
 
4
+ import atexit
4
5
  import threading
5
6
  import weakref
6
7
  from traceback import print_exc
@@ -54,6 +55,8 @@ class _GlobalBackend:
54
55
  self._thread_id: Optional[int] = None
55
56
  self._thread_ready = threading.Event()
56
57
  self._state_lock = threading.RLock()
58
+ self._atexit_registered = False
59
+ self._shutdown_lock = threading.Lock()
57
60
 
58
61
  @classmethod
59
62
  def instance(cls) -> _GlobalBackend:
@@ -75,14 +78,20 @@ class _GlobalBackend:
75
78
  stop_dispatcher()
76
79
  except Exception:
77
80
  pass
81
+ try:
82
+ if not self._alive_hooks():
83
+ self.shutdown()
84
+ except Exception:
85
+ pass
78
86
 
79
87
  with self._hooks_lock:
80
88
  self._hooks.append(weakref.ref(hook_obj, _on_hook_gc))
81
89
 
90
+ self._ensure_atexit()
82
91
  self._ensure_thread()
83
92
 
84
93
  def unregister(self, hook_obj) -> None:
85
- """Unregister a Hook frontend."""
94
+ """Unregister a Hook frontend. Last hook tears down the backend."""
86
95
  with self._hooks_lock:
87
96
  new_list = []
88
97
  for r in self._hooks:
@@ -93,6 +102,42 @@ class _GlobalBackend:
93
102
  continue
94
103
  new_list.append(r)
95
104
  self._hooks = new_list
105
+ remaining = bool(new_list)
106
+ if not remaining:
107
+ self.shutdown()
108
+
109
+ def _ensure_atexit(self) -> None:
110
+ if self._atexit_registered:
111
+ return
112
+ atexit.register(_atexit_shutdown_backend)
113
+ self._atexit_registered = True
114
+
115
+ def shutdown(self) -> None:
116
+ """Remove LL hooks and ask the backend thread to exit. Idempotent.
117
+
118
+ Does not join the backend thread: Unhook already frees the input chain.
119
+ """
120
+ with self._shutdown_lock:
121
+ with self._hooks_lock:
122
+ thread_id = self._thread_id
123
+
124
+ self._unhook_now()
125
+
126
+ if thread_id is not None:
127
+ try:
128
+ winput.post_thread_message(thread_id, winput.WM_QUIT, 0, 0)
129
+ except Exception:
130
+ pass
131
+
132
+ def _unhook_now(self) -> None:
133
+ try:
134
+ winput.unhook_keyboard()
135
+ except Exception:
136
+ pass
137
+ try:
138
+ winput.unhook_mouse()
139
+ except Exception:
140
+ pass
96
141
 
97
142
  def reinstall_hooks(self) -> None:
98
143
  if not self._thread_started:
@@ -136,7 +181,8 @@ class _GlobalBackend:
136
181
 
137
182
  def _ensure_thread(self) -> None:
138
183
  with self._hooks_lock:
139
- if self._thread_started:
184
+ t = self._thread
185
+ if t is not None and t.is_alive():
140
186
  return
141
187
  self._thread_started = True
142
188
 
@@ -147,15 +193,7 @@ class _GlobalBackend:
147
193
 
148
194
  def _on_backend_message(self, msg) -> bool:
149
195
  if int(msg.message) == WM_KEYBINDS_REINSTALL:
150
- try:
151
- winput.unhook_keyboard()
152
- except Exception:
153
- pass
154
- try:
155
- winput.unhook_mouse()
156
- except Exception:
157
- pass
158
-
196
+ self._unhook_now()
159
197
  self._clear_pressed_state()
160
198
  self._reset_all_hook_runtime_states()
161
199
 
@@ -179,15 +217,7 @@ class _GlobalBackend:
179
217
  except Exception:
180
218
  print_exc()
181
219
  finally:
182
- try:
183
- winput.unhook_keyboard()
184
- except Exception:
185
- pass
186
- try:
187
- winput.unhook_mouse()
188
- except Exception:
189
- pass
190
-
220
+ self._unhook_now()
191
221
  self._clear_pressed_state()
192
222
  with self._hooks_lock:
193
223
  self._thread_started = False
@@ -327,6 +357,16 @@ class _GlobalBackend:
327
357
  return flags
328
358
 
329
359
 
360
+ def _atexit_shutdown_backend() -> None:
361
+ inst = _GlobalBackend._instance
362
+ if inst is None:
363
+ return
364
+ try:
365
+ inst.shutdown()
366
+ except Exception:
367
+ pass
368
+
369
+
330
370
  def reinstall_hooks() -> None:
331
371
  """
332
372
  Reinstall global keyboard/mouse hooks so keybinds is placed later