Merge feature/31 into integration/spec16 (ticket #31: injection ladder order)
This commit is contained in:
@@ -1007,16 +1007,17 @@ class TextInjector:
|
||||
if ydotool_available:
|
||||
_warn_if_ydotool_globally_enabled()
|
||||
|
||||
# Prefer ydotool when the daemon is (or can be) ready. Flatpak ships
|
||||
# ydotool for native Wayland typing; wtype needs a Wayland socket.
|
||||
# The RemoteDesktop portal sits ahead of both in autodetection: it
|
||||
# is the only injection path Wayland sanctions, works inside the
|
||||
# Flatpak sandbox without /dev/uinput, and reaches native clients
|
||||
# on compositors with no input-method-v2 bridge. KDE is the
|
||||
# exception: KWin's portal scrambles letter case (#911), so on
|
||||
# Plasma a present ydotool goes first -- the ordering from before
|
||||
# the portal backend existed.
|
||||
kde_ydotool_first = ydotool_available and _is_kde_plasma_session()
|
||||
# Decided ladder (spec #16, ticket #31): the RemoteDesktop portal
|
||||
# sits ahead of both typing tools in autodetection -- it is the
|
||||
# only injection path Wayland sanctions, works inside the Flatpak
|
||||
# sandbox without /dev/uinput, and reaches native clients on
|
||||
# compositors with no input-method-v2 bridge. Then direct keysym
|
||||
# typing (wtype), then the virtual-input tool (ydotool, which
|
||||
# needs a ready ydotoold), then XWayland-only xdotool, then manual
|
||||
# paste. A daemonless ydotool must not outrank a working portal:
|
||||
# it may fail at injection time (no /dev/uinput), while the
|
||||
# portal delivers. The __init__ wtype probe demotes wtype to
|
||||
# ydotool when the compositor rejects the virtual keyboard.
|
||||
if forced == "portal" and portal_available:
|
||||
self._select_portal_backend(
|
||||
"%s=portal: using RemoteDesktop portal for Wayland injection",
|
||||
@@ -1029,30 +1030,22 @@ class TextInjector:
|
||||
self._ensure_ydotoold()
|
||||
self.wayland_tool = "ydotool"
|
||||
logger.info("%s=ydotool: using ydotool for Wayland injection", pin_source)
|
||||
elif portal_available and not kde_ydotool_first:
|
||||
elif portal_available:
|
||||
self._select_portal_backend(
|
||||
"Using the RemoteDesktop portal for Wayland text injection"
|
||||
)
|
||||
elif wtype_available:
|
||||
self.wayland_tool = "wtype"
|
||||
logger.info("Using wtype for Wayland text injection")
|
||||
elif ydotool_available and self._ensure_ydotoold():
|
||||
self.wayland_tool = "ydotool"
|
||||
logger.info("Using ydotool for Wayland text injection")
|
||||
elif portal_available:
|
||||
# Reached only on KDE after ydotool proved unusable; on other
|
||||
# desktops the earlier portal branch already fired. A daemonless
|
||||
# ydotool must not outrank a working portal: it may fail at
|
||||
# injection time (no /dev/uinput), while the portal delivers.
|
||||
self._select_portal_backend(
|
||||
"Using the RemoteDesktop portal for Wayland text injection"
|
||||
)
|
||||
elif ydotool_available and not wtype_available:
|
||||
elif ydotool_available:
|
||||
self.wayland_tool = "ydotool"
|
||||
logger.warning(
|
||||
"ydotoold not ready; using ydotool without daemon "
|
||||
"(may fail or have latency/permission issues)"
|
||||
)
|
||||
elif wtype_available:
|
||||
self.wayland_tool = "wtype"
|
||||
logger.info(f"Using {self.wayland_tool} for Wayland text injection")
|
||||
elif xdotool_available:
|
||||
# Fallback to xdotool with XWayland
|
||||
self.environment = DesktopEnvironment.WAYLAND_XDOTOOL
|
||||
@@ -1201,16 +1194,21 @@ class TextInjector:
|
||||
def _demote_portal_backend(self) -> bool:
|
||||
"""Drop the portal for this run and pick the next Wayland tool.
|
||||
|
||||
Mirrors the startup preference: ydotool with a ready daemon, then
|
||||
ydotool alone when wtype is absent, then wtype, then the XWayland
|
||||
fallback. Returns False when nothing else exists, leaving the
|
||||
clipboard fallback to report the failure. In a Flatpak, keep the
|
||||
portal and reset its session when ydotoold is unavailable so the next
|
||||
attempt can create a fresh session.
|
||||
Mirrors the startup preference (spec #16, ticket #31): direct keysym
|
||||
typing (wtype), then ydotool with a ready daemon, then daemonless
|
||||
ydotool, then the XWayland fallback. Returns False when nothing else
|
||||
exists, leaving the clipboard fallback to report the failure. In a
|
||||
Flatpak, keep the portal and reset its session when no typing tool
|
||||
is usable so the next attempt can create a fresh session.
|
||||
"""
|
||||
flatpak = bool(os.environ.get("FLATPAK_ID"))
|
||||
ydotool_available = shutil.which("ydotool") is not None
|
||||
if flatpak and (not ydotool_available or not self._ensure_ydotoold()):
|
||||
wtype_available = shutil.which("wtype") is not None
|
||||
if (
|
||||
flatpak
|
||||
and not wtype_available
|
||||
and (not ydotool_available or not self._ensure_ydotoold())
|
||||
):
|
||||
portal = getattr(self, "_portal", None)
|
||||
if portal is not None:
|
||||
try:
|
||||
@@ -1220,7 +1218,7 @@ class TextInjector:
|
||||
logger.warning(
|
||||
"RemoteDesktop portal failed; keeping it for the next attempt "
|
||||
"(the session will be recreated on the next attempt; no working "
|
||||
"ydotool daemon is available in the Flatpak)"
|
||||
"typing tool is available in the Flatpak)"
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -1230,17 +1228,16 @@ class TextInjector:
|
||||
self.wayland_tool = None
|
||||
|
||||
if flatpak:
|
||||
tool = "ydotool"
|
||||
tool = "wtype" if wtype_available else "ydotool"
|
||||
else:
|
||||
wtype_available = shutil.which("wtype") is not None
|
||||
xdotool_available = shutil.which("xdotool") is not None
|
||||
tool = None
|
||||
if ydotool_available and self._ensure_ydotoold():
|
||||
tool = "ydotool"
|
||||
elif ydotool_available and not wtype_available:
|
||||
tool = "ydotool"
|
||||
elif wtype_available:
|
||||
if wtype_available:
|
||||
tool = "wtype"
|
||||
elif ydotool_available and self._ensure_ydotoold():
|
||||
tool = "ydotool"
|
||||
elif ydotool_available:
|
||||
tool = "ydotool"
|
||||
elif xdotool_available:
|
||||
with self._state_lock:
|
||||
self.environment = DesktopEnvironment.WAYLAND_XDOTOOL
|
||||
@@ -1415,6 +1412,15 @@ class TextInjector:
|
||||
wtype_available = shutil.which("wtype") is not None
|
||||
xdotool_available = shutil.which("xdotool") is not None
|
||||
|
||||
# Decided ladder order (spec #16, ticket #31): direct keysym
|
||||
# typing first, then the virtual-input tool, then XWayland.
|
||||
if wtype_available:
|
||||
with self._state_lock:
|
||||
self.wayland_tool = "wtype"
|
||||
self.environment = DesktopEnvironment.WAYLAND
|
||||
logger.warning("IBus injection failed, switching to Wayland wtype fallback")
|
||||
return True
|
||||
|
||||
if ydotool_available:
|
||||
try:
|
||||
subprocess.run(
|
||||
@@ -1436,13 +1442,6 @@ class TextInjector:
|
||||
):
|
||||
logger.debug("ydotool fallback unavailable (daemon not running)")
|
||||
|
||||
if wtype_available:
|
||||
with self._state_lock:
|
||||
self.wayland_tool = "wtype"
|
||||
self.environment = DesktopEnvironment.WAYLAND
|
||||
logger.warning("IBus injection failed, switching to Wayland wtype fallback")
|
||||
return True
|
||||
|
||||
if xdotool_available:
|
||||
with self._state_lock:
|
||||
self.environment = DesktopEnvironment.WAYLAND_XDOTOOL
|
||||
@@ -1451,7 +1450,7 @@ class TextInjector:
|
||||
|
||||
logger.error(
|
||||
"IBus fallback failed: no Wayland text injection tools available "
|
||||
"(ydotool, wtype, xdotool)"
|
||||
"(wtype, ydotool, xdotool)"
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -1502,6 +1501,21 @@ class TextInjector:
|
||||
|
||||
logger.info("Checking for better Wayland text injection tools...")
|
||||
|
||||
# Decided ladder order (spec #16, ticket #31): direct keysym typing
|
||||
# first, then the virtual-input tool.
|
||||
# Check for wtype with compositor support
|
||||
if shutil.which("wtype"):
|
||||
try:
|
||||
result = self._probe_wtype_support()
|
||||
error_output = result.stderr.lower()
|
||||
if "compositor does not support" not in error_output and result.returncode == 0:
|
||||
self.wayland_tool = "wtype"
|
||||
self.environment = DesktopEnvironment.WAYLAND
|
||||
logger.info("Recovered to wtype - compositor now supports virtual keyboard")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Error testing wtype: {e}")
|
||||
|
||||
# Check for ydotool with daemon running
|
||||
if shutil.which("ydotool"):
|
||||
try:
|
||||
@@ -1519,19 +1533,6 @@ class TextInjector:
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
||||
logger.debug("ydotool available but daemon not running")
|
||||
|
||||
# Check for wtype with compositor support
|
||||
if shutil.which("wtype"):
|
||||
try:
|
||||
result = self._probe_wtype_support()
|
||||
error_output = result.stderr.lower()
|
||||
if "compositor does not support" not in error_output and result.returncode == 0:
|
||||
self.wayland_tool = "wtype"
|
||||
self.environment = DesktopEnvironment.WAYLAND
|
||||
logger.info("Recovered to wtype - compositor now supports virtual keyboard")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Error testing wtype: {e}")
|
||||
|
||||
logger.debug("No better tools available, continuing with xdotool fallback")
|
||||
return False
|
||||
|
||||
@@ -1794,31 +1795,66 @@ class TextInjector:
|
||||
"compositor does not support" in str(e).lower()
|
||||
or "compositor does not support" in stderr_msg.lower()
|
||||
)
|
||||
logger.warning(
|
||||
f"Wayland tool failed: {e}. stderr: {stderr_msg}. Falling back to xdotool"
|
||||
)
|
||||
# A wtype failure at injection time demotes to the next
|
||||
# ladder rung (ydotool) before the XWayland-only path,
|
||||
# mirroring the startup order (spec #16, ticket #31).
|
||||
ydotool_retry_succeeded = False
|
||||
if (
|
||||
unsupported_wayland
|
||||
and current_env == DesktopEnvironment.WAYLAND
|
||||
and _is_kde_plasma_session()
|
||||
self.wayland_tool == "wtype"
|
||||
and shutil.which("ydotool")
|
||||
and self._ensure_ydotoold()
|
||||
):
|
||||
logger.warning(
|
||||
"KDE Plasma Wayland rejected virtual keyboard injection. "
|
||||
f"{_kde_wayland_ibus_hint()}"
|
||||
)
|
||||
if unsupported_wayland and shutil.which("xdotool"):
|
||||
logger.info(
|
||||
"Switching to XWayland fallback - will re-check for better tools"
|
||||
f"Wayland tool failed: {e}. stderr: {stderr_msg}. "
|
||||
"Falling back to ydotool"
|
||||
)
|
||||
with self._state_lock:
|
||||
self.environment = DesktopEnvironment.WAYLAND_XDOTOOL
|
||||
self.wayland_tool = "ydotool"
|
||||
try:
|
||||
self._inject_with_xdotool(remaining)
|
||||
self._inject_with_wayland_tool(remaining)
|
||||
except _PartiallyTyped as nested:
|
||||
# Keep the count relative to the original text.
|
||||
raise _PartiallyTyped(typed + nested.typed, nested) from nested
|
||||
else:
|
||||
raise
|
||||
except subprocess.CalledProcessError as retry_error:
|
||||
e = retry_error
|
||||
stderr_msg = (
|
||||
retry_error.stderr.strip()
|
||||
if retry_error.stderr
|
||||
else "No stderr output"
|
||||
)
|
||||
unsupported_wayland = (
|
||||
"compositor does not support" in str(retry_error).lower()
|
||||
or "compositor does not support" in stderr_msg.lower()
|
||||
)
|
||||
else:
|
||||
ydotool_retry_succeeded = True
|
||||
if not ydotool_retry_succeeded:
|
||||
logger.warning(
|
||||
f"Wayland tool failed: {e}. stderr: {stderr_msg}. "
|
||||
"Falling back to xdotool"
|
||||
)
|
||||
if (
|
||||
unsupported_wayland
|
||||
and current_env == DesktopEnvironment.WAYLAND
|
||||
and _is_kde_plasma_session()
|
||||
):
|
||||
logger.warning(
|
||||
"KDE Plasma Wayland rejected virtual keyboard injection. "
|
||||
f"{_kde_wayland_ibus_hint()}"
|
||||
)
|
||||
if unsupported_wayland and shutil.which("xdotool"):
|
||||
logger.info(
|
||||
"Switching to XWayland fallback - will re-check for better tools"
|
||||
)
|
||||
with self._state_lock:
|
||||
self.environment = DesktopEnvironment.WAYLAND_XDOTOOL
|
||||
try:
|
||||
self._inject_with_xdotool(remaining)
|
||||
except _PartiallyTyped as nested:
|
||||
# Keep the count relative to the original text.
|
||||
raise _PartiallyTyped(typed + nested.typed, nested) from nested
|
||||
else:
|
||||
raise
|
||||
logger.info("Text injection completed successfully")
|
||||
|
||||
if self._should_copy_to_clipboard():
|
||||
|
||||
Reference in New Issue
Block a user