prkit 0.1.0__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.
prkit/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Presentation tools for the system tray."""
2
+
3
+ __version__ = "0.1.0"
prkit/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from prkit.app import main
2
+
3
+ raise SystemExit(main())
prkit/app.py ADDED
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from collections.abc import Sequence
5
+ from contextlib import suppress
6
+
7
+ from PySide6.QtCore import QObject, QSignalBlocker, QTimer
8
+ from PySide6.QtWidgets import QApplication, QMessageBox
9
+
10
+ from prkit.keyboard import KeyboardMonitor
11
+ from prkit.overlays import KeystrokeOverlay
12
+ from prkit.settings import SettingsStore, SettingsStoreProtocol
13
+ from prkit.spotlight import SpotlightBackend, create_spotlight
14
+ from prkit.tray import TrayController
15
+
16
+
17
+ class AppController(QObject):
18
+ def __init__(
19
+ self,
20
+ app: QApplication,
21
+ store: SettingsStoreProtocol | None = None,
22
+ monitor: KeyboardMonitor | None = None,
23
+ spotlight: SpotlightBackend | None = None,
24
+ ) -> None:
25
+ super().__init__()
26
+ self._app = app
27
+ self._store = store or SettingsStore()
28
+ self.settings = self._store.load()
29
+ self.spotlight = spotlight or create_spotlight(self.settings.spotlight)
30
+ self.keystroke = KeystrokeOverlay(self.settings.keystroke)
31
+ self.monitor = monitor or KeyboardMonitor(self)
32
+ self.tray = TrayController(self.settings, self)
33
+ self._shutting_down = False
34
+ self._keystroke_start_pending = False
35
+
36
+ self.tray.connect_settings(
37
+ spotlight_enabled=self.set_spotlight_enabled,
38
+ spotlight_color=self.set_spotlight_color,
39
+ spotlight_opacity=self.set_spotlight_opacity,
40
+ spotlight_size=self.set_spotlight_size,
41
+ keystroke_enabled=self.set_keystroke_enabled,
42
+ keystroke_color=self.set_keystroke_color,
43
+ keystroke_opacity=self.set_keystroke_opacity,
44
+ )
45
+ self.tray.exit_requested.connect(self.shutdown)
46
+ self.tray.menu.aboutToShow.connect(self.spotlight.sync_position)
47
+ self.tray.menu.aboutToHide.connect(self._start_pending_keyboard_monitor)
48
+ self.monitor.chord_pressed.connect(self.keystroke.show_chord)
49
+ self.monitor.all_released.connect(self.keystroke.keys_released)
50
+ self.monitor.failed.connect(self._keyboard_failed)
51
+ self._app.aboutToQuit.connect(self.cleanup)
52
+
53
+ def start(self) -> None:
54
+ self.tray.show()
55
+ self.spotlight.set_enabled(self.settings.spotlight.enabled)
56
+ if self.settings.keystroke.enabled:
57
+ self.set_keystroke_enabled(True)
58
+
59
+ def set_spotlight_enabled(self, enabled: bool) -> None:
60
+ try:
61
+ self.spotlight.set_enabled(enabled)
62
+ except Exception as error:
63
+ self._disable_spotlight_ui()
64
+ self.tray.notify_warning(f"無法啟用聚光燈游標:{error}")
65
+ return
66
+ self.settings.spotlight.enabled = enabled
67
+ self._save()
68
+
69
+ def set_spotlight_color(self, color: str) -> None:
70
+ self.settings.spotlight.color = color
71
+ self._apply_spotlight_appearance()
72
+
73
+ def set_spotlight_opacity(self, opacity: int) -> None:
74
+ self.settings.spotlight.opacity = opacity
75
+ self._apply_spotlight_appearance()
76
+
77
+ def set_spotlight_size(self, size: int) -> None:
78
+ self.settings.spotlight.size = size
79
+ self._apply_spotlight_appearance()
80
+
81
+ def _apply_spotlight_appearance(self) -> None:
82
+ value = self.settings.spotlight
83
+ try:
84
+ self.spotlight.set_appearance(value.color, value.opacity, value.size)
85
+ except Exception as error:
86
+ self._disable_spotlight_ui()
87
+ self.tray.notify_warning(f"無法更新聚光燈游標:{error}")
88
+ return
89
+ self._save()
90
+
91
+ def _disable_spotlight_ui(self) -> None:
92
+ with suppress(Exception):
93
+ self.spotlight.set_enabled(False)
94
+ self.settings.spotlight.enabled = False
95
+ with QSignalBlocker(self.tray.panel.spotlight_enabled):
96
+ self.tray.panel.set_spotlight_enabled(False)
97
+ self._save()
98
+
99
+ def set_keystroke_enabled(self, enabled: bool) -> None:
100
+ if enabled:
101
+ self._keystroke_start_pending = True
102
+ if not self.tray.menu.isVisible():
103
+ QTimer.singleShot(0, self._start_pending_keyboard_monitor)
104
+ return
105
+ else:
106
+ self._keystroke_start_pending = False
107
+ self.monitor.stop()
108
+ self.keystroke.hide_now()
109
+ self.settings.keystroke.enabled = enabled
110
+ self._save()
111
+
112
+ def _start_pending_keyboard_monitor(self) -> None:
113
+ if not self._keystroke_start_pending:
114
+ return
115
+ self._keystroke_start_pending = False
116
+ success, error = self.monitor.start()
117
+ if not success:
118
+ self._disable_keystroke_ui()
119
+ self.tray.notify_warning(f"無法啟用按鍵顯示:{error or '未知錯誤'}")
120
+ return
121
+ self.settings.keystroke.enabled = True
122
+ self._save()
123
+
124
+ def set_keystroke_color(self, color: str) -> None:
125
+ self.settings.keystroke.color = color
126
+ self._apply_keystroke_appearance()
127
+
128
+ def set_keystroke_opacity(self, opacity: int) -> None:
129
+ self.settings.keystroke.opacity = opacity
130
+ self._apply_keystroke_appearance()
131
+
132
+ def _apply_keystroke_appearance(self) -> None:
133
+ value = self.settings.keystroke
134
+ self.keystroke.set_appearance(value.color, value.opacity)
135
+ self._save()
136
+
137
+ def _keyboard_failed(self, message: str) -> None:
138
+ self._disable_keystroke_ui()
139
+ self.tray.notify_warning(f"按鍵監聽已停止:{message}")
140
+
141
+ def _disable_keystroke_ui(self) -> None:
142
+ self._keystroke_start_pending = False
143
+ self.monitor.stop()
144
+ self.keystroke.hide_now()
145
+ self.settings.keystroke.enabled = False
146
+ with QSignalBlocker(self.tray.panel.keystroke_enabled):
147
+ self.tray.panel.set_keystroke_enabled(False)
148
+ self._save()
149
+
150
+ def _save(self) -> None:
151
+ self._store.save(self.settings)
152
+
153
+ def shutdown(self) -> None:
154
+ if self._shutting_down:
155
+ return
156
+ self._shutting_down = True
157
+ self.cleanup()
158
+ self._app.quit()
159
+
160
+ def cleanup(self) -> None:
161
+ self._keystroke_start_pending = False
162
+ self.monitor.stop()
163
+ self.spotlight.set_enabled(False)
164
+ self.keystroke.hide_now()
165
+ self.tray.hide()
166
+
167
+
168
+ def create_application(arguments: Sequence[str] | None = None) -> QApplication:
169
+ app = QApplication(list(arguments) if arguments is not None else sys.argv)
170
+ app.setApplicationName("簡報瑞士刀")
171
+ app.setOrganizationName("prkit")
172
+ app.setQuitOnLastWindowClosed(False)
173
+ return app
174
+
175
+
176
+ def main() -> int:
177
+ app = create_application()
178
+ if not TrayController.is_available():
179
+ QMessageBox.critical(None, "簡報瑞士刀", "此桌面環境沒有可用的系統匣。")
180
+ return 1
181
+ controller = AppController(app)
182
+ controller.start()
183
+ return app.exec()
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import sys
5
+ from ctypes import wintypes
6
+
7
+ SPI_SETCURSORS = 0x0057
8
+ SYNCHRONIZE = 0x00100000
9
+ WAIT_OBJECT_0 = 0
10
+ INFINITE = 0xFFFFFFFF
11
+
12
+
13
+ def restore_system_cursors() -> None:
14
+ ctypes.windll.user32.SystemParametersInfoW(SPI_SETCURSORS, 0, None, 0)
15
+
16
+
17
+ def watch(parent_pid: int, stop_event_name: str) -> int:
18
+ kernel32 = ctypes.windll.kernel32
19
+ kernel32.OpenProcess.restype = wintypes.HANDLE
20
+ kernel32.OpenEventW.restype = wintypes.HANDLE
21
+ parent = kernel32.OpenProcess(SYNCHRONIZE, False, parent_pid)
22
+ stop_event = kernel32.OpenEventW(SYNCHRONIZE, False, stop_event_name)
23
+ if not parent or not stop_event:
24
+ restore_system_cursors()
25
+ if parent:
26
+ kernel32.CloseHandle(parent)
27
+ if stop_event:
28
+ kernel32.CloseHandle(stop_event)
29
+ return 1
30
+ try:
31
+ handles = (wintypes.HANDLE * 2)(parent, stop_event)
32
+ result = kernel32.WaitForMultipleObjects(2, handles, False, INFINITE)
33
+ if result == WAIT_OBJECT_0:
34
+ restore_system_cursors()
35
+ finally:
36
+ kernel32.CloseHandle(parent)
37
+ kernel32.CloseHandle(stop_event)
38
+ return 0
39
+
40
+
41
+ def main() -> int:
42
+ if sys.platform != "win32" or len(sys.argv) != 3:
43
+ return 2
44
+ return watch(int(sys.argv[1]), sys.argv[2])
45
+
46
+
47
+ if __name__ == "__main__":
48
+ raise SystemExit(main())
prkit/icon.py ADDED
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from PySide6.QtCore import QByteArray
4
+ from PySide6.QtGui import QIcon, QPainter, QPixmap
5
+ from PySide6.QtSvg import QSvgRenderer
6
+
7
+ TRAY_ICON_SVG = b"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
8
+ <defs><radialGradient id="g"><stop offset="0" stop-color="#fff"/>
9
+ <stop offset=".18" stop-color="#ffb3b3"/><stop offset=".48" stop-color="#f44336"/>
10
+ <stop offset="1" stop-color="#8b0000" stop-opacity=".25"/></radialGradient></defs>
11
+ <circle cx="32" cy="32" r="27" fill="url(#g)"/><circle cx="32" cy="32" r="8" fill="#fff"/>
12
+ </svg>"""
13
+
14
+
15
+ def tray_icon() -> QIcon:
16
+ renderer = QSvgRenderer(QByteArray(TRAY_ICON_SVG))
17
+ icon = QIcon()
18
+ for size in (16, 20, 24, 32, 48, 64):
19
+ pixmap = QPixmap(size, size)
20
+ pixmap.fill("transparent")
21
+ painter = QPainter(pixmap)
22
+ renderer.render(painter)
23
+ painter.end()
24
+ icon.addPixmap(pixmap)
25
+ return icon
prkit/keyboard.py ADDED
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import Any
5
+
6
+ from PySide6.QtCore import QObject, Signal
7
+
8
+ from prkit.keys import KeyChordTracker
9
+
10
+
11
+ class KeyboardMonitor(QObject):
12
+ chord_pressed = Signal(str)
13
+ all_released = Signal()
14
+ failed = Signal(str)
15
+
16
+ def __init__(self, parent: QObject | None = None) -> None:
17
+ super().__init__(parent)
18
+ self._listener: Any | None = None
19
+ self._tracker = KeyChordTracker()
20
+
21
+ @property
22
+ def running(self) -> bool:
23
+ return self._listener is not None
24
+
25
+ def start(self) -> tuple[bool, str | None]:
26
+ if self._listener is not None:
27
+ return True, None
28
+ try:
29
+ if sys.platform == "darwin":
30
+ from prkit.mac_keyboard import MacKeyboardListener
31
+
32
+ listener = MacKeyboardListener(self._on_press, self._on_release)
33
+ success, error = listener.start()
34
+ if not success:
35
+ return False, error
36
+ else:
37
+ from pynput import keyboard
38
+
39
+ listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release)
40
+ listener.start()
41
+ listener.wait()
42
+ self._listener = listener
43
+ except Exception as error: # pynput exposes backend-specific exceptions
44
+ self._listener = None
45
+ self._tracker.clear()
46
+ return False, str(error)
47
+ return True, None
48
+
49
+ def stop(self) -> None:
50
+ listener, self._listener = self._listener, None
51
+ self._tracker.clear()
52
+ if listener is not None:
53
+ listener.stop()
54
+
55
+ def _on_press(self, key: object) -> None:
56
+ try:
57
+ label = self._tracker.press(key)
58
+ if label:
59
+ self.chord_pressed.emit(label)
60
+ except Exception as error:
61
+ self.failed.emit(str(error))
62
+
63
+ def _on_release(self, key: object) -> None:
64
+ try:
65
+ if self._tracker.release(key):
66
+ self.all_released.emit()
67
+ except Exception as error:
68
+ self.failed.emit(str(error))
prkit/keys.py ADDED
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from collections.abc import Hashable
5
+ from dataclasses import dataclass, field
6
+
7
+ MODIFIER_ORDER = ("Ctrl", "⌘", "Alt", "⌥", "Shift", "Meta")
8
+
9
+ SPECIAL_KEYS = {
10
+ "alt": "⌥" if sys.platform == "darwin" else "Alt",
11
+ "alt_l": "⌥" if sys.platform == "darwin" else "Alt",
12
+ "alt_r": "⌥" if sys.platform == "darwin" else "Alt",
13
+ "alt_gr": "AltGr",
14
+ "backspace": "Backspace",
15
+ "caps_lock": "Caps Lock",
16
+ "cmd": "⌘" if sys.platform == "darwin" else "Meta",
17
+ "cmd_l": "⌘" if sys.platform == "darwin" else "Meta",
18
+ "cmd_r": "⌘" if sys.platform == "darwin" else "Meta",
19
+ "ctrl": "Ctrl",
20
+ "ctrl_l": "Ctrl",
21
+ "ctrl_r": "Ctrl",
22
+ "delete": "Delete",
23
+ "down": "↓",
24
+ "end": "End",
25
+ "enter": "Enter",
26
+ "esc": "Esc",
27
+ "f1": "F1",
28
+ "f2": "F2",
29
+ "f3": "F3",
30
+ "f4": "F4",
31
+ "f5": "F5",
32
+ "f6": "F6",
33
+ "f7": "F7",
34
+ "f8": "F8",
35
+ "f9": "F9",
36
+ "f10": "F10",
37
+ "f11": "F11",
38
+ "f12": "F12",
39
+ "home": "Home",
40
+ "insert": "Insert",
41
+ "left": "←",
42
+ "media_next": "Next Track",
43
+ "media_play_pause": "Play/Pause",
44
+ "media_previous": "Previous Track",
45
+ "page_down": "Page Down",
46
+ "page_up": "Page Up",
47
+ "pause": "Pause",
48
+ "print_screen": "Print Screen",
49
+ "right": "→",
50
+ "scroll_lock": "Scroll Lock",
51
+ "shift": "Shift",
52
+ "shift_l": "Shift",
53
+ "shift_r": "Shift",
54
+ "space": "Space",
55
+ "tab": "Tab",
56
+ "up": "↑",
57
+ }
58
+
59
+
60
+ def key_identity(key: object) -> Hashable:
61
+ name = getattr(key, "name", None)
62
+ if name:
63
+ return ("name", name)
64
+ vk = getattr(key, "vk", None)
65
+ if vk is not None:
66
+ return ("vk", vk)
67
+ char = getattr(key, "char", None)
68
+ if char is not None:
69
+ return ("char", char)
70
+ return ("object", str(key))
71
+
72
+
73
+ def key_label(key: object) -> str:
74
+ name = getattr(key, "name", None)
75
+ if name:
76
+ return SPECIAL_KEYS.get(name, name.replace("_", " ").title())
77
+ char = getattr(key, "char", None)
78
+ if char is not None:
79
+ if char == " ":
80
+ return "Space"
81
+ return char.upper() if len(char) == 1 and char.isalpha() else char
82
+ text = str(key)
83
+ if text.startswith("Key."):
84
+ text = text[4:]
85
+ return SPECIAL_KEYS.get(text, text.replace("_", " ").title())
86
+
87
+
88
+ @dataclass(slots=True)
89
+ class KeyChordTracker:
90
+ _pressed: dict[Hashable, str] = field(default_factory=dict)
91
+
92
+ def press(self, key: object) -> str | None:
93
+ identity = key_identity(key)
94
+ if identity in self._pressed:
95
+ return None
96
+ self._pressed[identity] = key_label(key)
97
+ return self.label
98
+
99
+ def release(self, key: object) -> bool:
100
+ self._pressed.pop(key_identity(key), None)
101
+ return not self._pressed
102
+
103
+ def clear(self) -> None:
104
+ self._pressed.clear()
105
+
106
+ @property
107
+ def label(self) -> str:
108
+ labels = list(dict.fromkeys(self._pressed.values()))
109
+ modifiers = [label for label in MODIFIER_ORDER if label in labels]
110
+ ordinary = [label for label in labels if label not in MODIFIER_ORDER]
111
+ return " + ".join([*modifiers, *ordinary])
prkit/mac_keyboard.py ADDED
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass
6
+
7
+ from CoreFoundation import (
8
+ CFMachPortCreateRunLoopSource,
9
+ CFRunLoopAddSource,
10
+ CFRunLoopGetCurrent,
11
+ CFRunLoopRun,
12
+ CFRunLoopStop,
13
+ kCFRunLoopDefaultMode,
14
+ )
15
+ from Quartz import (
16
+ CGEventGetFlags,
17
+ CGEventGetIntegerValueField,
18
+ CGEventMaskBit,
19
+ CGEventTapCreate,
20
+ CGEventTapEnable,
21
+ CGPreflightListenEventAccess,
22
+ kCGEventFlagMaskAlternate,
23
+ kCGEventFlagMaskCommand,
24
+ kCGEventFlagMaskControl,
25
+ kCGEventFlagMaskShift,
26
+ kCGEventFlagsChanged,
27
+ kCGEventKeyDown,
28
+ kCGEventKeyUp,
29
+ kCGEventTapOptionListenOnly,
30
+ kCGHeadInsertEventTap,
31
+ kCGHIDEventTap,
32
+ kCGKeyboardEventKeycode,
33
+ )
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class MacKey:
38
+ name: str | None = None
39
+ char: str | None = None
40
+ vk: int | None = None
41
+
42
+
43
+ SPECIAL_KEYCODES = {
44
+ 36: "enter",
45
+ 48: "tab",
46
+ 49: "space",
47
+ 51: "backspace",
48
+ 53: "esc",
49
+ 54: "cmd_r",
50
+ 55: "cmd_l",
51
+ 56: "shift_l",
52
+ 57: "caps_lock",
53
+ 58: "alt_l",
54
+ 59: "ctrl_l",
55
+ 60: "shift_r",
56
+ 61: "alt_r",
57
+ 62: "ctrl_r",
58
+ 117: "delete",
59
+ 123: "left",
60
+ 124: "right",
61
+ 125: "down",
62
+ 126: "up",
63
+ }
64
+
65
+ CHARACTER_KEYCODES = {
66
+ 0: "a",
67
+ 1: "s",
68
+ 2: "d",
69
+ 3: "f",
70
+ 4: "h",
71
+ 5: "g",
72
+ 6: "z",
73
+ 7: "x",
74
+ 8: "c",
75
+ 9: "v",
76
+ 11: "b",
77
+ 12: "q",
78
+ 13: "w",
79
+ 14: "e",
80
+ 15: "r",
81
+ 16: "y",
82
+ 17: "t",
83
+ 18: "1",
84
+ 19: "2",
85
+ 20: "3",
86
+ 21: "4",
87
+ 22: "6",
88
+ 23: "5",
89
+ 25: "9",
90
+ 26: "7",
91
+ 28: "8",
92
+ 29: "0",
93
+ 31: "o",
94
+ 32: "u",
95
+ 34: "i",
96
+ 35: "p",
97
+ 37: "l",
98
+ 38: "j",
99
+ 40: "k",
100
+ 24: "=",
101
+ 27: "-",
102
+ 30: "]",
103
+ 33: "[",
104
+ 39: "'",
105
+ 41: ";",
106
+ 42: "\\",
107
+ 43: ",",
108
+ 44: "/",
109
+ 45: "n",
110
+ 46: "m",
111
+ 47: ".",
112
+ 50: "`",
113
+ }
114
+
115
+ MODIFIER_FLAGS = {
116
+ 55: kCGEventFlagMaskCommand,
117
+ 56: kCGEventFlagMaskShift,
118
+ 58: kCGEventFlagMaskAlternate,
119
+ 59: kCGEventFlagMaskControl,
120
+ }
121
+
122
+
123
+ class MacKeyboardListener:
124
+ """Passive macOS event monitor that cannot consume or alter keystrokes."""
125
+
126
+ def __init__(
127
+ self,
128
+ on_press: Callable[[object], None],
129
+ on_release: Callable[[object], None],
130
+ ) -> None:
131
+ self._on_press = on_press
132
+ self._on_release = on_release
133
+ self._thread: threading.Thread | None = None
134
+ self._loop: object | None = None
135
+ self._ready = threading.Event()
136
+ self._error: str | None = None
137
+ self._pressed_modifiers: set[int] = set()
138
+ self._pressed_keycodes: set[int] = set()
139
+
140
+ def start(self) -> tuple[bool, str | None]:
141
+ if not CGPreflightListenEventAccess():
142
+ return False, "請在系統設定的「輸入監控」中允許 ChatGPT"
143
+ self._ready.clear()
144
+ self._error = None
145
+ self._thread = threading.Thread(target=self._run, name="prkit-mac-keyboard", daemon=True)
146
+ self._thread.start()
147
+ if not self._ready.wait(timeout=2):
148
+ return False, "鍵盤事件監聽啟動逾時"
149
+ return self._error is None, self._error
150
+
151
+ def stop(self) -> None:
152
+ loop, self._loop = self._loop, None
153
+ if loop is not None:
154
+ CFRunLoopStop(loop)
155
+ self._thread = None
156
+ self._pressed_modifiers.clear()
157
+ self._pressed_keycodes.clear()
158
+
159
+ def _run(self) -> None:
160
+ mask = (
161
+ CGEventMaskBit(kCGEventKeyDown)
162
+ | CGEventMaskBit(kCGEventKeyUp)
163
+ | CGEventMaskBit(kCGEventFlagsChanged)
164
+ )
165
+ tap = CGEventTapCreate(
166
+ kCGHIDEventTap,
167
+ kCGHeadInsertEventTap,
168
+ kCGEventTapOptionListenOnly,
169
+ mask,
170
+ self._handle_event,
171
+ None,
172
+ )
173
+ if tap is None:
174
+ self._error = "無法建立 macOS 唯讀鍵盤事件監看器"
175
+ self._ready.set()
176
+ return
177
+ source = CFMachPortCreateRunLoopSource(None, tap, 0)
178
+ self._loop = CFRunLoopGetCurrent()
179
+ CFRunLoopAddSource(self._loop, source, kCFRunLoopDefaultMode)
180
+ CGEventTapEnable(tap, True)
181
+ self._ready.set()
182
+ CFRunLoopRun()
183
+
184
+ def _handle_event(
185
+ self, proxy: object, event_type: int, event: object, refcon: object
186
+ ) -> object:
187
+ del proxy, refcon
188
+ keycode = int(CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode))
189
+ if event_type == kCGEventFlagsChanged:
190
+ self._update_modifiers(int(CGEventGetFlags(event)))
191
+ if keycode == 57:
192
+ self._press_transient_key(keycode)
193
+ return event
194
+ key = self._key(keycode)
195
+ if key is None:
196
+ return event
197
+ if event_type == kCGEventKeyDown:
198
+ self._pressed_keycodes.add(keycode)
199
+ self._on_press(key)
200
+ elif event_type == kCGEventKeyUp and keycode in self._pressed_keycodes:
201
+ self._release_key(keycode)
202
+ return event
203
+
204
+ def _update_modifiers(self, flags: int) -> None:
205
+ for keycode, flag in MODIFIER_FLAGS.items():
206
+ is_pressed = bool(flags & flag)
207
+ was_pressed = keycode in self._pressed_modifiers
208
+ if is_pressed and not was_pressed:
209
+ self._pressed_modifiers.add(keycode)
210
+ self._pressed_keycodes.add(keycode)
211
+ self._on_press(self._key(keycode))
212
+ elif not is_pressed and was_pressed:
213
+ self._pressed_modifiers.remove(keycode)
214
+ self._pressed_keycodes.discard(keycode)
215
+ self._on_release(self._key(keycode))
216
+
217
+ def _press_transient_key(self, keycode: int) -> None:
218
+ key = self._key(keycode)
219
+ if key is None:
220
+ return
221
+ self._pressed_keycodes.add(keycode)
222
+ self._on_press(key)
223
+ self._release_key(keycode)
224
+
225
+ def _release_key(self, keycode: int) -> None:
226
+ if keycode not in self._pressed_keycodes:
227
+ return
228
+ self._pressed_keycodes.remove(keycode)
229
+ self._on_release(self._key(keycode))
230
+
231
+ @staticmethod
232
+ def _key(keycode: int) -> MacKey | None:
233
+ if keycode == 255:
234
+ return None
235
+ name = SPECIAL_KEYCODES.get(keycode)
236
+ return (
237
+ MacKey(name=name, vk=keycode)
238
+ if name
239
+ else MacKey(char=CHARACTER_KEYCODES.get(keycode, f"Key {keycode}"), vk=keycode)
240
+ )