diff --git a/engine/scroll.py b/engine/scroll.py index 3926989..54ae9ab 100644 --- a/engine/scroll.py +++ b/engine/scroll.py @@ -14,34 +14,6 @@ from engine.terminal import RST, W_COOL, CLR, tw, th from engine.render import big_wrap, lr_gradient, make_block from engine.effects import noise, glitch_bar, fade_line, vis_trunc, next_headline, firehose_line -_ANSI_LINE_RE = re.compile(r"^(\033\[[0-9;]*m)(.*)(\033\[0m)$") -FIREHOSE_BG = "" - - -def _overlay_segments(ansi_line, bg=""): - """Return (1-indexed column, colored chunk) for non-space runs in a line.""" - m = _ANSI_LINE_RE.match(ansi_line) - if m: - color, text, _ = m.groups() - else: - color, text = "", ansi_line - segs = [] - i = 0 - while i < len(text): - if text[i] == " ": - i += 1 - continue - j = i + 1 - while j < len(text) and text[j] != " ": - j += 1 - chunk = text[i:j] - if bg or color: - segs.append((i + 1, f"{bg}{color}{chunk}{RST}")) - else: - segs.append((i + 1, chunk)) - i = j - return segs - def stream(items, ntfy_poller, mic_monitor): """Main render loop with four layers: message, ticker, scroll motion, firehose.""" @@ -56,25 +28,21 @@ def stream(items, ntfy_poller, mic_monitor): w, h = tw(), th() fh = config.FIREHOSE_H if config.FIREHOSE else 0 - ticker_view_h = h # ticker uses full viewport; firehose overlays it + ticker_view_h = h - fh # reserve fixed firehose strip at bottom GAP = 3 # blank rows between headlines scroll_step_interval = config.SCROLL_DUR / (ticker_view_h + 15) * 2 # Taxonomy: - # - message: ntfy interrupt panel at top + # - message: centered ntfy overlay panel # - ticker: large headline text content # - scroll: upward camera motion applied to ticker content - # - firehose: carriage-return style overlay drifting above ticker + # - firehose: fixed carriage-return style strip pinned at bottom # Active ticker blocks: (content_rows, color, canvas_y, meta_idx) active = [] scroll_cam = 0 # viewport top in virtual canvas coords ticker_next_y = ticker_view_h # canvas-y where next block starts (off-screen bottom) noise_cache = {} scroll_motion_accum = 0.0 - firehose_top = h - fh - firehose_rows = [firehose_line(items, w) for _ in range(fh)] if fh > 0 else [] - firehose_accum = 0.0 - firehose_interval = max(config.FRAME_DT, scroll_step_interval * 1.3) def _noise_at(cy): if cy not in noise_cache: @@ -90,16 +58,17 @@ def stream(items, ntfy_poller, mic_monitor): t0 = time.monotonic() w, h = tw(), th() fh = config.FIREHOSE_H if config.FIREHOSE else 0 - ticker_view_h = h + ticker_view_h = h - fh # ── Check for ntfy message ──────────────────────── - msg_h = 0 # rows consumed by message zone at top + msg_h = 0 + msg_overlay = [] msg = ntfy_poller.get_active_message() buf = [] if msg is not None: m_title, m_body, m_ts = msg - # ── Message zone: pinned to top, scroll continues below ── + # ── Message overlay: centered in the viewport ── display_text = m_body or m_title or "(empty)" display_text = re.sub(r"\s+", " ", display_text.upper()) cache_key = (display_text, w) @@ -113,10 +82,12 @@ def stream(items, ntfy_poller, mic_monitor): elapsed_s = int(time.monotonic() - m_ts) remaining = max(0, config.MESSAGE_DISPLAY_SECS - elapsed_s) ts_str = datetime.now().strftime("%H:%M:%S") + panel_h = len(msg_rows) + 2 # meta + border + panel_top = max(0, (h - panel_h) // 2) row_idx = 0 for mr in msg_rows: ln = vis_trunc(mr, w) - buf.append(f"\033[{row_idx+1};1H {ln}{RST}\033[K") + msg_overlay.append(f"\033[{panel_top + row_idx + 1};1H {ln}{RST}\033[K") row_idx += 1 # Meta line: title (if distinct) + source + countdown meta_parts = [] @@ -124,15 +95,13 @@ def stream(items, ntfy_poller, mic_monitor): meta_parts.append(m_title) meta_parts.append(f"ntfy \u00b7 {ts_str} \u00b7 {remaining}s") meta = " " + " \u00b7 ".join(meta_parts) if len(meta_parts) > 1 else " " + meta_parts[0] - buf.append(f"\033[{row_idx+1};1H{MSG_META}{meta}{RST}\033[K") + msg_overlay.append(f"\033[{panel_top + row_idx + 1};1H{MSG_META}{meta}{RST}\033[K") row_idx += 1 - # Border — constant boundary between message and scroll + # Border — constant boundary under message panel bar = "\u2500" * (w - 4) - buf.append(f"\033[{row_idx+1};1H {MSG_BORDER}{bar}{RST}\033[K") - row_idx += 1 - msg_h = row_idx + msg_overlay.append(f"\033[{panel_top + row_idx + 1};1H {MSG_BORDER}{bar}{RST}\033[K") - # Effective ticker draw height: below message + # Ticker draws above the fixed firehose strip; message is a centered overlay. ticker_h = ticker_view_h - msg_h # ── Ticker content + scroll motion (always runs) ── @@ -156,28 +125,13 @@ def stream(items, ntfy_poller, mic_monitor): if k < scroll_cam: del noise_cache[k] - # Firehose overlay drift (slower than main ticker) - if config.FIREHOSE and fh > 0: - while len(firehose_rows) < fh: - firehose_rows.append(firehose_line(items, w)) - if len(firehose_rows) > fh: - firehose_rows = firehose_rows[-fh:] - firehose_accum += config.FRAME_DT - while firehose_accum >= firehose_interval: - firehose_accum -= firehose_interval - firehose_top -= 1 - firehose_rows.pop(0) - firehose_rows.append(firehose_line(items, w)) - if firehose_top + fh < 0: - firehose_top = h - fh - - # Draw ticker zone (below message zone) + # Draw ticker zone (above fixed firehose strip) top_zone = max(1, int(ticker_h * 0.25)) bot_zone = max(1, int(ticker_h * 0.10)) grad_offset = (time.monotonic() * config.GRAD_SPEED) % 1.0 ticker_buf_start = len(buf) # track where ticker rows start in buf for r in range(ticker_h): - scr_row = msg_h + r + 1 # 1-indexed ANSI screen row + scr_row = r + 1 # 1-indexed ANSI screen row cy = scroll_cam + r top_f = min(1.0, r / top_zone) if top_zone > 0 else 1.0 bot_f = min(1.0, (ticker_h - 1 - r) / bot_zone) if bot_zone > 0 else 1.0 @@ -219,16 +173,16 @@ def stream(items, ntfy_poller, mic_monitor): if random.random() < glitch_prob and ticker_buf_len > 0: for _ in range(min(n_hits, ticker_buf_len)): gi = random.randint(0, ticker_buf_len - 1) - scr_row = msg_h + gi + 1 + scr_row = gi + 1 buf[ticker_buf_start + gi] = f"\033[{scr_row};1H{glitch_bar(w)}" if config.FIREHOSE and fh > 0: - for fr, fline in enumerate(firehose_rows): - scr_row = firehose_top + fr + 1 - if scr_row <= msg_h or scr_row > h: - continue - for col, chunk in _overlay_segments(fline, FIREHOSE_BG): - buf.append(f"\033[{scr_row};{col}H{chunk}") + for fr in range(fh): + scr_row = h - fh + fr + 1 + fline = firehose_line(items, w) + buf.append(f"\033[{scr_row};1H{fline}\033[K") + if msg_overlay: + buf.extend(msg_overlay) sys.stdout.buffer.write("".join(buf).encode()) sys.stdout.flush()