diff --git a/src/gui_2.py b/src/gui_2.py index d80a1ef4..6f5ebba0 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -698,6 +698,11 @@ class App: self.runner_params.callbacks.post_init = _profiled_post_init self._fetch_models(self.current_provider) md_options = markdown_helper.get_renderer().options + pre_install_result: Result[bool] = _install_default_layout_pre_run_result(self) + if not pre_install_result.ok: + err = pre_install_result.errors[0] + if hasattr(self, "_startup_timeline_errors"): + self._startup_timeline_errors.append(("_install_default_layout_pre_run", err)) run_result = _run_immapp_result(self) if not run_result.ok: err = run_result.errors[0] @@ -1488,7 +1493,8 @@ def _install_default_layout_if_empty(src_ini: Path, dst_ini: Path) -> Result[boo 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]""" + src/gui_2.py:App._post_init, + src/gui_2.py:App.run (pre-immapp disk write)]""" try: dst_text: str = dst_ini.read_text(encoding="utf-8", errors="replace") if dst_ini.exists() else "" except OSError: @@ -1532,6 +1538,56 @@ def _install_default_layout_if_empty_result(app: "App", src: Path, dst: Path) -> [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 _install_default_layout_pre_run_result(app: "App") -> Result[bool]: + """Pre-immapp.run disk-only install for App.run entry point. + + HelloImGui loads `runner_params.ini_filename` from disk BEFORE the + post_init callback fires (imgui.load_ini_settings_from_disk runs in + the load_user_pref phase before callback dispatch). So writing to cwd + after post_init is too late for this session. + + This drains the same helper but skips the live-session apply + (load_ini_settings_from_memory) because imgui is not yet initialized + when App.run calls this before _run_immapp_result. The disk write is + what matters -- HelloImGui then reads it as its initial state. + + Returns Result[data=True] when installed, Result[data=False] when + skipped (existing valid INI). + + [C: src/gui_2.py:App.run, src/gui_2.py:App._post_init]""" + from src.layouts import get_layouts_dir + src_path: Path = get_layouts_dir() / "default.ini" + dst_path: Path = Path.cwd() / "manualslop_layout.ini" + try: + dst_text: str = dst_path.read_text(encoding="utf-8", errors="replace") if dst_path.exists() else "" + except OSError: + dst_text = "" + is_empty: bool = len(dst_text) < 1000 or "[Window][" not in dst_text + if not is_empty: + return Result(data=False) + try: + src_text: str = src_path.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_path}: {e}", + source="gui_2._install_default_layout_pre_run_result", + original=e, + )]) + try: + dst_path.parent.mkdir(parents=True, exist_ok=True) + dst_path.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_path}: {e}", + source="gui_2._install_default_layout_pre_run_result", + original=e, + )]) + sys.stderr.write(f"[GUI] pre-run installed default layout: {src_path} -> {dst_path}\n") + return Result(data=True) def _run_immapp_result(app: "App") -> Result[None]: """Drain-aware variant of App.run immapp.run() call (L728 INTERNAL_SILENT_SWALLOW). diff --git a/tests/test_default_layout_install.py b/tests/test_default_layout_install.py index 7496c66d..e968e294 100644 --- a/tests/test_default_layout_install.py +++ b/tests/test_default_layout_install.py @@ -48,12 +48,15 @@ def _read_launch_log(log_suffix: str) -> str: return "" -def _assert_live_session_apply(log_suffix: str) -> None: +def _assert_install_applied(log_suffix: str) -> None: text: str = _read_launch_log(log_suffix) - assert "and applied to live session" in text, ( - f"install write succeeded but live-session apply did not happen; " - f"expected the live-apply confirmation line in stderr, got: " - f"{[l for l in text.splitlines() if 'installed' in l]!r}" + install_msgs: list[str] = [l for l in text.splitlines() if "pre-run installed" in l or "applied to live session" in l] + assert install_msgs, ( + f"install was not invoked for this run; expected one of " + f"'[GUI] pre-run installed default layout: ...' or " + f"'[GUI] installed default layout: ... (and applied to live session)' " + f"in stderr, got install-related lines: " + f"{[l for l in text.splitlines() if 'default layout' in l]!r}" ) @@ -185,7 +188,7 @@ def test_default_layout_installed_when_ini_missing(tmp_path: Path) -> None: proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_missing") try: _assert_installed_default(workspace) - _assert_live_session_apply("ini_missing") + _assert_install_applied("ini_missing") finally: _terminate(proc) shutil.rmtree(workspace, ignore_errors=True) @@ -199,7 +202,7 @@ def test_default_layout_installed_when_ini_empty(tmp_path: Path) -> None: proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_empty") try: _assert_installed_default(workspace) - _assert_live_session_apply("ini_empty") + _assert_install_applied("ini_empty") finally: _terminate(proc) shutil.rmtree(workspace, ignore_errors=True)