python-libei 0.5.0__tar.gz → 0.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_libei-0.5.0 → python_libei-0.5.2}/PKG-INFO +13 -28
- {python_libei-0.5.0 → python_libei-0.5.2}/README.md +12 -27
- {python_libei-0.5.0 → python_libei-0.5.2}/pyproject.toml +33 -2
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/__init__.py +1 -1
- python_libei-0.5.2/src/libei/_capi/__init__.py +9 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/ei.py +71 -15
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/eis.py +65 -17
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/oeffis.py +7 -2
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/portal.py +241 -76
- {python_libei-0.5.0 → python_libei-0.5.2}/src/python_libei.egg-info/PKG-INFO +13 -28
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_documentation_shape.py +55 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_ei_objects.py +2 -3
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_eis_objects.py +2 -3
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_inputcapture.py +186 -18
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_integration_extras.py +4 -4
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_oeffis.py +3 -3
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_portal.py +50 -7
- python_libei-0.5.0/src/libei/_capi/__init__.py +0 -6
- {python_libei-0.5.0 → python_libei-0.5.2}/LICENSE +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/setup.cfg +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/_capi/libei.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/_capi/libeis.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/_capi/liboeffis.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/_capi/loader.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/_cobject.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/libei/py.typed +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/python_libei.egg-info/SOURCES.txt +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/python_libei.egg-info/dependency_links.txt +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/python_libei.egg-info/requires.txt +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/src/python_libei.egg-info/top_level.txt +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_cobject.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_documented_examples.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_integration_socketpair.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.2}/tests/test_loader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-libei
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
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
|
|
@@ -156,7 +156,7 @@ and why, is in
|
|
|
156
156
|
|
|
157
157
|
## Status
|
|
158
158
|
|
|
159
|
-
Beta (`0.5.
|
|
159
|
+
Beta (`0.5.2`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
160
160
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
161
161
|
|
|
162
162
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -187,11 +187,11 @@ Exactly what was run, when, and against which versions:
|
|
|
187
187
|
portal paths are the part likeliest to come up short off Linux, since
|
|
188
188
|
they need an xdg-desktop-portal RemoteDesktop backend to talk to.
|
|
189
189
|
- CPython 3.10 or newer (tested on 3.13)
|
|
190
|
-
- The native
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
- `libei.portal` only: PyGObject
|
|
194
|
-
|
|
190
|
+
- The native `libei`, `libeis` and `liboeffis` libraries, which `pip` cannot
|
|
191
|
+
supply. Which package provides them on your distribution — and what to do
|
|
192
|
+
when the name does not match — is in [docs/install.md](docs/install.md).
|
|
193
|
+
- `libei.portal` only: PyGObject, via the `portal` extra, plus whatever
|
|
194
|
+
GObject-introspection libraries your distribution needs for `Gio`, since
|
|
195
195
|
PyPI's PyGObject wheel supplies the Python side only. Not needed for
|
|
196
196
|
`libei.ei`, `libei.eis` or `libei.oeffis`.
|
|
197
197
|
- libei 1.0.0 or newer for the core: connecting, binding a seat, and
|
|
@@ -224,22 +224,11 @@ Exactly what was run, when, and against which versions:
|
|
|
224
224
|
|
|
225
225
|
## Install
|
|
226
226
|
|
|
227
|
-
From [PyPI](https://pypi.org/project/python-libei/):
|
|
228
|
-
|
|
229
227
|
```sh
|
|
230
228
|
pip install python-libei
|
|
231
229
|
```
|
|
232
230
|
|
|
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:
|
|
231
|
+
From a checkout instead, to track `main` or to work on the package:
|
|
243
232
|
|
|
244
233
|
```sh
|
|
245
234
|
git clone https://github.com/ctrondlp/python-libei.git
|
|
@@ -247,15 +236,11 @@ cd python-libei
|
|
|
247
236
|
pip install . # or `pip install -e '.[dev]'` to develop
|
|
248
237
|
```
|
|
249
238
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if not ei.is_available():
|
|
257
|
-
... # fall back to another input backend
|
|
258
|
-
```
|
|
239
|
+
That is the half `pip` can do. The native `libei`/`libeis`/`liboeffis`
|
|
240
|
+
libraries it cannot supply, the `portal` extra, and how to check what actually
|
|
241
|
+
loaded are all in [docs/install.md](docs/install.md) — see
|
|
242
|
+
[Requirements](#requirements) above for the version floors. Importing is safe
|
|
243
|
+
without any of it: those libraries are loaded on first *use*, not at import.
|
|
259
244
|
|
|
260
245
|
## Concepts
|
|
261
246
|
|
|
@@ -119,7 +119,7 @@ and why, is in
|
|
|
119
119
|
|
|
120
120
|
## Status
|
|
121
121
|
|
|
122
|
-
Beta (`0.5.
|
|
122
|
+
Beta (`0.5.2`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
123
123
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
124
124
|
|
|
125
125
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -150,11 +150,11 @@ Exactly what was run, when, and against which versions:
|
|
|
150
150
|
portal paths are the part likeliest to come up short off Linux, since
|
|
151
151
|
they need an xdg-desktop-portal RemoteDesktop backend to talk to.
|
|
152
152
|
- CPython 3.10 or newer (tested on 3.13)
|
|
153
|
-
- The native
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
- `libei.portal` only: PyGObject
|
|
157
|
-
|
|
153
|
+
- The native `libei`, `libeis` and `liboeffis` libraries, which `pip` cannot
|
|
154
|
+
supply. Which package provides them on your distribution — and what to do
|
|
155
|
+
when the name does not match — is in [docs/install.md](docs/install.md).
|
|
156
|
+
- `libei.portal` only: PyGObject, via the `portal` extra, plus whatever
|
|
157
|
+
GObject-introspection libraries your distribution needs for `Gio`, since
|
|
158
158
|
PyPI's PyGObject wheel supplies the Python side only. Not needed for
|
|
159
159
|
`libei.ei`, `libei.eis` or `libei.oeffis`.
|
|
160
160
|
- libei 1.0.0 or newer for the core: connecting, binding a seat, and
|
|
@@ -187,22 +187,11 @@ Exactly what was run, when, and against which versions:
|
|
|
187
187
|
|
|
188
188
|
## Install
|
|
189
189
|
|
|
190
|
-
From [PyPI](https://pypi.org/project/python-libei/):
|
|
191
|
-
|
|
192
190
|
```sh
|
|
193
191
|
pip install python-libei
|
|
194
192
|
```
|
|
195
193
|
|
|
196
|
-
|
|
197
|
-
`pip show python-libei`, but `from libei import ei`.
|
|
198
|
-
|
|
199
|
-
Pure Python, no build step: the wheel is `py3-none-any` and ctypes talks to
|
|
200
|
-
the native libraries directly, so there is no compiler, no headers and no
|
|
201
|
-
`libei-devel` involved at install time. What `pip` does *not* bring is the
|
|
202
|
-
native libraries themselves -- see [Requirements](#requirements) above; on
|
|
203
|
-
Fedora, `sudo dnf install libei libeis liboeffis`.
|
|
204
|
-
|
|
205
|
-
To track `main` instead, or to hack on it, install from a checkout:
|
|
194
|
+
From a checkout instead, to track `main` or to work on the package:
|
|
206
195
|
|
|
207
196
|
```sh
|
|
208
197
|
git clone https://github.com/ctrondlp/python-libei.git
|
|
@@ -210,15 +199,11 @@ cd python-libei
|
|
|
210
199
|
pip install . # or `pip install -e '.[dev]'` to develop
|
|
211
200
|
```
|
|
212
201
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if not ei.is_available():
|
|
220
|
-
... # fall back to another input backend
|
|
221
|
-
```
|
|
202
|
+
That is the half `pip` can do. The native `libei`/`libeis`/`liboeffis`
|
|
203
|
+
libraries it cannot supply, the `portal` extra, and how to check what actually
|
|
204
|
+
loaded are all in [docs/install.md](docs/install.md) — see
|
|
205
|
+
[Requirements](#requirements) above for the version floors. Importing is safe
|
|
206
|
+
without any of it: those libraries are loaded on first *use*, not at import.
|
|
222
207
|
|
|
223
208
|
## Concepts
|
|
224
209
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-libei"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.2"
|
|
8
8
|
description = "Inject and receive input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -87,8 +87,39 @@ select = [
|
|
|
87
87
|
"E", "W", # pycodestyle
|
|
88
88
|
"F", # pyflakes
|
|
89
89
|
"I", # isort
|
|
90
|
-
"
|
|
90
|
+
"D", # pydocstyle
|
|
91
91
|
"B", # bugbear
|
|
92
|
+
"UP", # pyupgrade
|
|
93
|
+
"C4", # comprehensions
|
|
94
|
+
"SIM", # simplification
|
|
95
|
+
"RET", # return consistency
|
|
96
|
+
"ARG", # unused arguments
|
|
97
|
+
"C901", # cyclomatic complexity
|
|
98
|
+
]
|
|
99
|
+
ignore = [
|
|
100
|
+
"D203", # incompatible with D211; keep no blank line before class docstring
|
|
101
|
+
"D213", # incompatible with D212; keep the summary on the first line
|
|
102
|
+
"D401", # properties read better as noun phrases than imperatives
|
|
103
|
+
"D105", # __enter__/__exit__ and friends document themselves
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
[tool.ruff.lint.mccabe]
|
|
107
|
+
# pyguitest's ceiling; python-libei's own worst case (InputCaptureSession.
|
|
108
|
+
# negotiate(), forking on portal version) sits at 14, just under it. A
|
|
109
|
+
# ceiling on what gets added, not a claim everything else is close to it.
|
|
110
|
+
max-complexity = 15
|
|
111
|
+
|
|
112
|
+
[tool.ruff.lint.pydocstyle]
|
|
113
|
+
convention = "pep257"
|
|
114
|
+
|
|
115
|
+
[tool.ruff.lint.per-file-ignores]
|
|
116
|
+
# Tests use fakes with deliberately unused arguments and multi-with blocks
|
|
117
|
+
# that read better nested; matching pyguitest's/pyguitest-recorder's own
|
|
118
|
+
# per-file-ignores for the same reasons.
|
|
119
|
+
"tests/*" = [
|
|
120
|
+
"ARG001", "ARG002", "ARG003", "ARG005", # fakes accept arguments they ignore
|
|
121
|
+
"SIM117", # nested with-blocks read better in tests
|
|
122
|
+
"D100", "D101", "D102", "D103", "D104", "D107",
|
|
92
123
|
]
|
|
93
124
|
|
|
94
125
|
[tool.mypy]
|
|
@@ -87,6 +87,7 @@ class Error(Exception):
|
|
|
87
87
|
"""
|
|
88
88
|
|
|
89
89
|
def __init__(self, message: str, errno: int | None = None) -> None:
|
|
90
|
+
"""Record the failure message and, where libei reported one, errno."""
|
|
90
91
|
super().__init__(message)
|
|
91
92
|
self.message = message
|
|
92
93
|
self.errno = errno
|
|
@@ -199,6 +200,12 @@ class _LogPriority(enum.IntEnum):
|
|
|
199
200
|
|
|
200
201
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
201
202
|
class XkbModifiersEvent:
|
|
203
|
+
"""XKB modifier state from a KEYBOARD_MODIFIERS event.
|
|
204
|
+
|
|
205
|
+
``depressed``/``latched``/``locked`` are XKB's own mod-state bitmasks;
|
|
206
|
+
``group`` is the active keyboard layout group.
|
|
207
|
+
"""
|
|
208
|
+
|
|
202
209
|
depressed: int
|
|
203
210
|
latched: int
|
|
204
211
|
locked: int
|
|
@@ -207,48 +214,76 @@ class XkbModifiersEvent:
|
|
|
207
214
|
|
|
208
215
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
209
216
|
class KeyEvent:
|
|
217
|
+
"""Key code and press state from a KEYBOARD_KEY event.
|
|
218
|
+
|
|
219
|
+
``key`` is a Linux ``KEY_*`` code, the same numbering
|
|
220
|
+
:meth:`Device.keyboard_key` sends.
|
|
221
|
+
"""
|
|
222
|
+
|
|
210
223
|
key: int
|
|
211
224
|
is_press: bool
|
|
212
225
|
|
|
213
226
|
|
|
214
227
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
215
228
|
class ButtonEvent:
|
|
229
|
+
"""Button code and press state from a BUTTON_BUTTON event.
|
|
230
|
+
|
|
231
|
+
``button`` is a Linux ``BTN_*`` code, the same numbering
|
|
232
|
+
:meth:`Device.button` sends.
|
|
233
|
+
"""
|
|
234
|
+
|
|
216
235
|
button: int
|
|
217
236
|
is_press: bool
|
|
218
237
|
|
|
219
238
|
|
|
220
239
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
221
240
|
class PointerEvent:
|
|
241
|
+
"""Relative motion deltas, in logical pixels, from a POINTER_MOTION event."""
|
|
242
|
+
|
|
222
243
|
dx: float
|
|
223
244
|
dy: float
|
|
224
245
|
|
|
225
246
|
|
|
226
247
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
227
248
|
class PointerAbsoluteEvent:
|
|
249
|
+
"""Absolute position from a POINTER_MOTION_ABSOLUTE event.
|
|
250
|
+
|
|
251
|
+
In the logical pixel space of the :class:`Region` the emitting device
|
|
252
|
+
covers -- see :meth:`Event.pointer_absolute_event`.
|
|
253
|
+
"""
|
|
254
|
+
|
|
228
255
|
x: float
|
|
229
256
|
y: float
|
|
230
257
|
|
|
231
258
|
|
|
232
259
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
233
260
|
class ScrollEvent:
|
|
261
|
+
"""Smooth scroll deltas from a SCROLL_DELTA event."""
|
|
262
|
+
|
|
234
263
|
dx: float
|
|
235
264
|
dy: float
|
|
236
265
|
|
|
237
266
|
|
|
238
267
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
239
268
|
class ScrollDiscreteEvent:
|
|
269
|
+
"""Detent scroll deltas (120 per detent) from a SCROLL_DISCRETE event."""
|
|
270
|
+
|
|
240
271
|
dx: int
|
|
241
272
|
dy: int
|
|
242
273
|
|
|
243
274
|
|
|
244
275
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
245
276
|
class ScrollStopEvent:
|
|
277
|
+
"""Which axes stopped scrolling, from a SCROLL_STOP/SCROLL_CANCEL event."""
|
|
278
|
+
|
|
246
279
|
stop_x: bool
|
|
247
280
|
stop_y: bool
|
|
248
281
|
|
|
249
282
|
|
|
250
283
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
251
284
|
class TouchEvent:
|
|
285
|
+
"""Touch id and position from a TOUCH_DOWN or TOUCH_MOTION event."""
|
|
286
|
+
|
|
252
287
|
touchid: int
|
|
253
288
|
x: float
|
|
254
289
|
y: float
|
|
@@ -256,17 +291,26 @@ class TouchEvent:
|
|
|
256
291
|
|
|
257
292
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
258
293
|
class TouchUpEvent:
|
|
294
|
+
"""Touch id and cancellation flag from a TOUCH_UP event.
|
|
295
|
+
|
|
296
|
+
See :meth:`Event.touch_up_event` for when ``is_cancel`` is trustworthy.
|
|
297
|
+
"""
|
|
298
|
+
|
|
259
299
|
touchid: int
|
|
260
300
|
is_cancel: bool
|
|
261
301
|
|
|
262
302
|
|
|
263
303
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
264
304
|
class TextUtf8Event:
|
|
305
|
+
"""UTF-8 text carried by a TEXT_UTF8 event."""
|
|
306
|
+
|
|
265
307
|
text: str
|
|
266
308
|
|
|
267
309
|
|
|
268
310
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
269
311
|
class TextKeysymEvent:
|
|
312
|
+
"""Keysym and press state from a TEXT_KEYSYM event."""
|
|
313
|
+
|
|
270
314
|
keysym: int
|
|
271
315
|
is_press: bool
|
|
272
316
|
|
|
@@ -545,9 +589,10 @@ class Device(CObject):
|
|
|
545
589
|
return self
|
|
546
590
|
|
|
547
591
|
def frame(self, timestamp: int | None = None) -> Device:
|
|
548
|
-
"""Commit the events queued since the last frame as one logical
|
|
549
|
-
|
|
550
|
-
time.
|
|
592
|
+
"""Commit the events queued since the last frame as one logical hardware event.
|
|
593
|
+
|
|
594
|
+
``timestamp`` defaults to the context's current time.
|
|
595
|
+
"""
|
|
551
596
|
if timestamp is None:
|
|
552
597
|
timestamp = _capi.libei.now(_capi.libei.device_get_context(self))
|
|
553
598
|
_capi.libei.device_frame(self, timestamp)
|
|
@@ -564,8 +609,10 @@ class Device(CObject):
|
|
|
564
609
|
return self
|
|
565
610
|
|
|
566
611
|
def button(self, button: int, is_press: bool) -> Device:
|
|
567
|
-
"""Queue a button press or release.
|
|
568
|
-
|
|
612
|
+
"""Queue a button press or release.
|
|
613
|
+
|
|
614
|
+
``button`` is a Linux ``BTN_*`` code (e.g. ``0x110`` for ``BTN_LEFT``).
|
|
615
|
+
"""
|
|
569
616
|
_capi.libei.device_button_button(self, button, is_press)
|
|
570
617
|
return self
|
|
571
618
|
|
|
@@ -760,8 +807,11 @@ class Event(CObject):
|
|
|
760
807
|
|
|
761
808
|
@property
|
|
762
809
|
def event_type(self) -> EventType | int:
|
|
763
|
-
"""The event's type
|
|
764
|
-
|
|
810
|
+
"""The event's type.
|
|
811
|
+
|
|
812
|
+
Returns a raw int for a value newer than this package's
|
|
813
|
+
:class:`EventType` table -- see its docstring.
|
|
814
|
+
"""
|
|
765
815
|
raw = _capi.libei.event_get_type(self)
|
|
766
816
|
try:
|
|
767
817
|
return EventType(raw)
|
|
@@ -1009,11 +1059,14 @@ class Context(CObject):
|
|
|
1009
1059
|
_wrappable = False
|
|
1010
1060
|
|
|
1011
1061
|
def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1062
|
+
"""Wrap a freshly created ``struct ei *`` and arm its log handler.
|
|
1063
|
+
|
|
1064
|
+
_adopt is accepted and forwarded for signature consistency with
|
|
1065
|
+
CObject, but with _wrappable = False, _get_or_create() never
|
|
1066
|
+
actually reaches this constructor -- Context (and Sender/
|
|
1067
|
+
Receiver) are always built directly via cls(cls._new()) in
|
|
1068
|
+
create_for_fd()/create_for_socket().
|
|
1069
|
+
"""
|
|
1017
1070
|
super().__init__(pointer, _adopt=_adopt)
|
|
1018
1071
|
self._name: str | None = None
|
|
1019
1072
|
_capi.libei.log_set_handler(self, _log_handler)
|
|
@@ -1081,7 +1134,8 @@ class Context(CObject):
|
|
|
1081
1134
|
"""Use an already-connected socket as the transport.
|
|
1082
1135
|
|
|
1083
1136
|
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.
|
|
1137
|
+
object is duplicated first, so the caller's own object stays valid.
|
|
1138
|
+
"""
|
|
1085
1139
|
# ei_setup_backend_fd() takes ownership of the fd and will close it
|
|
1086
1140
|
# itself. A raw int is assumed to already be one the caller is
|
|
1087
1141
|
# handing off (matching what eis.Eis.add_client()/oeffis.eis_fd
|
|
@@ -1099,7 +1153,8 @@ class Context(CObject):
|
|
|
1099
1153
|
"""Connect to an EIS socket by path.
|
|
1100
1154
|
|
|
1101
1155
|
``None`` uses ``$LIBEI_SOCKET``; a relative path is resolved
|
|
1102
|
-
against ``$XDG_RUNTIME_DIR``.
|
|
1156
|
+
against ``$XDG_RUNTIME_DIR``.
|
|
1157
|
+
"""
|
|
1103
1158
|
encoded = os.fspath(path).encode("utf-8") if path else None
|
|
1104
1159
|
err = _capi.libei.setup_backend_socket(self, encoded)
|
|
1105
1160
|
if err < 0:
|
|
@@ -1162,7 +1217,8 @@ class Context(CObject):
|
|
|
1162
1217
|
"""Read from the connection and queue any events that arrive.
|
|
1163
1218
|
|
|
1164
1219
|
Call this before iterating :attr:`events`, which only drains what
|
|
1165
|
-
is already queued.
|
|
1220
|
+
is already queued.
|
|
1221
|
+
"""
|
|
1166
1222
|
_capi.libei.dispatch(self)
|
|
1167
1223
|
|
|
1168
1224
|
|
|
@@ -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
|
|
@@ -166,48 +167,75 @@ class _LogPriority(enum.IntEnum):
|
|
|
166
167
|
|
|
167
168
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
168
169
|
class KeyEvent:
|
|
170
|
+
"""Key code and press state received on a KEYBOARD_KEY event.
|
|
171
|
+
|
|
172
|
+
``key`` is a Linux ``KEY_*`` code, as sent by the client's
|
|
173
|
+
``ei.Device.keyboard_key``.
|
|
174
|
+
"""
|
|
175
|
+
|
|
169
176
|
key: int
|
|
170
177
|
is_press: bool
|
|
171
178
|
|
|
172
179
|
|
|
173
180
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
174
181
|
class ButtonEvent:
|
|
182
|
+
"""Button code and press state received on a BUTTON_BUTTON event.
|
|
183
|
+
|
|
184
|
+
``button`` is a Linux ``BTN_*`` code, as sent by the client's
|
|
185
|
+
``ei.Device.button``.
|
|
186
|
+
"""
|
|
187
|
+
|
|
175
188
|
button: int
|
|
176
189
|
is_press: bool
|
|
177
190
|
|
|
178
191
|
|
|
179
192
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
180
193
|
class PointerEvent:
|
|
194
|
+
"""Relative motion deltas, in logical pixels, from a POINTER_MOTION event."""
|
|
195
|
+
|
|
181
196
|
dx: float
|
|
182
197
|
dy: float
|
|
183
198
|
|
|
184
199
|
|
|
185
200
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
186
201
|
class PointerAbsoluteEvent:
|
|
202
|
+
"""Absolute position from a POINTER_MOTION_ABSOLUTE event.
|
|
203
|
+
|
|
204
|
+
In the logical pixel space of the region the sending device covers.
|
|
205
|
+
"""
|
|
206
|
+
|
|
187
207
|
x: float
|
|
188
208
|
y: float
|
|
189
209
|
|
|
190
210
|
|
|
191
211
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
192
212
|
class ScrollEvent:
|
|
213
|
+
"""Smooth scroll deltas from a SCROLL_DELTA event."""
|
|
214
|
+
|
|
193
215
|
dx: float
|
|
194
216
|
dy: float
|
|
195
217
|
|
|
196
218
|
|
|
197
219
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
198
220
|
class ScrollDiscreteEvent:
|
|
221
|
+
"""Detent scroll deltas (120 per detent) from a SCROLL_DISCRETE event."""
|
|
222
|
+
|
|
199
223
|
dx: int
|
|
200
224
|
dy: int
|
|
201
225
|
|
|
202
226
|
|
|
203
227
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
204
228
|
class ScrollStopEvent:
|
|
229
|
+
"""Which axes stopped scrolling, from a SCROLL_STOP/SCROLL_CANCEL event."""
|
|
230
|
+
|
|
205
231
|
stop_x: bool
|
|
206
232
|
stop_y: bool
|
|
207
233
|
|
|
208
234
|
|
|
209
235
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
210
236
|
class TouchEvent:
|
|
237
|
+
"""Touch id and position from a TOUCH_DOWN or TOUCH_MOTION event."""
|
|
238
|
+
|
|
211
239
|
touchid: int
|
|
212
240
|
x: float
|
|
213
241
|
y: float
|
|
@@ -215,17 +243,23 @@ class TouchEvent:
|
|
|
215
243
|
|
|
216
244
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
217
245
|
class TouchUpEvent:
|
|
246
|
+
"""Touch id and cancellation flag from a TOUCH_UP event."""
|
|
247
|
+
|
|
218
248
|
touchid: int
|
|
219
249
|
is_cancel: bool
|
|
220
250
|
|
|
221
251
|
|
|
222
252
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
223
253
|
class TextUtf8Event:
|
|
254
|
+
"""UTF-8 text carried by a TEXT_UTF8 event."""
|
|
255
|
+
|
|
224
256
|
text: str
|
|
225
257
|
|
|
226
258
|
|
|
227
259
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
228
260
|
class TextKeysymEvent:
|
|
261
|
+
"""Keysym and press state from a TEXT_KEYSYM event."""
|
|
262
|
+
|
|
229
263
|
keysym: int
|
|
230
264
|
is_press: bool
|
|
231
265
|
|
|
@@ -576,8 +610,10 @@ class Device(CObject):
|
|
|
576
610
|
return self
|
|
577
611
|
|
|
578
612
|
def frame(self, timestamp: int | None = None) -> Device:
|
|
579
|
-
"""Commit the events queued since the last frame as one logical
|
|
580
|
-
|
|
613
|
+
"""Commit the events queued since the last frame as one logical hardware event.
|
|
614
|
+
|
|
615
|
+
``timestamp`` defaults to the context's current time.
|
|
616
|
+
"""
|
|
581
617
|
if timestamp is None:
|
|
582
618
|
timestamp = _capi.libeis.now(_capi.libeis.device_get_context(self))
|
|
583
619
|
_capi.libeis.device_frame(self, timestamp)
|
|
@@ -837,8 +873,11 @@ class Event(CObject):
|
|
|
837
873
|
|
|
838
874
|
@property
|
|
839
875
|
def event_type(self) -> EventType | int:
|
|
840
|
-
"""The event's type
|
|
841
|
-
|
|
876
|
+
"""The event's type.
|
|
877
|
+
|
|
878
|
+
Returns a raw int for a value newer than this package's
|
|
879
|
+
:class:`EventType` table -- see its docstring.
|
|
880
|
+
"""
|
|
842
881
|
raw = _capi.libeis.event_get_type(self)
|
|
843
882
|
try:
|
|
844
883
|
return EventType(raw)
|
|
@@ -1069,10 +1108,13 @@ class Eis(CObject):
|
|
|
1069
1108
|
_wrappable = False
|
|
1070
1109
|
|
|
1071
1110
|
def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1111
|
+
"""Wrap a freshly created ``struct eis *`` and arm its log handler.
|
|
1112
|
+
|
|
1113
|
+
_adopt is accepted and forwarded for signature consistency with
|
|
1114
|
+
CObject, but with _wrappable = False, _get_or_create() never
|
|
1115
|
+
actually reaches this constructor -- Eis is always built directly
|
|
1116
|
+
via cls(cls._new()) in create_for_fd().
|
|
1117
|
+
"""
|
|
1076
1118
|
super().__init__(pointer, _adopt=_adopt)
|
|
1077
1119
|
_capi.libeis.log_set_handler(self, _log_handler)
|
|
1078
1120
|
_capi.libeis.log_set_priority(self, _LogPriority.DEBUG)
|
|
@@ -1146,7 +1188,8 @@ class Eis(CObject):
|
|
|
1146
1188
|
"""Read from the connection and queue any events that arrive.
|
|
1147
1189
|
|
|
1148
1190
|
Call this before iterating :attr:`events`, which only drains what
|
|
1149
|
-
is already queued.
|
|
1191
|
+
is already queued.
|
|
1192
|
+
"""
|
|
1150
1193
|
_capi.libeis.dispatch(self)
|
|
1151
1194
|
|
|
1152
1195
|
def add_client(self) -> int:
|
|
@@ -1172,14 +1215,16 @@ class Eis(CObject):
|
|
|
1172
1215
|
|
|
1173
1216
|
@classmethod
|
|
1174
1217
|
def create_for_fd(cls, flags: Sequence[Flag] = ()) -> Eis:
|
|
1175
|
-
"""Create a server using the fd backend
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1218
|
+
"""Create a server using the fd backend.
|
|
1219
|
+
|
|
1220
|
+
The one real compositors use, since it keeps each client's fd
|
|
1221
|
+
private rather than exposing a connectable socket path. Call
|
|
1222
|
+
:meth:`add_client` once per connection you want to accept.
|
|
1179
1223
|
|
|
1180
1224
|
``flags`` are applied here rather than left to the caller because
|
|
1181
1225
|
:meth:`set_flag` has to run before the backend is set up, and this
|
|
1182
|
-
method does both.
|
|
1226
|
+
method does both.
|
|
1227
|
+
"""
|
|
1183
1228
|
server = cls(cls._new())
|
|
1184
1229
|
for flag in flags:
|
|
1185
1230
|
server.set_flag(flag)
|
|
@@ -1190,9 +1235,12 @@ class Eis(CObject):
|
|
|
1190
1235
|
|
|
1191
1236
|
@classmethod
|
|
1192
1237
|
def create_for_socket(cls, path: Path, flags: Sequence[Flag] = ()) -> Eis:
|
|
1193
|
-
"""Create a server listening on a Unix socket
|
|
1194
|
-
|
|
1195
|
-
|
|
1238
|
+
"""Create a server listening on a Unix socket.
|
|
1239
|
+
|
|
1240
|
+
As a compositor would (this is the path a real
|
|
1241
|
+
``ei_setup_backend_socket()`` client connects to). See
|
|
1242
|
+
:meth:`create_for_fd` on ``flags``.
|
|
1243
|
+
"""
|
|
1196
1244
|
server = cls(cls._new())
|
|
1197
1245
|
for flag in flags:
|
|
1198
1246
|
server.set_flag(flag)
|
|
@@ -1,5 +1,7 @@
|
|
|
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
|
|
@@ -52,6 +54,7 @@ class DisconnectedError(Exception):
|
|
|
52
54
|
"""The portal session ended unexpectedly (error, or denied by the user)."""
|
|
53
55
|
|
|
54
56
|
def __init__(self, message: str | None) -> None:
|
|
57
|
+
"""Record why the session ended."""
|
|
55
58
|
super().__init__(message)
|
|
56
59
|
self.message = message
|
|
57
60
|
|
|
@@ -60,6 +63,7 @@ class SessionClosedError(DisconnectedError):
|
|
|
60
63
|
"""The portal explicitly closed the session (not necessarily an error)."""
|
|
61
64
|
|
|
62
65
|
def __init__(self) -> None:
|
|
66
|
+
"""Build the fixed "Session closed" message."""
|
|
63
67
|
super().__init__(message="Session closed")
|
|
64
68
|
|
|
65
69
|
|
|
@@ -100,6 +104,7 @@ class Oeffis:
|
|
|
100
104
|
"""
|
|
101
105
|
|
|
102
106
|
def __init__(self) -> None:
|
|
107
|
+
"""Create the underlying liboeffis context (no portal call yet)."""
|
|
103
108
|
pointer = _capi.liboeffis.new(None)
|
|
104
109
|
if not pointer:
|
|
105
110
|
raise DisconnectedError("oeffis_new() returned NULL")
|