8 Commits

9 changed files with 723 additions and 1051 deletions

114
engine/app.py Normal file
View File

@@ -0,0 +1,114 @@
"""
Application orchestrator — boot sequence, signal handling, main loop wiring.
"""
import sys
import time
import signal
import atexit
from engine import config
from engine.terminal import (
RST, G_HI, G_MID, G_DIM, W_DIM, W_GHOST, CLR, CURSOR_OFF, CURSOR_ON, tw,
slow_print, boot_ln,
)
from engine.fetch import fetch_all, fetch_poetry, load_cache, save_cache
from engine.ntfy import NtfyPoller
from engine.mic import MicMonitor
from engine.scroll import stream
TITLE = [
" ███╗ ███╗ █████╗ ██╗███╗ ██╗██╗ ██╗███╗ ██╗███████╗",
" ████╗ ████║██╔══██╗██║████╗ ██║██║ ██║████╗ ██║██╔════╝",
" ██╔████╔██║███████║██║██╔██╗ ██║██║ ██║██╔██╗ ██║█████╗ ",
" ██║╚██╔╝██║██╔══██║██║██║╚██╗██║██║ ██║██║╚██╗██║██╔══╝ ",
" ██║ ╚═╝ ██║██║ ██║██║██║ ╚████║███████╗██║██║ ╚████║███████╗",
" ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝",
]
def main():
atexit.register(lambda: print(CURSOR_ON, end="", flush=True))
def handle_sigint(*_):
print(f"\n\n {G_DIM}> SIGNAL LOST{RST}")
print(f" {W_GHOST}> connection terminated{RST}\n")
sys.exit(0)
signal.signal(signal.SIGINT, handle_sigint)
w = tw()
print(CLR, end="")
print(CURSOR_OFF, end="")
print()
time.sleep(0.4)
for ln in TITLE:
print(f"{G_HI}{ln}{RST}")
time.sleep(0.07)
print()
_subtitle = "literary consciousness stream" if config.MODE == 'poetry' else "digital consciousness stream"
print(f" {W_DIM}v0.1 · {_subtitle}{RST}")
print(f" {W_GHOST}{'' * (w - 4)}{RST}")
print()
time.sleep(0.4)
cached = load_cache() if '--refresh' not in sys.argv else None
if cached:
items = cached
boot_ln("Cache", f"LOADED [{len(items)} SIGNALS]", True)
elif config.MODE == 'poetry':
slow_print(" > INITIALIZING LITERARY CORPUS...\n")
time.sleep(0.2)
print()
items, linked, failed = fetch_poetry()
print()
print(f" {G_DIM}>{RST} {G_MID}{linked} TEXTS LOADED{RST} {W_GHOST}· {failed} DARK{RST}")
print(f" {G_DIM}>{RST} {G_MID}{len(items)} STANZAS ACQUIRED{RST}")
save_cache(items)
else:
slow_print(" > INITIALIZING FEED ARRAY...\n")
time.sleep(0.2)
print()
items, linked, failed = fetch_all()
print()
print(f" {G_DIM}>{RST} {G_MID}{linked} SOURCES LINKED{RST} {W_GHOST}· {failed} DARK{RST}")
print(f" {G_DIM}>{RST} {G_MID}{len(items)} SIGNALS ACQUIRED{RST}")
save_cache(items)
if not items:
print(f"\n {W_DIM}> NO SIGNAL — check network{RST}")
sys.exit(1)
print()
mic = MicMonitor(threshold_db=config.MIC_THRESHOLD_DB)
mic_ok = mic.start()
if mic.available:
boot_ln("Microphone", "ACTIVE" if mic_ok else "OFFLINE · check System Settings → Privacy → Microphone", bool(mic_ok))
ntfy = NtfyPoller(
config.NTFY_TOPIC,
poll_interval=config.NTFY_POLL_INTERVAL,
display_secs=config.MESSAGE_DISPLAY_SECS,
)
ntfy_ok = ntfy.start()
boot_ln("ntfy", "LISTENING" if ntfy_ok else "OFFLINE", ntfy_ok)
if config.FIREHOSE:
boot_ln("Firehose", "ENGAGED", True)
time.sleep(0.4)
slow_print(" > STREAMING...\n")
time.sleep(0.2)
print(f" {W_GHOST}{'' * (w - 4)}{RST}")
print()
time.sleep(0.4)
stream(items, ntfy, mic)
print()
print(f" {W_GHOST}{'' * (tw() - 4)}{RST}")
print(f" {G_DIM}> {config.HEADLINE_LIMIT} SIGNALS PROCESSED{RST}")
print(f" {W_GHOST}> end of stream{RST}")
print()

View File

@@ -21,10 +21,14 @@ FONT_PATH = "/Users/genejohnson/Documents/CS Bishop Drawn/CSBishopDrawn-Italic.o
FONT_SZ = 60
RENDER_H = 8 # terminal rows per rendered text line
# ─── FONT RENDERING (ADVANCED) ────────────────────────────
SSAA = 4 # super-sampling factor: render at SSAA× then downsample
# ─── SCROLL / FRAME ──────────────────────────────────────
SCROLL_DUR = 3.75 # seconds per headline
SCROLL_DUR = 5.625 # seconds per headline (2/3 original speed)
FRAME_DT = 0.05 # 50ms base frame rate (20 FPS)
FIREHOSE_H = 12 # firehose zone height (terminal rows)
GRAD_SPEED = 0.08 # gradient traversal speed (cycles/sec, ~12s full sweep)
# ─── GLYPHS ───────────────────────────────────────────────
GLITCH = "░▒▓█▌▐╌╍╎╏┃┆┇┊┋"

133
engine/effects.py Normal file
View File

@@ -0,0 +1,133 @@
"""
Visual effects: noise, glitch, fade, ANSI-aware truncation, firehose, headline pool.
Depends on: config, terminal, sources.
"""
import random
from datetime import datetime
from engine import config
from engine.terminal import RST, DIM, G_LO, G_DIM, W_GHOST, C_DIM
from engine.sources import FEEDS, POETRY_SOURCES
def noise(w):
d = random.choice([0.15, 0.25, 0.35, 0.12])
return "".join(
f"{random.choice([G_LO, G_DIM, C_DIM, W_GHOST])}"
f"{random.choice(config.GLITCH + config.KATA)}{RST}"
if random.random() < d
else " "
for _ in range(w)
)
def glitch_bar(w):
c = random.choice(["", "", "", ""])
n = random.randint(3, w // 2)
o = random.randint(0, w - n)
return " " * o + f"{G_LO}{DIM}" + c * n + RST
def fade_line(s, fade):
"""Dissolve a rendered line by probabilistically dropping characters."""
if fade >= 1.0:
return s
if fade <= 0.0:
return ''
result = []
i = 0
while i < len(s):
if s[i] == '\033' and i + 1 < len(s) and s[i + 1] == '[':
j = i + 2
while j < len(s) and not s[j].isalpha():
j += 1
result.append(s[i:j + 1])
i = j + 1
elif s[i] == ' ':
result.append(' ')
i += 1
else:
result.append(s[i] if random.random() < fade else ' ')
i += 1
return ''.join(result)
def vis_trunc(s, w):
"""Truncate string to visual width w, skipping ANSI escape codes."""
result = []
vw = 0
i = 0
while i < len(s):
if vw >= w:
break
if s[i] == '\033' and i + 1 < len(s) and s[i + 1] == '[':
j = i + 2
while j < len(s) and not s[j].isalpha():
j += 1
result.append(s[i:j + 1])
i = j + 1
else:
result.append(s[i])
vw += 1
i += 1
return ''.join(result)
def next_headline(pool, items, seen):
"""Pull the next unique headline from pool, refilling as needed."""
while True:
if not pool:
pool.extend(items)
random.shuffle(pool)
seen.clear()
title, src, ts = pool.pop()
sig = title.lower().strip()
if sig not in seen:
seen.add(sig)
return title, src, ts
def firehose_line(items, w):
"""Generate one line of rapidly cycling firehose content."""
r = random.random()
if r < 0.35:
# Raw headline text
title, src, ts = random.choice(items)
text = title[:w - 1]
color = random.choice([G_LO, G_DIM, W_GHOST, C_DIM])
return f"{color}{text}{RST}"
elif r < 0.55:
# Dense glitch noise
d = random.choice([0.45, 0.55, 0.65, 0.75])
return "".join(
f"{random.choice([G_LO, G_DIM, C_DIM, W_GHOST])}"
f"{random.choice(config.GLITCH + config.KATA)}{RST}"
if random.random() < d else " "
for _ in range(w)
)
elif r < 0.78:
# Status / program output
sources = FEEDS if config.MODE == 'news' else POETRY_SOURCES
src = random.choice(list(sources.keys()))
msgs = [
f" SIGNAL :: {src} :: {datetime.now().strftime('%H:%M:%S.%f')[:-3]}",
f" ░░ FEED ACTIVE :: {src}",
f" >> DECODE 0x{random.randint(0x1000, 0xFFFF):04X} :: {src[:24]}",
f" ▒▒ ACQUIRE :: {random.choice(['TCP', 'UDP', 'RSS', 'ATOM', 'XML'])} :: {src}",
f" {''.join(random.choice(config.KATA) for _ in range(3))} STRM "
f"{random.randint(0, 255):02X}:{random.randint(0, 255):02X}",
]
text = random.choice(msgs)[:w - 1]
color = random.choice([G_LO, G_DIM, W_GHOST])
return f"{color}{text}{RST}"
else:
# Headline fragment with glitch prefix
title, _, _ = random.choice(items)
start = random.randint(0, max(0, len(title) - 20))
frag = title[start:start + random.randint(10, 35)]
pad = random.randint(0, max(0, w - len(frag) - 8))
gp = ''.join(random.choice(config.GLITCH) for _ in range(random.randint(1, 3)))
text = (' ' * pad + gp + ' ' + frag)[:w - 1]
color = random.choice([G_LO, C_DIM, W_GHOST])
return f"{color}{text}{RST}"

133
engine/fetch.py Normal file
View File

@@ -0,0 +1,133 @@
"""
RSS feed fetching, Project Gutenberg parsing, and headline caching.
Depends on: config, sources, filter, terminal.
"""
import re
import json
import pathlib
import urllib.request
from datetime import datetime
import feedparser
from engine import config
from engine.sources import FEEDS, POETRY_SOURCES
from engine.filter import strip_tags, skip
from engine.terminal import boot_ln
# ─── SINGLE FEED ──────────────────────────────────────────
def fetch_feed(url):
try:
req = urllib.request.Request(url, headers={"User-Agent": "mainline/0.1"})
resp = urllib.request.urlopen(req, timeout=config.FEED_TIMEOUT)
return feedparser.parse(resp.read())
except Exception:
return None
# ─── ALL RSS FEEDS ────────────────────────────────────────
def fetch_all():
items = []
linked = failed = 0
for src, url in FEEDS.items():
feed = fetch_feed(url)
if feed is None or (feed.bozo and not feed.entries):
boot_ln(src, "DARK", False)
failed += 1
continue
n = 0
for e in feed.entries:
t = strip_tags(e.get("title", ""))
if not t or skip(t):
continue
pub = e.get("published_parsed") or e.get("updated_parsed")
try:
ts = datetime(*pub[:6]).strftime("%H:%M") if pub else "——:——"
except Exception:
ts = "——:——"
items.append((t, src, ts))
n += 1
if n:
boot_ln(src, f"LINKED [{n}]", True)
linked += 1
else:
boot_ln(src, "EMPTY", False)
failed += 1
return items, linked, failed
# ─── PROJECT GUTENBERG ────────────────────────────────────
def _fetch_gutenberg(url, label):
"""Download and parse stanzas/passages from a Project Gutenberg text."""
try:
req = urllib.request.Request(url, headers={"User-Agent": "mainline/0.1"})
resp = urllib.request.urlopen(req, timeout=15)
text = resp.read().decode('utf-8', errors='replace').replace('\r\n', '\n').replace('\r', '\n')
# Strip PG boilerplate
m = re.search(r'\*\*\*\s*START OF[^\n]*\n', text)
if m:
text = text[m.end():]
m = re.search(r'\*\*\*\s*END OF', text)
if m:
text = text[:m.start()]
# Split on blank lines into stanzas/passages
blocks = re.split(r'\n{2,}', text.strip())
items = []
for blk in blocks:
blk = ' '.join(blk.split()) # flatten to one line
if len(blk) < 20 or len(blk) > 280:
continue
if blk.isupper(): # skip all-caps headers
continue
if re.match(r'^[IVXLCDM]+\.?\s*$', blk): # roman numerals
continue
items.append((blk, label, ''))
return items
except Exception:
return []
def fetch_poetry():
"""Fetch all poetry/literature sources."""
items = []
linked = failed = 0
for label, url in POETRY_SOURCES.items():
stanzas = _fetch_gutenberg(url, label)
if stanzas:
boot_ln(label, f"LOADED [{len(stanzas)}]", True)
items.extend(stanzas)
linked += 1
else:
boot_ln(label, "DARK", False)
failed += 1
return items, linked, failed
# ─── CACHE ────────────────────────────────────────────────
_CACHE_DIR = pathlib.Path(__file__).resolve().parent.parent
def _cache_path():
return _CACHE_DIR / f".mainline_cache_{config.MODE}.json"
def load_cache():
"""Load cached items from disk if available."""
p = _cache_path()
if not p.exists():
return None
try:
data = json.loads(p.read_text())
items = [tuple(i) for i in data["items"]]
return items if items else None
except Exception:
return None
def save_cache(items):
"""Save fetched items to disk for fast subsequent runs."""
try:
_cache_path().write_text(json.dumps({"items": items}))
except Exception:
pass

62
engine/mic.py Normal file
View File

@@ -0,0 +1,62 @@
"""
Microphone input monitor — standalone, no internal dependencies.
Gracefully degrades if sounddevice/numpy are unavailable.
"""
import atexit
try:
import sounddevice as _sd
import numpy as _np
_HAS_MIC = True
except Exception:
_HAS_MIC = False
class MicMonitor:
"""Background mic stream that exposes current RMS dB level."""
def __init__(self, threshold_db=50):
self.threshold_db = threshold_db
self._db = -99.0
self._stream = None
@property
def available(self):
"""True if sounddevice is importable."""
return _HAS_MIC
@property
def db(self):
"""Current RMS dB level."""
return self._db
@property
def excess(self):
"""dB above threshold (clamped to 0)."""
return max(0.0, self._db - self.threshold_db)
def start(self):
"""Start background mic stream. Returns True on success, False/None otherwise."""
if not _HAS_MIC:
return None
def _cb(indata, frames, t, status):
rms = float(_np.sqrt(_np.mean(indata ** 2)))
self._db = 20 * _np.log10(rms) if rms > 0 else -99.0
try:
self._stream = _sd.InputStream(
callback=_cb, channels=1, samplerate=44100, blocksize=2048)
self._stream.start()
atexit.register(self.stop)
return True
except Exception:
return False
def stop(self):
"""Stop the mic stream if running."""
if self._stream:
try:
self._stream.stop()
except Exception:
pass
self._stream = None

75
engine/ntfy.py Normal file
View File

@@ -0,0 +1,75 @@
"""
ntfy.sh message poller — standalone, zero internal dependencies.
Reusable by any visualizer:
from engine.ntfy import NtfyPoller
poller = NtfyPoller("https://ntfy.sh/my_topic/json?since=20s&poll=1")
poller.start()
# in render loop:
msg = poller.get_active_message()
if msg:
title, body, ts = msg
render_my_message(title, body)
"""
import json
import time
import threading
import urllib.request
class NtfyPoller:
"""Background poller for ntfy.sh topics."""
def __init__(self, topic_url, poll_interval=15, display_secs=30):
self.topic_url = topic_url
self.poll_interval = poll_interval
self.display_secs = display_secs
self._message = None # (title, body, monotonic_timestamp) or None
self._lock = threading.Lock()
def start(self):
"""Start background polling thread. Returns True."""
t = threading.Thread(target=self._poll_loop, daemon=True)
t.start()
return True
def get_active_message(self):
"""Return (title, body, timestamp) if a message is active and not expired, else None."""
with self._lock:
if self._message is None:
return None
title, body, ts = self._message
if time.monotonic() - ts < self.display_secs:
return self._message
self._message = None
return None
def dismiss(self):
"""Manually dismiss the current message."""
with self._lock:
self._message = None
def _poll_loop(self):
while True:
try:
req = urllib.request.Request(
self.topic_url, headers={"User-Agent": "mainline/0.1"})
resp = urllib.request.urlopen(req, timeout=10)
for line in resp.read().decode('utf-8', errors='replace').strip().split('\n'):
if not line.strip():
continue
try:
data = json.loads(line)
except json.JSONDecodeError:
continue
if data.get("event") == "message":
with self._lock:
self._message = (
data.get("title", ""),
data.get("message", ""),
time.monotonic(),
)
except Exception:
pass
time.sleep(self.poll_interval)

View File

@@ -10,7 +10,7 @@ import random
from PIL import Image, ImageDraw, ImageFont
from engine import config
from engine.terminal import RST, W_COOL
from engine.terminal import RST
from engine.sources import SCRIPT_FONTS, SOURCE_LANGS, NO_UPPER
from engine.translate import detect_location_language, translate_headline
@@ -71,8 +71,11 @@ def render_line(text, fnt=None):
draw = ImageDraw.Draw(img)
draw.text((-bbox[0] + pad, -bbox[1] + pad), text, fill=255, font=fnt)
pix_h = config.RENDER_H * 2
scale = pix_h / max(img_h, 1)
new_w = max(1, int(img_w * scale))
hi_h = pix_h * config.SSAA
scale = hi_h / max(img_h, 1)
new_w_hi = max(1, int(img_w * scale))
img = img.resize((new_w_hi, hi_h), Image.Resampling.LANCZOS)
new_w = max(1, int(new_w_hi / config.SSAA))
img = img.resize((new_w, pix_h), Image.Resampling.LANCZOS)
data = img.tobytes()
thr = 80
@@ -129,8 +132,8 @@ def big_wrap(text, max_w, fnt=None):
return out
def lr_gradient(rows):
"""Color each non-space block character with a left-to-right gradient."""
def lr_gradient(rows, offset=0.0):
"""Color each non-space block character with a shifting left-to-right gradient."""
n = len(GRAD_COLS)
max_x = max((len(r.rstrip()) for r in rows if r.strip()), default=1)
out = []
@@ -143,7 +146,8 @@ def lr_gradient(rows):
if ch == ' ':
buf.append(' ')
else:
idx = min(round(x / max(max_x - 1, 1) * (n - 1)), n - 1)
shifted = (x / max(max_x - 1, 1) + offset) % 1.0
idx = min(round(shifted * (n - 1)), n - 1)
buf.append(f"{GRAD_COLS[idx]}{ch}\033[0m")
out.append("".join(buf))
return out
@@ -165,7 +169,6 @@ def make_block(title, src, ts, w):
("\u201d",'"'), ("\u2013","-"), ("\u2014","-")]:
title_up = title_up.replace(old, new)
big_rows = big_wrap(title_up, w - 4, lang_font)
big_rows = lr_gradient(big_rows)
hc = random.choice([
"\033[38;5;46m", # matrix green
"\033[38;5;34m", # dark green

188
engine/scroll.py Normal file
View File

@@ -0,0 +1,188 @@
"""
Scroll engine — the main frame loop with headline rendering and message display.
Depends on: config, terminal, render, effects, ntfy, mic.
"""
import re
import sys
import time
import random
from datetime import datetime
from engine import config
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
def stream(items, ntfy_poller, mic_monitor):
"""Main rendering loop. Scrolls headlines, shows ntfy messages, applies effects."""
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
sh = h - fh # scroll zone height
GAP = 3 # blank rows between headlines
scroll_interval = config.SCROLL_DUR / (sh + 15) * 2
# active blocks: (content_rows, color, canvas_y, meta_idx)
active = []
cam = 0 # viewport top in virtual canvas coords
next_y = sh # canvas-y where next block starts (off-screen bottom)
noise_cache = {}
scroll_accum = 0.0
def _noise_at(cy):
if cy not in noise_cache:
noise_cache[cy] = noise(w) if random.random() < 0.15 else None
return noise_cache[cy]
# Message color: bright cyan/white — distinct from headline greens
MSG_META = "\033[38;5;245m" # cool grey
MSG_BORDER = "\033[2;38;5;37m" # dim teal
_msg_cache = (None, None) # (cache_key, rendered_rows)
while queued < config.HEADLINE_LIMIT or active:
t0 = time.monotonic()
w, h = tw(), th()
fh = config.FIREHOSE_H if config.FIREHOSE else 0
sh = h - fh
# ── Check for ntfy message ────────────────────────
msg_h = 0 # rows consumed by message zone at top
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 ──
display_text = m_body or m_title or "(empty)"
display_text = re.sub(r"\s+", " ", display_text.upper())
cache_key = (display_text, w)
if _msg_cache[0] != cache_key:
msg_rows = big_wrap(display_text, w - 4)
_msg_cache = (cache_key, msg_rows)
else:
msg_rows = _msg_cache[1]
msg_rows = lr_gradient(msg_rows, (time.monotonic() * config.GRAD_SPEED) % 1.0)
# Layout: rendered text + meta + border
elapsed_s = int(time.monotonic() - m_ts)
remaining = max(0, config.MESSAGE_DISPLAY_SECS - elapsed_s)
ts_str = datetime.now().strftime("%H:%M:%S")
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")
row_idx += 1
# Meta line: title (if distinct) + source + countdown
meta_parts = []
if m_title and m_title != m_body:
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")
row_idx += 1
# Border — constant boundary between message and scroll
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
# Effective scroll zone: below message, above firehose
scroll_h = sh - msg_h
# ── Scroll: headline rendering (always runs) ──────
# Advance scroll on schedule
scroll_accum += config.FRAME_DT
while scroll_accum >= scroll_interval:
scroll_accum -= scroll_interval
cam += 1
# Enqueue new headlines when room at the bottom
while next_y < cam + sh + 10 and queued < config.HEADLINE_LIMIT:
t, src, ts = next_headline(pool, items, seen)
content, hc, midx = make_block(t, src, ts, w)
active.append((content, hc, next_y, midx))
next_y += len(content) + GAP
queued += 1
# Prune off-screen blocks and stale noise
active = [(c, hc, by, mi) for c, hc, by, mi in active
if by + len(c) > cam]
for k in list(noise_cache):
if k < cam:
del noise_cache[k]
# Draw scroll zone (below message zone, above firehose)
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
scroll_buf_start = len(buf) # track where scroll rows start in buf
for r in range(scroll_h):
scr_row = msg_h + r + 1 # 1-indexed ANSI screen row
cy = cam + r
top_f = min(1.0, r / top_zone) if top_zone > 0 else 1.0
bot_f = min(1.0, (scroll_h - 1 - r) / bot_zone) if bot_zone > 0 else 1.0
row_fade = min(top_f, bot_f)
drawn = False
for content, hc, by, midx in active:
cr = cy - by
if 0 <= cr < len(content):
raw = content[cr]
if cr != midx:
colored = lr_gradient([raw], grad_offset)[0]
else:
colored = raw
ln = vis_trunc(colored, w)
if row_fade < 1.0:
ln = fade_line(ln, row_fade)
if cr == midx:
buf.append(f"\033[{scr_row};1H{W_COOL}{ln}{RST}\033[K")
elif ln.strip():
buf.append(f"\033[{scr_row};1H{ln}{RST}\033[K")
else:
buf.append(f"\033[{scr_row};1H\033[K")
drawn = True
break
if not drawn:
n = _noise_at(cy)
if row_fade < 1.0 and n:
n = fade_line(n, row_fade)
if n:
buf.append(f"\033[{scr_row};1H{n}")
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
glitch_prob = 0.32 + min(0.9, mic_excess * 0.16)
n_hits = 4 + int(mic_excess / 2)
scroll_buf_len = len(buf) - scroll_buf_start
if random.random() < glitch_prob and scroll_buf_len > 0:
for _ in range(min(n_hits, scroll_buf_len)):
gi = random.randint(0, scroll_buf_len - 1)
scr_row = msg_h + gi + 1
buf[scroll_buf_start + gi] = f"\033[{scr_row};1H{glitch_bar(w)}"
sys.stdout.buffer.write("".join(buf).encode())
sys.stdout.flush()
# Precise frame timing
elapsed = time.monotonic() - t0
time.sleep(max(0, config.FRAME_DT - elapsed))
sys.stdout.write(CLR)
sys.stdout.flush()

File diff suppressed because it is too large Load Diff