feat: Implement Sideline plugin system with consistent terminology
This commit implements the Sideline/Mainline split with a clean plugin architecture: ## Core Changes ### Sideline Framework (New Directory) - Created directory containing the reusable pipeline framework - Moved pipeline core, controllers, adapters, and registry to - Moved display system to - Moved effects system to - Created plugin system with security and compatibility management in - Created preset pack system with ASCII art encoding in - Added default font (Corptic) to - Added terminal ANSI constants to ### Mainline Application (Updated) - Created for Mainline stage component registration - Updated to register Mainline stages at startup - Updated as a compatibility shim re-exporting from sideline ### Terminology Consistency - : Base class for all pipeline components (sources, effects, displays, cameras) - : Base class for distributable plugin packages (was ) - : Base class for visual effects (was ) - Backward compatibility aliases maintained for existing code ## Key Features - Plugin discovery via entry points and explicit registration - Security permissions system for plugins - Compatibility management with semantic version constraints - Preset pack system for distributable configurations - Default font bundled with Sideline (Corptic.otf) ## Testing - Updated tests to register Mainline stages before discovery - All StageRegistry tests passing Note: This is a major refactoring that separates the framework (Sideline) from the application (Mainline), enabling Sideline to be used by other applications.
This commit is contained in:
194
sideline/preset_packs/manager.py
Normal file
194
sideline/preset_packs/manager.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Preset pack manager for loading, validating, and managing preset packs.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import tomli
|
||||
|
||||
from sideline.preset_packs.pack_format import PresetPack, PresetPackMetadata
|
||||
from sideline.preset_packs.encoder import PresetPackEncoder
|
||||
from sideline.plugins.compatibility import CompatibilityManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PresetPackManager:
|
||||
"""Manages preset pack loading and validation."""
|
||||
|
||||
def __init__(self, pack_dir: Optional[str] = None):
|
||||
"""Initialize preset pack manager.
|
||||
|
||||
Args:
|
||||
pack_dir: Directory to search for preset packs
|
||||
"""
|
||||
self.pack_dir = pack_dir or os.path.expanduser("~/.config/sideline/packs")
|
||||
self._packs: Dict[str, PresetPack] = {}
|
||||
|
||||
def load_pack(self, pack_path: str) -> Optional[PresetPack]:
|
||||
"""Load a preset pack from a file.
|
||||
|
||||
Args:
|
||||
pack_path: Path to the preset pack file (.tpack or .toml)
|
||||
|
||||
Returns:
|
||||
Loaded PresetPack or None if failed
|
||||
"""
|
||||
try:
|
||||
with open(pack_path, "rb") as f:
|
||||
# Try loading as TOML first
|
||||
if pack_path.endswith(".toml"):
|
||||
data = tomli.load(f)
|
||||
pack = PresetPack.from_dict(data)
|
||||
elif pack_path.endswith(".tpack"):
|
||||
# Load ASCII art encoded pack
|
||||
content = f.read().decode("utf-8")
|
||||
pack = self._load_ascii_pack(content)
|
||||
else:
|
||||
logger.error(f"Unknown file format: {pack_path}")
|
||||
return None
|
||||
|
||||
# Validate compatibility
|
||||
if not CompatibilityManager.validate_compatibility(
|
||||
pack.metadata.sideline_version
|
||||
):
|
||||
error = CompatibilityManager.get_compatibility_error(
|
||||
pack.metadata.sideline_version
|
||||
)
|
||||
logger.warning(f"Pack {pack.metadata.name} incompatible: {error}")
|
||||
return None
|
||||
|
||||
# Store pack
|
||||
self._packs[pack.metadata.name] = pack
|
||||
logger.info(
|
||||
f"Loaded preset pack: {pack.metadata.name} v{pack.metadata.version}"
|
||||
)
|
||||
return pack
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load preset pack {pack_path}: {e}")
|
||||
return None
|
||||
|
||||
def _load_ascii_pack(self, content: str) -> PresetPack:
|
||||
"""Load pack from ASCII art encoded content."""
|
||||
# Extract TOML from ASCII art
|
||||
toml_data = PresetPackEncoder.decode_toml(content)
|
||||
|
||||
# Parse TOML
|
||||
import tomli
|
||||
|
||||
data = tomli.loads(toml_data)
|
||||
|
||||
return PresetPack.from_dict(data)
|
||||
|
||||
def load_directory(self, directory: Optional[str] = None) -> List[PresetPack]:
|
||||
"""Load all preset packs from a directory.
|
||||
|
||||
Args:
|
||||
directory: Directory to search (defaults to pack_dir)
|
||||
|
||||
Returns:
|
||||
List of loaded PresetPack objects
|
||||
"""
|
||||
directory = directory or self.pack_dir
|
||||
|
||||
if not os.path.exists(directory):
|
||||
logger.warning(f"Preset pack directory does not exist: {directory}")
|
||||
return []
|
||||
|
||||
loaded = []
|
||||
for filename in os.listdir(directory):
|
||||
if filename.endswith((".toml", ".tpack")):
|
||||
path = os.path.join(directory, filename)
|
||||
pack = self.load_pack(path)
|
||||
if pack:
|
||||
loaded.append(pack)
|
||||
|
||||
return loaded
|
||||
|
||||
def save_pack(
|
||||
self, pack: PresetPack, output_path: str, format: str = "toml"
|
||||
) -> bool:
|
||||
"""Save a preset pack to a file.
|
||||
|
||||
Args:
|
||||
pack: PresetPack to save
|
||||
output_path: Path to save the pack
|
||||
format: Output format ("toml" or "tpack")
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
if format == "toml":
|
||||
import tomli_w
|
||||
|
||||
with open(output_path, "w") as f:
|
||||
tomli_w.dump(pack.to_dict(), f)
|
||||
elif format == "tpack":
|
||||
# Encode as ASCII art
|
||||
toml_data = self._pack_to_toml(pack)
|
||||
ascii_art = PresetPackEncoder.encode_toml(toml_data, pack.metadata.name)
|
||||
|
||||
with open(output_path, "w") as f:
|
||||
f.write(ascii_art)
|
||||
else:
|
||||
logger.error(f"Unknown format: {format}")
|
||||
return False
|
||||
|
||||
logger.info(f"Saved preset pack: {output_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save preset pack: {e}")
|
||||
return False
|
||||
|
||||
def _pack_to_toml(self, pack: PresetPack) -> str:
|
||||
"""Convert PresetPack to TOML string."""
|
||||
import tomli_w
|
||||
|
||||
return tomli_w.dumps(pack.to_dict())
|
||||
|
||||
def get_pack(self, name: str) -> Optional[PresetPack]:
|
||||
"""Get a loaded preset pack by name."""
|
||||
return self._packs.get(name)
|
||||
|
||||
def list_packs(self) -> List[str]:
|
||||
"""List all loaded preset pack names."""
|
||||
return list(self._packs.keys())
|
||||
|
||||
def register_pack_plugins(self, pack: PresetPack):
|
||||
"""Register all plugins from a preset pack.
|
||||
|
||||
Args:
|
||||
pack: PresetPack containing plugins
|
||||
"""
|
||||
from sideline.pipeline import StageRegistry
|
||||
|
||||
for plugin_entry in pack.plugins:
|
||||
try:
|
||||
# Decode plugin code
|
||||
code = PresetPackEncoder.decode_plugin_code(plugin_entry.encoded_code)
|
||||
|
||||
# Execute plugin code to get the class
|
||||
local_ns = {}
|
||||
exec(code, local_ns)
|
||||
|
||||
# Find the plugin class (first class defined)
|
||||
plugin_class = None
|
||||
for obj in local_ns.values():
|
||||
if isinstance(obj, type) and hasattr(obj, "metadata"):
|
||||
plugin_class = obj
|
||||
break
|
||||
|
||||
if plugin_class:
|
||||
# Register the plugin
|
||||
StageRegistry.register(plugin_entry.category, plugin_class)
|
||||
logger.info(f"Registered plugin: {plugin_entry.name}")
|
||||
else:
|
||||
logger.warning(f"No plugin class found in {plugin_entry.name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to register plugin {plugin_entry.name}: {e}")
|
||||
Reference in New Issue
Block a user