diff --git a/src/vocalinux/text_injection/text_injector.py b/src/vocalinux/text_injection/text_injector.py index 11f2abf..f0ccee1 100644 --- a/src/vocalinux/text_injection/text_injector.py +++ b/src/vocalinux/text_injection/text_injector.py @@ -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: diff --git a/tests/test_text_injector_ext.py b/tests/test_text_injector_ext.py index 845dae1..dba42ac 100644 --- a/tests/test_text_injector_ext.py +++ b/tests/test_text_injector_ext.py @@ -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