An Electron desktop wrapper around NMRium
(cheminfo/nmrium), giving chemists a native install-and-open app — Start
Menu / Applications entry, a File → Open dialog, .dx/.jdx file
associations, and persistent local files — instead of a browser PWA install
flow.
- Native File → Open dialog and
.dx/.jdxfile-association double-click, wired into NMRium's own file-loading pipeline unmodified. - NMRium's own drag-and-drop still works as-is.
- A native File → Open Sample menu (once the optional sample data is installed, see below) replaces NMRium's own in-app demo dataset picker.
- File → Save As (
.nmriumarchive) and Export as SVG, backed by NMRium's ownNMRiumRefAPI(see "Native menu" below for what is and isn't reachable this way). - File → Import Molecule… (
.mol/.sdf) for NMRium's structure/atom-to-peak assignment panels — e.g. a molecule exported from Ketcher. Goes through the exact same delivery path as opening a spectrum; NMRium's own file loader already treats molecule files as a first-class input. - View → Workspace menu exposing NMRium's built-in workspace presets (Default, 1D Processing, Prediction, Assignment, Simulation, Exercise, Embedded) — otherwise undiscoverable from inside the app itself.
- Help → About, crediting Zakodium/cheminfo per their license terms.
- No changes to NMRium's processing/rendering logic — it's built from source
as a pinned git submodule. We render just the
<NMRium>library component itself (via our own thin renderer,renderer/), not NMRium's demo/docs-site app — that app's routes, sample-picker sidebar, etc. are demo-site chrome we don't need and don't ship.
NMRium's own NMRiumRefAPI (the only supported way to reach into a mounted
<NMRium> from outside) is intentionally tiny: loadFiles,
loadFileCollection, getNMRiumFile (full .nmrium archive export), and
getSpectraViewerAsBlob (SVG only, no PNG). That caps what a native menu can
responsibly do:
- Reachable, and wired up: Open, Open Sample, Import Molecule, Save As,
Export as SVG, Workspace switching (the
workspaceprop is live-reactive — no remount needed). - Not reachable, so not in the menu: Undo/Redo (NMRium has no working
undo/redo at all, even internally — it's dead reducer scaffolding
upstream, marked
@todo), individual panel/toolbar-button toggles (settable once via thepreferences/workspaceprops at mount, not callable afterwards), PNG export/clipboard-copy, print, and other export formats (NMReData, JCAMP-DX, TSV) — all internal-only in NMRium's own toolbar. - NMRium registers its own keyboard shortcuts on
Ctrl/Cmd+O/S/Shift+S/P/Cinternally (KeysListenerTracker.tsx,PrintContent.tsx). Our native Open… keepsCmdOrCtrl+O(no observed conflict); Save As / Export as SVG deliberately have no accelerator to avoid an untested collision with NMRium's own in-page listeners for those same combinations.
Two Linux downloads are published per release. Prefer the .deb on Debian,
Ubuntu and derivatives:
sudo dpkg -i nmrium-desktop_<version>_amd64.debIt has no runtime dependency beyond what the package declares, registers the
.dx/.jdx file associations, and puts NMRium Desktop in the applications
menu.
The AppImage is the portable option — no install, just mark it executable and run it.
Releases after v2.5.0 need nothing extra. Earlier ones (v2.3.0, v2.5.0) shipped
electron-builder's default AppImage runtime, which dlopen()s libfuse.so.2,
and Ubuntu 24.04+, Debian 13+ and current Fedora ship only FUSE 3. On those, an
older AppImage exits immediately with:
dlopen(): error loading libfuse.so.2
If you hit that, either grab a newer release, or:
sudo apt install libfuse2t64 # Ubuntu 24.04+ / Debian 13+
./NMRium\ Desktop-<version>.AppImage --appimage-extract-and-run # or skip FUSE entirelyCurrent builds replace that runtime with AppImage's statically-linked
type2-runtime, which has no libfuse
dependency at all — confirmed with strace: the old runtime makes one
openat() for libfuse.so.2, the new one makes none. See
scripts/appimage-runtime.cjs.
- Node.js 24 (see
.nvmrc) — matches the Node version NMRium itself pins via its own.nvmrc/Volta config. - git (needed for the submodule).
git clone --recurse-submodules <this-repo-url>
cd nmrium-desktop
npm install
npm run build:nmrium # installs the pinned NMRium submodule's own dependencies
npm start # builds renderer/ then launches ElectronIf you already cloned without --recurse-submodules:
git submodule update --initLocal builds only target Linux (AppImage + deb) — that's what this machine can run and test directly. Windows (nsis) and macOS (dmg) builds happen in CI (GitHub Actions), not locally.
npm run distpackage.json's build.compression is deliberately "normal", not
"maximum" — do not "optimize" this back. AppImage mounts its payload as a
FUSE-backed squashfs at launch, and "maximum" (xz) compression measured
~60s to get a window on screen on a cold cache, vs. ~12s at "normal",
for ~20MB more on disk. .deb installs are unaffected either way (dpkg
extracts to disk once at install time, no runtime decompression), so this
tradeoff only concerns the AppImage.
The packaged app ships without NMRium's own demo sample catalog (Cytisine,
ethylbenzene, teaching exercises, etc. — nmrium/public/data and
/exercises, ~250MB) since it's demo content for the public web app, not
something you need to open your own spectra. This is most of why the
installer is small.
npm run build/npm run dist always produce both companion packages
alongside the main app (dist/nmrium-desktop-samples_<version>_all.deb and
dist/nmrium-samples.zip) — they're a permanent part of the build, not an
opt-in extra step, so they're never at risk of getting lost in a clean
rebuild. Installing either is still optional and separate from the main app:
Debian/Ubuntu — companion .deb (recommended on Linux):
sudo apt install ./dist/nmrium-desktop-samples_2.3.0_all.debInstalls system-wide to /usr/share/nmrium-desktop/samples, which the app
checks automatically. The main app's own .deb lists this package as a
Suggests, not a Recommends, so a plain apt install nmrium-desktop
never pulls it in automatically.
Any OS — zip, extracted per-user:
unzip dist/nmrium-samples.zip -d ~/.config/nmrium-desktop/samples # Linux(On macOS: ~/Library/Application Support/nmrium-desktop/samples; on
Windows: %APPDATA%\nmrium-desktop\samples.) The app checks the per-user
copy first, then the system-wide .deb install, then falls back to the
(missing) bundled copy.
A Sync NMRium workflow can track upstream automatically — moving the
submodule pin to each new release, syncing the version, and pushing a tag that
builds a draft release. Its schedule is currently paused pending a GH_PAT
secret; see CONTRIBUTING.md.
Until it is enabled, bump upstream by hand:
npm run update-nmrium # checks out the latest vX.Y.Z tag, syncs the version
# or: npm run update-nmrium -- v2.4.0
npm run build:nmrium # install the new submodule's dependencies
npm test && npm run test:nmrium # wrapper suite + NMRium's own
npm run dist # then smoke-test the built app for real
git commit -am "chore: update NMRium to vX.Y.Z"The pin itself stays: the submodule records a commit SHA, not a tracked branch, so every release is reproducible and the version number always answers "which NMRium is inside?". Tracking upstream means moving that pin on a schedule, not removing it — see CONTRIBUTING.md.
nmrium-desktop's package.json version tracks NMRium's tag 1:1 (same
convention as ketcher-desktop tracking Ketcher). It is derived from the
submodule, never hand-edited — npm run check-version enforces this before
every packaging build and in CI, so a drifted version cannot ship.
nmrium-desktop/
├── electron/
│ ├── main.js # BrowserWindow, app:// protocol, native menu, file-open/save IPC
│ └── preload.js # feeds opened files into NMRium's own file input; contextBridge API for save/export/workspace
├── renderer/
│ ├── index.html
│ └── src/
│ ├── index.tsx # mounts <NMRium ref/workspace> — no demo-app chrome
│ ├── electron-api.d.ts # types for window.electronAPI
│ └── blueprint-icons-woff2.css
├── vite.config.js # builds renderer/ against nmrium/src/component/main
├── scripts/
│ ├── build-nmrium.sh # npm install inside nmrium/ (no build — see below)
│ ├── update-nmrium.sh
│ ├── build-samples-archive.sh # optional nmrium-samples.zip (see below)
│ ├── build-samples-deb.sh # optional nmrium-desktop-samples .deb (see below)
│ ├── generate-icons.cjs # build/icon.png -> build/icons/{16..1024}x*.png (via Electron's nativeImage)
│ └── appimage-wrap.cjs # afterPack: force --no-sandbox, strip dead weight, on Linux
├── build/
│ ├── icon.png # app icon SOURCE — NMRium's own brand mark (from nmrium.com/brand)
│ └── icons/ # GENERATED by generate-icons.cjs, gitignored — do not hand-edit
├── tests/ # wrapper test suite (node --test, no dependencies)
├── nmrium/ # git submodule -> github.com/cheminfo/nmrium, pinned to the matching vX.Y.Z
└── package.json # electron-builder config lives here
renderer/ is our own minimal Vite app: it imports the NMRium component
directly from nmrium/src/component/main (the actual library source, not
NMRium's demo/docs-site build) and mounts it with no other chrome. This
means npm run build:nmrium only needs to install the submodule's
dependencies — its own npm run build (which builds the demo app: routing,
sample-picker sidebar, etc.) is never invoked. vite.config.js sets
resolve.dedupe for react/react-dom/blueprint/react-science so our renderer
entry and NMRium's internals share a single copy of each, since the
submodule's own node_modules also carries them (as its devDependencies
for building its demo app).
The renderer build output (renderer/dist) is served through a custom
app:// protocol rather than file://, for secure-context treatment
consistent with what NMRium expects. Native File → Open (and File → Open
Sample, once sample data is installed) reads the file in the main process
and delivers it to NMRium's existing hidden file input (the same one its
own drag-and-drop UI uses), rather than modifying NMRium's source. Save
As/Export as SVG go the other direction — main asks the renderer (over a
contextBridge-exposed window.electronAPI, since there's no DOM element
to drive for these) to compute the export via NMRiumRefAPI, which hands
the bytes back for dialog.showSaveDialog + a plain file write.
Two easy-to-regress gotchas, both required for the app to actually show its own icon (taskbar/dash/Alt-Tab) instead of a generic one:
BrowserWindow'siconoption needs a loadednativeImage, not a raw path string — the latter silently produces an empty_NET_WM_ICON.- Electron's runtime
WM_CLASSis derived frompackage.json'snamefield (nmrium-desktop), notproductName(NMRium Desktop). GNOME Shell (and others) match a running window to its.desktopfile viaStartupWMClass, so that field is explicitly overridden inbuild.linux.desktop.StartupWMClassto match — electron-builder's default (productName) would otherwise never match, silently falling back to a generic icon with no error anywhere. Also note: under a native Wayland session (not XWayland fallback),_NET_WM_ICONmay stay empty even when everything is correctly configured — GNOME resolves the icon via the matched.desktopfile instead, so that alone isn't a sign of failure.
npm start # build the renderer and launch
npm test # wrapper test suite — no dependencies, no submodule neededReload with the View menu / devtools for debugging the loaded NMRium build.
See CONTRIBUTING.md for the full build, test and versioning guide, and CHANGELOG.md for what changed in each release.
This wrapper is MIT — see LICENSE — matching upstream NMRium.
NMRium itself is not our work. It is developed by Zakodium/cheminfo (with EU Horizon 2020 grant funding) and is bundled unmodified; see THIRD_PARTY_LICENSES for the full attribution and NMRium's MIT text verbatim, nmrium/LICENSE in the pinned checkout, and https://github.com/cheminfo/nmrium for the upstream project.
The packaged app omits Chromium's own bundled LICENSES.chromium.html
(~12MB, purely informational, not read at runtime) to keep install size
down — see https://www.chromium.org/Home for upstream Chromium's own
third-party license notices if needed.
