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,655 @@
1
+ """Xlib + extensions through ctypes.
2
+
3
+ Reference: docs/research/linux-x11-and-audio.md. Key safety rules:
4
+
5
+ * Xlib's default error handler calls ``exit()``: a process-global handler is
6
+ installed that records errors per Display; requests that may fail go
7
+ through :meth:`XDisplay.checked` (issue, ``XSync``, inspect).
8
+ * The default IO-error path also exits; ``XSetIOErrorExitHandler`` (libX11
9
+ >= 1.7) is used so a dying X server becomes a Python exception.
10
+ * One :class:`XDisplay` per thread; never shared.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import ctypes
16
+ import ctypes.util
17
+ import threading
18
+ from collections.abc import Callable
19
+ from ctypes import (
20
+ CFUNCTYPE,
21
+ POINTER,
22
+ Structure,
23
+ byref,
24
+ c_char_p,
25
+ c_int,
26
+ c_long,
27
+ c_short,
28
+ c_ubyte,
29
+ c_uint,
30
+ c_ulong,
31
+ c_ushort,
32
+ c_void_p,
33
+ )
34
+ from typing import Any, TypeVar
35
+
36
+ from ...errors import CapabilityUnavailableError, NativeError, TargetLostError
37
+
38
+ T = TypeVar("T")
39
+
40
+ XID = c_ulong
41
+ Window = c_ulong
42
+ Atom = c_ulong
43
+ Drawable = c_ulong
44
+ Pixmap = c_ulong
45
+ Bool = c_int
46
+ Status = c_int
47
+
48
+ ZPixmap = 2
49
+ AllPlanes = 0xFFFFFFFFFFFFFFFF
50
+ LSBFirst = 0
51
+ IsUnmapped, IsUnviewable, IsViewable = 0, 1, 2
52
+ InputOutput = 1
53
+ StructureNotifyMask = 1 << 17
54
+ PropertyChangeMask = 1 << 22
55
+ DestroyNotify, UnmapNotify, MapNotify, ConfigureNotify = 17, 18, 19, 22
56
+ CompositeRedirectAutomatic = 0
57
+ XDamageReportNonEmpty = 3
58
+ XA_WINDOW = 33
59
+ XA_CARDINAL = 6
60
+ AnyPropertyType = 0
61
+ BadWindow, BadMatch, BadDrawable, BadAccess = 3, 8, 9, 10
62
+ XRES_CLIENT_ID_PID_MASK = 1 << 1
63
+
64
+
65
+ def _load(name: str, soname: str, required: bool = True) -> ctypes.CDLL | None:
66
+ for cand in (soname, ctypes.util.find_library(name)):
67
+ if cand:
68
+ try:
69
+ return ctypes.CDLL(cand)
70
+ except OSError:
71
+ continue
72
+ if required:
73
+ raise CapabilityUnavailableError(
74
+ f"{soname} not found (install the X11 client library package, e.g. libx11-6)"
75
+ )
76
+ return None
77
+
78
+
79
+ # --- structures ------------------------------------------------------------------
80
+
81
+
82
+ class XImage(Structure):
83
+ _fields_ = [
84
+ ("width", c_int),
85
+ ("height", c_int),
86
+ ("xoffset", c_int),
87
+ ("format", c_int),
88
+ ("data", c_void_p),
89
+ ("byte_order", c_int),
90
+ ("bitmap_unit", c_int),
91
+ ("bitmap_bit_order", c_int),
92
+ ("bitmap_pad", c_int),
93
+ ("depth", c_int),
94
+ ("bytes_per_line", c_int),
95
+ ("bits_per_pixel", c_int),
96
+ ("red_mask", c_ulong),
97
+ ("green_mask", c_ulong),
98
+ ("blue_mask", c_ulong),
99
+ ("obdata", c_void_p),
100
+ ("funcs", c_void_p * 6),
101
+ ]
102
+
103
+
104
+ class Visual(Structure):
105
+ _fields_ = [
106
+ ("ext_data", c_void_p),
107
+ ("visualid", c_ulong),
108
+ ("class_", c_int),
109
+ ("red_mask", c_ulong),
110
+ ("green_mask", c_ulong),
111
+ ("blue_mask", c_ulong),
112
+ ("bits_per_rgb", c_int),
113
+ ("map_entries", c_int),
114
+ ]
115
+
116
+
117
+ class XShmSegmentInfo(Structure):
118
+ _fields_ = [("shmseg", c_ulong), ("shmid", c_int), ("shmaddr", c_void_p), ("readOnly", Bool)]
119
+
120
+
121
+ class XWindowAttributes(Structure):
122
+ _fields_ = [
123
+ ("x", c_int),
124
+ ("y", c_int),
125
+ ("width", c_int),
126
+ ("height", c_int),
127
+ ("border_width", c_int),
128
+ ("depth", c_int),
129
+ ("visual", c_void_p),
130
+ ("root", Window),
131
+ ("class_", c_int),
132
+ ("bit_gravity", c_int),
133
+ ("win_gravity", c_int),
134
+ ("backing_store", c_int),
135
+ ("backing_planes", c_ulong),
136
+ ("backing_pixel", c_ulong),
137
+ ("save_under", Bool),
138
+ ("colormap", c_ulong),
139
+ ("map_installed", Bool),
140
+ ("map_state", c_int),
141
+ ("all_event_masks", c_long),
142
+ ("your_event_mask", c_long),
143
+ ("do_not_propagate_mask", c_long),
144
+ ("override_redirect", Bool),
145
+ ("screen", c_void_p),
146
+ ]
147
+
148
+
149
+ class XErrorEvent(Structure):
150
+ _fields_ = [
151
+ ("type", c_int),
152
+ ("display", c_void_p),
153
+ ("resourceid", XID),
154
+ ("serial", c_ulong),
155
+ ("error_code", c_ubyte),
156
+ ("request_code", c_ubyte),
157
+ ("minor_code", c_ubyte),
158
+ ]
159
+
160
+
161
+ class XAnyEvent(Structure):
162
+ _fields_ = [
163
+ ("type", c_int),
164
+ ("serial", c_ulong),
165
+ ("send_event", Bool),
166
+ ("display", c_void_p),
167
+ ("window", Window),
168
+ ]
169
+
170
+
171
+ class XConfigureEvent(Structure):
172
+ _fields_ = [
173
+ ("type", c_int),
174
+ ("serial", c_ulong),
175
+ ("send_event", Bool),
176
+ ("display", c_void_p),
177
+ ("event", Window),
178
+ ("window", Window),
179
+ ("x", c_int),
180
+ ("y", c_int),
181
+ ("width", c_int),
182
+ ("height", c_int),
183
+ ("border_width", c_int),
184
+ ("above", Window),
185
+ ("override_redirect", Bool),
186
+ ]
187
+
188
+
189
+ class XStructureEvent(Structure): # Destroy/Unmap/Map share event+window at the start
190
+ _fields_ = [
191
+ ("type", c_int),
192
+ ("serial", c_ulong),
193
+ ("send_event", Bool),
194
+ ("display", c_void_p),
195
+ ("event", Window),
196
+ ("window", Window),
197
+ ]
198
+
199
+
200
+ XEvent = c_long * 24
201
+
202
+
203
+ class XRRMonitorInfo(Structure):
204
+ _fields_ = [
205
+ ("name", Atom),
206
+ ("primary", Bool),
207
+ ("automatic", Bool),
208
+ ("noutput", c_int),
209
+ ("x", c_int),
210
+ ("y", c_int),
211
+ ("width", c_int),
212
+ ("height", c_int),
213
+ ("mwidth", c_int),
214
+ ("mheight", c_int),
215
+ ("outputs", POINTER(c_ulong)),
216
+ ]
217
+
218
+
219
+ class XRRScreenResources(Structure):
220
+ _fields_ = [
221
+ ("timestamp", c_ulong),
222
+ ("configTimestamp", c_ulong),
223
+ ("ncrtc", c_int),
224
+ ("crtcs", POINTER(c_ulong)),
225
+ ("noutput", c_int),
226
+ ("outputs", POINTER(c_ulong)),
227
+ ("nmode", c_int),
228
+ ("modes", c_void_p),
229
+ ]
230
+
231
+
232
+ class XRRCrtcInfo(Structure):
233
+ _fields_ = [
234
+ ("timestamp", c_ulong),
235
+ ("x", c_int),
236
+ ("y", c_int),
237
+ ("width", c_uint),
238
+ ("height", c_uint),
239
+ ("mode", c_ulong),
240
+ ("rotation", c_ushort),
241
+ ("noutput", c_int),
242
+ ("outputs", POINTER(c_ulong)),
243
+ ("rotations", c_ushort),
244
+ ("npossible", c_int),
245
+ ("possible", POINTER(c_ulong)),
246
+ ]
247
+
248
+
249
+ class XineramaScreenInfo(Structure):
250
+ _fields_ = [
251
+ ("screen_number", c_int),
252
+ ("x_org", c_short),
253
+ ("y_org", c_short),
254
+ ("width", c_short),
255
+ ("height", c_short),
256
+ ]
257
+
258
+
259
+ class XResClientIdSpec(Structure):
260
+ _fields_ = [("client", XID), ("mask", c_uint)]
261
+
262
+
263
+ class XResClientIdValue(Structure):
264
+ _fields_ = [("spec", XResClientIdSpec), ("length", c_long), ("value", c_void_p)]
265
+
266
+
267
+ class XClassHint(Structure):
268
+ _fields_ = [("res_name", c_void_p), ("res_class", c_void_p)]
269
+
270
+
271
+ # --- library setup ------------------------------------------------------------------
272
+
273
+
274
+ def _sig(lib: ctypes.CDLL | None, name: str, restype: Any, *argtypes: Any) -> None:
275
+ if lib is None or not hasattr(lib, name):
276
+ return
277
+ fn = getattr(lib, name)
278
+ fn.restype = restype
279
+ fn.argtypes = argtypes
280
+
281
+
282
+ _x11_lib = _load("X11", "libX11.so.6")
283
+ assert _x11_lib is not None # required=True raises otherwise
284
+ x11: ctypes.CDLL = _x11_lib
285
+ xext = _load("Xext", "libXext.so.6", required=False)
286
+ xcomposite = _load("Xcomposite", "libXcomposite.so.1", required=False)
287
+ xdamage = _load("Xdamage", "libXdamage.so.1", required=False)
288
+ xfixes = _load("Xfixes", "libXfixes.so.3", required=False)
289
+ xrandr = _load("Xrandr", "libXrandr.so.2", required=False)
290
+ xres = _load("XRes", "libXRes.so.1", required=False)
291
+ xinerama = _load("Xinerama", "libXinerama.so.1", required=False)
292
+ libc = ctypes.CDLL(None, use_errno=True)
293
+
294
+ _sig(x11, "XInitThreads", Status)
295
+ _sig(x11, "XOpenDisplay", c_void_p, c_char_p)
296
+ _sig(x11, "XCloseDisplay", c_int, c_void_p)
297
+ _sig(x11, "XDefaultScreen", c_int, c_void_p)
298
+ _sig(x11, "XScreenCount", c_int, c_void_p)
299
+ _sig(x11, "XRootWindow", Window, c_void_p, c_int)
300
+ _sig(x11, "XDefaultVisual", c_void_p, c_void_p, c_int)
301
+ _sig(x11, "XDefaultDepth", c_int, c_void_p, c_int)
302
+ _sig(x11, "XDisplayWidth", c_int, c_void_p, c_int)
303
+ _sig(x11, "XDisplayHeight", c_int, c_void_p, c_int)
304
+ _sig(x11, "XSync", c_int, c_void_p, Bool)
305
+ _sig(x11, "XFlush", c_int, c_void_p)
306
+ _sig(x11, "XFree", c_int, c_void_p)
307
+ _sig(x11, "XNextRequest", c_ulong, c_void_p)
308
+ _sig(x11, "XSetErrorHandler", c_void_p, c_void_p)
309
+ _sig(x11, "XSetIOErrorExitHandler", None, c_void_p, c_void_p, c_void_p)
310
+ _sig(
311
+ x11, "XQueryExtension", Bool, c_void_p, c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)
312
+ )
313
+ _sig(x11, "XInternAtom", Atom, c_void_p, c_char_p, Bool)
314
+ _sig(x11, "XGetAtomName", c_void_p, c_void_p, Atom)
315
+ _sig(
316
+ x11,
317
+ "XGetWindowProperty",
318
+ c_int,
319
+ c_void_p,
320
+ Window,
321
+ Atom,
322
+ c_long,
323
+ c_long,
324
+ Bool,
325
+ Atom,
326
+ POINTER(Atom),
327
+ POINTER(c_int),
328
+ POINTER(c_ulong),
329
+ POINTER(c_ulong),
330
+ POINTER(c_void_p),
331
+ )
332
+ _sig(x11, "XGetWindowAttributes", Status, c_void_p, Window, POINTER(XWindowAttributes))
333
+ _sig(
334
+ x11,
335
+ "XTranslateCoordinates",
336
+ Bool,
337
+ c_void_p,
338
+ Window,
339
+ Window,
340
+ c_int,
341
+ c_int,
342
+ POINTER(c_int),
343
+ POINTER(c_int),
344
+ POINTER(Window),
345
+ )
346
+ _sig(
347
+ x11,
348
+ "XQueryTree",
349
+ Status,
350
+ c_void_p,
351
+ Window,
352
+ POINTER(Window),
353
+ POINTER(Window),
354
+ POINTER(POINTER(Window)),
355
+ POINTER(c_uint),
356
+ )
357
+ _sig(x11, "XGetClassHint", Status, c_void_p, Window, POINTER(XClassHint))
358
+ _sig(x11, "XFetchName", Status, c_void_p, Window, POINTER(c_void_p))
359
+ _sig(
360
+ x11,
361
+ "XGetImage",
362
+ POINTER(XImage),
363
+ c_void_p,
364
+ Drawable,
365
+ c_int,
366
+ c_int,
367
+ c_uint,
368
+ c_uint,
369
+ c_ulong,
370
+ c_int,
371
+ )
372
+ _sig(x11, "XDestroyImage", c_int, POINTER(XImage))
373
+ _sig(x11, "XSelectInput", c_int, c_void_p, Window, c_long)
374
+ _sig(x11, "XPending", c_int, c_void_p)
375
+ _sig(x11, "XNextEvent", c_int, c_void_p, POINTER(XEvent))
376
+ _sig(x11, "XFreePixmap", c_int, c_void_p, Pixmap)
377
+ _sig(x11, "XServerVendor", c_char_p, c_void_p)
378
+ _sig(x11, "XVendorRelease", c_int, c_void_p)
379
+
380
+ _sig(xext, "XShmQueryExtension", Bool, c_void_p)
381
+ _sig(
382
+ xext,
383
+ "XShmCreateImage",
384
+ POINTER(XImage),
385
+ c_void_p,
386
+ c_void_p,
387
+ c_uint,
388
+ c_int,
389
+ c_void_p,
390
+ POINTER(XShmSegmentInfo),
391
+ c_uint,
392
+ c_uint,
393
+ )
394
+ _sig(xext, "XShmAttach", Bool, c_void_p, POINTER(XShmSegmentInfo))
395
+ _sig(xext, "XShmDetach", Bool, c_void_p, POINTER(XShmSegmentInfo))
396
+ _sig(xext, "XShmGetImage", Bool, c_void_p, Drawable, POINTER(XImage), c_int, c_int, c_ulong)
397
+
398
+ _sig(xcomposite, "XCompositeQueryExtension", Bool, c_void_p, POINTER(c_int), POINTER(c_int))
399
+ _sig(xcomposite, "XCompositeQueryVersion", Status, c_void_p, POINTER(c_int), POINTER(c_int))
400
+ _sig(xcomposite, "XCompositeRedirectWindow", None, c_void_p, Window, c_int)
401
+ _sig(xcomposite, "XCompositeUnredirectWindow", None, c_void_p, Window, c_int)
402
+ _sig(xcomposite, "XCompositeNameWindowPixmap", Pixmap, c_void_p, Window)
403
+
404
+ _sig(xdamage, "XDamageQueryExtension", Bool, c_void_p, POINTER(c_int), POINTER(c_int))
405
+ _sig(xdamage, "XDamageCreate", XID, c_void_p, Drawable, c_int)
406
+ _sig(xdamage, "XDamageDestroy", None, c_void_p, XID)
407
+ _sig(xdamage, "XDamageSubtract", None, c_void_p, XID, XID, XID)
408
+
409
+ _sig(xrandr, "XRRQueryExtension", Bool, c_void_p, POINTER(c_int), POINTER(c_int))
410
+ _sig(xrandr, "XRRQueryVersion", Status, c_void_p, POINTER(c_int), POINTER(c_int))
411
+ _sig(xrandr, "XRRGetMonitors", POINTER(XRRMonitorInfo), c_void_p, Window, Bool, POINTER(c_int))
412
+ _sig(xrandr, "XRRFreeMonitors", None, POINTER(XRRMonitorInfo))
413
+ _sig(xrandr, "XRRGetScreenResourcesCurrent", POINTER(XRRScreenResources), c_void_p, Window)
414
+ _sig(xrandr, "XRRFreeScreenResources", None, POINTER(XRRScreenResources))
415
+ _sig(xrandr, "XRRGetCrtcInfo", POINTER(XRRCrtcInfo), c_void_p, POINTER(XRRScreenResources), c_ulong)
416
+ _sig(xrandr, "XRRFreeCrtcInfo", None, POINTER(XRRCrtcInfo))
417
+ _sig(xrandr, "XRRGetOutputPrimary", c_ulong, c_void_p, Window)
418
+
419
+ _sig(xinerama, "XineramaIsActive", Bool, c_void_p)
420
+ _sig(xinerama, "XineramaQueryScreens", POINTER(XineramaScreenInfo), c_void_p, POINTER(c_int))
421
+
422
+ _sig(xres, "XResQueryExtension", Bool, c_void_p, POINTER(c_int), POINTER(c_int))
423
+ _sig(
424
+ xres,
425
+ "XResQueryClientIds",
426
+ Status,
427
+ c_void_p,
428
+ c_long,
429
+ POINTER(XResClientIdSpec),
430
+ POINTER(c_long),
431
+ POINTER(POINTER(XResClientIdValue)),
432
+ )
433
+ _sig(xres, "XResGetClientPid", c_int, POINTER(XResClientIdValue))
434
+ _sig(xres, "XResClientIdsDestroy", None, c_long, POINTER(XResClientIdValue))
435
+
436
+ _sig(libc, "shmget", c_int, c_int, ctypes.c_size_t, c_int)
437
+ _sig(libc, "shmat", c_void_p, c_int, c_void_p, c_int)
438
+ _sig(libc, "shmdt", c_int, c_void_p)
439
+ _sig(libc, "shmctl", c_int, c_int, c_int, c_void_p)
440
+
441
+ IPC_PRIVATE, IPC_CREAT, IPC_RMID = 0, 0o1000, 0
442
+
443
+ # libX11 >= 1.8 calls XInitThreads itself; calling it again is harmless and
444
+ # needed for older versions (must precede any other Xlib call).
445
+ x11.XInitThreads()
446
+
447
+
448
+ # --- error handling -------------------------------------------------------------------
449
+
450
+
451
+ class XError(NativeError):
452
+ def __init__(self, what: str, error_code: int, request_code: int = 0) -> None:
453
+ self.error_code = error_code
454
+ self.request_code = request_code
455
+ super().__init__(f"{what}: X error {error_code} (request {request_code})", error_code)
456
+
457
+
458
+ class XConnectionLostError(TargetLostError):
459
+ """The X server connection died (server exited, display closed)."""
460
+
461
+
462
+ _errors_lock = threading.Lock()
463
+ _errors: dict[int, list[tuple[int, int, int]]] = {}
464
+ _dead: set[int] = set()
465
+ _ours: set[int] = set() # Display pointers opened by capturekit
466
+
467
+ _ERROR_HANDLER = CFUNCTYPE(c_int, c_void_p, POINTER(XErrorEvent))
468
+ _IO_ERROR_HANDLER = CFUNCTYPE(c_int, c_void_p)
469
+ _IO_EXIT_HANDLER = CFUNCTYPE(None, c_void_p, c_void_p)
470
+
471
+ _sig(x11, "XSetIOErrorHandler", c_void_p, c_void_p)
472
+
473
+ # Handlers are process-global. Errors on displays capturekit does not own are
474
+ # forwarded to whatever handler was installed before (e.g. Tk/Qt), so other
475
+ # X clients in the same process keep their behaviour.
476
+ _prev_error: Any = None
477
+ _prev_io_error: Any = None
478
+
479
+
480
+ def _on_error(dpy: int | None, ev: Any) -> int:
481
+ key = int(dpy or 0)
482
+ with _errors_lock:
483
+ mine = key in _ours
484
+ if mine:
485
+ e = ev.contents
486
+ _errors.setdefault(key, []).append(
487
+ (int(e.error_code), int(e.request_code), int(e.resourceid))
488
+ )
489
+ if not mine and _prev_error is not None:
490
+ return int(_prev_error(dpy, ev))
491
+ return 0
492
+
493
+
494
+ def _on_io_error(dpy: int | None) -> int:
495
+ # Xlib's default IO error handler calls exit(1) *before* the per-display
496
+ # exit handler runs (libX11 XlibInt.c, verified 2026-10-11), so ours must
497
+ # return for capturekit's displays.
498
+ key = int(dpy or 0)
499
+ with _errors_lock:
500
+ mine = key in _ours
501
+ if mine:
502
+ _dead.add(key)
503
+ if not mine and _prev_io_error is not None:
504
+ return int(_prev_io_error(dpy))
505
+ return 0
506
+
507
+
508
+ def _on_io_exit(dpy: int | None, _data: int | None) -> None:
509
+ # Must not call Xlib here. Mark the display dead; Python code checks it.
510
+ with _errors_lock:
511
+ _dead.add(int(dpy or 0))
512
+
513
+
514
+ _error_handler_ref = _ERROR_HANDLER(_on_error)
515
+ _io_error_ref = _IO_ERROR_HANDLER(_on_io_error)
516
+ _io_exit_ref = _IO_EXIT_HANDLER(_on_io_exit)
517
+ _old = x11.XSetErrorHandler(ctypes.cast(_error_handler_ref, c_void_p))
518
+ _prev_error = _ERROR_HANDLER(_old) if _old else None
519
+ #: libX11 >= 1.7: a dying X server can be survived. Older versions exit the
520
+ #: process once the IO error handler returns, so we leave their default alone.
521
+ HAS_IO_EXIT_HANDLER = hasattr(x11, "XSetIOErrorExitHandler")
522
+ if HAS_IO_EXIT_HANDLER:
523
+ _old_io = x11.XSetIOErrorHandler(ctypes.cast(_io_error_ref, c_void_p))
524
+ _prev_io_error = _IO_ERROR_HANDLER(_old_io) if _old_io else None
525
+
526
+
527
+ class XDisplay:
528
+ """A private X connection (one per thread)."""
529
+
530
+ def __init__(self, name: str) -> None:
531
+ self.name = name
532
+ ptr = x11.XOpenDisplay(name.encode())
533
+ if not ptr:
534
+ raise CapabilityUnavailableError(f"cannot open X display {name!r}")
535
+ self.ptr: int = int(ptr)
536
+ with _errors_lock:
537
+ _ours.add(self.ptr)
538
+ if HAS_IO_EXIT_HANDLER:
539
+ x11.XSetIOErrorExitHandler(self.ptr, ctypes.cast(_io_exit_ref, c_void_p), None)
540
+ self.screen = x11.XDefaultScreen(self.ptr)
541
+ self.root = int(x11.XRootWindow(self.ptr, self.screen))
542
+ self._atoms: dict[str, int] = {}
543
+ self._ext: dict[str, bool] = {}
544
+ self.thread = threading.get_ident()
545
+
546
+ # -- lifecycle -------------------------------------------------------------
547
+ def close(self) -> None:
548
+ if self.ptr:
549
+ ptr, self.ptr = self.ptr, 0
550
+ with _errors_lock:
551
+ _errors.pop(ptr, None)
552
+ # Even a dead connection must be closed to free Xlib's structures;
553
+ # XCloseDisplay on a broken connection does not touch the socket.
554
+ x11.XCloseDisplay(ptr)
555
+ with _errors_lock:
556
+ _dead.discard(ptr)
557
+ _ours.discard(ptr)
558
+
559
+ def __enter__(self) -> XDisplay:
560
+ return self
561
+
562
+ def __exit__(self, *exc: object) -> None:
563
+ self.close()
564
+
565
+ @property
566
+ def alive(self) -> bool:
567
+ with _errors_lock:
568
+ return bool(self.ptr) and self.ptr not in _dead
569
+
570
+ def ensure_alive(self) -> None:
571
+ if not self.alive:
572
+ raise XConnectionLostError(f"connection to X display {self.name!r} was lost")
573
+
574
+ # -- error-checked calls -----------------------------------------------------
575
+ def _take_errors(self) -> list[tuple[int, int, int]]:
576
+ with _errors_lock:
577
+ return _errors.pop(self.ptr, [])
578
+
579
+ def sync(self) -> None:
580
+ self.ensure_alive()
581
+ x11.XSync(self.ptr, False)
582
+ self.ensure_alive()
583
+
584
+ def checked(self, what: str, fn: Callable[..., T], *args: Any) -> T:
585
+ """Call ``fn``, round-trip with XSync and raise :class:`XError` on errors."""
586
+ self.ensure_alive()
587
+ self._take_errors()
588
+ res = fn(*args)
589
+ x11.XSync(self.ptr, False)
590
+ self.ensure_alive()
591
+ errs = self._take_errors()
592
+ if errs:
593
+ code, req, _rid = errs[0]
594
+ raise XError(what, code, req)
595
+ return res
596
+
597
+ # -- helpers ----------------------------------------------------------------------
598
+ def atom(self, name: str) -> int:
599
+ a = self._atoms.get(name)
600
+ if a is None:
601
+ a = int(x11.XInternAtom(self.ptr, name.encode(), False))
602
+ self._atoms[name] = a
603
+ return a
604
+
605
+ def has_extension(self, name: str) -> bool:
606
+ if name not in self._ext:
607
+ op, ev, er = c_int(), c_int(), c_int()
608
+ self._ext[name] = bool(
609
+ x11.XQueryExtension(self.ptr, name.encode(), byref(op), byref(ev), byref(er))
610
+ )
611
+ return self._ext[name]
612
+
613
+ def extension_event_base(self, name: str) -> int | None:
614
+ op, ev, er = c_int(), c_int(), c_int()
615
+ if x11.XQueryExtension(self.ptr, name.encode(), byref(op), byref(ev), byref(er)):
616
+ return int(ev.value)
617
+ return None
618
+
619
+ def get_property(
620
+ self,
621
+ window: int,
622
+ name: str,
623
+ req_type: int = AnyPropertyType,
624
+ ) -> tuple[int, int, bytes, int] | None:
625
+ """(type, format, raw bytes, item count) or None. 32-bit items are C longs."""
626
+ actual_type, actual_format = Atom(), c_int()
627
+ nitems, after = c_ulong(), c_ulong()
628
+ data = c_void_p()
629
+ status = self.checked(
630
+ f"XGetWindowProperty({name})", x11.XGetWindowProperty, self.ptr, window,
631
+ self.atom(name), 0, 0x7FFFFFFF, False, req_type, byref(actual_type),
632
+ byref(actual_format), byref(nitems), byref(after), byref(data),
633
+ ) # fmt: skip
634
+ if status != 0 or not data.value:
635
+ return None
636
+ try:
637
+ fmt = actual_format.value
638
+ item_size = {8: 1, 16: ctypes.sizeof(c_short), 32: ctypes.sizeof(c_long)}.get(fmt, 1)
639
+ raw = ctypes.string_at(data.value, nitems.value * item_size)
640
+ return int(actual_type.value), fmt, raw, int(nitems.value)
641
+ finally:
642
+ x11.XFree(data)
643
+
644
+ def get_longs(self, window: int, name: str, req_type: int = AnyPropertyType) -> list[int]:
645
+ prop = self.get_property(window, name, req_type)
646
+ if prop is None or prop[1] != 32:
647
+ return []
648
+ arr = (c_ulong * prop[3]).from_buffer_copy(prop[2])
649
+ return [int(v) for v in arr]
650
+
651
+ def get_string(self, window: int, name: str) -> str | None:
652
+ prop = self.get_property(window, name)
653
+ if prop is None or prop[1] != 8:
654
+ return None
655
+ return prop[2].decode("utf-8", "replace").rstrip("\x00")