#!/usr/bin/env python3
"""Drive the TUI in a pty and print each screen with ANSI stripped.
Usage: scripts/tui-tour.py [--root DIR] [--width N] [--height N] [--make-fixtures]
"""
import argparse
import fcntl
import os
import pty
import re
import select
import struct
import sys
import termios
import time
ANSI = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]")
OSC = re.compile(r"\x1b\][^\x07]*\x07")
CHARSET = re.compile(r"\x1b[()][A-Z0-9]")
def clean(data: bytes) -> list[str]:
"""Strip escapes and keep the last paint of each line."""
text = CHARSET.sub("", OSC.sub("", ANSI.sub("", data.decode("utf-8", "replace"))))
out: list[str] = []
for line in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
line = line.rstrip()
if line.strip() and (not out or out[-1] != line):
out.append(line)
return out
class Session:
def __init__(self, args):
self.args = args
self.pid, self.fd = pty.fork()
if self.pid == 0:
os.environ["INTAKE_THEME"] = args.theme
os.environ["TERM"] = "xterm-256color"
os.execv(args.binary, [
args.binary, "tui",
"--root", args.root,
"--state-dir", os.path.join(args.root, "state"),
])
fcntl.ioctl(self.fd, termios.TIOCSWINSZ,
struct.pack("HHHH", args.height, args.width, 0, 0))
def drain(self, seconds: float) -> bytes:
out = b""
end = time.time() + seconds
while time.time() < end:
ready, _, _ = select.select([self.fd], [], [], 0.25)
if ready:
try:
out += os.read(self.fd, 65536)
except OSError:
break
return out
def send(self, keys: bytes) -> bytes:
os.write(self.fd, keys)
return self.drain(self.args.wait)
def close(self):
try:
os.write(self.fd, b"q")
time.sleep(0.3)
except OSError:
pass
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--root", default="/tmp/intake-tour")
ap.add_argument("--binary", default="./out/intake")
ap.add_argument("--width", type=int, default=104)
ap.add_argument("--height", type=int, default=34)
ap.add_argument("--wait", type=float, default=3.0)
ap.add_argument("--theme", default="dark")
ap.add_argument("--make-fixtures", action="store_true",
help="generate fixtures into <root>/staging first")
args = ap.parse_args()
if args.make_fixtures:
os.makedirs(os.path.join(args.root, "staging"), exist_ok=True)
os.system(f"bash scripts/make-fixtures.sh {args.root}/staging >/dev/null 2>&1")
s = Session(args)
print(f"===== BROWSE ({args.width}x{args.height}) =====")
print("\n".join(clean(s.drain(4))))
for label, keys in [
("METADATA", b"\r"),
("TRACK NAMES", b"\r"),
("COVER ART", b"\r"),
("COVER PICKER", b"\r"),
]:
print(f"\n===== {label} =====")
print("\n".join(clean(s.send(keys))))
s.close()
return 0
if __name__ == "__main__":
sys.exit(main())