fix(text-injection): restore KDE Wayland IBus path (#502)

* fix(text-injection): restore bridged Wayland IBus path

* fix(text-injection): keep GNOME xkb fallback
This commit is contained in:
Jatin K Malik
2026-07-10 17:27:41 -07:00
committed by GitHub
parent 4e3637dfb5
commit a9a6ae3c51
2 changed files with 128 additions and 40 deletions
+28 -13
View File
@@ -242,21 +242,31 @@ class TextInjector:
# Prefer IBus on both X11 and Wayland - it sends Unicode directly,
# bypassing keyboard layout issues entirely
if is_ibus_available():
ibus_active = is_ibus_active_input_method()
kde_wayland = (
self.environment == DesktopEnvironment.WAYLAND and _is_kde_plasma_session()
)
gtk_im = os.environ.get("GTK_IM_MODULE", "").lower()
qt_im = os.environ.get("QT_IM_MODULE", "").lower()
xmodifiers = os.environ.get("XMODIFIERS", "").lower()
explicit_non_ibus_im = (
(gtk_im and "ibus" not in gtk_im)
or (qt_im and "ibus" not in qt_im)
or (xmodifiers and "@im=" in xmodifiers and "@im=ibus" not in xmodifiers)
)
kde_wayland_ibus = kde_wayland and not explicit_non_ibus_im
# Check if IBus is the active input method (not just installed)
# This is important because IBus may be installed but not being used,
# e.g., when the user has configured ydotool or Fcitx instead
if not is_ibus_active_input_method():
if self.environment == DesktopEnvironment.WAYLAND and _is_kde_plasma_session():
logger.warning(
"IBus is installed but is not active for KDE Plasma Wayland. "
f"{_kde_wayland_ibus_hint()} Falling back to alternative "
"text injection method."
)
else:
logger.info(
"IBus is installed but not the active input method. "
"Falling back to alternative text injection method."
)
# e.g., when the user has configured ydotool or Fcitx instead. KDE
# Plasma Wayland can still route the Vocalinux engine through IBus
# Wayland when its current engine is only an xkb:* layout; GNOME and
# other desktops must keep the real-engine guard from issue #478.
if not ibus_active and not kde_wayland_ibus:
logger.info(
"IBus is installed but not the active input method. "
"Falling back to alternative text injection method."
)
# Check if ibus-daemon is running before attempting setup
elif not is_ibus_daemon_running():
logger.info(
@@ -275,6 +285,11 @@ class TextInjector:
)
else:
try:
if kde_wayland_ibus and not ibus_active:
logger.info(
"KDE Plasma Wayland detected with ibus-daemon running; "
"using IBus Wayland despite the inactive input-method heuristic"
)
self._ibus_injector = IBusTextInjector(auto_activate=False)
ibus_requested = True
except Exception as e:
+100 -27
View File
@@ -150,39 +150,112 @@ class TestCheckDependencies(unittest.TestCase):
with self.assertRaises(RuntimeError):
obj._check_dependencies()
def test_kde_wayland_inactive_ibus_logs_virtual_keyboard_hint(self):
def test_kde_wayland_uses_ibus_when_daemon_runs_with_xkb_engine(self):
from vocalinux.text_injection.text_injector import DesktopEnvironment
obj = _make_injector(DesktopEnvironment.WAYLAND)
mock_ibus = MagicMock()
with (
patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "wayland", "XDG_CURRENT_DESKTOP": "KDE"},
clear=True,
),
patch(
"vocalinux.text_injection.text_injector.is_ibus_available",
return_value=True,
),
patch(
"vocalinux.text_injection.text_injector.is_ibus_active_input_method",
return_value=False,
),
patch(
"vocalinux.text_injection.text_injector.is_ibus_daemon_running",
return_value=True,
),
patch(
"vocalinux.text_injection.text_injector.IBusTextInjector",
return_value=mock_ibus,
),
patch.object(obj, "_start_ibus_initialization") as mock_start,
patch(
"shutil.which",
side_effect=lambda cmd: "/usr/bin/wtype" if cmd == "wtype" else None,
),
):
obj._check_dependencies()
self.assertIs(obj._ibus_injector, mock_ibus)
mock_start.assert_called_once_with()
self.assertEqual(obj.wayland_tool, "wtype")
def test_gnome_wayland_keeps_xkb_engine_fallback(self):
from vocalinux.text_injection.text_injector import DesktopEnvironment
obj = _make_injector(DesktopEnvironment.WAYLAND)
with patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "wayland", "XDG_CURRENT_DESKTOP": "KDE"},
clear=True,
):
with patch(
"vocalinux.text_injection.text_injector.is_ibus_available",
with (
patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "wayland", "XDG_CURRENT_DESKTOP": "GNOME"},
clear=True,
),
patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=True),
patch(
"vocalinux.text_injection.text_injector.is_ibus_active_input_method",
return_value=False,
),
patch(
"vocalinux.text_injection.text_injector.is_ibus_daemon_running",
return_value=True,
):
with patch(
"vocalinux.text_injection.text_injector.is_ibus_active_input_method",
return_value=False,
):
with patch(
"shutil.which",
side_effect=lambda cmd: "/usr/bin/ydotool" if cmd == "ydotool" else None,
):
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stderr="")
with self.assertLogs(
"vocalinux.text_injection.text_injector",
level="WARNING",
) as logs:
obj._check_dependencies()
),
patch("vocalinux.text_injection.text_injector.IBusTextInjector") as mock_ibus_class,
patch(
"shutil.which",
side_effect=lambda cmd: "/usr/bin/wtype" if cmd == "wtype" else None,
),
):
obj._check_dependencies()
log_output = "\n".join(logs.output)
self.assertIn("KDE Plasma Wayland", log_output)
self.assertIn("Virtual Keyboard", log_output)
mock_ibus_class.assert_not_called()
self.assertEqual(obj.wayland_tool, "wtype")
def test_kde_wayland_respects_explicit_non_ibus_input_method(self):
from vocalinux.text_injection.text_injector import DesktopEnvironment
obj = _make_injector(DesktopEnvironment.WAYLAND)
with (
patch.dict(
os.environ,
{
"XDG_SESSION_TYPE": "wayland",
"XDG_CURRENT_DESKTOP": "KDE",
"QT_IM_MODULE": "fcitx",
},
clear=True,
),
patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=True),
patch(
"vocalinux.text_injection.text_injector.is_ibus_active_input_method",
return_value=False,
),
patch(
"vocalinux.text_injection.text_injector.is_ibus_daemon_running",
return_value=True,
),
patch("vocalinux.text_injection.text_injector.IBusTextInjector") as mock_ibus_class,
patch(
"shutil.which",
side_effect=lambda cmd: "/usr/bin/ydotool" if cmd == "ydotool" else None,
),
patch.object(obj, "_is_ydotoold_running", return_value=True),
):
obj._check_dependencies()
mock_ibus_class.assert_not_called()
self.assertEqual(obj.wayland_tool, "ydotool")
def test_kde_wayland_wtype_probe_logs_ibus_hint(self):
from vocalinux.text_injection.text_injector import DesktopEnvironment, TextInjector