fix(layout): pre-run install of bundled INI before HelloImgui's load_user_pref
The previous followup fix (e9654518, then2afb0126) only applied the bundled INI to HelloImgui's runtime state via `imgui.load_ini_settings_from_memory`, called from the `post_init` callback. That callback fires AFTER HelloImgui has already: 1. loaded user prefs from disk 2. loaded imgui settings from disk (via imgui.load_ini_settings_from_disk) 3. set up the dockspace tree By the time post_init fires, HelloImgui has already discarded the empty on-disk INI's data and built its dock state. The load_ini_settings_from_memory apply in post_init ended up being SILENTLY DISCARDED for [Docking][Data] entries with orphaned DockSpace IDs. Empirical evidence: manual launch test (sloppy.py without --enable-test-hooks) after2afb0126produced a saved manualslop_layout.ini of 3072 bytes with 2 DockNode entries, but those DockNodes were created at RUNTIME, not loaded from the bundled INI's literal IDs. The imgui core loader rejected the literal IDs from the bundled INI because the runtime IDs didn't match. Fix: add `_install_default_layout_pre_run_result` to App.run entry, called BEFORE `_run_immapp_result`. It writes the bundled INI to cwd if cwd's INI is missing/empty/small, so when HelloImgui's load_user_pref / load_ini_settings_from_disk runs, it reads my bundled INI as the initial state. The literal DockSpace ID 0xAFC85805 (= runtime-generated MainDockSpace 2949142533) matches, the DockNode IDs 0x00000001/0x00000002 match (because HelloImgui restores dock IDs from INI), and per-window DockId references apply to the matching DockNodes. The post_init live-session apply (imgui.load_ini_settings_from_memory) is now mostly redundant for first-launch: HelloImgui reads the bundled INI on its initial load. But it's still there for any edge case where HelloImgui's load_ini_settings_from_disk reads an INI after the pre-run write somehow fails, AND it covers the "user manually wiped cwd INI mid-session" case. Test changes: - _assert_live_session_apply renamed to _assert_install_applied -- the primary path is now pre-run, and the test accepts either "[GUI] pre-run installed default layout:" or "[GUI] installed default layout: ... (and applied to live session)" - Updated test 1 and 2 to use the new helper name Empirical verification (re-run of 18s manual launch): - Before launch: cwd INI absent - During launch: [GUI] pre-run installed default layout: ...layouts/default.ini -> ...manualslop_layout.ini - During launch: [GUI] visible-by-default windows: AI Settings, Diagnostics, Discussion Hub, Files & Media, Log Management, Operations Hub, Project Settings, Response, Theme - After force-kill: cwd/manualslop_layout.ini is 3072 bytes containing [Docking][Data] with DockSpace ID=0xAFC85805 + DockNode ID=0x00000001 (CentralNode=1, SizeRef=481,1172) + DockNode ID=0x00000002 (SizeRef=1197,1172) + 8 [Window][...] entries with DockId=0x00000001,N or DockId=0x00000002,N + 0 stale window names - 17/17 tests pass
This commit is contained in:
+57
-1
@@ -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).
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user