430 lines
15 KiB
Python
430 lines
15 KiB
Python
"""Settings management for Development Hub."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
|
|
class Settings:
|
|
"""Application settings with file persistence."""
|
|
|
|
_instance = None
|
|
_settings_file = Path.home() / ".config" / "development-hub" / "settings.json"
|
|
_session_file = Path.home() / ".config" / "development-hub" / "session.json"
|
|
|
|
# Default settings
|
|
DEFAULTS = {
|
|
"deploy_docs_after_creation": True,
|
|
"default_project_path": str(Path.home() / "PycharmProjects"),
|
|
"preferred_editor": "auto", # "auto", "pycharm", "xed", "code", "gedit"
|
|
"project_search_paths": [str(Path.home() / "PycharmProjects")],
|
|
"project_ignore_folders": ["trash", "project-docs", ".cache", "__pycache__", "node_modules"],
|
|
"auto_start_docs_server": True,
|
|
# Git hosting settings
|
|
"git_host_type": "", # "gitea", "github", "gitlab", ""
|
|
"git_host_url": "", # e.g., "https://gitea.example.com" or "https://github.com"
|
|
"git_host_owner": "", # username or organization
|
|
"git_host_token": "", # API token (stored in settings, not ideal but simple)
|
|
# Documentation settings
|
|
"docs_mode": "auto", # "auto" | "standalone" | "project-docs"
|
|
"docs_root": "", # Empty = derive from mode
|
|
"docusaurus_path": "", # Path to project-docs
|
|
"pages_url": "", # Separate from git_host_url
|
|
# Integration paths
|
|
"cmdforge_path": "", # Override cmdforge location
|
|
"progress_dir": "", # Override progress directory
|
|
}
|
|
|
|
# Available editor choices with display names
|
|
EDITOR_CHOICES = [
|
|
("auto", "Auto-detect"),
|
|
("pycharm", "PyCharm (IDE with markdown preview)"),
|
|
("code", "VS Code"),
|
|
("xed", "Xed (quick text editor)"),
|
|
("gedit", "Gedit"),
|
|
("subl", "Sublime Text"),
|
|
]
|
|
|
|
# Git hosting provider choices
|
|
GIT_HOST_CHOICES = [
|
|
("gitea", "Gitea"),
|
|
("github", "GitHub"),
|
|
("gitlab", "GitLab"),
|
|
]
|
|
|
|
def __new__(cls):
|
|
"""Singleton pattern."""
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._settings = {}
|
|
cls._instance._load()
|
|
return cls._instance
|
|
|
|
def _load(self):
|
|
"""Load settings from file."""
|
|
self._settings = self.DEFAULTS.copy()
|
|
|
|
if self._settings_file.exists():
|
|
try:
|
|
with open(self._settings_file) as f:
|
|
saved = json.load(f)
|
|
self._settings.update(saved)
|
|
except (json.JSONDecodeError, OSError):
|
|
pass
|
|
|
|
def _save(self):
|
|
"""Save settings to file."""
|
|
self._settings_file.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(self._settings_file, "w") as f:
|
|
json.dump(self._settings, f, indent=2)
|
|
|
|
def get(self, key: str, default=None):
|
|
"""Get a setting value."""
|
|
return self._settings.get(key, default)
|
|
|
|
def set(self, key: str, value):
|
|
"""Set a setting value and save."""
|
|
self._settings[key] = value
|
|
self._save()
|
|
|
|
@property
|
|
def deploy_docs_after_creation(self) -> bool:
|
|
"""Whether to deploy docs after creating a new project."""
|
|
return self.get("deploy_docs_after_creation", True)
|
|
|
|
@deploy_docs_after_creation.setter
|
|
def deploy_docs_after_creation(self, value: bool):
|
|
self.set("deploy_docs_after_creation", value)
|
|
|
|
@property
|
|
def default_project_path(self) -> Path:
|
|
"""Default path for new projects."""
|
|
return Path(self.get("default_project_path", str(Path.home() / "PycharmProjects")))
|
|
|
|
@default_project_path.setter
|
|
def default_project_path(self, value: Path):
|
|
self.set("default_project_path", str(value))
|
|
|
|
@property
|
|
def preferred_editor(self) -> str:
|
|
"""Preferred editor for opening files."""
|
|
return self.get("preferred_editor", "auto")
|
|
|
|
@preferred_editor.setter
|
|
def preferred_editor(self, value: str):
|
|
self.set("preferred_editor", value)
|
|
|
|
@property
|
|
def project_search_paths(self) -> list[str]:
|
|
"""List of directories to search for projects."""
|
|
return self.get("project_search_paths", [str(Path.home() / "PycharmProjects")])
|
|
|
|
@project_search_paths.setter
|
|
def project_search_paths(self, value: list[str]):
|
|
self.set("project_search_paths", value)
|
|
|
|
@property
|
|
def project_ignore_folders(self) -> list[str]:
|
|
"""Folder names to ignore during project discovery."""
|
|
return self.get("project_ignore_folders", ["trash", ".cache", "__pycache__", "node_modules"])
|
|
|
|
@project_ignore_folders.setter
|
|
def project_ignore_folders(self, value: list[str]):
|
|
self.set("project_ignore_folders", value)
|
|
|
|
@property
|
|
def auto_start_docs_server(self) -> bool:
|
|
"""Whether to auto-start the docs server on application startup."""
|
|
return self.get("auto_start_docs_server", True)
|
|
|
|
@auto_start_docs_server.setter
|
|
def auto_start_docs_server(self, value: bool):
|
|
self.set("auto_start_docs_server", value)
|
|
|
|
@property
|
|
def git_host_type(self) -> str:
|
|
"""Git hosting type (gitea, github, gitlab)."""
|
|
return self.get("git_host_type", "")
|
|
|
|
@git_host_type.setter
|
|
def git_host_type(self, value: str):
|
|
self.set("git_host_type", value)
|
|
|
|
@property
|
|
def git_host_url(self) -> str:
|
|
"""Git host URL."""
|
|
return self.get("git_host_url", "")
|
|
|
|
@git_host_url.setter
|
|
def git_host_url(self, value: str):
|
|
self.set("git_host_url", value)
|
|
|
|
@property
|
|
def git_host_owner(self) -> str:
|
|
"""Git host username or organization."""
|
|
return self.get("git_host_owner", "")
|
|
|
|
@git_host_owner.setter
|
|
def git_host_owner(self, value: str):
|
|
self.set("git_host_owner", value)
|
|
|
|
@property
|
|
def git_host_token(self) -> str:
|
|
"""Git host API token."""
|
|
return self.get("git_host_token", "")
|
|
|
|
@git_host_token.setter
|
|
def git_host_token(self, value: str):
|
|
self.set("git_host_token", value)
|
|
|
|
@property
|
|
def is_git_configured(self) -> bool:
|
|
"""Check if git hosting is configured."""
|
|
return bool(self.git_host_type and self.git_host_url and self.git_host_owner)
|
|
|
|
@property
|
|
def docs_mode(self) -> str:
|
|
"""Documentation mode (auto, standalone, project-docs)."""
|
|
return self.get("docs_mode", "auto")
|
|
|
|
@docs_mode.setter
|
|
def docs_mode(self, value: str):
|
|
self.set("docs_mode", value)
|
|
|
|
@property
|
|
def effective_docs_mode(self) -> str:
|
|
"""Get effective docs mode, resolving 'auto' to actual mode."""
|
|
mode = self.docs_mode
|
|
if mode == "auto":
|
|
# Check if project-docs exists in default location
|
|
project_docs = Path(self.project_search_paths[0]) / "project-docs" if self.project_search_paths else None
|
|
if project_docs and project_docs.exists():
|
|
return "project-docs"
|
|
return "standalone"
|
|
return mode
|
|
|
|
@property
|
|
def docs_root(self) -> Path:
|
|
"""Get the documentation root path."""
|
|
explicit = self.get("docs_root", "")
|
|
if explicit:
|
|
return Path(explicit).expanduser()
|
|
# Derive from mode
|
|
if self.effective_docs_mode == "project-docs":
|
|
return self.docusaurus_path / "docs" if self.docusaurus_path else Path.home() / ".local" / "share" / "development-hub" / "docs"
|
|
return Path.home() / ".local" / "share" / "development-hub" / "docs"
|
|
|
|
@docs_root.setter
|
|
def docs_root(self, value: Path | str):
|
|
self.set("docs_root", str(value) if value else "")
|
|
|
|
@property
|
|
def docusaurus_path(self) -> Path | None:
|
|
"""Get the path to the docusaurus project."""
|
|
explicit = self.get("docusaurus_path", "")
|
|
if explicit:
|
|
return Path(explicit).expanduser()
|
|
# Default location
|
|
if self.project_search_paths:
|
|
default = Path(self.project_search_paths[0]) / "project-docs"
|
|
if default.exists():
|
|
return default
|
|
return None
|
|
|
|
@docusaurus_path.setter
|
|
def docusaurus_path(self, value: Path | str | None):
|
|
self.set("docusaurus_path", str(value) if value else "")
|
|
|
|
@property
|
|
def pages_url(self) -> str:
|
|
"""Get the pages URL for documentation hosting."""
|
|
explicit = self.get("pages_url", "")
|
|
if explicit:
|
|
return explicit
|
|
# Derive from git_host_url for gitea
|
|
if self.git_host_type == "gitea" and self.git_host_url:
|
|
# https://gitea.example.com -> https://pages.example.com
|
|
import re
|
|
match = re.match(r"https?://gitea\.(.+)", self.git_host_url)
|
|
if match:
|
|
return f"https://pages.{match.group(1)}"
|
|
return ""
|
|
|
|
@pages_url.setter
|
|
def pages_url(self, value: str):
|
|
self.set("pages_url", value)
|
|
|
|
@property
|
|
def cmdforge_path(self) -> Path | None:
|
|
"""Get the CmdForge path."""
|
|
explicit = self.get("cmdforge_path", "")
|
|
if explicit:
|
|
return Path(explicit).expanduser()
|
|
# Default locations
|
|
if self.project_search_paths:
|
|
default = Path(self.project_search_paths[0]) / "CmdForge"
|
|
if default.exists():
|
|
return default
|
|
return None
|
|
|
|
@cmdforge_path.setter
|
|
def cmdforge_path(self, value: Path | str | None):
|
|
self.set("cmdforge_path", str(value) if value else "")
|
|
|
|
@property
|
|
def progress_dir(self) -> Path:
|
|
"""Get the progress log directory."""
|
|
explicit = self.get("progress_dir", "")
|
|
if explicit:
|
|
return Path(explicit).expanduser()
|
|
# Default: under docs_root
|
|
return self.docs_root / "progress"
|
|
|
|
@progress_dir.setter
|
|
def progress_dir(self, value: Path | str):
|
|
self.set("progress_dir", str(value) if value else "")
|
|
|
|
@property
|
|
def is_docs_enabled(self) -> bool:
|
|
"""Check if documentation features are available."""
|
|
mode = self.effective_docs_mode
|
|
if mode == "project-docs":
|
|
return self.docusaurus_path is not None and self.docusaurus_path.exists()
|
|
return True # standalone mode always works
|
|
|
|
@property
|
|
def is_cmdforge_available(self) -> bool:
|
|
"""Check if CmdForge is available."""
|
|
import shutil
|
|
# Check explicit path
|
|
if self.cmdforge_path and (self.cmdforge_path / ".venv" / "bin" / "cmdforge").exists():
|
|
return True
|
|
# Check PATH
|
|
return shutil.which("cmdforge") is not None
|
|
|
|
def export_workspace(self, path: Path) -> None:
|
|
"""Export current settings to a workspace file.
|
|
|
|
Args:
|
|
path: Path to write the workspace YAML file
|
|
"""
|
|
import yaml
|
|
|
|
workspace = {
|
|
"name": f"{self.git_host_owner}'s Development Environment" if self.git_host_owner else "Development Environment",
|
|
"version": 1,
|
|
"paths": {
|
|
"projects_root": self.project_search_paths[0] if self.project_search_paths else str(Path.home() / "Projects"),
|
|
},
|
|
"documentation": {
|
|
"enabled": self.is_docs_enabled,
|
|
"mode": self.docs_mode,
|
|
"auto_start_server": self.auto_start_docs_server,
|
|
},
|
|
}
|
|
|
|
# Add docs_root if explicit
|
|
if self.get("docs_root"):
|
|
workspace["paths"]["docs_root"] = self.get("docs_root")
|
|
|
|
# Add docusaurus_path if in project-docs mode
|
|
if self.effective_docs_mode == "project-docs" and self.docusaurus_path:
|
|
workspace["documentation"]["docusaurus_path"] = str(self.docusaurus_path)
|
|
|
|
# Add git hosting if configured
|
|
if self.is_git_configured:
|
|
workspace["git_hosting"] = {
|
|
"type": self.git_host_type,
|
|
"url": self.git_host_url,
|
|
"owner": self.git_host_owner,
|
|
}
|
|
if self.pages_url:
|
|
workspace["git_hosting"]["pages_url"] = self.pages_url
|
|
|
|
# Add feature flags
|
|
workspace["features"] = {
|
|
"cmdforge_integration": self.is_cmdforge_available,
|
|
"progress_tracking": True,
|
|
}
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(path, "w") as f:
|
|
yaml.dump(workspace, f, default_flow_style=False, sort_keys=False)
|
|
|
|
def import_workspace(self, path: Path) -> dict:
|
|
"""Import settings from a workspace file.
|
|
|
|
Args:
|
|
path: Path to the workspace YAML file
|
|
|
|
Returns:
|
|
Dict with import results including any warnings
|
|
"""
|
|
import yaml
|
|
|
|
with open(path) as f:
|
|
workspace = yaml.safe_load(f)
|
|
|
|
results = {"imported": [], "warnings": []}
|
|
|
|
# Import paths
|
|
if "paths" in workspace:
|
|
paths = workspace["paths"]
|
|
if "projects_root" in paths:
|
|
projects_root = str(Path(paths["projects_root"]).expanduser())
|
|
self.project_search_paths = [projects_root]
|
|
self.default_project_path = Path(projects_root)
|
|
results["imported"].append("projects_root")
|
|
if "docs_root" in paths:
|
|
self.docs_root = paths["docs_root"]
|
|
results["imported"].append("docs_root")
|
|
|
|
# Import documentation settings
|
|
if "documentation" in workspace:
|
|
docs = workspace["documentation"]
|
|
if "mode" in docs:
|
|
self.docs_mode = docs["mode"]
|
|
results["imported"].append("docs_mode")
|
|
if "docusaurus_path" in docs:
|
|
self.docusaurus_path = docs["docusaurus_path"]
|
|
results["imported"].append("docusaurus_path")
|
|
if "auto_start_server" in docs:
|
|
self.auto_start_docs_server = docs["auto_start_server"]
|
|
results["imported"].append("auto_start_docs_server")
|
|
|
|
# Import git hosting
|
|
if "git_hosting" in workspace:
|
|
git = workspace["git_hosting"]
|
|
if "type" in git:
|
|
self.git_host_type = git["type"]
|
|
if "url" in git:
|
|
self.git_host_url = git["url"]
|
|
if "owner" in git:
|
|
self.git_host_owner = git["owner"]
|
|
if "pages_url" in git:
|
|
self.pages_url = git["pages_url"]
|
|
results["imported"].append("git_hosting")
|
|
|
|
# Mark setup as completed
|
|
self.set("setup_completed", True)
|
|
|
|
return results
|
|
|
|
def save_session(self, state: dict):
|
|
"""Save session state to file."""
|
|
self._session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
with open(self._session_file, "w") as f:
|
|
json.dump(state, f, indent=2)
|
|
except OSError:
|
|
pass
|
|
|
|
def load_session(self) -> dict:
|
|
"""Load session state from file."""
|
|
if self._session_file.exists():
|
|
try:
|
|
with open(self._session_file) as f:
|
|
return json.load(f)
|
|
except (json.JSONDecodeError, OSError):
|
|
pass
|
|
return {}
|