From f3cd7bc2ff0b651a47c9232417b8e66c5785de38 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 29 Jun 2026 14:48:22 -0400 Subject: [PATCH] feat(gui): add _install_default_layout_if_empty helpers for install-on-empty-INI Module-level _install_default_layout_if_empty(src, dst) reads the bundled layout from src, decides if dst is missing/empty/small (< 1000 bytes or no [Window][ header), copies src -> dst on true, and returns Result[bool]. On OSError reading/writing, returns Result[data=False, errors=[ErrorInfo]] so App._post_init can drain to _startup_timeline_errors per the data-oriented convention. _install_default_layout_if_empty_result(app, src, dst) is the drain-plane passthrough that mirrors _post_init_callback_result. Wiring into App._post_init lands in the next commit. --- src/gui_2.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/gui_2.py b/src/gui_2.py index 7fd2d76c..1f239f2a 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -1466,7 +1466,58 @@ def _post_init_callback_result(app: "App") -> Result[None]: message=f"on_warmup_complete callback registration failed: {e}", source="gui_2._post_init_callback_result", original=e, +)]) +def _install_default_layout_if_empty(src_ini: Path, dst_ini: Path) -> Result[bool]: + """Install bundled layout to cwd when the user's INI is missing/empty/small. + + Decision rule: dst_ini is "empty" when its content is fewer than 1000 + bytes OR has no [Window][ header. On empty, copies src_ini -> dst_ini + and returns Result(data=True). On non-empty (user customized), returns + Result(data=False) without overwriting. On OSError reading src or + writing dst, returns Result(data=False, errors=[ErrorInfo]) so the + legacy wrapper in App._post_init can drain to _startup_timeline_errors. + + [C: src/gui_2.py:_install_default_layout_if_empty_result, + src/gui_2.py:App._post_init]""" + try: + dst_text: str = dst_ini.read_text(encoding="utf-8", errors="replace") if dst_ini.exists() else "" + except OSError: + dst_text = "" + is_dst_empty: bool = len(dst_text) < 1000 or "[Window][" not in dst_text + if not is_dst_empty: + return Result(data=False) + try: + src_text: str = src_ini.read_text(encoding="utf-8", errors="replace") + except OSError as e: + return Result(data=False, errors=[ErrorInfo( + kind=ErrorKind.INTERNAL, + message=f"Could not read bundled layout {src_ini}: {e}", + source="gui_2._install_default_layout_if_empty", + original=e, )]) + try: + dst_ini.parent.mkdir(parents=True, exist_ok=True) + dst_ini.write_text(src_text, encoding="utf-8") + except OSError as e: + return Result(data=False, errors=[ErrorInfo( + kind=ErrorKind.INTERNAL, + message=f"Could not write layout {dst_ini}: {e}", + source="gui_2._install_default_layout_if_empty", + original=e, + )]) + sys.stderr.write(f"[GUI] installed default layout: {src_ini} -> {dst_ini}\n") + return Result(data=True) +def _install_default_layout_if_empty_result(app: "App", src: Path, dst: Path) -> Result[bool]: + """Drain-aware variant of _install_default_layout_if_empty. + + Passthrough wrapper so App._post_init can call the install via the + drain-plane naming convention (mirrors _post_init_callback_result). + The wrapped function already returns Result[bool] and catches OSError + internally; this wrapper exists for naming consistency and to give + any future exception-logging policy a single hook point. + + [C: src/gui_2.py:_install_default_layout_if_empty, src/gui_2.py:App._post_init]""" + return _install_default_layout_if_empty(src, dst) def _run_immapp_result(app: "App") -> Result[None]: """Drain-aware variant of App.run immapp.run() call (L728 INTERNAL_SILENT_SWALLOW).