python-libei 0.4.1__tar.gz → 0.5.1__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.
Files changed (36) hide show
  1. python_libei-0.5.1/PKG-INFO +419 -0
  2. python_libei-0.5.1/README.md +382 -0
  3. {python_libei-0.4.1 → python_libei-0.5.1}/pyproject.toml +2 -1
  4. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/__init__.py +4 -2
  5. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/portal.py +719 -4
  6. python_libei-0.5.1/src/python_libei.egg-info/PKG-INFO +419 -0
  7. {python_libei-0.4.1 → python_libei-0.5.1}/src/python_libei.egg-info/SOURCES.txt +1 -0
  8. {python_libei-0.4.1 → python_libei-0.5.1}/tests/test_cobject.py +13 -4
  9. python_libei-0.5.1/tests/test_inputcapture.py +649 -0
  10. {python_libei-0.4.1 → python_libei-0.5.1}/tests/test_portal.py +6 -1
  11. python_libei-0.4.1/PKG-INFO +0 -848
  12. python_libei-0.4.1/README.md +0 -812
  13. python_libei-0.4.1/src/python_libei.egg-info/PKG-INFO +0 -848
  14. {python_libei-0.4.1 → python_libei-0.5.1}/LICENSE +0 -0
  15. {python_libei-0.4.1 → python_libei-0.5.1}/setup.cfg +0 -0
  16. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/_capi/__init__.py +0 -0
  17. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/_capi/libei.py +0 -0
  18. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/_capi/libeis.py +0 -0
  19. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/_capi/liboeffis.py +0 -0
  20. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/_capi/loader.py +0 -0
  21. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/_cobject.py +0 -0
  22. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/ei.py +0 -0
  23. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/eis.py +0 -0
  24. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/oeffis.py +0 -0
  25. {python_libei-0.4.1 → python_libei-0.5.1}/src/libei/py.typed +0 -0
  26. {python_libei-0.4.1 → python_libei-0.5.1}/src/python_libei.egg-info/dependency_links.txt +0 -0
  27. {python_libei-0.4.1 → python_libei-0.5.1}/src/python_libei.egg-info/requires.txt +0 -0
  28. {python_libei-0.4.1 → python_libei-0.5.1}/src/python_libei.egg-info/top_level.txt +0 -0
  29. {python_libei-0.4.1 → python_libei-0.5.1}/tests/test_documentation_shape.py +0 -0
  30. {python_libei-0.4.1 → python_libei-0.5.1}/tests/test_documented_examples.py +0 -0
  31. {python_libei-0.4.1 → python_libei-0.5.1}/tests/test_ei_objects.py +0 -0
  32. {python_libei-0.4.1 → python_libei-0.5.1}/tests/test_eis_objects.py +0 -0
  33. {python_libei-0.4.1 → python_libei-0.5.1}/tests/test_integration_extras.py +0 -0
  34. {python_libei-0.4.1 → python_libei-0.5.1}/tests/test_integration_socketpair.py +0 -0
  35. {python_libei-0.4.1 → python_libei-0.5.1}/tests/test_loader.py +0 -0
  36. {python_libei-0.4.1 → python_libei-0.5.1}/tests/test_oeffis.py +0 -0
@@ -0,0 +1,419 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-libei
3
+ Version: 0.5.1
4
+ Summary: Inject and receive input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis
5
+ Author: Dennis K. Paulsen
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://github.com/ctrondlp/python-libei
8
+ Project-URL: repository, https://github.com/ctrondlp/python-libei
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
11
+ Keywords: wayland,libei,libeis,liboeffis,input,input-emulation,emulated-input,portal,xdg-desktop-portal,remote-desktop,automation,gui-testing,accessibility,ctypes
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX :: BSD :: FreeBSD
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Desktop Environment
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Topic :: Software Development :: Testing
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Provides-Extra: portal
31
+ Requires-Dist: PyGObject>=3.42; extra == "portal"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7; extra == "dev"
34
+ Requires-Dist: ruff>=0.6; extra == "dev"
35
+ Requires-Dist: mypy>=1.11; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # python-libei
39
+
40
+ Python bindings for [libei, libeis and liboeffis](https://libinput.pages.freedesktop.org/libei/) —
41
+ the Wayland input-emulation libraries. Use this to **move the pointer, click,
42
+ type, or scroll on a Wayland desktop** from Python, the way `xdotool` did on
43
+ X11.
44
+
45
+ Pure ctypes, no build step, no dependencies.
46
+
47
+ ```python
48
+ device.start_emulating().pointer_motion(5, 0).frame().stop_emulating()
49
+ ```
50
+
51
+ **The one concept:** events queue up, and **`frame()` is what sends them** as
52
+ one logical hardware event. Forget it and nothing happens — no exception, no
53
+ warning, no movement. Fill the package, then post it.
54
+
55
+ New here? [docs/getting-started.md](docs/getting-started.md) is install
56
+ through a first real pointer motion, in five minutes.
57
+
58
+ ## What it's for
59
+
60
+ Driving a Wayland desktop from Python, when you need real input events rather
61
+ than a widget-tree back door:
62
+
63
+ - **GUI test automation** — click and type at an application the way a user
64
+ does, against the real compositor.
65
+ - **Remote desktop and screen sharing** — inject the remote user's input into
66
+ the local session.
67
+ - **Accessibility tooling** — on-screen keyboards, dwell clicking, alternative
68
+ pointing devices.
69
+ - **Macros and scripting** — the `xdotool`-shaped jobs that stopped working
70
+ when the desktop moved off X11.
71
+ - **Compositor and protocol work** — `libei.eis` is the server half of the
72
+ protocol, so an EIS server (or a test double for one) can be written in
73
+ Python too.
74
+
75
+ ### What it isn't
76
+
77
+ - **Not an X11 tool.** This speaks the EI/EIS protocol to a compositor that
78
+ implements it. On an X server there is nothing to talk to, and nothing here
79
+ falls back to XTEST — despite the `xdotool` comparison above, it is not a
80
+ drop-in replacement for one.
81
+ - **Keyboard input is key positions, not characters.** `keyboard_key()`
82
+ takes Linux evdev *keycodes*, and what character one produces is the
83
+ compositor's layout to decide. `text_utf8()` does send characters
84
+ directly, but only against libei 1.6 with a TEXT-capable device — see
85
+ [Keys are positions, not characters](docs/recipes.md#keyboard-positions-not-characters).
86
+ - **Not a screen-reading library.** libei is input only. Pair it with the
87
+ ScreenCast portal and PipeWire if you also need pixels.
88
+ - **Not a way around user consent.** A real session goes through the portal's
89
+ consent dialog, by design. Input that must bypass that prompt belongs at the
90
+ kernel layer (`/dev/uinput`) instead — a different tool and a different
91
+ trust model.
92
+
93
+ ## Which API do I need?
94
+
95
+ Five modules, and most callers need exactly two of them: `oeffis` or `portal`
96
+ to get permission, then `ei` to inject.
97
+
98
+ | You want to… | Use | Notes |
99
+ | --- | --- | --- |
100
+ | **Inject input** into a desktop | `libei.ei` → `Sender` | The automation case. This is what the quick start below does |
101
+ | **Consume input** from a compositor | `libei.ei` → `Receiver` | Compositor-side or input-capture code; same connection dance |
102
+ | **Get permission**, simply | `libei.oeffis` | One call, pollable fd, no dependencies. The consent dialog appears on **every** run |
103
+ | **Get permission**, and not be asked again | `libei.portal` | Same handshake over D-Bus directly, with `persist_mode` / `restore_token`. Needs PyGObject |
104
+ | **Be the server**, for tests or a compositor | `libei.eis` | Drives your client code with no real compositor and no consent dialog |
105
+
106
+ Each module has `is_available()`, an `Error` exception, and an `EventType` /
107
+ `DeviceCapability` enum. `ei` and `eis` also share the shapes around them:
108
+ `Device`, `Seat`, `Region`, `Keymap`, `Touch`, `Ping`, `Event`, and the frozen
109
+ dataclasses its accessors return. The package ships `py.typed`, so callers
110
+ type-check against real annotations rather than `Any`.
111
+
112
+ ## What's implemented
113
+
114
+ Injection covers the input types most automation needs.
115
+
116
+ | `DeviceCapability` | What you get |
117
+ | --- | --- |
118
+ | `POINTER` | `pointer_motion()` |
119
+ | `POINTER_ABSOLUTE` | `pointer_motion_absolute()`, `device.regions` |
120
+ | `BUTTON` | `button()` |
121
+ | `KEYBOARD` | `keyboard_key()`, `device.keymap`, `KEYBOARD_MODIFIERS` events |
122
+ | `SCROLL` | `scroll_delta()`, `scroll_discrete()`, `scroll_stop()`, `scroll_cancel()` |
123
+ | `TOUCH` | `device.touch_new()` → `down()` / `motion()` / `up()` |
124
+ | `TEXT` | `text_utf8()`, `text_keysym()`, `TEXT_UTF8` / `TEXT_KEYSYM` events (libei 1.6+) |
125
+
126
+ ### Recognized, but not in any released libei
127
+
128
+ | `DeviceCapability` | State |
129
+ | --- | --- |
130
+ | `GESTURES` | Exists on libei's `main` branch only. **Binding it against a shipping library silently does nothing** — no error, no device, no events |
131
+ | `STYLUS` | Same |
132
+
133
+ These are a different case from the table above, and worth stating separately
134
+ because a skimming reader could otherwise take them as supported. 1.6.0's own
135
+ `enum ei_device_capability` stops at `TEXT`, and its `enum ei_event_type`
136
+ stops at `EI_EVENT_TEXT_UTF8` — so the swipe/pinch/hold/stylus members of
137
+ `EventType` cannot arrive either. The values here match upstream `main`
138
+ exactly, so they are ready for whatever release adds them. The 22
139
+ gesture/stylus accessor functions `main` adds are deliberately not bound:
140
+ nothing that ships today exports them, so nothing here could be verified
141
+ against a real library, which is the bar every other binding in this package
142
+ was held to.
143
+
144
+ `EventType` otherwise mirrors libei's enum in full, and any event type can be
145
+ *identified* and released safely whether or not it has an accessor.
146
+
147
+ Beyond sending input, the wrapper also covers ping/pong round trips
148
+ (`Context.new_ping()`), touch cancellation (`Touch.cancel()`), keymap transfer
149
+ (`Device.keymap`), region mapping ids and coordinate conversion,
150
+ `Context.disconnect()`, `Context.peek_event_type()`, and
151
+ `Seat.request_device()`. On the server side, `libei.eis` mirrors all of it and
152
+ adds `Eis.set_flag()` and `Client.pid`. Underneath, the ctypes layer binds 250
153
+ of the 302 functions the three libraries export as of 1.6.0; what is left out,
154
+ and why, is in
155
+ [docs/developers/architecture.md](docs/developers/architecture.md#what-is-bound-and-what-is-deliberately-not).
156
+
157
+ ## Status
158
+
159
+ Beta (`0.5.1`), published on [PyPI](https://pypi.org/project/python-libei/)
160
+ since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
161
+
162
+ The injection path is exercised end to end against the real libraries by the
163
+ integration tests, and is in use as the Wayland input backend of a separate
164
+ GUI-automation project. Both portal paths can only ever be verified by hand,
165
+ since they need a consent dialog nothing can drive automatically; both have
166
+ been, most recently `libei.portal` against a real GNOME Wayland session on
167
+ 2026-09-01. Development is against libei 1.6.0, and CI runs the suite against
168
+ Ubuntu's 1.2.1 so the 1.0.0 floor is exercised on a real old build rather than
169
+ asserted.
170
+
171
+ Exactly what was run, when, and against which versions:
172
+ [docs/developers/verification.md](docs/developers/verification.md).
173
+
174
+ ## Alternatives
175
+
176
+ | Instead of this | Why you might |
177
+ | --- | --- |
178
+ | [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. |
179
+ | 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. |
180
+ | `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. |
181
+
182
+ ## Requirements
183
+
184
+ - Linux or FreeBSD with a Wayland compositor (GNOME, KDE, Sway, …).
185
+ Nothing here is kernel-specific -- it is pure ctypes over the native
186
+ libraries, with no syscall the C library doesn't already abstract. The
187
+ portal paths are the part likeliest to come up short off Linux, since
188
+ they need an xdg-desktop-portal RemoteDesktop backend to talk to.
189
+ - CPython 3.10 or newer (tested on 3.13)
190
+ - The native libraries: on Fedora, `sudo dnf install libei libeis liboeffis`;
191
+ on FreeBSD, `pkg install libei` (the `x11/libei` port), which supplies all
192
+ three sonames including `liboeffis`
193
+ - `libei.portal` only: PyGObject (`pip install 'python-libei[portal]'`), plus
194
+ whatever GObject-introspection libraries your distro needs for `Gio` --
195
+ PyPI's PyGObject wheel supplies the Python side only. Not needed for
196
+ `libei.ei`, `libei.eis` or `libei.oeffis`.
197
+ - libei 1.0.0 or newer for the core: connecting, binding a seat, and
198
+ sending pointer, button, keyboard, scroll and touch input all use symbols
199
+ that have existed with a stable signature since 1.0.0, and upstream keeps
200
+ API/ABI back-compatible within the 1.x series. Only 1.5.0 and 1.6.0 have
201
+ actually been run against -- 1.6.0 on both Fedora 44 and FreeBSD 15, where
202
+ the injection path passes the full suite with nothing skipped.
203
+
204
+ Newer libei buys you more, per feature:
205
+
206
+ | Needs | For |
207
+ | --- | --- |
208
+ | 1.1 | `Region.mapping_id`, `Region.convert_point()`, `Device.region_at()` |
209
+ | 1.4 | ping/pong round trips (`Context.new_ping()`), `Context.disconnect()`, touch cancellation (`Touch.cancel()`) |
210
+ | 1.5 | `eis.Client.pid` |
211
+ | 1.6 | `Device.text_utf8()` / `text_keysym()` and TEXT events, `Seat.request_device()`, `Eis.set_flag()` |
212
+
213
+ Nothing is resolved until it is called, so a build without one of these
214
+ costs you only that call — it raises `LibraryNotFoundError` naming the
215
+ missing symbol, while the rest of the package keeps working. The one
216
+ exception is `Event.touch_up_event`, which degrades instead of raising:
217
+ on libei older than 1.4 it reports `is_cancel=False`, since a library
218
+ with no notion of cancellation genuinely never sends one.
219
+
220
+ These versions were established by building libei 1.2.1 and diffing its
221
+ exported symbols against every binding here, not by reading `@since`
222
+ annotations — three of them are missing upstream, and taking their
223
+ absence to mean 1.0 got touch cancellation wrong by four releases.
224
+
225
+ ## Install
226
+
227
+ From [PyPI](https://pypi.org/project/python-libei/):
228
+
229
+ ```sh
230
+ pip install python-libei
231
+ ```
232
+
233
+ The distribution is named `python-libei`, the import is `libei` -- so
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:
243
+
244
+ ```sh
245
+ git clone https://github.com/ctrondlp/python-libei.git
246
+ cd python-libei
247
+ pip install . # or `pip install -e '.[dev]'` to develop
248
+ ```
249
+
250
+ Importing is always safe, even where the native libraries are missing — they
251
+ are loaded on first use, not at import. Check before you rely on them:
252
+
253
+ ```python
254
+ from libei import ei
255
+
256
+ if not ei.is_available():
257
+ ... # fall back to another input backend
258
+ ```
259
+
260
+ ## Concepts
261
+
262
+ Five terms are enough to read the rest of this file:
263
+
264
+ | Term | Meaning |
265
+ | --- | --- |
266
+ | **Sender** | A client that *injects* input. This is what you want for automation. |
267
+ | **Receiver** | A client that *consumes* input. For compositor-side code. |
268
+ | **Seat** | A group of input devices, offered by the compositor. You ask it for the capabilities you need. |
269
+ | **Capability** | What kind of input you want: `POINTER`, `KEYBOARD`, `TOUCH`, `SCROLL`, `BUTTON`, … |
270
+ | **Device** | What you actually send events through, handed to you after you bind a capability. |
271
+
272
+ The flow is always the same: **connect → bind a capability on a seat → wait
273
+ for a device → send events through it.**
274
+
275
+ ## Quick start
276
+
277
+ Getting an EI connection means asking the desktop portal, which shows the user
278
+ a consent dialog. After that you have a fd, and everything else is the same
279
+ regardless of how you got it.
280
+
281
+ **The dialog comes back every run** with `libei.oeffis`, which exposes no way
282
+ to persist an approval. If being prompted once per launch is unacceptable for
283
+ what you're building, use `libei.portal` instead —
284
+ [Avoiding the consent dialog on every run](docs/recipes.md#avoiding-the-consent-dialog-on-every-run).
285
+
286
+ ```python
287
+ import select
288
+ from libei import ei, oeffis
289
+
290
+ # 1. Ask the portal for permission. The user sees a consent dialog.
291
+ session = oeffis.Oeffis.create(devices=oeffis.DeviceType.POINTER)
292
+ while True:
293
+ ready, _, _ = select.select([session.fd], [], [], 30)
294
+ if not ready:
295
+ raise TimeoutError("portal request timed out")
296
+ if session.dispatch():
297
+ break # session.eis_fd is now valid
298
+
299
+ # 2. Connect as a sender.
300
+ sender = ei.Sender.create_for_fd(session.eis_fd, name="my-app")
301
+
302
+ # 3. Ask for a pointer, and wait for the compositor to hand one over.
303
+ device = None
304
+ while device is None:
305
+ select.select([sender.fd], [], [], 5)
306
+ sender.dispatch() # events stays empty until dispatch() reads the socket
307
+ for event in sender.events:
308
+ if event.event_type is ei.EventType.SEAT_ADDED:
309
+ event.seat.bind((ei.DeviceCapability.POINTER,))
310
+ elif event.event_type is ei.EventType.DEVICE_RESUMED:
311
+ device = event.device
312
+
313
+ # 4. Send input.
314
+ device.start_emulating().pointer_motion(5, 0).frame().stop_emulating()
315
+ ```
316
+
317
+ Wait for `DEVICE_RESUMED`, not `DEVICE_ADDED` — a device arrives paused, and
318
+ libei calls sending events before it resumes "a client bug".
319
+
320
+ This loop takes the first device to resume, which is fine here because only
321
+ `POINTER` was bound. Bind more than one capability and a seat may resume
322
+ several devices — see
323
+ [Picking the right device](docs/recipes.md#picking-the-right-device-when-several-resume)
324
+ before reusing this pattern.
325
+
326
+ ## Sending input
327
+
328
+ Every burst of input is wrapped in `start_emulating()` … `frame()` …
329
+ `stop_emulating()`. `frame()` is what actually commits the queued events as one
330
+ logical hardware event; without it nothing is delivered. Each method returns
331
+ the device, so they chain.
332
+
333
+ ```python
334
+ # Move the pointer 10px right, 5px down
335
+ device.start_emulating().pointer_motion(10, 5).frame().stop_emulating()
336
+
337
+ # Left click (BTN_LEFT; codes are Linux input codes, from
338
+ # <linux/input-event-codes.h>)
339
+ BTN_LEFT = 0x110
340
+ device.start_emulating()
341
+ device.button(BTN_LEFT, True).frame()
342
+ device.button(BTN_LEFT, False).frame()
343
+ device.stop_emulating()
344
+
345
+ # Press the A key (KEY_A -- a key *position*, not the character "a")
346
+ KEY_A = 30
347
+ device.start_emulating()
348
+ device.keyboard_key(KEY_A, True).frame()
349
+ device.keyboard_key(KEY_A, False).frame()
350
+ device.stop_emulating()
351
+
352
+ # Scroll: smooth (logical pixels) or discrete (one detent is 120)
353
+ device.start_emulating().scroll_delta(0, 20).frame().stop_emulating()
354
+ device.start_emulating().scroll_discrete(0, 120).frame().stop_emulating()
355
+ ```
356
+
357
+ Keyboards need care, because a keycode is a key *position* and the character
358
+ it produces is the layout's business — `KEY_A = 30` types something else under
359
+ AZERTY. Absolute positioning needs the `POINTER_ABSOLUTE` capability and
360
+ coordinates inside one of `device.regions`, and binding it alongside `POINTER`
361
+ is what produces two devices where order cannot be trusted. Touch has its own
362
+ short-lived object rather than going through the device.
363
+
364
+ All four, with working code:
365
+ [docs/recipes.md](docs/recipes.md#pointer-buttons-and-scrolling).
366
+
367
+ ## Things that will bite you
368
+
369
+ - **`dispatch()` before `events`.** `events` drains only what is already
370
+ queued; it yields nothing until `dispatch()` has read from the socket.
371
+ - **Don't keep an event past its loop iteration.** Each event is released as
372
+ soon as the loop moves on, and using it afterwards raises `RuntimeError`.
373
+ Objects you pull *off* an event (`event.device`, `event.seat`) are safe to
374
+ keep — copy out `event.pointer_event` and friends rather than the event.
375
+ - **`frame()` or nothing happens.** Events queue up until a `frame()` commits
376
+ them.
377
+ - **`bind()` needs at least one capability.** Binding an empty set sends
378
+ nothing, so the device you are waiting for never arrives; this raises
379
+ `ValueError` rather than hanging.
380
+ - **Capabilities are per-seat.** A seat only offers some; check
381
+ `seat.capabilities` before binding.
382
+ - **One seat can resume several devices.** Bind both `POINTER` and
383
+ `POINTER_ABSOLUTE` and GNOME gives you two, relative first. Taking
384
+ whichever resumes first is a coin flip — select on `device.capabilities`
385
+ instead. Sending an event the device lacks the capability for is silently
386
+ ignored, which makes this look like the injection simply not working.
387
+ - **Read the accessor that matches the event type.** `event.key_event` on a
388
+ `POINTER_MOTION` event raises `TypeError` naming both types. libei itself
389
+ would have returned `KeyEvent(key=0, is_press=False)` — a real-looking
390
+ value — while logging a `Bug:` line the caller never sees, so branch on
391
+ `event_type` first. `TOUCH_UP` has its own `touch_up_event`, since it
392
+ carries no coordinates.
393
+ - **`GESTURES` and `STYLUS` are not in any released libei.** They match
394
+ upstream `main` and are here ready for it, but 1.6.0's capability enum
395
+ stops at `TEXT`. Binding them against a shipping library silently does
396
+ nothing — no error, no device, no events.
397
+
398
+ Nearly all of these fail *silently*, which is why
399
+ [docs/troubleshooting.md](docs/troubleshooting.md) is a checklist rather than
400
+ a list of error messages. Start there when nothing happens.
401
+
402
+ ## Documentation
403
+
404
+ - [docs/getting-started.md](docs/getting-started.md) — install through a first
405
+ pointer motion, in five minutes
406
+ - [docs/recipes.md](docs/recipes.md) — keyboards, touch, absolute positioning,
407
+ consent persistence, receiver mode, running your own EIS server, logging
408
+ - [docs/troubleshooting.md](docs/troubleshooting.md) — the "when nothing
409
+ happens" checklist
410
+ - [docs/vs-snegg.md](docs/vs-snegg.md) — how this differs from the reference
411
+ bindings, and two signature issues found by cross-checking the C source
412
+ - [docs/developers/](docs/developers/) — the four-layer architecture, and what
413
+ has actually been verified against which libei versions
414
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — setup, checks, testing against an old
415
+ libei, releasing
416
+
417
+ ## License
418
+
419
+ MIT.