python-libei 0.1.0__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.
libei/eis.py ADDED
@@ -0,0 +1,1234 @@
1
+ """Pythonic wrapper around libeis -- the EIS *server* library.
2
+
3
+ An EIS context represents the compositor side of the protocol: it accepts
4
+ client connections, advertises seats and devices, and receives the events an
5
+ :class:`libei.ei.Sender` injects. This is what a compositor implements, and
6
+ what a test harness for this package's own ``ei`` module drives instead of
7
+ a real compositor.
8
+
9
+ Typical usage (accepting one client via the fd backend)::
10
+
11
+ server = Eis.create_for_fd()
12
+ client_fd = server.add_client()
13
+ sender = ei.Sender.create_for_fd(client_fd, name="some-client")
14
+
15
+ for event in server.events:
16
+ if event.event_type is EventType.CLIENT_CONNECT:
17
+ event.client.connect()
18
+ seat = event.client.new_seat("default")
19
+ seat.configure_capabilities([DeviceCapability.POINTER])
20
+ seat.add()
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import contextlib
26
+ import dataclasses
27
+ import enum
28
+ import logging
29
+ import os
30
+ from collections.abc import Iterator, Sequence
31
+ from ctypes import c_void_p
32
+ from pathlib import Path
33
+ from typing import IO
34
+
35
+ from . import _capi
36
+ from ._capi.libei import log_handler_t
37
+ from ._capi.loader import LibraryNotFoundError
38
+ from ._cobject import CObject
39
+ from .ei import _next_emulating_sequence
40
+
41
+ logger = logging.getLogger("libei.eis")
42
+
43
+
44
+ def is_available() -> bool:
45
+ """Whether libeis.so.1 can be loaded on this system."""
46
+ return _capi.libeis.lib.is_available()
47
+
48
+
49
+ class Error(Exception):
50
+ """A libeis call failed.
51
+
52
+ ``errno`` is the positive errno where libeis reported one (its setup
53
+ functions return a negative errno rather than setting the global), and
54
+ ``None`` where the failure was a NULL return with no code attached.
55
+ """
56
+
57
+ def __init__(self, message: str, errno: int | None = None) -> None:
58
+ super().__init__(message)
59
+ self.message = message
60
+ self.errno = errno
61
+
62
+
63
+ class EventType(enum.IntEnum):
64
+ """Mirrors ``enum eis_event_type`` from libeis.h.
65
+
66
+ Like its ``ei`` counterpart, this enum is not exhaustive by libei's own
67
+ documented contract; :attr:`Event.event_type` returns a plain ``int``
68
+ for a value not listed here rather than raising.
69
+ """
70
+
71
+ CLIENT_CONNECT = 1
72
+ CLIENT_DISCONNECT = 2
73
+ SEAT_BIND = 3
74
+ DEVICE_CLOSED = 4
75
+ DEVICE_READY = 5
76
+ SEAT_DEVICE_REQUESTED = 6
77
+ PONG = 90
78
+ SYNC = 91
79
+ FRAME = 100
80
+ DEVICE_START_EMULATING = 200
81
+ DEVICE_STOP_EMULATING = 201
82
+ POINTER_MOTION = 300
83
+ POINTER_MOTION_ABSOLUTE = 400
84
+ BUTTON_BUTTON = 500
85
+ SCROLL_DELTA = 600
86
+ SCROLL_STOP = 601
87
+ SCROLL_CANCEL = 602
88
+ SCROLL_DISCRETE = 603
89
+ KEYBOARD_KEY = 700
90
+ TOUCH_DOWN = 800
91
+ TOUCH_UP = 801
92
+ TOUCH_MOTION = 802
93
+ TEXT_KEYSYM = 900
94
+ TEXT_UTF8 = 901
95
+ # As in ei.EventType: on libei's main branch, in no released version,
96
+ # values matching upstream main. No accessors are bound for them.
97
+ # See docs/vs-snegg.md.
98
+ SWIPE_BEGIN = 1000
99
+ SWIPE_UPDATE = 1001
100
+ SWIPE_END = 1002
101
+ PINCH_BEGIN = 1010
102
+ PINCH_UPDATE = 1011
103
+ PINCH_END = 1012
104
+ HOLD_BEGIN = 1020
105
+ HOLD_END = 1021
106
+ STYLUS_BIND_CAPABILITIES = 1100
107
+ STYLUS_PROXIMITY_IN = 1101
108
+ STYLUS_PROXIMITY_OUT = 1102
109
+ STYLUS_ERASE_START = 1103
110
+ STYLUS_ERASE_STOP = 1104
111
+ STYLUS_TIP_DOWN = 1105
112
+ STYLUS_TIP_UP = 1106
113
+ STYLUS_AXIS = 1107
114
+
115
+
116
+ class DeviceCapability(enum.IntFlag):
117
+ """Mirrors ``enum eis_device_capability``.
118
+
119
+ Server-side counterpart of :class:`libei.ei.DeviceCapability`: these
120
+ are what :meth:`Device.configure` announces, rather than what a client
121
+ asks for.
122
+ """
123
+
124
+ POINTER = 1 << 0
125
+ POINTER_ABSOLUTE = 1 << 1
126
+ KEYBOARD = 1 << 2
127
+ TOUCH = 1 << 3
128
+ SCROLL = 1 << 4
129
+ BUTTON = 1 << 5
130
+ TEXT = 1 << 6
131
+ # On libei's main branch only: 1.6.0's enum ei_device_capability
132
+ # stops at TEXT. Binding one of these against a released library is a
133
+ # silent noop -- no error, no device.
134
+ GESTURES = 1 << 7
135
+ STYLUS = 1 << 8
136
+
137
+
138
+ class DeviceType(enum.IntEnum):
139
+ """Whether a device is synthesised or backed by real hardware."""
140
+
141
+ VIRTUAL = 1
142
+ PHYSICAL = 2
143
+
144
+
145
+ class KeymapType(enum.IntEnum):
146
+ """Keymap format. libeis defines exactly one."""
147
+
148
+ XKB = 1
149
+
150
+
151
+ class Flag(enum.IntEnum):
152
+ """Context behavior toggles for :meth:`Eis.set_flag`."""
153
+
154
+ # Announce ei_device protocol version 3 or later. With this set, a
155
+ # device added via Device.add() must not be resumed until its
156
+ # DEVICE_READY event has arrived.
157
+ DEVICE_READY = 1
158
+
159
+
160
+ class _LogPriority(enum.IntEnum):
161
+ DEBUG = 10
162
+ INFO = 20
163
+ WARNING = 30
164
+ ERROR = 40
165
+
166
+
167
+ @dataclasses.dataclass(frozen=True, slots=True)
168
+ class KeyEvent:
169
+ key: int
170
+ is_press: bool
171
+
172
+
173
+ @dataclasses.dataclass(frozen=True, slots=True)
174
+ class ButtonEvent:
175
+ button: int
176
+ is_press: bool
177
+
178
+
179
+ @dataclasses.dataclass(frozen=True, slots=True)
180
+ class PointerEvent:
181
+ dx: float
182
+ dy: float
183
+
184
+
185
+ @dataclasses.dataclass(frozen=True, slots=True)
186
+ class PointerAbsoluteEvent:
187
+ x: float
188
+ y: float
189
+
190
+
191
+ @dataclasses.dataclass(frozen=True, slots=True)
192
+ class ScrollEvent:
193
+ dx: float
194
+ dy: float
195
+
196
+
197
+ @dataclasses.dataclass(frozen=True, slots=True)
198
+ class ScrollDiscreteEvent:
199
+ dx: int
200
+ dy: int
201
+
202
+
203
+ @dataclasses.dataclass(frozen=True, slots=True)
204
+ class ScrollStopEvent:
205
+ stop_x: bool
206
+ stop_y: bool
207
+
208
+
209
+ @dataclasses.dataclass(frozen=True, slots=True)
210
+ class TouchEvent:
211
+ touchid: int
212
+ x: float
213
+ y: float
214
+
215
+
216
+ @dataclasses.dataclass(frozen=True, slots=True)
217
+ class TouchUpEvent:
218
+ touchid: int
219
+ is_cancel: bool
220
+
221
+
222
+ @dataclasses.dataclass(frozen=True, slots=True)
223
+ class TextUtf8Event:
224
+ text: str
225
+
226
+
227
+ @dataclasses.dataclass(frozen=True, slots=True)
228
+ class TextKeysymEvent:
229
+ keysym: int
230
+ is_press: bool
231
+
232
+
233
+ @dataclasses.dataclass(frozen=True, slots=True)
234
+ class ConfigureRegion:
235
+ """A region to create, in the form :meth:`Device.configure` wants.
236
+
237
+ Plain description rather than a live :class:`Region`, because
238
+ ``configure()`` allocates, fills in and adds each region itself --
239
+ there is no point at which a caller holds one to set fields on.
240
+ """
241
+
242
+ offset: tuple[int, int]
243
+ size: tuple[int, int]
244
+ physical_scale: float = 1.0
245
+ # Device.configure() creates, configures and adds each region itself,
246
+ # so a caller has no window in which to call Region.set_mapping_id() --
247
+ # it has to be part of the description handed in. Requires libei 1.1;
248
+ # left None, nothing is set and no 1.1 symbol is touched.
249
+ mapping_id: str | None = None
250
+
251
+
252
+ class Region(CObject):
253
+ """A rectangular area of the desktop an absolute device maps onto.
254
+
255
+ Normally created for you from a :class:`ConfigureRegion` passed to
256
+ :meth:`Device.configure`; this class is what a getter hands back.
257
+ """
258
+
259
+ _ref_func = staticmethod(_capi.libeis.region_ref)
260
+ _unref_func = staticmethod(_capi.libeis.region_unref)
261
+
262
+ @property
263
+ def position(self) -> tuple[int, int]:
264
+ """Top-left corner of the region, in logical pixels."""
265
+ return (
266
+ _capi.libeis.region_get_x(self),
267
+ _capi.libeis.region_get_y(self),
268
+ )
269
+
270
+ @property
271
+ def dimension(self) -> tuple[int, int]:
272
+ """Width and height of the region, in logical pixels."""
273
+ return (
274
+ _capi.libeis.region_get_width(self),
275
+ _capi.libeis.region_get_height(self),
276
+ )
277
+
278
+ @property
279
+ def physical_scale(self) -> float:
280
+ """Scale between logical pixels and this region's physical size."""
281
+ return _capi.libeis.region_get_physical_scale(self)
282
+
283
+ @property
284
+ def mapping_id(self) -> str | None:
285
+ """Identifier shared by regions that map to the same thing.
286
+
287
+ ``None`` unless :meth:`set_mapping_id` has set one. Requires
288
+ libei 1.1.
289
+ """
290
+ raw = _capi.libeis.region_get_mapping_id(self)
291
+ return None if raw is None else raw.decode("utf-8")
292
+
293
+ def set_mapping_id(self, mapping_id: str) -> Region:
294
+ """Tag this region so clients can group it with others.
295
+
296
+ Call before :meth:`Device.add`, like the rest of a region's
297
+ configuration. Requires libei 1.1.
298
+ """
299
+ _capi.libeis.region_set_mapping_id(self, mapping_id.encode("utf-8"))
300
+ return self
301
+
302
+ def contains(self, x: float, y: float) -> bool:
303
+ """Whether the given logical-pixel point falls inside this region."""
304
+ return bool(_capi.libeis.region_contains(self, x, y))
305
+
306
+
307
+ class Keymap(CObject):
308
+ """An XKB keymap to hand a client's keyboard device.
309
+
310
+ Build one with :meth:`Device.new_keymap` from an fd holding the keymap
311
+ text, then :meth:`add` it before adding the device.
312
+ """
313
+
314
+ _ref_func = staticmethod(_capi.libeis.keymap_ref)
315
+ _unref_func = staticmethod(_capi.libeis.keymap_unref)
316
+
317
+ @property
318
+ def keymap_type(self) -> KeymapType:
319
+ """Keymap format; currently always XKB."""
320
+ return KeymapType(_capi.libeis.keymap_get_type(self))
321
+
322
+ @property
323
+ def size(self) -> int:
324
+ """Size of the keymap data, in bytes."""
325
+ return _capi.libeis.keymap_get_size(self)
326
+
327
+ @property
328
+ def fd(self) -> IO[bytes]:
329
+ """Memmap-able file descriptor holding the keymap data.
330
+
331
+ A fresh duplicate on each read, which the caller owns and should
332
+ close; the keymap keeps its own. Rewound to position 0 where the
333
+ fd allows it, so the data is simply readable.
334
+ """
335
+ # See ei.Keymap.fd: eis_keymap_get_fd() is a plain field read, not
336
+ # a duped/transferred fd -- duplicate it so os.fdopen()'s file
337
+ # object doesn't close a fd the keymap still owns.
338
+ raw_fd = _capi.libeis.keymap_get_fd(self)
339
+ if raw_fd < 0:
340
+ raise Error("eis_keymap_get_fd() reported no usable file descriptor")
341
+ duplicate = os.dup(raw_fd)
342
+ # dup(2) shares the file offset with the original, which is
343
+ # normally at EOF -- without this rewind a read returns zero bytes
344
+ # and no error, indistinguishable from an empty keymap. See
345
+ # ei.Keymap.fd for why a non-seekable fd is tolerated here.
346
+ with contextlib.suppress(OSError):
347
+ os.lseek(duplicate, 0, os.SEEK_SET)
348
+ return os.fdopen(duplicate, "rb")
349
+
350
+ def add(self) -> None:
351
+ """Publish this object to the client."""
352
+ _capi.libeis.keymap_add(self)
353
+
354
+
355
+ class Touch(CObject):
356
+ """One touch point, from :meth:`Device.touch_new` to up or cancel.
357
+
358
+ Created per touch rather than per device, so several can be in flight
359
+ at once. Like the :class:`Device` methods, these only queue --
360
+ :meth:`Device.frame` commits them.
361
+ """
362
+
363
+ # No _ref_func: eis_device_touch_new() is the only function that ever
364
+ # returns a struct eis_touch* (aside from eis_touch_ref/unref
365
+ # themselves), and its docs say the caller already owns that
366
+ # reference -- there's no borrowed-pointer getter elsewhere that would
367
+ # need wrap()'s extra ref. See Device.touch_new(), which uses adopt().
368
+ _unref_func = staticmethod(_capi.libeis.touch_unref)
369
+
370
+ @property
371
+ def device(self) -> Device:
372
+ """The device this touch belongs to."""
373
+ device = Device.wrap(_capi.libeis.touch_get_device(self))
374
+ # wrap() is typed `T | None` because the C API's getters may
375
+ # return NULL in general; this one is documented never to. The
376
+ # assert is here to narrow the type for mypy, not to enforce an
377
+ # invariant -- under `python -O` it vanishes and a surprise NULL
378
+ # surfaces as an AttributeError on None at the caller, which is
379
+ # survivable. Contrast _cobject.py's cross-class pointer check,
380
+ # which guards memory safety and so is a real `raise`. Every
381
+ # other `assert ... is not None` in this module is the same
382
+ # narrowing idiom.
383
+ assert device is not None
384
+ return device
385
+
386
+ def down(self, x: float, y: float) -> Touch:
387
+ """Begin the touch at the given point."""
388
+ _capi.libeis.touch_down(self, x, y)
389
+ return self
390
+
391
+ def motion(self, x: float, y: float) -> Touch:
392
+ """Move the in-progress touch to the given point."""
393
+ _capi.libeis.touch_motion(self, x, y)
394
+ return self
395
+
396
+ def up(self) -> Touch:
397
+ """End the touch."""
398
+ _capi.libeis.touch_up(self)
399
+ return self
400
+
401
+ def cancel(self) -> Touch:
402
+ """End the touch as cancelled rather than logically released.
403
+
404
+ Requires libei 1.4, and version 2 or later of the
405
+ ``ei_touchscreen`` interface on both sides; against an older client
406
+ it arrives as a plain release.
407
+ """
408
+ _capi.libeis.touch_cancel(self)
409
+ return self
410
+
411
+
412
+ class Device(CObject):
413
+ """A device this server offers to a client.
414
+
415
+ Create with :meth:`Seat.new_device`, describe it with
416
+ :meth:`configure`, then :meth:`add` and :meth:`resume` it before the
417
+ client may use it. For a receiving server the sending methods are
418
+ unused; they exist for a server that feeds input to a client.
419
+ """
420
+
421
+ _ref_func = staticmethod(_capi.libeis.device_ref)
422
+ _unref_func = staticmethod(_capi.libeis.device_unref)
423
+
424
+ def __repr__(self) -> str:
425
+ caps = "|".join(c.name or str(c.value) for c in self.capabilities)
426
+ return f"<Device {self.name!r} {self.device_type.name} {caps}>"
427
+
428
+ def configure(
429
+ self,
430
+ name: str | None = None,
431
+ device_type: DeviceType = DeviceType.VIRTUAL,
432
+ size: tuple[int, int] | None = None,
433
+ capabilities: tuple[DeviceCapability, ...] = (),
434
+ regions: tuple[ConfigureRegion, ...] = (),
435
+ ) -> Device:
436
+ """Set the device's properties. Call before :meth:`add`."""
437
+ if name is not None:
438
+ _capi.libeis.device_configure_name(self, name.encode("utf-8"))
439
+ _capi.libeis.device_configure_type(self, device_type)
440
+ if size is not None:
441
+ _capi.libeis.device_configure_size(self, size[0], size[1])
442
+ for cap in capabilities:
443
+ _capi.libeis.device_configure_capability(self, cap)
444
+ for region in regions:
445
+ pointer = _capi.libeis.device_new_region(self)
446
+ if not pointer:
447
+ raise Error("eis_device_new_region() returned NULL")
448
+ # eis_device_new_region() returns an owned reference (initial
449
+ # refcount 1); eis_region_add() registers it with the device
450
+ # but doesn't consume that reference. Adopt it so we can
451
+ # release it explicitly once added -- the raw pointer used to
452
+ # be discarded here with nothing ever unref'ing it, leaking
453
+ # one region every call.
454
+ region_obj = Region.adopt(pointer)
455
+ assert region_obj is not None
456
+ _capi.libeis.region_set_size(region_obj, *region.size)
457
+ _capi.libeis.region_set_offset(region_obj, *region.offset)
458
+ _capi.libeis.region_set_physical_scale(region_obj, region.physical_scale)
459
+ if region.mapping_id is not None:
460
+ region_obj.set_mapping_id(region.mapping_id)
461
+ _capi.libeis.region_add(region_obj)
462
+ region_obj.release()
463
+ return self
464
+
465
+ def new_keymap(self, keymap_type: KeymapType, fd: IO[bytes], size: int) -> Keymap:
466
+ """Attach an XKB keymap to this keyboard-capable device."""
467
+ pointer = _capi.libeis.device_new_keymap(self, keymap_type, fd.fileno(), size)
468
+ if not pointer:
469
+ raise Error("eis_device_new_keymap() returned NULL")
470
+ keymap = Keymap.adopt(pointer)
471
+ assert keymap is not None
472
+ return keymap
473
+
474
+ @property
475
+ def device_type(self) -> DeviceType:
476
+ """Whether the device is virtual or represents real hardware."""
477
+ return DeviceType(_capi.libeis.device_get_type(self))
478
+
479
+ @property
480
+ def name(self) -> str:
481
+ """The device name set via :meth:`configure`."""
482
+ return _capi.libeis.device_get_name(self).decode("utf-8")
483
+
484
+ @property
485
+ def width(self) -> int:
486
+ """Device width in logical pixels; 0 if unsized."""
487
+ return _capi.libeis.device_get_width(self)
488
+
489
+ @property
490
+ def height(self) -> int:
491
+ """Device height in logical pixels; 0 if unsized."""
492
+ return _capi.libeis.device_get_height(self)
493
+
494
+ @property
495
+ def capabilities(self) -> tuple[DeviceCapability, ...]:
496
+ """The capabilities this object actually has."""
497
+ return tuple(
498
+ c for c in DeviceCapability if _capi.libeis.device_has_capability(self, c)
499
+ )
500
+
501
+ @property
502
+ def regions(self) -> tuple[Region, ...]:
503
+ """The device's regions, in index order."""
504
+ regions = []
505
+ index = 0
506
+ while True:
507
+ pointer = _capi.libeis.device_get_region(self, index)
508
+ if not pointer:
509
+ break
510
+ region = Region.wrap(pointer)
511
+ assert region is not None
512
+ regions.append(region)
513
+ index += 1
514
+ return tuple(regions)
515
+
516
+ @property
517
+ def seat(self) -> Seat:
518
+ """The seat this device belongs to."""
519
+ seat = Seat.wrap(_capi.libeis.device_get_seat(self))
520
+ assert seat is not None
521
+ return seat
522
+
523
+ @property
524
+ def keymap(self) -> Keymap | None:
525
+ """The device's keymap, or None if it has no keyboard capability."""
526
+ return Keymap.wrap(_capi.libeis.device_keyboard_get_keymap(self))
527
+
528
+ def add(self) -> Device:
529
+ """Publish this object to the client."""
530
+ _capi.libeis.device_add(self)
531
+ return self
532
+
533
+ def remove(self) -> Device:
534
+ """Withdraw this object from the client."""
535
+ _capi.libeis.device_remove(self)
536
+ return self
537
+
538
+ def pause(self) -> Device:
539
+ """Suspend the device; the client may not send events while paused."""
540
+ _capi.libeis.device_pause(self)
541
+ return self
542
+
543
+ def resume(self) -> Device:
544
+ """Resume a paused device, allowing the client to send events again."""
545
+ _capi.libeis.device_resume(self)
546
+ return self
547
+
548
+ def keyboard_xkb_modifiers(
549
+ self, depressed: int, latched: int, locked: int, group: int
550
+ ) -> Device:
551
+ """Notify the client of the current XKB modifier state.
552
+
553
+ Call this whenever the modifier state or effective group changes,
554
+ for every affected keyboard device.
555
+ """
556
+ _capi.libeis.device_keyboard_send_xkb_modifiers(
557
+ self, depressed, latched, locked, group
558
+ )
559
+ return self
560
+
561
+ def start_emulating(self, sequence: int | None = None) -> Device:
562
+ """Begin an emulation transaction; pair with :meth:`stop_emulating`.
563
+
564
+ ``sequence`` must go up by at least 1 on each call; the default
565
+ draws from the same process-wide counter as
566
+ :meth:`libei.ei.Device.start_emulating`.
567
+ """
568
+ if sequence is None:
569
+ sequence = _next_emulating_sequence()
570
+ _capi.libeis.device_start_emulating(self, sequence)
571
+ return self
572
+
573
+ def stop_emulating(self) -> Device:
574
+ """End the transaction opened by :meth:`start_emulating`."""
575
+ _capi.libeis.device_stop_emulating(self)
576
+ return self
577
+
578
+ def frame(self, timestamp: int | None = None) -> Device:
579
+ """Commit the events queued since the last frame as one logical
580
+ hardware event. ``timestamp`` defaults to the context's current time."""
581
+ if timestamp is None:
582
+ timestamp = _capi.libeis.now(_capi.libeis.device_get_context(self))
583
+ _capi.libeis.device_frame(self, timestamp)
584
+ return self
585
+
586
+ def pointer_motion(self, dx: float, dy: float) -> Device:
587
+ """Send a relative pointer motion, in logical pixels."""
588
+ _capi.libeis.device_pointer_motion(self, dx, dy)
589
+ return self
590
+
591
+ def pointer_motion_absolute(self, x: float, y: float) -> Device:
592
+ """Send an absolute pointer motion, in the device's region."""
593
+ _capi.libeis.device_pointer_motion_absolute(self, x, y)
594
+ return self
595
+
596
+ def button(self, button: int, is_press: bool) -> Device:
597
+ """Send a button press or release, by Linux ``BTN_*`` code."""
598
+ _capi.libeis.device_button_button(self, button, is_press)
599
+ return self
600
+
601
+ def keyboard_key(self, key: int, is_press: bool) -> Device:
602
+ """Send a key press or release, by Linux ``KEY_*`` keycode."""
603
+ _capi.libeis.device_keyboard_key(self, key, is_press)
604
+ return self
605
+
606
+ def scroll_delta(self, dx: float, dy: float) -> Device:
607
+ """Send a smooth scroll, in logical pixels."""
608
+ _capi.libeis.device_scroll_delta(self, dx, dy)
609
+ return self
610
+
611
+ def scroll_discrete(self, dx: int, dy: int) -> Device:
612
+ """Send a discrete (detent) scroll; one detent is 120."""
613
+ _capi.libeis.device_scroll_discrete(self, dx, dy)
614
+ return self
615
+
616
+ def scroll_stop(self, stop_x: bool, stop_y: bool) -> Device:
617
+ """Signal that scrolling has stopped on the given axes."""
618
+ _capi.libeis.device_scroll_stop(self, stop_x, stop_y)
619
+ return self
620
+
621
+ def scroll_cancel(self, cancel_x: bool, cancel_y: bool) -> Device:
622
+ """Signal that scroll kinetics are cancelled on the given axes."""
623
+ _capi.libeis.device_scroll_cancel(self, cancel_x, cancel_y)
624
+ return self
625
+
626
+ def region_at(self, x: float, y: float) -> Region | None:
627
+ """The region containing this desktop-wide point, or None.
628
+
629
+ Requires libei 1.1.
630
+ """
631
+ return Region.wrap(_capi.libeis.device_get_region_at(self, x, y))
632
+
633
+ def text_utf8(self, text: str) -> Device:
634
+ """Send text to the client, for a device with the TEXT capability.
635
+
636
+ Requires libei 1.6 on both sides.
637
+ """
638
+ # Encoded and passed with an explicit length: the plain
639
+ # eis_device_text_utf8() takes a NUL-terminated string, which
640
+ # would silently truncate a str containing a NUL.
641
+ data = text.encode("utf-8")
642
+ _capi.libeis.device_text_utf8_with_length(self, data, len(data))
643
+ return self
644
+
645
+ def text_keysym(self, keysym: int, is_press: bool) -> Device:
646
+ """Send an XKB keysym, for a device with the TEXT capability.
647
+
648
+ Requires libei 1.6 on both sides.
649
+ """
650
+ _capi.libeis.device_text_keysym(self, keysym, is_press)
651
+ return self
652
+
653
+ def touch_new(self) -> Touch:
654
+ """Start a new touch on a device with the TOUCH capability."""
655
+ pointer = _capi.libeis.device_touch_new(self)
656
+ if not pointer:
657
+ raise Error("eis_device_touch_new() returned NULL")
658
+ touch = Touch.adopt(pointer)
659
+ assert touch is not None
660
+ return touch
661
+
662
+
663
+ class Seat(CObject):
664
+ """A group of devices offered to one client.
665
+
666
+ Create with :meth:`Client.new_seat`, announce what it can do with
667
+ :meth:`configure_capabilities`, then :meth:`add` it. The client
668
+ answers by binding the capabilities it wants, which arrives as a
669
+ SEAT_BIND event.
670
+ """
671
+
672
+ _ref_func = staticmethod(_capi.libeis.seat_ref)
673
+ _unref_func = staticmethod(_capi.libeis.seat_unref)
674
+
675
+ def __repr__(self) -> str:
676
+ caps = "|".join(c.name or str(c.value) for c in self.capabilities)
677
+ return f"<Seat {self.name!r} {caps}>"
678
+
679
+ @property
680
+ def name(self) -> str:
681
+ """The seat name this server gave the seat."""
682
+ return _capi.libeis.seat_get_name(self).decode("utf-8")
683
+
684
+ @property
685
+ def client(self) -> Client:
686
+ """The client this object belongs to."""
687
+ client = Client.wrap(_capi.libeis.seat_get_client(self))
688
+ assert client is not None
689
+ return client
690
+
691
+ @property
692
+ def capabilities(self) -> tuple[DeviceCapability, ...]:
693
+ """The capabilities this object actually has."""
694
+ return tuple(
695
+ c for c in DeviceCapability if _capi.libeis.seat_has_capability(self, c)
696
+ )
697
+
698
+ def configure_capabilities(
699
+ self, capabilities: tuple[DeviceCapability, ...]
700
+ ) -> Seat:
701
+ """Declare which capabilities this seat offers. Call before add()."""
702
+ for cap in capabilities:
703
+ _capi.libeis.seat_configure_capability(self, cap)
704
+ return self
705
+
706
+ def add(self) -> Seat:
707
+ """Publish this object to the client."""
708
+ _capi.libeis.seat_add(self)
709
+ return self
710
+
711
+ def remove(self) -> Seat:
712
+ """Withdraw this object from the client."""
713
+ _capi.libeis.seat_remove(self)
714
+ return self
715
+
716
+ def new_device(self) -> Device:
717
+ """Create a device on this seat. Configure it, then add()."""
718
+ pointer = _capi.libeis.seat_new_device(self)
719
+ if not pointer:
720
+ raise Error("eis_seat_new_device() returned NULL")
721
+ device = Device.adopt(pointer)
722
+ assert device is not None
723
+ return device
724
+
725
+
726
+ class Client(CObject):
727
+ """A client connection, arriving as a CLIENT_CONNECT event.
728
+
729
+ Call :meth:`connect` to accept it (or :meth:`disconnect` to refuse),
730
+ then offer it seats.
731
+ """
732
+
733
+ _ref_func = staticmethod(_capi.libeis.client_ref)
734
+ _unref_func = staticmethod(_capi.libeis.client_unref)
735
+
736
+ def __repr__(self) -> str:
737
+ return f"<Client {self.name!r} sender={self.is_sender}>"
738
+
739
+ @property
740
+ def is_sender(self) -> bool:
741
+ """Whether the client sends events (rather than receiving them)."""
742
+ return bool(_capi.libeis.client_is_sender(self))
743
+
744
+ @property
745
+ def name(self) -> str:
746
+ """The name the client announced for itself."""
747
+ return _capi.libeis.client_get_name(self).decode("utf-8")
748
+
749
+ @property
750
+ def pid(self) -> int:
751
+ """The client process's pid, via ``SO_PEERCRED``.
752
+
753
+ Requires libei 1.5. Socket-backend contexts only -- meaningless
754
+ for a context set up with :meth:`Eis.create_for_fd`, where there is
755
+ no peer socket to ask. Raises :class:`Error` if the library reports
756
+ a failure.
757
+ """
758
+ result = _capi.libeis.backend_socket_get_client_pid(self)
759
+ if result < 0:
760
+ raise Error(
761
+ f"eis_backend_socket_get_client_pid() failed with errno {-result}"
762
+ )
763
+ return result
764
+
765
+ def connect(self) -> None:
766
+ """Accept this client's connection."""
767
+ _capi.libeis.client_connect(self)
768
+
769
+ def disconnect(self) -> None:
770
+ """Disconnect this client."""
771
+ _capi.libeis.client_disconnect(self)
772
+
773
+ def new_seat(self, name: str) -> Seat:
774
+ """Create a seat to offer this client. Configure it, then add()."""
775
+ pointer = _capi.libeis.client_new_seat(self, name.encode("utf-8"))
776
+ if not pointer:
777
+ raise Error("eis_client_new_seat() returned NULL")
778
+ seat = Seat.adopt(pointer)
779
+ assert seat is not None
780
+ return seat
781
+
782
+ def new_ping(self) -> Ping:
783
+ """Create a round trip to this client. Requires libei 1.4.
784
+
785
+ Call :meth:`Ping.send` to start it; the reply is a PONG event.
786
+ """
787
+ pointer = _capi.libeis.client_new_ping(self)
788
+ if not pointer:
789
+ raise Error("eis_client_new_ping() returned NULL")
790
+ ping = Ping.adopt(pointer)
791
+ assert ping is not None
792
+ return ping
793
+
794
+
795
+ class Ping(CObject):
796
+ """A round trip to a client, answered by a PONG event.
797
+
798
+ Create one with :meth:`Client.new_ping`, call :meth:`send`, then watch
799
+ for :attr:`EventType.PONG` and compare :attr:`Event.pong` against this
800
+ object (or its :attr:`id`). Requires libei 1.4.
801
+ """
802
+
803
+ _ref_func = staticmethod(_capi.libeis.ping_ref)
804
+ _unref_func = staticmethod(_capi.libeis.ping_unref)
805
+
806
+ def __repr__(self) -> str:
807
+ return f"<Ping {self.id}>"
808
+
809
+ @property
810
+ def id(self) -> int:
811
+ """The identifier libeis assigned to this round trip."""
812
+ return _capi.libeis.ping_get_id(self)
813
+
814
+ def send(self) -> Ping:
815
+ """Start the round trip. The reply arrives as a PONG event."""
816
+ _capi.libeis.ping(self)
817
+ return self
818
+
819
+
820
+ class Event(CObject):
821
+ """One event from :attr:`Eis.events`.
822
+
823
+ :attr:`event_type` says which of the typed accessors below is valid;
824
+ reading the wrong one raises :class:`TypeError` rather than returning
825
+ the zeroes libeis would hand back.
826
+
827
+ Valid only for the loop iteration that yielded it -- :attr:`Eis.events`
828
+ releases each event as it resumes.
829
+ """
830
+
831
+ _unref_func = staticmethod(_capi.libeis.event_unref)
832
+
833
+ def __repr__(self) -> str:
834
+ event_type = self.event_type
835
+ label = event_type.name if isinstance(event_type, EventType) else event_type
836
+ return f"<Event {label}>"
837
+
838
+ @property
839
+ def event_type(self) -> EventType | int:
840
+ """The event's type, or a raw int for a value newer than this
841
+ package's :class:`EventType` table -- see its docstring."""
842
+ raw = _capi.libeis.event_get_type(self)
843
+ try:
844
+ return EventType(raw)
845
+ except ValueError:
846
+ return raw
847
+
848
+ @property
849
+ def time(self) -> int:
850
+ """Event timestamp in microseconds, in the context's clock domain."""
851
+ return _capi.libeis.event_get_time(self)
852
+
853
+ @property
854
+ def client(self) -> Client:
855
+ """The client this object belongs to."""
856
+ client = Client.wrap(_capi.libeis.event_get_client(self))
857
+ assert client is not None
858
+ return client
859
+
860
+ @property
861
+ def device(self) -> Device | None:
862
+ """The device this event concerns, or None if it has none."""
863
+ return Device.wrap(_capi.libeis.event_get_device(self))
864
+
865
+ @property
866
+ def seat(self) -> Seat | None:
867
+ """The seat this event concerns, or None if it has none.
868
+
869
+ Connect/disconnect events carry no seat.
870
+ """
871
+ return Seat.wrap(_capi.libeis.event_get_seat(self))
872
+
873
+ def _require(self, getter: str, *valid: EventType) -> None:
874
+ """Raise unless this event is one of ``valid``.
875
+
876
+ libeis's accessors do not report a type mismatch to the caller:
877
+ reading ``key_event`` off a POINTER_MOTION event returns
878
+ ``KeyEvent(key=0, is_press=False)``, logging an internal "Bug:"
879
+ line for some accessors and nothing at all for others. Checking
880
+ first turns a plausible-looking zero into an immediate error.
881
+ """
882
+ actual = self.event_type
883
+ if actual in valid:
884
+ return
885
+ wanted = " or ".join(v.name for v in valid)
886
+ seen = actual.name if isinstance(actual, EventType) else str(actual)
887
+ raise TypeError(f"Event.{getter} is only valid for {wanted} events, not {seen}")
888
+
889
+ @property
890
+ def seat_capabilities(self) -> tuple[DeviceCapability, ...]:
891
+ """Capabilities the client requested, for a SEAT_BIND event."""
892
+ self._require("seat_capabilities", EventType.SEAT_BIND)
893
+ return tuple(
894
+ c
895
+ for c in DeviceCapability
896
+ if _capi.libeis.event_seat_has_capability(self, c)
897
+ )
898
+
899
+ @property
900
+ def emulating_sequence(self) -> int:
901
+ """Sequence number of the start_emulating transaction."""
902
+ self._require("emulating_sequence", EventType.DEVICE_START_EMULATING)
903
+ return _capi.libeis.event_emulating_get_sequence(self)
904
+
905
+ @property
906
+ def key_event(self) -> KeyEvent:
907
+ """Key code and press/release state for a KEYBOARD_KEY event."""
908
+ self._require("key_event", EventType.KEYBOARD_KEY)
909
+ return KeyEvent(
910
+ key=_capi.libeis.event_keyboard_get_key(self),
911
+ is_press=bool(_capi.libeis.event_keyboard_get_key_is_press(self)),
912
+ )
913
+
914
+ @property
915
+ def button_event(self) -> ButtonEvent:
916
+ """Button code and press/release state for a BUTTON_BUTTON event."""
917
+ self._require("button_event", EventType.BUTTON_BUTTON)
918
+ return ButtonEvent(
919
+ button=_capi.libeis.event_button_get_button(self),
920
+ is_press=bool(_capi.libeis.event_button_get_is_press(self)),
921
+ )
922
+
923
+ @property
924
+ def pointer_event(self) -> PointerEvent:
925
+ """Relative motion deltas for a POINTER_MOTION event."""
926
+ self._require("pointer_event", EventType.POINTER_MOTION)
927
+ return PointerEvent(
928
+ dx=_capi.libeis.event_pointer_get_dx(self),
929
+ dy=_capi.libeis.event_pointer_get_dy(self),
930
+ )
931
+
932
+ @property
933
+ def pointer_absolute_event(self) -> PointerAbsoluteEvent:
934
+ """Absolute position for a POINTER_MOTION_ABSOLUTE event."""
935
+ self._require("pointer_absolute_event", EventType.POINTER_MOTION_ABSOLUTE)
936
+ return PointerAbsoluteEvent(
937
+ x=_capi.libeis.event_pointer_get_absolute_x(self),
938
+ y=_capi.libeis.event_pointer_get_absolute_y(self),
939
+ )
940
+
941
+ @property
942
+ def scroll_event(self) -> ScrollEvent:
943
+ """Smooth scroll deltas for a SCROLL_DELTA event."""
944
+ self._require("scroll_event", EventType.SCROLL_DELTA)
945
+ return ScrollEvent(
946
+ dx=_capi.libeis.event_scroll_get_dx(self),
947
+ dy=_capi.libeis.event_scroll_get_dy(self),
948
+ )
949
+
950
+ @property
951
+ def scroll_discrete_event(self) -> ScrollDiscreteEvent:
952
+ """Detent deltas for a SCROLL_DISCRETE event (120 per detent)."""
953
+ self._require("scroll_discrete_event", EventType.SCROLL_DISCRETE)
954
+ return ScrollDiscreteEvent(
955
+ dx=_capi.libeis.event_scroll_get_discrete_dx(self),
956
+ dy=_capi.libeis.event_scroll_get_discrete_dy(self),
957
+ )
958
+
959
+ @property
960
+ def scroll_stop_event(self) -> ScrollStopEvent:
961
+ """Which axes stopped, for a SCROLL_STOP/SCROLL_CANCEL event.
962
+
963
+ libeis's header documents these accessors for SCROLL_CANCEL only,
964
+ but both event types are accepted -- confirmed by round-tripping
965
+ each through a real client, with no internal "Bug:" log.
966
+ """
967
+ self._require(
968
+ "scroll_stop_event", EventType.SCROLL_STOP, EventType.SCROLL_CANCEL
969
+ )
970
+ return ScrollStopEvent(
971
+ stop_x=bool(_capi.libeis.event_scroll_get_stop_x(self)),
972
+ stop_y=bool(_capi.libeis.event_scroll_get_stop_y(self)),
973
+ )
974
+
975
+ @property
976
+ def touch_event(self) -> TouchEvent:
977
+ """Touch id and position for a TOUCH_DOWN or TOUCH_MOTION event.
978
+
979
+ Not TOUCH_UP: that event carries no position, so it has its own
980
+ accessor, :attr:`touch_up_event`.
981
+ """
982
+ self._require("touch_event", EventType.TOUCH_DOWN, EventType.TOUCH_MOTION)
983
+ return TouchEvent(
984
+ touchid=_capi.libeis.event_touch_get_id(self),
985
+ x=_capi.libeis.event_touch_get_x(self),
986
+ y=_capi.libeis.event_touch_get_y(self),
987
+ )
988
+
989
+ @property
990
+ def touch_up_event(self) -> TouchUpEvent:
991
+ """Touch id and cancellation flag for a TOUCH_UP event.
992
+
993
+ ``is_cancel`` distinguishes a cancelled touch from a logically
994
+ released one. It is False on libei older than 1.4, and against a
995
+ client older than ``ei_touchscreen`` version 2. The touch id is
996
+ available everywhere.
997
+ """
998
+ self._require("touch_up_event", EventType.TOUCH_UP)
999
+ try:
1000
+ is_cancel = bool(_capi.libeis.event_touch_get_is_cancel(self))
1001
+ except LibraryNotFoundError:
1002
+ # eis_event_touch_get_is_cancel() arrived in libei 1.4.
1003
+ # An older library has no way to express cancellation, so every
1004
+ # TOUCH_UP it reports really is a plain release: False is the
1005
+ # accurate answer, not a failure. Without this the whole
1006
+ # accessor would raise on a 1.0-1.3 install, taking the touch
1007
+ # id -- which those versions do provide -- down with it.
1008
+ is_cancel = False
1009
+ return TouchUpEvent(
1010
+ touchid=_capi.libeis.event_touch_get_id(self),
1011
+ is_cancel=is_cancel,
1012
+ )
1013
+
1014
+ @property
1015
+ def text_utf8_event(self) -> TextUtf8Event:
1016
+ """The text carried by a TEXT_UTF8 event. Requires libei 1.6."""
1017
+ self._require("text_utf8_event", EventType.TEXT_UTF8)
1018
+ raw = _capi.libeis.event_text_get_utf8(self)
1019
+ return TextUtf8Event(text="" if raw is None else raw.decode("utf-8"))
1020
+
1021
+ @property
1022
+ def text_keysym_event(self) -> TextKeysymEvent:
1023
+ """Keysym and press state for a TEXT_KEYSYM event. Requires libei 1.6."""
1024
+ self._require("text_keysym_event", EventType.TEXT_KEYSYM)
1025
+ return TextKeysymEvent(
1026
+ keysym=_capi.libeis.event_text_get_keysym(self),
1027
+ is_press=bool(_capi.libeis.event_text_get_keysym_is_press(self)),
1028
+ )
1029
+
1030
+ @property
1031
+ def pong(self) -> Ping:
1032
+ """The :class:`Ping` this PONG event answers. Requires libei 1.4."""
1033
+ self._require("pong", EventType.PONG)
1034
+ # Borrowed: the event owns this reference, so wrap() (which takes
1035
+ # its own ref) rather than adopt().
1036
+ ping = Ping.wrap(_capi.libeis.event_pong_get_ping(self))
1037
+ if ping is None:
1038
+ raise Error("eis_event_pong_get_ping() returned NULL for a PONG event")
1039
+ return ping
1040
+
1041
+
1042
+ def _log_callback(_eis: int, priority: int, message: bytes, _context: int) -> None:
1043
+ # See ei.py's _log_callback: look up the raw int, not
1044
+ # _LogPriority(priority), which would raise ValueError before .get()'s
1045
+ # default could apply -- silently, since this runs inside a ctypes
1046
+ # callback.
1047
+ level = {
1048
+ _LogPriority.DEBUG.value: logging.DEBUG,
1049
+ _LogPriority.INFO.value: logging.INFO,
1050
+ _LogPriority.WARNING.value: logging.WARNING,
1051
+ _LogPriority.ERROR.value: logging.ERROR,
1052
+ }.get(priority, logging.DEBUG)
1053
+ logger.log(level, message.decode("utf-8", errors="replace"))
1054
+
1055
+
1056
+ _log_handler = log_handler_t(_log_callback)
1057
+
1058
+
1059
+ class Eis(CObject):
1060
+ """An EIS server context, accepting one or more client connections."""
1061
+
1062
+ _unref_func = staticmethod(_capi.libeis.unref)
1063
+ # Only ever created fresh via _new() inside create_for_fd(), never
1064
+ # handed out as a sub-object -- so wrap()/adopt() on this class have no
1065
+ # legitimate caller. Blocking them stops a garbage pointer from ever
1066
+ # reaching __init__'s log_set_handler()/log_set_priority() calls below,
1067
+ # which would otherwise dereference it as a real `struct eis *` and
1068
+ # segfault.
1069
+ _wrappable = False
1070
+
1071
+ def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
1072
+ # _adopt is accepted and forwarded for signature consistency with
1073
+ # CObject, but with _wrappable = False, _get_or_create() never
1074
+ # actually reaches this constructor -- Eis is always built directly
1075
+ # via cls(cls._new()) in create_for_fd().
1076
+ super().__init__(pointer, _adopt=_adopt)
1077
+ _capi.libeis.log_set_handler(self, _log_handler)
1078
+ _capi.libeis.log_set_priority(self, _LogPriority.DEBUG)
1079
+
1080
+ @property
1081
+ def fd(self) -> int:
1082
+ """File descriptor to poll; readable when :meth:`dispatch` has work."""
1083
+ return _capi.libeis.get_fd(self)
1084
+
1085
+ @property
1086
+ def events(self) -> Iterator[Event]:
1087
+ """Drain currently-queued events.
1088
+
1089
+ Each event is released (unref'd) as soon as this generator resumes
1090
+ after yielding it -- see ``ei.Context.events`` for why that timing
1091
+ matters (a SYNC event's pong reply is sent precisely on unref, so
1092
+ leaving that to Python's own GC timing can silently stall a
1093
+ caller).
1094
+ """
1095
+ while True:
1096
+ pointer = _capi.libeis.get_event(self)
1097
+ if not pointer:
1098
+ break
1099
+ event = Event.wrap(pointer)
1100
+ assert event is not None
1101
+ # See ei.Context.events: try/finally so release() still runs
1102
+ # if the caller breaks out of the loop (GeneratorExit at the
1103
+ # yield would otherwise skip a bare call placed after it).
1104
+ try:
1105
+ yield event
1106
+ finally:
1107
+ event.release()
1108
+
1109
+ @property
1110
+ def now(self) -> int:
1111
+ """The context's current time, in microseconds."""
1112
+ return _capi.libeis.now(self)
1113
+
1114
+ def set_flag(self, flag: Flag) -> None:
1115
+ """Change this context's protocol behavior. Requires libei 1.6.
1116
+
1117
+ Must be called before the backend is set up, so in practice
1118
+ before :meth:`create_for_fd` / :meth:`create_for_socket` -- which
1119
+ also means this is only reachable on a context built by hand.
1120
+ Takes one flag, never a bitmask; call it again for another.
1121
+ """
1122
+ result = _capi.libeis.set_flag(self, flag)
1123
+ if result < 0:
1124
+ raise Error(f"eis_set_flag() failed with errno {-result}")
1125
+
1126
+ def peek_event_type(self) -> EventType | int | None:
1127
+ """Type of the next queued event, without consuming it.
1128
+
1129
+ ``None`` when the queue is empty. See
1130
+ :meth:`libei.ei.Context.peek_event_type` for why only the type is
1131
+ returned and never the event itself.
1132
+ """
1133
+ pointer = _capi.libeis.peek_event(self)
1134
+ if not pointer:
1135
+ return None
1136
+ try:
1137
+ raw = _capi.libeis.event_get_type(pointer)
1138
+ finally:
1139
+ _capi.libeis.event_unref(pointer)
1140
+ try:
1141
+ return EventType(raw)
1142
+ except ValueError:
1143
+ return raw
1144
+
1145
+ def dispatch(self) -> None:
1146
+ """Read from the connection and queue any events that arrive.
1147
+
1148
+ Call this before iterating :attr:`events`, which only drains what
1149
+ is already queued."""
1150
+ _capi.libeis.dispatch(self)
1151
+
1152
+ def add_client(self) -> int:
1153
+ """Mint a new, private fd for one client connection.
1154
+
1155
+ Hand the returned fd to a client's
1156
+ :meth:`libei.ei.Sender.create_for_fd` or
1157
+ :meth:`libei.ei.Receiver.create_for_fd` -- e.g. across an
1158
+ ``os.pipe()``/subprocess boundary, or directly in-process for a
1159
+ test. Only valid on a server created with :meth:`create_for_fd`.
1160
+ """
1161
+ fd = _capi.libeis.backend_fd_add_client(self)
1162
+ if fd < 0:
1163
+ raise Error(os.strerror(-fd), -fd)
1164
+ return fd
1165
+
1166
+ @classmethod
1167
+ def _new(cls) -> int:
1168
+ pointer = _capi.libeis.new(c_void_p(None))
1169
+ if not pointer:
1170
+ raise Error("eis_new() returned NULL")
1171
+ return pointer
1172
+
1173
+ @classmethod
1174
+ def create_for_fd(cls, flags: Sequence[Flag] = ()) -> Eis:
1175
+ """Create a server using the fd backend -- the one real compositors
1176
+ use, since it keeps each client's fd private rather than exposing a
1177
+ connectable socket path. Call :meth:`add_client` once per
1178
+ connection you want to accept.
1179
+
1180
+ ``flags`` are applied here rather than left to the caller because
1181
+ :meth:`set_flag` has to run before the backend is set up, and this
1182
+ method does both."""
1183
+ server = cls(cls._new())
1184
+ for flag in flags:
1185
+ server.set_flag(flag)
1186
+ err = _capi.libeis.setup_backend_fd(server)
1187
+ if err < 0:
1188
+ raise Error(os.strerror(-err), -err)
1189
+ return server
1190
+
1191
+ @classmethod
1192
+ def create_for_socket(cls, path: Path, flags: Sequence[Flag] = ()) -> Eis:
1193
+ """Create a server listening on a Unix socket, as a compositor
1194
+ would (this is the path a real ``ei_setup_backend_socket()`` client
1195
+ connects to). See :meth:`create_for_fd` on ``flags``."""
1196
+ server = cls(cls._new())
1197
+ for flag in flags:
1198
+ server.set_flag(flag)
1199
+ err = _capi.libeis.setup_backend_socket(server, os.fspath(path).encode("utf-8"))
1200
+ if err < 0:
1201
+ raise Error(os.strerror(-err), -err)
1202
+ return server
1203
+
1204
+
1205
+ __all__ = [
1206
+ "ButtonEvent",
1207
+ "Client",
1208
+ "ConfigureRegion",
1209
+ "Device",
1210
+ "DeviceCapability",
1211
+ "DeviceType",
1212
+ "Eis",
1213
+ "Error",
1214
+ "Event",
1215
+ "EventType",
1216
+ "Flag",
1217
+ "KeyEvent",
1218
+ "Keymap",
1219
+ "KeymapType",
1220
+ "Ping",
1221
+ "PointerAbsoluteEvent",
1222
+ "PointerEvent",
1223
+ "Region",
1224
+ "ScrollDiscreteEvent",
1225
+ "ScrollEvent",
1226
+ "ScrollStopEvent",
1227
+ "Seat",
1228
+ "TextKeysymEvent",
1229
+ "TextUtf8Event",
1230
+ "Touch",
1231
+ "TouchEvent",
1232
+ "TouchUpEvent",
1233
+ "is_available",
1234
+ ]