aioxlib 0.8.0__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.8.0 → aioxlib-0.9.0}/PKG-INFO +3 -3
  2. {aioxlib-0.8.0 → aioxlib-0.9.0}/README.md +2 -2
  3. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/__init__.py +1 -1
  4. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/api.py +21 -1
  5. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/constants.py +17 -1
  6. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/display.py +104 -25
  7. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/protocol.py +121 -1
  8. aioxlib-0.9.0/aioxlib/stream.py +145 -0
  9. aioxlib-0.9.0/aioxlib/widgets.py +428 -0
  10. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib.egg-info/PKG-INFO +3 -3
  11. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib.egg-info/SOURCES.txt +1 -0
  12. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_display.py +1 -1
  13. aioxlib-0.8.0/aioxlib/widgets.py +0 -211
  14. {aioxlib-0.8.0 → aioxlib-0.9.0}/LICENSE +0 -0
  15. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/_extensions.py +0 -0
  16. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/authority.py +0 -0
  17. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/bigreq.py +0 -0
  18. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/connection.py +0 -0
  19. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/dbe.py +0 -0
  20. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/dri3.py +0 -0
  21. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/glx.py +0 -0
  22. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/present.py +0 -0
  23. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/randr.py +0 -0
  24. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/render.py +0 -0
  25. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib/shm.py +0 -0
  26. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib.egg-info/dependency_links.txt +0 -0
  27. {aioxlib-0.8.0 → aioxlib-0.9.0}/aioxlib.egg-info/top_level.txt +0 -0
  28. {aioxlib-0.8.0 → aioxlib-0.9.0}/pyproject.toml +0 -0
  29. {aioxlib-0.8.0 → aioxlib-0.9.0}/setup.cfg +0 -0
  30. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_api.py +0 -0
  31. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_authority.py +0 -0
  32. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_bigreq.py +0 -0
  33. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_connection.py +0 -0
  34. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_constants.py +0 -0
  35. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_dbe.py +0 -0
  36. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_dri3.py +0 -0
  37. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_examples.py +0 -0
  38. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_extensions.py +0 -0
  39. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_glx.py +0 -0
  40. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_present.py +0 -0
  41. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_protocol.py +0 -0
  42. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_randr.py +0 -0
  43. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_render.py +0 -0
  44. {aioxlib-0.8.0 → aioxlib-0.9.0}/tests/test_shm.py +0 -0
  45. {aioxlib-0.8.0 → 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.8.0
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.8.0"
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
 
@@ -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,6 +29,7 @@ from .constants import (
28
29
  AnyPropertyType,
29
30
  Atom,
30
31
  Command,
32
+ CoordinateMode,
31
33
  CopyFromParent,
32
34
  ErrorCode,
33
35
  EventType,
@@ -59,6 +61,7 @@ from .protocol import (
59
61
  pack_get_atom_name,
60
62
  pack_get_font_path,
61
63
  pack_get_geometry,
64
+ pack_get_keyboard_mapping,
62
65
  pack_get_property,
63
66
  pack_image,
64
67
  pack_image_text,
@@ -72,10 +75,15 @@ from .protocol import (
72
75
  pack_poly_fill_raw_arcs,
73
76
  pack_poly_fill_raw_rectangles,
74
77
  pack_poly_fill_rectangles,
78
+ pack_poly_lines,
75
79
  pack_poly_points,
80
+ pack_poly_raw_lines,
81
+ pack_poly_raw_rectangles,
76
82
  pack_poly_raw_segments,
83
+ pack_poly_rectangles,
77
84
  pack_poly_segments,
78
85
  pack_query_extension,
86
+ pack_query_text_extents,
79
87
  pack_query_tree,
80
88
  pack_reparent_window,
81
89
  pack_set_font_path,
@@ -84,15 +92,16 @@ from .protocol import (
84
92
  pack_unmap_window,
85
93
  unpack_error,
86
94
  unpack_event,
87
- unpack_get_atom_name,
88
95
  unpack_get_font_path,
89
96
  unpack_get_geometry,
97
+ unpack_get_keyboard_mapping,
90
98
  unpack_get_property,
91
99
  unpack_intern_atom,
92
100
  unpack_list_extensions,
93
101
  unpack_list_fonts,
94
102
  unpack_list_properties,
95
103
  unpack_query_extension,
104
+ unpack_query_text_extents,
96
105
  unpack_query_tree,
97
106
  unpack_setup,
98
107
  unpack_setup_error,
@@ -101,7 +110,11 @@ from .protocol import (
101
110
  log = logging.getLogger("aioxlib")
102
111
 
103
112
 
104
- # --- 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
+
105
118
  class Extension:
106
119
  """Base class for X protocol extensions."""
107
120
 
@@ -117,7 +130,6 @@ class Extension:
117
130
  self.commands = {}
118
131
 
119
132
 
120
- # --- resource ---
121
133
  class Resource:
122
134
  """Base class for server-side resources identified by an XID."""
123
135
 
@@ -186,6 +198,10 @@ class Drawable(Resource):
186
198
  """Poly fill rectangles."""
187
199
  return await self.display.poly_fill_rectangles(self, gc, rectangles)
188
200
 
201
+ async def poly_rectangles(self, gc, rectangles):
202
+ """Poly rectangles."""
203
+ return await self.display.poly_rectangles(self, gc, rectangles)
204
+
189
205
  async def poly_fill_arcs(self, gc, arcs):
190
206
  """Fill elliptical arcs on this drawable.
191
207
 
@@ -202,6 +218,10 @@ class Drawable(Resource):
202
218
  """Poly points."""
203
219
  return await self.display.poly_points(self, gc, points, mode)
204
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
+
205
225
  async def put_text(self, gc, x, y, text, drawable=None):
206
226
  """Put text."""
207
227
  drawable = drawable or self
@@ -242,7 +262,6 @@ class ResourceManager:
242
262
  return self.base | i
243
263
 
244
264
 
245
- # --- gc ---
246
265
  class GC(Resource):
247
266
  """Graphics context resource used by core drawing requests."""
248
267
 
@@ -256,6 +275,11 @@ class GC(Resource):
256
275
  drawable = drawable or self.drawable
257
276
  return await self.display.poly_fill_rectangles(drawable, self, rectangles)
258
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
+
259
283
  async def poly_fill_arcs(self, arcs, drawable=None):
260
284
  """Poly fill arcs."""
261
285
  if drawable is None:
@@ -267,11 +291,16 @@ class GC(Resource):
267
291
  drawable = drawable or self.drawable
268
292
  return await self.display.poly_segments(drawable, self, segments)
269
293
 
270
- async def poly_points(self, points, mode, drawable=None):
294
+ async def poly_points(self, points, mode=CoordinateMode.Origin, drawable=None):
271
295
  """Poly points."""
272
296
  drawable = drawable or self.drawable
273
297
  return await self.display.poly_points(drawable, self, points, mode)
274
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
+
275
304
  async def put_text(self, x, y, text, drawable=None):
276
305
  """Put text."""
277
306
  if drawable is None:
@@ -279,16 +308,22 @@ class GC(Resource):
279
308
  payload = pack_image_text(drawable, self, x, y, text)
280
309
  self.display.connection.write(payload)
281
310
 
311
+ async def query_text_extents(self, string, font=None):
312
+ return await self.display.query_text_extents(font or self, string)
313
+
282
314
 
283
- # --- window ---
284
315
  class Window(Drawable):
285
316
  """X window resource with mapping, properties, and drawing helpers."""
286
317
 
318
+ def __init__(self, display, wid, depth):
319
+ super().__init__(display, wid)
320
+ self.depth = depth
321
+
287
322
  async def create_window(
288
323
  self,
289
324
  width,
290
325
  height,
291
- depth,
326
+ depth=None,
292
327
  x=0,
293
328
  y=0,
294
329
  border_width=0,
@@ -301,6 +336,8 @@ class Window(Drawable):
301
336
  ) -> Window:
302
337
  """Create window."""
303
338
  wid = self.display.resources.allocate()
339
+ if depth is None:
340
+ depth = self.depth
304
341
  payload = pack_create_window(
305
342
  depth=depth,
306
343
  wid=wid,
@@ -326,12 +363,16 @@ class Window(Drawable):
326
363
  wm_protocols = display.WM_PROTOCOLS
327
364
  payload += pack_change_property(wid, wm_protocols, Atom.ATOM, 32, protocols, PropMode.Replace)
328
365
  self.display.connection.write(payload)
329
- return Window(self.display, wid)
366
+ return Window(self.display, wid, depth)
330
367
 
331
368
  async def set_name(self, name):
332
369
  """Set name."""
333
370
  await self.change_text_property(Atom.WM_NAME, name)
334
371
 
372
+ async def get_name(self):
373
+ value = await self.get_property(Atom.WM_NAME)
374
+ return value["value"]
375
+
335
376
  async def set_motif_hints(self, functions=None, decorations=None, input_mode=None, status=None):
336
377
  data = build_motif_wm_hints(
337
378
  functions=functions,
@@ -601,7 +642,7 @@ class Screen:
601
642
  """Initialize a new Screen instance."""
602
643
  self.display = display
603
644
  self.info = info
604
- self.root = Window(display, info["root"])
645
+ self.root = Window(display, info["root"], self.root_depth)
605
646
 
606
647
  def __getattr__(self, name):
607
648
  return self.info[name]
@@ -626,8 +667,6 @@ class Screen:
626
667
  parent = self.root
627
668
  if depth is None:
628
669
  depth = self.root_depth
629
- if background_pixel is None:
630
- background_pixel = self.black_pixel
631
670
  return await parent.create_window(
632
671
  x=x,
633
672
  y=y,
@@ -642,8 +681,11 @@ class Screen:
642
681
  **attrs,
643
682
  )
644
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
+
645
688
 
646
- # --- display ---
647
689
  class Pipeline:
648
690
  """Context manager that batches write and write_read requests before flush.
649
691
 
@@ -705,7 +747,7 @@ class Display:
705
747
  self._events = {}
706
748
  self._atom_cache = {}
707
749
  self._extensions_cache = None
708
- self._task = None
750
+ self._event_task = None
709
751
  self._pipeline = None
710
752
 
711
753
  async def __aenter__(self):
@@ -722,19 +764,19 @@ class Display:
722
764
 
723
765
  def close(self):
724
766
  """Close."""
725
- if self._task:
726
- self._task.cancel()
767
+ if self._event_task:
768
+ self._event_task.cancel()
727
769
  self.connection.close()
728
770
 
729
771
  async def aclose(self):
730
772
  """Aclose."""
731
- if self._task:
732
- self._task.cancel()
773
+ if self._event_task:
774
+ self._event_task.cancel()
733
775
  try:
734
- await self._task
776
+ await self._event_task
735
777
  except asyncio.CancelledError:
736
778
  pass
737
- self._task = None
779
+ self._event_task = None
738
780
  await self.connection.aclose()
739
781
 
740
782
  def pipeline(self):
@@ -785,8 +827,9 @@ class Display:
785
827
 
786
828
  async def setup(self):
787
829
  """Perform the X11 connection handshake and populate screens."""
788
- if self._task is None:
789
- 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())
790
833
  entry = self.connection.authority()
791
834
  payload = pack_setup(entry["name"], entry["data"])
792
835
  info = await self.write_read(payload, unpack_setup)
@@ -801,11 +844,10 @@ class Display:
801
844
  reply = await self.write_read(payload, unpack_intern_atom)
802
845
  return reply["atom"]
803
846
 
804
- async def get_atom_name(self, atom):
847
+ def get_atom_name(self, atom):
805
848
  """Return the string name for *atom*."""
806
849
  payload = pack_get_atom_name(atom)
807
- reply = await self.write_read(payload, unpack_get_atom_name)
808
- return reply["name"]
850
+ return self.write_read(payload, unpack_get_atom_name)
809
851
 
810
852
  async def _list_extensions(self):
811
853
  payload = pack_list_extensions()
@@ -955,6 +997,16 @@ class Display:
955
997
  font = build_font(**kwargs) if name is None else name
956
998
  return await self.open_font_name(font)
957
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
+
958
1010
  async def tree(self, wnd):
959
1011
  """Tree."""
960
1012
  payload = pack_query_tree(wnd)
@@ -969,6 +1021,14 @@ class Display:
969
1021
  payload = pack_poly_fill_rectangles(drawable, gc, rectangles)
970
1022
  self.write(payload)
971
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
+
972
1032
  async def poly_fill_arcs(self, drawable, gc, arcs):
973
1033
  """PolyFillArc — fill elliptical arcs (or full ellipses).
974
1034
 
@@ -997,6 +1057,14 @@ class Display:
997
1057
  payload = pack_poly_points(drawable, gc, points, mode)
998
1058
  self.write(payload)
999
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
+
1000
1068
  async def clear_area(self, wnd, x, y, w, h, exposure=False):
1001
1069
  """Clear area."""
1002
1070
  payload = pack_clear_area(wnd, x, y, w, h, exposure)
@@ -1046,6 +1114,12 @@ class Display:
1046
1114
  payload = pack_reparent_window(window, parent, x, y)
1047
1115
  self.write(payload)
1048
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
+
1049
1123
  def add_listener(self, listener):
1050
1124
  """Add listener."""
1051
1125
  self.listeners.add(listener)
@@ -1116,7 +1190,12 @@ class Display:
1116
1190
  # An event
1117
1191
  if len(buffer) < 32:
1118
1192
  return True, buffer, current_decoder
1119
- 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
+
1120
1199
  for listener in self.listeners:
1121
1200
  await listener.put(event)
1122
1201
  return False, buffer[offset:], current_decoder
@@ -26,6 +26,7 @@ from .constants import (
26
26
  Atom,
27
27
  Command,
28
28
  ConfigureWindowAttributes,
29
+ CoordinateMode,
29
30
  CreateWindowAttributes,
30
31
  EventType,
31
32
  InitialState,
@@ -444,6 +445,15 @@ def pack_poly_fill_rectangles(drawable, gc, rectangles) -> bytes:
444
445
  return struct.pack(fmt, Command.PolyFillRectangle, length, drawable, gc, *coords)
445
446
 
446
447
 
448
+ def pack_poly_rectangles(drawable, gc, rectangles) -> bytes:
449
+ """Pack a wire-protocol poly rectangles request into ``bytes``."""
450
+ n = len(rectangles)
451
+ length = 3 + 2 * n
452
+ fmt = f"BxHII{4 * n}h"
453
+ coords = (c for rect in rectangles for c in rect)
454
+ return struct.pack(fmt, Command.PolyRectangle, length, drawable, gc, *coords)
455
+
456
+
447
457
  def pack_poly_fill_arcs(drawable, gc, arcs) -> bytes:
448
458
  """PolyFillArc: fill elliptical arcs (or full ellipses).
449
459
 
@@ -481,6 +491,15 @@ def pack_poly_fill_raw_rectangles(drawable, gc, rectangles: bytes) -> bytes:
481
491
  return struct.pack(fmt, Command.PolyFillRectangle, length, drawable, gc, rectangles)
482
492
 
483
493
 
494
+ def pack_poly_raw_rectangles(drawable, gc, rectangles: bytes) -> bytes:
495
+ """Pack a wire-protocol poly fill raw rectangles request into ``bytes``."""
496
+ size = len(rectangles)
497
+ n = size // 8
498
+ length = 3 + 2 * n
499
+ fmt = f"BxHII{size}s"
500
+ return struct.pack(fmt, Command.PolyRectangle, length, drawable, gc, rectangles)
501
+
502
+
484
503
  def pack_poly_segments(drawable, gc, segments):
485
504
  """Pack a wire-protocol poly segments request into ``bytes``."""
486
505
  n = len(segments)
@@ -499,7 +518,7 @@ def pack_poly_raw_segments(drawable, gc, segments: bytes) -> bytes:
499
518
  return struct.pack(fmt, Command.PolySegment, length, drawable, gc, segments)
500
519
 
501
520
 
502
- def pack_poly_points(drawable, gc, points, mode):
521
+ def pack_poly_points(drawable, gc, points, mode=CoordinateMode.Origin):
503
522
  """Pack a wire-protocol poly points request into ``bytes``."""
504
523
  n = len(points)
505
524
  length = 3 + n
@@ -508,6 +527,22 @@ def pack_poly_points(drawable, gc, points, mode):
508
527
  return struct.pack(fmt, Command.PolyPoint, mode, length, drawable, gc, *coords)
509
528
 
510
529
 
530
+ def pack_poly_lines(drawable, gc, points, mode=CoordinateMode.Origin):
531
+ n = len(points)
532
+ length = 3 + n
533
+ fmt = f"BBHII{2 * n}h"
534
+ coords = (coord for point in points for coord in point)
535
+ return struct.pack(fmt, Command.PolyLine, mode, length, drawable, gc, *coords)
536
+
537
+
538
+ def pack_poly_raw_lines(drawable, gc, points, mode=CoordinateMode.Origin):
539
+ size = len(points)
540
+ n = size // 4
541
+ length = 3 + n
542
+ fmt = f"BBHII{size}s"
543
+ return struct.pack(fmt, Command.PolyLine, mode, length, drawable, gc, points)
544
+
545
+
511
546
  def pack_open_font(fid, name, encoding=DEFAULT_STRING_ENCODING):
512
547
  """Pack a wire-protocol open font request into ``bytes``."""
513
548
  name = name.encode(encoding)
@@ -530,6 +565,77 @@ def pack_close_font(fid):
530
565
  return struct.pack("BxHI", Command.CloseFont, 8 // 4, fid)
531
566
 
532
567
 
568
+ def pack_query_text_extents(font, string, encoding=DEFAULT_STRING_ENCODING):
569
+ """Pack a QueryTextExtents request.
570
+
571
+ *font* is a Font or GContext id (Fontable).
572
+ *string* is a str or bytes (8-bit characters). Each character is sent as
573
+ a CHAR2B with the high byte zero, matching the classic Xlib behaviour of
574
+ XQueryTextExtents for Latin-1 / ASCII text.
575
+ """
576
+ if isinstance(string, str):
577
+ string = string.encode(encoding)
578
+ nchars = len(string)
579
+ # 2 bytes per character (CHAR2B)
580
+ nbytes = nchars * 2
581
+ p = pad(nbytes)
582
+ # request length in 4-byte units: header (2) + string(+pad)
583
+ length = 2 + (nbytes + p) // 4
584
+ odd = nchars & 1
585
+ # Build CHAR2B list: for each byte c -> (0, c)
586
+ chars = bytearray()
587
+ for c in string:
588
+ chars.append(0)
589
+ chars.append(c)
590
+ chars.extend(b"\x00" * p)
591
+ return struct.pack(
592
+ f"BBHI{nbytes + p}s",
593
+ Command.QueryTextExtents,
594
+ odd,
595
+ length,
596
+ int(font),
597
+ bytes(chars),
598
+ )
599
+
600
+
601
+ def unpack_query_text_extents(data, offset=0):
602
+ """Unpack a QueryTextExtents reply.
603
+
604
+ Returns a dict with:
605
+ draw_direction, font_ascent, font_descent,
606
+ overall_ascent, overall_descent, overall_width,
607
+ overall_left, overall_right
608
+ """
609
+ result, offset = unpack_reply_head(data, offset)
610
+ assert result["reply"] == 1
611
+ # after the 8-byte reply head:
612
+ # BYTE draw_direction was already consumed as result["n"]
613
+ result["draw_direction"] = result.pop("n")
614
+ fmt = "hhhhiii4x"
615
+ (
616
+ font_ascent,
617
+ font_descent,
618
+ overall_ascent,
619
+ overall_descent,
620
+ overall_width,
621
+ overall_left,
622
+ overall_right,
623
+ ) = struct.unpack_from(fmt, data, offset)
624
+ offset += struct.calcsize(fmt)
625
+ result.update(
626
+ {
627
+ "font_ascent": font_ascent,
628
+ "font_descent": font_descent,
629
+ "overall_ascent": overall_ascent,
630
+ "overall_descent": overall_descent,
631
+ "overall_width": overall_width,
632
+ "overall_left": overall_left,
633
+ "overall_right": overall_right,
634
+ }
635
+ )
636
+ return result, offset
637
+
638
+
533
639
  def unpack_reply_head(data: bytes, offset: int = 0) -> tuple[dict, int]:
534
640
  """Unpack a reply head reply or event from *data* starting at *offset*."""
535
641
  fmt = "BBHI"
@@ -1573,6 +1679,20 @@ def pack_reparent_window(window, parent, x, y) -> bytes:
1573
1679
  )
1574
1680
 
1575
1681
 
1682
+ def pack_get_keyboard_mapping(start, count):
1683
+ return struct.pack("BxHBB2x", Command.GetKeyboardMapping, 2, start, count)
1684
+
1685
+
1686
+ def unpack_get_keyboard_mapping(data, offset=0):
1687
+ result, offset = unpack_reply_head(data, offset)
1688
+ offset += 24
1689
+ n = result["n"]
1690
+ nm = result["length"]
1691
+ values = struct.unpack_from(f"{nm}I", data, offset)
1692
+ result["values"] = list(zip(*[iter(values)] * n, strict=True))
1693
+ return result, offset + 4 * nm
1694
+
1695
+
1576
1696
  def _gl_pad4(n: int) -> int:
1577
1697
  return (4 - (n & 3)) & 3
1578
1698