pyglet 2.1.13__py3-none-any.whl → 3.0.dev1__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 (267) hide show
  1. pyglet/__init__.py +67 -61
  2. pyglet/__init__.pyi +15 -8
  3. pyglet/app/__init__.py +22 -13
  4. pyglet/app/async_app.py +212 -0
  5. pyglet/app/base.py +2 -1
  6. pyglet/app/{xlib.py → linux.py} +3 -3
  7. pyglet/config/__init__.py +101 -0
  8. pyglet/config/gl/__init__.py +30 -0
  9. pyglet/config/gl/egl.py +120 -0
  10. pyglet/config/gl/macos.py +262 -0
  11. pyglet/config/gl/windows.py +267 -0
  12. pyglet/config/gl/x11.py +142 -0
  13. pyglet/customtypes.py +43 -2
  14. pyglet/display/__init__.py +8 -6
  15. pyglet/display/base.py +3 -63
  16. pyglet/display/cocoa.py +12 -17
  17. pyglet/display/emscripten.py +39 -0
  18. pyglet/display/headless.py +23 -30
  19. pyglet/display/wayland.py +157 -0
  20. pyglet/display/win32.py +5 -21
  21. pyglet/display/xlib.py +19 -27
  22. pyglet/display/xlib_vidmoderestore.py +2 -2
  23. pyglet/enums.py +183 -0
  24. pyglet/event.py +0 -1
  25. pyglet/experimental/geoshader_sprite.py +15 -13
  26. pyglet/experimental/hidraw.py +6 -15
  27. pyglet/experimental/multitexture_sprite.py +31 -19
  28. pyglet/experimental/particles.py +13 -35
  29. pyglet/font/__init__.py +251 -85
  30. pyglet/font/base.py +116 -61
  31. pyglet/font/dwrite/__init__.py +349 -204
  32. pyglet/font/dwrite/dwrite_lib.py +27 -5
  33. pyglet/font/fontconfig.py +14 -6
  34. pyglet/font/freetype.py +138 -87
  35. pyglet/font/freetype_lib.py +19 -0
  36. pyglet/font/group.py +179 -0
  37. pyglet/font/harfbuzz/__init__.py +3 -3
  38. pyglet/font/pyodide_js.py +310 -0
  39. pyglet/font/quartz.py +319 -126
  40. pyglet/font/ttf.py +45 -3
  41. pyglet/font/user.py +14 -19
  42. pyglet/font/win32.py +45 -21
  43. pyglet/graphics/__init__.py +8 -787
  44. pyglet/graphics/allocation.py +115 -1
  45. pyglet/graphics/api/__init__.py +77 -0
  46. pyglet/graphics/api/base.py +299 -0
  47. pyglet/graphics/api/gl/__init__.py +58 -0
  48. pyglet/graphics/api/gl/base.py +24 -0
  49. pyglet/graphics/{vertexbuffer.py → api/gl/buffer.py} +104 -159
  50. pyglet/graphics/api/gl/cocoa/context.py +76 -0
  51. pyglet/graphics/api/gl/context.py +391 -0
  52. pyglet/graphics/api/gl/default_shaders.py +0 -0
  53. pyglet/graphics/api/gl/draw.py +627 -0
  54. pyglet/graphics/api/gl/egl/__init__.py +0 -0
  55. pyglet/graphics/api/gl/egl/context.py +92 -0
  56. pyglet/graphics/api/gl/enums.py +76 -0
  57. pyglet/graphics/api/gl/framebuffer.py +315 -0
  58. pyglet/graphics/api/gl/gl.py +5463 -0
  59. pyglet/graphics/api/gl/gl_info.py +188 -0
  60. pyglet/graphics/api/gl/global_opengl.py +226 -0
  61. pyglet/{gl → graphics/api/gl}/lib.py +34 -18
  62. pyglet/graphics/api/gl/shader.py +1476 -0
  63. pyglet/graphics/api/gl/shapes.py +55 -0
  64. pyglet/graphics/api/gl/sprite.py +102 -0
  65. pyglet/graphics/api/gl/state.py +219 -0
  66. pyglet/graphics/api/gl/text.py +190 -0
  67. pyglet/graphics/api/gl/texture.py +1526 -0
  68. pyglet/graphics/{vertexarray.py → api/gl/vertexarray.py} +11 -13
  69. pyglet/graphics/api/gl/vertexdomain.py +751 -0
  70. pyglet/graphics/api/gl/win32/__init__.py +0 -0
  71. pyglet/graphics/api/gl/win32/context.py +108 -0
  72. pyglet/graphics/api/gl/win32/wgl_info.py +24 -0
  73. pyglet/graphics/api/gl/xlib/__init__.py +0 -0
  74. pyglet/graphics/api/gl/xlib/context.py +174 -0
  75. pyglet/{gl → graphics/api/gl/xlib}/glx_info.py +26 -31
  76. pyglet/graphics/api/gl1/__init__.py +0 -0
  77. pyglet/{gl → graphics/api/gl1}/gl_compat.py +3 -2
  78. pyglet/graphics/api/gl2/__init__.py +0 -0
  79. pyglet/graphics/api/gl2/buffer.py +320 -0
  80. pyglet/graphics/api/gl2/draw.py +600 -0
  81. pyglet/graphics/api/gl2/global_opengl.py +122 -0
  82. pyglet/graphics/api/gl2/shader.py +200 -0
  83. pyglet/graphics/api/gl2/shapes.py +51 -0
  84. pyglet/graphics/api/gl2/sprite.py +79 -0
  85. pyglet/graphics/api/gl2/text.py +175 -0
  86. pyglet/graphics/api/gl2/vertexdomain.py +364 -0
  87. pyglet/graphics/api/webgl/__init__.py +233 -0
  88. pyglet/graphics/api/webgl/buffer.py +302 -0
  89. pyglet/graphics/api/webgl/context.py +234 -0
  90. pyglet/graphics/api/webgl/draw.py +590 -0
  91. pyglet/graphics/api/webgl/enums.py +76 -0
  92. pyglet/graphics/api/webgl/framebuffer.py +360 -0
  93. pyglet/graphics/api/webgl/gl.py +1537 -0
  94. pyglet/graphics/api/webgl/gl_info.py +130 -0
  95. pyglet/graphics/api/webgl/shader.py +1346 -0
  96. pyglet/graphics/api/webgl/shapes.py +92 -0
  97. pyglet/graphics/api/webgl/sprite.py +102 -0
  98. pyglet/graphics/api/webgl/state.py +227 -0
  99. pyglet/graphics/api/webgl/text.py +187 -0
  100. pyglet/graphics/api/webgl/texture.py +1227 -0
  101. pyglet/graphics/api/webgl/vertexarray.py +54 -0
  102. pyglet/graphics/api/webgl/vertexdomain.py +616 -0
  103. pyglet/graphics/api/webgl/webgl_js.pyi +307 -0
  104. pyglet/{image → graphics}/atlas.py +33 -32
  105. pyglet/graphics/base.py +10 -0
  106. pyglet/graphics/buffer.py +245 -0
  107. pyglet/graphics/draw.py +578 -0
  108. pyglet/graphics/framebuffer.py +26 -0
  109. pyglet/graphics/instance.py +178 -69
  110. pyglet/graphics/shader.py +267 -1553
  111. pyglet/graphics/state.py +83 -0
  112. pyglet/graphics/texture.py +703 -0
  113. pyglet/graphics/vertexdomain.py +695 -538
  114. pyglet/gui/ninepatch.py +10 -10
  115. pyglet/gui/widgets.py +120 -10
  116. pyglet/image/__init__.py +20 -1973
  117. pyglet/image/animation.py +12 -12
  118. pyglet/image/base.py +730 -0
  119. pyglet/image/codecs/__init__.py +9 -0
  120. pyglet/image/codecs/bmp.py +53 -30
  121. pyglet/image/codecs/dds.py +53 -31
  122. pyglet/image/codecs/gdiplus.py +38 -14
  123. pyglet/image/codecs/gdkpixbuf2.py +0 -2
  124. pyglet/image/codecs/js_image.py +99 -0
  125. pyglet/image/codecs/ktx2.py +161 -0
  126. pyglet/image/codecs/pil.py +1 -1
  127. pyglet/image/codecs/png.py +1 -1
  128. pyglet/image/codecs/wic.py +11 -2
  129. pyglet/info.py +26 -24
  130. pyglet/input/__init__.py +8 -0
  131. pyglet/input/base.py +163 -105
  132. pyglet/input/controller.py +13 -19
  133. pyglet/input/controller_db.py +39 -24
  134. pyglet/input/emscripten/__init__.py +18 -0
  135. pyglet/input/emscripten/gamepad_js.py +397 -0
  136. pyglet/input/linux/__init__.py +11 -5
  137. pyglet/input/linux/evdev.py +10 -11
  138. pyglet/input/linux/x11_xinput.py +2 -2
  139. pyglet/input/linux/x11_xinput_tablet.py +1 -1
  140. pyglet/input/macos/__init__.py +7 -2
  141. pyglet/input/macos/darwin_gc.py +559 -0
  142. pyglet/input/win32/__init__.py +1 -1
  143. pyglet/input/win32/directinput.py +34 -29
  144. pyglet/input/win32/xinput.py +11 -61
  145. pyglet/lib.py +3 -3
  146. pyglet/libs/__init__.py +1 -1
  147. pyglet/{gl → libs/darwin}/agl.py +1 -1
  148. pyglet/libs/darwin/cocoapy/__init__.py +2 -2
  149. pyglet/libs/darwin/cocoapy/cocoahelpers.py +181 -0
  150. pyglet/libs/darwin/cocoapy/cocoalibs.py +31 -0
  151. pyglet/libs/darwin/cocoapy/cocoatypes.py +27 -0
  152. pyglet/libs/darwin/cocoapy/runtime.py +81 -45
  153. pyglet/libs/darwin/coreaudio.py +4 -4
  154. pyglet/{gl → libs/darwin}/lib_agl.py +9 -8
  155. pyglet/libs/darwin/quartzkey.py +1 -3
  156. pyglet/libs/egl/__init__.py +2 -0
  157. pyglet/libs/egl/egl_lib.py +576 -0
  158. pyglet/libs/egl/eglext.py +51 -5
  159. pyglet/libs/linux/__init__.py +0 -0
  160. pyglet/libs/linux/egl/__init__.py +0 -0
  161. pyglet/libs/linux/egl/eglext.py +22 -0
  162. pyglet/libs/linux/glx/__init__.py +0 -0
  163. pyglet/{gl → libs/linux/glx}/glx.py +13 -14
  164. pyglet/{gl → libs/linux/glx}/glxext_arb.py +408 -192
  165. pyglet/{gl → libs/linux/glx}/glxext_mesa.py +1 -1
  166. pyglet/{gl → libs/linux/glx}/glxext_nv.py +345 -164
  167. pyglet/{gl → libs/linux/glx}/lib_glx.py +3 -2
  168. pyglet/libs/linux/wayland/__init__.py +0 -0
  169. pyglet/libs/linux/wayland/client.py +1068 -0
  170. pyglet/libs/linux/wayland/lib_wayland.py +207 -0
  171. pyglet/libs/linux/wayland/wayland_egl.py +38 -0
  172. pyglet/libs/{wayland → linux/wayland}/xkbcommon.py +26 -0
  173. pyglet/libs/{x11 → linux/x11}/xf86vmode.py +4 -4
  174. pyglet/libs/{x11 → linux/x11}/xinerama.py +2 -2
  175. pyglet/libs/{x11 → linux/x11}/xinput.py +10 -10
  176. pyglet/libs/linux/x11/xrandr.py +0 -0
  177. pyglet/libs/{x11 → linux/x11}/xrender.py +1 -1
  178. pyglet/libs/shared/__init__.py +0 -0
  179. pyglet/libs/shared/spirv/__init__.py +0 -0
  180. pyglet/libs/shared/spirv/lib_shaderc.py +85 -0
  181. pyglet/libs/shared/spirv/lib_spirv_cross.py +126 -0
  182. pyglet/libs/win32/__init__.py +28 -8
  183. pyglet/libs/win32/constants.py +59 -48
  184. pyglet/libs/win32/context_managers.py +20 -3
  185. pyglet/libs/win32/dinput.py +105 -88
  186. pyglet/{gl → libs/win32}/lib_wgl.py +52 -26
  187. pyglet/libs/win32/types.py +58 -23
  188. pyglet/{gl → libs/win32}/wgl.py +32 -25
  189. pyglet/{gl → libs/win32}/wglext_arb.py +364 -2
  190. pyglet/media/__init__.py +9 -10
  191. pyglet/media/codecs/__init__.py +12 -1
  192. pyglet/media/codecs/base.py +99 -96
  193. pyglet/media/codecs/ffmpeg.py +2 -2
  194. pyglet/media/codecs/ffmpeg_lib/libavformat.py +3 -8
  195. pyglet/media/codecs/webaudio_pyodide.py +111 -0
  196. pyglet/media/drivers/__init__.py +9 -4
  197. pyglet/media/drivers/base.py +4 -4
  198. pyglet/media/drivers/openal/__init__.py +1 -1
  199. pyglet/media/drivers/openal/adaptation.py +3 -3
  200. pyglet/media/drivers/pulse/__init__.py +1 -1
  201. pyglet/media/drivers/pulse/adaptation.py +3 -3
  202. pyglet/media/drivers/pyodide_js/__init__.py +8 -0
  203. pyglet/media/drivers/pyodide_js/adaptation.py +288 -0
  204. pyglet/media/drivers/xaudio2/adaptation.py +3 -3
  205. pyglet/media/player.py +276 -193
  206. pyglet/media/player_worker_thread.py +1 -1
  207. pyglet/model/__init__.py +39 -29
  208. pyglet/model/codecs/base.py +4 -4
  209. pyglet/model/codecs/gltf.py +3 -3
  210. pyglet/model/codecs/obj.py +71 -43
  211. pyglet/resource.py +129 -78
  212. pyglet/shapes.py +154 -194
  213. pyglet/sprite.py +47 -164
  214. pyglet/text/__init__.py +44 -54
  215. pyglet/text/caret.py +12 -7
  216. pyglet/text/document.py +19 -17
  217. pyglet/text/formats/html.py +2 -2
  218. pyglet/text/formats/structured.py +10 -40
  219. pyglet/text/layout/__init__.py +20 -13
  220. pyglet/text/layout/base.py +176 -287
  221. pyglet/text/layout/incremental.py +9 -10
  222. pyglet/text/layout/scrolling.py +7 -95
  223. pyglet/window/__init__.py +183 -172
  224. pyglet/window/cocoa/__init__.py +62 -51
  225. pyglet/window/cocoa/pyglet_delegate.py +2 -25
  226. pyglet/window/cocoa/pyglet_view.py +9 -8
  227. pyglet/window/dialog/__init__.py +184 -0
  228. pyglet/window/dialog/base.py +99 -0
  229. pyglet/window/dialog/darwin.py +121 -0
  230. pyglet/window/dialog/linux.py +72 -0
  231. pyglet/window/dialog/windows.py +194 -0
  232. pyglet/window/emscripten/__init__.py +779 -0
  233. pyglet/window/headless/__init__.py +44 -28
  234. pyglet/window/key.py +2 -0
  235. pyglet/window/mouse.py +2 -2
  236. pyglet/window/wayland/__init__.py +377 -0
  237. pyglet/window/win32/__init__.py +101 -46
  238. pyglet/window/xlib/__init__.py +104 -66
  239. {pyglet-2.1.13.dist-info → pyglet-3.0.dev1.dist-info}/METADATA +2 -3
  240. pyglet-3.0.dev1.dist-info/RECORD +322 -0
  241. {pyglet-2.1.13.dist-info → pyglet-3.0.dev1.dist-info}/WHEEL +1 -1
  242. pyglet/gl/__init__.py +0 -208
  243. pyglet/gl/base.py +0 -499
  244. pyglet/gl/cocoa.py +0 -309
  245. pyglet/gl/gl.py +0 -4625
  246. pyglet/gl/gl.pyi +0 -2320
  247. pyglet/gl/gl_compat.pyi +0 -3097
  248. pyglet/gl/gl_info.py +0 -190
  249. pyglet/gl/headless.py +0 -166
  250. pyglet/gl/wgl_info.py +0 -36
  251. pyglet/gl/wglext_nv.py +0 -1096
  252. pyglet/gl/win32.py +0 -268
  253. pyglet/gl/xlib.py +0 -295
  254. pyglet/image/buffer.py +0 -274
  255. pyglet/image/codecs/s3tc.py +0 -354
  256. pyglet/libs/x11/xrandr.py +0 -166
  257. pyglet-2.1.13.dist-info/RECORD +0 -234
  258. /pyglet/{libs/wayland → graphics/api/gl/cocoa}/__init__.py +0 -0
  259. /pyglet/libs/{egl → linux/egl}/egl.py +0 -0
  260. /pyglet/libs/{egl → linux/egl}/lib.py +0 -0
  261. /pyglet/libs/{ioctl.py → linux/ioctl.py} +0 -0
  262. /pyglet/libs/{wayland → linux/wayland}/gbm.py +0 -0
  263. /pyglet/libs/{x11 → linux/x11}/__init__.py +0 -0
  264. /pyglet/libs/{x11 → linux/x11}/cursorfont.py +0 -0
  265. /pyglet/libs/{x11 → linux/x11}/xlib.py +0 -0
  266. /pyglet/libs/{x11 → linux/x11}/xsync.py +0 -0
  267. {pyglet-2.1.13.dist-info/licenses → pyglet-3.0.dev1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,1068 @@
1
+ from __future__ import annotations
2
+
3
+ import abc as _abc
4
+ import os as _os
5
+ import select as _select
6
+ import socket as _socket
7
+ import threading as _threading
8
+ import time
9
+ import weakref
10
+
11
+ from collections import defaultdict as _defaultdict
12
+ from collections import deque as _deque
13
+ from ctypes import CFUNCTYPE, POINTER, byref, c_char_p, c_int, c_int32, c_uint32, c_void_p, cast, pointer
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from struct import Struct as _Struct
17
+ from types import SimpleNamespace as _NameSpace
18
+ from typing import TYPE_CHECKING, Any, Callable
19
+ from xml.etree import ElementTree as _ElementTree
20
+ from xml.etree.ElementTree import Element, ParseError
21
+
22
+ if TYPE_CHECKING:
23
+ from ctypes import _Pointer
24
+
25
+
26
+ from pyglet.libs.linux.wayland.lib_wayland import (
27
+ as_proxy,
28
+ wl_array,
29
+ wl_display,
30
+ wl_display_cancel_read,
31
+ wl_display_connect,
32
+ wl_display_connect_to_fd,
33
+ wl_display_disconnect,
34
+ wl_display_dispatch_pending,
35
+ wl_display_flush,
36
+ wl_display_prepare_read,
37
+ wl_display_read_events,
38
+ wl_interface,
39
+ wl_message,
40
+ wl_proxy,
41
+ wl_proxy_add_listener,
42
+ wl_proxy_destroy,
43
+ wl_proxy_marshal,
44
+ wl_proxy_marshal_constructor_versioned,
45
+ wl_registry,
46
+ wl_registry_bind,
47
+ wl_display_get_fd,
48
+ )
49
+ from pyglet.util import debug_print
50
+
51
+
52
+ _debug_wayland = debug_print('debug_wayland')
53
+
54
+ USE_LIB_WAYLAND = True
55
+
56
+
57
+ class ObjectIDPool:
58
+ def __init__(self, minimum: int, maximum: int) -> None:
59
+ self._sequence = iter(range(minimum, maximum + 1))
60
+ self._recycle_pool = _deque()
61
+
62
+ def __next__(self) -> int:
63
+ if self._recycle_pool:
64
+ return self._recycle_pool.popleft()
65
+ return next(self._sequence)
66
+
67
+ def send(self, oid: int) -> None:
68
+ self._recycle_pool.append(oid)
69
+
70
+
71
+ ##################################
72
+ # Exceptions
73
+ ##################################
74
+
75
+
76
+ class WaylandException(BaseException):
77
+ """Base Wayland Exception"""
78
+
79
+
80
+ class WaylandServerError(WaylandException):
81
+ """A logical error from the Server."""
82
+
83
+
84
+ class WaylandSocketError(WaylandException, OSError):
85
+ """Errors related to Socket IO."""
86
+
87
+
88
+ class WaylandProtocolError(WaylandException, FileNotFoundError):
89
+ """A Protocol related error."""
90
+
91
+
92
+ ##################################
93
+ # Wayland data types
94
+ ##################################
95
+
96
+
97
+ class WaylandType(_abc.ABC):
98
+ struct: _Struct
99
+ length: int
100
+ value: int | float | str | bytes
101
+
102
+ @_abc.abstractmethod
103
+ def to_bytes(self) -> bytes:
104
+ ...
105
+
106
+ @classmethod
107
+ @_abc.abstractmethod
108
+ def from_bytes(cls, buffer: bytes) -> WaylandType:
109
+ ...
110
+
111
+ def __repr__(self):
112
+ return f"{self.__class__.__name__}(length={self.length}, value={self.value})"
113
+
114
+
115
+ class Int(WaylandType):
116
+ struct = _Struct('i')
117
+ length = struct.size
118
+
119
+ def __init__(self, value: int):
120
+ self.value = value
121
+
122
+ def to_bytes(self) -> bytes:
123
+ return self.struct.pack(self.value)
124
+
125
+ @classmethod
126
+ def from_bytes(cls, buffer: bytes) -> Int:
127
+ return cls(cls.struct.unpack(buffer[:cls.length])[0])
128
+
129
+
130
+ class UInt(WaylandType):
131
+ struct = _Struct('I')
132
+ length = struct.size
133
+
134
+ def __init__(self, value: int):
135
+ self.value = value
136
+
137
+ def to_bytes(self) -> bytes:
138
+ return self.struct.pack(self.value)
139
+
140
+ @classmethod
141
+ def from_bytes(cls, buffer: bytes) -> UInt:
142
+ return cls(cls.struct.unpack(buffer[:cls.length])[0])
143
+
144
+
145
+ class Fixed(WaylandType):
146
+ struct = _Struct('I')
147
+ length = struct.size
148
+
149
+ def __init__(self, value: int):
150
+ self.value = value
151
+
152
+ def to_bytes(self) -> bytes:
153
+ return self.struct.pack((int(self.value) << 8) + int((self.value % 1.0) * 256))
154
+
155
+ @classmethod
156
+ def from_bytes(cls, buffer: bytes) -> Fixed:
157
+ unpacked = cls.struct.unpack(buffer[:cls.length])[0]
158
+ return cls((unpacked >> 8) + (unpacked & 0xFF) / 256.0)
159
+
160
+
161
+ class String(WaylandType):
162
+ struct = _Struct('I')
163
+
164
+ def __init__(self, text: str):
165
+ # length uint + text length + 4byte rounding
166
+ self.length = 4 + len(text) + (-len(text) % 4)
167
+ self.value = text
168
+
169
+ def to_bytes(self) -> bytes:
170
+ string_length = len(self.value) + 1
171
+ padding = self.length - self.struct.size
172
+ encoded = self.value.encode()
173
+ return self.struct.pack(string_length) + encoded.ljust(padding, b'\x00')
174
+
175
+ @classmethod
176
+ def from_bytes(cls, buffer: bytes) -> String:
177
+ length = cls.struct.unpack(buffer[:4])[0] # 32-bit integer ('I')
178
+ text = buffer[4:4+length-1].decode() # strip padding byte
179
+ return cls(text)
180
+
181
+
182
+ class Array(WaylandType):
183
+ struct = _Struct('I')
184
+
185
+ def __init__(self, array: bytes):
186
+ # length uint + text length + 4byte padding
187
+ self.length = 4 + len(array) + (-len(array) % 4)
188
+ self.value = array
189
+
190
+ def to_bytes(self) -> bytes:
191
+ length = len(self.value)
192
+ padding_size = 4 - (length % 4)
193
+ return self.struct.pack(length) + b'\x00' * padding_size
194
+
195
+ @classmethod
196
+ def from_bytes(cls, buffer: bytes) -> Array:
197
+ length = cls.struct.unpack(buffer[:4])[0] # 32-bit integer
198
+ array = buffer[4:4+length]
199
+ return cls(array)
200
+
201
+
202
+ class Header(WaylandType):
203
+ struct = _Struct('IHH')
204
+ length = struct.size
205
+
206
+ def __init__(self, oid, opcode, size):
207
+ self.oid = oid
208
+ self.opcode = opcode
209
+ self.size = size
210
+ self.value = self.struct.pack(oid, opcode, size)
211
+
212
+ def to_bytes(self) -> bytes:
213
+ return self.value
214
+
215
+ @classmethod
216
+ def from_bytes(cls, buffer) -> Header:
217
+ return cls(*cls.struct.unpack(buffer))
218
+
219
+ def __repr__(self):
220
+ return f"{self.__class__.__name__}(oid={self.oid}, opcode={self.opcode}, size={self.size})"
221
+
222
+
223
+ class Object(UInt):
224
+ def __init__(self, value: int):
225
+ # Optional 'allow-null' (None) as 0:
226
+ super().__init__(value or 0)
227
+
228
+
229
+ class NewID(UInt):
230
+ def to_bytes(self) -> bytes:
231
+ # Special case for wl_registry.bind()
232
+ if isinstance(self.value, bytes):
233
+ return self.value
234
+ return super().to_bytes()
235
+
236
+
237
+ class FD(Int):
238
+ pass
239
+
240
+
241
+ class _ObjectSpace:
242
+ pass
243
+
244
+
245
+ ##################################
246
+ # Wayland abstractions
247
+ ##################################
248
+
249
+
250
+ class Argument:
251
+ _type_map = {
252
+ 'int': Int,
253
+ 'uint': UInt,
254
+ 'fixed': Fixed,
255
+ 'string': String,
256
+ 'object': Object,
257
+ 'new_id': NewID,
258
+ 'array': Array,
259
+ 'fd': FD,
260
+ }
261
+
262
+ _ctype_map = {
263
+ 'int': c_int32,
264
+ 'uint': c_uint32,
265
+ 'fixed': c_int32,
266
+ 'string': c_char_p,
267
+ 'object': POINTER(wl_proxy),
268
+ 'new_id': POINTER(wl_proxy),
269
+ 'array': POINTER(wl_array),
270
+ 'fd': c_int,
271
+ }
272
+
273
+ def __init__(self, request, element):
274
+ self._request = request
275
+ self._element = element
276
+ self.name = element.get('name')
277
+ self.type_name = element.get('type')
278
+ self.wl_type = self._type_map[self.type_name]
279
+ self.c_type = self._ctype_map[self.type_name]
280
+
281
+ self.summary = element.get('summary')
282
+ self.allow_null = True if element.get('allow-null') else False
283
+
284
+ self.interface = element.get('interface')
285
+ self.returns_new_object = self.wl_type is NewID and self.interface
286
+
287
+ def __call__(self, value) -> bytes:
288
+ return self.wl_type(value).to_bytes()
289
+
290
+ def from_bytes(self, buffer: bytes) -> WaylandType:
291
+ return self.wl_type.from_bytes(buffer)
292
+
293
+ def __repr__(self) -> str:
294
+ return f"{self.name}: {self.wl_type.__name__}"
295
+
296
+
297
+ class Entry:
298
+ def __init__(self, element):
299
+ self.name = element.get('name')
300
+ self.value = int(element.get('value'), 0)
301
+ self.summary = element.get('summary')
302
+
303
+ def __and__(self, other) -> bool:
304
+ return self.value & other > 0
305
+
306
+ def __repr__(self):
307
+ return f"{self.__class__.__name__}(name={self.name}, value={self.value})"
308
+
309
+
310
+ class Enum:
311
+ def __init__(self, interface, element):
312
+ self._interface = weakref.proxy(interface)
313
+ self._element = element
314
+
315
+ self.name = element.get('name')
316
+ self.description = getattr(element.find('description'), 'text', "")
317
+ self.summary = element.find('description').get('summary') if self.description else ""
318
+ self.bitfield = True if element.get('bitfield') else False
319
+ self.entries = [Entry(element) for element in self._element.findall('entry')]
320
+ self.entries.sort(key=lambda e: e.value)
321
+
322
+ def __getitem__(self, index):
323
+ return self.entries[index]
324
+
325
+ def __repr__(self):
326
+ return f"{self.__class__.__name__}('{self.name}', entries={len(self.entries)})"
327
+
328
+
329
+ class Event:
330
+ def __init__(self, interface, element, opcode):
331
+ self._interface = weakref.proxy(interface)
332
+ self._element = element
333
+ self.opcode = opcode
334
+
335
+ self.name = element.get('name')
336
+ self.description = getattr(element.find('description'), 'text', "")
337
+ self.summary = element.find('description').get('summary') if self.description else ""
338
+
339
+ self.arguments = [Argument(self, element) for element in element.findall('arg')]
340
+ self.arg_ctypes = [arg.c_type for arg in self.arguments]
341
+ self.arg_count = len(self.arguments)
342
+
343
+ def __call__(self, payload: bytes, fds: bytes) -> None:
344
+ if USE_LIB_WAYLAND:
345
+ raise Exception("Not to be called.")
346
+ decoded_values = []
347
+
348
+ for arg in self.arguments:
349
+ if arg.wl_type == FD:
350
+ wl_type = arg.wl_type.from_bytes(fds)
351
+ decoded_values.append(wl_type.value)
352
+ fds = fds[wl_type.length:]
353
+ else:
354
+ wl_type = arg.wl_type.from_bytes(payload)
355
+ decoded_values.append(wl_type.value)
356
+ # trim, and continue loop:
357
+ payload = payload[wl_type.length:]
358
+
359
+ # signature = tuple(f"{arg.name}={value}" for arg, value in zip(self.arguments, decoded_values))
360
+ # print(f"Event({self.name}), arguments={signature}")
361
+ self._interface.dispatch_event(self.name, *decoded_values)
362
+
363
+ def __repr__(self):
364
+ args = ', '.join(f'{a.name}={a.type_name}' for a in self.arguments)
365
+ return f"{self.__class__.__name__}(name={self.name}, opcode={self.opcode}, args=({args}))"
366
+
367
+
368
+ class Request:
369
+ def __init__(self, interface, element, opcode):
370
+ self._protocol = interface.protocol
371
+ self._client = interface.protocol.client
372
+ self.parent_oid = interface.oid
373
+ self.opcode = opcode
374
+ self.version = interface.version
375
+ self._interface = weakref.proxy(interface)
376
+
377
+ self.name = element.get('name')
378
+ self.description = getattr(element.find('description'), 'text', "")
379
+ self.summary = element.find('description').get('summary') if self.description else ""
380
+
381
+ self.arguments = [Argument(self, arg) for arg in element.findall('arg')]
382
+ self.is_constructor = any(a.get('type') == 'new_id' for a in element.findall('arg'))
383
+ self.new_interface = next(
384
+ (a.get('interface') for a in element.findall('arg') if a.get('type') == 'new_id'),
385
+ None,
386
+ )
387
+ # TODO: attempt to update a custom signature/annotations for the __call__ method.
388
+
389
+ def create_interface_proxy(self, name: str, *args: Any) -> None:
390
+ interface_struct = self._protocol._interface_structures[name]
391
+ _formated_args = []
392
+ for arg in args:
393
+ if isinstance(arg, int):
394
+ _formated_args.append(c_uint32(arg))
395
+ elif isinstance(arg, str):
396
+ _formated_args.append(c_char_p(arg.encode()))
397
+ elif hasattr(arg, "_proxy"):
398
+ # It has an interface with a proxy, like wl_surface.
399
+ _formated_args.append(cast(arg._proxy, c_void_p))
400
+ else:
401
+ msg = f"Unsupported arg type for {arg!r}"
402
+ raise TypeError(msg)
403
+
404
+ return wl_proxy_marshal_constructor_versioned(
405
+ self._interface._proxy,
406
+ c_uint32(self.opcode),
407
+ byref(interface_struct),
408
+ c_uint32(self.version),
409
+ *_formated_args,
410
+ )
411
+
412
+ def _send(self, bytestring, fds) -> None:
413
+ """Attach a Header to the payload, and send."""
414
+ size = Header.length + len(bytestring)
415
+ header = Header(self.parent_oid, self.opcode, size)
416
+ # final request and file descriptor payloads:
417
+ request = header.to_bytes() + bytestring
418
+ self._client.socket_sendmsg(request, fds)
419
+
420
+ def __call__(self, *args: Any) -> None | Interface:
421
+ assert len(args) == len(self.arguments), f"expected {len(self.arguments)} arguments: {self.arguments}"
422
+
423
+ if USE_LIB_WAYLAND:
424
+ assert _debug_wayland(f"> request called: {self.name} : args={args}. all={self.arguments}")
425
+
426
+ for argument, value in zip(self.arguments, args):
427
+ if argument.returns_new_object:
428
+ assert _debug_wayland(
429
+ f"> requires new interface: {self.new_interface} value={value} : args={args}. {self.arguments}",
430
+ )
431
+ proxy = self.create_interface_proxy(self.new_interface, *args)
432
+ interface = self._protocol.create_interface(argument.interface, value, proxy=proxy)
433
+ assert _debug_wayland(f"> created interface: {interface}")
434
+ return interface
435
+
436
+ assert _debug_wayland(f"> calling: {self.name} : args={args}")
437
+ wl_proxy_marshal(self._interface._proxy, self.opcode, *args)
438
+ return None
439
+
440
+ # Pure socket call.
441
+ interface = None
442
+ bytestring = b''
443
+ fds = b''
444
+
445
+ for argument, value in zip(self.arguments, args):
446
+ if argument.returns_new_object:
447
+ interface = self._protocol.create_interface(argument.interface, value)
448
+ if argument.wl_type is FD:
449
+ fds += argument(value)
450
+ continue
451
+ bytestring += argument(value)
452
+
453
+ self._send(bytestring, fds)
454
+ return interface
455
+
456
+ def __repr__(self) -> str:
457
+ return f"{self.name}(opcode={self.opcode}, args=({', '.join(f'{a}' for a in self.arguments)}))"
458
+
459
+
460
+ class Interface:
461
+ """Interface base class"""
462
+
463
+ _element: Element
464
+ protocol: Protocol
465
+ opcode: int
466
+
467
+ _handlers: dict
468
+
469
+ def __init__(self, oid: int, proxy: wl_proxy | None = None) -> None:
470
+ self.oid = oid
471
+ self.name = self._element.get('name')
472
+ self.version = int(self._element.get('version'), 0)
473
+
474
+ self.description = getattr(self._element.find('description'), 'text', "")
475
+ self.summary = self._element.find('description').get('summary') if self.description else ""
476
+
477
+ self.enums = {element.get('name'): Enum(self, element) for element in self._element.findall('enum')}
478
+ self.events = [Event(self, element, opc) for opc, element in enumerate(self._element.findall('event'))]
479
+ self.event_types = [event.name for event in self.events]
480
+
481
+ self.requests = [Request(self, elem, opcode) for opcode, elem in enumerate(self._element.findall('request'))]
482
+ for request in self.requests:
483
+ setattr(self, request.name, request)
484
+
485
+ self._handlers = dict()
486
+ for name in self.event_types:
487
+ self._handlers[name] = []
488
+
489
+ self._proxy = proxy
490
+
491
+ # Keeps listener callbacks in memory when created, to prevent GC.
492
+ self._listener_keepalive = None
493
+
494
+ def _install_listeners(self) -> None:
495
+ """Build and register the handlers as listeners."""
496
+ if not USE_LIB_WAYLAND:
497
+ return
498
+
499
+ if not self._proxy:
500
+ raise RuntimeError("No wl_proxy to attach listener to.")
501
+
502
+ c_funcs = []
503
+ wrappers = []
504
+
505
+ for ev in self.events:
506
+ if ev.name not in self._handlers:
507
+ assert _debug_wayland(f"> Handler for {ev.name} not found. Using dummy function.")
508
+
509
+ def _make_wrapper(event_name):
510
+ def _callback(data, proxy, *args): ...
511
+
512
+ return _callback
513
+ else:
514
+ # Wrap this since we don't use the user_data or wl_registry argument.
515
+ def _make_wrapper(event_name):
516
+ handler = self._handlers[event_name]
517
+
518
+ def _callback(data, proxy, *args):
519
+ handler(*args)
520
+
521
+ return _callback
522
+
523
+ # handler_func = self._handlers[ev.name]
524
+ cb_type = CFUNCTYPE(None, c_void_p, POINTER(wl_registry), *ev.arg_ctypes)
525
+ py_fn = _make_wrapper(ev.name)
526
+ c_fn = cb_type(py_fn)
527
+ # c_fn = cb_type(handler_func)
528
+ assert _debug_wayland(
529
+ f"> {self}: creating cb for: {ev.name} : {self._handlers.get(ev.name)} | ctype: {cb_type}",
530
+ )
531
+ wrappers.append(c_fn)
532
+ c_funcs.append(cast(c_fn, c_void_p).value)
533
+
534
+ # Pack into void* array in event order
535
+ arr = (c_void_p * len(c_funcs))()
536
+ for i, fn_ptr in enumerate(c_funcs):
537
+ arr[i] = fn_ptr
538
+
539
+ rc = wl_proxy_add_listener(self._proxy, arr, None)
540
+ if rc != 0:
541
+ msg = f"wl_proxy_add_listener failed for {self.name}, rc={rc}"
542
+ raise RuntimeError(msg)
543
+
544
+ self._listener_keepalive = (arr, wrappers)
545
+
546
+ def dispatch_event(self, name: str, *args: Any) -> None:
547
+ handler = self._handlers[name]
548
+ handler(*args)
549
+
550
+ def set_handler(self, name: str, handler: Callable) -> None:
551
+ assert self._listener_keepalive is None, "You have already set a handler. For multiple types, use set_handlers."
552
+ self._handlers[name] = handler
553
+ self._install_listeners()
554
+
555
+ def set_handlers(self, **handler_funcs: Callable) -> None:
556
+ assert self._listener_keepalive is None, "You have already set handlers."
557
+ self._handlers = handler_funcs
558
+ self._install_listeners()
559
+
560
+ def set_handlers_dict(self, handlers_dict: dict[str, Callable]) -> None:
561
+ """Same as set_handlers but takes a dict.
562
+
563
+ Used to prevent clashing of namespaces with kwargs, such as "global" for wl_registry.
564
+ """
565
+ self._handlers = handlers_dict
566
+ self._install_listeners()
567
+
568
+ def remove_handler(self, name: str) -> None:
569
+ if name in self._handlers:
570
+ del self._handlers[name]
571
+
572
+ def delete(self) -> None:
573
+ if self._proxy:
574
+ wl_proxy_destroy(self._proxy)
575
+ self._proxy = None
576
+ if self._listener_keepalive:
577
+ self._listener_keepalive = None
578
+
579
+ def __del__(self) -> None:
580
+ self.delete()
581
+
582
+ def __repr__(self) -> str:
583
+ return f"{self.__class__.__name__}(oid={self.oid}, opcode={self.opcode})"
584
+
585
+
586
+ def _generate_wl_interface(interface_element: Element, all_interfaces: dict) -> wl_interface:
587
+ """Convert <interface> XML element into ctypes structs."""
588
+
589
+ name = interface_element.attrib["name"]
590
+ version = int(interface_element.attrib.get("version", 1))
591
+
592
+ # Converts requests and events elements to ctypes pointers
593
+ def build_messages(elements: list[Element]) -> Array[wl_message]:
594
+ messages = (wl_message * len(elements))()
595
+ for i, elem in enumerate(elements):
596
+ msg_name = elem.attrib["name"].encode("utf-8")
597
+ since = elem.attrib.get("since")
598
+ sig = str(since).encode("utf-8") if since else b""
599
+ arg_elems = elem.findall("arg")
600
+ for arg in arg_elems:
601
+ wl_type = arg.attrib["type"]
602
+ nullable = arg.attrib.get("allow-null") == "true"
603
+
604
+ if wl_type == "new_id":
605
+ iface_name = arg.attrib.get("interface")
606
+ if iface_name:
607
+ # If it has an interface name, it behaves like normal object
608
+ sig += (b"?" if nullable else b"") + b"n"
609
+ else:
610
+ # No interface name for new_id? Dynamic new_id expands to su before n
611
+ # For example wl_registry's XML shows only 2 args, but is 4 in official.
612
+ sig += (b"?" if nullable else b"") + b"sun"
613
+ continue
614
+
615
+ sig += (b"?" if nullable else b"") + {
616
+ "int": b"i",
617
+ "uint": b"u",
618
+ "fixed": b"f",
619
+ "string": b"s",
620
+ "object": b"o",
621
+ "new_id": b"n",
622
+ "array": b"a",
623
+ "fd": b"h",
624
+ }[wl_type]
625
+
626
+ n_args = len(arg_elems)
627
+ types_array = (POINTER(wl_interface) * n_args)()
628
+
629
+ # fill any object/new_id args with pointers if known
630
+ for j, arg in enumerate(arg_elems):
631
+ wl_type = arg.attrib["type"]
632
+ if wl_type in ("object", "new_id"):
633
+ iface_name = arg.attrib.get("interface")
634
+ if iface_name and iface_name in all_interfaces:
635
+ types_array[j] = cast(pointer(all_interfaces[iface_name]), POINTER(wl_interface))
636
+ else:
637
+ types_array[j] = POINTER(wl_interface)()
638
+ else:
639
+ types_array[j] = POINTER(wl_interface)()
640
+
641
+ # always non-NULL even if len==0
642
+ types_ptr = cast(types_array, c_void_p) if n_args else c_void_p(0)
643
+ msg = wl_message(msg_name, sig, types_ptr)
644
+ msg._types_array = types_array # pin lifetime
645
+ messages[i] = msg
646
+
647
+ return messages
648
+
649
+ # Build requests and events
650
+ requests = interface_element.findall("request")
651
+ events = interface_element.findall("event")
652
+
653
+ req_array = build_messages(requests)
654
+ evt_array = build_messages(events)
655
+
656
+ iface = wl_interface(
657
+ name=name.encode(),
658
+ version=version,
659
+ method_count=len(requests),
660
+ methods=req_array,
661
+ event_count=len(events),
662
+ events=evt_array,
663
+ )
664
+
665
+ # Cache into all_interfaces for lookups
666
+ all_interfaces[name] = iface
667
+ return iface
668
+
669
+
670
+ class ClientDisplay(Interface):
671
+ def __init__(self, oid: int, proxy=None) -> None:
672
+ super().__init__(oid, proxy)
673
+ assert proxy is None, "Should not be created with proxy."
674
+ self._display = None
675
+ self._proxy = None
676
+
677
+ def connect(self, path: str | Path | None = None, fd: int | None = None) -> None:
678
+ if fd is not None:
679
+ self._display = wl_display_connect_to_fd(fd)
680
+ if not self._display:
681
+ raise WaylandException("Could not connect to Wayland Display.")
682
+ else:
683
+ if isinstance(path, str):
684
+ self._display = wl_display_connect(path.encode())
685
+ else:
686
+ # Should connect to default.
687
+ self._display = wl_display_connect(None)
688
+ if not self._display:
689
+ raise WaylandException("Could not connect to Wayland Display.")
690
+ self._proxy = as_proxy(self._display)
691
+
692
+ @property
693
+ def display(self) -> _Pointer[wl_display]:
694
+ return self._display
695
+
696
+ def disconnect(self) -> None:
697
+ if self._display:
698
+ wl_display_disconnect(self._display)
699
+ self._display = None
700
+ wl_proxy_destroy(self._proxy)
701
+ self._proxy = None
702
+
703
+ def _install_listeners(self) -> None:
704
+ """The wl_display object is special and does not support listeners added through wl_proxy_add_listener()."""
705
+ if USE_LIB_WAYLAND:
706
+ raise WaylandException("Wayland Client Display does not support listeners. Use WAYLAND_DEBUG environment variable.")
707
+
708
+
709
+ _special_classes = {
710
+ "wl_display": ClientDisplay, # Creation differs than normal interfaces.
711
+ }
712
+
713
+
714
+ class Protocol:
715
+ def __init__(self, client: Client, filename: str) -> None:
716
+ """A Wayland Protocol
717
+
718
+ Given a Wayland Protocol .xml file, this class will dynamically
719
+ introspect and define custom classes for all Interfaces defined
720
+ within. This class should not be instantiated directly. It will
721
+ automatically be created as part of a :py:class:`~wayland.Client`
722
+ instance.
723
+
724
+ Args:
725
+ client: The parent Client to which this Protocol belongs.
726
+ filename: The .xml file that contains the Protocol definition.
727
+ """
728
+ try:
729
+ self._root = _ElementTree.parse(filename).getroot()
730
+ except (FileNotFoundError, ParseError) as e:
731
+ raise WaylandProtocolError(e)
732
+
733
+ self.client = client
734
+ self.name = self._root.get('name')
735
+ self.copyright = getattr(self._root.find('copyright'), 'text', "")
736
+ assert _debug_wayland(f"> {self}: initializing...")
737
+
738
+ self._interface_classes = {}
739
+ self._interface_structures = {}
740
+
741
+ # Iterate over all defined interfaces and dynamically create custom Interface
742
+ # classes using the _InterfaceBase class. Opcodes are determined by enumeration order.
743
+ for i, element in enumerate(self._root.findall('interface')):
744
+ name = element.get('name')
745
+ interface_base_class = _special_classes.get(name, Interface)
746
+ interface_class = type(name, (interface_base_class,), {'protocol': self, '_element': element, 'opcode': i})
747
+ self._interface_classes[name] = interface_class
748
+ assert _debug_wayland(f" * found interface: '{name}'")
749
+ if USE_LIB_WAYLAND:
750
+ iface_struct = _generate_wl_interface(element, self._interface_structures)
751
+ assert _debug_wayland(f" * generated: {name} -> {iface_struct}")
752
+
753
+ def bind_interface(self, name: str, index: int = 0) -> Interface:
754
+ """Create an Interface instance & bind it to a global object.
755
+
756
+ In case there are multiple global objects with the same interface
757
+ name, an ``index`` can be provided.
758
+
759
+ Args:
760
+ name: The interface name to bind.
761
+ index: The index of the global to bind, if more than one.
762
+ """
763
+ # Find a global match, and create a local instance for it.
764
+ interface_global = self.client.globals[name][index]
765
+ iface_name = interface_global.interface.decode()
766
+ # Inform the server of the new relationship:
767
+ # Request...
768
+ if USE_LIB_WAYLAND:
769
+ proxy = interface_global.bind_proxy(self._interface_structures[iface_name])
770
+ else:
771
+ proxy = None
772
+
773
+ interface_instance = self.create_interface(iface_name, proxy=proxy)
774
+
775
+ # Will cause a leak if uncommented. Just keep your instance alive.
776
+ # self.client.bound_globals[interface_global.name] = interface_instance
777
+
778
+ if not USE_LIB_WAYLAND:
779
+ _string = String(name).to_bytes()
780
+ _version = UInt(interface_global.version).to_bytes()
781
+ _new_id = NewID(interface_instance.oid).to_bytes()
782
+ combined_new_id = _string + _version + _new_id
783
+ self.client.wl_registry.bind(interface_global.name, combined_new_id)
784
+
785
+ assert _debug_wayland(f"> {self}.bind_interface: global {name}")
786
+ return interface_instance
787
+
788
+ def create_interface(self, name: str, oid: int | None = None, proxy = None) -> Interface:
789
+ """Create an Interface instance by name.
790
+
791
+ Args:
792
+ name: The Interface name.
793
+ oid: If not provided, an oid will be generated by the Client.
794
+ proxy:
795
+ """
796
+ if name not in self._interface_classes:
797
+ raise WaylandProtocolError(f"The '{self.name}' Protocol does not define an interface named '{name}'")
798
+
799
+ oid = oid or next(self.client.oid_pool)
800
+ interface = self._interface_classes[name](oid=oid, proxy=proxy)
801
+ assert _debug_wayland(f"> {self}.create_interface: {interface}")
802
+ #self.client._oid_interface_map[oid] = interface
803
+ return interface
804
+
805
+ # def delete_interface(self, oid: int) -> None:
806
+ # """Delete an Interface, by its oid.
807
+ #
808
+ # Args:
809
+ # oid: The object ID (oid) of the interface.
810
+ # """
811
+ # interface = self.client._oid_interface_map.pop(oid)
812
+ # self.client.oid_pool.send(oid) # to reuse later
813
+ # assert _debug_wayland(f"> {self}.delete_interface: {interface}")
814
+
815
+ @property
816
+ def interface_names(self) -> list[str]:
817
+ return list(self._interface_classes)
818
+
819
+ def __repr__(self) -> str:
820
+ return f"{self.__class__.__name__}('{self.name}')"
821
+
822
+
823
+ @dataclass
824
+ class GlobalObject:
825
+ registry: _Pointer[wl_registry]
826
+ name: int
827
+ interface: bytes
828
+ version: int
829
+
830
+ def bind_proxy(self, wl_interface_struct: wl_interface) -> _Pointer[wl_proxy]:
831
+ return wl_registry_bind(self.registry, self.name, byref(wl_interface_struct), self.interface, self.version)
832
+
833
+
834
+ ##################################
835
+ # User API
836
+ ##################################
837
+
838
+
839
+ class Client:
840
+ """Wayland Client
841
+
842
+ The Client class establishes a connection to the Wayland domain socket.
843
+ As per the Wayland specification, the `WAYLAND_DISPLAY` environmental
844
+ variable is queried for the endpoint name. If this is an absolute path,
845
+ it is used as-is. If not, the final socket path will be made by joining
846
+ the ``XDG_RUNTIME_DIR`` + ``WAYLAND_DISPLAY`` environmental variables.
847
+
848
+ To create an instance of this class, at least one Wayland Protocol file
849
+ must be provided. Protocol files are XML, and are generally found under
850
+ the ``/usr/share/wayland*`` directories. At a minimum, the base Wayland
851
+ protocol file (``wayland.xml``) is required.
852
+
853
+ When instantiated, the Client automatically connects to the socket and
854
+ creates the main Display (``wl_display``) interface, which is available
855
+ as ``Client.wl_display``.
856
+ """
857
+ _sock: _socket.socket | None
858
+
859
+ def __init__(self, *protocols: str) -> None:
860
+ """Create a Wayland Client connection.
861
+
862
+ Args:
863
+ *protocols: The file path(s) to one or more <protocol>.xml files.
864
+ """
865
+ assert protocols, (
866
+ "At a minimum you must provide at least a wayland.xml "
867
+ "protocol file, commonly '/usr/share/wayland/wayland.xml'."
868
+ )
869
+ self._sock = None
870
+ self._recv_buffer = b""
871
+
872
+ self._endpoint = Path(_os.environ.get('WAYLAND_DISPLAY', default='wayland-0'))
873
+
874
+ if self._endpoint.is_absolute():
875
+ path = self._endpoint
876
+ else:
877
+ _runtime_dir = Path(_os.environ.get('XDG_RUNTIME_DIR', default='/run/user/1000'))
878
+ path = _runtime_dir / self._endpoint
879
+
880
+ assert _debug_wayland(f"endpoint: {path}")
881
+
882
+ if not path.exists():
883
+ msg = f"Wayland endpoint not found: {path}"
884
+ raise WaylandSocketError(msg)
885
+
886
+ # Client side object ID generation:
887
+ self.oid_pool = ObjectIDPool(minimum=1, maximum=0xFEFFFFFF)
888
+
889
+ self._oid_interface_map: dict[int, Interface] = {} # oid: Interface
890
+
891
+ self.globals: dict[str, list[GlobalObject]] = _defaultdict(list) # interface_name: [GlobalObject]
892
+ self.global_interface_map: dict[int, str] = {} # global_name: interface_name
893
+ # self.bound_globals: dict[int, Interface] = {} # global_name: interface_instance
894
+
895
+ self.protocol_dict = {p.name: p for p in [Protocol(self, filename) for filename in protocols]}
896
+ self.protocols = _NameSpace(**self.protocol_dict)
897
+
898
+ assert 'wayland' in self.protocol_dict, "You must provide at minimum a wayland.xml protocol file."
899
+
900
+ # Create global display interface:
901
+ self.wl_display: ClientDisplay = self.protocols.wayland.create_interface(name='wl_display')
902
+
903
+ if USE_LIB_WAYLAND:
904
+ # Can use an opened socket if you want to read from it too, but you CANNOT write to it.
905
+ # self._sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM, 0)
906
+ # self._sock.connect(path)
907
+ # self.wl_display.connect(fd=self._sock.fileno())
908
+
909
+ self.wl_display.connect(self._endpoint)
910
+ self.wl_display_p = cast(self.wl_display._display, c_void_p)
911
+ else:
912
+ self._sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM, 0)
913
+ self._sock.connect(path.as_posix())
914
+ assert _debug_wayland(f"connected to: {self._sock.getpeername()}")
915
+ # !!! Not supported with lib Wayland.
916
+ self.wl_display.set_handlers(error=self._wl_display_error_handler,
917
+ delete_id=self._wl_display_delete_id_handler)
918
+
919
+ # Create global registry:
920
+ self.wl_registry: Interface = self.wl_display.get_registry(next(self.oid_pool))
921
+ self.wl_registry.set_handlers_dict(
922
+ {"global": self._wl_registry_global, "global_remove": self._wl_registry_global_remove},
923
+ )
924
+
925
+ self._sync_done = _threading.Event()
926
+ self._thread_running = _threading.Event()
927
+
928
+ self._receive_loop_method = self._receive_loop if USE_LIB_WAYLAND else self._receive_loop_socket
929
+ self._receive_thread = _threading.Thread(target=self._receive_loop_method, daemon=True)
930
+ self._receive_thread.start()
931
+ self._thread_running.wait()
932
+
933
+ def _receive_loop(self) -> None:
934
+ """A threaded method for continuously reading Server messages."""
935
+ self._thread_running.set()
936
+ dpy = self.wl_display._display
937
+ fd = wl_display_get_fd(dpy)
938
+ while self._thread_running.is_set():
939
+ # Prepare to read. if it returns -1, just dispatch pending
940
+ if wl_display_prepare_read(dpy) != 0:
941
+ wl_display_dispatch_pending(dpy)
942
+ continue
943
+
944
+ # Flush any queued requests
945
+ wl_display_flush(dpy)
946
+
947
+ # Wake immediately if fd is readable, otherwise poll once per 0.005 seconds.
948
+ _, _, _ = _select.select([fd], [], [], 0.005)
949
+
950
+ if wl_display_read_events(dpy) != 0:
951
+ wl_display_cancel_read(dpy) # errors
952
+ wl_display_dispatch_pending(dpy)
953
+
954
+ def sync(self) -> None:
955
+ """Helper shortcut for wl_display.sync calls.
956
+
957
+ This method calls ``wl_display.sync``, and obtains a new ``wl_callback``
958
+ object. It then blocks until the ``wl_callback.done`` event is received
959
+ from the server, ensuring that all prior events are received as well.
960
+ """
961
+ def _wl_display_sync_handler(serial):
962
+ self._sync_done.set()
963
+
964
+ wl_callback = self.wl_display.sync(next(self.oid_pool))
965
+ wl_callback.set_handler('done', _wl_display_sync_handler)
966
+
967
+ if self._sync_done.wait(5.0) is False:
968
+ raise WaylandSocketError("wl_display.sync timed out")
969
+ self._sync_done.clear()
970
+
971
+ def socket_sendmsg(self, request: bytes, fds: bytes) -> None:
972
+ """Send prepared requests and (optional) file descriptors to the server.
973
+
974
+ This method expects the data to be pre-packed into bytestrings. This usually
975
+ means preparing the appropriate Header and payload (WaylandTypes), and using
976
+ their ``to_bytes`` methods to pack them into their raw byte representations.
977
+ If a request is also passing file descriptors, they should be separately packed
978
+ into the ``fds`` bytestring as file descriptors are sent as ancillary data.
979
+
980
+ Args:
981
+ request: a raw bytestring representing a concatenated Header & Request.
982
+ fds: a raw bytestring representing file descriptors.
983
+ """
984
+ self._sock.sendmsg([request], [(_socket.SOL_SOCKET, _socket.SCM_RIGHTS, fds)])
985
+
986
+ def _receive_loop_socket(self) -> None:
987
+ """A threaded method for continuously reading Server messages."""
988
+ self._thread_running.set()
989
+ while self._thread_running.is_set():
990
+ self._receive_socket()
991
+
992
+ def _receive_socket(self) -> None:
993
+ """Receive and process Wayland Events (messages) from the server."""
994
+ _header_len = Header.length
995
+
996
+ try:
997
+ new_data, ancdata, msg_flags, _ = self._sock.recvmsg(4096, _socket.CMSG_SPACE(64))
998
+ except ConnectionError:
999
+ raise WaylandSocketError("Socket is closed")
1000
+
1001
+ if new_data == b"":
1002
+ raise WaylandSocketError("Socket is dead")
1003
+
1004
+ # Include any leftover partial data:
1005
+ data = self._recv_buffer + new_data
1006
+ fds = b"".join([fds for _, _, fds in ancdata])
1007
+
1008
+ # Parse the events in chunks:
1009
+ while len(data) > _header_len:
1010
+ # The first part of the data is the header:
1011
+ header = Header.from_bytes(data[:_header_len])
1012
+
1013
+ # Do we have enough data for the full message?
1014
+ if len(data) < header.size:
1015
+ print("WARNING! Pending FDS!", fds)
1016
+ break
1017
+
1018
+ # - find the matching object (interface) from the header.oid
1019
+ # - find the matching event by its header.opcode
1020
+ # - pass the raw payload into the event, which will decode it
1021
+ # TODO: handle "dead" interfaces:
1022
+ interface = self._oid_interface_map[header.oid]
1023
+ event = interface.events[header.opcode]
1024
+ event(data[_header_len:header.size], fds)
1025
+
1026
+ # trim, and continue loop
1027
+ data = data[header.size:]
1028
+
1029
+ # Keep leftover for next time:
1030
+ self._recv_buffer = data
1031
+
1032
+ def __del__(self) -> None:
1033
+ self._thread_running.clear()
1034
+ if hasattr(self, '_sock') and self._sock:
1035
+ self._sock.close()
1036
+ self._sock = None
1037
+
1038
+ def __repr__(self) -> str:
1039
+ return f"{self.__class__.__name__}(socket='{self._sock.getpeername() if self._sock else self._endpoint}')"
1040
+
1041
+ # Event handlers
1042
+
1043
+ def _wl_display_delete_id_handler(self, oid):
1044
+ self.protocols.wayland.delete_interface(oid)
1045
+
1046
+ def _wl_display_error_handler(self, oid: int, code: int, message: str):
1047
+ try:
1048
+ error_enum = self._oid_interface_map[oid].enums['error']
1049
+ error_entry = error_enum.entries[code]
1050
+ raise WaylandServerError(f"'{error_entry.name}': {error_entry.summary}; {message}.")
1051
+ except (IndexError, KeyError):
1052
+ raise WaylandServerError(f"oid={oid}, code={code}, message={message}")
1053
+
1054
+ def _wl_registry_global(self, global_name_id, interface_name: str | bytes, version):
1055
+ # wayland lib sends as bytes, socket reader sends as string.
1056
+ fmt_interface_name = interface_name.decode()
1057
+ assert _debug_wayland(f"wl_registry global: {global_name_id}, {fmt_interface_name}, {version}")
1058
+ self.globals[fmt_interface_name].append(GlobalObject(self.wl_registry._proxy, global_name_id, interface_name, version))
1059
+ self.global_interface_map[global_name_id] = fmt_interface_name
1060
+
1061
+ def _wl_registry_global_remove(self, global_name):
1062
+ assert _debug_wayland(f"wl_registry global_remove: {global_name}")
1063
+ interface = self.global_interface_map.pop(global_name)
1064
+ self.globals[interface] = [g for g in self.globals[interface] if g.name != global_name]
1065
+
1066
+ if instance := self.bound_globals.pop(global_name, None):
1067
+ # TODO:
1068
+ pass