#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-only # udt-tv: send the wallpaper accent to the tv-debloat launcher on the TV. # Copyright (C) 2026 Danilo M. # Licensed under the GNU General Public License v2 only. """Tint the TV launcher's focus highlight, pills and title glow in the accent. Usage: udt-tv send the window border's three hues udt-tv --selftest The colours are the three udt-accent wrote for the window border, the accent between its two hue neighbours: the launcher draws its focus highlight's edge as a gradient of all three and uses the middle one everywhere else. One UDP datagram to the launcher's port 47800, "<#a,#b,#c> ", keyed with the secret the launcher was deployed with. The launcher drops anything unsigned, more than a minute off its clock, or not newer than the last one it took, and sends no reply, so this cannot tell whether the TV took it (a TV in standby or a launcher that Android killed misses it; the launcher keeps the last colour it got). The TV's address comes from ~/.config/udt/tv.json ({"host": ""}), the secret from ~/.config/tv-debloat/accent.secret, the file tv-debloat's deploy-launcher.sh pushes to the TV. Without either, this does nothing. """ import hashlib import hmac import json import re import socket import sys import time from pathlib import Path CONFIG = Path.home() / ".config/udt/tv.json" SECRET = Path.home() / ".config/tv-debloat/accent.secret" BORDER = Path.home() / ".cache/udt/udt-border.lua" PORT = 47800 def colours(border): """The border's rgb(rrggbb) hues as the launcher's "#a,#b,#c" field.""" return ",".join("#" + h for h in re.findall(r"rgb\(([0-9a-fA-F]{6})\)", border)) def message(field, secret, now): body = f"{field} {now}" return f"{body} {hmac.new(secret, body.encode(), hashlib.sha256).hexdigest()}" def selftest(): border = 'return { "rgb(fcd317)", "rgb(36d190)", "rgb(37daea)" }' assert colours(border) == "#fcd317,#36d190,#37daea" msg = message("#fcd317,#36d190,#37daea", b"k", 1700000000) field, ts, mac = msg.split(" ") assert (field, ts) == ("#fcd317,#36d190,#37daea", "1700000000") assert mac == hmac.new(b"k", f"{field} {ts}".encode(), hashlib.sha256).hexdigest() print("ok") def main(): args = sys.argv[1:] if args == ["--selftest"]: return selftest() if args: sys.exit(__doc__.split("\n\n")[1]) try: host = json.loads(CONFIG.read_text())["host"] secret = SECRET.read_text().strip().encode() field = colours(BORDER.read_text()) except (OSError, ValueError, KeyError): return # The launcher takes one colour or three; anything else it would drop. if field.count(",") not in (0, 2) or not field or not secret: return with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: try: s.sendto(message(field, secret, int(time.time())).encode(), (host, PORT)) except OSError as e: print(f"udt-tv: {host}: {e}", file=sys.stderr) if __name__ == "__main__": main()