"""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