feat: Implement a drifting firehose overlay that scrolls independently over the main ticker content.
This commit is contained in:
@@ -14,6 +14,30 @@ 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)$")
|
||||
|
||||
|
||||
def _overlay_segments(ansi_line):
|
||||
"""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]
|
||||
segs.append((i + 1, f"{color}{chunk}{RST}" if color else chunk))
|
||||
i = j
|
||||
return segs
|
||||
|
||||
|
||||
def stream(items, ntfy_poller, mic_monitor):
|
||||
"""Main rendering loop. Scrolls headlines, shows ntfy messages, applies effects."""
|
||||
@@ -28,7 +52,7 @@ def stream(items, ntfy_poller, mic_monitor):
|
||||
|
||||
w, h = tw(), th()
|
||||
fh = config.FIREHOSE_H if config.FIREHOSE else 0
|
||||
sh = h - fh # scroll zone height
|
||||
sh = h # headline scroll uses full viewport
|
||||
GAP = 3 # blank rows between headlines
|
||||
scroll_interval = config.SCROLL_DUR / (sh + 15) * 2
|
||||
|
||||
@@ -38,6 +62,10 @@ def stream(items, ntfy_poller, mic_monitor):
|
||||
next_y = sh # canvas-y where next block starts (off-screen bottom)
|
||||
noise_cache = {}
|
||||
scroll_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_interval * 1.3)
|
||||
|
||||
def _noise_at(cy):
|
||||
if cy not in noise_cache:
|
||||
@@ -53,7 +81,7 @@ def stream(items, ntfy_poller, mic_monitor):
|
||||
t0 = time.monotonic()
|
||||
w, h = tw(), th()
|
||||
fh = config.FIREHOSE_H if config.FIREHOSE else 0
|
||||
sh = h - fh
|
||||
sh = h
|
||||
|
||||
# ── Check for ntfy message ────────────────────────
|
||||
msg_h = 0 # rows consumed by message zone at top
|
||||
@@ -95,7 +123,7 @@ def stream(items, ntfy_poller, mic_monitor):
|
||||
row_idx += 1
|
||||
msg_h = row_idx
|
||||
|
||||
# Effective scroll zone: below message, above firehose
|
||||
# Effective ticker zone: below message
|
||||
scroll_h = sh - msg_h
|
||||
|
||||
# ── Scroll: headline rendering (always runs) ──────
|
||||
@@ -120,7 +148,22 @@ def stream(items, ntfy_poller, mic_monitor):
|
||||
if k < cam:
|
||||
del noise_cache[k]
|
||||
|
||||
# Draw scroll zone (below message zone, above firehose)
|
||||
# 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)
|
||||
top_zone = max(1, int(scroll_h * 0.25))
|
||||
bot_zone = max(1, int(scroll_h * 0.10))
|
||||
grad_offset = (time.monotonic() * config.GRAD_SPEED) % 1.0
|
||||
@@ -160,11 +203,6 @@ def stream(items, ntfy_poller, mic_monitor):
|
||||
else:
|
||||
buf.append(f"\033[{scr_row};1H\033[K")
|
||||
|
||||
# Draw firehose zone
|
||||
if config.FIREHOSE and fh > 0:
|
||||
for fr in range(fh):
|
||||
fline = firehose_line(items, w)
|
||||
buf.append(f"\033[{sh + fr + 1};1H{fline}\033[K")
|
||||
|
||||
# Glitch — base rate + mic-reactive spikes (scroll zone only)
|
||||
mic_excess = mic_monitor.excess
|
||||
@@ -177,6 +215,14 @@ def stream(items, ntfy_poller, mic_monitor):
|
||||
scr_row = msg_h + gi + 1
|
||||
buf[scroll_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):
|
||||
buf.append(f"\033[{scr_row};{col}H{chunk}")
|
||||
|
||||
sys.stdout.buffer.write("".join(buf).encode())
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user