#!/usr/bin/env python3 """Make the UP / page number / DOWN keys on every page follow the status page's ALL button. The status page (page 19) carries coloured copies of Companion's own nav keys (see make_page.py and nav_templates.json). This copies them over the built-in nav keys on every other page, wherever those sit, using the same "copy button" call as the Companion UI. Each key keeps its normal press action. python3 nav_keys.py --dry-run list what would change python3 nav_keys.py apply (also refreshes keys already converted) python3 nav_keys.py --revert put Companion's built-in nav keys back everywhere Only nav keys are touched. Every location changed is recorded in nav_locations.json. """ import argparse import gzip import json import os import urllib.error import urllib.request from make_page import NAV_LOCATIONS, NAV_MARKER from push_page import Trpc, WebSocket HERE = os.path.dirname(os.path.abspath(__file__)) RECORD = os.path.join(HERE, "nav_locations.json") NAV_TYPES = ("pageup", "pagenum", "pagedown") def page_export(host, port, page): url = "http://%s:%d/int/export/page/%d?includeSecrets=false" % (host, port, page) try: with urllib.request.urlopen(url, timeout=10) as r: raw = r.read() except urllib.error.HTTPError: return None try: raw = gzip.decompress(raw) except OSError: pass return json.loads(raw) def nav_kind(control): if control.get("type") in NAV_TYPES: return control["type"] notes = control.get("options", {}).get("notes", "") or "" if control.get("type") == "button-layered" and notes.startswith(NAV_MARKER): for kind in NAV_TYPES: if "(%s)" % kind in notes: return kind return None def find_nav_keys(host, port, status_page): """[(page, row, column, kind)] for every nav key outside the status page.""" found, page = [], 1 while True: export = page_export(host, port, page) if export is None: break if page != status_page: for r, cols in export["page"]["controls"].items(): for c, control in cols.items(): kind = nav_kind(control or {}) if kind: found.append((page, int(r), int(c), kind)) page += 1 return found def main(): cfg = json.load(open(os.path.join(HERE, "config.json"))) ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("--host", default="na-ingest.local") ap.add_argument("--port", type=int, default=8000) ap.add_argument("--dry-run", action="store_true") ap.add_argument("--revert", action="store_true") args = ap.parse_args() status_page = cfg["page"] record = json.load(open(RECORD)) if os.path.exists(RECORD) else {} if args.revert: targets = [(int(k.split("/")[0]), int(k.split("/")[1]), int(k.split("/")[2]), v) for k, v in record.items()] else: targets = find_nav_keys(args.host, args.port, status_page) print("%d nav keys on %d pages" % (len(targets), len({t[0] for t in targets}))) if args.dry_run: for t in targets: print(" page %d row %d col %d: %s" % t) return ws = WebSocket(args.host, args.port, "/trpc") done = 0 try: api = Trpc(ws) for page, row, col, kind in targets: location = {"pageNumber": page, "row": row, "column": col} if args.revert: api.mutation("controls.resetControl", {"location": location, "newType": kind}) record.pop("%d/%d/%d" % (page, row, col), None) else: r, c = NAV_LOCATIONS[kind] source = {"pageNumber": status_page, "row": int(r), "column": int(c)} if not api.mutation("controls.copyControl", {"fromLocation": source, "toLocation": location}): raise RuntimeError("copy to page %d row %d col %d failed" % (page, row, col)) record["%d/%d/%d" % (page, row, col)] = kind done += 1 finally: ws.close() json.dump(record, open(RECORD, "w"), indent=1, sort_keys=True) print("%s %d nav keys" % ("restored" if args.revert else "coloured", done)) if __name__ == "__main__": main()