python-libei 0.5.1__py3-none-any.whl → 0.6.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 +1 -1
- libei/_capi/__init__.py +5 -2
- libei/_capi/loader.py +7 -0
- libei/_cobject.py +6 -0
- libei/ei.py +90 -15
- libei/eis.py +82 -17
- libei/oeffis.py +19 -3
- libei/portal.py +127 -33
- {python_libei-0.5.1.dist-info → python_libei-0.6.0.dist-info}/METADATA +33 -36
- python_libei-0.6.0.dist-info/RECORD +17 -0
- python_libei-0.5.1.dist-info/RECORD +0 -17
- {python_libei-0.5.1.dist-info → python_libei-0.6.0.dist-info}/WHEEL +0 -0
- {python_libei-0.5.1.dist-info → python_libei-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {python_libei-0.5.1.dist-info → python_libei-0.6.0.dist-info}/top_level.txt +0 -0
libei/__init__.py
CHANGED
libei/_capi/__init__.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
"""Low-level ctypes bindings.
|
|
2
|
-
|
|
1
|
+
"""Low-level ctypes bindings.
|
|
2
|
+
|
|
3
|
+
Not part of the public API -- use ``libei.ei``, ``libei.eis`` and
|
|
4
|
+
``libei.oeffis`` instead.
|
|
5
|
+
"""
|
|
3
6
|
|
|
4
7
|
from . import libei, libeis, liboeffis
|
|
5
8
|
|
libei/_capi/loader.py
CHANGED
|
@@ -25,12 +25,18 @@ class LazyLibrary:
|
|
|
25
25
|
"""A ctypes.CDLL that only opens the library on first real use."""
|
|
26
26
|
|
|
27
27
|
def __init__(self, soname: str) -> None:
|
|
28
|
+
"""Remember the soname; nothing is opened until first use."""
|
|
28
29
|
self._soname = soname
|
|
29
30
|
self._lib: ctypes.CDLL | None = None
|
|
30
31
|
self._load_error: OSError | None = None
|
|
31
32
|
self._lock = threading.Lock()
|
|
32
33
|
|
|
33
34
|
def _ensure_loaded(self) -> ctypes.CDLL:
|
|
35
|
+
"""Return the opened library, opening it on first call.
|
|
36
|
+
|
|
37
|
+
Raises LibraryNotFoundError on every later call too after a failure:
|
|
38
|
+
the failed load is cached rather than retried.
|
|
39
|
+
"""
|
|
34
40
|
# Double-checked: the unlocked read is the fast path taken by every
|
|
35
41
|
# call after the first, and the repeated check inside the lock is
|
|
36
42
|
# what makes it safe -- two threads can both fall through the first
|
|
@@ -91,6 +97,7 @@ class LazyLibrary:
|
|
|
91
97
|
cache: dict[str, Any] = {}
|
|
92
98
|
|
|
93
99
|
def call(*args: Any) -> Any:
|
|
100
|
+
"""Resolve the C function on first call, then pass straight through."""
|
|
94
101
|
# Resolution happens here, on first call, not at bind time --
|
|
95
102
|
# that is the whole point of this module (see its docstring).
|
|
96
103
|
bound = cache.get("f")
|
libei/_cobject.py
CHANGED
|
@@ -66,6 +66,7 @@ class CObject:
|
|
|
66
66
|
_instances_lock: ClassVar[threading.RLock]
|
|
67
67
|
|
|
68
68
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
69
|
+
"""Give each wrapper hierarchy one identity cache, shared by subclasses."""
|
|
69
70
|
super().__init_subclass__(**kwargs)
|
|
70
71
|
# One cache per wrapper *hierarchy*, not per class. Only a root
|
|
71
72
|
# wrapper class -- one whose only CObject ancestor is CObject
|
|
@@ -98,6 +99,7 @@ class CObject:
|
|
|
98
99
|
cls._instances_lock = threading.RLock()
|
|
99
100
|
|
|
100
101
|
def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
|
|
102
|
+
"""Take a reference on a non-NULL pointer and cache this wrapper for it."""
|
|
101
103
|
if not pointer:
|
|
102
104
|
raise ValueError(f"{type(self).__name__} cannot wrap a NULL pointer")
|
|
103
105
|
self._pointer = pointer
|
|
@@ -121,6 +123,7 @@ class CObject:
|
|
|
121
123
|
|
|
122
124
|
@property
|
|
123
125
|
def _as_parameter_(self) -> int:
|
|
126
|
+
"""The raw pointer ctypes passes to C -- an error once it is released."""
|
|
124
127
|
if self._pointer == 0:
|
|
125
128
|
raise RuntimeError(
|
|
126
129
|
f"{type(self).__name__} has already been released; "
|
|
@@ -159,6 +162,7 @@ class CObject:
|
|
|
159
162
|
|
|
160
163
|
@classmethod
|
|
161
164
|
def _get_or_create(cls: type[T], pointer: int | None, *, adopt: bool) -> T | None:
|
|
165
|
+
"""The wrapper already caching this pointer, or a new one; None for NULL."""
|
|
162
166
|
if not pointer:
|
|
163
167
|
return None
|
|
164
168
|
if not cls._wrappable:
|
|
@@ -232,6 +236,7 @@ class CObject:
|
|
|
232
236
|
return cls._get_or_create(pointer, adopt=True)
|
|
233
237
|
|
|
234
238
|
def __eq__(self, other: object) -> bool:
|
|
239
|
+
"""Equal when the types match and the wrapped pointers are the same."""
|
|
235
240
|
if not isinstance(other, CObject):
|
|
236
241
|
return NotImplemented
|
|
237
242
|
if type(self) is not type(other):
|
|
@@ -245,6 +250,7 @@ class CObject:
|
|
|
245
250
|
return self._pointer == other._pointer
|
|
246
251
|
|
|
247
252
|
def __hash__(self) -> int:
|
|
253
|
+
"""Stable for the object's life: the original pointer, not the live one."""
|
|
248
254
|
# Deliberately keyed on _hash_key, not _pointer: release() zeroes
|
|
249
255
|
# _pointer, and an object whose hash changes mid-life vanishes
|
|
250
256
|
# from any set or dict it was placed in. Two wrappers can share a
|
libei/ei.py
CHANGED
|
@@ -67,6 +67,7 @@ _emulating_sequence = itertools.count(1)
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def _next_emulating_sequence() -> int:
|
|
70
|
+
"""The next emulating-event sequence number: 32-bit, and never 0."""
|
|
70
71
|
# Masked into uint32 to match the C parameter. libei asks callers to
|
|
71
72
|
# keep wraparound detection "reasonable"; skipping 0 keeps the value
|
|
72
73
|
# away from anything that might read as unset.
|
|
@@ -87,6 +88,7 @@ class Error(Exception):
|
|
|
87
88
|
"""
|
|
88
89
|
|
|
89
90
|
def __init__(self, message: str, errno: int | None = None) -> None:
|
|
91
|
+
"""Record the failure message and, where libei reported one, errno."""
|
|
90
92
|
super().__init__(message)
|
|
91
93
|
self.message = message
|
|
92
94
|
self.errno = errno
|
|
@@ -191,6 +193,8 @@ class KeymapType(enum.IntEnum):
|
|
|
191
193
|
|
|
192
194
|
|
|
193
195
|
class _LogPriority(enum.IntEnum):
|
|
196
|
+
"""The log levels, as the integers the C library passes to its handler."""
|
|
197
|
+
|
|
194
198
|
DEBUG = 10
|
|
195
199
|
INFO = 20
|
|
196
200
|
WARNING = 30
|
|
@@ -199,6 +203,12 @@ class _LogPriority(enum.IntEnum):
|
|
|
199
203
|
|
|
200
204
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
201
205
|
class XkbModifiersEvent:
|
|
206
|
+
"""XKB modifier state from a KEYBOARD_MODIFIERS event.
|
|
207
|
+
|
|
208
|
+
``depressed``/``latched``/``locked`` are XKB's own mod-state bitmasks;
|
|
209
|
+
``group`` is the active keyboard layout group.
|
|
210
|
+
"""
|
|
211
|
+
|
|
202
212
|
depressed: int
|
|
203
213
|
latched: int
|
|
204
214
|
locked: int
|
|
@@ -207,48 +217,76 @@ class XkbModifiersEvent:
|
|
|
207
217
|
|
|
208
218
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
209
219
|
class KeyEvent:
|
|
220
|
+
"""Key code and press state from a KEYBOARD_KEY event.
|
|
221
|
+
|
|
222
|
+
``key`` is a Linux ``KEY_*`` code, the same numbering
|
|
223
|
+
:meth:`Device.keyboard_key` sends.
|
|
224
|
+
"""
|
|
225
|
+
|
|
210
226
|
key: int
|
|
211
227
|
is_press: bool
|
|
212
228
|
|
|
213
229
|
|
|
214
230
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
215
231
|
class ButtonEvent:
|
|
232
|
+
"""Button code and press state from a BUTTON_BUTTON event.
|
|
233
|
+
|
|
234
|
+
``button`` is a Linux ``BTN_*`` code, the same numbering
|
|
235
|
+
:meth:`Device.button` sends.
|
|
236
|
+
"""
|
|
237
|
+
|
|
216
238
|
button: int
|
|
217
239
|
is_press: bool
|
|
218
240
|
|
|
219
241
|
|
|
220
242
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
221
243
|
class PointerEvent:
|
|
244
|
+
"""Relative motion deltas, in logical pixels, from a POINTER_MOTION event."""
|
|
245
|
+
|
|
222
246
|
dx: float
|
|
223
247
|
dy: float
|
|
224
248
|
|
|
225
249
|
|
|
226
250
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
227
251
|
class PointerAbsoluteEvent:
|
|
252
|
+
"""Absolute position from a POINTER_MOTION_ABSOLUTE event.
|
|
253
|
+
|
|
254
|
+
In the logical pixel space of the :class:`Region` the emitting device
|
|
255
|
+
covers -- see :meth:`Event.pointer_absolute_event`.
|
|
256
|
+
"""
|
|
257
|
+
|
|
228
258
|
x: float
|
|
229
259
|
y: float
|
|
230
260
|
|
|
231
261
|
|
|
232
262
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
233
263
|
class ScrollEvent:
|
|
264
|
+
"""Smooth scroll deltas from a SCROLL_DELTA event."""
|
|
265
|
+
|
|
234
266
|
dx: float
|
|
235
267
|
dy: float
|
|
236
268
|
|
|
237
269
|
|
|
238
270
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
239
271
|
class ScrollDiscreteEvent:
|
|
272
|
+
"""Detent scroll deltas (120 per detent) from a SCROLL_DISCRETE event."""
|
|
273
|
+
|
|
240
274
|
dx: int
|
|
241
275
|
dy: int
|
|
242
276
|
|
|
243
277
|
|
|
244
278
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
245
279
|
class ScrollStopEvent:
|
|
280
|
+
"""Which axes stopped scrolling, from a SCROLL_STOP/SCROLL_CANCEL event."""
|
|
281
|
+
|
|
246
282
|
stop_x: bool
|
|
247
283
|
stop_y: bool
|
|
248
284
|
|
|
249
285
|
|
|
250
286
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
251
287
|
class TouchEvent:
|
|
288
|
+
"""Touch id and position from a TOUCH_DOWN or TOUCH_MOTION event."""
|
|
289
|
+
|
|
252
290
|
touchid: int
|
|
253
291
|
x: float
|
|
254
292
|
y: float
|
|
@@ -256,17 +294,26 @@ class TouchEvent:
|
|
|
256
294
|
|
|
257
295
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
258
296
|
class TouchUpEvent:
|
|
297
|
+
"""Touch id and cancellation flag from a TOUCH_UP event.
|
|
298
|
+
|
|
299
|
+
See :meth:`Event.touch_up_event` for when ``is_cancel`` is trustworthy.
|
|
300
|
+
"""
|
|
301
|
+
|
|
259
302
|
touchid: int
|
|
260
303
|
is_cancel: bool
|
|
261
304
|
|
|
262
305
|
|
|
263
306
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
264
307
|
class TextUtf8Event:
|
|
308
|
+
"""UTF-8 text carried by a TEXT_UTF8 event."""
|
|
309
|
+
|
|
265
310
|
text: str
|
|
266
311
|
|
|
267
312
|
|
|
268
313
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
269
314
|
class TextKeysymEvent:
|
|
315
|
+
"""Keysym and press state from a TEXT_KEYSYM event."""
|
|
316
|
+
|
|
270
317
|
keysym: int
|
|
271
318
|
is_press: bool
|
|
272
319
|
|
|
@@ -285,6 +332,7 @@ class Region(CObject):
|
|
|
285
332
|
_unref_func = staticmethod(_capi.libei.region_unref)
|
|
286
333
|
|
|
287
334
|
def __repr__(self) -> str:
|
|
335
|
+
"""The size and position, as WxH+X+Y."""
|
|
288
336
|
w, h = self.dimension
|
|
289
337
|
x, y = self.position
|
|
290
338
|
return f"<Region {w}x{h}+{x}+{y}>"
|
|
@@ -466,6 +514,7 @@ class Device(CObject):
|
|
|
466
514
|
_unref_func = staticmethod(_capi.libei.device_unref)
|
|
467
515
|
|
|
468
516
|
def __repr__(self) -> str:
|
|
517
|
+
"""Name, device type and capabilities -- what tells two devices apart."""
|
|
469
518
|
caps = "|".join(c.name or str(c.value) for c in self.capabilities)
|
|
470
519
|
return f"<Device {self.name!r} {self.device_type.name} {caps}>"
|
|
471
520
|
|
|
@@ -545,9 +594,10 @@ class Device(CObject):
|
|
|
545
594
|
return self
|
|
546
595
|
|
|
547
596
|
def frame(self, timestamp: int | None = None) -> Device:
|
|
548
|
-
"""Commit the events queued since the last frame as one logical
|
|
549
|
-
|
|
550
|
-
time.
|
|
597
|
+
"""Commit the events queued since the last frame as one logical hardware event.
|
|
598
|
+
|
|
599
|
+
``timestamp`` defaults to the context's current time.
|
|
600
|
+
"""
|
|
551
601
|
if timestamp is None:
|
|
552
602
|
timestamp = _capi.libei.now(_capi.libei.device_get_context(self))
|
|
553
603
|
_capi.libei.device_frame(self, timestamp)
|
|
@@ -564,8 +614,10 @@ class Device(CObject):
|
|
|
564
614
|
return self
|
|
565
615
|
|
|
566
616
|
def button(self, button: int, is_press: bool) -> Device:
|
|
567
|
-
"""Queue a button press or release.
|
|
568
|
-
|
|
617
|
+
"""Queue a button press or release.
|
|
618
|
+
|
|
619
|
+
``button`` is a Linux ``BTN_*`` code (e.g. ``0x110`` for ``BTN_LEFT``).
|
|
620
|
+
"""
|
|
569
621
|
_capi.libei.device_button_button(self, button, is_press)
|
|
570
622
|
return self
|
|
571
623
|
|
|
@@ -645,6 +697,7 @@ class Seat(CObject):
|
|
|
645
697
|
_unref_func = staticmethod(_capi.libei.seat_unref)
|
|
646
698
|
|
|
647
699
|
def __repr__(self) -> str:
|
|
700
|
+
"""The seat's name and capabilities."""
|
|
648
701
|
caps = "|".join(c.name or str(c.value) for c in self.capabilities)
|
|
649
702
|
return f"<Seat {self.name!r} {caps}>"
|
|
650
703
|
|
|
@@ -727,6 +780,7 @@ class Ping(CObject):
|
|
|
727
780
|
_unref_func = staticmethod(_capi.libei.ping_unref)
|
|
728
781
|
|
|
729
782
|
def __repr__(self) -> str:
|
|
783
|
+
"""The ping's id, which is all a ping carries."""
|
|
730
784
|
return f"<Ping {self.id}>"
|
|
731
785
|
|
|
732
786
|
@property
|
|
@@ -754,14 +808,18 @@ class Event(CObject):
|
|
|
754
808
|
_unref_func = staticmethod(_capi.libei.event_unref)
|
|
755
809
|
|
|
756
810
|
def __repr__(self) -> str:
|
|
811
|
+
"""The event type by name, or the raw value for one we do not model."""
|
|
757
812
|
event_type = self.event_type
|
|
758
813
|
label = event_type.name if isinstance(event_type, EventType) else event_type
|
|
759
814
|
return f"<Event {label}>"
|
|
760
815
|
|
|
761
816
|
@property
|
|
762
817
|
def event_type(self) -> EventType | int:
|
|
763
|
-
"""The event's type
|
|
764
|
-
|
|
818
|
+
"""The event's type.
|
|
819
|
+
|
|
820
|
+
Returns a raw int for a value newer than this package's
|
|
821
|
+
:class:`EventType` table -- see its docstring.
|
|
822
|
+
"""
|
|
765
823
|
raw = _capi.libei.event_get_type(self)
|
|
766
824
|
try:
|
|
767
825
|
return EventType(raw)
|
|
@@ -958,6 +1016,11 @@ class Event(CObject):
|
|
|
958
1016
|
|
|
959
1017
|
|
|
960
1018
|
def _log_callback(_ei: int, priority: int, message: bytes, _context: int) -> None:
|
|
1019
|
+
"""Forward libei's log lines into the logging module.
|
|
1020
|
+
|
|
1021
|
+
Runs inside a ctypes callback, where an exception is printed to stderr
|
|
1022
|
+
and then dropped, so every lookup below falls back rather than raising.
|
|
1023
|
+
"""
|
|
961
1024
|
# Look up the raw int, not _LogPriority(priority): constructing the
|
|
962
1025
|
# enum from an unrecognized value raises ValueError immediately, which
|
|
963
1026
|
# would happen *before* .get()'s default ever gets a chance to apply
|
|
@@ -1009,11 +1072,14 @@ class Context(CObject):
|
|
|
1009
1072
|
_wrappable = False
|
|
1010
1073
|
|
|
1011
1074
|
def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1075
|
+
"""Wrap a freshly created ``struct ei *`` and arm its log handler.
|
|
1076
|
+
|
|
1077
|
+
_adopt is accepted and forwarded for signature consistency with
|
|
1078
|
+
CObject, but with _wrappable = False, _get_or_create() never
|
|
1079
|
+
actually reaches this constructor -- Context (and Sender/
|
|
1080
|
+
Receiver) are always built directly via cls(cls._new()) in
|
|
1081
|
+
create_for_fd()/create_for_socket().
|
|
1082
|
+
"""
|
|
1017
1083
|
super().__init__(pointer, _adopt=_adopt)
|
|
1018
1084
|
self._name: str | None = None
|
|
1019
1085
|
_capi.libei.log_set_handler(self, _log_handler)
|
|
@@ -1081,7 +1147,8 @@ class Context(CObject):
|
|
|
1081
1147
|
"""Use an already-connected socket as the transport.
|
|
1082
1148
|
|
|
1083
1149
|
libei takes ownership of a raw int fd and closes it itself; a file
|
|
1084
|
-
object is duplicated first, so the caller's own object stays valid.
|
|
1150
|
+
object is duplicated first, so the caller's own object stays valid.
|
|
1151
|
+
"""
|
|
1085
1152
|
# ei_setup_backend_fd() takes ownership of the fd and will close it
|
|
1086
1153
|
# itself. A raw int is assumed to already be one the caller is
|
|
1087
1154
|
# handing off (matching what eis.Eis.add_client()/oeffis.eis_fd
|
|
@@ -1099,7 +1166,8 @@ class Context(CObject):
|
|
|
1099
1166
|
"""Connect to an EIS socket by path.
|
|
1100
1167
|
|
|
1101
1168
|
``None`` uses ``$LIBEI_SOCKET``; a relative path is resolved
|
|
1102
|
-
against ``$XDG_RUNTIME_DIR``.
|
|
1169
|
+
against ``$XDG_RUNTIME_DIR``.
|
|
1170
|
+
"""
|
|
1103
1171
|
encoded = os.fspath(path).encode("utf-8") if path else None
|
|
1104
1172
|
err = _capi.libei.setup_backend_socket(self, encoded)
|
|
1105
1173
|
if err < 0:
|
|
@@ -1162,7 +1230,8 @@ class Context(CObject):
|
|
|
1162
1230
|
"""Read from the connection and queue any events that arrive.
|
|
1163
1231
|
|
|
1164
1232
|
Call this before iterating :attr:`events`, which only drains what
|
|
1165
|
-
is already queued.
|
|
1233
|
+
is already queued.
|
|
1234
|
+
"""
|
|
1166
1235
|
_capi.libei.dispatch(self)
|
|
1167
1236
|
|
|
1168
1237
|
|
|
@@ -1171,6 +1240,7 @@ class Sender(Context):
|
|
|
1171
1240
|
|
|
1172
1241
|
@classmethod
|
|
1173
1242
|
def _new(cls) -> int:
|
|
1243
|
+
"""A new sender from the C library, or an error if it returned NULL."""
|
|
1174
1244
|
pointer = _capi.libei.new_sender(c_void_p(None))
|
|
1175
1245
|
if not pointer:
|
|
1176
1246
|
raise Error("ei_new_sender() returned NULL")
|
|
@@ -1194,6 +1264,7 @@ class Receiver(Context):
|
|
|
1194
1264
|
|
|
1195
1265
|
@classmethod
|
|
1196
1266
|
def _new(cls) -> int:
|
|
1267
|
+
"""A new receiver from the C library, or an error if it returned NULL."""
|
|
1197
1268
|
pointer = _capi.libei.new_receiver(c_void_p(None))
|
|
1198
1269
|
if not pointer:
|
|
1199
1270
|
raise Error("ei_new_receiver() returned NULL")
|
|
@@ -1224,6 +1295,10 @@ __all__ = [
|
|
|
1224
1295
|
"KeyEvent",
|
|
1225
1296
|
"Keymap",
|
|
1226
1297
|
"KeymapType",
|
|
1298
|
+
# Imported rather than defined here: every call bound through
|
|
1299
|
+
# _capi.libei can raise it, so a caller importing from this module
|
|
1300
|
+
# catches it from here too -- see docs/troubleshooting.md.
|
|
1301
|
+
"LibraryNotFoundError",
|
|
1227
1302
|
"Ping",
|
|
1228
1303
|
"PointerAbsoluteEvent",
|
|
1229
1304
|
"PointerEvent",
|
libei/eis.py
CHANGED
|
@@ -55,6 +55,7 @@ class Error(Exception):
|
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
57
|
def __init__(self, message: str, errno: int | None = None) -> None:
|
|
58
|
+
"""Record the failure message and, where libeis reported one, errno."""
|
|
58
59
|
super().__init__(message)
|
|
59
60
|
self.message = message
|
|
60
61
|
self.errno = errno
|
|
@@ -158,6 +159,8 @@ class Flag(enum.IntEnum):
|
|
|
158
159
|
|
|
159
160
|
|
|
160
161
|
class _LogPriority(enum.IntEnum):
|
|
162
|
+
"""The log levels, as the integers libeis passes to its log handler."""
|
|
163
|
+
|
|
161
164
|
DEBUG = 10
|
|
162
165
|
INFO = 20
|
|
163
166
|
WARNING = 30
|
|
@@ -166,48 +169,75 @@ class _LogPriority(enum.IntEnum):
|
|
|
166
169
|
|
|
167
170
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
168
171
|
class KeyEvent:
|
|
172
|
+
"""Key code and press state received on a KEYBOARD_KEY event.
|
|
173
|
+
|
|
174
|
+
``key`` is a Linux ``KEY_*`` code, as sent by the client's
|
|
175
|
+
``ei.Device.keyboard_key``.
|
|
176
|
+
"""
|
|
177
|
+
|
|
169
178
|
key: int
|
|
170
179
|
is_press: bool
|
|
171
180
|
|
|
172
181
|
|
|
173
182
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
174
183
|
class ButtonEvent:
|
|
184
|
+
"""Button code and press state received on a BUTTON_BUTTON event.
|
|
185
|
+
|
|
186
|
+
``button`` is a Linux ``BTN_*`` code, as sent by the client's
|
|
187
|
+
``ei.Device.button``.
|
|
188
|
+
"""
|
|
189
|
+
|
|
175
190
|
button: int
|
|
176
191
|
is_press: bool
|
|
177
192
|
|
|
178
193
|
|
|
179
194
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
180
195
|
class PointerEvent:
|
|
196
|
+
"""Relative motion deltas, in logical pixels, from a POINTER_MOTION event."""
|
|
197
|
+
|
|
181
198
|
dx: float
|
|
182
199
|
dy: float
|
|
183
200
|
|
|
184
201
|
|
|
185
202
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
186
203
|
class PointerAbsoluteEvent:
|
|
204
|
+
"""Absolute position from a POINTER_MOTION_ABSOLUTE event.
|
|
205
|
+
|
|
206
|
+
In the logical pixel space of the region the sending device covers.
|
|
207
|
+
"""
|
|
208
|
+
|
|
187
209
|
x: float
|
|
188
210
|
y: float
|
|
189
211
|
|
|
190
212
|
|
|
191
213
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
192
214
|
class ScrollEvent:
|
|
215
|
+
"""Smooth scroll deltas from a SCROLL_DELTA event."""
|
|
216
|
+
|
|
193
217
|
dx: float
|
|
194
218
|
dy: float
|
|
195
219
|
|
|
196
220
|
|
|
197
221
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
198
222
|
class ScrollDiscreteEvent:
|
|
223
|
+
"""Detent scroll deltas (120 per detent) from a SCROLL_DISCRETE event."""
|
|
224
|
+
|
|
199
225
|
dx: int
|
|
200
226
|
dy: int
|
|
201
227
|
|
|
202
228
|
|
|
203
229
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
204
230
|
class ScrollStopEvent:
|
|
231
|
+
"""Which axes stopped scrolling, from a SCROLL_STOP/SCROLL_CANCEL event."""
|
|
232
|
+
|
|
205
233
|
stop_x: bool
|
|
206
234
|
stop_y: bool
|
|
207
235
|
|
|
208
236
|
|
|
209
237
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
210
238
|
class TouchEvent:
|
|
239
|
+
"""Touch id and position from a TOUCH_DOWN or TOUCH_MOTION event."""
|
|
240
|
+
|
|
211
241
|
touchid: int
|
|
212
242
|
x: float
|
|
213
243
|
y: float
|
|
@@ -215,17 +245,23 @@ class TouchEvent:
|
|
|
215
245
|
|
|
216
246
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
217
247
|
class TouchUpEvent:
|
|
248
|
+
"""Touch id and cancellation flag from a TOUCH_UP event."""
|
|
249
|
+
|
|
218
250
|
touchid: int
|
|
219
251
|
is_cancel: bool
|
|
220
252
|
|
|
221
253
|
|
|
222
254
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
223
255
|
class TextUtf8Event:
|
|
256
|
+
"""UTF-8 text carried by a TEXT_UTF8 event."""
|
|
257
|
+
|
|
224
258
|
text: str
|
|
225
259
|
|
|
226
260
|
|
|
227
261
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
228
262
|
class TextKeysymEvent:
|
|
263
|
+
"""Keysym and press state from a TEXT_KEYSYM event."""
|
|
264
|
+
|
|
229
265
|
keysym: int
|
|
230
266
|
is_press: bool
|
|
231
267
|
|
|
@@ -422,6 +458,7 @@ class Device(CObject):
|
|
|
422
458
|
_unref_func = staticmethod(_capi.libeis.device_unref)
|
|
423
459
|
|
|
424
460
|
def __repr__(self) -> str:
|
|
461
|
+
"""Name, device type and capabilities -- what tells two devices apart."""
|
|
425
462
|
caps = "|".join(c.name or str(c.value) for c in self.capabilities)
|
|
426
463
|
return f"<Device {self.name!r} {self.device_type.name} {caps}>"
|
|
427
464
|
|
|
@@ -576,8 +613,10 @@ class Device(CObject):
|
|
|
576
613
|
return self
|
|
577
614
|
|
|
578
615
|
def frame(self, timestamp: int | None = None) -> Device:
|
|
579
|
-
"""Commit the events queued since the last frame as one logical
|
|
580
|
-
|
|
616
|
+
"""Commit the events queued since the last frame as one logical hardware event.
|
|
617
|
+
|
|
618
|
+
``timestamp`` defaults to the context's current time.
|
|
619
|
+
"""
|
|
581
620
|
if timestamp is None:
|
|
582
621
|
timestamp = _capi.libeis.now(_capi.libeis.device_get_context(self))
|
|
583
622
|
_capi.libeis.device_frame(self, timestamp)
|
|
@@ -673,6 +712,7 @@ class Seat(CObject):
|
|
|
673
712
|
_unref_func = staticmethod(_capi.libeis.seat_unref)
|
|
674
713
|
|
|
675
714
|
def __repr__(self) -> str:
|
|
715
|
+
"""The seat's name and capabilities."""
|
|
676
716
|
caps = "|".join(c.name or str(c.value) for c in self.capabilities)
|
|
677
717
|
return f"<Seat {self.name!r} {caps}>"
|
|
678
718
|
|
|
@@ -734,6 +774,7 @@ class Client(CObject):
|
|
|
734
774
|
_unref_func = staticmethod(_capi.libeis.client_unref)
|
|
735
775
|
|
|
736
776
|
def __repr__(self) -> str:
|
|
777
|
+
"""The client's name and which side of the protocol it is on."""
|
|
737
778
|
return f"<Client {self.name!r} sender={self.is_sender}>"
|
|
738
779
|
|
|
739
780
|
@property
|
|
@@ -804,6 +845,7 @@ class Ping(CObject):
|
|
|
804
845
|
_unref_func = staticmethod(_capi.libeis.ping_unref)
|
|
805
846
|
|
|
806
847
|
def __repr__(self) -> str:
|
|
848
|
+
"""The ping's id, which is all a ping carries."""
|
|
807
849
|
return f"<Ping {self.id}>"
|
|
808
850
|
|
|
809
851
|
@property
|
|
@@ -831,14 +873,18 @@ class Event(CObject):
|
|
|
831
873
|
_unref_func = staticmethod(_capi.libeis.event_unref)
|
|
832
874
|
|
|
833
875
|
def __repr__(self) -> str:
|
|
876
|
+
"""The event type by name, or the raw value for one we do not model."""
|
|
834
877
|
event_type = self.event_type
|
|
835
878
|
label = event_type.name if isinstance(event_type, EventType) else event_type
|
|
836
879
|
return f"<Event {label}>"
|
|
837
880
|
|
|
838
881
|
@property
|
|
839
882
|
def event_type(self) -> EventType | int:
|
|
840
|
-
"""The event's type
|
|
841
|
-
|
|
883
|
+
"""The event's type.
|
|
884
|
+
|
|
885
|
+
Returns a raw int for a value newer than this package's
|
|
886
|
+
:class:`EventType` table -- see its docstring.
|
|
887
|
+
"""
|
|
842
888
|
raw = _capi.libeis.event_get_type(self)
|
|
843
889
|
try:
|
|
844
890
|
return EventType(raw)
|
|
@@ -1040,6 +1086,11 @@ class Event(CObject):
|
|
|
1040
1086
|
|
|
1041
1087
|
|
|
1042
1088
|
def _log_callback(_eis: int, priority: int, message: bytes, _context: int) -> None:
|
|
1089
|
+
"""Forward libeis's log lines into the logging module.
|
|
1090
|
+
|
|
1091
|
+
The same constraint as ei.py's callback of that name: it runs inside a
|
|
1092
|
+
ctypes callback, so it falls back rather than raising.
|
|
1093
|
+
"""
|
|
1043
1094
|
# See ei.py's _log_callback: look up the raw int, not
|
|
1044
1095
|
# _LogPriority(priority), which would raise ValueError before .get()'s
|
|
1045
1096
|
# default could apply -- silently, since this runs inside a ctypes
|
|
@@ -1069,10 +1120,13 @@ class Eis(CObject):
|
|
|
1069
1120
|
_wrappable = False
|
|
1070
1121
|
|
|
1071
1122
|
def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1123
|
+
"""Wrap a freshly created ``struct eis *`` and arm its log handler.
|
|
1124
|
+
|
|
1125
|
+
_adopt is accepted and forwarded for signature consistency with
|
|
1126
|
+
CObject, but with _wrappable = False, _get_or_create() never
|
|
1127
|
+
actually reaches this constructor -- Eis is always built directly
|
|
1128
|
+
via cls(cls._new()) in create_for_fd().
|
|
1129
|
+
"""
|
|
1076
1130
|
super().__init__(pointer, _adopt=_adopt)
|
|
1077
1131
|
_capi.libeis.log_set_handler(self, _log_handler)
|
|
1078
1132
|
_capi.libeis.log_set_priority(self, _LogPriority.DEBUG)
|
|
@@ -1146,7 +1200,8 @@ class Eis(CObject):
|
|
|
1146
1200
|
"""Read from the connection and queue any events that arrive.
|
|
1147
1201
|
|
|
1148
1202
|
Call this before iterating :attr:`events`, which only drains what
|
|
1149
|
-
is already queued.
|
|
1203
|
+
is already queued.
|
|
1204
|
+
"""
|
|
1150
1205
|
_capi.libeis.dispatch(self)
|
|
1151
1206
|
|
|
1152
1207
|
def add_client(self) -> int:
|
|
@@ -1165,6 +1220,7 @@ class Eis(CObject):
|
|
|
1165
1220
|
|
|
1166
1221
|
@classmethod
|
|
1167
1222
|
def _new(cls) -> int:
|
|
1223
|
+
"""A new server from the C library, or an error if it returned NULL."""
|
|
1168
1224
|
pointer = _capi.libeis.new(c_void_p(None))
|
|
1169
1225
|
if not pointer:
|
|
1170
1226
|
raise Error("eis_new() returned NULL")
|
|
@@ -1172,14 +1228,16 @@ class Eis(CObject):
|
|
|
1172
1228
|
|
|
1173
1229
|
@classmethod
|
|
1174
1230
|
def create_for_fd(cls, flags: Sequence[Flag] = ()) -> Eis:
|
|
1175
|
-
"""Create a server using the fd backend
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1231
|
+
"""Create a server using the fd backend.
|
|
1232
|
+
|
|
1233
|
+
The one real compositors use, since it keeps each client's fd
|
|
1234
|
+
private rather than exposing a connectable socket path. Call
|
|
1235
|
+
:meth:`add_client` once per connection you want to accept.
|
|
1179
1236
|
|
|
1180
1237
|
``flags`` are applied here rather than left to the caller because
|
|
1181
1238
|
:meth:`set_flag` has to run before the backend is set up, and this
|
|
1182
|
-
method does both.
|
|
1239
|
+
method does both.
|
|
1240
|
+
"""
|
|
1183
1241
|
server = cls(cls._new())
|
|
1184
1242
|
for flag in flags:
|
|
1185
1243
|
server.set_flag(flag)
|
|
@@ -1190,9 +1248,12 @@ class Eis(CObject):
|
|
|
1190
1248
|
|
|
1191
1249
|
@classmethod
|
|
1192
1250
|
def create_for_socket(cls, path: Path, flags: Sequence[Flag] = ()) -> Eis:
|
|
1193
|
-
"""Create a server listening on a Unix socket
|
|
1194
|
-
|
|
1195
|
-
|
|
1251
|
+
"""Create a server listening on a Unix socket.
|
|
1252
|
+
|
|
1253
|
+
As a compositor would (this is the path a real
|
|
1254
|
+
``ei_setup_backend_socket()`` client connects to). See
|
|
1255
|
+
:meth:`create_for_fd` on ``flags``.
|
|
1256
|
+
"""
|
|
1196
1257
|
server = cls(cls._new())
|
|
1197
1258
|
for flag in flags:
|
|
1198
1259
|
server.set_flag(flag)
|
|
@@ -1217,6 +1278,10 @@ __all__ = [
|
|
|
1217
1278
|
"KeyEvent",
|
|
1218
1279
|
"Keymap",
|
|
1219
1280
|
"KeymapType",
|
|
1281
|
+
# Imported rather than defined here: every call bound through
|
|
1282
|
+
# _capi.libeis can raise it, so a caller importing from this module
|
|
1283
|
+
# catches it from here too -- see docs/troubleshooting.md.
|
|
1284
|
+
"LibraryNotFoundError",
|
|
1220
1285
|
"Ping",
|
|
1221
1286
|
"PointerAbsoluteEvent",
|
|
1222
1287
|
"PointerEvent",
|
libei/oeffis.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
"""Pythonic wrapper around liboeffis
|
|
2
|
-
|
|
1
|
+
"""Pythonic wrapper around liboeffis.
|
|
2
|
+
|
|
3
|
+
Negotiates an EIS connection through the
|
|
4
|
+
``org.freedesktop.portal.RemoteDesktop`` XDG desktop portal.
|
|
3
5
|
|
|
4
6
|
This is the path a sandboxed or otherwise non-privileged client uses to get
|
|
5
7
|
an EI socket: it asks the portal, the user is shown a consent dialog, and on
|
|
6
8
|
approval this hands back a file descriptor to pass to
|
|
7
|
-
:meth:`libei.ei.Sender.create_for_fd
|
|
9
|
+
:meth:`libei.ei.Sender.create_for_fd`::
|
|
8
10
|
|
|
9
11
|
oeffis = Oeffis.create(devices=DeviceType.POINTER)
|
|
10
12
|
while True:
|
|
@@ -39,6 +41,7 @@ import logging
|
|
|
39
41
|
import os
|
|
40
42
|
|
|
41
43
|
from . import _capi
|
|
44
|
+
from ._capi.loader import LibraryNotFoundError
|
|
42
45
|
|
|
43
46
|
logger = logging.getLogger("libei.oeffis")
|
|
44
47
|
|
|
@@ -52,6 +55,7 @@ class DisconnectedError(Exception):
|
|
|
52
55
|
"""The portal session ended unexpectedly (error, or denied by the user)."""
|
|
53
56
|
|
|
54
57
|
def __init__(self, message: str | None) -> None:
|
|
58
|
+
"""Record why the session ended."""
|
|
55
59
|
super().__init__(message)
|
|
56
60
|
self.message = message
|
|
57
61
|
|
|
@@ -60,6 +64,7 @@ class SessionClosedError(DisconnectedError):
|
|
|
60
64
|
"""The portal explicitly closed the session (not necessarily an error)."""
|
|
61
65
|
|
|
62
66
|
def __init__(self) -> None:
|
|
67
|
+
"""Build the fixed "Session closed" message."""
|
|
63
68
|
super().__init__(message="Session closed")
|
|
64
69
|
|
|
65
70
|
|
|
@@ -100,6 +105,7 @@ class Oeffis:
|
|
|
100
105
|
"""
|
|
101
106
|
|
|
102
107
|
def __init__(self) -> None:
|
|
108
|
+
"""Create the underlying liboeffis context (no portal call yet)."""
|
|
103
109
|
pointer = _capi.liboeffis.new(None)
|
|
104
110
|
if not pointer:
|
|
105
111
|
raise DisconnectedError("oeffis_new() returned NULL")
|
|
@@ -117,6 +123,11 @@ class Oeffis:
|
|
|
117
123
|
self._state = _EventType.NONE
|
|
118
124
|
|
|
119
125
|
def __del__(self) -> None:
|
|
126
|
+
"""Release the session, closing the EIS fd if nobody claimed it.
|
|
127
|
+
|
|
128
|
+
Safe on a half-built object: __init__ can raise before every
|
|
129
|
+
attribute exists, and this still runs.
|
|
130
|
+
"""
|
|
120
131
|
# getattr() with defaults rather than plain attribute access:
|
|
121
132
|
# __init__ raises DisconnectedError when oeffis_new() returns NULL,
|
|
122
133
|
# and Python still calls __del__ on the half-built object, where
|
|
@@ -232,6 +243,11 @@ class Oeffis:
|
|
|
232
243
|
__all__ = [
|
|
233
244
|
"DeviceType",
|
|
234
245
|
"DisconnectedError",
|
|
246
|
+
# Imported rather than defined here: every call bound through
|
|
247
|
+
# _capi.liboeffis can raise it -- Oeffis.create() on a machine with no
|
|
248
|
+
# liboeffis is the ordinary one -- so a caller importing from this
|
|
249
|
+
# module catches it from here too. See docs/troubleshooting.md.
|
|
250
|
+
"LibraryNotFoundError",
|
|
235
251
|
"Oeffis",
|
|
236
252
|
"SessionClosedError",
|
|
237
253
|
"is_available",
|
libei/portal.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
"""Negotiate an EIS connection by driving a portal directly over D-Bus
|
|
2
|
-
|
|
1
|
+
"""Negotiate an EIS connection by driving a portal directly over D-Bus.
|
|
2
|
+
|
|
3
|
+
Not through :mod:`libei.oeffis` -- see that module instead for the simpler,
|
|
4
|
+
liboeffis-backed path.
|
|
3
5
|
|
|
4
6
|
Two portals, two directions. :class:`RemoteDesktopSession` negotiates
|
|
5
7
|
``org.freedesktop.portal.RemoteDesktop`` to *inject* input; below it,
|
|
@@ -19,7 +21,7 @@ should be handled by an application talking to DBus directly"
|
|
|
19
21
|
(https://libinput.pages.freedesktop.org/libei/api/group__liboeffis.html).
|
|
20
22
|
This module is that: the ``CreateSession`` -> ``SelectDevices`` -> ``Start``
|
|
21
23
|
-> ``ConnectToEIS`` sequence driven directly, with ``persist_mode`` and
|
|
22
|
-
``restore_token`` exposed as real parameters
|
|
24
|
+
``restore_token`` exposed as real parameters::
|
|
23
25
|
|
|
24
26
|
with RemoteDesktopSession.negotiate(
|
|
25
27
|
devices=DeviceType.POINTER | DeviceType.KEYBOARD,
|
|
@@ -69,6 +71,7 @@ Three things worth knowing before building on this:
|
|
|
69
71
|
|
|
70
72
|
from __future__ import annotations
|
|
71
73
|
|
|
74
|
+
import contextlib
|
|
72
75
|
import enum
|
|
73
76
|
import logging
|
|
74
77
|
import os
|
|
@@ -104,7 +107,7 @@ _MIN_REMOTE_DESKTOP_VERSION = 2 # ConnectToEIS needs v2+
|
|
|
104
107
|
_MIN_INPUT_CAPTURE_VERSION = 2 # CreateSession2 is a v2-only method
|
|
105
108
|
|
|
106
109
|
_DEFAULT_TIMEOUT = 60.0
|
|
107
|
-
"""Seconds to wait for one portal round trip. Generous, because a
|
|
110
|
+
"""Seconds to wait for one portal round trip. Generous, because a user has
|
|
108
111
|
to see and answer the consent dialog `Start` raises -- but bounded, because
|
|
109
112
|
the alternative is a caller wedged forever if the portal dies after
|
|
110
113
|
accepting the call and before sending its `Response`."""
|
|
@@ -156,11 +159,12 @@ class PortalTimeoutError(PortalError):
|
|
|
156
159
|
|
|
157
160
|
Distinct from a decline: the portal accepted the call and then never
|
|
158
161
|
sent its ``Response`` signal. Most often the consent dialog is simply
|
|
159
|
-
still waiting for a
|
|
162
|
+
still waiting for a user, so raise the timeout rather than treating
|
|
160
163
|
this as a failure if that is expected.
|
|
161
164
|
"""
|
|
162
165
|
|
|
163
166
|
def __init__(self, step: str, timeout: float) -> None:
|
|
167
|
+
"""Name which step timed out and after how long."""
|
|
164
168
|
super().__init__(f"{step} did not answer within {timeout:g}s")
|
|
165
169
|
self.step = step
|
|
166
170
|
self.timeout = timeout
|
|
@@ -175,6 +179,7 @@ class PortalDeniedError(PortalError):
|
|
|
175
179
|
"""
|
|
176
180
|
|
|
177
181
|
def __init__(self, step: str, message: str | None = None) -> None:
|
|
182
|
+
"""Name which step was refused and, where the portal gave one, why."""
|
|
178
183
|
super().__init__(message or f"{step} was not approved")
|
|
179
184
|
self.step = step
|
|
180
185
|
self.message = message
|
|
@@ -422,18 +427,21 @@ def _request(
|
|
|
422
427
|
params: Any,
|
|
423
428
|
*_a: Any,
|
|
424
429
|
) -> None:
|
|
430
|
+
"""Record the first reply and quit the loop; later replies are ignored."""
|
|
425
431
|
if result: # both subscriptions may fire; the first reply wins
|
|
426
432
|
return
|
|
427
433
|
result["code"], result["results"] = params.unpack()
|
|
428
434
|
loop.quit()
|
|
429
435
|
|
|
430
436
|
def on_timeout() -> bool:
|
|
437
|
+
"""Stop the loop and record that it timed out rather than finished."""
|
|
431
438
|
nonlocal timed_out
|
|
432
439
|
timed_out = True
|
|
433
440
|
loop.quit()
|
|
434
441
|
return False # one-shot; GLib removes the source when this is False
|
|
435
442
|
|
|
436
443
|
def subscribe(path: str) -> None:
|
|
444
|
+
"""Subscribe to Response on one path, keeping the handle alive."""
|
|
437
445
|
subscriptions.append(
|
|
438
446
|
connection.signal_subscribe(
|
|
439
447
|
busname,
|
|
@@ -481,10 +489,16 @@ def _request(
|
|
|
481
489
|
try:
|
|
482
490
|
loop.run()
|
|
483
491
|
finally:
|
|
484
|
-
#
|
|
485
|
-
#
|
|
486
|
-
#
|
|
487
|
-
|
|
492
|
+
# on_timeout() returns False, which is GLib's own signal to
|
|
493
|
+
# deregister a fired one-shot source -- removing it again
|
|
494
|
+
# here trips a real "Source ID N was not found when
|
|
495
|
+
# attempting to remove it" warning, the same bug already
|
|
496
|
+
# fixed for _wait_for_signal below. Only remove it when the
|
|
497
|
+
# *Response* woke the loop instead; leaving a live source in
|
|
498
|
+
# that case holds a reference to this closure and fires it
|
|
499
|
+
# into a dead loop on some later request.
|
|
500
|
+
if not timed_out:
|
|
501
|
+
GLib.source_remove(timeout_source)
|
|
488
502
|
finally:
|
|
489
503
|
for subscription in subscriptions:
|
|
490
504
|
connection.signal_unsubscribe(subscription)
|
|
@@ -584,6 +598,11 @@ class RemoteDesktopSession:
|
|
|
584
598
|
session_handle: str | None = None,
|
|
585
599
|
busname: str = _BUS_NAME,
|
|
586
600
|
) -> None:
|
|
601
|
+
"""Hold a negotiated session's handle, connection and EIS fd.
|
|
602
|
+
|
|
603
|
+
Not for direct use -- built by :meth:`negotiate` once ``Start`` and
|
|
604
|
+
``ConnectToEIS`` have both already succeeded.
|
|
605
|
+
"""
|
|
587
606
|
self._connection = connection
|
|
588
607
|
self._eis_fd: int | None = eis_fd
|
|
589
608
|
self._session_handle = session_handle
|
|
@@ -669,12 +688,20 @@ class RemoteDesktopSession:
|
|
|
669
688
|
self._connection = None
|
|
670
689
|
|
|
671
690
|
def __enter__(self) -> RemoteDesktopSession:
|
|
691
|
+
"""Return self: entering performs no action of its own."""
|
|
672
692
|
return self
|
|
673
693
|
|
|
674
694
|
def __exit__(self, *_exc: Any) -> None:
|
|
695
|
+
"""Close the session on the way out, whatever happened."""
|
|
675
696
|
self.close()
|
|
676
697
|
|
|
677
698
|
def __del__(self) -> None:
|
|
699
|
+
"""Close the fd only: __del__ can run during interpreter shutdown.
|
|
700
|
+
|
|
701
|
+
A synchronous D-Bus round trip there may hang or fail with nothing
|
|
702
|
+
left able to report it, so the D-Bus half of close() is deliberately
|
|
703
|
+
not attempted.
|
|
704
|
+
"""
|
|
678
705
|
# Deliberately only the fd, not the D-Bus half of close(): __del__
|
|
679
706
|
# can run during interpreter shutdown, where a synchronous D-Bus
|
|
680
707
|
# round trip may hang or fail in ways nothing can report. Closing an
|
|
@@ -687,10 +714,9 @@ class RemoteDesktopSession:
|
|
|
687
714
|
return
|
|
688
715
|
eis_fd = getattr(self, "_eis_fd", None)
|
|
689
716
|
if eis_fd is not None:
|
|
690
|
-
|
|
717
|
+
# an exception here is only printed to stderr anyway
|
|
718
|
+
with contextlib.suppress(OSError):
|
|
691
719
|
os.close(eis_fd)
|
|
692
|
-
except OSError:
|
|
693
|
-
pass # an exception here is only printed to stderr anyway
|
|
694
720
|
|
|
695
721
|
@classmethod
|
|
696
722
|
def negotiate(
|
|
@@ -819,7 +845,7 @@ class RemoteDesktopSession:
|
|
|
819
845
|
timeout=timeout,
|
|
820
846
|
)
|
|
821
847
|
except BaseException:
|
|
822
|
-
# BaseException, not Exception: `Start` blocks on a
|
|
848
|
+
# BaseException, not Exception: `Start` blocks on a user
|
|
823
849
|
# answering a consent dialog, so Ctrl-C during that wait is a
|
|
824
850
|
# routine way out of this function -- and it strands an
|
|
825
851
|
# approved session exactly as a decline does.
|
|
@@ -980,10 +1006,10 @@ def _wait_for_signal(
|
|
|
980
1006
|
busname: str,
|
|
981
1007
|
interface: str,
|
|
982
1008
|
signal: str,
|
|
983
|
-
|
|
1009
|
+
session_handle: str,
|
|
984
1010
|
timeout: float | None,
|
|
985
1011
|
) -> tuple[Any, ...]:
|
|
986
|
-
"""Block for one emission of ``signal``
|
|
1012
|
+
"""Block for one emission of ``signal`` for ``session_handle``, unpacked.
|
|
987
1013
|
|
|
988
1014
|
Unlike `_request`, nothing here *triggers* the signal: ``Activated`` and
|
|
989
1015
|
``Deactivated`` fire whenever the compositor decides a pointer barrier
|
|
@@ -991,7 +1017,23 @@ def _wait_for_signal(
|
|
|
991
1017
|
before this is even called (a long-enabled session, subscribed to
|
|
992
1018
|
late) or not for a long time. ``timeout=None`` waits indefinitely --
|
|
993
1019
|
the read a caller wants when there is nothing else useful to do but
|
|
994
|
-
wait for a
|
|
1020
|
+
wait for a user to move the pointer.
|
|
1021
|
+
|
|
1022
|
+
**These signals are emitted on the portal object, not on the session
|
|
1023
|
+
object.** Subscribing with the session handle as the D-Bus object path
|
|
1024
|
+
-- the obvious reading of "a signal for this session", and what this
|
|
1025
|
+
did until 2026-09-09 -- matches nothing, delivers nothing, and is
|
|
1026
|
+
indistinguishable from a compositor that never fires the signal at all:
|
|
1027
|
+
it cost this project a live investigation across two GNOME versions, a
|
|
1028
|
+
standalone C reproducer and an upstream Mutter bug report before an
|
|
1029
|
+
xdg-desktop-portal developer pointed out the mistake. The session is
|
|
1030
|
+
identified by the signal's own first argument instead (``o
|
|
1031
|
+
session_handle``, per the portal spec), so the subscription is on
|
|
1032
|
+
`_OBJECT_PATH` and the filtering happens here, on the payload. It is
|
|
1033
|
+
deliberately not done with ``signal_subscribe``'s ``arg0`` filter: the
|
|
1034
|
+
D-Bus specification restricts plain ``arg0=`` match rules to arguments
|
|
1035
|
+
of type STRING, and this one is an OBJECT_PATH, which is the same shape
|
|
1036
|
+
of silent non-delivery all over again.
|
|
995
1037
|
"""
|
|
996
1038
|
loop = GLib.MainLoop()
|
|
997
1039
|
result: dict[str, Any] = {}
|
|
@@ -1006,12 +1048,29 @@ def _wait_for_signal(
|
|
|
1006
1048
|
params: Any,
|
|
1007
1049
|
*_a: Any,
|
|
1008
1050
|
) -> None:
|
|
1051
|
+
"""Record the first matching signal; anything else is logged and dropped."""
|
|
1009
1052
|
if result: # a subscription that outlives its own wait can fire twice
|
|
1010
1053
|
return
|
|
1011
|
-
|
|
1054
|
+
args = params.unpack()
|
|
1055
|
+
if args[0] != session_handle:
|
|
1056
|
+
# Logged, not silently dropped: this filter can hide a signal
|
|
1057
|
+
# that *did* arrive just as effectively as the wrong-path
|
|
1058
|
+
# subscription above did, and the two are indistinguishable
|
|
1059
|
+
# from the outside -- both look exactly like a compositor that
|
|
1060
|
+
# never fired. One debug line is what tells them apart.
|
|
1061
|
+
logger.debug(
|
|
1062
|
+
"ignoring %s for session %s while waiting for %s",
|
|
1063
|
+
signal,
|
|
1064
|
+
args[0],
|
|
1065
|
+
session_handle,
|
|
1066
|
+
)
|
|
1067
|
+
return
|
|
1068
|
+
logger.debug("received %s for session %s", signal, session_handle)
|
|
1069
|
+
result["args"] = args
|
|
1012
1070
|
loop.quit()
|
|
1013
1071
|
|
|
1014
1072
|
def on_timeout() -> bool:
|
|
1073
|
+
"""Stop the loop and record that it timed out rather than finished."""
|
|
1015
1074
|
nonlocal timed_out
|
|
1016
1075
|
timed_out = True
|
|
1017
1076
|
loop.quit()
|
|
@@ -1021,12 +1080,20 @@ def _wait_for_signal(
|
|
|
1021
1080
|
busname,
|
|
1022
1081
|
interface,
|
|
1023
1082
|
signal,
|
|
1024
|
-
|
|
1083
|
+
_OBJECT_PATH,
|
|
1025
1084
|
None,
|
|
1026
1085
|
Gio.DBusSignalFlags.NONE,
|
|
1027
1086
|
on_signal,
|
|
1028
1087
|
None,
|
|
1029
1088
|
)
|
|
1089
|
+
logger.debug(
|
|
1090
|
+
"waiting up to %s for %s.%s on %s for session %s",
|
|
1091
|
+
"forever" if timeout is None else f"{timeout:g}s",
|
|
1092
|
+
interface,
|
|
1093
|
+
signal,
|
|
1094
|
+
_OBJECT_PATH,
|
|
1095
|
+
session_handle,
|
|
1096
|
+
)
|
|
1030
1097
|
try:
|
|
1031
1098
|
if not result:
|
|
1032
1099
|
timeout_source = None
|
|
@@ -1035,7 +1102,16 @@ def _wait_for_signal(
|
|
|
1035
1102
|
try:
|
|
1036
1103
|
loop.run()
|
|
1037
1104
|
finally:
|
|
1038
|
-
|
|
1105
|
+
# A fired timeout source has already deregistered itself --
|
|
1106
|
+
# on_timeout() returns False, which is GLib's signal to
|
|
1107
|
+
# remove it -- so only remove it here when the *signal*
|
|
1108
|
+
# woke the loop instead. Removing it unconditionally trips
|
|
1109
|
+
# a real GLib warning ("Source ID N was not found when
|
|
1110
|
+
# attempting to remove it"), confirmed live: it fired on
|
|
1111
|
+
# every real timeout this session hit, never against
|
|
1112
|
+
# test_portal.py's fake GLib.source_remove, which just
|
|
1113
|
+
# clears pending_timeout with no complaint either way.
|
|
1114
|
+
if timeout_source is not None and not timed_out:
|
|
1039
1115
|
GLib.source_remove(timeout_source)
|
|
1040
1116
|
finally:
|
|
1041
1117
|
connection.signal_unsubscribe(subscription)
|
|
@@ -1049,8 +1125,9 @@ def _wait_for_signal(
|
|
|
1049
1125
|
|
|
1050
1126
|
|
|
1051
1127
|
class Activation(NamedTuple):
|
|
1052
|
-
"""One ``Activated`` signal's payload
|
|
1053
|
-
|
|
1128
|
+
"""One ``Activated`` signal's payload.
|
|
1129
|
+
|
|
1130
|
+
See :meth:`InputCaptureSession.wait_for_activation`.
|
|
1054
1131
|
"""
|
|
1055
1132
|
|
|
1056
1133
|
activation_id: int
|
|
@@ -1077,8 +1154,8 @@ class InputCaptureSession:
|
|
|
1077
1154
|
direction: instead of injecting synthetic input, this receives real
|
|
1078
1155
|
input from the user's own devices once the compositor decides to divert
|
|
1079
1156
|
it here. That decision is the whole point of the protocol and is never
|
|
1080
|
-
this session's to make -- see :meth:`enable` and
|
|
1081
|
-
wait_for_activation`.
|
|
1157
|
+
this session's to make -- see :meth:`enable` and
|
|
1158
|
+
:meth:`wait_for_activation`.
|
|
1082
1159
|
|
|
1083
1160
|
**Capturing is exclusive.** Once the compositor activates a capture,
|
|
1084
1161
|
the events it captures stop reaching the desktop entirely and are sent
|
|
@@ -1101,10 +1178,10 @@ class InputCaptureSession:
|
|
|
1101
1178
|
|
|
1102
1179
|
**Never live-tested.** Every other class in this module that talks to a
|
|
1103
1180
|
real portal carries a hand-verification note in its own docstring; this
|
|
1104
|
-
one does not, because verifying it means a
|
|
1105
|
-
consent dialog *and* accepting that their pointer will be diverted
|
|
1106
|
-
from their own desktop for the length of the test -- not something
|
|
1107
|
-
trigger without asking first, unlike everything else here. Designed
|
|
1181
|
+
one does not, because verifying it means a developer clicking through
|
|
1182
|
+
the consent dialog *and* accepting that their pointer will be diverted
|
|
1183
|
+
away from their own desktop for the length of the test -- not something
|
|
1184
|
+
to trigger without asking first, unlike everything else here. Designed
|
|
1108
1185
|
against ``/usr/share/dbus-1/interfaces/org.freedesktop.portal.
|
|
1109
1186
|
InputCapture.xml`` (the shipped portal spec, not the header alone) and
|
|
1110
1187
|
unit-tested against a fake connection reproducing that spec's documented
|
|
@@ -1120,6 +1197,11 @@ class InputCaptureSession:
|
|
|
1120
1197
|
restore_token: str | None,
|
|
1121
1198
|
busname: str = _BUS_NAME,
|
|
1122
1199
|
) -> None:
|
|
1200
|
+
"""Hold a negotiated capture session's handle, connection and EIS fd.
|
|
1201
|
+
|
|
1202
|
+
Not for direct use -- built by :meth:`negotiate` once ``ConnectToEIS``
|
|
1203
|
+
has already succeeded.
|
|
1204
|
+
"""
|
|
1123
1205
|
self._connection = connection
|
|
1124
1206
|
self._session_handle: str | None = session_handle
|
|
1125
1207
|
self._eis_fd: int | None = eis_fd
|
|
@@ -1320,13 +1402,24 @@ class InputCaptureSession:
|
|
|
1320
1402
|
)
|
|
1321
1403
|
if code != 0:
|
|
1322
1404
|
raise PortalDeniedError("SetPointerBarriers")
|
|
1323
|
-
|
|
1405
|
+
failed = list(results.get("failed_barriers", []))
|
|
1406
|
+
# A partially-refused set is the quiet failure here: the call
|
|
1407
|
+
# succeeds, some barriers stand, and an edge the caller believes is
|
|
1408
|
+
# armed simply never triggers. Callers see only the returned list,
|
|
1409
|
+
# which they may or may not act on -- this says it either way.
|
|
1410
|
+
logger.debug(
|
|
1411
|
+
"SetPointerBarriers: %d requested %s, refused: %s",
|
|
1412
|
+
len(barriers),
|
|
1413
|
+
[b[0] for b in barriers],
|
|
1414
|
+
failed or "none",
|
|
1415
|
+
)
|
|
1416
|
+
return failed
|
|
1324
1417
|
|
|
1325
1418
|
def wait_for_activation(self, timeout: float | None = None) -> Activation:
|
|
1326
1419
|
"""Block until the compositor activates capture, or ``timeout``.
|
|
1327
1420
|
|
|
1328
1421
|
Only returns once a real barrier crossing has been reported --
|
|
1329
|
-
which, on hardware, means a
|
|
1422
|
+
which, on hardware, means a user moved a physical pointer across
|
|
1330
1423
|
one. There is no way to trigger this synthetically (see the class
|
|
1331
1424
|
docstring's third paragraph), so this call can legitimately hang
|
|
1332
1425
|
until someone does that, and `timeout=None` -- the default -- waits
|
|
@@ -1407,21 +1500,22 @@ class InputCaptureSession:
|
|
|
1407
1500
|
self._connection = None
|
|
1408
1501
|
|
|
1409
1502
|
def __enter__(self) -> InputCaptureSession:
|
|
1503
|
+
"""Return self: entering performs no action of its own."""
|
|
1410
1504
|
return self
|
|
1411
1505
|
|
|
1412
1506
|
def __exit__(self, *_exc: Any) -> None:
|
|
1507
|
+
"""Close the session on the way out, whatever happened."""
|
|
1413
1508
|
self.close()
|
|
1414
1509
|
|
|
1415
1510
|
def __del__(self) -> None:
|
|
1511
|
+
"""Close the fd only, for the reason RemoteDesktopSession.__del__ gives."""
|
|
1416
1512
|
# See RemoteDesktopSession.__del__ for why this closes only the fd.
|
|
1417
1513
|
if getattr(self, "_eis_fd_claimed", True):
|
|
1418
1514
|
return
|
|
1419
1515
|
eis_fd = getattr(self, "_eis_fd", None)
|
|
1420
1516
|
if eis_fd is not None:
|
|
1421
|
-
|
|
1517
|
+
with contextlib.suppress(OSError):
|
|
1422
1518
|
os.close(eis_fd)
|
|
1423
|
-
except OSError:
|
|
1424
|
-
pass
|
|
1425
1519
|
|
|
1426
1520
|
@classmethod
|
|
1427
1521
|
def negotiate(
|
|
@@ -1590,7 +1684,7 @@ class InputCaptureSession:
|
|
|
1590
1684
|
timeout,
|
|
1591
1685
|
)
|
|
1592
1686
|
except BaseException:
|
|
1593
|
-
# BaseException, not Exception: Start blocks on a
|
|
1687
|
+
# BaseException, not Exception: Start blocks on a user
|
|
1594
1688
|
# answering a consent dialog, so Ctrl-C during that wait is a
|
|
1595
1689
|
# routine way out of this function -- and it strands an
|
|
1596
1690
|
# approved session exactly as a decline does.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-libei
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Inject and receive input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis
|
|
5
5
|
Author: Dennis K. Paulsen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,6 +37,10 @@ Dynamic: license-file
|
|
|
37
37
|
|
|
38
38
|
# python-libei
|
|
39
39
|
|
|
40
|
+
[](https://github.com/ctrondlp/python-libei/actions/workflows/ci.yml)
|
|
41
|
+
[](https://pypi.org/project/python-libei/)
|
|
42
|
+
[](https://github.com/ctrondlp/python-libei/blob/main/LICENSE)
|
|
43
|
+
|
|
40
44
|
Python bindings for [libei, libeis and liboeffis](https://libinput.pages.freedesktop.org/libei/) —
|
|
41
45
|
the Wayland input-emulation libraries. Use this to **move the pointer, click,
|
|
42
46
|
type, or scroll on a Wayland desktop** from Python, the way `xdotool` did on
|
|
@@ -103,11 +107,15 @@ to get permission, then `ei` to inject.
|
|
|
103
107
|
| **Get permission**, and not be asked again | `libei.portal` | Same handshake over D-Bus directly, with `persist_mode` / `restore_token`. Needs PyGObject |
|
|
104
108
|
| **Be the server**, for tests or a compositor | `libei.eis` | Drives your client code with no real compositor and no consent dialog |
|
|
105
109
|
|
|
106
|
-
Each module has `is_available()
|
|
107
|
-
`
|
|
108
|
-
`
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
Each module has `is_available()`. `ei` and `eis` share the shapes around them:
|
|
111
|
+
an `Error` exception, an `EventType` and `DeviceCapability` enum, and `Device`,
|
|
112
|
+
`Seat`, `Region`, `Keymap`, `Touch`, `Ping`, `Event` with the frozen dataclasses
|
|
113
|
+
its accessors return. `oeffis` and `portal` are smaller — `DeviceType`, no
|
|
114
|
+
`EventType` or `DeviceCapability`, `Activation` as the one frozen result a
|
|
115
|
+
portal wait hands back, and their own exception classes rather than an `Error`.
|
|
116
|
+
[docs/troubleshooting.md](docs/troubleshooting.md) names every class they raise.
|
|
117
|
+
The package ships `py.typed`, so callers type-check against real
|
|
118
|
+
annotations rather than `Any`.
|
|
111
119
|
|
|
112
120
|
## What's implemented
|
|
113
121
|
|
|
@@ -149,14 +157,16 @@ Beyond sending input, the wrapper also covers ping/pong round trips
|
|
|
149
157
|
(`Device.keymap`), region mapping ids and coordinate conversion,
|
|
150
158
|
`Context.disconnect()`, `Context.peek_event_type()`, and
|
|
151
159
|
`Seat.request_device()`. On the server side, `libei.eis` mirrors all of it and
|
|
152
|
-
adds `Eis.set_flag()`
|
|
160
|
+
adds `Eis.set_flag()` (with the `Flag` values it takes), `Client.pid`, and
|
|
161
|
+
`Device.configure()` with the `ConfigureRegion` descriptions it accepts.
|
|
162
|
+
Underneath, the ctypes layer binds 250
|
|
153
163
|
of the 302 functions the three libraries export as of 1.6.0; what is left out,
|
|
154
164
|
and why, is in
|
|
155
165
|
[docs/developers/architecture.md](docs/developers/architecture.md#what-is-bound-and-what-is-deliberately-not).
|
|
156
166
|
|
|
157
167
|
## Status
|
|
158
168
|
|
|
159
|
-
Beta (`0.
|
|
169
|
+
Beta (`0.6.0`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
160
170
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
161
171
|
|
|
162
172
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -187,11 +197,11 @@ Exactly what was run, when, and against which versions:
|
|
|
187
197
|
portal paths are the part likeliest to come up short off Linux, since
|
|
188
198
|
they need an xdg-desktop-portal RemoteDesktop backend to talk to.
|
|
189
199
|
- CPython 3.10 or newer (tested on 3.13)
|
|
190
|
-
- The native
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
- `libei.portal` only: PyGObject
|
|
194
|
-
|
|
200
|
+
- The native `libei`, `libeis` and `liboeffis` libraries, which `pip` cannot
|
|
201
|
+
supply. Which package provides them on your distribution — and what to do
|
|
202
|
+
when the name does not match — is in [docs/install.md](docs/install.md).
|
|
203
|
+
- `libei.portal` only: PyGObject, via the `portal` extra, plus whatever
|
|
204
|
+
GObject-introspection libraries your distribution needs for `Gio`, since
|
|
195
205
|
PyPI's PyGObject wheel supplies the Python side only. Not needed for
|
|
196
206
|
`libei.ei`, `libei.eis` or `libei.oeffis`.
|
|
197
207
|
- libei 1.0.0 or newer for the core: connecting, binding a seat, and
|
|
@@ -224,22 +234,11 @@ Exactly what was run, when, and against which versions:
|
|
|
224
234
|
|
|
225
235
|
## Install
|
|
226
236
|
|
|
227
|
-
From [PyPI](https://pypi.org/project/python-libei/):
|
|
228
|
-
|
|
229
237
|
```sh
|
|
230
238
|
pip install python-libei
|
|
231
239
|
```
|
|
232
240
|
|
|
233
|
-
|
|
234
|
-
`pip show python-libei`, but `from libei import ei`.
|
|
235
|
-
|
|
236
|
-
Pure Python, no build step: the wheel is `py3-none-any` and ctypes talks to
|
|
237
|
-
the native libraries directly, so there is no compiler, no headers and no
|
|
238
|
-
`libei-devel` involved at install time. What `pip` does *not* bring is the
|
|
239
|
-
native libraries themselves -- see [Requirements](#requirements) above; on
|
|
240
|
-
Fedora, `sudo dnf install libei libeis liboeffis`.
|
|
241
|
-
|
|
242
|
-
To track `main` instead, or to hack on it, install from a checkout:
|
|
241
|
+
From a checkout instead, to track `main` or to work on the package:
|
|
243
242
|
|
|
244
243
|
```sh
|
|
245
244
|
git clone https://github.com/ctrondlp/python-libei.git
|
|
@@ -247,15 +246,11 @@ cd python-libei
|
|
|
247
246
|
pip install . # or `pip install -e '.[dev]'` to develop
|
|
248
247
|
```
|
|
249
248
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if not ei.is_available():
|
|
257
|
-
... # fall back to another input backend
|
|
258
|
-
```
|
|
249
|
+
That is the half `pip` can do. The native `libei`/`libeis`/`liboeffis`
|
|
250
|
+
libraries it cannot supply, the `portal` extra, and how to check what actually
|
|
251
|
+
loaded are all in [docs/install.md](docs/install.md) — see
|
|
252
|
+
[Requirements](#requirements) above for the version floors. Importing is safe
|
|
253
|
+
without any of it: those libraries are loaded on first *use*, not at import.
|
|
259
254
|
|
|
260
255
|
## Concepts
|
|
261
256
|
|
|
@@ -409,8 +404,10 @@ a list of error messages. Start there when nothing happens.
|
|
|
409
404
|
happens" checklist
|
|
410
405
|
- [docs/vs-snegg.md](docs/vs-snegg.md) — how this differs from the reference
|
|
411
406
|
bindings, and two signature issues found by cross-checking the C source
|
|
412
|
-
- [docs/developers/](docs/developers/)
|
|
413
|
-
|
|
407
|
+
- [docs/developers/architecture.md](docs/developers/architecture.md) and
|
|
408
|
+
[docs/developers/verification.md](docs/developers/verification.md) — the
|
|
409
|
+
four-layer architecture, and what has actually been verified against which
|
|
410
|
+
libei versions
|
|
414
411
|
- [CONTRIBUTING.md](CONTRIBUTING.md) — setup, checks, testing against an old
|
|
415
412
|
libei, releasing
|
|
416
413
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
libei/__init__.py,sha256=ypdFYwFMulRHAB3iYKc2NfCvMRvAq9TUFuwQN9b3PWw,1858
|
|
2
|
+
libei/_cobject.py,sha256=XjbUNJpZ87jAyPWh6dmRslk6cMy831oMRFhWmfjLDFo,13344
|
|
3
|
+
libei/ei.py,sha256=W4akKc_BNVfL0FroSXLGD1Z_L3OmYjZT5QDHNnEtMP8,49367
|
|
4
|
+
libei/eis.py,sha256=X_umAaAAbAMC9d2RdnMReeJZI61WsnKz3cySY5YrGr0,45841
|
|
5
|
+
libei/oeffis.py,sha256=gJHR90s9VhQrgkjWUMDx-lxZ8qKxkttnqbx_xo-7oxs,10035
|
|
6
|
+
libei/portal.py,sha256=eTYzNkA_65VaUlbSkATiXmOA8c6_uUDle2c2lUgZZHY,69698
|
|
7
|
+
libei/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
libei/_capi/__init__.py,sha256=raISSWSV9hAcE92DcLCz-vUb1IPcsmxRZaS-iAoQJBE,211
|
|
9
|
+
libei/_capi/libei.py,sha256=gpp42bhqPonhVD2zmFWCl4rdk_lm4XvD_7W2Rw5RsuI,11308
|
|
10
|
+
libei/_capi/libeis.py,sha256=jQR0Z0YN9qa71mAcSJ__-UaoF8PsVsBs8OXwjfqstFk,13080
|
|
11
|
+
libei/_capi/liboeffis.py,sha256=V8jdmJm3qOQUzzNiCAXt4Jr2JY11Y4-w6NIfIxbxGI4,1218
|
|
12
|
+
libei/_capi/loader.py,sha256=QD1xQJo05q4eSWhOCRpAWOlzHD5fcnHtI6yQyy4rKs0,5052
|
|
13
|
+
python_libei-0.6.0.dist-info/licenses/LICENSE,sha256=l6xbMU6Y-JZDzmBciBj-J5t6h6jNg_Bcyp4bHxDepqU,1074
|
|
14
|
+
python_libei-0.6.0.dist-info/METADATA,sha256=NC7ygbrH78TjzREhLvF7-FSz8kNgf8tPE21NZQiykp4,20963
|
|
15
|
+
python_libei-0.6.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
+
python_libei-0.6.0.dist-info/top_level.txt,sha256=_DQXzGjDsUBENI_cNkiOxPB4xi8coCbQS1lq18FMudQ,6
|
|
17
|
+
python_libei-0.6.0.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
libei/__init__.py,sha256=ZknUZWRNN4JfCH08NGpbBhqr_e2YZjHnk8B1aXe2J9Y,1858
|
|
2
|
-
libei/_cobject.py,sha256=msrbviABSWjc5fKsXHSCg7nQ0Y4Etcq_Dkh0fblW-sY,12845
|
|
3
|
-
libei/ei.py,sha256=Da3ezXnFPXtejlcJFH8bcE5f4pxKZXXqb6ltCh2zAdE,46704
|
|
4
|
-
libei/eis.py,sha256=aYWOCN1NlRchcWd6NjIUi1eYMFWJbp0UHMcQFEJ0ii4,43642
|
|
5
|
-
libei/oeffis.py,sha256=UcnDwLErFxB0xuktNpIzAK01ylpl6ZrmaGnWiUOcFOo,9302
|
|
6
|
-
libei/portal.py,sha256=LGRINrYQ-x46i0pzGOzpM6ZT3xek_AoExNIgR2BZRB8,64498
|
|
7
|
-
libei/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
libei/_capi/__init__.py,sha256=3VxixYYlr_ZcuiK0GwrCpbE3VvEv_O990mJzdIJ8i74,209
|
|
9
|
-
libei/_capi/libei.py,sha256=gpp42bhqPonhVD2zmFWCl4rdk_lm4XvD_7W2Rw5RsuI,11308
|
|
10
|
-
libei/_capi/libeis.py,sha256=jQR0Z0YN9qa71mAcSJ__-UaoF8PsVsBs8OXwjfqstFk,13080
|
|
11
|
-
libei/_capi/liboeffis.py,sha256=V8jdmJm3qOQUzzNiCAXt4Jr2JY11Y4-w6NIfIxbxGI4,1218
|
|
12
|
-
libei/_capi/loader.py,sha256=k7fb_Nz0fg5QkuLJ_duKXj8bESSktLQ2ZyS5LNQ5v2I,4689
|
|
13
|
-
python_libei-0.5.1.dist-info/licenses/LICENSE,sha256=l6xbMU6Y-JZDzmBciBj-J5t6h6jNg_Bcyp4bHxDepqU,1074
|
|
14
|
-
python_libei-0.5.1.dist-info/METADATA,sha256=Bg1meYzLmMAp9HqeMWbU4SzZZOurlSGzIR49ZmoJNgo,20475
|
|
15
|
-
python_libei-0.5.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
-
python_libei-0.5.1.dist-info/top_level.txt,sha256=_DQXzGjDsUBENI_cNkiOxPB4xi8coCbQS1lq18FMudQ,6
|
|
17
|
-
python_libei-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|