From b6fb2b838b058b214d723ea4a1660e546d1b9e3b Mon Sep 17 00:00:00 2001
From: Graham Jenkins <6607975+GrahamJenkins@users.noreply.github.com>
Date: Tue, 17 Mar 2026 18:11:31 -0700
Subject: [PATCH] fix(text-injection): use register_component for IBus engine
activation (#304)
* fix(ibus): add register_component for standalone launch and --xml/--ibus flag handling (#303)
* refactor(ibus): consolidate engine metadata and add command_line/request_name checks (#303)
---
src/vocalinux/text_injection/ibus_engine.py | 137 +++++++++++++++++---
1 file changed, 118 insertions(+), 19 deletions(-)
diff --git a/src/vocalinux/text_injection/ibus_engine.py b/src/vocalinux/text_injection/ibus_engine.py
index 49a7ca1..3c4844c 100644
--- a/src/vocalinux/text_injection/ibus_engine.py
+++ b/src/vocalinux/text_injection/ibus_engine.py
@@ -68,6 +68,24 @@ ENGINE_LONGNAME = "Vocalinux"
ENGINE_DESCRIPTION = "Vocalinux voice dictation (use as default input method)"
COMPONENT_NAME = "org.freedesktop.IBus.Vocalinux"
+# Shared metadata used by component XML, --xml output, and runtime registration.
+# Kept in one place to prevent drift between the three consumers.
+ENGINE_RANK = 50
+_ENGINE_META = {
+ "language": "other",
+ "license": "GPL-3.0",
+ "author": "Vocalinux",
+ "icon": "audio-input-microphone",
+ "layout": "default",
+}
+_COMPONENT_META = {
+ "version": "1.0",
+ "license": "GPL-3.0",
+ "author": "Vocalinux",
+ "homepage": "https://github.com/jatinkrmalik/vocalinux",
+ "textdomain": "vocalinux",
+}
+
def ensure_ibus_dir() -> None:
"""Ensure the IBus data directory exists with secure permissions."""
@@ -210,32 +228,38 @@ def start_ibus_daemon():
return False
+def _get_exec_command() -> str:
+ """Return the engine exec command for component XML and runtime registration."""
+ engine_script = Path(__file__).resolve()
+ return f"{sys.executable} {engine_script} --ibus"
+
+
def _get_expected_component_xml() -> str:
"""Generate the expected component XML content for the current installation."""
- engine_script = Path(__file__).resolve()
- python_exec = sys.executable
+ e = _ENGINE_META
+ c = _COMPONENT_META
return f"""
{COMPONENT_NAME}
{ENGINE_DESCRIPTION}
- {python_exec} {engine_script}
- 1.0
- Vocalinux
- GPL-3.0
- https://github.com/jatinkrmalik/vocalinux
- vocalinux
+ {_get_exec_command()}
+ {c['version']}
+ {c['author']}
+ {c['license']}
+ {c['homepage']}
+ {c['textdomain']}
{ENGINE_NAME}
- other
- GPL-3.0
- Vocalinux
- audio-input-microphone
- default
{ENGINE_LONGNAME}
+ {e['language']}
+ {e['license']}
+ {e['author']}
+ {e['icon']}
+ {e['layout']}
{ENGINE_DESCRIPTION}
- 50
+ {ENGINE_RANK}
@@ -612,22 +636,63 @@ class VocalinuxEngineApplication:
This is used when the engine is launched by IBus as a separate process.
"""
- def __init__(self):
- """Initialize the engine application."""
+ def __init__(self, exec_by_ibus: bool = False):
+ """Initialize the engine application.
+
+ Args:
+ exec_by_ibus: True when IBus launched this process (via --ibus flag).
+ False when Vocalinux launched it directly.
+ """
if not IBUS_AVAILABLE:
raise RuntimeError("IBus is not available")
self.mainloop = GLib.MainLoop()
self.bus = IBus.Bus()
+
+ if not self.bus.is_connected():
+ logger.error("IBus.Bus() is NOT connected — engine cannot register")
+ raise RuntimeError("IBus bus not connected")
+
self.bus.connect("disconnected", self._on_disconnected)
- self.factory = IBus.Factory.new(self.bus.get_connection())
+ conn = self.bus.get_connection()
+ if conn is None:
+ logger.error("bus.get_connection() returned None")
+ raise RuntimeError("IBus bus connection is None")
+ self.factory = IBus.Factory.new(conn)
self.factory.add_engine(
ENGINE_NAME,
GObject.type_from_name("VocalinuxEngine"),
)
- self.bus.request_name(COMPONENT_NAME, 0)
+ if exec_by_ibus:
+ # IBus launched us — it already knows about our component,
+ # just claim the well-known D-Bus name.
+ if not self.bus.request_name(COMPONENT_NAME, 0):
+ logger.error("bus.request_name() failed")
+ raise RuntimeError("Failed to acquire IBus D-Bus name")
+ else:
+ # Launched by Vocalinux directly — register the full component
+ # so IBus discovers our engine without having launched us.
+ component = IBus.Component(
+ name=COMPONENT_NAME,
+ description=ENGINE_DESCRIPTION,
+ command_line=_get_exec_command(),
+ **_COMPONENT_META,
+ )
+ engine_desc = IBus.EngineDesc(
+ name=ENGINE_NAME,
+ longname=ENGINE_LONGNAME,
+ description=ENGINE_DESCRIPTION,
+ rank=ENGINE_RANK,
+ **_ENGINE_META,
+ )
+ component.add_engine(engine_desc)
+ if not self.bus.register_component(component):
+ logger.error("bus.register_component() failed")
+ raise RuntimeError("Failed to register IBus component")
+ logger.info("Registered component with IBus (standalone mode)")
+
logger.info("Vocalinux IBus engine started")
# Start the socket server immediately so it's ready for connections
@@ -851,8 +916,41 @@ def install_ibus_component(system_wide: bool = False) -> bool:
return False
+def _get_engines_xml() -> str:
+ """Return engine XML for IBus --xml discovery.
+
+ IBus invokes `` --xml`` during ``ibus write-cache`` and
+ ``ibus list-engine`` to discover available engines. The expected
+ output is a bare ```` block printed to stdout.
+ """
+ e = _ENGINE_META
+
+ return f"""
+
+ {ENGINE_NAME}
+ {ENGINE_LONGNAME}
+ {e['language']}
+ {e['license']}
+ {e['author']}
+ {e['icon']}
+ {e['layout']}
+
+
+ {ENGINE_DESCRIPTION}
+ {ENGINE_RANK}
+
+"""
+
+
def main():
"""Entry point when run as IBus engine process."""
+ # IBus calls the exec with --xml to discover engines during
+ # ibus write-cache and ibus list-engine. Respond and exit
+ # immediately — do not enter the GLib main loop.
+ if "--xml" in sys.argv:
+ print(_get_engines_xml())
+ return 0
+
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
@@ -863,7 +961,8 @@ def main():
return 1
IBus.init()
- app = VocalinuxEngineApplication()
+ exec_by_ibus = "--ibus" in sys.argv
+ app = VocalinuxEngineApplication(exec_by_ibus=exec_by_ibus)
app.run()
return 0