* fix(ibus): require a real IM engine on Wayland (#478) On Wayland a bare xkb:* IBus engine is a layout passthrough that never delivers commit_text() to applications, so selecting the IBus backend there silently no-ops (niri/sway, and GNOME/Mutter via the env-var branch). Require a real IM engine before treating IBus as active on Wayland, regardless of how it was detected; X11/XWayland (XIM) is unchanged. Complements #486's compositor denylist. * test(ibus): bind #478 patches to the module under test Patch get_current_engine/is_ibus_daemon_running via patch.object on a held ibus_engine reference and call is_ibus_active_input_method through it, so the patched functions and the function under test always share one module object. Fixes flakiness in the full CI suite where other test files re-import ibus_engine and the dotted-string patch bound a different module object.
This commit is contained in:
@@ -152,39 +152,51 @@ def is_ibus_daemon_running() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _is_real_ibus_engine(engine: Optional[str]) -> bool:
|
||||
"""Return True only for a real IBus IM engine, not a bare XKB layout.
|
||||
|
||||
IBus reports a bare ``xkb:*`` engine (e.g. ``xkb:us::eng``) when it is doing
|
||||
keyboard-layout passthrough with no input-method client attached. In that
|
||||
state ``commit_text()`` succeeds at the IBus layer but never reaches the
|
||||
focused application, so treating it as active causes a silent no-op on
|
||||
Wayland (see issue #478). Real IM engines (anthy, libpinyin, hangul, ...) do
|
||||
not use the ``xkb:`` prefix.
|
||||
"""
|
||||
if not engine:
|
||||
return False
|
||||
engine = engine.strip()
|
||||
if not engine or engine.lower() == "no global engine":
|
||||
return False
|
||||
return not engine.startswith("xkb:")
|
||||
|
||||
|
||||
def is_ibus_active_input_method() -> bool:
|
||||
"""
|
||||
Check if IBus is the currently active input method.
|
||||
|
||||
This checks environment variables and, on Wayland where env vars may not
|
||||
be set, also checks if the IBus daemon is running and actively managing
|
||||
an engine. On some desktop environments (e.g., KDE Plasma Wayland), IBus
|
||||
is configured via the DE's Virtual Keyboard setting and IBus itself
|
||||
recommends unsetting the legacy env vars.
|
||||
Checks environment variables and, where they are not set (common on
|
||||
Wayland), whether the IBus daemon is running with an engine.
|
||||
|
||||
On Wayland there is an extra requirement: IBus can only deliver
|
||||
``commit_text()`` to applications through a *real* IM engine. A bare
|
||||
``xkb:*`` engine is a layout passthrough, so on compositors that don't fully
|
||||
bridge IBus to native clients (niri, sway, and even GNOME/Mutter with only
|
||||
an ``xkb`` engine) injection silently reaches nothing. We therefore require a
|
||||
real engine on Wayland regardless of how IBus was detected (issue #478). On
|
||||
X11/XWayland IBus reaches apps through XIM, so that check is not applied
|
||||
there.
|
||||
|
||||
Returns:
|
||||
True if IBus appears to be the active input method, False otherwise
|
||||
"""
|
||||
# Check GTK_IM_MODULE for GTK applications
|
||||
gtk_im = os.environ.get("GTK_IM_MODULE", "").lower()
|
||||
if gtk_im and "ibus" in gtk_im:
|
||||
logger.debug(f"IBus detected as active input method via GTK_IM_MODULE={gtk_im}")
|
||||
return True
|
||||
|
||||
# Check QT_IM_MODULE for Qt applications
|
||||
qt_im = os.environ.get("QT_IM_MODULE", "").lower()
|
||||
if qt_im and "ibus" in qt_im:
|
||||
logger.debug(f"IBus detected as active input method via QT_IM_MODULE={qt_im}")
|
||||
return True
|
||||
|
||||
# On X11/XWayland, check XMODIFIERS for XIM compatibility
|
||||
xmodifiers = os.environ.get("XMODIFIERS", "").lower()
|
||||
if "@im=ibus" in xmodifiers:
|
||||
logger.debug(f"IBus detected as active input method via XMODIFIERS={xmodifiers}")
|
||||
return True
|
||||
|
||||
# If another input method is explicitly configured, respect that
|
||||
if gtk_im or qt_im or (xmodifiers and "@im=" in xmodifiers):
|
||||
ibus_via_env = ("ibus" in gtk_im) or ("ibus" in qt_im) or ("@im=ibus" in xmodifiers)
|
||||
|
||||
# If another (non-IBus) input method is explicitly configured, respect it.
|
||||
if not ibus_via_env and (gtk_im or qt_im or (xmodifiers and "@im=" in xmodifiers)):
|
||||
logger.debug(
|
||||
"Another input method is explicitly configured "
|
||||
f"(GTK_IM_MODULE={gtk_im or 'not set'}, "
|
||||
@@ -193,26 +205,48 @@ def is_ibus_active_input_method() -> bool:
|
||||
)
|
||||
return False
|
||||
|
||||
# No env vars set at all — common on Wayland (KDE Plasma, etc.) where
|
||||
# IBus is configured via the DE's Virtual Keyboard setting and IBus
|
||||
# recommends unsetting GTK_IM_MODULE / QT_IM_MODULE.
|
||||
# Check if ibus-daemon is running and has an active engine.
|
||||
if is_ibus_daemon_running():
|
||||
engine = get_current_engine()
|
||||
if engine:
|
||||
logger.debug(
|
||||
f"IBus detected as active input method via running daemon "
|
||||
f"(no env vars set, current engine: {engine})"
|
||||
)
|
||||
return True
|
||||
daemon_running = is_ibus_daemon_running()
|
||||
|
||||
logger.debug(
|
||||
"IBus does not appear to be the active input method "
|
||||
f"(GTK_IM_MODULE={gtk_im or 'not set'}, "
|
||||
f"QT_IM_MODULE={qt_im or 'not set'}, "
|
||||
f"XMODIFIERS={xmodifiers or 'not set'}, "
|
||||
f"daemon running: {is_ibus_daemon_running()})"
|
||||
)
|
||||
# Neither an IBus env-var signal nor a running daemon -> IBus is not active.
|
||||
if not ibus_via_env and not daemon_running:
|
||||
logger.debug(
|
||||
"IBus does not appear to be the active input method "
|
||||
f"(GTK_IM_MODULE={gtk_im or 'not set'}, "
|
||||
f"QT_IM_MODULE={qt_im or 'not set'}, "
|
||||
f"XMODIFIERS={xmodifiers or 'not set'}, daemon running: False)"
|
||||
)
|
||||
return False
|
||||
|
||||
if _is_wayland_session():
|
||||
# On Wayland a bare xkb:* engine can't deliver commits to apps, so
|
||||
# require a real IM engine regardless of how IBus was detected (#478).
|
||||
engine = get_current_engine() if daemon_running else None
|
||||
if not _is_real_ibus_engine(engine):
|
||||
logger.debug(
|
||||
"IBus is present on Wayland but the current engine "
|
||||
f"({engine or 'none'}) is a bare XKB layout or unset; "
|
||||
"commit_text() would not reach applications, so treating IBus "
|
||||
"as inactive (see #478)."
|
||||
)
|
||||
return False
|
||||
logger.debug(f"IBus detected as active input method on Wayland (engine: {engine})")
|
||||
return True
|
||||
|
||||
# X11 / XWayland: IBus reaches apps via XIM. An env-var signal, or a running
|
||||
# daemon with any engine (KDE Plasma with legacy env vars unset), counts.
|
||||
if ibus_via_env:
|
||||
logger.debug("IBus detected as active input method via environment variables")
|
||||
return True
|
||||
|
||||
engine = get_current_engine()
|
||||
if engine:
|
||||
logger.debug(
|
||||
"IBus detected as active input method via running daemon "
|
||||
f"(no env vars set, current engine: {engine})"
|
||||
)
|
||||
return True
|
||||
|
||||
logger.debug("IBus does not appear to be the active input method (daemon has no engine)")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Regression tests for issue #478.
|
||||
|
||||
On Wayland, IBus only delivers commit_text() to applications through a *real*
|
||||
IM engine. A bare ``xkb:*`` engine (or none) is a layout passthrough, so
|
||||
selecting the IBus backend there is a silent no-op. is_ibus_active_input_method()
|
||||
must require a real engine on Wayland regardless of how IBus was detected, while
|
||||
leaving X11/XWayland (XIM) behavior unchanged.
|
||||
|
||||
Note: we patch via ``patch.object`` on a held module reference (and call the
|
||||
function through that same reference) rather than by dotted string, so the
|
||||
patched ``get_current_engine`` / ``is_ibus_daemon_running`` and the function
|
||||
under test always share one module object even when other test files re-import
|
||||
``ibus_engine`` during a full-suite run.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# Mock GI so importing ibus_engine does not require a real IBus/GTK stack.
|
||||
_mock_gi = MagicMock()
|
||||
_mock_gi_repo = MagicMock()
|
||||
_mock_gi_repo.IBus = MagicMock()
|
||||
_mock_gi_repo.GLib = MagicMock()
|
||||
_mock_gi_repo.GObject = MagicMock()
|
||||
_mock_gi_repo.IBus.Engine = MagicMock
|
||||
_mock_gi_repo.GLib.MainLoop = MagicMock
|
||||
sys.modules["gi"] = _mock_gi
|
||||
sys.modules["gi.repository"] = _mock_gi_repo
|
||||
|
||||
for _key in list(sys.modules.keys()):
|
||||
if "vocalinux" in _key and "ibus_engine" in _key:
|
||||
del sys.modules[_key]
|
||||
|
||||
import vocalinux.text_injection.ibus_engine as ibus_engine # noqa: E402
|
||||
|
||||
|
||||
class TestIsRealIBusEngine(unittest.TestCase):
|
||||
def test_real_engines(self):
|
||||
for engine in ("anthy", "libpinyin", "hangul", "mozc-jp"):
|
||||
self.assertTrue(ibus_engine._is_real_ibus_engine(engine), engine)
|
||||
|
||||
def test_layout_only_or_missing_engines(self):
|
||||
for engine in ("xkb:us::eng", "xkb:de::ger", "xkb:gb::eng", "", None, "No global engine"):
|
||||
self.assertFalse(ibus_engine._is_real_ibus_engine(engine), repr(engine))
|
||||
|
||||
|
||||
class TestWaylandRequiresRealEngine(unittest.TestCase):
|
||||
@patch.object(ibus_engine, "get_current_engine", return_value="xkb:us::eng")
|
||||
@patch.object(ibus_engine, "is_ibus_daemon_running", return_value=True)
|
||||
@patch.dict("os.environ", {"XDG_SESSION_TYPE": "wayland", "QT_IM_MODULE": "ibus"}, clear=True)
|
||||
def test_bare_xkb_engine_inactive_even_with_env_var(self, *_):
|
||||
# GNOME/Mutter: QT_IM_MODULE=ibus but only a bare xkb engine -> no-op.
|
||||
self.assertFalse(ibus_engine.is_ibus_active_input_method())
|
||||
|
||||
@patch.object(ibus_engine, "get_current_engine", return_value="anthy")
|
||||
@patch.object(ibus_engine, "is_ibus_daemon_running", return_value=True)
|
||||
@patch.dict("os.environ", {"XDG_SESSION_TYPE": "wayland"}, clear=True)
|
||||
def test_real_engine_is_active(self, *_):
|
||||
self.assertTrue(ibus_engine.is_ibus_active_input_method())
|
||||
|
||||
@patch.object(ibus_engine, "get_current_engine", return_value=None)
|
||||
@patch.object(ibus_engine, "is_ibus_daemon_running", return_value=True)
|
||||
@patch.dict("os.environ", {"XDG_SESSION_TYPE": "wayland", "GTK_IM_MODULE": "ibus"}, clear=True)
|
||||
def test_no_engine_is_inactive(self, *_):
|
||||
self.assertFalse(ibus_engine.is_ibus_active_input_method())
|
||||
|
||||
|
||||
class TestX11BehaviorUnchanged(unittest.TestCase):
|
||||
@patch.object(ibus_engine, "get_current_engine", return_value="xkb:us::eng")
|
||||
@patch.object(ibus_engine, "is_ibus_daemon_running", return_value=True)
|
||||
@patch.dict("os.environ", {"XDG_SESSION_TYPE": "x11"}, clear=True)
|
||||
def test_daemon_with_xkb_engine_still_active_on_x11(self, *_):
|
||||
# XIM delivers even with a bare xkb engine, so the daemon fallback stands.
|
||||
self.assertTrue(ibus_engine.is_ibus_active_input_method())
|
||||
|
||||
@patch.object(ibus_engine, "get_current_engine", return_value=None)
|
||||
@patch.object(ibus_engine, "is_ibus_daemon_running", return_value=False)
|
||||
@patch.dict("os.environ", {"XDG_SESSION_TYPE": "x11", "GTK_IM_MODULE": "ibus"}, clear=True)
|
||||
def test_env_var_is_active_on_x11(self, *_):
|
||||
self.assertTrue(ibus_engine.is_ibus_active_input_method())
|
||||
|
||||
@patch.object(ibus_engine, "get_current_engine", return_value=None)
|
||||
@patch.object(ibus_engine, "is_ibus_daemon_running", return_value=False)
|
||||
@patch.dict("os.environ", {"XDG_SESSION_TYPE": "x11", "GTK_IM_MODULE": "fcitx"}, clear=True)
|
||||
def test_other_im_configured_is_inactive(self, *_):
|
||||
self.assertFalse(ibus_engine.is_ibus_active_input_method())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user