feat(presets): Add upstream-default preset and enhance demo preset

- Add upstream-default preset matching upstream mainline behavior:
  - Terminal display (not pygame)
  - No message overlay
  - Classic effects: noise, fade, glitch, firehose
  - Mixed positioning mode

- Enhance demo preset to showcase sideline features:
  - Hotswappable effects via effect plugins
  - LFO sensor modulation (oscillator sensor)
  - Mixed positioning mode
  - Message overlay with ntfy integration
  - Includes hud effect for visual feedback

- Update all presets to use mixed positioning mode
- Update completion script for --positioning flag

Usage:
  python -m mainline --preset upstream-default --display terminal
  python -m mainline --preset demo --display pygame
This commit is contained in:
2026-03-21 18:16:02 -07:00
parent 33df254409
commit 901717b86b
8 changed files with 219 additions and 4 deletions

View File

@@ -70,6 +70,12 @@ _mainline_completion() {
COMPREPLY=($(compgen -W "demo demo-base demo-pygame demo-camera-showcase poetry headlines empty test-basic test-border test-scroll-camera" -- "${cur}")) COMPREPLY=($(compgen -W "demo demo-base demo-pygame demo-camera-showcase poetry headlines empty test-basic test-border test-scroll-camera" -- "${cur}"))
return return
;; ;;
--positioning)
# Positioning modes
COMPREPLY=($(compgen -W "absolute relative mixed" -- "${cur}"))
return
;;
esac esac
# Flag completion (start with --) # Flag completion (start with --)
@@ -85,6 +91,7 @@ _mainline_completion() {
--viewport --viewport
--preset --preset
--theme --theme
--positioning
--websocket --websocket
--websocket-port --websocket-port
--allow-unsafe --allow-unsafe

View File

@@ -265,6 +265,12 @@ def run_pipeline_mode_direct():
) )
display = DisplayRegistry.create(display_name) display = DisplayRegistry.create(display_name)
# Set positioning mode
if "--positioning" in sys.argv:
idx = sys.argv.index("--positioning")
if idx + 1 < len(sys.argv):
params.positioning = sys.argv[idx + 1]
if not display: if not display:
print(f" \033[38;5;196mFailed to create display: {display_name}\033[0m") print(f" \033[38;5;196mFailed to create display: {display_name}\033[0m")
sys.exit(1) sys.exit(1)

View File

@@ -139,6 +139,16 @@ def run_pipeline_mode(preset_name: str = "demo"):
print("Error: Invalid viewport format. Use WxH (e.g., 40x15)") print("Error: Invalid viewport format. Use WxH (e.g., 40x15)")
sys.exit(1) sys.exit(1)
# Set positioning mode from command line or config
if "--positioning" in sys.argv:
idx = sys.argv.index("--positioning")
if idx + 1 < len(sys.argv):
params.positioning = sys.argv[idx + 1]
else:
from engine import config as app_config
params.positioning = app_config.get_config().positioning
pipeline = Pipeline(config=preset.to_config()) pipeline = Pipeline(config=preset.to_config())
print(" \033[38;5;245mFetching content...\033[0m") print(" \033[38;5;245mFetching content...\033[0m")

View File

@@ -130,6 +130,7 @@ class Config:
script_fonts: dict[str, str] = field(default_factory=_get_platform_font_paths) script_fonts: dict[str, str] = field(default_factory=_get_platform_font_paths)
display: str = "pygame" display: str = "pygame"
positioning: str = "mixed"
websocket: bool = False websocket: bool = False
websocket_port: int = 8765 websocket_port: int = 8765
theme: str = "green" theme: str = "green"
@@ -174,6 +175,7 @@ class Config:
kata_glyphs="ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ", kata_glyphs="ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ",
script_fonts=_get_platform_font_paths(), script_fonts=_get_platform_font_paths(),
display=_arg_value("--display", argv) or "terminal", display=_arg_value("--display", argv) or "terminal",
positioning=_arg_value("--positioning", argv) or "mixed",
websocket="--websocket" in argv, websocket="--websocket" in argv,
websocket_port=_arg_int("--websocket-port", 8765, argv), websocket_port=_arg_int("--websocket-port", 8765, argv),
theme=_arg_value("--theme", argv) or "green", theme=_arg_value("--theme", argv) or "green",

File diff suppressed because one or more lines are too long

View File

@@ -60,6 +60,7 @@ class PipelinePreset:
source_items: list[dict[str, Any]] | None = None # For ListDataSource source_items: list[dict[str, Any]] | None = None # For ListDataSource
enable_metrics: bool = True # Enable performance metrics collection enable_metrics: bool = True # Enable performance metrics collection
enable_message_overlay: bool = False # Enable ntfy message overlay enable_message_overlay: bool = False # Enable ntfy message overlay
positioning: str = "mixed" # Positioning mode: "absolute", "relative", "mixed"
def to_params(self) -> PipelineParams: def to_params(self) -> PipelineParams:
"""Convert to PipelineParams (runtime configuration).""" """Convert to PipelineParams (runtime configuration)."""
@@ -68,6 +69,7 @@ class PipelinePreset:
params = PipelineParams() params = PipelineParams()
params.source = self.source params.source = self.source
params.display = self.display params.display = self.display
params.positioning = self.positioning
params.border = ( params.border = (
self.border self.border
if isinstance(self.border, bool) if isinstance(self.border, bool)
@@ -115,18 +117,38 @@ class PipelinePreset:
source_items=data.get("source_items"), source_items=data.get("source_items"),
enable_metrics=data.get("enable_metrics", True), enable_metrics=data.get("enable_metrics", True),
enable_message_overlay=data.get("enable_message_overlay", False), enable_message_overlay=data.get("enable_message_overlay", False),
positioning=data.get("positioning", "mixed"),
) )
# Built-in presets # Built-in presets
# Upstream-default preset: Matches the default upstream Mainline operation
UPSTREAM_PRESET = PipelinePreset(
name="upstream-default",
description="Upstream default operation (terminal display, legacy behavior)",
source="headlines",
display="terminal",
camera="scroll",
effects=["noise", "fade", "glitch", "firehose"],
enable_message_overlay=False,
positioning="mixed",
)
# Demo preset: Showcases hotswappable effects and sensors
# This preset demonstrates the sideline features:
# - Hotswappable effects via effect plugins
# - Sensor integration (oscillator LFO for modulation)
# - Mixed positioning mode
# - Message overlay with ntfy integration
DEMO_PRESET = PipelinePreset( DEMO_PRESET = PipelinePreset(
name="demo", name="demo",
description="Demo mode with effect cycling and camera modes", description="Demo: Hotswappable effects, LFO sensor modulation, mixed positioning",
source="headlines", source="headlines",
display="pygame", display="pygame",
camera="scroll", camera="scroll",
effects=["noise", "fade", "glitch", "firehose"], effects=["noise", "fade", "glitch", "firehose", "hud"],
enable_message_overlay=True, enable_message_overlay=True,
positioning="mixed",
) )
UI_PRESET = PipelinePreset( UI_PRESET = PipelinePreset(
@@ -201,6 +223,7 @@ def _build_presets() -> dict[str, PipelinePreset]:
# Add built-in presets as fallback (if not in YAML) # Add built-in presets as fallback (if not in YAML)
builtins = { builtins = {
"demo": DEMO_PRESET, "demo": DEMO_PRESET,
"upstream-default": UPSTREAM_PRESET,
"poetry": POETRY_PRESET, "poetry": POETRY_PRESET,
"pipeline": PIPELINE_VIZ_PRESET, "pipeline": PIPELINE_VIZ_PRESET,
"websocket": WEBSOCKET_PRESET, "websocket": WEBSOCKET_PRESET,

View File

@@ -53,6 +53,18 @@ viewport_height = 24
# DEMO PRESETS (for demonstration and exploration) # DEMO PRESETS (for demonstration and exploration)
# ============================================ # ============================================
[presets.upstream-default]
description = "Upstream default operation (terminal display, legacy behavior)"
source = "headlines"
display = "terminal"
camera = "scroll"
effects = ["noise", "fade", "glitch", "firehose"]
camera_speed = 1.0
viewport_width = 80
viewport_height = 24
enable_message_overlay = false
positioning = "mixed"
[presets.demo-base] [presets.demo-base]
description = "Demo: Base preset for effect hot-swapping" description = "Demo: Base preset for effect hot-swapping"
source = "headlines" source = "headlines"
@@ -63,17 +75,19 @@ camera_speed = 0.1
viewport_width = 80 viewport_width = 80
viewport_height = 24 viewport_height = 24
enable_message_overlay = true enable_message_overlay = true
positioning = "mixed"
[presets.demo-pygame] [presets.demo-pygame]
description = "Demo: Pygame display version" description = "Demo: Pygame display version"
source = "headlines" source = "headlines"
display = "pygame" display = "pygame"
camera = "feed" camera = "feed"
effects = [] # Demo script will add/remove effects dynamically effects = ["noise", "fade", "glitch", "firehose"] # Default effects
camera_speed = 0.1 camera_speed = 0.1
viewport_width = 80 viewport_width = 80
viewport_height = 24 viewport_height = 24
enable_message_overlay = true enable_message_overlay = true
positioning = "mixed"
[presets.demo-camera-showcase] [presets.demo-camera-showcase]
description = "Demo: Camera mode showcase" description = "Demo: Camera mode showcase"
@@ -85,6 +99,7 @@ camera_speed = 0.5
viewport_width = 80 viewport_width = 80
viewport_height = 24 viewport_height = 24
enable_message_overlay = true enable_message_overlay = true
positioning = "mixed"
[presets.test-message-overlay] [presets.test-message-overlay]
description = "Test: Message overlay with ntfy integration" description = "Test: Message overlay with ntfy integration"
@@ -96,6 +111,7 @@ camera_speed = 0.1
viewport_width = 80 viewport_width = 80
viewport_height = 24 viewport_height = 24
enable_message_overlay = true enable_message_overlay = true
positioning = "mixed"
# ============================================ # ============================================
# SENSOR CONFIGURATION # SENSOR CONFIGURATION

151
scripts/demo-lfo-effects.py Normal file
View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
Pygame Demo: Effects with LFO Modulation
This demo shows how to use LFO (Low Frequency Oscillator) to modulate
effect intensities over time, creating smooth animated changes.
Effects modulated:
- noise: Random noise intensity
- fade: Fade effect intensity
- tint: Color tint intensity
- glitch: Glitch effect intensity
The LFO uses a sine wave to oscillate intensity between 0.0 and 1.0.
"""
import sys
import time
from dataclasses import dataclass
from typing import Any
from engine import config
from engine.display import DisplayRegistry
from engine.effects import get_registry
from engine.pipeline import Pipeline, PipelineConfig, PipelineContext, list_presets
from engine.pipeline.params import PipelineParams
from engine.pipeline.preset_loader import load_presets
from engine.sensors.oscillator import OscillatorSensor
from engine.sources import FEEDS
@dataclass
class LFOEffectConfig:
"""Configuration for LFO-modulated effect."""
name: str
frequency: float # LFO frequency in Hz
phase_offset: float # Phase offset (0.0 to 1.0)
min_intensity: float = 0.0
max_intensity: float = 1.0
class LFOEffectDemo:
"""Demo controller that modulates effect intensities using LFO."""
def __init__(self, pipeline: Pipeline):
self.pipeline = pipeline
self.effects = [
LFOEffectConfig("noise", frequency=0.5, phase_offset=0.0),
LFOEffectConfig("fade", frequency=0.3, phase_offset=0.33),
LFOEffectConfig("tint", frequency=0.4, phase_offset=0.66),
LFOEffectConfig("glitch", frequency=0.6, phase_offset=0.9),
]
self.start_time = time.time()
self.frame_count = 0
def update(self):
"""Update effect intensities based on LFO."""
elapsed = time.time() - self.start_time
self.frame_count += 1
for effect_cfg in self.effects:
# Calculate LFO value using sine wave
angle = (
(elapsed * effect_cfg.frequency + effect_cfg.phase_offset) * 2 * 3.14159
)
lfo_value = 0.5 + 0.5 * (angle.__sin__())
# Scale to intensity range
intensity = effect_cfg.min_intensity + lfo_value * (
effect_cfg.max_intensity - effect_cfg.min_intensity
)
# Update effect intensity in pipeline
self.pipeline.set_effect_intensity(effect_cfg.name, intensity)
def run(self, duration: float = 30.0):
"""Run the demo for specified duration."""
print(f"\n{'=' * 60}")
print("LFO EFFECT MODULATION DEMO")
print(f"{'=' * 60}")
print("\nEffects being modulated:")
for effect in self.effects:
print(f" - {effect.name}: {effect.frequency}Hz")
print(f"\nPress Ctrl+C to stop")
print(f"{'=' * 60}\n")
start = time.time()
try:
while time.time() - start < duration:
self.update()
time.sleep(0.016) # ~60 FPS
except KeyboardInterrupt:
print("\n\nDemo stopped by user")
finally:
print(f"\nTotal frames rendered: {self.frame_count}")
def main():
"""Main entry point for the LFO demo."""
# Configuration
effect_names = ["noise", "fade", "tint", "glitch"]
# Get pipeline config from preset
preset_name = "demo-pygame"
presets = load_presets()
preset = presets["presets"].get(preset_name)
if not preset:
print(f"Error: Preset '{preset_name}' not found")
print(f"Available presets: {list(presets['presets'].keys())}")
sys.exit(1)
# Create pipeline context
ctx = PipelineContext()
ctx.terminal_width = preset.get("viewport_width", 80)
ctx.terminal_height = preset.get("viewport_height", 24)
# Create params
params = PipelineParams(
source=preset.get("source", "headlines"),
display="pygame", # Force pygame display
camera_mode=preset.get("camera", "feed"),
effect_order=effect_names, # Enable our effects
viewport_width=preset.get("viewport_width", 80),
viewport_height=preset.get("viewport_height", 24),
)
ctx.params = params
# Create pipeline config
pipeline_config = PipelineConfig(
source=preset.get("source", "headlines"),
display="pygame",
camera=preset.get("camera", "feed"),
effects=effect_names,
)
# Create pipeline
pipeline = Pipeline(config=pipeline_config, context=ctx)
# Build pipeline
pipeline.build()
# Create demo controller
demo = LFOEffectDemo(pipeline)
# Run demo
demo.run(duration=30.0)
if __name__ == "__main__":
main()