#!/usr/bin/env python3 """ simulate.py — Preview lightshows in the terminal without hardware. Reads the same .txt show files the Arduino firmware uses and runs the same playback logic: blending, step timing, loop/single mode, and SHOW_SINGLE auto-advance. Controls: → or Space next show ← previous show r reset to show 0 (home) q / Ctrl-C quit SPDX-License-Identifier: BSD-2-Clause """ import sys import select import termios import time import tty from pathlib import Path # Reuse the show parser from the converter. sys.path.insert(0, str(Path(__file__).parent)) from convert_all import parse_show_file, SHOWS_DIR, HOME_SHOW # How many colored blocks to draw for the strip preview. STRIP_WIDTH = 44 # ---- Show loading ---------------------------------------------------------- def load_shows() -> list[dict]: txt_files = sorted(SHOWS_DIR.glob("*.txt"), key=lambda p: p.stem.lower()) home = SHOWS_DIR / f"{HOME_SHOW}.txt" others = [f for f in txt_files if f.stem != HOME_SHOW] ordered = ([home] + others) if home.exists() else txt_files shows = [] for path in ordered: try: steps, mode = parse_show_file(path) shows.append({"name": path.stem, "steps": steps, "mode": mode}) except ValueError as e: print(f" Warning: {e}", file=sys.stderr) return shows # ---- Playback -------------------------------------------------------------- def blend_color(c1: tuple, c2: tuple, t: float) -> tuple: return tuple(int(a + (b - a) * t) for a, b in zip(c1, c2)) # ---- Terminal rendering ---------------------------------------------------- RESET = "\x1b[0m" def ansi_bg(r: int, g: int, b: int) -> str: return f"\x1b[48;2;{r};{g};{b}m" def render(color: tuple, show_name: str, step_idx: int, step_count: int, mode: str) -> None: r, g, b = color # Dim the foreground text so it's readable on any background. bar = ansi_bg(r, g, b) + " " * STRIP_WIDTH + RESET name = show_name.replace("_", " ") info = f" {name} [{mode}] step {step_idx + 1}/{step_count} #{r:02X}{g:02X}{b:02X}" sys.stdout.write(f"\r{bar}{info} ") sys.stdout.flush() # ---- Non-blocking keyboard input ------------------------------------------- def get_key() -> str | None: if not select.select([sys.stdin], [], [], 0)[0]: return None ch = sys.stdin.read(1) if ch == "\x1b": more = select.select([sys.stdin], [], [], 0.02)[0] rest = sys.stdin.read(2) if more else "" if rest == "[C": return "right" if rest == "[D": return "left" return "esc" return ch # ---- Main ------------------------------------------------------------------ def main() -> None: shows = load_shows() if not shows: print(f"No .txt files found in {SHOWS_DIR}") sys.exit(1) print(f"Loaded {len(shows)} show(s). Controls: → next ← prev r reset q quit\n") # Playback state — mirrors the Arduino sketch variables. show_idx = 0 step_idx = 0 step_start = time.monotonic() from_color = (0, 0, 0) def switch_show(idx: int) -> None: nonlocal show_idx, step_idx, step_start, from_color show_idx = idx % len(shows) step_idx = 0 step_start = time.monotonic() from_color = (0, 0, 0) if not sys.stdin.isatty(): print("simulate.py must be run in an interactive terminal.", file=sys.stderr) sys.exit(1) old_settings = termios.tcgetattr(sys.stdin) try: tty.setraw(sys.stdin.fileno()) while True: # ---- Button input ---- key = get_key() if key in ("q", "\x03"): # q or Ctrl-C break elif key in ("right", " "): switch_show(show_idx + 1) elif key == "left": switch_show(show_idx - 1) elif key in ("r", "R"): switch_show(0) # ---- Playback ---- show = shows[show_idx] steps = show["steps"] mode = show["mode"] r, g, b, duration_ms = steps[step_idx] to = (r, g, b) now = time.monotonic() elapsed_ms = (now - step_start) * 1000.0 if duration_ms == 0: t = 1.0 else: t = min(elapsed_ms / duration_ms, 1.0) render(blend_color(from_color, to, t), show["name"], step_idx, len(steps), mode) # ---- Advance step when complete ---- if t >= 1.0: from_color = to next_step = step_idx + 1 if next_step >= len(steps): if mode == "SHOW_SINGLE": switch_show(show_idx + 1) else: step_idx = 0 step_start = time.monotonic() else: step_idx = next_step step_start = time.monotonic() time.sleep(0.016) # ~60 fps finally: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) sys.stdout.write(f"\r{RESET}\n") sys.stdout.flush() if __name__ == "__main__": main()