Compare commits
8 Commits
e7de09be50
...
drift
| Author | SHA1 | Date | |
|---|---|---|---|
| 66c13b5829 | |||
| 086214f05e | |||
| 0f762475b5 | |||
| b00b612da0 | |||
| 39dab4b22b | |||
| 47f17e12ef | |||
| 851c4a77b4 | |||
| cdbb6dfd1c |
87
README.md
87
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> *Digital consciousness stream. Matrix aesthetic · THX-1138 hue.*
|
> *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.
|
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](https://ntfy.sh).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -12,44 +12,102 @@ A full-screen terminal news ticker that renders live global headlines in large O
|
|||||||
python3 mainline.py # news stream
|
python3 mainline.py # news stream
|
||||||
python3 mainline.py --poetry # literary consciousness mode
|
python3 mainline.py --poetry # literary consciousness mode
|
||||||
python3 mainline.py -p # same
|
python3 mainline.py -p # same
|
||||||
|
python3 mainline.py --firehose # dense rapid-fire headline mode
|
||||||
|
python3 mainline.py --refresh # force re-fetch (bypass cache)
|
||||||
```
|
```
|
||||||
|
|
||||||
First run bootstraps a local `.mainline_venv/` and installs deps (`feedparser`, `Pillow`, `sounddevice`, `numpy`). Subsequent runs start immediately.
|
First run bootstraps a local `.mainline_venv/` and installs deps (`feedparser`, `Pillow`, `sounddevice`, `numpy`). Subsequent runs start immediately, loading from cache.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
At the top of `mainline.py`:
|
All constants live in `engine/config.py`:
|
||||||
|
|
||||||
| Constant | Default | What it does |
|
| Constant | Default | What it does |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `HEADLINE_LIMIT` | `1000` | Total headlines per session |
|
| `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 |
|
| `MIC_THRESHOLD_DB` | `50` | dB floor above which glitches spike |
|
||||||
| `_FONT_PATH` | hardcoded path | Path to your OTF/TTF display font |
|
| `FONT_PATH` | hardcoded path | Path to your OTF/TTF display font |
|
||||||
| `_FONT_SZ` | `60` | Font render size (affects block density) |
|
| `FONT_SZ` | `60` | Font render size (affects block density) |
|
||||||
| `_RENDER_H` | `8` | Terminal rows per headline line |
|
| `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 |
|
||||||
|
|
||||||
**Font:** `_FONT_PATH` is hardcoded to a local path. Update it to point to whatever display font you want — anything with strong contrast and wide letterforms works well.
|
**Font:** `FONT_PATH` is hardcoded to a local path. Update it to point to whatever display font you want — anything with strong contrast and wide letterforms works well.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
- Feeds are fetched and filtered on startup (sports and vapid content stripped)
|
- 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 into half-block characters (`▀▄█ `) at the configured font size
|
- Headlines are rasterized via Pillow with 4× SSAA into half-block characters (`▀▄█ `) at the configured font size
|
||||||
- A left-to-right ANSI gradient colors each character: white-hot leading edge trails off to near-black
|
- A left-to-right ANSI gradient colors each character: white-hot leading edge trails off to near-black; the gradient sweeps continuously across the full scroll canvas
|
||||||
- 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
|
- 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 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
|
- 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(), boot sequence, signal handler
|
||||||
|
```
|
||||||
|
|
||||||
|
`ntfy.py` and `mic.py` have zero internal dependencies and can be imported by any other visualizer.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Feeds
|
## Feeds
|
||||||
|
|
||||||
~25 sources across four categories: **Science & Technology**, **Economics & Business**, **World & Politics**, **Culture & Ideas**. Add or swap in `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.
|
**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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -62,7 +120,6 @@ At the top of `mainline.py`:
|
|||||||
|
|
||||||
### Graphics
|
### Graphics
|
||||||
- **Matrix rain underlay** — katakana column rain rendered at low opacity beneath the scrolling blocks as a background layer
|
- **Matrix rain underlay** — katakana column rain rendered at low opacity beneath the scrolling blocks as a background layer
|
||||||
- **Animated gradient** — shift the white-hot leading edge left/right each frame for a pulse/comet effect
|
|
||||||
- **CRT simulation** — subtle dim scanlines every N rows, occasional brightness ripple across the full screen
|
- **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
|
- **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
|
- **Parallax secondary column** — a second, dimmer, faster-scrolling stream of ambient text at reduced opacity on one side
|
||||||
@@ -75,6 +132,10 @@ At the top of `mainline.py`:
|
|||||||
- **Persona modes** — `--surveillance`, `--oracle`, `--underground` as feed presets with matching color themes and boot copy
|
- **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
|
- **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 port** — `ntfy.py` and `render.py` are the natural first targets; clear module boundaries make incremental porting viable
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*macOS only (system font paths hardcoded). Python 3.9+.*
|
*macOS only (system font paths hardcoded). Python 3.9+.*
|
||||||
|
|||||||
111
engine/scroll.py
111
engine/scroll.py
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Scroll engine — the main frame loop with headline rendering and message display.
|
Render engine — ticker content, scroll motion, message panel, and firehose overlay.
|
||||||
Depends on: config, terminal, render, effects, ntfy, mic.
|
Depends on: config, terminal, render, effects, ntfy, mic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ from engine.effects import noise, glitch_bar, fade_line, vis_trunc, next_headlin
|
|||||||
|
|
||||||
|
|
||||||
def stream(items, ntfy_poller, mic_monitor):
|
def stream(items, ntfy_poller, mic_monitor):
|
||||||
"""Main rendering loop. Scrolls headlines, shows ntfy messages, applies effects."""
|
"""Main render loop with four layers: message, ticker, scroll motion, firehose."""
|
||||||
random.shuffle(items)
|
random.shuffle(items)
|
||||||
pool = list(items)
|
pool = list(items)
|
||||||
seen = set()
|
seen = set()
|
||||||
@@ -28,16 +28,21 @@ def stream(items, ntfy_poller, mic_monitor):
|
|||||||
|
|
||||||
w, h = tw(), th()
|
w, h = tw(), th()
|
||||||
fh = config.FIREHOSE_H if config.FIREHOSE else 0
|
fh = config.FIREHOSE_H if config.FIREHOSE else 0
|
||||||
sh = h - fh # scroll zone height
|
ticker_view_h = h - fh # reserve fixed firehose strip at bottom
|
||||||
GAP = 3 # blank rows between headlines
|
GAP = 3 # blank rows between headlines
|
||||||
scroll_interval = config.SCROLL_DUR / (sh + 15) * 2
|
scroll_step_interval = config.SCROLL_DUR / (ticker_view_h + 15) * 2
|
||||||
|
|
||||||
# active blocks: (content_rows, color, canvas_y, meta_idx)
|
# Taxonomy:
|
||||||
|
# - message: centered ntfy overlay panel
|
||||||
|
# - ticker: large headline text content
|
||||||
|
# - scroll: upward camera motion applied to ticker content
|
||||||
|
# - firehose: fixed carriage-return style strip pinned at bottom
|
||||||
|
# Active ticker blocks: (content_rows, color, canvas_y, meta_idx)
|
||||||
active = []
|
active = []
|
||||||
cam = 0 # viewport top in virtual canvas coords
|
scroll_cam = 0 # viewport top in virtual canvas coords
|
||||||
next_y = sh # canvas-y where next block starts (off-screen bottom)
|
ticker_next_y = ticker_view_h # canvas-y where next block starts (off-screen bottom)
|
||||||
noise_cache = {}
|
noise_cache = {}
|
||||||
scroll_accum = 0.0
|
scroll_motion_accum = 0.0
|
||||||
|
|
||||||
def _noise_at(cy):
|
def _noise_at(cy):
|
||||||
if cy not in noise_cache:
|
if cy not in noise_cache:
|
||||||
@@ -53,16 +58,17 @@ def stream(items, ntfy_poller, mic_monitor):
|
|||||||
t0 = time.monotonic()
|
t0 = time.monotonic()
|
||||||
w, h = tw(), th()
|
w, h = tw(), th()
|
||||||
fh = config.FIREHOSE_H if config.FIREHOSE else 0
|
fh = config.FIREHOSE_H if config.FIREHOSE else 0
|
||||||
sh = h - fh
|
ticker_view_h = h - fh
|
||||||
|
|
||||||
# ── Check for ntfy message ────────────────────────
|
# ── Check for ntfy message ────────────────────────
|
||||||
msg_h = 0 # rows consumed by message zone at top
|
msg_h = 0
|
||||||
|
msg_overlay = []
|
||||||
msg = ntfy_poller.get_active_message()
|
msg = ntfy_poller.get_active_message()
|
||||||
|
|
||||||
buf = []
|
buf = []
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
m_title, m_body, m_ts = msg
|
m_title, m_body, m_ts = msg
|
||||||
# ── Message zone: pinned to top, scroll continues below ──
|
# ── Message overlay: centered in the viewport ──
|
||||||
display_text = m_body or m_title or "(empty)"
|
display_text = m_body or m_title or "(empty)"
|
||||||
display_text = re.sub(r"\s+", " ", display_text.upper())
|
display_text = re.sub(r"\s+", " ", display_text.upper())
|
||||||
cache_key = (display_text, w)
|
cache_key = (display_text, w)
|
||||||
@@ -76,10 +82,12 @@ def stream(items, ntfy_poller, mic_monitor):
|
|||||||
elapsed_s = int(time.monotonic() - m_ts)
|
elapsed_s = int(time.monotonic() - m_ts)
|
||||||
remaining = max(0, config.MESSAGE_DISPLAY_SECS - elapsed_s)
|
remaining = max(0, config.MESSAGE_DISPLAY_SECS - elapsed_s)
|
||||||
ts_str = datetime.now().strftime("%H:%M:%S")
|
ts_str = datetime.now().strftime("%H:%M:%S")
|
||||||
|
panel_h = len(msg_rows) + 2 # meta + border
|
||||||
|
panel_top = max(0, (h - panel_h) // 2)
|
||||||
row_idx = 0
|
row_idx = 0
|
||||||
for mr in msg_rows:
|
for mr in msg_rows:
|
||||||
ln = vis_trunc(mr, w)
|
ln = vis_trunc(mr, w)
|
||||||
buf.append(f"\033[{row_idx+1};1H {ln}{RST}\033[K")
|
msg_overlay.append(f"\033[{panel_top + row_idx + 1};1H {ln}{RST}\033[K")
|
||||||
row_idx += 1
|
row_idx += 1
|
||||||
# Meta line: title (if distinct) + source + countdown
|
# Meta line: title (if distinct) + source + countdown
|
||||||
meta_parts = []
|
meta_parts = []
|
||||||
@@ -87,49 +95,46 @@ def stream(items, ntfy_poller, mic_monitor):
|
|||||||
meta_parts.append(m_title)
|
meta_parts.append(m_title)
|
||||||
meta_parts.append(f"ntfy \u00b7 {ts_str} \u00b7 {remaining}s")
|
meta_parts.append(f"ntfy \u00b7 {ts_str} \u00b7 {remaining}s")
|
||||||
meta = " " + " \u00b7 ".join(meta_parts) if len(meta_parts) > 1 else " " + meta_parts[0]
|
meta = " " + " \u00b7 ".join(meta_parts) if len(meta_parts) > 1 else " " + meta_parts[0]
|
||||||
buf.append(f"\033[{row_idx+1};1H{MSG_META}{meta}{RST}\033[K")
|
msg_overlay.append(f"\033[{panel_top + row_idx + 1};1H{MSG_META}{meta}{RST}\033[K")
|
||||||
row_idx += 1
|
row_idx += 1
|
||||||
# Border — constant boundary between message and scroll
|
# Border — constant boundary under message panel
|
||||||
bar = "\u2500" * (w - 4)
|
bar = "\u2500" * (w - 4)
|
||||||
buf.append(f"\033[{row_idx+1};1H {MSG_BORDER}{bar}{RST}\033[K")
|
msg_overlay.append(f"\033[{panel_top + row_idx + 1};1H {MSG_BORDER}{bar}{RST}\033[K")
|
||||||
row_idx += 1
|
|
||||||
msg_h = row_idx
|
|
||||||
|
|
||||||
# Effective scroll zone: below message, above firehose
|
# Ticker draws above the fixed firehose strip; message is a centered overlay.
|
||||||
scroll_h = sh - msg_h
|
ticker_h = ticker_view_h - msg_h
|
||||||
|
|
||||||
# ── Scroll: headline rendering (always runs) ──────
|
# ── Ticker content + scroll motion (always runs) ──
|
||||||
# Advance scroll on schedule
|
scroll_motion_accum += config.FRAME_DT
|
||||||
scroll_accum += config.FRAME_DT
|
while scroll_motion_accum >= scroll_step_interval:
|
||||||
while scroll_accum >= scroll_interval:
|
scroll_motion_accum -= scroll_step_interval
|
||||||
scroll_accum -= scroll_interval
|
scroll_cam += 1
|
||||||
cam += 1
|
|
||||||
|
|
||||||
# Enqueue new headlines when room at the bottom
|
# Enqueue new headlines when room at the bottom
|
||||||
while next_y < cam + sh + 10 and queued < config.HEADLINE_LIMIT:
|
while ticker_next_y < scroll_cam + ticker_view_h + 10 and queued < config.HEADLINE_LIMIT:
|
||||||
t, src, ts = next_headline(pool, items, seen)
|
t, src, ts = next_headline(pool, items, seen)
|
||||||
content, hc, midx = make_block(t, src, ts, w)
|
ticker_content, hc, midx = make_block(t, src, ts, w)
|
||||||
active.append((content, hc, next_y, midx))
|
active.append((ticker_content, hc, ticker_next_y, midx))
|
||||||
next_y += len(content) + GAP
|
ticker_next_y += len(ticker_content) + GAP
|
||||||
queued += 1
|
queued += 1
|
||||||
|
|
||||||
# Prune off-screen blocks and stale noise
|
# Prune off-screen blocks and stale noise
|
||||||
active = [(c, hc, by, mi) for c, hc, by, mi in active
|
active = [(c, hc, by, mi) for c, hc, by, mi in active
|
||||||
if by + len(c) > cam]
|
if by + len(c) > scroll_cam]
|
||||||
for k in list(noise_cache):
|
for k in list(noise_cache):
|
||||||
if k < cam:
|
if k < scroll_cam:
|
||||||
del noise_cache[k]
|
del noise_cache[k]
|
||||||
|
|
||||||
# Draw scroll zone (below message zone, above firehose)
|
# Draw ticker zone (above fixed firehose strip)
|
||||||
top_zone = max(1, int(scroll_h * 0.25))
|
top_zone = max(1, int(ticker_h * 0.25))
|
||||||
bot_zone = max(1, int(scroll_h * 0.10))
|
bot_zone = max(1, int(ticker_h * 0.10))
|
||||||
grad_offset = (time.monotonic() * config.GRAD_SPEED) % 1.0
|
grad_offset = (time.monotonic() * config.GRAD_SPEED) % 1.0
|
||||||
scroll_buf_start = len(buf) # track where scroll rows start in buf
|
ticker_buf_start = len(buf) # track where ticker rows start in buf
|
||||||
for r in range(scroll_h):
|
for r in range(ticker_h):
|
||||||
scr_row = msg_h + r + 1 # 1-indexed ANSI screen row
|
scr_row = r + 1 # 1-indexed ANSI screen row
|
||||||
cy = cam + r
|
cy = scroll_cam + r
|
||||||
top_f = min(1.0, r / top_zone) if top_zone > 0 else 1.0
|
top_f = min(1.0, r / top_zone) if top_zone > 0 else 1.0
|
||||||
bot_f = min(1.0, (scroll_h - 1 - r) / bot_zone) if bot_zone > 0 else 1.0
|
bot_f = min(1.0, (ticker_h - 1 - r) / bot_zone) if bot_zone > 0 else 1.0
|
||||||
row_fade = min(top_f, bot_f)
|
row_fade = min(top_f, bot_f)
|
||||||
drawn = False
|
drawn = False
|
||||||
for content, hc, by, midx in active:
|
for content, hc, by, midx in active:
|
||||||
@@ -160,22 +165,24 @@ def stream(items, ntfy_poller, mic_monitor):
|
|||||||
else:
|
else:
|
||||||
buf.append(f"\033[{scr_row};1H\033[K")
|
buf.append(f"\033[{scr_row};1H\033[K")
|
||||||
|
|
||||||
# Draw firehose zone
|
# Glitch — base rate + mic-reactive spikes (ticker zone only)
|
||||||
if config.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 = mic_monitor.excess
|
mic_excess = mic_monitor.excess
|
||||||
glitch_prob = 0.32 + min(0.9, mic_excess * 0.16)
|
glitch_prob = 0.32 + min(0.9, mic_excess * 0.16)
|
||||||
n_hits = 4 + int(mic_excess / 2)
|
n_hits = 4 + int(mic_excess / 2)
|
||||||
scroll_buf_len = len(buf) - scroll_buf_start
|
ticker_buf_len = len(buf) - ticker_buf_start
|
||||||
if random.random() < glitch_prob and scroll_buf_len > 0:
|
if random.random() < glitch_prob and ticker_buf_len > 0:
|
||||||
for _ in range(min(n_hits, scroll_buf_len)):
|
for _ in range(min(n_hits, ticker_buf_len)):
|
||||||
gi = random.randint(0, scroll_buf_len - 1)
|
gi = random.randint(0, ticker_buf_len - 1)
|
||||||
scr_row = msg_h + gi + 1
|
scr_row = gi + 1
|
||||||
buf[scroll_buf_start + gi] = f"\033[{scr_row};1H{glitch_bar(w)}"
|
buf[ticker_buf_start + gi] = f"\033[{scr_row};1H{glitch_bar(w)}"
|
||||||
|
|
||||||
|
if config.FIREHOSE and fh > 0:
|
||||||
|
for fr in range(fh):
|
||||||
|
scr_row = h - fh + fr + 1
|
||||||
|
fline = firehose_line(items, w)
|
||||||
|
buf.append(f"\033[{scr_row};1H{fline}\033[K")
|
||||||
|
if msg_overlay:
|
||||||
|
buf.extend(msg_overlay)
|
||||||
|
|
||||||
sys.stdout.buffer.write("".join(buf).encode())
|
sys.stdout.buffer.write("".join(buf).encode())
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|||||||
Reference in New Issue
Block a user