from __future__ import annotations
import argparse
import json
import sys
import py2tosc
from py2tosc import Conversion, Orientation, TriggerCondition, ui
FRAME = (0, 0, 1024, 768)
GAP = 8
ACCENT = "#4fc3cf"
LEVEL = "#5cb85c"
INK = "#e8e8e8"
def sends(address: str, *, args=None, on: str = "ANY") -> py2tosc.OscMessage:
return ui.osc(address, args=args, on=on, receive=False)
def mirrors(address: str, *, args=None) -> py2tosc.OscMessage:
return ui.osc(address, args=args)
def shows(address: str, *, args=None) -> py2tosc.OscMessage:
return ui.osc(address, args=args, triggers=[], send=False)
def label(name: str, address: str, size: int, placeholder: str) -> py2tosc.Control:
control = py2tosc.label(
name=name,
text_size=size,
text_color=INK,
messages=[shows(address, args=[ui.value("text", conversion=Conversion.STRING)])],
)
control.value("text").default = placeholder
return control
def captioned(control: py2tosc.Control, text: str, size: int) -> py2tosc.Control:
group = ui.labelled(control, text, size=size)
group[1].text_color = INK
return group
def fader(name: str, message: py2tosc.OscMessage, **props) -> py2tosc.Control:
props.setdefault("color", ACCENT)
return py2tosc.fader(
name=name, orientation=Orientation.EAST, messages=[message], **props
)
def radio(name: str, message: py2tosc.OscMessage, words: list[str], **props) -> py2tosc.Control:
control = py2tosc.radio(
name=name,
steps=len(words),
orientation=Orientation.EAST,
color=ACCENT,
messages=[message],
**props,
)
captions = [
py2tosc.label(
name=word, background=False, interactive=False, text_size=20, text_color=INK
)
for word in words
]
for caption, word in zip(captions, words):
caption.value("text").default = word
return ui.stack(control, ui.row(*captions, name=f"{name} captions"), name=f"{name} group")
def index(i: int) -> list:
return [ui.const(str(i), conversion=Conversion.FLOAT)]
def build(playlists: int) -> py2tosc.Document:
integer = [ui.value("x", conversion=Conversion.INTEGER)]
now = ui.column(
label("title", "/playr/title", 36, "playr"),
label("artist", "/playr/artist", 24, ""),
ui.row(
label("time", "/playr/time", 20, "0:00 / --"),
radio(
"state",
shows("/playr/state", args=integer),
["Stopped", "Playing", "Paused"],
interactive=False,
),
sizes=(1, 2),
gap=GAP,
),
sizes=(2, 1, 1),
name="now playing",
)
transport = ui.row(
[
captioned(
py2tosc.button(name=word.lower(), color=ACCENT, messages=[sends(address)]),
word,
24,
)
for word, address in [
("Prev", "/playr/prev"),
("Pause", "/playr/pause"),
("Stop", "/playr/stop"),
("Next", "/playr/next"),
]
],
gap=GAP,
name="transport",
)
settings = ui.column(
captioned(fader("volume", mirrors("/playr/volume")), "Volume", 20),
captioned(
fader(
"speed",
mirrors("/playr/speed", args=[ui.value("x", scale=(-12.0, 12.0))]),
centered=True,
),
"Speed",
20,
),
radio(
"mode",
mirrors("/playr/mode", args=integer),
["Normal", "Shuffle", "Repeat", "Repeat one"],
),
gap=GAP,
name="settings",
)
lists = ui.tiles(
[
captioned(
py2tosc.button(
name=f"playlist {i + 1}",
color=ACCENT,
messages=[sends("/playr/playlist", args=index(i), on=TriggerCondition.RISE)],
),
str(i + 1),
24,
)
for i in range(playlists)
],
columns=min(playlists, 8),
gap=GAP,
name="playlists",
)
root = ui.column(
now,
fader("progress", mirrors("/playr/progress")),
fader("level", shows("/playr/level"), interactive=False, color=LEVEL),
transport,
settings,
lists,
sizes=(3, 1, 0.4, 1.5, 3, 1.5),
gap=GAP,
pad=GAP,
frame=FRAME,
name="playr",
)
doc = py2tosc.Document(root=root)
doc.resolve()
return doc
def addresses(doc: py2tosc.Document) -> tuple[set[str], set[str]]:
sent, taken = set(), set()
for control in doc.walk():
for message in control.messages:
if not isinstance(message, py2tosc.OscMessage):
continue
address = "".join(p.value for p in message.path)
if message.send:
sent.add(address)
if message.receive:
taken.add(address)
return sent, taken
def mismatches(schema: dict, doc: py2tosc.Document) -> list[str]:
sent, taken = addresses(doc)
received = {a["address"] for a in schema["received"]}
replies = {a["address"] for a in schema["sent"]}
problems = []
for address in sorted(received - sent):
problems.append(f"the server receives {address}, but no control sends it")
for address in sorted(sent - received):
problems.append(f"a control sends {address}, which the server does not receive")
for address in sorted(replies - taken):
problems.append(f"the server sends {address}, but no control shows it")
for address in sorted(taken - replies):
problems.append(f"a control waits for {address}, which the server does not send")
return problems
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("schema", help="the output of playr-server osc-schema")
parser.add_argument("output", help="the layout to write: .tosc, .xml or .json")
parser.add_argument("--playlists", type=int, default=8, help="playlist buttons, 1 to 32")
args = parser.parse_args()
if not 1 <= args.playlists <= 32:
parser.error("--playlists is 1 to 32")
with open(args.schema, encoding="utf-8") as f:
schema = json.load(f)
doc = build(args.playlists)
problems = mismatches(schema, doc)
if problems:
for problem in problems:
print(f"layout.py: {problem}", file=sys.stderr)
return 1
doc.save(args.output, validate=True)
for issue in doc.validate():
print(f"layout.py: {issue}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())