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.
195 lines
6.3 KiB
Python
195 lines
6.3 KiB
Python
"""
|
|
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}")
|