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/__init__.py +31 -0
- libei/_capi/__init__.py +6 -0
- libei/_capi/libei.py +247 -0
- libei/_capi/libeis.py +285 -0
- libei/_capi/liboeffis.py +29 -0
- libei/_capi/loader.py +112 -0
- libei/_cobject.py +252 -0
- libei/ei.py +1244 -0
- libei/eis.py +1234 -0
- libei/oeffis.py +238 -0
- libei/py.typed +0 -0
- python_libei-0.1.0.dist-info/METADATA +770 -0
- python_libei-0.1.0.dist-info/RECORD +16 -0
- python_libei-0.1.0.dist-info/WHEEL +5 -0
- python_libei-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_libei-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
]
|