Split monolithic scroll.py into focused modules: - viewport.py: terminal size (tw/th), ANSI positioning helpers - frame.py: FrameTimer class, scroll step calculation - layers.py: message overlay, ticker zone, firehose rendering - scroll.py: simplified orchestrator, imports from new modules Add stream controller and event types for future event-driven architecture: - controller.py: StreamController for source initialization and stream lifecycle - events.py: EventType enum and event dataclasses (HeadlineEvent, FrameTickEvent, etc.) Added tests for new modules: - test_viewport.py: 8 tests for viewport utilities - test_frame.py: 10 tests for frame timing - test_layers.py: 13 tests for layer compositing - test_events.py: 11 tests for event types - test_controller.py: 6 tests for stream controller This enables: - Testable chunks with clear responsibilities - Reusable viewport utilities across modules - Better separation of concerns in render pipeline - Foundation for future event-driven architecture Also includes Phase 1 documentation updates in code comments.
108 lines
3.1 KiB
Python
108 lines
3.1 KiB
Python
"""
|
|
Render engine — ticker content, scroll motion, message panel, and firehose overlay.
|
|
Orchestrates viewport, frame timing, and layers.
|
|
"""
|
|
|
|
import random
|
|
import sys
|
|
import time
|
|
|
|
from engine import config
|
|
from engine.frame import calculate_scroll_step
|
|
from engine.layers import (
|
|
apply_glitch,
|
|
render_firehose,
|
|
render_message_overlay,
|
|
render_ticker_zone,
|
|
)
|
|
from engine.terminal import CLR
|
|
from engine.viewport import th, tw
|
|
|
|
|
|
def stream(items, ntfy_poller, mic_monitor):
|
|
"""Main render loop with four layers: message, ticker, scroll motion, firehose."""
|
|
random.shuffle(items)
|
|
pool = list(items)
|
|
seen = set()
|
|
queued = 0
|
|
|
|
time.sleep(0.5)
|
|
sys.stdout.write(CLR)
|
|
sys.stdout.flush()
|
|
|
|
w, h = tw(), th()
|
|
fh = config.FIREHOSE_H if config.FIREHOSE else 0
|
|
ticker_view_h = h - fh
|
|
GAP = 3
|
|
scroll_step_interval = calculate_scroll_step(config.SCROLL_DUR, ticker_view_h)
|
|
|
|
active = []
|
|
scroll_cam = 0
|
|
ticker_next_y = ticker_view_h
|
|
noise_cache = {}
|
|
scroll_motion_accum = 0.0
|
|
msg_cache = (None, None)
|
|
|
|
while queued < config.HEADLINE_LIMIT or active:
|
|
t0 = time.monotonic()
|
|
w, h = tw(), th()
|
|
fh = config.FIREHOSE_H if config.FIREHOSE else 0
|
|
ticker_view_h = h - fh
|
|
|
|
msg = ntfy_poller.get_active_message()
|
|
msg_overlay, msg_cache = render_message_overlay(msg, w, h, msg_cache)
|
|
|
|
buf = []
|
|
ticker_h = ticker_view_h
|
|
|
|
scroll_motion_accum += config.FRAME_DT
|
|
while scroll_motion_accum >= scroll_step_interval:
|
|
scroll_motion_accum -= scroll_step_interval
|
|
scroll_cam += 1
|
|
|
|
while (
|
|
ticker_next_y < scroll_cam + ticker_view_h + 10
|
|
and queued < config.HEADLINE_LIMIT
|
|
):
|
|
from engine.effects import next_headline
|
|
from engine.render import make_block
|
|
|
|
t, src, ts = next_headline(pool, items, seen)
|
|
ticker_content, hc, midx = make_block(t, src, ts, w)
|
|
active.append((ticker_content, hc, ticker_next_y, midx))
|
|
ticker_next_y += len(ticker_content) + GAP
|
|
queued += 1
|
|
|
|
active = [
|
|
(c, hc, by, mi) for c, hc, by, mi in active if by + len(c) > scroll_cam
|
|
]
|
|
for k in list(noise_cache):
|
|
if k < scroll_cam:
|
|
del noise_cache[k]
|
|
|
|
grad_offset = (time.monotonic() * config.GRAD_SPEED) % 1.0
|
|
ticker_buf_start = len(buf)
|
|
|
|
ticker_buf, noise_cache = render_ticker_zone(
|
|
active, scroll_cam, ticker_h, w, noise_cache, grad_offset
|
|
)
|
|
buf.extend(ticker_buf)
|
|
|
|
mic_excess = mic_monitor.excess
|
|
buf = apply_glitch(buf, ticker_buf_start, mic_excess, w)
|
|
|
|
firehose_buf = render_firehose(items, w, fh, h)
|
|
buf.extend(firehose_buf)
|
|
|
|
if msg_overlay:
|
|
buf.extend(msg_overlay)
|
|
|
|
sys.stdout.buffer.write("".join(buf).encode())
|
|
sys.stdout.flush()
|
|
|
|
elapsed = time.monotonic() - t0
|
|
time.sleep(max(0, config.FRAME_DT - elapsed))
|
|
|
|
sys.stdout.write(CLR)
|
|
sys.stdout.flush()
|