prkit 0.2.0__tar.gz → 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: prkit
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A cross-platform system-tray presentation toolkit
5
5
  Author: meebox
6
6
  Author-email: meebox <meebox@gmail.com>
@@ -36,12 +36,12 @@ Description-Content-Type: text/markdown
36
36
 
37
37
  | 平台 | 下載檔案 | 啟動方式 |
38
38
  | --- | --- | --- |
39
- | Windows x64 | `PRKit-0.2.0-windows-x64.zip` | 完整解壓後執行 `PRKit/PRKit.exe` |
40
- | macOS Apple Silicon | `PRKit-0.2.0-macos-arm64.zip` | 解壓後將 `PRKit.app` 移到「應用程式」再開啟 |
39
+ | Windows x64 | `PRKit-0.2.1-windows-x64.exe` | 直接開啟執行 |
40
+ | macOS Apple Silicon | `PRKit-0.2.1-macos-arm64.dmg` | 開啟 DMG,將 `PRKit.app` 拖到「Applications」捷徑後啟動 |
41
41
 
42
- Windows 須保留同資料夾的 `prkit-cursor-watchdog.exe` 與 `_internal`,不可只複製主程式。macOS 目前僅提供 Apple Silicon 版。程式啟動後顯示在系統匣/選單列,不會開啟主視窗。
42
+ Windows 為單一 EXE,已包含執行環境與游標復原 watchdog,不需額外下載檔案。macOS 目前僅提供 Apple Silicon 版。程式啟動後顯示在系統匣/選單列,不會開啟主視窗。
43
43
 
44
- 桌面版尚未使用 Windows 發行者憑證或 Apple Developer ID 簽署/公證;macOS 僅使用 ad-hoc 簽章。若系統阻擋你確認來源的下載檔案,請依系統安全提示允許開啟。macOS 按鍵顯示仍需在「輸入監控」允許 `PRKit`;輸入法狀態模式不需此權限。每個 ZIP 另提供 `.sha256` 校驗檔。
44
+ 桌面版尚未使用 Windows 發行者憑證或 Apple Developer ID 簽署/公證;macOS 僅使用 ad-hoc 簽章。若系統阻擋你確認來源的下載檔案,請依系統安全提示允許開啟。macOS 按鍵顯示仍需在「輸入監控」允許 `PRKit`;輸入法狀態模式不需此權限。每個下載檔另提供 `.sha256` 校驗檔。
45
45
 
46
46
  版本標籤會自動觸發 GitHub 雲端打包與成品測試;手動建置可在 Actions 的 **Build desktop apps** 執行,成品在該次執行的 Artifacts。詳細操作見[發佈指南](docs/RELEASING.md)。
47
47
 
@@ -68,7 +68,7 @@ uv tool install git+https://github.com/codemee/prkit.git@latest
68
68
  prkit
69
69
  ```
70
70
 
71
- 若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.2.0`。
71
+ 若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.2.1`。
72
72
 
73
73
  若安裝後找不到 `prkit` 指令,執行 `uv tool update-shell`,再重新開啟終端機。移除工具可使用 `uv tool uninstall prkit`。
74
74
 
@@ -19,12 +19,12 @@
19
19
 
20
20
  | 平台 | 下載檔案 | 啟動方式 |
21
21
  | --- | --- | --- |
22
- | Windows x64 | `PRKit-0.2.0-windows-x64.zip` | 完整解壓後執行 `PRKit/PRKit.exe` |
23
- | macOS Apple Silicon | `PRKit-0.2.0-macos-arm64.zip` | 解壓後將 `PRKit.app` 移到「應用程式」再開啟 |
22
+ | Windows x64 | `PRKit-0.2.1-windows-x64.exe` | 直接開啟執行 |
23
+ | macOS Apple Silicon | `PRKit-0.2.1-macos-arm64.dmg` | 開啟 DMG,將 `PRKit.app` 拖到「Applications」捷徑後啟動 |
24
24
 
25
- Windows 須保留同資料夾的 `prkit-cursor-watchdog.exe` 與 `_internal`,不可只複製主程式。macOS 目前僅提供 Apple Silicon 版。程式啟動後顯示在系統匣/選單列,不會開啟主視窗。
25
+ Windows 為單一 EXE,已包含執行環境與游標復原 watchdog,不需額外下載檔案。macOS 目前僅提供 Apple Silicon 版。程式啟動後顯示在系統匣/選單列,不會開啟主視窗。
26
26
 
27
- 桌面版尚未使用 Windows 發行者憑證或 Apple Developer ID 簽署/公證;macOS 僅使用 ad-hoc 簽章。若系統阻擋你確認來源的下載檔案,請依系統安全提示允許開啟。macOS 按鍵顯示仍需在「輸入監控」允許 `PRKit`;輸入法狀態模式不需此權限。每個 ZIP 另提供 `.sha256` 校驗檔。
27
+ 桌面版尚未使用 Windows 發行者憑證或 Apple Developer ID 簽署/公證;macOS 僅使用 ad-hoc 簽章。若系統阻擋你確認來源的下載檔案,請依系統安全提示允許開啟。macOS 按鍵顯示仍需在「輸入監控」允許 `PRKit`;輸入法狀態模式不需此權限。每個下載檔另提供 `.sha256` 校驗檔。
28
28
 
29
29
  版本標籤會自動觸發 GitHub 雲端打包與成品測試;手動建置可在 Actions 的 **Build desktop apps** 執行,成品在該次執行的 Artifacts。詳細操作見[發佈指南](docs/RELEASING.md)。
30
30
 
@@ -51,7 +51,7 @@ uv tool install git+https://github.com/codemee/prkit.git@latest
51
51
  prkit
52
52
  ```
53
53
 
54
- 若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.2.0`。
54
+ 若要固定特定版本,將 `latest` 改成版本標籤,例如 `v0.2.1`。
55
55
 
56
56
  若安裝後找不到 `prkit` 指令,執行 `uv tool update-shell`,再重新開啟終端機。移除工具可使用 `uv tool uninstall prkit`。
57
57
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prkit"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "A cross-platform system-tray presentation toolkit"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "prkit"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "A cross-platform system-tray presentation toolkit"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,3 +1,3 @@
1
1
  """Presentation tools for the system tray."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
@@ -6,6 +6,7 @@ from ctypes import wintypes
6
6
 
7
7
  SPI_SETCURSORS = 0x0057
8
8
  SYNCHRONIZE = 0x00100000
9
+ EVENT_MODIFY_STATE = 0x0002
9
10
  WAIT_OBJECT_0 = 0
10
11
  INFINITE = 0xFFFFFFFF
11
12
 
@@ -14,10 +15,20 @@ def restore_system_cursors() -> None:
14
15
  ctypes.windll.user32.SystemParametersInfoW(SPI_SETCURSORS, 0, None, 0)
15
16
 
16
17
 
17
- def watch(parent_pid: int, stop_event_name: str) -> int:
18
+ def watch(parent_pid: int, stop_event_name: str, ready_event_name: str | None = None) -> int:
18
19
  kernel32 = ctypes.windll.kernel32
19
20
  kernel32.OpenProcess.restype = wintypes.HANDLE
21
+ kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
20
22
  kernel32.OpenEventW.restype = wintypes.HANDLE
23
+ kernel32.OpenEventW.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.LPCWSTR]
24
+ kernel32.SetEvent.argtypes = [wintypes.HANDLE]
25
+ kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
26
+ kernel32.WaitForMultipleObjects.argtypes = [
27
+ wintypes.DWORD,
28
+ ctypes.POINTER(wintypes.HANDLE),
29
+ wintypes.BOOL,
30
+ wintypes.DWORD,
31
+ ]
21
32
  parent = kernel32.OpenProcess(SYNCHRONIZE, False, parent_pid)
22
33
  stop_event = kernel32.OpenEventW(SYNCHRONIZE, False, stop_event_name)
23
34
  if not parent or not stop_event:
@@ -28,6 +39,15 @@ def watch(parent_pid: int, stop_event_name: str) -> int:
28
39
  kernel32.CloseHandle(stop_event)
29
40
  return 1
30
41
  try:
42
+ if ready_event_name is not None:
43
+ ready = kernel32.OpenEventW(EVENT_MODIFY_STATE, False, ready_event_name)
44
+ if not ready:
45
+ return 1
46
+ try:
47
+ if not kernel32.SetEvent(ready):
48
+ return 1
49
+ finally:
50
+ kernel32.CloseHandle(ready)
31
51
  handles = (wintypes.HANDLE * 2)(parent, stop_event)
32
52
  result = kernel32.WaitForMultipleObjects(2, handles, False, INFINITE)
33
53
  if result == WAIT_OBJECT_0:
@@ -39,9 +59,9 @@ def watch(parent_pid: int, stop_event_name: str) -> int:
39
59
 
40
60
 
41
61
  def main() -> int:
42
- if sys.platform != "win32" or len(sys.argv) != 3:
62
+ if sys.platform != "win32" or len(sys.argv) not in {3, 4}:
43
63
  return 2
44
- return watch(int(sys.argv[1]), sys.argv[2])
64
+ return watch(int(sys.argv[1]), sys.argv[2], sys.argv[3] if len(sys.argv) == 4 else None)
45
65
 
46
66
 
47
67
  if __name__ == "__main__":
@@ -101,29 +101,61 @@ class CursorWatchdog:
101
101
  def __init__(self) -> None:
102
102
  self._kernel32 = ctypes.windll.kernel32
103
103
  self._kernel32.CreateEventW.restype = wintypes.HANDLE
104
+ self._kernel32.CreateEventW.argtypes = [
105
+ ctypes.c_void_p,
106
+ wintypes.BOOL,
107
+ wintypes.BOOL,
108
+ wintypes.LPCWSTR,
109
+ ]
110
+ self._kernel32.SetEvent.argtypes = [wintypes.HANDLE]
111
+ self._kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
112
+ self._kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
104
113
  self._event_name = f"Local\\PrtoolsCursorRestore-{uuid.uuid4()}"
114
+ self._ready_event_name = self._event_name + "-ready"
105
115
  self._event = self._kernel32.CreateEventW(None, True, False, self._event_name)
106
116
  if not self._event:
107
117
  raise CursorBackendError("無法建立游標復原事件")
118
+ ready = self._kernel32.CreateEventW(None, True, False, self._ready_event_name)
119
+ if not ready:
120
+ self._kernel32.CloseHandle(self._event)
121
+ self._event = None
122
+ raise CursorBackendError("無法建立游標監控就緒事件")
108
123
  try:
124
+ environment = os.environ.copy()
125
+ if getattr(sys, "frozen", False):
126
+ # The watchdog must outlive the GUI and own its unpacked runtime.
127
+ environment["PYINSTALLER_RESET_ENVIRONMENT"] = "1"
109
128
  self._process = subprocess.Popen(
110
129
  self._command(),
111
130
  creationflags=CREATE_NO_WINDOW,
112
131
  close_fds=True,
132
+ env=environment,
113
133
  )
114
- time.sleep(0.05)
115
- if self._process.poll() is not None:
116
- raise CursorBackendError("游標復原監控程序啟動失敗")
134
+ deadline = time.monotonic() + 30
135
+ while self._kernel32.WaitForSingleObject(ready, 100) != 0:
136
+ if self._process.poll() is not None:
137
+ raise CursorBackendError("游標復原監控程序啟動失敗")
138
+ if time.monotonic() >= deadline:
139
+ raise CursorBackendError("游標復原監控程序啟動逾時")
117
140
  except Exception:
141
+ self._kernel32.SetEvent(self._event)
142
+ if hasattr(self, "_process") and self._process.poll() is None:
143
+ self._process.terminate()
144
+ self._process.wait(timeout=5)
118
145
  self._kernel32.CloseHandle(self._event)
119
146
  self._event = None
120
147
  raise
148
+ finally:
149
+ self._kernel32.CloseHandle(ready)
121
150
 
122
151
  def _command(self) -> list[str]:
152
+ arguments = [str(os.getpid()), self._event_name, self._ready_event_name]
153
+ if getattr(sys, "frozen", False):
154
+ return [sys.executable, "--cursor-watchdog", *arguments]
123
155
  sibling = Path(sys.executable).with_name("prkit-cursor-watchdog.exe")
124
156
  if sibling.is_file():
125
- return [str(sibling), str(os.getpid()), self._event_name]
126
- if getattr(sys, "frozen", False) or "__compiled__" in globals():
157
+ return [str(sibling), *arguments]
158
+ if "__compiled__" in globals():
127
159
  raise CursorBackendError("找不到 prkit-cursor-watchdog.exe")
128
160
  pythonw = Path(sys.executable).with_name("pythonw.exe")
129
161
  executable = pythonw if pythonw.is_file() else Path(sys.executable)
@@ -131,8 +163,7 @@ class CursorWatchdog:
131
163
  str(executable),
132
164
  "-m",
133
165
  "prkit.cursor_watchdog",
134
- str(os.getpid()),
135
- self._event_name,
166
+ *arguments,
136
167
  ]
137
168
 
138
169
  def stop(self) -> None:
@@ -140,7 +171,7 @@ class CursorWatchdog:
140
171
  return
141
172
  self._kernel32.SetEvent(self._event)
142
173
  try:
143
- self._process.wait(timeout=2)
174
+ self._process.wait(timeout=10)
144
175
  except subprocess.TimeoutExpired:
145
176
  self._process.terminate()
146
177
  self._kernel32.CloseHandle(self._event)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes