280 lines
9.1 KiB
Python
280 lines
9.1 KiB
Python
"""
|
|
Application orchestrator — boot sequence, signal handling, main loop wiring.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import time
|
|
import signal
|
|
import atexit
|
|
import termios
|
|
import tty
|
|
|
|
from engine import config, render
|
|
from engine.terminal import (
|
|
RST, G_HI, G_MID, G_DIM, W_DIM, W_GHOST, CLR, CURSOR_OFF, CURSOR_ON, tw,
|
|
slow_print, boot_ln,
|
|
)
|
|
from engine.fetch import fetch_all, fetch_poetry, load_cache, save_cache
|
|
from engine.ntfy import NtfyPoller
|
|
from engine.mic import MicMonitor
|
|
from engine.scroll import stream
|
|
|
|
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_PATH[: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['index']}] {face['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']}{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 a font file."""
|
|
if not config.FONT_PICKER:
|
|
return
|
|
|
|
print(CLR, end="")
|
|
print(CURSOR_OFF, end="")
|
|
print()
|
|
print(f" {G_HI}FONT PICKER{RST}")
|
|
print(f" {W_GHOST}{'─' * (tw() - 4)}{RST}")
|
|
print(f" {W_DIM}{config.FONT_PATH}{RST}")
|
|
print()
|
|
|
|
try:
|
|
faces = render.list_font_faces(config.FONT_PATH, max_faces=64)
|
|
except Exception as exc:
|
|
print(f" {G_DIM}> unable to load font file: {exc}{RST}")
|
|
print(f" {W_GHOST}> startup aborted (font selection is required){RST}")
|
|
time.sleep(1.4)
|
|
sys.exit(1)
|
|
|
|
face_ids = [face["index"] for face in faces]
|
|
default = config.FONT_INDEX if config.FONT_INDEX in face_ids else faces[0]["index"]
|
|
|
|
prepared = []
|
|
for face in faces:
|
|
idx = face["index"]
|
|
name = face["name"]
|
|
try:
|
|
fnt = render.load_font_face(config.FONT_PATH, idx)
|
|
rows = _normalize_preview_rows(render.render_line(name, fnt))
|
|
except Exception:
|
|
rows = ["(preview unavailable)"]
|
|
prepared.append({"index": idx, "name": name, "preview_rows": rows})
|
|
|
|
selected = next((i for i, f in enumerate(prepared) if f["index"] == default), 0)
|
|
if not sys.stdin.isatty():
|
|
selected_index = prepared[selected]["index"]
|
|
config.set_font_selection(font_index=selected_index)
|
|
render.clear_font_cache()
|
|
print(f" {G_DIM}> using face {selected_index}{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 = prepared[selected]["index"]
|
|
|
|
config.set_font_selection(font_index=selected)
|
|
render.clear_font_cache()
|
|
print(f" {G_DIM}> using face {selected}{RST}")
|
|
time.sleep(0.8)
|
|
print(CLR, end="")
|
|
print(CURSOR_OFF, end="")
|
|
print()
|
|
|
|
|
|
def main():
|
|
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)
|
|
|
|
w = tw()
|
|
print(CLR, end="")
|
|
print(CURSOR_OFF, end="")
|
|
pick_font_face()
|
|
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()
|
|
mic = MicMonitor(threshold_db=config.MIC_THRESHOLD_DB)
|
|
mic_ok = mic.start()
|
|
if mic.available:
|
|
boot_ln("Microphone", "ACTIVE" if mic_ok else "OFFLINE · check System Settings → Privacy → Microphone", bool(mic_ok))
|
|
|
|
ntfy = NtfyPoller(
|
|
config.NTFY_TOPIC,
|
|
poll_interval=config.NTFY_POLL_INTERVAL,
|
|
display_secs=config.MESSAGE_DISPLAY_SECS,
|
|
)
|
|
ntfy_ok = ntfy.start()
|
|
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)
|
|
|
|
stream(items, ntfy, mic)
|
|
|
|
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()
|