aboutsummaryrefslogtreecommitdiffstats
path: root/bin/udt-tv
diff options
context:
space:
mode:
Diffstat (limited to 'bin/udt-tv')
-rwxr-xr-xbin/udt-tv83
1 files changed, 83 insertions, 0 deletions
diff --git a/bin/udt-tv b/bin/udt-tv
new file mode 100755
index 0000000..4406f7a
--- /dev/null
+++ b/bin/udt-tv
@@ -0,0 +1,83 @@
+#!/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. <danix@danix.xyz>
+# 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> <unix seconds> <hex
+HMAC-SHA256 of the first two fields>", 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": "<ip>"}), 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()