mirror of
https://github.com/mileswolfallen2/gelectron.git
synced 2026-09-08 12:43:16 +00:00
update
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/gelectron-core"]
|
members = ["crates/gelectron-core", "crates/gelectron-app"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
+5
-5
@@ -113,8 +113,8 @@ if (process.env.GELECTRON_LOG) {
|
|||||||
console.log(`[gelectron] native addon: ${nativeAddonPath || 'not found (using build target)'}`);
|
console.log(`[gelectron] native addon: ${nativeAddonPath || 'not found (using build target)'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rustBin = path.join(nativeDir, 'target', 'release', 'gelectron');
|
const rustBin = path.join(__dirname, '..', 'target', 'release', 'gelectron');
|
||||||
const rustBinDebug = path.join(nativeDir, 'target', 'debug', 'gelectron');
|
const rustBinDebug = path.join(__dirname, '..', 'target', 'debug', 'gelectron');
|
||||||
|
|
||||||
let executable;
|
let executable;
|
||||||
if (fs.existsSync(rustBin)) {
|
if (fs.existsSync(rustBin)) {
|
||||||
@@ -124,15 +124,15 @@ if (fs.existsSync(rustBin)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (executable) {
|
if (executable) {
|
||||||
const child = spawn(executable, args.slice(1), {
|
console.log(`[gelectron] Using native binary: ${executable}`);
|
||||||
|
const child = spawn(executable, args, {
|
||||||
env,
|
env,
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
});
|
});
|
||||||
child.on('exit', (code) => process.exit(code || 0));
|
child.on('exit', (code) => process.exit(code || 0));
|
||||||
} else {
|
} else {
|
||||||
console.log(`[gelectron] Native binary not found. Running main script directly via Node.js.`);
|
console.log(`[gelectron] Native binary not found. Run: cargo build --release -p gelectron`);
|
||||||
console.log(`[gelectron] Build with: cargo build --release -p gelectron-core`);
|
|
||||||
console.log(`[gelectron] Falling back to Node.js runtime...\n`);
|
console.log(`[gelectron] Falling back to Node.js runtime...\n`);
|
||||||
|
|
||||||
require('../src/electron/runtime.js').run(mainScript, env);
|
require('../src/electron/runtime.js').run(mainScript, env);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "gelectron"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Gelectron - Firefox-engine alternative to Electron"
|
||||||
|
license = "MIT"
|
||||||
|
authors = ["mileswa1q22"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "gelectron"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
env_logger = { workspace = true }
|
||||||
|
wry = "0.47"
|
||||||
|
tao = "0.30"
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::thread;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tao::event::{Event, StartCause, WindowEvent};
|
||||||
|
use tao::event_loop::{ControlFlow, EventLoopBuilder};
|
||||||
|
use tao::window::{Fullscreen, WindowBuilder, WindowId};
|
||||||
|
use wry::{WebView, WebViewBuilder};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
enum ToRust {
|
||||||
|
#[serde(rename = "create-window")]
|
||||||
|
CreateWindow { id: u32, options: WindowOpts },
|
||||||
|
#[serde(rename = "load-url")]
|
||||||
|
LoadUrl { id: u32, url: String },
|
||||||
|
#[serde(rename = "load-file")]
|
||||||
|
LoadFile { id: u32, path: String },
|
||||||
|
#[serde(rename = "destroy-window")]
|
||||||
|
DestroyWindow { id: u32 },
|
||||||
|
#[serde(rename = "set-title")]
|
||||||
|
SetTitle { id: u32, title: String },
|
||||||
|
#[serde(rename = "set-size")]
|
||||||
|
SetSize { id: u32, width: u32, height: u32 },
|
||||||
|
#[serde(rename = "show")]
|
||||||
|
Show { id: u32 },
|
||||||
|
#[serde(rename = "hide")]
|
||||||
|
Hide { id: u32 },
|
||||||
|
#[serde(rename = "focus")]
|
||||||
|
Focus { id: u32 },
|
||||||
|
#[serde(rename = "minimize")]
|
||||||
|
Minimize { id: u32 },
|
||||||
|
#[serde(rename = "maximize")]
|
||||||
|
Maximize { id: u32 },
|
||||||
|
#[serde(rename = "close")]
|
||||||
|
Close { id: u32 },
|
||||||
|
#[serde(rename = "ipc-message")]
|
||||||
|
IpcMessage {
|
||||||
|
id: u32,
|
||||||
|
channel: String,
|
||||||
|
data: serde_json::Value,
|
||||||
|
},
|
||||||
|
#[serde(rename = "eval-js")]
|
||||||
|
EvalJs { id: u32, script: String },
|
||||||
|
#[serde(rename = "quit")]
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
enum ToNode {
|
||||||
|
#[serde(rename = "window-closed")]
|
||||||
|
WindowClosed { id: u32 },
|
||||||
|
#[serde(rename = "window-focus")]
|
||||||
|
WindowFocus { id: u32 },
|
||||||
|
#[serde(rename = "ipc-message")]
|
||||||
|
IpcMessage {
|
||||||
|
id: u32,
|
||||||
|
channel: String,
|
||||||
|
data: serde_json::Value,
|
||||||
|
},
|
||||||
|
#[serde(rename = "ready")]
|
||||||
|
Ready,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
|
||||||
|
struct WindowOpts {
|
||||||
|
#[serde(default)]
|
||||||
|
width: Option<u32>,
|
||||||
|
#[serde(default)]
|
||||||
|
height: Option<u32>,
|
||||||
|
#[serde(default)]
|
||||||
|
title: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
show: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
resizable: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
always_on_top: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
fullscreen: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WindowPair {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
window: tao::window::Window,
|
||||||
|
webview: WebView,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
windows: HashMap<u32, WindowPair>,
|
||||||
|
window_wids: HashMap<WindowId, u32>,
|
||||||
|
node_stdin: Option<std::process::ChildStdin>,
|
||||||
|
node_exited: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
fn new(node_exited: Arc<AtomicBool>) -> Self {
|
||||||
|
Self {
|
||||||
|
windows: HashMap::new(),
|
||||||
|
window_wids: HashMap::new(),
|
||||||
|
node_stdin: None,
|
||||||
|
node_exited,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_to_node(&mut self, msg: &ToNode) {
|
||||||
|
let json = serde_json::to_string(msg).unwrap();
|
||||||
|
if let Some(ref mut stdin) = self.node_stdin {
|
||||||
|
if writeln!(stdin, "{}", json).is_err() || stdin.flush().is_err() {
|
||||||
|
// Node.js process has likely exited; mark it so the event loop can shut down
|
||||||
|
self.node_exited.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preload_script() -> String {
|
||||||
|
r#"
|
||||||
|
(function() {
|
||||||
|
if (window.__gelectron_loaded) return;
|
||||||
|
window.__gelectron_loaded = true;
|
||||||
|
window.__ipc_pending = [];
|
||||||
|
window.gelectron = {
|
||||||
|
send: function(channel, ...args) {
|
||||||
|
window.__ipc_pending.push(JSON.stringify({channel: channel, args: args}));
|
||||||
|
},
|
||||||
|
receive: function(channel, callback) {
|
||||||
|
window.addEventListener('message', function(e) {
|
||||||
|
if (e.source === window && typeof e.data === 'string') {
|
||||||
|
try {
|
||||||
|
var msg = JSON.parse(e.data);
|
||||||
|
if (msg.__from_node && msg.channel === channel) {
|
||||||
|
callback(msg.data);
|
||||||
|
}
|
||||||
|
} catch(err) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
"#
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
if args.len() < 2 {
|
||||||
|
eprintln!("Usage: gelectron <path-to-app>");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_path = std::path::PathBuf::from(&args[1]);
|
||||||
|
if !app_path.exists() {
|
||||||
|
eprintln!("Error: path not found: {}", app_path.display());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonicalize so relative paths (e.g. "demo/") become absolute.
|
||||||
|
// Node.js -e resolves require() relative to [eval], not cwd.
|
||||||
|
let app_path = std::fs::canonicalize(&app_path).unwrap_or(app_path);
|
||||||
|
|
||||||
|
let main_script = if app_path.is_dir() {
|
||||||
|
let pkg_path = app_path.join("package.json");
|
||||||
|
if pkg_path.exists() {
|
||||||
|
let pkg: serde_json::Value =
|
||||||
|
serde_json::from_str(&std::fs::read_to_string(&pkg_path).unwrap_or_default())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let main = pkg
|
||||||
|
.get("main")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("index.js");
|
||||||
|
app_path.join(main)
|
||||||
|
} else {
|
||||||
|
app_path.join("index.js")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app_path.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if !main_script.exists() {
|
||||||
|
eprintln!("Error: main script not found: {}", main_script.display());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let gelectron_dir = std::env::current_exe()
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| std::fs::canonicalize(p).ok())
|
||||||
|
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// From target/release/ or target/debug/, go up to project root
|
||||||
|
let project_root = if gelectron_dir.ends_with("release") || gelectron_dir.ends_with("debug") {
|
||||||
|
gelectron_dir.parent()
|
||||||
|
.and_then(|p| p.parent())
|
||||||
|
.map(|p| p.to_path_buf())
|
||||||
|
.unwrap_or_else(|| gelectron_dir.clone())
|
||||||
|
} else {
|
||||||
|
gelectron_dir.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let compat_dir = if project_root.join("src/electron/index.js").exists() {
|
||||||
|
project_root.join("src/electron")
|
||||||
|
} else if project_root.join("crates/gelectron-app/src/main.rs").exists() {
|
||||||
|
project_root.join("src/electron")
|
||||||
|
} else {
|
||||||
|
// npm installed: try to find in node_modules
|
||||||
|
project_root.join("node_modules/gelectron/src/electron")
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!("App: {}", app_path.display());
|
||||||
|
log::info!("Script: {}", main_script.display());
|
||||||
|
log::info!("Compat: {}", compat_dir.display());
|
||||||
|
|
||||||
|
let node_path = which_node().unwrap_or_else(|| {
|
||||||
|
eprintln!("Error: node not found in PATH");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let setup_script = format!(
|
||||||
|
r#"
|
||||||
|
process.env.GELECTRON_NATIVE = '1';
|
||||||
|
process.env.GELECTRON_MAIN_SCRIPT = '{}';
|
||||||
|
process.env.GELECTRON_APP_PATH = '{}';
|
||||||
|
|
||||||
|
const Module = require('module');
|
||||||
|
const path = require('path');
|
||||||
|
const compatPath = '{}';
|
||||||
|
|
||||||
|
// Purge any cached 'electron' module from the real npm package
|
||||||
|
Object.keys(Module._cache).forEach(function(key) {{
|
||||||
|
var normalized = key.replace(/\\/g, '/');
|
||||||
|
if (normalized.endsWith('/electron') || normalized.endsWith('/electron/index.js') || normalized.endsWith('/electron/index.cjs')) {{
|
||||||
|
delete Module._cache[key];
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
const origResolve = Module._resolveFilename;
|
||||||
|
Module._resolveFilename = function(request, parent, isMain, options) {{
|
||||||
|
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') return path.join(compatPath, 'index.js');
|
||||||
|
if (request === 'electron/renderer') return path.join(compatPath, 'ipc-renderer.js');
|
||||||
|
return origResolve.call(this, request, parent, isMain, options);
|
||||||
|
}};
|
||||||
|
|
||||||
|
// Also patch _resolveRequest for Node >= 22
|
||||||
|
if (typeof Module._resolveRequest === 'function') {{
|
||||||
|
var origResolveRequest = Module._resolveRequest;
|
||||||
|
Module._resolveRequest = function(request, parent, isMain, options) {{
|
||||||
|
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') return path.join(compatPath, 'index.js');
|
||||||
|
if (request === 'electron/renderer') return path.join(compatPath, 'ipc-renderer.js');
|
||||||
|
return origResolveRequest.call(this, request, parent, isMain, options);
|
||||||
|
}};
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Pre-load the gelectron shim so require('electron') hits cache
|
||||||
|
require(path.join(compatPath, 'index.js'));
|
||||||
|
|
||||||
|
require('{}');
|
||||||
|
"#,
|
||||||
|
main_script.display().to_string().replace('\\', "\\\\").replace('\'', "\\'"),
|
||||||
|
app_path.display().to_string().replace('\\', "\\\\").replace('\'', "\\'"),
|
||||||
|
compat_dir.display().to_string().replace('\\', "\\\\").replace('\'', "\\'"),
|
||||||
|
main_script.display().to_string().replace('\\', "\\\\").replace('\'', "\\'"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut child: Child = Command::new(&node_path)
|
||||||
|
.arg("-e")
|
||||||
|
.arg(&setup_script)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to spawn Node.js");
|
||||||
|
|
||||||
|
let child_stdin = child.stdin.take().unwrap();
|
||||||
|
let child_stdout = child.stdout.take().unwrap();
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel::<ToRust>();
|
||||||
|
let node_exited = Arc::new(AtomicBool::new(false));
|
||||||
|
let node_exited_clone = node_exited.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let reader = BufReader::new(child_stdout);
|
||||||
|
for line in reader.lines() {
|
||||||
|
match line {
|
||||||
|
Ok(line) => {
|
||||||
|
let trimmed = line.trim().to_string();
|
||||||
|
if trimmed.is_empty() || !trimmed.starts_with('{') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(msg) = serde_json::from_str::<ToRust>(&trimmed) {
|
||||||
|
if tx.send(msg).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Node.js process stdout closed – the child has exited
|
||||||
|
node_exited_clone.store(true, Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
|
||||||
|
let event_loop = EventLoopBuilder::new().build();
|
||||||
|
let mut state = AppState::new(node_exited.clone());
|
||||||
|
state.node_stdin = Some(child_stdin);
|
||||||
|
let state = Rc::new(RefCell::new(state));
|
||||||
|
|
||||||
|
state.borrow_mut().send_to_node(&ToNode::Ready);
|
||||||
|
|
||||||
|
event_loop.run(move |event, event_loop_target, control_flow| {
|
||||||
|
*control_flow = ControlFlow::Poll;
|
||||||
|
let mut st = state.borrow_mut();
|
||||||
|
|
||||||
|
// If the Node.js child process has exited, shut down
|
||||||
|
if st.node_exited.load(Ordering::SeqCst) {
|
||||||
|
log::info!("Node.js process exited, shutting down");
|
||||||
|
st.windows.clear();
|
||||||
|
st.window_wids.clear();
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::NewEvents(StartCause::Poll) => {
|
||||||
|
while let Ok(msg) = rx.try_recv() {
|
||||||
|
match msg {
|
||||||
|
ToRust::CreateWindow { id, options } => {
|
||||||
|
let mut wb = WindowBuilder::new()
|
||||||
|
.with_title(options.title.clone().unwrap_or_else(|| "Gelectron".into()))
|
||||||
|
.with_inner_size(tao::dpi::LogicalSize::new(
|
||||||
|
options.width.unwrap_or(800) as f64,
|
||||||
|
options.height.unwrap_or(600) as f64,
|
||||||
|
));
|
||||||
|
if let Some(r) = options.resizable { wb = wb.with_resizable(r); }
|
||||||
|
if let Some(a) = options.always_on_top { wb = wb.with_always_on_top(a); }
|
||||||
|
if let Some(true) = options.fullscreen {
|
||||||
|
wb = wb.with_fullscreen(Some(Fullscreen::Borderless(None)));
|
||||||
|
}
|
||||||
|
wb = wb.with_visible(options.show.unwrap_or(true));
|
||||||
|
|
||||||
|
match wb.build(event_loop_target) {
|
||||||
|
Ok(window) => {
|
||||||
|
let url = options.url.unwrap_or_else(|| "about:blank".into());
|
||||||
|
log::info!("Creating window {} - '{}'", id, url);
|
||||||
|
match WebViewBuilder::new()
|
||||||
|
.with_url(&url)
|
||||||
|
.with_initialization_script(&preload_script())
|
||||||
|
.with_devtools(true)
|
||||||
|
.build(&window)
|
||||||
|
{
|
||||||
|
Ok(webview) => {
|
||||||
|
let wid = window.id();
|
||||||
|
st.windows.insert(id, WindowPair { window, webview });
|
||||||
|
st.window_wids.insert(wid, id);
|
||||||
|
log::info!("Window {} ready", id);
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("WebView error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Window error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToRust::LoadUrl { id, url } => {
|
||||||
|
log::info!("Loading url in window {}: {}", id, url);
|
||||||
|
if let Some(pair) = st.windows.get(&id) {
|
||||||
|
let wv = &pair.webview;
|
||||||
|
let _ = wv.evaluate_script(&format!(
|
||||||
|
"window.location.replace({});",
|
||||||
|
serde_json::to_string(&url).unwrap()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToRust::LoadFile { id, path } => {
|
||||||
|
let url = format!("file://{}", std::fs::canonicalize(&path).unwrap_or_default().display());
|
||||||
|
log::info!("Loading file in window {}: {}", id, url);
|
||||||
|
// Rebuild the WebView with the correct file URL.
|
||||||
|
// evaluate_script navigation from about:blank to file:// is
|
||||||
|
// unreliable on macOS WKWebView, so we recreate the WebView.
|
||||||
|
if let Some(pair) = st.windows.get_mut(&id) {
|
||||||
|
let window = &pair.window;
|
||||||
|
match WebViewBuilder::new()
|
||||||
|
.with_url(&url)
|
||||||
|
.with_initialization_script(&preload_script())
|
||||||
|
.with_devtools(true)
|
||||||
|
.build(window)
|
||||||
|
{
|
||||||
|
Ok(webview) => {
|
||||||
|
pair.webview = webview;
|
||||||
|
log::info!("WebView rebuilt for window {}", id);
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("WebView rebuild error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToRust::DestroyWindow { id } => {
|
||||||
|
st.windows.remove(&id);
|
||||||
|
st.window_wids.retain(|_, v| *v != id);
|
||||||
|
if st.windows.is_empty() { *control_flow = ControlFlow::Exit; }
|
||||||
|
}
|
||||||
|
ToRust::Close { id } => {
|
||||||
|
st.windows.remove(&id);
|
||||||
|
st.window_wids.retain(|_, v| *v != id);
|
||||||
|
if st.windows.is_empty() { *control_flow = ControlFlow::Exit; }
|
||||||
|
}
|
||||||
|
ToRust::IpcMessage { id, channel, data } => {
|
||||||
|
if let Some(pair) = st.windows.get(&id) {
|
||||||
|
let msg = serde_json::json!({"__from_node":true,"channel":channel,"data":data});
|
||||||
|
let js = format!("window.postMessage({},'*');", serde_json::to_string(&msg).unwrap());
|
||||||
|
let _ = pair.webview.evaluate_script(&js);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToRust::EvalJs { id, script } => {
|
||||||
|
if let Some(pair) = st.windows.get(&id) {
|
||||||
|
let _ = pair.webview.evaluate_script(&script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToRust::Quit => {
|
||||||
|
st.windows.clear();
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::WindowEvent { event: WindowEvent::CloseRequested, window_id, .. } => {
|
||||||
|
if let Some(&id) = st.window_wids.get(&window_id) {
|
||||||
|
st.send_to_node(&ToNode::WindowClosed { id });
|
||||||
|
st.windows.remove(&id);
|
||||||
|
st.window_wids.remove(&window_id);
|
||||||
|
log::info!("Window {} closed", id);
|
||||||
|
if st.windows.is_empty() { *control_flow = ControlFlow::Exit; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::WindowEvent { event: WindowEvent::Focused(true), window_id, .. } => {
|
||||||
|
if let Some(&id) = st.window_wids.get(&window_id) {
|
||||||
|
st.send_to_node(&ToNode::WindowFocus { id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn which_node() -> Option<String> {
|
||||||
|
Command::new("which")
|
||||||
|
.arg("node")
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|o| if o.status.success() { String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string()) } else { None })
|
||||||
|
.or_else(|| {
|
||||||
|
for p in &["/usr/local/bin/node", "/opt/homebrew/bin/node", "/usr/bin/node"] {
|
||||||
|
if std::path::Path::new(p).exists() { return Some(p.to_string()); }
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
+295
@@ -0,0 +1,295 @@
|
|||||||
|
<!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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>Gelectron v0.1.0 — 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;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const { app, BrowserWindow } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
let mainWindow = null;
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 900,
|
||||||
|
height: 680,
|
||||||
|
title: 'Gelectron Demo',
|
||||||
|
backgroundColor: '#1a1a2e',
|
||||||
|
webPreferences: {
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
||||||
|
|
||||||
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "gelectron-demo",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Minimal demo for gelectron — HTML, CSS, JavaScript rendering",
|
||||||
|
"main": "main.js"
|
||||||
|
}
|
||||||
@@ -6,7 +6,11 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"gelectron_core.darwin-arm64.node"
|
"gelectron_core.darwin-arm64.node"
|
||||||
],
|
],
|
||||||
"os": ["darwin"],
|
"os": [
|
||||||
"cpu": ["arm64"],
|
"darwin"
|
||||||
|
],
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
+6
-1
@@ -57,6 +57,11 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"gelectron-darwin-arm64": "0.1.0"
|
"gelectron-darwin-arm64": "0.1.0",
|
||||||
|
"gelectron-darwin-x64": "0.1.0",
|
||||||
|
"gelectron-win32-x64-msvc": "0.1.0",
|
||||||
|
"gelectron-win32-arm64-msvc": "0.1.0",
|
||||||
|
"gelectron-linux-x64-gnu": "0.1.0",
|
||||||
|
"gelectron-linux-arm64-gnu": "0.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,15 @@ class App extends EventEmitter {
|
|||||||
} : null;
|
} : null;
|
||||||
|
|
||||||
this._argv = process.argv.slice();
|
this._argv = process.argv.slice();
|
||||||
|
|
||||||
|
// Emit 'ready' automatically on the next tick, matching real Electron
|
||||||
|
// where the app becomes ready once the process has initialised.
|
||||||
|
process.nextTick(() => {
|
||||||
|
if (!this._ready) {
|
||||||
|
this._ready = true;
|
||||||
|
this.emit('ready');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get commandLine() {
|
get commandLine() {
|
||||||
|
|||||||
@@ -3,7 +3,15 @@
|
|||||||
/**
|
/**
|
||||||
* Gelectron - autoUpdater module (Electron compatible)
|
* Gelectron - autoUpdater module (Electron compatible)
|
||||||
* Stub implementation for electron-updater compatibility.
|
* Stub implementation for electron-updater compatibility.
|
||||||
* electron-updater uses require("electron").autoUpdater as its native backend.
|
*
|
||||||
|
* 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');
|
const { EventEmitter } = require('events');
|
||||||
@@ -13,6 +21,7 @@ class AutoUpdater extends EventEmitter {
|
|||||||
super();
|
super();
|
||||||
this._isUpdateAvailable = false;
|
this._isUpdateAvailable = false;
|
||||||
this._updateInfo = null;
|
this._updateInfo = null;
|
||||||
|
this._feedURL = null;
|
||||||
this.autoDownload = true;
|
this.autoDownload = true;
|
||||||
this.autoInstallOnAppQuit = false;
|
this.autoInstallOnAppQuit = false;
|
||||||
this.autoRunAppAfterInstall = true;
|
this.autoRunAppAfterInstall = true;
|
||||||
@@ -20,10 +29,12 @@ class AutoUpdater extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFeedURL() {
|
getFeedURL() {
|
||||||
return null;
|
return this._feedURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFeedURL() {}
|
setFeedURL(options) {
|
||||||
|
this._feedURL = options;
|
||||||
|
}
|
||||||
|
|
||||||
async checkForUpdates() {
|
async checkForUpdates() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { bridge, isNative } = require('./native-bridge');
|
||||||
|
|
||||||
class WebContents extends EventEmitter {
|
class WebContents extends EventEmitter {
|
||||||
constructor(id) {
|
constructor(id) {
|
||||||
@@ -52,22 +53,41 @@ class WebContents extends EventEmitter {
|
|||||||
this._url = targetUrl;
|
this._url = targetUrl;
|
||||||
this._isLoading = true;
|
this._isLoading = true;
|
||||||
this.emit('did-start-loading');
|
this.emit('did-start-loading');
|
||||||
|
if (isNative) {
|
||||||
|
bridge.loadUrl(this.id, targetUrl);
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._isLoading = false;
|
this._isLoading = false;
|
||||||
this.emit('did-stop-loading');
|
this.emit('did-stop-loading');
|
||||||
this.emit('did-finish-load');
|
this.emit('did-finish-load');
|
||||||
this.emit('dom-ready');
|
this.emit('dom-ready');
|
||||||
}, 100);
|
}, isNative ? 500 : 100);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFile(filePath) {
|
loadFile(filePath) {
|
||||||
return this.loadURL(`file://${path.resolve(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() {
|
reload() {
|
||||||
this._isLoading = true;
|
this._isLoading = true;
|
||||||
this.emit('did-start-loading');
|
this.emit('did-start-loading');
|
||||||
|
if (isNative && this._url) {
|
||||||
|
bridge.loadUrl(this.id, this._url);
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._isLoading = false;
|
this._isLoading = false;
|
||||||
this.emit('did-stop-loading');
|
this.emit('did-stop-loading');
|
||||||
@@ -82,6 +102,9 @@ class WebContents extends EventEmitter {
|
|||||||
goForward() {}
|
goForward() {}
|
||||||
|
|
||||||
executeJavaScript(code, userGesture = true) {
|
executeJavaScript(code, userGesture = true) {
|
||||||
|
if (isNative) {
|
||||||
|
bridge.evalJs(this.id, code);
|
||||||
|
}
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +112,11 @@ class WebContents extends EventEmitter {
|
|||||||
insertJS(code, hasUserGesture = true) { return this.executeJavaScript(code, hasUserGesture); }
|
insertJS(code, hasUserGesture = true) { return this.executeJavaScript(code, hasUserGesture); }
|
||||||
|
|
||||||
send(channel, ...args) {
|
send(channel, ...args) {
|
||||||
console.log(`[gelectron] webContents.send('${channel}')`);
|
if (isNative) {
|
||||||
|
bridge.sendToRenderer(this.id, channel, ...args);
|
||||||
|
} else {
|
||||||
|
console.log(`[gelectron] webContents.send('${channel}')`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendInputEvent() {}
|
sendInputEvent() {}
|
||||||
@@ -178,6 +205,18 @@ class BrowserWindow extends EventEmitter {
|
|||||||
|
|
||||||
BrowserWindow._windows.set(this.id, this);
|
BrowserWindow._windows.set(this.id, this);
|
||||||
|
|
||||||
|
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) {
|
if (this._options.show) {
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (!this._isDestroyed) {
|
if (!this._isDestroyed) {
|
||||||
@@ -209,23 +248,28 @@ class BrowserWindow extends EventEmitter {
|
|||||||
|
|
||||||
loadURL(targetUrl) {
|
loadURL(targetUrl) {
|
||||||
this._url = targetUrl;
|
this._url = targetUrl;
|
||||||
|
if (isNative) {
|
||||||
|
bridge.loadUrl(this.id, targetUrl);
|
||||||
|
}
|
||||||
return this.webContents.loadURL(targetUrl);
|
return this.webContents.loadURL(targetUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFile(filePath) {
|
loadFile(filePath) {
|
||||||
return this.loadURL(`file://${path.resolve(filePath)}`);
|
return this.webContents.loadFile(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
if (this._isDestroyed) return;
|
if (this._isDestroyed) return;
|
||||||
this._isVisible = true;
|
this._isVisible = true;
|
||||||
this._isMinimized = false;
|
this._isMinimized = false;
|
||||||
|
if (isNative) bridge.showWindow(this.id);
|
||||||
this.emit('show');
|
this.emit('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
if (this._isDestroyed) return;
|
if (this._isDestroyed) return;
|
||||||
this._isVisible = false;
|
this._isVisible = false;
|
||||||
|
if (isNative) bridge.hideWindow(this.id);
|
||||||
this.emit('hide');
|
this.emit('hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,15 +282,16 @@ class BrowserWindow extends EventEmitter {
|
|||||||
destroy() {
|
destroy() {
|
||||||
if (this._isDestroyed) return;
|
if (this._isDestroyed) return;
|
||||||
this._isDestroyed = true;
|
this._isDestroyed = true;
|
||||||
|
if (isNative) bridge.destroyWindow(this.id);
|
||||||
BrowserWindow._windows.delete(this.id);
|
BrowserWindow._windows.delete(this.id);
|
||||||
this.emit('closed');
|
this.emit('closed');
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() { if (!this._isDestroyed) this.emit('focus'); }
|
focus() { if (!this._isDestroyed) { if (isNative) bridge.focusWindow(this.id); this.emit('focus'); } }
|
||||||
blur() {}
|
blur() {}
|
||||||
|
|
||||||
minimize() { if (!this._isDestroyed) { this._isMinimized = true; this.emit('minimize'); } }
|
minimize() { if (!this._isDestroyed) { this._isMinimized = true; if (isNative) bridge.minimizeWindow(this.id); this.emit('minimize'); } }
|
||||||
maximize() { if (!this._isDestroyed) { this._isMaximized = true; this.emit('maximize'); } }
|
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'); } }
|
unmaximize() { if (!this._isDestroyed) { this._isMaximized = false; this.emit('unmaximize'); } }
|
||||||
restore() { if (!this._isDestroyed) { this._isMinimized = false; this._isMaximized = false; this.emit('restore'); } }
|
restore() { if (!this._isDestroyed) { this._isMinimized = false; this._isMaximized = false; this.emit('restore'); } }
|
||||||
|
|
||||||
@@ -282,7 +327,7 @@ class BrowserWindow extends EventEmitter {
|
|||||||
setClosable(v) { this._options.closable = v; }
|
setClosable(v) { this._options.closable = v; }
|
||||||
isClosable() { return this._options.closable; }
|
isClosable() { return this._options.closable; }
|
||||||
|
|
||||||
setTitle(title) { this._options.title = title; this.webContents._title = title; }
|
setTitle(title) { this._options.title = title; this.webContents._title = title; if (isNative) bridge.setTitle(this.id, title); }
|
||||||
getTitle() { return this._options.title; }
|
getTitle() { return this._options.title; }
|
||||||
|
|
||||||
setSkipTaskbar() {}
|
setSkipTaskbar() {}
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ const safeStorage = require('./safe-storage');
|
|||||||
const contextBridge = require('./context-bridge');
|
const contextBridge = require('./context-bridge');
|
||||||
const webContents = require('./web-contents');
|
const webContents = require('./web-contents');
|
||||||
const { autoUpdater, AutoUpdater } = require('./auto-updater');
|
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)
|
// Session stub (electron-updater calls session.fromPartition)
|
||||||
const sessionStub = {
|
const sessionStub = {
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
'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 };
|
||||||
+37
-8
@@ -14,24 +14,53 @@ function run(mainScript, env) {
|
|||||||
|
|
||||||
// Patch require so that 'electron' resolves to gelectron's compat layer
|
// Patch require so that 'electron' resolves to gelectron's compat layer
|
||||||
const electronCompatPath = path.join(__dirname, 'index.js');
|
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;
|
const originalResolveFilename = Module._resolveFilename;
|
||||||
Module._resolveFilename = function (request, parent, isMain, options) {
|
Module._resolveFilename = function (request, parent, isMain, options) {
|
||||||
if (request === 'electron') {
|
if (request === 'electron' || request === 'electron/main' || request === 'electron/common') {
|
||||||
return electronCompatPath;
|
|
||||||
}
|
|
||||||
if (request === 'electron/main') {
|
|
||||||
return electronCompatPath;
|
return electronCompatPath;
|
||||||
}
|
}
|
||||||
if (request === 'electron/renderer') {
|
if (request === 'electron/renderer') {
|
||||||
return path.join(__dirname, 'ipc-renderer.js');
|
return ipcRendererPath;
|
||||||
}
|
|
||||||
if (request === 'electron/common') {
|
|
||||||
return electronCompatPath;
|
|
||||||
}
|
}
|
||||||
return originalResolveFilename.call(this, request, parent, isMain, options);
|
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
|
// Require the app's main script
|
||||||
try {
|
try {
|
||||||
require(mainScript);
|
require(mainScript);
|
||||||
|
|||||||
Reference in New Issue
Block a user