From 3e9c1be6d277d80e1489f8792ff972acc7d6d45b Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 16 Mar 2026 00:53:13 -0700 Subject: [PATCH] feat(app): add demo mode with HUD effect plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add --demo flag that runs effect showcase with pygame display - Add HUD effect plugin (effects_plugins/hud.py) that displays: - FPS and frame time - Current effect name with intensity bar - Pipeline order - Demo mode cycles through noise, fade, glitch, firehose effects - Ramps intensity 0→1→0 over 5 seconds per effect --- effects_plugins/hud.py | 61 +++++++++++++++++++++ engine/app.py | 121 +++++++++++++++++++++++++++++++++++++++++ engine/config.py | 4 ++ 3 files changed, 186 insertions(+) create mode 100644 effects_plugins/hud.py diff --git a/effects_plugins/hud.py b/effects_plugins/hud.py new file mode 100644 index 0000000..7ab91be --- /dev/null +++ b/effects_plugins/hud.py @@ -0,0 +1,61 @@ +from engine.effects.performance import get_monitor +from engine.effects.types import EffectConfig, EffectContext, EffectPlugin + + +class HudEffect(EffectPlugin): + name = "hud" + config = EffectConfig(enabled=True, intensity=1.0) + + def process(self, buf: list[str], ctx: EffectContext) -> list[str]: + result = list(buf) + monitor = get_monitor() + + fps = 0.0 + frame_time = 0.0 + if monitor: + stats = monitor.get_stats() + if stats: + fps = stats.fps + frame_time = stats.avg_frame_time_ms + + w = ctx.terminal_width + h = ctx.terminal_height + + effect_name = self.config.params.get("display_effect", "none") + effect_intensity = self.config.params.get("display_intensity", 0.0) + + hud_lines = [] + hud_lines.append( + f"\033[1;1H\033[38;5;46mMAINLINE DEMO\033[0m \033[38;5;245m|\033[0m \033[38;5;39mFPS: {fps:.1f}\033[0m \033[38;5;245m|\033[0m \033[38;5;208m{frame_time:.1f}ms\033[0m" + ) + + bar_width = 20 + filled = int(bar_width * effect_intensity) + bar = ( + "\033[38;5;82m" + + "█" * filled + + "\033[38;5;240m" + + "░" * (bar_width - filled) + + "\033[0m" + ) + hud_lines.append( + f"\033[2;1H\033[38;5;45mEFFECT:\033[0m \033[1;38;5;227m{effect_name:12s}\033[0m \033[38;5;245m|\033[0m {bar} \033[38;5;245m|\033[0m \033[38;5;219m{effect_intensity * 100:.0f}%\033[0m" + ) + + from engine.effects import get_effect_chain + + chain = get_effect_chain() + order = chain.get_order() + pipeline_str = ",".join(order) if order else "(none)" + hud_lines.append(f"\033[3;1H\033[38;5;44mPIPELINE:\033[0m {pipeline_str}") + + for i, line in enumerate(hud_lines): + if i < len(result): + result[i] = line + result[i][len(line) :] + else: + result.append(line) + + return result + + def configure(self, config: EffectConfig) -> None: + self.config = config diff --git a/engine/app.py b/engine/app.py index 3770bd3..039b213 100644 --- a/engine/app.py +++ b/engine/app.py @@ -351,7 +351,128 @@ def pick_effects_config(): termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) +def run_demo_mode(): + """Run demo mode - showcases effects with pygame display.""" + import random + + from engine import config + from engine.display import DisplayRegistry + from engine.effects import ( + EffectContext, + PerformanceMonitor, + get_effect_chain, + get_registry, + set_monitor, + ) + + print(" \033[1;38;5;46mMAINLINE DEMO MODE\033[0m") + print(" \033[38;5;245mInitializing pygame display...\033[0m") + + import effects_plugins + + effects_plugins.discover_plugins() + + registry = get_registry() + chain = get_effect_chain() + chain.set_order(["hud"]) + + monitor = PerformanceMonitor() + set_monitor(monitor) + chain._monitor = monitor + + display = DisplayRegistry.create("pygame") + if not display: + print(" \033[38;5;196mFailed to create pygame display\033[0m") + sys.exit(1) + + display.init(80, 24) + display.clear() + + effects_to_demo = ["noise", "fade", "glitch", "firehose"] + w, h = 80, 24 + + base_buffer = [] + for row in range(h): + line = "" + for col in range(w): + char = random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ") + line += char + base_buffer.append(line) + + print(" \033[38;5;82mStarting effect demo...\033[0m") + print(" \033[38;5;245mPress Ctrl+C to exit\033[0m\n") + + effect_idx = 0 + effect_name = effects_to_demo[effect_idx] + effect_start_time = time.time() + current_intensity = 0.0 + ramping_up = True + + frame_count = 0 + + try: + while True: + elapsed = time.time() - effect_start_time + duration = config.DEMO_EFFECT_DURATION + + if elapsed >= duration: + effect_idx = (effect_idx + 1) % len(effects_to_demo) + effect_name = effects_to_demo[effect_idx] + effect_start_time = time.time() + elapsed = 0 + current_intensity = 0.0 + ramping_up = True + + progress = elapsed / duration + if ramping_up: + current_intensity = progress + if progress >= 1.0: + ramping_up = False + else: + current_intensity = 1.0 - progress + + for effect in registry.list_all().values(): + if effect.name == effect_name: + effect.config.enabled = True + effect.config.intensity = current_intensity + elif effect.name not in ("hud", effect_name): + effect.config.enabled = False + + hud_effect = registry.get("hud") + if hud_effect: + hud_effect.config.params["display_effect"] = effect_name + hud_effect.config.params["display_intensity"] = current_intensity + + ctx = EffectContext( + terminal_width=w, + terminal_height=h, + scroll_cam=0, + ticker_height=h, + mic_excess=0.0, + grad_offset=time.time() % 1.0, + frame_number=frame_count, + has_message=False, + items=[], + ) + + result = chain.process(base_buffer, ctx) + display.show(result) + + frame_count += 1 + time.sleep(1 / 60) + + except KeyboardInterrupt: + pass + finally: + display.cleanup() + print("\n \033[38;5;245mDemo ended\033[0m") + + def main(): + if config.DEMO: + run_demo_mode() + return + atexit.register(lambda: print(CURSOR_ON, end="", flush=True)) def handle_sigint(*_): diff --git a/engine/config.py b/engine/config.py index efce6ca..6542563 100644 --- a/engine/config.py +++ b/engine/config.py @@ -241,6 +241,10 @@ DISPLAY = _arg_value("--display", sys.argv) or "terminal" WEBSOCKET = "--websocket" in sys.argv WEBSOCKET_PORT = _arg_int("--websocket-port", 8765) +# ─── DEMO MODE ──────────────────────────────────────────── +DEMO = "--demo" in sys.argv +DEMO_EFFECT_DURATION = 5.0 # seconds per effect + def set_font_selection(font_path=None, font_index=None): """Set runtime primary font selection."""