Plugins exposed via d-bus remote.
This commit is contained in:
@@ -138,6 +138,24 @@ are categorized as **Commands**, **Runtime Getters**, and **Runtime Setters**:
|
||||
|
||||
You can discover and execute these for each module.
|
||||
|
||||
### Plugin Modules
|
||||
|
||||
Plugins that expose D-Bus decorators are automatically registered as modules using the naming
|
||||
convention `Plugin_<ModuleName>` (e.g., `Plugin_GameMode`, `Plugin_WindowTitleReader`). Use
|
||||
`ListModules` to discover available plugin modules at runtime.
|
||||
|
||||
### PluginSystemManager Module
|
||||
|
||||
The `PluginSystemManager` module provides session-only plugin control:
|
||||
|
||||
- `ListPlugins`
|
||||
- `ListActivePlugins`
|
||||
- `IsPluginActive` (parameterized)
|
||||
- `SetPluginActive` (parameterized)
|
||||
- `RescanPlugins`
|
||||
|
||||
These calls do **not** persist changes to user preferences.
|
||||
|
||||
### Discovering Module Capabilities
|
||||
|
||||
#### List Commands for a Module
|
||||
|
||||
@@ -45,6 +45,49 @@ toolkit, OpenOffice/LibreOffice, Gecko, WebKitGtk, and KDE Qt toolkit.
|
||||
- **External integration**: Other applications can speak through Cthulhu
|
||||
- **Simple protocol**: `echo "text" | socat - UNIX-CLIENT:/tmp/cthulhu.sock`
|
||||
|
||||
## D-Bus Remote Controller
|
||||
|
||||
Cthulhu exposes a D-Bus service for external automation and integrations.
|
||||
|
||||
### Service Details
|
||||
- **Service Name**: `org.stormux.Cthulhu.Service`
|
||||
- **Main Object Path**: `/org/stormux/Cthulhu/Service`
|
||||
- **Module Object Paths**: `/org/stormux/Cthulhu/Service/<ModuleName>`
|
||||
|
||||
### Discovering Capabilities
|
||||
|
||||
```bash
|
||||
# List registered modules
|
||||
gdbus call --session --dest org.stormux.Cthulhu.Service \
|
||||
--object-path /org/stormux/Cthulhu/Service \
|
||||
--method org.stormux.Cthulhu.Service.ListModules
|
||||
|
||||
# List commands on a module
|
||||
gdbus call --session --dest org.stormux.Cthulhu.Service \
|
||||
--object-path /org/stormux/Cthulhu/Service/ModuleName \
|
||||
--method org.stormux.Cthulhu.Module.ListCommands
|
||||
```
|
||||
|
||||
### Plugin Modules
|
||||
|
||||
Plugins that expose D-Bus decorators are automatically registered as modules using the naming
|
||||
convention `Plugin_<ModuleName>` (for example, `Plugin_GameMode`, `Plugin_WindowTitleReader`).
|
||||
|
||||
### PluginSystemManager Module
|
||||
|
||||
The `PluginSystemManager` module provides **session-only** plugin control (no preference changes):
|
||||
|
||||
- `ListPlugins`
|
||||
- `ListActivePlugins`
|
||||
- `IsPluginActive` (parameterized)
|
||||
- `SetPluginActive` (parameterized)
|
||||
- `RescanPlugins`
|
||||
|
||||
### More Documentation
|
||||
|
||||
See `README-REMOTE-CONTROLLER.md` and `REMOTE-CONTROLLER-COMMANDS.md` for the full D-Bus API
|
||||
and usage examples.
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Requirements
|
||||
|
||||
@@ -49,13 +49,22 @@ busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service \
|
||||
|
||||
## Module-Level Commands
|
||||
|
||||
Currently, no additional modules are exposed via D-Bus beyond the base service commands.
|
||||
Module-level commands are available and can be discovered via `ListModules`. Two key additions are:
|
||||
|
||||
**Planned modules** (to be implemented):
|
||||
- `SpeechAndVerbosityManager` - Speech settings control (muting, verbosity, punctuation, etc.)
|
||||
- `TypingEchoManager` - Typing echo settings (character/word/sentence echo)
|
||||
- `DefaultScript` - Core Cthulhu commands
|
||||
- Additional navigation and presenter modules
|
||||
### PluginSystemManager
|
||||
|
||||
Session-only plugin control (does not persist preferences):
|
||||
|
||||
- `ListPlugins`
|
||||
- `ListActivePlugins`
|
||||
- `IsPluginActive` (parameterized)
|
||||
- `SetPluginActive` (parameterized)
|
||||
- `RescanPlugins`
|
||||
|
||||
### Plugin Modules
|
||||
|
||||
Plugins that expose D-Bus decorators are automatically registered as modules using the naming
|
||||
convention `Plugin_<ModuleName>` (e.g., `Plugin_GameMode`, `Plugin_WindowTitleReader`).
|
||||
|
||||
See [README-REMOTE-CONTROLLER.md](README-REMOTE-CONTROLLER.md) for comprehensive D-Bus API documentation and usage examples.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user