forked from genewildish/Mainline
The old engine/pipeline/core.py file was removed as part of the Sideline/Mainline split. All imports that referenced engine.pipeline.core have been updated to use engine.pipeline which re-exports from sideline.pipeline.core. This ensures consistency and avoids duplicate DataType enum instances.
186 lines
6.1 KiB
Python
186 lines
6.1 KiB
Python
"""PositionStage - Configurable positioning mode for terminal rendering.
|
|
|
|
This module provides positioning stages that allow choosing between
|
|
different ANSI positioning approaches:
|
|
- ABSOLUTE: Use cursor positioning codes (\\033[row;colH) for all lines
|
|
- RELATIVE: Use newlines for all lines
|
|
- MIXED: Base content uses newlines, effects use cursor positioning (default)
|
|
"""
|
|
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
from engine.pipeline import DataType, PipelineContext, Stage
|
|
|
|
|
|
class PositioningMode(Enum):
|
|
"""Positioning mode for terminal rendering."""
|
|
|
|
ABSOLUTE = "absolute" # All lines have cursor positioning codes
|
|
RELATIVE = "relative" # Lines use newlines (no cursor codes)
|
|
MIXED = "mixed" # Mixed: newlines for base, cursor codes for overlays (default)
|
|
|
|
|
|
class PositionStage(Stage):
|
|
"""Applies positioning mode to buffer before display.
|
|
|
|
This stage allows configuring how lines are positioned in the terminal:
|
|
- ABSOLUTE: Each line has \\033[row;colH prefix (precise control)
|
|
- RELATIVE: Lines are joined with \\n (natural flow)
|
|
- MIXED: Leaves buffer as-is (effects add their own positioning)
|
|
"""
|
|
|
|
def __init__(
|
|
self, mode: PositioningMode = PositioningMode.RELATIVE, name: str = "position"
|
|
):
|
|
self.mode = mode
|
|
self.name = name
|
|
self.category = "position"
|
|
self._mode_str = mode.value
|
|
|
|
def save_state(self) -> dict[str, Any]:
|
|
"""Save positioning mode for restoration."""
|
|
return {"mode": self.mode.value}
|
|
|
|
def restore_state(self, state: dict[str, Any]) -> None:
|
|
"""Restore positioning mode from saved state."""
|
|
mode_value = state.get("mode", "relative")
|
|
self.mode = PositioningMode(mode_value)
|
|
|
|
@property
|
|
def capabilities(self) -> set[str]:
|
|
return {"position.output"}
|
|
|
|
@property
|
|
def dependencies(self) -> set[str]:
|
|
# Position stage typically runs after render but before effects
|
|
# Effects may add their own positioning codes
|
|
return {"render.output"}
|
|
|
|
@property
|
|
def inlet_types(self) -> set:
|
|
return {DataType.TEXT_BUFFER}
|
|
|
|
@property
|
|
def outlet_types(self) -> set:
|
|
return {DataType.TEXT_BUFFER}
|
|
|
|
def init(self, ctx: PipelineContext) -> bool:
|
|
"""Initialize the positioning stage."""
|
|
return True
|
|
|
|
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
|
"""Apply positioning mode to the buffer.
|
|
|
|
Args:
|
|
data: List of strings (buffer lines)
|
|
ctx: Pipeline context
|
|
|
|
Returns:
|
|
Buffer with applied positioning mode
|
|
"""
|
|
if data is None:
|
|
return data
|
|
|
|
if not isinstance(data, list):
|
|
return data
|
|
|
|
if self.mode == PositioningMode.ABSOLUTE:
|
|
return self._to_absolute(data, ctx)
|
|
elif self.mode == PositioningMode.RELATIVE:
|
|
return self._to_relative(data, ctx)
|
|
else: # MIXED
|
|
return data # No transformation
|
|
|
|
def _to_absolute(self, data: list[str], ctx: PipelineContext) -> list[str]:
|
|
"""Convert buffer to absolute positioning (all lines have cursor codes).
|
|
|
|
This mode prefixes each line with \\033[row;colH to move cursor
|
|
to the exact position before writing the line.
|
|
|
|
Args:
|
|
data: List of buffer lines
|
|
ctx: Pipeline context (provides terminal dimensions)
|
|
|
|
Returns:
|
|
Buffer with cursor positioning codes for each line
|
|
"""
|
|
result = []
|
|
viewport_height = ctx.params.viewport_height if ctx.params else 24
|
|
|
|
for i, line in enumerate(data):
|
|
if i >= viewport_height:
|
|
break # Don't exceed viewport
|
|
|
|
# Check if line already has cursor positioning
|
|
if "\033[" in line and "H" in line:
|
|
# Already has cursor positioning - leave as-is
|
|
result.append(line)
|
|
else:
|
|
# Add cursor positioning for this line
|
|
# Row is 1-indexed
|
|
result.append(f"\033[{i + 1};1H{line}")
|
|
|
|
return result
|
|
|
|
def _to_relative(self, data: list[str], ctx: PipelineContext) -> list[str]:
|
|
"""Convert buffer to relative positioning (use newlines).
|
|
|
|
This mode removes explicit cursor positioning codes from lines
|
|
(except for effects that specifically add them).
|
|
|
|
Note: Effects like HUD add their own cursor positioning codes,
|
|
so we can't simply remove all of them. We rely on the terminal
|
|
display to join lines with newlines.
|
|
|
|
Args:
|
|
data: List of buffer lines
|
|
ctx: Pipeline context (unused)
|
|
|
|
Returns:
|
|
Buffer with minimal cursor positioning (only for overlays)
|
|
"""
|
|
# For relative mode, we leave the buffer as-is
|
|
# The terminal display handles joining with newlines
|
|
# Effects that need absolute positioning will add their own codes
|
|
|
|
# Filter out lines that would cause double-positioning
|
|
result = []
|
|
for i, line in enumerate(data):
|
|
# Check if this line looks like base content (no cursor code at start)
|
|
# vs an effect line (has cursor code at start)
|
|
if line.startswith("\033[") and "H" in line[:20]:
|
|
# This is an effect with positioning - keep it
|
|
result.append(line)
|
|
else:
|
|
# Base content - strip any inline cursor codes (rare)
|
|
# but keep color codes
|
|
result.append(line)
|
|
|
|
return result
|
|
|
|
def cleanup(self) -> None:
|
|
"""Clean up positioning stage."""
|
|
pass
|
|
|
|
|
|
# Convenience function to create positioning stage
|
|
def create_position_stage(
|
|
mode: str = "relative", name: str = "position"
|
|
) -> PositionStage:
|
|
"""Create a positioning stage with the specified mode.
|
|
|
|
Args:
|
|
mode: Positioning mode ("absolute", "relative", or "mixed")
|
|
name: Name for the stage
|
|
|
|
Returns:
|
|
PositionStage instance
|
|
"""
|
|
try:
|
|
positioning_mode = PositioningMode(mode)
|
|
except ValueError:
|
|
positioning_mode = PositioningMode.RELATIVE
|
|
|
|
return PositionStage(mode=positioning_mode, name=name)
|