Only the page number key follows the ALL button; UP and DOWN are normal again.

New config setting nav_keys (now ["pagenum"]) picks which nav keys are
coloured; nav_keys.py restores the built-in key for any kind not listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 09:44:03 -04:00
co-authored by Claude Opus 5.5
parent 3118cdc281
commit 1da404c299
6 changed files with 32 additions and 210 deletions
+1
View File
@@ -2,6 +2,7 @@
"companion": { "host": "127.0.0.1", "port": 8000 }, "companion": { "host": "127.0.0.1", "port": 8000 },
"page": 19, "page": 19,
"page_name": "19\\nStatus", "page_name": "19\\nStatus",
"nav_keys": ["pagenum"],
"variables": { "state": "sp_state", "heartbeat": "sp_hb", "command": "sp_cmd" }, "variables": { "state": "sp_state", "heartbeat": "sp_hb", "command": "sp_cmd" },
"service_windows": [ "service_windows": [
+2 -1
View File
@@ -162,8 +162,9 @@ def nav_key(kind, names):
def build(cfg): def build(cfg):
controls = {} controls = {}
coloured = cfg.get("nav_keys", list(NAV_LOCATIONS)) # which nav keys follow the ALL button
for kind, (r, c) in NAV_LOCATIONS.items(): for kind, (r, c) in NAV_LOCATIONS.items():
controls.setdefault(r, {})[c] = nav_key(kind, cfg["variables"]) controls.setdefault(r, {})[c] = nav_key(kind, cfg["variables"]) if kind in coloured else {"type": kind}
for tile in cfg["tiles"]: for tile in cfg["tiles"]:
if "pos" not in tile: if "pos" not in tile:
continue continue
+24 -14
View File
@@ -1,10 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Make the UP / page number / DOWN keys on every page follow the status page's ALL button. """Make the page nav keys on every page follow the status page's ALL button.
Which keys follow it is "nav_keys" in config.json (now just the page number).
The status page (page 19) carries coloured copies of Companion's own nav keys 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 (see make_page.py and nav_templates.json). This copies those over the built-in
nav keys on every other page, wherever those sit, using the same "copy button" keys on every other page, using the same "copy button" call as the Companion UI,
call as the Companion UI. Each key keeps its normal press action. and puts back the built-in key for any kind no longer listed. Each key keeps
its normal press action.
python3 nav_keys.py --dry-run list what would change python3 nav_keys.py --dry-run list what would change
python3 nav_keys.py apply (also refreshes keys already converted) python3 nav_keys.py apply (also refreshes keys already converted)
@@ -52,8 +54,12 @@ def nav_kind(control):
return None return None
def is_ours(control):
return (control.get("options", {}).get("notes", "") or "").startswith(NAV_MARKER)
def find_nav_keys(host, port, status_page): def find_nav_keys(host, port, status_page):
"""[(page, row, column, kind)] for every nav key outside the status page.""" """[(page, row, column, kind, ours)] for every nav key outside the status page."""
found, page = [], 1 found, page = [], 1
while True: while True:
export = page_export(host, port, page) export = page_export(host, port, page)
@@ -64,7 +70,7 @@ def find_nav_keys(host, port, status_page):
for c, control in cols.items(): for c, control in cols.items():
kind = nav_kind(control or {}) kind = nav_kind(control or {})
if kind: if kind:
found.append((page, int(r), int(c), kind)) found.append((page, int(r), int(c), kind, is_ours(control)))
page += 1 page += 1
return found return found
@@ -79,24 +85,28 @@ def main():
args = ap.parse_args() args = ap.parse_args()
status_page = cfg["page"] status_page = cfg["page"]
coloured = cfg.get("nav_keys", list(NAV_LOCATIONS))
record = json.load(open(RECORD)) if os.path.exists(RECORD) else {} record = json.load(open(RECORD)) if os.path.exists(RECORD) else {}
found = find_nav_keys(args.host, args.port, status_page)
if args.revert: if args.revert:
targets = [(int(k.split("/")[0]), int(k.split("/")[1]), int(k.split("/")[2]), v) for k, v in record.items()] work = [(p, r, c, k, "restore") for p, r, c, k, ours in found if ours]
else: else:
targets = find_nav_keys(args.host, args.port, status_page) work = [(p, r, c, k, "colour") for p, r, c, k, ours in found if k in coloured]
print("%d nav keys on %d pages" % (len(targets), len({t[0] for t in targets}))) work += [(p, r, c, k, "restore") for p, r, c, k, ours in found if ours and k not in coloured]
print("%d nav keys to colour, %d to restore, on %d pages" % (
sum(1 for w in work if w[4] == "colour"), sum(1 for w in work if w[4] == "restore"), len({w[0] for w in work})))
if args.dry_run: if args.dry_run:
for t in targets: for w in work:
print(" page %d row %d col %d: %s" % t) print(" page %d row %d col %d: %s -> %s" % w)
return return
ws = WebSocket(args.host, args.port, "/trpc") ws = WebSocket(args.host, args.port, "/trpc")
done = 0 done = 0
try: try:
api = Trpc(ws) api = Trpc(ws)
for page, row, col, kind in targets: for page, row, col, kind, what in work:
location = {"pageNumber": page, "row": row, "column": col} location = {"pageNumber": page, "row": row, "column": col}
if args.revert: if what == "restore":
api.mutation("controls.resetControl", {"location": location, "newType": kind}) api.mutation("controls.resetControl", {"location": location, "newType": kind})
record.pop("%d/%d/%d" % (page, row, col), None) record.pop("%d/%d/%d" % (page, row, col), None)
else: else:
@@ -109,7 +119,7 @@ def main():
finally: finally:
ws.close() ws.close()
json.dump(record, open(RECORD, "w"), indent=1, sort_keys=True) json.dump(record, open(RECORD, "w"), indent=1, sort_keys=True)
print("%s %d nav keys" % ("restored" if args.revert else "coloured", done)) print("done: %d nav keys changed" % done)
if __name__ == "__main__": if __name__ == "__main__":
+1 -193
View File
@@ -1,290 +1,98 @@
{ {
"1/0/0": "pageup",
"1/1/0": "pagenum", "1/1/0": "pagenum",
"1/2/0": "pagedown",
"10/0/0": "pageup",
"10/1/0": "pagenum", "10/1/0": "pagenum",
"10/2/0": "pagedown",
"11/0/0": "pageup",
"11/1/0": "pagenum", "11/1/0": "pagenum",
"11/2/0": "pagedown",
"12/0/0": "pageup",
"12/1/0": "pagenum", "12/1/0": "pagenum",
"12/2/0": "pagedown",
"13/0/0": "pageup",
"13/1/0": "pagenum", "13/1/0": "pagenum",
"13/2/0": "pagedown",
"14/0/0": "pageup",
"14/1/0": "pagenum", "14/1/0": "pagenum",
"14/2/0": "pagedown",
"15/0/0": "pageup",
"15/0/1": "pagenum", "15/0/1": "pagenum",
"15/1/0": "pagenum", "15/1/0": "pagenum",
"15/2/0": "pagedown",
"16/0/0": "pageup",
"16/1/0": "pagenum", "16/1/0": "pagenum",
"16/2/0": "pagedown",
"17/0/0": "pageup",
"17/1/0": "pagenum", "17/1/0": "pagenum",
"17/2/0": "pagedown",
"18/0/0": "pageup",
"18/1/0": "pagenum", "18/1/0": "pagenum",
"18/2/0": "pagedown",
"2/0/0": "pageup",
"2/1/0": "pagenum", "2/1/0": "pagenum",
"2/2/0": "pagedown",
"20/0/0": "pageup",
"20/1/0": "pagenum", "20/1/0": "pagenum",
"20/2/0": "pagedown",
"21/0/0": "pageup",
"21/1/0": "pagenum", "21/1/0": "pagenum",
"21/2/0": "pagedown",
"22/0/0": "pageup",
"22/1/0": "pagenum", "22/1/0": "pagenum",
"22/2/0": "pagedown",
"23/0/0": "pageup",
"23/1/0": "pagenum", "23/1/0": "pagenum",
"23/2/0": "pagedown",
"24/0/0": "pageup",
"24/1/0": "pagenum", "24/1/0": "pagenum",
"24/2/0": "pagedown",
"25/0/0": "pageup",
"25/1/0": "pagenum", "25/1/0": "pagenum",
"25/2/0": "pagedown",
"26/0/0": "pageup",
"26/1/0": "pagenum", "26/1/0": "pagenum",
"26/2/0": "pagedown",
"27/0/0": "pageup",
"27/1/0": "pagenum", "27/1/0": "pagenum",
"27/2/0": "pagedown",
"28/0/0": "pageup",
"28/1/0": "pagenum", "28/1/0": "pagenum",
"28/2/0": "pagedown",
"29/0/0": "pageup",
"29/1/0": "pagenum", "29/1/0": "pagenum",
"29/2/0": "pagedown",
"3/0/0": "pageup",
"3/1/0": "pagenum", "3/1/0": "pagenum",
"3/2/0": "pagedown",
"30/0/0": "pageup",
"30/1/0": "pagenum", "30/1/0": "pagenum",
"30/2/0": "pagedown",
"31/0/0": "pageup",
"31/1/0": "pagenum", "31/1/0": "pagenum",
"31/2/0": "pagedown",
"32/0/0": "pageup",
"32/1/0": "pagenum", "32/1/0": "pagenum",
"32/2/0": "pagedown",
"33/0/0": "pageup",
"33/1/0": "pagenum", "33/1/0": "pagenum",
"33/2/0": "pagedown",
"34/0/0": "pageup",
"34/1/0": "pagenum", "34/1/0": "pagenum",
"34/2/0": "pagedown",
"35/0/0": "pageup",
"35/1/0": "pagenum", "35/1/0": "pagenum",
"35/2/0": "pagedown",
"36/0/0": "pageup",
"36/1/0": "pagenum", "36/1/0": "pagenum",
"36/2/0": "pagedown",
"37/0/0": "pageup",
"37/1/0": "pagenum", "37/1/0": "pagenum",
"37/2/0": "pagedown",
"38/0/0": "pageup",
"38/1/0": "pagenum", "38/1/0": "pagenum",
"38/2/0": "pagedown",
"39/0/0": "pageup",
"39/1/0": "pagenum", "39/1/0": "pagenum",
"39/2/0": "pagedown",
"4/0/0": "pageup",
"4/1/0": "pagenum", "4/1/0": "pagenum",
"4/2/0": "pagedown",
"40/0/0": "pageup",
"40/1/0": "pagenum", "40/1/0": "pagenum",
"40/2/0": "pagedown",
"41/0/0": "pageup",
"41/1/0": "pagenum", "41/1/0": "pagenum",
"41/2/0": "pagedown",
"42/0/0": "pageup",
"42/1/0": "pagenum", "42/1/0": "pagenum",
"42/2/0": "pagedown",
"43/0/0": "pageup",
"43/1/0": "pagenum", "43/1/0": "pagenum",
"43/2/0": "pagedown",
"44/0/0": "pageup",
"44/1/0": "pagenum", "44/1/0": "pagenum",
"44/2/0": "pagedown",
"45/0/0": "pageup",
"45/1/0": "pagenum", "45/1/0": "pagenum",
"45/2/0": "pagedown",
"46/0/0": "pageup",
"46/1/0": "pagenum", "46/1/0": "pagenum",
"46/2/0": "pagedown",
"47/0/0": "pageup",
"47/1/0": "pagenum", "47/1/0": "pagenum",
"47/2/0": "pagedown",
"48/0/0": "pageup",
"48/1/0": "pagenum", "48/1/0": "pagenum",
"48/2/0": "pagedown",
"49/0/0": "pageup",
"49/1/0": "pagenum", "49/1/0": "pagenum",
"49/2/0": "pagedown",
"5/0/0": "pageup",
"5/1/0": "pagenum", "5/1/0": "pagenum",
"5/2/0": "pagedown",
"50/0/0": "pageup",
"50/1/0": "pagenum", "50/1/0": "pagenum",
"50/2/0": "pagedown",
"51/0/0": "pageup",
"51/1/0": "pagenum", "51/1/0": "pagenum",
"51/2/0": "pagedown",
"52/0/0": "pageup",
"52/1/0": "pagenum", "52/1/0": "pagenum",
"52/2/0": "pagedown",
"53/0/0": "pageup",
"53/1/0": "pagenum", "53/1/0": "pagenum",
"53/2/0": "pagedown",
"54/0/0": "pageup",
"54/1/0": "pagenum", "54/1/0": "pagenum",
"54/2/0": "pagedown",
"55/0/0": "pageup",
"55/1/0": "pagenum", "55/1/0": "pagenum",
"55/2/0": "pagedown",
"56/0/0": "pageup",
"56/1/0": "pagenum", "56/1/0": "pagenum",
"56/2/0": "pagedown",
"57/0/0": "pageup",
"57/1/0": "pagenum", "57/1/0": "pagenum",
"57/2/0": "pagedown",
"58/0/0": "pageup",
"58/1/0": "pagenum", "58/1/0": "pagenum",
"58/2/0": "pagedown",
"59/0/0": "pageup",
"59/1/0": "pagenum", "59/1/0": "pagenum",
"59/2/0": "pagedown",
"6/0/0": "pageup",
"6/1/0": "pagenum", "6/1/0": "pagenum",
"6/2/0": "pagedown",
"60/0/0": "pageup",
"60/1/0": "pagenum", "60/1/0": "pagenum",
"60/2/0": "pagedown",
"61/0/0": "pageup",
"61/1/0": "pagenum", "61/1/0": "pagenum",
"61/2/0": "pagedown",
"62/0/0": "pageup",
"62/1/0": "pagenum", "62/1/0": "pagenum",
"62/2/0": "pagedown",
"63/0/0": "pageup",
"63/1/0": "pagenum", "63/1/0": "pagenum",
"63/2/0": "pagedown",
"64/0/0": "pageup",
"64/1/0": "pagenum", "64/1/0": "pagenum",
"64/2/0": "pagedown",
"65/0/0": "pageup",
"65/1/0": "pagenum", "65/1/0": "pagenum",
"65/2/0": "pagedown",
"66/0/0": "pageup",
"66/1/0": "pagenum", "66/1/0": "pagenum",
"66/2/0": "pagedown",
"67/0/0": "pageup",
"67/1/0": "pagenum", "67/1/0": "pagenum",
"67/2/0": "pagedown",
"68/0/0": "pageup",
"68/1/0": "pagenum", "68/1/0": "pagenum",
"68/2/0": "pagedown",
"69/0/0": "pageup",
"69/1/0": "pagenum", "69/1/0": "pagenum",
"69/2/0": "pagedown",
"7/0/0": "pageup",
"7/1/0": "pagenum", "7/1/0": "pagenum",
"7/2/0": "pagedown",
"70/0/0": "pageup",
"70/1/0": "pagenum", "70/1/0": "pagenum",
"70/2/0": "pagedown",
"71/0/0": "pageup",
"71/1/0": "pagenum", "71/1/0": "pagenum",
"71/2/0": "pagedown",
"72/0/0": "pageup",
"72/1/0": "pagenum", "72/1/0": "pagenum",
"72/2/0": "pagedown",
"73/0/0": "pageup",
"73/1/0": "pagenum", "73/1/0": "pagenum",
"73/2/0": "pagedown",
"74/0/0": "pageup",
"74/1/0": "pagenum", "74/1/0": "pagenum",
"74/2/0": "pagedown",
"75/0/0": "pageup",
"75/1/0": "pagenum", "75/1/0": "pagenum",
"75/2/0": "pagedown",
"76/0/0": "pageup",
"76/1/0": "pagenum", "76/1/0": "pagenum",
"76/2/0": "pagedown",
"77/0/0": "pageup",
"77/1/0": "pagenum", "77/1/0": "pagenum",
"77/2/0": "pagedown",
"78/0/0": "pageup",
"78/1/0": "pagenum", "78/1/0": "pagenum",
"78/2/0": "pagedown",
"79/0/0": "pageup",
"79/1/0": "pagenum", "79/1/0": "pagenum",
"79/2/0": "pagedown",
"8/0/0": "pageup",
"8/1/0": "pagenum", "8/1/0": "pagenum",
"8/2/0": "pagedown",
"80/0/0": "pageup",
"80/1/0": "pagenum", "80/1/0": "pagenum",
"80/2/0": "pagedown",
"81/0/0": "pageup",
"81/1/0": "pagenum", "81/1/0": "pagenum",
"81/2/0": "pagedown",
"82/0/0": "pageup",
"82/1/0": "pagenum", "82/1/0": "pagenum",
"82/2/0": "pagedown",
"83/0/0": "pageup",
"83/1/0": "pagenum", "83/1/0": "pagenum",
"83/2/0": "pagedown",
"84/0/0": "pageup",
"84/1/0": "pagenum", "84/1/0": "pagenum",
"84/2/0": "pagedown",
"85/0/0": "pageup",
"85/1/0": "pagenum", "85/1/0": "pagenum",
"85/2/0": "pagedown",
"86/0/0": "pageup",
"86/1/0": "pagenum", "86/1/0": "pagenum",
"86/2/0": "pagedown",
"87/0/0": "pageup",
"87/1/0": "pagenum", "87/1/0": "pagenum",
"87/2/0": "pagedown",
"88/0/0": "pageup",
"88/1/0": "pagenum", "88/1/0": "pagenum",
"88/2/0": "pagedown",
"89/0/0": "pageup",
"89/1/0": "pagenum", "89/1/0": "pagenum",
"89/2/0": "pagedown",
"9/0/0": "pageup",
"9/1/0": "pagenum", "9/1/0": "pagenum",
"9/2/0": "pagedown",
"90/0/0": "pageup",
"90/1/0": "pagenum", "90/1/0": "pagenum",
"90/2/0": "pagedown",
"91/0/0": "pageup",
"91/1/0": "pagenum", "91/1/0": "pagenum",
"91/2/0": "pagedown",
"92/0/0": "pageup",
"92/1/0": "pagenum", "92/1/0": "pagenum",
"92/2/0": "pagedown",
"93/0/0": "pageup",
"93/1/0": "pagenum", "93/1/0": "pagenum",
"93/2/0": "pagedown",
"94/0/0": "pageup",
"94/1/0": "pagenum", "94/1/0": "pagenum",
"94/2/0": "pagedown",
"95/0/0": "pageup",
"95/1/0": "pagenum", "95/1/0": "pagenum",
"95/2/0": "pagedown", "97/1/0": "pagenum"
"96/0/0": "pageup",
"96/2/0": "pagedown",
"97/0/0": "pageup",
"97/1/0": "pagenum",
"97/2/0": "pagedown"
} }
Binary file not shown.
+4 -2
View File
@@ -555,7 +555,9 @@ class TestPage(unittest.TestCase):
def test_layout(self): def test_layout(self):
self.assertEqual(self.export["type"], "page") self.assertEqual(self.export["type"], "page")
self.assertEqual(self.export["oldPageNumber"], 19) self.assertEqual(self.export["oldPageNumber"], 19)
for (r, kind, action) in (("0", "pageup", "inc_page"), ("1", "pagenum", "set_page"), ("2", "pagedown", "dec_page")): self.assertEqual(self.controls["0"]["0"], {"type": "pageup"}) # only the page number follows ALL
self.assertEqual(self.controls["2"]["0"], {"type": "pagedown"})
for (r, kind, action) in (("1", "pagenum", "set_page"),):
key = self.controls[r]["0"] key = self.controls[r]["0"]
self.assertEqual(key["type"], "button-layered") self.assertEqual(key["type"], "button-layered")
self.assertTrue(key["options"]["notes"].startswith("STATUS nav key (%s)" % kind)) self.assertTrue(key["options"]["notes"].startswith("STATUS nav key (%s)" % kind))
@@ -565,7 +567,7 @@ class TestPage(unittest.TestCase):
self.assertIn("'$.all.b'", box["color"]["value"]) # follows the ALL button self.assertIn("'$.all.b'", box["color"]["value"]) # follows the ALL button
positioned = [t for t in CONFIG["tiles"] if "pos" in t] positioned = [t for t in CONFIG["tiles"] if "pos" in t]
n = sum(1 for r in self.controls.values() for c in r.values() if c["type"] == "button-layered") n = sum(1 for r in self.controls.values() for c in r.values() if c["type"] == "button-layered")
self.assertEqual(n, len(positioned) + 3) # + the three nav keys self.assertEqual(n, len(positioned) + 1) # + the page number key
# The master is where the smallest Stream Deck can see it. # The master is where the smallest Stream Deck can see it.
self.assertEqual(CONFIG["tiles"][0]["pos"], [0, 1]) self.assertEqual(CONFIG["tiles"][0]["pos"], [0, 1])