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