aioxlib 0.0.1__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.
aioxlib/__init__.py ADDED
@@ -0,0 +1,1571 @@
1
+ #
2
+ # This file is part of the aioxlib project
3
+ #
4
+ # Copyright (c) 2026 Tiago Coutinho
5
+ # Distributed under the GPLv3 license. See LICENSE for more info.
6
+
7
+ __version__ = "0.0.1"
8
+
9
+ import asyncio
10
+ import enum
11
+ import logging
12
+ import os
13
+ import re
14
+ import socket
15
+ import struct
16
+ import sys
17
+
18
+ log = logging.getLogger("aioxlib")
19
+
20
+ BIG_ENDIAN = 0x42
21
+ LITTLE_ENDIAN = 0x6C
22
+ BYTE_ORDER = LITTLE_ENDIAN if sys.byteorder == "little" else BIG_ENDIAN
23
+
24
+ PROTOCOL_MAJOR = 11
25
+ PROTOCOL_MINOR = 0
26
+
27
+ TYPE = b"MIT-MAGIC-COOKIE-1"
28
+ SUPPORTED_PROTOCOLS = "unix", "tcp"
29
+ DISPLAY_RE = re.compile(r"^((?P<protocol>tcp|unix)/)?(?P<host>[-a-zA-Z0-9._]*):(?P<port>\d+)(\.(?P<screen>\d+))?$")
30
+
31
+ BASE_TCP_PORT = 6000
32
+ UNIX_PATH_TEMPLATE = "/tmp/.X11-unix/X{}"
33
+
34
+ DEFAULT_STRING_ENCODING = "ISO-8859-1"
35
+ UTF8_STRING_ENCODING = "UTF-8"
36
+
37
+ NONE = 0
38
+
39
+ ParentRelative = 1 # background pixmap in CreateWindow
40
+ # and ChangeWindowAttributes
41
+
42
+ CopyFromParent = 0 # border pixmap in CreateWindow
43
+ # and ChangeWindowAttributes
44
+ # special VisualID and special window
45
+ # class passed to CreateWindow
46
+
47
+ PointerWindow = 0 # destination window in SendEvent
48
+ InputFocus = 1 # destination window in SendEvent
49
+ PointerRoot = 1 # focus window in SetInputFocus
50
+ AnyPropertyType = 0 # special Atom, passed to GetProperty
51
+ AnyKey = 0 # special Key Code, passed to GrabKey
52
+ AnyButton = 0 # special Button Code, passed to GrabButton
53
+ AllTemporary = 0 # special Resource ID passed to KillClient
54
+ CurrentTime = 0 # special Time
55
+ NoSymbol = 0 # special KeySym
56
+
57
+
58
+ class EventMask(enum.IntFlag):
59
+ NoEvent = 0
60
+ KeyPress = 1 << 0
61
+ KeyRelease = 1 << 1
62
+ ButtonPress = 1 << 2
63
+ ButtonRelease = 1 << 3
64
+ EnterWindow = 1 << 4
65
+ LeaveWindow = 1 << 5
66
+ PointerMotion = 1 << 6
67
+ PointerMotionHint = 1 << 7
68
+ Button1Motion = 1 << 8
69
+ Button2Motion = 1 << 9
70
+ Button3Motion = 1 << 10
71
+ Button4Motion = 1 << 11
72
+ Button5Motion = 1 << 12
73
+ ButtonMotion = 1 << 13
74
+ KeymapState = 1 << 14
75
+ Exposure = 1 << 15
76
+ VisibilityChange = 1 << 16
77
+ StructureNotify = 1 << 17
78
+ ResizeRedirect = 1 << 18
79
+ SubstructureNotify = 1 << 19
80
+ SubstructureRedirect = 1 << 20
81
+ FocusChange = 1 << 21
82
+ PropertyChange = 1 << 22
83
+ ColormapChange = 1 << 23
84
+ OwnerGrabButton = 1 << 24
85
+
86
+
87
+ class PacketType(enum.IntEnum):
88
+ Error = 0
89
+ Reply = 1
90
+
91
+
92
+ class Event(enum.IntEnum):
93
+ KeyPress = 2
94
+ KeyRelease = 3
95
+ ButtonPress = 4
96
+ ButtonRelease = 5
97
+ MotionNotify = 6
98
+ EnterNotify = 7
99
+ LeaveNotify = 8
100
+ FocusIn = 9
101
+ FocusOut = 10
102
+ KeymapNotify = 11
103
+ Expose = 12
104
+ GraphicsExpose = 13
105
+ NoExpose = 14
106
+ VisibilityNotify = 15
107
+ CreateNotify = 16
108
+ DestroyNotify = 17
109
+ UnmapNotify = 18
110
+ MapNotify = 19
111
+ MapRequest = 20
112
+ ReparentNotify = 21
113
+ ConfigureNotify = 22
114
+ ConfigureRequest = 23
115
+ GravityNotify = 24
116
+ ResizeRequest = 25
117
+ CirculateNotify = 26
118
+ CirculateRequest = 27
119
+ PropertyNotify = 28
120
+ SelectionClear = 29
121
+ SelectionRequest = 30
122
+ SelectionNotify = 31
123
+ ColormapNotify = 32
124
+ ClientMessage = 33
125
+ MappingNotify = 34
126
+
127
+
128
+ class KeyMask(enum.IntFlag):
129
+ Shift = 1 << 0
130
+ Lock = 1 << 1
131
+ Control = 1 << 2
132
+ Mod1 = 1 << 3
133
+ Mod2 = 1 << 4
134
+ Mod3 = 1 << 5
135
+ Mod4 = 1 << 6
136
+ Mod5 = 1 << 7
137
+
138
+
139
+ class Modifier(enum.IntEnum):
140
+ ShiftMapIndex = 0
141
+ LockMapIndex = 1
142
+ ControlMapIndex = 2
143
+ Mod1MapIndex = 3
144
+ Mod2MapIndex = 4
145
+ Mod3MapIndex = 5
146
+ Mod4MapIndex = 6
147
+ Mod5MapIndex = 7
148
+
149
+
150
+ class ButtonMask(enum.IntFlag):
151
+ Button1Mask = 1 << 8
152
+ Button2Mask = 1 << 9
153
+ Button3Mask = 1 << 10
154
+ Button4Mask = 1 << 11
155
+ Button5Mask = 1 << 12
156
+ AnyModifier = 1 << 15 # used in GrabButton, GrabKey
157
+
158
+
159
+ class Button(enum.IntEnum):
160
+ Button1 = 1
161
+ Button2 = 2
162
+ Button3 = 3
163
+ Button4 = 4
164
+ Button5 = 5
165
+
166
+
167
+ class Family(enum.IntEnum):
168
+ Internet = 0
169
+ DECnet = 1
170
+ Chaos = 2
171
+ ServerInterpreted = 5
172
+ InternetV6 = 6
173
+ Local = 256
174
+
175
+
176
+ class Status(enum.IntEnum):
177
+ Failed = 0
178
+ Success = 1
179
+ Authenticate = 2
180
+
181
+
182
+ class Command(enum.IntEnum):
183
+ CreateWindow = 1
184
+ ChangeWindowAttributes = 2
185
+ GetWindowAttributes = 3
186
+ DestroyWindow = 4
187
+ DestroySubwindows = 5
188
+ ChangeSaveSet = 6
189
+ ReparentWindow = 7
190
+ MapWindow = 8
191
+ MapSubwindows = 9
192
+ UnmapWindow = 10
193
+ UnmapSubwindows = 11
194
+ ConfigureWindow = 12
195
+ CirculateWindow = 13
196
+ GetGeometry = 14
197
+ QueryTree = 15
198
+ InternAtom = 16
199
+ GetAtomName = 17
200
+ ChangeProperty = 18
201
+ DeleteProperty = 19
202
+ GetProperty = 20
203
+ ListProperties = 21
204
+ SetSelectionOwner = 22
205
+ GetSelectionOwner = 23
206
+ ConvertSelection = 24
207
+ SendEvent = 25
208
+ GrabPointer = 26
209
+ UngrabPointer = 27
210
+ GrabButton = 28
211
+ UngrabButton = 29
212
+ ChangeActivePointerGrab = 30
213
+ GrabKeyboard = 31
214
+ UngrabKeyboard = 32
215
+ GrabKey = 33
216
+ UngrabKey = 34
217
+ AllowEvents = 35
218
+ GrabServer = 36
219
+ UngrabServer = 37
220
+ QueryPointer = 38
221
+ GetMotionEvents = 39
222
+ TranslateCoordinates = 40
223
+ WarpPointer = 41
224
+ SetInputFocus = 42
225
+ GetInputFocus = 43
226
+ QueryKeymap = 44
227
+ OpenFont = 45
228
+ CloseFont = 46
229
+ QueryFont = 47
230
+ QueryTextExtents = 48
231
+ ListFonts = 49
232
+ ListFontsWithInfo = 50
233
+ SetFontPath = 51
234
+ GetFontPath = 52
235
+ CreatePixmap = 53
236
+ FreePixmap = 54
237
+ CreateGC = 55
238
+ ChangeGC = 56
239
+ CopyGC = 57
240
+ SetDashes = 58
241
+ SetClipRectangles = 59
242
+ FreeGC = 60
243
+ ClearArea = 61
244
+ CopyArea = 62
245
+ CopyPlane = 63
246
+ PolyPoint = 64
247
+ PolyLine = 65
248
+ PolySegment = 66
249
+ PolyRectangle = 67
250
+ PolyArc = 68
251
+ FillPoly = 69
252
+ PolyFillRectangle = 70
253
+ PolyFillArc = 71
254
+ PutImage = 72
255
+ GetImage = 73
256
+ PolyText8 = 74
257
+ PolyText16 = 75
258
+ ImageText8 = 76
259
+ ImageText16 = 77
260
+ QueryExtension = 98
261
+ ListExtensions = 99
262
+
263
+ SetScreenSaver = 107
264
+ GetScreenSaver = 108
265
+ GetModifierMapping = 119
266
+ NoOperation = 127
267
+
268
+
269
+ # -----------------------------------------------------------------------
270
+ # XXX These still need documentation -- for now, read <X11/X.h>
271
+ #
272
+ #
273
+ NotifyNormal = 0
274
+ NotifyGrab = 1
275
+ NotifyUngrab = 2
276
+ NotifyWhileGrabbed = 3
277
+ NotifyHint = 1
278
+ NotifyAncestor = 0
279
+ NotifyVirtual = 1
280
+ NotifyInferior = 2
281
+ NotifyNonlinear = 3
282
+ NotifyNonlinearVirtual = 4
283
+ NotifyPointer = 5
284
+ NotifyPointerRoot = 6
285
+ NotifyDetailNone = 7
286
+ VisibilityUnobscured = 0
287
+ VisibilityPartiallyObscured = 1
288
+ VisibilityFullyObscured = 2
289
+ PlaceOnTop = 0
290
+ PlaceOnBottom = 1
291
+
292
+ PropertyNewValue = 0
293
+ PropertyDelete = 1
294
+ ColormapUninstalled = 0
295
+ ColormapInstalled = 1
296
+ GrabModeSync = 0
297
+ GrabModeAsync = 1
298
+ GrabSuccess = 0
299
+ AlreadyGrabbed = 1
300
+ GrabInvalidTime = 2
301
+ GrabNotViewable = 3
302
+ GrabFrozen = 4
303
+ AsyncPointer = 0
304
+ SyncPointer = 1
305
+ ReplayPointer = 2
306
+ AsyncKeyboard = 3
307
+ SyncKeyboard = 4
308
+ ReplayKeyboard = 5
309
+ AsyncBoth = 6
310
+ SyncBoth = 7
311
+ RevertToNone = 0
312
+ RevertToPointerRoot = PointerRoot
313
+ RevertToParent = 2
314
+ Success = 0
315
+
316
+
317
+ class ErrorCode(enum.IntEnum):
318
+ BadRequest = 1
319
+ BadValue = 2
320
+ BadWindow = 3
321
+ BadPixmap = 4
322
+ BadAtom = 5
323
+ BadCursor = 6
324
+ BadFont = 7
325
+ BadMatch = 8
326
+ BadDrawable = 9
327
+ BadAccess = 10
328
+ BadAlloc = 11
329
+ BadColor = 12
330
+ BadGC = 13
331
+ BadIDChoice = 14
332
+ BadName = 15
333
+ BadLength = 16
334
+ BadImplementation = 17
335
+
336
+
337
+ class PropMode(enum.IntEnum):
338
+ Replace = 0
339
+ Prepend = 1
340
+ Append = 2
341
+
342
+
343
+ class Atom(enum.IntEnum):
344
+ PRIMARY = 1
345
+ SECONDARY = 2
346
+ ARC = 3
347
+ ATOM = 4
348
+ BITMAP = 5
349
+ CARDINAL = 6
350
+ COLORMAP = 7
351
+ CURSOR = 8
352
+ CUT_BUFFER0 = 9
353
+ CUT_BUFFER1 = 10
354
+ CUT_BUFFER2 = 11
355
+ CUT_BUFFER3 = 12
356
+ CUT_BUFFER4 = 13
357
+ CUT_BUFFER5 = 14
358
+ CUT_BUFFER6 = 15
359
+ CUT_BUFFER7 = 16
360
+ DRAWABLE = 17
361
+ FONT = 18
362
+ INTEGER = 19
363
+ PIXMAP = 20
364
+ POINT = 21
365
+ RECTANGLE = 22
366
+ RESOURCE_MANAGER = 23
367
+ RGB_COLOR_MAP = 24
368
+ RGB_BEST_MAP = 25
369
+ RGB_BLUE_MAP = 26
370
+ RGB_DEFAULT_MAP = 27
371
+ RGB_GRAY_MAP = 28
372
+ RGB_GREEN_MAP = 29
373
+ RGB_RED_MAP = 30
374
+ STRING = 31
375
+ VISUALID = 32
376
+ WINDOW = 33
377
+ WM_COMMAND = 34
378
+ WM_HINTS = 35
379
+ WM_CLIENT_MACHINE = 36
380
+ WM_ICON_NAME = 37
381
+ WM_ICON_SIZE = 38
382
+ WM_NAME = 39
383
+ WM_NORMAL_HINTS = 40
384
+ WM_SIZE_HINTS = 41
385
+ WM_ZOOM_HINTS = 42
386
+ MIN_SPACE = 43
387
+ NORM_SPACE = 44
388
+ MAX_SPACE = 45
389
+ END_SPACE = 46
390
+ SUPERSCRIPT_X = 47
391
+ SUPERSCRIPT_Y = 48
392
+ SUBSCRIPT_X = 49
393
+ SUBSCRIPT_Y = 50
394
+ UNDERLINE_POSITION = 51
395
+ UNDERLINE_THICKNESS = 52
396
+ STRIKEOUT_ASCENT = 53
397
+ STRIKEOUT_DESCENT = 54
398
+ ITALIC_ANGLE = 55
399
+ X_HEIGHT = 56
400
+ QUAD_WIDTH = 57
401
+ WEIGHT = 58
402
+ POINT_SIZE = 59
403
+ RESOLUTION = 60
404
+ COPYRIGHT = 61
405
+ NOTICE = 62
406
+ FONT_NAME = 63
407
+ FAMILY_NAME = 64
408
+ FULL_NAME = 65
409
+ CAP_HEIGHT = 66
410
+ WM_CLASS = 67
411
+ WM_TRANSIENT_FOR = 68
412
+
413
+
414
+ class ImageFormat(enum.IntEnum):
415
+ XYBitmap = 0
416
+ XYPixmap = 1
417
+ ZPixmap = 2
418
+
419
+
420
+ FirstExtensionError = 128
421
+ LastExtensionError = 255
422
+ InputOutput = 1
423
+ InputOnly = 2
424
+ CWBackPixmap = 1 << 0
425
+ CWBackPixel = 1 << 1
426
+ CWBorderPixmap = 1 << 2
427
+ CWBorderPixel = 1 << 3
428
+ CWBitGravity = 1 << 4
429
+ CWWinGravity = 1 << 5
430
+ CWBackingStore = 1 << 6
431
+ CWBackingPlanes = 1 << 7
432
+ CWBackingPixel = 1 << 8
433
+ CWOverrideRedirect = 1 << 9
434
+ CWSaveUnder = 1 << 10
435
+ CWEventMask = 1 << 11
436
+ CWDontPropagate = 1 << 12
437
+ CWColormap = 1 << 13
438
+ CWCursor = 1 << 14
439
+ CWX = 1 << 0
440
+ CWY = 1 << 1
441
+ CWWidth = 1 << 2
442
+ CWHeight = 1 << 3
443
+ CWBorderWidth = 1 << 4
444
+ CWSibling = 1 << 5
445
+ CWStackMode = 1 << 6
446
+ ForgetGravity = 0
447
+ NorthWestGravity = 1
448
+ NorthGravity = 2
449
+ NorthEastGravity = 3
450
+ WestGravity = 4
451
+ CenterGravity = 5
452
+ EastGravity = 6
453
+ SouthWestGravity = 7
454
+ SouthGravity = 8
455
+ SouthEastGravity = 9
456
+ StaticGravity = 10
457
+ UnmapGravity = 0
458
+ NotUseful = 0
459
+ WhenMapped = 1
460
+ Always = 2
461
+ IsUnmapped = 0
462
+ IsUnviewable = 1
463
+ IsViewable = 2
464
+ SetModeInsert = 0
465
+ SetModeDelete = 1
466
+ DestroyAll = 0
467
+ RetainPermanent = 1
468
+ RetainTemporary = 2
469
+ Above = 0
470
+ Below = 1
471
+ TopIf = 2
472
+ BottomIf = 3
473
+ Opposite = 4
474
+ RaiseLowest = 0
475
+ LowerHighest = 1
476
+
477
+ GXclear = 0x0
478
+ GXand = 0x1
479
+ GXandReverse = 0x2
480
+ GXcopy = 0x3
481
+ GXandInverted = 0x4
482
+ GXnoop = 0x5
483
+ GXxor = 0x6
484
+ GXor = 0x7
485
+ GXnor = 0x8
486
+ GXequiv = 0x9
487
+ GXinvert = 0xA
488
+ GXorReverse = 0xB
489
+ GXcopyInverted = 0xC
490
+ GXorInverted = 0xD
491
+ GXnand = 0xE
492
+ GXset = 0xF
493
+ LineSolid = 0
494
+ LineOnOffDash = 1
495
+ LineDoubleDash = 2
496
+ CapNotLast = 0
497
+ CapButt = 1
498
+ CapRound = 2
499
+ CapProjecting = 3
500
+ JoinMiter = 0
501
+ JoinRound = 1
502
+ JoinBevel = 2
503
+ FillSolid = 0
504
+ FillTiled = 1
505
+ FillStippled = 2
506
+ FillOpaqueStippled = 3
507
+ EvenOddRule = 0
508
+ WindingRule = 1
509
+ ClipByChildren = 0
510
+ IncludeInferiors = 1
511
+ Unsorted = 0
512
+ YSorted = 1
513
+ YXSorted = 2
514
+ YXBanded = 3
515
+ CoordModeOrigin = 0
516
+ CoordModePrevious = 1
517
+ Complex = 0
518
+ Nonconvex = 1
519
+ Convex = 2
520
+ ArcChord = 0
521
+ ArcPieSlice = 1
522
+ GCFunction = 1 << 0
523
+ GCPlaneMask = 1 << 1
524
+ GCForeground = 1 << 2
525
+ GCBackground = 1 << 3
526
+ GCLineWidth = 1 << 4
527
+ GCLineStyle = 1 << 5
528
+ GCCapStyle = 1 << 6
529
+ GCJoinStyle = 1 << 7
530
+ GCFillStyle = 1 << 8
531
+ GCFillRule = 1 << 9
532
+ GCTile = 1 << 10
533
+ GCStipple = 1 << 11
534
+ GCTileStipXOrigin = 1 << 12
535
+ GCTileStipYOrigin = 1 << 13
536
+ GCFont = 1 << 14
537
+ GCSubwindowMode = 1 << 15
538
+ GCGraphicsExposures = 1 << 16
539
+ GCClipXOrigin = 1 << 17
540
+ GCClipYOrigin = 1 << 18
541
+ GCClipMask = 1 << 19
542
+ GCDashOffset = 1 << 20
543
+ GCDashList = 1 << 21
544
+ GCArcMode = 1 << 22
545
+ GCLastBit = 22
546
+ FontLeftToRight = 0
547
+ FontRightToLeft = 1
548
+ FontChange = 255
549
+
550
+ AllocNone = 0
551
+ AllocAll = 1
552
+ DoRed = 1 << 0
553
+ DoGreen = 1 << 1
554
+ DoBlue = 1 << 2
555
+ CursorShape = 0
556
+ TileShape = 1
557
+ StippleShape = 2
558
+ AutoRepeatModeOff = 0
559
+ AutoRepeatModeOn = 1
560
+ AutoRepeatModeDefault = 2
561
+ LedModeOff = 0
562
+ LedModeOn = 1
563
+ KBKeyClickPercent = 1 << 0
564
+ KBBellPercent = 1 << 1
565
+ KBBellPitch = 1 << 2
566
+ KBBellDuration = 1 << 3
567
+ KBLed = 1 << 4
568
+ KBLedMode = 1 << 5
569
+ KBKey = 1 << 6
570
+ KBAutoRepeatMode = 1 << 7
571
+ MappingSuccess = 0
572
+ MappingBusy = 1
573
+ MappingFailed = 2
574
+ MappingModifier = 0
575
+ MappingKeyboard = 1
576
+ MappingPointer = 2
577
+ DontPreferBlanking = 0
578
+ PreferBlanking = 1
579
+ DefaultBlanking = 2
580
+ DisableScreenSaver = 0
581
+ DisableScreenInterval = 0
582
+ DontAllowExposures = 0
583
+ AllowExposures = 1
584
+ DefaultExposures = 2
585
+ ScreenSaverReset = 0
586
+ ScreenSaverActive = 1
587
+ HostInsert = 0
588
+ HostDelete = 1
589
+ EnableAccess = 1
590
+ DisableAccess = 0
591
+ StaticGray = 0
592
+ GrayScale = 1
593
+ StaticColor = 2
594
+ PseudoColor = 3
595
+ TrueColor = 4
596
+ DirectColor = 5
597
+ LSBFirst = 0
598
+ MSBFirst = 1
599
+
600
+
601
+ GenericEventCode = 35
602
+
603
+
604
+ def pad(n: int) -> int:
605
+ return (4 - n % 4) % 4
606
+
607
+
608
+ def round_bytes(s: bytes) -> tuple[bytes, int, int]:
609
+ n = len(s)
610
+ r = pad(n)
611
+ return s + r * b"\0", n, n + r
612
+
613
+
614
+ def pack_setup(auth_name, auth_data, byte_order=BYTE_ORDER, protocol=(PROTOCOL_MAJOR, PROTOCOL_MINOR)):
615
+ auth_name, n1, n2 = round_bytes(auth_name)
616
+ auth_data, d1, d2 = round_bytes(auth_data)
617
+ fmt = f"=BxHHHH2x{n2}s{d2}s"
618
+ return struct.pack(fmt, byte_order, *protocol, n1, d1, auth_name, auth_data)
619
+
620
+
621
+ async def read_setup(reader):
622
+ data = await reader.readexactly(1)
623
+ status = Status(ord(data))
624
+ if status == Status.Success:
625
+ head = "BxHHH"
626
+ head_len = struct.calcsize(head)
627
+ data += await reader.readexactly(head_len - 1)
628
+ status, proto_major, proto_minor, length = struct.unpack_from(head, data)
629
+ assert proto_major >= 11
630
+ length *= 4
631
+ data = await reader.readexactly(length)
632
+ return unpack_successfull_setup(data)[0]
633
+ elif status == Status.Failed:
634
+ head = "BBHHH"
635
+ head_len = struct.calcsize(head)
636
+ data += await reader.readexactly(head_len - 1)
637
+ status, _, proto_major, proto_minor, length = struct.unpack_from(head, data)
638
+ length *= 4
639
+ reason = (await reader.readexactly(length)).decode()
640
+ raise RuntimeError(reason)
641
+ elif status == Status.Authenticate:
642
+ head = "BxH"
643
+ head_len = struct.calcsize(head)
644
+ data = await reader.readexactly(head_len - 1)
645
+ status, length = struct.unpack_from(head, data)
646
+ n = ord(data[1]) * 4
647
+ reason = (await reader.readexactly(n)).decode()
648
+ raise ValueError(reason)
649
+
650
+
651
+ def unpack_successfull_setup(data, offset=0):
652
+ fmt = "4I2H8B4x"
653
+ fields = (
654
+ "release",
655
+ "resource_id_base",
656
+ "resource_id_mask",
657
+ "motion_buffer_size",
658
+ "vendor_length",
659
+ "max_request_length",
660
+ "root_length",
661
+ "pixmap_length",
662
+ "image_byte_order",
663
+ "bitmap_format_bit_order",
664
+ "bitmap_format_scanline_unit",
665
+ "bitmap_format_scanline_pad",
666
+ "min_keycode",
667
+ "max_keycode",
668
+ )
669
+ setup = dict(zip(fields, struct.unpack_from(fmt, data, offset), strict=True))
670
+ offset += struct.calcsize(fmt)
671
+ vendor_length = setup["vendor_length"]
672
+ setup["vendor"] = struct.unpack_from(f"{vendor_length}s", data, offset)[0].decode()
673
+ offset += vendor_length
674
+ setup["pixmap_formats"] = pixmap_formats = []
675
+ for _ in range(setup["pixmap_length"]):
676
+ pixmap_format, offset = unpack_pixmap_format(data, offset)
677
+ pixmap_formats.append(pixmap_format)
678
+
679
+ setup["screens"] = screens = []
680
+ for _ in range(setup["root_length"]):
681
+ screen, offset = unpack_screen(data, offset)
682
+ screens.append(screen)
683
+ return setup, offset
684
+
685
+
686
+ def unpack_pixmap_format(data, offset) -> tuple[dict, int]:
687
+ fmt = "BBB5x"
688
+ fields = "depth", "bits_per_pixel", "pad"
689
+ pixel_format = dict(zip(fields, struct.unpack_from(fmt, data, offset), strict=True))
690
+ offset += struct.calcsize(fmt)
691
+ return pixel_format, offset
692
+
693
+
694
+ def unpack_screen(data, offset) -> tuple[dict, int]:
695
+ fields = (
696
+ "root",
697
+ "colormap",
698
+ "white_pixel",
699
+ "black_pixel",
700
+ "current_input_mask",
701
+ "width",
702
+ "height",
703
+ "width_in_mms",
704
+ "height_in_mms",
705
+ "min_installed_maps",
706
+ "max_installed_maps",
707
+ "root_visual",
708
+ "backing_store",
709
+ "save_unders",
710
+ "root_depth",
711
+ "depths_length",
712
+ )
713
+ fmt = "5I6HI4B"
714
+ screen = dict(zip(fields, struct.unpack_from(fmt, data, offset), strict=True))
715
+ offset += struct.calcsize(fmt)
716
+ screen["depths"] = depths = []
717
+ for _ in range(screen["depths_length"]):
718
+ depth, offset = unpack_depth(data, offset)
719
+ depths.append(depth)
720
+ return screen, offset
721
+
722
+
723
+ def unpack_depth(data, offset) -> tuple[dict, int]:
724
+ fmt = "BxH4x"
725
+ fields = "depth", "nb_visuals"
726
+ depth = dict(zip(fields, struct.unpack_from(fmt, data, offset), strict=True))
727
+ offset += struct.calcsize(fmt)
728
+ depth["visuals"] = visuals = []
729
+ for _ in range(depth["nb_visuals"]):
730
+ visual, offset = unpack_visual(data, offset)
731
+ visuals.append(visual)
732
+ return depth, offset
733
+
734
+
735
+ def unpack_visual(data, offset) -> tuple[dict, int]:
736
+ fmt = "IBBHIII4x"
737
+ fields = (
738
+ "visual_id",
739
+ "visual_class",
740
+ "bits_per_rgb_value",
741
+ "colormap_entries",
742
+ "red_mask",
743
+ "green_mask",
744
+ "blue_mask",
745
+ )
746
+ visual = dict(zip(fields, struct.unpack_from(fmt, data, offset), strict=True))
747
+ offset += struct.calcsize(fmt)
748
+ return visual, offset
749
+
750
+
751
+ def pack_create_window(*args, **kwargs) -> bytes:
752
+ from Xlib.protocol.request import CreateWindow
753
+
754
+ return CreateWindow._request.to_binary(*args, **kwargs)
755
+
756
+
757
+ def pack_map_window(wid) -> bytes:
758
+ from Xlib.protocol.request import MapWindow
759
+
760
+ return MapWindow._request.to_binary(wid)
761
+
762
+
763
+ def pack_destroy_window(wid) -> bytes:
764
+ from Xlib.protocol.request import DestroyWindow
765
+
766
+ return DestroyWindow._request.to_binary(wid)
767
+
768
+
769
+ def pack_intern_atom(name, only_if_exists=False):
770
+ from Xlib.protocol.request import InternAtom
771
+
772
+ return InternAtom._request.to_binary(name=name, only_if_exists=only_if_exists)
773
+
774
+
775
+ def pack_poly_fill_rectangles(wid, gc, rectangles) -> bytes:
776
+ from Xlib.protocol.request import PolyFillRectangle
777
+
778
+ rectangles = [
779
+ r if isinstance(r, dict) else dict(zip(['x', 'y', 'width', 'height'], r))
780
+ for r in rectangles
781
+ ]
782
+
783
+ return PolyFillRectangle._request.to_binary(wid, gc, rectangles)
784
+
785
+
786
+
787
+ def unpack_reply_head(data: bytes, offset: int = 0) -> tuple[dict, int]:
788
+ fmt = "BBHI"
789
+ reply = struct.unpack_from(fmt, data, offset)
790
+ fields = "reply", "n", "sequence_number", "length"
791
+ return dict(zip(fields, reply, strict=True)), offset + struct.calcsize(fmt)
792
+
793
+
794
+ def unpack_intern_atom(data: bytes, offset: int = 0) -> tuple[dict, int]:
795
+ result, offset = unpack_reply_head(data, offset)
796
+ assert result["reply"] == 1
797
+ fmt = "I20x"
798
+ (atom,) = struct.unpack_from(fmt, data, offset)
799
+ offset += struct.calcsize(fmt)
800
+ result["atom"] = atom
801
+ return result, offset
802
+
803
+
804
+ def unpack_error(data: bytes, offset: int = 0):
805
+ fmt = "BBHIHB21x"
806
+ fields = "error", "code", "sequence_number", "value", "minor_opcode", "major_opcode"
807
+ reply = struct.unpack_from(fmt, data, offset)
808
+ offset += struct.calcsize(fmt)
809
+ result = dict(zip(fields, reply, strict=True))
810
+ result["code"] = ErrorCode(result["code"])
811
+ return result, offset
812
+
813
+
814
+ def unpack_event(event_map, data: bytes, offset: int = 0):
815
+ raw_event_type = data[0]
816
+ # Skip bit 8, that is set if this event came from an SendEvent
817
+ from_send = bool(raw_event_type & 0x80)
818
+ event_code = raw_event_type & 0x7F
819
+ if handler := event_map.get(event_code):
820
+ result, offset = handler(data, offset)
821
+ else:
822
+ fmt = "BBHIIII12x"
823
+ fields = "raw_code", "detail", "sequence_number", "time", "root", "event", "child"
824
+ reply = struct.unpack_from(fmt, data, offset)
825
+ offset += struct.calcsize(fmt)
826
+ result = dict(zip(fields, reply, strict=True))
827
+ result["name"] = Event(event_code).name if event_code in Event else event_code
828
+ result["code"] = event_code
829
+ result["send_event"] = from_send
830
+ return result, offset
831
+
832
+
833
+ def unpack_client_message(data, offset):
834
+ format = data[1]
835
+ fmt = "BBHII"
836
+ fields = "code", "format", "sequence_number", "window", "type"
837
+ reply = struct.unpack_from(fmt, data, offset)
838
+ offset += struct.calcsize(fmt)
839
+ result = dict(zip(fields, reply, strict=True))
840
+ data_fmt = "20B"
841
+ if format == 16:
842
+ data_fmt = "10H"
843
+ elif format == 32:
844
+ data_fmt = "5I"
845
+ result["data"] = struct.unpack_from(data_fmt, data, offset)
846
+ result["name"] = "ClientMessage"
847
+ offset += struct.calcsize(data_fmt)
848
+ return result, offset
849
+
850
+
851
+ def pack_gc(cid, drawable, attrs):
852
+ from Xlib.protocol.request import CreateGC
853
+
854
+ return CreateGC._request.to_binary(cid=cid, drawable=drawable, attrs=attrs)
855
+
856
+
857
+ def pack_change_property(wid, property, property_type, format, data, mode):
858
+ from Xlib.protocol.request import ChangeProperty
859
+
860
+ return ChangeProperty._request.to_binary(
861
+ mode=mode, window=wid, property=property, type=property_type, data=(format, data)
862
+ )
863
+
864
+
865
+ def pack_list_extensions():
866
+ from Xlib.protocol.request import ListExtensions
867
+
868
+ return ListExtensions._request.to_binary()
869
+
870
+
871
+ def unpack_list_extensions(data: bytes, offset: int = 0):
872
+ result, offset = unpack_reply_head(data, offset)
873
+ n = result["n"]
874
+ assert result["reply"] == 1
875
+ offset += 24
876
+ result["extensions"] = extensions = []
877
+ for _ in range(n):
878
+ (size,) = struct.unpack_from("B", data, offset)
879
+ offset += 1
880
+ (extension,) = struct.unpack_from(f"{size}s", data, offset)
881
+ offset += size
882
+ extensions.append(extension.decode())
883
+ return result, offset + 32 + result["length"] * 4
884
+
885
+
886
+ def pack_query_extension(name: bytes) -> bytes:
887
+ from Xlib.protocol.request import QueryExtension
888
+
889
+ return QueryExtension._request.to_binary(name)
890
+
891
+
892
+ def unpack_query_extension(data: bytes, offset: int = 0):
893
+ result, offset = unpack_reply_head(data, offset)
894
+ assert result["reply"] == 1
895
+ fmt = "BBBB20x"
896
+ fields = "present", "opcode", "base_event", "base_error"
897
+ result.update(zip(fields, struct.unpack_from(fmt, data, offset), strict=True))
898
+ offset += struct.calcsize(fmt)
899
+ return result, offset
900
+
901
+
902
+ def pack_image(format, drawable, gc, w, h, x, y, left_pad, depth, data):
903
+ fmt = "BBHIIHHhhBB2x"
904
+ n = len(data)
905
+ p = pad(n)
906
+ length = 6 + (n + p) // 4
907
+ head = struct.pack(fmt, 72, format, length, drawable, gc, w, h, x, y, left_pad, depth)
908
+ return head, data, p * b"\x00"
909
+
910
+
911
+ def get_auth_config(filename: str) -> list[dict]:
912
+ with open(filename, "rb") as f:
913
+ raw = f.read()
914
+
915
+ offset, entries = 0, []
916
+ while offset < len(raw):
917
+ family, addrlen = struct.unpack_from(">HH", raw, offset)
918
+ offset += 4
919
+ addr = raw[offset : offset + addrlen]
920
+ offset += addrlen
921
+ (numlen,) = struct.unpack_from(">H", raw, offset)
922
+ offset += 2
923
+ num = raw[offset : offset + numlen]
924
+ offset += numlen
925
+ (namelen,) = struct.unpack_from(">H", raw, offset)
926
+ offset += 2
927
+ name = raw[offset : offset + namelen]
928
+ offset += namelen
929
+ (datalen,) = struct.unpack_from(">H", raw, offset)
930
+ offset += 2
931
+ data = raw[offset : offset + datalen]
932
+ offset += datalen
933
+ entries.append({"family": family, "address": addr, "port": num, "name": name, "data": data})
934
+ return entries
935
+
936
+
937
+ def get_auth_filename(filename: str | None = None) -> str:
938
+ if filename is None:
939
+ if (filename := os.getenv("XAUTHORITY")) is None:
940
+ filename = f"{os.getenv('HOME')}/.Xauthority"
941
+ return filename
942
+
943
+
944
+ class Screen:
945
+ def __init__(self, display, info):
946
+ self.display = display
947
+ self.info = info
948
+ self.root = Window(display, info["root"])
949
+
950
+ def __getattr__(self, name):
951
+ return self.info[name]
952
+
953
+ async def create_window(
954
+ self,
955
+ x,
956
+ y,
957
+ width,
958
+ height,
959
+ border_width,
960
+ parent=None,
961
+ depth=None,
962
+ window_class=CopyFromParent,
963
+ visual=CopyFromParent,
964
+ **attrs,
965
+ ):
966
+ if parent is None:
967
+ parent = self.root
968
+ if depth is None:
969
+ depth = self.root_depth
970
+ return await parent.create_window(x, y, width, height, border_width, depth, window_class, visual, **attrs)
971
+
972
+
973
+ class Resource:
974
+ def __init__(self, display, rid):
975
+ self.display = display
976
+ self.id = rid
977
+
978
+ def __int__(self):
979
+ return self.id
980
+
981
+
982
+ class Drawable(Resource):
983
+ async def create_gc(self, **kwargs) -> "GC":
984
+ cid = self.display.resources.allocate()
985
+ payload = pack_gc(cid=cid, drawable=self.id, attrs=kwargs)
986
+ self.display.connection.write(payload)
987
+ return GC(self.display, cid, self)
988
+
989
+ async def put_image(self, gc, x, y, w, h, depth, data, format=ImageFormat.ZPixmap, left_pad=0, drawable=None):
990
+ if drawable is None:
991
+ drawable = self
992
+ payloads = pack_image(format, int(drawable), int(gc), w, h, x, y, left_pad, depth, data)
993
+ payload = b"".join(payloads)
994
+ self.display.connection.write(payload)
995
+
996
+ async def draw_image(self, gc, x, y, w, h, depth, data, drawable=None):
997
+ max_size = self.display.info["max_request_length"]
998
+ nb_pixels = w * h
999
+ n = len(data)
1000
+ pixel_size = n // nb_pixels
1001
+ nb_bytes_per_line = w * pixel_size
1002
+ lines_per_packet = (max_size - 64) // nb_bytes_per_line
1003
+ data = memoryview(data)
1004
+ y1 = 0
1005
+ while y1 < h:
1006
+ start = y1 * nb_bytes_per_line
1007
+ end = (y1 + lines_per_packet) * nb_bytes_per_line
1008
+ d = data[start:end]
1009
+ lines = min(lines_per_packet, h - y1)
1010
+ await self.put_image(gc, x, y, w, lines, depth, d, drawable=drawable)
1011
+ y1 += lines_per_packet
1012
+ y += lines_per_packet
1013
+
1014
+ async def poly_fill_rectangles(self, gc, rectangles):
1015
+ payload = pack_poly_fill_rectangles(int(self), int(gc), rectangles)
1016
+ self.display.connection.write(payload)
1017
+
1018
+
1019
+ class Window(Drawable):
1020
+ async def create_window(
1021
+ self,
1022
+ x,
1023
+ y,
1024
+ width,
1025
+ height,
1026
+ border_width,
1027
+ depth,
1028
+ window_class=CopyFromParent,
1029
+ visual=CopyFromParent,
1030
+ **attrs,
1031
+ ):
1032
+ wid = self.display.resources.allocate()
1033
+ payload = pack_create_window(
1034
+ depth=depth,
1035
+ wid=wid,
1036
+ parent=self.id,
1037
+ x=x,
1038
+ y=y,
1039
+ width=width,
1040
+ height=height,
1041
+ border_width=border_width,
1042
+ window_class=window_class,
1043
+ visual=visual,
1044
+ attrs=attrs,
1045
+ )
1046
+ self.display.connection.write(payload)
1047
+ return Window(self.display, wid)
1048
+
1049
+ async def set_name(self, name):
1050
+ await self.change_text_property(Atom.WM_NAME, name)
1051
+
1052
+ async def change_text_property(self, property, data, property_type=Atom.STRING, mode=PropMode.Replace):
1053
+ if not isinstance(data, bytes):
1054
+ if property_type == Atom.STRING:
1055
+ data = data.encode(DEFAULT_STRING_ENCODING)
1056
+ elif property_type == self.display.get_atom("UTF8_STRING"):
1057
+ data = data.encode()
1058
+ await self.change_property(property, property_type, 8, data, mode)
1059
+
1060
+ async def map(self):
1061
+ payload = pack_map_window(self.id)
1062
+ self.display.connection.write(payload)
1063
+
1064
+ async def destroy(self):
1065
+ payload = pack_destroy_window(self.id)
1066
+ self.display.connection.write(payload)
1067
+
1068
+ async def change_property(self, property, property_type, format, data, mode=PropMode.Replace):
1069
+ payload = pack_change_property(self.id, property, property_type, format, data, mode)
1070
+ self.display.connection.write(payload)
1071
+
1072
+ async def set_wm_protocols(self, protocols):
1073
+ id = await self.display.get_atom("WM_PROTOCOLS")
1074
+ await self.change_property(id, Atom.ATOM, 32, protocols)
1075
+
1076
+
1077
+ class GC(Resource):
1078
+ def __init__(self, display, rid, drawable):
1079
+ super().__init__(display, rid)
1080
+ self.drawable = drawable
1081
+
1082
+
1083
+ class LogBytes:
1084
+ __slots__ = ["data"]
1085
+ Limit = 32
1086
+
1087
+ def __init__(self, data):
1088
+ self.data = data
1089
+
1090
+ def __str__(self):
1091
+ if len(self.data) > self.Limit:
1092
+ return " ".join(f"{b:02X}" for b in self.data[: self.Limit]) + " [...]"
1093
+ return " ".join(f"{b:02X}" for b in self.data)
1094
+
1095
+
1096
+ class Connection:
1097
+ def __init__(self, reader, writer, config):
1098
+ self.reader = reader
1099
+ self.writer = writer
1100
+ self.config = config
1101
+
1102
+ def close(self):
1103
+ self.writer.close()
1104
+
1105
+ async def aclose(self):
1106
+ self.writer.close()
1107
+ await self.writer.wait_closed()
1108
+
1109
+ def get_extra_info(self, item):
1110
+ return self.writer.get_extra_info(item)
1111
+
1112
+ @property
1113
+ def socket(self):
1114
+ return self.get_extra_info("socket")
1115
+
1116
+ @property
1117
+ def xfamily(self) -> Family | None:
1118
+ family = self.socket.family
1119
+ if family == socket.AF_UNIX:
1120
+ return Family.Local
1121
+ elif family == socket.AF_INET:
1122
+ return Family.Internet
1123
+ elif family == socket.AF_INET6:
1124
+ return Family.InternetV6
1125
+
1126
+ def write(self, payload):
1127
+ self.writer.write(payload)
1128
+
1129
+ def writelines(self, lines):
1130
+ l0 = lines[0]
1131
+ code = l0[0]
1132
+ if code in {LITTLE_ENDIAN, BIG_ENDIAN}:
1133
+ cmd_name = "Setup"
1134
+ else:
1135
+ cmd_name = Command(code).name
1136
+ log.debug("WRITEL(%3d): %s %s", len(lines), cmd_name, LogBytes(l0))
1137
+ self.writer.writelines(lines)
1138
+
1139
+ async def flush(self):
1140
+ return await self.writer.drain()
1141
+
1142
+ async def read(self, n=-1):
1143
+ return await self.reader.read(n)
1144
+
1145
+ async def readexactly(self, n):
1146
+ return await self.reader.readexactly(n)
1147
+
1148
+
1149
+ async def open_connection(config: dict) -> Connection:
1150
+ protocol = config["protocol"]
1151
+ if protocol == "unix":
1152
+ path = UNIX_PATH_TEMPLATE.format(config["port"])
1153
+ r, w = await asyncio.open_unix_connection(path)
1154
+ elif protocol == "tcp":
1155
+ port = BASE_TCP_PORT + config["port"]
1156
+ r, w = await asyncio.open_connection(config["host"], port)
1157
+ else:
1158
+ raise ValueError(f"Unsupported protocol {protocol!r}")
1159
+ return Connection(r, w, config)
1160
+
1161
+
1162
+ class Xauthority:
1163
+ def __init__(self, filename: str | None = None):
1164
+ self.filename = get_auth_filename(filename)
1165
+ self.entries = get_auth_config(self.filename)
1166
+
1167
+ def __call__(self, connection: Connection, types=(TYPE,)) -> dict:
1168
+ family = connection.xfamily
1169
+ if connection.config["protocol"] == "tcp":
1170
+ if connection.config["host"] == "localhost":
1171
+ family = Family.Local
1172
+ address = socket.gethostname().encode()
1173
+ else:
1174
+ if family == Family.Internet:
1175
+ octets = connection.socket.getpeername()[0].split(".")
1176
+ address = bytearray(int(x) for x in octets)
1177
+ elif family == Family.InternetV6:
1178
+ address = socket.gethostname().encode()
1179
+ else:
1180
+ address = socket.gethostname().encode()
1181
+
1182
+ port = connection.config["port"]
1183
+ bport = f"{port}".encode()
1184
+
1185
+ matches = {}
1186
+ for entry in self.entries:
1187
+ ename, eport = entry["name"], entry["port"]
1188
+ if not eport and entry["name"] not in matches:
1189
+ eport = bport
1190
+ if entry["family"] == family and entry["address"] == address and eport == bport:
1191
+ matches[ename] = entry
1192
+ for type_ in types:
1193
+ try:
1194
+ return matches[type_]
1195
+ except KeyError:
1196
+ pass
1197
+ raise ValueError("Cannot find auth")
1198
+
1199
+
1200
+ class ResourceManager:
1201
+ def __init__(self, base, mask):
1202
+ self.base = base
1203
+ self.mask = mask
1204
+ self.last = 0
1205
+ self.ids = set()
1206
+
1207
+ def allocate(self):
1208
+ i = self.last
1209
+ while i in self.ids:
1210
+ i = i + 1
1211
+ if i > self.mask:
1212
+ i = 0
1213
+ if i == self.last:
1214
+ raise RuntimeError("out of resource ids")
1215
+ self.ids.add(i)
1216
+ self.last = i
1217
+ return self.base | i
1218
+
1219
+
1220
+ class SharedMemoryOperation(enum.IntEnum):
1221
+ QueryVersion = 0
1222
+ Attach = 1
1223
+ Detach = 2
1224
+ PutImage = 3
1225
+ GetImage = 4
1226
+ CreatePixmap = 5
1227
+ AttachFd = 6
1228
+ CreateSegment = 7
1229
+
1230
+
1231
+ def pack_shm_attach(opcode, rid, shmid, read_only=True):
1232
+ fmt = "BBHIIB3x"
1233
+ length = struct.calcsize(fmt) // 4
1234
+ return struct.pack(fmt, opcode, SharedMemoryOperation.Attach, length, rid, shmid, 1 if read_only else 0)
1235
+
1236
+
1237
+ def pack_shm_attach_fd(opcode, fd, read_only=True):
1238
+ fmt = "BBHIB3x"
1239
+ length = struct.calcsize(fmt) // 4
1240
+ return struct.pack(fmt, opcode, SharedMemoryOperation.AttachFd, length, fd, 1 if read_only else 0)
1241
+
1242
+
1243
+ def pack_shm_version(opcode):
1244
+ fmt = "BBH"
1245
+ length = struct.calcsize(fmt) // 4
1246
+ return struct.pack(fmt, opcode, SharedMemoryOperation.QueryVersion, length)
1247
+
1248
+
1249
+ def unpack_shm_version(data, offset=0):
1250
+ result, offset = unpack_reply_head(data, offset)
1251
+ fmt = "HHHHB15x"
1252
+ fields = "major_version", "minor_version", "uid", "gid", "pixmap_format"
1253
+ result.update(zip(fields, struct.unpack_from(fmt, data, offset), strict=True))
1254
+ offset += struct.calcsize(fmt)
1255
+ result["shared_pixmaps"] = bool(result.pop("n"))
1256
+ return result, offset
1257
+
1258
+
1259
+ def pack_shm_put_image(
1260
+ opcode,
1261
+ wnd,
1262
+ gc,
1263
+ total_width,
1264
+ total_height,
1265
+ src_x,
1266
+ src_y,
1267
+ src_width,
1268
+ src_height,
1269
+ dst_x,
1270
+ dst_y,
1271
+ depth,
1272
+ send_event,
1273
+ rid,
1274
+ offset,
1275
+ ):
1276
+ fmt = "BBHIIHHHHHHhhBBBBII"
1277
+ length = struct.calcsize(fmt) // 4
1278
+ return struct.pack(
1279
+ fmt,
1280
+ opcode,
1281
+ SharedMemoryOperation.PutImage,
1282
+ length,
1283
+ wnd,
1284
+ gc,
1285
+ total_width,
1286
+ total_height,
1287
+ src_x,
1288
+ src_y,
1289
+ src_width,
1290
+ src_height,
1291
+ dst_x,
1292
+ dst_y,
1293
+ depth,
1294
+ ImageFormat.ZPixmap,
1295
+ send_event,
1296
+ 0,
1297
+ rid,
1298
+ offset,
1299
+ )
1300
+
1301
+
1302
+ def unpack_shm_complete(data, offset=0):
1303
+ fmt = "BxHIHBxII12x"
1304
+ fields = "code", "sequence_number", "window", "minor", "major", "shm_id", "offset"
1305
+ size = struct.calcsize(fmt)
1306
+ result = dict(zip(fields, struct.unpack_from(fmt, data, offset), strict=True))
1307
+ result["name"] = "ShmComplete"
1308
+ return result, offset + size
1309
+
1310
+
1311
+ class Extension:
1312
+ def __init__(self, display, info):
1313
+ self.display = display
1314
+ self.info = info
1315
+ self.code = info["opcode"]
1316
+ self.events = {}
1317
+ self.errors = {}
1318
+ self.commands = {}
1319
+
1320
+
1321
+ class SharedMemory(Extension):
1322
+ BaseShmCompletion = 0
1323
+ BaseBadShmSeg = 0
1324
+
1325
+ def __init__(self, display, info):
1326
+ super().__init__(display, info)
1327
+ self.ShmCompletion = info["base_event"] + self.BaseShmCompletion
1328
+ self.BadShmSeg = info["base_error"] + self.BaseBadShmSeg
1329
+ self.events = {self.ShmCompletion: unpack_shm_complete}
1330
+ self.errors = {self.BadShmSeg: "BadShmSeg"}
1331
+ self.commands = {self.code: "Shm"}
1332
+
1333
+ async def version(self):
1334
+ payload = pack_shm_version(self.code)
1335
+ reply = await self.display.write_read(payload, unpack_shm_version)
1336
+ return reply
1337
+
1338
+ async def attach(self, shm_id, read_only=True):
1339
+ rid = self.display.resources.allocate()
1340
+ payload = pack_shm_attach(self.code, rid, shm_id, read_only)
1341
+ self.display.write(payload)
1342
+ return rid
1343
+
1344
+ async def attach_fd(self, fd, read_only=True):
1345
+ rid = self.display.resources.allocate()
1346
+ payload = pack_shm_attach_fd(self.code, rid, read_only)
1347
+ sock = self.display.connection.socket._sock
1348
+ socket.send_fds(sock, (payload,), (fd,))
1349
+ return rid
1350
+
1351
+ async def put_image(
1352
+ self,
1353
+ wnd,
1354
+ gc,
1355
+ total_width,
1356
+ total_height,
1357
+ src_x,
1358
+ src_y,
1359
+ src_width,
1360
+ src_height,
1361
+ dst_x,
1362
+ dst_y,
1363
+ depth,
1364
+ send_event,
1365
+ rid,
1366
+ offset,
1367
+ ):
1368
+ payload = pack_shm_put_image(
1369
+ self.code,
1370
+ int(wnd),
1371
+ int(gc),
1372
+ total_width,
1373
+ total_height,
1374
+ src_x,
1375
+ src_y,
1376
+ src_width,
1377
+ src_height,
1378
+ dst_x,
1379
+ dst_y,
1380
+ depth,
1381
+ send_event,
1382
+ rid,
1383
+ offset,
1384
+ )
1385
+ self.display.write(payload)
1386
+
1387
+
1388
+ EXTENSIONS = {"MIT-SHM": SharedMemory}
1389
+
1390
+
1391
+ class Display:
1392
+ def __init__(self, connection, config):
1393
+ self.connection = connection
1394
+ self.config = config
1395
+ self.info = None
1396
+ self.resources = None
1397
+ self.screens = None
1398
+ self.events = {Event.ClientMessage: unpack_client_message}
1399
+ self.commands = {cmd: cmd.name for cmd in Command}
1400
+ self.requests = asyncio.Queue()
1401
+ self.listeners = set()
1402
+ self._atom_cache = {}
1403
+ self._extensions_cache = None
1404
+ self._task = None
1405
+
1406
+ async def __aenter__(self):
1407
+ await self.setup()
1408
+ return self
1409
+
1410
+ async def __aexit__(self, *args):
1411
+ await self.aclose()
1412
+
1413
+ def close(self):
1414
+ if self._task:
1415
+ self._task.cancel()
1416
+ self.connection.close()
1417
+
1418
+ async def aclose(self):
1419
+ if self._task:
1420
+ self._task.cancel()
1421
+ try:
1422
+ await self._task
1423
+ except asyncio.CancelledError:
1424
+ pass
1425
+ self._task = None
1426
+ await self.connection.aclose()
1427
+
1428
+ def write(self, payload):
1429
+ code = payload[0]
1430
+ cmd_name = code
1431
+ if code in {LITTLE_ENDIAN, BIG_ENDIAN}:
1432
+ cmd_name = "Setup"
1433
+ else:
1434
+ cmd_name = self.commands.get(code)
1435
+ log.debug("WRITE: %s", cmd_name)
1436
+ self.connection.write(payload)
1437
+
1438
+ async def read(self, n=-1):
1439
+ return await self.connection.read(n)
1440
+
1441
+ async def readexactly(self, n):
1442
+ payload = await self.connection.readexactly(n)
1443
+ log.debug("READ: %d", len(payload))
1444
+ return payload
1445
+
1446
+ def write_read(self, payload, callback):
1447
+ future = asyncio.Future()
1448
+ code = payload[0]
1449
+ cmd = Command(code).name if code in Command else code
1450
+ self.requests.put_nowait((callback, future, cmd))
1451
+ self.write(payload)
1452
+ return future
1453
+
1454
+ async def flush(self):
1455
+ await self.connection.flush()
1456
+
1457
+ async def setup(self):
1458
+ entry = auth(self.connection)
1459
+ req = pack_setup(entry["name"], entry["data"])
1460
+ self.write(req)
1461
+ await self.flush()
1462
+ info = await read_setup(self)
1463
+ self.info = info
1464
+ self.resources = ResourceManager(info["resource_id_base"], info["resource_id_mask"])
1465
+ self.screens = [Screen(self, sinfo) for sinfo in info["screens"]]
1466
+ self._task = asyncio.create_task(self._loop())
1467
+ return info
1468
+
1469
+ async def intern_atom(self, name, only_if_exists=False):
1470
+ payload = pack_intern_atom(name, only_if_exists)
1471
+ reply = await self.write_read(payload, unpack_intern_atom)
1472
+ return reply["atom"]
1473
+
1474
+ async def _list_extensions(self):
1475
+ payload = pack_list_extensions()
1476
+ reply = await self.write_read(payload, unpack_list_extensions)
1477
+ return reply["extensions"]
1478
+
1479
+ async def _query_extension(self, name):
1480
+ payload = pack_query_extension(name)
1481
+ reply = await self.write_read(payload, unpack_query_extension)
1482
+ return reply
1483
+
1484
+ async def list_extensions(self):
1485
+ if self._extensions_cache is None:
1486
+ self._extensions_cache = dict.fromkeys(await self._list_extensions())
1487
+ return set(self._extensions_cache)
1488
+
1489
+ async def query_extension(self, name):
1490
+ extensions = await self.list_extensions()
1491
+ if name in extensions:
1492
+ if ext := self._extensions_cache[name]:
1493
+ return ext
1494
+ ext = await self._query_extension(name)
1495
+ if klass := EXTENSIONS.get(name):
1496
+ ext = klass(self, ext)
1497
+ self._extensions_cache[name] = ext
1498
+ self.events.update(ext.events)
1499
+ self.commands.update(ext.commands)
1500
+ return ext
1501
+
1502
+ async def get_atom(self, name, only_if_exists=False):
1503
+ if name in self._atom_cache:
1504
+ return self._atom_cache[name]
1505
+
1506
+ atom = await self.intern_atom(name, only_if_exists)
1507
+ if atom != NONE: # don't cache NONE responses in case someone creates this later
1508
+ self._atom_cache[name] = atom
1509
+ return atom
1510
+
1511
+ def default_screen(self) -> Screen:
1512
+ return self.screens[self.config["screen"]]
1513
+
1514
+ async def create_window(self, *args, **kwargs) -> Window:
1515
+ parent = self.default_screen().root
1516
+ return await parent.create_window(*args, **kwargs)
1517
+
1518
+ def add_listener(self, listener):
1519
+ self.listeners.add(listener)
1520
+
1521
+ async def _loop(self):
1522
+ buffer = b""
1523
+ while True:
1524
+ if not (data := await self.read(16 * 1024)):
1525
+ log.info("Connection closed")
1526
+ return
1527
+ buffer += data
1528
+ packet_type = buffer[0]
1529
+ if packet_type in PacketType:
1530
+ if packet_type == PacketType.Reply:
1531
+ decode, future, cmd = await self.requests.get()
1532
+ reply, offset = decode(buffer, 0)
1533
+ log.debug("REPLY: %s", cmd)
1534
+ future.set_result(reply)
1535
+ elif packet_type == PacketType.Error:
1536
+ error, offset = unpack_error(buffer)
1537
+ log.warning("ERROR: %s", error)
1538
+ else:
1539
+ event, offset = unpack_event(self.events, buffer)
1540
+ for listener in self.listeners:
1541
+ await listener.put(event)
1542
+ log.debug("EVENT: %s", event["name"])
1543
+ buffer = buffer[offset:]
1544
+
1545
+
1546
+ def get_display_config(uri: str) -> dict:
1547
+ if (match := DISPLAY_RE.match(uri)) is None:
1548
+ raise ValueError(f"Invalid DISPLAY {uri!r}")
1549
+ display = match.groupdict()
1550
+ protocol = display.get("protocol")
1551
+ if protocol:
1552
+ if protocol == "unix" and display["host"]:
1553
+ raise ValueError(f"Invalid DISPLAY {uri!r}")
1554
+ else:
1555
+ protocol = "tcp" if display["host"] else "unix"
1556
+ display["protocol"] = protocol
1557
+ display["port"] = int(display["port"])
1558
+ display["uri"] = uri
1559
+ display["screen"] = display["screen"] or 0
1560
+ return display
1561
+
1562
+
1563
+ async def get_display(uri: str | None = None) -> Display:
1564
+ uri = uri or os.environ["DISPLAY"]
1565
+ config = get_display_config(uri)
1566
+ connection = await open_connection(config)
1567
+ display = Display(connection, config)
1568
+ return display
1569
+
1570
+
1571
+ auth = Xauthority()