python-libei 0.5.0__py3-none-any.whl → 0.5.1__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 CHANGED
@@ -31,6 +31,6 @@ the full breakdown, including which features need which libei version.
31
31
  Beta: the API is not frozen.
32
32
  """
33
33
 
34
- __version__ = "0.5.0"
34
+ __version__ = "0.5.1"
35
35
 
36
36
  __all__ = ["__version__"]
libei/portal.py CHANGED
@@ -926,6 +926,53 @@ def _input_capture_version(
926
926
  return int(version)
927
927
 
928
928
 
929
+ def _create_input_capture_session_v1(
930
+ connection: Any,
931
+ Gio: Any,
932
+ GLib: Any,
933
+ busname: str,
934
+ types: int,
935
+ timeout: float,
936
+ ) -> str:
937
+ """Create an InputCapture session on a portal that predates v2.
938
+
939
+ v1 has no ``Start`` at all -- that method, like ``CreateSession2``, was
940
+ added in version 2. Here ``CreateSession`` itself carries
941
+ ``capabilities`` and raises the consent dialog, and the session is ready
942
+ for ``GetZones`` / ``SetPointerBarriers`` / ``Enable`` the moment its
943
+ ``Response`` arrives. ``ConnectToEIS`` is a v1 original, so the rest of
944
+ this class works against such a portal unchanged.
945
+
946
+ Returns the session handle. ``persist_mode`` and ``restore_token`` have
947
+ no v1 equivalent -- both are documented as version 2 additions to
948
+ ``Start``, which does not exist here -- so the caller must reject them
949
+ before getting this far rather than have them silently ignored.
950
+ """
951
+ code, results = _request(
952
+ connection,
953
+ Gio,
954
+ GLib,
955
+ busname,
956
+ _INPUT_CAPTURE,
957
+ "CreateSession",
958
+ "(sa{sv})",
959
+ ("",),
960
+ {
961
+ "session_handle_token": GLib.Variant("s", uuid.uuid4().hex),
962
+ "capabilities": GLib.Variant("u", int(types)),
963
+ },
964
+ timeout,
965
+ )
966
+ if code != 0:
967
+ raise PortalDeniedError(
968
+ "CreateSession", "the user declined the input-capture consent dialog"
969
+ )
970
+ session_handle = results.get("session_handle")
971
+ if not isinstance(session_handle, str):
972
+ raise PortalError("CreateSession returned no session_handle")
973
+ return session_handle
974
+
975
+
929
976
  def _wait_for_signal(
930
977
  connection: Any,
931
978
  Gio: Any,
@@ -1392,11 +1439,18 @@ class InputCaptureSession:
1392
1439
  Blocks until ``CreateSession2`` -> ``Start`` -> ``ConnectToEIS``
1393
1440
  resolves, prompting the user for consent along the way unless
1394
1441
  ``restore_token`` lets the portal skip that. Raises
1395
- :class:`PortalVersionError` if the compositor's InputCapture portal
1396
- predates ``CreateSession2`` (needs v2+ -- this module never speaks
1397
- the deprecated v1 ``CreateSession``), :class:`PortalDeniedError` if
1398
- ``Start`` is declined, and :class:`PortalTimeoutError` if any one
1399
- round trip exceeds ``timeout`` seconds.
1442
+ :class:`PortalDeniedError` if consent is declined, and
1443
+ :class:`PortalTimeoutError` if any one round trip exceeds ``timeout``
1444
+ seconds.
1445
+
1446
+ A portal older than v2 has neither ``CreateSession2`` nor ``Start``,
1447
+ and is negotiated through the deprecated v1 ``CreateSession``
1448
+ instead -- one call that carries ``capabilities`` and raises the
1449
+ consent dialog itself. Everything after negotiation is identical;
1450
+ ``ConnectToEIS`` is a v1 original. The one thing v1 cannot do is
1451
+ persist: ``persist_mode`` and ``restore_token`` were added to
1452
+ ``Start``, so passing either against such a portal raises
1453
+ :class:`PortalVersionError` rather than being quietly ignored.
1400
1454
 
1401
1455
  Returns before anything is actually captured: :meth:`set_pointer_barriers`
1402
1456
  and :meth:`enable` still have to be called, and even then nothing
@@ -1435,62 +1489,95 @@ class InputCaptureSession:
1435
1489
  raise PortalError(f"cannot reach the session bus: {exc}") from exc
1436
1490
 
1437
1491
  version = _input_capture_version(connection, Gio, GLib, busname, timeout)
1438
- if version < _MIN_INPUT_CAPTURE_VERSION:
1492
+ # A version below 2 is not a portal to give up on -- it is a portal
1493
+ # that speaks the older half of the same interface. Only
1494
+ # `CreateSession2` and `Start` are version 2 additions; `GetZones`,
1495
+ # `SetPointerBarriers`, `Enable`, `Disable`, `Release` and, crucially,
1496
+ # `ConnectToEIS` are all v1 originals, so everything this class does
1497
+ # after negotiation works either way and only session creation forks.
1498
+ #
1499
+ # This is not hypothetical. xdg-desktop-portal-gnome 50 reports
1500
+ # version 0 -- it registers the full impl interface and never sets the
1501
+ # property -- and calling `CreateSession2` against it fails with
1502
+ # `UnknownMethod`, while v1 `CreateSession` is dispatched normally.
1503
+ # Note that its introspection XML advertises `CreateSession2` anyway:
1504
+ # that XML is static, so it says nothing about what the frontend will
1505
+ # actually dispatch. The `version` property is the only usable signal,
1506
+ # which is why it is read first and believed.
1507
+ legacy = version < _MIN_INPUT_CAPTURE_VERSION
1508
+
1509
+ if legacy and (persist_mode != PersistMode.NONE or restore_token is not None):
1439
1510
  raise PortalVersionError(
1440
- f"InputCapture version {version} is too old for "
1441
- f"CreateSession2 (need {_MIN_INPUT_CAPTURE_VERSION}+)"
1511
+ f"InputCapture version {version} has no Start method, and "
1512
+ f"persist_mode/restore_token are version "
1513
+ f"{_MIN_INPUT_CAPTURE_VERSION} additions to it -- this portal "
1514
+ f"cannot persist a session. Retry with persist_mode=NONE and "
1515
+ f"no restore_token to negotiate a one-shot session."
1442
1516
  )
1443
1517
 
1444
1518
  if capabilities == DeviceType.ALL_DEVICES:
1445
1519
  types = _ALL_DEVICE_TYPES
1446
1520
  else:
1447
1521
  types = capabilities
1448
- reply = _call_sync(
1449
- connection,
1450
- Gio,
1451
- GLib,
1452
- busname,
1453
- _OBJECT_PATH,
1454
- _INPUT_CAPTURE,
1455
- "CreateSession2",
1456
- GLib.Variant(
1457
- "(a{sv})",
1458
- ({"session_handle_token": GLib.Variant("s", uuid.uuid4().hex)},),
1459
- ),
1460
- None,
1461
- int(timeout * 1000),
1462
- )
1463
- (results,) = reply.unpack()
1464
- session_handle = results.get("session_handle")
1465
- if not isinstance(session_handle, str):
1466
- raise PortalError("CreateSession2 returned no session_handle")
1467
1522
 
1468
- # Past this point a session exists inside xdg-desktop-portal, and
1469
- # nothing else can close it -- see RemoteDesktopSession.negotiate's
1470
- # identical reasoning, which this mirrors line for line.
1471
- try:
1472
- options: dict[str, Any] = {"capabilities": GLib.Variant("u", int(types))}
1473
- if persist_mode != PersistMode.NONE:
1474
- options["persist_mode"] = GLib.Variant("u", int(persist_mode))
1475
- if restore_token is not None:
1476
- options["restore_token"] = GLib.Variant("s", restore_token)
1477
- code, start_results = _request(
1523
+ if legacy:
1524
+ session_handle = _create_input_capture_session_v1(
1525
+ connection, Gio, GLib, busname, int(types), timeout
1526
+ )
1527
+ else:
1528
+ reply = _call_sync(
1478
1529
  connection,
1479
1530
  Gio,
1480
1531
  GLib,
1481
1532
  busname,
1533
+ _OBJECT_PATH,
1482
1534
  _INPUT_CAPTURE,
1483
- "Start",
1484
- "(osa{sv})",
1485
- (session_handle, ""),
1486
- options,
1487
- timeout,
1535
+ "CreateSession2",
1536
+ GLib.Variant(
1537
+ "(a{sv})",
1538
+ ({"session_handle_token": GLib.Variant("s", uuid.uuid4().hex)},),
1539
+ ),
1540
+ None,
1541
+ int(timeout * 1000),
1488
1542
  )
1489
- if code != 0:
1490
- raise PortalDeniedError(
1491
- "Start", "the user declined the input-capture consent dialog"
1543
+ (results,) = reply.unpack()
1544
+ session_handle = results.get("session_handle")
1545
+ if not isinstance(session_handle, str):
1546
+ raise PortalError("CreateSession2 returned no session_handle")
1547
+
1548
+ # Past this point a session exists inside xdg-desktop-portal, and
1549
+ # nothing else can close it -- see RemoteDesktopSession.negotiate's
1550
+ # identical reasoning, which this mirrors line for line.
1551
+ try:
1552
+ # v1 asked for consent and capabilities in CreateSession itself,
1553
+ # and has no Start to call; it can therefore never issue a
1554
+ # restore token either.
1555
+ new_restore_token = None
1556
+ if not legacy:
1557
+ options: dict[str, Any] = {
1558
+ "capabilities": GLib.Variant("u", int(types))
1559
+ }
1560
+ if persist_mode != PersistMode.NONE:
1561
+ options["persist_mode"] = GLib.Variant("u", int(persist_mode))
1562
+ if restore_token is not None:
1563
+ options["restore_token"] = GLib.Variant("s", restore_token)
1564
+ code, start_results = _request(
1565
+ connection,
1566
+ Gio,
1567
+ GLib,
1568
+ busname,
1569
+ _INPUT_CAPTURE,
1570
+ "Start",
1571
+ "(osa{sv})",
1572
+ (session_handle, ""),
1573
+ options,
1574
+ timeout,
1492
1575
  )
1493
- new_restore_token = start_results.get("restore_token")
1576
+ if code != 0:
1577
+ raise PortalDeniedError(
1578
+ "Start", "the user declined the input-capture consent dialog"
1579
+ )
1580
+ new_restore_token = start_results.get("restore_token")
1494
1581
 
1495
1582
  eis_fd = _call_for_fd(
1496
1583
  connection,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-libei
3
- Version: 0.5.0
3
+ Version: 0.5.1
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.0`), published on [PyPI](https://pypi.org/project/python-libei/)
159
+ Beta (`0.5.1`), 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
@@ -1,17 +1,17 @@
1
- libei/__init__.py,sha256=ovsIL7-rEp8LMbiWdmlGSoU6r4lNS_stMLULlkFgwGg,1858
1
+ libei/__init__.py,sha256=ZknUZWRNN4JfCH08NGpbBhqr_e2YZjHnk8B1aXe2J9Y,1858
2
2
  libei/_cobject.py,sha256=msrbviABSWjc5fKsXHSCg7nQ0Y4Etcq_Dkh0fblW-sY,12845
3
3
  libei/ei.py,sha256=Da3ezXnFPXtejlcJFH8bcE5f4pxKZXXqb6ltCh2zAdE,46704
4
4
  libei/eis.py,sha256=aYWOCN1NlRchcWd6NjIUi1eYMFWJbp0UHMcQFEJ0ii4,43642
5
5
  libei/oeffis.py,sha256=UcnDwLErFxB0xuktNpIzAK01ylpl6ZrmaGnWiUOcFOo,9302
6
- libei/portal.py,sha256=-OAg_g0Q6s0BNN2B6bzfyD2T5N_dCuHbSpqtcL8dCUk,60488
6
+ libei/portal.py,sha256=LGRINrYQ-x46i0pzGOzpM6ZT3xek_AoExNIgR2BZRB8,64498
7
7
  libei/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  libei/_capi/__init__.py,sha256=3VxixYYlr_ZcuiK0GwrCpbE3VvEv_O990mJzdIJ8i74,209
9
9
  libei/_capi/libei.py,sha256=gpp42bhqPonhVD2zmFWCl4rdk_lm4XvD_7W2Rw5RsuI,11308
10
10
  libei/_capi/libeis.py,sha256=jQR0Z0YN9qa71mAcSJ__-UaoF8PsVsBs8OXwjfqstFk,13080
11
11
  libei/_capi/liboeffis.py,sha256=V8jdmJm3qOQUzzNiCAXt4Jr2JY11Y4-w6NIfIxbxGI4,1218
12
12
  libei/_capi/loader.py,sha256=k7fb_Nz0fg5QkuLJ_duKXj8bESSktLQ2ZyS5LNQ5v2I,4689
13
- python_libei-0.5.0.dist-info/licenses/LICENSE,sha256=l6xbMU6Y-JZDzmBciBj-J5t6h6jNg_Bcyp4bHxDepqU,1074
14
- python_libei-0.5.0.dist-info/METADATA,sha256=YDry3cY8Id0Z9bwQh_27SZkR8U8Ntf-S2t7Elf2nojo,20475
15
- python_libei-0.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
16
- python_libei-0.5.0.dist-info/top_level.txt,sha256=_DQXzGjDsUBENI_cNkiOxPB4xi8coCbQS1lq18FMudQ,6
17
- python_libei-0.5.0.dist-info/RECORD,,
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,,