Gene Johnson 733b4dd9ec style: apply ruff lint fixes and formatting to figment modules
Fixes: unused imports, import sorting, unused variable, overly broad
exception type in test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 11:09:46 -07:00
2026-03-19 00:32:52 -07:00

MAINLINE

Digital consciousness stream. Matrix aesthetic · THX-1138 hue.

A full-screen terminal news ticker that renders live global headlines in large OTF-font block characters with selectable color gradients (Verdant Green, Molten Orange, or Violet Purple). Headlines auto-translate into the native script of their subject region. Ambient mic input warps the glitch rate in real time. A --poetry mode replaces the feed with public-domain literary passages. Live messages can be pushed to the display over ntfy.sh.


Using

Run

python3 mainline.py                      # news stream
python3 mainline.py --poetry             # literary consciousness mode
python3 mainline.py -p                   # same
python3 mainline.py --firehose           # dense rapid-fire headline mode
python3 mainline.py --refresh            # force re-fetch (bypass cache)
python3 mainline.py --no-font-picker     # skip interactive font picker
python3 mainline.py --font-file path.otf # use a specific font file
python3 mainline.py --font-dir ~/fonts   # scan a different font folder
python3 mainline.py --font-index 1       # select face index within a collection

Or with uv:

uv run mainline.py

First run bootstraps a local .mainline_venv/ and installs deps (feedparser, Pillow, sounddevice, numpy). Subsequent runs start immediately, loading from cache. With uv, run uv sync or uv sync --all-extras (includes mic support) instead.

Config

All constants live in engine/config.py:

Constant Default What it does
HEADLINE_LIMIT 1000 Total headlines per session
FEED_TIMEOUT 10 Per-feed HTTP timeout (seconds)
MIC_THRESHOLD_DB 50 dB floor above which glitches spike
FONT_DIR fonts/ Folder scanned for .otf, .ttf, .ttc files
FONT_PATH first file in FONT_DIR Active display font (overridden by picker or --font-file)
FONT_INDEX 0 Face index within a font collection file
FONT_PICKER True Show interactive font picker at boot (--no-font-picker to skip)
FONT_SZ 60 Font render size (affects block density)
RENDER_H 8 Terminal rows per headline line
SSAA 4 Super-sampling factor (render at 4× then downsample)
SCROLL_DUR 5.625 Seconds per headline
FRAME_DT 0.05 Frame interval in seconds (20 FPS)
GRAD_SPEED 0.08 Gradient sweep speed (cycles/sec, ~12s full sweep)
FIREHOSE_H 12 Firehose zone height (terminal rows)
NTFY_TOPIC klubhaus URL ntfy.sh JSON stream endpoint
NTFY_RECONNECT_DELAY 5 Seconds before reconnecting after a dropped SSE stream
MESSAGE_DISPLAY_SECS 30 How long an ntfy message holds the screen

Feeds

~25 sources across four categories: Science & Technology, Economics & Business, World & Politics, Culture & Ideas. Add or swap feeds in engine/sources.pyFEEDS.

Poetry mode pulls from Project Gutenberg: Whitman, Dickinson, Thoreau, Emerson. Sources are in engine/sources.pyPOETRY_SOURCES.

Fonts

A fonts/ directory is bundled with demo faces (AgorTechnoDemo, AlphatronDemo, CSBishopDrawn, CubaTechnologyDemo, CyberformDemo, KATA, Microbots, ModernSpaceDemo, Neoform, Pixel Sparta, RaceHugoDemo, Resond, Robocops, Synthetix, Xeonic, and others). On startup, an interactive picker lists all discovered faces with a live half-block preview rendered at your configured size.

Navigation: / or j/k to move, Enter or q to select. The selected face persists for that session.

To add your own fonts, drop .otf, .ttf, or .ttc files into fonts/ (or point --font-dir at any other folder). Font collections (.ttc, multi-face .otf) are enumerated face-by-face.

Color Schemes

Mainline supports three color themes for the scrolling gradient: Verdant Green, Molten Orange, and Violet Purple. Each theme uses a precise color-opposite palette for ntfy message queue rendering (magenta, blue, and yellow respectively).

On startup, an interactive picker presents all available color schemes:

  [1] Verdant Green    (white-hot → deep green)
  [2] Molten Orange    (white-hot → deep orange)
  [3] Violet Purple    (white-hot → deep purple)

Navigation: / or j/k to move, Enter or q to select. The selection applies only to the current session; you'll pick a fresh theme each run.

Note: The boot UI (title, status lines, font picker menu) uses a hardcoded green accent color for visual continuity. Only the scrolling headlines and incoming messages render in the selected theme gradient.

ntfy.sh

Mainline polls a configurable ntfy.sh topic in the background. When a message arrives, the scroll pauses and the message renders full-screen for MESSAGE_DISPLAY_SECS seconds, then the stream resumes.

To push a message:

curl -d "Body text" -H "Title: Alert title" https://ntfy.sh/your_topic

Update NTFY_TOPIC in engine/config.py to point at your own topic.


Internals

How it works

  • On launch, the font picker scans fonts/ and presents a live-rendered TUI for face selection; --no-font-picker skips directly to stream
  • Feeds are fetched and filtered on startup (sports and vapid content stripped); results are cached to .mainline_cache_news.json / .mainline_cache_poetry.json for fast restarts
  • Headlines are rasterized via Pillow with 4× SSAA into half-block characters (▀▄█ ) at the configured font size
  • The ticker uses a sweeping white-hot → deep green gradient; ntfy messages use a complementary white-hot → magenta/maroon gradient to distinguish them visually
  • Subject-region detection runs a regex pass on each headline; matches trigger a Google Translate call and font swap to the appropriate script (CJK, Arabic, Devanagari, etc.) using macOS system fonts
  • The mic stream runs in a background thread, feeding RMS dB into the glitch probability calculation each frame
  • The viewport scrolls through a virtual canvas of pre-rendered blocks; fade zones at top and bottom dissolve characters probabilistically
  • An ntfy.sh SSE stream runs in a background thread; incoming messages interrupt the scroll and render full-screen until dismissed or expired

Architecture

mainline.py is a thin entrypoint (venv bootstrap → engine.app.main()). All logic lives in the engine/ package:

engine/
  __init__.py       package marker
  app.py            main(), font picker TUI, boot sequence, signal handler
  config.py         constants, CLI flags, glyph tables
  sources.py        FEEDS, POETRY_SOURCES, language/script maps
  terminal.py       ANSI codes, tw/th, type_out, boot_ln
  filter.py         HTML stripping, content filter
  translate.py      Google Translate wrapper + region detection
  render.py         OTF → half-block pipeline (SSAA, gradient)
  effects.py        noise, glitch_bar, fade, firehose
  fetch.py          RSS/Gutenberg fetching + cache load/save
  ntfy.py           NtfyPoller — standalone, zero internal deps
  mic.py            MicMonitor — standalone, graceful fallback
  scroll.py         stream() frame loop + message rendering
  viewport.py       terminal dimension tracking (tw/th)
  frame.py          scroll step calculation, timing
  layers.py         ticker zone, firehose, message overlay rendering
  eventbus.py       thread-safe event publishing for decoupled communication
  events.py         event types and definitions
  controller.py     coordinates ntfy/mic monitoring and event publishing
  emitters.py       background emitters for ntfy and mic
  types.py          type definitions and dataclasses

ntfy.py and mic.py have zero internal dependencies and can be imported by any other visualizer.


Extending

ntfy.py and mic.py are fully standalone and designed to be reused by any terminal visualizer. engine.render is the importable rendering pipeline for non-terminal targets.

NtfyPoller

from engine.ntfy import NtfyPoller

poller = NtfyPoller("https://ntfy.sh/my_topic/json")
poller.start()

# in your render loop:
msg = poller.get_active_message()  # → (title, body, timestamp) or None
if msg:
    title, body, ts = msg
    render_my_message(title, body)  # visualizer-specific

Dependencies: urllib.request, json, threading, time — stdlib only. The since= parameter is managed automatically on reconnect.

MicMonitor

from engine.mic import MicMonitor

mic = MicMonitor(threshold_db=50)
result = mic.start()   # None = sounddevice unavailable; False = stream failed; True = ok
if result:
    excess = mic.excess    # dB above threshold, clamped to 0
    db = mic.db            # raw RMS dB level

Dependencies: sounddevice, numpy — both optional; degrades gracefully if unavailable.

Render pipeline

engine.render exposes the OTF → raster pipeline independently of the terminal scroll loop. The planned serve.py extension will import it directly to pre-render headlines as 1-bit bitmaps for an ESP32 thin client:

# planned — serve.py does not yet exist
from engine.render import render_line, big_wrap
from engine.fetch import fetch_all

headlines = fetch_all()
for h in headlines:
    rows = big_wrap(h.text, font, width=800)  # list of half-block rows
    # threshold to 1-bit, pack bytes, serve over HTTP

See Mainline Renderer + ntfy Message Queue for ESP32.md for the full server + thin client architecture.


Development

Setup

Requires Python 3.10+ and uv.

uv sync                              # minimal (no mic)
uv sync --all-extras                 # with mic support (sounddevice + numpy)
uv sync --all-extras --group dev     # full dev environment

Tasks

With mise:

mise run test            # run test suite
mise run test-cov        # run with coverage report
mise run lint            # ruff check
mise run lint-fix        # ruff check --fix
mise run format          # ruff format
mise run run             # uv run mainline.py
mise run run-poetry      # uv run mainline.py --poetry
mise run run-firehose    # uv run mainline.py --firehose

Testing

Tests live in tests/ and cover config, filter, mic, ntfy, sources, and terminal.

uv run pytest
uv run pytest --cov=engine --cov-report=term-missing

Linting

uv run ruff check engine/ mainline.py
uv run ruff format engine/ mainline.py

Pre-commit hooks run lint automatically via hk.


Roadmap

Performance

  • Concurrent feed fetching — startup currently blocks sequentially on ~25 HTTP requests; concurrent.futures.ThreadPoolExecutor would cut load time to the slowest single feed
  • Background refresh — re-fetch feeds in a daemon thread so a long session stays current without restart
  • Translation pre-fetch — run translate calls concurrently during the boot sequence rather than on first render

Graphics

  • Matrix rain underlay — katakana column rain rendered at low opacity beneath the scrolling blocks as a background layer
  • CRT simulation — subtle dim scanlines every N rows, occasional brightness ripple across the full screen
  • Sixel / iTerm2 inline images — bypass half-blocks entirely and stream actual bitmap frames for true resolution; would require a capable terminal
  • Parallax secondary column — a second, dimmer, faster-scrolling stream of ambient text at reduced opacity on one side

Cyberpunk Vibes

  • Keyword watch list — highlight or strobe any headline matching tracked terms (names, topics, tickers)
  • Breaking interrupt — full-screen flash + synthesized blip when a high-priority keyword hits
  • Live data overlay — secondary ticker strip at screen edge: BTC price, ISS position, geomagnetic index
  • Theme switcher--amber (phosphor), --ice (electric cyan), --red (alert state) palette modes via CLI flag
  • Persona modes--surveillance, --oracle, --underground as feed presets with matching color themes and boot copy
  • Synthesized audio — short static bursts tied to glitch events, independent of mic input

Extensibility

  • serve.py — HTTP server that imports engine.render and engine.fetch directly to stream 1-bit bitmaps to an ESP32 display
  • Rust portntfy.py and render.py are the natural first targets; clear module boundaries make incremental porting viable

macOS only (script/system font paths for translation are hardcoded). Primary display font is user-selectable via the bundled fonts/ picker. Python 3.10+.

Description
A full-screen terminal news stream in a Matrix/THX-1138 aesthetic — scrolling live global headlines in large block type, with per-region translation and mic-reactive glitch effects. Supports a --poetry mode for public-domain literary passages.
Readme 2.6 MiB
Languages
Python 99.7%
Pkl 0.3%