from __future__ import annotations import re import shutil from pathlib import Path from editor_app import config DEFAULT_MAIN_PY = 'print("Hello, World!")\n' # Self-contained demos copied from shipped `workspace/code/` (stdlib + machine/neopixel/time only). # New accounts get a copy of each one in their own `code/` folder so the # editor has something to show on first login. They're treated as # starting points — users can edit/delete freely without affecting the # shipped originals. _CANONICAL_DEMO_FILENAMES = ( "pattern_rainbow_demo.py", "pattern_twinkle_demo.py", "pattern_chase_demo.py", "adc_slider_demo.py", "pin_demo.py", "serial_demo.py", ) def safe_workspace_leaf(username: str, user_id: int) -> str: base = re.sub(r"[^a-zA-Z0-9._-]+", "-", username.strip()).strip("-").lower() or "user" return f"{base}-{user_id}" def user_workspace_root(user_id: int, username: str, workspace_root: Path | None = None) -> Path: root = (workspace_root or config.WORKSPACE_ROOT).resolve() return root / "users" / safe_workspace_leaf(username, user_id) def _seed_canonical_demos_into_code(code_dir: Path) -> None: """Copy bundled demos into a user's `code/` if missing. Reads from `BUNDLED_DEMOS_DIR` (single source of truth, ships under `src/static/bundled-demos/`), never from `workspace/`, so this works even when `workspace/` is empty (gitignored runtime directory). """ src_root = config.BUNDLED_DEMOS_DIR.resolve() for filename in _CANONICAL_DEMO_FILENAMES: dst = code_dir / filename if dst.exists(): continue src = src_root / filename if src.is_file(): dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") def ensure_default_code_main(user_root: Path) -> None: """Ensure code/ has main.py and the canonical NeoPixel demos. Demos are sourced from `BUNDLED_DEMOS_DIR` (the single committed home for sample scripts). Only files listed in `_CANONICAL_DEMO_FILENAMES` get auto-seeded — the rest are available via the editor's "Reset demos" button or a manual copy.""" code_dir = user_root / "code" code_dir.mkdir(parents=True, exist_ok=True) main_py = code_dir / "main.py" if not main_py.exists(): main_py.write_text(DEFAULT_MAIN_PY, encoding="utf-8") _seed_canonical_demos_into_code(code_dir) def seed_all_bundled_demos(user_root: Path) -> None: """Copy *every* file in `BUNDLED_DEMOS_DIR` into `/code/`. Used at app startup to populate a fresh `workspace/code/` with the full sample set so a no-auth dev install (`pipenv run dev` after `git clone`) has something to play with. Existing files are not overwritten — user edits are preserved. """ code_dir = user_root / "code" code_dir.mkdir(parents=True, exist_ok=True) main_py = code_dir / "main.py" if not main_py.exists(): main_py.write_text(DEFAULT_MAIN_PY, encoding="utf-8") src_root = config.BUNDLED_DEMOS_DIR.resolve() if not src_root.is_dir(): return for src in sorted(src_root.iterdir()): if not src.is_file() or not src.name.endswith(".py"): continue dst = code_dir / src.name if dst.exists(): continue try: dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") except (UnicodeDecodeError, OSError): continue def rename_user_workspace_leaf( user_id: int, old_username: str, new_username: str, workspace_root: Path | None = None ) -> None: """Rename per-user workspace directory when login name changes.""" root = (workspace_root or config.WORKSPACE_ROOT).resolve() users_dir = root / "users" src = users_dir / safe_workspace_leaf(old_username, user_id) dst = users_dir / safe_workspace_leaf(new_username, user_id) if src.resolve() == dst.resolve(): return dst.parent.mkdir(parents=True, exist_ok=True) if dst.exists(): raise ValueError("Workspace folder for new username already exists; pick another username.") if src.exists(): shutil.move(str(src), str(dst)) else: ensure_default_code_main(dst)