Source code for simvx.ide.config

"""Persistent IDE settings stored in ~/.config/simvx/ide.json.

``IDEConfig`` delegates field storage to the unified ``AppConfig`` system
while preserving backward-compatible save/load using the legacy flat JSON
format.  All colours come from the ``AppTheme`` singleton.
"""


from __future__ import annotations

import json
import logging
import os
from pathlib import Path

from simvx.core.config import AppConfig

log = logging.getLogger(__name__)

CONFIG_DIR = Path.home() / ".config" / "simvx"
CONFIG_FILE = CONFIG_DIR / "ide.json"


[docs] class IDEConfig: """IDE preferences with JSON persistence. All fields delegate to ``AppConfig.general`` and ``AppConfig.ide`` sections. Colours are provided by the ``AppTheme`` singleton (see ``simvx.core.ui.theme``). """ def __init__(self) -> None: self._config = AppConfig() # IDE uses a narrower default window than the editor. self._config.general.window_width = 1400 # -- General section properties ------------------------------------------ @property def font_size(self) -> int: return int(self._config.general.font_size) @font_size.setter def font_size(self, value: int) -> None: self._config.general.font_size = float(value) @property def window_width(self) -> int: return self._config.general.window_width @window_width.setter def window_width(self, value: int) -> None: self._config.general.window_width = value @property def window_height(self) -> int: return self._config.general.window_height @window_height.setter def window_height(self, value: int) -> None: self._config.general.window_height = value @property def recent_files(self) -> list[str]: return self._config.general.recent_files @recent_files.setter def recent_files(self, value: list[str]) -> None: self._config.general.recent_files = value @property def recent_folders(self) -> list[str]: return self._config.general.recent_folders @recent_folders.setter def recent_folders(self, value: list[str]) -> None: self._config.general.recent_folders = value @property def theme_preset(self) -> str: return self._config.general.theme_preset @theme_preset.setter def theme_preset(self, value: str) -> None: self._config.general.theme_preset = value # -- IDE section properties ---------------------------------------------- @property def tab_size(self) -> int: return self._config.ide.tab_size @tab_size.setter def tab_size(self, value: int) -> None: self._config.ide.tab_size = value @property def insert_spaces(self) -> bool: return self._config.ide.insert_spaces @insert_spaces.setter def insert_spaces(self, value: bool) -> None: self._config.ide.insert_spaces = value @property def show_line_numbers(self) -> bool: return self._config.ide.show_line_numbers @show_line_numbers.setter def show_line_numbers(self, value: bool) -> None: self._config.ide.show_line_numbers = value @property def show_minimap(self) -> bool: return self._config.ide.show_minimap @show_minimap.setter def show_minimap(self, value: bool) -> None: self._config.ide.show_minimap = value @property def show_code_folding(self) -> bool: return self._config.ide.show_code_folding @show_code_folding.setter def show_code_folding(self, value: bool) -> None: self._config.ide.show_code_folding = value @property def show_indent_guides(self) -> bool: return self._config.ide.show_indent_guides @show_indent_guides.setter def show_indent_guides(self, value: bool) -> None: self._config.ide.show_indent_guides = value @property def auto_save(self) -> bool: return self._config.ide.auto_save @auto_save.setter def auto_save(self, value: bool) -> None: self._config.ide.auto_save = value @property def format_on_save(self) -> bool: return self._config.ide.format_on_save @format_on_save.setter def format_on_save(self, value: bool) -> None: self._config.ide.format_on_save = value @property def sidebar_width(self) -> int: return self._config.ide.sidebar_width @sidebar_width.setter def sidebar_width(self, value: int) -> None: self._config.ide.sidebar_width = value @property def bottom_panel_height(self) -> int: return self._config.ide.bottom_panel_height @bottom_panel_height.setter def bottom_panel_height(self, value: int) -> None: self._config.ide.bottom_panel_height = value @property def sidebar_visible(self) -> bool: return self._config.ide.sidebar_visible @sidebar_visible.setter def sidebar_visible(self, value: bool) -> None: self._config.ide.sidebar_visible = value @property def bottom_panel_visible(self) -> bool: return self._config.ide.bottom_panel_visible @bottom_panel_visible.setter def bottom_panel_visible(self, value: bool) -> None: self._config.ide.bottom_panel_visible = value @property def lsp_enabled(self) -> bool: return self._config.ide.lsp_enabled @lsp_enabled.setter def lsp_enabled(self, value: bool) -> None: self._config.ide.lsp_enabled = value @property def lsp_command(self) -> str: return self._config.ide.lsp_command @lsp_command.setter def lsp_command(self, value: str) -> None: self._config.ide.lsp_command = value @property def lsp_args(self) -> list[str]: return self._config.ide.lsp_args @lsp_args.setter def lsp_args(self, value: list[str]) -> None: self._config.ide.lsp_args = value @property def lint_enabled(self) -> bool: return self._config.ide.lint_enabled @lint_enabled.setter def lint_enabled(self, value: bool) -> None: self._config.ide.lint_enabled = value @property def lint_on_save(self) -> bool: return self._config.ide.lint_on_save @lint_on_save.setter def lint_on_save(self, value: bool) -> None: self._config.ide.lint_on_save = value @property def lint_command(self) -> str: return self._config.ide.lint_command @lint_command.setter def lint_command(self, value: str) -> None: self._config.ide.lint_command = value @property def format_command(self) -> str: return self._config.ide.format_command @format_command.setter def format_command(self, value: str) -> None: self._config.ide.format_command = value @property def python_path(self) -> str: return self._config.ide.python_path @python_path.setter def python_path(self, value: str) -> None: self._config.ide.python_path = value @property def venv_path(self) -> str: return self._config.ide.venv_path @venv_path.setter def venv_path(self, value: str) -> None: self._config.ide.venv_path = value @property def auto_detect_venv(self) -> bool: return self._config.ide.auto_detect_venv @auto_detect_venv.setter def auto_detect_venv(self, value: bool) -> None: self._config.ide.auto_detect_venv = value @property def debug_adapter(self) -> str: return self._config.ide.debug_adapter @debug_adapter.setter def debug_adapter(self, value: str) -> None: self._config.ide.debug_adapter = value @property def keybindings(self) -> dict[str, str]: return self._config.ide.keybindings @keybindings.setter def keybindings(self, value: dict[str, str]) -> None: self._config.ide.keybindings = value # -- Persistence (legacy flat format) ------------------------------------ # All property names that should be serialized. _PROP_NAMES = ( "font_size", "tab_size", "insert_spaces", "show_line_numbers", "show_minimap", "show_code_folding", "show_indent_guides", "auto_save", "format_on_save", "window_width", "window_height", "sidebar_width", "bottom_panel_height", "sidebar_visible", "bottom_panel_visible", "lsp_enabled", "lsp_command", "lsp_args", "lint_enabled", "lint_on_save", "lint_command", "format_command", "python_path", "venv_path", "auto_detect_venv", "debug_adapter", "recent_files", "recent_folders", "keybindings", "theme_preset", )
[docs] def load(self) -> None: if not CONFIG_FILE.exists(): return try: data = json.loads(CONFIG_FILE.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError): return for key, value in data.items(): if hasattr(self, key): current = getattr(self, key) if isinstance(current, tuple) and isinstance(value, list): setattr(self, key, tuple(value)) else: setattr(self, key, value)
[docs] def save(self) -> None: CONFIG_DIR.mkdir(parents=True, exist_ok=True) data: dict = {} for name in self._PROP_NAMES: data[name] = getattr(self, name) CONFIG_FILE.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
# -- Recent files/folders ------------------------------------------------
[docs] def add_recent_file(self, path: str) -> None: if path in self.recent_files: self.recent_files.remove(path) self.recent_files.insert(0, path) self.recent_files = self.recent_files[:20]
[docs] def add_recent_folder(self, path: str) -> None: if path in self.recent_folders: self.recent_folders.remove(path) self.recent_folders.insert(0, path) self.recent_folders = self.recent_folders[:10]
# -- Virtual environment detection ---------------------------------------
[docs] @staticmethod def detect_venv(project_root: str) -> str | None: """Detect a virtual environment in *project_root*. Checks common venv directory names (.venv, venv, .env) for a Python interpreter. Returns the absolute path to the venv directory, or None. """ if not project_root: return None root = Path(project_root) for name in (".venv", "venv", ".env"): python = root / name / "bin" / "python" if python.is_file(): return str(root / name) return None
[docs] def get_python_command(self, project_root: str) -> str: """Return the Python interpreter path to use for this project. Priority: explicit ``python_path`` setting > auto-detected venv > system ``python``. """ if self.python_path: return self.python_path venv = self._resolve_venv(project_root) if venv: return str(Path(venv) / "bin" / "python") return "python"
[docs] def get_env(self, project_root: str) -> dict[str, str]: """Build a subprocess environment dict with the detected venv activated.""" env = os.environ.copy() venv = self._resolve_venv(project_root) if venv: venv_bin = str(Path(venv) / "bin") env["VIRTUAL_ENV"] = venv env["PATH"] = venv_bin + os.pathsep + env.get("PATH", "") env.pop("PYTHONHOME", None) return env
def _resolve_venv(self, project_root: str) -> str | None: """Return the venv path from explicit config or auto-detection.""" if self.venv_path: return self.venv_path if self.auto_detect_venv: return self.detect_venv(project_root) return None # -- Theme presets ------------------------------------------------------- _THEME_FACTORIES: dict[str, str] | None = None @staticmethod def _get_factories() -> dict: from simvx.core.ui.theme import AppTheme return { "dark": AppTheme.dark, "abyss": AppTheme.abyss, "midnight": AppTheme.midnight, "light": AppTheme.light, "monokai": AppTheme.monokai, "solarised_dark": AppTheme.solarised_dark, "nord": AppTheme.nord, }
[docs] def apply_theme(self, preset: str): """Apply a theme preset and update the global singleton.""" from simvx.core.ui.theme import set_theme factories = self._get_factories() factory = factories.get(preset) if not factory: return self.theme_preset = preset set_theme(factory()) self.save()
[docs] def get_theme(self): """Return an ``AppTheme`` matching the current preset and set the global singleton.""" from simvx.core.ui.theme import set_theme factories = self._get_factories() theme = factories.get(self.theme_preset, factories["dark"])() set_theme(theme) return theme