Function Bundles

A reusable, ordered sequence of SASpro commands — each with its own preset — that can be applied in one gesture to the active view, fanned out across every view in a View Bundle, or packed into a chip on the shortcut canvas. Bundles are built by dragging shortcut tiles into the dialog; each tile dropped becomes a step. The dialog remembers your bundles between sessions and persists the chip layout on the canvas across restarts.

Workflow
Open Function Bundles. Create a bundle (New) and give it a name (F2 or double-click to rename). Drop shortcut tiles from the canvas onto the Steps list — each drop appends a step with that command's current preset. Reorder with the arrow buttons or by dragging within the list; edit a preset by double-clicking a step or using the Edit Preset button. Apply to Active View runs the whole bundle on the focused doc; Apply to View Bundle… fans it out across every view (and every file path) in a saved view bundle. Compress to Chip drops a purple chip on the shortcut canvas — drag it onto any view to apply, or Alt-drag it elsewhere to pass the bundle as a drop payload.

The signature behaviour of this tool is synchronous step chaining across asynchronous SASpro commands. Many SASpro tools (Statistical Stretch, GraXpert, Cosmic Clarity, RC-Astro, etc.) return from their drop handler immediately and continue processing in a background worker — so a naive sequence would run step N+1 against step N's unmodified image. The bundle runner solves this by arming a document-change watcher before every step, then blocking the next step until the document reports a change (either via its changed signal or via a bumped edit counter), with a special indefinite wait when Cosmic Clarity's headless flag is set. The result is a reliable, order-preserving pipeline that respects each tool's real completion.

Contents
1. The bundle concept
2. Building a bundle
3. Editing steps
4. The preset editor
5. Running a bundle
6. The function-bundle chip
7. Progress and step status
8. Script-level entry point
9. Technical details

1. The bundle concept

A Function Bundle is a named, ordered list of steps. Each step is a dictionary with at least a command_id (the id of a SASpro command, e.g. stretch_statistical, graxpert_bg, cosmic_clarity_sharpen) and an optional preset dictionary carrying that command's parameters. Steps are shaped exactly like the payloads a shortcut tile drops onto a view, which is why building a bundle is literally “dragging shortcut tiles into a list.”

A single SASpro session can hold any number of bundles, each with its own name. Bundle data lives in QSettings under the key functionbundles/v1 as a JSON array, so bundles survive restart and can be included in .sass shortcut-export files for sharing.

step = { "command_id": "stretch_statistical", "preset": { "target_median": 0.25, "linked": false }, } bundle = { "name": "Linear → Stretched", "steps": [ step_1, step_2, … ], }

The two panes of the dialog mirror this: the left pane lists bundles, the right pane shows the steps of the currently selected bundle. Everything you can do to a bundle (new, duplicate, delete, rename) is on the left; everything you can do to its steps (reorder, remove, clear, edit preset) is on the right.

2. Building a bundle

Three ways to add steps to the currently selected bundle:

Dragging a shortcut tile captures the preset at the moment of the drop. If you later change that tile's preset on the canvas, the bundle still runs with the preset you dropped. Rebuild the step (remove and redrop) if you want the new preset, or edit the step's preset in place with the preset editor.

A newly created bundle gets a placeholder name like “Function Bundle 3”; rename it with F2 or by double-clicking the entry. Duplicates get a “ (copy)” suffix. The badge on the chip (if any) updates automatically to reflect the current step count.

3. Editing steps

Each step shows as a row command_id — preset_label. The preset label is derived automatically: if the preset has a name or label field, that string is shown; otherwise a compact summary of its first three keys ({k1, k2, k3…}) is used. Hover a row to see the full line as a tooltip (useful when the row elides).

Available step actions:

Shortcuts in the Steps list
Delete / Backspace — remove the selected rows.
Ctrl+A — select every step.
Return / Enter — open the preset editor on the selected step.
Ctrl+drag within the list — reorder (standard Qt internal-move DnD).

Shortcuts in the Bundle list
F2 — rename the current bundle.
Right-click a bundle → Rename…

4. The preset editor

When you edit a step's preset, SASpro tries the step command's bespoke preset editor first — the same editor you'd see if you right-clicked that command's shortcut tile and chose “Edit Preset…”. These editors know what parameters the command accepts, their ranges, their units, and their validation rules. Not every command has a bespoke editor (particularly third-party plug-ins); for those, the editor falls back to a plain JSON text editor.

Fallback behaviour matters: if a bespoke editor exists and the user cancels it, nothing changes — the fallback JSON editor is NOT opened. Cancelling a rich UI means “I don't want to change this preset,” not “show me the raw JSON instead.” The JSON editor only appears when the command has no bespoke editor at all.

The JSON editor is a plain multiline text box seeded with the current preset in indented JSON. On OK, the text is parsed; if parsing fails, the error is shown and nothing changes. An empty text box commits a None preset (equivalent to clearing it).

5. Running a bundle

Three apply targets at the bottom of the right pane:

During a run, each step logs its index, command id, payload, and completion reason (signal, count, signal_sync, count_sync, timeout, or noop) via the main window's _log hook plus stdout. Cosmic Clarity steps get their own >>> BEGIN CC / <<< END CC markers so they're easy to spot in long logs.

An error raised by a single step does not stop the bundle. The error is captured, the next step runs anyway, and all errors are summarised in a single message box at the end. This is deliberate — a bundle run against twenty views shouldn't abort because one view hit a transient issue — but it means you should check the log and the status label after a run completes.

6. The function-bundle chip

Compress to Chip spawns a small purple widget — the function-bundle chip — on SASpro's shortcut canvas. The chip stays live for the whole session (and re-appears next launch); the dialog itself can be closed without destroying the chip.

Chip behaviours:

The number badge on the chip shows the current step count and recolours (purple when the bundle has steps, grey when it's empty). The chip's title tracks the bundle's name; rename the bundle and the chip updates automatically.

Multiple chips can live on the canvas at once (one per bundle). On launch they're placed in a staggered row near the upper-right so view-bundle chips (which land upper-centre) don't collide with them.

7. Progress and step status

Below the Steps list sits a status label and progress bar. During a run:

If you run the bundle against a View Bundle, the status covers one view at a time — so you see progress through step N/total for view 1, then view 2, and so on.

8. Script-level entry point

Function Bundles also integrates with SASpro's command/script system via the CommandSpec id function_bundle. Scripts can call it two ways:

Optional preset keys:

The runner does not step through ctx.run_command itself; it synthesises a single function_bundle payload and hands it to MainWindow._handle_command_drop, which is the same path a UI drop takes. This keeps script execution and UI execution bit-for-bit identical.

9. Technical details

Internal mechanisms numbered continuing from the usage sections.

10. Payload shape and normalisation

The internal step payload accepted by _handle_command_drop looks like:

{ "command_id": "stretch_statistical", # required "preset": { … }, # optional, per-command schema "on_base": true, # optional, apply to base layer only "label": "First stretch", # optional, for logging … # any extra keys pass through }

Bundles saved from older SASpro versions sometimes stored the command id under cid or id instead of command_id. The script runner's _normalize_steps_for_hcd accepts all three (in order of preference) and rewrites each step into the canonical shape before dispatch. Unknown extra keys pass through unchanged, so new per-command fields don't break old bundles.

11. Doc-change wait mechanism

The core of the sequential runner. For each step, the runner follows the arm → run → wait cycle:

# 1) ARM — before the step runs handle = arm_doc_change_watcher(sw) # captures: doc, edit_count_0, connects 'changed' signal # 2) RUN — synchronous call into main-window dispatcher mw._handle_command_drop(step, target_sw=sw) # 3) WAIT — until one of these is true: # a) the 'changed' signal has fired, OR # b) edit_count(doc) > edit_count_0, OR # c) elapsed >= timeout_ms AND Cosmic Clarity is NOT running reason = wait_for_doc_change(mw, sw, cid, handle, timeout_ms=120_000)

Arming before running is essential. Many SASpro tools apply synchronously inside their drop handler and emit changed before the dispatcher returns — if the watcher connected after the dispatch, that fire would be missed and the wait would time out even though the step had completed. The watcher connects first, the dispatch happens, and any synchronous signal reaches the listener.

The edit counter is a resilience hedge: not every doc class in SASpro's history has emitted changed consistently. The waiter looks for a counter on each of these attribute names in order: edit_count, _edit_count, revision, _revision, version; failing all of those, it falls back to len(history) / len(_history) / len(undo_stack). Callables are invoked. If none of them exist, the counter check is skipped and the wait depends on the signal alone.

The wait loop pumps Qt events every 25 ms with a sleep in between, so the UI stays responsive — the progress bar updates, status label refreshes, and buttons stay clickable (though their actions queue until the wait returns). On timeout, the wait returns timeout as the completion reason and the runner moves on; the per-step error list stays empty unless the dispatch itself raised.

12. No-op exemptions

Some steps intentionally don't modify the image — saves, exports, viewers, inspectors. Waiting for a document change after one of these would always hit timeout. The waiter maintains an explicit no-op set:

_NOOP_CIDS = { "checkpoint_save", "save_project", "save_active", "save_active_as", "export_fits", "export_fits_bundle", "plate_solve", # writes header, may or may not emit 'changed' "psf_viewer", "image_peeker", }

For any step whose command_id is in this set, the wait is skipped entirely and the runner moves immediately to the next step. plate_solve is in this set because it writes WCS cards into the header without necessarily emitting changed — keeping it here means plate solving in the middle of a bundle doesn't stall for two minutes before timing out.

13. Cosmic Clarity wait

Cosmic Clarity is SASpro's AI noise reduction and sharpening tool; it runs as a separate headless process and can take minutes on large images. The bundle runner knows about it specifically.

While a Cosmic Clarity run is in progress, two flags indicate it:

If either is true during the per-step wait loop, the 120 second timeout is bypassed — the loop keeps polling as long as CC is running, up to an upper bound of two hours. After CC's flag clears, a secondary _wait_for_cosmicclarity pass runs to make sure the subprocess has fully exited before the next step begins. This two-stage guard means a long CC call followed by Statistical Stretch never races the stretch against a mid-flight CC write.

14. Chip persistence

Chip positions are saved to QSettings under functionbundles/chips_v1 as a JSON array of { "index": …, "x": …, "y": … } records. Writes happen on every chip-drag release and on bundle delete, so the layout is always current.

Restore runs at dialog construction time: for each saved record, the matching bundle is looked up by index (chips bind to bundle positions, so reordering the bundle list without reassigning chips can produce stale bindings — this is why bundle delete explicitly removes the chip for that index). A missing or out-of-range index silently skips. If the shortcut canvas isn't available yet (startup race), the restore is a no-op — it'll run again the next time the dialog is opened.

Chip restore at app startup is driven by restore_function_bundle_chips(parent), which constructs the dialog singleton with auto_spawn_only=True — the dialog itself stays hidden, but its chips land on the canvas as if the dialog had just been opened.

15. Preset editor dispatch

Bespoke preset editors live under setiastro.saspro.shortcuts and are discovered via two lazy imports: _has_preset_editor_for_command(cid) checks whether an editor exists; _open_preset_editor_for_command(parent, cid, preset) opens it. The import is lazy so the Function Bundles module stays independent of the shortcuts module's own circular-import constraints.

If the shortcuts module fails to import or has no editor for the step's command id, the JSON editor fallback runs directly in _edit_preset_dialog — it's a plain QPlainTextEdit seeded with json.dumps(current, indent=2) and accepted only when the content parses as valid JSON.

16. Import / export in .sass files

Function bundles ride along with SASpro's shortcut-export files (.sass). The export side is export_function_bundles_payload(), which reads both the bundle definitions and the chip layout from QSettings and returns them as a { "bundles": […], "chips": […] } dict.

The import side is import_function_bundles_payload(payload, parent, replace_existing=False):

After the import, any live dialog is refreshed via reload_from_settings_after_import(); if no dialog exists, chip restore runs so the imported chips land immediately on the canvas.

Credits

Function Bundles is a workflow tool for SetiAstro Suite Pro by Franklin Marek. It builds on standard SASpro machinery: the shortcut MIME protocol (MIME_CMD), the main-window drop dispatcher (_handle_command_drop), the document model's changed signal, and the shortcut-canvas chip layer. There are no external algorithmic references — this tool orchestrates SASpro's existing commands rather than introducing new image-processing primitives. Qt and PyQt6 provide the UI framework, drag-and-drop infrastructure, and QSettings persistence.