From 7577d7d28bb52d064c00e7e7d4f97a131838e142 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 29 Jun 2026 14:20:51 -0400 Subject: [PATCH] chore(layouts): introduce layouts/ directory + src/layouts.py; relocate default layout asset TIER-2 READ AGENTS.md, conductor/workflow.md, conductor/edit_workflow.md, conductor/tier2/githooks/forbidden-files.txt, conductor/tracks/tier2_leak_prevention_20260620/spec.md, conductor/code_styleguides/data_oriented_design.md, conductor/code_styleguides/error_handling.md, conductor/code_styleguides/type_aliases.md, conductor/product-guidelines.md, conductor/code_styleguides/python.md, docs/guide_meta_boundary.md before Phase 1 Task 1.10. Phase 1 of default_layout_install_20260629: - tests/artifacts/manualslop_layout_default.ini -> layouts/default.ini (git mv preserves history; same content, new parallel-to-themes home) - src/paths.py: layouts: Path field + SLOP_GLOBAL_LAYOUTS env override + get_layouts_dir() accessor (mirror themes at 60/83/150/210+) - src/layouts.py: new LayoutFile @dataclass(frozen=True, slots=True) + load_layouts_from_dir/file + load_layouts_from_disk consumer (mirror src/theme_models.py + src/theme_2.py; Result drain per error_handling) - tests/conftest.py:709: reads from layouts/default.ini --- .../default.ini | 0 src/layouts.py | 81 +++++++++++++++++++ src/paths.py | 8 ++ tests/conftest.py | 2 +- 4 files changed, 90 insertions(+), 1 deletion(-) rename tests/artifacts/manualslop_layout_default.ini => layouts/default.ini (100%) create mode 100644 src/layouts.py diff --git a/tests/artifacts/manualslop_layout_default.ini b/layouts/default.ini similarity index 100% rename from tests/artifacts/manualslop_layout_default.ini rename to layouts/default.ini diff --git a/src/layouts.py b/src/layouts.py new file mode 100644 index 00000000..c13de356 --- /dev/null +++ b/src/layouts.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import sys + +from dataclasses import dataclass +from pathlib import Path + +from src.paths import get_layouts_dir +from src.result_types import ErrorInfo, ErrorKind, Result + + +@dataclass(frozen=True, slots=True) +class LayoutFile: + """A bundled Manual Slop layout asset (.ini). Stores raw INI text + intended to be installed to cwd/manualslop_layout.ini by App._post_init + when the user's INI is empty or missing. The raw text is opaque to + this module; structure parsing (ImGui sections, [Window] entries) + is the consumer's responsibility. + [C: src/layouts.py:load_layouts_from_dir, src/gui_2.py:_install_default_layout_if_empty]""" + name: str + raw_text: str + source_path: Path + scope: str + + +def load_layouts_from_file(path: Path, scope: str) -> dict[str, LayoutFile]: + """Load ONE layout file and return a 1-entry dict. Public API matches + themes' load_themes_from_toml shape (dict out). + [C: src/layouts.py:load_layouts_from_dir]""" + out: dict[str, LayoutFile] = {} + if not path.exists() or not path.is_file(): + return out + try: + raw = path.read_text(encoding="utf-8", errors="replace") + except OSError as e: + _layout_err = Result(data=None, errors=[ErrorInfo(kind=ErrorKind.INVALID_INPUT, message=f"failed to read layout {path}: {e}", source="layouts.load_layouts_from_file", original=e)]) + print(f"warning: failed to read {path}: {e}", file=sys.stderr) + return out + out[path.stem] = LayoutFile(name=path.stem, raw_text=raw, source_path=path, scope=scope) + return out + + +def load_layouts_from_dir(path: Path, scope: str) -> dict[str, LayoutFile]: + """Load every .ini layout in `path`. Empty dict on missing dir + or all-skip (per the 'delete to turn off' pattern in + conductor/code_styleguides/feature_flags.md). Per-file errors are + drained via Result + stderr warn, mirroring + src/theme_models.py:load_themes_from_dir. + [C: src/layouts.py:load_layouts_from_disk]""" + out: dict[str, LayoutFile] = {} + if not path.exists() or not path.is_dir(): + return out + for child in sorted(path.iterdir()): + if not child.is_file(): + continue + if child.suffix.lower() != ".ini": + continue + try: + raw = child.read_text(encoding="utf-8", errors="replace") + except OSError as e: + _layout_err = Result(data=None, errors=[ErrorInfo(kind=ErrorKind.INVALID_INPUT, message=f"failed to read layout {child}: {e}", source="layouts.load_layouts_from_dir", original=e)]) + print(f"warning: failed to read {child}: {e}", file=sys.stderr) + continue + layout = LayoutFile(name=child.stem, raw_text=raw, source_path=child, scope=scope) + out[layout.name] = layout + return out + + +_LAYOUTS_CACHE: dict[str, LayoutFile] = {} + + +def load_layouts_from_disk() -> dict[str, LayoutFile]: + """Load all bundled layouts from the global layouts dir + (resolved via src/paths.py:get_layouts_dir()). Cached at the + module level; callers iterate the returned dict. The 'delete to + turn off' guarantee: a missing or empty layouts/ dir returns {}. + [C: src/gui_2.py:_install_default_layout_if_empty]""" + global _LAYOUTS_CACHE + layouts_dir = get_layouts_dir() + _LAYOUTS_CACHE = load_layouts_from_dir(layouts_dir, scope="global") + return _LAYOUTS_CACHE diff --git a/src/paths.py b/src/paths.py index 506ac258..d27e6c6a 100644 --- a/src/paths.py +++ b/src/paths.py @@ -58,6 +58,7 @@ class PathsConfig: tool_presets: Path personas: Path themes: Path + layouts: Path workspace_profiles: Path credentials: Path logs_dir: Path @@ -81,6 +82,7 @@ def _default_paths_config() -> PathsConfig: tool_presets = root_dir / "tool_presets.toml", personas = root_dir / "personas.toml", themes = root_dir / "themes", + layouts = root_dir / "layouts", workspace_profiles = root_dir / "workspace_profiles.toml", credentials = root_dir / "credentials.toml", logs_dir = root_dir / "logs" / "sessions", @@ -148,6 +150,7 @@ def initialize_paths(config_path: Optional[Path] = None) -> PathsConfig: tool_presets = _resolve_path("SLOP_GLOBAL_TOOL_PRESETS", "tool_presets", root_dir / "tool_presets.toml", config_path), personas = _resolve_path("SLOP_GLOBAL_PERSONAS", "personas", root_dir / "personas.toml", config_path), themes = _resolve_path("SLOP_GLOBAL_THEMES", "themes", root_dir / "themes", config_path), + layouts = _resolve_path("SLOP_GLOBAL_LAYOUTS", "layouts", root_dir / "layouts", config_path), workspace_profiles = _resolve_path("SLOP_GLOBAL_WORKSPACE_PROFILES", "workspace_profiles", root_dir / "workspace_profiles.toml", config_path), credentials = _resolve_path("SLOP_CREDENTIALS", "credentials", root_dir / "credentials.toml", config_path), logs_dir = _resolve_path("SLOP_LOGS_DIR", "logs_dir", root_dir / "logs" / "sessions", config_path), @@ -211,6 +214,11 @@ def get_global_themes_path() -> Path: [C: src/theme_2.py:load_themes_from_disk]""" return _cfg().themes +def get_layouts_dir() -> Path: + """Global layouts directory. Frozen at initialize_paths() time. + [C: src/layouts.py:load_layouts_from_disk]""" + return _cfg().layouts + def get_project_themes_path(project_root: Path) -> Path: """[C: src/theme_2.py:load_themes_from_disk]""" return project_root / "project_themes.toml" diff --git a/tests/conftest.py b/tests/conftest.py index 0e97796b..948e09f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -706,7 +706,7 @@ def live_gui(request) -> Generator["_LiveGuiHandle", None, None]: # To iterate: open sloppy.py interactively, arrange the layout, quit # (HelloImGui auto-saves to cwd), then copy manualslop_layout.ini over the # artifact below. - _default_layout_src = project_root / "tests" / "artifacts" / "manualslop_layout_default.ini" + _default_layout_src = project_root / "layouts" / "default.ini" if _default_layout_src.exists(): shutil.copy2(_default_layout_src, temp_workspace / "manualslop_layout.ini")