forked from genewildish/Mainline
- Fixes issue #45: Add state property to EffectContext for motionblur/afterimage effects - Fixes issue #44: Reset camera bounce direction state in reset() method - Fixes issue #43: Implement pipeline hot-rebuild with state preservation - Adds radial camera mode for polar coordinate scanning - Adds afterimage and motionblur effects - Adds acceptance tests for camera and pipeline rebuild Closes #43, #44, #45
132 lines
4.4 KiB
Python
132 lines
4.4 KiB
Python
"""
|
|
ANSI terminal display backend.
|
|
"""
|
|
|
|
import os
|
|
|
|
|
|
class TerminalDisplay:
|
|
"""ANSI terminal display backend.
|
|
|
|
Renders buffer to stdout using ANSI escape codes.
|
|
Supports reuse - when reuse=True, skips re-initializing terminal state.
|
|
Auto-detects terminal dimensions on init.
|
|
"""
|
|
|
|
width: int = 80
|
|
height: int = 24
|
|
_initialized: bool = False
|
|
|
|
def __init__(self, target_fps: float = 30.0):
|
|
self.target_fps = target_fps
|
|
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
|
|
|
|
def init(self, width: int, height: int, reuse: bool = False) -> None:
|
|
"""Initialize display with dimensions.
|
|
|
|
If width/height are not provided (0/None), auto-detects terminal size.
|
|
Otherwise uses provided dimensions or falls back to terminal size
|
|
if the provided dimensions exceed terminal capacity.
|
|
|
|
Args:
|
|
width: Desired terminal width (0 = auto-detect)
|
|
height: Desired terminal height (0 = auto-detect)
|
|
reuse: If True, skip terminal re-initialization
|
|
"""
|
|
from engine.terminal import CURSOR_OFF
|
|
|
|
# Auto-detect terminal size (handle case where no terminal)
|
|
try:
|
|
term_size = os.get_terminal_size()
|
|
term_width = term_size.columns
|
|
term_height = term_size.lines
|
|
except OSError:
|
|
# No terminal available (e.g., in tests)
|
|
term_width = width if width > 0 else 80
|
|
term_height = height if height > 0 else 24
|
|
|
|
# Use provided dimensions if valid, otherwise use terminal size
|
|
if width > 0 and height > 0:
|
|
self.width = min(width, term_width)
|
|
self.height = min(height, term_height)
|
|
else:
|
|
self.width = term_width
|
|
self.height = term_height
|
|
|
|
if not reuse or not self._initialized:
|
|
print(CURSOR_OFF, end="", flush=True)
|
|
self._initialized = True
|
|
|
|
def get_dimensions(self) -> tuple[int, int]:
|
|
"""Get current terminal dimensions.
|
|
|
|
Returns cached dimensions to avoid querying terminal every frame,
|
|
which can cause inconsistent results. Dimensions are only refreshed
|
|
when they actually change.
|
|
|
|
Returns:
|
|
(width, height) in character cells
|
|
"""
|
|
try:
|
|
term_size = os.get_terminal_size()
|
|
new_dims = (term_size.columns, term_size.lines)
|
|
except OSError:
|
|
new_dims = (self.width, self.height)
|
|
|
|
# Only update cached dimensions if they actually changed
|
|
if self._cached_dimensions is None or self._cached_dimensions != new_dims:
|
|
self._cached_dimensions = new_dims
|
|
self.width = new_dims[0]
|
|
self.height = new_dims[1]
|
|
|
|
return self._cached_dimensions
|
|
|
|
def show(self, buffer: list[str], border: bool = False) -> None:
|
|
import sys
|
|
|
|
from engine.display import get_monitor, render_border
|
|
|
|
# Note: Frame rate limiting is handled by the caller (e.g., FrameTimer).
|
|
# This display renders every frame it receives.
|
|
|
|
# Get metrics for border display
|
|
fps = 0.0
|
|
frame_time = 0.0
|
|
monitor = get_monitor()
|
|
if monitor:
|
|
stats = monitor.get_stats()
|
|
avg_ms = stats.get("pipeline", {}).get("avg_ms", 0) if stats else 0
|
|
frame_count = stats.get("frame_count", 0) if stats else 0
|
|
if avg_ms and frame_count > 0:
|
|
fps = 1000.0 / avg_ms
|
|
frame_time = avg_ms
|
|
|
|
# Apply border if requested
|
|
if border:
|
|
buffer = render_border(buffer, self.width, self.height, fps, frame_time)
|
|
|
|
# Write buffer with cursor home + erase down to avoid flicker
|
|
output = "\033[H\033[J" + "".join(buffer)
|
|
sys.stdout.buffer.write(output.encode())
|
|
sys.stdout.flush()
|
|
|
|
def clear(self) -> None:
|
|
from engine.terminal import CLR
|
|
|
|
print(CLR, end="", flush=True)
|
|
|
|
def cleanup(self) -> None:
|
|
from engine.terminal import CURSOR_ON
|
|
|
|
print(CURSOR_ON, end="", flush=True)
|
|
|
|
def is_quit_requested(self) -> bool:
|
|
"""Check if quit was requested (optional protocol method)."""
|
|
return False
|
|
|
|
def clear_quit_request(self) -> None:
|
|
"""Clear quit request (optional protocol method)."""
|
|
pass
|