DazedTL/util/tl_inspector/config.py
2026-06-13 15:50:25 -05:00

144 lines
4.8 KiB
Python

"""TL Inspector configuration — .env storage, editor detection, JS patching."""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
from util.playtest.config import load_config as _load_playtest_config, save_config as _save_playtest_config
ENV_EDITOR = "tlEditorCmd"
CFG_KEYS = ("editorCmd", "workspaceFolder", "hotkey", "uiScale")
DEFAULTS = {
"editorCmd": "auto",
"workspaceFolder": "auto",
"hotkey": "F9",
"uiScale": "auto",
}
_PKG_ROOT = Path(__file__).resolve().parent
BUNDLED_PLUGIN = _PKG_ROOT / "TLInspector.js"
ENV_PATH = Path(".env")
def detect_editors() -> list[tuple[str, Path]]:
"""Return installed code editors as (label, executable path) pairs."""
out: list[tuple[str, Path]] = []
seen: set[str] = set()
def add(label: str, path: Path) -> None:
key = str(path).lower()
if key in seen or not path.is_file():
return
seen.add(key)
out.append((label, path))
if sys.platform == "win32":
bases = [
os.environ.get("LOCALAPPDATA"),
os.environ.get("ProgramFiles"),
os.environ.get("ProgramFiles(x86)"),
]
for raw in bases:
if not raw:
continue
base = Path(raw)
add("VS Code", base / "Programs" / "Microsoft VS Code" / "Code.exe")
add("VS Code", base / "Microsoft VS Code" / "Code.exe")
add(
"VS Code Insiders",
base / "Programs" / "Microsoft VS Code Insiders" / "Code - Insiders.exe",
)
add("Cursor", base / "Programs" / "cursor" / "Cursor.exe")
add("Cursor", base / "cursor" / "Cursor.exe")
add("VSCodium", base / "Programs" / "VSCodium" / "VSCodium.exe")
add("VSCodium", base / "VSCodium" / "VSCodium.exe")
add("Windsurf", base / "Programs" / "Windsurf" / "Windsurf.exe")
elif sys.platform == "darwin":
add(
"VS Code",
Path("/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"),
)
add(
"VS Code Insiders",
Path(
"/Applications/Visual Studio Code - Insiders.app"
"/Contents/Resources/app/bin/code"
),
)
add("Cursor", Path("/Applications/Cursor.app/Contents/Resources/app/bin/cursor"))
add("VSCodium", Path("/Applications/VSCodium.app/Contents/Resources/app/bin/codium"))
add("Windsurf", Path("/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf"))
else:
for path in (
Path("/usr/bin/code"),
Path("/usr/share/code/bin/code"),
Path("/snap/bin/code"),
Path("/usr/bin/cursor"),
Path("/usr/bin/codium"),
Path("/usr/bin/windsurf"),
):
if "cursor" in path.name:
label = "Cursor"
elif "codium" in path.name:
label = "VSCodium"
elif "windsurf" in path.name:
label = "Windsurf"
else:
label = "VS Code"
add(label, path)
return out
def detect_primary_editor() -> Path | None:
editors = detect_editors()
return editors[0][1] if editors else None
def load_config(env_path: Path | None = None) -> dict:
return _load_playtest_config(env_path)
def save_config(cfg: dict, env_path: Path | None = None) -> None:
current = _load_playtest_config(env_path)
current.update(cfg)
_save_playtest_config(current, env_path)
def _js_literal(value) -> str:
if value is None:
return "null"
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return str(value)
return json.dumps(str(value))
def patch_cfg_block(js: str, overrides: dict) -> str:
for key, val in overrides.items():
if key not in CFG_KEYS:
continue
lit = _js_literal(val)
pattern = rf"({re.escape(key)}:\s*)(?:'[^']*'|\"[^\"]*\"|true|false|null|\d+)"
# Use a replacement *function*, not a template: a template would reinterpret
# backslashes in `lit` (e.g. Windows paths like C:\\Users\\... collapse to
# C:\Users\... -> invalid JS string escapes). A function inserts `lit` verbatim.
js, count = re.subn(pattern, lambda m, _lit=lit: m.group(1) + _lit, js, count=1)
if count == 0:
raise ValueError(f"Could not patch CFG.{key} in TLInspector.js")
return js
def prepare_plugin_js(source: Path | None = None, cfg: dict | None = None) -> str:
src = source or BUNDLED_PLUGIN
text = src.read_text(encoding="utf-8")
effective = {**DEFAULTS, **(cfg or load_config())}
effective["workspaceFolder"] = "auto"
return patch_cfg_block(text, effective)