162 lines
4.6 KiB
Python
162 lines
4.6 KiB
Python
"""Folder-organized macro library for the BT Keyboard streamer.
|
|
|
|
A *macro* is a recorded sequence of keyboard + mouse + Ctrl-Alt-Del events
|
|
captured in the BT Keyboard window. Macros are grouped into user-managed
|
|
*folders*. Folders can be created / renamed / deleted; macros are NOT
|
|
renameable by design (re-record to change one).
|
|
|
|
Event format (tagged arrays so a single list can mix keys and mouse):
|
|
key: ["k", t_ms, action, hid] action 0=down/1=up
|
|
mouse: ["m", t_ms, buttons, x, y, wheel] x/y absolute 0..32767
|
|
|
|
Stored as one JSON file in the app config dir (same convention as
|
|
bt_profiles.py — atomic write under a lock):
|
|
|
|
{ "folders": { "<folder>": { "<macro>": {"events":[...],
|
|
"duration_ms": int,
|
|
"created": <ts>} } } }
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import threading
|
|
import time
|
|
|
|
from utils.constants import APPDATA_DIR
|
|
|
|
_FILE = os.path.join(APPDATA_DIR, "bt_kbd_macros.json")
|
|
_lock = threading.Lock()
|
|
|
|
DEFAULT_FOLDER = "Default"
|
|
|
|
|
|
def _load_all() -> dict:
|
|
try:
|
|
with open(_FILE, "r", encoding="utf-8") as f:
|
|
d = json.load(f)
|
|
except (OSError, json.JSONDecodeError):
|
|
return {}
|
|
folders = d.get("folders") if isinstance(d, dict) else None
|
|
return folders if isinstance(folders, dict) else {}
|
|
|
|
|
|
def _save_all(folders: dict) -> None:
|
|
os.makedirs(APPDATA_DIR, exist_ok=True)
|
|
tmp = _FILE + ".tmp"
|
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
json.dump({"folders": folders}, f, indent=2)
|
|
os.replace(tmp, _FILE)
|
|
|
|
|
|
def _clean(name: str) -> str:
|
|
return (name or "").strip()
|
|
|
|
|
|
# ---- Folders ----
|
|
|
|
def list_folders() -> list:
|
|
"""All folder names, sorted, with DEFAULT_FOLDER guaranteed present."""
|
|
with _lock:
|
|
folders = _load_all()
|
|
if DEFAULT_FOLDER not in folders:
|
|
folders[DEFAULT_FOLDER] = {}
|
|
_save_all(folders)
|
|
return sorted(folders.keys())
|
|
|
|
|
|
def create_folder(name: str) -> bool:
|
|
name = _clean(name)
|
|
if not name:
|
|
return False
|
|
with _lock:
|
|
folders = _load_all()
|
|
if name in folders:
|
|
return False
|
|
folders[name] = {}
|
|
_save_all(folders)
|
|
return True
|
|
|
|
|
|
def rename_folder(old: str, new: str) -> bool:
|
|
old, new = _clean(old), _clean(new)
|
|
if not old or not new or old == new:
|
|
return False
|
|
with _lock:
|
|
folders = _load_all()
|
|
if old not in folders or new in folders:
|
|
return False
|
|
folders[new] = folders.pop(old) # moves all contained macros
|
|
_save_all(folders)
|
|
return True
|
|
|
|
|
|
def delete_folder(name: str) -> bool:
|
|
name = _clean(name)
|
|
with _lock:
|
|
folders = _load_all()
|
|
if name not in folders:
|
|
return False
|
|
del folders[name]
|
|
if not folders:
|
|
folders[DEFAULT_FOLDER] = {}
|
|
_save_all(folders)
|
|
return True
|
|
|
|
|
|
# ---- Macros ----
|
|
|
|
def list_macros(folder: str) -> list:
|
|
"""Macro names in a folder, sorted."""
|
|
folder = _clean(folder)
|
|
with _lock:
|
|
return sorted(_load_all().get(folder, {}).keys())
|
|
|
|
|
|
def save_macro(folder: str, name: str, events, duration_ms: int = 0) -> bool:
|
|
"""Create/overwrite ``name`` in ``folder`` (folder auto-created)."""
|
|
folder, name = _clean(folder) or DEFAULT_FOLDER, _clean(name)
|
|
if not name:
|
|
return False
|
|
with _lock:
|
|
folders = _load_all()
|
|
folders.setdefault(folder, {})
|
|
folders[folder][name] = {
|
|
"events": [list(e) for e in events],
|
|
"duration_ms": int(duration_ms),
|
|
"created": time.time(),
|
|
}
|
|
_save_all(folders)
|
|
return True
|
|
|
|
|
|
def load_macro(folder: str, name: str) -> dict | None:
|
|
"""Return {"events","duration_ms","created"} or None."""
|
|
folder, name = _clean(folder), _clean(name)
|
|
with _lock:
|
|
m = _load_all().get(folder, {}).get(name)
|
|
if not isinstance(m, dict):
|
|
return None
|
|
evs = m.get("events")
|
|
return {
|
|
"events": evs if isinstance(evs, list) else [],
|
|
"duration_ms": int(m.get("duration_ms", 0) or 0),
|
|
"created": m.get("created", 0),
|
|
}
|
|
|
|
|
|
def macro_exists(folder: str, name: str) -> bool:
|
|
folder, name = _clean(folder), _clean(name)
|
|
with _lock:
|
|
return name in _load_all().get(folder, {})
|
|
|
|
|
|
def delete_macro(folder: str, name: str) -> bool:
|
|
folder, name = _clean(folder), _clean(name)
|
|
with _lock:
|
|
folders = _load_all()
|
|
if name in folders.get(folder, {}):
|
|
del folders[folder][name]
|
|
_save_all(folders)
|
|
return True
|
|
return False
|