82 lines
3.0 KiB
Python
82 lines
3.0 KiB
Python
"""Parsing/validation for the x-mnotifd-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-mnotifd-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
|