""" 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()