Private
Public Access
0
0

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
This commit is contained in:
2026-06-29 14:20:51 -04:00
parent 89f4d1029e
commit 7577d7d28b
4 changed files with 90 additions and 1 deletions
+81
View File
@@ -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
+8
View File
@@ -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"
+1 -1
View File
@@ -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")