- Add --pipeline-diagram flag to generate mermaid diagrams - Create engine/pipeline.py with PipelineIntrospector - Outputs flowchart, sequence diagram, and camera state diagram - Run with: python mainline.py --pipeline-diagram
671 lines
21 KiB
Python
671 lines
21 KiB
Python
"""
|
|
Application orchestrator — boot sequence, signal handling, main loop wiring.
|
|
"""
|
|
|
|
import atexit
|
|
import os
|
|
import signal
|
|
import sys
|
|
import termios
|
|
import time
|
|
import tty
|
|
|
|
from engine import config, render
|
|
from engine.controller import StreamController
|
|
from engine.fetch import fetch_all, fetch_poetry, load_cache, save_cache
|
|
from engine.terminal import (
|
|
CLR,
|
|
CURSOR_OFF,
|
|
CURSOR_ON,
|
|
G_DIM,
|
|
G_HI,
|
|
G_MID,
|
|
RST,
|
|
W_DIM,
|
|
W_GHOST,
|
|
boot_ln,
|
|
slow_print,
|
|
tw,
|
|
)
|
|
|
|
TITLE = [
|
|
" ███╗ ███╗ █████╗ ██╗███╗ ██╗██╗ ██╗███╗ ██╗███████╗",
|
|
" ████╗ ████║██╔══██╗██║████╗ ██║██║ ██║████╗ ██║██╔════╝",
|
|
" ██╔████╔██║███████║██║██╔██╗ ██║██║ ██║██╔██╗ ██║█████╗ ",
|
|
" ██║╚██╔╝██║██╔══██║██║██║╚██╗██║██║ ██║██║╚██╗██║██╔══╝ ",
|
|
" ██║ ╚═╝ ██║██║ ██║██║██║ ╚████║███████╗██║██║ ╚████║███████╗",
|
|
" ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝",
|
|
]
|
|
|
|
|
|
def _read_picker_key():
|
|
ch = sys.stdin.read(1)
|
|
if ch == "\x03":
|
|
return "interrupt"
|
|
if ch in ("\r", "\n"):
|
|
return "enter"
|
|
if ch == "\x1b":
|
|
c1 = sys.stdin.read(1)
|
|
if c1 != "[":
|
|
return None
|
|
c2 = sys.stdin.read(1)
|
|
if c2 == "A":
|
|
return "up"
|
|
if c2 == "B":
|
|
return "down"
|
|
return None
|
|
if ch in ("k", "K"):
|
|
return "up"
|
|
if ch in ("j", "J"):
|
|
return "down"
|
|
if ch in ("q", "Q"):
|
|
return "enter"
|
|
return None
|
|
|
|
|
|
def _normalize_preview_rows(rows):
|
|
"""Trim shared left padding and trailing spaces for stable on-screen previews."""
|
|
non_empty = [r for r in rows if r.strip()]
|
|
if not non_empty:
|
|
return [""]
|
|
left_pad = min(len(r) - len(r.lstrip(" ")) for r in non_empty)
|
|
out = []
|
|
for row in rows:
|
|
if left_pad < len(row):
|
|
out.append(row[left_pad:].rstrip())
|
|
else:
|
|
out.append(row.rstrip())
|
|
return out
|
|
|
|
|
|
def _draw_font_picker(faces, selected):
|
|
w = tw()
|
|
h = 24
|
|
try:
|
|
h = os.get_terminal_size().lines
|
|
except Exception:
|
|
pass
|
|
|
|
max_preview_w = max(24, w - 8)
|
|
header_h = 6
|
|
footer_h = 3
|
|
preview_h = max(4, min(config.RENDER_H + 2, max(4, h // 2)))
|
|
visible = max(1, h - header_h - preview_h - footer_h)
|
|
top = max(0, selected - (visible // 2))
|
|
bottom = min(len(faces), top + visible)
|
|
top = max(0, bottom - visible)
|
|
|
|
print(CLR, end="")
|
|
print(CURSOR_OFF, end="")
|
|
print()
|
|
print(f" {G_HI}FONT PICKER{RST}")
|
|
print(f" {W_GHOST}{'─' * (w - 4)}{RST}")
|
|
print(f" {W_DIM}{config.FONT_DIR[:max_preview_w]}{RST}")
|
|
print(f" {W_GHOST}↑/↓ move · Enter select · q accept current{RST}")
|
|
print()
|
|
|
|
for pos in range(top, bottom):
|
|
face = faces[pos]
|
|
active = pos == selected
|
|
pointer = "▶" if active else " "
|
|
color = G_HI if active else W_DIM
|
|
print(
|
|
f" {color}{pointer} {face['name']}{RST}{W_GHOST} · {face['file_name']}{RST}"
|
|
)
|
|
|
|
if top > 0:
|
|
print(f" {W_GHOST}… {top} above{RST}")
|
|
if bottom < len(faces):
|
|
print(f" {W_GHOST}… {len(faces) - bottom} below{RST}")
|
|
|
|
print()
|
|
print(f" {W_GHOST}{'─' * (w - 4)}{RST}")
|
|
print(
|
|
f" {W_DIM}Preview: {faces[selected]['name']} · {faces[selected]['file_name']}{RST}"
|
|
)
|
|
preview_rows = faces[selected]["preview_rows"][:preview_h]
|
|
for row in preview_rows:
|
|
shown = row[:max_preview_w]
|
|
print(f" {shown}")
|
|
|
|
|
|
def pick_font_face():
|
|
"""Interactive startup picker for selecting a face from repo OTF files."""
|
|
if not config.FONT_PICKER:
|
|
return
|
|
|
|
font_files = config.list_repo_font_files()
|
|
if not font_files:
|
|
print(CLR, end="")
|
|
print(CURSOR_OFF, end="")
|
|
print()
|
|
print(f" {G_HI}FONT PICKER{RST}")
|
|
print(f" {W_GHOST}{'─' * (tw() - 4)}{RST}")
|
|
print(f" {G_DIM}> no .otf/.ttf/.ttc files found in: {config.FONT_DIR}{RST}")
|
|
print(f" {W_GHOST}> add font files to the fonts folder, then rerun{RST}")
|
|
time.sleep(1.8)
|
|
sys.exit(1)
|
|
|
|
prepared = []
|
|
for font_path in font_files:
|
|
try:
|
|
faces = render.list_font_faces(font_path, max_faces=64)
|
|
except Exception:
|
|
fallback = os.path.splitext(os.path.basename(font_path))[0]
|
|
faces = [{"index": 0, "name": fallback}]
|
|
for face in faces:
|
|
idx = face["index"]
|
|
name = face["name"]
|
|
file_name = os.path.basename(font_path)
|
|
try:
|
|
fnt = render.load_font_face(font_path, idx)
|
|
rows = _normalize_preview_rows(render.render_line(name, fnt))
|
|
except Exception:
|
|
rows = ["(preview unavailable)"]
|
|
prepared.append(
|
|
{
|
|
"font_path": font_path,
|
|
"font_index": idx,
|
|
"name": name,
|
|
"file_name": file_name,
|
|
"preview_rows": rows,
|
|
}
|
|
)
|
|
|
|
if not prepared:
|
|
print(CLR, end="")
|
|
print(CURSOR_OFF, end="")
|
|
print()
|
|
print(f" {G_HI}FONT PICKER{RST}")
|
|
print(f" {W_GHOST}{'─' * (tw() - 4)}{RST}")
|
|
print(f" {G_DIM}> no readable font faces found in: {config.FONT_DIR}{RST}")
|
|
time.sleep(1.8)
|
|
sys.exit(1)
|
|
|
|
def _same_path(a, b):
|
|
try:
|
|
return os.path.samefile(a, b)
|
|
except Exception:
|
|
return os.path.abspath(a) == os.path.abspath(b)
|
|
|
|
selected = next(
|
|
(
|
|
i
|
|
for i, f in enumerate(prepared)
|
|
if _same_path(f["font_path"], config.FONT_PATH)
|
|
and f["font_index"] == config.FONT_INDEX
|
|
),
|
|
0,
|
|
)
|
|
|
|
if not sys.stdin.isatty():
|
|
selected_font = prepared[selected]
|
|
config.set_font_selection(
|
|
font_path=selected_font["font_path"],
|
|
font_index=selected_font["font_index"],
|
|
)
|
|
render.clear_font_cache()
|
|
print(
|
|
f" {G_DIM}> using {selected_font['name']} ({selected_font['file_name']}){RST}"
|
|
)
|
|
time.sleep(0.8)
|
|
print(CLR, end="")
|
|
print(CURSOR_OFF, end="")
|
|
print()
|
|
return
|
|
|
|
fd = sys.stdin.fileno()
|
|
old_settings = termios.tcgetattr(fd)
|
|
try:
|
|
tty.setcbreak(fd)
|
|
while True:
|
|
_draw_font_picker(prepared, selected)
|
|
key = _read_picker_key()
|
|
if key == "up":
|
|
selected = max(0, selected - 1)
|
|
elif key == "down":
|
|
selected = min(len(prepared) - 1, selected + 1)
|
|
elif key == "enter":
|
|
break
|
|
elif key == "interrupt":
|
|
raise KeyboardInterrupt
|
|
finally:
|
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
|
|
selected_font = prepared[selected]
|
|
config.set_font_selection(
|
|
font_path=selected_font["font_path"],
|
|
font_index=selected_font["font_index"],
|
|
)
|
|
render.clear_font_cache()
|
|
print(
|
|
f" {G_DIM}> using {selected_font['name']} ({selected_font['file_name']}){RST}"
|
|
)
|
|
time.sleep(0.8)
|
|
print(CLR, end="")
|
|
print(CURSOR_OFF, end="")
|
|
print()
|
|
|
|
|
|
def pick_effects_config():
|
|
"""Interactive picker for configuring effects pipeline."""
|
|
import effects_plugins
|
|
from engine.effects import get_effect_chain, get_registry
|
|
|
|
effects_plugins.discover_plugins()
|
|
|
|
registry = get_registry()
|
|
chain = get_effect_chain()
|
|
chain.set_order(["noise", "fade", "glitch", "firehose"])
|
|
|
|
effects = list(registry.list_all().values())
|
|
if not effects:
|
|
return
|
|
|
|
selected = 0
|
|
editing_intensity = False
|
|
intensity_value = 1.0
|
|
|
|
def _draw_effects_picker():
|
|
w = tw()
|
|
print(CLR, end="")
|
|
print("\033[1;1H", end="")
|
|
print(" \033[1;38;5;231mEFFECTS CONFIG\033[0m")
|
|
print(f" \033[2;38;5;37m{'─' * (w - 4)}\033[0m")
|
|
print()
|
|
|
|
for i, effect in enumerate(effects):
|
|
prefix = " > " if i == selected else " "
|
|
marker = "[*]" if effect.config.enabled else "[ ]"
|
|
if editing_intensity and i == selected:
|
|
print(
|
|
f"{prefix}{marker} \033[1;38;5;82m{effect.name}\033[0m: intensity={intensity_value:.2f} (use +/- to adjust, Enter to confirm)"
|
|
)
|
|
else:
|
|
print(
|
|
f"{prefix}{marker} {effect.name}: intensity={effect.config.intensity:.2f}"
|
|
)
|
|
|
|
print()
|
|
print(f" \033[2;38;5;37m{'─' * (w - 4)}\033[0m")
|
|
print(
|
|
" \033[38;5;245mControls: space=toggle on/off | +/-=adjust intensity | arrows=move | Enter=next effect | q=done\033[0m"
|
|
)
|
|
|
|
def _read_effects_key():
|
|
ch = sys.stdin.read(1)
|
|
if ch == "\x03":
|
|
return "interrupt"
|
|
if ch in ("\r", "\n"):
|
|
return "enter"
|
|
if ch == " ":
|
|
return "toggle"
|
|
if ch == "q":
|
|
return "quit"
|
|
if ch == "+" or ch == "=":
|
|
return "up"
|
|
if ch == "-" or ch == "_":
|
|
return "down"
|
|
if ch == "\x1b":
|
|
c1 = sys.stdin.read(1)
|
|
if c1 != "[":
|
|
return None
|
|
c2 = sys.stdin.read(1)
|
|
if c2 == "A":
|
|
return "up"
|
|
if c2 == "B":
|
|
return "down"
|
|
return None
|
|
return None
|
|
|
|
if not sys.stdin.isatty():
|
|
return
|
|
|
|
fd = sys.stdin.fileno()
|
|
old_settings = termios.tcgetattr(fd)
|
|
try:
|
|
tty.setcbreak(fd)
|
|
while True:
|
|
_draw_effects_picker()
|
|
key = _read_effects_key()
|
|
|
|
if key == "quit" or key == "enter":
|
|
break
|
|
elif key == "up" and editing_intensity:
|
|
intensity_value = min(1.0, intensity_value + 0.1)
|
|
effects[selected].config.intensity = intensity_value
|
|
elif key == "down" and editing_intensity:
|
|
intensity_value = max(0.0, intensity_value - 0.1)
|
|
effects[selected].config.intensity = intensity_value
|
|
elif key == "up":
|
|
selected = max(0, selected - 1)
|
|
intensity_value = effects[selected].config.intensity
|
|
elif key == "down":
|
|
selected = min(len(effects) - 1, selected + 1)
|
|
intensity_value = effects[selected].config.intensity
|
|
elif key == "toggle":
|
|
effects[selected].config.enabled = not effects[selected].config.enabled
|
|
elif key == "interrupt":
|
|
raise KeyboardInterrupt
|
|
finally:
|
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
|
|
|
|
def run_demo_mode():
|
|
"""Run demo mode - showcases effects and camera modes with real content."""
|
|
import random
|
|
|
|
from engine import config
|
|
from engine.camera import Camera, CameraMode
|
|
from engine.display import DisplayRegistry
|
|
from engine.effects import (
|
|
EffectContext,
|
|
PerformanceMonitor,
|
|
get_effect_chain,
|
|
get_registry,
|
|
set_monitor,
|
|
)
|
|
from engine.fetch import fetch_all, fetch_poetry, load_cache
|
|
from engine.scroll import calculate_scroll_step
|
|
|
|
print(" \033[1;38;5;46mMAINLINE DEMO MODE\033[0m")
|
|
print(" \033[38;5;245mInitializing...\033[0m")
|
|
|
|
import effects_plugins
|
|
|
|
effects_plugins.discover_plugins()
|
|
|
|
registry = get_registry()
|
|
chain = get_effect_chain()
|
|
chain.set_order(["noise", "fade", "glitch", "firehose", "hud"])
|
|
|
|
monitor = PerformanceMonitor()
|
|
set_monitor(monitor)
|
|
chain._monitor = monitor
|
|
|
|
display = DisplayRegistry.create("pygame")
|
|
if not display:
|
|
print(" \033[38;5;196mFailed to create pygame display\033[0m")
|
|
sys.exit(1)
|
|
|
|
w, h = 80, 24
|
|
display.init(w, h)
|
|
display.clear()
|
|
|
|
print(" \033[38;5;245mFetching content...\033[0m")
|
|
|
|
cached = load_cache()
|
|
if cached:
|
|
items = cached
|
|
elif config.MODE == "poetry":
|
|
items, _, _ = fetch_poetry()
|
|
else:
|
|
items, _, _ = fetch_all()
|
|
|
|
if not items:
|
|
print(" \033[38;5;196mNo content available\033[0m")
|
|
sys.exit(1)
|
|
|
|
random.shuffle(items)
|
|
pool = list(items)
|
|
seen = set()
|
|
active = []
|
|
ticker_next_y = 0
|
|
noise_cache = {}
|
|
scroll_motion_accum = 0.0
|
|
frame_number = 0
|
|
|
|
GAP = 3
|
|
scroll_step_interval = calculate_scroll_step(config.SCROLL_DUR, h)
|
|
|
|
camera = Camera.vertical(speed=1.0)
|
|
|
|
effects_to_demo = ["noise", "fade", "glitch", "firehose"]
|
|
effect_idx = 0
|
|
effect_name = effects_to_demo[effect_idx]
|
|
effect_start_time = time.time()
|
|
current_intensity = 0.0
|
|
ramping_up = True
|
|
|
|
camera_modes = [
|
|
(CameraMode.VERTICAL, "vertical"),
|
|
(CameraMode.HORIZONTAL, "horizontal"),
|
|
(CameraMode.OMNI, "omni"),
|
|
(CameraMode.FLOATING, "floating"),
|
|
]
|
|
camera_mode_idx = 0
|
|
camera_start_time = time.time()
|
|
|
|
print(" \033[38;5;82mStarting effect & camera demo...\033[0m")
|
|
print(" \033[38;5;245mPress Ctrl+C to exit\033[0m\n")
|
|
|
|
try:
|
|
while True:
|
|
elapsed = time.time() - effect_start_time
|
|
camera_elapsed = time.time() - camera_start_time
|
|
duration = config.DEMO_EFFECT_DURATION
|
|
|
|
if elapsed >= duration:
|
|
effect_idx = (effect_idx + 1) % len(effects_to_demo)
|
|
effect_name = effects_to_demo[effect_idx]
|
|
effect_start_time = time.time()
|
|
elapsed = 0
|
|
current_intensity = 0.0
|
|
ramping_up = True
|
|
|
|
if camera_elapsed >= duration * 2:
|
|
camera_mode_idx = (camera_mode_idx + 1) % len(camera_modes)
|
|
mode, mode_name = camera_modes[camera_mode_idx]
|
|
camera = Camera(mode=mode, speed=1.0)
|
|
camera_start_time = time.time()
|
|
camera_elapsed = 0
|
|
|
|
progress = elapsed / duration
|
|
if ramping_up:
|
|
current_intensity = progress
|
|
if progress >= 1.0:
|
|
ramping_up = False
|
|
else:
|
|
current_intensity = 1.0 - progress
|
|
|
|
for effect in registry.list_all().values():
|
|
if effect.name == effect_name:
|
|
effect.config.enabled = True
|
|
effect.config.intensity = current_intensity
|
|
elif effect.name not in ("hud",):
|
|
effect.config.enabled = False
|
|
|
|
hud_effect = registry.get("hud")
|
|
if hud_effect:
|
|
mode_name = camera_modes[camera_mode_idx][1]
|
|
hud_effect.config.params["display_effect"] = (
|
|
f"{effect_name} / {mode_name}"
|
|
)
|
|
hud_effect.config.params["display_intensity"] = current_intensity
|
|
|
|
scroll_motion_accum += config.FRAME_DT
|
|
while scroll_motion_accum >= scroll_step_interval:
|
|
scroll_motion_accum -= scroll_step_interval
|
|
camera.update(config.FRAME_DT)
|
|
|
|
while ticker_next_y < camera.y + h + 10 and len(active) < 50:
|
|
from engine.effects import next_headline
|
|
from engine.render import make_block
|
|
|
|
t, src, ts = next_headline(pool, items, seen)
|
|
ticker_content, hc, midx = make_block(t, src, ts, w)
|
|
active.append((ticker_content, hc, ticker_next_y, midx))
|
|
ticker_next_y += len(ticker_content) + GAP
|
|
|
|
active = [
|
|
(c, hc, by, mi)
|
|
for c, hc, by, mi in active
|
|
if by + len(c) > camera.y
|
|
]
|
|
for k in list(noise_cache):
|
|
if k < camera.y:
|
|
del noise_cache[k]
|
|
|
|
grad_offset = (time.time() * config.GRAD_SPEED) % 1.0
|
|
|
|
from engine.layers import render_ticker_zone
|
|
|
|
buf, noise_cache = render_ticker_zone(
|
|
active,
|
|
scroll_cam=camera.y,
|
|
camera_x=camera.x,
|
|
ticker_h=h,
|
|
w=w,
|
|
noise_cache=noise_cache,
|
|
grad_offset=grad_offset,
|
|
)
|
|
|
|
from engine.layers import render_firehose
|
|
|
|
firehose_buf = render_firehose(items, w, 0, h)
|
|
buf.extend(firehose_buf)
|
|
|
|
ctx = EffectContext(
|
|
terminal_width=w,
|
|
terminal_height=h,
|
|
scroll_cam=camera.y,
|
|
ticker_height=h,
|
|
camera_x=camera.x,
|
|
mic_excess=0.0,
|
|
grad_offset=grad_offset,
|
|
frame_number=frame_number,
|
|
has_message=False,
|
|
items=items,
|
|
)
|
|
|
|
result = chain.process(buf, ctx)
|
|
display.show(result)
|
|
|
|
new_w, new_h = display.get_dimensions()
|
|
if new_w != w or new_h != h:
|
|
w, h = new_w, new_h
|
|
scroll_step_interval = calculate_scroll_step(config.SCROLL_DUR, h)
|
|
active = []
|
|
noise_cache = {}
|
|
|
|
frame_number += 1
|
|
time.sleep(1 / 60)
|
|
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
display.cleanup()
|
|
print("\n \033[38;5;245mDemo ended\033[0m")
|
|
|
|
|
|
def main():
|
|
from engine import config
|
|
from engine.pipeline import generate_pipeline_diagram
|
|
|
|
if config.PIPELINE_DIAGRAM:
|
|
print(generate_pipeline_diagram())
|
|
return
|
|
|
|
if config.DEMO:
|
|
run_demo_mode()
|
|
return
|
|
|
|
atexit.register(lambda: print(CURSOR_ON, end="", flush=True))
|
|
|
|
def handle_sigint(*_):
|
|
print(f"\n\n {G_DIM}> SIGNAL LOST{RST}")
|
|
print(f" {W_GHOST}> connection terminated{RST}\n")
|
|
sys.exit(0)
|
|
|
|
signal.signal(signal.SIGINT, handle_sigint)
|
|
|
|
StreamController.warmup_topics()
|
|
|
|
w = tw()
|
|
print(CLR, end="")
|
|
print(CURSOR_OFF, end="")
|
|
pick_font_face()
|
|
pick_effects_config()
|
|
w = tw()
|
|
print()
|
|
time.sleep(0.4)
|
|
|
|
for ln in TITLE:
|
|
print(f"{G_HI}{ln}{RST}")
|
|
time.sleep(0.07)
|
|
|
|
print()
|
|
_subtitle = (
|
|
"literary consciousness stream"
|
|
if config.MODE == "poetry"
|
|
else "digital consciousness stream"
|
|
)
|
|
print(f" {W_DIM}v0.1 · {_subtitle}{RST}")
|
|
print(f" {W_GHOST}{'─' * (w - 4)}{RST}")
|
|
print()
|
|
time.sleep(0.4)
|
|
|
|
cached = load_cache() if "--refresh" not in sys.argv else None
|
|
if cached:
|
|
items = cached
|
|
boot_ln("Cache", f"LOADED [{len(items)} SIGNALS]", True)
|
|
elif config.MODE == "poetry":
|
|
slow_print(" > INITIALIZING LITERARY CORPUS...\n")
|
|
time.sleep(0.2)
|
|
print()
|
|
items, linked, failed = fetch_poetry()
|
|
print()
|
|
print(
|
|
f" {G_DIM}>{RST} {G_MID}{linked} TEXTS LOADED{RST} {W_GHOST}· {failed} DARK{RST}"
|
|
)
|
|
print(f" {G_DIM}>{RST} {G_MID}{len(items)} STANZAS ACQUIRED{RST}")
|
|
save_cache(items)
|
|
else:
|
|
slow_print(" > INITIALIZING FEED ARRAY...\n")
|
|
time.sleep(0.2)
|
|
print()
|
|
items, linked, failed = fetch_all()
|
|
print()
|
|
print(
|
|
f" {G_DIM}>{RST} {G_MID}{linked} SOURCES LINKED{RST} {W_GHOST}· {failed} DARK{RST}"
|
|
)
|
|
print(f" {G_DIM}>{RST} {G_MID}{len(items)} SIGNALS ACQUIRED{RST}")
|
|
save_cache(items)
|
|
|
|
if not items:
|
|
print(f"\n {W_DIM}> NO SIGNAL — check network{RST}")
|
|
sys.exit(1)
|
|
|
|
print()
|
|
controller = StreamController()
|
|
mic_ok, ntfy_ok = controller.initialize_sources()
|
|
|
|
if controller.mic and controller.mic.available:
|
|
boot_ln(
|
|
"Microphone",
|
|
"ACTIVE"
|
|
if mic_ok
|
|
else "OFFLINE · check System Settings → Privacy → Microphone",
|
|
bool(mic_ok),
|
|
)
|
|
|
|
boot_ln("ntfy", "LISTENING" if ntfy_ok else "OFFLINE", ntfy_ok)
|
|
|
|
if config.FIREHOSE:
|
|
boot_ln("Firehose", "ENGAGED", True)
|
|
|
|
time.sleep(0.4)
|
|
slow_print(" > STREAMING...\n")
|
|
time.sleep(0.2)
|
|
print(f" {W_GHOST}{'─' * (w - 4)}{RST}")
|
|
print()
|
|
time.sleep(0.4)
|
|
|
|
controller.run(items)
|
|
|
|
print()
|
|
print(f" {W_GHOST}{'─' * (tw() - 4)}{RST}")
|
|
print(f" {G_DIM}> {config.HEADLINE_LIMIT} SIGNALS PROCESSED{RST}")
|
|
print(f" {W_GHOST}> end of stream{RST}")
|
|
print()
|