""" Main entry point and CLI argument parsing for the application. """ import sys import time from engine import config from engine.display import BorderMode, DisplayRegistry from engine.effects import get_registry from engine.fetch import fetch_all, fetch_poetry, load_cache from engine.pipeline import ( Pipeline, PipelineConfig, PipelineContext, list_presets, ) from engine.pipeline.adapters import ( CameraStage, DataSourceStage, EffectPluginStage, create_stage_from_display, create_stage_from_effect, ) from engine.pipeline.params import PipelineParams from engine.pipeline.ui import UIConfig, UIPanel from engine.pipeline.validation import validate_pipeline_config try: from engine.display.backends.websocket import WebSocketDisplay except ImportError: WebSocketDisplay = None from .pipeline_runner import run_pipeline_mode def main(): """Main entry point - all modes now use presets or CLI construction.""" if config.PIPELINE_DIAGRAM: try: from engine.pipeline import generate_pipeline_diagram except ImportError: print("Error: pipeline diagram not available") return print(generate_pipeline_diagram()) return # Check for direct pipeline construction flags if "--pipeline-source" in sys.argv: # Construct pipeline directly from CLI args run_pipeline_mode_direct() return preset_name = None if config.PRESET: preset_name = config.PRESET elif config.PIPELINE_MODE: preset_name = config.PIPELINE_PRESET else: preset_name = "demo" available = list_presets() if preset_name not in available: print(f"Error: Unknown preset '{preset_name}'") print(f"Available presets: {', '.join(available)}") sys.exit(1) run_pipeline_mode(preset_name) def run_pipeline_mode_direct(): """Construct and run a pipeline directly from CLI arguments. Usage: python -m engine.app --pipeline-source headlines --pipeline-effects noise,fade --display null python -m engine.app --pipeline-source fixture --pipeline-effects glitch --pipeline-ui --display null Flags: --pipeline-source : Headlines, fixture, poetry, empty, pipeline-inspect --pipeline-effects : Comma-separated list (noise, fade, glitch, firehose, hud, tint, border, crop) --pipeline-camera : scroll, feed, horizontal, omni, floating, bounce --pipeline-display : terminal, pygame, websocket, null, multi:term,pygame --pipeline-ui: Enable UI panel (BorderMode.UI) --pipeline-border : off, simple, ui """ from engine.camera import Camera from engine.data_sources.pipeline_introspection import PipelineIntrospectionSource from engine.data_sources.sources import EmptyDataSource, ListDataSource from engine.pipeline.adapters import ( FontStage, ViewportFilterStage, ) # Parse CLI arguments source_name = None effect_names = [] camera_type = None display_name = None ui_enabled = False border_mode = BorderMode.OFF source_items = None allow_unsafe = False i = 1 argv = sys.argv while i < len(argv): arg = argv[i] if arg == "--pipeline-source" and i + 1 < len(argv): source_name = argv[i + 1] i += 2 elif arg == "--pipeline-effects" and i + 1 < len(argv): effect_names = [e.strip() for e in argv[i + 1].split(",") if e.strip()] i += 2 elif arg == "--pipeline-camera" and i + 1 < len(argv): camera_type = argv[i + 1] i += 2 elif arg == "--pipeline-display" and i + 1 < len(argv): display_name = argv[i + 1] i += 2 elif arg == "--pipeline-ui": ui_enabled = True i += 1 elif arg == "--pipeline-border" and i + 1 < len(argv): mode = argv[i + 1] if mode == "simple": border_mode = True elif mode == "ui": border_mode = BorderMode.UI else: border_mode = False i += 2 elif arg == "--allow-unsafe": allow_unsafe = True i += 1 else: i += 1 if not source_name: print("Error: --pipeline-source is required") print( "Usage: python -m engine.app --pipeline-source [--pipeline-effects ] ..." ) sys.exit(1) print(" \033[38;5;245mDirect pipeline construction\033[0m") print(f" Source: {source_name}") print(f" Effects: {effect_names}") print(f" Camera: {camera_type}") print(f" Display: {display_name}") print(f" UI Enabled: {ui_enabled}") # Create initial config and params params = PipelineParams() params.source = source_name params.camera_mode = camera_type if camera_type is not None else "" params.effect_order = effect_names params.border = border_mode # Create minimal config for validation config_obj = PipelineConfig( source=source_name, display=display_name or "", # Will be filled by validation camera=camera_type if camera_type is not None else "", effects=effect_names, ) # Run MVP validation result = validate_pipeline_config(config_obj, params, allow_unsafe=allow_unsafe) if result.warnings and not allow_unsafe: print(" \033[38;5;226mWarning: MVP validation found issues:\033[0m") for warning in result.warnings: print(f" - {warning}") if result.changes: print(" \033[38;5;226mApplied MVP defaults:\033[0m") for change in result.changes: print(f" {change}") if not result.valid: print( " \033[38;5;196mPipeline configuration invalid and could not be fixed\033[0m" ) sys.exit(1) # Show MVP summary print(" \033[38;5;245mMVP Configuration:\033[0m") print(f" Source: {result.config.source}") print(f" Display: {result.config.display}") print(f" Camera: {result.config.camera or 'static (none)'}") print(f" Effects: {result.config.effects if result.config.effects else 'none'}") print(f" Border: {result.params.border}") # Load source items if source_name == "headlines": cached = load_cache() if cached: source_items = cached else: source_items, _, _ = fetch_all() elif source_name == "fixture": source_items = load_cache() if not source_items: print(" \033[38;5;196mNo fixture cache available\033[0m") sys.exit(1) elif source_name == "poetry": source_items, _, _ = fetch_poetry() elif source_name == "empty" or source_name == "pipeline-inspect": source_items = [] else: print(f" \033[38;5;196mUnknown source: {source_name}\033[0m") sys.exit(1) if source_items is not None: print(f" \033[38;5;82mLoaded {len(source_items)} items\033[0m") # Set border mode if ui_enabled: border_mode = BorderMode.UI # Build pipeline using validated config and params params = result.params params.viewport_width = 80 params.viewport_height = 24 ctx = PipelineContext() ctx.params = params # Create display using validated display name display_name = result.config.display or "terminal" # Default to terminal if empty display = DisplayRegistry.create(display_name) if not display: print(f" \033[38;5;196mFailed to create display: {display_name}\033[0m") sys.exit(1) display.init(0, 0) # Create pipeline using validated config pipeline = Pipeline(config=result.config, context=ctx) # Add stages # Source stage if source_name == "pipeline-inspect": introspection_source = PipelineIntrospectionSource( pipeline=None, viewport_width=params.viewport_width, viewport_height=params.viewport_height, ) pipeline.add_stage( "source", DataSourceStage(introspection_source, name="pipeline-inspect") ) elif source_name == "empty": empty_source = EmptyDataSource( width=params.viewport_width, height=params.viewport_height ) pipeline.add_stage("source", DataSourceStage(empty_source, name="empty")) else: list_source = ListDataSource(source_items, name=source_name) pipeline.add_stage("source", DataSourceStage(list_source, name=source_name)) # Add viewport filter and font for headline sources if source_name in ["headlines", "poetry", "fixture"]: pipeline.add_stage( "viewport_filter", ViewportFilterStage(name="viewport-filter") ) pipeline.add_stage("font", FontStage(name="font")) # Add camera speed = getattr(params, "camera_speed", 1.0) camera = None if camera_type == "feed": camera = Camera.feed(speed=speed) elif camera_type == "scroll": camera = Camera.scroll(speed=speed) elif camera_type == "horizontal": camera = Camera.horizontal(speed=speed) elif camera_type == "omni": camera = Camera.omni(speed=speed) elif camera_type == "floating": camera = Camera.floating(speed=speed) elif camera_type == "bounce": camera = Camera.bounce(speed=speed) if camera: pipeline.add_stage("camera", CameraStage(camera, name=camera_type)) # Add effects effect_registry = get_registry() for effect_name in effect_names: effect = effect_registry.get(effect_name) if effect: pipeline.add_stage( f"effect_{effect_name}", create_stage_from_effect(effect, effect_name) ) # Add display pipeline.add_stage("display", create_stage_from_display(display, display_name)) pipeline.build() if not pipeline.initialize(): print(" \033[38;5;196mFailed to initialize pipeline\033[0m") sys.exit(1) # Create UI panel if border mode is UI ui_panel = None if params.border == BorderMode.UI: ui_panel = UIPanel(UIConfig(panel_width=24, start_with_preset_picker=True)) # Enable raw mode for terminal input if supported if hasattr(display, "set_raw_mode"): display.set_raw_mode(True) for stage in pipeline.stages.values(): if isinstance(stage, EffectPluginStage): effect = stage._effect enabled = effect.config.enabled if hasattr(effect, "config") else True stage_control = ui_panel.register_stage(stage, enabled=enabled) stage_control.effect = effect # type: ignore[attr-defined] if ui_panel.stages: first_stage = next(iter(ui_panel.stages)) ui_panel.select_stage(first_stage) ctrl = ui_panel.stages[first_stage] if hasattr(ctrl, "effect"): effect = ctrl.effect if hasattr(effect, "config"): config = effect.config try: import dataclasses if dataclasses.is_dataclass(config): for field_name, field_obj in dataclasses.fields(config): if field_name == "enabled": continue value = getattr(config, field_name, None) if value is not None: ctrl.params[field_name] = value ctrl.param_schema[field_name] = { "type": type(value).__name__, "min": 0 if isinstance(value, (int, float)) else None, "max": 1 if isinstance(value, float) else None, "step": 0.1 if isinstance(value, float) else 1, } except Exception: pass # Run pipeline loop from engine.display import render_ui_panel ctx.set("display", display) ctx.set("items", source_items) ctx.set("pipeline", pipeline) ctx.set("pipeline_order", pipeline.execution_order) current_width = params.viewport_width current_height = params.viewport_height print(" \033[38;5;82mStarting pipeline...\033[0m") print(" \033[38;5;245mPress Ctrl+C to exit\033[0m\n") try: frame = 0 while True: params.frame_number = frame ctx.params = params result = pipeline.execute(source_items) if not result.success: print(" \033[38;5;196mPipeline execution failed\033[0m") break # Render with UI panel if ui_panel is not None: buf = render_ui_panel( result.data, current_width, current_height, ui_panel ) display.show(buf, border=False) else: display.show(result.data, border=border_mode) # Handle keyboard events if UI is enabled if ui_panel is not None: # Try pygame first if hasattr(display, "_pygame"): try: import pygame for event in pygame.event.get(): if event.type == pygame.KEYDOWN: ui_panel.process_key_event(event.key, event.mod) except (ImportError, Exception): pass # Try terminal input elif hasattr(display, "get_input_keys"): try: keys = display.get_input_keys() for key in keys: ui_panel.process_key_event(key, 0) except Exception: pass # Check for quit request if hasattr(display, "is_quit_requested") and display.is_quit_requested(): if hasattr(display, "clear_quit_request"): display.clear_quit_request() raise KeyboardInterrupt() time.sleep(1 / 60) frame += 1 except KeyboardInterrupt: pipeline.cleanup() display.cleanup() print("\n \033[38;5;245mPipeline stopped\033[0m") return pipeline.cleanup() display.cleanup() print("\n \033[38;5;245mPipeline stopped\033[0m")