130 lines
3.7 KiB
Python
130 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Config storage for the ButtonTask web configurator.
|
|
|
|
Reads/writes the same JSON config file consumed by the Qt5 application.
|
|
Defaults here must stay in sync with ConfigManager::ensureDefaults() in C++.
|
|
"""
|
|
import copy
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
ALLOWED_ICON_EXT = {".png", ".jpg", ".jpeg", ".bmp", ".svg", ".gif"}
|
|
|
|
|
|
def config_path():
|
|
"""Resolve the active config path.
|
|
|
|
Priority:
|
|
1. BUTTONTASK_CONFIG environment variable.
|
|
2. <repo>/config/config.json next to this package (dev fallback).
|
|
"""
|
|
env = os.environ.get("BUTTONTASK_CONFIG")
|
|
if env:
|
|
return Path(env)
|
|
# webconfig/ -> repo root -> config/config.json
|
|
return (Path(__file__).resolve().parent.parent / "config" / "config.json")
|
|
|
|
|
|
def _defaults():
|
|
return {
|
|
"version": 2,
|
|
"layout": {"mode": "grid", "columns": 2, "spacing": 10, "showLabels": True, "labelColor": "#ffffff"},
|
|
"background": {
|
|
"type": "gradient",
|
|
"color1": "#00007b",
|
|
"color2": "#7b007b",
|
|
"animated": True,
|
|
"imagePath": "",
|
|
},
|
|
"feedback": {
|
|
"okColor": "#00cc44",
|
|
"errorColor": "#cc2200",
|
|
"okWidth": 6,
|
|
"errorWidth": 6,
|
|
"glowRadius": 20,
|
|
"holdColor": "#ffffff",
|
|
"pendingColor": "#ffffff",
|
|
"pendingWidth": 6,
|
|
},
|
|
"settings": {
|
|
"darkMode": True,
|
|
"password": "admin_26",
|
|
"kioskMode": False,
|
|
"iconsDir": "./icons",
|
|
"webPort": 8080,
|
|
"brightness": 60,
|
|
},
|
|
"buttons": [],
|
|
}
|
|
|
|
|
|
def ensure_defaults(cfg):
|
|
"""Fill in any missing sections/keys without overwriting existing values."""
|
|
base = _defaults()
|
|
if not isinstance(cfg, dict):
|
|
return base
|
|
cfg.setdefault("version", base["version"])
|
|
for section in ("layout", "background", "feedback", "settings"):
|
|
sec = cfg.setdefault(section, {})
|
|
if not isinstance(sec, dict):
|
|
sec = {}
|
|
cfg[section] = sec
|
|
for k, v in base[section].items():
|
|
sec.setdefault(k, v)
|
|
if not isinstance(cfg.get("buttons"), list):
|
|
cfg["buttons"] = []
|
|
cfg.setdefault("settings", {})["darkMode"] = True
|
|
return cfg
|
|
|
|
|
|
def load_config():
|
|
p = config_path()
|
|
if not p.exists():
|
|
cfg = ensure_defaults(_defaults())
|
|
# point iconsDir at a sensible absolute default for the web side
|
|
cfg["settings"]["iconsDir"] = str((p.parent.parent / "icons"))
|
|
return cfg
|
|
with p.open("r", encoding="utf-8") as f:
|
|
raw = json.load(f)
|
|
migrated = ensure_defaults(raw if isinstance(raw, dict) else {})
|
|
if migrated != raw:
|
|
save_config(migrated)
|
|
return migrated
|
|
|
|
|
|
def save_config(cfg):
|
|
p = config_path()
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
fd, tmp = tempfile.mkstemp(prefix=".cfg.", suffix=".tmp", dir=str(p.parent))
|
|
try:
|
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
json.dump(cfg, f, indent=4, ensure_ascii=False)
|
|
os.replace(tmp, p)
|
|
except Exception:
|
|
if os.path.exists(tmp):
|
|
os.unlink(tmp)
|
|
raise
|
|
|
|
|
|
def icons_dir():
|
|
cfg = load_config()
|
|
d = cfg.get("settings", {}).get("iconsDir") or "./icons"
|
|
p = Path(d)
|
|
if not p.is_absolute():
|
|
p = (config_path().parent.parent / d).resolve()
|
|
p.mkdir(parents=True, exist_ok=True)
|
|
return p
|
|
|
|
|
|
def list_icons():
|
|
return sorted(
|
|
f.name for f in icons_dir().iterdir()
|
|
if f.is_file() and f.suffix.lower() in ALLOWED_ICON_EXT
|
|
)
|
|
|
|
|
|
def default_config():
|
|
return copy.deepcopy(_defaults())
|