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.
- {keybinds-1.3.2 → keybinds-1.3.4}/PKG-INFO +13 -6
- {keybinds-1.3.2 → keybinds-1.3.4}/README.md +12 -5
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/__init__.py +48 -48
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_backend.py +60 -20
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_base_bind.py +196 -183
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_keyboard.py +119 -19
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_mouse.py +18 -1
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/logical/abbreviation.py +352 -337
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/logical/keyboard.py +179 -25
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/winput/winput.py +12 -6
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds.egg-info/PKG-INFO +13 -6
- {keybinds-1.3.2 → keybinds-1.3.4}/pyproject.toml +1 -1
- {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_bind_wait_and_is_pressed.py +41 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_runtime_edge_cases.py +118 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/LICENSE +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_async.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_bind_registry.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_constants.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_dispatcher.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_hook.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_parsing.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_state.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/_window.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/bind.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/decorators.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/__init__.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/analysis.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/core.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/models.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/reporting.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/diagnostics/tracing.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/logical/__init__.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/logical/parsing.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/logical/translate.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/presets.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/simple.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/types.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/winput/LICENSE +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/winput/__init__.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds/winput/vk_codes.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds.egg-info/SOURCES.txt +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds.egg-info/dependency_links.txt +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds.egg-info/requires.txt +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/keybinds.egg-info/top_level.txt +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/setup.cfg +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/setup.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_bind_registry.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_decorators_and_simple.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_diagnostics.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_logical_parsing.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_parsing.py +0 -0
- {keybinds-1.3.2 → keybinds-1.3.4}/tests/test_runtime_bindings.py +0 -0
- {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.
|
|
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
|

|
|
56
56
|

|
|
57
57
|

|
|
58
|
-

|
|
59
59
|

|
|
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
|
|
197
|
+
All bind objects expose small runtime helpers:
|
|
198
198
|
|
|
199
199
|
```python
|
|
200
200
|
if bind.is_pressed():
|
|
201
|
-
print("
|
|
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()`
|
|
208
|
-
`
|
|
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
|

|
|
6
6
|

|
|
7
7
|

|
|
8
|
-

|
|
9
9
|

|
|
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
|
|
147
|
+
All bind objects expose small runtime helpers:
|
|
148
148
|
|
|
149
149
|
```python
|
|
150
150
|
if bind.is_pressed():
|
|
151
|
-
print("
|
|
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()`
|
|
158
|
-
`
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|