1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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()
|