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.
|
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
|
### Discovering Module Capabilities
|
||||||
|
|
||||||
#### List Commands for a Module
|
#### 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
|
- **External integration**: Other applications can speak through Cthulhu
|
||||||
- **Simple protocol**: `echo "text" | socat - UNIX-CLIENT:/tmp/cthulhu.sock`
|
- **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
|
## Dependencies
|
||||||
|
|
||||||
### Core Requirements
|
### Core Requirements
|
||||||
|
|||||||
@@ -49,13 +49,22 @@ busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service \
|
|||||||
|
|
||||||
## Module-Level Commands
|
## 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):
|
### PluginSystemManager
|
||||||
- `SpeechAndVerbosityManager` - Speech settings control (muting, verbosity, punctuation, etc.)
|
|
||||||
- `TypingEchoManager` - Typing echo settings (character/word/sentence echo)
|
Session-only plugin control (does not persist preferences):
|
||||||
- `DefaultScript` - Core Cthulhu commands
|
|
||||||
- Additional navigation and presenter modules
|
- `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.
|
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()
|
typing_echo_manager = typing_echo_presenter.getManager()
|
||||||
dbus_service.get_remote_controller().register_decorated_module("TypingEchoManager", typing_echo_manager)
|
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:
|
except Exception as e:
|
||||||
msg = f"CTHULHU: Failed to start D-Bus service: {e}"
|
msg = f"CTHULHU: Failed to start D-Bus service: {e}"
|
||||||
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
|
debug.printMessage(debug.LEVEL_SEVERE, msg, True)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ __license__ = "LGPL"
|
|||||||
import enum
|
import enum
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
from typing import Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from dasbus.connection import SessionMessageBus
|
from dasbus.connection import SessionMessageBus
|
||||||
@@ -151,6 +151,22 @@ def _extract_function_parameters(func: Callable) -> list[tuple[str, str]]:
|
|||||||
|
|
||||||
return parameters
|
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:
|
class _HandlerInfo:
|
||||||
"""Stores processed information about a function exposed via D-Bus."""
|
"""Stores processed information about a function exposed via D-Bus."""
|
||||||
@@ -691,7 +707,13 @@ class CthulhuRemoteController:
|
|||||||
if script is None:
|
if script is None:
|
||||||
manager = script_manager.get_manager()
|
manager = script_manager.get_manager()
|
||||||
script = manager.get_default_script()
|
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)
|
_get_input_event_manager().get_manager().process_remote_controller_event(event)
|
||||||
return rv
|
return rv
|
||||||
return _wrapper
|
return _wrapper
|
||||||
@@ -715,7 +737,13 @@ class CthulhuRemoteController:
|
|||||||
if script is None:
|
if script is None:
|
||||||
manager = script_manager.get_manager()
|
manager = script_manager.get_manager()
|
||||||
script = manager.get_default_script()
|
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)
|
_get_input_event_manager().get_manager().process_remote_controller_event(event)
|
||||||
return rv
|
return rv
|
||||||
return _wrapper
|
return _wrapper
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ class Plugin:
|
|||||||
self.version = plugin_info.get_version()
|
self.version = plugin_info.get_version()
|
||||||
self.description = plugin_info.get_description()
|
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
|
@cthulhu_hookimpl
|
||||||
def activate(self, plugin=None):
|
def activate(self, plugin=None):
|
||||||
"""Activate the plugin. Override this in subclasses."""
|
"""Activate the plugin. Override this in subclasses."""
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
|
from . import dbus_service
|
||||||
|
|
||||||
# Import pluggy if available
|
# Import pluggy if available
|
||||||
try:
|
try:
|
||||||
import pluggy
|
import pluggy
|
||||||
@@ -638,6 +640,118 @@ class PluginSystemManager:
|
|||||||
active_instances.append(plugin_info.instance)
|
active_instances.append(plugin_info.instance)
|
||||||
return active_instances
|
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):
|
def setActivePlugins(self, activePlugins):
|
||||||
"""Set active plugins and sync their state."""
|
"""Set active plugins and sync their state."""
|
||||||
logger.info(f"PLUGIN SYSTEM: setActivePlugins called with: {activePlugins}")
|
logger.info(f"PLUGIN SYSTEM: setActivePlugins called with: {activePlugins}")
|
||||||
@@ -939,6 +1053,8 @@ class PluginSystemManager:
|
|||||||
pluginInfo.loaded = True
|
pluginInfo.loaded = True
|
||||||
logger.info(f"Successfully loaded plugin: {module_name}")
|
logger.info(f"Successfully loaded plugin: {module_name}")
|
||||||
|
|
||||||
|
self._register_plugin_dbus_module(pluginInfo)
|
||||||
|
|
||||||
# Register any global keybindings from the plugin
|
# Register any global keybindings from the plugin
|
||||||
self.register_plugin_global_keybindings(pluginInfo.instance)
|
self.register_plugin_global_keybindings(pluginInfo.instance)
|
||||||
|
|
||||||
@@ -974,6 +1090,8 @@ class PluginSystemManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deactivating plugin {module_name}: {e}")
|
logger.error(f"Error deactivating plugin {module_name}: {e}")
|
||||||
|
|
||||||
|
self._deregister_plugin_dbus_module(pluginInfo)
|
||||||
|
|
||||||
# Unregister from pluggy
|
# Unregister from pluggy
|
||||||
self.plugin_manager.unregister(plugin_instance)
|
self.plugin_manager.unregister(plugin_instance)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user