forked from genewildish/Mainline
- Replace estimate_block_height (PIL-based) with estimate_simple_height (word wrap) - Update viewport filter tests to match new height-based filtering (~4 items vs 24) - Fix CI task duplication in mise.toml (remove redundant depends) Closes #38 Closes #36
266 lines
7.4 KiB
Python
266 lines
7.4 KiB
Python
"""Adapters for transform stages (viewport, font, image, canvas)."""
|
|
|
|
from typing import Any
|
|
|
|
import engine.render
|
|
from engine.data_sources import SourceItem
|
|
from engine.pipeline.core import DataType, PipelineContext, Stage
|
|
|
|
|
|
def estimate_simple_height(text: str, width: int) -> int:
|
|
"""Estimate height in terminal rows using simple word wrap.
|
|
|
|
Uses conservative estimation suitable for headlines.
|
|
Each wrapped line is approximately 6 terminal rows (big block rendering).
|
|
"""
|
|
words = text.split()
|
|
if not words:
|
|
return 6
|
|
|
|
lines = 1
|
|
current_len = 0
|
|
for word in words:
|
|
word_len = len(word)
|
|
if current_len + word_len + 1 > width - 4: # -4 for margins
|
|
lines += 1
|
|
current_len = word_len
|
|
else:
|
|
current_len += word_len + 1
|
|
|
|
return lines * 6 # 6 rows per line for big block rendering
|
|
|
|
|
|
class ViewportFilterStage(Stage):
|
|
"""Filter items to viewport height based on rendered height."""
|
|
|
|
def __init__(self, name: str = "viewport-filter"):
|
|
self.name = name
|
|
self.category = "render"
|
|
self.optional = True
|
|
self._layout: list[int] = []
|
|
|
|
@property
|
|
def stage_type(self) -> str:
|
|
return "render"
|
|
|
|
@property
|
|
def capabilities(self) -> set[str]:
|
|
return {"source.filtered"}
|
|
|
|
@property
|
|
def dependencies(self) -> set[str]:
|
|
return {"source"}
|
|
|
|
@property
|
|
def inlet_types(self) -> set:
|
|
return {DataType.SOURCE_ITEMS}
|
|
|
|
@property
|
|
def outlet_types(self) -> set:
|
|
return {DataType.SOURCE_ITEMS}
|
|
|
|
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
|
"""Filter items to viewport height based on rendered height."""
|
|
if data is None:
|
|
return data
|
|
|
|
if not isinstance(data, list):
|
|
return data
|
|
|
|
if not data:
|
|
return []
|
|
|
|
# Get viewport parameters from context
|
|
viewport_height = ctx.params.viewport_height if ctx.params else 24
|
|
viewport_width = ctx.params.viewport_width if ctx.params else 80
|
|
camera_y = ctx.get("camera_y", 0)
|
|
|
|
# Estimate height for each item and cache layout
|
|
self._layout = []
|
|
cumulative_heights = []
|
|
current_height = 0
|
|
|
|
for item in data:
|
|
title = item.content if isinstance(item, SourceItem) else str(item)
|
|
# Use simple height estimation (not PIL-based)
|
|
estimated_height = estimate_simple_height(title, viewport_width)
|
|
self._layout.append(estimated_height)
|
|
current_height += estimated_height
|
|
cumulative_heights.append(current_height)
|
|
|
|
# Find visible range based on camera_y and viewport_height
|
|
# camera_y is the scroll offset (how many rows are scrolled up)
|
|
start_y = camera_y
|
|
end_y = camera_y + viewport_height
|
|
|
|
# Find start index (first item that intersects with visible range)
|
|
start_idx = 0
|
|
for i, total_h in enumerate(cumulative_heights):
|
|
if total_h > start_y:
|
|
start_idx = i
|
|
break
|
|
|
|
# Find end index (first item that extends beyond visible range)
|
|
end_idx = len(data)
|
|
for i, total_h in enumerate(cumulative_heights):
|
|
if total_h >= end_y:
|
|
end_idx = i + 1
|
|
break
|
|
|
|
# Return visible items
|
|
return data[start_idx:end_idx]
|
|
|
|
|
|
class FontStage(Stage):
|
|
"""Render items using font."""
|
|
|
|
def __init__(self, name: str = "font"):
|
|
self.name = name
|
|
self.category = "render"
|
|
self.optional = False
|
|
|
|
@property
|
|
def stage_type(self) -> str:
|
|
return "render"
|
|
|
|
@property
|
|
def capabilities(self) -> set[str]:
|
|
return {"render.output"}
|
|
|
|
@property
|
|
def dependencies(self) -> set[str]:
|
|
return {"source"}
|
|
|
|
@property
|
|
def inlet_types(self) -> set:
|
|
return {DataType.SOURCE_ITEMS}
|
|
|
|
@property
|
|
def outlet_types(self) -> set:
|
|
return {DataType.TEXT_BUFFER}
|
|
|
|
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
|
"""Render items to text buffer using font."""
|
|
if data is None:
|
|
return []
|
|
|
|
if not isinstance(data, list):
|
|
return [str(data)]
|
|
|
|
viewport_width = ctx.params.viewport_width if ctx.params else 80
|
|
|
|
result = []
|
|
for item in data:
|
|
if isinstance(item, SourceItem):
|
|
title = item.content
|
|
src = item.source
|
|
ts = item.timestamp
|
|
content_lines, _, _ = engine.render.make_block(
|
|
title, src, ts, viewport_width
|
|
)
|
|
result.extend(content_lines)
|
|
elif hasattr(item, "content"):
|
|
title = str(item.content)
|
|
content_lines, _, _ = engine.render.make_block(
|
|
title, "", "", viewport_width
|
|
)
|
|
result.extend(content_lines)
|
|
else:
|
|
result.append(str(item))
|
|
return result
|
|
|
|
|
|
class ImageToTextStage(Stage):
|
|
"""Convert image items to text."""
|
|
|
|
def __init__(self, name: str = "image-to-text"):
|
|
self.name = name
|
|
self.category = "render"
|
|
self.optional = True
|
|
|
|
@property
|
|
def stage_type(self) -> str:
|
|
return "render"
|
|
|
|
@property
|
|
def capabilities(self) -> set[str]:
|
|
return {"render.output"}
|
|
|
|
@property
|
|
def dependencies(self) -> set[str]:
|
|
return {"source"}
|
|
|
|
@property
|
|
def inlet_types(self) -> set:
|
|
return {DataType.SOURCE_ITEMS}
|
|
|
|
@property
|
|
def outlet_types(self) -> set:
|
|
return {DataType.TEXT_BUFFER}
|
|
|
|
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
|
"""Convert image items to text representation."""
|
|
if data is None:
|
|
return []
|
|
|
|
if not isinstance(data, list):
|
|
return [str(data)]
|
|
|
|
result = []
|
|
for item in data:
|
|
# Check if item is an image
|
|
if hasattr(item, "image_path") or hasattr(item, "image_data"):
|
|
# Placeholder: would normally render image to ASCII art
|
|
result.append(f"[Image: {getattr(item, 'image_path', 'data')}]")
|
|
elif isinstance(item, SourceItem):
|
|
result.extend(item.content.split("\n"))
|
|
else:
|
|
result.append(str(item))
|
|
return result
|
|
|
|
|
|
class CanvasStage(Stage):
|
|
"""Render items to canvas."""
|
|
|
|
def __init__(self, name: str = "canvas"):
|
|
self.name = name
|
|
self.category = "render"
|
|
self.optional = False
|
|
|
|
@property
|
|
def stage_type(self) -> str:
|
|
return "render"
|
|
|
|
@property
|
|
def capabilities(self) -> set[str]:
|
|
return {"render.output"}
|
|
|
|
@property
|
|
def dependencies(self) -> set[str]:
|
|
return {"source"}
|
|
|
|
@property
|
|
def inlet_types(self) -> set:
|
|
return {DataType.SOURCE_ITEMS}
|
|
|
|
@property
|
|
def outlet_types(self) -> set:
|
|
return {DataType.TEXT_BUFFER}
|
|
|
|
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
|
"""Render items to canvas."""
|
|
if data is None:
|
|
return []
|
|
|
|
if not isinstance(data, list):
|
|
return [str(data)]
|
|
|
|
# Simple canvas rendering
|
|
result = []
|
|
for item in data:
|
|
if isinstance(item, SourceItem):
|
|
result.extend(item.content.split("\n"))
|
|
else:
|
|
result.append(str(item))
|
|
return result
|