From 5a0e16da899c37868730c8a960db6606ca174bec Mon Sep 17 00:00:00 2001 From: Xavier Karma Date: Fri, 6 Nov 2026 19:53:04 +0530 Subject: [PATCH] fix(inject): explain the silent-drop risk of an explicit IBus pin on niri An explicit ibus pin (VOCULINUX_FORCE_BACKEND or text_injection.backend) is still honoured on niri, as the documented escape hatch. The one warning it logs now names niri and says IBus there can report success while text is silently dropped, instead of the generic compositor wording. Refs #16 --- src/vocalinux/text_injection/text_injector.py | 25 ++++++++++++----- tests/test_text_injector_ext.py | 28 ++++++++++++++++++- 2 files changed, 45 insertions(+), 8 deletions(-) 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