From ce81f94a9be79188ad7a0b7d560a22e799a259d2 Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Sat, 14 Mar 2026 16:31:11 -0700 Subject: [PATCH 1/5] feat: add a `--firehose` mode with a dynamic bottom display zone and include standard Python ignores in .gitignore. --- .gitignore | 3 ++ mainline.py | 86 +++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f065e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +.mainline_venv/ diff --git a/mainline.py b/mainline.py index 46e37d4..3337443 100755 --- a/mainline.py +++ b/mainline.py @@ -56,6 +56,7 @@ HEADLINE_LIMIT = 1000 FEED_TIMEOUT = 10 MIC_THRESHOLD_DB = 50 # dB above which glitches intensify MODE = 'poetry' if '--poetry' in sys.argv or '-p' in sys.argv else 'news' +FIREHOSE = '--firehose' in sys.argv # Poetry/literature sources — public domain via Project Gutenberg POETRY_SOURCES = { @@ -461,6 +462,7 @@ def fetch_poetry(): # ─── STREAM ─────────────────────────────────────────────── _SCROLL_DUR = 3.75 # seconds per headline +FIREHOSE_H = 6 # firehose zone height (terminal rows) _mic_db = -99.0 # current mic level, written by background thread _mic_stream = None @@ -676,6 +678,51 @@ def _make_block(title, src, ts, w): return content, hc, len(content) - 1 # (rows, color, meta_row_index) +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(GLITCH + KATA)}{RST}" + if random.random() < d else " " + for _ in range(w) + ) + elif r < 0.78: + # Status / program output + sources = FEEDS if 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(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(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}" + + def stream(items): random.shuffle(items) pool = list(items) @@ -687,13 +734,15 @@ def stream(items): sys.stdout.flush() w, h = tw(), th() + fh = FIREHOSE_H if FIREHOSE else 0 + sh = h - fh # scroll zone height GAP = 3 # blank rows between headlines - dt = _SCROLL_DUR / (h + 15) * 2 # 2x slower scroll + dt = _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 = h # canvas-y where next block starts (off-screen bottom) + next_y = sh # canvas-y where next block starts (off-screen bottom) noise_cache = {} def _noise_at(cy): @@ -703,22 +752,26 @@ def stream(items): while queued < HEADLINE_LIMIT or active: w, h = tw(), th() + fh = FIREHOSE_H if FIREHOSE else 0 + sh = h - fh # Enqueue new headlines when room at the bottom - while next_y < cam + h + 10 and queued < HEADLINE_LIMIT: + while next_y < cam + sh + 10 and queued < 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 - # Draw frame - top_zone = max(1, int(h * 0.25)) # 25% fade zone at top (exit) - bot_zone = max(1, int(h * 0.10)) # 10% fade zone at bottom (entry) + # Draw scroll zone + top_zone = max(1, int(sh * 0.25)) + bot_zone = max(1, int(sh * 0.10)) buf = [] - for r in range(h): + for r in range(sh): cy = cam + r - row_fade = min(1.0, min(r / top_zone, (h - 1 - r) / bot_zone)) + top_f = min(1.0, r / top_zone) + bot_f = 1.0 if FIREHOSE else min(1.0, (sh - 1 - r) / bot_zone) + row_fade = min(top_f, bot_f) drawn = False for content, hc, by, midx in active: cr = cy - by @@ -743,13 +796,20 @@ def stream(items): else: buf.append(f"\033[{r+1};1H\033[K") - # Glitch — base rate + mic-reactive spikes + # Draw firehose zone + if 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 = max(0.0, _mic_db - MIC_THRESHOLD_DB) glitch_prob = 0.32 + min(0.9, mic_excess * 0.16) n_hits = 4 + int(mic_excess / 2) - if random.random() < glitch_prob and buf: - for _ in range(min(n_hits, h)): - gi = random.randint(0, len(buf) - 1) + g_limit = sh if FIREHOSE else len(buf) + if random.random() < glitch_prob and g_limit > 0: + for _ in range(min(n_hits, g_limit)): + gi = random.randint(0, g_limit - 1) buf[gi] = f"\033[{gi+1};1H{glitch_bar(w)}" sys.stdout.write("".join(buf)) @@ -833,6 +893,8 @@ def main(): mic_ok = _start_mic() if _HAS_MIC: boot_ln("Microphone", "ACTIVE" if mic_ok else "OFFLINE · check System Settings → Privacy → Microphone", mic_ok) + if FIREHOSE: + boot_ln("Firehose", "ENGAGED", True) time.sleep(0.4) slow_print(" > STREAMING...\n") From 20ebe96ea61edf8bca3ec0441144187d99e64528 Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Sat, 14 Mar 2026 16:38:56 -0700 Subject: [PATCH 2/5] refactor: remove `FIREHOSE` conditional from `bot_f` calculation. --- mainline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainline.py b/mainline.py index 3337443..0ff787a 100755 --- a/mainline.py +++ b/mainline.py @@ -770,7 +770,7 @@ def stream(items): for r in range(sh): cy = cam + r top_f = min(1.0, r / top_zone) - bot_f = 1.0 if FIREHOSE else min(1.0, (sh - 1 - r) / bot_zone) + bot_f = min(1.0, (sh - 1 - r) / bot_zone) row_fade = min(top_f, bot_f) drawn = False for content, hc, by, midx in active: From b69515238cb18cfa23744dd86c1c1a721dd23ddc Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Sat, 14 Mar 2026 16:40:48 -0700 Subject: [PATCH 3/5] Increase firehose zone height and adjust scroll duration multiplier for firehose mode. --- mainline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mainline.py b/mainline.py index 0ff787a..725bad8 100755 --- a/mainline.py +++ b/mainline.py @@ -462,7 +462,7 @@ def fetch_poetry(): # ─── STREAM ─────────────────────────────────────────────── _SCROLL_DUR = 3.75 # seconds per headline -FIREHOSE_H = 6 # firehose zone height (terminal rows) +FIREHOSE_H = 12 # firehose zone height (terminal rows) _mic_db = -99.0 # current mic level, written by background thread _mic_stream = None @@ -737,7 +737,7 @@ def stream(items): fh = FIREHOSE_H if FIREHOSE else 0 sh = h - fh # scroll zone height GAP = 3 # blank rows between headlines - dt = _SCROLL_DUR / (sh + 15) * 2 + dt = _SCROLL_DUR / (sh + 15) * (4 if FIREHOSE else 2) # active blocks: (content_rows, color, canvas_y, meta_idx) active = [] From 8f95ee5df928414619d855854fb917c84f2181be Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Sat, 14 Mar 2026 16:43:59 -0700 Subject: [PATCH 4/5] feat: Implement caching for fetched items to improve startup performance and ignore cache files. --- .gitignore | 1 + mainline.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3f065e3..bd7c683 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__/ *.pyc .mainline_venv/ +.mainline_cache_*.json diff --git a/mainline.py b/mainline.py index 725bad8..d7f6456 100755 --- a/mainline.py +++ b/mainline.py @@ -460,6 +460,35 @@ def fetch_poetry(): return items, linked, failed +# ─── CACHE ──────────────────────────────────────────────── +_CACHE_DIR = pathlib.Path(__file__).resolve().parent + + +def _cache_path(): + return _CACHE_DIR / f".mainline_cache_{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 + + # ─── STREAM ─────────────────────────────────────────────── _SCROLL_DUR = 3.75 # seconds per headline FIREHOSE_H = 12 # firehose zone height (terminal rows) @@ -868,7 +897,11 @@ def main(): print() time.sleep(0.4) - if MODE == 'poetry': + 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 MODE == 'poetry': slow_print(" > INITIALIZING LITERARY CORPUS...\n") time.sleep(0.2) print() @@ -876,6 +909,7 @@ def main(): 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) @@ -884,6 +918,7 @@ def main(): 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}") From 69081344d57376b85af65a8943a150a951962ee0 Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Sat, 14 Mar 2026 16:54:33 -0700 Subject: [PATCH 5/5] refactor: Implement fixed frame rate and precise timing for the scrolling animation loop. --- mainline.py | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/mainline.py b/mainline.py index d7f6456..7d34886 100755 --- a/mainline.py +++ b/mainline.py @@ -491,6 +491,7 @@ def _save_cache(items): # ─── STREAM ─────────────────────────────────────────────── _SCROLL_DUR = 3.75 # seconds per headline +_FRAME_DT = 0.05 # 50ms base frame rate (20 FPS) FIREHOSE_H = 12 # firehose zone height (terminal rows) _mic_db = -99.0 # current mic level, written by background thread _mic_stream = None @@ -766,13 +767,14 @@ def stream(items): fh = FIREHOSE_H if FIREHOSE else 0 sh = h - fh # scroll zone height GAP = 3 # blank rows between headlines - dt = _SCROLL_DUR / (sh + 15) * (4 if FIREHOSE else 2) + scroll_interval = _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: @@ -780,17 +782,31 @@ def stream(items): return noise_cache[cy] while queued < HEADLINE_LIMIT or active: + t0 = time.monotonic() w, h = tw(), th() fh = FIREHOSE_H if FIREHOSE else 0 sh = h - fh - # Enqueue new headlines when room at the bottom - while next_y < cam + sh + 10 and queued < 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 + # Advance scroll on schedule + scroll_accum += _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 < 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 top_zone = max(1, int(sh * 0.25)) @@ -841,19 +857,12 @@ def stream(items): gi = random.randint(0, g_limit - 1) buf[gi] = f"\033[{gi+1};1H{glitch_bar(w)}" - sys.stdout.write("".join(buf)) + sys.stdout.buffer.write("".join(buf).encode()) sys.stdout.flush() - time.sleep(dt) - # Advance viewport - cam += 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] + # Precise frame timing + elapsed = time.monotonic() - t0 + time.sleep(max(0, _FRAME_DT - elapsed)) sys.stdout.write(CLR) sys.stdout.flush()