fix: skip unbridged IBus on KDE Plasma Wayland (#577)

* fix(injection): skip unbridged IBus on KDE Plasma Wayland

Do not auto-start XIM ibus-daemon on Wayland (circular viability check).
On KDE, require KWin VirtualKeyboard enabled before selecting IBus.

Fixes #574

* refactor(injection): tighten KDE VirtualKeyboard bridge check

Ponytail cleanup: shorter logs and fewer failure branches, same behavior.

* style: black-format ibus daemon Wayland test for CI

* fix(injection): parse KWin VirtualKeyboard gdbus variants strictly

Match <<true>>/<<false>> wrappers instead of bare substrings so ambiguous
gdbus output cannot flip the #574 IBus bridge decision.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
This commit is contained in:
Jatin K Malik
2026-07-26 23:06:26 -07:00
committed by GitHub
co-authored by Jatin K Malik Cursor Agent
parent 4c070ded46
commit c59fe8b577
5 changed files with 251 additions and 5 deletions
+9 -2
View File
@@ -269,8 +269,10 @@ def start_ibus_daemon():
"""
Start the IBus daemon if it's not already running.
This is useful for desktop environments where IBus doesn't start automatically,
such as some KDE Plasma installations or minimal window managers.
X11 only. On Wayland we must not spawn ``ibus-daemon -x -d -r``: that XIM-mode
daemon is not bridged to the compositor, but still makes
``is_ibus_daemon_running()`` return True and leads to silent no-op injection
(issue #574). The compositor/DE must own the Wayland-bridged IBus instance.
Returns:
True if daemon was started or already running, False on failure
@@ -278,6 +280,11 @@ def start_ibus_daemon():
if is_ibus_daemon_running():
return True
if _is_wayland_session():
# XIM -x is not compositor-bridged; spawning it only fools the pgrep check (#574).
logger.debug("Not starting ibus-daemon on Wayland (see #574)")
return False
if not is_ibus_available():
return False
+60 -1
View File
@@ -233,6 +233,57 @@ class TextInjector:
"weston",
)
def _kde_virtual_keyboard_enabled(self) -> bool:
"""Return True when KWin VirtualKeyboard / input method is enabled.
On KDE Plasma Wayland, IBus only reaches native apps when this is on
(issue #574). Disabled or unqueryable → treat IBus as unbridged.
"""
try:
result = subprocess.run(
[
"gdbus",
"call",
"--session",
"--dest",
"org.kde.KWin",
"--object-path",
"/VirtualKeyboard",
"--method",
"org.freedesktop.DBus.Properties.Get",
"org.kde.kwin.VirtualKeyboard",
"enabled",
],
capture_output=True,
text=True,
timeout=2,
)
except (subprocess.SubprocessError, FileNotFoundError) as e:
logger.info(
"Could not query KWin VirtualKeyboard (%s); treating IBus as unbridged.",
e,
)
return False
out = (result.stdout or "").strip().lower() if result.returncode == 0 else ""
# gdbus prints variant wrappers like: (<<true>>,) or (<<false>>,)
if "<<true>>" in out:
return True
if "<<false>>" in out:
logger.info(
"KWin Virtual Keyboard is disabled; IBus commits will not reach "
"native apps. Falling back to ydotool/wtype. Enable: System "
"Settings → Keyboard → Virtual Keyboard → IBus Wayland."
)
return False
logger.info(
"KWin VirtualKeyboard not confirmed (rc=%s out=%r); treating IBus as unbridged.",
result.returncode,
result.stdout,
)
return False
def _wayland_compositor_bridges_ibus(self) -> bool:
"""Whether native Wayland clients receive IBus commits on this compositor.
@@ -242,6 +293,10 @@ class TextInjector:
those we must use a virtual-keyboard tool (wtype/ydotool) instead. We use
a denylist rather than an allowlist so that unrecognised desktops keep the
previous IBus-preferred behaviour.
On KDE Plasma Wayland, bridging also requires KWin VirtualKeyboard to be
enabled (issue #574); otherwise commit_text succeeds at the IBus layer
but never reaches apps.
"""
if self.environment != DesktopEnvironment.WAYLAND:
# X11 / XWayland: IBus reaches apps through XIM regardless of DE.
@@ -250,7 +305,11 @@ class TextInjector:
os.environ.get(var, "")
for var in ("XDG_CURRENT_DESKTOP", "XDG_SESSION_DESKTOP", "DESKTOP_SESSION")
).lower()
return not any(name in desktop for name in self._IBUS_UNBRIDGED_COMPOSITORS)
if any(name in desktop for name in self._IBUS_UNBRIDGED_COMPOSITORS):
return False
if _is_kde_plasma_session():
return self._kde_virtual_keyboard_enabled()
return True
def _ydotool_socket_paths(self) -> list:
"""Return candidate Unix socket paths used by ydotoold."""