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
This commit is contained in:
Jatin K Malik
2026-02-25 11:56:50 -08:00
committed by GitHub
parent 98bf30cfa6
commit cb9b66adf3
4 changed files with 113 additions and 1 deletions
+14
View File
@@ -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", {})
+1
View File
@@ -10,4 +10,5 @@ from .ibus_engine import ( # noqa: F401
is_engine_active,
is_engine_registered,
is_ibus_available,
start_ibus_daemon,
)
@@ -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()
+60 -1
View File
@@ -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: