spacr.qt.widgets.home

HomePage — the Home screen.

┌────────────────────────────────────────────────────────────────┐ │ 🖼 spaCR End-to-end microscopy → single-cell measurements … │ │ ┌ Mask · running ────── 41 of 96 ──── [Open] [Pause] ────────┐ │ │ │ Home │ Core │ Data │ Segmentation models │ Results │ Toxo │ │ QUEUED │ │ CORE 9 ────────────────────────────────────────────────── │ │ RECENT │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ SYSTEM │ │ │ ▧ │ │ ▧ │ │ ▧ │ │ ▧ │ │ ▧ │ │ │ NEWS │ │ │Mask│ │Time│ │Moti│ │Meas│ │Anno│ │ │ TOTALS │ │ └────┘ └────┘ └────┘ └────┘ └────┘ │ │ ──────── │ │ DATA 6 ───────────────────────────────────────────────────│ │ ● Alpha │ │ ┌────┐ ┌────┐ … │ │ ● Beta │ └────────────────────────────────────────────────────────────┘ │ ● Stable │ Hover a tile to see what it does. │ └────────────────────────────────────────────────────────────────┘

The first tab displays every registered app grouped into the same sections as the category-filter tabs. AppTile supplies one consistent icon-and- name tile in every view; tooltips and the hint bar provide descriptions. categories and bands remain separate inputs so navigation and the all-app layout can evolve independently while both derive from spacr.qt.app.APPS.

Maturity is represented by each tile’s stage property and the legend, using spacr.qt.app.APP_STAGE and spacr.qt.theme.STAGE_HOVER. The right column presents state rather than navigation: queued work, recent runs, system information, release news, totals, and maturity labels.

Home subscribes to spacr.qt.bridge.registry to display jobs started by any screen. RunningBanner exposes only the controls supported by the worker; cooperative Pause remains disabled when the pipeline has no spacr.qt.bridge.PauseGate checkpoints. Widget colors are resolved from spacr.qt.theme.active_palette() so runtime theme changes remain consistent.

Attributes

Classes

AppTile

The tile: a large square-ish button, icon over module name.

HomePage

Home. tile_clicked(str key) fires when a tile is pressed.

NewsPanel

Every spaCR release, with links, in a box the reader can resize.

Panel

Captioned box for the right-hand column.

QueuedPanel

The plate queue, when there is one.

RecentRunsPanel

Last few journalled runs of a REAL module; each row navigates.

RunningBanner

"spaCR is doing something right now" — with honest controls.

StageLegend

What the three hover colours mean. One row per stage.

SystemPanel

GPU / VRAM / Disk, read on build and on every Home revisit.

TotalsPanel

Aggregate counts from the automatically complete run journal.

Functions

active_palette(→ dict)

The palette for the theme that is on screen right now.

Module Contents

class spacr.qt.widgets.home.AppTile(text: str, description: str = '', icon: PySide6.QtGui.QIcon | None = None, *, width: int, height: int, icon_px: int = 52, stage: str = 'stable', parent=None)[source]

Bases: PySide6.QtWidgets.QPushButton

The tile: a large square-ish button, icon over module name.

One class for every tab including Home. There used to be two — a dense icon-beside-name row for Home and a tall card carrying the app’s one-line description everywhere else — and the difference made the first tab read as a list and the rest as a launcher. The description is gone with them: it was a third copy of a sentence already on the tooltip and in the hint bar at the foot of the page, and three lines of 11 px grey under every tile is what made the tiles small enough to need two sizes in the first place.

Deliberately not an HTile subclass. HTile is a horizontal row whose name and description live in a QLabel stack beside the button’s own icon; this is a vertical stack with the icon drawn as a child label. It has its own object name, AppTile, so the stylesheet can give it a height floor without giving one to every horizontal tile in the app.

Its height floor is in the QSS, not here, and that is not a style choice. setFixedSize does not survive polish, and neither does answering through sizeHint / minimumSizeHint: the app stylesheet’s blanket QPushButton { min-height: 22px } becomes a real setMinimumHeight(22), and qSmartMinSize lets an explicit minimum override the hints. On a page that does not fit, every tile then collapses to 22 px and paints its name over its icon. See spacr.qt.theme.TILE_H. The hints below are still worth having — they are what the layout prefers — and heightForWidth is overridden with them because QWidgetItem::sizeHint reads it in preference to sizeHint().height() whenever it is available.

Parameters:

stage – stable / beta / alpha. Set as a Qt property, which is what the stylesheet’s QPushButton#AppTile[stage="alpha"]:hover rule selects on. Set before the widget is first polished, or the rule does not apply until something else forces a repolish.

Build one tile: an icon over a module name.

Parameters:
  • text – the module name drawn on the tile, and what the tile is identified by.

  • description – accepted and NOT DRAWN. The tile stopped showing it because it was a third copy of a sentence already on the tooltip and in the hint bar, and three lines of grey under every tile is what forced two tile sizes in the first place. Kept in the signature so callers that pass it still work.

  • icon – the module’s mark, drawn above the name.

  • width – tile width in pixels.

  • height – tile height in pixels.

  • icon_px – the icon’s edge in pixels.

  • stage – maturity – "stable", "beta" or "alpha". Set as a Qt property, so the theme paints the badge and the maturity filter can find the tile without reading its text.

  • parent – parent widget.

heightForWidth(width: int) → int[source]

At least the tile height, more if a child somehow needs it.

Parameters:

width – proposed tile width in pixels, passed to the base implementation.

is_name_elided() → bool[source]

Whether the module name is being cut short to fit.

Returns:

True when the label is eliding.

minimumSizeHint() → PySide6.QtCore.QSize[source]

The same as sizeHint(): a tile does not shrink below its shape.

Returns:

the minimum size.

sizeHint() → PySide6.QtCore.QSize[source]

The tile’s preferred size, height derived from its width.

HEIGHT FOLLOWS WIDTH because the tile is icon-over-name in a fixed proportion; asking for a free height would let the grid stretch one tile and not its neighbours.

Returns:

the preferred size.

property name_label[source]

The label showing the module’s name.

Exposed so the text-fits sweep can measure it: a tile that elides its own name is a module the user cannot identify.

Returns:

the label.

property stage: str[source]

stable / beta / alpha — what the hover colour says.

property text_label: str[source]

The tile’s app name, matching HTile.text_label.

class spacr.qt.widgets.home.HomePage(apps: List[Tuple[str, str, str, str]], icon_provider: Callable[[str], PySide6.QtGui.QIcon | None], parent=None, *, section_notes: Dict[str, str] | None = None, categories: Sequence[Tuple[str, Sequence[str]]] | None = None, bands: Sequence[Tuple[str, Sequence[str]]] | None = None, stages: Dict[str, str] | None = None)[source]

Bases: PySide6.QtWidgets.QWidget

Home. tile_clicked(str key) fires when a tile is pressed.

Drop-in for the page it replaces: same constructor, same signal, same set_reserved_content escape hatch.

Parameters:
  • apps – (key, name, description, section) per app.

  • icon_provider – app key → QIcon (or None).

  • section_notes – optional section → one line, drawn under that category’s heading on its own tab. A category with two apps in it looks broken until it says why; passed in rather than imported so this widget still knows nothing about spacr.qt.app.

  • categories – optional ordered (title, [app key]) — one entry per tab after Home. Defaults to grouping apps by their section in first-appearance order, which is what every test that builds a HomePage out of a handful of tuples wants.

  • bands – optional ordered (title, [app key]) for the Home tab. Same default. Kept separate from categories because the two answer different questions, even when — as today — they return the same list. See the module docstring.

  • stages – optional app key → stable / beta / alpha. Becomes each tile’s stage property, which is what the app stylesheet turns into its hover colour, and what the legend at the foot of the aside is drawn from. Anything missing is stable.

  • parent – parent widget; ownership only.

Build Home: the hero, the module tabs and the right-hand column.

Parameters:

parent – parent widget.

active_jobs() → int[source]

How many journal-reading threads are still winding down.

apply_release_news(releases) → None[source]

Hand a fetched release list to the News panel.

The answer to news_refresh_requested, and the only way in: the window never reaches into the panel, so a page rebuilt at a new font scale simply asks again.

Parameters:

releases – records from spacr.updater.fetch_release_notes(), or anything at all.

closeEvent(event)[source]

Stop Home-page background activity before closing.

Shut down the journal reader, disconnect run-registry notifications, and stop the refresh ticker before delegating to the base close handler. This prevents pending work from invoking a page that Qt is destroying.

Parameters:

event – the close event; it is passed on to the base class after background activity stops.

eventFilter(obj, event)[source]

Watch the widgets this filter is installed on.

Parameters:
  • obj – the object the event is for.

  • event – the event.

Returns:

True to stop the event going further.

page_fill()[source]

The flat colour Home paints itself, or None.

The same rule, and the same reasoning, as spacr.qt.screens.app_screen.AppScreen.page_fill(): with an animation installed the animation is the page, with an image theme the window’s wallpaper is, and otherwise it is this — a real colour rather than the bg slab that no page-opacity setting can reach.

Never raises.

paintEvent(event) → None[source]

Paint the page under everything Home lays out.

Does not chain to super() when it fills: the base implementation is what draws the stylesheet background, and that background is the slab being replaced.

Parameters:

event – the paint event; passed to the base class only when no page fill colour is set.

refresh() → None[source]

Re-read everything that can change while Home is off screen.

The two run-journal panels are read on a worker thread. Together recent_runs + journal_totals walk every manifest under the runs root twice — 774 ms on a machine with 4 865 journalled runs, measured, and it grows with the journal — and this used to run inline on every single return to Home, which is the most-travelled navigation in the application.

The panels keep whatever they are already showing until the worker delivers; a stale count for half a second beats a frozen window, and on the first ever call they are showing their empty state anyway. Everything else here is cheap (a JSON read and three stat calls) and stays inline.

resizeEvent(event)[source]

Re-flow the tile grid for the new width.

Parameters:

event – the Qt resize event.

set_reserved_content(widget: PySide6.QtWidgets.QWidget) → None[source]

Fill the featured/news surface with real content.

Parameters:

widget – the widget to show in the featured/news panel in place of the release notes.

show_module_hint(key: str, summary: str = '') → bool[source]

Explain key in the strip. Called by the DOCK as well as Home.

The dock’s rows and Home’s tiles name the same modules, so they say the same thing in the same place – MainWindow._show_module_hint routes a dock hover here whenever Home is the page on screen.

Parameters:
  • key – the module to explain.

  • summary – the sentence, already resolved and translated by the caller. Empty falls back to Home’s own registry, which is what a tile hover uses – it has the description in hand and has no reason to ask the window for it.

Returns:

whether anything was written. A key Home does not know is not an error: the dock lists Help modules that have no tile.

property legend: StageLegend[source]

The colour-to-maturity key at the foot of the right column.

property news_panel: NewsPanel[source]

The News panel. For tests and for the window’s own wiring.

class spacr.qt.widgets.home.NewsPanel(version: str = '', parent=None)[source]

Bases: Panel

Every spaCR release, with links, in a box the reader can resize.

THE NOTES ARE BUNDLED, and the bundle is what draws. They come from spacr/resources/release_notes.json, which tools/build_release_notes.py writes from the GitHub releases and which .github/workflows/release.yml refreshes on every release. This panel is on the first screen the application shows, so making its CONTENT depend on api.github.com would mean a dashboard that is empty offline, throttled behind a shared NAT, and slower to draw than the window it is in. The bundled file therefore remains the offline source of truth and the panel is complete before anything touches a socket.

AND THEN IT CATCHES UP. The wheel for a release cannot contain its own release note – the note is written when the GitHub release is published, which is after that wheel is on PyPI – so a bundled file is always one release behind the build carrying it, and that is what went wrong: “im on 1.5.1.0 and the news only goes to 1.5.0.7. the news section should always automatically reflect the latest spacr release news.” So after the page is shown, refresh_requested asks the window to read the public releases list on a worker thread, and apply_releases() merges whatever comes back in front of the bundled list. Nothing here opens a socket: the panel only asks, and a fetch that fails, is rate-limited, is switched off in Preferences, or simply finds nothing newer leaves the bundled list exactly as it was drawn.

There was previously no feed at all and this panel said so – “No release notes bundled with this build” – which was honest and useless. What it shows now is the real thing: “News should contain all the releas information with links. i nkow that there was release information for the current 1.5.0.4 version. this one as well as all of the other ones should be scrollable and the user should be able to controll the height.”

So: every release newest-first, each one’s body rendered with its links live, the whole list inside a scroll area, and a grip along the bottom edge that drags the box taller or shorter. The height is remembered between sessions – a reader who made it tall wants it tall next time.

The update check stays a BUTTON. Offering to install something is a decision, so it is still made only when pressed.

Parameters:
  • version – the build to name in the heading. Empty leaves the heading as the translated word alone – the two are kept separate because the catalog is keyed on “News”, so composing the release into the caption first would leave the only aside panel that names a build in English.

  • parent – parent widget.

Build the panel and its caption.

Parameters:

parent – parent widget.

apply_releases(fetched) → None[source]

Merge a fetched release list into the list on screen.

Silence is the contract. An empty list, a list of rubbish, or a list that says nothing the bundled file did not already say leaves the panel untouched and says nothing to the reader – the failure of an unasked-for background fetch is not the reader’s problem.

Parameters:

fetched – release records from spacr.updater.fetch_release_notes(), or anything at all.

static merge_releases(bundled, fetched) → list[source]

The bundled and fetched lists as one, newest first.

One record per tag, and a fetched record wins: the same release can have its notes edited on GitHub after it ships, and the live copy is then the true one. Ordering is by publication date and then by the version in the tag, so a release published on the same day as the one before it still lands above it.

Parameters:
  • bundled – the records read from the wheel.

  • fetched – the records read from GitHub, or None.

Returns:

a new list; neither argument is modified.

static read_releases() → list[source]

The bundled release records, newest first, or [].

Degrades to an empty list on ANY failure, and the panel then draws the placeholder it drew before there was a feed. A dashboard must not fail to appear because a resource file is missing from a wheel.

static render_body(body: str, link_colour: str) → str[source]

Turn a release body into the small HTML subset a QLabel draws.

NOT A MARKDOWN RENDERER, and not trying to be. Release bodies are GitHub-flavoured markdown; what they actually contain is bullet lists, bare URLs and **bold**, and a QLabel understands a handful of tags. So: escape everything first – a body is text from a web page and must never reach a rich-text widget as markup – then put back the three constructs that are worth having.

Escaping FIRST is what makes this safe. Linkifying first and escaping after would escape the anchors too and show the reader their own tags; escaping after building the HTML is the mistake that turns a release note into an injection.

Parameters:
  • body – release-note text in GitHub-flavoured markdown; it is HTML-escaped before bare URLs, **bold**, ## headings and bullet lines are turned into tags.

  • link_colour – CSS colour for the generated links.

set_content(widget: PySide6.QtWidgets.QWidget) → None[source]

Replace the release list with real content.

The escape hatch HomePage.set_reserved_content() exposes. It hides the bundled notes rather than deleting them, so a caller that drops content in has not thrown the feed away.

Parameters:

widget – the widget to insert at the top of the panel body; any previous content widget is deleted and the bundled notes are hidden.

showEvent(event)[source]

Ask for a refresh the first time the panel is shown.

AFTER the page exists and ON THE EVENT LOOP, not during construction: the single-shot timer means the emit lands on a later turn than this show, so Home’s first paint is never waiting on it. Once per panel, because a page that is shown again – a tab revisited, a font-scale rebuild – is not news.

Parameters:

event – the Qt show event.

property grip: _HeightGrip[source]

The drag handle under the list. For tests.

property notes_view: PySide6.QtWidgets.QScrollArea[source]

The scrolling list of releases. For tests.

property releases: list[source]

The release records currently drawn, newest first.

class spacr.qt.widgets.home.Panel(title: str, parent=None, *, beta: bool = False)[source]

Bases: PySide6.QtWidgets.QWidget

Captioned box for the right-hand column.

The border lives on a QFrame with its own object name and the rule is scoped to it. An unscoped border rule cascades into every child row and outlines each one — a mistake this codebase has already made once on the Home dashboard.

Parameters:
  • beta – mark the header with BETA_SUFFIX. The suffix is appended after the upper-casing, so it stays lower case and reads as a mark on the heading rather than part of it.

  • title – the caption above the panel. Drawn OUTSIDE the panel’s frame, beside any action word, rather than inside it.

  • parent – parent widget; ownership only.

Build a captioned box for the right-hand column.

Parameters:
  • title – the caption.

  • parent – parent widget.

action(text: str) → PySide6.QtWidgets.QPushButton | None[source]

The action word named text, or None. For tests.

Parameters:

text – the action’s label, matched case-insensitively.

add(widget: PySide6.QtWidgets.QWidget) → PySide6.QtWidgets.QWidget[source]

Add a widget to the panel body, transparent so the box shows through.

The border and fill live on the frame around the body, so a child painting its own background would draw a square inside the rounded box rather than sitting in it.

Parameters:

widget – the widget to add.

Returns:

the same widget, for chaining.

add_action(text: str, *, kind: str = 'danger', tip: str = '') → PySide6.QtWidgets.QPushButton[source]

Put an action WORD on the right of the panel’s caption.

Not a button in the styled sense – no frame, no fill, no padding that would make it look pressable. It is the word alone, in the muted caption ink, and it takes its colour when the pointer is on it or while it is held down: “just the text clear which turns red upon hover or click” (2026-09-03).

Rendered through a QPushButton rather than a QLabel because the word is a CONTROL: a button is what Tab reaches, what Space activates, what a screen reader announces as pressable, and what already has a :pressed state to hang the click colour on. A clickable QLabel has none of that.

Parameters:
  • text – the word to draw. Not upper-cased – the caption beside it is, and matching it would make the word read as a second heading.

  • kind – danger for a word that throws something away, drawn red; safe for one that only re-reads, drawn in the accent.

  • tip – optional hover help. Also becomes the accessible description, because that is what a screen reader reads.

Returns:

the button, so the caller can connect it.

class spacr.qt.widgets.home.QueuedPanel(parent=None)[source]

Bases: Panel

The plate queue, when there is one.

Reads ~/.spacr/queue.json through spacr.qt.plate_queue. PlateQueue — read-only; the Queue screen owns writes. The panel hides itself when the queue is empty rather than drawing an empty box, which is the difference between “nothing queued” and “queue broken”.

Parameters:

parent – parent widget.

Build the panel and its caption.

Parameters:

parent – parent widget.

clear_queue() → int[source]

Drop every item from the plate queue. Returns how many went.

WRITES, which no other part of this panel does – the class docstring says the Queue screen owns writes, and this is the one exception: Clear empties the queue from here rather than sending the reader to another screen to do it.

Not confirmed, and deliberately. A queue entry is a plate waiting to be processed – settings and a source path, no results – so clearing one throws away a few seconds of setting up, not any data. A confirmation on a two-word action that costs that little is a dialog people learn to dismiss without reading.

Every failure mode ends with the panel telling the truth about what is in the queue, because it re-reads from disk afterwards either way.

queue_items() → List[source]

Whatever is in the saved plate queue right now.

Read from disk on each call rather than cached: the queue is written by other screens, and a Home page showing a stale count is worse than one that costs a file read when it is looked at.

Returns:

the queued items, empty when there is no queue.

refresh() → None[source]

Re-read the queue and redraw the panel.

class spacr.qt.widgets.home.RecentRunsPanel(limit: int = 4, known_keys=None, parent=None, read_now: bool = True)[source]

Bases: Panel

Last few journalled runs of a REAL module; each row navigates.

“Real module” is doing work here. The panel used to list whatever the run journal’s newest manifests said, and those rows were clickable without representing runs – they opened a _job module. On one machine the journal held 11,046 run folders of which 7,323 were written under the app_key _job or job: a test fixture’s key, written straight into the real spacr/runs because nothing sandboxed it (fixed in tests/conftest.py). So most rows named a module that does not exist, and clicking one asked the window to open it.

The pollution is stopped at the source now, but a filter here is still the right thing: this panel NAVIGATES, so a row it draws is a promise that pressing it goes somewhere. Anything whose key is not a module Home knows about is dropped.

Parameters:
  • limit – how many journalled runs to show.

  • known_keys – callable returning the module keys that exist, or a mapping of key to display name (or either, directly). None filters nothing, which is what a standalone panel with no registry to consult has to do.

  • parent – parent widget.

Build the panel and its caption.

Parameters:
  • parent – parent widget.

  • read_now – read the journal on the calling thread now. Home passes False and fills the panel from its worker read, so the window is not held up by the journal.

clear_list() → None[source]

Hide every run listed, by moving the watermark to now.

NOTHING IS DELETED. See spacr.qt.preferences.get_dashboard_watermark() for why: this panel reads the run journal, and the journal is the record Run History searches and a run’s manifest documents.

known() → set | None[source]

The module keys that exist, or None for “do not filter”.

name_for(key: str) → str[source]

key’s display name when Home knows one, else key itself.

A row used to be captioned with the raw app_key, which is what put _job and mask on the dashboard in the same typeface as each other. The names are already in Home; the panel just had no way to ask for them.

Parameters:

key – application key to name; returned unchanged when the name registry does not know it.

read() → list[source]

The journal entries this panel would show. Worker-thread safe.

Split out of refresh() so HomePage can call it off the GUI thread: it touches no widget, only the run journal. recent_runs opens and JSON-parses every manifest in a bounded window of the newest run folders before it sorts and truncates – measured at 540 ms over 4,865 of them – so it is not something the GUI thread should be doing on the way back to Home.

THE FILTERING HAPPENS HERE, not in refresh, for two reasons. It is the half that runs off the GUI thread, and it changes how much has to be read: dropping four rows in five after asking for five leaves one, so this asks for a multiple of the limit and truncates afterwards.

refresh(runs: list | None = None) → None[source]

Redraw the panel.

Parameters:

runs – entries a worker has already read. None reads them here, on the calling thread — which is what a standalone panel and the tests do, and what HomePage deliberately does not.

class spacr.qt.widgets.home.RunningBanner(icon_provider: Callable[[str], PySide6.QtGui.QIcon | None], names: Dict[str, str], parent=None)[source]

Bases: PySide6.QtWidgets.QFrame

“spaCR is doing something right now” — with honest controls.

Reads spacr.qt.bridge.registry(), so it reflects a job started from any screen. Hidden entirely when nothing is running, which is the common case and should cost the page nothing.

On the Pause button. It is enabled if and only if the running job’s entry point declares itself bridge.pausable — i.e. it actually polls bridge.checkpoint(). No shipped pipeline does, so today it renders disabled with PAUSE_UNAVAILABLE as its tooltip. That is the whole point: a Pause button that stops the thread wherever it happens to be is not a pause, and the honest thing to draw is a control that says so.

Parameters:
  • icon_provider – called with a module key for that module’s icon. A callable rather than a dict of icons, so the banner does not build artwork for modules that never run.

  • names – module key to the name a human reads, for the banner’s text and for open_requested’s meaning.

  • parent – parent widget.

Build the banner for one running module.

Parameters:
  • icon_provider – how to get a module’s icon.

  • names – display names, keyed by module.

  • parent – parent widget.

bind(handle) → None[source]

Show handle’s job, or hide the banner when it is None.

Parameters:

handle – the running job’s handle, whose app_key sets the icon and title; None hides the banner.

refresh() → None[source]

Re-read elapsed time + progress from the handle.

property pause_button: PySide6.QtWidgets.QPushButton[source]

The pause control, exposed so a test can drive it.

Returns:

the button.

class spacr.qt.widgets.home.StageLegend(parent=None)[source]

Bases: Panel

What the three hover colours mean. One row per stage.

The legend follows the numeric status panels in the right-hand column because it explains the module tiles rather than reporting machine state.

Each row draws the stage’s hue as a filled swatch and names the stage in words. Colour alone would fail WCAG 1.4.1 and would be invisible to the colour-blind mode this app already ships — the words are what make it a legend rather than a palette.

The rows are built from spacr.qt.theme.STAGE_HOVER, which is the same table the stylesheet builds the hover rules from, so the swatch and the tile it explains cannot drift apart.

Parameters:

parent – parent widget.

Build the panel and its caption.

Parameters:

parent – parent widget.

row_for(stage: str) → PySide6.QtWidgets.QWidget | None[source]

The legend row explaining one maturity stage.

Parameters:

stage – the stage’s name.

Returns:

the row widget, or None when that stage has no row.

static swatch_colour(stage: str) → str[source]

The hex this legend draws for stage.

The same function the stylesheet builds the hover rules from, so the swatch and the tile it explains cannot come apart.

Parameters:

stage – maturity stage name; an unknown stage gets the 'stable' colour.

class spacr.qt.widgets.home.SystemPanel(parent=None)[source]

Bases: Panel

GPU / VRAM / Disk, read on build and on every Home revisit.

Every reading degrades to a string rather than vanishing — a blank row reads as “broken”, while n/a honestly says the lightweight system probe could not measure a device. Home must not import a model runtime merely to decorate the dashboard.

Parameters:

parent – parent widget.

Build the panel and its caption.

Parameters:

parent – parent widget.

static disk_used() → str[source]

Disk use for the working volume, as text for display.

Returns:

the reading, or a dash when it cannot be taken.

static gpu_util() → str[source]

Current GPU utilisation, as text for display.

NEVER RAISES. A missing NVML, a machine with no GPU and a driver mismatch are all ordinary here, and none of them is a reason for the Home page to fail to build.

Returns:

the reading, or a dash when it cannot be taken.

static gpu_vram() → str[source]

Current VRAM use, as text for display.

Never raises, for the same reason as gpu_util().

Returns:

the reading, or a dash when it cannot be taken.

refresh() → None[source]

Re-read GPU, VRAM and disk, and redraw the panel.

class spacr.qt.widgets.home.TotalsPanel(parent=None, read_now: bool = True)[source]

Bases: Panel

Aggregate counts from the automatically complete run journal.

Parameters:

parent – parent widget.

Build the panel and its caption.

Parameters:
  • parent – parent widget.

  • read_now – read the journal on the calling thread now. Home passes False and fills the panel from its worker read.

read() → dict[source]

The journal totals. Worker-thread safe — see RecentRunsPanel.read(); journal_totals walks the same thousands of manifests, measured at 247 ms.

Counted from the Reset watermark when one is set, which is the one case this cannot answer out of the cached totals file: that file holds LIFETIME counts, so a windowed count has to walk the runs the window covers. It is bounded by the window, which is the property that makes the feature affordable — somebody who reset yesterday is counting yesterday’s runs, not eleven thousand of them.

refresh(totals: dict | None = None) → None[source]

Redraw the panel.

Parameters:

totals – counts a worker has already read; None reads them on the calling thread.

reset_counts() → None[source]

Count from now on, by moving the watermark.

NOTHING IS DELETED, for the same reason Clear on Recent runs deletes nothing — see spacr.qt.preferences.get_dashboard_watermark(). What these counts are FOR is telling the user how much this installation has done, and a reset that removed the manifests would take Run History and every run’s provenance with it.

spacr.qt.widgets.home.active_palette() → dict[source]

The palette for the theme that is on screen right now.

Not theme.PALETTE: that module-level dict is the dark palette and nothing ever updates it, so a widget that inlines colours from it renders dark on every theme. On the light theme that produced black panels with black text in the right-hand column — unreadable, and invisible to any test that only checks widget structure.

Home is rebuilt from scratch on a theme change (MainWindow._rebuild_startup_page), so resolving once per widget construction is enough.

spacr.qt.widgets.home.PAUSE_UNAVAILABLE = Multiline-String[source]
Show Value
"""Pause is not available for this module.

Pausing means holding the pipeline at a point where nothing is half-written — between fields, not mid-write. spaCR's pipelines do not yet offer such a checkpoint, so a Pause button here could only freeze the thread wherever it happened to be, which can truncate a mask file or leave a field measured into some tables and not others.

Use Stop, or queue plates so the run can be halted between them."""

Nested helpers

HomePage._build_hero._draw_mark(label=label, source=logo, side=logo_px)

Redraw the mark at the ratio of the screen it is on.

Always from the 3334 px master. Re-scaling whatever is already on the label would compound one resample for every screen the window has been dragged across.

spacr/qt/widgets/home.py:2290

_scale_text_under.scaled(sheet: str) → str

sheet with every font-size in it multiplied by ratio.

spacr/qt/widgets/home.py:1816

_scale_text_under.scaled.one(match)

One font-size declaration, scaled and floored.

spacr/qt/widgets/home.py:1818