SPEC.md of the spam-gui repository, shown as HTML.spam-gui: specification for new graphical modules
This file describes how the graphical modules of spam-gui are built, how they look, and how they behave. Every new module must follow it, so that all the modules of spam-gui feel like one program.
It is written for spam developers and for Claude, together. To build a module with Claude, open Claude Code in this repository and ask, for example:
Read SPEC.md, then let's write the spec of a new module for <what the module does>, from specs/TEMPLATE.md. Then build it, following both, and check in with me after each step you build.
The specs:
| File | What it says |
|---|---|
SPEC.md (this file) |
What is common to all the modules: the rules, the shared window, how a module is built, layout, large images, testing, how to work with the maintainers |
specs/TEMPLATE.md |
The form of a module's spec, filled in with the maintainer before the module is written |
specs/modules/<module>.md |
What one module does: its spam functions, its steps, its state, the decisions taken for it, its tests (e.g., specs/modules/mmr.md) |
TODO.md |
The work planned, in order (e.g., the milestones of the reorganisation), and what « done » means for each |
OPEN_QUESTIONS.md |
Everything waiting for the maintainer, one line each (« to decide », « to confirm »), pointing to the details |
TO_SPAM.md |
The analysis functions written in spam-gui while spam lacks them, to move into spam (section 1) |
specs/common-steps.md (planned) |
The steps several modules share (load two images, eye registration, crop): what they need and what they give |
Words. spam-gui is made of modules: a module is one kind of work the user does in the window, with its own
panel, chosen with the module button at the top left or on the command line (spam-gui <module>). The multimodal
registration is a module; a display of the strains of a regular grid on the images would be another. A module may be
made of steps (StepModule), or not. Never call a module a « tool ». To avoid two other meanings of « module »:
- a Python file (e.g.,
gui/window.py) is called a file, not a module; - a part of spam (e.g.,
spam.DIC) is called a spam package.
Other words with one meaning each: a step (of a module made of steps), a pane (a box of a step, numbered), the view (« Single », « Double », « Triple »: one, two or three image windows), the mouse mode (of an image window: pan and zoom, or move points).
This file does not repeat what a module's spec says: its examples are short, and point to the module's spec for more.
The multimodal registration (src/spam_gui/modules/mmr.py, spec specs/modules/mmr.md) is the reference
implementation: when this file and that code seem to disagree, the code shows how it is actually done, and this file
says why.
The reorganisation in progress. The code is being reorganised for more modules (section 3.4). What is marked « (planned) » here does not exist yet; until it does, the code is where it is now.
1. Ground rules
These are not negotiable.
-
Never change spam's algorithms. The modules call spam's functions as they are (e.g.,
spam.DIC.multimodalRegistration). If spam gives a result that looks wrong, explain why (with figures if useful) and offer options. Don't patch spam, and don't replace its calculation with a reimplementation. -
No image analysis in spam-gui: it belongs in spam. spam-gui is a graphical interface: it calls spam, shows its results, and lets the user make the choices. It implements no image analysis (e.g., histograms of images, peak finding, fits, phase diagrams, registration, filters, measurements).
-
spam-gui stays independent of spam's development. It must work with the released spam (0.9.1 or later), installed from PyPI. When a module needs an analysis function that the released spam does not have:
- it is written in
src/spam_gui/tospam/, in folders that mirror spam's packages (tospam/DIC/forspam.DIC, so that the file moves into spam at the same place; the file's docstring names the spam package it is meant for), as it would be in spam: spam's conventions and names, a docstring, unit tests. Nothing there may import Qt, napari, or another file of spam-gui; - it is listed in
TO_SPAM.md, at the root of the repository: what it does, which module uses it, and its status (waiting, proposed to spam, in a spam release); - once a released spam has it, it is removed from
tospam/, the modules call spam's, and spam-gui requires that release (pyproject.toml).
Everything that concerns spam is written in
TO_SPAM.md, and that's it: functions missing in it, problems found in it (how to reproduce them, the cause, a fix), code that may belong in it. spam-gui does not work around spam's problems.Before writing one, check that spam has nothing that does it, and ask the maintainer. Code that only runs spam's functions by parts on images larger than the memory, or computes what is drawn on screen, is not analysis; when in doubt, list it in « To decide » of
TO_SPAM.mdand ask. - it is written in
-
A module is a sequence of steps. It subclasses
spam_gui.gui.steps.StepModuleand is listed insrc/spam_gui/modules/__init__.py(MODULES). It never builds its own window, menus, or docks: the window is shared (section 2). -
Nothing is committed unless the maintainer asks.
-
Each source file starts with the GPL-3.0-or-later header used in the other files.
2. The window
All modules share one window, made by spam_gui.gui.window (installSpamWindow). A module fills the parts marked module in the
picture below and leaves the rest alone.
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│[Multimodal registration ▾] [1 Load images] › [2 Eye registration] › [3 Crop] › … [Summary] │ top bar, row 1
│◧ ◨ │ top bar, row 2
│┌ 4. Joint histogram ───────┐ ┌ 1 Z Y X 3D │1 2 1+2 ▦│✥ ↖│Fit ┐ ┌ 2 … ┐ ┌ Windows ─────────┐ │
││ ▾ 1. Grey level mapping │ │ │ │ │ │ View [Single]… │ │
││ ▾ 2. Bin and peak sel… │ │ image window 1 │ │ 2 │ │ Compare along Z Y X│
││ ▾ 3. Joint histogram … │ │ │ │ │ └──────────────────┘ │
││ (module: the step) │ │ │ │ │ ┌ ▾ Layer controls ─┐ │
│└───────────────────────────┘ │ │ │ │ │ opacity, histogram│ │
│┌ ▾ Current Phi ────────────┐ │ │ │ │ │ ▸ Advanced │ │
││ [Initial][Eye][MMR] │ │ │ │ │ └───────────────────┘ │
││ ⎡ … ⎤ (module: footer)│ │ │ │ │ ┌ ▾ Layer list ─────┐ │
│└───────────────────────────┘ └───────────────────────────────┘ └─────┘ └───────────────────┘ │
│ ◢ │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
Top bar (SpamWindow.toolbar):
- Module button. It sits at the far left. It is a drop-down shaped like the step buttons, in a quieter blue than the
current step (
moduleColours,MODULE_SHADE), with white bold text and an arrow to choose another module. There is no « Module » label. - Step buttons (
StepBar). They are rounded and separated by›.- The current step is blue and bold.
- A done step is filled grey with a ✓.
- A step that can be shown now is outlined.
- A step that can't be reached yet is dashed and greyed.
- A step shown at any time (e.g., the Summary) comes last, apart.
- When the bar is too narrow, only the current step keeps its title, then none do, and the titles go into the tooltips.
- Hide/show buttons. On a second row, two slim buttons at the far left and far right (◧ ◨) hide and show the
side panels. Each sits a few pixels above its panel and lines up with that panel's box (
BAR_EDGES). - napari's menus (File, View, Layers…) are hidden. Ctrl+M shows them again.
Left panel (a module's panel):
- Step box. The current step's options, in a box framed in the blue of the current step, under the step's title. It scrolls when the step is taller than the panel.
- Footer. Below the step box and apart from it, an optional pane that is shown whatever the step
(
StepModule.makeFooter). The MMR module uses it for the Current Phi pane. - Width. The panel is as wide as the widest step needs (
panelWidth), within a share of the window (LEFT_SHARE).
Centre, the image windows (spam_gui.gui.windows):
- Views. One, two or three napari canvases side by side: « Single », « Double », « Triple ».
- Window bar. Each window has its own bar, with its cut (Z/Y/X or 3D), its images (1, 2, 1+2, checkerboard), the mouse mode (pan/zoom or move points) and « Fit ».
- Keep module controls out of the viewing windows. A module tells the windows which two images to show
(
windows.setImages(layer1, layer2)) and never adds its own controls inside a viewing window or its bar.
Right panels. These are fixed panes: they can't be closed, moved or floated, and each can be collapsed by clicking its title.
- Windows: the view, then rows arranging the windows (« Compare along », « Cuts Z, Y, X of », « Same view as », « Lock »).
- Layer controls: napari's controls, with only the basic ones shown (opacity, histogram with contrast limits, colormap). The others are behind « ▸ Advanced ». Among them, the 3D controls are labelled « (3D) » and shown whatever the view. There is no auto-contrast button, and no camera/transform buttons.
- Layer list.
Bottom right: a visible triangle to resize the window.
3. Building a module
A module starts with its spec. Before any code, copy specs/TEMPLATE.md to specs/modules/<module>.md and fill it in
with the maintainer: the spam functions it calls, its steps (titles, sub-panes, ready() messages), its state, what
is carried between binnings. Then build it step by step (section 7), and keep the spec up to date: each decision taken
with the maintainer goes in its « Decisions », with the reason.
Reuse before writing. A step that another module already has (e.g., loading two images, an eye registration, a crop)
is reused, not copied (planned: modules/common/, specs/common-steps.md). If it must change for the new module, change
it for all the modules, and check them all.
3.1 Skeleton
from spam_gui.gui.steps import Step, StepModule
class LoadStep(Step):
title = "Load images"
def __init__(self, module):
super().__init__(module)
... # build the widgets (section 4)
def enter(self): # the step is shown: prepare it from self.state
...
def exit(self): # another step is shown instead
...
def ready(self): # what is missing before the next step (a short sentence), or None. Must not change anything.
return None if self.state.image is not None else "Load an image first (« Load »)."
def leave(self): # check (ready()), then store the results in self.state; an error message keeps the user here
error = self.ready()
if error:
return error
self.state.something = ...
return None
def fingerprint(self): # a summary of the results: when it changes, the later steps are reset
return ...
def reset(self): # an earlier step changed: forget what depended on it
...
class MyTool(StepModule):
name = "My module" # the text of the module button
description = "One sentence: what it does (tooltip of the module list, the help of the command line)."
command = "mytool" # its subcommand: spam-gui mytool [its arguments]
stepClasses = [LoadStep, ...] # in order; a Step with anytime = True (e.g., a summary) comes last
def makeState(self): # what the steps share
...
def makeFooter(self): # optional: a pane under the step box, shown whatever the step
return None
@classmethod
def addArguments(cls, parser): # optional: its arguments on the command line (argparse), e.g. its images
parser.add_argument("image", nargs="?", help="a TIFF file, or a folder of TIFF slices")
def useArguments(self, args): # optional: those given, once the window is open
...
Then add MyTool to MODULES in src/spam_gui/modules/__init__.py.
3.2 Steps and navigation
- No « Next » / « Previous » buttons. The user clicks the step buttons. Going forward runs
leave()on each step in between. If something is missing, a fading message under the step button says what, in the words ofready(). ready()is short and actionable: it says what to do and names the button, e.g. « Tick at least one peak (a phase of the material), in the table or on the histogram. »- Results are reset only when they must be.
fingerprint()decides which later steps must be redone. Keep results that are still valid. - Some steps can be shown at any time: a step with
anytime = True(e.g., a summary) can be shown whatever the steps done. - Show stale results. When an input changed since a result was computed, say so next to the result (a
spamWarninglabel, e.g., « ⚠ The grey levels (1) or the bins (2) changed since this histogram was computed: « Compute histogram » (2) computes it with them. »), and makeready()refuse it.
3.3 What the shared files provide
Use these instead of writing your own (files of spam_gui.gui, and of spam_gui.core for parts, io, phi,
volumes):
| Need | Use |
|---|---|
| A box with a title, optionally collapsible (▾/▸) | widgets.PanelBox(title, content, collapsible=True, margins=…) |
| Buttons side by side, one checked at a time | widgets.buttonGroup(parent, layout, labels, tooltips, slot) |
| An on/off button | widgets.toggleButton(parent, layout, label, tooltip, slot) |
| Widgets that wrap onto more lines in a narrow panel | widgets.FlowLayout |
| A two-handle slider, a histogram over a slider | widgets.RangeSlider, widgets.SliderHistogram |
| A message, a question, a file, a folder, a choice, a colour | dialogs.ask, warn, openFile, saveFile, chooseFolder, chooseItem, chooseColour |
| A long calculation (thread, progress bar, cancel) | busy.runWithProgress(parent, text, function) |
| Work on a large image by parts, in threads | parts.runParts, parts.slabs, parts.block, parts.stage |
| Memory: available, fits?, readable size, scratch arrays | parts.availableMemory, fitsInMemory, gigabytes, allocate, scratchArray |
| Read or map an image, bin it | io.openVolume, io.mapVolume, io.readBinned |
| Φ between spam and napari, binnings, crops | phi.phiToAffine, phi.convertPhi, phi.convertBox, phi.phiForCrop |
| A napari layer added or replaced, removed | layers.setImageLayer, layers.removeLayer |
| A plot in the theme's colours | plotting.PlotCanvas(viewer): newAxes(projection), colourbar, finish |
| A number box or drop-down made after the panel: the wheel must not change it while the panel scrolls | module.guardWheel(box) |
| A short note under a button | module.say(text, anchor) |
| The images shown in the windows; refusing a 3D view that would not fit in memory | module.windows.setImages(…), windows.refuse3D = … |
| Information, errors | napari.utils.notifications.show_info, show_error |
Never call QFileDialog.getOpenFileName, QMessageBox.question, etc. directly. The functions in spam_gui.gui.dialogs
place every pop-up at the centre of the window, on its screen. Otherwise, with several screens, a pop-up can open
unseen on another screen and the program seems frozen.
3.4 Where the code goes
src/spam_gui/
app.py panel.py napari.yaml # the spam-gui command; the entry from napari's menu
gui/ # the shared window, Qt: window, windows, controls, widgets, dialogs,
# busy, steps, plotting, layers (napari's); points (pairs of points, for any
# registration module)
core/ # no Qt, no napari: calculations by parts, volumes computed on demand,
# reading images and binning them (io), Φ conversions (phi)
tospam/ # analysis functions spam does not have yet, to move into spam (section 1,
# TO_SPAM.md), in folders as spam's: tospam/DIC/ for spam.DIC, ...
modules/
__init__.py # MODULES: the modules, in the order of the module button's list
common/ # (planned) steps several modules share
<module>/ # (planned) one module: __init__.py (its StepModule), state.py, one file per step,
# what only it uses
tests/
conftest.py # a temporary napari settings file for each run
unit/ # (planned) tests of what has no Qt (core/, tospam/, a module's calculations)
gui/ # the window tests: window.py (planned: the shared window, whatever the module),
# screenshots.py (planned), modules/<module>.py (planned: a module's workflow)
specs/ # the specs (see the top of this file)
scripts/pages.py # the Markdown files as a website, published by GitLab Pages (.gitlab-ci.yml)
Until the reorganisation is done, the MMR module is one file (modules/mmr.py) and its phase layers are
src/spam_gui/phases.py; the unit tests are tests/test_*.py, and the window test is tests/gui/workflow.py.
The rules each folder follows:
core/andtospam/import neither Qt nor napari. Their unit tests run without a display.tospam/imports no other file of spam-gui either (section 1). Neither does image analysis that belongs in spam:core/only reads images, runs spam's functions by parts, and computes what is displayed.gui/knows no module. What only one module uses goes in that module's folder (e.g., the phase layers of the MMR module), with its layers named after the module (« mmr crop »).- A module imports
gui/,core/,tospam/andmodules/common/, never another module. What two modules need goes inmodules/common/(a step) or ingui//core/(anything else).
4. Layout and appearance
4.1 Organise a step as numbered sub-panes
-
Split by stage. When a step has several stages, split it into numbered, collapsible sub-panes, in the order the user goes through them, with short titles:
box = PanelBox("1. Grey level mapping", content, collapsible=True, margins=SUBPANE_MARGINS) # (9, 4, 9, 9), as in modules/mmr.pyFor example, the joint histogram step has « 1. Grey level mapping », « 2. Bin and peak selection » (with « Compute histogram »), and « 3. Joint histogram and peak selection ».
-
Refer by number. Panes refer to each other by number, e.g. « the bins (2) ».
-
Closed panes are compact. A collapsed pane leaves only its title, and the frame closes under it. Put a stretch after the last pane, so that collapsed panes stay at the top.
-
One layout per pane. Give a pane's content widget its own layout (a grid, or a form) rather than nesting a grid inside another layout. Qt can report nested grids as wider than they are, which widens the whole panel (section 4.4).
4.2 Rows, labels, buttons
- Names in a column. Put a short name in a column, and the controls right after it. Don't push the controls to the far right, away from their name.
- Line up small buttons. Small buttons (Z, Y, X, 1, 2, 3) all have the same width and line up from row to row.
- No extra headings. Don't add headings that only take room (e.g., "Arrange the windows").
- Fixed size for buttons. Buttons take the size of their text (
QSizePolicy.Fixed) and don't take the keyboard focus. - Number boxes use
setKeyboardTracking(False)(one change once the value is typed, not one per digit). - No auto-contrast buttons.
4.3 Text
-
Short labels. Labels are short and to the point: « Compare along », « Same view as », « Rotate », « Lock » [Slice] [Pan and zoom].
-
Details in tooltips. The full explanation goes in the tooltip: what it does, what it is computed from, and when it matters.
-
At most one intro line. A pane has at most one line of introduction, and only if it says something the controls can't (e.g., « below the min: the first bin; above the max: the last »).
-
Name buttons with « ». Refer to buttons with « », e.g., « Compute histogram », and to panes by number.
-
Language. British spelling, as in the code (colour, centre). Symbols use their real glyphs: Φ (not "Phi") in what is displayed, °, ×, ⚠.
-
Matrices. Show them as a monospace block with bracket pieces, e.g.
⎡ 1.0012 0.0017 0.0019 2.5300 ⎤ Φ = ⎢ -0.0002 0.9395 0.3407 -0.1200 ⎥ ⎣ -0.0026 -0.3387 0.9405 0.3100 ⎦
4.4 A layout that doesn't jump
The user must never see panels move by themselves.
- The image windows give way; nothing hides unless asked. When the window is too narrow for everything (e.g.,
« Triple » on a 1366×768 screen), the image windows get narrower: the panels keep the width they need (the step
panel: that of its widest step), and nothing is hidden that the user did not hide (◧ ◨). Each window's own
controls wrap onto more lines, and napari's slice slider under each canvas may be short
(
windows.narrowSliders). When the window gets wider again, a panel left narrower than it needs gets its width back (window.widenLeft). Decided with the maintainer, 2026-11-13. - Fixed heights. A pane whose text changes keeps one height: the height of its longest usual state. For example, Current Phi is always as tall as with an MMR Φ, and other states leave the extra room at the bottom. It may grow if a rare text needs it, but then it keeps that height.
- Stable panels. The right panels don't change size when their content changes (e.g., a histogram drawn in the layer controls).
- No horizontal scroll bar when the window opens, on any screen. The left panel's width comes from the steps' minimum widths. After a change, check that the panel width is unchanged, unless that was the point.
- Tables fit their panel. Use short headers (e.g., « im1 », « im2 »), make number-box columns as wide as the box needs (not as it would like), and stretch the last column.
- Sizes follow the font. Sizes come from the font (napari's font-size preference) and the theme, not fixed pixels, except for small margins.
4.5 Colours
- Use napari's theme. Colours come from napari's theme (
get_theme(viewer.theme):current,foreground,secondary,warning, …), so that the light and dark themes both work.- The current step and checked buttons are
current(blue). - The module button is a quieter blue.
- Warnings use
QLabel#spamWarning.
- The current step and checked buttons are
- No loud colours. An orange module button was tried and rejected as too visible.
- Style sheet names. Styles are in
window.STYLE, keyed by object names:spamStepButton,spamModuleChoice,spamToggle,spamIconToggle,spamPanelToggle,spamStepBox,spamWarning.
4.6 Plots
- Canvas. Use
PlotCanvas, which follows the theme's colours. - 2D first. Clicks on the plot can act, e.g., ticking a peak.
- 3D. When a 3D plot is useful, show it as a fixed isometric view: parallel projection, elevation 35.26°.
- A « Rotate » slider turns it about the vertical axis, from −180° to +180°, with 0° (the isometric view) in the middle.
- Mouse rotation and zoom are off (
ax.disable_mouse_rotation()). - Rotating changes only the view angle (
view_initanddraw_idle); the plot isn't built again. - While the slider is dragged, draw a coarser surface, and the whole one once it is released.
- Keep the angle when the plot is drawn again.
5. Large images and responsiveness
The images can be 2 to 4 GB each, larger than the memory. A module must stay responsive.
- Coarse binning first. Work at a coarse binning first, then finer ones. Keep the results of each binning, and
restore them when the user comes back to it. Which results are carried to another binning is decided with the
maintainer, and written in the module's spec (« State »): e.g., the MMR module carries its eye registration, crop and
MMR Φ, and makes its joint histogram and phase diagram again, because they change with the binning
(
specs/modules/mmr.md). - Don't read whole images. Map an image from its file (
io.mapVolume), read only the blocks needed (parts.block), and work by parts in threads (parts.runParts). Results too large for memory go to scratch files (parts.scratchArray,SPAM_GUI_SCRATCH). - Long work in a thread. Anything that may last runs through
busy.runWithProgress.- It runs in a thread, while the window keeps drawing.
- A progress bar centred on the window appears after 0.5 s, with « Cancel ».
- The function receives
report(fraction)andcancelled(), and stops by raisingparts.Cancelled. - Don't use Qt from that thread.
- Say how much memory a calculation needs. If a calculation needs more than is available (spam's registration needs
about 60 bytes per voxel of its region), say so before it starts, and ask (
dialogs.warn). - Refuse what doesn't fit. For example, a 3D view of images that don't fit in memory is refused, with the reason.
- Recompute only what changed. For example:
- changing how peaks are found searches the histogram already computed, without computing it again;
- a fit runs once the value stops changing (a short timer);
- redraws are coalesced (
draw_idle).
6. Testing and checking the result
-
Unit tests. Non-graphical code has tests in
tests/test_*.py(planned:tests/unit/), run bypytest tests. Where spam computes the same thing, compare with spam's result. -
Window test. The window is tested by
tests/gui/workflow.py. It drives the real window through a whole module, on small images it makes with a known answer (e.g., a known Φ), and prints oneOK/FAILline per check. Run it withSPAM_GUI_TESTS=1 pytest tests/test_gui.py. A new module adds its own checks, of behaviour and of layout (e.g., three numbered panes, a constant pane height, buttons lined up), listed in its spec (« Tests »). Planned: the checks of the shared window intests/gui/window.py, each module's intests/gui/modules/<module>.py. -
A virtual screen (xvfb). The window test and the screenshots run on a virtual screen, unseen, at any size:
xvfb-run -a -s "-screen 0 1920x1080x24" env QT_QPA_PLATFORM=xcb LIBGL_ALWAYS_SOFTWARE=1 <command>(sudo apt install xvfb;LIBGL_ALWAYS_SOFTWARE=1: OpenGL drawn by the processor). The window test checks the layout at the size of that screen only: run it at the sizes below too when the layout changed.python tests/gui/screens.pyruns every window test (the workflow, images of different sizes, closing) at 1366×768, 1920×1080 and 3840×2160, several at once (--only <test>: one of them). A new window test is added to its list (TESTS), as totests/test_gui.py. -
napari's settings. napari keeps the window's size and state in its settings (
~/.config/napari), and restores them: a test or a screenshot would start from the maintainer's own window (e.g., maximised at the size of their screen, which a virtual screen without window manager does not change), and save its own when it closes. Give it a settings file of its own:NAPARI_CONFIG=<a temporary folder>/settings.yaml(planned:tests/conftest.py). -
Look at it: screenshots.
python tests/gui/screenshots.pytakes them, intotests/gui/screenshots/(not kept by git): each step with images loaded and its work done (not only step 1), at 1366×768, 1920×1080 and 3840×2160, in both themes. It is not a test (nothing passes or fails): someone looks at them.- Every new feature, module, step or window has its screenshots, added to
tests/gui/screenshots.py: each stage it can be in that the user sees (e.g., a step before and after its calculation, in 2D and 3D, in « Triple »). - Their names say the module and the stage:
<size>/<theme>/<module>-<step number>-<step title>[-<variant>].png, from the module'scommandand the step's title (e.g.,1366x768/dark/mmr-4-joint-histogram-3d.png); for the window without a module's step:window-<what>.png.
Check:
- nothing is cut or scrolls sideways;
- pop-ups open at the centre of the window;
- panels don't jump when the content changes;
- the light and dark themes both work.
- Every new feature, module, step or window has its screenshots, added to
-
Large images. Test with a large pair (e.g., 1024³) for time and memory, not only with small ones.
-
Lint.
pyflakes src testsreports nothing. -
Closing. Closing the window must not raise errors. Qt deletes widgets while signals are still arriving: guard late callbacks with
window.alive(...).
7. Working with the maintainers (for Claude)
- Check in often. Give short updates while working, stop at each milestone (a step built, a layout changed) with a screenshot and a few lines, and wait for feedback. For a request with several parts, report on each part.
- Ask before building a guess. When a placement or a look is ambiguous, ask first: give two or three options, each with a small ASCII sketch. Then build exactly what the chosen sketch shows.
- Keep their words. Use the names the maintainer gives (titles, labels), with typos corrected.
- Say what else changed. Say when something else had to change for a request (a test adjusted, a margin moved), and why.
- Problems in spam. When the cause of a problem is in spam rather than the GUI, say so plainly, with the evidence; the GUI does not work around spam's algorithms (section 1).
- Report what is true. Say what was tested and how. If a check failed, say so.
- Keep the module's spec up to date. A decision taken with the maintainer (a look, a behaviour, a name) goes in the
« Decisions » of the module's spec, with its reason and date. When the reason is not known, write « (to confirm) » and
ask: don't invent one. Each « (to confirm) » or « to decide » also gets one line in
OPEN_QUESTIONS.md, deleted once it is answered (the answer goes in the file it points to).
8. Checklist for a new module
- [ ] Its spec,
specs/modules/<module>.md, written with the maintainer fromspecs/TEMPLATE.md, and up to date (its « Open questions » answered or agreed to stay open). - [ ] Its subcommand (
command) and arguments (addArguments,useArguments):spam-gui <command> --helpis right. - [ ] A
StepModulesubclass insrc/spam_gui/modules/(planned:modules/<module>/), listed inMODULES; non-graphical no image analysis: spam's functions, or (if the released spam lacks one)src/spam_gui/tospam/, listed inTO_SPAM.md. - [ ] Steps another module already has are reused (planned:
modules/common/), not copied; no import of another module. - [ ] Each step has
ready()(short, says what to do),leave(),fingerprint()andreset(). There are no Next/Previous buttons. - [ ] Steps with several stages use numbered, collapsible sub-panes in process order.
- [ ] Labels are short, details are in tooltips, buttons are named with « », and panes are referred to by number.
- [ ] Names are in a column with their controls after them; small buttons line up; there are no empty headings.
- [ ] Pop-ups go through
spam_gui.gui.dialogs; long work goes throughbusy.runWithProgress, by parts. - [ ] The memory needed is said before big calculations, and what can't fit is refused with the reason.
- [ ] No pane changes height or width by itself; there's no horizontal scroll at start, on 1366×768 to 3840×2160.
- [ ] Nothing of the module is inside the image windows; the shared window (top bar, side panels) is unchanged.
- [ ] Colours come from the theme, and both themes were checked.
- [ ] Its screenshots, in
tests/gui/screenshots.py(each step and stage, named<module>-<step>-…), looked at on the three sizes, in both themes. - [ ] Unit tests, plus checks in
tests/gui/workflow.py(planned:tests/gui/modules/<module>.py).pyflakesis clean. Closing the window raises no error. - [ ] spam's algorithms are untouched, and it works with the released spam.
- [ ] README: one line for the module in the list at the top, and anything the user must know.
9. Decisions
Choices made with the maintainers for the whole program, each with its reason and what was set aside, so that a later
change does not undo them unknowingly. Those of one module are in its spec (« Decisions »). A reason not known is
written « (to confirm) » and listed in OPEN_QUESTIONS.md.
- Qt 6 (PyQt6), not Qt 5 (2026-11-13). Why: napari 0.10 requires Qt 6.7 or later, and Qt 5 is no longer
maintained. Set aside: staying on PyQt5 with napari below 0.10. Consequences:
libxcb-cursor0is needed (apt); an environment installed with PyQt5 must remove it (README); sizes may differ from Qt 5, hence the window tests at three screen sizes. - X11 on Linux, also in a Wayland session (2026-11-13): the
spam-guicommand setsQT_QPA_PLATFORM=xcb(unless set to something else than Wayland) andPYOPENGL_PLATFORM=glx(unless set). Why: under Wayland a program cannot place its windows (pop-ups at the centre of the window, the window within its screen), and napari's drawing may fail (it does under WSL); with X11, PyOpenGL must look for Qt's OpenGL context with GLX, or napari's drawing fails (« no valid context »). Started from napari's menu, spam-gui uses the display napari chose. - A subcommand per module on the command line,
spam-gui <module> [its arguments](e.g.,spam-gui mmr image1 image2; 2026-11-13). Each module declares its name (command) and its arguments (addArguments,useArguments). Why: each module has its own arguments and help, which stay right as modules are added. Set aside: a--moduleoption with arguments shared by the modules (one help for all).spam-gui --helploads the modules to list them (about 2 s), rather than describing them a second time. - No module named: a pop-up asks which one, before the window opens (2026-11-13). Why: (to confirm). Set aside: a start page in the window listing the modules; opening on the module used last time.
- The image windows give way; nothing hides unless asked (2026-11-13, SPEC.md 4.4). On a small screen (e.g., « Triple » at 1366×768), the image windows are narrower and the panels keep the width they need; when the window gets wider, a panel left narrower gets its width back. Why: the options of the step must stay usable (no sideways scroll, no width changing from one step to the next) while the images are compared. Set aside: a narrow step panel that scrolls; hiding the step panel by itself in « Triple » on a small screen.
- Called a module (2026-11-13): the kinds of work spam-gui offers (e.g., the multimodal registration, a strain
display) are modules, everywhere: on screen, on the command line (
spam-gui MODULE), in the docs and in the code (modules/,StepModule,MODULES). Why: the word the maintainer uses, which fits a module made of steps as well as one that only displays. Set aside: « tool » (disliked), « workflow » (not every module is a sequence of steps), « workspace », « plugin » (spam-gui is itself a napari plugin), « spam-mode » (« mode » is already the view and the mouse mode, on screen). « Module » then means nothing else: a Python file is a « file », a part of spam a « spam package » (see « Words » at the top). - No napari notifications (
show_info; 2026-11-13). Why: they appear over the image windows, at the bottom, for several seconds. Instead: what the user must know is shown in the panel (e.g., the step's text, a ⚠ note) or under a step's button (module.say); errors stay napari's (show_error). - No image analysis in spam-gui; what concerns spam in
TO_SPAM.mdonly (2026-11-13, section 1): functions spam lacks are written intospam/(folders as spam's packages) until a spam release has them; problems found in spam are documented there (how to reproduce, cause, fix), and not worked around. - The tests never use the user's napari settings (2026-11-13): each run has its own (
tests/conftest.py, and each window test its own file). Why: napari restores the saved window (size, maximised) and saves its own when closing: a test would depend on the user's screen, and change their settings.