git/intake/main
scripts/tui-tour.py
107 lines3 KB last change raw · history
#!/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())