Initial public release
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user