keybinds 0.0.1__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ import sys
2
+
3
+ if sys.platform != "win32":
4
+ raise RuntimeError("keybinds works on Windows only")
5
+
6
+ from importlib.metadata import version, PackageNotFoundError
7
+
8
+ from . import winput
9
+
10
+ from .types import (
11
+ Trigger,
12
+ SuppressPolicy,
13
+ ChordPolicy,
14
+ OrderPolicy,
15
+ Timing,
16
+ Constraints,
17
+ Checks,
18
+ BindConfig,
19
+ MouseButton,
20
+ MouseBindConfig,
21
+ Callback,
22
+ Predicate,
23
+ )
24
+
25
+ from .bind import Bind, MouseBind, Hook, get_default_hook, set_default_hook, join
26
+ from .decorators import bind_key, bind_mouse
27
+ from . import presets
28
+
29
+ from ._constants import register_key_token
30
+
31
+ try:
32
+ __version__ = version("keybinds")
33
+ except PackageNotFoundError:
34
+ __version__ = "0.0.0"
35
+
36
+ __all__ = [
37
+ "Trigger","SuppressPolicy","ChordPolicy","OrderPolicy",
38
+ "Timing","Constraints","Checks","BindConfig",
39
+ "MouseButton","MouseBindConfig","Callback","Predicate",
40
+ "Bind","MouseBind","Hook","get_default_hook","set_default_hook","join",
41
+ "bind_key","bind_mouse","presets","register_key_token","winput"
42
+ ]
keybinds/_backend.py ADDED
@@ -0,0 +1,244 @@
1
+ # keybinds/_backend.py
2
+ from __future__ import annotations
3
+
4
+ import threading
5
+ import weakref
6
+ from traceback import print_exc
7
+ from typing import Optional, List
8
+
9
+ from . import winput
10
+
11
+ from ._constants import (
12
+ WM_KEYDOWN, WM_SYSKEYDOWN, WM_KEYUP, WM_SYSKEYUP,
13
+ WM_LBUTTONDOWN, WM_LBUTTONUP,
14
+ WM_RBUTTONDOWN, WM_RBUTTONUP,
15
+ WM_MBUTTONDOWN, WM_MBUTTONUP,
16
+ WM_XBUTTONDOWN, WM_XBUTTONUP,
17
+ )
18
+ from .types import MouseButton
19
+ from ._state import InputState
20
+
21
+
22
+ class _GlobalBackend:
23
+ """
24
+ One per-process backend:
25
+ - installs winput hooks once
26
+ - runs wait_messages() once
27
+ - dispatches events to all active Hook instances
28
+ """
29
+
30
+ _instance: Optional["_GlobalBackend"] = None
31
+ _instance_lock = threading.Lock()
32
+
33
+ def __init__(self) -> None:
34
+ self._hooks: List[weakref.ReferenceType] = []
35
+ self._hooks_lock = threading.Lock()
36
+
37
+ self._thread: Optional[threading.Thread] = None
38
+ self._thread_started = False
39
+
40
+ # physical only
41
+ self._pressed_keys: set[int] = set()
42
+ self._pressed_mouse: set[MouseButton] = set()
43
+
44
+ # physical + injected
45
+ self._pressed_keys_all: set[int] = set()
46
+ self._pressed_mouse_all: set[MouseButton] = set()
47
+
48
+ # injected only
49
+ self._pressed_keys_injected: set[int] = set()
50
+ self._pressed_mouse_injected: set[MouseButton] = set()
51
+
52
+ @classmethod
53
+ def instance(cls) -> _GlobalBackend:
54
+ with cls._instance_lock:
55
+ if cls._instance is None:
56
+ cls._instance = _GlobalBackend()
57
+ return cls._instance
58
+
59
+ # -------------------------
60
+ # lifecycle
61
+ # -------------------------
62
+
63
+ def register(self, hook_obj) -> None:
64
+ """Register a Hook frontend and ensure backend thread is running."""
65
+ with self._hooks_lock:
66
+ self._hooks.append(weakref.ref(hook_obj))
67
+
68
+ self._ensure_thread()
69
+
70
+ def unregister(self, hook_obj) -> None:
71
+ """Unregister a Hook frontend. (We keep backend thread alive; optional stop can be added later.)"""
72
+ with self._hooks_lock:
73
+ new_list = []
74
+ for r in self._hooks:
75
+ o = r()
76
+ if o is None:
77
+ continue
78
+ if o is hook_obj:
79
+ continue
80
+ new_list.append(r)
81
+ self._hooks = new_list
82
+
83
+ def _ensure_thread(self) -> None:
84
+ with self._hooks_lock:
85
+ if self._thread_started:
86
+ return
87
+ self._thread_started = True
88
+
89
+ t = threading.Thread(target=self._thread_main, name="keybinds-backend", daemon=True)
90
+ self._thread = t
91
+ t.start()
92
+
93
+ def _thread_main(self) -> None:
94
+ # Install hooks and pump messages on the SAME thread.
95
+ try:
96
+ winput.hook_keyboard(self._on_keyboard)
97
+ winput.hook_mouse(self._on_mouse)
98
+ winput.wait_messages()
99
+ except Exception:
100
+ # If something goes wrong, try to unhook to restore input.
101
+ try:
102
+ winput.unhook_keyboard()
103
+ except Exception:
104
+ pass
105
+ try:
106
+ winput.unhook_mouse()
107
+ except Exception:
108
+ pass
109
+
110
+ # -------------------------
111
+ # dispatch
112
+ # -------------------------
113
+
114
+ def _alive_hooks(self) -> list:
115
+ """Return strong refs to currently alive Hook objects."""
116
+ out = []
117
+ with self._hooks_lock:
118
+ new_refs = []
119
+ for r in self._hooks:
120
+ o = r()
121
+ if o is None:
122
+ continue
123
+ out.append(o)
124
+ new_refs.append(r)
125
+ self._hooks = new_refs
126
+ return out
127
+
128
+ def _on_keyboard(self, event: winput.KeyboardEvent) -> int:
129
+ vk = int(event.vkCode)
130
+ is_down = event.action in (WM_KEYDOWN, WM_SYSKEYDOWN)
131
+ is_up = event.action in (WM_KEYUP, WM_SYSKEYUP)
132
+
133
+ injected = bool(getattr(event, "injected", False))
134
+
135
+ # Choose domain for repeat detection on KEYDOWN only
136
+ if is_down:
137
+ base = self._pressed_keys_injected if injected else self._pressed_keys
138
+ was_down = vk in base
139
+ else:
140
+ was_down = False
141
+
142
+ if is_down:
143
+ self._pressed_keys_all.add(vk)
144
+ elif is_up:
145
+ self._pressed_keys_all.discard(vk)
146
+
147
+ if is_down:
148
+ if injected:
149
+ self._pressed_keys_injected.add(vk)
150
+ else:
151
+ self._pressed_keys.add(vk)
152
+
153
+ elif is_up:
154
+ self._pressed_keys.discard(vk)
155
+ self._pressed_keys_injected.discard(vk)
156
+
157
+ # Mark OS auto-repeat (keydown while already pressed in that domain)
158
+ try:
159
+ setattr(event, "_sb_is_repeat", bool(is_down and was_down))
160
+ except Exception:
161
+ pass
162
+
163
+ state = InputState(
164
+ self._pressed_keys,
165
+ self._pressed_mouse,
166
+ self._pressed_keys_all,
167
+ self._pressed_mouse_all,
168
+ self._pressed_keys_injected,
169
+ self._pressed_mouse_injected,
170
+ )
171
+
172
+ flags = winput.WP_CONTINUE
173
+ for h in self._alive_hooks():
174
+ try:
175
+ flags |= h._handle_keyboard_event(event, state)
176
+ except Exception:
177
+ print_exc()
178
+ return flags
179
+
180
+ def _on_mouse(self, event: winput.MouseEvent) -> int:
181
+ # Hard filter: only handle button up/down (everything else is noise).
182
+ act = event.action
183
+ if act not in (
184
+ WM_LBUTTONDOWN, WM_LBUTTONUP,
185
+ WM_RBUTTONDOWN, WM_RBUTTONUP,
186
+ WM_MBUTTONDOWN, WM_MBUTTONUP,
187
+ WM_XBUTTONDOWN, WM_XBUTTONUP,
188
+ ):
189
+ return winput.WP_CONTINUE
190
+
191
+ injected = bool(getattr(event, "injected", False))
192
+
193
+ def _apply(target: set[MouseButton]) -> None:
194
+ if act == WM_LBUTTONDOWN:
195
+ target.add(MouseButton.LEFT)
196
+ elif act == WM_LBUTTONUP:
197
+ target.discard(MouseButton.LEFT)
198
+ elif act == WM_RBUTTONDOWN:
199
+ target.add(MouseButton.RIGHT)
200
+ elif act == WM_RBUTTONUP:
201
+ target.discard(MouseButton.RIGHT)
202
+ elif act == WM_MBUTTONDOWN:
203
+ target.add(MouseButton.MIDDLE)
204
+ elif act == WM_MBUTTONUP:
205
+ target.discard(MouseButton.MIDDLE)
206
+ elif act == WM_XBUTTONDOWN:
207
+ which = int(getattr(event, "additional_data", 0) or 0)
208
+ if which == 1:
209
+ target.add(MouseButton.X1)
210
+ elif which == 2:
211
+ target.add(MouseButton.X2)
212
+ elif act == WM_XBUTTONUP:
213
+ which = int(getattr(event, "additional_data", 0) or 0)
214
+ if which == 1:
215
+ target.discard(MouseButton.X1)
216
+ elif which == 2:
217
+ target.discard(MouseButton.X2)
218
+
219
+ # Update ALL-state always
220
+ _apply(self._pressed_mouse_all)
221
+
222
+ # Update injected-only state only for injected events
223
+ if injected:
224
+ _apply(self._pressed_mouse_injected)
225
+ else:
226
+ # Update PHYSICAL-state only for non-injected events
227
+ _apply(self._pressed_mouse)
228
+
229
+ state = InputState(
230
+ self._pressed_keys,
231
+ self._pressed_mouse,
232
+ self._pressed_keys_all,
233
+ self._pressed_mouse_all,
234
+ self._pressed_keys_injected,
235
+ self._pressed_mouse_injected,
236
+ )
237
+
238
+ flags = winput.WP_CONTINUE
239
+ for h in self._alive_hooks():
240
+ try:
241
+ flags |= h._handle_mouse_event(event, state)
242
+ except Exception:
243
+ pass
244
+ return flags
keybinds/_constants.py ADDED
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, Set
4
+
5
+ from . import winput
6
+
7
+ # Common VK codes (fallbacks); winput also exposes VK_* constants.
8
+ VK_SHIFT = getattr(winput, "VK_SHIFT", 0x10)
9
+ VK_CONTROL = getattr(winput, "VK_CONTROL", 0x11)
10
+ VK_MENU = getattr(winput, "VK_MENU", 0x12) # ALT
11
+ VK_LSHIFT = getattr(winput, "VK_LSHIFT", 0xA0)
12
+ VK_RSHIFT = getattr(winput, "VK_RSHIFT", 0xA1)
13
+ VK_LCONTROL = getattr(winput, "VK_LCONTROL", 0xA2)
14
+ VK_RCONTROL = getattr(winput, "VK_RCONTROL", 0xA3)
15
+ VK_LMENU = getattr(winput, "VK_LMENU", 0xA4)
16
+ VK_RMENU = getattr(winput, "VK_RMENU", 0xA5)
17
+ VK_LWIN = getattr(winput, "VK_LWIN", 0x5B)
18
+ VK_RWIN = getattr(winput, "VK_RWIN", 0x5C)
19
+
20
+ # Mouse wParams
21
+ WM_LBUTTONDOWN = winput.WM_LBUTTONDOWN
22
+ WM_LBUTTONUP = winput.WM_LBUTTONUP
23
+ WM_RBUTTONDOWN = winput.WM_RBUTTONDOWN
24
+ WM_RBUTTONUP = winput.WM_RBUTTONUP
25
+ WM_MBUTTONDOWN = winput.WM_MBUTTONDOWN
26
+ WM_MBUTTONUP = winput.WM_MBUTTONUP
27
+ WM_XBUTTONDOWN = winput.WM_XBUTTONDOWN
28
+ WM_XBUTTONUP = winput.WM_XBUTTONUP
29
+
30
+ WM_KEYDOWN = winput.WM_KEYDOWN
31
+ WM_KEYUP = winput.WM_KEYUP
32
+ WM_SYSKEYDOWN = winput.WM_SYSKEYDOWN
33
+ WM_SYSKEYUP = winput.WM_SYSKEYUP
34
+
35
+ # mouse move/wheel constants may exist; values are stable win32.
36
+ WM_MOUSEMOVE = getattr(winput, "WM_MOUSEMOVE", 0x0200)
37
+ WM_MOUSEWHEEL = getattr(winput, "WM_MOUSEWHEEL", 0x020A)
38
+ WM_MOUSEHWHEEL = getattr(winput, "WM_MOUSEHWHEEL", 0x020E)
39
+
40
+
41
+ _MOD_GROUPS: Dict[str, Set[int]] = {
42
+ "shift": {VK_SHIFT, VK_LSHIFT, VK_RSHIFT},
43
+ "ctrl": {VK_CONTROL, VK_LCONTROL, VK_RCONTROL},
44
+ "control": {VK_CONTROL, VK_LCONTROL, VK_RCONTROL},
45
+ "alt": {VK_MENU, VK_LMENU, VK_RMENU},
46
+ "menu": {VK_MENU, VK_LMENU, VK_RMENU},
47
+ "win": {VK_LWIN, VK_RWIN},
48
+ "lwin": {VK_LWIN},
49
+ "rwin": {VK_RWIN},
50
+ }
51
+
52
+
53
+ SPECIAL_KEYS: Dict[str, int] = {
54
+ "esc": getattr(winput, "VK_ESCAPE", 0x1B),
55
+ "escape": getattr(winput, "VK_ESCAPE", 0x1B),
56
+ "enter": getattr(winput, "VK_RETURN", 0x0D),
57
+ "return": getattr(winput, "VK_RETURN", 0x0D),
58
+ "tab": getattr(winput, "VK_TAB", 0x09),
59
+ "space": getattr(winput, "VK_SPACE", 0x20),
60
+ "backspace": getattr(winput, "VK_BACK", 0x08),
61
+ "delete": getattr(winput, "VK_DELETE", 0x2E),
62
+ "del": getattr(winput, "VK_DELETE", 0x2E),
63
+ "insert": getattr(winput, "VK_INSERT", 0x2D),
64
+ "home": getattr(winput, "VK_HOME", 0x24),
65
+ "end": getattr(winput, "VK_END", 0x23),
66
+ "pgup": getattr(winput, "VK_PRIOR", 0x21),
67
+ "pageup": getattr(winput, "VK_PRIOR", 0x21),
68
+ "pgdn": getattr(winput, "VK_NEXT", 0x22),
69
+ "pagedown": getattr(winput, "VK_NEXT", 0x22),
70
+ "up": getattr(winput, "VK_UP", 0x26),
71
+ "down": getattr(winput, "VK_DOWN", 0x28),
72
+ "left": getattr(winput, "VK_LEFT", 0x25),
73
+ "right": getattr(winput, "VK_RIGHT", 0x27),
74
+ "volumeup": getattr(winput, "VK_VOLUMEUP", 0xAF),
75
+ "volumedown": getattr(winput, "VK_VOLUMEDOWN", 0xAE),
76
+ "mute": getattr(winput, "VK_VOLUMEMUTE", 0xAD),
77
+ }
78
+
79
+ SPECIAL_KEYS.update({
80
+ "`": getattr(winput, "VK_OEM_3", 0xC0), # `~
81
+ "backtick": getattr(winput, "VK_OEM_3", 0xC0),
82
+ "grave": getattr(winput, "VK_OEM_3", 0xC0),
83
+ "tilde": getattr(winput, "VK_OEM_3", 0xC0),
84
+
85
+ "-": getattr(winput, "VK_OEM_MINUS", 0xBD), # -_
86
+ "=": getattr(winput, "VK_OEM_PLUS", 0xBB), # =+
87
+ "[": getattr(winput, "VK_OEM_4", 0xDB), # [{
88
+ "]": getattr(winput, "VK_OEM_6", 0xDD), # ]}
89
+ "\\": getattr(winput, "VK_OEM_5", 0xDC), # \|
90
+ ";": getattr(winput, "VK_OEM_1", 0xBA), # ;:
91
+ "'": getattr(winput, "VK_OEM_7", 0xDE), # '"
92
+ ",": getattr(winput, "VK_OEM_COMMA", 0xBC), # ,<
93
+ ".": getattr(winput, "VK_OEM_PERIOD", 0xBE), # .>
94
+ "/": getattr(winput, "VK_OEM_2", 0xBF), # /?
95
+ })
96
+
97
+ for i in range(1, 25):
98
+ SPECIAL_KEYS[f"f{i}"] = getattr(winput, f"VK_F{i}", 0x70 + (i - 1))
99
+
100
+
101
+ def is_modifier_vk(vk: int) -> bool:
102
+ return vk in (
103
+ VK_SHIFT,
104
+ VK_LSHIFT,
105
+ VK_RSHIFT,
106
+ VK_CONTROL,
107
+ VK_LCONTROL,
108
+ VK_RCONTROL,
109
+ VK_MENU,
110
+ VK_LMENU,
111
+ VK_RMENU,
112
+ VK_LWIN,
113
+ VK_RWIN,
114
+ )
115
+
116
+
117
+ def register_key_token(name: str, vk: int) -> None:
118
+ """
119
+ Register a custom key token at runtime.
120
+
121
+ Example:
122
+ register_key_token("`", 0xC0)
123
+ """
124
+ SPECIAL_KEYS[name.lower()] = vk
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import queue
4
+ import threading
5
+ from traceback import print_exc
6
+ from typing import Callable, List, Optional
7
+
8
+
9
+ class _CallbackDispatcher:
10
+ """Executes user callbacks on a small worker pool.
11
+
12
+ Critical for low-level hooks: do *not* create a new Thread per event.
13
+ """
14
+
15
+ def __init__(self, workers: int = 1) -> None:
16
+ self._q: "queue.SimpleQueue[Optional[Callable[[], None]]]" = queue.SimpleQueue()
17
+ self._threads: List[threading.Thread] = []
18
+ self._workers = max(1, int(workers))
19
+ for i in range(self._workers):
20
+ t = threading.Thread(target=self._worker, name=f"bind-worker-{i}", daemon=True)
21
+ t.start()
22
+ self._threads.append(t)
23
+
24
+ def submit(self, fn: Callable[[], None]) -> None:
25
+ self._q.put(fn)
26
+
27
+ def stop(self) -> None:
28
+ for _ in range(self._workers):
29
+ self._q.put(None)
30
+
31
+ def _worker(self) -> None:
32
+ while True:
33
+ fn = self._q.get()
34
+ if fn is None:
35
+ return
36
+ try:
37
+ fn()
38
+ except Exception:
39
+ # never let user callbacks kill the worker
40
+ print_exc()
keybinds/_hook.py ADDED
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ import signal
4
+ import threading
5
+ from contextlib import contextmanager
6
+ from typing import Callable, Optional, Union, Generator
7
+
8
+ from ._backend import _GlobalBackend
9
+ from ._dispatcher import _CallbackDispatcher
10
+ from ._keyboard import Bind
11
+ from ._mouse import MouseBind
12
+ from .types import BindConfig, MouseBindConfig, MouseButton
13
+
14
+ _default_hook: Optional[Hook] = None
15
+
16
+
17
+ def get_default_hook() -> Hook:
18
+ global _default_hook
19
+ if _default_hook is None:
20
+ _default_hook = Hook()
21
+ return _default_hook
22
+
23
+
24
+ def set_default_hook(hook: Hook) -> None:
25
+ global _default_hook
26
+ _default_hook = hook
27
+
28
+
29
+ def join(hook: Optional[Hook] = None) -> None:
30
+ """Block until the hook is stopped.
31
+
32
+ If hook is None, the default hook will be used.
33
+ """
34
+
35
+ if hook is None:
36
+ hook = get_default_hook()
37
+
38
+ def handler(sig, frame):
39
+ hook.stop()
40
+
41
+ signal.signal(signal.SIGINT, handler)
42
+
43
+ try:
44
+ hook.wait()
45
+ finally:
46
+ hook.close()
47
+
48
+
49
+ class Hook:
50
+ def __init__(
51
+ self,
52
+ *,
53
+ callback_workers: int = 1,
54
+ default_config: Optional[BindConfig] = None,
55
+ default_mouse_config: Optional[MouseBindConfig] = None,
56
+ ) -> None:
57
+ self._lock = threading.Lock()
58
+ self._stop_event = threading.Event()
59
+
60
+ self._pause_count = 0
61
+ self._paused = False
62
+
63
+ self.default_config = default_config
64
+ self.default_mouse_config = default_mouse_config
65
+
66
+ self._dispatcher = _CallbackDispatcher(workers=callback_workers)
67
+
68
+ # binds live in this frontend
69
+ self._keyboard_binds: list[Bind] = []
70
+ self._mouse_binds: list[MouseBind] = []
71
+
72
+ # snapshots used by backend hot path (tuples are cheap to iterate)
73
+ self._keyboard_snapshot: tuple[Bind, ...] = ()
74
+ self._mouse_snapshot: tuple[MouseBind, ...] = ()
75
+
76
+ # Attach to global backend (installs hooks once)
77
+ _GlobalBackend.instance().register(self)
78
+
79
+ # -------------------------
80
+ # public API
81
+ # -------------------------
82
+
83
+ def bind(self, expr: str, callback: Callable[[], None], *, config: Optional[BindConfig] = None, hwnd=None) -> Bind:
84
+ cfg = config or self.default_config or BindConfig()
85
+ b = Bind(expr, callback, config=cfg, hwnd=hwnd, dispatch=self._dispatcher.submit)
86
+ with self._lock:
87
+ self._keyboard_binds.append(b)
88
+ self._keyboard_snapshot = tuple(self._keyboard_binds)
89
+ return b
90
+
91
+ def bind_mouse(self, button: Union[MouseButton, str], callback: Callable[[], None], *, config: Optional[MouseBindConfig] = None, hwnd=None) -> MouseBind:
92
+ cfg = config or self.default_mouse_config or MouseBindConfig()
93
+ b = MouseBind(button, callback, config=cfg, hwnd=hwnd, dispatch=self._dispatcher.submit)
94
+ with self._lock:
95
+ self._mouse_binds.append(b)
96
+ self._mouse_snapshot = tuple(self._mouse_binds)
97
+ return b
98
+
99
+ def unbind(self, b: Bind) -> None:
100
+ with self._lock:
101
+ try:
102
+ self._keyboard_binds.remove(b)
103
+ except ValueError:
104
+ return
105
+ self._keyboard_snapshot = tuple(self._keyboard_binds)
106
+
107
+ def unbind_mouse(self, b: MouseBind) -> None:
108
+ with self._lock:
109
+ try:
110
+ self._mouse_binds.remove(b)
111
+ except ValueError:
112
+ return
113
+ self._mouse_snapshot = tuple(self._mouse_binds)
114
+
115
+ def pause(self) -> None:
116
+ """Pause the hook (no callbacks will be called until resume() is called).
117
+
118
+ Useful for temporarily disabling the hook while it's running.
119
+ """
120
+ with self._lock:
121
+ self._pause_count += 1
122
+ self._paused = True
123
+
124
+ def resume(self) -> None:
125
+ """Resume the hook (callbacks will be called again after calling pause()).
126
+
127
+ Useful for re-enabling the hook after temporarily disabling it with pause().
128
+ """
129
+ with self._lock:
130
+ if self._pause_count == 0:
131
+ return
132
+ self._pause_count -= 1
133
+ if self._pause_count == 0:
134
+ self._paused = False
135
+
136
+ def is_paused(self) -> bool:
137
+ """Get whether the hook is currently paused.
138
+
139
+ Returns True if the hook is paused, False otherwise.
140
+ """
141
+ return self._paused
142
+
143
+ @contextmanager
144
+ def paused(self) -> Generator[None, None, None]:
145
+ """Pause and resume the hook using the with statement.
146
+
147
+ Example:
148
+ >>> with hook.paused():
149
+ # do something that requires the hook to be paused
150
+ """
151
+ self.pause()
152
+ try:
153
+ yield
154
+ finally:
155
+ self.resume()
156
+
157
+ def stop(self) -> None:
158
+ """Signal to stop waiting in wait().
159
+
160
+ After calling stop(), wait() will return True as soon as possible.
161
+ """
162
+ self._stop_event.set()
163
+
164
+ def wait(self, timeout: Optional[float] = None) -> bool:
165
+ """Wait for stop() to be called.
166
+
167
+ If timeout is not None, wait up to that amount of time (in seconds) for stop() to be called.
168
+ Returns True if stop() was called, False otherwise.
169
+ """
170
+ if timeout is not None:
171
+ return self._stop_event.wait(timeout)
172
+
173
+ try:
174
+ while not self._stop_event.wait(0.2):
175
+ pass
176
+ return True
177
+ except KeyboardInterrupt:
178
+ return True
179
+
180
+ def join(self) -> None:
181
+ """Block until the hook is stopped."""
182
+ join(self)
183
+
184
+ def close(self) -> None:
185
+ # just detach this frontend; backend keeps running if others exist
186
+ _GlobalBackend.instance().unregister(self)
187
+ self._dispatcher.stop()
188
+
189
+ # -------------------------
190
+ # called by backend
191
+ # -------------------------
192
+
193
+ def _handle_keyboard_event(self, event, state) -> int:
194
+ if self._paused:
195
+ return 0
196
+
197
+ snap = self._keyboard_snapshot
198
+ if not snap:
199
+ return 0 # winput.WP_CONTINUE (backend ORs anyway)
200
+ flags = 0
201
+ for b in snap:
202
+ flags |= b.handle(event, state)
203
+ return flags
204
+
205
+ def _handle_mouse_event(self, event, state) -> int:
206
+ if self._paused:
207
+ return 0
208
+
209
+ snap = self._mouse_snapshot
210
+ if not snap:
211
+ return 0
212
+ flags = 0
213
+ for b in snap:
214
+ flags |= b.handle(event, state)
215
+ return flags