143 lines
4.8 KiB
Python
143 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")
|
|
|
|
DEFAULTS = {
|
|
"editorCmd": "auto",
|
|
"workspaceFolder": "auto",
|
|
"hotkey": "F9",
|
|
}
|
|
|
|
_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)
|