git.lucas.co / hou-control
SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git

commitcf4be060ce1fbd3e132cd8396a24fc2b28ade399
parent85a2505989
authorLucas Galante <[email protected]>
date2026-09-12 09:37
hc: add a setting to turn the startup prompt off

startup.show_prompt (default true, so nothing changes until it is turned
off) gates the "Open last file?" dialog in 123.py. With it off, Houdini
starts file-less -- not being asked is the same answer as clicking No --
and the autosave preference the dialog would have set is still applied
from default_autosave_state, or turning the prompt off would quietly take
autosave with it.

123.py deliberately does not import hc: an exception in a startup script
costs the whole launch, so it reads the JSON itself rather than going
through HCSettings and its DEFAULTS merge. That means it carries its own
fallbacks, and they had already drifted -- it fell back to
default_autosave_state=False while hcschema declared True. Both fallbacks
now sit in one _DEFAULTS dict, aligned with the schema, and
tools/check.py reads them back out of the source (importing the module
would run the startup logic) to assert they stay that way.

Co-Authored-By: Claude Opus 5 <[email protected]>

 python3.13libs/hc/hcschema.py |  5 +++++
 scripts/123.py                | 30 +++++++++++++++++++++----
 tools/check.py                | 51 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 82 insertions(+), 4 deletions(-)

diff --git a/python3.13libs/hc/hcschema.py b/python3.13libs/hc/hcschema.py
index d408668..d0a4059 100644
--- a/python3.13libs/hc/hcschema.py
+++ b/python3.13libs/hc/hcschema.py
@@ -61,6 +61,11 @@ SCHEMA = {
         help="Takes effect on next start.",
     ),
     "startup": {
+        "show_prompt": Setting(
+            "bool", True, label="Show Startup Prompt",
+            help="The 'Open last file?' dialog shown when Houdini starts with "
+                 "no .hip. With it off, Houdini starts file-less.",
+        ),
         "default_autosave_state": Setting("bool", True, label="Default Autosave State"),
     },
     "keycam": {
diff --git a/scripts/123.py b/scripts/123.py
index f6880c7..fb1e210 100644
--- a/scripts/123.py
+++ b/scripts/123.py
@@ -34,8 +34,19 @@ def _write_settings(data):
     settings_path.write_text(json.dumps(data, indent=4) + "\n")
 
 
-def _default_autosave_state():
-    return bool(_read_settings().get("startup", {}).get("default_autosave_state", False))
+# Fallbacks for the settings this script reads. It deliberately does not
+# import hc -- this runs before the package is needed, and an exception in a
+# startup script costs the whole launch -- so it cannot use HCSettings and its
+# DEFAULTS merge. tools/check.py asserts these still agree with hcschema.
+_DEFAULTS = {
+    "show_prompt": True,
+    "default_autosave_state": True,
+}
+
+
+def _startup_setting(key):
+    value = _read_settings().get("startup", {}).get(key, _DEFAULTS[key])
+    return bool(value)
 
 
 def _current_desktop_mode():
@@ -64,7 +75,7 @@ class StartupPrompt(QtWidgets.QDialog):
         layout.addWidget(label)
 
         self.autosave_checkbox = QtWidgets.QCheckBox("Enable autosave")
-        self.autosave_checkbox.setChecked(_default_autosave_state())
+        self.autosave_checkbox.setChecked(_startup_setting("default_autosave_state"))
         self.autosave_checkbox.setFocusPolicy(Qt.NoFocus)
         layout.addWidget(self.autosave_checkbox)
 
@@ -137,7 +148,18 @@ else:
     # Also runs under hython and batch, where there is no hou.qt and
     # StartupPrompt raises AttributeError before the rest of the package
     # has loaded. Skip the prompt and leave the session file-less.
-    if last_file and hou.isUIAvailable():
+    show_prompt = (last_file and hou.isUIAvailable()
+                   and _startup_setting("show_prompt"))
+
+    if hou.isUIAvailable() and not show_prompt:
+        # Nothing asked, so nothing is opened -- not asking is the same answer
+        # as clicking No. The autosave preference the dialog would have set is
+        # still applied, or turning the prompt off would quietly take autosave
+        # with it.
+        hou.setPreference(
+            "autoSave", "1" if _startup_setting("default_autosave_state") else "0")
+
+    if show_prompt:
         dialog = StartupPrompt(last_file)
         dialog.exec()
 
diff --git a/tools/check.py b/tools/check.py
index 5544202..ff001b8 100644
--- a/tools/check.py
+++ b/tools/check.py
@@ -836,6 +836,56 @@ def check_node_colors():
     check("updateNodeColors undo grouping", recolor_is_one_undo)
 
 
+def check_startup_script():
+    """scripts/123.py reads hc_settings.json without importing hc.
+
+    That is deliberate -- an exception in a startup script costs the whole
+    launch -- but it means the file carries its own fallbacks instead of going
+    through HCSettings and its DEFAULTS merge, and those can drift. They had:
+    123.py fell back to default_autosave_state=False while the schema declared
+    True. Importing the module here would run the startup logic, so read the
+    literals out of the source instead.
+    """
+    print("startup script")
+
+    import ast
+
+    def fallbacks_match_the_schema():
+        path = ROOT / "scripts" / "123.py"
+        tree = ast.parse(path.read_text())
+        declared = None
+        for node in tree.body:
+            if isinstance(node, ast.Assign) and any(
+                    getattr(t, "id", None) == "_DEFAULTS" for t in node.targets):
+                declared = ast.literal_eval(node.value)
+        assert declared, "123.py no longer declares a _DEFAULTS dict"
+
+        for key, value in declared.items():
+            setting = hcschema.lookup(("startup", key))
+            assert setting is not None, f"startup.{key} is not declared in hcschema"
+            assert setting.default == value, (
+                f"123.py falls back to startup.{key}={value!r}, "
+                f"hcschema says {setting.default!r}"
+            )
+        return f"{len(declared)} fallback(s) agree with hcschema"
+
+    check("123.py fallbacks", fallbacks_match_the_schema)
+
+    def prompt_is_gated_on_the_setting():
+        """The dialog must not be constructed when show_prompt is off."""
+        source = (ROOT / "scripts" / "123.py").read_text()
+        assert 'show_prompt' in source, "123.py does not read show_prompt"
+        assert source.count("StartupPrompt(last_file)") == 1, \
+            "more than one path constructs the dialog"
+        # The only construction site has to sit under the gate.
+        gate = source.index("if show_prompt:")
+        assert gate < source.index("StartupPrompt(last_file)"), \
+            "the dialog is built before the show_prompt gate"
+        return "the dialog is built only under the gate"
+
+    check("startup prompt gate", prompt_is_gated_on_the_setting)
+
+
 def main():
     check_settings()
     check_panel()
@@ -847,6 +897,7 @@ def main():
     check_geometry()
     check_node_ops()
     check_node_colors()
+    check_startup_script()
     print(f"\n{passed} passed, {failed} failed")
     return 1 if failed else 0