fix(ibus): start engine process before checking registration (fixes #360) (#361)

* fix(ibus): start engine process before checking registration

_setup_engine() gated on is_engine_registered() which queries
`ibus list-engine`. On systems where the IBus daemon doesn't scan
user-level component directories (~/.local/share/ibus/component/),
the engine never appeared in the list, blocking start_engine_process()
from ever being called — even though its register_component() D-Bus
call is the correct registration path for standalone-launched engines.

Reorder _setup_engine() to start the engine process first, relying on
register_component() for runtime registration. Remove the XML
install step from the setup path since install_ibus_component()
triggers `ibus restart` which kills the engine process.

Follow-up to PR #304 which added register_component() support.

* fix(ibus): remove dead code and add engine readiness check

Address review feedback:
- Remove install_ibus_component(), is_engine_registered(),
  is_component_up_to_date(), _get_expected_component_xml()
  and their tests — no longer called after setup order fix
- Add SOCKET_PATH poll after start_engine_process() to verify
  engine is fully initialized before activation
- Rebase onto current main
This commit is contained in:
Sbenazar
2026-04-05 17:31:35 -07:00
committed by GitHub
parent 04c8645e2a
commit c4f5b797ce
5 changed files with 35 additions and 465 deletions
-2
View File
@@ -6,9 +6,7 @@ from . import text_injector # noqa: F401
from .ibus_engine import ( # noqa: F401
IBusSetupError,
IBusTextInjector,
install_ibus_component,
is_engine_active,
is_engine_registered,
is_ibus_active_input_method,
is_ibus_available,
start_ibus_daemon,
+23 -162
View File
@@ -259,76 +259,6 @@ def _get_exec_command() -> str:
return f"{sys.executable} {engine_script} --ibus"
def _get_expected_component_xml() -> str:
"""Generate the expected component XML content for the current installation."""
e = _ENGINE_META
c = _COMPONENT_META
return f"""<?xml version="1.0" encoding="utf-8"?>
<component>
<name>{COMPONENT_NAME}</name>
<description>{ENGINE_DESCRIPTION}</description>
<exec>{_get_exec_command()}</exec>
<version>{c['version']}</version>
<author>{c['author']}</author>
<license>{c['license']}</license>
<homepage>{c['homepage']}</homepage>
<textdomain>{c['textdomain']}</textdomain>
<engines>
<engine>
<name>{ENGINE_NAME}</name>
<longname>{ENGINE_LONGNAME}</longname>
<language>{e['language']}</language>
<license>{e['license']}</license>
<author>{e['author']}</author>
<icon>{e['icon']}</icon>
<layout>{e['layout']}</layout>
<description>{ENGINE_DESCRIPTION}</description>
<rank>{ENGINE_RANK}</rank>
</engine>
</engines>
</component>
"""
def is_component_up_to_date() -> bool:
"""
Check if the installed IBus component XML matches the current installation.
Returns:
True if component exists and matches expected content, False otherwise
"""
component_file = Path.home() / ".local" / "share" / "ibus" / "component" / "vocalinux.xml"
if not component_file.exists():
return False
try:
installed_content = component_file.read_text()
expected_content = _get_expected_component_xml()
return installed_content.strip() == expected_content.strip()
except Exception as e:
logger.debug(f"Failed to check component XML: {e}")
return False
def is_engine_registered() -> bool:
"""Check if the Vocalinux IBus engine is registered."""
if not IBUS_AVAILABLE:
return False
try:
result = subprocess.run(
["ibus", "list-engine"],
capture_output=True,
text=True,
timeout=5,
)
return ENGINE_NAME in result.stdout
except (subprocess.SubprocessError, FileNotFoundError):
return False
def is_engine_active() -> bool:
"""Check if the Vocalinux IBus engine is currently active."""
try:
@@ -837,11 +767,12 @@ class IBusTextInjector:
This class connects to the Vocalinux IBus engine via Unix socket
and sends text to be injected. On initialization, it automatically:
1. Installs the IBus component if not registered
2. Saves the current engine
1. Starts the engine process (registers via D-Bus register_component)
2. Saves the current engine and XKB layout
3. Switches to the Vocalinux engine
4. Restores the XKB layout
On cleanup (stop), it restores the previous engine.
On cleanup (stop), it restores the previous engine and layout.
"""
def __init__(self, auto_activate: bool = True):
@@ -863,27 +794,28 @@ class IBusTextInjector:
def _setup_engine(self) -> None:
"""Install and activate the IBus engine."""
# Install or update component if needed
if not is_engine_registered():
logger.info("IBus engine not registered, installing...")
if not install_ibus_component(system_wide=False):
logger.info("User-level install failed, trying system-wide...")
install_ibus_component(system_wide=True)
elif not is_component_up_to_date():
logger.info("IBus component XML is outdated, updating...")
install_ibus_component(system_wide=False)
# Check again after installation
if not is_engine_registered():
raise IBusSetupError(
"Failed to register IBus engine. "
"Try running 'ibus list-engine' to verify installation."
)
# Start the engine process (Vocalinux starts it, not IBus)
# Start the engine process — it calls register_component() via
# D-Bus which makes the engine available even when the IBus daemon
# doesn't scan ~/.local/share/ibus/component/.
# Follow-up to PR #304: register_component() is the reliable path,
# so we no longer gate on is_engine_registered() / ibus list-engine.
if not start_engine_process():
raise IBusSetupError("Failed to start IBus engine process. Check logs for details.")
# Verify the engine is fully ready before proceeding.
# start_engine_process() only confirms the subprocess is alive —
# register_component() and socket setup may still be in progress.
for _attempt in range(15):
if SOCKET_PATH.exists():
logger.debug("Engine socket is ready")
break
time.sleep(0.2)
else:
logger.warning(
"Engine process started but socket not ready after retries; "
"proceeding with activation attempt"
)
# Capture current XKB layout before switching engines
# This is critical for preserving the user's keyboard layout
# when IBus engine switching might override it
@@ -995,77 +927,6 @@ class IBusTextInjector:
return False
def install_ibus_component(system_wide: bool = False) -> bool:
"""
Install the IBus component XML file.
Args:
system_wide: If True, install to /usr/share/ibus/component/ (requires root).
If False, install to user directory and set IBUS_COMPONENT_PATH.
Returns:
True if installation was successful, False otherwise
"""
component_xml = _get_expected_component_xml()
try:
if system_wide:
component_dir = Path("/usr/share/ibus/component")
component_file = component_dir / "vocalinux.xml"
# Write to temp file and move with sudo
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".xml", delete=False) as f:
f.write(component_xml)
temp_path = f.name
result = subprocess.run(
["sudo", "cp", temp_path, str(component_file)],
capture_output=True,
text=True,
)
os.unlink(temp_path)
if result.returncode != 0:
logger.error(f"Failed to install component: {result.stderr}")
return False
subprocess.run(
["sudo", "chmod", "644", str(component_file)],
capture_output=True,
)
else:
# User-level installation
component_dir = Path.home() / ".local" / "share" / "ibus" / "component"
component_dir.mkdir(parents=True, exist_ok=True)
component_file = component_dir / "vocalinux.xml"
component_file.write_text(component_xml)
logger.info(f"Component installed to {component_file}")
# Refresh IBus - restart with component path set so it picks up user components
import time
user_component_dir = Path.home() / ".local" / "share" / "ibus" / "component"
env = os.environ.copy()
env["IBUS_COMPONENT_PATH"] = f"{user_component_dir}:/usr/share/ibus/component"
subprocess.run(["ibus", "write-cache"], capture_output=True, env=env)
subprocess.run(["ibus", "restart"], capture_output=True, env=env)
time.sleep(1) # Give IBus time to restart
logger.info(
"IBus component installed successfully.\n"
"Set 'Vocalinux' as your input method in system settings "
"to enable voice dictation."
)
return True
except Exception as e:
logger.error(f"Failed to install IBus component: {e}")
return False
def _get_engines_xml() -> str:
"""Return engine XML for IBus --xml discovery.