move python clide to legacy/
Clide is being rebuilt as a Flutter desktop app. The Python Textual implementation moves wholesale into legacy/ rather than being deleted: its pane model, panel set, git skills, and panel communication design are real thought that should remain readable next to the new code while the rebuild finds its shape. Git's rename tracking preserves history, so `git log -- legacy/` still works. The Flutter rebuild lives at the repo root alongside a Go sidecar (the architecture claudian was heading toward, which folds into clide as a core component rather than a separate plugin project). Bootstrap of the new shape lands in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
"""Extension system for Clide using pluggy."""
|
||||
|
||||
from clide.extensions.hookspecs import ClideHookSpec, hookimpl, hookspec
|
||||
from clide.extensions.manager import ExtensionManager
|
||||
|
||||
__all__ = ["ClideHookSpec", "ExtensionManager", "hookimpl", "hookspec"]
|
||||
@@ -0,0 +1 @@
|
||||
"""Built-in extensions for Clide."""
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Pluggy hook specifications for Clide extensions."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pluggy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from textual.app import App
|
||||
from textual.widget import Widget
|
||||
|
||||
from clide.services.file_watcher import FileEvent
|
||||
|
||||
hookspec = pluggy.HookspecMarker("clide")
|
||||
hookimpl = pluggy.HookimplMarker("clide")
|
||||
|
||||
|
||||
class ClideHookSpec:
|
||||
"""Hook specifications for Clide extensions.
|
||||
|
||||
Extensions implement these hooks to extend functionality.
|
||||
"""
|
||||
|
||||
@hookspec
|
||||
def clide_register_panel(self) -> dict[str, Any] | None:
|
||||
"""Register a custom panel for the UI.
|
||||
|
||||
Returns:
|
||||
Dictionary with panel configuration:
|
||||
- name: Panel identifier
|
||||
- widget: Widget class to instantiate
|
||||
- position: "left", "right", or "bottom"
|
||||
- keybinding: Optional keyboard shortcut
|
||||
"""
|
||||
|
||||
@hookspec
|
||||
def clide_register_commands(self) -> list[dict[str, Any]] | None:
|
||||
"""Register custom commands for the command palette.
|
||||
|
||||
Returns:
|
||||
List of command dictionaries:
|
||||
- name: Command display name
|
||||
- callback: Async callable to execute
|
||||
- description: Help text
|
||||
"""
|
||||
|
||||
@hookspec
|
||||
def clide_on_app_startup(self, app: "App[object]") -> None:
|
||||
"""Called when the application starts.
|
||||
|
||||
Args:
|
||||
app: The Clide application instance
|
||||
"""
|
||||
|
||||
@hookspec
|
||||
def clide_on_app_shutdown(self, app: "App[object]") -> None:
|
||||
"""Called when the application is shutting down.
|
||||
|
||||
Args:
|
||||
app: The Clide application instance
|
||||
"""
|
||||
|
||||
@hookspec
|
||||
def clide_on_file_open(self, path: str) -> None:
|
||||
"""Called when a file is opened in the file browser.
|
||||
|
||||
Args:
|
||||
path: Absolute path to the opened file
|
||||
"""
|
||||
|
||||
@hookspec
|
||||
def clide_modify_widget(self, widget: "Widget") -> "Widget":
|
||||
"""Modify a widget before it's mounted.
|
||||
|
||||
Args:
|
||||
widget: The widget about to be mounted
|
||||
|
||||
Returns:
|
||||
The modified (or original) widget
|
||||
"""
|
||||
|
||||
@hookspec
|
||||
def clide_on_file_changed(self, event: "FileEvent") -> None:
|
||||
"""Called when a file is created, modified, deleted, or moved.
|
||||
|
||||
Extensions can use this to:
|
||||
- Refresh TODO scanning
|
||||
- Re-run linters
|
||||
- Update Jira issue links
|
||||
- Trigger custom actions
|
||||
|
||||
Args:
|
||||
event: The file event with path, type, and timestamp
|
||||
"""
|
||||
|
||||
@hookspec
|
||||
def clide_on_file_saved(self, path: Path) -> None:
|
||||
"""Called after a file is saved by the editor.
|
||||
|
||||
More specific than file_changed - only for user saves.
|
||||
|
||||
Args:
|
||||
path: Absolute path to the saved file
|
||||
"""
|
||||
|
||||
@hookspec
|
||||
def clide_on_claude_event(self, event_type: str, data: dict[str, Any]) -> None:
|
||||
"""Called when Claude Code performs an action.
|
||||
|
||||
Extensions can use this to react to Claude's actions,
|
||||
such as opening files, making edits, or running commands.
|
||||
|
||||
Args:
|
||||
event_type: Type of event (e.g., "file_read", "file_edit", "tool_use")
|
||||
data: Event-specific data (e.g., {"path": "/path/to/file"})
|
||||
"""
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Extension manager for loading and managing Clide extensions."""
|
||||
|
||||
from importlib.metadata import entry_points
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pluggy
|
||||
|
||||
from clide.extensions.hookspecs import ClideHookSpec
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from textual.app import App
|
||||
|
||||
from clide.services.file_watcher import FileEvent
|
||||
|
||||
EXTENSION_NAMESPACE = "clide.extensions"
|
||||
|
||||
|
||||
class ExtensionManager:
|
||||
"""Manages loading and lifecycle of Clide extensions."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._pm = pluggy.PluginManager("clide")
|
||||
self._pm.add_hookspecs(ClideHookSpec)
|
||||
self._loaded: list[str] = []
|
||||
|
||||
@property
|
||||
def hook(self) -> pluggy.HookRelay:
|
||||
"""Access the hook relay for calling hooks."""
|
||||
return self._pm.hook
|
||||
|
||||
def load_extensions(self) -> None:
|
||||
"""Load all extensions from entry points."""
|
||||
eps = entry_points(group=EXTENSION_NAMESPACE)
|
||||
for ep in eps:
|
||||
try:
|
||||
plugin = ep.load()
|
||||
self._pm.register(plugin, name=ep.name)
|
||||
self._loaded.append(ep.name)
|
||||
except Exception as e:
|
||||
# Log but don't crash on extension load failure
|
||||
print(f"Failed to load extension {ep.name}: {e}")
|
||||
|
||||
def register_plugin(self, plugin: object, name: str) -> None:
|
||||
"""Manually register a plugin instance.
|
||||
|
||||
Args:
|
||||
plugin: Plugin object with hookimpl methods
|
||||
name: Unique name for the plugin
|
||||
"""
|
||||
self._pm.register(plugin, name=name)
|
||||
self._loaded.append(name)
|
||||
|
||||
def unregister_plugin(self, name: str) -> None:
|
||||
"""Unregister a plugin by name.
|
||||
|
||||
Args:
|
||||
name: Name of the plugin to unregister
|
||||
"""
|
||||
plugin = self._pm.get_plugin(name)
|
||||
if plugin:
|
||||
self._pm.unregister(plugin)
|
||||
self._loaded.remove(name)
|
||||
|
||||
def list_extensions(self) -> list[str]:
|
||||
"""Get list of loaded extension names."""
|
||||
return self._loaded.copy()
|
||||
|
||||
async def trigger_app_startup(self, app: "App[object]") -> None:
|
||||
"""Trigger startup hooks for all extensions.
|
||||
|
||||
Args:
|
||||
app: The Clide application instance
|
||||
"""
|
||||
self.hook.clide_on_app_startup(app=app)
|
||||
|
||||
async def trigger_app_shutdown(self, app: "App[object]") -> None:
|
||||
"""Trigger shutdown hooks for all extensions.
|
||||
|
||||
Args:
|
||||
app: The Clide application instance
|
||||
"""
|
||||
self.hook.clide_on_app_shutdown(app=app)
|
||||
|
||||
def trigger_file_changed(self, event: "FileEvent") -> None:
|
||||
"""Trigger file change hooks for all extensions.
|
||||
|
||||
Args:
|
||||
event: The file event with path, type, and timestamp
|
||||
"""
|
||||
self.hook.clide_on_file_changed(event=event)
|
||||
|
||||
def trigger_file_saved(self, path: Path) -> None:
|
||||
"""Trigger file saved hooks for all extensions.
|
||||
|
||||
Args:
|
||||
path: Path to the saved file
|
||||
"""
|
||||
self.hook.clide_on_file_saved(path=path)
|
||||
|
||||
def trigger_claude_event(self, event_type: str, data: dict[str, Any]) -> None:
|
||||
"""Trigger Claude event hooks for all extensions.
|
||||
|
||||
Args:
|
||||
event_type: Type of event (e.g., "file_read", "file_edit")
|
||||
data: Event-specific data
|
||||
"""
|
||||
self.hook.clide_on_claude_event(event_type=event_type, data=data)
|
||||
Reference in New Issue
Block a user