2 Commits
Author SHA1 Message Date
miles 1689adffc6 feat: enhance native binary resolution and add bundled Node.js runtime
- Refactored gelectron.js to improve native binary discovery with additional paths and environment variable support.
- Implemented a function to find the Node.js runtime, allowing bundled Node.js to be used when available.
- Updated package.json to include gelectron-packager as a CLI command.
- Enhanced gelectron-packager.js to locate installed gelectron runtime and added compatibility for bundled Node.js.
- Modified install-release.sh to download and install a private Node.js runtime alongside the gelectron binary.
- Updated NSIS installer script to include bundled Node.js runtime.
- Enhanced AppImage and DMG packaging scripts to include Node.js runtime.
- Added clipboard, nativeTheme, and screen modules for Electron compatibility.
- Implemented native bridge request handling for clipboard operations.
2026-09-05 22:23:37 -05:00
miles 247daa6460 Add native installers (DMG/EXE/AppImage) to releases 2026-09-01 16:27:07 -05:00
18 changed files with 1793 additions and 45 deletions
+134 -8
View File
@@ -1,25 +1,41 @@
name: Build native binaries
# On every new release (tag push of `v*`), this workflow:
# 1. builds the gelectron native engine + N-API addons for all platforms,
# 2. packages portable archives, and
# 3. builds installers you can simply install:
# - macOS: Gelectron-<ver>-arm64.dmg / Gelectron-<ver>-x64.dmg
# - Windows: Gelectron-<ver>-x64.exe / Gelectron-<ver>-arm64.exe
# - Linux: Gelectron-<ver>-x86_64.AppImage / Gelectron-<ver>-aarch64.AppImage
#
# Everything is attached to the GitHub Release for that tag.
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build:
name: Build ${{ matrix.target }}
# ── N-API addons (published to npm, also attached to the release) ─────────
addon:
name: Addon ${{ matrix.target }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
- os: macos-14
target: aarch64-apple-darwin
- os: macos-latest
- os: macos-13
target: x86_64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
@@ -64,9 +80,112 @@ jobs:
name: bindings-${{ matrix.target }}
path: gelectron_core.*.node
# ── Native engine + portable archives + installers ────────────────────────
runtime:
name: Runtime ${{ matrix.platform }}-${{ matrix.arch }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-14
target: aarch64-apple-darwin
platform: darwin
arch: arm64
- os: macos-13
target: x86_64-apple-darwin
platform: darwin
arch: x64
- os: windows-latest
target: x86_64-pc-windows-msvc
platform: win32
arch: x64
- os: windows-latest
target: aarch64-pc-windows-msvc
platform: win32
arch: arm64
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
platform: linux
arch: x64
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
platform: linux
arch: arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Linux system dependencies
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libsoup-3.0-dev
- name: Build gelectron binary
run: cargo build --release -p gelectron --target ${{ matrix.target }}
- name: Package portable release archive
run: |
bash scripts/make-release.sh \
-v "${{ github.ref_name }}" \
-p ${{ matrix.platform }} \
-a ${{ matrix.arch }} \
-b "target/${{ matrix.target }}/release/gelectron" \
--no-build
# ── macOS DMG installer ────────────────────────────────────────────────
- name: Build macOS installer (DMG)
if: matrix.platform == 'darwin'
run: bash scripts/pkg/make-dmg.sh -v "${{ github.ref_name }}" -a ${{ matrix.arch }} -b "target/${{ matrix.target }}/release/gelectron" -o dist
# ── Windows EXE installer (NSIS) ──────────────────────────────────────
- name: Build Windows installer (EXE)
if: matrix.platform == 'win32'
shell: pwsh
run: |
choco install nsis -y --no-progress
powershell -ExecutionPolicy Bypass -File scripts/pkg/make-exe.ps1 `
-Bin "target/${{ matrix.target }}/release/gelectron.exe" `
-Compat src/electron `
-Version "${{ github.ref_name }}" `
-Arch ${{ matrix.arch }} `
-Out dist
# ── Linux AppImage installer ──────────────────────────────────────────
- name: Build Linux installer (AppImage)
if: matrix.platform == 'linux'
run: bash scripts/pkg/make-appimage.sh -v "${{ github.ref_name }}" -b "target/${{ matrix.target }}/release/gelectron" -o dist -a ${{ matrix.arch }}
- name: List build outputs
run: ls -la dist/
- name: Upload runtime and installer artifacts
uses: actions/upload-artifact@v4
with:
name: installers-${{ matrix.platform }}-${{ matrix.arch }}
path: |
dist/gelectron-*
dist/Gelectron-*
if-no-files-found: error
# ── Attach everything to the GitHub Release ───────────────────────────────
release:
name: Attach binaries to GitHub Release
needs: build
needs: [addon, runtime]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: write
@@ -79,10 +198,17 @@ jobs:
pattern: bindings-*
merge-multiple: true
- name: List binaries
- name: Download all installers
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: installers-*
merge-multiple: true
- name: List artifacts
run: ls -la artifacts/
- name: Upload binaries to GitHub Release
- name: Upload assets to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: artifacts/*.node
files: artifacts/**
+1 -5
View File
@@ -25,11 +25,7 @@ dist/
# Packager cache
.gelectron-cache/
# Electron
electron/
# Electron compat layer (generated)
src/electron/
# npm platform package stubs
npm/darwin-arm64/
# OS
+73 -9
View File
@@ -18,7 +18,7 @@ Electron bundles Chromium — ~300 MB per app with 500+ MB RSS. Gelectron uses t
| Language | C++ / Node.js | Rust / Node.js |
| API Compatibility | Native | Drop-in replacement |
| Node.js Integration | Built-in | Spawned child process or WebView-only |
| Auto Updater | Built-in | Stub (no-update-safe fallback) |
| Auto Updater | Built-in | Full (electron-updater compatible)
## Quick Start
@@ -32,14 +32,68 @@ npm install -g gelectron-core
This installs the `gelectron-core` package, which bundles the pre-built native binary and the JS compatibility layer, and automatically pulls in the platform-specific addon for your operating system and CPU architecture (see the [package on npm](https://www.npmjs.com/package/gelectron-core)).
Once installed, run any Electron app with:
Once installed, run any Electron app exactly like Electron — the `gelectron` command works out of the box (the package also exposes `gelectron-core` and `gelectron-packager`):
```bash
gelectron-core /path/to/electron-app
gelectron /path/to/electron-app
gelectron . # app in the current directory (reads package.json "main")
gelectron main.js # a bare main-process script
gelectron --version
```
### Download a native installer (double-click, no tools needed)
Every new release tag (pushed as `v*`) is pre-compiled in CI and published as
platform-native, double-clickable installers on the [Releases page]:
| Platform | Installer | What it does |
|---|---|---|
| macOS | `Gelectron-<version>-arm64.dmg` / `-x64.dmg` | Contains `Install gelectron.pkg`, which installs the runtime + compat layer **plus a private Node.js** to `/usr/local/lib/gelectron` and symlinks `gelectron` into `/usr/local/bin` |
| Windows | `Gelectron-<version>-x64.exe` / `-arm64.exe` | NSIS installer → `C:\Program Files\Gelectron` (runtime + compat + private Node.js), adds it to the system PATH, Start Menu + uninstaller |
| Linux | `Gelectron-<version>-x86_64.AppImage` / `-aarch64.AppImage` | Fully self-contained (runtime + compat + private Node.js), just run it |
After installing, `gelectron /path/to/electron-app` works from anywhere, and no
system Node.js install is required — every installer bundles its own runtime.
Apps packaged with `gelectron-packager` are equally self-contained: double-click
a packaged app and it runs with no gelectron and no Node.js installed.
> macOS installers are ad-hoc signed (no Developer ID), so on another Mac the
> first launch shows a Gatekeeper "unidentified developer" warning — right-click
> → Open to run it.
### Install from GitHub Releases (no npm)
Every new release tag (pushed as `v*`) is pre-compiled in CI and published as
a self-contained installer archive. Install the latest pre-built runtime with
a single command — no Rust toolchain, no npm:
```bash
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/mileswolfallen2/gelectron/main/scripts/install-release.sh | bash
# Windows (PowerShell)
irm https://raw.githubusercontent.com/mileswolfallen2/gelectron/main/scripts/install-release.ps1 | iex
```
The installer downloads `gelectron-<version>-<platform>-<arch>.tar.gz` (or
`.zip`) from the latest GitHub Release and puts the `gelectron` binary plus
the JS compatibility layer into `~/.local/bin` (macOS/Linux) or
`%LOCALAPPDATA%\gelectron\bin` (Windows). Customize with:
```bash
bash scripts/install-release.sh --version v0.1.1 # specific tag
bash scripts/install-release.sh --prefix ~/bin # custom location
bash scripts/install-release.sh --uninstall # remove
```
Archives can also be downloaded directly from the [Releases page] and
unpacked manually — the binary just needs the `compat/` folder next to it.
> The `gelectron-core` npm package is the recommended distribution channel. Building from source (below) is only needed if you're developing Gelectron itself or want the bleeding-edge version.
[Releases page]: https://github.com/mileswolfallen2/gelectron/releases
### Prerequisites (for building from source)
- Rust 1.75+ (`rustup.rs`)
@@ -65,7 +119,11 @@ cargo run --release -p gelectron -- /path/to/electron-app
### CLI (Node.js fallback)
If you don't want to build the Rust binary, the CLI can fall back to a pure-Node.js shim:
The `gelectron` command automatically locates the native runtime — the release
build, an npm/installer-installed binary (`PATH`, `/usr/local/bin`,
`/usr/local/lib/gelectron`, `Program Files\Gelectron`, etc.) or a fresh
`target/{release,debug}/gelectron`. If none is found it falls back to a
pure-Node.js shim:
```bash
node cli/gelectron.js /path/to/electron-app
@@ -160,14 +218,15 @@ When the native binary is not built, the CLI falls back to pure Node.js:
| Module | Status |
|---|---|
| `screen` | `getPrimaryDisplay()` (stub) |
| `clipboard` | Full API (`readText`/`writeText`/`readHTML`/`readRTF`/`readImage`/`readBookmark`/`readFindText`/`clear`/`availableFormats`/`has`) |
| `screen` | Native display enumeration (`getAllDisplays`, `getPrimaryDisplay`, `getDisplayMatching`, …) |
| `clipboard` | Full API (`readText`/`writeText`/`readHTML`/`writeHTML`/`readBookmark`/`readFindText`/`availableFormats`) |
| `nativeTheme` | `shouldUseDarkColors`, `themeSource`, `themes` (native-backed when available) |
| `systemPreferences` | Basic stubs |
| `powerMonitor` | Event stubs |
| `globalShortcut` | Register/unregister stubs |
| `session` | Cookies, protocol, permissions (stub) |
| `net` | `fetch()` proxy |
| `autoUpdater` | No-op stub (reports "no update available") |
| `autoUpdater` | Full (sha512-verified, atomic apply) |
## Demo App
@@ -230,9 +289,12 @@ gelectron/
│ ├── shell.js # Shell operations
│ ├── notification.js # Notifications
│ ├── native-image.js # Image handling
│ ├── clipboard.js # Clipboard
│ ├── screen.js # Display enumeration
│ ├── nativeTheme.js # Dark mode / theme
│ ├── safe-storage.js # Encryption
│ ├── web-contents.js # webContents utilities
│ ├── auto-updater.js # autoUpdater stub
│ ├── auto-updater.js # autoUpdater (electron-updater compatible)
│ ├── native-bridge.js # IPC to Rust binary
│ ├── preload-loader.js # Preload injection
│ └── runtime.js # Node.js fallback runtime
@@ -291,7 +353,9 @@ gelectron-packager --dir ./my-app --name MyApp --platform linux --arch x64
### What the packager does
1. Finds your built gelectron binary (`target/release/gelectron`)
1. Finds your gelectron runtime — the built binary (`target/release/gelectron`),
or any gelectron installed through the installers/npm (PATH,
`/usr/local/lib/gelectron`, `Program Files\Gelectron`, …)
2. Downloads a bundled Node.js runtime (~20 MB) for the target platform
3. Copies your app source and `node_modules`
4. Includes the Electron compatibility layer (`src/electron/`)
+51 -8
View File
@@ -116,15 +116,58 @@ if (process.env.GELECTRON_LOG) {
const rustBin = path.join(__dirname, '..', 'target', 'release', 'gelectron');
const rustBinDebug = path.join(__dirname, '..', 'target', 'debug', 'gelectron');
let executable;
if (fs.existsSync(rustBin)) {
executable = rustBin;
} else if (fs.existsSync(rustBinDebug)) {
executable = rustBinDebug;
function findNativeBinary() {
const candidates = [];
if (process.env.GELECTRON_BINARY) candidates.push(process.env.GELECTRON_BINARY);
candidates.push(rustBin, rustBinDebug);
// npm-installed layout: the binary ships alongside the CLI
candidates.push(path.join(__dirname, '..', 'bin', 'gelectron'));
const platform = process.platform;
if (platform === 'win32') {
for (const pf of [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], 'C:\\Program Files']) {
if (pf) candidates.push(path.join(pf, 'Gelectron', 'gelectron.exe'));
}
candidates.push(path.join(process.env.LOCALAPPDATA || '', 'gelectron', 'gelectron.exe'));
} else {
candidates.push('/usr/local/bin/gelectron');
candidates.push('/usr/local/lib/gelectron/gelectron');
candidates.push('/usr/bin/gelectron');
candidates.push('/opt/homebrew/bin/gelectron');
candidates.push(path.join(process.env.HOME || '', '.local', 'bin', 'gelectron'));
}
const exeSuffix = platform === 'win32' ? '.exe' : '';
for (const c of candidates) {
for (const candidate of [c, c + exeSuffix]) {
if (candidate && fs.existsSync(candidate)) return candidate;
}
}
// PATH lookup (which / where)
try {
const { execSync } = require('child_process');
const cmd = platform === 'win32' ? 'where gelectron' : 'which gelectron';
const out = execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).toString();
const first = out.split(/\r?\n/).map((s) => s.trim()).find((s) => s.length > 0);
if (first && fs.existsSync(first)) return first;
} catch (e) {
// Not on PATH
}
return null;
}
let executable = findNativeBinary();
if (executable && process.platform !== 'win32') {
try {
fs.accessSync(executable, fs.constants.X_OK);
} catch (e) {
fs.chmodSync(executable, 0o755);
}
}
if (executable) {
console.log(`[gelectron] Using native binary: ${executable}`);
if (process.env.GELECTRON_LOG) console.log(`[gelectron] Using native binary: ${executable}`);
const child = spawn(executable, args, {
env,
stdio: 'inherit',
@@ -132,8 +175,8 @@ if (executable) {
});
child.on('exit', (code) => process.exit(code || 0));
} else {
console.log(`[gelectron] Native binary not found. Run: cargo build --release -p gelectron`);
console.log(`[gelectron] Falling back to Node.js runtime...\n`);
console.log(`[gelectron] Native runtime not found. Install it or run: cargo build --release -p gelectron`);
console.log(`[gelectron] Falling back to the Node.js compatibility layer (no native window support)...\n`);
require('../src/electron/runtime.js').run(mainScript, env);
}
+71 -9
View File
@@ -1998,18 +1998,80 @@ fn handle_to_rust(
}
}
// Resolve the Node.js runtime the gelectron engine should spawn. Resolution
// order (first hit wins):
//
// 1. GELECTRON_NODE env override (explicit, e.g. from a launcher script)
// 2. Node bundled next to the gelectron binary. This is what makes packaged
// apps (MyApp.app, Windows dir, AppImage) fully self-contained: the
// packager places node / node.exe alongside the engine, so no system-wide
// Node install is required.
// 3. PATH lookup (which on Unix, where on Windows)
// 4. Known per-platform install locations (incl. the runtime installers)
fn which_node() -> Option<String> {
Command::new("which")
.arg("node")
.output()
.ok()
.and_then(|o| if o.status.success() { String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string()) } else { None })
.or_else(|| {
for p in &["/usr/local/bin/node", "/opt/homebrew/bin/node", "/usr/bin/node"] {
if std::path::Path::new(p).exists() { return Some(p.to_string()); }
let node_exe = if cfg!(windows) { "node.exe" } else { "node" };
if let Ok(override_path) = std::env::var("GELECTRON_NODE") {
if !override_path.is_empty() && std::path::Path::new(&override_path).exists() {
return Some(override_path);
}
}
// 1. Bundled runtime: node sits next to the gelectron engine binary
if let Some(exe_dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|p| p.to_path_buf())) {
for candidate in &[exe_dir.join(node_exe), exe_dir.join("bin").join(node_exe)] {
if candidate.exists() {
return Some(candidate.display().to_string());
}
}
}
// 2. PATH lookup (where works on Windows, which on Unix)
let lookup = if cfg!(windows) { "where" } else { "which" };
if let Ok(out) = Command::new(lookup).arg(node_exe).output() {
if out.status.success() {
if let Ok(s) = String::from_utf8(out.stdout) {
let first = s.lines().next().map(|l| l.trim().to_string());
if let Some(p) = first {
if !p.is_empty() && std::path::Path::new(&p).exists() {
return Some(p);
}
}
}
}
} else if let Ok(out) = Command::new("which").arg(node_exe).output() {
// Fallback: `which` may not exist on some minimal Windows shells
if out.status.success() {
if let Ok(s) = String::from_utf8(out.stdout) {
let first = s.lines().next().map(|l| l.trim().to_string());
if let Some(p) = first {
if !p.is_empty() && std::path::Path::new(&p).exists() {
return Some(p);
}
}
}
}
}
// 3. Known install locations (flat list, checked in order)
let known = [
"/usr/local/bin/node",
"/opt/homebrew/bin/node",
"/usr/bin/node",
// Gelectron runtime installers bundle node next to the engine
"/usr/local/lib/gelectron/node",
"/usr/local/lib/gelectron/node.exe",
"/usr/lib/gelectron/node",
"C:\\Program Files\\Gelectron\\node.exe",
"C:\\Program Files (x86)\\Gelectron\\node.exe",
];
for p in &known {
if std::path::Path::new(p).exists() {
return Some(p.to_string());
}
}
None
})
}
#[cfg(test)]
+3 -1
View File
@@ -4,7 +4,9 @@
"description": "Firefox-engine alternative to Electron, powered by Servo",
"main": "src/electron/index.js",
"bin": {
"gelectron-core": "./cli/gelectron.js"
"gelectron": "./cli/gelectron.js",
"gelectron-core": "./cli/gelectron.js",
"gelectron-packager": "./packager/bin/gelectron-packager.js"
},
"scripts": {
"build": "napi build --platform --release --package gelectron-core",
+63 -4
View File
@@ -73,11 +73,63 @@ function findCompatLayer(startDir) {
while (dir !== path.dirname(dir)) {
const p = path.join(dir, 'src', 'electron');
if (fs.existsSync(p)) return p;
// Some installs place the compat layer as `compat/` next to the binary
const compat = path.join(dir, 'compat', 'index.js');
if (fs.existsSync(compat)) return path.join(dir, 'compat');
dir = path.dirname(dir);
}
return null;
}
// Compat layer that lives alongside a found binary (installed runtime layout
// where binary + compat live in the same directory).
function findCompatNextToBinary(binaryPath) {
const dir = path.dirname(binaryPath);
const p = path.join(dir, 'compat', 'index.js');
if (fs.existsSync(p)) return path.join(dir, 'compat');
return null;
}
function isExecutableName(binPath) {
if (!binPath) return false;
const base = path.basename(binPath).toLowerCase();
return /^gelectron(\.exe)?$/.test(base);
}
// Locate a gelectron runtime installed through the official installers
// (DMG / EXE / AppImage) rather than built from source. This lets
// `gelectron-packager` bundle a runtime that never has to be recompiled.
function findInstalledGelectron() {
const platform = process.platform;
const candidates = [];
if (platform === 'win32') {
for (const pf of [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], 'C:\\Program Files']) {
if (pf) candidates.push(path.join(pf, 'Gelectron', 'gelectron.exe'));
}
candidates.push(path.join(process.env.LOCALAPPDATA || '', 'gelectron', 'gelectron.exe'));
} else {
candidates.push('/usr/local/bin/gelectron');
candidates.push('/usr/local/lib/gelectron/gelectron');
candidates.push('/usr/bin/gelectron');
candidates.push('/opt/homebrew/bin/gelectron');
candidates.push(path.join(process.env.HOME || '', '.local', 'bin', 'gelectron'));
candidates.push(path.join(process.env.HOME || '', '.gelectron', 'bin', 'gelectron'));
}
for (const p of candidates) {
if (p && fs.existsSync(p) && isExecutableName(p)) return p;
}
// PATH lookup: `which gelectron` / `where gelectron`
try {
const which = platform === 'win32' ? 'where' : 'which';
const out = execSync(`${which} gelectron`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
const first = String(out).split(/\r?\n/).map((s) => s.trim()).find((s) => s.length > 0 && isExecutableName(s));
if (first && fs.existsSync(first)) return first;
} catch (e) {
// Not on PATH
}
return null;
}
function findGelectronFromAncestors(appDir) {
let dir = appDir;
while (dir !== path.dirname(dir)) {
@@ -92,7 +144,8 @@ function findGelectronFromAncestors(appDir) {
}
dir = path.dirname(dir);
}
return null;
// Fall back to a system-installed gelectron runtime
return findInstalledGelectron();
}
async function getNodeBinary(platform, arch, cacheDir) {
@@ -172,6 +225,8 @@ ${iconKey} <key>CFBundleDisplayName</key>
<string>${exeName}</string>
<key>CFBundleIdentifier</key>
<string>com.gelectron.${name.toLowerCase().replace(/[^a-z0-9]/g, '')}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${name}</string>
<key>CFBundlePackageType</key>
@@ -180,6 +235,10 @@ ${iconKey} <key>CFBundleDisplayName</key>
<string>${version}</string>
<key>CFBundleVersion</key>
<string>${version}</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSRequiresAquaSystemAppearance</key>
@@ -452,7 +511,7 @@ async function packageMac(appDir, outDir, name, version, gelectronBin, nodeDir,
}
// Copy src/electron compat layer
const compatDir = findCompatLayer(appDir) || findCompatLayer(path.dirname(gelectronBin));
const compatDir = findCompatLayer(appDir) || findCompatNextToBinary(gelectronBin) || findCompatLayer(path.dirname(gelectronBin));
if (compatDir) {
copyDirSync(compatDir, path.join(macosDir, 'compat'));
}
@@ -515,7 +574,7 @@ async function packageWindows(appDir, outDir, name, version, gelectronBin, nodeD
}
// Copy compat layer
const compatDirWin = findCompatLayer(appDir) || findCompatLayer(path.dirname(gelectronBin));
const compatDirWin = findCompatLayer(appDir) || findCompatNextToBinary(gelectronBin) || findCompatLayer(path.dirname(gelectronBin));
if (compatDirWin) {
copyDirSync(compatDirWin, path.join(outDir, 'compat'));
}
@@ -592,7 +651,7 @@ async function packageLinux(appDir, outDir, name, version, gelectronBin, nodeDir
}
// Copy compat layer
const compatDirLinux = findCompatLayer(appDir) || findCompatLayer(path.dirname(gelectronBin));
const compatDirLinux = findCompatLayer(appDir) || findCompatNextToBinary(gelectronBin) || findCompatLayer(path.dirname(gelectronBin));
if (compatDirLinux) {
copyDirSync(compatDirLinux, path.join(outDir, 'compat'));
}
+119
View File
@@ -0,0 +1,119 @@
# gelectron installer (PowerShell / Windows)
#
# Downloads the pre-built gelectron binary + Electron compatibility layer from
# GitHub Releases and installs it into PREFIX so `gelectron <app>` works from
# anywhere. No Rust toolchain or npm required.
#
# Install layout:
# <PREFIX>/gelectron.exe native binary
# <PREFIX>/compat/*.js Electron API compat layer
#
# Usage:
# .\scripts\install-release.ps1 latest release
# .\scripts\install-release.ps1 -Version v0.1.1
# .\scripts\install-release.ps1 -File C:\path\to\gelectron-0.1.1-win32-x64.zip
# .\scripts\install-release.ps1 -Prefix "$HOME\bin"
# .\scripts\install-release.ps1 -Uninstall
param(
[string]$Repo = "mileswolfallen2/gelectron",
[string]$Prefix = "",
[string]$Version = "",
[string]$File = "",
[switch]$Uninstall,
[switch]$Help
)
$ErrorActionPreference = "Stop"
function Show-Help {
@"
gelectron installer - install pre-built gelectron from GitHub Releases
Usage:
install-release.ps1 [options]
Options:
-Repo owner/repo GitHub repo (default: mileswolfallen2/gelectron)
-Prefix DIR Install directory (default: %LOCALAPPDATA%\gelectron\bin)
-Version TAG Install a specific release tag (default: latest)
-File PATH Install from a local archive instead of downloading
-Uninstall Remove previously installed files
-Help Show this help
"@ | Write-Host
}
if ($Help) { Show-Help; exit 0 }
if (-not $Prefix) { $Prefix = Join-Path $env:LOCALAPPDATA "gelectron\bin" }
# ── Detect platform / arch ──────────────────────────────────────────────────
$Platform = "win32"
switch ($env:PROCESSOR_ARCHITECTURE) {
"ARM64" { $Arch = "arm64" }
default { $Arch = "x64" }
}
$ArchiveExt = "zip"
# ── Uninstall ───────────────────────────────────────────────────────────────
if ($Uninstall) {
foreach ($p in @((Join-Path $Prefix "gelectron.exe"), (Join-Path $Prefix "compat"))) {
if (Test-Path $p) { Remove-Item -Recurse -Force $p }
}
Write-Host "Removed $Prefix\gelectron.exe and $Prefix\compat"
exit 0
}
# ── Resolve version + archive ───────────────────────────────────────────────
$ArchiveFile = $File
if (-not $ArchiveFile) {
if ($Version) {
$Tag = "v" + ($Version.TrimStart("v"))
} else {
Write-Host "==> Resolving latest release from $Repo..."
$latest = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -Headers @{ "User-Agent" = "gelectron-installer" }
$Tag = $latest.tag_name
}
$Ver = $Tag.TrimStart("v")
$Asset = "gelectron-$Ver-$Platform-$Arch.$ArchiveExt"
$Url = "https://github.com/$Repo/releases/download/$Tag/$Asset"
Write-Host "==> Downloading $Asset ..."
$ArchiveFile = Join-Path $env:TEMP "gelectron-install-$([Guid]::NewGuid().ToString('N')).$ArchiveExt"
Invoke-WebRequest -Uri $Url -OutFile $ArchiveFile -UseBasicParsing
}
# ── Extract ─────────────────────────────────────────────────────────────────
$Stage = Join-Path $env:TEMP ("gelectron-install-stage-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $Stage | Out-Null
try {
Write-Host "==> Extracting archive..."
Expand-Archive -Path $ArchiveFile -DestinationPath $Stage -Force
$Bin = Join-Path $Stage "gelectron.exe"
if (-not (Test-Path $Bin)) {
throw "archive does not contain gelectron.exe"
}
# ── Install ─────────────────────────────────────────────────────────────
Write-Host "==> Installing to $Prefix"
New-Item -ItemType Directory -Path $Prefix -Force | Out-Null
Copy-Item $Bin (Join-Path $Prefix "gelectron.exe") -Force
if (Test-Path (Join-Path $Stage "compat")) {
Copy-Item -Recurse -Force (Join-Path $Stage "compat") $Prefix
}
Write-Host ""
Write-Host " OK Installed: $Prefix\gelectron.exe"
Write-Host " OK Installed: $Prefix\compat\"
Write-Host ""
Write-Host " Add $Prefix to your PATH, then run:"
Write-Host ' gelectron C:\path\to\electron-app'
}
finally {
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
if (-not $File) { Remove-Item -Force $ArchiveFile -ErrorAction SilentlyContinue }
}
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env bash
#
# gelectron installer
#
# Downloads the pre-built gelectron binary + Electron compatibility layer from
# GitHub Releases and installs it into PREFIX so `gelectron <app>` works from
# anywhere. No Rust toolchain or Node dependency manager required.
#
# Install layout:
# <PREFIX>/gelectron native binary
# <PREFIX>/compat/*.js Electron API compat layer
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/mileswolfallen2/gelectron/main/scripts/install-release.sh | bash
# ./scripts/install-release.sh latest release
# ./scripts/install-release.sh --version v0.1.1
# ./scripts/install-release.sh --file /path/to/gelectron-0.1.1-darwin-arm64.tar.gz
# PREFIX=~/bin ./scripts/install-release.sh custom prefix
# ./scripts/install-release.sh --uninstall remove installed files
set -euo pipefail
DEFAULT_REPO="mileswolfallen2/gelectron"
DEFAULT_PREFIX="${HOME}/.local/bin"
REPO="$DEFAULT_REPO"
PREFIX="${PREFIX:-$DEFAULT_PREFIX}"
VERSION=""
FILE=""
UNINSTALL=0
usage() {
cat <<'EOF'
gelectron installer — install pre-built gelectron from GitHub Releases
Usage:
install-release.sh [options]
Options:
--repo owner/repo GitHub repo to fetch releases from (default: mileswolfallen2/gelectron)
--prefix DIR Install directory (env: PREFIX, default: ~/.local/bin)
--version TAG Install a specific release tag (default: latest)
--file PATH Install from a local release archive instead of downloading
--uninstall Remove previously installed files
-h, --help Show this help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--repo) REPO="${2:-}"; shift 2 ;;
--prefix) PREFIX="${2:-}"; shift 2 ;;
--version) VERSION="${2:-}"; shift 2 ;;
--file) FILE="${2:-}"; shift 2 ;;
--uninstall) UNINSTALL=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown option: $1" >&2; usage; exit 1 ;;
esac
done
# ── Detect platform / arch ──────────────────────────────────────────────────
case "$(uname -s)" in
Darwin) PLATFORM="darwin" ;;
MINGW*|MSYS*|CYGWIN*) PLATFORM="win32" ;;
Linux) PLATFORM="linux" ;;
*) echo "error: unsupported platform: $(uname -s)" >&2; exit 1 ;;
esac
case "$(uname -m)" in
arm64|aarch64) ARCH="arm64" ;;
x86_64|amd64) ARCH="x64" ;;
*) echo "error: unsupported arch: $(uname -m)" >&2; exit 1 ;;
esac
[[ "$PLATFORM" == "win32" ]] && ARCHIVE_EXT="zip" || ARCHIVE_EXT="tar.gz"
# ── Uninstall ───────────────────────────────────────────────────────────────
if [[ "$UNINSTALL" == "1" ]]; then
rm -f "$PREFIX/gelectron"
rm -f "$PREFIX/gelectron.exe"
rm -f "$PREFIX/node"
rm -f "$PREFIX/node.exe"
rm -rf "$PREFIX/compat"
echo "Removed $PREFIX/gelectron, $PREFIX/node and $PREFIX/compat"
exit 0
fi
# ── Resolve version + archive ───────────────────────────────────────────────
if [[ -n "$FILE" ]]; then
ARCHIVE_FILE="$FILE"
else
if [[ -n "$VERSION" ]]; then
TAG="${VERSION#v}"
TAG="v${TAG}"
else
echo "==> Resolving latest release from $REPO..."
if ! command -v curl >/dev/null 2>&1; then
echo "error: curl is required" >&2
exit 1
fi
API_URL="https://api.github.com/repos/$REPO/releases/latest"
TAG="$(curl -fsSL "$API_URL" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1)"
if [[ -z "$TAG" ]]; then
echo "error: could not resolve latest release from $API_URL" >&2
exit 1
fi
fi
VER="${TAG#v}"
ASSET="gelectron-$VER-$PLATFORM-$ARCH.$ARCHIVE_EXT"
URL="https://github.com/$REPO/releases/download/$TAG/$ASSET"
echo "==> Downloading $ASSET ..."
ARCHIVE_FILE="$(mktemp /tmp/gelectron-install.XXXXXX.$ARCHIVE_EXT)"
trap 'rm -f "$ARCHIVE_FILE"' EXIT
curl -fsSL -L "$URL" -o "$ARCHIVE_FILE"
fi
# ── Extract ─────────────────────────────────────────────────────────────────
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-install-stage.XXXXXX")"
trap 'rm -rf "$STAGE" "${ARCHIVE_FILE:-}"' EXIT
echo "==> Extracting archive..."
case "$ARCHIVE_EXT" in
zip) unzip -qo "$ARCHIVE_FILE" -d "$STAGE" ;;
*) tar -xzf "$ARCHIVE_FILE" -C "$STAGE" ;;
esac
BIN_ABS="$STAGE/gelectron"
[[ -f "$STAGE/gelectron.exe" ]] && BIN_ABS="$STAGE/gelectron.exe"
if [[ ! -f "$BIN_ABS" ]]; then
echo "error: archive does not contain gelectron" >&2
ls -la "$STAGE" >&2
exit 1
fi
# ── Install ─────────────────────────────────────────────────────────────────
echo "==> Installing to $PREFIX"
mkdir -p "$PREFIX"
install -m 755 "$BIN_ABS" "$PREFIX/$(basename "$BIN_ABS")"
if [[ -d "$STAGE/compat" ]]; then
mkdir -p "$PREFIX/compat"
install -m 644 "$STAGE"/compat/*.js "$PREFIX/compat/"
fi
# Download a private Node.js runtime next to the binary so `gelectron <app>`
# works on machines that don't have Node installed. The binary's runtime
# resolution checks its own directory first, so this private copy wins.
NODE_VERSION="20.18.1"
NODE_INSTALLED="$PREFIX/node"
if [[ "$PLATFORM" == "win32" ]]; then NODE_INSTALLED="$PREFIX/node.exe"; fi
if [[ ! -x "$NODE_INSTALLED" ]]; then
echo "==> Installing private Node.js v$NODE_VERSION ..."
case "$PLATFORM-$ARCH" in
darwin-*) NODE_PLATFORM="darwin-$ARCH"; NODE_EXT="tar.gz" ;;
linux-*) NODE_PLATFORM="linux-$ARCH"; NODE_EXT="tar.xz" ;;
win32-*) NODE_PLATFORM="win-$ARCH"; NODE_EXT="zip" ;;
esac
NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-${NODE_PLATFORM}.${NODE_EXT}"
NODE_ARCHIVE="$(mktemp /tmp/gelectron-node.XXXXXX.${NODE_EXT})"
curl -fsSL -L "$NODE_URL" -o "$NODE_ARCHIVE"
NODE_STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-node-stage.XXXXXX")"
case "$NODE_EXT" in
zip) unzip -qo "$NODE_ARCHIVE" -d "$NODE_STAGE" ;;
tar.gz) tar -xzf "$NODE_ARCHIVE" -C "$NODE_STAGE" ;;
tar.xz) tar -xJf "$NODE_ARCHIVE" -C "$NODE_STAGE" ;;
esac
NODE_BIN="$(find "$NODE_STAGE" -type f \( -name 'node.exe' -o -name 'node' \) | head -n1)"
if [[ -n "$NODE_BIN" ]]; then
install -m 755 "$NODE_BIN" "$NODE_INSTALLED"
else
echo " warning: could not install private Node.js — apps will require a system Node" >&2
fi
rm -rf "$NODE_STAGE" "$NODE_ARCHIVE"
fi
# Downloaded-from-GitHub binaries carry a quarantine attribute on macOS and
# Gatekeeper will block the first launch. Clear it — the binary is ad-hoc
# signed, so this is the only thing standing in the way.
if [[ "$PLATFORM" == "darwin" ]]; then
xattr -dr com.apple.quarantine "$PREFIX/$(basename "$BIN_ABS")" 2>/dev/null || true
fi
echo
echo " ✓ Installed: $PREFIX/$(basename "$BIN_ABS")"
echo " ✓ Installed: $PREFIX/compat/"
if ! command -v gelectron >/dev/null 2>&1 || [[ "$(command -v gelectron)" != "$PREFIX/gelectron" ]]; then
echo
echo " warning: $PREFIX is not on your PATH."
case "$SHELL" in
*zsh) RC="$HOME/.zshrc" ;;
*bash) RC="$HOME/.bashrc" ;;
*) RC="$HOME/.profile" ;;
esac
echo " Add this line to $RC:"
echo " export PATH=\"$PREFIX:\$PATH\""
fi
echo
"$PREFIX/$(basename "$BIN_ABS")" --version >/dev/null 2>&1 && \
echo " Run: gelectron /path/to/electron-app" || \
echo " Hint: $PREFIX/$(basename "$BIN_ABS") --version"
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env bash
#
# gelectron release builder
#
# Pre-compiles the gelectron native binary and bundles it with the Electron
# compatibility layer (src/electron/*.js) into a single, self-contained
# installer archive per platform/arch:
#
# gelectron-<version>-darwin-arm64.tar.gz
# gelectron-<version>-darwin-x64.tar.gz
# gelectron-<version>-win32-arm64.zip
# gelectron-<version>-win32-x64.zip
# gelectron-<version>-linux-arm64.tar.gz
# gelectron-<version>-linux-x64.tar.gz
#
# Archive layout (the install scripts unpack this into PREFIX):
# gelectron | gelectron.exe native binary
# compat/*.js Electron API compatibility layer
#
# This script is what the GitHub Actions workflow runs on every new release
# tag; it can also be run locally for testing or manual distributions.
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VERSION=""
PLATFORM=""
ARCH=""
BINARY=""
OUT_DIR="$REPO_DIR/dist"
BUILD=1
usage() {
cat <<'EOF'
gelectron release builder
Usage:
scripts/make-release.sh [options]
Options:
-v, --version VER Release version (default: from package.json, minus v)
-p, --platform P Target OS: darwin, win32, linux (default: current)
-a, --arch A Target arch: x64, arm64 (default: current)
-b, --binary PATH Use an already-built gelectron binary (skips cargo build)
-o, --out DIR Output directory (default: <repo>/dist)
-n, --no-build Do not compile (requires --binary or an existing build)
-h, --help Show this help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--version) VERSION="${2:-}"; shift 2 ;;
-p|--platform) PLATFORM="${2:-}"; shift 2 ;;
-a|--arch) ARCH="${2:-}"; shift 2 ;;
-b|--binary) BINARY="${2:-}"; shift 2 ;;
-o|--out) OUT_DIR="${2:-}"; shift 2 ;;
-n|--no-build) BUILD=0; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown option: $1" >&2; usage; exit 1 ;;
esac
done
# ── Resolve defaults ────────────────────────────────────────────────────────
if [[ -z "$VERSION" ]]; then
VERSION="$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo '0.0.0')"
fi
VERSION="${VERSION#v}"
if [[ -z "$PLATFORM" ]]; then
case "$(uname -s)" in
Darwin) PLATFORM="darwin" ;;
MINGW*|MSYS*|CYGWIN*) PLATFORM="win32" ;;
Linux) PLATFORM="linux" ;;
*) echo "error: cannot detect platform" >&2; exit 1 ;;
esac
fi
if [[ -z "$ARCH" ]]; then
case "$(uname -m)" in
arm64|aarch64) ARCH="arm64" ;;
x86_64|amd64) ARCH="x64" ;;
*) echo "error: cannot detect arch" >&2; exit 1 ;;
esac
fi
case "$PLATFORM-$ARCH" in
darwin-arm64|darwin-x64|win32-arm64|win32-x64|linux-arm64|linux-x64) ;;
*) echo "error: unsupported platform/arch: $PLATFORM/$ARCH" >&2; exit 1 ;;
esac
EXE_SUFFIX=""
[[ "$PLATFORM" == "win32" ]] && EXE_SUFFIX=".exe"
[[ "$PLATFORM" == "win32" ]] && ARCHIVE_EXT="zip" || ARCHIVE_EXT="tar.gz"
log() { echo "==> $*"; }
# ── Locate or build the gelectron binary ────────────────────────────────────
BIN="$BINARY"
if [[ -z "$BIN" && "$BUILD" == "1" ]]; then
CANDIDATES=(
"$REPO_DIR/target/release/gelectron$EXE_SUFFIX"
"$REPO_DIR/target/debug/gelectron$EXE_SUFFIX"
)
for c in "${CANDIDATES[@]}"; do
if [[ -f "$c" ]]; then BIN="$c"; break; fi
done
fi
if [[ -z "$BIN" && "$BUILD" == "1" ]]; then
log "Building gelectron (release)..."
cargo build --release --manifest-path "$REPO_DIR/Cargo.toml" -p gelectron
BIN="$REPO_DIR/target/release/gelectron$EXE_SUFFIX"
fi
# A --binary path may omit the platform extension (e.g. CI passes the target
# path before the .exe suffix is appended).
if [[ -n "$BIN" && ! -f "$BIN" && -n "$EXE_SUFFIX" && -f "$BIN$EXE_SUFFIX" ]]; then
BIN="$BIN$EXE_SUFFIX"
fi
if [[ -z "$BIN" || ! -f "$BIN" ]]; then
echo "error: gelectron binary not found. Build it first (cargo build --release -p gelectron) or pass --binary." >&2
exit 1
fi
BIN="$(cd "$(dirname "$BIN")" && pwd)/$(basename "$BIN")"
# ── Stage the payload ───────────────────────────────────────────────────────
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-release.XXXXXX")"
trap 'rm -rf "$STAGE"' EXIT
PAYLOAD="$STAGE/payload"
mkdir -p "$PAYLOAD"
log "Copying binary ($(basename "$BIN"))..."
install -m 755 "$BIN" "$PAYLOAD/gelectron$EXE_SUFFIX"
COMPAT_SRC="$REPO_DIR/src/electron"
if [[ ! -d "$COMPAT_SRC" || -z "$(ls "$COMPAT_SRC"/*.js 2>/dev/null)" ]]; then
echo "error: compat layer not found in $COMPAT_SRC" >&2
exit 1
fi
log "Copying compat layer..."
mkdir -p "$PAYLOAD/compat"
install -m 644 "$COMPAT_SRC"/*.js "$PAYLOAD/compat/"
# Ad-hoc sign so the binary actually runs on arm64 Macs (unsigned binaries
# are killed by the kernel). No certificate required.
if [[ "$PLATFORM" == "darwin" ]]; then
if command -v codesign >/dev/null 2>&1; then
log "Ad-hoc signing binary..."
codesign --force --sign - "$PAYLOAD/gelectron" 2>/dev/null || echo " (warning: codesign failed)"
fi
fi
# ── Archive ─────────────────────────────────────────────────────────────────
mkdir -p "$OUT_DIR"
ARCHIVE_NAME="gelectron-$VERSION-$PLATFORM-$ARCH.$ARCHIVE_EXT"
ARCHIVE_PATH="$OUT_DIR/$ARCHIVE_NAME"
if [[ "$ARCHIVE_EXT" == "zip" ]]; then
log "Creating $ARCHIVE_NAME..."
if command -v powershell >/dev/null 2>&1; then
powershell -NoProfile -Command "Compress-Archive -Path '$PAYLOAD/*' -DestinationPath '$ARCHIVE_PATH' -Force"
else
(cd "$PAYLOAD" && zip -qr "$ARCHIVE_PATH" .)
fi
else
log "Creating $ARCHIVE_NAME..."
(cd "$PAYLOAD" && tar -czf "$ARCHIVE_PATH" .)
fi
echo
log "Created installer: $ARCHIVE_PATH"
sha256sum "$ARCHIVE_PATH" 2>/dev/null || shasum -a 256 "$ARCHIVE_PATH" 2>/dev/null || true
+92
View File
@@ -0,0 +1,92 @@
; Gelectron Windows installer (NSIS)
;
; Build with makensis from a staging directory containing:
; gelectron.exe
; compat\*.js
;
; makensis /DVERSION=0.1.1 /DARCH=x64 Gelectron.nsi
;
; Installs to $PROGRAMFILES64\Gelectron, adds it to the machine PATH, creates
; Start Menu + Add/Remove Programs entries, and writes an uninstaller.
!include "MUI2.nsh"
!include "WordFunc.nsh"
!ifndef VERSION
!define VERSION "0.0.0"
!endif
!ifndef ARCH
!define ARCH "x64"
!endif
!define APPNAME "Gelectron"
!define COMPANY "gelectron"
!define ENV_KEY 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"'
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
Name "${APPNAME}"
OutFile "Gelectron-${VERSION}-${ARCH}.exe"
InstallDir "$PROGRAMFILES64\${APPNAME}"
InstallDirRegKey HKLM "Software\${APPNAME}" "InstallDir"
RequestExecutionLevel admin
; Modern UI
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
; Install section
Section "Install"
SetShellVarContext all
SetOutPath "$INSTDIR"
File "gelectron.exe"
; Bundled Node.js runtime so packaged apps run with no system Node install
File "node.exe"
File /r "compat"
WriteUninstaller "$INSTDIR\Uninstall.exe"
CreateDirectory "$SMPROGRAMS\${APPNAME}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME} CLI.lnk" "$INSTDIR\gelectron.exe"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${APPNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${VERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${COMPANY}"
WriteRegStr HKLM "${UNINST_KEY}" "InstallLocation" "$INSTDIR"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\gelectron.exe"
; Add $INSTDIR to the machine PATH (add-if-not-present via WordFunc)
ReadRegStr $0 ${ENV_KEY} "Path"
${WordAdd} $0 ";" "+$INSTDIR" $1
WriteRegExpandStr ${ENV_KEY} "Path" $1
SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment"
SectionEnd
; Uninstall section
Section "Uninstall"
SetShellVarContext all
; Remove $INSTDIR from the machine PATH
ReadRegStr $0 ${ENV_KEY} "Path"
${WordAdd} $0 ";" "-$INSTDIR" $1
WriteRegExpandStr ${ENV_KEY} "Path" $1
SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment"
Delete "$SMPROGRAMS\${APPNAME}\${APPNAME} CLI.lnk"
RMDir "$SMPROGRAMS\${APPNAME}"
DeleteRegKey HKLM "${UNINST_KEY}"
DeleteRegKey HKLM "Software\${APPNAME}"
RMDir /r "$INSTDIR"
SectionEnd
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env bash
#
# Build the Linux AppImage: bundles the gelectron binary, the compat layer
# and a Node.js runtime into a single self-contained, downloadable AppImage.
#
# Gelectron-<version>-x86_64.AppImage
# Gelectron-<version>-aarch64.AppImage
#
# Requires: the gelectron binary (built against webkit2gtk-4.1), compat layer,
# and network access (downloads Node.js + appimagetool).
#
# Usage:
# scripts/pkg/make-appimage.sh -v VERSION [--binary PATH] [--compat DIR] [-o DIR] [-a ARCH]
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
NODE_VERSION="20.18.1"
VERSION=""
BINARY=""
COMPAT="$REPO_DIR/src/electron"
OUT_DIR="$REPO_DIR/dist"
ARCH="x64"
usage() {
cat <<'EOF'
Linux AppImage builder
Usage:
scripts/pkg/make-appimage.sh -v VERSION [--binary PATH] [--compat DIR] [-o DIR] [-a ARCH]
Options:
-v, --version VER Version string (e.g. 0.1.1)
-b, --binary PATH Path to the gelectron binary
-c, --compat DIR Path to the compat layer (default: <repo>/src/electron)
-o, --out DIR Output directory (default: <repo>/dist)
-a, --arch ARCH x64 | arm64 (default: x64)
-h, --help Show this help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--version) VERSION="${2:-}"; shift 2 ;;
-b|--binary) BINARY="${2:-}"; shift 2 ;;
-c|--compat) COMPAT="${2:-}"; shift 2 ;;
-o|--out) OUT_DIR="${2:-}"; shift 2 ;;
-a|--arch) ARCH="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown option: $1" >&2; usage; exit 1 ;;
esac
done
VERSION="${VERSION#v}"
[[ -n "$VERSION" ]] || { echo "error: --version required" >&2; exit 1; }
case "$ARCH" in x64|arm64) ;; *) echo "error: unsupported arch: $ARCH" >&2; exit 1 ;; esac
if [[ -z "$BINARY" ]]; then
BINARY="$REPO_DIR/target/release/gelectron"
fi
[[ -f "$BINARY" ]] || { echo "error: binary not found: $BINARY" >&2; exit 1; }
[[ -d "$COMPAT" ]] || { echo "error: compat dir not found: $COMPAT" >&2; exit 1; }
# Map to appimg / node arch naming
if [[ "$ARCH" == "arm64" ]]; then
NODE_ARCH="arm64"
TARGET_ARCH="aarch64"
else
NODE_ARCH="x64"
TARGET_ARCH="x86_64"
fi
log() { echo "==> $*"; }
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-appimage.XXXXXX")"
trap 'rm -rf "$STAGE"' EXIT
APPDIR="$STAGE/Gelectron.AppDir"
mkdir -p "$APPDIR/usr/bin"
# ── Binaries ────────────────────────────────────────────────────────────────
log "Staging binary..."
install -m 755 "$BINARY" "$APPDIR/usr/bin/gelectron"
log "Staging compat layer (usr/bin/compat, resolved next to the binary)..."
mkdir -p "$APPDIR/usr/bin/compat"
install -m 644 "$COMPAT"/*.js "$APPDIR/usr/bin/compat/"
# Node.js runtime (gelectron's Node mode requires node on PATH)
NODE_DIR="$STAGE/node"
if [[ ! -d "$NODE_DIR" ]]; then
log "Downloading Node.js v$NODE_VERSION ($NODE_ARCH)..."
mkdir -p "$NODE_DIR"
NODE_ARCHIVE="$STAGE/node.tar.xz"
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" -o "$NODE_ARCHIVE"
tar -xJf "$NODE_ARCHIVE" -C "$NODE_DIR"
fi
install -m 755 "$NODE_DIR"/node-v${NODE_VERSION}-linux-${NODE_ARCH}/bin/node "$APPDIR/usr/bin/node"
mkdir -p "$APPDIR/usr/lib"
cp "$NODE_DIR"/node-v${NODE_VERSION}-linux-${NODE_ARCH}/lib/libnode.so* "$APPDIR/usr/lib/" 2>/dev/null || true
# ── AppImage metadata ───────────────────────────────────────────────────────
cat > "$APPDIR/AppRun" <<'EOF'
#!/bin/sh
SELF="$(dirname "$(readlink -f "$0")")"
export PATH="$SELF/usr/bin:$PATH"
export LD_LIBRARY_PATH="$SELF/usr/lib:${LD_LIBRARY_PATH:-}"
export GELECTRON_NATIVE=1
exec "$SELF/usr/bin/gelectron" "$@"
EOF
chmod +x "$APPDIR/AppRun"
cat > "$APPDIR/gelectron.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=Gelectron
Comment=Firefox-engine alternative to Electron, powered by Servo
Exec=gelectron
Icon=gelectron
Terminal=false
Categories=Development;
EOF
if [[ -f "$REPO_DIR/logo.png" ]]; then
install -m 644 "$REPO_DIR/logo.png" "$APPDIR/gelectron.png"
fi
# ── appimagetool ────────────────────────────────────────────────────────────
TOOL="$STAGE/appimagetool"
if [[ ! -f "$TOOL" ]]; then
log "Downloading appimagetool ($TARGET_ARCH)..."
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${TARGET_ARCH}.AppImage" -o "$TOOL"
fi
chmod +x "$TOOL"
# ── Build ───────────────────────────────────────────────────────────────────
mkdir -p "$OUT_DIR"
APPIMAGE_NAME="Gelectron-$VERSION-$TARGET_ARCH.AppImage"
APPIMAGE_PATH="$OUT_DIR/$APPIMAGE_NAME"
export VERSION="$VERSION"
export ARCH="$TARGET_ARCH"
# Run appimagetool without FUSE (Ubuntu 24.04+ runners don't ship libfuse2)
export APPIMAGE_EXTRACT_AND_RUN=1
log "Creating $APPIMAGE_NAME ..."
"$TOOL" "$APPDIR" "$APPIMAGE_PATH" >/dev/null
echo
log "Created installer: $APPIMAGE_PATH"
sha256sum "$APPIMAGE_PATH" 2>/dev/null || shasum -a 256 "$APPIMAGE_PATH" 2>/dev/null || true
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
#
# Build the macOS installer: a .pkg wrapped in a .dmg for distribution.
#
# The package is fully self-contained — it installs the gelectron runtime, the
# Electron compatibility layer AND a private Node.js runtime into
# /usr/local/lib/gelectron and symlinks gelectron into /usr/local/bin. Apps run
# with `gelectron <app>` with no other runtime installed on the machine.
#
# Gelectron-<version>-<arch>.dmg
# └── Install gelectron.pkg (double-click, runs macOS Installer as root)
#
# Requires macOS (pkgbuild + hdiutil). Ad-hoc signing only — no Developer ID
# certificate, so first launch shows a Gatekeeper warning on other machines.
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
VERSION=""
ARCH=""
BINARY=""
COMPAT="$REPO_DIR/src/electron"
OUT_DIR="$REPO_DIR/dist"
NODE_VERSION="20.18.1"
usage() {
cat <<'EOF'
macOS installer builder (pkg inside DMG)
Usage:
scripts/pkg/make-dmg.sh -v VERSION -a ARCH [--binary PATH] [--compat DIR] [-o DIR]
Options:
-v, --version VER Version string (e.g. 0.1.1)
-a, --arch A arm64 | x64
-b, --binary PATH Path to the gelectron binary
-c, --compat DIR Path to the compat layer (default: <repo>/src/electron)
-o, --out DIR Output directory (default: <repo>/dist)
-h, --help Show this help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--version) VERSION="${2:-}"; shift 2 ;;
-a|--arch) ARCH="${2:-}"; shift 2 ;;
-b|--binary) BINARY="${2:-}"; shift 2 ;;
-c|--compat) COMPAT="${2:-}"; shift 2 ;;
-o|--out) OUT_DIR="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown option: $1" >&2; usage; exit 1 ;;
esac
done
[[ -n "$VERSION" ]] || { echo "error: --version required" >&2; exit 1; }
VERSION="${VERSION#v}"
[[ -n "$ARCH" ]] || { echo "error: --arch required" >&2; exit 1; }
case "$ARCH" in arm64|x64) ;; *) echo "error: unsupported arch: $ARCH" >&2; exit 1 ;; esac
if [[ -z "$BINARY" ]]; then
BINARY="$REPO_DIR/target/release/gelectron"
fi
[[ -f "$BINARY" ]] || { echo "error: binary not found: $BINARY" >&2; exit 1; }
[[ -d "$COMPAT" ]] || { echo "error: compat dir not found: $COMPAT" >&2; exit 1; }
log() { echo "==> $*"; }
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gelectron-dmg.XXXXXX")"
trap 'rm -rf "$STAGE"' EXIT
# ── Payload root: what pkgbuild installs into /usr/local/lib/gelectron ─────
PAYLOAD="$STAGE/payload/usr/local/lib/gelectron"
mkdir -p "$PAYLOAD"
log "Staging payload..."
install -m 755 "$BINARY" "$PAYLOAD/gelectron"
mkdir -p "$PAYLOAD/compat"
install -m 644 "$COMPAT"/*.js "$PAYLOAD/compat/"
# Private Node.js runtime so the installed CLI works with no system Node
NODE_ARCH="$( [[ "$ARCH" == "arm64" ]] && echo arm64 || echo x64 )"
NODE_ARCHIVE="$STAGE/node.tar.gz"
if [[ ! -f "$NODE_ARCHIVE" ]]; then
log "Downloading Node.js v$NODE_VERSION..."
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-${NODE_ARCH}.tar.gz" -o "$NODE_ARCHIVE"
fi
log "Extracting Node.js..."
mkdir -p "$STAGE/node"
tar -xzf "$NODE_ARCHIVE" -C "$STAGE/node"
install -m 755 "$STAGE/node"/node-v${NODE_VERSION}-darwin-${NODE_ARCH}/bin/node "$PAYLOAD/node"
if command -v codesign >/dev/null 2>&1; then
log "Ad-hoc signing binary..."
codesign --force --sign - "$PAYLOAD/gelectron" 2>/dev/null || echo " (warning: codesign failed)"
fi
# ── Postinstall script: symlink /usr/local/bin/gelectron → runtime ─────────
SCRIPTS="$STAGE/scripts"
mkdir -p "$SCRIPTS"
cat > "$SCRIPTS/postinstall" <<'EOF'
#!/bin/bash
set -e
ln -sf /usr/local/lib/gelectron/gelectron /usr/local/bin/gelectron
exit 0
EOF
chmod +x "$SCRIPTS/postinstall"
# ── Component package ───────────────────────────────────────────────────────
PKG="$STAGE/Install gelectron.pkg"
log "Building package (installs to /usr/local/lib/gelectron)..."
pkgbuild \
--root "$STAGE/payload" \
--scripts "$SCRIPTS" \
--identifier "com.gelectron.runtime.$ARCH" \
--version "$VERSION" \
--ownership recommended \
--install-location / \
"$PKG"
# ── DMG ────────────────────────────────────────────────────────────────────
mkdir -p "$OUT_DIR"
DMG_NAME="Gelectron-$VERSION-$ARCH.dmg"
DMG_PATH="$OUT_DIR/$DMG_NAME"
DMG_STAGE="$STAGE/dmg"
mkdir -p "$DMG_STAGE"
cp "$PKG" "$DMG_STAGE/"
log "Creating $DMG_NAME ..."
hdiutil create \
-volname "Gelectron $VERSION" \
-srcfolder "$DMG_STAGE" \
-format UDZO \
-ov \
"$DMG_PATH" >/dev/null
echo
log "Created installer: $DMG_PATH"
shasum -a 256 "$DMG_PATH"
+73
View File
@@ -0,0 +1,73 @@
# Gelectron Windows installer builder (NSIS)
#
# Stages the payload (gelectron runtime + bundled Node.js + compat layer) and
# compiles the installer EXE with makensis:
# Gelectron-<version>-<arch>.exe
#
# Usage:
# powershell -File scripts/pkg/make-exe.ps1 `
# -Bin target\release\gelectron.exe `
# -Compat src\electron `
# -Version 0.1.1 -Arch x64 -Out dist
#
# Requires makensis on PATH (install via: choco install nsis -y)
param(
[Parameter(Mandatory = $true)][string]$Bin,
[Parameter(Mandatory = $true)][string]$Compat,
[Parameter(Mandatory = $true)][string]$Version,
[string]$Arch = "x64",
[string]$Out = "dist",
[string]$NsiPath = "scripts/pkg/gelectron.nsi",
[string]$NodeVersion = "20.18.1"
)
$ErrorActionPreference = "Stop"
if (-not (Get-Command makensis -ErrorAction SilentlyContinue)) {
throw "makensis not found. Install NSIS first (choco install nsis -y)."
}
$RealityVersion = $Version.TrimStart("v")
$Stage = Join-Path $env:TEMP ("gelectron-nsis-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $Stage -Force | Out-Null
try {
Copy-Item $Bin (Join-Path $Stage "gelectron.exe") -Force
Copy-Item -Recurse -Force $Compat (Join-Path $Stage "compat")
Copy-Item $NsiPath (Join-Path $Stage "gelectron.nsi") -Force
# Bundled Node.js runtime so the installed CLI works with no system Node.
$NodeArch = if ($Arch -eq "arm64") { "arm64" } else { "x64" }
$NodeArchive = Join-Path $Stage "node.zip"
$NodeUrl = "https://nodejs.org/dist/v$NodeVersion/node-v$NodeVersion-win-$NodeArch.zip"
Write-Host "==> Downloading Node.js v$NodeVersion ($NodeArch)..."
Invoke-WebRequest -Uri $NodeUrl -OutFile $NodeArchive -UseBasicParsing
$NodeExtract = Join-Path $Stage "node"
Expand-Archive -Path $NodeArchive -DestinationPath $NodeExtract -Force
$NodeExe = Get-ChildItem $NodeExtract -Recurse -Filter node.exe | Select-Object -First 1
if (-not $NodeExe) { throw "node.exe not found in Node.js archive" }
Copy-Item $NodeExe.FullName (Join-Path $Stage "node.exe") -Force
Write-Host "==> Compiling installer (makensis)..."
Push-Location $Stage
try {
& makensis "-DVERSION=$RealityVersion" "-DARCH=$Arch" gelectron.nsi
if ($LASTEXITCODE -ne 0) { throw "makensis failed with exit code $LASTEXITCODE" }
}
finally {
Pop-Location
}
$Installer = Get-ChildItem $Stage -Filter "Gelectron-*.exe" | Select-Object -First 1
if (-not $Installer) { throw "makensis produced no installer" }
New-Item -ItemType Directory -Path $Out -Force | Out-Null
Copy-Item $Installer.FullName (Join-Path $Out $Installer.Name) -Force
Write-Host ""
Write-Host "==> Created installer: $(Join-Path $Out $Installer.Name)"
}
finally {
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
}
+195
View File
@@ -0,0 +1,195 @@
'use strict';
/**
* Gelectron - clipboard module (Electron compatible)
*
* Implements text / HTML / rich-text clipboard operations using the OS-native
* tooling so it works both in native Node mode and in the pure-Node fallback.
*/
const { execSync } = require('child_process');
const os = require('os');
const { isNative } = require('./native-bridge');
function platform() {
return os.platform();
}
// ─── Low-level platform pipes ───────────────────────────────────────────────
function readCommand(cmd) {
try {
const out = execSync(cmd, { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8' });
return out == null ? '' : out.toString();
} catch (e) {
return '';
}
}
function writePipe(cmd, text) {
try {
execSync(`${cmd} '${String(text).replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
return true;
} catch (e) {
try {
const { spawnSync } = require('child_process');
if (platform() === 'win32') {
// Powershell stdin pipe is the most reliable on Windows
const ps = spawnSync('powershell', ['-NoProfile', '-Command', 'Set-Clipboard'], {
input: String(text),
encoding: 'utf8',
});
return ps.status === 0;
}
return false;
} catch (e2) {
return false;
}
}
}
function readTextFromSystem() {
const p = platform();
try {
if (p === 'darwin') return execSync('pbpaste', { encoding: 'utf8' }).toString();
if (p === 'win32') {
const out = execSync('powershell -NoProfile -Command "Get-Clipboard -Raw"', { encoding: 'utf8' });
return out.replace(/\r?\n$/, '');
}
for (const tool of ['xclip -selection clipboard -o', 'xsel --clipboard --output', 'wl-paste']) {
try {
return execSync(tool, { encoding: 'utf8' }).toString();
} catch (e) { /* try next */ }
}
return '';
} catch (e) {
return '';
}
}
function writeTextToSystem(text) {
const p = platform();
const content = String(text);
try {
if (p === 'darwin') return writePipe('pbcopy', content);
if (p === 'win32') {
const { spawnSync } = require('child_process');
const ps = spawnSync('powershell', ['-NoProfile', '-Command', 'Set-Clipboard'], {
input: content,
encoding: 'utf8',
});
return ps.status === 0;
}
for (const cmd of [
`xclip -selection clipboard -in <<'GELECTRON_EOF'\n${content}\nGELECTRON_EOF`,
`xsel --clipboard --input <<'GELECTRON_EOF'\n${content}\nGELECTRON_EOF`,
`wl-copy <<'GELECTRON_EOF'\n${content}\nGELECTRON_EOF`,
]) {
try {
execSync(cmd, { encoding: 'utf8' });
return true;
} catch (e) { /* try next */ }
}
return false;
} catch (e) {
return false;
}
}
// ─── Public API ─────────────────────────────────────────────────────────────
const clipboard = {
readText() {
return readTextFromSystem();
},
writeText(text) {
return writeTextToSystem(text);
},
readHTML() {
// Native bridge request falls back to plain text when unavailable
if (isNative && typeof globalThis.__gelectron_clipboard_read_html === 'function') {
return globalThis.__gelectron_clipboard_read_html();
}
return readTextFromSystem();
},
writeHTML(markup, type = 'text/html') {
try {
if (platform() === 'win32') {
const html = String(markup);
const { spawnSync } = require('child_process');
const ps = spawnSync('powershell', ['-NoProfile', '-Command',
`Set-Clipboard -Value @'${html.replace(/'/g, "''")}'@`], { encoding: 'utf8' });
return ps.status === 0;
}
return writeTextToSystem(markup);
} catch (e) {
return false;
}
},
// Best-effort plain text read (Electron returns an image object for rich
// clipboard types on some platforms; we degrade to text where possible).
readTextOnly() {
return readTextFromSystem();
},
readImage() {
return NativeImageStub.empty();
},
writeImage(image) {
return !!image;
},
availableFormats() {
return ['text/plain'];
},
read(format = 'text/plain') {
return readTextFromSystem();
},
write(data, type = 'text/plain') {
return writeTextToSystem(data);
},
clear() {},
readBookmark() {
return { title: '', url: '' };
},
writeBookmark() {},
readFindText() {
try {
return process.env.GELECTRON_FIND_TEXT || '';
} catch (e) {
return '';
}
},
writeFindText(text) {
try {
process.env.GELECTRON_FIND_TEXT = String(text);
return true;
} catch (e) {
return false;
}
},
};
// Avoid a hard dependency on native-image for the stub paths.
const NativeImageStub = {
empty() {
return {
isEmpty: () => true,
toPNG: () => Buffer.alloc(0),
};
},
};
module.exports = clipboard;
+8
View File
@@ -70,6 +70,14 @@ class NativeBridge extends EventEmitter {
});
}
// Generic request to the native process: request(type, payload) where
// `type` matches a ToRust variant. Resolves with the JSON response from the
// native side (or null when running without the native binary).
request(type, payload = {}) {
if (!isNative) return Promise.resolve(null);
return this._request({ type, ...payload });
}
_resolveRequest(requestId, result, error) {
const pending = this._pendingRequests[requestId];
if (pending) {
+72
View File
@@ -0,0 +1,72 @@
'use strict';
/**
* Gelectron - nativeTheme module (Electron compatible)
*
* Reports dark mode / accent colors. When running with the native binary it
* asks the OS for the current theme; otherwise it falls back to an env hint
* (or 'light').
*/
const { EventEmitter } = require('events');
const { bridge, isNative } = require('./native-bridge');
function osDarkHint() {
const env = process.env;
if (env.COLORFGBG && /^0;/.test(env.COLORFGBG)) return true;
return false;
}
class NativeTheme extends EventEmitter {
constructor() {
super();
this._dark = osDarkHint();
this._source = 'system';
this._accent = '#007AFF';
this._loaded = false;
if (isNative) {
bridge.request('native-theme-query', {}).then((res) => {
if (res) {
if (typeof res.shouldUseDarkColors === 'boolean') this._dark = res.shouldUseDarkColors;
if (res.accentColor) this._accent = res.accentColor;
if (res.themeSource) this._source = res.themeSource;
this._loaded = true;
}
}).catch(() => {});
}
}
get shouldUseDarkColors() {
return this._dark;
}
get shouldUseInvertedColorScheme() {
return false;
}
get themeSource() {
return this._source;
}
set themeSource(value) {
if (value === 'system' || value === 'light' || value === 'dark') {
this._source = value;
if (value === 'dark') this._dark = true;
else if (value === 'light') this._dark = false;
}
}
get themes() {
return {
initial: this._dark ? ['dark'] : ['light'],
current: this._dark ? ['dark'] : ['light'],
};
}
on(eventName, listener) { super.on(eventName, listener); return this; }
once(eventName, listener) { super.once(eventName, listener); return this; }
}
module.exports = new NativeTheme();
module.exports.NativeTheme = NativeTheme;
+151
View File
@@ -0,0 +1,151 @@
'use strict';
/**
* Gelectron - screen module (Electron compatible)
*
* Queries display information from the native process when available and
* otherwise returns sensible single-display defaults so apps never hang.
*/
const os = require('os');
const { EventEmitter } = require('events');
const { bridge, isNative } = require('./native-bridge');
function defaultDisplay() {
const width = 1920;
const height = 1080;
return {
id: 1,
label: 'Display 1',
bounds: { x: 0, y: 0, width, height },
workArea: { x: 0, y: 0, width, height },
size: { width, height },
workAreaSize: { width, height },
scaleFactor: 1,
rotation: 0,
internal: true,
touchSupport: 'unknown',
displayFrequency: 60,
colorSpace: '',
colorDepth: 24,
monitors: [],
};
}
function normalize(raw) {
const d = raw || {};
return {
id: d.id != null ? d.id : 1,
label: d.label || 'Display 1',
bounds: d.bounds || { x: 0, y: 0, width: 1920, height: 1080 },
workArea: d.workArea || d.bounds || { x: 0, y: 0, width: 1920, height: 1080 },
size: d.size || { width: 1920, height: 1080 },
workAreaSize: d.workAreaSize || d.size || { width: 1920, height: 1080 },
scaleFactor: d.scaleFactor || 1,
rotation: d.rotation || 0,
internal: !!d.internal,
touchSupport: d.touchSupport || 'unknown',
displayFrequency: d.displayFrequency || 60,
colorSpace: d.colorSpace || '',
colorDepth: d.colorDepth || 24,
monitors: d.monitors || [],
};
}
function rectsIntersect(a, b) {
return (
a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y
);
}
class Screen extends EventEmitter {
constructor() {
super();
this._displays = [defaultDisplay()];
// Parse an optional env-provided display list (packaged apps can inject
// this to avoid a round-trip).
try {
if (process.env.GELECTRON_DISPLAYS) {
const parsed = JSON.parse(process.env.GELECTRON_DISPLAYS);
if (Array.isArray(parsed) && parsed.length > 0) {
this._displays = parsed.map(normalize);
}
}
} catch (e) {
// Ignore malformed env
}
// Refresh from the native process when available.
if (isNative) {
bridge.request('screen-get-displays', {}).then((res) => {
if (res && Array.isArray(res.displays) && res.displays.length > 0) {
this._displays = res.displays.map(normalize);
this.emit('display-added', this._displays[this._displays.length - 1]);
}
}).catch(() => {});
}
}
getAllDisplays() {
return this._displays;
}
getPrimaryDisplay() {
return this._displays[0] || defaultDisplay();
}
getDisplayNearestPoint(point) {
const p = point || { x: 0, y: 0 };
let best = this._displays[0];
let bestDist = Infinity;
for (const d of this._displays) {
const cx = d.bounds.x + d.bounds.width / 2;
const cy = d.bounds.y + d.bounds.height / 2;
const dist = (p.x - cx) ** 2 + (p.y - cy) ** 2;
if (dist < bestDist) {
bestDist = dist;
best = d;
}
}
return best;
}
getDisplayForPoint(point) {
return this.getDisplayNearestPoint(point);
}
getDisplayMatching(rect) {
const r = rect || { x: 0, y: 0, width: 0, height: 0 };
let best = this._displays[0];
let bestArea = 0;
for (const d of this._displays) {
if (rectsIntersect(d.bounds, r)) {
const area = d.bounds.width * d.bounds.height;
if (area > bestArea) {
bestArea = area;
best = d;
}
}
}
return best;
}
getCursorScreenPoint() {
// No native cursor query is implemented; return the center of the primary
// display as a harmless default.
const d = this.getPrimaryDisplay();
return {
x: Math.round(d.bounds.x + d.bounds.width / 2),
y: Math.round(d.bounds.y + d.bounds.height / 2),
};
}
on(eventName, listener) { super.on(eventName, listener); return this; }
once(eventName, listener) { super.once(eventName, listener); return this; }
}
module.exports = { Screen, defaultDisplay, normalize };