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.
- pyglet/__init__.py +67 -61
- pyglet/__init__.pyi +15 -8
- pyglet/app/__init__.py +22 -13
- pyglet/app/async_app.py +212 -0
- pyglet/app/base.py +2 -1
- pyglet/app/{xlib.py → linux.py} +3 -3
- pyglet/config/__init__.py +101 -0
- pyglet/config/gl/__init__.py +30 -0
- pyglet/config/gl/egl.py +120 -0
- pyglet/config/gl/macos.py +262 -0
- pyglet/config/gl/windows.py +267 -0
- pyglet/config/gl/x11.py +142 -0
- pyglet/customtypes.py +43 -2
- pyglet/display/__init__.py +8 -6
- pyglet/display/base.py +3 -63
- pyglet/display/cocoa.py +12 -17
- pyglet/display/emscripten.py +39 -0
- pyglet/display/headless.py +23 -30
- pyglet/display/wayland.py +157 -0
- pyglet/display/win32.py +5 -21
- pyglet/display/xlib.py +19 -27
- pyglet/display/xlib_vidmoderestore.py +2 -2
- pyglet/enums.py +183 -0
- pyglet/event.py +0 -1
- pyglet/experimental/geoshader_sprite.py +15 -13
- pyglet/experimental/hidraw.py +6 -15
- pyglet/experimental/multitexture_sprite.py +31 -19
- pyglet/experimental/particles.py +13 -35
- pyglet/font/__init__.py +251 -85
- pyglet/font/base.py +116 -61
- pyglet/font/dwrite/__init__.py +349 -204
- pyglet/font/dwrite/dwrite_lib.py +27 -5
- pyglet/font/fontconfig.py +14 -6
- pyglet/font/freetype.py +138 -87
- pyglet/font/freetype_lib.py +19 -0
- pyglet/font/group.py +179 -0
- pyglet/font/harfbuzz/__init__.py +3 -3
- pyglet/font/pyodide_js.py +310 -0
- pyglet/font/quartz.py +319 -126
- pyglet/font/ttf.py +45 -3
- pyglet/font/user.py +14 -19
- pyglet/font/win32.py +45 -21
- pyglet/graphics/__init__.py +8 -787
- pyglet/graphics/allocation.py +115 -1
- pyglet/graphics/api/__init__.py +77 -0
- pyglet/graphics/api/base.py +299 -0
- pyglet/graphics/api/gl/__init__.py +58 -0
- pyglet/graphics/api/gl/base.py +24 -0
- pyglet/graphics/{vertexbuffer.py → api/gl/buffer.py} +104 -159
- pyglet/graphics/api/gl/cocoa/context.py +76 -0
- pyglet/graphics/api/gl/context.py +391 -0
- pyglet/graphics/api/gl/default_shaders.py +0 -0
- pyglet/graphics/api/gl/draw.py +627 -0
- pyglet/graphics/api/gl/egl/__init__.py +0 -0
- pyglet/graphics/api/gl/egl/context.py +92 -0
- pyglet/graphics/api/gl/enums.py +76 -0
- pyglet/graphics/api/gl/framebuffer.py +315 -0
- pyglet/graphics/api/gl/gl.py +5463 -0
- pyglet/graphics/api/gl/gl_info.py +188 -0
- pyglet/graphics/api/gl/global_opengl.py +226 -0
- pyglet/{gl → graphics/api/gl}/lib.py +34 -18
- pyglet/graphics/api/gl/shader.py +1476 -0
- pyglet/graphics/api/gl/shapes.py +55 -0
- pyglet/graphics/api/gl/sprite.py +102 -0
- pyglet/graphics/api/gl/state.py +219 -0
- pyglet/graphics/api/gl/text.py +190 -0
- pyglet/graphics/api/gl/texture.py +1526 -0
- pyglet/graphics/{vertexarray.py → api/gl/vertexarray.py} +11 -13
- pyglet/graphics/api/gl/vertexdomain.py +751 -0
- pyglet/graphics/api/gl/win32/__init__.py +0 -0
- pyglet/graphics/api/gl/win32/context.py +108 -0
- pyglet/graphics/api/gl/win32/wgl_info.py +24 -0
- pyglet/graphics/api/gl/xlib/__init__.py +0 -0
- pyglet/graphics/api/gl/xlib/context.py +174 -0
- pyglet/{gl → graphics/api/gl/xlib}/glx_info.py +26 -31
- pyglet/graphics/api/gl1/__init__.py +0 -0
- pyglet/{gl → graphics/api/gl1}/gl_compat.py +3 -2
- pyglet/graphics/api/gl2/__init__.py +0 -0
- pyglet/graphics/api/gl2/buffer.py +320 -0
- pyglet/graphics/api/gl2/draw.py +600 -0
- pyglet/graphics/api/gl2/global_opengl.py +122 -0
- pyglet/graphics/api/gl2/shader.py +200 -0
- pyglet/graphics/api/gl2/shapes.py +51 -0
- pyglet/graphics/api/gl2/sprite.py +79 -0
- pyglet/graphics/api/gl2/text.py +175 -0
- pyglet/graphics/api/gl2/vertexdomain.py +364 -0
- pyglet/graphics/api/webgl/__init__.py +233 -0
- pyglet/graphics/api/webgl/buffer.py +302 -0
- pyglet/graphics/api/webgl/context.py +234 -0
- pyglet/graphics/api/webgl/draw.py +590 -0
- pyglet/graphics/api/webgl/enums.py +76 -0
- pyglet/graphics/api/webgl/framebuffer.py +360 -0
- pyglet/graphics/api/webgl/gl.py +1537 -0
- pyglet/graphics/api/webgl/gl_info.py +130 -0
- pyglet/graphics/api/webgl/shader.py +1346 -0
- pyglet/graphics/api/webgl/shapes.py +92 -0
- pyglet/graphics/api/webgl/sprite.py +102 -0
- pyglet/graphics/api/webgl/state.py +227 -0
- pyglet/graphics/api/webgl/text.py +187 -0
- pyglet/graphics/api/webgl/texture.py +1227 -0
- pyglet/graphics/api/webgl/vertexarray.py +54 -0
- pyglet/graphics/api/webgl/vertexdomain.py +616 -0
- pyglet/graphics/api/webgl/webgl_js.pyi +307 -0
- pyglet/{image → graphics}/atlas.py +33 -32
- pyglet/graphics/base.py +10 -0
- pyglet/graphics/buffer.py +245 -0
- pyglet/graphics/draw.py +578 -0
- pyglet/graphics/framebuffer.py +26 -0
- pyglet/graphics/instance.py +178 -69
- pyglet/graphics/shader.py +267 -1553
- pyglet/graphics/state.py +83 -0
- pyglet/graphics/texture.py +703 -0
- pyglet/graphics/vertexdomain.py +695 -538
- pyglet/gui/ninepatch.py +10 -10
- pyglet/gui/widgets.py +120 -10
- pyglet/image/__init__.py +20 -1973
- pyglet/image/animation.py +12 -12
- pyglet/image/base.py +730 -0
- pyglet/image/codecs/__init__.py +9 -0
- pyglet/image/codecs/bmp.py +53 -30
- pyglet/image/codecs/dds.py +53 -31
- pyglet/image/codecs/gdiplus.py +38 -14
- pyglet/image/codecs/gdkpixbuf2.py +0 -2
- pyglet/image/codecs/js_image.py +99 -0
- pyglet/image/codecs/ktx2.py +161 -0
- pyglet/image/codecs/pil.py +1 -1
- pyglet/image/codecs/png.py +1 -1
- pyglet/image/codecs/wic.py +11 -2
- pyglet/info.py +26 -24
- pyglet/input/__init__.py +8 -0
- pyglet/input/base.py +163 -105
- pyglet/input/controller.py +13 -19
- pyglet/input/controller_db.py +39 -24
- pyglet/input/emscripten/__init__.py +18 -0
- pyglet/input/emscripten/gamepad_js.py +397 -0
- pyglet/input/linux/__init__.py +11 -5
- pyglet/input/linux/evdev.py +10 -11
- pyglet/input/linux/x11_xinput.py +2 -2
- pyglet/input/linux/x11_xinput_tablet.py +1 -1
- pyglet/input/macos/__init__.py +7 -2
- pyglet/input/macos/darwin_gc.py +559 -0
- pyglet/input/win32/__init__.py +1 -1
- pyglet/input/win32/directinput.py +34 -29
- pyglet/input/win32/xinput.py +11 -61
- pyglet/lib.py +3 -3
- pyglet/libs/__init__.py +1 -1
- pyglet/{gl → libs/darwin}/agl.py +1 -1
- pyglet/libs/darwin/cocoapy/__init__.py +2 -2
- pyglet/libs/darwin/cocoapy/cocoahelpers.py +181 -0
- pyglet/libs/darwin/cocoapy/cocoalibs.py +31 -0
- pyglet/libs/darwin/cocoapy/cocoatypes.py +27 -0
- pyglet/libs/darwin/cocoapy/runtime.py +81 -45
- pyglet/libs/darwin/coreaudio.py +4 -4
- pyglet/{gl → libs/darwin}/lib_agl.py +9 -8
- pyglet/libs/darwin/quartzkey.py +1 -3
- pyglet/libs/egl/__init__.py +2 -0
- pyglet/libs/egl/egl_lib.py +576 -0
- pyglet/libs/egl/eglext.py +51 -5
- pyglet/libs/linux/__init__.py +0 -0
- pyglet/libs/linux/egl/__init__.py +0 -0
- pyglet/libs/linux/egl/eglext.py +22 -0
- pyglet/libs/linux/glx/__init__.py +0 -0
- pyglet/{gl → libs/linux/glx}/glx.py +13 -14
- pyglet/{gl → libs/linux/glx}/glxext_arb.py +408 -192
- pyglet/{gl → libs/linux/glx}/glxext_mesa.py +1 -1
- pyglet/{gl → libs/linux/glx}/glxext_nv.py +345 -164
- pyglet/{gl → libs/linux/glx}/lib_glx.py +3 -2
- pyglet/libs/linux/wayland/__init__.py +0 -0
- pyglet/libs/linux/wayland/client.py +1068 -0
- pyglet/libs/linux/wayland/lib_wayland.py +207 -0
- pyglet/libs/linux/wayland/wayland_egl.py +38 -0
- pyglet/libs/{wayland → linux/wayland}/xkbcommon.py +26 -0
- pyglet/libs/{x11 → linux/x11}/xf86vmode.py +4 -4
- pyglet/libs/{x11 → linux/x11}/xinerama.py +2 -2
- pyglet/libs/{x11 → linux/x11}/xinput.py +10 -10
- pyglet/libs/linux/x11/xrandr.py +0 -0
- pyglet/libs/{x11 → linux/x11}/xrender.py +1 -1
- pyglet/libs/shared/__init__.py +0 -0
- pyglet/libs/shared/spirv/__init__.py +0 -0
- pyglet/libs/shared/spirv/lib_shaderc.py +85 -0
- pyglet/libs/shared/spirv/lib_spirv_cross.py +126 -0
- pyglet/libs/win32/__init__.py +28 -8
- pyglet/libs/win32/constants.py +59 -48
- pyglet/libs/win32/context_managers.py +20 -3
- pyglet/libs/win32/dinput.py +105 -88
- pyglet/{gl → libs/win32}/lib_wgl.py +52 -26
- pyglet/libs/win32/types.py +58 -23
- pyglet/{gl → libs/win32}/wgl.py +32 -25
- pyglet/{gl → libs/win32}/wglext_arb.py +364 -2
- pyglet/media/__init__.py +9 -10
- pyglet/media/codecs/__init__.py +12 -1
- pyglet/media/codecs/base.py +99 -96
- pyglet/media/codecs/ffmpeg.py +2 -2
- pyglet/media/codecs/ffmpeg_lib/libavformat.py +3 -8
- pyglet/media/codecs/webaudio_pyodide.py +111 -0
- pyglet/media/drivers/__init__.py +9 -4
- pyglet/media/drivers/base.py +4 -4
- pyglet/media/drivers/openal/__init__.py +1 -1
- pyglet/media/drivers/openal/adaptation.py +3 -3
- pyglet/media/drivers/pulse/__init__.py +1 -1
- pyglet/media/drivers/pulse/adaptation.py +3 -3
- pyglet/media/drivers/pyodide_js/__init__.py +8 -0
- pyglet/media/drivers/pyodide_js/adaptation.py +288 -0
- pyglet/media/drivers/xaudio2/adaptation.py +3 -3
- pyglet/media/player.py +276 -193
- pyglet/media/player_worker_thread.py +1 -1
- pyglet/model/__init__.py +39 -29
- pyglet/model/codecs/base.py +4 -4
- pyglet/model/codecs/gltf.py +3 -3
- pyglet/model/codecs/obj.py +71 -43
- pyglet/resource.py +129 -78
- pyglet/shapes.py +154 -194
- pyglet/sprite.py +47 -164
- pyglet/text/__init__.py +44 -54
- pyglet/text/caret.py +12 -7
- pyglet/text/document.py +19 -17
- pyglet/text/formats/html.py +2 -2
- pyglet/text/formats/structured.py +10 -40
- pyglet/text/layout/__init__.py +20 -13
- pyglet/text/layout/base.py +176 -287
- pyglet/text/layout/incremental.py +9 -10
- pyglet/text/layout/scrolling.py +7 -95
- pyglet/window/__init__.py +183 -172
- pyglet/window/cocoa/__init__.py +62 -51
- pyglet/window/cocoa/pyglet_delegate.py +2 -25
- pyglet/window/cocoa/pyglet_view.py +9 -8
- pyglet/window/dialog/__init__.py +184 -0
- pyglet/window/dialog/base.py +99 -0
- pyglet/window/dialog/darwin.py +121 -0
- pyglet/window/dialog/linux.py +72 -0
- pyglet/window/dialog/windows.py +194 -0
- pyglet/window/emscripten/__init__.py +779 -0
- pyglet/window/headless/__init__.py +44 -28
- pyglet/window/key.py +2 -0
- pyglet/window/mouse.py +2 -2
- pyglet/window/wayland/__init__.py +377 -0
- pyglet/window/win32/__init__.py +101 -46
- pyglet/window/xlib/__init__.py +104 -66
- {pyglet-2.1.13.dist-info → pyglet-3.0.dev1.dist-info}/METADATA +2 -3
- pyglet-3.0.dev1.dist-info/RECORD +322 -0
- {pyglet-2.1.13.dist-info → pyglet-3.0.dev1.dist-info}/WHEEL +1 -1
- pyglet/gl/__init__.py +0 -208
- pyglet/gl/base.py +0 -499
- pyglet/gl/cocoa.py +0 -309
- pyglet/gl/gl.py +0 -4625
- pyglet/gl/gl.pyi +0 -2320
- pyglet/gl/gl_compat.pyi +0 -3097
- pyglet/gl/gl_info.py +0 -190
- pyglet/gl/headless.py +0 -166
- pyglet/gl/wgl_info.py +0 -36
- pyglet/gl/wglext_nv.py +0 -1096
- pyglet/gl/win32.py +0 -268
- pyglet/gl/xlib.py +0 -295
- pyglet/image/buffer.py +0 -274
- pyglet/image/codecs/s3tc.py +0 -354
- pyglet/libs/x11/xrandr.py +0 -166
- pyglet-2.1.13.dist-info/RECORD +0 -234
- /pyglet/{libs/wayland → graphics/api/gl/cocoa}/__init__.py +0 -0
- /pyglet/libs/{egl → linux/egl}/egl.py +0 -0
- /pyglet/libs/{egl → linux/egl}/lib.py +0 -0
- /pyglet/libs/{ioctl.py → linux/ioctl.py} +0 -0
- /pyglet/libs/{wayland → linux/wayland}/gbm.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/__init__.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/cursorfont.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/xlib.py +0 -0
- /pyglet/libs/{x11 → linux/x11}/xsync.py +0 -0
- {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
|