Compare commits

...

4 Commits

Author SHA1 Message Date
a050e26c03 Add Ctrl+C quit handling to REPL
- Add _quit_requested flag to TerminalDisplay
- Add request_quit() method to TerminalDisplay
- Handle 'ctrl_c' key in REPL input loops in both pipeline_runner.py and main.py
- When Ctrl+C is pressed, request_quit() is called which sets the flag
- The main loop checks is_quit_requested() and raises KeyboardInterrupt
2026-03-22 16:48:05 -07:00
d5406a6b11 Fix REPL HUD layout by removing cursor positioning codes
- Remove \033[1;1H, \033[2;1H, \033[3;1H from HUD rendering
- HUD text now appears at correct positions without cursor interference
- Prompt appears at left margin as expected
2026-03-22 16:46:32 -07:00
3fac583d94 Add REPL usage documentation and fix raw mode handling
- Fix raw mode enabling to not duplicate with UI border mode
- Add REPL_USAGE.md with comprehensive guide
- Add examples/repl_demo_terminal.py example script
2026-03-22 16:42:40 -07:00
995badbffc Add REPL support to run_pipeline_mode_direct()
- Detect REPL effect in pipeline and enable interactive mode
- Enable raw terminal mode for REPL input capture
- Add keyboard input loop for REPL commands
- Add _handle_pipeline_mutation() function for pipeline control
2026-03-22 16:41:52 -07:00
6 changed files with 309 additions and 9 deletions

116
REPL_USAGE.md Normal file
View File

@@ -0,0 +1,116 @@
# REPL Usage Guide
The REPL (Read-Eval-Print Loop) effect provides an interactive command-line interface for controlling Mainline's pipeline in real-time.
## How to Access the REPL
### Method 1: Using CLI Arguments (Recommended)
Run Mainline with the `repl` effect added to the effects list:
```bash
# With empty source (for testing)
python mainline.py --pipeline-source empty --pipeline-effects repl
# With headlines source (requires network)
python mainline.py --pipeline-source headlines --pipeline-effects repl
# With poetry source
python mainline.py --pipeline-source poetry --pipeline-effects repl
```
### Method 2: Using a Preset
Add a preset to your `~/.config/mainline/presets.toml` or `./presets.toml`:
```toml
[presets.repl]
description = "Interactive REPL control"
source = "headlines"
display = "terminal"
effects = ["repl"]
viewport_width = 80
viewport_height = 24
```
Then run:
```bash
python mainline.py --preset repl
```
### Method 3: Using Graph Config
Create a TOML file (e.g., `repl_config.toml`):
```toml
source = "empty"
display = "terminal"
effects = ["repl"]
```
Then run:
```bash
python mainline.py --graph-config repl_config.toml
```
## REPL Commands
Once the REPL is active, you can type commands:
- **help** - Show available commands
- **status** - Show pipeline status and metrics
- **effects** - List all effects in the pipeline
- **effect \<name\> \<on|off\>** - Toggle an effect
- **param \<effect\> \<param\> \<value\>** - Set effect parameter
- **pipeline** - Show current pipeline order
- **clear** - Clear output buffer
- **quit/exit** - Show exit message (use Ctrl+C to actually exit)
## Keyboard Controls
- **Enter** - Execute command
- **Up/Down arrows** - Navigate command history
- **Backspace** - Delete last character
- **Ctrl+C** - Exit Mainline
## Visual Features
The REPL displays:
- **HUD header** (top 3 lines): Shows FPS, frame time, command count, and output buffer size
- **Content area**: Main content from the data source
- **Separator line**: Visual divider
- **REPL area**: Output buffer and input prompt
## Example Session
```
MAINLINE REPL | FPS: 60.0 | 12.5ms
COMMANDS: 3 | [2/3]
OUTPUT: 5 lines
────────────────────────────────────────
Content from source appears here...
More content...
────────────────────────────────────────
> help
Available commands:
help - Show this help
status - Show pipeline status
effects - List all effects
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
> effects
Pipeline effects:
1. repl
> effect repl off
Effect 'repl' set to off
```
## Notes
- The REPL effect needs a content source to overlay on (e.g., headlines, poetry, empty)
- The REPL uses terminal display with raw input mode
- Command history is preserved across sessions (up to 50 commands)
- Pipeline mutations (enabling/disabling effects) are handled automatically

View File

@@ -34,6 +34,82 @@ except ImportError:
from .pipeline_runner import run_pipeline_mode
def _handle_pipeline_mutation(pipeline: Pipeline, command: dict) -> bool:
"""Handle pipeline mutation commands from REPL or other external control.
Args:
pipeline: The pipeline to mutate
command: Command dictionary with 'action' and other parameters
Returns:
True if command was successfully handled, False otherwise
"""
action = command.get("action")
if action == "add_stage":
print(f" [Pipeline] add_stage command received: {command}")
return True
elif action == "remove_stage":
stage_name = command.get("stage")
if stage_name:
result = pipeline.remove_stage(stage_name)
print(f" [Pipeline] Removed stage '{stage_name}': {result is not None}")
return result is not None
elif action == "replace_stage":
stage_name = command.get("stage")
print(f" [Pipeline] replace_stage command received: {command}")
return True
elif action == "swap_stages":
stage1 = command.get("stage1")
stage2 = command.get("stage2")
if stage1 and stage2:
result = pipeline.swap_stages(stage1, stage2)
print(f" [Pipeline] Swapped stages '{stage1}' and '{stage2}': {result}")
return result
elif action == "move_stage":
stage_name = command.get("stage")
after = command.get("after")
before = command.get("before")
if stage_name:
result = pipeline.move_stage(stage_name, after, before)
print(f" [Pipeline] Moved stage '{stage_name}': {result}")
return result
elif action == "enable_stage":
stage_name = command.get("stage")
if stage_name:
result = pipeline.enable_stage(stage_name)
print(f" [Pipeline] Enabled stage '{stage_name}': {result}")
return result
elif action == "disable_stage":
stage_name = command.get("stage")
if stage_name:
result = pipeline.disable_stage(stage_name)
print(f" [Pipeline] Disabled stage '{stage_name}': {result}")
return result
elif action == "cleanup_stage":
stage_name = command.get("stage")
if stage_name:
pipeline.cleanup_stage(stage_name)
print(f" [Pipeline] Cleaned up stage '{stage_name}'")
return True
elif action == "can_hot_swap":
stage_name = command.get("stage")
if stage_name:
can_swap = pipeline.can_hot_swap(stage_name)
print(f" [Pipeline] Can hot-swap '{stage_name}': {can_swap}")
return True
return False
def main():
"""Main entry point - all modes now use presets or CLI construction."""
if config.PIPELINE_DIAGRAM:
@@ -391,6 +467,21 @@ def run_pipeline_mode_direct():
except Exception:
pass
# Check for REPL effect in pipeline
repl_effect = None
for stage in pipeline.stages.values():
if isinstance(stage, EffectPluginStage) and stage._effect.name == "repl":
repl_effect = stage._effect
print(
" \033[38;5;46mREPL effect detected - Interactive mode enabled\033[0m"
)
break
# Enable raw mode for REPL if present and not already enabled
# Also enable for UI border mode (already handled above)
if repl_effect and ui_panel is None and hasattr(display, "set_raw_mode"):
display.set_raw_mode(True)
# Run pipeline loop
from engine.display import render_ui_panel
@@ -453,6 +544,37 @@ def run_pipeline_mode_direct():
except Exception:
pass
# --- REPL Input Handling ---
if repl_effect and hasattr(display, "get_input_keys"):
# Get keyboard input (non-blocking)
keys = display.get_input_keys(timeout=0.0)
for key in keys:
if key == "ctrl_c":
# Request quit when Ctrl+C is pressed
if hasattr(display, "request_quit"):
display.request_quit()
else:
raise KeyboardInterrupt()
elif key == "return":
# Get command string before processing
cmd_str = repl_effect.state.current_command
if cmd_str:
repl_effect.process_command(cmd_str, ctx)
# Check for pending pipeline mutations
pending = repl_effect.get_pending_command()
if pending:
_handle_pipeline_mutation(pipeline, pending)
elif key == "up":
repl_effect.navigate_history(-1)
elif key == "down":
repl_effect.navigate_history(1)
elif key == "backspace":
repl_effect.backspace()
elif len(key) == 1:
repl_effect.append_to_command(key)
# --- End REPL Input Handling ---
# Check for quit request
if hasattr(display, "is_quit_requested") and display.is_quit_requested():
if hasattr(display, "clear_quit_request"):

View File

@@ -986,7 +986,13 @@ def run_pipeline_mode(preset_name: str = "demo", graph_config: str | None = None
keys = display.get_input_keys(timeout=0.0)
for key in keys:
if key == "return":
if key == "ctrl_c":
# Request quit when Ctrl+C is pressed
if hasattr(display, "request_quit"):
display.request_quit()
else:
raise KeyboardInterrupt()
elif key == "return":
# Get command string before processing
cmd_str = repl_effect.state.current_command
if cmd_str:

View File

@@ -28,6 +28,7 @@ class TerminalDisplay:
self._cached_dimensions: tuple[int, int] | None = None
self._raw_mode_enabled: bool = False
self._original_termios: list = []
self._quit_requested: bool = False
def init(self, width: int, height: int, reuse: bool = False) -> None:
"""Initialize display with dimensions.
@@ -163,11 +164,15 @@ class TerminalDisplay:
def is_quit_requested(self) -> bool:
"""Check if quit was requested (optional protocol method)."""
return False
return self._quit_requested
def clear_quit_request(self) -> None:
"""Clear quit request (optional protocol method)."""
pass
self._quit_requested = False
def request_quit(self) -> None:
"""Request quit (e.g., when Ctrl+C is pressed)."""
self._quit_requested = True
def set_raw_mode(self, enable: bool = True) -> None:
"""Enable/disable raw terminal mode for input capture.

View File

@@ -138,7 +138,7 @@ class ReplEffect(EffectPlugin):
fps_str = f"FPS: {fps:.1f}" if fps > 0 else "FPS: --"
time_str = f"{frame_time:.1f}ms" if frame_time > 0 else "--ms"
line1 = (
f"\033[1;1H\033[38;5;46mMAINLINE REPL\033[0m "
f"\033[38;5;46mMAINLINE REPL\033[0m "
f"\033[38;5;245m|\033[0m \033[38;5;39m{fps_str}\033[0m "
f"\033[38;5;245m|\033[0m \033[38;5;208m{time_str}\033[0m"
)
@@ -150,7 +150,7 @@ class ReplEffect(EffectPlugin):
f"[{self.state.history_index + 1}/{cmd_count}]" if cmd_count > 0 else ""
)
line2 = (
f"\033[2;1H\033[38;5;45mCOMMANDS:\033[0m "
f"\033[38;5;45mCOMMANDS:\033[0m "
f"\033[1;38;5;227m{cmd_count}\033[0m "
f"\033[38;5;245m|\033[0m \033[38;5;219m{hist_idx}\033[0m"
)
@@ -158,10 +158,7 @@ class ReplEffect(EffectPlugin):
# Line 3: Output buffer count
out_count = len(self.state.output_buffer)
line3 = (
f"\033[3;1H\033[38;5;44mOUTPUT:\033[0m "
f"\033[1;38;5;227m{out_count}\033[0m lines"
)
line3 = f"\033[38;5;44mOUTPUT:\033[0m \033[1;38;5;227m{out_count}\033[0m lines"
lines.append(line3[:width])
return lines

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
REPL Demo with Terminal Display - Shows how to use the REPL effect
Usage:
python examples/repl_demo_terminal.py
This demonstrates the REPL effect with terminal display and interactive input.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from engine.effects.plugins import discover_plugins
from engine.pipeline.hybrid_config import PipelineConfig
def main():
"""Run REPL demo with terminal display."""
print("REPL Demo with Terminal Display")
print("=" * 50)
# Discover plugins
discover_plugins()
# Create a pipeline with REPL effect
# Using empty source so there's content to overlay on
config = PipelineConfig(
source="empty",
effects=[{"name": "repl", "intensity": 1.0}],
display="terminal",
)
pipeline = config.to_pipeline(viewport_width=80, viewport_height=24)
# Initialize pipeline
if not pipeline.initialize():
print("Failed to initialize pipeline")
return
print("\nREPL is now active!")
print("Try typing commands:")
print(" help - Show available commands")
print(" status - Show pipeline status")
print(" effects - List all effects")
print(" pipeline - Show current pipeline order")
print(" clear - Clear output buffer")
print("\nPress Ctrl+C to exit")
if __name__ == "__main__":
main()