aioxlib 0.7.2__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {aioxlib-0.7.2 → aioxlib-0.9.0}/PKG-INFO +3 -3
  2. {aioxlib-0.7.2 → aioxlib-0.9.0}/README.md +2 -2
  3. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/__init__.py +1 -1
  4. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/api.py +21 -1
  5. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/constants.py +53 -1
  6. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/display.py +159 -27
  7. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/protocol.py +227 -1
  8. aioxlib-0.9.0/aioxlib/stream.py +145 -0
  9. aioxlib-0.9.0/aioxlib/widgets.py +428 -0
  10. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib.egg-info/PKG-INFO +3 -3
  11. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib.egg-info/SOURCES.txt +1 -0
  12. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_display.py +1 -1
  13. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_protocol.py +97 -0
  14. aioxlib-0.7.2/aioxlib/widgets.py +0 -211
  15. {aioxlib-0.7.2 → aioxlib-0.9.0}/LICENSE +0 -0
  16. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/_extensions.py +0 -0
  17. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/authority.py +0 -0
  18. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/bigreq.py +0 -0
  19. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/connection.py +0 -0
  20. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/dbe.py +0 -0
  21. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/dri3.py +0 -0
  22. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/glx.py +0 -0
  23. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/present.py +0 -0
  24. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/randr.py +0 -0
  25. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/render.py +0 -0
  26. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib/shm.py +0 -0
  27. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib.egg-info/dependency_links.txt +0 -0
  28. {aioxlib-0.7.2 → aioxlib-0.9.0}/aioxlib.egg-info/top_level.txt +0 -0
  29. {aioxlib-0.7.2 → aioxlib-0.9.0}/pyproject.toml +0 -0
  30. {aioxlib-0.7.2 → aioxlib-0.9.0}/setup.cfg +0 -0
  31. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_api.py +0 -0
  32. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_authority.py +0 -0
  33. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_bigreq.py +0 -0
  34. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_connection.py +0 -0
  35. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_constants.py +0 -0
  36. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_dbe.py +0 -0
  37. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_dri3.py +0 -0
  38. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_examples.py +0 -0
  39. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_extensions.py +0 -0
  40. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_glx.py +0 -0
  41. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_present.py +0 -0
  42. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_randr.py +0 -0
  43. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_render.py +0 -0
  44. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_shm.py +0 -0
  45. {aioxlib-0.7.2 → aioxlib-0.9.0}/tests/test_widgets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioxlib
3
- Version: 0.7.2
3
+ Version: 0.9.0
4
4
  Summary: Human friendly interface to XLib subsystems using asyncio python
5
5
  Author-email: Jose Tiago Macara Coutinho <coutinhotiago@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -38,9 +38,9 @@ Open displays, create windows, draw with GCs and the RENDER extension, and strea
38
38
  events — without blocking the event loop and without linking to libX11 or xcb.
39
39
 
40
40
  <p align="center">
41
- <img src="docs/images/game_of_life.gif" style="height: 300px;" alt="Conway's Game of Life on X11 via aioxlib"/>
41
+ <img src="docs/images/game_of_life.gif" style="height: 300px;" height="300" alt="Conway's Game of Life on X11 via aioxlib"/>
42
42
  &nbsp;
43
- <img src="docs/images/3d_cube.gif" style="height: 300px;" alt="Rotating 3D wireframe via aioxlib"/>
43
+ <img src="docs/images/3d_cube.gif" style="height: 300px;" height="300" alt="Rotating 3D wireframe via aioxlib"/>
44
44
  </p>
45
45
 
46
46
  <p align="center"><em>Examples shipped with the library: Game of Life · 3D wireframe</em></p>
@@ -12,9 +12,9 @@ Open displays, create windows, draw with GCs and the RENDER extension, and strea
12
12
  events — without blocking the event loop and without linking to libX11 or xcb.
13
13
 
14
14
  <p align="center">
15
- <img src="docs/images/game_of_life.gif" style="height: 300px;" alt="Conway's Game of Life on X11 via aioxlib"/>
15
+ <img src="docs/images/game_of_life.gif" style="height: 300px;" height="300" alt="Conway's Game of Life on X11 via aioxlib"/>
16
16
  &nbsp;
17
- <img src="docs/images/3d_cube.gif" style="height: 300px;" alt="Rotating 3D wireframe via aioxlib"/>
17
+ <img src="docs/images/3d_cube.gif" style="height: 300px;" height="300" alt="Rotating 3D wireframe via aioxlib"/>
18
18
  </p>
19
19
 
20
20
  <p align="center"><em>Examples shipped with the library: Game of Life · 3D wireframe</em></p>
@@ -16,7 +16,7 @@ Types and protocol helpers live in submodules::
16
16
  from aioxlib.protocol import pack_create_window
17
17
  """
18
18
 
19
- __version__ = "0.7.2"
19
+ __version__ = "0.9.0"
20
20
 
21
21
  from . import api, constants
22
22
  from .api import * # noqa: F403
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  from .constants import (
10
10
  Allocate,
11
11
  AnyPropertyType,
12
+ CoordinateMode,
12
13
  PropMode,
13
14
  )
14
15
  from .display import Display
@@ -307,6 +308,17 @@ async def open_font(name=None, display: Display | None = None, **kwargs):
307
308
  return await d.open_font(name=name, **kwargs)
308
309
 
309
310
 
311
+ async def query_text_extents(font, string, display: Display | None = None):
312
+ """QueryTextExtents — bounding box of *string* in *font* (or GC).
313
+
314
+ Returns a dict with draw_direction, font_ascent, font_descent,
315
+ overall_ascent, overall_descent, overall_width, overall_left,
316
+ overall_right.
317
+ """
318
+ d = await _resolve_display(display)
319
+ return await d.query_text_extents(font, string)
320
+
321
+
310
322
  async def tree(wnd, display: Display | None = None):
311
323
  """Query the window tree for *wnd*."""
312
324
  d = await _resolve_display(display)
@@ -331,12 +343,18 @@ async def poly_segments(drawable, gc, segments, display: Display | None = None):
331
343
  return await d.poly_segments(drawable, gc, segments)
332
344
 
333
345
 
334
- async def poly_points(drawable, gc, points, mode, display: Display | None = None):
346
+ async def poly_points(drawable, gc, points, mode=CoordinateMode.Origin, display: Display | None = None):
335
347
  """PolyPoint on *drawable*."""
336
348
  d = await _resolve_display(display)
337
349
  return await d.poly_points(drawable, gc, points, mode)
338
350
 
339
351
 
352
+ async def poly_lines(drawable, gc, points, mode=CoordinateMode.Origin, display: Display | None = None):
353
+ """PolyLine on *drawable*."""
354
+ d = await _resolve_display(display)
355
+ return await d.poly_lines(drawable, gc, points, mode)
356
+
357
+
340
358
  async def clear_area(wnd, x, y, w, h, exposure=False, display: Display | None = None):
341
359
  """ClearArea on *wnd*."""
342
360
  d = await _resolve_display(display)
@@ -421,11 +439,13 @@ __all__ = [
421
439
  "delete_property",
422
440
  "open_font_name",
423
441
  "open_font",
442
+ "query_text_extents",
424
443
  "tree",
425
444
  "poly_fill_rectangles",
426
445
  "poly_fill_arcs",
427
446
  "poly_segments",
428
447
  "poly_points",
448
+ "poly_lines",
429
449
  "clear_area",
430
450
  "set_input_focus",
431
451
  "get_geometry",
@@ -282,9 +282,25 @@ class Command(enum.IntEnum):
282
282
  FreeColormap = 79
283
283
  QueryExtension = 98
284
284
  ListExtensions = 99
285
-
285
+ ChangeKeyboardMapping = 100
286
+ GetKeyboardMapping = 101
287
+ ChangeKeyboardControl = 102
288
+ GetKeyboardControl = 103
289
+ Bell = 104
290
+ ChangePointerControl = 105
291
+ GetPointerControl = 106
286
292
  SetScreenSaver = 107
287
293
  GetScreenSaver = 108
294
+ ChangeHosts = 109
295
+ ListHosts = 110
296
+ SetAccessControl = 111
297
+ SetCloseDownMode = 112
298
+ KillClient = 113
299
+ RotateProperties = 114
300
+ ForceScreenSaver = 115
301
+ SetPointerMapping = 116
302
+ GetPointerMapping = 117
303
+ SetModifierMapping = 118
288
304
  GetModifierMapping = 119
289
305
  NoOperation = 127
290
306
 
@@ -925,6 +941,36 @@ class WinGravity(enum.IntEnum):
925
941
  Static = 10
926
942
 
927
943
 
944
+ # Number of 32-bit fields in the ICCCM / Xlib XWMHints structure
945
+ XWM_HINTS_LEN = 9
946
+
947
+
948
+ class WMHintsFlag(enum.IntFlag):
949
+ """Bits for the ``flags`` field — which of the following fields are valid.
950
+
951
+ Values match the ICCCM / Xlib ``XWMHints`` definition.
952
+ """
953
+
954
+ InputHint = 1 << 0
955
+ StateHint = 1 << 1
956
+ IconPixmapHint = 1 << 2
957
+ IconWindowHint = 1 << 3
958
+ IconPositionHint = 1 << 4
959
+ IconMaskHint = 1 << 5
960
+ WindowGroupHint = 1 << 6
961
+ # bit 7 was WithdrawnHint (obsolete)
962
+ UrgencyHint = 1 << 8
963
+
964
+
965
+ class InitialState(enum.IntEnum):
966
+ """Values for the ``initial_state`` field."""
967
+
968
+ Withdrawn = 0
969
+ Normal = 1
970
+ # 2 was ZoomState (obsolete)
971
+ Iconic = 3
972
+
973
+
928
974
  LSBFirst = 0
929
975
 
930
976
  MSBFirst = 1
@@ -1148,6 +1194,12 @@ __all__ = [
1148
1194
  "MotifFunc",
1149
1195
  "MotifDecor",
1150
1196
  "MotifInputMode",
1197
+ "XSIZE_HINTS_LEN",
1198
+ "SizeHintsFlag",
1199
+ "WinGravity",
1200
+ "XWM_HINTS_LEN",
1201
+ "WMHintsFlag",
1202
+ "InitialState",
1151
1203
  "LSBFirst",
1152
1204
  "MSBFirst",
1153
1205
  "GenericEventCode",
@@ -17,6 +17,7 @@ import asyncio
17
17
  import logging
18
18
  import os
19
19
 
20
+ from . import protocol
20
21
  from .connection import open_connection
21
22
  from .constants import (
22
23
  BYTE_ORDER_TEXT,
@@ -28,10 +29,12 @@ from .constants import (
28
29
  AnyPropertyType,
29
30
  Atom,
30
31
  Command,
32
+ CoordinateMode,
31
33
  CopyFromParent,
32
34
  ErrorCode,
33
35
  EventType,
34
36
  ImageFormat,
37
+ InitialState,
35
38
  PropMode,
36
39
  StackMode,
37
40
  Status,
@@ -42,6 +45,7 @@ from .protocol import (
42
45
  build_font,
43
46
  build_motif_wm_hints,
44
47
  build_size_hints,
48
+ build_wm_hints,
45
49
  pack_change_property,
46
50
  pack_clear_area,
47
51
  pack_configure_window,
@@ -57,6 +61,7 @@ from .protocol import (
57
61
  pack_get_atom_name,
58
62
  pack_get_font_path,
59
63
  pack_get_geometry,
64
+ pack_get_keyboard_mapping,
60
65
  pack_get_property,
61
66
  pack_image,
62
67
  pack_image_text,
@@ -70,10 +75,15 @@ from .protocol import (
70
75
  pack_poly_fill_raw_arcs,
71
76
  pack_poly_fill_raw_rectangles,
72
77
  pack_poly_fill_rectangles,
78
+ pack_poly_lines,
73
79
  pack_poly_points,
80
+ pack_poly_raw_lines,
81
+ pack_poly_raw_rectangles,
74
82
  pack_poly_raw_segments,
83
+ pack_poly_rectangles,
75
84
  pack_poly_segments,
76
85
  pack_query_extension,
86
+ pack_query_text_extents,
77
87
  pack_query_tree,
78
88
  pack_reparent_window,
79
89
  pack_set_font_path,
@@ -82,15 +92,16 @@ from .protocol import (
82
92
  pack_unmap_window,
83
93
  unpack_error,
84
94
  unpack_event,
85
- unpack_get_atom_name,
86
95
  unpack_get_font_path,
87
96
  unpack_get_geometry,
97
+ unpack_get_keyboard_mapping,
88
98
  unpack_get_property,
89
99
  unpack_intern_atom,
90
100
  unpack_list_extensions,
91
101
  unpack_list_fonts,
92
102
  unpack_list_properties,
93
103
  unpack_query_extension,
104
+ unpack_query_text_extents,
94
105
  unpack_query_tree,
95
106
  unpack_setup,
96
107
  unpack_setup_error,
@@ -99,7 +110,11 @@ from .protocol import (
99
110
  log = logging.getLogger("aioxlib")
100
111
 
101
112
 
102
- # --- extension ---
113
+ def unpack_get_atom_name(data, offset=0):
114
+ reply, offset = protocol.unpack_get_atom_name(data, offset)
115
+ return reply["name"], offset
116
+
117
+
103
118
  class Extension:
104
119
  """Base class for X protocol extensions."""
105
120
 
@@ -115,7 +130,6 @@ class Extension:
115
130
  self.commands = {}
116
131
 
117
132
 
118
- # --- resource ---
119
133
  class Resource:
120
134
  """Base class for server-side resources identified by an XID."""
121
135
 
@@ -184,6 +198,10 @@ class Drawable(Resource):
184
198
  """Poly fill rectangles."""
185
199
  return await self.display.poly_fill_rectangles(self, gc, rectangles)
186
200
 
201
+ async def poly_rectangles(self, gc, rectangles):
202
+ """Poly rectangles."""
203
+ return await self.display.poly_rectangles(self, gc, rectangles)
204
+
187
205
  async def poly_fill_arcs(self, gc, arcs):
188
206
  """Fill elliptical arcs on this drawable.
189
207
 
@@ -200,6 +218,10 @@ class Drawable(Resource):
200
218
  """Poly points."""
201
219
  return await self.display.poly_points(self, gc, points, mode)
202
220
 
221
+ async def poly_lines(self, gc, points, mode=CoordinateMode.Origin):
222
+ """Poly lines."""
223
+ return await self.display.poly_lines(self, gc, points, mode)
224
+
203
225
  async def put_text(self, gc, x, y, text, drawable=None):
204
226
  """Put text."""
205
227
  drawable = drawable or self
@@ -240,7 +262,6 @@ class ResourceManager:
240
262
  return self.base | i
241
263
 
242
264
 
243
- # --- gc ---
244
265
  class GC(Resource):
245
266
  """Graphics context resource used by core drawing requests."""
246
267
 
@@ -254,6 +275,11 @@ class GC(Resource):
254
275
  drawable = drawable or self.drawable
255
276
  return await self.display.poly_fill_rectangles(drawable, self, rectangles)
256
277
 
278
+ async def poly_rectangles(self, rectangles, drawable=None):
279
+ """Poly rectangles."""
280
+ drawable = drawable or self.drawable
281
+ return await self.display.poly_rectangles(drawable, self, rectangles)
282
+
257
283
  async def poly_fill_arcs(self, arcs, drawable=None):
258
284
  """Poly fill arcs."""
259
285
  if drawable is None:
@@ -265,11 +291,16 @@ class GC(Resource):
265
291
  drawable = drawable or self.drawable
266
292
  return await self.display.poly_segments(drawable, self, segments)
267
293
 
268
- async def poly_points(self, points, mode, drawable=None):
294
+ async def poly_points(self, points, mode=CoordinateMode.Origin, drawable=None):
269
295
  """Poly points."""
270
296
  drawable = drawable or self.drawable
271
297
  return await self.display.poly_points(drawable, self, points, mode)
272
298
 
299
+ async def poly_lines(self, points, mode=CoordinateMode.Origin, drawable=None):
300
+ """Poly points."""
301
+ drawable = drawable or self.drawable
302
+ return await self.display.poly_lines(drawable, self, points, mode)
303
+
273
304
  async def put_text(self, x, y, text, drawable=None):
274
305
  """Put text."""
275
306
  if drawable is None:
@@ -277,16 +308,22 @@ class GC(Resource):
277
308
  payload = pack_image_text(drawable, self, x, y, text)
278
309
  self.display.connection.write(payload)
279
310
 
311
+ async def query_text_extents(self, string, font=None):
312
+ return await self.display.query_text_extents(font or self, string)
313
+
280
314
 
281
- # --- window ---
282
315
  class Window(Drawable):
283
316
  """X window resource with mapping, properties, and drawing helpers."""
284
317
 
318
+ def __init__(self, display, wid, depth):
319
+ super().__init__(display, wid)
320
+ self.depth = depth
321
+
285
322
  async def create_window(
286
323
  self,
287
324
  width,
288
325
  height,
289
- depth,
326
+ depth=None,
290
327
  x=0,
291
328
  y=0,
292
329
  border_width=0,
@@ -299,6 +336,8 @@ class Window(Drawable):
299
336
  ) -> Window:
300
337
  """Create window."""
301
338
  wid = self.display.resources.allocate()
339
+ if depth is None:
340
+ depth = self.depth
302
341
  payload = pack_create_window(
303
342
  depth=depth,
304
343
  wid=wid,
@@ -324,12 +363,16 @@ class Window(Drawable):
324
363
  wm_protocols = display.WM_PROTOCOLS
325
364
  payload += pack_change_property(wid, wm_protocols, Atom.ATOM, 32, protocols, PropMode.Replace)
326
365
  self.display.connection.write(payload)
327
- return Window(self.display, wid)
366
+ return Window(self.display, wid, depth)
328
367
 
329
368
  async def set_name(self, name):
330
369
  """Set name."""
331
370
  await self.change_text_property(Atom.WM_NAME, name)
332
371
 
372
+ async def get_name(self):
373
+ value = await self.get_property(Atom.WM_NAME)
374
+ return value["value"]
375
+
333
376
  async def set_motif_hints(self, functions=None, decorations=None, input_mode=None, status=None):
334
377
  data = build_motif_wm_hints(
335
378
  functions=functions,
@@ -338,7 +381,7 @@ class Window(Drawable):
338
381
  status=status,
339
382
  )
340
383
  atom = await self.display.get_atom(MOTIF_WM_HINTS_ATOM)
341
- await self.change_property(atom, atom, 32, list(data))
384
+ await self.change_property(atom, atom, 32, data)
342
385
 
343
386
  async def set_size_hints(
344
387
  self,
@@ -409,7 +452,58 @@ class Window(Drawable):
409
452
  Atom.WM_NORMAL_HINTS,
410
453
  Atom.WM_SIZE_HINTS,
411
454
  32,
412
- list(data),
455
+ data,
456
+ )
457
+
458
+ async def set_wm_hints(
459
+ window: Window,
460
+ *,
461
+ input: bool | None = None,
462
+ initial_state: InitialState | int | None = None,
463
+ icon_pixmap: int | None = None,
464
+ icon_window: int | None = None,
465
+ icon_x: int | None = None,
466
+ icon_y: int | None = None,
467
+ icon_mask: int | None = None,
468
+ window_group: int | None = None,
469
+ urgent: bool | None = None,
470
+ ) -> None:
471
+ """Apply ICCCM window-manager hints to *window*.
472
+
473
+ Pass individual fields, or supply a pre-built 9-element list via *data*.
474
+ When *data* is given it takes precedence over the keyword fields.
475
+
476
+ Examples
477
+ --------
478
+ Normal window that accepts keyboard focus::
479
+
480
+ await set_wm_hints(wnd, input=True, initial_state=InitialState.Normal)
481
+
482
+ Start iconic (minimized)::
483
+
484
+ await set_wm_hints(wnd, initial_state=InitialState.Iconic)
485
+
486
+ Mark urgent (attention)::
487
+
488
+ await set_wm_hints(wnd, urgent=True)
489
+ """
490
+ data = build_wm_hints(
491
+ input=input,
492
+ initial_state=initial_state,
493
+ icon_pixmap=icon_pixmap,
494
+ icon_window=icon_window,
495
+ icon_x=icon_x,
496
+ icon_y=icon_y,
497
+ icon_mask=icon_mask,
498
+ window_group=window_group,
499
+ urgent=urgent,
500
+ )
501
+
502
+ await window.change_property(
503
+ Atom.WM_HINTS,
504
+ Atom.WM_HINTS,
505
+ 32,
506
+ data,
413
507
  )
414
508
 
415
509
  async def change_text_property(self, property, data, property_type=Atom.STRING, mode=PropMode.Replace):
@@ -548,7 +642,7 @@ class Screen:
548
642
  """Initialize a new Screen instance."""
549
643
  self.display = display
550
644
  self.info = info
551
- self.root = Window(display, info["root"])
645
+ self.root = Window(display, info["root"], self.root_depth)
552
646
 
553
647
  def __getattr__(self, name):
554
648
  return self.info[name]
@@ -573,8 +667,6 @@ class Screen:
573
667
  parent = self.root
574
668
  if depth is None:
575
669
  depth = self.root_depth
576
- if background_pixel is None:
577
- background_pixel = self.black_pixel
578
670
  return await parent.create_window(
579
671
  x=x,
580
672
  y=y,
@@ -589,8 +681,11 @@ class Screen:
589
681
  **attrs,
590
682
  )
591
683
 
684
+ async def create_gc(self, drawable=None, **kwargs) -> GC:
685
+ drawable = drawable or self.root
686
+ return await drawable.create_gc(**kwargs)
687
+
592
688
 
593
- # --- display ---
594
689
  class Pipeline:
595
690
  """Context manager that batches write and write_read requests before flush.
596
691
 
@@ -652,7 +747,7 @@ class Display:
652
747
  self._events = {}
653
748
  self._atom_cache = {}
654
749
  self._extensions_cache = None
655
- self._task = None
750
+ self._event_task = None
656
751
  self._pipeline = None
657
752
 
658
753
  async def __aenter__(self):
@@ -669,19 +764,19 @@ class Display:
669
764
 
670
765
  def close(self):
671
766
  """Close."""
672
- if self._task:
673
- self._task.cancel()
767
+ if self._event_task:
768
+ self._event_task.cancel()
674
769
  self.connection.close()
675
770
 
676
771
  async def aclose(self):
677
772
  """Aclose."""
678
- if self._task:
679
- self._task.cancel()
773
+ if self._event_task:
774
+ self._event_task.cancel()
680
775
  try:
681
- await self._task
776
+ await self._event_task
682
777
  except asyncio.CancelledError:
683
778
  pass
684
- self._task = None
779
+ self._event_task = None
685
780
  await self.connection.aclose()
686
781
 
687
782
  def pipeline(self):
@@ -732,8 +827,9 @@ class Display:
732
827
 
733
828
  async def setup(self):
734
829
  """Perform the X11 connection handshake and populate screens."""
735
- if self._task is None:
736
- self._task = asyncio.create_task(self._loop())
830
+ if self._event_task is None:
831
+ self._event_queue = asyncio.Queue()
832
+ self._event_task = asyncio.create_task(self._loop())
737
833
  entry = self.connection.authority()
738
834
  payload = pack_setup(entry["name"], entry["data"])
739
835
  info = await self.write_read(payload, unpack_setup)
@@ -748,11 +844,10 @@ class Display:
748
844
  reply = await self.write_read(payload, unpack_intern_atom)
749
845
  return reply["atom"]
750
846
 
751
- async def get_atom_name(self, atom):
847
+ def get_atom_name(self, atom):
752
848
  """Return the string name for *atom*."""
753
849
  payload = pack_get_atom_name(atom)
754
- reply = await self.write_read(payload, unpack_get_atom_name)
755
- return reply["name"]
850
+ return self.write_read(payload, unpack_get_atom_name)
756
851
 
757
852
  async def _list_extensions(self):
758
853
  payload = pack_list_extensions()
@@ -902,6 +997,16 @@ class Display:
902
997
  font = build_font(**kwargs) if name is None else name
903
998
  return await self.open_font_name(font)
904
999
 
1000
+ async def query_text_extents(self, font, string):
1001
+ """QueryTextExtents — bounding box of *string* in *font* (or GC).
1002
+
1003
+ Returns a dict with draw_direction, font_ascent, font_descent,
1004
+ overall_ascent, overall_descent, overall_width, overall_left,
1005
+ overall_right.
1006
+ """
1007
+ payload = pack_query_text_extents(font, string)
1008
+ return await self.write_read(payload, unpack_query_text_extents)
1009
+
905
1010
  async def tree(self, wnd):
906
1011
  """Tree."""
907
1012
  payload = pack_query_tree(wnd)
@@ -916,6 +1021,14 @@ class Display:
916
1021
  payload = pack_poly_fill_rectangles(drawable, gc, rectangles)
917
1022
  self.write(payload)
918
1023
 
1024
+ async def poly_rectangles(self, drawable, gc, rectangles):
1025
+ """Poly rectangles."""
1026
+ if isinstance(rectangles, bytes | bytearray):
1027
+ payload = pack_poly_raw_rectangles(drawable, gc, rectangles)
1028
+ else:
1029
+ payload = pack_poly_rectangles(drawable, gc, rectangles)
1030
+ self.write(payload)
1031
+
919
1032
  async def poly_fill_arcs(self, drawable, gc, arcs):
920
1033
  """PolyFillArc — fill elliptical arcs (or full ellipses).
921
1034
 
@@ -944,6 +1057,14 @@ class Display:
944
1057
  payload = pack_poly_points(drawable, gc, points, mode)
945
1058
  self.write(payload)
946
1059
 
1060
+ async def poly_lines(self, drawable, gc, points, mode=CoordinateMode.Origin):
1061
+ """Poly lines."""
1062
+ if isinstance(points, bytes):
1063
+ payload = pack_poly_raw_lines(drawable, gc, points, mode)
1064
+ else:
1065
+ payload = pack_poly_lines(drawable, gc, points, mode)
1066
+ self.write(payload)
1067
+
947
1068
  async def clear_area(self, wnd, x, y, w, h, exposure=False):
948
1069
  """Clear area."""
949
1070
  payload = pack_clear_area(wnd, x, y, w, h, exposure)
@@ -993,6 +1114,12 @@ class Display:
993
1114
  payload = pack_reparent_window(window, parent, x, y)
994
1115
  self.write(payload)
995
1116
 
1117
+ async def get_keyboard_mapping(self, start=None, count=None):
1118
+ start = start or self.info["min_keycode"]
1119
+ count = count or (self.info["max_keycode"] - start)
1120
+ payload = pack_get_keyboard_mapping(start, count)
1121
+ return await self.write_read(payload, unpack_get_keyboard_mapping)
1122
+
996
1123
  def add_listener(self, listener):
997
1124
  """Add listener."""
998
1125
  self.listeners.add(listener)
@@ -1063,7 +1190,12 @@ class Display:
1063
1190
  # An event
1064
1191
  if len(buffer) < 32:
1065
1192
  return True, buffer, current_decoder
1066
- event, offset = unpack_event(self._events, buffer)
1193
+ try:
1194
+ event, offset = unpack_event(self._events, buffer)
1195
+ except ValueError:
1196
+ log.exception("discarded buffer: %s", buffer)
1197
+ return False, b"", current_decoder
1198
+
1067
1199
  for listener in self.listeners:
1068
1200
  await listener.put(event)
1069
1201
  return False, buffer[offset:], current_decoder