python-libei 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_libei-0.2.0/src/python_libei.egg-info → python_libei-0.3.0}/PKG-INFO +82 -20
- {python_libei-0.2.0 → python_libei-0.3.0}/README.md +78 -19
- {python_libei-0.2.0 → python_libei-0.3.0}/pyproject.toml +16 -1
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/__init__.py +4 -1
- python_libei-0.3.0/src/libei/portal.py +708 -0
- {python_libei-0.2.0 → python_libei-0.3.0/src/python_libei.egg-info}/PKG-INFO +82 -20
- {python_libei-0.2.0 → python_libei-0.3.0}/src/python_libei.egg-info/SOURCES.txt +3 -1
- {python_libei-0.2.0 → python_libei-0.3.0}/src/python_libei.egg-info/requires.txt +3 -0
- python_libei-0.3.0/tests/test_portal.py +632 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/LICENSE +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/setup.cfg +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/_capi/__init__.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/_capi/libei.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/_capi/libeis.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/_capi/liboeffis.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/_capi/loader.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/_cobject.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/ei.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/eis.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/oeffis.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/libei/py.typed +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/python_libei.egg-info/dependency_links.txt +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/src/python_libei.egg-info/top_level.txt +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/tests/test_cobject.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/tests/test_documentation_shape.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/tests/test_documented_examples.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/tests/test_ei_objects.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/tests/test_eis_objects.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/tests/test_integration_extras.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/tests/test_integration_socketpair.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/tests/test_loader.py +0 -0
- {python_libei-0.2.0 → python_libei-0.3.0}/tests/test_oeffis.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-libei
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
7
7
|
Project-URL: homepage, https://github.com/ctrondlp/python-libei
|
|
8
8
|
Project-URL: repository, https://github.com/ctrondlp/python-libei
|
|
9
9
|
Project-URL: issues, https://github.com/ctrondlp/python-libei/issues
|
|
10
|
+
Project-URL: changelog, https://github.com/ctrondlp/python-libei/blob/main/CHANGELOG.md
|
|
10
11
|
Keywords: wayland,libei,libeis,liboeffis,input,input-emulation,emulated-input,portal,xdg-desktop-portal,remote-desktop,automation,gui-testing,accessibility,ctypes
|
|
11
12
|
Classifier: Development Status :: 3 - Alpha
|
|
12
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -25,6 +26,8 @@ Classifier: Typing :: Typed
|
|
|
25
26
|
Requires-Python: >=3.10
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
27
28
|
License-File: LICENSE
|
|
29
|
+
Provides-Extra: portal
|
|
30
|
+
Requires-Dist: PyGObject>=3.42; extra == "portal"
|
|
28
31
|
Provides-Extra: dev
|
|
29
32
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
30
33
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
@@ -133,7 +136,7 @@ pointer.
|
|
|
133
136
|
|
|
134
137
|
## Status
|
|
135
138
|
|
|
136
|
-
Alpha (`0.
|
|
139
|
+
Alpha (`0.3.0`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
137
140
|
since `0.1.0`, and the API is not frozen — expect renames before 1.0. What
|
|
138
141
|
that qualifier covers, concretely:
|
|
139
142
|
|
|
@@ -144,9 +147,20 @@ that qualifier covers, concretely:
|
|
|
144
147
|
- Text input, touch cancellation, ping/pong, keymap transfer, region mapping
|
|
145
148
|
ids and `peek_event_type()` are each round-tripped through a real libeis
|
|
146
149
|
server in `tests/test_integration_extras.py`.
|
|
147
|
-
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
- Both portal paths (`libei.oeffis` and `libei.portal`) can only ever be
|
|
151
|
+
verified by hand, since they need an interactive consent dialog that
|
|
152
|
+
nothing here can drive automatically — `tests/test_portal.py` covers
|
|
153
|
+
`libei.portal`'s orchestration (raceless subscribe-before-call, the
|
|
154
|
+
`session_handle_token` crash workaround, persist_mode/restore_token)
|
|
155
|
+
against a fake D-Bus connection only. `libei.oeffis` was verified by hand
|
|
156
|
+
on 2026-08-25 (see [Troubleshooting](#troubleshooting)), and
|
|
157
|
+
`libei.portal` on 2026-09-01 against a real GNOME Wayland session
|
|
158
|
+
(`RemoteDesktop` v2): a first run raised the consent dialog and was
|
|
159
|
+
approved (5.4s), a second replaying the `restore_token` was granted with
|
|
160
|
+
no dialog at all (0.2s), three devices resumed on the returned fd
|
|
161
|
+
(relative pointer, keyboard, absolute pointer — in that order, the device
|
|
162
|
+
race `ei`-side callers must handle), and `Session.Close()` was exercised.
|
|
163
|
+
No input was injected — emulation is `libei.ei`'s job.
|
|
150
164
|
- Verified against libei 1.6.0 on Fedora 44 / GNOME 50.4, and against a
|
|
151
165
|
locally built 1.2.1 (130 passed, 4 skipped — the 1.4 and 1.6 features
|
|
152
166
|
gate themselves out). CI repeats the 1.2.1 run on Python 3.10-3.13, so
|
|
@@ -158,7 +172,7 @@ that qualifier covers, concretely:
|
|
|
158
172
|
| Instead of this | Why you might |
|
|
159
173
|
| --- | --- |
|
|
160
174
|
| [snegg](https://gitlab.freedesktop.org/whot/snegg) | The reference bindings, by libei's own author — closer to upstream, and first to get new API. Self-described as for "rapid prototyping" with an explicitly unstable API, and `import snegg.ei` fails outright where libei isn't installed. [`docs/vs-snegg.md`](docs/vs-snegg.md) covers the differences in detail. |
|
|
161
|
-
| The portal's D-Bus API directly (`
|
|
175
|
+
| The RemoteDesktop portal's own D-Bus API directly (`NotifyPointerMotion`, `NotifyKeyboardKeycode`, …), bypassing libei/EI entirely | `libei.portal` already gets you the D-Bus session and its `persist_mode`/`restore_token` handling — reach past libei entirely only if you don't want the EI protocol at all. The catch if you do: `NotifyPointerMotionAbsolute` needs a PipeWire stream id, which only exists after a second, separate ScreenCast consent dialog. libei has no such requirement. |
|
|
162
176
|
| `ydotool` and other `/dev/uinput` tools | Kernel-level, so they work under any compositor and need no portal session — at the cost of a privileged daemon, and of sidestepping the consent model that EI exists to enforce. |
|
|
163
177
|
|
|
164
178
|
## Requirements
|
|
@@ -166,6 +180,10 @@ that qualifier covers, concretely:
|
|
|
166
180
|
- Linux with a Wayland compositor (GNOME, KDE, Sway, …)
|
|
167
181
|
- CPython 3.10 or newer (tested on 3.13)
|
|
168
182
|
- The native libraries: on Fedora, `sudo dnf install libei libeis liboeffis`
|
|
183
|
+
- `libei.portal` only: PyGObject (`pip install 'python-libei[portal]'`), plus
|
|
184
|
+
whatever GObject-introspection libraries your distro needs for `Gio` --
|
|
185
|
+
PyPI's PyGObject wheel supplies the Python side only. Not needed for
|
|
186
|
+
`libei.ei`, `libei.eis` or `libei.oeffis`.
|
|
169
187
|
- libei 1.0.0 or newer for the core: connecting, binding a seat, and
|
|
170
188
|
sending pointer, button, keyboard, scroll and touch input all use symbols
|
|
171
189
|
that have existed with a stable signature since 1.0.0, and upstream keeps
|
|
@@ -301,23 +319,59 @@ several devices — see the absolute-positioning notes under
|
|
|
301
319
|
call and exposes no options dict, so the two things that make an approval
|
|
302
320
|
persist — `persist_mode` on `SelectDevices`, and the `restore_token` that
|
|
303
321
|
comes back on `Start` — are unreachable through it. This is a limitation of
|
|
304
|
-
the C library, not of these bindings;
|
|
322
|
+
the C library, not of these bindings; upstream's own docs say as much:
|
|
323
|
+
liboeffis is "intentionally kept simple, any more complex needs should be
|
|
324
|
+
handled by an application talking to DBus directly"
|
|
325
|
+
([source](https://libinput.pages.freedesktop.org/libei/api/group__liboeffis.html)).
|
|
305
326
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
327
|
+
`libei.portal` is that: the same `CreateSession` → `SelectDevices` → `Start`
|
|
328
|
+
→ `ConnectToEIS` sequence, driven directly over D-Bus (needs PyGObject —
|
|
329
|
+
`pip install 'python-libei[portal]'`), with `persist_mode`/`restore_token`
|
|
330
|
+
as real parameters:
|
|
309
331
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
332
|
+
```python
|
|
333
|
+
from libei import ei, portal
|
|
334
|
+
|
|
335
|
+
with portal.RemoteDesktopSession.negotiate(
|
|
336
|
+
devices=portal.DeviceType.POINTER,
|
|
337
|
+
persist_mode=portal.PersistMode.UNTIL_REVOKED,
|
|
338
|
+
restore_token=saved_token, # None on the first run
|
|
339
|
+
) as session:
|
|
340
|
+
save_somewhere(session.restore_token) # a fresh token every time -- save it
|
|
341
|
+
sender = ei.Sender.create_for_fd(session.eis_fd, name="my-app")
|
|
342
|
+
... # inject input for as long as the session is needed
|
|
343
|
+
```
|
|
315
344
|
|
|
316
345
|
Save the token somewhere durable and pass it back next time; the portal then
|
|
317
346
|
restores the session without prompting. Treat it as a credential — anyone
|
|
318
347
|
holding it can reopen input injection on that desktop, so it belongs
|
|
319
348
|
wherever you'd keep a password, and the decision to store it at all belongs
|
|
320
|
-
to the application rather than to
|
|
349
|
+
to the application rather than to this library, which never writes it
|
|
350
|
+
anywhere itself.
|
|
351
|
+
|
|
352
|
+
Save whatever comes back on **every** run, not just the first: the portal is
|
|
353
|
+
free to hand back a different token each time, and a caller that keeps only
|
|
354
|
+
the original would eventually present a stale one. (On GNOME the same token
|
|
355
|
+
comes back on each restore — that is one portal's behaviour, not a
|
|
356
|
+
guarantee.) Passing `restore_token` *without* a `persist_mode` raises
|
|
357
|
+
`ValueError`: the portal answers such a request with no token at all, so
|
|
358
|
+
storing what came back would write `None` over the token you just spent.
|
|
359
|
+
|
|
360
|
+
Three differences from `Oeffis` above worth knowing:
|
|
361
|
+
|
|
362
|
+
- **Blocking, not event-driven.** `negotiate()` runs its own nested
|
|
363
|
+
`GLib.MainLoop` per D-Bus round trip and returns only once connected, or
|
|
364
|
+
raises `PortalVersionError` / `PortalDeniedError` / `PortalTimeoutError`.
|
|
365
|
+
- **Bounded.** Each round trip gets `timeout` seconds (60 by default —
|
|
366
|
+
generous, since `Start` waits on a human answering a dialog). Without it a
|
|
367
|
+
portal that dies after accepting the call would wedge the calling thread
|
|
368
|
+
forever, which is the one thing `Oeffis`'s pollable fd protects against.
|
|
369
|
+
- **Close it.** The portal session lives in xdg-desktop-portal and outlives
|
|
370
|
+
the object unless `Session.Close()` is called — `Gio.bus_get_sync()` hands
|
|
371
|
+
back GLib's *shared* connection, so dropping the session tears nothing
|
|
372
|
+
down, and a long-running process that negotiates repeatedly accumulates
|
|
373
|
+
live sessions. The `with` block above handles it; otherwise call
|
|
374
|
+
`session.close()`.
|
|
321
375
|
|
|
322
376
|
## Sending input
|
|
323
377
|
|
|
@@ -646,6 +700,7 @@ the capability you bound (`seat.capabilities`).
|
|
|
646
700
|
| `libei.ei` | Clients: `Sender` (inject), `Receiver` (consume) |
|
|
647
701
|
| `libei.eis` | Servers: `Eis`, for compositors and for testing clients |
|
|
648
702
|
| `libei.oeffis` | Getting an EI fd from the desktop portal |
|
|
703
|
+
| `libei.portal` | The same, over D-Bus directly, with `persist_mode`/`restore_token` |
|
|
649
704
|
|
|
650
705
|
Each module has `is_available()`, an `Error` exception, and an `EventType` /
|
|
651
706
|
`DeviceCapability` enum. `ei` and `eis` also share the shapes around them:
|
|
@@ -670,6 +725,12 @@ them in this order -- each one only makes sense once the one below it does.
|
|
|
670
725
|
| [`_cobject.py`](src/libei/_cobject.py) | `CObject`: pointer ownership, refcounting, and the identity cache that every wrapper class inherits |
|
|
671
726
|
| [`ei.py`](src/libei/ei.py), [`eis.py`](src/libei/eis.py), [`oeffis.py`](src/libei/oeffis.py) | The public API: Python classes, enums and dataclasses over the raw calls |
|
|
672
727
|
|
|
728
|
+
[`portal.py`](src/libei/portal.py) sits outside this stack entirely -- there
|
|
729
|
+
is no C library behind it, so no `_capi` binding and no `CObject`. It talks
|
|
730
|
+
D-Bus directly through PyGObject (`Gio`/`GLib`, imported lazily the same way
|
|
731
|
+
the C libraries are loaded lazily) and only ever produces a plain fd, which
|
|
732
|
+
is where it hands off to `ei.Sender.create_for_fd()`.
|
|
733
|
+
|
|
673
734
|
**Read `_cobject.py` first.** It is the smallest file with the most
|
|
674
735
|
consequence: get `wrap()` vs `adopt()`, the `staticmethod()` wrapping of
|
|
675
736
|
`_ref_func`/`_unref_func`, or the `_wrappable` flag wrong and the failure is
|
|
@@ -734,16 +795,17 @@ Versions are SemVer and live in two places -- `pyproject.toml` and
|
|
|
734
795
|
`src/libei/__init__.py` -- which have to agree with each other and with the
|
|
735
796
|
tag. Nothing enforces that yet.
|
|
736
797
|
|
|
737
|
-
A release is an annotated, `v`-prefixed tag
|
|
798
|
+
A release is an annotated, `v`-prefixed tag. Pushing it is the whole of it;
|
|
799
|
+
PyPI is the only place a release is published, and no GitHub Release is cut:
|
|
738
800
|
|
|
739
801
|
```sh
|
|
740
802
|
git tag -a v0.2.0 -m "0.2.0"
|
|
741
803
|
git push origin v0.2.0
|
|
742
|
-
gh release create v0.2.0 --generate-notes --prerelease
|
|
743
804
|
```
|
|
744
805
|
|
|
745
|
-
|
|
746
|
-
|
|
806
|
+
While the API is unfrozen, the pre-release signal lives in the version
|
|
807
|
+
itself: a PEP 440 suffix (`0.2.0a1`) keeps a plain `pip install
|
|
808
|
+
python-libei` off it, and a `0.x` version already says the API can move.
|
|
747
809
|
|
|
748
810
|
Publishing runs from CI on a `v*` tag using PyPI
|
|
749
811
|
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC), so
|
|
@@ -100,7 +100,7 @@ pointer.
|
|
|
100
100
|
|
|
101
101
|
## Status
|
|
102
102
|
|
|
103
|
-
Alpha (`0.
|
|
103
|
+
Alpha (`0.3.0`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
104
104
|
since `0.1.0`, and the API is not frozen — expect renames before 1.0. What
|
|
105
105
|
that qualifier covers, concretely:
|
|
106
106
|
|
|
@@ -111,9 +111,20 @@ that qualifier covers, concretely:
|
|
|
111
111
|
- Text input, touch cancellation, ping/pong, keymap transfer, region mapping
|
|
112
112
|
ids and `peek_event_type()` are each round-tripped through a real libeis
|
|
113
113
|
server in `tests/test_integration_extras.py`.
|
|
114
|
-
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
- Both portal paths (`libei.oeffis` and `libei.portal`) can only ever be
|
|
115
|
+
verified by hand, since they need an interactive consent dialog that
|
|
116
|
+
nothing here can drive automatically — `tests/test_portal.py` covers
|
|
117
|
+
`libei.portal`'s orchestration (raceless subscribe-before-call, the
|
|
118
|
+
`session_handle_token` crash workaround, persist_mode/restore_token)
|
|
119
|
+
against a fake D-Bus connection only. `libei.oeffis` was verified by hand
|
|
120
|
+
on 2026-08-25 (see [Troubleshooting](#troubleshooting)), and
|
|
121
|
+
`libei.portal` on 2026-09-01 against a real GNOME Wayland session
|
|
122
|
+
(`RemoteDesktop` v2): a first run raised the consent dialog and was
|
|
123
|
+
approved (5.4s), a second replaying the `restore_token` was granted with
|
|
124
|
+
no dialog at all (0.2s), three devices resumed on the returned fd
|
|
125
|
+
(relative pointer, keyboard, absolute pointer — in that order, the device
|
|
126
|
+
race `ei`-side callers must handle), and `Session.Close()` was exercised.
|
|
127
|
+
No input was injected — emulation is `libei.ei`'s job.
|
|
117
128
|
- Verified against libei 1.6.0 on Fedora 44 / GNOME 50.4, and against a
|
|
118
129
|
locally built 1.2.1 (130 passed, 4 skipped — the 1.4 and 1.6 features
|
|
119
130
|
gate themselves out). CI repeats the 1.2.1 run on Python 3.10-3.13, so
|
|
@@ -125,7 +136,7 @@ that qualifier covers, concretely:
|
|
|
125
136
|
| Instead of this | Why you might |
|
|
126
137
|
| --- | --- |
|
|
127
138
|
| [snegg](https://gitlab.freedesktop.org/whot/snegg) | The reference bindings, by libei's own author — closer to upstream, and first to get new API. Self-described as for "rapid prototyping" with an explicitly unstable API, and `import snegg.ei` fails outright where libei isn't installed. [`docs/vs-snegg.md`](docs/vs-snegg.md) covers the differences in detail. |
|
|
128
|
-
| The portal's D-Bus API directly (`
|
|
139
|
+
| The RemoteDesktop portal's own D-Bus API directly (`NotifyPointerMotion`, `NotifyKeyboardKeycode`, …), bypassing libei/EI entirely | `libei.portal` already gets you the D-Bus session and its `persist_mode`/`restore_token` handling — reach past libei entirely only if you don't want the EI protocol at all. The catch if you do: `NotifyPointerMotionAbsolute` needs a PipeWire stream id, which only exists after a second, separate ScreenCast consent dialog. libei has no such requirement. |
|
|
129
140
|
| `ydotool` and other `/dev/uinput` tools | Kernel-level, so they work under any compositor and need no portal session — at the cost of a privileged daemon, and of sidestepping the consent model that EI exists to enforce. |
|
|
130
141
|
|
|
131
142
|
## Requirements
|
|
@@ -133,6 +144,10 @@ that qualifier covers, concretely:
|
|
|
133
144
|
- Linux with a Wayland compositor (GNOME, KDE, Sway, …)
|
|
134
145
|
- CPython 3.10 or newer (tested on 3.13)
|
|
135
146
|
- The native libraries: on Fedora, `sudo dnf install libei libeis liboeffis`
|
|
147
|
+
- `libei.portal` only: PyGObject (`pip install 'python-libei[portal]'`), plus
|
|
148
|
+
whatever GObject-introspection libraries your distro needs for `Gio` --
|
|
149
|
+
PyPI's PyGObject wheel supplies the Python side only. Not needed for
|
|
150
|
+
`libei.ei`, `libei.eis` or `libei.oeffis`.
|
|
136
151
|
- libei 1.0.0 or newer for the core: connecting, binding a seat, and
|
|
137
152
|
sending pointer, button, keyboard, scroll and touch input all use symbols
|
|
138
153
|
that have existed with a stable signature since 1.0.0, and upstream keeps
|
|
@@ -268,23 +283,59 @@ several devices — see the absolute-positioning notes under
|
|
|
268
283
|
call and exposes no options dict, so the two things that make an approval
|
|
269
284
|
persist — `persist_mode` on `SelectDevices`, and the `restore_token` that
|
|
270
285
|
comes back on `Start` — are unreachable through it. This is a limitation of
|
|
271
|
-
the C library, not of these bindings;
|
|
286
|
+
the C library, not of these bindings; upstream's own docs say as much:
|
|
287
|
+
liboeffis is "intentionally kept simple, any more complex needs should be
|
|
288
|
+
handled by an application talking to DBus directly"
|
|
289
|
+
([source](https://libinput.pages.freedesktop.org/libei/api/group__liboeffis.html)).
|
|
272
290
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
291
|
+
`libei.portal` is that: the same `CreateSession` → `SelectDevices` → `Start`
|
|
292
|
+
→ `ConnectToEIS` sequence, driven directly over D-Bus (needs PyGObject —
|
|
293
|
+
`pip install 'python-libei[portal]'`), with `persist_mode`/`restore_token`
|
|
294
|
+
as real parameters:
|
|
276
295
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
296
|
+
```python
|
|
297
|
+
from libei import ei, portal
|
|
298
|
+
|
|
299
|
+
with portal.RemoteDesktopSession.negotiate(
|
|
300
|
+
devices=portal.DeviceType.POINTER,
|
|
301
|
+
persist_mode=portal.PersistMode.UNTIL_REVOKED,
|
|
302
|
+
restore_token=saved_token, # None on the first run
|
|
303
|
+
) as session:
|
|
304
|
+
save_somewhere(session.restore_token) # a fresh token every time -- save it
|
|
305
|
+
sender = ei.Sender.create_for_fd(session.eis_fd, name="my-app")
|
|
306
|
+
... # inject input for as long as the session is needed
|
|
307
|
+
```
|
|
282
308
|
|
|
283
309
|
Save the token somewhere durable and pass it back next time; the portal then
|
|
284
310
|
restores the session without prompting. Treat it as a credential — anyone
|
|
285
311
|
holding it can reopen input injection on that desktop, so it belongs
|
|
286
312
|
wherever you'd keep a password, and the decision to store it at all belongs
|
|
287
|
-
to the application rather than to
|
|
313
|
+
to the application rather than to this library, which never writes it
|
|
314
|
+
anywhere itself.
|
|
315
|
+
|
|
316
|
+
Save whatever comes back on **every** run, not just the first: the portal is
|
|
317
|
+
free to hand back a different token each time, and a caller that keeps only
|
|
318
|
+
the original would eventually present a stale one. (On GNOME the same token
|
|
319
|
+
comes back on each restore — that is one portal's behaviour, not a
|
|
320
|
+
guarantee.) Passing `restore_token` *without* a `persist_mode` raises
|
|
321
|
+
`ValueError`: the portal answers such a request with no token at all, so
|
|
322
|
+
storing what came back would write `None` over the token you just spent.
|
|
323
|
+
|
|
324
|
+
Three differences from `Oeffis` above worth knowing:
|
|
325
|
+
|
|
326
|
+
- **Blocking, not event-driven.** `negotiate()` runs its own nested
|
|
327
|
+
`GLib.MainLoop` per D-Bus round trip and returns only once connected, or
|
|
328
|
+
raises `PortalVersionError` / `PortalDeniedError` / `PortalTimeoutError`.
|
|
329
|
+
- **Bounded.** Each round trip gets `timeout` seconds (60 by default —
|
|
330
|
+
generous, since `Start` waits on a human answering a dialog). Without it a
|
|
331
|
+
portal that dies after accepting the call would wedge the calling thread
|
|
332
|
+
forever, which is the one thing `Oeffis`'s pollable fd protects against.
|
|
333
|
+
- **Close it.** The portal session lives in xdg-desktop-portal and outlives
|
|
334
|
+
the object unless `Session.Close()` is called — `Gio.bus_get_sync()` hands
|
|
335
|
+
back GLib's *shared* connection, so dropping the session tears nothing
|
|
336
|
+
down, and a long-running process that negotiates repeatedly accumulates
|
|
337
|
+
live sessions. The `with` block above handles it; otherwise call
|
|
338
|
+
`session.close()`.
|
|
288
339
|
|
|
289
340
|
## Sending input
|
|
290
341
|
|
|
@@ -613,6 +664,7 @@ the capability you bound (`seat.capabilities`).
|
|
|
613
664
|
| `libei.ei` | Clients: `Sender` (inject), `Receiver` (consume) |
|
|
614
665
|
| `libei.eis` | Servers: `Eis`, for compositors and for testing clients |
|
|
615
666
|
| `libei.oeffis` | Getting an EI fd from the desktop portal |
|
|
667
|
+
| `libei.portal` | The same, over D-Bus directly, with `persist_mode`/`restore_token` |
|
|
616
668
|
|
|
617
669
|
Each module has `is_available()`, an `Error` exception, and an `EventType` /
|
|
618
670
|
`DeviceCapability` enum. `ei` and `eis` also share the shapes around them:
|
|
@@ -637,6 +689,12 @@ them in this order -- each one only makes sense once the one below it does.
|
|
|
637
689
|
| [`_cobject.py`](src/libei/_cobject.py) | `CObject`: pointer ownership, refcounting, and the identity cache that every wrapper class inherits |
|
|
638
690
|
| [`ei.py`](src/libei/ei.py), [`eis.py`](src/libei/eis.py), [`oeffis.py`](src/libei/oeffis.py) | The public API: Python classes, enums and dataclasses over the raw calls |
|
|
639
691
|
|
|
692
|
+
[`portal.py`](src/libei/portal.py) sits outside this stack entirely -- there
|
|
693
|
+
is no C library behind it, so no `_capi` binding and no `CObject`. It talks
|
|
694
|
+
D-Bus directly through PyGObject (`Gio`/`GLib`, imported lazily the same way
|
|
695
|
+
the C libraries are loaded lazily) and only ever produces a plain fd, which
|
|
696
|
+
is where it hands off to `ei.Sender.create_for_fd()`.
|
|
697
|
+
|
|
640
698
|
**Read `_cobject.py` first.** It is the smallest file with the most
|
|
641
699
|
consequence: get `wrap()` vs `adopt()`, the `staticmethod()` wrapping of
|
|
642
700
|
`_ref_func`/`_unref_func`, or the `_wrappable` flag wrong and the failure is
|
|
@@ -701,16 +759,17 @@ Versions are SemVer and live in two places -- `pyproject.toml` and
|
|
|
701
759
|
`src/libei/__init__.py` -- which have to agree with each other and with the
|
|
702
760
|
tag. Nothing enforces that yet.
|
|
703
761
|
|
|
704
|
-
A release is an annotated, `v`-prefixed tag
|
|
762
|
+
A release is an annotated, `v`-prefixed tag. Pushing it is the whole of it;
|
|
763
|
+
PyPI is the only place a release is published, and no GitHub Release is cut:
|
|
705
764
|
|
|
706
765
|
```sh
|
|
707
766
|
git tag -a v0.2.0 -m "0.2.0"
|
|
708
767
|
git push origin v0.2.0
|
|
709
|
-
gh release create v0.2.0 --generate-notes --prerelease
|
|
710
768
|
```
|
|
711
769
|
|
|
712
|
-
|
|
713
|
-
|
|
770
|
+
While the API is unfrozen, the pre-release signal lives in the version
|
|
771
|
+
itself: a PEP 440 suffix (`0.2.0a1`) keeps a plain `pip install
|
|
772
|
+
python-libei` off it, and a `0.x` version already says the API can move.
|
|
714
773
|
|
|
715
774
|
Publishing runs from CI on a `v*` tag using PyPI
|
|
716
775
|
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC), so
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-libei"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
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"
|
|
@@ -50,12 +50,18 @@ classifiers = [
|
|
|
50
50
|
dependencies = []
|
|
51
51
|
|
|
52
52
|
[project.optional-dependencies]
|
|
53
|
+
# libei.portal negotiates the RemoteDesktop portal directly over D-Bus, which
|
|
54
|
+
# needs PyGObject for Gio/GLib. The native GObject-introspection libraries
|
|
55
|
+
# still have to come from the distro -- PyPI's PyGObject wheel only supplies
|
|
56
|
+
# the Python side. Not needed for libei.ei/libei.eis/libei.oeffis.
|
|
57
|
+
portal = ["PyGObject>=3.42"]
|
|
53
58
|
dev = ["pytest>=7", "ruff>=0.6", "mypy>=1.11"]
|
|
54
59
|
|
|
55
60
|
[project.urls]
|
|
56
61
|
homepage = "https://github.com/ctrondlp/python-libei"
|
|
57
62
|
repository = "https://github.com/ctrondlp/python-libei"
|
|
58
63
|
issues = "https://github.com/ctrondlp/python-libei/issues"
|
|
64
|
+
changelog = "https://github.com/ctrondlp/python-libei/blob/main/CHANGELOG.md"
|
|
59
65
|
|
|
60
66
|
[tool.setuptools.packages.find]
|
|
61
67
|
where = ["src"]
|
|
@@ -95,3 +101,12 @@ strict = true
|
|
|
95
101
|
# Every property in ei.py/eis.py/oeffis.py re-types a raw C call's result,
|
|
96
102
|
# so this fires everywhere in the wrapper layer for no actionable reason.
|
|
97
103
|
warn_return_any = false
|
|
104
|
+
|
|
105
|
+
# PyGObject is an optional extra (see [project.optional-dependencies].portal),
|
|
106
|
+
# so `gi` may or may not be importable where mypy runs. Without this, the two
|
|
107
|
+
# error codes differ by environment -- import-untyped when PyGObject is
|
|
108
|
+
# installed, import-not-found when it isn't -- and an inline ignore pinned to
|
|
109
|
+
# either one fails in the other. libei.portal already treats gi as Any.
|
|
110
|
+
[[tool.mypy.overrides]]
|
|
111
|
+
module = ["gi.*"]
|
|
112
|
+
ignore_missing_imports = true
|
|
@@ -13,6 +13,9 @@ Use the submodules directly:
|
|
|
13
13
|
drive a test harness for the ``ei`` module without a real compositor)
|
|
14
14
|
- :mod:`libei.oeffis` -- negotiate an EI connection through the
|
|
15
15
|
``org.freedesktop.portal.RemoteDesktop`` XDG desktop portal
|
|
16
|
+
- :mod:`libei.portal` -- negotiate that same portal directly over D-Bus
|
|
17
|
+
instead, for ``persist_mode``/``restore_token`` support liboeffis's C API
|
|
18
|
+
doesn't expose
|
|
16
19
|
|
|
17
20
|
Scope, in short: this needs a compositor speaking EI/EIS -- there is no X11
|
|
18
21
|
fallback. Pointer (relative and absolute), button, keyboard, scroll, touch
|
|
@@ -26,6 +29,6 @@ the full breakdown, including which features need which libei version.
|
|
|
26
29
|
Alpha: the API is not frozen.
|
|
27
30
|
"""
|
|
28
31
|
|
|
29
|
-
__version__ = "0.
|
|
32
|
+
__version__ = "0.3.0"
|
|
30
33
|
|
|
31
34
|
__all__ = ["__version__"]
|