Plugins exposed via d-bus remote.

This commit is contained in:
Storm Dragon
2026-01-11 23:21:52 -05:00
parent d3c48b1e84
commit 75ad2f0dec
7 changed files with 236 additions and 9 deletions
+5
View File
@@ -648,6 +648,11 @@ def _start_dbus_service():
typing_echo_manager = typing_echo_presenter.getManager()
dbus_service.get_remote_controller().register_decorated_module("TypingEchoManager", typing_echo_manager)
# Register plugin system manager
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Registering PluginSystemManager D-Bus module', True)
plugin_manager = cthulhuApp.getPluginSystemManager()
dbus_service.get_remote_controller().register_decorated_module("PluginSystemManager", plugin_manager)
except Exception as e:
msg = f"CTHULHU: Failed to start D-Bus service: {e}"
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
+31 -3
View File
@@ -30,7 +30,7 @@ __license__ = "LGPL"
import enum
import inspect
import os
from typing import Callable, Optional
from typing import Any, Callable, Optional
try:
from dasbus.connection import SessionMessageBus
@@ -151,6 +151,22 @@ def _extract_function_parameters(func: Callable) -> list[tuple[str, str]]:
return parameters
def _filter_kwargs_for_callable(method: Callable, kwargs: dict[str, Any]) -> dict[str, Any]:
"""Filters kwargs down to what the callable accepts."""
try:
sig = inspect.signature(method)
except (TypeError, ValueError):
return kwargs
for param in sig.parameters.values():
if param.kind == inspect.Parameter.VAR_KEYWORD:
return kwargs
allowed = set(sig.parameters.keys())
allowed.discard("self")
return {key: value for key, value in kwargs.items() if key in allowed}
class _HandlerInfo:
"""Stores processed information about a function exposed via D-Bus."""
@@ -691,7 +707,13 @@ class CthulhuRemoteController:
if script is None:
manager = script_manager.get_manager()
script = manager.get_default_script()
rv = method(script=script, event=event, notify_user=notify_user)
kwargs = {
"script": script,
"event": event,
"notify_user": notify_user
}
call_kwargs = _filter_kwargs_for_callable(method, kwargs)
rv = method(**call_kwargs)
_get_input_event_manager().get_manager().process_remote_controller_event(event)
return rv
return _wrapper
@@ -715,7 +737,13 @@ class CthulhuRemoteController:
if script is None:
manager = script_manager.get_manager()
script = manager.get_default_script()
rv = method(script=script, event=event, **kwargs)
merged_kwargs = {
"script": script,
"event": event
}
merged_kwargs.update(kwargs)
call_kwargs = _filter_kwargs_for_callable(method, merged_kwargs)
rv = method(**call_kwargs)
_get_input_event_manager().get_manager().process_remote_controller_event(event)
return rv
return _wrapper
+6
View File
@@ -63,6 +63,12 @@ class Plugin:
self.version = plugin_info.get_version()
self.description = plugin_info.get_description()
def get_dbus_module_name(self):
"""Return the D-Bus module name for this plugin, or an empty string to opt out."""
if not self.module_name:
return ''
return f"Plugin_{self.module_name}"
@cthulhu_hookimpl
def activate(self, plugin=None):
"""Activate the plugin. Override this in subclasses."""
+118
View File
@@ -19,6 +19,8 @@ import shutil
import subprocess
from enum import IntEnum
from . import dbus_service
# Import pluggy if available
try:
import pluggy
@@ -638,6 +640,118 @@ class PluginSystemManager:
active_instances.append(plugin_info.instance)
return active_instances
def _resolve_plugin_info(self, plugin_name):
if not plugin_name:
return None
if not self._plugins:
self.rescanPlugins()
plugin_name_lower = plugin_name.lower()
candidates = []
for info in self.plugins:
module_name = info.get_module_name()
canonical_name = info.get_canonical_name()
if plugin_name == module_name:
return info
if plugin_name_lower == module_name.lower():
candidates.append(info)
elif plugin_name_lower == canonical_name.lower():
candidates.append(info)
if not candidates:
return None
for info in candidates:
if info.builtin:
return info
for info in candidates:
if info.preferred_alias:
return info
return candidates[0]
def _get_plugin_dbus_module_name(self, plugin_info, plugin_instance):
if not plugin_info or not plugin_instance:
return ''
if hasattr(plugin_instance, "get_dbus_module_name"):
module_name = plugin_instance.get_dbus_module_name()
else:
module_name = f"Plugin_{plugin_info.get_module_name()}"
return module_name or ''
def _register_plugin_dbus_module(self, plugin_info):
if not plugin_info or not plugin_info.instance:
return
module_name = self._get_plugin_dbus_module_name(plugin_info, plugin_info.instance)
if not module_name:
return
try:
controller = dbus_service.get_remote_controller()
controller.register_decorated_module(module_name, plugin_info.instance)
logger.info(f"Registered D-Bus module for plugin {plugin_info.get_module_name()}: {module_name}")
except Exception as error:
logger.error(f"Failed to register D-Bus module for plugin {plugin_info.get_module_name()}: {error}")
def _deregister_plugin_dbus_module(self, plugin_info):
if not plugin_info or not plugin_info.instance:
return
module_name = self._get_plugin_dbus_module_name(plugin_info, plugin_info.instance)
if not module_name:
return
try:
controller = dbus_service.get_remote_controller()
controller.deregister_module_commands(module_name)
logger.info(f"Deregistered D-Bus module for plugin {plugin_info.get_module_name()}: {module_name}")
except Exception as error:
logger.error(f"Failed to deregister D-Bus module for plugin {plugin_info.get_module_name()}: {error}")
@dbus_service.command
def list_plugins(self):
"""Returns a list of available plugin module names."""
if not self._plugins:
self.rescanPlugins()
return [info.get_module_name() for info in self.plugins]
@dbus_service.command
def list_active_plugins(self):
"""Returns a list of currently active plugin module names."""
return [info.get_module_name() for info in self.plugins if info.loaded]
@dbus_service.parameterized_command
def is_plugin_active(self, plugin_name: str) -> bool:
"""Returns True if the specified plugin is active."""
plugin_info = self._resolve_plugin_info(plugin_name)
if not plugin_info:
return False
return self.isPluginActive(plugin_info)
@dbus_service.parameterized_command
def set_plugin_active(self, plugin_name: str, active: bool) -> bool:
"""Enable or disable a plugin for this session only."""
plugin_info = self._resolve_plugin_info(plugin_name)
if not plugin_info:
return False
if plugin_info.builtin and not active:
logger.warning(f"Plugin {plugin_info.get_module_name()} is builtin and cannot be disabled")
return False
self.setPluginActive(plugin_info, active)
if active:
return plugin_info.loaded
return not plugin_info.loaded
@dbus_service.command
def rescan_plugins(self):
"""Rescans plugin directories and refreshes the plugin list."""
self.rescanPlugins()
return True
def setActivePlugins(self, activePlugins):
"""Set active plugins and sync their state."""
logger.info(f"PLUGIN SYSTEM: setActivePlugins called with: {activePlugins}")
@@ -939,6 +1053,8 @@ class PluginSystemManager:
pluginInfo.loaded = True
logger.info(f"Successfully loaded plugin: {module_name}")
self._register_plugin_dbus_module(pluginInfo)
# Register any global keybindings from the plugin
self.register_plugin_global_keybindings(pluginInfo.instance)
@@ -974,6 +1090,8 @@ class PluginSystemManager:
except Exception as e:
logger.error(f"Error deactivating plugin {module_name}: {e}")
self._deregister_plugin_dbus_module(pluginInfo)
# Unregister from pluggy
self.plugin_manager.unregister(plugin_instance)