Reviewed-on: #18
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 a white-hot → deep green gradient. 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.
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
First run bootstraps a local .mainline_venv/ and installs deps (feedparser, Pillow, sounddevice, numpy). Subsequent runs start immediately, loading from cache.
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 endpoint to poll |
NTFY_POLL_INTERVAL |
15 |
Seconds between ntfy polls |
MESSAGE_DISPLAY_SECS |
30 |
How long an ntfy message holds the screen |
Fonts
A fonts/ directory is bundled with demo faces (AlphatronDemo, CSBishopDrawn, CyberformDemo, KATA, Microbots, Neoform, Pixel Sparta, Robocops, 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.
How it works
- On launch, the font picker scans
fonts/and presents a live-rendered TUI for face selection;--no-font-pickerskips 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.jsonfor 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 poller 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/
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
app.py main(), font picker TUI, boot sequence, signal handler
ntfy.py and mic.py have zero internal dependencies and can be imported by any other visualizer.
Feeds
~25 sources across four categories: Science & Technology, Economics & Business, World & Politics, Culture & Ideas. Add or swap feeds in engine/sources.py → FEEDS.
Poetry mode pulls from Project Gutenberg: Whitman, Dickinson, Thoreau, Emerson. Sources are in engine/sources.py → POETRY_SOURCES.
ntfy.sh Integration
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. The NtfyPoller class is fully standalone and can be reused by other visualizers:
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() # returns (title, body, timestamp) or None
Ideas / Future
Performance
- Concurrent feed fetching — startup currently blocks sequentially on ~25 HTTP requests;
concurrent.futures.ThreadPoolExecutorwould 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,--undergroundas 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.renderandengine.fetchdirectly to stream 1-bit bitmaps to an ESP32 display - Rust port —
ntfy.pyandrender.pyare 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.9+.