init
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
#!/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},
|
||||
"background": {
|
||||
"type": "gradient",
|
||||
"color1": "#00007b",
|
||||
"color2": "#7b007b",
|
||||
"animated": True,
|
||||
"imagePath": "",
|
||||
},
|
||||
"feedback": {
|
||||
"okColor": "#00cc44",
|
||||
"errorColor": "#cc2200",
|
||||
"okWidth": 6,
|
||||
"errorWidth": 6,
|
||||
"glowRadius": 20,
|
||||
},
|
||||
"settings": {
|
||||
"darkMode": True,
|
||||
"password": "admin",
|
||||
"kioskMode": False,
|
||||
"iconsDir": "./icons",
|
||||
"webPort": 8080,
|
||||
},
|
||||
"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"] = []
|
||||
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:
|
||||
cfg = json.load(f)
|
||||
return ensure_defaults(cfg)
|
||||
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user