96 lines
3.2 KiB
Python
96 lines
3.2 KiB
Python
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()
|
|
|