from dataclasses import dataclass, field import json import os from typing import Any @dataclass class ProjectState: version: int = 2 name: str = "" open_files: list[str] = field(default_factory=list) active_file: str | None = None cursor_positions: dict[str, int] = field(default_factory=dict) scratchpad_open: bool = False class ProjectStateService: """Stable project file API for desktop and mobile clients.""" def parse_cursor_positions(self, raw: Any) -> dict[str, int]: if not isinstance(raw, dict): return {} parsed: dict[str, int] = {} for path, position in raw.items(): if not isinstance(path, str): continue try: parsed[path] = max(0, int(position)) except (TypeError, ValueError): continue return parsed def from_dict(self, payload: dict[str, Any], fallback_name: str = "") -> ProjectState: open_files: list[str] = [] for path in payload.get("open_files", []): if isinstance(path, str): open_files.append(path) active_file = payload.get("active_file") if not isinstance(active_file, str): active_file = None name = payload.get("name") if not isinstance(name, str): name = fallback_name version = payload.get("version", 2) try: version = int(version) except (TypeError, ValueError): version = 2 scratchpad_open = payload.get("scratchpad_open", False) if not isinstance(scratchpad_open, bool): scratchpad_open = False return ProjectState( version=max(1, version), name=name, open_files=open_files, active_file=active_file, cursor_positions=self.parse_cursor_positions(payload.get("cursor_positions")), scratchpad_open=scratchpad_open, ) def to_dict(self, state: ProjectState) -> dict[str, Any]: active_file = state.active_file if isinstance(state.active_file, str) else None return { "version": max(1, int(state.version)), "name": state.name, "open_files": [p for p in state.open_files if isinstance(p, str)], "active_file": active_file, "cursor_positions": self.parse_cursor_positions(state.cursor_positions), "scratchpad_open": state.scratchpad_open, } def read_project(self, project_file: str) -> ProjectState: project_file = os.path.abspath(project_file) with open(project_file, "r", encoding="utf-8") as f: payload = json.load(f) if not isinstance(payload, dict): payload = {} fallback_name = os.path.basename(os.path.dirname(project_file)) return self.from_dict(payload, fallback_name=fallback_name) def write_project(self, project_file: str, state: ProjectState) -> None: project_file = os.path.abspath(project_file) payload = self.to_dict(state) with open(project_file, "w", encoding="utf-8") as f: json.dump(payload, f, indent=4) project_state_service = ProjectStateService()