name: eeglab-gui-visual-parity description: Build, port, or iterate on EEGPrep GUI features so they visually and behaviorally match EEGLAB MATLAB UI. Use when implementing a pop_ function GUI, adding an EEGPrep Qt dialog/window, creating or updating visual parity cases under tools/visual_parity, comparing EEGLAB and EEGPrep screenshots, debugging MATLAB GUI capture under X11, or tuning layout/style from an end-user screenshot feedback loop.
EEGLAB GUI Visual Parity
Use this skill to implement an EEGPrep GUI component with an EEGLAB reference
screenshot in the loop. The goal is not pixel perfection; it is end-user parity:
same controls, labels, order, enabled state, layout, and obvious hierarchy.
EEGLAB users must feel at home while using EEGPrep, so compare alignment,
arrangement, button placement, text fields, labels, defaults, enabled states,
and workflow before accepting a GUI as complete. Remember, while we are doing a porting project, EEGPrep must work well standalone and must be a delight to use for EEG Researchers.
When the GUI behavior, menu path, help surface, command history, or
GUI/eegprep-console synchronization changes, update the relevant Sphinx
workflow docs and packaged help resources in the same branch.
Work From The Repo Root
Run commands from the EEGPrep repo root:
git rev-parse --show-toplevel
Use the uv-managed project environment by default:
uv run pytest tests/test_visual_parity.py
For repeated capture runs after the environment is synced, skip dependency resolution:
uv run --no-sync python tools/visual_parity/capture.py --list
uv run --no-sync python tools/visual_parity/compare.py --case adjust_events_dialog
Install optional GUI dependencies before Python dialog capture:
uv sync --extra gui --group dev
Choose The Capture Path First
Before starting MATLAB or any display server, identify the machine class and then use the matching section below. Do not start X11 tooling on a Mac, and do not use the macOS desktop workflow on the SCCN Linux server.
uname -s
uname -m
hostname
command -v matlab || true
command -v uv || true
command -v conda || true
command -v Xvnc || true
command -v Xvfb || true
command -v xdpyinfo || true
command -v screencapture || true
command -v osascript || true
Use this decision rule:
- If
uname -sisLinuxand the machine has the SCCN server setup (conda,eegprep-dev, and usuallyXvnc), use SCCN Server Fast Path. - If
uname -sisDarwin, use MacBook Fast Path. - If neither path fits, probe dependencies first and then adapt the closest path without hard-coding checkout-specific absolute paths.
Always probe the Python used by capture before judging screenshots:
uv run --no-sync python - <<'PY'
import importlib.util, platform, sys
print(platform.platform())
print(sys.executable)
for name in ("numpy", "PySide6", "PIL"):
print(name, importlib.util.find_spec(name) is not None)
PY
SCCN Server Fast Path
This is the tested path on the shared server environment used for EEGPrep GUI parity work. Run it from the repo root; do not hard-code a checkout path.
Probe tools and choose the Python environment first:
command -v matlab || true
command -v Xvfb || true
command -v openbox || true
command -v Xvnc || true
command -v xdpyinfo || true
command -v identify || true
# On this server, use the EEGPrep dev env, not the default Python, unless this
# dependency probe passes with another interpreter.
conda activate eegprep-dev 2>/dev/null || true
PYTHON=${PYTHON:-$(command -v python)}
"$PYTHON" - <<'PY'
import importlib.util, sys
print(sys.executable)
for name in ("numpy", "PySide6"):
print(name, importlib.util.find_spec(name) is not None)
PY
If conda activate is not available in the non-interactive shell, use this
path-free fallback instead of guessing a checkout-specific interpreter path:
PYTHON=$(conda run -n eegprep-dev python -c 'import sys; print(sys.executable)')
If numpy or PySide6 is missing, set PYTHON to an interpreter from the
eegprep-dev environment or install .[gui] into the active environment. The
visual capture command uses sys.executable, so the capture subprocess will use
the interpreter running tools/visual_parity/capture.py.
Start a detached X11 display. This server may not have Xvfb or openbox, so
the reliable path is Xvnc. Use setsid so the display survives after the
shell command returns, and use >| because some shells enable noclobber.
if ! DISPLAY=:99 xdpyinfo >/tmp/xdpyinfo99.log 2>&1; then
setsid Xvnc :99 -geometry 1920x1080 -depth 24 \
-SecurityTypes None -localhost -ac -noreset +extension GLX +render \
>| /tmp/xvnc99-eegprep.log 2>&1 < /dev/null &
sleep 2
fi
export DISPLAY=:99
if locale -a | grep -qi '^en_US\.utf8$'; then
export LANG=en_US.utf8
export LC_ALL=en_US.utf8
fi
xdpyinfo | head
Then run the capture and compare loop:
PYTHONPATH=src "$PYTHON" tools/visual_parity/capture.py \
--case adjust_events_dialog \
--target both \
--timeout 180
PYTHONPATH=src "$PYTHON" tools/visual_parity/compare.py \
--case adjust_events_dialog
identify .visual-parity/adjust_events_dialog/eeglab.png \
.visual-parity/adjust_events_dialog/eegprep.png \
.visual-parity/adjust_events_dialog/side_by_side.png
sed -n '1,120p' .visual-parity/adjust_events_dialog/report.md
Open .visual-parity/adjust_events_dialog/side_by_side.png and judge the UI
like a user. In the successful server run, both screenshots were 858x169; the
remaining differences after tuning were font rendering and native widget bevels.
MacBook Fast Path
Use this path on a logged-in macOS desktop. Do not start Xvfb, Xvnc, or
openbox on macOS. MATLAB and Qt can render on the physical desktop, and the
capture helpers should be run from the uv environment.
Probe the Mac desktop tools first:
uname -s
sw_vers
command -v matlab || true
command -v screencapture || true
command -v osascript || true
uv run --no-sync python - <<'PY'
import importlib.util, platform, sys
print(platform.platform())
print(sys.executable)
for name in ("numpy", "PySide6", "PIL"):
print(name, importlib.util.find_spec(name) is not None)
PY
If matlab is missing, add the MATLAB command-line launcher to PATH before
running visual parity. If Qt or NumPy is missing, run:
uv sync --extra gui --group dev
For screenshots of main windows and modal dialogs, use the normal generated
capture scripts. They use MATLAB -nosplash -nodesktop -r and the Python
capture entrypoint; no DISPLAY variable is needed.
If you need cursor-level QA rather than screenshot capture, remember that
eegprep-console uses the IPython Qt input hook and may not expose the same
accessibility surface as a normal Qt event-loop app. Use the GUI-only launcher
for Computer Use checks, and verify shared console state separately.
uv run --no-sync python tools/visual_parity/capture.py --list
uv run --no-sync python tools/visual_parity/capture.py \
--case main_window \
--target both \
--timeout 180
uv run --no-sync python tools/visual_parity/compare.py --case main_window
sed -n '1,120p' .visual-parity/main_window/report.md
For iteration, keep the EEGLAB screenshot fixed and recapture only EEGPrep after each patch:
uv run --no-sync python tools/visual_parity/capture.py \
--case main_window \
--target eegprep \
--timeout 60
uv run --no-sync python tools/visual_parity/compare.py --case main_window
On macOS Retina displays, small numeric deltas can come from device-pixel-ratio
scaling, MATLAB font rendering, and Qt antialiasing. Treat compare.py metrics
as a smoke signal and inspect side_by_side.png at original size.
For main-window menu parity on macOS, prefer inventory parity over open-menu screenshots. MATLAB figure menus can be hard to capture reliably on macOS because the native application menu bar may receive focus instead of the EEGLAB figure menu. Use screenshots for the main window and modal dialogs; use menu inventories for menu labels, order, separators, and enabled state.
Export the EEGLAB inventories from the sibling checkout when present:
mkdir -p .visual-parity/menu_inventory
matlab -nosplash -nodesktop -r "try, addpath('tools/visual_parity/matlab'); states = {'startup','continuous','epoched','multiple'}; for i = 1:numel(states), export_eeglab_menu_inventory(['.visual-parity/menu_inventory/eeglab_' states{i} '_default.json'], '../eeglab', states{i}); end; catch ME, disp(getReport(ME, 'extended')); exit(1); end; exit(0);"
Then export and compare EEGPrep:
for state in startup continuous epoched multiple; do
uv run --no-sync python tools/visual_parity/export_eegprep_menu_inventory.py \
--state "$state" \
--output ".visual-parity/menu_inventory/eegprep_${state}_default.json"
uv run --no-sync python tools/visual_parity/menu_inventory.py \
--reference ".visual-parity/menu_inventory/eeglab_${state}_default.json" \
--candidate ".visual-parity/menu_inventory/eegprep_${state}_default.json" \
--report ".visual-parity/menu_inventory/${state}_default_report.md"
done
Inspect any differences:
for state in startup continuous epoched multiple; do
echo "== $state =="
sed -n '1,160p' ".visual-parity/menu_inventory/${state}_default_report.md"
done
If you need a quick local screenshot of an EEGPrep-opened menu on macOS, capture the EEGPrep target only:
uv run --no-sync python tools/visual_parity/capture.py \
--case file_menu \
--target eegprep \
--timeout 60
Do not treat a failed or blank EEGLAB open-menu screenshot on macOS as a menu implementation failure. Verify the menu tree with the inventory exporter.
Physical Desktop UX Check
Use this when the user wants to try the flow manually on a logged-in desktop, not just inspect screenshots. Run from the repo root in a desktop terminal so MATLAB and Qt can open real windows.
For EEGLAB, launch MATLAB desktop with the bundled EEGLAB on the path:
matlab -desktop
In MATLAB:
addpath(genpath(fullfile(pwd, 'src/eegprep/eeglab')));
close(findobj('tag','EEGLAB'));
eeglab full
EEG = eeg_emptyset;
EEG.setname = 'adjustevents demo';
EEG.data = zeros(1,1000);
EEG.nbchan = 1;
EEG.pnts = 1000;
EEG.trials = 1;
EEG.srate = 250;
EEG.xmin = 0;
EEG.xmax = (EEG.pnts-1)/EEG.srate;
EEG.chanlocs = struct('labels', {'Cz'});
EEG.event = struct( ...
'type', {'stim', 'resp', 'stim'}, ...
'latency', {100, 350, 700}, ...
'duration', {0, 0, 0});
EEG = eeg_checkset(EEG, 'eventconsistency');
[ALLEEG, EEG, CURRENTSET] = eeg_store(ALLEEG, EEG, 0);
eeglab redraw
Then use the real EEGLAB user path. For pop_adjustevents, this is:
Edit > Adjust event latencies
Try the same values you will try in EEGPrep, for example event type stim,
milliseconds 20, then OK.
If the menu item is missing, it is usually because EEGLAB is in simplified menu
mode. Use eeglab full, or enable all/advanced menu items in EEGLAB
preferences, then restart EEGLAB. Verify path and menu state with:
which eeglab -all
which pop_adjustevents -all
findall(findobj('tag','EEGLAB'), 'Label', 'Adjust event latencies')
For EEGPrep, the current user-facing flow for the first pop-function GUI is the pop function call with no processing arguments. There is not yet an EEGPrep main window menu path unless that shell/menu layer has been built for the feature.
PYTHON=${PYTHON:-python}
PYTHONPATH=src "$PYTHON" - <<'PY'
import numpy as np
from eegprep import pop_adjustevents
EEG = {
"data": np.zeros((1, 1000), dtype=np.float32),
"nbchan": 1,
"pnts": 1000,
"trials": 1,
"srate": 250.0,
"xmin": 0.0,
"xmax": 3.996,
"event": [
{"type": "stim", "latency": 100.0, "duration": 0.0},
{"type": "resp", "latency": 350.0, "duration": 0.0},
{"type": "stim", "latency": 700.0, "duration": 0.0},
],
}
out, com = pop_adjustevents(EEG, return_com=True)
print(com)
print([event["latency"] for event in out["event"]])
PY
Enter the same values as the EEGLAB run. With stim and 20 ms at 250 Hz, the
printed stim latencies should shift by 5 samples. For another pop function,
replace the demo data, menu item, and pop_adjustevents(EEG) call with that
feature's real EEGLAB path and EEGPrep pop-function entrypoint.
Implementation Pattern
For an EEGLAB pop function such as pop_adjustevents:
Read the MATLAB source first:
sed -n '1,260p' src/eegprep/eeglab/functions/popfunc/pop_adjustevents.mKeep the Python files simple and parallel to EEGLAB:
- Backend/API and dialog spec:
src/eegprep/functions/popfunc/pop_<name>.py - Export:
src/eegprep/__init__.py - Shared GUI primitives:
src/eegprep/functions/guifunc/spec.py,src/eegprep/functions/guifunc/inputgui.py,src/eegprep/functions/guifunc/qt.py - Visual capture entrypoint:
src/eegprep/functions/guifunc/visual_capture.py - Tests:
tests/test_pop_<name>.py,tests/test_gui_pop_<name>.py,tests/test_visual_parity.py
- Backend/API and dialog spec:
Make the dialog spec mirror EEGLAB's
uilistanduigeom.Keep labels, control order,
tagvalues, dialog title, and callback intent close to MATLAB. Store original MATLAB callback strings as metadata when it helps future agents maintain parity, but implement callbacks in explicit Python functions.Put toolkit-specific tuning in the renderer, not the pop-function spec.
The spec should describe the EEGLAB-like dialog. The Qt renderer can own colors, margins, button order, checkbox rendering, and fixed widget sizing needed to look like MATLAB.
Test backend behavior separately from screenshots.
Use ordinary unit tests for argument parsing, data mutation, errors, command history, GUI cancel, and renderer-returned values. Do not rely on screenshots to prove numerical behavior.
Start A Virtual X11 Display
MATLAB GUI capture needs a real X11 display. Do not use matlab -batch for GUI
screenshots.
Preferred setup when available:
pkill -f "Xvfb :99" || true
Xvfb :99 -screen 0 1920x1080x24 -ac +extension GLX +render -noreset \
> /tmp/xvfb99.log 2>&1 &
export DISPLAY=:99
openbox > /tmp/openbox99.log 2>&1 &
xdpyinfo | head
Fallback on servers that have TigerVNC but not Xvfb/openbox:
# Keep this running in a long-lived process.
Xvnc :99 -geometry 1920x1080 -depth 24 -SecurityTypes None -localhost \
-ac -noreset +extension GLX +render
Then in the capture shell:
export DISPLAY=:99
xdpyinfo | head
Clean up after the loop:
pkill -u "$USER" -x Xvnc || true
pkill -f "Xvfb :99" || true
pkill -u "$USER" -x openbox || true
pgrep -a -u "$USER" 'Xvnc|Xvfb|openbox' || true
Add Or Update A Visual Parity Case
Add a case to tools/visual_parity/cases.json:
{
"id": "adjust_events_dialog",
"description": "Adjust event latencies dialog opened from pop_adjustevents.",
"window_size": [858, 169],
"timeout_seconds": 120,
"targets": {
"eeglab": {
"type": "matlab_dialog",
"action": "pop_adjustevents"
},
"eegprep": {
"type": "command",
"action": "adjust_events_dialog",
"command": [
"{python}",
"-m",
"eegprep.functions.guifunc.visual_capture",
"--case",
"{case_id}",
"--output",
"{output}"
]
}
}
}
For MATLAB modal dialogs, generate a temporary MATLAB script from
tools/visual_parity/capture.py. Use:
matlab -nosplash -nodesktop -r \
"try, run('/absolute/path/to/generated_capture.m'); catch ME, disp(getReport(ME, 'extended')); exit(1); end; exit(0);"
Use a MATLAB timer to capture modal dialogs while inputgui is open. Capture
only after the dialog's OK button exists, then set its userdata to unblock
inputgui. Capturing too early can delete or close the figure while EEGLAB is
still constructing it.
Screenshot Feedback Loop
Run the loop from one shell with DISPLAY set:
export DISPLAY=:99
PYTHON=${PYTHON:-python}
PYTHONPATH=src "$PYTHON" tools/visual_parity/capture.py --list
PYTHONPATH=src "$PYTHON" tools/visual_parity/capture.py \
--case adjust_events_dialog \
--target eeglab \
--timeout 180
PYTHONPATH=src "$PYTHON" tools/visual_parity/capture.py \
--case adjust_events_dialog \
--target eegprep \
--timeout 60
PYTHONPATH=src "$PYTHON" tools/visual_parity/compare.py \
--case adjust_events_dialog
For iteration, keep the EEGLAB screenshot fixed and recapture only EEGPrep after each Python-side patch:
PYTHONPATH=src "$PYTHON" tools/visual_parity/capture.py \
--case adjust_events_dialog \
--target eegprep \
--timeout 60
PYTHONPATH=src "$PYTHON" tools/visual_parity/compare.py \
--case adjust_events_dialog
Inspect:
identify .visual-parity/adjust_events_dialog/eeglab.png
identify .visual-parity/adjust_events_dialog/eegprep.png
sed -n '1,120p' .visual-parity/adjust_events_dialog/report.md
Open or view these artifacts:
.visual-parity/<case>/eeglab.png.visual-parity/<case>/eegprep.png.visual-parity/<case>/side_by_side.png.visual-parity/<case>/diff.png.visual-parity/<case>/report.md
When iterating, patch the smallest relevant layer:
- Wrong labels/order/tags: patch the pop-function dialog spec.
- Wrong behavior after clicking/editing: patch callbacks or backend parsing.
- Wrong spacing/colors/button order/native widget shape: patch the Qt renderer.
- MATLAB capture blank or missing: patch the generated MATLAB capture script.
- Python capture blank or wrong state: patch
eegprep.functions.guifunc.visual_capture. - Import errors after file moves: fix the moved module imports before judging
screenshots. For example, after moving pop functions into
functions/popfunc, relative imports such as.utilsshould use the current EEGPrep package path for that helper.
Treat pixel metrics as a smoke signal, not the final judge. A good dialog can still have differences from font rendering, antialiasing, native bevels, or OS theme. Prioritize user-visible structure.
Do not commit generated .visual-parity/ screenshots unless explicitly asked
for a durable reference. For PRs involving GUI work, attach side-by-side images
as GitHub user attachments using gh attach --comment instead of committing
them.
Actionable PR attachment workflow:
- Keep comparison artifacts local, usually under
.visual-parity/<case>/side_by_side.pngor/tmp. - Upload images with
gh attach --comment <pr-number> <path-to-png> ...; it stores the files as GitHub user attachments and posts or refreshes the PR comment. - Put all GUI cases in one concise PR comment, starting with
🤖, and wrap the image list in<details>when there are many screenshots. - If you need a custom comment body, run
gh attach <pr-number> <path-to-png> ...without--comment, then include the returned Markdown in a PR comment that starts with🤖. - Label each image with the feature/state it covers. If an image is UX-only evidence rather than strict EEGLAB parity, say that explicitly.
Iterative Development Checklist
Read the EEGLAB MATLAB source and confirm the dialog title, labels,
uilist,uigeom, tags, callbacks, and default values.Capture EEGLAB and EEGPrep once with
--target both; if either capture fails, fix the capture environment before changing GUI code.Inspect
side_by_side.pngat original size. Prefer structural parity over exact pixel metrics: labels, order, enabled state, control sizes, alignment, and button placement matter most.Patch the smallest layer: spec for structure, renderer for toolkit styling, visual capture for deterministic screenshot state, capture script for MATLAB.
Recapture only EEGPrep and rerun
compare.py; repeat until user-visible differences are down to font rendering/native bevels.Recapture both targets once at the end so the final artifacts came from the same display session.
Run focused tests and pre-commit:
PYTHONPATH=src "$PYTHON" -m unittest \ tests.test_pop_adjustevents \ tests.test_gui_pop_adjustevents \ tests.test_visual_parity ./pre-commit.py <changed files>Stop the display you started:
pgrep -a -u "$USER" 'Xvnc|Xvfb|openbox|MATLAB|matlab' || true pkill -u "$USER" -x Xvnc || true pkill -f "Xvfb :99" || true pkill -u "$USER" -x openbox || true
Validation Commands
Run focused tests during development:
PYTHONPATH=src "$PYTHON" -m unittest \
tests.test_pop_adjustevents \
tests.test_gui_pop_adjustevents \
tests.test_visual_parity
Run compile checks after editing GUI/capture code:
PYTHONPATH=src "$PYTHON" -m compileall -q \
src/eegprep/functions/popfunc/pop_adjustevents.py \
src/eegprep/functions/guifunc \
tools/visual_parity \
tests/test_pop_adjustevents.py \
tests/test_gui_pop_adjustevents.py \
tests/test_visual_parity.py
Before finishing, verify no display sessions were left behind:
pgrep -a -u "$USER" 'Xvnc|Xvfb|openbox' || true
Troubleshooting
xdpyinfocannot open:99: the virtual display is not running orDISPLAYwas not exported in the current shell.Xvncstarts and then disappears: start it withsetsid ... < /dev/null &. A foreground X server tied to a short-lived shell may exit before capture./tmp/xvnc99-eegprep.log: cannot overwrite existing file: use>|instead of>or delete the old log. Some shells enablenoclobber.- Qt says it cannot load
xcb: the display may have died, or system packages such aslibxcb-cursor0may be missing. - Qt warns about locale
C: setLANG=en_US.utf8andLC_ALL=en_US.utf8iflocale -alistsen_US.utf8. The warning is usually non-fatal. - EEGPrep capture fails with
ModuleNotFoundErrorbefore opening a window: fix import breakage first. Do not tune screenshots against a failing import. - MATLAB capture hangs: use
-nosplash -nodesktop -r, not-batch; ensure the timer starts before opening the modal dialog. - MATLAB capture creates a blank image: verify the dialog is visible on
DISPLAY=:99; the capture helper should trygetframefirst and Java Robot second. If both fail, use an external X11 screenshot tool such asimportorscrot. - MATLAB prints
Not enough parameters selectedafter dialog capture: expected forpop_adjusteventswhen the timer presses OK on an empty dialog. The PNG is still valid if the capture command exits successfully. - MATLAB prints a software OpenGL warning: expected under virtual X11 and not a visual parity failure by itself.
- Default
pythonlacksnumpyorPySide6: activateeegprep-devor setPYTHONto that environment's interpreter before running capture. - Wayland desktop attach fails: use Xvfb, Xvnc, VNC, or an Xorg desktop. Wayland blocks arbitrary screenshot/window automation by design.