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:
2026-03-23 20:42:33 -07:00
parent 2d28e92594
commit e4b143ff36
58 changed files with 10163 additions and 50 deletions

View File

@@ -0,0 +1,211 @@
"""
Preset pack encoder with ASCII art compression.
Compresses plugin code and encodes it as ASCII art for fun and version control.
"""
import base64
import zlib
import textwrap
from typing import Tuple
class PresetPackEncoder:
"""Encodes and decodes preset packs with ASCII art compression."""
# ASCII art frame characters
FRAME_TOP_LEFT = ""
FRAME_TOP_RIGHT = ""
FRAME_BOTTOM_LEFT = ""
FRAME_BOTTOM_RIGHT = ""
FRAME_HORIZONTAL = ""
FRAME_VERTICAL = ""
# Data block characters (for visual representation)
DATA_CHARS = " ░▒▓█"
@classmethod
def encode_plugin_code(cls, code: str, name: str = "plugin") -> str:
"""Encode plugin code as ASCII art.
Args:
code: Python source code to encode
name: Plugin name for metadata
Returns:
ASCII art encoded plugin code
"""
# Compress the code
compressed = zlib.compress(code.encode("utf-8"))
# Encode as base64
b64 = base64.b64encode(compressed).decode("ascii")
# Wrap in ASCII art frame
return cls._wrap_in_ascii_art(b64, name)
@classmethod
def decode_plugin_code(cls, ascii_art: str) -> str:
"""Decode ASCII art to plugin code.
Args:
ascii_art: ASCII art encoded plugin code
Returns:
Decoded Python source code
"""
# Extract base64 from ASCII art
b64 = cls._extract_from_ascii_art(ascii_art)
# Decode base64
compressed = base64.b64decode(b64)
# Decompress
code = zlib.decompress(compressed).decode("utf-8")
return code
@classmethod
def _wrap_in_ascii_art(cls, data: str, name: str) -> str:
"""Wrap data in ASCII art frame."""
# Calculate frame width
max_line_length = 60
lines = textwrap.wrap(data, max_line_length)
# Find longest line for frame width
longest_line = max(len(line) for line in lines) if lines else 0
frame_width = longest_line + 4 # 2 padding + 2 borders
# Build ASCII art
result = []
# Top border
result.append(
cls.FRAME_TOP_LEFT
+ cls.FRAME_HORIZONTAL * (frame_width - 2)
+ cls.FRAME_TOP_RIGHT
)
# Plugin name header
name_line = f" {name} "
name_padding = frame_width - 2 - len(name_line)
left_pad = name_padding // 2
right_pad = name_padding - left_pad
result.append(
cls.FRAME_VERTICAL
+ " " * left_pad
+ name_line
+ " " * right_pad
+ cls.FRAME_VERTICAL
)
# Separator line
result.append(
cls.FRAME_VERTICAL
+ cls.FRAME_HORIZONTAL * (frame_width - 2)
+ cls.FRAME_VERTICAL
)
# Data lines
for line in lines:
padding = frame_width - 2 - len(line)
result.append(
cls.FRAME_VERTICAL + line + " " * padding + cls.FRAME_VERTICAL
)
# Bottom border
result.append(
cls.FRAME_BOTTOM_LEFT
+ cls.FRAME_HORIZONTAL * (frame_width - 2)
+ cls.FRAME_BOTTOM_RIGHT
)
return "\n".join(result)
@classmethod
def _extract_from_ascii_art(cls, ascii_art: str) -> str:
"""Extract base64 data from ASCII art frame."""
lines = ascii_art.strip().split("\n")
# Skip top and bottom borders, header, and separator
data_lines = lines[3:-1]
# Extract data from between frame characters
extracted = []
for line in data_lines:
if len(line) > 2:
# Remove frame characters and extract content
content = line[1:-1].rstrip()
extracted.append(content)
return "".join(extracted)
@classmethod
def encode_toml(cls, toml_data: str, name: str = "pack") -> str:
"""Encode TOML data as ASCII art.
Args:
toml_data: TOML configuration data
name: Pack name
Returns:
ASCII art encoded TOML
"""
# Compress
compressed = zlib.compress(toml_data.encode("utf-8"))
# Encode as base64
b64 = base64.b64encode(compressed).decode("ascii")
# Create visual representation using data characters
visual_data = cls._data_to_visual(b64)
return cls._wrap_in_ascii_art(visual_data, name)
@classmethod
def decode_toml(cls, ascii_art: str) -> str:
"""Decode ASCII art to TOML data.
Args:
ascii_art: ASCII art encoded TOML
Returns:
Decoded TOML data
"""
# Extract base64 from ASCII art
b64 = cls._extract_from_ascii_art(ascii_art)
# Decode base64
compressed = base64.b64decode(b64)
# Decompress
toml_data = zlib.decompress(compressed).decode("utf-8")
return toml_data
@classmethod
def _data_to_visual(cls, data: str) -> str:
"""Convert base64 data to visual representation.
This creates a fun visual pattern based on the data.
"""
# Simple mapping: each character to a data block character
# This is purely for visual appeal
visual = ""
for i, char in enumerate(data):
# Use character code to select visual block
idx = ord(char) % len(cls.DATA_CHARS)
visual += cls.DATA_CHARS[idx]
# Add line breaks for visual appeal
if (i + 1) % 60 == 0:
visual += "\n"
return visual
@classmethod
def get_visual_representation(cls, data: str) -> str:
"""Get a visual representation of data for display."""
compressed = zlib.compress(data.encode("utf-8"))
b64 = base64.b64encode(compressed).decode("ascii")
return cls._data_to_visual(b64)