- Add EventBus class with pub/sub messaging (thread-safe) - Add emitter Protocol classes (EventEmitter, Startable, Stoppable) - Add event emission to NtfyPoller (NtfyMessageEvent) - Add event emission to MicMonitor (MicLevelEvent) - Update StreamController to publish stream start/end events - Add comprehensive tests for eventbus and emitters modules
8.9 KiB
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/
__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.
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+.