Files
gelectron/CONTRIBUTING.md

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's main.js imports electron, 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_menu for the app's lifetime.
  • The default menu (install_default_app_menu) must be stored too, not just the one from SetApplicationMenu.

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_icon must set Transformations::EXPAND | STRIP_16 on the decoder so downstream code can assume 8-bit RGBA bytes. Treating a 16-bit buffer as 8-bit mangles the icon.
  • Packager: generateIcns downconverts the source to 8-bit (via sips or PIL) before building the .icns, since sips preserves 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 rustfmt defaults. Run cargo fmt before 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 missing link.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 and process.platform checks in JS (the compat layer already exposes isMac/isWin style helpers in places).

Opening a PR

  1. Fork and create a feature branch.
  2. Make your changes, keeping the "Rules of the road" in mind.
  3. Run cargo fmt, cargo build --release -p gelectron, and cargo test -p gelectron.
  4. Verify against a real app (cargo run --release -p gelectron -- /path/to/app).
  5. Open the PR with a short description of what changed and why, and note how you verified it.

Getting help