"""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)