pyLyricFlow/src/lyricflow_core/api/project_state.py
2026-02-24 13:22:10 -06:00

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