feat(daemon): add daemon mode with C&C and display abstraction
- Add display abstraction with swappable backends (TerminalDisplay, NullDisplay) - Separate C&C into RX/TX topics for serial-like communication - Add StreamController with automatic ntfy topic warmup - Add performance monitoring for render + display stages - Update AGENTS.md and README.md with daemon/cmd operating procedures - Add clean/clobber tasks to mise.toml - Add tests for display, effects controller, and controller warmup - Fix bug where /effects reorder command was unreachable
This commit is contained in:
@@ -4,10 +4,16 @@ Orchestrates viewport, frame timing, and layers.
|
||||
"""
|
||||
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
|
||||
from engine import config
|
||||
from engine.display import (
|
||||
Display,
|
||||
TerminalDisplay,
|
||||
)
|
||||
from engine.display import (
|
||||
get_monitor as _get_display_monitor,
|
||||
)
|
||||
from engine.frame import calculate_scroll_step
|
||||
from engine.layers import (
|
||||
apply_glitch,
|
||||
@@ -16,24 +22,24 @@ from engine.layers import (
|
||||
render_message_overlay,
|
||||
render_ticker_zone,
|
||||
)
|
||||
from engine.terminal import CLR
|
||||
from engine.viewport import th, tw
|
||||
|
||||
USE_EFFECT_CHAIN = True
|
||||
|
||||
|
||||
def stream(items, ntfy_poller, mic_monitor):
|
||||
def stream(items, ntfy_poller, mic_monitor, display: Display | None = None):
|
||||
"""Main render loop with four layers: message, ticker, scroll motion, firehose."""
|
||||
if display is None:
|
||||
display = TerminalDisplay()
|
||||
random.shuffle(items)
|
||||
pool = list(items)
|
||||
seen = set()
|
||||
queued = 0
|
||||
|
||||
time.sleep(0.5)
|
||||
sys.stdout.write(CLR)
|
||||
sys.stdout.flush()
|
||||
|
||||
w, h = tw(), th()
|
||||
display.init(w, h)
|
||||
display.clear()
|
||||
fh = config.FIREHOSE_H if config.FIREHOSE else 0
|
||||
ticker_view_h = h - fh
|
||||
GAP = 3
|
||||
@@ -97,6 +103,7 @@ def stream(items, ntfy_poller, mic_monitor):
|
||||
buf.extend(ticker_buf)
|
||||
|
||||
mic_excess = mic_monitor.excess
|
||||
render_start = time.perf_counter()
|
||||
|
||||
if USE_EFFECT_CHAIN:
|
||||
buf = process_effects(
|
||||
@@ -119,12 +126,16 @@ def stream(items, ntfy_poller, mic_monitor):
|
||||
if msg_overlay:
|
||||
buf.extend(msg_overlay)
|
||||
|
||||
sys.stdout.buffer.write("".join(buf).encode())
|
||||
sys.stdout.flush()
|
||||
render_elapsed = (time.perf_counter() - render_start) * 1000
|
||||
monitor = _get_display_monitor()
|
||||
if monitor:
|
||||
chars = sum(len(line) for line in buf)
|
||||
monitor.record_effect("render", render_elapsed, chars, chars)
|
||||
|
||||
display.show(buf)
|
||||
|
||||
elapsed = time.monotonic() - t0
|
||||
time.sleep(max(0, config.FRAME_DT - elapsed))
|
||||
frame_number += 1
|
||||
|
||||
sys.stdout.write(CLR)
|
||||
sys.stdout.flush()
|
||||
display.cleanup()
|
||||
|
||||
Reference in New Issue
Block a user