feat(repl): Add REPL effect with HUD-style interactive interface

Implement a Read-Eval-Print Loop (REPL) effect that provides a
HUD-style overlay for interactive pipeline control.

## New Files

- engine/effects/plugins/repl.py - REPL effect plugin with command processor
- engine/display/backends/terminal.py - Added raw mode and input handling
- examples/repl_simple.py - Simple demonstration script
- tests/test_repl_effect.py - 18 comprehensive tests

## Features

### REPL Interface
- HUD-style overlay showing FPS, command history, output buffer size
- Command history navigation (Up/Down arrows)
- Command execution (Enter)
- Character input and backspace support
- Output buffer with scrolling

### Commands
- help - Show available commands
- status - Show pipeline status and metrics
- effects - List all effects in pipeline
- effect <name> <on|off> - Toggle effect
- param <effect> <param> <value> - Set parameter
- pipeline - Show current pipeline order
- clear - Clear output buffer
- quit - Show exit message

### Terminal Input Support
- Added set_raw_mode() to TerminalDisplay for capturing keystrokes
- Added get_input_keys() to read keyboard input
- Proper terminal state restoration on cleanup

## Usage

Add 'repl' to effects in your configuration:

## Testing

All 18 REPL tests pass, covering:
- Effect registration
- Command processing
- Navigation (history, editing)
- Configuration
- Rendering

## Integration

The REPL effect integrates with the existing pipeline system:
- Uses EffectPlugin interface
- Supports partial updates
- Reads metrics from EffectContext
- Can be controlled via keyboard when terminal display is in raw mode

Next steps:
- Integrate REPL input handling into pipeline_runner.py
- Add keyboard event processing loop
- Create full demo with interactive features
This commit is contained in:
2026-03-21 21:12:38 -07:00
parent 2c23c423a0
commit fb0dd4592f
6 changed files with 975 additions and 12 deletions

View File

@@ -3,6 +3,10 @@ ANSI terminal display backend.
"""
import os
import select
import sys
import termios
import tty
class TerminalDisplay:
@@ -22,6 +26,8 @@ class TerminalDisplay:
self._frame_period = 1.0 / target_fps if target_fps > 0 else 0
self._last_frame_time = 0.0
self._cached_dimensions: tuple[int, int] | None = None
self._raw_mode_enabled: bool = False
self._original_termios: list = []
def init(self, width: int, height: int, reuse: bool = False) -> None:
"""Initialize display with dimensions.
@@ -150,6 +156,9 @@ class TerminalDisplay:
def cleanup(self) -> None:
from engine.terminal import CURSOR_ON
# Restore normal terminal mode if raw mode was enabled
self.set_raw_mode(False)
print(CURSOR_ON, end="", flush=True)
def is_quit_requested(self) -> bool:
@@ -159,3 +168,90 @@ class TerminalDisplay:
def clear_quit_request(self) -> None:
"""Clear quit request (optional protocol method)."""
pass
def set_raw_mode(self, enable: bool = True) -> None:
"""Enable/disable raw terminal mode for input capture.
When raw mode is enabled:
- Keystrokes are read immediately without echo
- Special keys (arrows, Ctrl+C, etc.) are captured
- Terminal is not in cooked/canonical mode
Args:
enable: True to enable raw mode, False to restore normal mode
"""
try:
if enable and not self._raw_mode_enabled:
# Save original terminal settings
self._original_termios = termios.tcgetattr(sys.stdin)
# Set raw mode
tty.setraw(sys.stdin.fileno())
self._raw_mode_enabled = True
elif not enable and self._raw_mode_enabled:
# Restore original terminal settings
if self._original_termios:
termios.tcsetattr(
sys.stdin, termios.TCSADRAIN, self._original_termios
)
self._raw_mode_enabled = False
except (termios.error, OSError):
# Terminal might not support raw mode (e.g., in tests)
pass
def get_input_keys(self, timeout: float = 0.0) -> list[str]:
"""Get available keyboard input.
Reads available keystrokes from stdin. Should be called
with raw mode enabled for best results.
Args:
timeout: Maximum time to wait for input (seconds)
Returns:
List of key symbols as strings
"""
keys = []
try:
# Check if input is available
if select.select([sys.stdin], [], [], timeout)[0]:
char = sys.stdin.read(1)
if char == "\x1b": # Escape sequence
# Read next character to determine key
seq = sys.stdin.read(2)
if seq == "[A":
keys.append("up")
elif seq == "[B":
keys.append("down")
elif seq == "[C":
keys.append("right")
elif seq == "[D":
keys.append("left")
else:
# Unknown escape sequence
keys.append("escape")
elif char == "\n" or char == "\r":
keys.append("return")
elif char == "\t":
keys.append("tab")
elif char == " ":
keys.append(" ")
elif char == "\x7f" or char == "\x08": # Backspace or Ctrl+H
keys.append("backspace")
elif char == "\x03": # Ctrl+C
keys.append("ctrl_c")
elif char == "\x04": # Ctrl+D
keys.append("ctrl_d")
elif char == "\x1b": # Escape
keys.append("escape")
elif char.isprintable():
keys.append(char)
except (OSError, IOError):
pass
return keys
def is_raw_mode_enabled(self) -> bool:
"""Check if raw mode is currently enabled."""
return self._raw_mode_enabled