feat(app): add demo mode with HUD effect plugin

- 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
This commit is contained in:
2026-03-16 00:53:13 -07:00
parent 0f2d8bf5c2
commit 3e9c1be6d2
3 changed files with 186 additions and 0 deletions

61
effects_plugins/hud.py Normal file
View File

@@ -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

View File

@@ -351,7 +351,128 @@ def pick_effects_config():
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 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(): def main():
if config.DEMO:
run_demo_mode()
return
atexit.register(lambda: print(CURSOR_ON, end="", flush=True)) atexit.register(lambda: print(CURSOR_ON, end="", flush=True))
def handle_sigint(*_): def handle_sigint(*_):

View File

@@ -241,6 +241,10 @@ DISPLAY = _arg_value("--display", sys.argv) or "terminal"
WEBSOCKET = "--websocket" in sys.argv WEBSOCKET = "--websocket" in sys.argv
WEBSOCKET_PORT = _arg_int("--websocket-port", 8765) 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): def set_font_selection(font_path=None, font_index=None):
"""Set runtime primary font selection.""" """Set runtime primary font selection."""