#!/usr/bin/env python3 """Check a fresh, physical arrows-first Steam Big Picture run of Cthulhu.""" import argparse import json from pathlib import Path def check_log(path: Path) -> None: records = [json.loads(line) for line in path.read_text().splitlines() if line.strip()] starts = [index for index, record in enumerate(records) if record.get("message") == "starting Cthulhu"] if not starts: raise ValueError("no Cthulhu startup in log") records = records[starts[-1]:] steam_pid = None steam_document = False first_link = None first_caret = None tab_focus = False home = power = confirm = cancel = False links_before_arrow = set() for record in records: message = record.get("message") if message == "X11 foreground ownership changed": pid = record.get("pid") or 0 if first_link is not None and pid and pid != steam_pid and not cancel: raise ValueError("foreground left Steam before the dialog check finished") if first_link is None and pid: steam_pid = pid continue if message == "suspended for XTerm focus handoff" and first_link is not None and not cancel: raise ValueError("focus switched away from Steam before the dialog check finished") if (message == "delivering accessible presentation" and record.get("role") == "document web" and record.get("utterance", "").startswith("Steam Big Picture Mode")): steam_document = True if message == "document caret navigation completed" and record.get("moved") is True: if first_link is not None and first_caret is None: first_caret = record.get("time") continue if message != "delivering accessible presentation" or record.get("cause") != "object:state-changed:focused": continue role = record.get("role") utterance = record.get("utterance", "") if role == "link" and utterance and steam_document: if first_link is None: first_link = record.get("time") links_before_arrow.add(utterance) elif first_caret is None: links_before_arrow.add(utterance) else: tab_focus = True if first_caret is None: continue if role == "menu item" and utterance == "Home, menu item": home = True elif role == "menu item" and utterance == "Power, menu item": power = home elif role == "button" and utterance == "Confirm, button": confirm = power elif role == "button" and utterance == "Cancel, button": cancel = confirm if cancel: break if not steam_document or first_link is None: raise ValueError("Steam never exposed its document and an initial focused link") if len(links_before_arrow) != 1: raise ValueError("focus moved between links before the first browse arrow; test arrows first") if first_caret is None: raise ValueError("no successful browse arrow after Steam became ready") if not tab_focus: raise ValueError("no focused link after the first arrow and Tab") if not home or not power: raise ValueError("Escape did not expose Home and Power") if not confirm or not cancel: raise ValueError("Power did not expose both Confirm and Cancel buttons") def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("log", type=Path, help="fresh Cthulhu debug log") arguments = parser.parse_args() try: check_log(arguments.log) except (OSError, ValueError, json.JSONDecodeError) as error: parser.exit(1, f"Steam Big Picture check failed: {error}\n") print("Steam Big Picture arrows-first focus, menu, and dialog check passed") if __name__ == "__main__": main()