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.
@@ -0,0 +1,467 @@
1
+ from __future__ import annotations
2
+
3
+ import ctypes
4
+ import math
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import time
9
+ import uuid
10
+ from ctypes import wintypes
11
+ from pathlib import Path
12
+
13
+ from prkit.cursor_watchdog import restore_system_cursors
14
+ from prkit.settings import SpotlightSettings
15
+
16
+ DIB_RGB_COLORS = 0
17
+ DI_NORMAL = 0x0003
18
+ BI_RGB = 0
19
+ IMAGE_CURSOR = 2
20
+ SPI_SETCURSORS = 0x0057
21
+ CREATE_NO_WINDOW = 0x08000000
22
+
23
+ SYSTEM_CURSOR_IDS = (
24
+ 32512, # OCR_NORMAL
25
+ 32513, # OCR_IBEAM
26
+ 32514, # OCR_WAIT
27
+ 32515, # OCR_CROSS
28
+ 32516, # OCR_UP
29
+ 32642, # OCR_SIZENWSE
30
+ 32643, # OCR_SIZENESW
31
+ 32644, # OCR_SIZEWE
32
+ 32645, # OCR_SIZENS
33
+ 32646, # OCR_SIZEALL
34
+ 32648, # OCR_NO
35
+ 32649, # OCR_HAND
36
+ 32650, # OCR_APPSTARTING
37
+ 32651, # OCR_HELP
38
+ )
39
+
40
+
41
+ class CursorBackendError(RuntimeError):
42
+ pass
43
+
44
+
45
+ class ICONINFO(ctypes.Structure):
46
+ _fields_ = [
47
+ ("fIcon", wintypes.BOOL),
48
+ ("xHotspot", wintypes.DWORD),
49
+ ("yHotspot", wintypes.DWORD),
50
+ ("hbmMask", wintypes.HBITMAP),
51
+ ("hbmColor", wintypes.HBITMAP),
52
+ ]
53
+
54
+
55
+ class BITMAPINFOHEADER(ctypes.Structure):
56
+ _fields_ = [
57
+ ("biSize", wintypes.DWORD),
58
+ ("biWidth", wintypes.LONG),
59
+ ("biHeight", wintypes.LONG),
60
+ ("biPlanes", wintypes.WORD),
61
+ ("biBitCount", wintypes.WORD),
62
+ ("biCompression", wintypes.DWORD),
63
+ ("biSizeImage", wintypes.DWORD),
64
+ ("biXPelsPerMeter", wintypes.LONG),
65
+ ("biYPelsPerMeter", wintypes.LONG),
66
+ ("biClrUsed", wintypes.DWORD),
67
+ ("biClrImportant", wintypes.DWORD),
68
+ ]
69
+
70
+
71
+ class RGBQUAD(ctypes.Structure):
72
+ _fields_ = [
73
+ ("rgbBlue", ctypes.c_ubyte),
74
+ ("rgbGreen", ctypes.c_ubyte),
75
+ ("rgbRed", ctypes.c_ubyte),
76
+ ("rgbReserved", ctypes.c_ubyte),
77
+ ]
78
+
79
+
80
+ class BITMAPINFO(ctypes.Structure):
81
+ _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", RGBQUAD * 1)]
82
+
83
+
84
+ class MONO_BITMAPINFO(ctypes.Structure):
85
+ _fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", RGBQUAD * 2)]
86
+
87
+
88
+ class BITMAP(ctypes.Structure):
89
+ _fields_ = [
90
+ ("bmType", wintypes.LONG),
91
+ ("bmWidth", wintypes.LONG),
92
+ ("bmHeight", wintypes.LONG),
93
+ ("bmWidthBytes", wintypes.LONG),
94
+ ("bmPlanes", wintypes.WORD),
95
+ ("bmBitsPixel", wintypes.WORD),
96
+ ("bmBits", ctypes.c_void_p),
97
+ ]
98
+
99
+
100
+ class CursorWatchdog:
101
+ def __init__(self) -> None:
102
+ self._kernel32 = ctypes.windll.kernel32
103
+ self._kernel32.CreateEventW.restype = wintypes.HANDLE
104
+ self._event_name = f"Local\\PrtoolsCursorRestore-{uuid.uuid4()}"
105
+ self._event = self._kernel32.CreateEventW(None, True, False, self._event_name)
106
+ if not self._event:
107
+ raise CursorBackendError("無法建立游標復原事件")
108
+ try:
109
+ self._process = subprocess.Popen(
110
+ self._command(),
111
+ creationflags=CREATE_NO_WINDOW,
112
+ close_fds=True,
113
+ )
114
+ time.sleep(0.05)
115
+ if self._process.poll() is not None:
116
+ raise CursorBackendError("游標復原監控程序啟動失敗")
117
+ except Exception:
118
+ self._kernel32.CloseHandle(self._event)
119
+ self._event = None
120
+ raise
121
+
122
+ def _command(self) -> list[str]:
123
+ sibling = Path(sys.executable).with_name("prkit-cursor-watchdog.exe")
124
+ if sibling.is_file():
125
+ return [str(sibling), str(os.getpid()), self._event_name]
126
+ if "__compiled__" in globals():
127
+ raise CursorBackendError("找不到 prkit-cursor-watchdog.exe")
128
+ pythonw = Path(sys.executable).with_name("pythonw.exe")
129
+ executable = pythonw if pythonw.is_file() else Path(sys.executable)
130
+ return [
131
+ str(executable),
132
+ "-m",
133
+ "prkit.cursor_watchdog",
134
+ str(os.getpid()),
135
+ self._event_name,
136
+ ]
137
+
138
+ def stop(self) -> None:
139
+ if not self._event:
140
+ return
141
+ self._kernel32.SetEvent(self._event)
142
+ try:
143
+ self._process.wait(timeout=2)
144
+ except subprocess.TimeoutExpired:
145
+ self._process.terminate()
146
+ self._kernel32.CloseHandle(self._event)
147
+ self._event = None
148
+
149
+
150
+ class WindowsCursorSpotlight:
151
+ def __init__(self, settings: SpotlightSettings) -> None:
152
+ self._color = settings.color
153
+ self._opacity = settings.opacity
154
+ self._diameter = settings.size
155
+ self._enabled = False
156
+ self._watchdog: CursorWatchdog | None = None
157
+
158
+ @property
159
+ def diameter(self) -> int:
160
+ return self._diameter
161
+
162
+ def set_enabled(self, enabled: bool) -> None:
163
+ if enabled == self._enabled:
164
+ return
165
+ if enabled:
166
+ watchdog = CursorWatchdog()
167
+ try:
168
+ self._replace_cursors()
169
+ except Exception:
170
+ restore_system_cursors()
171
+ watchdog.stop()
172
+ raise
173
+ self._watchdog = watchdog
174
+ self._enabled = True
175
+ else:
176
+ restore_system_cursors()
177
+ self._enabled = False
178
+ if self._watchdog:
179
+ self._watchdog.stop()
180
+ self._watchdog = None
181
+
182
+ def sync_position(self) -> None:
183
+ """The Windows implementation is the system cursor and needs no syncing."""
184
+
185
+ def set_appearance(self, color: str, opacity: int, diameter: int) -> None:
186
+ self._color = color
187
+ self._opacity = opacity
188
+ self._diameter = diameter
189
+ if self._enabled:
190
+ restore_system_cursors()
191
+ try:
192
+ self._replace_cursors()
193
+ except Exception:
194
+ restore_system_cursors()
195
+ self._enabled = False
196
+ if self._watchdog:
197
+ self._watchdog.stop()
198
+ self._watchdog = None
199
+ raise
200
+
201
+ def _replace_cursors(self) -> None:
202
+ user32 = ctypes.windll.user32
203
+ user32.LoadCursorW.restype = wintypes.HANDLE
204
+ user32.LoadCursorW.argtypes = [wintypes.HINSTANCE, ctypes.c_void_p]
205
+ user32.SetSystemCursor.argtypes = [wintypes.HANDLE, wintypes.DWORD]
206
+ user32.SetSystemCursor.restype = wintypes.BOOL
207
+ user32.DestroyCursor.argtypes = [wintypes.HANDLE]
208
+ originals: list[tuple[int, int]] = []
209
+ for cursor_id in SYSTEM_CURSOR_IDS:
210
+ cursor = user32.LoadCursorW(None, ctypes.c_void_p(cursor_id))
211
+ if cursor:
212
+ originals.append((cursor_id, cursor))
213
+ custom: list[tuple[int, int]] = []
214
+ owned_handles: set[int] = set()
215
+ try:
216
+ for cursor_id, original in originals:
217
+ cursor = _create_spotlight_cursor(
218
+ original, self._color, self._opacity, self._diameter
219
+ )
220
+ custom.append((cursor_id, cursor))
221
+ owned_handles.add(cursor)
222
+ for cursor_id, cursor in custom:
223
+ if not user32.SetSystemCursor(cursor, cursor_id):
224
+ raise CursorBackendError(f"無法替換系統游標 {cursor_id}")
225
+ owned_handles.remove(cursor)
226
+ except Exception:
227
+ for cursor in owned_handles:
228
+ user32.DestroyCursor(cursor)
229
+ raise
230
+ if not custom:
231
+ raise CursorBackendError("找不到可替換的 Windows 系統游標")
232
+
233
+
234
+ def _create_spotlight_cursor(original: int, color: str, opacity: int, size: int) -> int:
235
+ user32 = ctypes.windll.user32
236
+ gdi32 = ctypes.windll.gdi32
237
+ user32.GetIconInfo.argtypes = [wintypes.HANDLE, ctypes.POINTER(ICONINFO)]
238
+ user32.GetIconInfo.restype = wintypes.BOOL
239
+ user32.GetDC.argtypes = [wintypes.HWND]
240
+ user32.GetDC.restype = wintypes.HDC
241
+ user32.ReleaseDC.argtypes = [wintypes.HWND, wintypes.HDC]
242
+ user32.DrawIconEx.argtypes = [
243
+ wintypes.HDC,
244
+ ctypes.c_int,
245
+ ctypes.c_int,
246
+ wintypes.HANDLE,
247
+ ctypes.c_int,
248
+ ctypes.c_int,
249
+ wintypes.UINT,
250
+ wintypes.HBRUSH,
251
+ wintypes.UINT,
252
+ ]
253
+ user32.CreateIconIndirect.argtypes = [ctypes.POINTER(ICONINFO)]
254
+ user32.CreateIconIndirect.restype = wintypes.HANDLE
255
+ gdi32.CreateDIBSection.restype = wintypes.HBITMAP
256
+ gdi32.CreateDIBSection.argtypes = [
257
+ wintypes.HDC,
258
+ ctypes.POINTER(BITMAPINFO),
259
+ wintypes.UINT,
260
+ ctypes.POINTER(ctypes.c_void_p),
261
+ wintypes.HANDLE,
262
+ wintypes.DWORD,
263
+ ]
264
+ gdi32.CreateBitmap.restype = wintypes.HBITMAP
265
+ gdi32.CreateBitmap.argtypes = [
266
+ ctypes.c_int,
267
+ ctypes.c_int,
268
+ wintypes.UINT,
269
+ wintypes.UINT,
270
+ ctypes.c_void_p,
271
+ ]
272
+ gdi32.CreateCompatibleDC.restype = wintypes.HDC
273
+ gdi32.CreateCompatibleDC.argtypes = [wintypes.HDC]
274
+ gdi32.SelectObject.restype = wintypes.HANDLE
275
+ gdi32.SelectObject.argtypes = [wintypes.HDC, wintypes.HANDLE]
276
+ gdi32.DeleteDC.argtypes = [wintypes.HDC]
277
+ gdi32.DeleteObject.argtypes = [wintypes.HANDLE]
278
+ gdi32.GetObjectW.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p]
279
+ gdi32.GetDIBits.argtypes = [
280
+ wintypes.HDC,
281
+ wintypes.HBITMAP,
282
+ wintypes.UINT,
283
+ wintypes.UINT,
284
+ ctypes.c_void_p,
285
+ ctypes.c_void_p,
286
+ wintypes.UINT,
287
+ ]
288
+ icon_info = ICONINFO()
289
+ if not user32.GetIconInfo(original, ctypes.byref(icon_info)):
290
+ raise CursorBackendError("無法讀取原始游標")
291
+ try:
292
+ bitmap_info = BITMAPINFO()
293
+ bitmap_info.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
294
+ bitmap_info.bmiHeader.biWidth = size
295
+ bitmap_info.bmiHeader.biHeight = -size
296
+ bitmap_info.bmiHeader.biPlanes = 1
297
+ bitmap_info.bmiHeader.biBitCount = 32
298
+ bitmap_info.bmiHeader.biCompression = BI_RGB
299
+ bits = ctypes.c_void_p()
300
+ screen_dc = user32.GetDC(None)
301
+ color_bitmap = gdi32.CreateDIBSection(
302
+ screen_dc,
303
+ ctypes.byref(bitmap_info),
304
+ DIB_RGB_COLORS,
305
+ ctypes.byref(bits),
306
+ None,
307
+ 0,
308
+ )
309
+ user32.ReleaseDC(None, screen_dc)
310
+ if not color_bitmap or not bits.value:
311
+ raise CursorBackendError("無法建立游標圖像")
312
+ mask_bitmap = gdi32.CreateBitmap(size, size, 1, 1, None)
313
+ if not mask_bitmap:
314
+ gdi32.DeleteObject(color_bitmap)
315
+ raise CursorBackendError("無法建立游標遮罩")
316
+ try:
317
+ _paint_circle(bits.value, color, opacity, size)
318
+ if icon_info.hbmColor:
319
+ memory_dc = gdi32.CreateCompatibleDC(None)
320
+ previous = gdi32.SelectObject(memory_dc, color_bitmap)
321
+ try:
322
+ user32.DrawIconEx(
323
+ memory_dc,
324
+ size // 2 - icon_info.xHotspot,
325
+ size // 2 - icon_info.yHotspot,
326
+ original,
327
+ 0,
328
+ 0,
329
+ 0,
330
+ None,
331
+ DI_NORMAL,
332
+ )
333
+ finally:
334
+ gdi32.SelectObject(memory_dc, previous)
335
+ gdi32.DeleteDC(memory_dc)
336
+ else:
337
+ _paint_monochrome_cursor(
338
+ bits.value,
339
+ icon_info.hbmMask,
340
+ icon_info.xHotspot,
341
+ icon_info.yHotspot,
342
+ size,
343
+ )
344
+ combined = ICONINFO(
345
+ False,
346
+ size // 2,
347
+ size // 2,
348
+ mask_bitmap,
349
+ color_bitmap,
350
+ )
351
+ cursor = user32.CreateIconIndirect(ctypes.byref(combined))
352
+ if not cursor:
353
+ raise CursorBackendError("無法建立合成游標")
354
+ return cursor
355
+ finally:
356
+ gdi32.DeleteObject(mask_bitmap)
357
+ gdi32.DeleteObject(color_bitmap)
358
+ finally:
359
+ if icon_info.hbmMask:
360
+ gdi32.DeleteObject(icon_info.hbmMask)
361
+ if icon_info.hbmColor:
362
+ gdi32.DeleteObject(icon_info.hbmColor)
363
+
364
+
365
+ def _paint_circle(address: int, color: str, opacity: int, size: int) -> None:
366
+ rgb = int(color.removeprefix("#"), 16)
367
+ red, green, blue = (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF
368
+ maximum_alpha = round(255 * opacity / 100)
369
+ radius = size / 2 - 1
370
+ center = (size - 1) / 2
371
+ pixels = (ctypes.c_uint32 * (size * size)).from_address(address)
372
+ for y in range(size):
373
+ for x in range(size):
374
+ distance = math.hypot(x - center, y - center)
375
+ coverage = max(0.0, min(1.0, radius + 0.75 - distance))
376
+ alpha = round(maximum_alpha * coverage)
377
+ premultiplied_red = red * alpha // 255
378
+ premultiplied_green = green * alpha // 255
379
+ premultiplied_blue = blue * alpha // 255
380
+ pixels[y * size + x] = (
381
+ alpha << 24
382
+ | premultiplied_red << 16
383
+ | premultiplied_green << 8
384
+ | premultiplied_blue
385
+ )
386
+
387
+
388
+ def _paint_monochrome_cursor(
389
+ destination: int,
390
+ mask_bitmap: int,
391
+ hotspot_x: int,
392
+ hotspot_y: int,
393
+ destination_size: int,
394
+ ) -> int:
395
+ """Composite a monochrome AND/XOR cursor mask without its bounding box."""
396
+ user32 = ctypes.windll.user32
397
+ gdi32 = ctypes.windll.gdi32
398
+ user32.GetDC.argtypes = [wintypes.HWND]
399
+ user32.GetDC.restype = wintypes.HDC
400
+ user32.ReleaseDC.argtypes = [wintypes.HWND, wintypes.HDC]
401
+ gdi32.GetObjectW.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p]
402
+ gdi32.GetDIBits.argtypes = [
403
+ wintypes.HDC,
404
+ wintypes.HBITMAP,
405
+ wintypes.UINT,
406
+ wintypes.UINT,
407
+ ctypes.c_void_p,
408
+ ctypes.c_void_p,
409
+ wintypes.UINT,
410
+ ]
411
+ bitmap = BITMAP()
412
+ if not gdi32.GetObjectW(mask_bitmap, ctypes.sizeof(bitmap), ctypes.byref(bitmap)):
413
+ raise CursorBackendError("無法讀取單色游標遮罩")
414
+ cursor_width = bitmap.bmWidth
415
+ cursor_height = bitmap.bmHeight // 2
416
+ mask_height = cursor_height * 2
417
+ stride = ((cursor_width + 31) // 32) * 4
418
+ mask_bytes = (ctypes.c_ubyte * (stride * mask_height))()
419
+ bitmap_info = MONO_BITMAPINFO()
420
+ bitmap_info.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
421
+ bitmap_info.bmiHeader.biWidth = cursor_width
422
+ bitmap_info.bmiHeader.biHeight = -mask_height
423
+ bitmap_info.bmiHeader.biPlanes = 1
424
+ bitmap_info.bmiHeader.biBitCount = 1
425
+ bitmap_info.bmiHeader.biCompression = BI_RGB
426
+ bitmap_info.bmiColors[1] = RGBQUAD(255, 255, 255, 0)
427
+ screen_dc = user32.GetDC(None)
428
+ try:
429
+ rows = gdi32.GetDIBits(
430
+ screen_dc,
431
+ mask_bitmap,
432
+ 0,
433
+ mask_height,
434
+ mask_bytes,
435
+ ctypes.byref(bitmap_info),
436
+ DIB_RGB_COLORS,
437
+ )
438
+ finally:
439
+ user32.ReleaseDC(None, screen_dc)
440
+ if rows != mask_height:
441
+ raise CursorBackendError("無法擷取單色游標遮罩")
442
+
443
+ def bit_at(x: int, y: int) -> int:
444
+ return (mask_bytes[y * stride + x // 8] >> (7 - x % 8)) & 1
445
+
446
+ pixels = (ctypes.c_uint32 * (destination_size * destination_size)).from_address(destination)
447
+ origin_x = destination_size // 2 - hotspot_x
448
+ origin_y = destination_size // 2 - hotspot_y
449
+ painted = 0
450
+ for cursor_y in range(cursor_height):
451
+ target_y = origin_y + cursor_y
452
+ if not 0 <= target_y < destination_size:
453
+ continue
454
+ for cursor_x in range(cursor_width):
455
+ target_x = origin_x + cursor_x
456
+ if not 0 <= target_x < destination_size:
457
+ continue
458
+ and_bit = bit_at(cursor_x, cursor_y)
459
+ xor_bit = bit_at(cursor_x, cursor_y + cursor_height)
460
+ if and_bit and not xor_bit:
461
+ continue
462
+ # Fixed black/white pixels replace XOR inversion. The translucent
463
+ # spotlight beneath them provides a predictable contrast surface.
464
+ pixel = 0xFFFFFFFF if xor_bit and not and_bit else 0xFF000000
465
+ pixels[target_y * destination_size + target_x] = pixel
466
+ painted += 1
467
+ return painted
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.3
2
+ Name: prkit
3
+ Version: 0.1.0
4
+ Summary: A cross-platform system-tray presentation toolkit
5
+ Author: meebox
6
+ Author-email: meebox <meebox@gmail.com>
7
+ Requires-Dist: pynput>=1.8.2
8
+ Requires-Dist: pyobjc-framework-cocoa>=12.2.1 ; sys_platform == 'darwin'
9
+ Requires-Dist: pyside6>=6.11.1
10
+ Requires-Dist: python-xlib>=0.33 ; sys_platform == 'linux'
11
+ Requires-Python: >=3.12
12
+ Project-URL: Homepage, https://github.com/codemee/prtools
13
+ Project-URL: Repository, https://github.com/codemee/prtools
14
+ Project-URL: Issues, https://github.com/codemee/prtools/issues
15
+ Description-Content-Type: text/markdown
16
+
17
+ # PRKit:簡報瑞士刀
18
+
19
+ [繁體中文](README.md) | [English](README.en.md)
20
+
21
+ 跨平台的系統匣簡報輔助工具。程式不會開啟主視窗,所有設定都在系統匣圖示的右鍵選單中完成。
22
+
23
+ ## 功能
24
+
25
+ - 聚光燈游標:可調整顏色、透明度與 48–320 px 圓圈大小;Windows 使用真正的系統游標,macOS/Linux 使用不攔截點擊的透明覆蓋層。
26
+ - 按鍵顯示:在游標所在螢幕中央下方顯示單鍵或組合鍵,不會攔截原本的鍵盤輸入。
27
+ - 自動保存:記住兩項功能的啟用狀態與外觀設定。
28
+ - 多螢幕與高 DPI:使用 Qt 邏輯像素定位與繪製。
29
+
30
+ ## 從 PyPI 安裝
31
+
32
+ 需要先安裝 [uv](https://docs.astral.sh/uv/),再執行:
33
+
34
+ ```powershell
35
+ uv tool install prkit
36
+ prkit
37
+ ```
38
+
39
+ 也可以從已下載的專案目錄安裝:
40
+
41
+ ```powershell
42
+ uv tool install .
43
+ prkit
44
+ ```
45
+
46
+ 或直接從 GitHub 安裝最新正式版;`latest` 標籤會在每次正式發布時更新:
47
+
48
+ ```powershell
49
+ uv tool install git+https://github.com/codemee/prtools.git@latest
50
+ prkit
51
+ ```
52
+
53
+ 若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.1.0`。
54
+
55
+ 若安裝後找不到 `prkit` 指令,執行 `uv tool update-shell`,再重新開啟終端機。移除工具可使用 `uv tool uninstall prkit`。
56
+
57
+ ## 使用 uvx 免安裝執行
58
+
59
+ 直接從 PyPI 執行:
60
+
61
+ ```powershell
62
+ uvx prkit
63
+ ```
64
+
65
+ 也可以在已下載的專案目錄執行,或直接從 GitHub 執行最新正式版:
66
+
67
+ ```powershell
68
+ uvx --from . prkit
69
+ uvx --from git+https://github.com/codemee/prtools.git@latest prkit
70
+ ```
71
+
72
+ `uvx` 會在隔離環境中準備並執行程式,不會把 `prkit` 持久安裝到工具目錄。
73
+
74
+ ## 開發
75
+
76
+ Python 3.12、虛擬環境及相依套件均由 uv 管理:
77
+
78
+ ```powershell
79
+ uv sync
80
+ uv run prkit
81
+ ```
82
+
83
+ 品質檢查:
84
+
85
+ ```powershell
86
+ uv run ruff check .
87
+ uv run ruff format --check .
88
+ uv run pytest -q
89
+ ```
90
+
91
+ ## 平台注意事項
92
+
93
+ - Windows:不需要額外權限。啟用聚光燈時會暫時替換系統游標,停用或結束時自動恢復;隨工具一併安裝的獨立 watchdog 會在主程式異常終止時復原游標。
94
+ - macOS:按鍵顯示使用 HID 層的唯讀事件監看,需要在「系統設定 → 隱私權與安全性 → 輸入監控」允許啟動本程式的應用程式(例如 ChatGPT、Terminal 或其他啟動 `prkit` 的終端程式)。監看器不會修改或攔截事件;即使組合鍵被其他快捷鍵工具處理,仍可先顯示按鍵。
95
+ - Linux:完整支援目標為 X11,桌面環境必須提供 StatusNotifierItem 或 XEmbed 系統匣。GNOME 可能需要 AppIndicator 類型的擴充套件。
96
+ - Wayland:受全域輸入與覆蓋層協定限制,按鍵顯示和點擊穿透不保證可用;程式會在選單中顯示警告。
97
+
98
+ 按鍵內容只用於即時畫面顯示,不會寫入設定、檔案或日誌。
99
+
100
+ 實作架構、macOS 原生視窗層級、權限模型與鍵盤事件流程請參閱[技術設計](docs/TECHNICAL.md)。
@@ -0,0 +1,19 @@
1
+ prkit/__init__.py,sha256=S3gN5txBfmJ41RnyRwk6vSMj2etHuB4PMWYmzDjqJnk,69
2
+ prkit/__main__.py,sha256=Yxf43osXdzsSFHRiC5vtFsNRlJi3oGtCHEnffBJFdv8,53
3
+ prkit/app.py,sha256=w9rPbBPvT0LIM9deASDmzeRmdftkOMX_UreTZBO6GdI,6797
4
+ prkit/cursor_watchdog.py,sha256=emSwvjv6Rq_OipLY46ga0358_4wRifKdpsHAi_bUdYg,1369
5
+ prkit/icon.py,sha256=4JpRX_BgYywdK-PIJBkUrII8j68HhHJNlzzuyMlPp54,932
6
+ prkit/keyboard.py,sha256=TE_KFo3iUTq6zDPBTeUc6y1EOM7HRP8wKxMBcPGlg4s,2110
7
+ prkit/keys.py,sha256=AINvI4kek5PzQBBJx2Vko49vCraeQcXsIAHNKtrH6l8,3132
8
+ prkit/mac_keyboard.py,sha256=a2c4AntRos2rMq9bMlaDcCowR08rSsFzet8bCAy6k3o,6417
9
+ prkit/overlays.py,sha256=SZgN7igtgxtAUub2YVc4py6aBBJ_wFjS-IGHIK3sHao,6056
10
+ prkit/platform.py,sha256=NOoxBOfsNsFaloUe2M8-wqQpFxG2K9RN88AGm_Aal0Q,4809
11
+ prkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ prkit/settings.py,sha256=pZs3lg-vD6Sa57yW4gkMcJBrOxSgTk3xSQ7v73U8jr0,3187
13
+ prkit/spotlight.py,sha256=Oodu3viROiaqwFbM3WFIAoIGH4ymEcv2MB75ixZx91A,687
14
+ prkit/tray.py,sha256=JF368eMWEzOSh1iYttyfD_YlZwk33Kzx1Kb6e6bISJU,8685
15
+ prkit/windows_cursor.py,sha256=W_8dg6IhXYsDfH60gSPLWKNJZC_us-Dokxvnl3YhFa4,16118
16
+ prkit-0.1.0.dist-info/WHEEL,sha256=4OL6Foqnnp3xRY5wMkjgc25_i5YJC6dKsC6LPcjqEoU,80
17
+ prkit-0.1.0.dist-info/entry_points.txt,sha256=kp0Fsg2I36s8FJwhwL61MIUltXf9Y1z5355OByVYmhI,93
18
+ prkit-0.1.0.dist-info/METADATA,sha256=SAp2rYiKuX1BZp3UouP6pl5PoisYq0rG105rClrGVgo,3566
19
+ prkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.5
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ prkit = prkit.app:main
3
+ prkit-cursor-watchdog = prkit.cursor_watchdog:main
4
+