spacr.qt.app

QApplication bootstrap + MainWindow.

launch(argv) is the public entry point called by spacr-qt and python -m spacr.qt.

Classes

MainWindow

Top-level window: sidebar + stacked screens + status bar.

Sidebar

The application's navigation dock.

Functions

app_is_visible(→ bool)

Whether key should appear in module navigation.

app_stage(→ str)

How finished key is — one of STAGES.

dock_rows(→ List[Tuple[str, str, str, str]])

The dock's TOP-LEVEL rows: Home's tiles, then Help, in that order.

folded_children(→ Dict[str, Tuple[str, ...]])

host key -> the module keys folded onto its masthead.

home_bands(→ List[Tuple[str, List[Tuple[str, str, str, ...)

(band, rows) for the Home tab, in SECTIONS order.

home_categories(→ List[Tuple[str, List[str]]])

(section, [app key]) for every tab after Home, in tab order.

home_stages(→ dict)

app key → stage, for every app in APPS.

install_the_dialog_filters(→ tuple[str, ...])

Install the registered dialog event filters on a Qt application.

install_the_spaceout_fractal(→ bool)

Put the spaceout fractal behind screen, if this is spaceout.

launch(→ int)

Bootstrap QApplication and show the main window.

make_home_page([parent])

Build the Home page exactly as the running app builds it.

register_app(→ Tuple[str, str, str, str])

Add one app to the registry. The seam a new module registers through.

registered_entry(key)

Import and return the pipeline callable app key registered.

registered_factory(key)

The factory registered for key, or None.

registered_metadata(→ dict)

{app key: value} for one APP_META field, empty ones dropped.

section_members(→ List[Tuple[str, str, str, str]])

The APPS rows a category's tab shows, in registry order.

tile_sort_key(→ Tuple[int, int])

Sort key placing row where SECTION_TILE_ORDER says.

tiled_apps(→ List[Tuple[str, str, str, str]])

The APPS rows that get a TILE, in registry order.

unregister_app(→ bool)

Remove app key from the registry. True if there was one.

visible_apps(→ List[Tuple[str, str, str, str]])

Registry rows allowed by the Alpha/Beta visibility preferences.

Module Contents

class spacr.qt.app.MainWindow(initial_app: str | None = None)[source]

Bases: PySide6.QtWidgets.QMainWindow

Top-level window: sidebar + stacked screens + status bar.

Parameters:

initial_app – optional app key to navigate to on show; when omitted the window opens on the Home startup page.

Build the main window: the dock slot, the screen stack and the status bar.

The window is frameless and the menu bar is what you drag it by. Its first backing store is filled with the splash colour so the compositor cannot expose stale desktop pixels before anything has drawn – deliberately without WA_OpaquePaintEvent, which is a promise to paint every pixel that this window cannot keep once the application stylesheet clears autoFillBackground again. Claiming it left whatever was already on screen underneath, and transparent children drew on top: overlapping text in the corner, and flicker on every text surface over the animated backdrop.

The dock slot exists in every dock mode, because a QMainWindow’s central widget cannot be swapped without re-parenting the stack, and re-parenting a stack holding live screens is how locking the dock would cost you the screen you were looking at.

Parameters:

initial_app – the module to open on startup; None opens Home.

__init__(initial_app: str | None = None)[source]

Build the main window: the dock slot, the screen stack and the status bar.

The window is frameless and the menu bar is what you drag it by. Its first backing store is filled with the splash colour so the compositor cannot expose stale desktop pixels before anything has drawn – deliberately without WA_OpaquePaintEvent, which is a promise to paint every pixel that this window cannot keep once the application stylesheet clears autoFillBackground again. Claiming it left whatever was already on screen underneath, and transparent children drew on top: overlapping text in the corner, and flicker on every text surface over the animated backdrop.

The dock slot exists in every dock mode, because a QMainWindow’s central widget cannot be swapped without re-parenting the stack, and re-parenting a stack holding live screens is how locking the dock would cost you the screen you were looking at.

Parameters:

initial_app – the module to open on startup; None opens Home.

apply_dock_mode(mode: str | None = None) → None[source]

Put the app list where the preference says it goes.

Two modes, one Sidebar object:

locked the sidebar sits in the window’s dock slot, where it

is an ordinary column in the layout: it never animates, and because it is a LAYOUT MEMBER rather than an overlay the page beside it is narrower by the dock’s width instead of running underneath it.

hidden the slot stays empty. The “All apps” action is

disabled with a tooltip that says where to turn it back on — a control that silently does nothing is worse than one that is greyed out.

THERE IS NO REVEAL-ON-HOVER MODE. It slid the dock in OVER the page, so the home screen’s module tiles sat underneath it and did not move aside, and it needed a second container behind the dock’s own panel to be legible over whatever it covered. Both were the complaint. The drawer object still exists and is kept closed and disabled, because it also carries the keyboard path that the “All apps” action uses.

Idempotent, and safe to call before the menu exists.

changeEvent(event)[source]

Re-lay the menu bar when the window state changes.

A menu opens where the BAR SAYS its action is. Going fullscreen resizes the bar and its corner widget in one step, and a menu opened before the layout has caught up is placed against the previous action rectangle – which is how pressing spaCR drops a menu under Help.

Parameters:

event – the change event; only a window-state change re-lays the menu bar.

closeEvent(event)[source]

Cooperatively drain analysis and UI workers before destruction.

Parameters:

event – the close event; ignored, leaving the window open, when a worker or an application screen does not stop, otherwise passed to the base class, and the application quits if it is accepted.

dock_mode() → str[source]

The user’s dock preference — auto / locked / hidden.

Read through here rather than inlined so a headless build, or one with an unwritable settings file, still gets the default rather than an exception during __init__.

eventFilter(watched, event)[source]

Handle frameless-window menu-bar drag and double-click gestures.

Teardown can invalidate the Qt menu bar while events are still queued. This filter returns False instead of allowing that condition to escape through PySide’s callback boundary.

Parameters:
  • watched – Qt object receiving the event.

  • event – Qt event delivered to the main window’s filter.

Returns:

True when a menu-bar mouse gesture is consumed; otherwise False.

keyPressEvent(event) → None[source]

Up and Down change the spaceout zoom rate; Ctrl+R starts over.

Parameters:

event – the key press; its key and Ctrl modifier are read, and keys the backdrop does not take are passed to the base class.

open_module(app_key: str) → str[source]

Navigate to the screen that carries app_key, folded or not.

A module folded into a host keeps its key everywhere a key is saved: a run journal, the force-restart record, spacr-qt <app> in somebody’s shell history. That key no longer names a screen, and navigating to it anyway BUILDS one – an orphan page with no sidebar row, no tile and no way back to it, which is the second front door the fold exists to remove.

So the key is resolved to the host that took it over, and the fold it names is switched on: asking for Timelapse is asking for what the tracking switch reveals, not for mask generation with the switch off.

Parameters:

app_key – the key that was asked for.

Returns:

the app key actually opened.

pin_all_menu_roles() → None[source]

Give every menu-bar action an explicit macOS role. Idempotent.

Called at the end of _build_menu_bar AND again from spacr.qt.shortcuts.install, because recipes, walkthrough and feature_dictionary all add to Help afterwards. Each of those also pins its own – defence in depth – but a central re-sweep is what makes a module added later safe without its author knowing this problem exists.

rebuild_app_screen(key: str, values=None) → None[source]

Build key’s screen again, carrying values across.

Parameters:
  • key – the app whose screen is to be rebuilt.

  • values – settings to apply to the new screen.

WHY A WHOLE SCREEN. Which settings a form holds depends on a few of its own values – the organelle count, and whether an object’s channel names a plane – so a committed change to one of those means a different form, not a changed one. Rebuilding is the same path every module open already takes.

The old screen is dropped from the cache and destroyed, so the new one is built from scratch rather than reusing widgets that belong to a shape that no longer applies.

refresh_language() → None[source]

Apply the persisted language to existing static UI text.

refresh_theme() → None[source]

Rebuild everything preferences cannot update through QSS alone.

Three things do not follow a setStyleSheet call: the Home tiles set sizes/margins from the font scale in Python, every QIcon baked its pixmap at the theme in force when it was built, and the dock’s mode and the page’s opacity are layout decisions rather than colours. Called after the Preferences dialog closes, and found by duck-typing from spacr.qt.preferences.apply_preferences_to_app() so a preference change from anywhere reaches the widgets.

resizeEvent(event)[source]

Keep the loading screen covering the whole window, and erase the menu bar’s old band.

EVERY RESIZE, not only a state change. Dragging an edge moves the corner buttons without changing the window state, so changeEvent never fires – and the marks are redrawn at their new x over the old ones. That is the “sometimes” in the report: it is not intermittent, it is every resize that is not a fullscreen toggle.

Parameters:

event – the resize event, passed to the base class; the new geometry is read from the window itself.

resume_after_restart() → str[source]

Reopen the module a Force restart saved. Returns its key, or “”.

TAKEN, NOT READ: restart_state.take deletes the state as it hands it over, so a crash on the way back up cannot leave spaCR reopening the same wedged module on every launch afterwards – which would turn one bad afternoon into a permanently broken installation.

THE SETTINGS ARE APPLIED, THE RUN IS NOT STARTED. 142 C: the runs do not come back, only the configuration, and starting one unasked would be the opposite of what somebody who just force-restarted wants.

show_preferences_on(tab: str = '', label: str = '') → bool[source]

Open Preferences, on a named tab, with a named row marked.

The route the Help search field takes for a preference result: the dialog carries nine tabs and a result that opened it on whichever one it happened to start with has answered half the question.

Found by OBJECT NAME rather than by tab index or caption. An index moves whenever a tab is added, and a caption is translated – a Korean interface would have matched nothing. Every page sets its own PreferencesTab* name, and tools/build_help_search_index.py reads those same names out of this dialog’s source, so the two ends of the hand-off are the same string by construction.

Parameters:
  • tab – the page’s object name, e.g. "PreferencesTabTheme"; "" opens the dialog as it opens from the menu.

  • label – the row caption to mark; "" marks nothing.

Returns:

True when the named tab was found and shown.

stylesheet_roots()[source]

The widgets that carry the application sheet, instead of me.

WHY NOT THE WINDOW. Setting a stylesheet on a window repolishes every descendant, and a session that has opened four modules owns 8,002 widgets of which 7,595 are inside module screens – ONE of which is on screen. Measured on this box, four modules open, alternating dark and light:

the whole window 945 ms first, ~1,479 ms steady chrome + the visible page 300 ms first, ~310 ms steady

WHAT IS IN THE LIST, and the shape is not the obvious one. The chrome is 407 widgets and only TWO of them are reachable from the central widget – the dock, the sidebar and the status strip hang off the window itself. An implementation that walked the central widget would leave 405 unstyled and look almost right, which is why tests/qt/test_every_widget_wears_the_current_theme.py does exactly that on purpose and asserts it is caught.

So: every direct child of the window except the central widget, every direct child of the central widget except the stack, and the stack’s CURRENT page. The pages that are not showing are marked instead, and sheeted on their own showEvent before they are painted.

toggle_app_drawer() → None[source]

Put keyboard focus on the app dock. The menu and Ctrl+Shift+A path.

THE DOCK NO LONGER SLIDES, so there is nothing to open: it is a permanent column when the preference is locked. What the action still has to do is get a keyboard user INTO it, because a column you can only reach by tabbing through the page is one that is hard to reach at all.

A no-op when the dock is hidden – the user asked for it not to be there, and a shortcut that overrules a preference is a bug. The action is disabled in that mode anyway, so this is the second half of a belt and braces rather than the only guard.

toggle_fullscreen(*_args) → bool[source]

Enter or leave true fullscreen. Returns whether it is now full.

wheelEvent(event) → None[source]

The wheel does the same, a notch at a time.

Parameters:

event – the wheel event; its vertical angle delta is converted to notches of 120.

window_backdrop()[source]

The one backdrop behind the dock AND the page, or None.

Read by the screens so they do not build a second one on top of it – see _install_screen_backdrop().

class spacr.qt.app.Sidebar(parent=None)[source]

Bases: spacr.qt.widgets.dock.Dock

The application’s navigation dock.

The behaviour lives in spacr.qt.widgets.dock.Dock, rewritten on 2026-09-03 after four rounds of fixes to the old one had not settled it. This is only the binding to the registry, and it keeps the old NAME because the theme’s #Sidebar rules, the tutorial highlighter, the home-variant generators and the tests all reach the dock by it.

What used to be here was 1,095 lines of _DockRow and Sidebar: a translucent slab painted in paintEvent (the “black box” four commits chased), a per-row icon-size model that relaid the column out under the pointer, a name drawn only while hovered, and a folded second level with its own expand state. See the module docstring of spacr.qt.widgets.dock for what replaced each one.

Parameters:

parent – parent widget; ownership only.

Build the dock with Home above the registered module rows.

Parameters:

parent – parent widget, or None.

spacr.qt.app.app_is_visible(key: str) → bool[source]

Whether key should appear in module navigation.

Preferences are imported lazily so this registry remains safe for packaging and headless callers. If preferences cannot be read, preserve the historical all-modules-visible behaviour.

NOT A TILE CHECK. This answers “is the user allowed to reach this module”, which is the maturity preference and nothing else. A folded module is still reachable – from its host’s button, from Help, and from the command palette – so it is still VISIBLE; it just has no tile. Use tiled_apps() for the tile question.

This function did briefly answer both, and the command palette is what caught it: filtering TILELESS_APPS out here took nine modules out of Ctrl+K, so the folds removed a door instead of moving one. Two questions, two functions.

Parameters:

key – the app key whose stage is checked against the maturity preference.

spacr.qt.app.app_stage(key: str) → str[source]

How finished key is — one of STAGES.

Unknown keys read as stable rather than raising: a stage is an annotation on an app, and an app with no annotation is one nobody has flagged.

Parameters:

key – the app key looked up in APP_STAGE; unknown keys give STAGE_STABLE.

spacr.qt.app.dock_rows() → List[Tuple[str, str, str, str]][source]

The dock’s TOP-LEVEL rows: Home’s tiles, then Help, in that order.

ONE RULE: a row is a dock host if, and only if, Home draws a tile for it. Everything else in the registry reaches the dock as an indented child under its fold host, or under one of the Help modules, and nothing appears twice.

THIS USED TO RETURN ALL OF APPS, and the result was a dock that did not match the Home screen: modules belonging to Help sat outside it, and modules that should have been nested under a host were listed flat. The dock is meant to mimic the screen module tiles … everything else is nested and in help dropdown.”

Measured before the change: 36 top-level dock rows against 19 Home tiles, and NINE keys drawn twice – convert, external_masks, investigate_hit, layer_viewer, lineage, plate_view, profiler, tabulate and train_compare each had a top-level row AND an indented row under the host they fold into. A module that appears in two places in the same column is a module the reader cannot learn the location of.

tiled_apps() is the authority rather than a second hand-written list, so the dock cannot drift from Home again: promoting a module to a tile gives it a dock row, and folding one takes its dock row away, both without touching this function. That is the same lesson _HELP_MODULES already encodes below – 330’s note records one bug from a duplicated list and the retired-section map records another.

WHY HELP IS NOT A REAL SECTION: see SECTION_HELP. A section is Home’s categorisation and every Help module is tileless, so Help would be a tab with nothing on it. The dock has no such constraint.

The dock walks these rows in order and starts a new heading whenever the section changes, so grouping IS ordering here – a row out of place draws its heading a second time.

spacr.qt.app.folded_children() → Dict[str, Tuple[str, ...]][source]

host key -> the module keys folded onto its masthead.

ONE MAPPING, read from the hosts themselves, so the dock, the menu bar and the fold strips cannot disagree about what belongs where. Each host declares its own FOLDED_APPS; that tuple is the truth and this only collects them.

The dock and the spaCR menu both draw the nested structure from this, showing a folded module one level below the host it belongs to.

Never raises: a host whose module cannot be imported contributes nothing, because a navigation aid must not be able to stop the window being built.

spacr.qt.app.home_bands(apps: List[Tuple[str, str, str, str]] | None = None) → List[Tuple[str, List[Tuple[str, str, str, str]]]][source]

(band, rows) for the Home tab, in SECTIONS order.

The same grouping the tabs use — every app once, under what it is about. Home is the “all of it” view and each later tab is a filter of it, which is only true while both read this one table.

Bands with no apps are dropped rather than drawn empty. None are, today; the guard is what stops a heading appearing over nothing the day a section’s last app is retired.

spacr.qt.app.home_categories(apps: List[Tuple[str, str, str, str]] | None = None) → List[Tuple[str, List[str]]][source]

(section, [app key]) for every tab after Home, in tab order.

Computed rather than written down, so a section cannot acquire a tab it has no apps for or lose one it does.

spacr.qt.app.home_stages() → dict[source]

app key → stage, for every app in APPS.

What make_home_page() hands the Home page, so the tiles and the legend colour from the same table this module owns and the widget still knows nothing about what a stage means.

spacr.qt.app.install_the_dialog_filters(app) → tuple[str, ...][source]

Install the registered dialog event filters on a Qt application.

Installers are idempotent and isolated: a failure in one filter does not prevent the remaining filters from being installed.

Parameters:

app – the Qt application each filter installer is called with.

Returns:

Names of the filters installed successfully.

spacr.qt.app.install_the_spaceout_fractal(screen) → bool[source]

Put the spaceout fractal behind screen, if this is spaceout.

Parameters:

screen – the widget the fractal is parented to and sized to fill; an event filter on it keeps the fractal following it.

Returns:

True when it was installed, so the caller knows to skip the ordinary ambient backdrop. False in every normal launch – which is what keeps the mode hidden – and false rather than raising when the fractal cannot be built, so a machine that cannot draw it still gets the animation it always had.

The settings come from Preferences, which shows them only under spaceout, so one place decides both what is offered and what is drawn.

spacr.qt.app.launch(argv: list[str] | None = None) → int[source]

Bootstrap QApplication and show the main window.

spacr.qt.app.make_home_page(parent=None)[source]

Build the Home page exactly as the running app builds it.

The grouping, the stages, the notes and the icon provider are four arguments that have to agree, and a test that assembles its own HomePage is testing a page that does not ship — which is the exact class of bug #16i was: Home grouped by one table and the tabs by another. One constructor call, used by MainWindow and by the suite.

spacr.qt.app.register_app(key: str, name: str, desc: str, section: str, *, factory=None, stage: str | None = None, title: str | None = None, intro: str | None = None, cli_note: str | None = None, api_module: str | None = None, entry: str | None = None, defaults_module: str | None = None, translations: Tuple[str, ...] | None = None) → Tuple[str, str, str, str][source]

Add one app to the registry. The seam a new module registers through.

Call it at import time from the module that owns the app:

from spacr.qt.app import register_app, SECTION_EXPLORE, STAGE_ALPHA

register_app("graph_builder", "Graph Builder",
             "Drag columns onto x / y / colour / facet",
             SECTION_EXPLORE, factory=make_screen, stage=STAGE_ALPHA)

The first four arguments put a tile on Home and a row in the sidebar. The keyword arguments after stage are what make that tile a working app: they are the app’s strings, given ONCE here, and fanned out into the tables that used to need a hand-edit each — see APP_META.

Parameters:
  • key – stable app id. Load-bearing — bridge, cli, validate, the drag-and-drop handlers, settings_model and saved user state all key off it, so it is chosen once and never renamed. Must be unique across built-ins and plugins.

  • name – display name; the sidebar row, tile and menu entry.

  • desc – one-line summary; the tooltip and status tip.

  • section – one of SECTION_ORDER. A section with no apps has no tab, so registering the first app into a new section is what makes that section appear.

  • factory – optional zero-argument callable returning the app’s QWidget screen. It may declare app_key and/or host keyword parameters — host is the MainWindow, for a screen that has signals to connect — and is given whichever of the two it accepts. Omit it and the app gets the generic settings-driven AppScreen, like every pipeline module.

  • stage – optional STAGES member. Omitted means stable, which is also what deleting the entry later means.

  • title – header shown at the top of the app’s own screen. Defaults to name; give it only when the screen wants the longer form (“Illumination Correction” over a tile that reads “Illumination”). Reaches app_screen.APP_TITLES.

  • intro – the paragraph beside that header — what the module does, in a sentence or two. Defaults to desc. Reaches app_screen.APP_INTROS.

  • cli_note – for an app with NO headless path: one sentence saying so and what to do instead. Reaches cli.INTERACTIVE_ONLY, which is what spacr-run <key> prints instead of “unknown module”. Mutually exclusive with entry in spirit — an app is one or the other.

  • api_module – dotted-or-slashed module path under the generated API docs (“qt/layer_viewer”), for the ⓘ link beside the settings. Reaches settings_model._APP_API_MODULE.

  • entry – "module:function" of the callable the Run button runs. Resolved lazily by registered_entry() and consulted by spacr.qt.bridge.resolve_pipeline_entry(); without it the Run button answers “Not runnable”.

  • defaults_module – the module whose import calls spacr.settings.register_defaults() for this key. settings_model.resolve_default_settings imports it before asking whether the key has registered defaults — otherwise a module that registers its settings at import has no settings panel until something else happens to import it, and the app opens on an empty form. Imported on demand, so registering an app costs no numpy/pandas/torch at startup.

  • translations – the display name in the nine non-English UI languages, in spacr.qt.i18n.LANGUAGES order. Reaches i18n._ROWS and its catalogs.

Returns:

the row that was appended, so a caller can keep it.

Raises:
  • ValueError – on a duplicate key, an unknown section, an unknown stage or an empty name/description.

  • TypeError – if factory is not callable.

spacr.qt.app.registered_entry(key: str)[source]

Import and return the pipeline callable app key registered.

None when the app registered no entry=, which is what an interactive-only app (its own screen, no Run button) does.

spacr.qt.bridge.resolve_pipeline_entry() consults this after its own built-in chain, so register_app(..., entry="mod:func") is all a new pipeline app needs for its Run button to run something. Imported here, on demand, rather than at registration: registering an app must not drag numpy, torch or pandas into a process that only wanted to draw a sidebar.

Parameters:

key – the app key whose entry metadata ('module:function') is imported.

spacr.qt.app.registered_factory(key: str)[source]

The factory registered for key, or None.

A declared app is registered with a LazyScreenFactory standing in for the real callable, and this is where the stand-in ends: asking for the factory imports the screen and returns the module’s own function.

That is deliberate, and it is what makes laziness invisible to everything downstream. _call_screen_factory() decides whether to pass app_key and host by reading the factory’s signature, and a stand-in’s signature is its own, not the real one’s; a test that asserts the registered factory is the module’s function would likewise be comparing against the wrapper. Resolving here means neither ever sees one.

The resolved callable replaces the stand-in in APP_FACTORIES, so the import happens once even if the screen is opened and closed all afternoon. A stand-in whose module fails to import is left in place and None is returned: the app falls back to the generic settings screen rather than taking the window down.

Parameters:

key – the app key looked up in APP_FACTORIES.

spacr.qt.app.registered_metadata(field: str) → dict[source]

{app key: value} for one APP_META field, empty ones dropped.

The PULL half of the seam. A side table calls this at the end of its own import to absorb every app that registered before it existed:

_app = sys.modules.get("spacr.qt.app")
if _app is not None:
    for _key, _value in _app.registered_metadata("title").items():
        APP_TITLES.setdefault(_key, _value)

setdefault, not assignment: a table’s own hand-written entry is the more specific one and wins.

Parameters:

field – the APP_META field to collect, such as 'title'.

spacr.qt.app.section_members(section: str, apps: List[Tuple[str, str, str, str]] | None = None) → List[Tuple[str, str, str, str]][source]

The APPS rows a category’s tab shows, in registry order.

TILELESS APPS ARE NOT MEMBERS. This is what the tab DRAWS and what its label COUNTS, so a module with no tile must be out of both – otherwise the label reads “Results & QC (7)” over three tiles, which is a count of something the user cannot see.

An explicit apps list is filtered too. A caller passing its own rows is asking “which of these belong to this section”, and a tileless one does not belong to any tab whichever list it arrives in.

Parameters:

section – the category name, compared with the fourth element of each APPS row.

spacr.qt.app.tile_sort_key(row: Tuple[str, str, str, str]) → Tuple[int, int][source]

Sort key placing row where SECTION_TILE_ORDER says.

Parameters:

row – an APPS row.

Returns:

(section index, position in that section). An unlisted key sorts after every listed one, keeping registry order among the unlisted by virtue of Python’s stable sort.

spacr.qt.app.tiled_apps(apps: List[Tuple[str, str, str, str]] | None = None) → List[Tuple[str, str, str, str]][source]

The APPS rows that get a TILE, in registry order.

APPS is what EXISTS; this is what Home DRAWS. The two stopped being the same thing when modules began folding into hosts – a folded module keeps its registry row (it still has a screen, an icon, a section and a key to navigate to) and loses only its tile, because it is now reached from a button on its host or from the Help menu.

Anything asking “what does the user see on Home”, “how many tiles are in this band”, or “does this tile’s label fit” wants this. Anything asking “can this key be navigated to”, “does this app have artwork”, or “does every app have a screen” wants APPS – a folded module must still answer yes to all three.

An app registered with the alpha gate has no tile while Preferences > Show alpha features is off: Home does not draw it, so nothing that counts or lists Home’s tiles may count it either.

spacr.qt.app.unregister_app(key: str) → bool[source]

Remove app key from the registry. True if there was one.

The counterpart to register_app(), for a plugin that unloads and for tests that must not leak a registration into the next one — a stray row in APPS is a stray tile, a stray sidebar entry and a stray Ctrl+N binding for every test that follows.

Parameters:

key – the app key to remove, converted to a string.

spacr.qt.app.visible_apps() → List[Tuple[str, str, str, str]][source]

Registry rows allowed by the Alpha/Beta visibility preferences.

Nested helpers

MainWindow.__init__._finish_installer_onboarding()

Show the installer consent, then start the tour after it closes.

spacr/qt/app.py:3010

MainWindow._retry_screen_backdrop.again() → None

Try the backdrop once more, if the screen is still alive.

spacr/qt/app.py:6633

MainWindow._show_about._line(html, size, *, muted=False, weight=400, gap=0)

One centred line of the About box, at the given size.

spacr/qt/app.py:4125

MainWindow._start_a_sample_project.open_it(key)

Open the actual module, including its page inside a host screen.

spacr/qt/app.py:5833

_collect_paint_diagnostics.failed(what)

Log the exception being handled and name it in errors.

spacr/qt/app.py:2428

_declared_folds._as_string(node, _depth=0)

The string an AST node stands for, or None if it is not one.

Resolves a literal, a module constant, and another module’s constant read the same way rather than by importing it. _depth bounds that indirection so a constant defined in terms of itself stops instead of recursing.

spacr/qt/app.py:1640

_import_the_data_libraries_off_the_gui_thread._work() → None

Import each library, logging rather than raising a failure.

spacr/qt/app.py:387

_start_icon_prewarm.warm()

Fill the icon caches, and never let a bad file reach the GUI.

spacr/qt/app.py:7294

_start_settings_prewarm._warm_the_settings_imports()

Import each module, logging rather than raising a failure.

spacr/qt/app.py:7156

_start_settings_prewarm.warm()

Import the remaining settings dependencies without building widgets.

spacr/qt/app.py:7150

launch._drain_ai()

Stop every job runner before Qt starts destroying widgets.

Connected to aboutToQuit, which fires however the application exits. See the comment below for why this covers every runner and not only the consoles.

spacr/qt/app.py:7522