fix(snap): type into native Wayland apps with ydotool (#823)

* fix(snap): inject into native Wayland apps via ydotool

Edge rev 7 only ships wtype and xdotool. GNOME rejects the virtual
keyboard protocol, so the snap falls back to xdotool and types only
into XWayland windows. Native GTK/Qt apps get nothing.

Stage ydotool, request the uinput plug, and prefer ydotool when wtype
is rejected. Distro ydotool 0.1.x still needs a writable /dev/uinput;
log `snap connect vocalinux:uinput` when the Snap cannot open it.

* docs(snap): v0.16.2 edge has no uinput plug

The store snap (0.16.2, latest/edge rev 7) does not declare uinput.
`sudo snap connect vocalinux:uinput` fails with "has no plug named
uinput" and dictation only reaches XWayland apps. Spell that out in
README, INSTALL, UPDATE, the website changelog, and the store copy so
people do not run the 0.17 connect command against rev 7.

* docs(snap): drop the unconfirmed 0.17 version from uinput notes

Greptile P2: the tree is still 0.16.2 and the release process does not
name the next tag. Tell people to refresh until `snap info` lists the
uinput plug, instead of waiting on a version we have not cut.
This commit is contained in:
Jatin K Malik
2026-09-12 12:58:35 -07:00
committed by GitHub
parent 70d5ce7688
commit 4b6e81d441
8 changed files with 156 additions and 15 deletions
+55 -5
View File
@@ -149,9 +149,18 @@ class TextInjector:
"KDE Plasma Wayland detected. wtype is not a reliable "
f"text injection path on this compositor. {_kde_wayland_ibus_hint()}"
)
if shutil.which("xdotool"):
# xdotool only types into XWayland windows. Prefer ydotool
# (uinput) so native Wayland apps still receive text.
if shutil.which("ydotool") and self._ensure_ydotoold():
self.wayland_tool = "ydotool"
logger.info(
"wtype unsupported on this compositor; "
"using ydotool (uinput) for native Wayland apps"
)
elif shutil.which("xdotool"):
logger.info("Automatically switching to XWayland fallback with xdotool")
self.environment = DesktopEnvironment.WAYLAND_XDOTOOL
self._warn_if_snap_xwayland_only()
else:
logger.error("No fallback text injection method available")
except Exception as e:
@@ -426,15 +435,22 @@ class TextInjector:
ydotool 1.x talks to a daemon that owns /dev/uinput. Inside Flatpak we
start the daemon on demand when the app is granted device access.
Host ydotool 0.1.x often works without a daemon; starting one is still
safe when ydotoold is installed.
safe when ydotoold is installed. The CLI still needs a writable
/dev/uinput (Snap: ``snap connect vocalinux:uinput``).
"""
if self._is_ydotoold_running():
return True
ydotoold = shutil.which("ydotoold")
if not ydotoold:
# Distro ydotool 0.1.x may not ship a daemon; treat as ready.
return shutil.which("ydotool") is not None
if not os.path.exists("/dev/uinput"):
# Distro ydotool 0.1.x may not ship a daemon; treat as ready
# only when /dev/uinput is actually writable.
if shutil.which("ydotool") is None:
return False
if not self._uinput_usable():
logger.warning(self._uinput_permission_hint())
return False
return True
if not self._uinput_usable():
logger.warning(
"ydotoold needs /dev/uinput (Flatpak: grant --device=all). "
"Text injection into native Wayland apps will fail."
@@ -459,6 +475,40 @@ class TextInjector:
logger.warning("ydotoold did not become ready in time")
return False
@staticmethod
def _uinput_usable() -> bool:
"""Return True when this process can open ``/dev/uinput`` for write."""
try:
fd = os.open("/dev/uinput", os.O_WRONLY | os.O_NONBLOCK)
except OSError:
return False
os.close(fd)
return True
@staticmethod
def _uinput_permission_hint() -> str:
"""How to grant ydotool access to ``/dev/uinput`` in this install."""
if os.environ.get("SNAP"):
return (
"ydotool cannot open /dev/uinput. For the Snap, run: "
"sudo snap connect vocalinux:uinput"
)
return (
"ydotool cannot open /dev/uinput. "
"Add your user to the input group and log out, or start ydotoold. "
f"{_ydotool_install_guidance()}"
)
@staticmethod
def _warn_if_snap_xwayland_only() -> None:
"""Log that Snap XWayland fallback will miss native Wayland apps."""
if not os.environ.get("SNAP"):
return
logger.warning(
"Snap XWayland fallback only types into X11/XWayland apps. "
"Native Wayland apps need: sudo snap connect vocalinux:uinput"
)
@staticmethod
def _forced_backend() -> str:
"""Backend pinned via ``VOCALINUX_FORCE_BACKEND``, or ``"auto"``.