diff --git a/src/vocalinux/text_injection/text_injector.py b/src/vocalinux/text_injection/text_injector.py index 2de0d23..6836b9e 100644 --- a/src/vocalinux/text_injection/text_injector.py +++ b/src/vocalinux/text_injection/text_injector.py @@ -117,6 +117,14 @@ def _is_niri_session() -> bool: return any("niri" in value.lower() for value in desktop_values) +#: Logged once when an explicit ibus pin is honoured on niri (spec #16). +_NIRI_IBUS_PIN_WARNING = ( + "IBus on niri can report success while text is silently dropped: niri does not " + "relay IBus commits to native Wayland windows. The explicit ibus pin is honoured; " + "remove it to type directly with wtype or ydotool instead." +) + + def _kde_wayland_ibus_hint() -> str: """Return the KDE Plasma Wayland IBus setup hint.""" return ( @@ -989,13 +997,16 @@ class TextInjector: # reports the commit as delivered and the text never arrives. # The other two guards are out of scope for this warning. if force_ibus and not self._wayland_compositor_bridges_ibus(): - logger.warning( - "Compositor '%s' does not bridge IBus to native Wayland apps, but an " - "explicit ibus pin overrides that check. Dictation may silently do " - "nothing in native Wayland windows; remove the pin to fall back to " - "wtype/ydotool.", - os.environ.get("XDG_CURRENT_DESKTOP", "unknown"), - ) + if _is_niri_session(): + logger.warning(_NIRI_IBUS_PIN_WARNING) + else: + logger.warning( + "Compositor '%s' does not bridge IBus to native Wayland apps, but an " + "explicit ibus pin overrides that check. Dictation may silently do " + "nothing in native Wayland windows; remove the pin to fall back to " + "wtype/ydotool.", + os.environ.get("XDG_CURRENT_DESKTOP", "unknown"), + ) try: if wayland_scoped_ibus and not ibus_active: logger.info( diff --git a/tests/test_text_injector_ext.py b/tests/test_text_injector_ext.py index 98d5518..c083787 100644 --- a/tests/test_text_injector_ext.py +++ b/tests/test_text_injector_ext.py @@ -497,6 +497,7 @@ class TestCheckDependencies(unittest.TestCase): desktop: str = "Hyprland", env: Any = None, config_pin: bool = False, + ibus_class: Any = None, ) -> tuple[list[str], int]: """Drive _check_dependencies with an ibus pin; return (logs, bridge_call_count).""" from vocalinux.text_injection.text_injector import DesktopEnvironment, TextInjector @@ -536,7 +537,10 @@ class TestCheckDependencies(unittest.TestCase): patch( "vocalinux.text_injection.text_injector.is_ibus_daemon_running", return_value=True ), - patch("vocalinux.text_injection.text_injector.IBusTextInjector"), + patch( + "vocalinux.text_injection.text_injector.IBusTextInjector", + new=ibus_class if ibus_class is not None else MagicMock(), + ), patch.object(TextInjector, "_start_ibus_initialization", lambda s: None), # shutil.which is patched to claim every tool exists, so without this # the real probe shells out to a ydotoold that may not be installed @@ -595,6 +599,28 @@ class TestCheckDependencies(unittest.TestCase): _, call_count = self._run_ibus_pin(bridges=bridges) self.assertEqual(call_count, 1, f"bridges={bridges}: expected 1 call, got {call_count}") + def test_ibus_pin_on_niri_is_honoured_with_one_silent_drop_warning(self): + """An explicit IBus pin stays an escape hatch on niri, with one clear warning. + + Both pin sources must construct the IBus injector (the pin is honoured) + and log exactly one WARNING saying IBus on niri can report success while + text is silently dropped. + """ + for config_pin in (False, True): + with self.subTest(config_pin=config_pin): + ibus_class = MagicMock() + output, _ = self._run_ibus_pin( + bridges=False, + desktop="niri", + config_pin=config_pin, + ibus_class=ibus_class, + ) + ibus_class.assert_called_once() + warnings = [line for line in output if line.startswith("WARNING:")] + self.assertEqual(len(warnings), 1, f"expected one WARNING, got: {warnings}") + self.assertIn("niri", warnings[0]) + self.assertIn("can report success while text is silently dropped", warnings[0]) + def test_ibus_pin_on_x11_does_not_warn(self): """The denylist is a Wayland concern; X11 reaches apps through XIM.""" from vocalinux.text_injection.text_injector import DesktopEnvironment