pyblock/pybitblock/config.py
GaltRanch 4488bd7aac Add 5 visual features to block clock: miner pool, weight, histogram, peers, moon
- Miner pool tag: shows who mined the last block (coinbase decode for local/remote, API for lite)
- Block weight meter: colored fullness bar (green/yellow/red)
- Block time histogram: sparkline of last 14 block intervals with color-coded speed + streak detection
- Peer count: network connections indicator with health coloring
- Moon phase: current lunar phase emoji + name

All toggleable via Settings → D (Clock Display Settings).
Also fixes negative countdown timer (clamp to 0) and countdown row tracking bug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 09:08:18 -03:00

173 lines
5.8 KiB
Python

"""
Centralized configuration singleton for PyBLOCK.
Loads all .conf files once at startup and caches them in memory.
Call cfg.load() once, then access cfg.path, cfg.lndconnectload, etc.
Call cfg.reload() after the setup wizard writes new config files.
Supports Umbrel/Docker environment variables for auto-configuration:
BITCOIN_RPC_HOST, BITCOIN_RPC_PORT, BITCOIN_RPC_USER, BITCOIN_RPC_PASS
BITCOIN_CLI_PATH
LND_HOST, LND_GRPC_PORT, LND_TLS_CERT_PATH, LND_MACAROON_PATH, LND_CLI_PATH
PYBLOCK_MODE (A=Bitcoin+Lightning, B=Bitcoin, C=Lite)
"""
import json
import os
_DEFAULT_PATH = {"ip_port": "", "rpcuser": "", "rpcpass": "", "bitcoincli": ""}
_DEFAULT_LND = {"ip_port": "", "tls": "", "macaroon": "", "ln": ""}
_DEFAULT_SETTINGS = {"gradient": "", "design": "block", "colorA": "green", "colorB": "yellow"}
_DEFAULT_SETTINGS_CLOCK = {
"gradient": "", "colorA": "green", "colorB": "yellow",
"show_countdown": True, "show_epoch_bar": True,
"show_fee_rates": True, "show_sparkline": False,
"show_utc_time": False, "zen_mode": False,
"animation": "matrix",
"fireworks": True,
"generative_art": False,
"sound": "bell",
"heartbeat": True,
"show_miner_pool": True,
"show_block_weight": False,
"show_block_times": True,
"show_peers": False,
"show_moon": False,
}
def _env_bitcoin_config():
"""Build Bitcoin config from environment variables (Umbrel/Docker)."""
host = os.environ.get("BITCOIN_RPC_HOST", "")
port = os.environ.get("BITCOIN_RPC_PORT", "8332")
user = os.environ.get("BITCOIN_RPC_USER", "")
passwd = os.environ.get("BITCOIN_RPC_PASS", "")
cli = os.environ.get("BITCOIN_CLI_PATH", "")
if host and user:
return {
"ip_port": f"http://{host}:{port}",
"rpcuser": user,
"rpcpass": passwd,
"bitcoincli": cli,
}
return None
def _env_lnd_config():
"""Build LND config from environment variables (Umbrel/Docker)."""
host = os.environ.get("LND_HOST", "")
port = os.environ.get("LND_GRPC_PORT", "10009")
tls = os.environ.get("LND_TLS_CERT_PATH", "")
macaroon = os.environ.get("LND_MACAROON_PATH", "")
cli = os.environ.get("LND_CLI_PATH", "")
if host or tls or macaroon:
return {
"ip_port": f"{host}:{port}" if host else "",
"tls": tls,
"macaroon": macaroon,
"ln": cli,
}
return None
def _env_mode():
"""Get PyBLOCK mode from environment variable."""
return os.environ.get("PYBLOCK_MODE", "")
class Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._loaded = False
return cls._instance
def __init__(self):
if not self._loaded:
self.config_dir = self._find_config_dir()
self.path = dict(_DEFAULT_PATH)
self.lndconnectload = dict(_DEFAULT_LND)
self.settings = dict(_DEFAULT_SETTINGS)
self.settings_clock = dict(_DEFAULT_SETTINGS_CLOCK)
self.intro_mode = None
def _find_config_dir(self):
candidates = [
os.path.join(os.path.dirname(__file__), "config"),
"config",
os.path.join(os.path.dirname(__file__), "SPV", "config"),
]
for d in candidates:
if os.path.isdir(d):
return d
return "config"
def _load_json(self, filename, defaults=None):
filepath = os.path.join(self.config_dir, filename)
if os.path.isfile(filepath):
with open(filepath, "r") as f:
data = json.load(f)
if defaults and isinstance(data, dict):
merged = dict(defaults)
merged.update(data)
return merged
return data
return dict(defaults) if defaults else None
def _apply_env_overrides(self):
"""Apply environment variable overrides (Umbrel/Docker mode).
Env vars take priority over config files. If env vars are set,
they also auto-generate the config files for consistency.
"""
btc_env = _env_bitcoin_config()
if btc_env:
self.path.update(btc_env)
self._ensure_config("bclock.conf", self.path)
lnd_env = _env_lnd_config()
if lnd_env:
self.lndconnectload.update(lnd_env)
self._ensure_config("blndconnect.conf", self.lndconnectload)
mode_env = _env_mode()
if mode_env and mode_env in ("A", "B", "C"):
self.intro_mode = mode_env
self._ensure_config("intro.conf", mode_env)
def _ensure_config(self, filename, data):
"""Write config file if it doesn't exist or env vars are set."""
filepath = os.path.join(self.config_dir, filename)
os.makedirs(self.config_dir, exist_ok=True)
with open(filepath, "w") as f:
json.dump(data, f, indent=2)
def load(self):
self.path = self._load_json("bclock.conf", _DEFAULT_PATH)
self.lndconnectload = self._load_json("blndconnect.conf", _DEFAULT_LND)
self.settings = self._load_json("pyblocksettings.conf", _DEFAULT_SETTINGS)
self.settings_clock = self._load_json("pyblocksettingsClock.conf", _DEFAULT_SETTINGS_CLOCK)
self.intro_mode = self._load_json("intro.conf")
self._apply_env_overrides()
self._loaded = True
def reload(self):
self._loaded = False
self.load()
def save(self, filename, data):
filepath = os.path.join(self.config_dir, filename)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w") as f:
json.dump(data, f, indent=2)
self.reload()
def has_config(self, filename):
return os.path.isfile(os.path.join(self.config_dir, filename))
cfg = Config()