From cb9b66adf32981cfd2dfa454a5ef6ba56c5bc1f1 Mon Sep 17 00:00:00 2001 From: Jatin K Malik Date: Wed, 25 Feb 2026 11:56:50 -0800 Subject: [PATCH] fix: resolve text injection issues from issue #34 (#261) * fix(text_injection): re-check for better tools after xdotool fallback When wtype fails due to compositor not supporting virtual keyboard protocol, the code switches to xdotool permanently. This prevents using ydotool even if its daemon is started later. This fix adds _try_recover_from_fallback() method that checks for better tools (ydotool with running daemon, or wtype with compositor support) on each text injection attempt when in fallback mode. Fixes issue #34 comment 3 (logs show xdotool but ydotool works) * feat(main): auto-start IBus daemon on Vocalinux launch This resolves the IBus autostart issue mentioned in issue #34. When Vocalinux starts, it now automatically starts the IBus daemon if: - IBus is installed on the system - IBus daemon is not already running This removes the need for users to manually configure IBus autostart on desktop environments where it doesn't start automatically (e.g., some KDE Plasma installations or minimal window managers). Changes: - Add start_ibus_daemon() function in ibus_engine.py - Call start_ibus_daemon() in main() after logging is initialized - Export start_ibus_daemon in text_injection/__init__.py --- src/vocalinux/main.py | 14 +++++ src/vocalinux/text_injection/__init__.py | 1 + src/vocalinux/text_injection/ibus_engine.py | 38 ++++++++++++ src/vocalinux/text_injection/text_injector.py | 61 ++++++++++++++++++- 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/vocalinux/main.py b/src/vocalinux/main.py index e68b76e..0c29f38 100644 --- a/src/vocalinux/main.py +++ b/src/vocalinux/main.py @@ -188,6 +188,20 @@ def main(): initialize_logging() logger.info("Logging system initialized") + # Try to start IBus daemon if not running (for text injection) + # This helps on desktop environments where IBus doesn't start automatically + try: + from .text_injection import start_ibus_daemon + + if start_ibus_daemon(): + logger.debug("IBus daemon started for text injection") + except Exception as e: + logger.debug(f"Could not start IBus daemon: {e}") + + config_manager = ConfigManager() + initialize_logging() + logger.info("Logging system initialized") + config_manager = ConfigManager() saved_settings = config_manager.get_settings().get("speech_recognition", {}) audio_settings = config_manager.get_settings().get("audio", {}) diff --git a/src/vocalinux/text_injection/__init__.py b/src/vocalinux/text_injection/__init__.py index 3ebadab..e4f4779 100644 --- a/src/vocalinux/text_injection/__init__.py +++ b/src/vocalinux/text_injection/__init__.py @@ -10,4 +10,5 @@ from .ibus_engine import ( # noqa: F401 is_engine_active, is_engine_registered, is_ibus_available, + start_ibus_daemon, ) diff --git a/src/vocalinux/text_injection/ibus_engine.py b/src/vocalinux/text_injection/ibus_engine.py index 4b5c2e0..06c6c7a 100644 --- a/src/vocalinux/text_injection/ibus_engine.py +++ b/src/vocalinux/text_injection/ibus_engine.py @@ -22,6 +22,7 @@ import struct import subprocess import sys import threading +import time from pathlib import Path from typing import Optional @@ -133,6 +134,43 @@ def is_ibus_daemon_running() -> bool: return False +def start_ibus_daemon(): + """ + Start the IBus daemon if it's not already running. + + This is useful for desktop environments where IBus doesn't start automatically, + such as some KDE Plasma installations or minimal window managers. + + Returns: + True if daemon was started or already running, False on failure + """ + if is_ibus_daemon_running(): + return True + + if not is_ibus_available(): + return False + + try: + # Start ibus-daemon in the background with XIM support + # -x: Enable XIM (X Input Method) + # -d: Run as daemon + # -r: Replace existing daemon + subprocess.Popen( + ["ibus-daemon", "-x", "-d", "-r"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + ) + # Give daemon time to start + time.sleep(0.5) + return is_ibus_daemon_running() + except FileNotFoundError: + # ibus-daemon not found + return False + except Exception: + return False + + def _get_expected_component_xml() -> str: """Generate the expected component XML content for the current installation.""" engine_script = Path(__file__).resolve() diff --git a/src/vocalinux/text_injection/text_injector.py b/src/vocalinux/text_injection/text_injector.py index c6c99ab..1bca457 100644 --- a/src/vocalinux/text_injection/text_injector.py +++ b/src/vocalinux/text_injection/text_injector.py @@ -239,6 +239,58 @@ class TextInjector: except Exception as e: logger.error(f"Failed to test XWayland fallback: {e}") + def _try_recover_from_fallback(self): + """ + Try to recover from xdotool fallback mode by re-checking for better tools. + + This allows switching to ydotool if the daemon was started after initial detection, + or to wtype if the compositor now supports virtual keyboard. + + Returns: + True if a better tool was found and environment was updated, False otherwise + """ + if self.environment != DesktopEnvironment.WAYLAND_XDOTOOL: + return False + + logger.info("Checking for better Wayland text injection tools...") + + # Check for ydotool with daemon running + if shutil.which("ydotool"): + try: + subprocess.run( + ["ydotool", "type", ""], + check=True, + stderr=subprocess.PIPE, + timeout=2, + ) + self.wayland_tool = "ydotool" + self.environment = DesktopEnvironment.WAYLAND + logger.info("Recovered to ydotool - ydotoold daemon is now running") + return True + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + logger.debug("ydotool available but daemon not running") + + # Check for wtype with compositor support + if shutil.which("wtype"): + try: + result = subprocess.run( + ["wtype", "test"], + stderr=subprocess.PIPE, + text=True, + check=False, + ) + error_output = result.stderr.lower() + if "compositor does not support" not in error_output and result.returncode == 0: + self.wayland_tool = "wtype" + self.environment = DesktopEnvironment.WAYLAND + logger.info("Recovered to wtype - compositor now supports virtual keyboard") + return True + except Exception as e: + logger.debug(f"Error testing wtype: {e}") + + logger.debug("No better tools available, continuing with xdotool fallback") + return False + def inject_text(self, text: str) -> bool: """ Inject text into the currently focused application. @@ -263,6 +315,11 @@ class TextInjector: # which passes text directly without shell interpretation logger.debug(f"Text to inject: '{text}'") + # Re-check for available tools in Wayland fallback mode + # This allows switching to ydotool if the daemon was started after initial detection + if self.environment == DesktopEnvironment.WAYLAND_XDOTOOL: + self._try_recover_from_fallback() + try: if ( self.environment == DesktopEnvironment.WAYLAND_IBUS @@ -289,7 +346,9 @@ class TextInjector: if "compositor does not support" in str( e ).lower() + " " + stderr_msg.lower() and shutil.which("xdotool"): - logger.info("Automatically switching to XWayland fallback permanently") + logger.info( + "Switching to XWayland fallback - will re-check for better tools" + ) self.environment = DesktopEnvironment.WAYLAND_XDOTOOL self._inject_with_xdotool(text) else: