View Bundles

A named group of SASpro views and / or disk files that can be targeted together with a single drop: drop a shortcut tile onto a bundle and that shortcut runs against every view in the bundle; drop a Function Bundle onto the group and every step runs against every target in order. Views are added by dragging their tab onto the dialog or a chip; files are added through a file picker, a recursive directory scan, or by dropping them from the OS. Each bundle has a persistent UUID so chips survive bundle reordering, and the file-path path runs headlessly — no view opens, no DocManager entry is created, so you can batch-process a hundred files without touching the MDI.

Workflow
Open View Bundles. Create a bundle (New Bundle) and give it a name (F2 or double-click). Add targets three ways: drag view tabs onto the dialog / bundle chip, click Add from Open… for a checkbox picker of all open views (with Select All), or use Add Files… / Add Directory (Recursive)… for disk files. Drop a shortcut onto the dialog, the Views list, or the bundle's chip to apply it to every target in the bundle. Drop a Function Bundle to apply its whole step sequence to every target. Compress to Chip pins the bundle as an orange chip on the shortcut canvas.

The signature behaviour of this tool is mixed on-disk + in-memory targeting. A single bundle can carry both open views (identified by their document pointer) and disk files (identified by path), and when a shortcut lands on the bundle, SASpro handles each kind correctly: views get the shortcut dispatched to their MDI subwindow and process in place; files are loaded headlessly into a transient ImageDocument, processed through the same dispatcher via a fake-subwindow shim, and saved back to disk — without ever appearing in the MDI or the DocManager. The file path is the critical bit: it lets you batch 100 raw frames through a pipeline overnight without opening 100 tabs.

Contents
1. The bundle concept
2. Adding views
3. Adding files
4. Removing and per-item actions
5. Applying a shortcut
6. Applying a Function Bundle
7. The view-bundle chip
8. Technical details

1. The bundle concept

A View Bundle is a named group of two kinds of targets:

Each bundle also has a persistent UUID (uuid.uuid4()) that identifies it independently of its name or position in the bundle list. Chips bind to the UUID, so renaming or reordering bundles never breaks a chip's binding. Bundle data lives in QSettings under viewbundles/v3 as a JSON array; older viewbundles/v2 and viewbundles/v1 stores are automatically read on first launch.

bundle = { "uuid": "d4f1…", # stable identity "name": "Narrowband set", "doc_ptrs": [ 140234567, 140234592 ], # id(doc) for open views "file_paths": [ "/data/H_1.fits", … ], # absolute paths }

The two panes mirror this: the left pane lists bundles, the right pane shows the targets in the currently selected bundle — views labelled [view] with their window title, files labelled [file] with their absolute path. Resolved view titles update automatically as the underlying windows are renamed.

2. Adding views

Three ways to add open views to the current bundle:

Views are stored by their current id(). If a view is closed and reopened, its pointer changes and the old entry becomes unresolvable (shown as (unresolved) [<number>] in the list); drag the new tab in to refresh. The alternative — binding by file path — is covered by the Files path (next section), where files are explicitly re-loadable on demand.

Duplicate adds are silently ignored: the bundle's view list dedupes on insert, preserving insertion order.

3. Adding files

Files — raw calibration frames, stacked lights, masters to update — can live in a bundle without ever being opened. Three ways to add them:

Files are stored as absolute paths and deduplicated on insert. The “last opened folder” QSetting is updated so the next picker opens where you were.

The file path is the unit of persistence: a bundle saved and reloaded still references the exact same file on disk, even if the file was moved between sessions. If the file no longer exists at apply time, that one target errors (reported in the summary) but doesn't stop the rest of the batch.

4. Removing and per-item actions

The Views list supports multi-select and standard removal:

Per-target actions in the right pane (via right-click on a row):

5. Applying a shortcut

Drop a shortcut tile onto the dialog background, the Views list, or the bundle's chip. SASpro parses the payload and applies the shortcut to every target in the bundle:

Views are processed first, files second. All errors (per-view and per-file) are collected and summarised at the end in a single dialog. The summary distinguishes view errors from file errors so you can see where things went wrong.

A per-target error does not stop the batch. A bundle of 50 files where 2 are corrupt will finish the remaining 48 and report the 2 errors at the end. If you need fail-fast behaviour, run the shortcut on views one at a time.

Dropping a nested bundle payload (command_id = "bundle") onto a View Bundle is ignored — bundle recursion isn't allowed, since the semantics (apply this bundle's views to that bundle's views?) are undefined. A function_bundle drop is the correct way to fan a multi-step sequence out across the view bundle; see the next section.

6. Applying a Function Bundle

Dropping a Function Bundle (command_id = "function_bundle") onto the dialog or chip runs its whole step sequence against every target in the view bundle, in order:

for target in view_bundle.targets: # views, then files activate(target) for step in function_bundle.steps: apply(step, target) wait_for_change() # (views only; see Function Bundles doc)

The waiter between steps (described in the Function Bundles doc) applies to view targets but is skipped for file targets — the headless file path runs each step synchronously on the transient document, so there's no asynchronous completion to wait for.

A function bundle dropped from the Function Bundle Chip carries its full step payload inline (the chip was designed to be a dependency-free drop target); dropping from the Function Bundles dialog works the same way. If the function bundle is empty, nothing happens.

7. The view-bundle chip

Compress to Chip spawns an orange widget — the view-bundle chip — on the shortcut canvas. The chip is bound to the bundle's UUID (not its position), so it survives bundle reordering, renaming, duplication, and .sass import.

Chip behaviours:

The badge shows the total target count (views + files combined) and recolours by composition: brown-orange when the bundle has open views, dark amber when it has only files, grey when empty. The title tracks the bundle's name live.

Multiple chips can live on the canvas at once (one per bundle). On first spawn, chips land near the upper-centre of the canvas, offset slightly per chip, so function-bundle chips (which land upper-right) don't collide.

8. Technical details

Internal mechanisms numbered continuing from the usage sections.

9. Headless file execution

The file-path target is the technically interesting part. Rather than opening each file in the MDI (which would create a visible window, a DocManager entry, and a documentAdded signal cascade), SASpro runs each file through a shim chain that reuses the normal command dispatcher on a transient document.

For each file target, the chain is:

1. load_image(path) # legacy I/O, returns (img, header, bit_depth, is_mono) 2. ImageDocument(img, meta) # transient — NOT registered with DocManager 3. view = _HeadlessView(doc, mw) # has .document and .apply_command() 4. sw = _FakeSubWindow(view) # has .widget() returning the view 5. for step in steps: mw._handle_command_drop(step, target_sw=sw) 6. save_image(doc.image, path, …) # legacy I/O writes back to disk

The two shim classes are small by design:

This design means nearly every SASpro command works headlessly out of the box, because they already go through the target_sw interface. The transient document is never added to the DocManager, so no signals fire, no UI updates, and the garbage collector can reclaim it once the save completes.

10. Save-back behaviour

After all steps have run against the transient document, the result is saved back to disk via the legacy save_image writer, which mirrors DocManager's parameter mapping:

save_image( img_array = doc.image, filename = target_path, original_format = ext, # inferred from target_path bit_depth = doc.metadata["bit_depth"], original_header = doc.metadata["original_header"], is_mono = doc.metadata["is_mono"], image_meta = doc.metadata.get("image_meta"), file_meta = doc.metadata.get("file_meta"), )

The default target path is the source path (overwrite). An alternate out_dir can be passed through the UI-based save path (_apply_payload_to_single_file_via_ui) if a non-destructive copy is wanted; the headless path currently always overwrites.

Format is inferred from the target path's extension. JPEG is normalised to jpg. Original header, bit depth, and mono flag flow through from the loaded document's metadata so round-tripping preserves what matters.

11. Doc resolution for views

Views are identified by id(), which is specific to a particular ImageDocument instance. Resolving a stored doc_ptr back to the live doc and its subwindow goes through two layers:

If both layers fail, the view is marked (unresolved) in the targets list and skipped at apply time (not an error, just not applied).

12. Payload unwrap

Some older code paths and some third-party scripts wrap command payloads a layer too deep, like:

{ "command_id": { "command_id": "stretch_statistical", "preset": … }, "preset": … }

The _unwrap_cmd_payload helper detects this shape (outer command_id is itself a dict containing command_id) and returns the inner dict. All drop handlers run their incoming payload through unwrap first, so wrapped payloads apply correctly without changes elsewhere.

13. Chip persistence

Chip positions are stored in QSettings under viewbundles/chips_v1 as a JSON array of { "uuid": …, "x": …, "y": … } records. Each record is keyed by the bundle UUID (not its index), so chips survive bundle reordering and renaming.

Writes happen on every drag release, bundle delete, and compress-to-chip call. Restore runs at dialog construction time: for each saved record, the matching bundle is looked up by UUID; if it still exists, a chip is spawned on the shortcut canvas with the saved position. Missing bundles (deleted outside the UI, or imported-away) are silently skipped.

Startup chip restore is driven by restore_view_bundle_chips(parent), which constructs the dialog singleton with auto_spawn_only=True — the dialog stays hidden, but its chips land on the canvas.

14. UUID bundle identity

Each bundle gets a uuid.uuid4() on creation. The UUID is what makes chips independent of bundle ordering and renaming: when a chip looks up its bundle, it does so by UUID, not by index. Duplicating a bundle generates a fresh UUID (so the duplicate gets its own chip if compressed); importing bundles from a .sass file preserves the saved UUIDs unless they collide with existing ones (in which case the imported bundle keeps its data but may conflict — current import does not resolve collisions, which is a known edge case for cross-user bundle sharing).

The older viewbundles/v2 and viewbundles/v1 schemas didn't carry UUIDs; the loader back-fills a fresh UUID per bundle on first read, which is why upgrading SASpro doesn't lose existing bundles but may re-anchor any pre-existing chip layout.

Credits

View 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 view-state tab-drag protocol (MIME_VIEWSTATE), the main-window drop dispatcher (_handle_command_drop), DocManager's resolve_doc_from_drag, the legacy image I/O (load_image / save_image), and the shortcut-canvas chip layer. The headless file-execution shim is new work for this tool; 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.