ascript 1.3.27__py3-none-win_amd64.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.
Files changed (177) hide show
  1. ascript/__init__.py +8 -0
  2. ascript/hooks/__init__.py +0 -0
  3. ascript/hooks/hook-ascript.py +2 -0
  4. ascript/windows/__init__.py +29 -0
  5. ascript/windows/daos/__init__.py +0 -0
  6. ascript/windows/daos/screen.py +143 -0
  7. ascript/windows/hardware/__init__.py +5 -0
  8. ascript/windows/hardware/keyboard.py +279 -0
  9. ascript/windows/hardware/logitech_driver.py +179 -0
  10. ascript/windows/hardware/mouse.py +308 -0
  11. ascript/windows/license/__init__.py +16 -0
  12. ascript/windows/license/achttp.cp310-win_amd64.pyd +0 -0
  13. ascript/windows/license/achttp.cp311-win_amd64.pyd +0 -0
  14. ascript/windows/license/achttp.cp312-win_amd64.pyd +0 -0
  15. ascript/windows/license/achttp.cp313-win_amd64.pyd +0 -0
  16. ascript/windows/license/achttp.cp314-win_amd64.pyd +0 -0
  17. ascript/windows/license/achttp.cp38-win_amd64.pyd +0 -0
  18. ascript/windows/license/achttp.cp39-win_amd64.pyd +0 -0
  19. ascript/windows/media/__init__.py +0 -0
  20. ascript/windows/resources/__init__.py +0 -0
  21. ascript/windows/resources/libs/logitech/x64/LogitechGkeyEnginesWrapper.dll +0 -0
  22. ascript/windows/resources/libs/logitech/x86/LogitechGkeyEnginesWrapper.dll +0 -0
  23. ascript/windows/resources/templates/__init__.py +0 -0
  24. ascript/windows/resources/templates/__init__py.win.template +51 -0
  25. ascript/windows/resources/templates/build.as.win.template +14 -0
  26. ascript/windows/resources/templates/build.py +71 -0
  27. ascript/windows/resources/templates/logo_windows.ico +0 -0
  28. ascript/windows/resources/templates/windows/main.py +50 -0
  29. ascript/windows/resources/templates/windows/pyproject.toml +44 -0
  30. ascript/windows/resources/templates/windows/res/img/logo.ico +0 -0
  31. ascript/windows/screen/__init__.py +13 -0
  32. ascript/windows/screen/as_cv.py +382 -0
  33. ascript/windows/screen/canny.py +128 -0
  34. ascript/windows/screen/code_scanner.py +72 -0
  35. ascript/windows/screen/color_algorithms.py +277 -0
  36. ascript/windows/screen/find_colors.py +251 -0
  37. ascript/windows/screen/find_images.py +217 -0
  38. ascript/windows/screen/ocr/__init__.py +4 -0
  39. ascript/windows/screen/ocr/ocr.py +223 -0
  40. ascript/windows/screen/ocr/rapidocr_hook.py +301 -0
  41. ascript/windows/screen/ocrx.py +169 -0
  42. ascript/windows/screen/screen_core.py +139 -0
  43. ascript/windows/store/__init__.py +21 -0
  44. ascript/windows/system/__init__.py +8 -0
  45. ascript/windows/system/device.py +355 -0
  46. ascript/windows/system/easy_print.py +115 -0
  47. ascript/windows/system/env.py +492 -0
  48. ascript/windows/system/exception/__init__.py +1 -0
  49. ascript/windows/system/key_value.py +145 -0
  50. ascript/windows/system/pip.py +175 -0
  51. ascript/windows/system/r.py +165 -0
  52. ascript/windows/system/runtime.py +62 -0
  53. ascript/windows/tools/__init__.py +27 -0
  54. ascript/windows/tools/__main__.py +21 -0
  55. ascript/windows/tools/action.json +33 -0
  56. ascript/windows/tools/action.py +168 -0
  57. ascript/windows/tools/colors_tool.py +391 -0
  58. ascript/windows/tools/config.py +26 -0
  59. ascript/windows/tools/dumptool.py +18 -0
  60. ascript/windows/tools/eel_api.py +495 -0
  61. ascript/windows/tools/gpplug/__init__.py +0 -0
  62. ascript/windows/tools/gpplug/plug.py +70 -0
  63. ascript/windows/tools/mouse_catch.py +168 -0
  64. ascript/windows/tools/plug_manager.py +38 -0
  65. ascript/windows/tools/tool_api.py +906 -0
  66. ascript/windows/tools/tool_window.py +56 -0
  67. ascript/windows/tools/uia_worker.py +365 -0
  68. ascript/windows/tools/web/app_store_window.html +481 -0
  69. ascript/windows/tools/web/colors.html +532 -0
  70. ascript/windows/tools/web/css/screen.css +1342 -0
  71. ascript/windows/tools/web/css/viewtree.css +357 -0
  72. ascript/windows/tools/web/flot_control.html +195 -0
  73. ascript/windows/tools/web/img/ico/favicon.png +0 -0
  74. ascript/windows/tools/web/img/ico/paintpalette.png +0 -0
  75. ascript/windows/tools/web/img/svg/find_colors.svg +1 -0
  76. ascript/windows/tools/web/img/svg/find_images.svg +1 -0
  77. ascript/windows/tools/web/img/svg/illustration-6.svg +89 -0
  78. ascript/windows/tools/web/img/svg/img_default.svg +1 -0
  79. ascript/windows/tools/web/img/svg/ocr.svg +1 -0
  80. ascript/windows/tools/web/js/screen/colors.js +466 -0
  81. ascript/windows/tools/web/js/screen/gp_plug.js +108 -0
  82. ascript/windows/tools/web/js/screen/image_mouse.js +464 -0
  83. ascript/windows/tools/web/js/screen/img_view.js +558 -0
  84. ascript/windows/tools/web/js/screen/plug.js +246 -0
  85. ascript/windows/tools/web/js/screen/screen.js +690 -0
  86. ascript/windows/tools/web/js/screen/screen_api.js +778 -0
  87. ascript/windows/tools/web/js/screen/screen_maker.js +10 -0
  88. ascript/windows/tools/web/js/utils/map.js +73 -0
  89. ascript/windows/tools/web/js/utils/upload.js +157 -0
  90. ascript/windows/tools/web/js/viewtree/niv_js.js +27 -0
  91. ascript/windows/tools/web/js/viewtree/vtree.js +1510 -0
  92. ascript/windows/tools/web/libs/bootstrap/css/bootstrap.min.css +6 -0
  93. ascript/windows/tools/web/libs/bootstrap/js/bootstrap.bundle.min.js +7 -0
  94. ascript/windows/tools/web/libs/bootstrap-icons/0-circle.svg +4 -0
  95. ascript/windows/tools/web/libs/bootstrap-icons/0-square.svg +4 -0
  96. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.css +2078 -0
  97. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.json +2052 -0
  98. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.min.css +5 -0
  99. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.scss +2090 -0
  100. ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff +0 -0
  101. ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff2 +0 -0
  102. ascript/windows/tools/web/libs/feather-icons/dist/feather.min.js +13 -0
  103. ascript/windows/tools/web/libs/feather-icons/dist/icons.json +1 -0
  104. ascript/windows/tools/web/libs/jquery/core.js +399 -0
  105. ascript/windows/tools/web/libs/jquery/jquery.js +10598 -0
  106. ascript/windows/tools/web/libs/jquery/jquery.min.js +4 -0
  107. ascript/windows/tools/web/libs/jquery/jquery.min.js.zip +0 -0
  108. ascript/windows/tools/web/libs/jquery/jquery.slim.js +8495 -0
  109. ascript/windows/tools/web/libs/jquery/jquery.slim.min.js +2 -0
  110. ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.js +2134 -0
  111. ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.min.css +16 -0
  112. ascript/windows/tools/web/libs/jquery-zTree/css/zTreeStyle.css +97 -0
  113. ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_close.png +0 -0
  114. ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_open.png +0 -0
  115. ascript/windows/tools/web/libs/jquery-zTree/img/diy/2.png +0 -0
  116. ascript/windows/tools/web/libs/jquery-zTree/img/diy/3.png +0 -0
  117. ascript/windows/tools/web/libs/jquery-zTree/img/diy/4.png +0 -0
  118. ascript/windows/tools/web/libs/jquery-zTree/img/diy/5.png +0 -0
  119. ascript/windows/tools/web/libs/jquery-zTree/img/diy/6.png +0 -0
  120. ascript/windows/tools/web/libs/jquery-zTree/img/diy/7.png +0 -0
  121. ascript/windows/tools/web/libs/jquery-zTree/img/diy/8.png +0 -0
  122. ascript/windows/tools/web/libs/jquery-zTree/img/diy/9.png +0 -0
  123. ascript/windows/tools/web/libs/jquery-zTree/img/line_conn.gif +0 -0
  124. ascript/windows/tools/web/libs/jquery-zTree/img/loading.gif +0 -0
  125. ascript/windows/tools/web/libs/jquery-zTree/img/tree-node-btn.png +0 -0
  126. ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.gif +0 -0
  127. ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.png +0 -0
  128. ascript/windows/tools/web/libs/jquery-zTree/js/jquery.ztree.all.min.js +3 -0
  129. ascript/windows/tools/web/libs/treeview/css/jquery.treeview.css +79 -0
  130. ascript/windows/tools/web/libs/treeview/img/file.png +0 -0
  131. ascript/windows/tools/web/libs/treeview/img/ico_loading.png +0 -0
  132. ascript/windows/tools/web/libs/treeview/img/icon4.png +0 -0
  133. ascript/windows/tools/web/libs/treeview/img/icon5.png +0 -0
  134. ascript/windows/tools/web/libs/treeview/img/icon6.png +0 -0
  135. ascript/windows/tools/web/libs/treeview/img/py.png +0 -0
  136. ascript/windows/tools/web/libs/treeview/img/treeview-black-line.gif +0 -0
  137. ascript/windows/tools/web/libs/treeview/img/treeview-black.gif +0 -0
  138. ascript/windows/tools/web/libs/treeview/img/treeview-default-line.gif +0 -0
  139. ascript/windows/tools/web/libs/treeview/img/treeview-default.gif +0 -0
  140. ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam-line.gif +0 -0
  141. ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam.gif +0 -0
  142. ascript/windows/tools/web/libs/treeview/img/treeview-gray-line.gif +0 -0
  143. ascript/windows/tools/web/libs/treeview/img/treeview-gray.gif +0 -0
  144. ascript/windows/tools/web/libs/treeview/img/treeview-red-line.gif +0 -0
  145. ascript/windows/tools/web/libs/treeview/img/treeview-red.gif +0 -0
  146. ascript/windows/tools/web/libs/treeview/js/jquery.treeview.edit.js +37 -0
  147. ascript/windows/tools/web/libs/treeview/js/jquery.treeview.js +250 -0
  148. ascript/windows/tools/web/license_window.html +255 -0
  149. ascript/windows/tools/web/plugs/a.html +653 -0
  150. ascript/windows/tools/web/plugs/demo.html +52 -0
  151. ascript/windows/tools/web/plugs/find_colors.html +629 -0
  152. ascript/windows/tools/web/plugs/find_images.html +699 -0
  153. ascript/windows/tools/web/plugs/rapidocr.html +954 -0
  154. ascript/windows/tools/web/vtree.html +496 -0
  155. ascript/windows/tools/webview/assets/index-HymBPl-I.css +1 -0
  156. ascript/windows/tools/webview/assets/index-PqA_m8tL.js +17 -0
  157. ascript/windows/tools/webview/index.html +13 -0
  158. ascript/windows/ui/__init__.py +6 -0
  159. ascript/windows/ui/app_store_window.py +272 -0
  160. ascript/windows/ui/draw_window.py +231 -0
  161. ascript/windows/ui/flot_test.py +297 -0
  162. ascript/windows/ui/flot_window.py +488 -0
  163. ascript/windows/ui/license_window.py +111 -0
  164. ascript/windows/ui/web_window.py +466 -0
  165. ascript/windows/utils/__init__.py +0 -0
  166. ascript/windows/utils/node_utils.py +20 -0
  167. ascript/windows/utils/screen_utils.py +280 -0
  168. ascript/windows/window/Selector.py +1015 -0
  169. ascript/windows/window/Window.py +747 -0
  170. ascript/windows/window/__init__.py +5 -0
  171. ascript/windows/window/launcher.py +213 -0
  172. ascript/windows/window/uia_runtime.py +26 -0
  173. ascript-1.3.27.dist-info/METADATA +41 -0
  174. ascript-1.3.27.dist-info/RECORD +177 -0
  175. ascript-1.3.27.dist-info/WHEEL +5 -0
  176. ascript-1.3.27.dist-info/entry_points.txt +2 -0
  177. ascript-1.3.27.dist-info/top_level.txt +1 -0
@@ -0,0 +1,747 @@
1
+ """Native Windows window discovery, control, input and capture helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import math
7
+ import os
8
+ import re
9
+ import time
10
+ from ctypes import wintypes
11
+ from enum import Enum, auto
12
+ from typing import List, NamedTuple, Optional, Sequence, Union
13
+
14
+ import psutil
15
+ import win32con
16
+ import win32gui
17
+ import win32process
18
+ import win32ui
19
+ from PIL import Image, ImageGrab
20
+ from ..hardware import Mouse
21
+
22
+
23
+ _USER32 = ctypes.windll.user32
24
+
25
+
26
+ def _enable_dpi_awareness() -> None:
27
+ try:
28
+ setter = _USER32.SetProcessDpiAwarenessContext
29
+ setter.argtypes = (ctypes.c_void_p,)
30
+ setter.restype = wintypes.BOOL
31
+ setter(ctypes.c_void_p(-4))
32
+ return
33
+ except (AttributeError, OSError):
34
+ pass
35
+ try:
36
+ ctypes.windll.shcore.SetProcessDpiAwareness(2)
37
+ except (AttributeError, OSError):
38
+ try:
39
+ _USER32.SetProcessDPIAware()
40
+ except (AttributeError, OSError):
41
+ pass
42
+
43
+
44
+ _enable_dpi_awareness()
45
+
46
+ _IS_HUNG_APP_WINDOW = getattr(_USER32, "IsHungAppWindow", None)
47
+ if _IS_HUNG_APP_WINDOW is not None:
48
+ _IS_HUNG_APP_WINDOW.argtypes = (wintypes.HWND,)
49
+ _IS_HUNG_APP_WINDOW.restype = wintypes.BOOL
50
+
51
+ _DWM_GET_WINDOW_ATTRIBUTE = None
52
+ try:
53
+ _DWM_GET_WINDOW_ATTRIBUTE = ctypes.windll.dwmapi.DwmGetWindowAttribute
54
+ _DWM_GET_WINDOW_ATTRIBUTE.argtypes = (
55
+ wintypes.HWND, wintypes.DWORD, ctypes.c_void_p, wintypes.DWORD
56
+ )
57
+ _DWM_GET_WINDOW_ATTRIBUTE.restype = ctypes.c_long
58
+ except (AttributeError, OSError):
59
+ pass
60
+
61
+ _DWMWA_EXTENDED_FRAME_BOUNDS = 9
62
+ _PW_CLIENTONLY = 0x1
63
+ _PW_RENDERFULLCONTENT = 0x2
64
+
65
+
66
+ class _GUITHREADINFO(ctypes.Structure):
67
+ _fields_ = [
68
+ ("cbSize", wintypes.DWORD),
69
+ ("flags", wintypes.DWORD),
70
+ ("hwndActive", wintypes.HWND),
71
+ ("hwndFocus", wintypes.HWND),
72
+ ("hwndCapture", wintypes.HWND),
73
+ ("hwndMenuOwner", wintypes.HWND),
74
+ ("hwndMoveSize", wintypes.HWND),
75
+ ("hwndCaret", wintypes.HWND),
76
+ ("rcCaret", wintypes.RECT),
77
+ ]
78
+
79
+
80
+ class WindowRect(NamedTuple):
81
+ """A rectangle in physical screen coordinates."""
82
+
83
+ left: int
84
+ top: int
85
+ right: int
86
+ bottom: int
87
+ width: int
88
+ height: int
89
+
90
+ @classmethod
91
+ def from_ltrb(cls, left: int, top: int, right: int, bottom: int) -> "WindowRect":
92
+ return cls(left, top, right, bottom, right - left, bottom - top)
93
+
94
+
95
+ class WindowCaptureFormat(Enum):
96
+ PIL = auto()
97
+ NUMPY = auto()
98
+ OPENCV = auto()
99
+
100
+ def convert(self, pil_image: Image.Image):
101
+ if self is WindowCaptureFormat.PIL:
102
+ return pil_image
103
+ import numpy as np
104
+
105
+ array = np.asarray(pil_image)
106
+ if self is WindowCaptureFormat.NUMPY:
107
+ return array
108
+ if self is WindowCaptureFormat.OPENCV:
109
+ return array[:, :, ::-1].copy()
110
+ raise ValueError(f"Unsupported capture format: {self!r}")
111
+
112
+
113
+ class Window:
114
+ """A lightweight wrapper around a top-level Windows HWND."""
115
+
116
+ def __init__(self, hwnd: int):
117
+ if not isinstance(hwnd, int) or isinstance(hwnd, bool):
118
+ raise TypeError("hwnd must be an integer")
119
+ self.hwnd = hwnd
120
+ self._pid: Optional[int] = None
121
+
122
+ def __repr__(self) -> str:
123
+ try:
124
+ return f"<Window title={self.title!r} process={self.process_name!r} hwnd={self.hwnd}>"
125
+ except (OSError, win32gui.error):
126
+ return f"<Window hwnd={self.hwnd} invalid>"
127
+
128
+ def __bool__(self) -> bool:
129
+ return self.is_valid
130
+
131
+ def __eq__(self, other) -> bool:
132
+ return isinstance(other, Window) and self.hwnd == other.hwnd
133
+
134
+ def __hash__(self) -> int:
135
+ return hash(self.hwnd)
136
+
137
+ @staticmethod
138
+ def _compile_title_pattern(title_re: Optional[str]):
139
+ if title_re is None:
140
+ return None
141
+ if not isinstance(title_re, str):
142
+ raise TypeError("title_re must be a string or None")
143
+ try:
144
+ return re.compile(title_re)
145
+ except re.error as exc:
146
+ raise ValueError(f"invalid title regular expression: {exc}") from exc
147
+
148
+ @classmethod
149
+ def active(cls) -> Optional["Window"]:
150
+ hwnd = win32gui.GetForegroundWindow()
151
+ return cls(hwnd) if hwnd else None
152
+
153
+ @staticmethod
154
+ def screen_size() -> tuple[int, int]:
155
+ """Return the virtual desktop size, including secondary monitors."""
156
+ return (
157
+ int(_USER32.GetSystemMetrics(78)),
158
+ int(_USER32.GetSystemMetrics(79)),
159
+ )
160
+
161
+ @classmethod
162
+ def find_all(
163
+ cls,
164
+ title_re: Optional[str] = None,
165
+ process_name: Optional[str] = None,
166
+ class_name: Optional[str] = None,
167
+ visible_only: bool = True,
168
+ ) -> List["Window"]:
169
+ pattern = cls._compile_title_pattern(title_re)
170
+ process_filter = process_name.casefold() if process_name else None
171
+ results: List[Window] = []
172
+
173
+ def callback(hwnd, _):
174
+ try:
175
+ if visible_only and not win32gui.IsWindowVisible(hwnd):
176
+ return True
177
+ candidate = cls(hwnd)
178
+ if class_name is not None and candidate.class_name != class_name:
179
+ return True
180
+ if process_filter is not None and process_filter not in candidate.process_name.casefold():
181
+ return True
182
+ if pattern is not None and pattern.search(candidate.title) is None:
183
+ return True
184
+ results.append(candidate)
185
+ except (OSError, psutil.Error, win32gui.error):
186
+ pass
187
+ return True
188
+
189
+ win32gui.EnumWindows(callback, None)
190
+ return results
191
+
192
+ @classmethod
193
+ def find(
194
+ cls,
195
+ title_re: Optional[str] = None,
196
+ process_name: Optional[str] = None,
197
+ class_name: Optional[str] = None,
198
+ visible_only: bool = True,
199
+ timeout: float = 0,
200
+ ) -> Optional["Window"]:
201
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
202
+ raise TypeError("timeout must be a number")
203
+ timeout = float(timeout)
204
+ if timeout < 0 or not math.isfinite(timeout):
205
+ raise ValueError("timeout must be finite and non-negative")
206
+ deadline = time.monotonic() + timeout
207
+ while True:
208
+ matches = cls.find_all(title_re, process_name, class_name, visible_only)
209
+ if matches:
210
+ return matches[0]
211
+ remaining = deadline - time.monotonic()
212
+ if timeout == 0 or remaining <= 0:
213
+ return None
214
+ time.sleep(min(0.2, remaining))
215
+
216
+ def find_children(
217
+ self,
218
+ title_re: Optional[str] = None,
219
+ class_name: Optional[str] = None,
220
+ visible_only: bool = True,
221
+ ) -> List["Window"]:
222
+ """Return descendant windows matching stable window attributes."""
223
+ if not self.is_valid:
224
+ return []
225
+ pattern = self._compile_title_pattern(title_re)
226
+ results: List[Window] = []
227
+
228
+ def callback(hwnd, _):
229
+ try:
230
+ if visible_only and not win32gui.IsWindowVisible(hwnd):
231
+ return True
232
+ candidate = Window(int(hwnd))
233
+ if class_name is not None and candidate.class_name != class_name:
234
+ return True
235
+ if pattern is not None and pattern.search(candidate.title) is None:
236
+ return True
237
+ results.append(candidate)
238
+ except (OSError, psutil.Error, win32gui.error):
239
+ pass
240
+ return True
241
+
242
+ win32gui.EnumChildWindows(self.hwnd, callback, None)
243
+ return results
244
+
245
+ def find_child(
246
+ self,
247
+ title_re: Optional[str] = None,
248
+ class_name: Optional[str] = None,
249
+ visible_only: bool = True,
250
+ timeout: float = 0,
251
+ ) -> Optional["Window"]:
252
+ """Find the first matching descendant, optionally waiting for it."""
253
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
254
+ raise TypeError("timeout must be a number")
255
+ timeout = float(timeout)
256
+ if timeout < 0 or not math.isfinite(timeout):
257
+ raise ValueError("timeout must be finite and non-negative")
258
+ deadline = time.monotonic() + timeout
259
+ while True:
260
+ matches = self.find_children(title_re, class_name, visible_only)
261
+ if matches:
262
+ return matches[0]
263
+ remaining = deadline - time.monotonic()
264
+ if timeout == 0 or remaining <= 0:
265
+ return None
266
+ time.sleep(min(0.2, remaining))
267
+
268
+ @classmethod
269
+ def launch(
270
+ cls,
271
+ name: Optional[str] = None,
272
+ path: Optional[str] = None,
273
+ allow_multiple: bool = False,
274
+ timeout: float = 15,
275
+ ) -> Optional["Window"]:
276
+ from .launcher import Launcher
277
+
278
+ if not name and not path:
279
+ raise ValueError("name or path is required")
280
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
281
+ raise TypeError("timeout must be a number")
282
+ timeout = float(timeout)
283
+ if timeout < 0 or not math.isfinite(timeout):
284
+ raise ValueError("timeout must be finite and non-negative")
285
+
286
+ resolved_path, process_name = Launcher.get_info(name=name, path=path)
287
+ launch_target = resolved_path or path
288
+ if not process_name and launch_target:
289
+ process_name = os.path.basename(launch_target)
290
+ title_filter = name if name and not process_name else None
291
+ existing = cls.find_all(
292
+ title_re=title_filter, process_name=process_name, visible_only=False
293
+ )
294
+ if existing and not allow_multiple:
295
+ existing[0].activate()
296
+ return existing[0]
297
+
298
+ if not Launcher.run(name=name, path=launch_target):
299
+ return None
300
+
301
+ old_handles = {window.hwnd for window in existing}
302
+ deadline = time.monotonic() + timeout
303
+ while True:
304
+ current = cls.find_all(
305
+ title_re=title_filter, process_name=process_name, visible_only=False
306
+ )
307
+ candidates = (
308
+ [window for window in current if window.hwnd not in old_handles]
309
+ if allow_multiple else current
310
+ )
311
+ if candidates:
312
+ candidates[0].activate()
313
+ return candidates[0]
314
+ remaining = deadline - time.monotonic()
315
+ if remaining <= 0:
316
+ return existing[0] if existing else None
317
+ time.sleep(min(0.2, remaining))
318
+
319
+ @property
320
+ def is_valid(self) -> bool:
321
+ return bool(win32gui.IsWindow(self.hwnd))
322
+
323
+ @property
324
+ def title(self) -> str:
325
+ return win32gui.GetWindowText(self.hwnd)
326
+
327
+ @property
328
+ def class_name(self) -> str:
329
+ return win32gui.GetClassName(self.hwnd)
330
+
331
+ @property
332
+ def pid(self) -> int:
333
+ if self._pid is None:
334
+ _, self._pid = win32process.GetWindowThreadProcessId(self.hwnd)
335
+ return self._pid
336
+
337
+ @property
338
+ def process_path(self) -> str:
339
+ try:
340
+ return psutil.Process(self.pid).exe()
341
+ except (psutil.Error, OSError):
342
+ return "Unknown"
343
+
344
+ @property
345
+ def process_name(self) -> str:
346
+ try:
347
+ return psutil.Process(self.pid).name()
348
+ except (psutil.Error, OSError):
349
+ return "Unknown"
350
+
351
+ @property
352
+ def is_visible(self) -> bool:
353
+ if not self.is_valid or not win32gui.IsWindowVisible(self.hwnd):
354
+ return False
355
+ left, top, right, bottom = win32gui.GetWindowRect(self.hwnd)
356
+ return right > left and bottom > top and left > -30000 and top > -30000
357
+
358
+ @property
359
+ def is_minimized(self) -> bool:
360
+ return bool(win32gui.IsIconic(self.hwnd))
361
+
362
+ @property
363
+ def is_maximized(self) -> bool:
364
+ return bool(win32gui.IsZoomed(self.hwnd))
365
+
366
+ @property
367
+ def is_responding(self) -> bool:
368
+ if not self.is_valid:
369
+ return False
370
+ if _IS_HUNG_APP_WINDOW is not None:
371
+ return not bool(_IS_HUNG_APP_WINDOW(self.hwnd))
372
+ return self._check_responding_via_timeout()
373
+
374
+ def _check_responding_via_timeout(self) -> bool:
375
+ try:
376
+ win32gui.SendMessageTimeout(
377
+ self.hwnd, win32con.WM_NULL, 0, 0,
378
+ win32con.SMTO_ABORTIFHUNG | win32con.SMTO_BLOCK, 100,
379
+ )
380
+ return True
381
+ except win32gui.error:
382
+ return False
383
+
384
+ @property
385
+ def is_always_on_top(self) -> bool:
386
+ style = win32gui.GetWindowLong(self.hwnd, win32con.GWL_EXSTYLE)
387
+ return bool(style & win32con.WS_EX_TOPMOST)
388
+
389
+ @property
390
+ def rect(self) -> WindowRect:
391
+ native = win32gui.GetWindowRect(self.hwnd)
392
+ if _DWM_GET_WINDOW_ATTRIBUTE is not None:
393
+ visual = wintypes.RECT()
394
+ result = _DWM_GET_WINDOW_ATTRIBUTE(
395
+ self.hwnd, _DWMWA_EXTENDED_FRAME_BOUNDS,
396
+ ctypes.byref(visual), ctypes.sizeof(visual),
397
+ )
398
+ if result == 0 and visual.right > visual.left and visual.bottom > visual.top:
399
+ return WindowRect.from_ltrb(
400
+ visual.left, visual.top, visual.right, visual.bottom
401
+ )
402
+ return WindowRect.from_ltrb(*native)
403
+
404
+ @property
405
+ def client_rect(self) -> WindowRect:
406
+ left, top, right, bottom = win32gui.GetClientRect(self.hwnd)
407
+ screen_left, screen_top = win32gui.ClientToScreen(self.hwnd, (left, top))
408
+ return WindowRect.from_ltrb(
409
+ screen_left, screen_top,
410
+ screen_left + right - left, screen_top + bottom - top,
411
+ )
412
+
413
+ def set_rect(
414
+ self, x: Optional[int] = None, y: Optional[int] = None,
415
+ width: Optional[int] = None, height: Optional[int] = None,
416
+ ):
417
+ for value, label in ((x, "x"), (y, "y"), (width, "width"), (height, "height")):
418
+ if value is not None and (not isinstance(value, int) or isinstance(value, bool)):
419
+ raise TypeError(f"{label} must be an integer or None")
420
+ if (width is not None and width <= 0) or (height is not None and height <= 0):
421
+ raise ValueError("width and height must be greater than zero")
422
+
423
+ visual = self.rect
424
+ native_left, native_top, native_right, native_bottom = win32gui.GetWindowRect(self.hwnd)
425
+ left_inset = visual.left - native_left
426
+ top_inset = visual.top - native_top
427
+ horizontal_insets = (visual.left - native_left) + (native_right - visual.right)
428
+ vertical_insets = (visual.top - native_top) + (native_bottom - visual.bottom)
429
+ desired_x = visual.left if x is None else x
430
+ desired_y = visual.top if y is None else y
431
+ desired_width = visual.width if width is None else width
432
+ desired_height = visual.height if height is None else height
433
+ win32gui.SetWindowPos(
434
+ self.hwnd, 0, desired_x - left_inset, desired_y - top_inset,
435
+ desired_width + horizontal_insets, desired_height + vertical_insets,
436
+ win32con.SWP_NOZORDER | win32con.SWP_NOACTIVATE,
437
+ )
438
+ return self
439
+
440
+ def move(self, x: int, y: int):
441
+ return self.set_rect(x=x, y=y)
442
+
443
+ def resize(self, width: int, height: int):
444
+ return self.set_rect(width=width, height=height)
445
+
446
+ def activate(self) -> bool:
447
+ if not self.is_valid:
448
+ return False
449
+ win32gui.ShowWindow(self.hwnd, win32con.SW_RESTORE if self.is_minimized else win32con.SW_SHOW)
450
+ try:
451
+ win32gui.SetForegroundWindow(self.hwnd)
452
+ except win32gui.error:
453
+ foreground = win32gui.GetForegroundWindow()
454
+ current_thread = _USER32.GetCurrentThreadId()
455
+ target_thread, _ = win32process.GetWindowThreadProcessId(self.hwnd)
456
+ foreground_thread = win32process.GetWindowThreadProcessId(foreground)[0] if foreground else 0
457
+ attached = []
458
+ try:
459
+ for thread_id in {target_thread, foreground_thread}:
460
+ if thread_id and thread_id != current_thread and _USER32.AttachThreadInput(current_thread, thread_id, True):
461
+ attached.append(thread_id)
462
+ win32gui.BringWindowToTop(self.hwnd)
463
+ win32gui.SetForegroundWindow(self.hwnd)
464
+ except win32gui.error:
465
+ return False
466
+ finally:
467
+ for thread_id in reversed(attached):
468
+ _USER32.AttachThreadInput(current_thread, thread_id, False)
469
+ return win32gui.GetForegroundWindow() == self.hwnd
470
+
471
+ def set_always_on_top(self, toggle: bool = True):
472
+ z_order = win32con.HWND_TOPMOST if bool(toggle) else win32con.HWND_NOTOPMOST
473
+ win32gui.SetWindowPos(
474
+ self.hwnd, z_order, 0, 0, 0, 0,
475
+ win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_NOACTIVATE,
476
+ )
477
+ return self
478
+
479
+ def minimize(self):
480
+ win32gui.ShowWindow(self.hwnd, win32con.SW_MINIMIZE)
481
+ return self
482
+
483
+ def is_active(self) -> bool:
484
+ return win32gui.GetForegroundWindow() == self.hwnd
485
+
486
+ def placement(self):
487
+ return win32gui.GetWindowPlacement(self.hwnd)
488
+
489
+ def frame(self, x=None, y=None, w=None, h=None):
490
+ return self.set_rect(x=x, y=y, width=w, height=h)
491
+
492
+ def mize_mini(self):
493
+ return self.minimize()
494
+
495
+ def mize_max(self):
496
+ win32gui.ShowWindow(self.hwnd, win32con.SW_MAXIMIZE)
497
+ return self
498
+
499
+ def mize_normal(self):
500
+ win32gui.ShowWindow(self.hwnd, win32con.SW_NORMAL)
501
+ return self
502
+
503
+ def to_dict(self):
504
+ return {
505
+ "hwnd": self.hwnd,
506
+ "name": self.class_name,
507
+ "class_name": self.class_name,
508
+ "title": self.title,
509
+ "rect": (self.rect.left, self.rect.top, self.rect.right, self.rect.bottom),
510
+ "placement": self.placement(),
511
+ "is_active": self.is_active(),
512
+ }
513
+
514
+ def close(self) -> bool:
515
+ return bool(win32gui.PostMessage(self.hwnd, win32con.WM_CLOSE, 0, 0))
516
+
517
+ def kill(self) -> bool:
518
+ try:
519
+ psutil.Process(self.pid).kill()
520
+ return True
521
+ except (psutil.Error, OSError):
522
+ return False
523
+
524
+ @staticmethod
525
+ def _normalize_capture_rect(rect, client: WindowRect) -> WindowRect:
526
+ if rect is None:
527
+ return client
528
+ if isinstance(rect, WindowRect):
529
+ target = rect
530
+ elif isinstance(rect, Sequence) and not isinstance(rect, (str, bytes)) and len(rect) == 4:
531
+ left, top, right, bottom = map(int, rect)
532
+ target = WindowRect.from_ltrb(left, top, right, bottom)
533
+ else:
534
+ raise TypeError("rect must be WindowRect, (left, top, right, bottom), or None")
535
+ left = max(client.left, target.left)
536
+ top = max(client.top, target.top)
537
+ right = min(client.right, target.right)
538
+ bottom = min(client.bottom, target.bottom)
539
+ if right <= left or bottom <= top:
540
+ raise ValueError("capture rect does not overlap the window client area")
541
+ return WindowRect.from_ltrb(left, top, right, bottom)
542
+
543
+ def _capture_client_printwindow(self, width: int, height: int) -> Optional[Image.Image]:
544
+ hwnd_dc = mfc_dc = save_dc = bitmap = None
545
+ try:
546
+ hwnd_dc = win32gui.GetWindowDC(self.hwnd)
547
+ mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
548
+ save_dc = mfc_dc.CreateCompatibleDC()
549
+ bitmap = win32ui.CreateBitmap()
550
+ bitmap.CreateCompatibleBitmap(mfc_dc, width, height)
551
+ save_dc.SelectObject(bitmap)
552
+ rendered = _USER32.PrintWindow(
553
+ self.hwnd, save_dc.GetSafeHdc(), 0x1 | 0x2,
554
+ )
555
+ if not rendered:
556
+ return None
557
+ info = bitmap.GetInfo()
558
+ bits = bitmap.GetBitmapBits(True)
559
+ return Image.frombuffer(
560
+ "RGB", (info["bmWidth"], info["bmHeight"]),
561
+ bits, "raw", "BGRX", 0, 1,
562
+ ).copy()
563
+ finally:
564
+ if bitmap is not None:
565
+ try:
566
+ win32gui.DeleteObject(bitmap.GetHandle())
567
+ except win32gui.error:
568
+ pass
569
+ if save_dc is not None:
570
+ save_dc.DeleteDC()
571
+ if mfc_dc is not None:
572
+ mfc_dc.DeleteDC()
573
+ if hwnd_dc is not None:
574
+ win32gui.ReleaseDC(self.hwnd, hwnd_dc)
575
+
576
+ def capture(
577
+ self, save_path: Optional[str] = None,
578
+ format: WindowCaptureFormat = WindowCaptureFormat.PIL,
579
+ rect: Optional[Union[WindowRect, Sequence[int]]] = None,
580
+ force_activate: bool = True,
581
+ ):
582
+ """Capture the client area; *rect* uses physical screen coordinates."""
583
+ if not isinstance(format, WindowCaptureFormat):
584
+ raise TypeError("format must be a WindowCaptureFormat value")
585
+ if not self.is_valid:
586
+ return None
587
+ previous = win32gui.GetForegroundWindow() if force_activate else 0
588
+ try:
589
+ if force_activate:
590
+ self.activate()
591
+ client = self.client_rect
592
+ if client.width <= 0 or client.height <= 0:
593
+ return None
594
+ target = self._normalize_capture_rect(rect, client)
595
+ image = self._capture_client_printwindow(client.width, client.height)
596
+ if image is None:
597
+ if not self.is_visible:
598
+ return None
599
+ image = ImageGrab.grab(
600
+ bbox=(client.left, client.top, client.right, client.bottom),
601
+ all_screens=True,
602
+ ).convert("RGB")
603
+ crop = (
604
+ target.left - client.left, target.top - client.top,
605
+ target.right - client.left, target.bottom - client.top,
606
+ )
607
+ image = image.crop(crop)
608
+ if save_path:
609
+ image.save(os.fspath(save_path))
610
+ return format.convert(image)
611
+ except (OSError, ValueError, win32gui.error):
612
+ return None
613
+ finally:
614
+ if force_activate and previous and previous != self.hwnd and win32gui.IsWindow(previous):
615
+ try:
616
+ win32gui.SetForegroundWindow(previous)
617
+ except win32gui.error:
618
+ pass
619
+
620
+ def _client_point(self, x: int, y: int, is_relative: bool) -> tuple[int, int]:
621
+ if not isinstance(x, int) or isinstance(x, bool) or not isinstance(y, int) or isinstance(y, bool):
622
+ raise TypeError("x and y must be integers")
623
+ return (x, y) if is_relative else win32gui.ScreenToClient(self.hwnd, (x, y))
624
+
625
+ def _get_lparam(self, x: int, y: int, is_relative: bool = True) -> int:
626
+ client_x, client_y = self._client_point(x, y, is_relative)
627
+ return ((client_y & 0xFFFF) << 16) | (client_x & 0xFFFF)
628
+
629
+ @staticmethod
630
+ def _button_messages(button: str):
631
+ if not isinstance(button, str):
632
+ raise TypeError("button must be a string")
633
+ token = button.casefold()
634
+ swapped = bool(_USER32.GetSystemMetrics(23))
635
+ if token == "primary":
636
+ token = "right" if swapped else "left"
637
+ elif token == "secondary":
638
+ token = "left" if swapped else "right"
639
+ mapping = {
640
+ "left": (win32con.WM_LBUTTONDOWN, win32con.WM_LBUTTONUP, win32con.MK_LBUTTON),
641
+ "right": (win32con.WM_RBUTTONDOWN, win32con.WM_RBUTTONUP, win32con.MK_RBUTTON),
642
+ "middle": (win32con.WM_MBUTTONDOWN, win32con.WM_MBUTTONUP, win32con.MK_MBUTTON),
643
+ }
644
+ if token not in mapping:
645
+ raise ValueError("button must be left, middle, right, primary or secondary")
646
+ return mapping[token]
647
+
648
+ def click(
649
+ self, x: int, y: int, clicks: int = 1, interval: float = 0.1,
650
+ button: str = "primary", move_duration: float = 0.0,
651
+ duration: float = 0.0, offset: int = 0, *, is_relative: bool = True,
652
+ ):
653
+ """Post background mouse messages without moving the physical pointer."""
654
+ # Keep the historical positional API while accepting the documented
655
+ # ``is_relative=`` keyword. This also handles ``click(x, y, False, 'right')``.
656
+ if isinstance(clicks, bool):
657
+ legacy_is_relative = clicks
658
+ legacy_button = interval if isinstance(interval, str) else button
659
+ clicks, interval, button, is_relative = 1, 0.1, legacy_button, legacy_is_relative
660
+ if not isinstance(clicks, int) or isinstance(clicks, bool) or clicks < 1:
661
+ raise ValueError("clicks must be a positive integer")
662
+ for value, label in ((interval, "interval"), (duration, "duration"), (move_duration, "move_duration")):
663
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or value < 0:
664
+ raise ValueError(f"{label} must be a non-negative number")
665
+ if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
666
+ raise ValueError("offset must be a non-negative integer")
667
+ if move_duration or offset:
668
+ if is_relative:
669
+ x, y = win32gui.ClientToScreen(self.hwnd, (x, y))
670
+ Mouse.click(
671
+ x, y, clicks=clicks, interval=interval, button=button,
672
+ move_duration=move_duration, duration=duration, offset=offset,
673
+ )
674
+ return self
675
+ lparam = self._get_lparam(x, y, is_relative)
676
+ down, up, state = self._button_messages(button)
677
+ for index in range(clicks):
678
+ win32gui.PostMessage(self.hwnd, win32con.WM_MOUSEMOVE, 0, lparam)
679
+ win32gui.PostMessage(self.hwnd, down, state, lparam)
680
+ if duration:
681
+ time.sleep(duration)
682
+ win32gui.PostMessage(self.hwnd, up, 0, lparam)
683
+ if interval and index + 1 < clicks:
684
+ time.sleep(interval)
685
+ return self
686
+
687
+ def long_click(
688
+ self, x: int, y: int, duration: float = 1.0,
689
+ is_relative: bool = True, button: str = "left",
690
+ ):
691
+ return self.click(x, y, is_relative=is_relative, button=button, duration=duration)
692
+
693
+ def drag(
694
+ self, x1: int, y1: int, x2: int, y2: int, duration: float = 0.5,
695
+ is_relative: bool = True, button: str = "left",
696
+ ):
697
+ if isinstance(duration, bool) or not isinstance(duration, (int, float)) or duration < 0:
698
+ raise ValueError("duration must be a non-negative number")
699
+ start_x, start_y = self._client_point(x1, y1, is_relative)
700
+ end_x, end_y = self._client_point(x2, y2, is_relative)
701
+ down, up, state = self._button_messages(button)
702
+ start = self._get_lparam(start_x, start_y, True)
703
+ win32gui.PostMessage(self.hwnd, win32con.WM_MOUSEMOVE, 0, start)
704
+ win32gui.PostMessage(self.hwnd, down, state, start)
705
+ steps = max(1, min(240, round(max(duration, 0.01) * 60)))
706
+ started = time.perf_counter()
707
+ try:
708
+ for step in range(1, steps + 1):
709
+ progress = step / steps
710
+ current_x = round(start_x + (end_x - start_x) * progress)
711
+ current_y = round(start_y + (end_y - start_y) * progress)
712
+ current = self._get_lparam(current_x, current_y, True)
713
+ win32gui.PostMessage(self.hwnd, win32con.WM_MOUSEMOVE, state, current)
714
+ delay = started + duration * progress - time.perf_counter()
715
+ if delay > 0:
716
+ time.sleep(delay)
717
+ finally:
718
+ end = self._get_lparam(end_x, end_y, True)
719
+ win32gui.PostMessage(self.hwnd, up, 0, end)
720
+ return self
721
+
722
+ def _text_target(self) -> int:
723
+ """Return the focused child control for this window's GUI thread."""
724
+ thread_id, _ = win32process.GetWindowThreadProcessId(self.hwnd)
725
+ info = _GUITHREADINFO(cbSize=ctypes.sizeof(_GUITHREADINFO))
726
+ try:
727
+ if _USER32.GetGUIThreadInfo(thread_id, ctypes.byref(info)) and info.hwndFocus:
728
+ return int(info.hwndFocus)
729
+ except (AttributeError, OSError):
730
+ pass
731
+ return self.hwnd
732
+
733
+ def type_text(self, text: str, interval: float = 0.01):
734
+ """Post UTF-16 WM_CHAR messages to the focused child control."""
735
+ if not isinstance(text, str):
736
+ raise TypeError("text must be a string")
737
+ if isinstance(interval, bool) or not isinstance(interval, (int, float)) or interval < 0:
738
+ raise ValueError("interval must be a non-negative number")
739
+ encoded = text.encode("utf-16-le", errors="surrogatepass")
740
+ units = [int.from_bytes(encoded[index:index + 2], "little")
741
+ for index in range(0, len(encoded), 2)]
742
+ target = self._text_target()
743
+ for index, unit in enumerate(units):
744
+ win32gui.PostMessage(target, win32con.WM_CHAR, unit, 1)
745
+ if interval and index + 1 < len(units):
746
+ time.sleep(interval)
747
+ return self