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: