Dotfiles/desktopenvs/hyprdrive/beacon/controls.py

82 lines
3.0 KiB
Python

"""Parsing/validation for the x-beacon-controls custom notification hint.
Sending apps opt into rich, embedded controls (beyond the spec's plain
fire-and-dismiss `actions` array) via a hint whose value is a JSON string —
chosen over a nested D-Bus variant struct so any app, in any language, can
produce it without touching GVariant construction. Advertised in
GetCapabilities as "x-beacon-controls" so senders can detect support and
fall back to plain `actions` against any other freedesktop-compliant daemon.
Shape:
[
{"type": "button", "id": "reply", "label": "Reply"},
{"type": "toggle", "id": "mute", "label": "Mute", "value": false},
{"type": "slider", "id": "volume", "label": "Volume", "value": 40, "min": 0, "max": 100},
{"type": "entry", "id": "msg", "label": "Message", "placeholder": "Type a reply..."}
]
Unknown "type"s and unrecognised extra keys are silently dropped rather than
erroring — forward-compatible with control types added later, and never lets
a malformed/hostile payload (any local session-bus process can call Notify)
crash the daemon.
"""
from __future__ import annotations
import json
from typing import Optional
_TYPES = {"button", "toggle", "slider", "entry"}
# a button auto-dismisses like a classic action by default; the stateful
# controls default to staying on-screen so you can keep adjusting them.
_DEFAULT_DISMISS = {"button": True, "toggle": False, "slider": False, "entry": False}
def parse_controls(raw: Optional[str]) -> list[dict]:
if not raw:
return []
try:
data = json.loads(raw)
except (json.JSONDecodeError, TypeError):
return []
if not isinstance(data, list):
return []
out: list[dict] = []
for item in data:
if not isinstance(item, dict):
continue
ctype = item.get("type")
cid = item.get("id")
if ctype not in _TYPES or not isinstance(cid, str) or not cid:
continue
label = item.get("label")
label = label if isinstance(label, str) else cid
dismiss = item.get("dismiss")
dismiss = bool(dismiss) if isinstance(dismiss, bool) else _DEFAULT_DISMISS[ctype]
control = {"type": ctype, "id": cid, "label": label, "dismiss": dismiss}
if ctype == "toggle":
control["value"] = bool(item.get("value", False))
elif ctype == "slider":
control["value"] = _as_float(item.get("value"), 0.0)
control["min"] = _as_float(item.get("min"), 0.0)
control["max"] = _as_float(item.get("max"), 100.0)
control["step"] = _as_float(item.get("step"), 1.0)
if control["max"] <= control["min"]:
control["max"] = control["min"] + 1.0
elif ctype == "entry":
placeholder = item.get("placeholder")
control["placeholder"] = placeholder if isinstance(placeholder, str) else ""
out.append(control)
return out
def _as_float(value, default: float) -> float:
try:
return float(value)
except (TypeError, ValueError):
return default