"""Persistent IDE settings stored via the unified ``AppConfig``.
``Config`` exposes flat-name accessors over ``AppConfig.general`` and
``AppConfig.ide`` sections. All colours come from the ``AppTheme`` singleton.
"""
import logging
import os
from pathlib import Path
from simvx.core.config import AppConfig
log = logging.getLogger(__name__)
[docs]
class Config:
"""IDE preferences backed by the unified ``AppConfig``.
Flat attribute accessors map 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)
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
@property
def sidebar_width(self) -> int:
return self._config.ide.sidebar_width
@property
def bottom_panel_height(self) -> int:
return self._config.ide.bottom_panel_height
[docs]
@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
@property
def bottom_panel_visible(self) -> bool:
return self._config.ide.bottom_panel_visible
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
@property
def python_path(self) -> str:
return self._config.ide.python_path
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@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
[docs]
@keybindings.setter
def keybindings(self, value: dict[str, str]) -> None:
self._config.ide.keybindings = value
# -- Persistence ---------------------------------------------------------
[docs]
def load(self) -> None:
self._config.load()
[docs]
def save(self) -> None:
self._config.save()
# -- 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