capturekit 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.
Files changed (41) hide show
  1. capturekit/__init__.py +164 -0
  2. capturekit/_backends/__init__.py +0 -0
  3. capturekit/_backends/base.py +89 -0
  4. capturekit/_backends/linux/__init__.py +1 -0
  5. capturekit/_backends/linux/_xlib.py +655 -0
  6. capturekit/_backends/linux/api.py +160 -0
  7. capturekit/_backends/linux/pulse.py +464 -0
  8. capturekit/_backends/linux/x11.py +720 -0
  9. capturekit/_backends/windows/__init__.py +0 -0
  10. capturekit/_backends/windows/_com.py +350 -0
  11. capturekit/_backends/windows/_d3d11.py +307 -0
  12. capturekit/_backends/windows/_win32.py +420 -0
  13. capturekit/_backends/windows/_winrt.py +162 -0
  14. capturekit/_backends/windows/api.py +214 -0
  15. capturekit/_backends/windows/cursor.py +66 -0
  16. capturekit/_backends/windows/dxgi_duplication.py +310 -0
  17. capturekit/_backends/windows/wasapi.py +570 -0
  18. capturekit/_backends/windows/wgc.py +398 -0
  19. capturekit/_pipeline/__init__.py +1 -0
  20. capturekit/_pipeline/aligner.py +177 -0
  21. capturekit/_pipeline/clock.py +47 -0
  22. capturekit/_pipeline/frame_queue.py +120 -0
  23. capturekit/_pipeline/mixer.py +80 -0
  24. capturekit/_pipeline/timeline.py +133 -0
  25. capturekit/_platform.py +24 -0
  26. capturekit/_version.py +1 -0
  27. capturekit/_worker.py +100 -0
  28. capturekit/capabilities.py +79 -0
  29. capturekit/capture.py +141 -0
  30. capturekit/config.py +85 -0
  31. capturekit/encoding/__init__.py +5 -0
  32. capturekit/encoding/ffmpeg.py +356 -0
  33. capturekit/encoding/pipes.py +212 -0
  34. capturekit/errors.py +109 -0
  35. capturekit/py.typed +0 -0
  36. capturekit/recording.py +940 -0
  37. capturekit/sources.py +124 -0
  38. capturekit/types.py +211 -0
  39. capturekit-0.1.0.dist-info/METADATA +112 -0
  40. capturekit-0.1.0.dist-info/RECORD +41 -0
  41. capturekit-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,420 @@
1
+ """Win32 helpers: DPI context, monitor and window enumeration, process info."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import ctypes
7
+ import os
8
+ from collections.abc import Iterator
9
+ from ctypes import POINTER, Structure, byref, c_int, c_long, c_uint, c_void_p, sizeof
10
+ from ctypes import wintypes as wt
11
+
12
+ from ...types import Monitor, Rect, Window
13
+
14
+ user32 = ctypes.WinDLL("user32", use_last_error=True)
15
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
16
+ dwmapi = ctypes.WinDLL("dwmapi")
17
+ try:
18
+ shcore: ctypes.WinDLL | None = ctypes.WinDLL("shcore")
19
+ except OSError: # pragma: no cover - Windows 7
20
+ shcore = None
21
+
22
+
23
+ class MONITORINFOEXW(Structure):
24
+ _fields_ = [
25
+ ("cbSize", wt.DWORD),
26
+ ("rcMonitor", wt.RECT),
27
+ ("rcWork", wt.RECT),
28
+ ("dwFlags", wt.DWORD),
29
+ ("szDevice", wt.WCHAR * 32),
30
+ ]
31
+
32
+
33
+ class DISPLAY_DEVICEW(Structure):
34
+ _fields_ = [
35
+ ("cb", wt.DWORD),
36
+ ("DeviceName", wt.WCHAR * 32),
37
+ ("DeviceString", wt.WCHAR * 128),
38
+ ("StateFlags", wt.DWORD),
39
+ ("DeviceID", wt.WCHAR * 128),
40
+ ("DeviceKey", wt.WCHAR * 128),
41
+ ]
42
+
43
+
44
+ class DEVMODEW(Structure):
45
+ _fields_ = [
46
+ ("dmDeviceName", wt.WCHAR * 32),
47
+ ("dmSpecVersion", wt.WORD),
48
+ ("dmDriverVersion", wt.WORD),
49
+ ("dmSize", wt.WORD),
50
+ ("dmDriverExtra", wt.WORD),
51
+ ("dmFields", wt.DWORD),
52
+ ("dmPositionX", wt.LONG),
53
+ ("dmPositionY", wt.LONG),
54
+ ("dmDisplayOrientation", wt.DWORD),
55
+ ("dmDisplayFixedOutput", wt.DWORD),
56
+ ("dmColor", ctypes.c_short),
57
+ ("dmDuplex", ctypes.c_short),
58
+ ("dmYResolution", ctypes.c_short),
59
+ ("dmTTOption", ctypes.c_short),
60
+ ("dmCollate", ctypes.c_short),
61
+ ("dmFormName", wt.WCHAR * 32),
62
+ ("dmLogPixels", wt.WORD),
63
+ ("dmBitsPerPel", wt.DWORD),
64
+ ("dmPelsWidth", wt.DWORD),
65
+ ("dmPelsHeight", wt.DWORD),
66
+ ("dmDisplayFlags", wt.DWORD),
67
+ ("dmDisplayFrequency", wt.DWORD),
68
+ ("dmICMMethod", wt.DWORD),
69
+ ("dmICMIntent", wt.DWORD),
70
+ ("dmMediaType", wt.DWORD),
71
+ ("dmDitherType", wt.DWORD),
72
+ ("dmReserved1", wt.DWORD),
73
+ ("dmReserved2", wt.DWORD),
74
+ ("dmPanningWidth", wt.DWORD),
75
+ ("dmPanningHeight", wt.DWORD),
76
+ ]
77
+
78
+
79
+ MONITORENUMPROC = ctypes.WINFUNCTYPE(wt.BOOL, wt.HMONITOR, wt.HDC, POINTER(wt.RECT), wt.LPARAM)
80
+ WNDENUMPROC = ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM)
81
+
82
+ user32.EnumDisplayMonitors.argtypes = (wt.HDC, c_void_p, MONITORENUMPROC, wt.LPARAM)
83
+ user32.EnumDisplayMonitors.restype = wt.BOOL
84
+ user32.GetMonitorInfoW.argtypes = (wt.HMONITOR, c_void_p)
85
+ user32.GetMonitorInfoW.restype = wt.BOOL
86
+ user32.EnumDisplaySettingsW.argtypes = (wt.LPCWSTR, wt.DWORD, c_void_p)
87
+ user32.EnumDisplaySettingsW.restype = wt.BOOL
88
+ user32.EnumDisplayDevicesW.argtypes = (wt.LPCWSTR, wt.DWORD, c_void_p, wt.DWORD)
89
+ user32.EnumDisplayDevicesW.restype = wt.BOOL
90
+ user32.EnumWindows.argtypes = (WNDENUMPROC, wt.LPARAM)
91
+ user32.EnumWindows.restype = wt.BOOL
92
+ user32.IsWindow.argtypes = (wt.HWND,)
93
+ user32.IsWindow.restype = wt.BOOL
94
+ user32.IsWindowVisible.argtypes = (wt.HWND,)
95
+ user32.IsWindowVisible.restype = wt.BOOL
96
+ user32.IsIconic.argtypes = (wt.HWND,)
97
+ user32.IsIconic.restype = wt.BOOL
98
+ user32.GetWindowTextLengthW.argtypes = (wt.HWND,)
99
+ user32.GetWindowTextLengthW.restype = c_int
100
+ user32.GetWindowTextW.argtypes = (wt.HWND, wt.LPWSTR, c_int)
101
+ user32.GetWindowTextW.restype = c_int
102
+ user32.GetClassNameW.argtypes = (wt.HWND, wt.LPWSTR, c_int)
103
+ user32.GetClassNameW.restype = c_int
104
+ user32.GetWindowThreadProcessId.argtypes = (wt.HWND, POINTER(wt.DWORD))
105
+ user32.GetWindowThreadProcessId.restype = wt.DWORD
106
+ user32.GetWindowLongPtrW.argtypes = (wt.HWND, c_int)
107
+ user32.GetWindowLongPtrW.restype = ctypes.c_ssize_t
108
+ user32.GetAncestor.argtypes = (wt.HWND, c_uint)
109
+ user32.GetAncestor.restype = wt.HWND
110
+ user32.GetWindowRect.argtypes = (wt.HWND, POINTER(wt.RECT))
111
+ user32.GetWindowRect.restype = wt.BOOL
112
+ user32.GetClientRect.argtypes = (wt.HWND, POINTER(wt.RECT))
113
+ user32.GetClientRect.restype = wt.BOOL
114
+ user32.ClientToScreen.argtypes = (wt.HWND, POINTER(wt.POINT))
115
+ user32.ClientToScreen.restype = wt.BOOL
116
+ user32.GetWindowDisplayAffinity.argtypes = (wt.HWND, POINTER(wt.DWORD))
117
+ user32.GetWindowDisplayAffinity.restype = wt.BOOL
118
+ user32.MonitorFromWindow.argtypes = (wt.HWND, wt.DWORD)
119
+ user32.MonitorFromWindow.restype = wt.HMONITOR
120
+ user32.SetThreadDpiAwarenessContext.argtypes = (c_void_p,)
121
+ user32.SetThreadDpiAwarenessContext.restype = c_void_p
122
+ dwmapi.DwmGetWindowAttribute.argtypes = (wt.HWND, wt.DWORD, c_void_p, wt.DWORD)
123
+ dwmapi.DwmGetWindowAttribute.restype = c_long
124
+ kernel32.OpenProcess.argtypes = (wt.DWORD, wt.BOOL, wt.DWORD)
125
+ kernel32.OpenProcess.restype = wt.HANDLE
126
+ kernel32.CloseHandle.argtypes = (wt.HANDLE,)
127
+ kernel32.CloseHandle.restype = wt.BOOL
128
+ kernel32.QueryFullProcessImageNameW.argtypes = (wt.HANDLE, wt.DWORD, wt.LPWSTR, POINTER(wt.DWORD))
129
+ kernel32.QueryFullProcessImageNameW.restype = wt.BOOL
130
+ if shcore is not None:
131
+ shcore.GetDpiForMonitor.argtypes = (wt.HMONITOR, c_int, POINTER(c_uint), POINTER(c_uint))
132
+ shcore.GetDpiForMonitor.restype = c_long
133
+
134
+ MONITORINFOF_PRIMARY = 1
135
+ ENUM_CURRENT_SETTINGS = 0xFFFFFFFF
136
+ GWL_STYLE = -16
137
+ GWL_EXSTYLE = -20
138
+ WS_CHILD = 0x40000000
139
+ WS_EX_TOOLWINDOW = 0x00000080
140
+ WS_EX_NOREDIRECTIONBITMAP = 0x00200000
141
+ GA_ROOT = 2
142
+ DWMWA_EXTENDED_FRAME_BOUNDS = 9
143
+ DWMWA_CLOAKED = 14
144
+ WDA_NONE = 0
145
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
146
+ DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = c_void_p(-4)
147
+
148
+
149
+ @contextlib.contextmanager
150
+ def per_monitor_dpi_aware() -> Iterator[None]:
151
+ """Use physical-pixel coordinates on this thread only.
152
+
153
+ The process-wide DPI awareness belongs to the host application, so we
154
+ switch the *thread* context temporarily instead.
155
+ """
156
+ old = user32.SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
157
+ try:
158
+ yield
159
+ finally:
160
+ if old:
161
+ user32.SetThreadDpiAwarenessContext(c_void_p(old))
162
+
163
+
164
+ def _rect(r: wt.RECT) -> Rect:
165
+ return Rect.from_ltrb(r.left, r.top, r.right, r.bottom)
166
+
167
+
168
+ # --- monitors ----------------------------------------------------------------
169
+
170
+
171
+ def list_monitors() -> list[Monitor]:
172
+ handles: list[int] = []
173
+
174
+ def cb(hmon: int, _hdc: int, _rc: object, _lp: int) -> bool:
175
+ handles.append(hmon)
176
+ return True
177
+
178
+ proc = MONITORENUMPROC(cb)
179
+ monitors: list[Monitor] = []
180
+ with per_monitor_dpi_aware():
181
+ user32.EnumDisplayMonitors(None, None, proc, 0)
182
+ for hmon in handles:
183
+ info = MONITORINFOEXW()
184
+ info.cbSize = sizeof(info)
185
+ if not user32.GetMonitorInfoW(hmon, byref(info)):
186
+ continue
187
+ device = info.szDevice
188
+ scale = 1.0
189
+ if shcore is not None:
190
+ dx, dy = c_uint(), c_uint()
191
+ if shcore.GetDpiForMonitor(hmon, 0, byref(dx), byref(dy)) == 0:
192
+ scale = dx.value / 96.0
193
+ mode = DEVMODEW()
194
+ mode.dmSize = sizeof(mode)
195
+ refresh = None
196
+ if user32.EnumDisplaySettingsW(device, ENUM_CURRENT_SETTINGS, byref(mode)):
197
+ refresh = float(mode.dmDisplayFrequency) or None
198
+ dd = DISPLAY_DEVICEW()
199
+ dd.cb = sizeof(dd)
200
+ name = device
201
+ if user32.EnumDisplayDevicesW(device, 0, byref(dd), 0) and dd.DeviceString:
202
+ name = dd.DeviceString
203
+ monitors.append(
204
+ Monitor(
205
+ id=device,
206
+ index=0,
207
+ name=name,
208
+ rect=_rect(info.rcMonitor),
209
+ is_primary=bool(info.dwFlags & MONITORINFOF_PRIMARY),
210
+ scale=scale,
211
+ refresh_rate=refresh,
212
+ native_handle=int(hmon),
213
+ )
214
+ )
215
+ # Stable order: primary first, then left-to-right, top-to-bottom.
216
+ monitors.sort(key=lambda m: (not m.is_primary, m.rect.x, m.rect.y))
217
+ return [
218
+ Monitor(
219
+ m.id, i, m.name, m.rect, m.is_primary, m.scale, m.refresh_rate, None, m.native_handle
220
+ )
221
+ for i, m in enumerate(monitors)
222
+ ]
223
+
224
+
225
+ # --- windows -------------------------------------------------------------------
226
+
227
+
228
+ def is_window(hwnd: int) -> bool:
229
+ return bool(user32.IsWindow(hwnd))
230
+
231
+
232
+ def window_title(hwnd: int) -> str:
233
+ n = user32.GetWindowTextLengthW(hwnd)
234
+ if n <= 0:
235
+ return ""
236
+ buf = ctypes.create_unicode_buffer(n + 1)
237
+ user32.GetWindowTextW(hwnd, buf, n + 1)
238
+ return buf.value
239
+
240
+
241
+ def window_class(hwnd: int) -> str:
242
+ buf = ctypes.create_unicode_buffer(256)
243
+ user32.GetClassNameW(hwnd, buf, 256)
244
+ return buf.value
245
+
246
+
247
+ def window_pid(hwnd: int) -> int | None:
248
+ pid = wt.DWORD()
249
+ user32.GetWindowThreadProcessId(hwnd, byref(pid))
250
+ return pid.value or None
251
+
252
+
253
+ def process_image_path(pid: int) -> str | None:
254
+ h = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
255
+ if not h:
256
+ return None
257
+ try:
258
+ size = wt.DWORD(1024)
259
+ buf = ctypes.create_unicode_buffer(size.value)
260
+ if kernel32.QueryFullProcessImageNameW(h, 0, buf, byref(size)):
261
+ return buf.value
262
+ return None
263
+ finally:
264
+ kernel32.CloseHandle(h)
265
+
266
+
267
+ def process_name(pid: int | None) -> str | None:
268
+ if not pid:
269
+ return None
270
+ path = process_image_path(pid)
271
+ return os.path.basename(path) if path else None
272
+
273
+
274
+ def is_cloaked(hwnd: int) -> bool:
275
+ cloaked = wt.DWORD()
276
+ hr = dwmapi.DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, byref(cloaked), sizeof(cloaked))
277
+ return hr == 0 and cloaked.value != 0
278
+
279
+
280
+ def window_bounds(hwnd: int) -> Rect:
281
+ """Visible window bounds (DWM extended frame bounds, physical pixels).
282
+
283
+ This matches the content that Windows Graphics Capture returns for a
284
+ window (no invisible resize borders / drop shadow).
285
+ """
286
+ r = wt.RECT()
287
+ with per_monitor_dpi_aware():
288
+ hr = dwmapi.DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, byref(r), sizeof(r))
289
+ if hr != 0:
290
+ user32.GetWindowRect(hwnd, byref(r))
291
+ return _rect(r)
292
+
293
+
294
+ def client_rect_in_window(hwnd: int) -> Rect:
295
+ """Client area relative to the visible window bounds (physical pixels)."""
296
+ with per_monitor_dpi_aware():
297
+ bounds = window_bounds(hwnd)
298
+ rc = wt.RECT()
299
+ user32.GetClientRect(hwnd, byref(rc))
300
+ pt = wt.POINT(0, 0)
301
+ user32.ClientToScreen(hwnd, byref(pt))
302
+ return Rect(pt.x - bounds.x, pt.y - bounds.y, rc.right - rc.left, rc.bottom - rc.top)
303
+
304
+
305
+ def display_affinity(hwnd: int) -> int:
306
+ aff = wt.DWORD()
307
+ if user32.GetWindowDisplayAffinity(hwnd, byref(aff)):
308
+ return int(aff.value)
309
+ return WDA_NONE
310
+
311
+
312
+ def describe_window(hwnd: int) -> Window:
313
+ pid = window_pid(hwnd)
314
+ return Window(
315
+ handle=int(hwnd),
316
+ title=window_title(hwnd),
317
+ class_name=window_class(hwnd),
318
+ pid=pid,
319
+ process_name=process_name(pid),
320
+ rect=window_bounds(hwnd),
321
+ is_visible=bool(user32.IsWindowVisible(hwnd)),
322
+ is_minimized=bool(user32.IsIconic(hwnd)),
323
+ display=None,
324
+ capture_excluded=display_affinity(hwnd) != WDA_NONE,
325
+ )
326
+
327
+
328
+ def _is_capturable(hwnd: int, include_untitled: bool) -> bool:
329
+ if not user32.IsWindowVisible(hwnd):
330
+ return False
331
+ style = user32.GetWindowLongPtrW(hwnd, GWL_STYLE)
332
+ exstyle = user32.GetWindowLongPtrW(hwnd, GWL_EXSTYLE)
333
+ if style & WS_CHILD or exstyle & WS_EX_TOOLWINDOW:
334
+ return False
335
+ if user32.GetAncestor(hwnd, GA_ROOT) != hwnd:
336
+ return False
337
+ if is_cloaked(hwnd): # hidden UWP frames, windows on other virtual desktops
338
+ return False
339
+ if not include_untitled and user32.GetWindowTextLengthW(hwnd) <= 0:
340
+ return False
341
+ return True
342
+
343
+
344
+ def list_windows(include_untitled: bool = False) -> list[Window]:
345
+ hwnds: list[int] = []
346
+
347
+ def cb(hwnd: int, _lp: int) -> bool:
348
+ hwnds.append(hwnd)
349
+ return True
350
+
351
+ proc = WNDENUMPROC(cb)
352
+ user32.EnumWindows(proc, 0)
353
+ out: list[Window] = []
354
+ for hwnd in hwnds:
355
+ if not hwnd or not _is_capturable(hwnd, include_untitled):
356
+ continue
357
+ w = describe_window(hwnd)
358
+ if w.rect.is_empty() and not w.is_minimized:
359
+ continue
360
+ out.append(w)
361
+ return out
362
+
363
+
364
+ # --- process tree --------------------------------------------------------------
365
+
366
+
367
+ class PROCESSENTRY32W(Structure):
368
+ _fields_ = [
369
+ ("dwSize", wt.DWORD),
370
+ ("cntUsage", wt.DWORD),
371
+ ("th32ProcessID", wt.DWORD),
372
+ ("th32DefaultHeapID", ctypes.c_size_t),
373
+ ("th32ModuleID", wt.DWORD),
374
+ ("cntThreads", wt.DWORD),
375
+ ("th32ParentProcessID", wt.DWORD),
376
+ ("pcPriClassBase", c_long),
377
+ ("dwFlags", wt.DWORD),
378
+ ("szExeFile", wt.WCHAR * 260),
379
+ ]
380
+
381
+
382
+ kernel32.CreateToolhelp32Snapshot.argtypes = (wt.DWORD, wt.DWORD)
383
+ kernel32.CreateToolhelp32Snapshot.restype = wt.HANDLE
384
+ kernel32.Process32FirstW.argtypes = (wt.HANDLE, POINTER(PROCESSENTRY32W))
385
+ kernel32.Process32FirstW.restype = wt.BOOL
386
+ kernel32.Process32NextW.argtypes = (wt.HANDLE, POINTER(PROCESSENTRY32W))
387
+ kernel32.Process32NextW.restype = wt.BOOL
388
+ TH32CS_SNAPPROCESS = 0x2
389
+
390
+
391
+ def process_parents() -> dict[int, int]:
392
+ """Map pid -> parent pid for all processes (snapshot)."""
393
+ snap = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
394
+ if not snap or snap == wt.HANDLE(-1).value:
395
+ return {}
396
+ try:
397
+ entry = PROCESSENTRY32W()
398
+ entry.dwSize = sizeof(entry)
399
+ result: dict[int, int] = {}
400
+ ok = kernel32.Process32FirstW(snap, byref(entry))
401
+ while ok:
402
+ result[int(entry.th32ProcessID)] = int(entry.th32ParentProcessID)
403
+ ok = kernel32.Process32NextW(snap, byref(entry))
404
+ return result
405
+ finally:
406
+ kernel32.CloseHandle(snap)
407
+
408
+
409
+ def process_tree(pid: int) -> set[int]:
410
+ """``pid`` and all its descendants (the set WASAPI process loopback covers)."""
411
+ parents = process_parents()
412
+ tree = {pid}
413
+ changed = True
414
+ while changed:
415
+ changed = False
416
+ for child, parent in parents.items():
417
+ if parent in tree and child not in tree and child != parent:
418
+ tree.add(child)
419
+ changed = True
420
+ return tree
@@ -0,0 +1,162 @@
1
+ """WinRT ABI helpers: HSTRING, activation factories, pinterface IIDs, delegates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import uuid
7
+ from collections.abc import Callable
8
+ from ctypes import POINTER, byref, c_int64, c_long, c_uint, c_void_p, c_wchar_p
9
+
10
+ from ._com import GUID, ComObject, ComPtr, check_hr, ensure_mta_usage
11
+
12
+ _combase = ctypes.WinDLL("combase")
13
+ _combase.RoInitialize.argtypes = (c_uint,)
14
+ _combase.RoInitialize.restype = c_long
15
+ _combase.RoUninitialize.argtypes = ()
16
+ _combase.RoUninitialize.restype = None
17
+ _combase.WindowsCreateString.argtypes = (c_wchar_p, c_uint, POINTER(c_void_p))
18
+ _combase.WindowsCreateString.restype = c_long
19
+ _combase.WindowsDeleteString.argtypes = (c_void_p,)
20
+ _combase.WindowsDeleteString.restype = c_long
21
+ _combase.WindowsGetStringRawBuffer.argtypes = (c_void_p, POINTER(c_uint))
22
+ _combase.WindowsGetStringRawBuffer.restype = c_wchar_p
23
+ _combase.RoGetActivationFactory.argtypes = (c_void_p, c_void_p, POINTER(c_void_p))
24
+ _combase.RoGetActivationFactory.restype = c_long
25
+
26
+ RO_INIT_MULTITHREADED = 1
27
+ RPC_E_CHANGED_MODE = -2147417850
28
+
29
+
30
+ class RoApartment:
31
+ """Initialise the Windows Runtime (MTA) on the current thread."""
32
+
33
+ def __enter__(self) -> RoApartment:
34
+ ensure_mta_usage()
35
+ hr = _combase.RoInitialize(RO_INIT_MULTITHREADED)
36
+ self._uninit = hr >= 0
37
+ if hr < 0 and hr != RPC_E_CHANGED_MODE:
38
+ check_hr(hr, "RoInitialize")
39
+ return self
40
+
41
+ def __exit__(self, *exc: object) -> None:
42
+ if self._uninit:
43
+ _combase.RoUninitialize()
44
+
45
+
46
+ class HString:
47
+ def __init__(self, text: str) -> None:
48
+ self.handle = c_void_p()
49
+ check_hr(
50
+ _combase.WindowsCreateString(text, len(text), byref(self.handle)),
51
+ "WindowsCreateString",
52
+ )
53
+
54
+ def close(self) -> None:
55
+ if self.handle:
56
+ _combase.WindowsDeleteString(self.handle)
57
+ self.handle = c_void_p()
58
+
59
+ def __enter__(self) -> HString:
60
+ return self
61
+
62
+ def __exit__(self, *exc: object) -> None:
63
+ self.close()
64
+
65
+
66
+ def hstring_to_str(handle: int | None) -> str:
67
+ """Read and delete an HSTRING returned by a WinRT method."""
68
+ if not handle:
69
+ return ""
70
+ length = c_uint()
71
+ text = _combase.WindowsGetStringRawBuffer(handle, byref(length)) or ""
72
+ _combase.WindowsDeleteString(handle)
73
+ return text[: length.value]
74
+
75
+
76
+ def activation_factory(class_name: str, iid: GUID) -> ComPtr:
77
+ with HString(class_name) as hs:
78
+ out = c_void_p()
79
+ check_hr(
80
+ _combase.RoGetActivationFactory(hs.handle, byref(iid), byref(out)),
81
+ f"RoGetActivationFactory({class_name})",
82
+ )
83
+ return ComPtr(out.value, f"{class_name} factory")
84
+
85
+
86
+ _PINTERFACE_NS = uuid.UUID("11f47ad5-7b73-42c0-abae-878b1e16adee")
87
+ _TYPED_EVENT_HANDLER = "{9de1c534-6ae1-11e0-84e1-18a905bcc53f}"
88
+
89
+
90
+ def pinterface_iid(signature: str) -> GUID:
91
+ """IID of a parameterised WinRT interface (UUIDv5 over the signature)."""
92
+ return GUID.from_buffer_copy(uuid.uuid5(_PINTERFACE_NS, signature).bytes_le)
93
+
94
+
95
+ def typed_event_handler_iid(sender_class: str, sender_default_iid: str) -> GUID:
96
+ """IID of ``TypedEventHandler<sender_class, IInspectable>``."""
97
+ sig = (
98
+ f"pinterface({_TYPED_EVENT_HANDLER};"
99
+ f"rc({sender_class};{{{sender_default_iid.lower()}}});cinterface(IInspectable))"
100
+ )
101
+ return pinterface_iid(sig)
102
+
103
+
104
+ class EventDelegate(ComObject):
105
+ """``TypedEventHandler<TSender, IInspectable>`` implemented in Python.
106
+
107
+ ``callback(sender_ptr, args_ptr)`` runs on a WinRT thread-pool thread; keep
108
+ it short (signal an event).
109
+ """
110
+
111
+ _methods_ = (("Invoke", c_long, (c_void_p, c_void_p)),)
112
+
113
+ def __init__(self, iid: GUID, callback: Callable[[int | None, int | None], None]) -> None:
114
+ self._callback = callback
115
+ super().__init__((iid,))
116
+
117
+ def Invoke(self, sender: int | None, args: int | None) -> None:
118
+ self._callback(sender, args)
119
+
120
+
121
+ class EventRegistration:
122
+ """Holds a delegate registered with ``add_X``; ``remove()`` unregisters."""
123
+
124
+ def __init__(self, source: ComPtr, remove_index: int, token: int, delegate: ComObject):
125
+ self._source = source
126
+ self._remove_index = remove_index
127
+ self._token = token
128
+ self._delegate: ComObject | None = delegate
129
+
130
+ @staticmethod
131
+ def add(
132
+ source: ComPtr, add_index: int, remove_index: int, delegate: ComObject, what: str
133
+ ) -> EventRegistration:
134
+ token = c_int64()
135
+ source.hcall(
136
+ add_index, (c_void_p, c_void_p), c_void_p(delegate.ptr), byref(token), what=what
137
+ )
138
+ return EventRegistration(source, remove_index, token.value, delegate)
139
+
140
+ def remove(self) -> None:
141
+ if self._delegate is not None:
142
+ try:
143
+ self._source.call(self._remove_index, c_long, (c_int64,), self._token)
144
+ finally:
145
+ self._delegate.release()
146
+ self._delegate = None
147
+
148
+
149
+ def close_winrt(obj: ComPtr | None) -> None:
150
+ """``IClosable::Close`` then release (ignores objects without IClosable)."""
151
+ if obj is None:
152
+ return
153
+ closable = obj.try_query(IID_IClosable, "IClosable")
154
+ if closable is not None:
155
+ try:
156
+ closable.call(6, c_long, ())
157
+ finally:
158
+ closable.release()
159
+ obj.release()
160
+
161
+
162
+ IID_IClosable = GUID.from_str("30D5A829-7FA4-4026-83BB-D75BAE4EA99E")