spacr.qt.screens.project_browser

Workflow inputs and outputs

Project Browser

Locate projects and inspect their stage, last run, disk use and stale outputs.

Open: the application’s Help/tools menus.

Inputs and outputs below include conditional alternatives. The guidance and handoff notes say which route applies.

Inputs

  • Run history and artifacts — Project run records, settings, output paths, artifact provenance, status and logs.

Outputs

  • Run history and artifacts — Project run records, settings, output paths, artifact provenance, status and logs.

API reference.

Module tutorial.

N4 — the Project Browser: every project on disk, in one table.

Navigating by folder is how spaCR has always been used, and it is why nobody can answer “which plates have been measured?”, “which one is the 400 GB?” or “which results no longer match the masks under them?” without opening six windows. This screen is that answer as a list.

It computes nothing. Every column comes from spacr.projects, which in turn assembles spacr.data_manager.scan_project() (the size and the unaccounted-for bytes), spacr.ports.declared_outputs() (the stage), spacr.artifacts.Registry.is_stale() (what is out of date) and spacr.chaining.next_steps() (what could run next). A browser with its own opinion about any of those is a browser that disagrees with the screen the user opens next.

Two things about it are load-bearing rather than cosmetic.

A project the registry has never seen is still listed. Projects are found by walking the disk, so a folder copied from a colleague appears the moment the browser is pointed at its parent — with its stage, its size and the date its files were last written. What it does not show is “0 stale”, which would read as clean: with no provenance there is nothing to compare against, so the state column says “unknown — nothing recorded” and the note says why.

The scan never blocks the window. Walking a plate folder is tens of thousands of stat calls, and doing it on the GUI thread is a frozen application for however long the filesystem takes. Everything goes through spacr.qt.job_runner.JobRunner, whose completion handlers here are bound methods — read that module’s docstring for what a closure connected to thread.finished costs. The background activity spinner follows the run registry on its own, so nothing here has to drive it.

And neither does opening the screen, nor opening its folder chooser. Two things here take a path the user typed on some other screen: the search folders make_project_browser_screen() seeds from the recent-source list, and the folder the “Add folder…” dialog starts in. Both used to be settled with an os.path.isdir on the GUI thread. Measured on one workstation, one such folder was under an autofs mount whose share was asleep and a single isdir on it had not returned after TWENTY SECONDS — which is not a slow screen, it is the whole application frozen with no traceback, and it was reported as “opening the project browser crashes spacr”. Both now ask spacr.qt.path_probe, which answers from a cache and stats in the background. Those two are the whole inventory: the walk runs on a worker, and every column, note and detail line the screen draws is rendered from the frozen spacr.projects.ProjectSummary that walk already returned, so filling the table and the detail pane reads nothing.

register() is not called at import; read its docstring.

Classes

ProjectBrowserScreen

The browser: a roots picker, a table of projects, and a detail pane.

Functions

make_project_browser_screen(→ PySide6.QtWidgets.QWidget)

Factory handed to spacr.qt.app.register_app().

register(→ bool)

Put the Project Browser in the app registry. Idempotent.

Module Contents

class spacr.qt.screens.project_browser.ProjectBrowserScreen(parent: PySide6.QtWidgets.QWidget | None = None, *, threaded: bool = True, roots: Tuple[str, ...] = ())[source]

Bases: PySide6.QtWidgets.QWidget

The browser: a roots picker, a table of projects, and a detail pane.

Parameters:
  • threaded – False runs the scan inline through the same JobRunner code path, so a test drives the real thing synchronously.

  • roots – folders to search on the first scan.

  • parent – parent widget; ownership only.

Build the project browser.

The project table (“Projects”) and the detail pane (“Project details”) fold by their headings and share a draggable edge in a CollapsibleSplitter.

Parameters:
  • parent – parent widget, or None.

  • threaded – scan on a worker thread. Set False in tests so a refresh finishes before it returns.

  • roots – folders to scan for projects.

active_jobs() → int[source]

How many worker threads are still winding down.

add_root(path: str, *, scan: bool = True) → bool[source]

Add a folder to search. True when it was not already there.

Parameters:

path – folder to add; ~ is expanded and the path made absolute. It is also recorded as a recent folder.

choose_root() → None[source]

Ask for a folder and add it.

The dialog opens on a folder the probe cache has already confirmed rather than simply on the last one searched; _start_directory() says why that distinction is the difference between a chooser and a frozen window.

closeEvent(event)[source]

Stop background work and unlink before going away.

Parameters:

event – the Qt close event.

forget_selected_root() → None[source]

Drop the selected search folder and scan again.

is_busy() → bool[source]

True while a scan is in flight.

rescan() → None[source]

Walk the search folders again, off the GUI thread.

roots() → Tuple[str, ...][source]

The folders that will be searched.

selected_root() → str[source]

The selected project’s root, or "".

show_detail(root: str) → str[source]

Draw the detail pane for one project. Returns what it drew.

Parameters:

root – absolute project root as listed; a root with no listed summary clears the pane and returns "".

summaries() → Tuple[source]

Every spacr.projects.ProjectSummary currently listed.

summary_for(root: str)[source]

The listed summary for one root, or None.

Parameters:

root – absolute project root, compared exactly with each summary’s root.

spacr.qt.screens.project_browser.make_project_browser_screen(app_key: str | None = None) → PySide6.QtWidgets.QWidget[source]

Factory handed to spacr.qt.app.register_app().

Seeds the search folders from the ones the user last pointed any source-taking screen at, so the browser is useful on first open instead of empty.

spacr.qt.screens.project_browser.register() → bool[source]

Put the Project Browser in the app registry. Idempotent.

Called from spacr.qt.SELF_REGISTERING_MODULES, which spacr.qt.run() runs after spacr.qt.app is fully executed and before MainWindow.__init__ reads the registry.

The row itself – the key, the name, the blurb, the section, the “no headless run” sentence, the API doc link and the nine translations of the display name – is declared in spacr.qt.app_catalog. spacr.qt.app.register_app() distributes those into the four tables each used to need a hand-edit in, and this function’s whole job is to name which row. That is what lets the app be registered without importing this module at all: the launch reads the table, and the screen is imported when somebody opens it.

Returns:

True if this call is what registered it. Safe to call again: a module imported twice, or a test that re-imports it, must not raise on the duplicate key.