spacr.qt.recipes

Settings templates — named bundles you can reuse and share.

The historical Recipe API, JSON format and storage paths remain compatible.

A lab does not run one set of settings; it runs a handful, each tied to a preparation. “Toxo PVM, 40×” is a real thing people say to each other, and until now the only way to carry it between sessions was a settings CSV in a folder somebody had to remember, with no name on it, no record of which module it belonged to, and no way to tell whether it was written by the version of spaCR about to consume it.

A recipe fixes all four:

  • it has a name the user chose;

  • it knows its module, so applying a Mask recipe to Measure is refused rather than silently writing seventeen keys that mean nothing there;

  • it records the spaCR version it was captured with, and applying it under a different one says so, listing the settings that no longer exist and the ones that have appeared since;

  • it is one file, so sharing it is sending a file.

Storage is ~/.spacr/recipes/<module>/<slug>.json, honouring RECIPE_DIR_ENV — the same shape spacr.macro.macros_dir() uses, so a lab that redirects one redirects both.

The file format is deliberately boring:

{
  "spacr_recipe": 1,
  "name": "Toxo PVM, 40x",
  "app_key": "mask",
  "spacr_version": "1.3.6",
  "created": "2026-08-03T22:41:07",
  "notes": "",
  "settings": {"cell_channel": 1, ...}
}

spacr_recipe is a format version, not the spaCR version: the two change for unrelated reasons and conflating them is how a reader ends up refusing a file it could have read.

Classes

Recipe

One named settings bundle.

RecipeDialog

List, apply, share and delete the recipes for one module.

Functions

apply_recipe(→ int)

Write a recipe's settings into screen. Returns how many landed.

capture_recipe(→ Recipe)

Build a recipe from a screen's current settings.

compatibility_note(→ str)

Which of the recipe's settings this build has no home for.

delete_recipe(→ bool)

Remove a recipe's file. True when something was removed.

install(→ Optional[PySide6.QtWidgets.QToolButton])

Add a Templates button to screen's settings search strip.

install_help_action(→ Optional[PySide6.QtGui.QAction])

Add Settings templates… to the window's Help menu.

install_window_hooks(→ Optional[_StackWatcher])

Wire recipes into a live main window.

list_recipes(→ List[Recipe])

Every readable recipe, newest first.

load_recipe(→ Recipe)

Read one recipe file.

open_recipes(→ RecipeDialog)

Open the recipe dialog for screen.

recipes_dir(→ str)

The folder holding recipes, optionally for one module.

save_recipe(→ str)

Write recipe and return its path.

spacr_version(→ str)

The running spaCR version, or "unknown" if it cannot be read.

version_note(→ str)

What to tell the user about the version gap, or "" if there is none.

Module Contents

class spacr.qt.recipes.Recipe[source]

One named settings bundle.

Parameters:
  • name – display name chosen by the user; its slug becomes the file stem when the recipe is saved.

  • app_key – key of the module the settings belong to; it picks the recipe’s subfolder and is checked against the screen on apply.

  • settings – setting key to value mapping written into the screen.

  • spacr_version – spaCR version the recipe was saved with; filled in on save when empty.

  • created – ISO-8601 timestamp of creation, to the second; filled in on save when empty and used to sort listings newest first.

  • notes – free-text notes stored with the recipe.

  • path – file the recipe was loaded from or saved to; not written into the file itself.

classmethod from_json(data: Dict[str, Any], path: str = '') → Recipe[source]

Build a recipe from a parsed file.

Parameters:

data – the parsed JSON mapping; it must carry an integer spacr_recipe format version no newer than this build reads and a settings dict. Missing name falls back to "Untitled".

Raises:

ValueError – when the mapping is not a recipe at all, or is a format version this build does not understand. Both are worth an explicit error: silently treating an arbitrary JSON file as a settings bundle is how a user ends up applying somebody’s package.json to a segmentation run.

to_json() → Dict[str, Any][source]

The on-disk mapping. path is where it lives, not part of it.

class spacr.qt.recipes.RecipeDialog(screen, parent: PySide6.QtWidgets.QWidget | None = None)[source]

Bases: PySide6.QtWidgets.QDialog

List, apply, share and delete the recipes for one module.

Parameters:
  • screen – the module screen whose recipes these are. Its app_key is what the list is filtered by, and it becomes the dialog’s parent when none is given.

  • parent – parent widget. Defaults to screen.

Build the recipe dialog for one module screen.

Parameters:
  • screen – the module screen whose settings recipes are saved and applied; its app_key scopes which recipes are listed.

  • parent – parent widget; defaults to screen.

detail_text() → str[source]

The line under the list. Public so tests read what users read.

recipes() → List[Recipe][source]

The recipes currently listed.

reload() → None[source]

Re-read the module’s recipe folder and repopulate the list.

selected() → Recipe | None[source]

The highlighted recipe, or None.

set_confirmation_runner(runner: Callable[[PySide6.QtWidgets.QMessageBox], Any]) → None[source]

Replace how the version/compatibility confirmation is run.

The default enters the real modal Apply/Cancel loop. Tests inject a runner that inspects the fully configured message box and returns an answer without blocking; a host can use the same seam for a custom presentation.

Parameters:

runner – callable given the configured Apply/Cancel QMessageBox; the recipe is applied only when it returns QMessageBox.Apply.

spacr.qt.recipes.apply_recipe(recipe: Recipe, screen) → int[source]

Write a recipe’s settings into screen. Returns how many landed.

Delegates to AppScreen.apply_settings_dict, which is the same path the settings-CSV import takes — so a recipe cannot reach a widget that an imported CSV could not, and neither can drift from the other.

Raises:

ValueError – when the recipe belongs to a different module. It is refused rather than partially applied: the keys that happen to overlap between two modules are exactly the generic ones (src, verbose, n_jobs), so a “successful” cross-module apply writes the least meaningful half and reports success.

Parameters:
  • recipe – the recipe to apply; its settings are copied, not mutated.

  • screen – the module screen to write into; its app_key must match the recipe’s (when both are set) and it must provide apply_settings_dict.

spacr.qt.recipes.capture_recipe(screen, name: str, notes: str = '') → Recipe[source]

Build a recipe from a screen’s current settings.

Uses SettingsWidgets.collect, so what is captured is exactly what a Run would use — including the defaults the user never touched. That is deliberate: a recipe is meant to reproduce a result, and a bundle of only the edits reproduces something different the day a default changes.

Parameters:
  • screen – the module screen to capture; it must have a settings model (_settings_model) or ValueError is raised, and its app_key is stored on the recipe.

  • name – display name for the recipe; empty gives "Untitled".

spacr.qt.recipes.compatibility_note(recipe: Recipe, model) → str[source]

Which of the recipe’s settings this build has no home for.

The version gap says that something may have moved; this says what. Reported as counts plus the first few names, because a recipe from two releases back can differ in thirty keys and a wall of them is not a warning, it is noise.

Parameters:
  • recipe – the bundle.

  • model – the screen’s SettingsWidgets.

spacr.qt.recipes.delete_recipe(recipe: Recipe) → bool[source]

Remove a recipe’s file. True when something was removed.

Parameters:

recipe – the recipe whose path is deleted; an empty or missing path removes nothing.

spacr.qt.recipes.install(screen) → PySide6.QtWidgets.QToolButton | None[source]

Add a Templates button to screen’s settings search strip.

The strip is where a settings bundle belongs — directly above the settings it bundles — and it already exists, so this costs no chrome of its own. Returns None when the screen has no strip (a bespoke screen), or when one is already installed.

Parameters:

screen – the module screen; its _settings_search strip must provide add_trailing_widget. If the screen already has a _recipe_button, that button is returned unchanged.

spacr.qt.recipes.install_help_action(window: PySide6.QtWidgets.QMainWindow) → PySide6.QtGui.QAction | None[source]

Add Settings templates… to the window’s Help menu.

Returns the action, or None when there is no Help menu or one is already installed. The command palette mirrors menu actions, so this also makes recipes reachable from Ctrl+K for free.

Parameters:

window – the main window; its menu-bar menu titled “Help” (& ignored) receives the action, before the first separator.

spacr.qt.recipes.install_window_hooks(window: PySide6.QtWidgets.QMainWindow) → _StackWatcher | None[source]

Wire recipes into a live main window.

Called once from spacr.qt.shortcuts.install(). Every failure is logged and swallowed: a missing recipe button must not cost a window.

Parameters:

window – the main window; the Help-menu action is added to it and its _stack screen stack is watched so each shown screen gets a Templates button. Without a _stack the result is None.

spacr.qt.recipes.list_recipes(app_key: str | None = None) → List[Recipe][source]

Every readable recipe, newest first.

Unreadable files are logged and skipped rather than raising: one corrupt file must not make the whole list unopenable.

Parameters:

app_key – restrict to one module.

spacr.qt.recipes.load_recipe(path: str) → Recipe[source]

Read one recipe file.

Parameters:

path – path to a recipe JSON file; it is recorded on the returned recipe.

Raises:

ValueError – on anything that is not a readable recipe.

spacr.qt.recipes.open_recipes(screen, parent: PySide6.QtWidgets.QWidget | None = None) → RecipeDialog[source]

Open the recipe dialog for screen.

Parameters:

screen – the module screen whose recipes are listed and applied, passed to RecipeDialog.

spacr.qt.recipes.recipes_dir(app_key: str | None = None) → str[source]

The folder holding recipes, optionally for one module.

~/.spacr/recipes (or RECIPE_DIR_ENV), with one subfolder per module so a listing is already scoped and a user can hand over a whole module’s worth by copying one directory. Created on first use.

Parameters:

app_key – restrict to one module’s folder.

spacr.qt.recipes.save_recipe(recipe: Recipe, directory: str | None = None) → str[source]

Write recipe and return its path.

Parameters:
  • recipe – the bundle. Its spacr_version and created are filled in here when empty, so a caller never has to remember to stamp them — the stamp is the point of the format.

  • directory – override the destination (used by export).

spacr.qt.recipes.spacr_version() → str[source]

The running spaCR version, or "unknown" if it cannot be read.

spacr.qt.recipes.version_note(recipe: Recipe, current: str | None = None) → str[source]

What to tell the user about the version gap, or "" if there is none.

Returns a sentence, not a boolean, because “captured with 1.3.4, you are on 1.3.6” is the information — a bare warning icon says only that something might be wrong and leaves the user no way to judge it.

Parameters:
  • recipe – the bundle about to be applied.

  • current – override the running version (tests).