8.7 KiB
Contributing to Gelectron
Thanks for your interest in Gelectron! This guide covers how the project is put
together and how to contribute changes that build, pass tests, and don't regress
the app you might be targeting (e.g. vanilla-sh, the reference consumer).
What Gelectron is
Gelectron is a drop-in replacement for Electron that uses the OS-native web view (WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux) instead of bundling Chromium. It has two halves that talk to each other:
gelectron-app— a standalone Rust binary built on tao (windowing) and wry (WebView). It owns windows, the native menu bar, dialogs, clipboard, and the dock/taskbar icon.src/electron/— a JavaScript compatibility layer that implements the Electron API surface (app,BrowserWindow,Menu,ipcMain, …). Your Electron app'smain.jsimportselectron, which resolves to this layer.
The two halves communicate over JSON-line IPC. The Rust side spawns Node.js
as a child process (or runs the JS layer inside the WebView in --no-node mode)
and both sides exchange messages like create-window, load-url,
ipc-message, and set-application-menu.
Read README.md for the full architecture diagram and supported-API table.
Repository layout
gelectron/
├── Cargo.toml # Rust workspace (patches wry → vendor/wry)
├── crates/
│ ├── gelectron-app/ # ★ The main native binary (tao + wry)
│ │ └── src/main.rs # event loop, IPC, menus, icons, dialogs
│ └── gelectron-core/ # Legacy N-API addon (not the primary path)
├── src/electron/ # JS Electron compatibility layer
├── vendor/wry/ # Vendored wry (patched via [patch.crates-io])
├── packager/bin/gelectron-packager.js # Bundles apps into distributables
├── cli/gelectron.js # Pure-Node.js fallback runner
├── demo/ # Minimal reference app
├── benchmark/ # Memory/perf comparison vs Electron
└── scripts/install.sh # Installs binary + compat layer
Most day-to-day work happens in crates/gelectron-app/src/main.rs (native
behavior) and src/electron/ (JS API behavior). main.rs is ~1900 lines;
it's the file where menu, icon, window, and IPC logic lives.
Prerequisites
- Rust 1.75+ —
rustup.rs - Node.js 18+ and npm
- macOS builds also want Xcode command-line tools (
xcode-select --install)
First-time setup
git clone https://github.com/mileswolfallen2/gelectron.git
cd gelectron
npm install
Development loop
The most common workflow is to build the binary, then run an app against it:
# Build the native binary
cargo build --release -p gelectron
# Run the bundled demo
cargo run --release -p gelectron -- demo/
# Run a real Electron app against your build
cargo run --release -p gelectron -- /path/to/your-app
If you're iterating on the JS compat layer only, scripts/install.sh installs
both the binary and src/electron/ so gelectron <app> works from anywhere:
./scripts/install.sh
Environment variables
| Variable | Use |
|---|---|
RUST_LOG=info |
Rust-side logging (gelectron crate) |
GELECTRON_LOG=1 |
Verbose JS-side logging |
GELECTRON_DEV=1 |
Development mode |
VITE_DEV_SERVER_URL=<url> |
Point the app at a Vite dev server |
Testing
Run the Rust unit tests:
cargo test -p gelectron
The suite covers image/icon decoders (BMP-in-ICO, PNG-in-ICO, PNG round-trip). Keep these green — they're fast and catch byte-layout regressions in icon handling.
There is no end-to-end test harness yet. Manual verification looks like:
# 1. Build
cargo build --release -p gelectron
# 2. Package the reference app and launch it
# (from the vanilla-sh checkout)
npm run build
open dist/VanillaChat.app
# 3. Sanity-check the pieces you touched, e.g.:
# - Menu bar: Apple, AppName, File, Edit, View, Window, Help
# - Edit menu: Undo/Redo/Cut/Copy/Paste/Select All with Cmd key equivalents
# - Dock icon renders without distortion
# - Cmd+C / Cmd+V work inside the webview
To verify menu behavior programmatically (macOS), you can query System Events:
osascript -e 'tell application "System Events" to tell process "<AppName>" \
to get name of every menu bar item of menu bar 1'
Rules of the road
These rules exist because each one corresponds to a bug that has actually happened. Please respect them.
1. Native menus must stay alive after init_for_nsapp()
On macOS, AppKit stores raw pointers into the muda menu structures as
instance variables on the native NSMenuItems. If you drop the muda::Menu
after init_for_nsapp(), clicking any menu item dereferences freed memory →
random, non-deterministic crashes.
- Keep the built menu in
AppState.app_menufor the app's lifetime. - The default menu (
install_default_app_menu) must be stored too, not just the one fromSetApplicationMenu.
2. Menu submenus come in two shapes
The compat layer serializes a submenu either as a bare array or as an object
with an items array ({"items": [...]}). Always normalize with
submenu_items() before iterating, and treat an item as a submenu when
type == "submenu" or a submenu field is present. The vanilla-sh menu
template relies on the second form, and the app's own menu builder relies on the
first.
3. Icons: normalize 16-bit PNGs
logo.png-style source icons are often 16-bit RGBA. Two code paths must handle
this:
- Rust:
decode_png_iconmust setTransformations::EXPAND | STRIP_16on the decoder so downstream code can assume 8-bit RGBA bytes. Treating a 16-bit buffer as 8-bit mangles the icon. - Packager:
generateIcnsdownconverts the source to 8-bit (viasipsor PIL) before building the.icns, sincesipspreserves 16-bit at 1024px and macOS renders that poorly.
4. macOS APIs require the main thread
Menu construction, setApplicationIconImage, and window operations all need the
main/event-loop thread. Don't call them from background threads.
5. Don't add comments unless they earn their place
The codebase is deliberately comment-light. A comment should explain why something is non-obvious (e.g. "AppKit stores raw pointers here") rather than restating what the code does.
6. Keep the wry patch minimal
wry is vendored and patched via [patch.crates-io] in Cargo.toml. Only
change vendor/wry/ when you genuinely need a WebView-level behavior change
(e.g. media permissions). Prefer fixing things in gelectron-app or the JS
layer.
Code style
- Rust: follow
rustfmtdefaults. Runcargo fmtbefore committing. - JS: CommonJS, 2-space indent,
'use strict'in CLI scripts. - Match the surrounding code's conventions — this project has no linter config, so consistency is on you.
Packaging changes
If your change touches distribution, rebuild and package a real app to verify:
# From packager/ (or via npm link)
gelectron-packager --dir ./my-app --name MyApp
# macOS: verify the bundle's icon and signature
file dist/MyApp.app/Contents/Resources/AppIcon.icns
codesign --verify --deep --strict dist/MyApp.app
The packager: copies node_modules into Resources/app/, generates AppIcon.icns
from icon.png, writes Info.plist with the icon + mic/camera usage strings, and
re-signs the bundle with the entitlements. If any of that seems off, check
packager/bin/gelectron-packager.js and vanilla-sh/scripts/build-desktop.sh.
Cross-platform notes
- macOS is the primary dev platform and the most battle-tested path.
- Windows builds target GNU (
x86_64-pc-windows-gnu). MSVC cross-compiles from macOS are blocked by a missinglink.exe— use the GNU toolchain. - Linux needs WebKitGTK dev packages; cross-compiling from macOS is unconfigured.
- Platform-specific behavior should be
#[cfg(target_os = "...")]in Rust andprocess.platformchecks in JS (the compat layer already exposesisMac/isWinstyle helpers in places).
Opening a PR
- Fork and create a feature branch.
- Make your changes, keeping the "Rules of the road" in mind.
- Run
cargo fmt,cargo build --release -p gelectron, andcargo test -p gelectron. - Verify against a real app (
cargo run --release -p gelectron -- /path/to/app). - Open the PR with a short description of what changed and why, and note how you verified it.
Getting help
- Open an issue at https://github.com/mileswolfallen2/gelectron/issues
- The
vanilla-shcheckout (/Users/milesallen/Documents/GitHub/vanilla-sh) is the reference consumer — changes that regress it are treated as bugs.