feat: implement application icon support for macOS

- Removed package.json and code signature files from the macOS app bundle.
- Added support for setting application icons via base64 encoded PNG strings.
- Enhanced the main application logic to detect and apply icons from various locations.
- Updated the Electron app and browser window classes to handle icon setting.
- Introduced new methods for decoding PNG icons and applying them to the application and window.
- Updated Cargo.toml to include the cocoa dependency for macOS.
This commit is contained in:
2026-07-31 04:07:23 -05:00
parent a7b6f9a072
commit da521a33a0
37 changed files with 513 additions and 5076 deletions
Generated
+1
View File
@@ -1405,6 +1405,7 @@ version = "0.1.0"
dependencies = [
"arboard",
"base64",
"cocoa",
"env_logger",
"log",
"muda",
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>GeelectronDemo</string>
<key>CFBundleExecutable</key>
<string>GeelectronDemo</string>
<key>CFBundleIdentifier</key>
<string>com.gelectron.geelectrondemo</string>
<key>CFBundleName</key>
<string>GeelectronDemo</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSRequiresAquaSystemAppearance</key>
<false/>
</dict>
</plist>
@@ -1,5 +0,0 @@
#!/bin/bash
DIR="$(cd "$(dirname "$0")" && pwd)"
export PATH="$DIR:$PATH"
export GELECTRON_NATIVE=1
exec "$DIR/gelectron-bin" "$@"
@@ -1,352 +0,0 @@
'use strict';
/**
* Gelectron - app module (Electron compatible)
*
* Full implementation matching the Electron app API surface.
* Events: ready, second-instance, activate, window-all-closed, before-quit,
* will-quit, quit, focus, blur, browser-window-focus, browser-window-blur,
* web-contents-created, render-process-gone, child-process-gone,
* keyboard-visibility-changed, new-window-for-tab
*/
const { EventEmitter } = require('events');
const path = require('path');
class App extends EventEmitter {
constructor() {
super();
this._ready = false;
this._windowCount = 0;
this._relaunchOptions = null;
this._relaunching = false;
this._aboutPanelOptions = {};
this._badgeCount = 0;
this._secureKeyboardEntryEnabled = false;
this._userAgent = null;
this._name = 'Gelectron App';
this._names = null;
const os = require('os');
const home = os.homedir();
this._paths = {
home,
appData: path.join(home, process.platform === 'win32' ? '' : '.', process.platform === 'darwin' ? 'Library/Application Support' : 'config', 'gelectron'),
userData: path.join(home, process.platform === 'win32' ? '' : '.', process.platform === 'darwin' ? 'Library/Application Support' : 'config', 'gelectron'),
desktop: path.join(home, 'Desktop'),
documents: path.join(home, 'Documents'),
downloads: path.join(home, 'Downloads'),
temp: os.tmpdir(),
exe: process.execPath,
module: __dirname,
crashDumps: path.join(home, process.platform === 'win32' ? '' : '.', process.platform === 'darwin' ? 'Library/Application Support' : 'config', 'gelectron', 'crashDumps'),
logs: path.join(home, process.platform === 'win32' ? '' : '.', process.platform === 'darwin' ? 'Library/Application Support' : 'config', 'gelectron', 'logs'),
};
this._commandLine = new Map();
this._dock = process.platform === 'darwin' ? {
setIcon: (icon) => {},
bounce: () => 0,
cancelBounce: () => {},
setBadge: () => {},
getBadge: () => '',
hide: () => {},
show: () => {},
isVisible: () => true,
setMenu: () => {},
setBadgeCount: (count) => { return 0; },
getBadgeCount: () => 0,
setThumbnail: () => {},
setThumbnailClip: () => {},
setThumbnailToolTip: () => {},
} : null;
this._argv = process.argv.slice();
// Emit 'ready' automatically on the next tick, matching real Electron
process.nextTick(() => {
if (!this._ready) {
this._ready = true;
this.emit('ready');
}
});
}
// ─── Properties ──────────────────────────────────────────────
get commandLine() {
const self = this;
return {
appendSwitch: (name, value) => { self._commandLine.set(name, value || ''); },
removeSwitch: (name) => { self._commandLine.delete(name); },
getSwitch: (name) => { return self._commandLine.get(name) || ''; },
hasSwitch: (name) => { return self._commandLine.has(name); },
};
}
get dock() { return this._dock; }
get argv() { return this._argv; }
get isPackaged() {
return !process.argv[0].includes('node') && !process.argv[0].includes('gelectron');
}
get name() { return this._name; }
set name(val) { this._name = val || 'Gelectron App'; }
get version() { return process.env.GELECTRON_VERSION || '0.1.0'; }
get locale() { return this.getLocale(); }
get userAgent() { return this.getUserAgent(); }
get isReady() { return this._ready; }
get appPath() { return this.getAppPath(); }
// ─── Core Methods ────────────────────────────────────────────
getAppPath() { return process.env.GELECTRON_APP_PATH || process.cwd(); }
getPath(name) {
if (name === 'app' || name === 'appData' || name === 'userData') {
return this._paths[name === 'app' ? 'appData' : name] || this._paths.userData;
}
return this._paths[name] || this._paths.userData;
}
setPath(name, value) { this._paths[name] = value; }
getName() { return this._name; }
setName(name) { this._name = name; }
getVersion() { return this.version; }
getLocale() { return Intl.DateTimeFormat().resolvedOptions().locale || 'en-US'; }
getUserAgent() {
return this._userAgent || `Gelectron/${this.version} (${process.platform} ${process.arch}) Node.js/${process.versions ? process.versions.node : 'unknown'}`;
}
setUserAgent(userAgent) { this._userAgent = userAgent || null; }
getPathForProtocol(protocol) { return null; }
getApplicationInfoForProtocol(protocol) {
return Promise.resolve({
defaultIcon: null,
icon: null,
});
}
// ─── Lifecycle ───────────────────────────────────────────────
async whenReady() {
if (this._ready) return Promise.resolve();
return new Promise((resolve) => { this.once('ready', resolve); });
}
isReady() { return this._ready; }
requestSingleInstanceLock() { return true; }
acquireSingleInstanceLock() { return true; }
releaseSingleInstanceLock() {}
quit(exitCode = 0) {
if (this.listenerCount('before-quit') > 0 || this.listenerCount('will-quit') > 0) {
this.emit('before-quit');
this.emit('will-quit');
}
this.emit('quit', exitCode);
process.exit(exitCode);
}
exit(exitCode = 0) {
if (this._ready) {
this.emit('before-quit');
this.emit('will-quit');
}
this.emit('quit', exitCode);
process.exit(exitCode);
}
relaunch(options = {}) {
this._relaunchOptions = options;
this._relaunching = true;
this.emit('before-quit');
const { spawn } = require('child_process');
const execPath = options.execPath || process.execPath;
const args = options.args || process.argv.slice(1);
spawn(execPath, args, {
detached: true,
stdio: 'ignore',
env: process.env,
}).unref();
process.exit(0);
}
// ─── Window Management Helpers ───────────────────────────────
focus() {
if (process.platform === 'darwin') { this.dock && this.dock.show(); }
this.emit('focus');
}
hide() { if (this.dock) this.dock.hide(); this.emit('blur'); }
show() { if (this.dock) this.dock.show(); this.emit('focus'); }
isVisible() { return this.dock ? this.dock.isVisible() : true; }
isHidden() { return this.dock ? !this.dock.isVisible() : false; }
// ─── App Metrics / GPU ──────────────────────────────────────
getAppMetrics() {
return [
{
creationTime: Date.now() - (process.uptime() * 1000) | 0,
pid: process.pid,
rid: 0,
type: 'Browser',
memory: { workingSetSize: 0, peakWorkingSetSize: 0, privateBytes: 0, sharedBytes: 0 },
sandboxed: false,
},
];
}
getGPUInfo(infoType) {
if (infoType === 'basic') {
return Promise.resolve({
gpuDevice: [{ driverVendor: 'Gelectron', driverVersion: '0.0.0', driverDate: '', active: false }],
GPUActive: false,
});
}
return Promise.resolve({
gpuDevice: [{ driverVendor: 'Gelectron', driverVersion: '0.0.0', driverDate: '', active: false }],
GPUActive: false,
auxAttributes: {},
gpuDriver: 'Gelectron',
gpuDriverVersion: '0.0.0',
gpuWorkingSetSize: 0,
});
}
getGPUFeatureStatus() {
return {
gpuCompositing: 'disabled',
multipleRasterThreads: 'disabled',
nativeGpuMemoryBuffers: 'disabled',
rasterization: 'disabled',
smoothScrolling: 'disabled',
videoDecode: 'disabled',
videoEncode: 'disabled',
webgl: 'disabled',
webgl2: 'disabled',
};
}
disableHardwareAcceleration() {}
disableDomainBlockingFor3DAPIs() {}
// ─── Badge ───────────────────────────────────────────────────
setBadgeCount(count) {
this._badgeCount = count || 0;
if (this.dock) this.dock.setBadgeCount(this._badgeCount);
return this._badgeCount;
}
getBadgeCount() { return this._badgeCount; }
// ─── Recent Documents ────────────────────────────────────────
addRecentDocument(path) {}
clearRecentDocuments() {}
setRecentDocumentLabel(label) {}
// ─── App User Model ID ──────────────────────────────────────
setAppUserModelId(id) {}
getAppUserModelId() { return ''; }
// ─── Login Items ─────────────────────────────────────────────
getLoginItemSettings() {
return {
openAtLogin: false,
openAsHidden: false,
launchAtLogin: false,
launchItems: [],
};
}
setLoginItemSettings(settings) {}
// ─── Applications Folder ─────────────────────────────────────
isInApplicationsFolder() { return true; }
moveToApplicationsFolder() {}
// ─── File Icons ──────────────────────────────────────────────
getFileIcon(path, options, callback) {
if (typeof options === 'function') { callback = options; }
if (callback) callback(null, null);
}
// ─── About Panel ─────────────────────────────────────────────
setAboutPanelOptions(options = {}) {
this._aboutPanelOptions = {
applicationName: this._name,
applicationVersion: this.version,
copyright: '',
credits: '',
authors: [],
website: '',
iconPath: '',
...options,
};
}
getAboutPanelOptions() {
return { ...this._aboutPanelOptions };
}
// ─── Certificate Trust ───────────────────────────────────────
async showCertificateTrustDialog() {}
// ─── Secure Keyboard Entry ──────────────────────────────────
setSecureKeyboardEntryEnabled(enabled) {
this._secureKeyboardEntryEnabled = !!enabled;
}
isSecureKeyboardEntryEnabled() {
return this._secureKeyboardEntryEnabled;
}
// ─── Window Tracking ────────────────────────────────────────
_trackWindow() { this._windowCount++; }
_untrackWindow() {
this._windowCount--;
if (this._windowCount <= 0) {
this._windowCount = 0;
this.emit('window-all-closed');
}
}
getWindowCount() { return this._windowCount; }
// ─── EventEmitter overrides (return this for chaining) ──────
on(eventName, listener) { super.on(eventName, listener); return this; }
once(eventName, listener) { super.once(eventName, listener); return this; }
addListener(eventName, listener) { super.addListener(eventName, listener); return this; }
removeListener(eventName, listener) { super.removeListener(eventName, listener); return this; }
removeAllListeners(eventName) { super.removeAllListeners(eventName); return this; }
}
const app = new App();
module.exports = { app, App };
@@ -1,84 +0,0 @@
'use strict';
/**
* Gelectron - autoUpdater module (Electron compatible)
* Stub implementation for electron-updater compatibility.
*
* electron-updater's MacUpdater / NsisUpdater / AppImageUpdater all
* access `require("electron").autoUpdater` and expect it to be an
* EventEmitter with the native Electron autoUpdater API surface:
* .on("error", …), .on("update-downloaded", …)
* .setFeedURL(), .getFeedURL()
* .checkForUpdates()
* .quitAndInstall()
* .removeListener()
*/
const { EventEmitter } = require('events');
class AutoUpdater extends EventEmitter {
constructor() {
super();
this._isUpdateAvailable = false;
this._updateInfo = null;
this._feedURL = null;
this.autoDownload = true;
this.autoInstallOnAppQuit = false;
this.autoRunAppAfterInstall = true;
this._logger = console;
}
getFeedURL() {
return this._feedURL;
}
setFeedURL(options) {
this._feedURL = options;
}
async checkForUpdates() {
return {
updateInfo: null,
isUpdateAvailable: false,
};
}
async checkForAndNotifyIfAvailable() {
return this.checkForUpdates();
}
async downloadUpdate() {}
quitAndInstall(isSilent = false, isForceRunAfter = false) {
this.emit('before-quit-for-update');
process.exit(0);
}
quitAndInstallAgain(isSilent = false, isForceRunAfter = false) {
this.quitAndInstall(isSilent, isForceRunAfter);
}
async updateDownloaded() {
return false;
}
get isUpdateActive() {
return false;
}
get updateInfo() {
return this._updateInfo;
}
get logger() {
return this._logger;
}
set logger(val) {
this._logger = val;
}
}
const autoUpdater = new AutoUpdater();
module.exports = { autoUpdater, AutoUpdater };
@@ -1,389 +0,0 @@
'use strict';
/**
* Gelectron - BrowserWindow module (Electron compatible)
*/
const { EventEmitter } = require('events');
const path = require('path');
const { bridge, isNative } = require('./native-bridge');
const { app } = require('./app');
class WebContents extends EventEmitter {
constructor(id) {
super();
this.id = id;
this._url = '';
this._title = '';
this._isLoading = false;
this._zoomFactor = 1.0;
this._zoomLevel = 0;
this._userAgent = '';
this._audioMuted = false;
}
get URL() { return this._url; }
get isLoading() { return this._isLoading; }
get title() { return this._title; }
get session() {
return {
id: `session-${this.id}`,
cookies: {
get: async () => [],
set: async () => {},
remove: async () => {},
},
protocol: {
registerFileProtocol: () => {},
registerHttpProtocol: () => {},
unregisterProtocol: () => {},
isProtocolRegistered: () => false,
},
setPermissionRequestHandler: () => {},
setPermissionCheckHandler: () => {},
webRequest: { onBeforeRequest: () => {}, onHeadersReceived: () => {} },
setUserAgent: () => {},
getUserAgent: () => '',
};
}
get processId() { return process.pid; }
loadURL(targetUrl) {
this._url = targetUrl;
this._isLoading = true;
this.emit('did-start-loading');
if (isNative) {
bridge.loadUrl(this.id, targetUrl);
}
setTimeout(() => {
this._isLoading = false;
this.emit('did-stop-loading');
this.emit('did-finish-load');
this.emit('dom-ready');
}, isNative ? 500 : 100);
return Promise.resolve();
}
loadFile(filePath) {
const url = `file://${path.resolve(filePath)}`;
this._url = url;
this._isLoading = true;
this.emit('did-start-loading');
if (isNative) {
bridge.loadFile(this.id, filePath);
}
setTimeout(() => {
this._isLoading = false;
this.emit('did-stop-loading');
this.emit('did-finish-load');
this.emit('dom-ready');
}, isNative ? 500 : 100);
return Promise.resolve();
}
reload() {
this._isLoading = true;
this.emit('did-start-loading');
if (isNative && this._url) {
bridge.loadUrl(this.id, this._url);
}
setTimeout(() => {
this._isLoading = false;
this.emit('did-stop-loading');
this.emit('did-finish-load');
}, 50);
return Promise.resolve();
}
canGoBack() { return false; }
canGoForward() { return false; }
goBack() {}
goForward() {}
executeJavaScript(code, userGesture = true) {
if (isNative) {
bridge.evalJs(this.id, code);
}
return Promise.resolve(null);
}
insertCSS(css) { return Promise.resolve(0); }
insertJS(code, hasUserGesture = true) { return this.executeJavaScript(code, hasUserGesture); }
send(channel, ...args) {
if (isNative) {
bridge.sendToRenderer(this.id, channel, ...args);
} else {
console.log(`[gelectron] webContents.send('${channel}')`);
}
}
sendInputEvent() {}
setZoomFactor(factor) { this._zoomFactor = factor; }
getZoomFactor() { return this._zoomFactor; }
setZoomLevel(level) { this._zoomLevel = level; }
getZoomLevel() { return this._zoomLevel; }
setUserAgent(ua) { this._userAgent = ua; }
getUserAgent() { return this._userAgent; }
setAudioMuted(muted) { this._audioMuted = muted; }
isAudioMuted() { return this._audioMuted; }
openDevTools() { console.log('[gelectron] openDevTools'); }
closeDevTools() {}
isDevToolsOpened() { return false; }
toggleDevTools() {}
inspectElement() {}
setIgnoreMenuShortcuts() {}
setWindowOpenHandler() { return { action: 'deny' }; }
setPermissionRequestHandler() {}
setCertificateVerifyProc() {}
setBackgroundColor() {}
isCrashed() { return false; }
capturePage() { return Promise.resolve(null); }
getResourceUsage() { return { images: 0, scripts: 0, css: 0, xhr: 0, webgl: 0 }; }
type() { return 'backgroundPage'; }
focused() { return false; }
isFocused() { return false; }
destroy() {}
}
class BrowserWindow extends EventEmitter {
static _windows = new Map();
static _nextId = 1;
constructor(options = {}) {
super();
this.id = BrowserWindow._nextId++;
this._options = {
width: options.width || 800,
height: options.height || 600,
minWidth: options.minWidth || 0,
minHeight: options.minHeight || 0,
maxWidth: options.maxWidth || 0,
maxHeight: options.maxHeight || 0,
title: options.title || 'Gelectron',
backgroundColor: options.backgroundColor || '#ffffff',
show: options.show !== false,
frame: options.frame !== false,
resizable: options.resizable !== false,
minimizable: options.minimizable !== false,
maximizable: options.maximizable !== false,
closable: options.closable !== false,
fullscreen: options.fullscreen || false,
alwaysOnTop: options.alwaysOnTop || false,
transparent: options.transparent || false,
decorations: options.decorations !== false,
icon: options.icon || null,
titleBarStyle: options.titleBarStyle || 'default',
trafficLightPosition: options.trafficLightPosition || null,
vibrancy: options.vibrancy || null,
webPreferences: {
preload: options.webPreferences?.preload || null,
contextIsolation: options.webPreferences?.contextIsolation !== false,
nodeIntegration: options.webPreferences?.nodeIntegration || false,
sandbox: options.webPreferences?.sandbox || false,
devTools: options.webPreferences?.devTools !== false,
webSecurity: options.webPreferences?.webSecurity !== false,
allowRunningInsecureContent: options.webPreferences?.allowRunningInsecureContent || false,
experimentalFeatures: options.webPreferences?.experimentalFeatures || false,
...options.webPreferences,
},
};
this._url = '';
this._isDestroyed = false;
this._isVisible = this._options.show;
this._isMinimized = false;
this._isMaximized = false;
this._isFullScreen = this._options.fullscreen;
this.webContents = new WebContents(this.id);
BrowserWindow._windows.set(this.id, this);
if (typeof app._trackWindow === 'function') {
app._trackWindow();
}
if (isNative) {
bridge.createWindow(this.id, {
width: this._options.width,
height: this._options.height,
title: this._options.title,
show: this._options.show,
resizable: this._options.resizable,
alwaysOnTop: this._options.alwaysOnTop,
fullscreen: this._options.fullscreen,
});
}
if (this._options.show) {
process.nextTick(() => {
if (!this._isDestroyed) {
this.emit('ready-to-show');
this.show();
}
});
}
}
static fromWebContents(webContents) {
for (const win of BrowserWindow._windows.values()) {
if (win.webContents && win.webContents.id === webContents.id) return win;
}
return null;
}
static getAllWindows() {
return Array.from(BrowserWindow._windows.values()).filter((w) => !w.isDestroyed());
}
static getFocusedWindow() { return null; }
static fromId(id) { return BrowserWindow._windows.get(id) || null; }
static addExtension() {}
static removeExtension() {}
static getExtensions() { return {}; }
loadURL(targetUrl) {
this._url = targetUrl;
if (isNative) {
bridge.loadUrl(this.id, targetUrl);
}
return this.webContents.loadURL(targetUrl);
}
loadFile(filePath) {
return this.webContents.loadFile(filePath);
}
show() {
if (this._isDestroyed) return;
this._isVisible = true;
this._isMinimized = false;
if (isNative) bridge.showWindow(this.id);
this.emit('show');
}
hide() {
if (this._isDestroyed) return;
this._isVisible = false;
if (isNative) bridge.hideWindow(this.id);
this.emit('hide');
}
close() {
if (this._isDestroyed) return;
this.emit('close');
this.destroy();
}
destroy() {
if (this._isDestroyed) return;
this._isDestroyed = true;
if (isNative) bridge.destroyWindow(this.id);
BrowserWindow._windows.delete(this.id);
if (typeof app._untrackWindow === 'function') {
app._untrackWindow();
}
this.emit('closed');
}
focus() { if (!this._isDestroyed) { if (isNative) bridge.focusWindow(this.id); this.emit('focus'); } }
blur() {}
minimize() { if (!this._isDestroyed) { this._isMinimized = true; if (isNative) bridge.minimizeWindow(this.id); this.emit('minimize'); } }
maximize() { if (!this._isDestroyed) { this._isMaximized = true; if (isNative) bridge.maximizeWindow(this.id); this.emit('maximize'); } }
unmaximize() { if (!this._isDestroyed) { this._isMaximized = false; this.emit('unmaximize'); } }
restore() { if (!this._isDestroyed) { this._isMinimized = false; this._isMaximized = false; this.emit('restore'); } }
setFullScreen(flag) { this._isFullScreen = flag; this.emit('enter-full-screen'); }
isFullScreen() { return this._isFullScreen; }
isMinimized() { return this._isMinimized; }
isMaximized() { return this._isMaximized; }
isVisible() { return this._isVisible; }
isDestroyed() { return this._isDestroyed; }
isFocused() { return false; }
isNormal() { return !this._isMinimized && !this._isMaximized && !this._isFullScreen; }
setAlwaysOnTop(flag) { this._options.alwaysOnTop = flag; }
isAlwaysOnTop() { return this._options.alwaysOnTop; }
setPosition(x, y) {}
getPosition() { return [0, 0]; }
setSize(w, h) { this._options.width = w; this._options.height = h; }
getSize() { return [this._options.width, this._options.height]; }
setMinimumSize(w, h) { this._options.minWidth = w; this._options.minHeight = h; }
getMinimumSize() { return [this._options.minWidth, this._options.minHeight]; }
setMaximumSize(w, h) { this._options.maxWidth = w; this._options.maxHeight = h; }
getMaximumSize() { return [this._options.maxWidth, this._options.maxHeight]; }
setResizable(v) { this._options.resizable = v; }
isResizable() { return this._options.resizable; }
setMovable() {}
isMovable() { return true; }
setMinimizable(v) { this._options.minimizable = v; }
isMinimizable() { return this._options.minimizable; }
setMaximizable(v) { this._options.maximizable = v; }
isMaximizable() { return this._options.maximizable; }
setClosable(v) { this._options.closable = v; }
isClosable() { return this._options.closable; }
setTitle(title) { this._options.title = title; this.webContents._title = title; if (isNative) bridge.setTitle(this.id, title); }
getTitle() { return this._options.title; }
setSkipTaskbar() {}
setKiosk() {}
isKiosk() { return false; }
center() {}
setBounds() {}
getBounds() { return { x: 0, y: 0, width: this._options.width, height: this._options.height }; }
setSimpleFullScreen() {}
isSimpleFullScreen() { return false; }
setAutoHideCursor() {}
setContentBounds() {}
getContentBounds() { return this.getBounds(); }
isEnabled() { return true; }
setEnabled() {}
setProgressBar() {}
setOverlayIcon() {}
setVisibleOnAllWorkspaces() {}
isVisibleOnAllWorkspaces() { return false; }
setVibrancy() {}
getNativeWindowHandle() { return Buffer.alloc(0); }
setHasShadow() {}
hasShadow() { return true; }
setOpacity() {}
getOpacity() { return 1.0; }
setThumbarButtons() {}
setThumbnailClip() {}
setThumbnailToolTip() {}
setAppDetails() {}
setForeground() {}
flashFrame() {}
setIcon() {}
setBackgroundColor(c) { this._options.backgroundColor = c; }
getBackgroundColor() { return this._options.backgroundColor; }
capturePage() { return this.webContents.capturePage(); }
print() { console.log('[gelectron] print called'); }
printToPDF() { return Promise.resolve(Buffer.alloc(0)); }
setParentWindow() {}
getParentWindow() { return null; }
getChildWindows() { return []; }
selectPreviousTab() {}
selectNextTab() {}
mergeAllWindows() {}
moveTabToNewWindow() {}
toggleTabBar() {}
addTabbedWindow() {}
}
module.exports = { BrowserWindow, WebContents };
@@ -1,33 +0,0 @@
'use strict';
/**
* Gelectron - contextBridge module (Electron compatible)
*/
const contextBridge = {
exposeInMainWorld(key, api) {
if (typeof globalThis.window === 'undefined') {
return;
}
// Create a frozen copy of the API
const safeApi = deepFreeze(JSON.parse(JSON.stringify(api)));
globalThis.window[key] = safeApi;
console.log(`[gelectron] contextBridge: exposed '${key}' to main world`);
},
};
function deepFreeze(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
Object.freeze(obj);
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object' && obj[key] !== null && !Object.isFrozen(obj[key])) {
deepFreeze(obj[key]);
}
});
return obj;
}
module.exports = contextBridge;
@@ -1,70 +0,0 @@
'use strict';
/**
* Gelectron - dialog module (Electron compatible)
*/
const dialog = {
showOpenDialog(browserWindowOrOptions, options) {
let opts = options || browserWindowOrOptions || {};
if (browserWindowOrOptions && browserWindowOrOptions.webContents) {
opts = options || {};
}
// Route through native if available
if (typeof globalThis.__gelectron_dialog_open === 'function') {
return globalThis.__gelectron_dialog_open(opts);
}
return Promise.resolve({ canceled: true, filePaths: [] });
},
showSaveDialog(browserWindowOrOptions, options) {
let opts = options || browserWindowOrOptions || {};
if (browserWindowOrOptions && browserWindowOrOptions.webContents) {
opts = options || {};
}
if (typeof globalThis.__gelectron_dialog_save === 'function') {
return globalThis.__gelectron_dialog_save(opts);
}
return Promise.resolve({ canceled: true, filePath: undefined });
},
showMessageBox(browserWindowOrOptions, options) {
let opts = options || browserWindowOrOptions || {};
if (browserWindowOrOptions && browserWindowOrOptions.webContents) {
opts = options || {};
}
if (typeof globalThis.__gelectron_dialog_message === 'function') {
return globalThis.__gelectron_dialog_message(opts);
}
return Promise.resolve({ response: 0, checkboxChecked: false });
},
showErrorBox(title, content) {
console.error(`[gelectron] ${title}: ${content}`);
},
showMessageBoxSync(browserWindowOrOptions, options) {
let opts = options || browserWindowOrOptions || {};
if (browserWindowOrOptions && browserWindowOrOptions.webContents) {
opts = options || {};
}
if (typeof globalThis.__gelectron_dialog_message_sync === 'function') {
return globalThis.__gelectron_dialog_message_sync(opts);
}
return 0;
},
showCertificateTrustDialog(browserWindowOrOptions, options) {
return Promise.resolve();
},
};
module.exports = dialog;
@@ -1,159 +0,0 @@
'use strict';
/**
* Gelectron - Electron compatibility layer.
* Drop-in replacement for require('electron').
*/
const { app } = require('./app');
const { BrowserWindow } = require('./browser-window');
const ipcMain = require('./ipc-main');
const { Menu, MenuItem } = require('./menu');
const { Tray } = require('./tray');
const dialog = require('./dialog');
const shell = require('./shell');
const { Notification } = require('./notification');
const nativeImage = require('./native-image');
const safeStorage = require('./safe-storage');
const contextBridge = require('./context-bridge');
const webContents = require('./web-contents');
const { autoUpdater, AutoUpdater } = require('./auto-updater');
const { bridge, isNative } = require('./native-bridge');
if (isNative) {
bridge.on('ipc-message', (windowId, channel, data) => {
const event = { sender: { id: windowId }, channel };
ipcMain._emit(channel, event, data);
});
}
// Session stub (electron-updater calls session.fromPartition)
const sessionStub = {
defaultSession: {
cookies: {
get: async () => [],
set: async () => {},
remove: async () => {},
getSession: () => null,
},
protocol: {
registerFileProtocol: () => {},
registerHttpProtocol: () => {},
unregisterProtocol: () => {},
isProtocolRegistered: () => false,
},
setPermissionRequestHandler: () => {},
setPermissionCheckHandler: () => {},
webRequest: {
onBeforeRequest: () => {},
onHeadersReceived: () => {},
},
setUserAgent: () => {},
getUserAgent: () => '',
},
fromPartition: (partition, options) => ({
cookies: {
get: async () => [],
set: async () => {},
remove: async () => {},
},
protocol: {
registerFileProtocol: () => {},
registerHttpProtocol: () => {},
unregisterProtocol: () => {},
isProtocolRegistered: () => false,
},
webRequest: {
onBeforeRequest: () => {},
onHeadersReceived: () => {},
resolveProxy: async () => '',
},
setUserAgent: () => {},
getUserAgent: () => '',
clearCache: async () => {},
clearStorageData: async () => {},
setProxy: async () => {},
getProxy: async () => ({ mode: 'direct' }),
}),
};
module.exports = {
app,
BrowserWindow,
ipcMain,
Menu,
MenuItem,
Tray,
dialog,
shell,
Notification,
nativeImage,
safeStorage,
contextBridge,
webContents,
autoUpdater,
AutoUpdater,
session: sessionStub,
// Aliases for common imports
clipboard: {
readText: () => '',
writeText: () => {},
readImage: () => nativeImage.createEmpty(),
writeImage: () => {},
},
screen: {
getPrimaryDisplay: () => ({
id: 0,
label: '',
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
workArea: { x: 0, y: 0, width: 1920, height: 1040 },
size: { width: 1920, height: 1080 },
workAreaSize: { width: 1920, height: 1040 },
scaleFactor: 1.0,
rotation: 0,
internal: false,
touchSupport: 'unknown',
}),
getAllDisplays: () => [],
getDisplayMatching: () => null,
},
systemPreferences: {
isDarkMode: () => false,
getAccentColor: () => '#007AFF',
getColor: () => '#ffffff',
isSwipeTrackingFromScrollEventsEnabled: () => false,
subscribeNotification: () => () => {},
unsubscribeNotification: () => {},
subscribeLocalNotification: () => () => {},
unsubscribeLocalNotification: () => {},
getUserDefault: () => null,
setUserDefault: () => {},
removeUserDefault: () => {},
},
powerMonitor: {
on: () => {},
off: () => {},
once: () => {},
getSystemIdleState: () => 'active',
getSystemIdleTime: () => 0,
isInLowPowerMode: () => false,
},
globalShortcut: {
register: () => true,
unregister: () => {},
unregisterAll: () => {},
isRegistered: () => false,
},
net: {
fetch: globalThis.fetch || (() => Promise.reject(new Error('fetch not available'))),
},
// Constants
IPCRenderer: {
invoke: () => Promise.resolve(),
send: () => {},
on: () => () => {},
removeListener: () => {},
},
};
@@ -1,107 +0,0 @@
'use strict';
/**
* Gelectron - ipcMain module (Electron compatible)
*/
const { EventEmitter } = require('events');
class IpcMain extends EventEmitter {
constructor() {
super();
this._handlers = new Map();
this._onceHandlers = new Map();
}
handle(channel, handler) {
if (typeof handler !== 'function') {
throw new TypeError(`Expected function for channel '${channel}'`);
}
this._handlers.set(channel, handler);
return this;
}
handleOnce(channel, handler) {
if (typeof handler !== 'function') {
throw new TypeError(`Expected function for channel '${channel}'`);
}
this._onceHandlers.set(channel, handler);
return this;
}
on(channel, listener) {
if (typeof listener !== 'function') {
throw new TypeError(`Expected function for channel '${channel}'`);
}
super.on(channel, listener);
return this;
}
once(channel, listener) {
if (typeof listener !== 'function') {
throw new TypeError(`Expected function for channel '${channel}'`);
}
super.once(channel, listener);
return this;
}
removeHandler(channel) {
this._handlers.delete(channel);
return this;
}
removeListener(channel, listener) {
super.removeListener(channel, listener);
return this;
}
removeAllListeners(channel) {
super.removeAllListeners(channel);
return this;
}
listenerCount(channel) {
return super.listenerCount(channel);
}
rawListeners(channel) {
return super.rawListeners(channel);
}
eventNames() {
return super.eventNames();
}
_invoke(channel, ...args) {
const handler = this._handlers.get(channel);
if (!handler) {
return Promise.reject(new Error(`No handler registered for channel '${channel}'`));
}
try {
const result = handler(...args);
if (result instanceof Promise) {
return result;
}
return Promise.resolve(result);
} catch (err) {
return Promise.reject(err);
}
}
_emit(channel, ...args) {
return this.emit(channel, ...args);
}
_emitOnce(channel, ...args) {
const handler = this._onceHandlers.get(channel);
if (handler) {
this._onceHandlers.delete(channel);
return handler(...args);
}
return this.emit(channel, ...args);
}
}
const ipcMain = new IpcMain();
module.exports = ipcMain;
@@ -1,145 +0,0 @@
'use strict';
/**
* Gelectron - ipcRenderer module (Electron compatible)
*/
const { EventEmitter } = require('events');
class IpcRendererEvent {
constructor(sender, channel) {
this.sender = sender;
this.channel = channel;
this._returnValue = undefined;
this._defaultPrevented = false;
}
preventDefault() {
this._defaultPrevented = true;
}
get defaultPrevented() {
return this._defaultPrevented;
}
}
class IpcRenderer extends EventEmitter {
constructor() {
super();
this._requestId = 0;
this._pending = new Map();
this._listeners = new Map();
}
async invoke(channel, ...args) {
const requestId = `ipc-${++this._requestId}-${Date.now()}`;
const argsJson = JSON.stringify(args);
// In full integration, this calls the native ipc_renderer_invoke
// For now, route through the global IPC bridge if available
if (typeof globalThis.__gelectron_ipc === 'function') {
return globalThis.__gelectron_ipc(channel, ...args);
}
// Fallback: emit event for main process handling
return new Promise((resolve, reject) => {
this._pending.set(requestId, { resolve, reject });
// Emit for any registered handlers
this.emit(`invoke:${channel}`, ...args);
// Timeout after 30 seconds
setTimeout(() => {
if (this._pending.has(requestId)) {
this._pending.delete(requestId);
reject(new Error(`IPC invoke timed out for channel '${channel}'`));
}
}, 30000);
});
}
send(channel, ...args) {
const argsJson = JSON.stringify(args);
if (typeof globalThis.__gelectron_send === 'function') {
globalThis.__gelectron_send(channel, ...args);
}
this.emit(`send:${channel}`, ...args);
}
sendSync(channel, ...args) {
// Synchronous IPC is deprecated in Electron; return undefined
return undefined;
}
postMessage(channel, message, transfer) {
this.send(channel, message);
}
on(channel, listener) {
const wrappedListener = (event, ...args) => {
listener(event, ...args);
};
// Store wrapped reference for removeListener
if (!this._listeners.has(channel)) {
this._listeners.set(channel, new Map());
}
this._listeners.get(channel).set(listener, wrappedListener);
return super.on(channel, wrappedListener);
}
once(channel, listener) {
const wrappedListener = (event, ...args) => {
listener(event, ...args);
};
return super.once(channel, wrappedListener);
}
removeListener(channel, listener) {
const channelListeners = this._listeners.get(channel);
if (channelListeners) {
const wrapped = channelListeners.get(listener);
if (wrapped) {
channelListeners.delete(listener);
return super.removeListener(channel, wrapped);
}
}
return super.removeListener(channel, listener);
}
removeAllListeners(channel) {
this._listeners.delete(channel);
return super.removeAllListeners(channel);
}
// Internal method to receive messages from main process
_receiveMessage(channel, data) {
const event = new IpcRendererEvent(this, channel);
this.emit(channel, event, data);
}
// Internal method to resolve a pending invoke
_resolveInvoke(requestId, result) {
const pending = this._pending.get(requestId);
if (pending) {
this._pending.delete(requestId);
pending.resolve(result);
}
}
// Internal method to reject a pending invoke
_rejectInvoke(requestId, error) {
const pending = this._pending.get(requestId);
if (pending) {
this._pending.delete(requestId);
pending.reject(new Error(error));
}
}
}
const ipcRenderer = new IpcRenderer();
module.exports = { ipcRenderer, IpcRendererEvent };
@@ -1,183 +0,0 @@
'use strict';
/**
* Gelectron - menu module (Electron compatible)
*
* Supports native menu rendering through the Rust/tao bridge.
* When running in native mode, setApplicationMenu and popup route
* through the bridge for native OS menu rendering.
*/
const { EventEmitter } = require('events');
class MenuItem extends EventEmitter {
constructor(options = {}) {
super();
this.id = options.id || '';
this.label = options.label || '';
this.type = options.type || 'normal';
this.role = options.role || '';
this.accelerator = options.accelerator || '';
this.enabled = options.enabled !== false;
this.visible = options.visible !== false;
this.checked = options.checked || false;
this.submenu = options.submenu || null;
this.toolTip = options.toolTip || '';
this.icon = options.icon || null;
this._click = options.click || null;
// Recursively build submenu
if (this.submenu && !(this.submenu instanceof Menu)) {
if (Array.isArray(this.submenu)) {
this.submenu = Menu.buildFromTemplate(this.submenu);
} else {
this.submenu = Menu.buildFromTemplate(this.submenu.items || []);
}
}
}
click() {
if (this._click) {
this._click(this, null);
}
this.emit('click');
}
}
class Menu extends EventEmitter {
constructor() {
super();
this.items = [];
this._id = Menu._nextId++;
}
static _nextId = 1;
static _applicationMenu = null;
static buildFromTemplate(template) {
const menu = new Menu();
if (Array.isArray(template)) {
menu.items = template.map((item) => {
if (item instanceof MenuItem) return item;
if (item.type === 'separator') {
return new MenuItem({ type: 'separator' });
}
return new MenuItem(item);
});
}
return menu;
}
static getApplicationMenu() {
return Menu._applicationMenu || null;
}
static setApplicationMenu(menu) {
Menu._applicationMenu = menu;
if (menu) {
// Route through bridge if available
try {
const { bridge, isNative } = require('./native-bridge');
if (isNative && bridge) {
bridge._send({ type: 'set-application-menu', menu: menu._serialize() });
}
} catch (e) {
// Not available in all contexts
}
}
}
append(menuItem) {
if (menuItem instanceof MenuItem) {
this.items.push(menuItem);
} else {
this.items.push(new MenuItem(menuItem));
}
return this;
}
insert(menuItem, position) {
if (menuItem instanceof MenuItem) {
this.items.splice(position, 0, menuItem);
} else {
this.items.splice(position, 0, new MenuItem(menuItem));
}
return this;
}
popup(options = {}) {
try {
const { bridge, isNative } = require('./native-bridge');
if (isNative && bridge) {
bridge._send({ type: 'popup-menu', menu: this._serialize(), x: options.x, y: options.y });
return;
}
} catch (e) {}
console.log('[gelectron] Menu.popup called');
}
closePopup() {
try {
const { bridge, isNative } = require('./native-bridge');
if (isNative && bridge) {
bridge._send({ type: 'close-popup-menu' });
return;
}
} catch (e) {}
console.log('[gelectron] Menu.closePopup called');
}
getMenuItemById(id) {
return this._findMenuItemById(this.items, id);
}
_findMenuItemById(items, id) {
for (const item of items) {
if (item.id === id) return item;
if (item.submenu) {
const submenu = item.submenu instanceof Menu ? item.submenu.items : (item.submenu.items || item.submenu);
const found = this._findMenuItemById(submenu, id);
if (found) return found;
}
}
return null;
}
_serialize() {
return {
items: this.items.map((item) => {
const obj = {
id: item.id,
label: item.label,
type: item.type,
role: item.role,
accelerator: item.accelerator,
enabled: item.enabled,
visible: item.visible,
checked: item.checked,
toolTip: item.toolTip,
};
if (item.submenu instanceof Menu) {
obj.submenu = item.submenu._serialize();
} else if (item.submenu && Array.isArray(item.submenu)) {
obj.submenu = Menu.buildFromTemplate(item.submenu)._serialize();
}
return obj;
}),
};
}
items() {
return this.items;
}
getApplicationMenu() {
return Menu._applicationMenu;
}
setApplicationMenu(menu) {
Menu._applicationMenu = menu;
}
}
module.exports = { Menu, MenuItem };
@@ -1,142 +0,0 @@
'use strict';
/**
* Gelectron Native Bridge - IPC between Node.js and the Rust binary.
* When GELECTRON_NATIVE=1, communicates via stdin/stdout JSON lines.
*/
const readline = require('readline');
const { EventEmitter } = require('events');
const isNative = process.env.GELECTRON_NATIVE === '1';
class NativeBridge extends EventEmitter {
constructor() {
super();
this._ready = false;
this._readyCallbacks = [];
this._windowListeners = new Map();
if (isNative) {
this._setupStdio();
}
}
_setupStdio() {
const rl = readline.createInterface({ input: process.stdin, terminal: false });
rl.on('line', (line) => {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith('{')) return;
try {
const msg = JSON.parse(trimmed);
this._handleMessage(msg);
} catch (e) {
// Not IPC, ignore
}
});
process.stdout.on('error', () => {});
}
_handleMessage(msg) {
switch (msg.type) {
case 'ready':
this._ready = true;
for (const cb of this._readyCallbacks) cb();
this._readyCallbacks = [];
this.emit('ready');
break;
case 'window-closed':
this.emit('window-closed', msg.id);
break;
case 'window-focus':
this.emit('window-focus', msg.id);
break;
case 'ipc-message':
this.emit('ipc-message', msg.id, msg.channel, msg.data);
break;
}
}
_send(msg) {
if (!isNative) return;
try {
process.stdout.write(JSON.stringify(msg) + '\n');
} catch (e) {
// stdout may be closed
}
}
onReady(cb) {
if (this._ready) {
cb();
} else {
this._readyCallbacks.push(cb);
}
}
createWindow(id, options) {
this._send({ type: 'create-window', id, options });
}
loadUrl(id, url) {
this._send({ type: 'load-url', id, url });
}
loadFile(id, filePath) {
this._send({ type: 'load-file', id, path: filePath });
}
destroyWindow(id) {
this._send({ type: 'destroy-window', id });
}
setTitle(id, title) {
this._send({ type: 'set-title', id, title });
}
setSize(id, width, height) {
this._send({ type: 'set-size', id, width, height });
}
showWindow(id) {
this._send({ type: 'show', id });
}
hideWindow(id) {
this._send({ type: 'hide', id });
}
focusWindow(id) {
this._send({ type: 'focus', id });
}
minimizeWindow(id) {
this._send({ type: 'minimize', id });
}
maximizeWindow(id) {
this._send({ type: 'maximize', id });
}
closeWindow(id) {
this._send({ type: 'close', id });
}
sendToRenderer(id, channel, ...data) {
this._send({ type: 'ipc-message', id, channel, data: data.length === 1 ? data[0] : data });
}
evalJs(id, script) {
this._send({ type: 'eval-js', id, script });
}
quit() {
this._send({ type: 'quit' });
}
}
const bridge = new NativeBridge();
module.exports = { bridge, isNative };
@@ -1,117 +0,0 @@
'use strict';
/**
* Gelectron - nativeImage module (Electron compatible)
*/
class NativeImage {
constructor() {
this._isEmpty = true;
this._width = 0;
this._height = 0;
this._data = null;
}
static createFromPath(path) {
const img = new NativeImage();
try {
const fs = require('fs');
const data = fs.readFileSync(path);
// Basic PNG header parsing for dimensions
if (data.length > 24 && data[0] === 0x89 && data[1] === 0x50) {
img._width = data.readUInt32BE(16);
img._height = data.readUInt32BE(20);
img._data = data;
img._isEmpty = false;
}
} catch (err) {
console.error(`[gelectron] Failed to load image: ${path}`, err.message);
}
return img;
}
static createFromBuffer(buffer, options = {}) {
const img = new NativeImage();
if (buffer && buffer.length > 0) {
img._data = buffer;
img._isEmpty = false;
img._width = options.width || 0;
img._height = options.height || 0;
// Try to detect PNG dimensions
if (buffer.length > 24 && buffer[0] === 0x89 && buffer[1] === 0x50) {
img._width = buffer.readUInt32BE(16);
img._height = buffer.readUInt32BE(20);
}
}
return img;
}
static createFromDataURL(dataURL) {
const matches = dataURL.match(/^data:[^;]+;base64,(.+)$/);
if (matches) {
return NativeImage.createFromBuffer(Buffer.from(matches[1], 'base64'));
}
return NativeImage.createEmpty();
}
static createEmpty() {
return new NativeImage();
}
toPNG(options = {}) {
return this._data || Buffer.alloc(0);
}
toJPEG(quality) {
return this._data || Buffer.alloc(0);
}
toBitmap(options = {}) {
return this._data || Buffer.alloc(0);
}
toDataURL() {
if (!this._data) return 'data:,';
const base64 = this._data.toString('base64');
return `data:image/png;base64,${base64}`;
}
resize(options) {
const resized = new NativeImage();
resized._width = options.width || this._width;
resized._height = options.height || this._height;
resized._data = this._data;
resized._isEmpty = this._isEmpty;
return resized;
}
crop(rect) {
return this.resize({ width: rect.width, height: rect.height });
}
getBitmap(options = {}) {
return this._data || Buffer.alloc(0);
}
getSize() {
return { width: this._width, height: this._height };
}
isEmpty() {
return this._isEmpty;
}
setTemplateImage(template) {
this._isTemplate = template;
}
isTemplateImage() {
return this._isTemplate || false;
}
toPNG(options) { return this.toPNG(options); }
toJPEG(quality) { return this.toJPEG(quality); }
}
module.exports = NativeImage;
@@ -1,59 +0,0 @@
'use strict';
/**
* Gelectron - Notification module (Electron compatible)
*/
const { EventEmitter } = require('events');
class Notification extends EventEmitter {
static _notifications = new Map();
static _nextId = 1;
constructor(options = {}) {
super();
this.id = Notification._nextId++;
this.title = options.title || '';
this.body = options.body || '';
this.subtitle = options.subtitle || '';
this.silent = options.silent || false;
this.icon = options.icon || null;
this.urgency = options.urgency || 'normal';
this.timeoutType = options.timeoutType || 'default';
this.closeButtonText = options.closeButtonText || '';
this.toastXml = options.toastXml || '';
this.actions = options.actions || [];
this.replyPlaceholder = options.replyPlaceholder || '';
Notification._notifications.set(this.id, this);
}
static isSupported() {
return true;
}
show() {
if (typeof Notification !== 'undefined' && Notification.permission === 'granted') {
new globalThis.Notification(this.title, {
body: this.body,
icon: this.icon,
});
}
console.log(`[gelectron] Notification shown: ${this.title}`);
this.emit('show');
return true;
}
close() {
Notification._notifications.delete(this.id);
console.log(`[gelectron] Notification ${this.id} closed`);
this.emit('close');
}
static fromNotification(notification) {
return notification;
}
}
module.exports = { Notification };
@@ -1,156 +0,0 @@
'use strict';
/**
* Gelectron - Preload script injection
*
* This script is injected into the renderer process before the page loads.
* It provides the ipcRenderer and contextBridge APIs to preload scripts.
*/
function getPreloadScript() {
return `
(function() {
'use strict';
// Expose gelectron preload APIs
window.__gelectron = window.__gelectron || {};
// IPC Renderer stub
window.__gelectron.ipcRenderer = {
_requestId: 0,
_pending: {},
invoke: function(channel) {
var args = Array.prototype.slice.call(arguments, 1);
var requestId = 'ipc-' + (++this._requestId) + '-' + Date.now();
var message = {
type: 'invoke',
requestId: requestId,
channel: channel,
args: args
};
if (typeof window.__gelectron_send === 'function') {
window.__gelectron_send(JSON.stringify(message));
}
return new Promise(function(resolve, reject) {
window.__gelectron.ipcRenderer._pending[requestId] = { resolve: resolve, reject: reject };
setTimeout(function() {
if (window.__gelectron.ipcRenderer._pending[requestId]) {
delete window.__gelectron.ipcRenderer._pending[requestId];
reject(new Error('IPC invoke timed out for channel: ' + channel));
}
}, 30000);
});
},
send: function(channel) {
var args = Array.prototype.slice.call(arguments, 1);
var message = {
type: 'send',
channel: channel,
args: args
};
if (typeof window.__gelectron_send === 'function') {
window.__gelectron_send(JSON.stringify(message));
}
},
sendSync: function() { return undefined; },
postMessage: function(channel, message) { this.send(channel, message); },
on: function(channel, listener) {
var wrappedListener = function(event) {
listener(event, Array.prototype.slice.call(arguments, 1));
};
window.addEventListener('message', function(event) {
if (event.data && event.data.__gelectron_channel === channel) {
wrappedListener(event, event.data.args);
}
});
return this;
},
once: function(channel, listener) {
var self = this;
var wrappedListener = function(event) {
listener(event, Array.prototype.slice.call(arguments, 1));
self.removeListener(channel, listener);
};
return this.on(channel, wrappedListener);
},
removeListener: function(channel, listener) {
return this;
},
removeAllListeners: function(channel) {
return this;
},
_resolveInvoke: function(requestId, result) {
var pending = this._pending[requestId];
if (pending) {
delete this._pending[requestId];
pending.resolve(result);
}
},
_rejectInvoke: function(requestId, error) {
var pending = this._pending[requestId];
if (pending) {
delete this._pending[requestId];
pending.reject(new Error(error));
}
}
};
// Context Bridge API
window.__gelectron.contextBridge = {
exposeInMainWorld: function(key, api) {
if (typeof window[key] !== 'undefined') {
console.warn('[gelectron] contextBridge: window.' + key + ' already exists, skipping');
return;
}
Object.defineProperty(window, key, {
value: Object.freeze(JSON.parse(JSON.stringify(api))),
writable: false,
configurable: false,
enumerable: true
});
}
};
// Receive messages from main process
window.addEventListener('message', function(event) {
if (event.data && event.data.__gelectron_channel) {
var channel = event.data.__gelectron_channel;
var args = event.data.args;
// Resolve pending invoke
if (channel === '__gelectron_ipc_resolve') {
var requestId = args[0];
var result = args[1];
window.__gelectron.ipcRenderer._resolveInvoke(requestId, result);
} else if (channel === '__gelectron_ipc_reject') {
var requestId = args[0];
var error = args[1];
window.__gelectron.ipcRenderer._rejectInvoke(requestId, error);
} else {
// Regular IPC message from main process
window.__gelectron.ipcRenderer._receiveMessage(channel, args);
}
}
});
// Expose as global for preload scripts
window.electron = window.electron || {};
window.electron.ipcRenderer = window.__gelectron.ipcRenderer;
window.electron.contextBridge = window.__gelectron.contextBridge;
})();
`;
}
module.exports = { getPreloadScript };
@@ -1,74 +0,0 @@
'use strict';
/**
* Gelectron Runtime - Node.js fallback when native binary is not available.
* Loads and runs an Electron app's main process using the JS compatibility layer.
*/
const path = require('path');
const Module = require('module');
function run(mainScript, env) {
// Set up the environment
Object.assign(process.env, env);
// Patch require so that 'electron' resolves to gelectron's compat layer
const electronCompatPath = path.join(__dirname, 'index.js');
const ipcRendererPath = path.join(__dirname, 'ipc-renderer.js');
// Purge any cached 'electron' module from the real npm package so that
// our _resolveFilename patch takes absolute priority. The real 'electron'
// npm package (devDependency in the target app) exports a *path string*
// to the Electron binary it does NOT have .autoUpdater, .app, etc.
for (const key of Object.keys(Module._cache)) {
const normalized = key.replace(/\\/g, '/');
if (
normalized.endsWith('/electron') ||
normalized.endsWith('/electron/index.js') ||
normalized.endsWith('/electron/index.cjs')
) {
delete Module._cache[key];
}
}
const originalResolveFilename = Module._resolveFilename;
Module._resolveFilename = function (request, parent, isMain, options) {
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') {
return electronCompatPath;
}
if (request === 'electron/renderer') {
return ipcRendererPath;
}
return originalResolveFilename.call(this, request, parent, isMain, options);
};
// Also monkey-patch Module._resolveRequest for Node ≥ 22 where it may
// be used internally instead of _resolveFilename.
if (typeof Module._resolveRequest === 'function') {
const originalResolveRequest = Module._resolveRequest;
Module._resolveRequest = function (request, parent, isMain, options) {
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') {
return electronCompatPath;
}
if (request === 'electron/renderer') {
return ipcRendererPath;
}
return originalResolveRequest.call(this, request, parent, isMain, options);
};
}
// Pre-load the gelectron shim into the module cache so that every
// subsequent require('electron') hits cache immediately.
require(electronCompatPath);
// Require the app's main script
try {
require(mainScript);
} catch (err) {
console.error(`[gelectron] Error loading main script: ${mainScript}`);
console.error(err);
process.exit(1);
}
}
module.exports = { run };
@@ -1,30 +0,0 @@
'use strict';
/**
* Gelectron - safeStorage module (Electron compatible)
*/
const safeStorage = {
isAvailable() {
return true;
},
async encryptString(plaintext) {
// Basic obfuscation; in production, this routes through the native keyring
return Buffer.from(plaintext).toString('base64');
},
async decryptString(encrypted) {
return Buffer.from(encrypted, 'base64').toString('utf-8');
},
async encryptBuffer(buffer) {
return Buffer.from(buffer).toString('base64');
},
async decryptBuffer(encrypted) {
return Buffer.from(encrypted, 'base64');
},
};
module.exports = safeStorage;
@@ -1,100 +0,0 @@
'use strict';
/**
* Gelectron - shell module (Electron compatible)
*/
const { exec } = require('child_process');
const path = require('path');
const os = require('os');
const shell = {
openExternal(url, options = {}) {
const platform = os.platform();
let cmd;
if (platform === 'darwin') {
cmd = `open "${url}"`;
} else if (platform === 'win32') {
cmd = `start "" "${url}"`;
} else {
cmd = `xdg-open "${url}"`;
}
return new Promise((resolve, reject) => {
exec(cmd, (error) => {
if (error) reject(error);
else resolve();
});
});
},
openPath(path) {
return new Promise((resolve, reject) => {
const platform = os.platform();
let cmd;
if (platform === 'darwin') {
cmd = `open "${path}"`;
} else if (platform === 'win32') {
cmd = `start "" "${path}"`;
} else {
cmd = `xdg-open "${path}"`;
}
exec(cmd, (error) => {
if (error) reject(error.message);
else resolve('');
});
});
},
showItemInFolder(fullPath) {
const platform = os.platform();
let cmd;
if (platform === 'darwin') {
cmd = `open -R "${fullPath}"`;
} else if (platform === 'win32') {
cmd = `explorer /select,"${fullPath}"`;
} else {
const dir = path.dirname(fullPath);
cmd = `xdg-open "${dir}"`;
}
exec(cmd);
},
moveItemToTrash(fullPath) {
// Use fs.rm or platform-specific trash command
const fs = require('fs');
try {
fs.rmSync(fullPath, { recursive: true, force: true });
return Promise.resolve('');
} catch (err) {
return Promise.reject(err.message);
}
},
beep() {
process.stdout.write('\x07');
},
writeShortcutLink(shortcutPath, options = {}) {
console.log('[gelectron] writeShortcutLink called');
return true;
},
readShortcutLink(shortcutPath) {
return {
target: '',
cwd: '',
args: '',
description: '',
icon: '',
iconIndex: 0,
};
},
};
module.exports = shell;
@@ -1,74 +0,0 @@
'use strict';
/**
* Gelectron - Tray module (Electron compatible)
*/
const { EventEmitter } = require('events');
class Tray extends EventEmitter {
static _trays = new Map();
static _nextId = 1;
constructor(image) {
super();
this.id = Tray._nextId++;
this._image = image;
this._tooltip = '';
this._menu = null;
this._isDestroyed = false;
Tray._trays.set(this.id, this);
console.log(`[gelectron] Tray ${this.id} created`);
}
setToolTip(tooltip) {
this._tooltip = tooltip;
}
getToolTip() {
return this._tooltip;
}
setImage(image) {
this._image = image;
}
setPressedImage(image) {}
setContextMenu(menu) {
this._menu = menu;
}
getContextMenu() {
return this._menu;
}
popupContextMenu(options) {
console.log('[gelectron] Tray.popupContextMenu');
}
isDestroyed() {
return this._isDestroyed;
}
destroy() {
this._isDestroyed = true;
Tray._trays.delete(this.id);
this.emit('destroy');
}
getBounds() {
return { x: 0, y: 0, width: 22, height: 22 };
}
static fromId(id) {
return Tray._trays.get(id) || null;
}
static getAllTrays() {
return Array.from(Tray._trays.values());
}
}
module.exports = { Tray };
@@ -1,25 +0,0 @@
'use strict';
/**
* Gelectron - webContents utilities (Electron compatible)
*/
const { BrowserWindow } = require('./browser-window');
const webContents = {
getAllWebContents() {
return BrowserWindow.getAllWindows().map((win) => win.webContents);
},
getFocusedWebContents() {
const focused = BrowserWindow.getFocusedWindow();
return focused ? focused.webContents : null;
},
fromId(id) {
const win = BrowserWindow.fromId(id);
return win ? win.webContents : null;
},
};
module.exports = webContents;
@@ -1,706 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gelectron Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: #0f0f1a;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-bottom: 1px solid #2a2a4a;
padding: 24px 32px;
}
header h1 {
font-size: 28px;
font-weight: 700;
background: linear-gradient(90deg, #7b68ee, #00d4ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
header p {
margin-top: 6px;
color: #8888aa;
font-size: 14px;
}
main {
flex: 1;
padding: 32px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
align-content: start;
}
.card {
background: #1a1a2e;
border: 1px solid #2a2a4a;
border-radius: 12px;
padding: 24px;
}
.card h2 {
font-size: 16px;
font-weight: 600;
color: #7b68ee;
margin-bottom: 12px;
}
.card p, .card li {
font-size: 14px;
line-height: 1.7;
color: #b0b0cc;
}
.card ul {
list-style: none;
padding: 0;
}
.card ul li::before {
content: '\2713';
color: #00d4ff;
margin-right: 8px;
font-weight: bold;
}
.counter {
display: flex;
align-items: center;
gap: 16px;
margin-top: 8px;
}
.counter button {
background: #2a2a4a;
color: #e0e0e0;
border: 1px solid #3a3a6a;
border-radius: 8px;
padding: 10px 20px;
font-size: 18px;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
}
.counter button:hover { background: #3a3a6a; }
.counter button:active { transform: scale(0.95); }
.counter .value {
font-size: 36px;
font-weight: 700;
color: #00d4ff;
min-width: 60px;
text-align: center;
}
.canvas-wrap {
margin-top: 12px;
}
canvas {
background: #12121f;
border: 1px solid #2a2a4a;
border-radius: 8px;
display: block;
width: 100%;
height: 200px;
}
.color-bar {
display: flex;
gap: 8px;
margin-top: 12px;
}
.color-bar span {
flex: 1;
height: 32px;
border-radius: 6px;
}
#clock {
font-size: 32px;
font-weight: 300;
color: #00d4ff;
font-variant-numeric: tabular-nums;
margin-top: 4px;
}
.env-table {
width: 100%;
border-collapse: collapse;
margin-top: 8px;
}
.env-table td {
padding: 6px 0;
font-size: 13px;
border-bottom: 1px solid #1f1f35;
}
.env-table td:first-child {
color: #7b68ee;
font-weight: 600;
width: 40%;
}
.env-table td:last-child {
color: #b0b0cc;
font-family: 'SF Mono', Menlo, monospace;
}
.test-card { grid-column: 1 / -1; }
.test-results { margin-top: 12px; }
.test-row {
display: flex;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #1f1f35;
font-size: 13px;
}
.test-row:last-child { border-bottom: none; }
.test-pass { color: #6bcb77; }
.test-fail { color: #ff6b6b; }
.test-name { flex: 1; color: #b0b0cc; }
.test-value { color: #8888aa; font-family: 'SF Mono', Menlo, monospace; margin-left: 12px; }
.btn-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.btn-grid button {
background: #2a2a4a;
color: #e0e0e0;
border: 1px solid #3a3a6a;
border-radius: 8px;
padding: 8px 14px;
font-size: 13px;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
}
.btn-grid button:hover { background: #3a3a6a; }
.btn-grid button:active { transform: scale(0.95); }
.btn-grid button.active { background: #7b68ee; border-color: #7b68ee; }
.demo-output {
margin-top: 10px;
background: #12121f;
border: 1px solid #2a2a4a;
border-radius: 8px;
padding: 12px;
font-family: 'SF Mono', Menlo, monospace;
font-size: 12px;
color: #b0b0cc;
white-space: pre-wrap;
word-break: break-all;
max-height: 180px;
overflow-y: auto;
display: none;
}
footer {
padding: 16px 32px;
border-top: 1px solid #2a2a4a;
text-align: center;
font-size: 12px;
color: #555577;
}
</style>
</head>
<body>
<header>
<h1>Gelectron Demo</h1>
<p>HTML + CSS + JavaScript rendering via Gecko / Servo engine</p>
</header>
<main>
<div class="card">
<h2>Interactive Counter</h2>
<p>Proof that JavaScript runs and DOM updates work.</p>
<div class="counter">
<button onclick="dec()">\u2212</button>
<div class="value" id="count">0</div>
<button onclick="inc()">+</button>
</div>
</div>
<div class="card">
<h2>Live Clock</h2>
<p>requestAnimationFrame-driven rendering.</p>
<div id="clock">--:--:--</div>
</div>
<div class="card">
<h2>Canvas 2D</h2>
<p>Animated canvas with moving shapes.</p>
<div class="canvas-wrap">
<canvas id="scene" width="400" height="200"></canvas>
</div>
</div>
<div class="card">
<h2>Runtime Info</h2>
<p>Environment details from the gelectron shim.</p>
<table class="env-table">
<tr><td>Platform</td><td id="env-platform"></td></tr>
<tr><td>Arch</td><td id="env-arch"></td></tr>
<tr><td>Node.js</td><td id="env-node"></td></tr>
<tr><td>User Agent</td><td id="env-ua"></td></tr>
</table>
</div>
<div class="card" style="grid-column: 1 / -1;">
<h2>CSS Features</h2>
<p>Grid, gradients, border-radius, transitions, flexbox, backdrop styling.</p>
<div class="color-bar">
<span style="background: #7b68ee;"></span>
<span style="background: #00d4ff;"></span>
<span style="background: #ff6b9d;"></span>
<span style="background: #ffd93d;"></span>
<span style="background: #6bcb77;"></span>
<span style="background: #ee5a24;"></span>
</div>
</div>
<div class="card" style="grid-column: 1 / -1;">
<h2>App Module Demo</h2>
<p>Interactive demo of the Electron app API — properties, methods, and lifecycle.</p>
<div class="btn-grid">
<button onclick="demoApp('version')">getVersion()</button>
<button onclick="demoApp('locale')">getLocale()</button>
<button onclick="demoApp('userAgent')">getUserAgent()</button>
<button onclick="demoApp('setUserAgent')">setUserAgent()</button>
<button onclick="demoApp('appPath')">getAppPath()</button>
<button onclick="demoApp('isReady')">isReady</button>
<button onclick="demoApp('badge')">setBadgeCount(7)</button>
<button onclick="demoApp('metrics')">getAppMetrics()</button>
<button onclick="demoApp('gpu')">getGPUInfo()</button>
<button onclick="demoApp('gpuFeatures')">getGPUFeatureStatus()</button>
<button onclick="demoApp('loginItem')">getLoginItemSettings()</button>
<button onclick="demoApp('aboutPanel')">getAboutPanelOptions()</button>
<button onclick="demoApp('secureKeyboard')">setSecureKeyboardEntry()</button>
<button onclick="demoApp('name')">app.name =</button>
<button onclick="demoApp('paths')">getPath(home)</button>
<button onclick="demoApp('dock')">dock.bounce()</button>
</div>
<div class="demo-output" id="app-demo-output"></div>
</div>
<div class="card" style="grid-column: 1 / -1;">
<h2>Menu Module Demo</h2>
<p>Interactive demo of the Electron Menu API — build, set, and inspect native menus.</p>
<div class="btn-grid">
<button onclick="demoMenu('build')">Menu.buildFromTemplate()</button>
<button onclick="demoMenu('set')">Menu.setApplicationMenu()</button>
<button onclick="demoMenu('get')">Menu.getApplicationMenu()</button>
<button onclick="demoMenu('findById')">getMenuItemById()</button>
<button onclick="demoMenu('append')">menu.append()</button>
<button onclick="demoMenu('serialize')">menu._serialize()</button>
<button onclick="demoMenu('count')">menu.items.length</button>
<button onclick="demoMenu('submenu')">Submenu nesting</button>
</div>
<div class="demo-output" id="menu-demo-output"></div>
</div>
<div class="card test-card">
<h2>App Module Tests</h2>
<p>Automated tests for the Electron app compatibility layer.</p>
<div id="app-tests" class="test-results">
<p style="color: #8888aa; font-size: 13px;">Waiting for test results...</p>
</div>
</div>
</main>
<footer>Gelectron v0.1.0 &mdash; Electron API compatibility layer powered by Gecko/Servo</footer>
<script>
// Counter
var count = 0;
var countEl = document.getElementById('count');
function inc() { countEl.textContent = ++count; }
function dec() { countEl.textContent = --count; }
// Live clock
function tick() {
var now = new Date();
var h = String(now.getHours()).padStart(2, '0');
var m = String(now.getMinutes()).padStart(2, '0');
var s = String(now.getSeconds()).padStart(2, '0');
document.getElementById('clock').textContent = h + ':' + m + ':' + s;
requestAnimationFrame(tick);
}
tick();
// Canvas animation
var canvas = document.getElementById('scene');
var ctx = canvas.getContext('2d');
var t = 0;
function drawFrame() {
t += 0.02;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// bouncing ball
var bx = 50 + Math.sin(t) * 150;
var by = 100 + Math.cos(t * 1.3) * 60;
ctx.beginPath();
ctx.arc(bx, by, 18, 0, Math.PI * 2);
ctx.fillStyle = '#7b68ee';
ctx.fill();
// second ball
var bx2 = 200 + Math.cos(t * 0.7) * 120;
var by2 = 80 + Math.sin(t * 1.1) * 70;
ctx.beginPath();
ctx.arc(bx2, by2, 14, 0, Math.PI * 2);
ctx.fillStyle = '#00d4ff';
ctx.fill();
// trailing line
ctx.beginPath();
ctx.moveTo(0, 100 + Math.sin(t) * 40);
for (var x = 0; x < canvas.width; x += 4) {
ctx.lineTo(x, 100 + Math.sin(t + x * 0.03) * 40);
}
ctx.strokeStyle = 'rgba(123,104,238,0.3)';
ctx.lineWidth = 2;
ctx.stroke();
requestAnimationFrame(drawFrame);
}
drawFrame();
// Runtime info
document.getElementById('env-platform').textContent = navigator.platform || 'unknown';
document.getElementById('env-arch').textContent = 'unknown (browser sandbox)';
document.getElementById('env-node').textContent = 'N/A in renderer';
document.getElementById('env-ua').textContent = navigator.userAgent;
// App module interactive demo
function demoApp(action) {
var out = document.getElementById('app-demo-output');
out.style.display = 'block';
var electron = window.electron;
if (!electron || !electron.app) {
out.textContent = 'Error: window.electron.app not available (renderer-only mode)';
return;
}
var app = electron.app;
var result = '';
switch (action) {
case 'version':
result = 'app.getVersion() → ' + app.getVersion() +
'\napp.version (property) → ' + app.version;
break;
case 'locale':
result = 'app.getLocale() → ' + app.getLocale() +
'\napp.locale (property) → ' + app.locale;
break;
case 'userAgent':
result = 'app.getUserAgent() → ' + app.getUserAgent() +
'\napp.userAgent (property) → ' + app.userAgent;
break;
case 'setUserAgent':
app.setUserAgent('GelectronDemo/2.0');
result = 'app.setUserAgent("GelectronDemo/2.0")' +
'\napp.getUserAgent() → ' + app.getUserAgent();
app.setUserAgent(null);
result += '\napp.setUserAgent(null) reset → ' + app.getUserAgent();
break;
case 'appPath':
result = 'app.getAppPath() → ' + app.getAppPath() +
'\napp.appPath (property) → ' + app.appPath;
break;
case 'isReady':
result = 'app.isReady (property) → ' + app.isReady +
'\napp.isReady() method → ' + app.isReady() +
'\napp.whenReady() → [Promise resolved]';
app.whenReady().then(function() {
out.textContent += '\n→ whenReady promise resolved!';
});
break;
case 'badge':
app.setBadgeCount(7);
result = 'app.setBadgeCount(7)' +
'\napp.getBadgeCount() → ' + app.getBadgeCount();
app.setBadgeCount(0);
result += '\napp.setBadgeCount(0) → ' + app.getBadgeCount();
break;
case 'metrics':
var m = app.getAppMetrics();
result = 'app.getAppMetrics() → [' + m.length + ' entries]' +
'\n pid: ' + m[0].pid +
'\n type: ' + m[0].type +
'\n memory: ' + JSON.stringify(m[0].memory) +
'\n sandboxed: ' + m[0].sandboxed;
break;
case 'gpu':
app.getGPUInfo('basic').then(function(info) {
out.textContent = 'app.getGPUInfo("basic") →\n' +
' GPUActive: ' + info.GPUActive +
'\n gpuDevice: ' + JSON.stringify(info.gpuDevice, null, 2);
});
result = 'Loading GPU info...';
break;
case 'gpuFeatures':
var f = app.getGPUFeatureStatus();
var lines = ['app.getGPUFeatureStatus() →'];
for (var k in f) lines.push(' ' + k + ': ' + f[k]);
result = lines.join('\n');
break;
case 'loginItem':
var s = app.getLoginItemSettings();
result = 'app.getLoginItemSettings() →\n' +
' openAtLogin: ' + s.openAtLogin +
'\n openAsHidden: ' + s.openAsHidden +
'\n launchAtLogin: ' + s.launchAtLogin +
'\n launchItems: ' + JSON.stringify(s.launchItems);
break;
case 'aboutPanel':
app.setAboutPanelOptions({ applicationName: 'Gelectron Demo', copyright: 'MIT' });
var o = app.getAboutPanelOptions();
result = 'app.setAboutPanelOptions({ applicationName: "Gelectron Demo", copyright: "MIT" })' +
'\napp.getAboutPanelOptions() →\n' + JSON.stringify(o, null, 2);
break;
case 'secureKeyboard':
app.setSecureKeyboardEntryEnabled(true);
result = 'app.setSecureKeyboardEntryEnabled(true)' +
'\napp.isSecureKeyboardEntryEnabled() → ' + app.isSecureKeyboardEntryEnabled();
app.setSecureKeyboardEntryEnabled(false);
result += '\napp.setSecureKeyboardEntryEnabled(false)' +
'\napp.isSecureKeyboardEntryEnabled() → ' + app.isSecureKeyboardEntryEnabled();
break;
case 'name':
var old = app.name;
app.name = 'Gelectron Demo App';
result = 'app.name = "Gelectron Demo App"' +
'\napp.name → ' + app.name +
'\napp.getName() → ' + app.getName();
app.name = old;
result += '\n(app.name restored to "' + old + '")';
break;
case 'paths':
result = 'app.getPath("home") → ' + app.getPath('home') +
'\napp.getPath("userData") → ' + app.getPath('userData') +
'\napp.getPath("temp") → ' + app.getPath('temp') +
'\napp.getPath("desktop") → ' + app.getPath('desktop') +
'\napp.getPath("documents") → ' + app.getPath('documents') +
'\napp.getPath("downloads") → ' + app.getPath('downloads') +
'\napp.getPath("logs") → ' + app.getPath('logs');
break;
case 'dock':
if (app.dock) {
var id = app.dock.bounce();
result = 'app.dock.bounce() → ' + id +
'\napp.dock.isVisible() → ' + app.dock.isVisible() +
'\napp.dock.getBadge() → "' + app.dock.getBadge() + '"';
} else {
result = 'app.dock → null (not macOS)';
}
break;
}
out.textContent = result;
}
// Menu module interactive demo
function demoMenu(action) {
var out = document.getElementById('menu-demo-output');
out.style.display = 'block';
var electron = window.electron;
if (!electron || !electron.Menu) {
out.textContent = 'Error: window.electron.Menu not available';
return;
}
var Menu = electron.Menu;
var MenuItem = electron.MenuItem;
var result = '';
switch (action) {
case 'build': {
var menu = Menu.buildFromTemplate([
{ label: 'File', submenu: [
{ label: 'New', accelerator: 'CmdOrCtrl+N' },
{ label: 'Open', accelerator: 'CmdOrCtrl+O' },
{ type: 'separator' },
{ label: 'Quit', accelerator: 'CmdOrCtrl+Q' },
]},
{ label: 'Edit', submenu: [
{ label: 'Cut', accelerator: 'CmdOrCtrl+X' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V' },
]},
{ label: 'Help', submenu: [
{ label: 'About Gelectron' },
]},
]);
result = 'Menu.buildFromTemplate([...]) → Menu {\n' +
' items.length: ' + menu.items.length + '\n' +
' items[0].label: "' + menu.items[0].label + '"\n' +
' items[0].submenu.items.length: ' + menu.items[0].submenu.items.length + '\n' +
' items[1].label: "' + menu.items[1].label + '"\n' +
' items[2].label: "' + menu.items[2].label + '"\n' +
'}';
break;
}
case 'set': {
var menu = Menu.buildFromTemplate([
{ label: 'File', submenu: [
{ label: 'New Window', accelerator: 'CmdOrCtrl+N' },
{ type: 'separator' },
{ label: 'Quit', accelerator: 'CmdOrCtrl+Q' },
]},
{ label: 'View', submenu: [
{ label: 'Reload', accelerator: 'CmdOrCtrl+R' },
{ label: 'Toggle DevTools', accelerator: 'CmdOrCtrl+Shift+I' },
]},
]);
Menu.setApplicationMenu(menu);
var current = Menu.getApplicationMenu();
result = 'Menu.setApplicationMenu(menu) → sent to native bridge' +
'\nMenu.getApplicationMenu() → ' + (current ? 'Menu { items: ' + current.items.length + ' }' : 'null');
break;
}
case 'get': {
var current = Menu.getApplicationMenu();
if (current) {
var labels = current.items.map(function(i) { return i.label; });
result = 'Menu.getApplicationMenu() → Menu {\n' +
' items: [' + labels.join(', ') + ']\n' +
' items.length: ' + current.items.length + '\n' +
'}';
} else {
result = 'Menu.getApplicationMenu() → null\n(No application menu set yet. Click "Menu.setApplicationMenu()" first.)';
}
break;
}
case 'findById': {
var menu = Menu.buildFromTemplate([
{ id: 'file-menu', label: 'File', submenu: [
{ id: 'new-file', label: 'New File' },
{ id: 'open-file', label: 'Open File' },
]},
{ id: 'edit-menu', label: 'Edit', submenu: [
{ id: 'copy-item', label: 'Copy' },
]},
]);
var item = menu.getMenuItemById('new-file');
var item2 = menu.getMenuItemById('copy-item');
var missing = menu.getMenuItemById('nonexistent');
result = 'menu.getMenuItemById("new-file") →\n' +
' label: "' + (item ? item.label : 'null') + '"\n' +
' id: "' + (item ? item.id : 'null') + '"' +
'\nmenu.getMenuItemById("copy-item") →\n' +
' label: "' + (item2 ? item2.label : 'null') + '"' +
'\nmenu.getMenuItemById("nonexistent") → ' + (missing ? 'found' : 'null');
break;
}
case 'append': {
var menu = Menu.buildFromTemplate([
{ label: 'Existing Item' },
]);
result = 'Before append: items.length = ' + menu.items.length;
menu.append(new MenuItem({ label: 'Appended Item', type: 'normal' }));
result += '\nmenu.append(new MenuItem({ label: "Appended Item" }))' +
'\nAfter append: items.length = ' + menu.items.length +
'\n items[0].label: "' + menu.items[0].label + '"' +
'\n items[1].label: "' + menu.items[1].label + '"';
break;
}
case 'serialize': {
var menu = Menu.buildFromTemplate([
{ label: 'File', submenu: [
{ label: 'New', accelerator: 'CmdOrCtrl+N' },
{ type: 'separator' },
{ label: 'Quit' },
]},
{ label: 'Edit', enabled: false },
]);
var s = menu._serialize();
result = 'menu._serialize() →\n' + JSON.stringify(s, null, 2);
break;
}
case 'count': {
var menu = Menu.buildFromTemplate([
{ label: 'File', submenu: [{ label: 'New' }] },
{ label: 'Edit' },
{ label: 'View' },
{ label: 'Help' },
]);
result = 'Menu.buildFromTemplate(4 top-level items)' +
'\nmenu.items.length → ' + menu.items.length +
'\nmenu.items.map(i => i.label) → [' +
menu.items.map(function(i) { return '"' + i.label + '"'; }).join(', ') + ']' +
'\nmenu.items[0].submenu.items.length → ' + menu.items[0].submenu.items.length;
break;
}
case 'submenu': {
var menu = Menu.buildFromTemplate([
{ label: 'Format', submenu: [
{ label: 'Font', submenu: [
{ label: 'Bold', type: 'checkbox' },
{ label: 'Italic', type: 'checkbox' },
{ label: 'Underline', type: 'checkbox' },
]},
{ type: 'separator' },
{ label: 'Align Left' },
{ label: 'Align Center' },
{ label: 'Align Right' },
]},
]);
var fmt = menu.items[0];
result = 'Menu with nested submenus:' +
'\nmenu.items[0].label → "' + fmt.label + '"' +
'\nmenu.items[0].submenu.items.length → ' + fmt.submenu.items.length +
'\nmenu.items[0].submenu.items[0].label → "' + fmt.submenu.items[0].label + '"' +
'\nmenu.items[0].submenu.items[0].submenu.items.length → ' +
fmt.submenu.items[0].submenu.items.length +
'\n nested items: ' +
fmt.submenu.items[0].submenu.items.map(function(i) {
return '"' + i.label + '"';
}).join(', ');
break;
}
}
out.textContent = result;
}
// App test results renderer
function renderAppTests() {
var results = window.__appTestResults;
if (!results) return;
var el = document.getElementById('app-tests');
var html = '';
var pass = 0, fail = 0;
for (var i = 0; i < results.length; i++) {
var r = results[i];
var cls = r.status === 'PASS' ? 'test-pass' : 'test-fail';
var icon = r.status === 'PASS' ? '\u2713' : '\u2717';
if (r.status === 'PASS') pass++; else fail++;
html += '<div class="test-row">' +
'<span class="' + cls + '">' + icon + '</span>' +
'<span class="test-name">' + r.name + '</span>' +
'<span class="test-value">' + r.value + '</span>' +
'</div>';
}
html = '<div style="margin-bottom: 12px; font-size: 14px; font-weight: 600;">' +
'<span class="test-pass">' + pass + ' passed</span> &middot; ' +
'<span class="test-fail">' + fail + ' failed</span> &middot; ' +
(pass + fail) + ' total</div>' + html;
el.innerHTML = html;
}
</script>
</body>
</html>
@@ -1,488 +0,0 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
const results = [];
function test(name, fn) {
try {
const val = fn();
results.push({ name, status: 'PASS', value: String(val) });
} catch (e) {
results.push({ name, status: 'FAIL', value: e.message });
}
}
function asyncTest(name, fn) {
return fn().then((val) => {
results.push({ name, status: 'PASS', value: String(val) });
}).catch((e) => {
results.push({ name, status: 'FAIL', value: e.message });
});
}
let mainWindow = null;
// ─── Properties ────────────────────────────────────────────────
test('app.requestSingleInstanceLock() returns boolean', () => {
const lock = app.requestSingleInstanceLock();
if (typeof lock !== 'boolean') throw new Error('Expected boolean, got ' + typeof lock);
return lock;
});
test('app.isReady() (method)', () => app.isReady());
test('app.isReady (property)', () => {
if (typeof app.isReady !== 'boolean') throw new Error('Expected boolean, got ' + typeof app.isReady);
return app.isReady;
});
test('app.appPath (property)', () => {
if (typeof app.appPath !== 'string') throw new Error('Expected string, got ' + typeof app.appPath);
return app.appPath;
});
test('app.locale (property)', () => {
if (typeof app.locale !== 'string') throw new Error('Expected string, got ' + typeof app.locale);
return app.locale;
});
test('app.userAgent (property)', () => {
if (typeof app.userAgent !== 'string') throw new Error('Expected string, got ' + typeof app.userAgent);
return app.userAgent;
});
test('app.getVersion()', () => app.getVersion());
test('app.getName()', () => app.getName());
test('app.getAppPath()', () => app.getAppPath());
test('app.name get/set', () => {
const old = app.name;
app.name = 'TestApp';
const result = app.name;
app.name = old;
if (result !== 'TestApp') throw new Error('Expected TestApp, got ' + result);
return result;
});
test('app.name set null reverts', () => {
app.name = null;
return app.name === 'Gelectron App';
});
// ─── Path Methods ──────────────────────────────────────────────
test('app.getPath("home")', () => app.getPath('home'));
test('app.getPath("userData")', () => app.getPath('userData'));
test('app.getPath("temp")', () => app.getPath('temp'));
test('app.getPath("desktop")', () => app.getPath('desktop'));
test('app.getPath("documents")', () => app.getPath('documents'));
test('app.getPath("downloads")', () => app.getPath('downloads'));
test('app.getPath("logs")', () => app.getPath('logs'));
test('app.getPath("crashDumps")', () => app.getPath('crashDumps'));
test('app.getPath("app") returns userData', () => {
return app.getPath('app') === app.getPath('userData');
});
test('app.setPath / app.getPath roundtrip', () => {
app.setPath('testPath', '/tmp/test-gelectron');
return app.getPath('testPath') === '/tmp/test-gelectron';
});
test('app.getPath("unknown") returns userData fallback', () => {
return app.getPath('nonexistent') === app.getPath('userData');
});
// ─── User Agent / Locale ───────────────────────────────────────
test('app.getLocale()', () => app.getLocale());
test('app.getUserAgent()', () => app.getUserAgent());
test('app.setUserAgent()', () => {
app.setUserAgent('TestAgent/1.0');
const result = app.getUserAgent();
app.setUserAgent(null);
return result;
});
test('app.setUserAgent(null) resets', () => {
app.setUserAgent(null);
const ua = app.getUserAgent();
if (!ua.includes('Gelectron')) throw new Error('Expected Gelectron UA, got ' + ua);
return true;
});
// ─── Lifecycle ─────────────────────────────────────────────────
test('app.isPackaged is boolean', () => {
if (typeof app.isPackaged !== 'boolean') throw new Error('Expected boolean');
return app.isPackaged;
});
test('app.whenReady() returns Promise', () => {
const p = app.whenReady();
if (!p || typeof p.then !== 'function') throw new Error('Not a Promise');
return true;
});
test('app.requestSingleInstanceLock() sync', () => {
const result = app.requestSingleInstanceLock();
return typeof result === 'boolean';
});
test('app.acquireSingleInstanceLock() sync', () => {
const result = app.acquireSingleInstanceLock();
return typeof result === 'boolean';
});
test('app.releaseSingleInstanceLock() no-op', () => {
app.releaseSingleInstanceLock();
return true;
});
// ─── Events ────────────────────────────────────────────────────
test('app.on / app.removeListener', () => {
let called = false;
const fn = () => { called = true; };
app.on('test-event', fn);
app.emit('test-event');
app.removeListener('test-event', fn);
return called;
});
test('app.once fires once', () => {
let count = 0;
app.once('test-once', () => { count++; });
app.emit('test-once');
app.emit('test-once');
return count === 1;
});
test('app.addListener returns this', () => {
const fn = () => {};
const result = app.addListener('test-chain', fn);
app.removeListener('test-chain', fn);
return result === app;
});
test('app.emit("activate") does not crash', () => {
app.emit('activate');
return true;
});
test('app.emit("focus") does not crash', () => {
app.emit('focus');
return true;
});
test('app.emit("blur") does not crash', () => {
app.emit('blur');
return true;
});
test('app.emit("browser-window-focus") does not crash', () => {
app.emit('browser-window-focus');
return true;
});
test('app.emit("browser-window-blur") does not crash', () => {
app.emit('browser-window-blur');
return true;
});
test('app.emit("web-contents-created") does not crash', () => {
app.emit('web-contents-created', {}, {});
return true;
});
// ─── Dock (macOS) ──────────────────────────────────────────────
test('app.dock exists (macOS)', () => {
if (process.platform === 'darwin') {
if (!app.dock) throw new Error('app.dock is null on macOS');
return typeof app.dock;
}
return 'N/A (not macOS)';
});
test('app.dock.bounce', () => {
if (process.platform === 'darwin') return app.dock.bounce();
return 'N/A';
});
test('app.dock.setBadgeCount / getBadgeCount', () => {
if (process.platform === 'darwin') {
app.dock.setBadgeCount(5);
return app.dock.getBadgeCount();
}
return 'N/A';
});
// ─── Command Line ──────────────────────────────────────────────
test('app.commandLine.appendSwitch', () => {
app.commandLine.appendSwitch('test-switch', 'test-value');
return app.commandLine.hasSwitch('test-switch');
});
test('app.commandLine.getSwitch', () => app.commandLine.getSwitch('test-switch'));
test('app.commandLine.removeSwitch', () => {
app.commandLine.removeSwitch('test-switch');
return !app.commandLine.hasSwitch('test-switch');
});
// ─── Badge ─────────────────────────────────────────────────────
test('app.setBadgeCount / getBadgeCount', () => {
const result = app.setBadgeCount(3);
if (app.getBadgeCount() !== 3) throw new Error('Expected 3, got ' + app.getBadgeCount());
app.setBadgeCount(0);
return result;
});
test('app.setBadgeCount(0) resets', () => {
app.setBadgeCount(5);
app.setBadgeCount(0);
return app.getBadgeCount() === 0;
});
// ─── Metrics / GPU ────────────────────────────────────────────
test('app.getAppMetrics() returns array', () => {
const metrics = app.getAppMetrics();
if (!Array.isArray(metrics)) throw new Error('Expected array');
if (metrics.length === 0) throw new Error('Expected at least one entry');
const m = metrics[0];
if (typeof m.pid !== 'number') throw new Error('Expected pid to be number');
if (!m.memory) throw new Error('Expected memory object');
return metrics.length + ' entries';
});
test('app.getGPUInfo("basic")', async () => {
const info = await app.getGPUInfo('basic');
if (!info || !info.gpuDevice) throw new Error('Missing gpuDevice');
return info.GPUActive;
});
test('app.getGPUInfo("complete")', async () => {
const info = await app.getGPUInfo('complete');
if (!info || !info.gpuDriver) throw new Error('Missing gpuDriver');
return info.gpuDriver;
});
test('app.getGPUFeatureStatus()', () => {
const status = app.getGPUFeatureStatus();
if (!status || typeof status.gpuCompositing !== 'string') throw new Error('Missing gpuCompositing');
return Object.keys(status).length + ' features';
});
test('app.disableHardwareAcceleration() no-op', () => {
app.disableHardwareAcceleration();
return true;
});
test('app.disableDomainBlockingFor3DAPIs() no-op', () => {
app.disableDomainBlockingFor3DAPIs();
return true;
});
// ─── Login Items ───────────────────────────────────────────────
test('app.getLoginItemSettings()', () => {
const s = app.getLoginItemSettings();
if (typeof s.openAtLogin !== 'boolean') throw new Error('Missing openAtLogin');
if (!s.hasOwnProperty('launchAtLogin')) throw new Error('Missing launchAtLogin');
return true;
});
test('app.setLoginItemSettings() no-op', () => {
app.setLoginItemSettings({ openAtLogin: true });
return true;
});
// ─── About Panel ───────────────────────────────────────────────
test('app.setAboutPanelOptions / getAboutPanelOptions', () => {
app.setAboutPanelOptions({ applicationName: 'Test', copyright: 'MIT' });
const opts = app.getAboutPanelOptions();
if (opts.applicationName !== 'Test') throw new Error('Expected Test, got ' + opts.applicationName);
return opts.copyright;
});
test('app.setAboutPanelOptions() with no args resets', () => {
app.setAboutPanelOptions();
const opts = app.getAboutPanelOptions();
return typeof opts === 'object';
});
// ─── Recent Documents ──────────────────────────────────────────
test('app.addRecentDocument() no-op', () => { app.addRecentDocument('/tmp/test'); return true; });
test('app.clearRecentDocuments() no-op', () => { app.clearRecentDocuments(); return true; });
test('app.setRecentDocumentLabel() no-op', () => { app.setRecentDocumentLabel('Test'); return true; });
// ─── App User Model ID ────────────────────────────────────────
test('app.setAppUserModelId() no-op', () => { app.setAppUserModelId('com.test'); return true; });
test('app.getAppUserModelId()', () => {
const result = app.getAppUserModelId();
if (typeof result !== 'string') throw new Error('Expected string');
return result === '' ? '(empty)' : result;
});
// ─── Applications Folder ──────────────────────────────────────
test('app.isInApplicationsFolder()', () => app.isInApplicationsFolder());
test('app.moveToApplicationsFolder() no-op', () => { app.moveToApplicationsFolder(); return true; });
// ─── File Icons ────────────────────────────────────────────────
test('app.getFileIcon() callback', (done) => {
return new Promise((resolve) => {
app.getFileIcon('/tmp', (err, icon) => {
resolve(true);
});
});
});
// ─── Certificate Trust ────────────────────────────────────────
test('app.showCertificateTrustDialog()', async () => {
await app.showCertificateTrustDialog();
return true;
});
// ─── Secure Keyboard Entry ────────────────────────────────────
test('app.setSecureKeyboardEntryEnabled / isSecureKeyboardEntryEnabled', () => {
app.setSecureKeyboardEntryEnabled(true);
const result = app.isSecureKeyboardEntryEnabled();
app.setSecureKeyboardEntryEnabled(false);
return result;
});
// ─── Window Count ──────────────────────────────────────────────
test('app.getWindowCount() initial', () => {
return app.getWindowCount();
});
test('app.getPath("appData")', () => app.getPath('appData'));
test('app.getPath("exe")', () => app.getPath('exe'));
test('app.getPath("module")', () => app.getPath('module'));
// ─── Launch App ────────────────────────────────────────────────
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
}
});
app.whenReady().then(() => {
test('app.whenReady() resolved', () => true);
mainWindow = new BrowserWindow({
width: 900,
height: 680,
title: 'Gelectron Demo',
backgroundColor: '#1a1a2e',
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
},
});
test('BrowserWindow created', () => mainWindow.id > 0);
test('BrowserWindow.getSize()', () => {
const [w, h] = mainWindow.getSize();
if (w < 100 || h < 100) throw new Error('Size too small: ' + w + 'x' + h);
return w + 'x' + h;
});
test('BrowserWindow.getTitle()', () => mainWindow.getTitle());
test('BrowserWindow.isVisible()', () => mainWindow.isVisible());
test('BrowserWindow.isDestroyed()', () => !mainWindow.isDestroyed());
test('BrowserWindow.isNormal()', () => mainWindow.isNormal());
test('BrowserWindow.isResizable()', () => mainWindow.isResizable());
test('BrowserWindow.getAllWindows().length', () => BrowserWindow.getAllWindows().length);
test('BrowserWindow.fromId(id)', () => {
const win = BrowserWindow.fromId(mainWindow.id);
return win ? win.id === mainWindow.id : false;
});
test('window-all-closed listener registered', () => {
app.on('window-all-closed', () => {
app.quit();
});
return true;
});
test('app.relaunch is a function', () => typeof app.relaunch === 'function');
test('app.exit is a function', () => typeof app.exit === 'function');
test('app.quit is a function', () => typeof app.quit === 'function');
test('app.getWindowCount() after create', () => app.getWindowCount());
test('app.getPathForProtocol()', () => {
const result = app.getPathForProtocol('https:');
return result === null ? 'null' : result;
});
test('app.getApplicationInfoForProtocol()', async () => {
const result = await app.getApplicationInfoForProtocol('https:');
return typeof result === 'object';
});
mainWindow.loadFile(path.join(__dirname, 'index.html'));
mainWindow.webContents.on('did-finish-load', () => {
const testResults = JSON.stringify(results);
mainWindow.webContents.executeJavaScript(
`window.__appTestResults = ${testResults}; renderAppTests();`
);
});
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
mainWindow.on('closed', () => {
mainWindow = null;
});
});
app.on('window-all-closed', () => {
app.quit();
});
}
@@ -1,6 +0,0 @@
{
"name": "gelectron-demo",
"version": "1.0.0",
"description": "Minimal demo for gelectron — HTML, CSS, JavaScript rendering",
"main": "main.js"
}
@@ -1,339 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Resources/app/index.html</key>
<data>
sG0CkHSfK6EH/aUgSy18Gzs2drg=
</data>
<key>Resources/app/main.js</key>
<data>
e16s5rQbnpal4xcLf8x/NhGbS2g=
</data>
<key>Resources/app/package.json</key>
<data>
Y7NDit/zkULoNATi0P/tZR+DKxM=
</data>
</dict>
<key>files2</key>
<dict>
<key>MacOS/compat/app.js</key>
<dict>
<key>cdhash</key>
<data>
1lkepb5Ez0HGGLSXwgy8EDApOdE=
</data>
<key>requirement</key>
<string>cdhash H"d6591ea5be44cf41c618b497c20cbc10302939d1"</string>
</dict>
<key>MacOS/compat/auto-updater.js</key>
<dict>
<key>cdhash</key>
<data>
6hr81wA+Q4cJzyBNArOA45tEkVY=
</data>
<key>requirement</key>
<string>cdhash H"ea1afcd7003e438709cf204d02b380e39b449156"</string>
</dict>
<key>MacOS/compat/browser-window.js</key>
<dict>
<key>cdhash</key>
<data>
qAdLtfb7mpG6gR6xmy9gCKs6FBE=
</data>
<key>requirement</key>
<string>cdhash H"a8074bb5f6fb9a91ba811eb19b2f6008ab3a1411"</string>
</dict>
<key>MacOS/compat/context-bridge.js</key>
<dict>
<key>cdhash</key>
<data>
KafHH/4oRUwwDuZqwipWOhxM268=
</data>
<key>requirement</key>
<string>cdhash H"29a7c71ffe28454c300ee66ac22a563a1c4cdbaf"</string>
</dict>
<key>MacOS/compat/dialog.js</key>
<dict>
<key>cdhash</key>
<data>
SXCSgmcZ/vl0xxt9pd2zVWxEYjU=
</data>
<key>requirement</key>
<string>cdhash H"497092826719fef974c71b7da5ddb3556c446235"</string>
</dict>
<key>MacOS/compat/index.js</key>
<dict>
<key>cdhash</key>
<data>
Nn0CHTP29YJYvNdElbvNoaIx+rc=
</data>
<key>requirement</key>
<string>cdhash H"367d021d33f6f58258bcd74495bbcda1a231fab7"</string>
</dict>
<key>MacOS/compat/ipc-main.js</key>
<dict>
<key>cdhash</key>
<data>
16s5WbfI+qEVIALfGZh0f3mwIz4=
</data>
<key>requirement</key>
<string>cdhash H"d7ab3959b7c8faa1152002df1998747f79b0233e"</string>
</dict>
<key>MacOS/compat/ipc-renderer.js</key>
<dict>
<key>cdhash</key>
<data>
3kHQy1UIFXY4M9e+jjEaGf+4PZA=
</data>
<key>requirement</key>
<string>cdhash H"de41d0cb550815763833d7be8e311a19ffb83d90"</string>
</dict>
<key>MacOS/compat/menu.js</key>
<dict>
<key>cdhash</key>
<data>
mG00gq46cpyWtov3xc27eZWOsAQ=
</data>
<key>requirement</key>
<string>cdhash H"986d3482ae3a729c96b68bf7c5cdbb79958eb004"</string>
</dict>
<key>MacOS/compat/native-bridge.js</key>
<dict>
<key>cdhash</key>
<data>
Rb9fSRqUVjk1KzuBt3EGHUE31s8=
</data>
<key>requirement</key>
<string>cdhash H"45bf5f491a945639352b3b81b771061d4137d6cf"</string>
</dict>
<key>MacOS/compat/native-image.js</key>
<dict>
<key>cdhash</key>
<data>
eutIvrwoL/WLitX7ZSm7Qzo+hRo=
</data>
<key>requirement</key>
<string>cdhash H"7aeb48bebc282ff58b8ad5fb6529bb433a3e851a"</string>
</dict>
<key>MacOS/compat/notification.js</key>
<dict>
<key>cdhash</key>
<data>
tzWUf6RK0ECImeqn3BvgQnZxJiI=
</data>
<key>requirement</key>
<string>cdhash H"b735947fa44ad0408899eaa7dc1be04276712622"</string>
</dict>
<key>MacOS/compat/preload-loader.js</key>
<dict>
<key>cdhash</key>
<data>
2pD8SFhJ1ghYKM9eDCVn5UnRamo=
</data>
<key>requirement</key>
<string>cdhash H"da90fc485849d6085828cf5e0c2567e549d16a6a"</string>
</dict>
<key>MacOS/compat/runtime.js</key>
<dict>
<key>cdhash</key>
<data>
OtT9xp7FBV99spiMrCjN4POHTD8=
</data>
<key>requirement</key>
<string>cdhash H"3ad4fdc69ec5055f7db2988cac28cde0f3874c3f"</string>
</dict>
<key>MacOS/compat/safe-storage.js</key>
<dict>
<key>cdhash</key>
<data>
C6FIL05pKS9uwt8HzfQNaDijvw4=
</data>
<key>requirement</key>
<string>cdhash H"0ba1482f4e69292f6ec2df07cdf40d6838a3bf0e"</string>
</dict>
<key>MacOS/compat/shell.js</key>
<dict>
<key>cdhash</key>
<data>
pdQ3SzT0lEAMd4rkCN40O3QlWWo=
</data>
<key>requirement</key>
<string>cdhash H"a5d4374b34f494400c778ae408de343b7425596a"</string>
</dict>
<key>MacOS/compat/tray.js</key>
<dict>
<key>cdhash</key>
<data>
AoFk0BeiEzIY9sU1Oh+PQwgduZM=
</data>
<key>requirement</key>
<string>cdhash H"028164d017a2133218f6c5353a1f8f43081db993"</string>
</dict>
<key>MacOS/compat/web-contents.js</key>
<dict>
<key>cdhash</key>
<data>
KLiqJQByq2M9brxskLOF0YX1jkk=
</data>
<key>requirement</key>
<string>cdhash H"28b8aa250072ab633d6ebc6c90b385d185f58e49"</string>
</dict>
<key>MacOS/compat/webview-bundle.js</key>
<dict>
<key>cdhash</key>
<data>
XJO9bSeIHn+ykzJJLFGKXj1bL1c=
</data>
<key>requirement</key>
<string>cdhash H"5c93bd6d27881e7fb29332492c518a5e3d5b2f57"</string>
</dict>
<key>MacOS/gelectron-bin</key>
<dict>
<key>cdhash</key>
<data>
jhz++m3PyWCRtj/c/AJ/ZTdlXio=
</data>
<key>requirement</key>
<string>cdhash H"8e1cfefa6dcfc96091b63fdcfc027f6537655e2a"</string>
</dict>
<key>MacOS/node</key>
<dict>
<key>cdhash</key>
<data>
nhZUTCVVb73fdsZ0dHft7VPJ7e8=
</data>
<key>requirement</key>
<string>cdhash H"9e16544c25556fbddf76c6747477eded53c9edef"</string>
</dict>
<key>Resources/app/index.html</key>
<dict>
<key>hash2</key>
<data>
q1YGzqi9XhTXA9N6blykkfje2XPlK7ODkCZ2za6Umjc=
</data>
</dict>
<key>Resources/app/main.js</key>
<dict>
<key>hash2</key>
<data>
oRbv77Ab5dvdtFjb5nLPDG3VkG2mnGDabYl/xdgnjqg=
</data>
</dict>
<key>Resources/app/package.json</key>
<dict>
<key>hash2</key>
<data>
Gkj33l7Q2sTuSFNjF+J+1cp5p3yOfGwHvM8MQI89/bw=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^Resources/</key>
<true/>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>
+3
View File
@@ -23,3 +23,6 @@ rfd = "0.15"
png = "0.18"
base64 = "0.22"
url = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.26"
+440
View File
@@ -31,6 +31,8 @@ enum ToRust {
SetTitle { id: u32, title: String },
#[serde(rename = "set-size")]
SetSize { id: u32, width: u32, height: u32 },
#[serde(rename = "set-app-icon")]
SetAppIcon { icon: String },
#[serde(rename = "show")]
Show { id: u32 },
#[serde(rename = "hide")]
@@ -146,6 +148,8 @@ struct WindowOpts {
always_on_top: Option<bool>,
#[serde(default)]
fullscreen: Option<bool>,
#[serde(default)]
icon: Option<String>,
}
struct WindowPair {
@@ -161,6 +165,15 @@ struct AppState {
node_exited: Arc<AtomicBool>,
bundle_js: Option<String>,
response_tx: Option<mpsc::Sender<(String, serde_json::Value)>>,
app_icon: Option<DecodedIcon>,
}
#[derive(Clone)]
struct DecodedIcon {
raw: Vec<u8>,
rgba: Vec<u8>,
width: u32,
height: u32,
}
impl AppState {
@@ -172,6 +185,7 @@ impl AppState {
node_exited,
bundle_js: None,
response_tx: None,
app_icon: None,
}
}
@@ -406,6 +420,7 @@ require('{}');
let mut state = AppState::new(node_exited.clone());
state.node_stdin = Some(child_stdin);
state.response_tx = Some(response_tx);
detect_and_apply_icon(&app_path, &mut state);
let state = Rc::new(RefCell::new(state));
state.borrow_mut().send_to_node(&ToNode::Ready);
@@ -503,6 +518,7 @@ window.__gelectron_run_main(`{}`);
let mut app_state = AppState::new(node_exited.clone());
app_state.bundle_js = Some(bundle_js.clone());
app_state.response_tx = Some(response_tx);
detect_and_apply_icon(&app_path, &mut app_state);
let state = Rc::new(RefCell::new(app_state));
event_loop.run(move |event, event_loop_target, control_flow| {
@@ -731,6 +747,299 @@ fn detect_dark_mode() -> bool {
}
}
fn decode_png_icon(png_bytes: &[u8]) -> Option<DecodedIcon> {
use std::io::Cursor;
let cursor = Cursor::new(png_bytes);
let decoder = png::Decoder::new(cursor);
let mut reader = decoder.read_info().ok()?;
let info = reader.info().clone();
let width = info.width;
let height = info.height;
if width == 0 || height == 0 || width * height > 4096 * 4096 {
return None;
}
let mut buf = vec![0; reader.output_buffer_size().unwrap_or(0)];
let info = reader.next_frame(&mut buf).ok()?;
let rgba = match info.color_type {
png::ColorType::Rgba => buf[..(width as usize * height as usize * 4)].to_vec(),
png::ColorType::Rgb => {
let mut out = Vec::with_capacity(width as usize * height as usize * 4);
for px in buf[..(width as usize * height as usize * 3)].chunks_exact(3) {
out.extend_from_slice(&[px[0], px[1], px[2], 255]);
}
out
}
png::ColorType::Grayscale => {
let mut out = Vec::with_capacity(width as usize * height as usize * 4);
for &g in buf.iter().take(width as usize * height as usize) {
out.extend_from_slice(&[g, g, g, 255]);
}
out
}
png::ColorType::GrayscaleAlpha => {
let mut out = Vec::with_capacity(width as usize * height as usize * 4);
for px in buf[..(width as usize * height as usize * 2)].chunks_exact(2) {
out.extend_from_slice(&[px[0], px[0], px[0], px[1]]);
}
out
}
_ => return None,
};
Some(DecodedIcon {
raw: png_bytes.to_vec(),
rgba,
width,
height,
})
}
fn decode_ico_bmp(data: &[u8], raw: Vec<u8>) -> Option<DecodedIcon> {
if data.len() < 40 {
return None;
}
let width = i32::from_le_bytes([data[4], data[5], data[6], data[7]]);
let height = i32::from_le_bytes([data[8], data[9], data[10], data[11]]);
let bit_count = u16::from_le_bytes([data[14], data[15]]);
let compression = u32::from_le_bytes([data[16], data[17], data[18], data[19]]);
if width <= 0 || height == 0 || compression != 0 || (bit_count != 32 && bit_count != 24) {
return None;
}
let mut height = height.unsigned_abs();
if bit_count == 24 && height >= 2 && height % 2 == 0 {
height /= 2;
}
let width = width as usize;
let height = height as usize;
if width == 0 || height == 0 || width * height > 4096 * 4096 {
return None;
}
let bpp = bit_count as usize / 8;
let stride = (width * bpp + 3) & !3;
let px_start = 40usize;
let mut rgba = vec![0u8; width * height * 4];
for y in 0..height {
let src_row = px_start + (height - 1 - y) * stride;
if src_row + width * bpp > data.len() {
return None;
}
for x in 0..width {
let si = src_row + x * bpp;
let di = (y * width + x) * 4;
rgba[di] = data[si + 2];
rgba[di + 1] = data[si + 1];
rgba[di + 2] = data[si];
rgba[di + 3] = if bit_count == 32 { data[si + 3] } else { 255 };
}
}
Some(DecodedIcon {
raw,
rgba,
width: width as u32,
height: height as u32,
})
}
fn decode_ico_icon(bytes: &[u8]) -> Option<DecodedIcon> {
if bytes.len() < 6 {
return None;
}
let count = u16::from_le_bytes([bytes[2], bytes[3]]) as usize;
if count == 0 || 6 + 16 * count > bytes.len() {
return None;
}
let mut best: Option<(u64, usize, usize)> = None;
for i in 0..count {
let e = 6 + 16 * i;
let w = if bytes[e] == 0 { 256 } else { bytes[e] as usize };
let h = if bytes[e + 1] == 0 { 256 } else { bytes[e + 1] as usize };
let size = u32::from_le_bytes([bytes[e + 8], bytes[e + 9], bytes[e + 10], bytes[e + 11]]) as usize;
let offset =
u32::from_le_bytes([bytes[e + 12], bytes[e + 13], bytes[e + 14], bytes[e + 15]]) as usize;
let area = (w * h) as u64;
if best.map_or(true, |(a, _, _)| area > a) {
best = Some((area, size, offset));
}
}
let (_, size, offset) = best?;
if offset + size > bytes.len() {
return None;
}
let data = &bytes[offset..offset + size];
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
return decode_png_icon(data).map(|mut ic| {
ic.raw = bytes.to_vec();
ic
});
}
decode_ico_bmp(data, bytes.to_vec())
}
fn decode_icon_bytes(bytes: &[u8]) -> Option<DecodedIcon> {
if let Some(icon) = decode_png_icon(bytes) {
return Some(icon);
}
if let Some(icon) = decode_ico_icon(bytes) {
return Some(icon);
}
#[cfg(target_os = "macos")]
if bytes.starts_with(b"icns") {
return Some(DecodedIcon {
raw: bytes.to_vec(),
rgba: Vec::new(),
width: 0,
height: 0,
});
}
None
}
fn decode_base64_icon(base64_str: &str) -> Option<DecodedIcon> {
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(base64_str.trim())
.ok()?;
decode_icon_bytes(&bytes)
}
fn find_app_icon_file(app_path: &std::path::Path) -> Option<PathBuf> {
let mut candidates: Vec<PathBuf> = vec![];
for name in [
"icon.png",
"app.png",
"icon.icns",
"icon.ico",
"app.ico",
"assets/icon.png",
"assets/app.png",
"build/icon.png",
"build/icon.icns",
"build/icon.ico",
"resources/icon.png",
"resources/icon.icns",
"resources/icon.ico",
"public/icon.png",
"public/icons/icon.png",
"static/icon.png",
] {
candidates.push(app_path.join(name));
}
if let Ok(pkg) = std::fs::read_to_string(app_path.join("package.json")) {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&pkg) {
if let Some(s) = value.get("icon").and_then(|v| v.as_str()) {
candidates.push(app_path.join(s));
}
if let Some(build) = value.get("build").and_then(|v| v.get("icon")) {
if let Some(s) = build.as_str() {
candidates.push(app_path.join(s));
}
}
}
}
candidates
.into_iter()
.find(|p| p.exists() && p.is_file())
}
fn apply_dock_icon(icon: &DecodedIcon) {
#[cfg(target_os = "macos")]
{
use cocoa::appkit::{NSApplication, NSImage};
use cocoa::base::{id, nil};
use cocoa::foundation::NSData;
let img_bytes = if !icon.rgba.is_empty() {
rgba_to_png(&icon.rgba, icon.width, icon.height).unwrap_or_else(|| icon.raw.clone())
} else {
icon.raw.clone()
};
unsafe {
let data = NSData::dataWithBytes_length_(
nil,
img_bytes.as_ptr() as *const std::ffi::c_void,
img_bytes.len() as u64,
);
let image: id = NSImage::initWithData_(NSImage::alloc(nil), data);
if image != nil {
let app = NSApplication::sharedApplication(nil);
let _: () = app.setApplicationIconImage_(image);
}
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = icon;
}
}
fn rgba_to_png(rgba: &[u8], width: u32, height: u32) -> Option<Vec<u8>> {
use std::io::Cursor;
let mut out = Cursor::new(Vec::new());
let mut encoder = png::Encoder::new(&mut out, width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().ok()?;
writer.write_image_data(rgba).ok()?;
drop(writer);
Some(out.into_inner())
}
fn apply_window_icon(window: &tao::window::Window, icon: &DecodedIcon) {
#[cfg(target_os = "macos")]
{
let _ = window;
let _ = icon;
}
#[cfg(not(target_os = "macos"))]
{
if let Ok(tao_icon) = tao::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) {
window.set_window_icon(Some(tao_icon));
}
}
}
fn detect_and_apply_icon(app_path: &std::path::Path, st: &mut AppState) {
if st.app_icon.is_some() {
return;
}
let Some(icon_path) = find_app_icon_file(app_path) else {
return;
};
let is_icns = icon_path.extension().map(|e| e == "icns").unwrap_or(false);
if is_icns {
#[cfg(target_os = "macos")]
{
apply_dock_icon_from_path(&icon_path);
log::info!("Loaded app icon from {}", icon_path.display());
}
#[cfg(not(target_os = "macos"))]
{
let _ = icon_path;
}
return;
}
if let Ok(bytes) = std::fs::read(&icon_path) {
if let Some(icon) = decode_icon_bytes(&bytes) {
st.app_icon = Some(icon.clone());
apply_dock_icon(&icon);
log::info!("Loaded app icon from {}", icon_path.display());
}
}
}
#[cfg(target_os = "macos")]
fn apply_dock_icon_from_path(path: &std::path::Path) {
use cocoa::appkit::{NSApplication, NSImage};
use cocoa::base::{id, nil};
use cocoa::foundation::NSString;
unsafe {
let ns_path = NSString::alloc(nil).init_str(&path.display().to_string());
let image: id = NSImage::initWithContentsOfFile_(NSImage::alloc(nil), ns_path);
if image != nil {
let app = NSApplication::sharedApplication(nil);
let _: () = app.setApplicationIconImage_(image);
}
}
}
fn handle_to_rust(
msg: ToRust,
st: &mut AppState,
@@ -793,6 +1102,17 @@ fn handle_to_rust(
{
Ok(webview) => {
let wid = window.id();
if let Some(icon) = options
.icon
.as_deref()
.and_then(decode_base64_icon)
{
st.app_icon = Some(icon.clone());
apply_dock_icon(&icon);
apply_window_icon(&window, &icon);
} else if let Some(icon) = st.app_icon.clone() {
apply_window_icon(&window, &icon);
}
st.windows.insert(id, WindowPair { window, webview });
st.window_wids.insert(wid, id);
log::info!("Window {} ready", id);
@@ -1211,6 +1531,56 @@ fn handle_to_rust(
}
}
}
ToRust::SetTitle { id, title } => {
if let Some(pair) = st.windows.get(&id) {
pair.window.set_title(&title);
}
}
ToRust::SetSize { id, width, height } => {
if let Some(pair) = st.windows.get(&id) {
pair.window.set_inner_size(tao::dpi::LogicalSize::new(
width as f64,
height as f64,
));
}
}
ToRust::SetAppIcon { icon } => {
if let Some(decoded) = decode_base64_icon(&icon) {
st.app_icon = Some(decoded.clone());
apply_dock_icon(&decoded);
for pair in st.windows.values() {
apply_window_icon(&pair.window, &decoded);
}
log::info!("Set app icon");
} else {
log::warn!("SetAppIcon: could not decode icon");
}
}
ToRust::Show { id } => {
if let Some(pair) = st.windows.get(&id) {
pair.window.set_visible(true);
}
}
ToRust::Hide { id } => {
if let Some(pair) = st.windows.get(&id) {
pair.window.set_visible(false);
}
}
ToRust::Focus { id } => {
if let Some(pair) = st.windows.get(&id) {
pair.window.set_focus();
}
}
ToRust::Minimize { id } => {
if let Some(pair) = st.windows.get(&id) {
pair.window.set_minimized(true);
}
}
ToRust::Maximize { id } => {
if let Some(pair) = st.windows.get(&id) {
pair.window.set_maximized(true);
}
}
_ => {}
}
}
@@ -1228,3 +1598,73 @@ fn which_node() -> Option<String> {
None
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ico_32bpp_bmp_decode() {
let w = 4usize;
let pixels: [[u8; 4]; 4] = [
[255, 0, 0, 255],
[0, 255, 0, 128],
[0, 0, 255, 64],
[255, 255, 255, 0],
];
let mut bmp = vec![0u8; 40];
bmp[4..8].copy_from_slice(&(w as i32).to_le_bytes());
bmp[8..12].copy_from_slice(&(w as i32).to_le_bytes());
bmp[14..16].copy_from_slice(&32u16.to_le_bytes());
for y in (0..w).rev() {
for x in 0..w {
let p = pixels[y];
bmp.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
}
}
let mut ico = vec![0u8; 6];
ico[2..4].copy_from_slice(&1u16.to_le_bytes());
ico.extend_from_slice(&[w as u8, w as u8, 0, 0]);
ico.extend_from_slice(&1u16.to_le_bytes());
ico.extend_from_slice(&32u16.to_le_bytes());
ico.extend_from_slice(&(bmp.len() as u32).to_le_bytes());
ico.extend_from_slice(&22u32.to_le_bytes());
ico.extend_from_slice(&bmp);
let icon = decode_ico_icon(&ico).expect("should decode 32bpp BMP-in-ICO");
assert_eq!(icon.width, w as u32);
assert_eq!(icon.height, w as u32);
for y in 0..w {
for x in 0..w {
let i = (y * w + x) * 4;
assert_eq!(&icon.rgba[i..i + 4], &pixels[y]);
}
}
}
#[test]
fn png_in_ico_decode() {
let mut png = Vec::new();
{
let mut enc = png::Encoder::new(&mut png, 2, 2);
enc.set_color(png::ColorType::Rgba);
enc.set_depth(png::BitDepth::Eight);
let mut w = enc.write_header().unwrap();
w.write_image_data(&[10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255])
.unwrap();
}
let mut ico = vec![0u8; 6];
ico[2..4].copy_from_slice(&1u16.to_le_bytes());
ico.extend_from_slice(&[0, 0, 0, 0]);
ico.extend_from_slice(&1u16.to_le_bytes());
ico.extend_from_slice(&32u16.to_le_bytes());
ico.extend_from_slice(&(png.len() as u32).to_le_bytes());
ico.extend_from_slice(&22u32.to_le_bytes());
ico.extend_from_slice(&png);
let icon = decode_ico_icon(&ico).expect("should decode PNG-in-ICO");
assert_eq!(icon.width, 2);
assert_eq!(icon.height, 2);
assert_eq!(&icon.rgba[..4], &[10, 20, 30, 255]);
}
}
+33 -2
View File
@@ -12,6 +12,8 @@
const { EventEmitter } = require('events');
const path = require('path');
const { bridge } = require('./native-bridge');
const NativeImage = require('./native-image');
class App extends EventEmitter {
constructor() {
@@ -24,7 +26,7 @@ class App extends EventEmitter {
this._badgeCount = 0;
this._secureKeyboardEntryEnabled = false;
this._userAgent = null;
this._name = 'Gelectron App';
this._name = this._detectAppName();
this._names = null;
const os = require('os');
@@ -45,7 +47,10 @@ class App extends EventEmitter {
this._commandLine = new Map();
this._dock = process.platform === 'darwin' ? {
setIcon: (icon) => {},
setIcon: (icon) => {
const b64 = this._iconToBase64Png(icon);
if (b64) bridge.setAppIcon(b64);
},
bounce: () => 0,
cancelBounce: () => {},
setBadge: () => {},
@@ -72,6 +77,32 @@ class App extends EventEmitter {
});
}
_detectAppName() {
try {
const appDir = process.env.GELECTRON_APP_PATH || process.cwd();
const pkg = require(path.join(appDir, 'package.json'));
return pkg.productName || pkg.name || 'Gelectron App';
} catch (e) {
return 'Gelectron App';
}
}
_iconToBase64Png(icon) {
try {
let img = icon;
if (typeof icon === 'string') {
img = NativeImage.createFromPath(icon);
}
if (img && typeof img.toPNG === 'function' && !img.isEmpty()) {
const png = img.toPNG();
if (png && png.length > 0) return png.toString('base64');
}
} catch (e) {
// Ignore invalid icon
}
return null;
}
// ─── Properties ──────────────────────────────────────────────
get commandLine() {
+19 -1
View File
@@ -8,6 +8,7 @@ const { EventEmitter } = require('events');
const path = require('path');
const { bridge, isNative } = require('./native-bridge');
const { app } = require('./app');
const NativeImage = require('./native-image');
class WebContents extends EventEmitter {
constructor(id) {
@@ -219,6 +220,7 @@ class BrowserWindow extends EventEmitter {
resizable: this._options.resizable,
alwaysOnTop: this._options.alwaysOnTop,
fullscreen: this._options.fullscreen,
icon: this._iconToBase64Png(this._options.icon),
});
}
@@ -232,6 +234,23 @@ class BrowserWindow extends EventEmitter {
}
}
_iconToBase64Png(icon) {
if (!icon) return null;
try {
let img = icon;
if (typeof icon === 'string') {
img = NativeImage.createFromPath(icon);
}
if (img && typeof img.toPNG === 'function' && !img.isEmpty()) {
const png = img.toPNG();
if (png && png.length > 0) return png.toString('base64');
}
} catch (e) {
// Ignore invalid icon
}
return null;
}
static fromWebContents(webContents) {
for (const win of BrowserWindow._windows.values()) {
if (win.webContents && win.webContents.id === webContents.id) return win;
@@ -270,7 +289,6 @@ class BrowserWindow extends EventEmitter {
if (isNative) bridge.showWindow(this.id);
this.emit('show');
}
hide() {
if (this._isDestroyed) return;
this._isVisible = false;
+4
View File
@@ -152,6 +152,10 @@ class NativeBridge extends EventEmitter {
quit() {
this._send({ type: 'quit' });
}
setAppIcon(base64Png) {
this._send({ type: 'set-app-icon', icon: base64Png });
}
}
const bridge = new NativeBridge();
+13 -6
View File
@@ -17,12 +17,22 @@ class NativeImage {
try {
const fs = require('fs');
const data = fs.readFileSync(path);
// Basic PNG header parsing for dimensions
img._data = data;
img._isEmpty = false;
if (data.length > 24 && data[0] === 0x89 && data[1] === 0x50) {
img._width = data.readUInt32BE(16);
img._height = data.readUInt32BE(20);
img._data = data;
img._isEmpty = false;
} else if (data.length > 22 && data[0] === 0x00 && data[1] === 0x00 && data[2] === 0x01 && data[3] === 0x00) {
img._width = data[6] || 256;
img._height = data[7] || 256;
} else if (data.length > 12 && data[0] === 0x69 && data[1] === 0x63 && data[2] === 0x6e && data[3] === 0x73) {
const type = data.toString('ascii', 8, 12);
const c = type.charCodeAt(1);
if (c >= 0x30 && c <= 0x39) {
img._width = img._height = 1 << (c - 0x30);
} else if (type[0] === 'p' && c >= 0x34 && c <= 0x38) {
img._width = img._height = 16 << (c - 0x34);
}
}
} catch (err) {
console.error(`[gelectron] Failed to load image: ${path}`, err.message);
@@ -109,9 +119,6 @@ class NativeImage {
isTemplateImage() {
return this._isTemplate || false;
}
toPNG(options) { return this.toPNG(options); }
toJPEG(quality) { return this.toJPEG(quality); }
}
module.exports = NativeImage;