mirror of
https://github.com/mileswolfallen2/gelectron.git
synced 2026-09-08 12:43:16 +00:00
feat: add support for WebView-only mode and improve argument handling in main function
This commit is contained in:
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc;
|
||||
@@ -168,10 +169,11 @@ fn main() {
|
||||
.unwrap_or_default();
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let no_node = args.iter().any(|a| a == "--no-node");
|
||||
|
||||
// If no args, try to auto-detect app path for packaged apps
|
||||
let raw_app_path = if args.len() >= 2 {
|
||||
args[1].clone()
|
||||
let raw_app_path = if args.iter().skip(1).find(|a| !a.starts_with('-')).is_some() {
|
||||
args.iter().skip(1).find(|a| !a.starts_with('-')).unwrap().clone()
|
||||
} else {
|
||||
// Packaged macOS .app bundle: app is at ../Resources/app relative to binary
|
||||
let resources_app = gelectron_dir.join("../Resources/app");
|
||||
@@ -193,7 +195,6 @@ fn main() {
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@@ -245,6 +246,16 @@ fn main() {
|
||||
log::info!("Script: {}", main_script.display());
|
||||
log::info!("Compat: {}", compat_dir.display());
|
||||
|
||||
if no_node || which_node().is_none() {
|
||||
log::info!("Mode: WebView-only (no Node.js child process)");
|
||||
run_webview_only(app_path, main_script, compat_dir);
|
||||
} else {
|
||||
log::info!("Mode: Node.js child process");
|
||||
run_with_node(app_path, main_script, compat_dir);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_with_node(app_path: PathBuf, main_script: PathBuf, compat_dir: PathBuf) {
|
||||
let node_path = which_node().unwrap_or_else(|| {
|
||||
eprintln!("Error: node not found in PATH");
|
||||
std::process::exit(1);
|
||||
@@ -260,7 +271,6 @@ 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')) {{
|
||||
@@ -275,7 +285,6 @@ Module._resolveFilename = function(request, parent, isMain, options) {{
|
||||
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) {{
|
||||
@@ -285,10 +294,7 @@ if (typeof Module._resolveRequest === 'function') {{
|
||||
}};
|
||||
}}
|
||||
|
||||
// Pre-load the gelectron shim so require('electron') hits cache
|
||||
require(path.join(compatPath, 'index.js'));
|
||||
|
||||
// Periodic GC to keep memory low
|
||||
setInterval(function() {{ if (global.gc) global.gc(); }}, 5000);
|
||||
|
||||
require('{}');
|
||||
@@ -323,19 +329,14 @@ require('{}');
|
||||
match line {
|
||||
Ok(line) => {
|
||||
let trimmed = line.trim().to_string();
|
||||
if trimmed.is_empty() || !trimmed.starts_with('{') {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -344,14 +345,12 @@ require('{}');
|
||||
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) {
|
||||
st.windows.clear();
|
||||
st.window_wids.clear();
|
||||
@@ -361,11 +360,209 @@ require('{}');
|
||||
|
||||
match event {
|
||||
Event::NewEvents(StartCause::Poll) => {
|
||||
// Drain IPC messages from webview → Node
|
||||
while let Ok(msg) = ipc_rx.try_recv() {
|
||||
st.send_to_node(&msg);
|
||||
}
|
||||
while let Ok(msg) = ipc_rx.try_recv() { st.send_to_node(&msg); }
|
||||
while let Ok(msg) = rx.try_recv() {
|
||||
handle_to_rust(msg, &mut st, event_loop_target, &ipc_tx, control_flow);
|
||||
}
|
||||
}
|
||||
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 run_webview_only(app_path: PathBuf, main_script: PathBuf, compat_dir: PathBuf) {
|
||||
// Load the self-contained WebView bundle (no Node.js needed)
|
||||
let bundle_path = compat_dir.join("webview-bundle.js");
|
||||
if !bundle_path.exists() {
|
||||
eprintln!("Error: webview-bundle.js not found at {}", bundle_path.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
let bundle_js = std::fs::read_to_string(&bundle_path)
|
||||
.expect("Failed to read webview-bundle.js");
|
||||
|
||||
// Read the user's main script
|
||||
let main_js = std::fs::read_to_string(&main_script)
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Error reading main script: {}", e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let main_js_escaped = main_js.replace('\\', "\\\\").replace('`', "\\`").replace("${", "\\${");
|
||||
|
||||
let init_script = format!(
|
||||
r#"
|
||||
{}
|
||||
window.__dirname = '{}';
|
||||
window.__filename = '{}';
|
||||
window.process.env.GELECTRON_APP_PATH = '{}';
|
||||
window.process.env.GELECTRON_MAIN_SCRIPT = '{}';
|
||||
window.process.env.GELECTRON_NATIVE = '1';
|
||||
window.process.argv = ['gelectron', '{}'];
|
||||
window.__gelectron_run_main(`{}`);
|
||||
"#,
|
||||
bundle_js,
|
||||
app_path.display(),
|
||||
main_script.display(),
|
||||
app_path.display(),
|
||||
main_script.display(),
|
||||
main_script.display(),
|
||||
main_js_escaped,
|
||||
);
|
||||
|
||||
let event_loop = EventLoopBuilder::new().build();
|
||||
let (ipc_tx, ipc_rx) = mpsc::channel::<ToNode>();
|
||||
// Channel for ToRust messages coming from the WebView IPC handler
|
||||
let (to_rust_tx, to_rust_rx) = mpsc::channel::<ToRust>();
|
||||
let to_rust_tx = Arc::new(to_rust_tx);
|
||||
let node_exited = Arc::new(AtomicBool::new(false));
|
||||
let state = Rc::new(RefCell::new(AppState::new(node_exited.clone())));
|
||||
|
||||
event_loop.run(move |event, event_loop_target, control_flow| {
|
||||
*control_flow = ControlFlow::Poll;
|
||||
let mut st = state.borrow_mut();
|
||||
|
||||
match event {
|
||||
Event::NewEvents(StartCause::Poll) => {
|
||||
// Drain IPC messages from webview → forward back into the same webview
|
||||
while let Ok(msg) = ipc_rx.try_recv() {
|
||||
if let Some(pair) = st.windows.get(&1u32) {
|
||||
let js_msg = match &msg {
|
||||
ToNode::IpcMessage { channel, data, .. } => {
|
||||
serde_json::json!({"__from_node":true,"channel":channel,"data":data})
|
||||
}
|
||||
ToNode::WindowClosed { id } => {
|
||||
serde_json::json!({"__from_node":true,"type":"window-closed","id":id})
|
||||
}
|
||||
ToNode::WindowFocus { id } => {
|
||||
serde_json::json!({"__from_node":true,"type":"window-focus","id":id})
|
||||
}
|
||||
ToNode::Ready => {
|
||||
serde_json::json!({"__from_node":true,"type":"ready"})
|
||||
}
|
||||
};
|
||||
let _ = pair.webview.evaluate_script(
|
||||
&format!("window.postMessage({},'*');", serde_json::to_string(&js_msg).unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ToRust messages from WebView compat layer
|
||||
while let Ok(msg) = to_rust_rx.try_recv() {
|
||||
handle_to_rust(msg, &mut st, event_loop_target, &ipc_tx, control_flow);
|
||||
}
|
||||
|
||||
// Auto-create initial window if none exist
|
||||
if st.windows.is_empty() {
|
||||
drop(st);
|
||||
create_initial_webview_window(
|
||||
event_loop_target,
|
||||
&state,
|
||||
&to_rust_tx,
|
||||
&init_script,
|
||||
control_flow,
|
||||
);
|
||||
}
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::CloseRequested, window_id, .. } => {
|
||||
if let Some(&id) = st.window_wids.get(&window_id) {
|
||||
for pair in st.windows.values() {
|
||||
let js = serde_json::to_string(&serde_json::json!({
|
||||
"__from_node": true, "type": "window-closed", "id": id,
|
||||
})).unwrap();
|
||||
let _ = pair.webview.evaluate_script(&format!("window.postMessage({},'*');", js));
|
||||
}
|
||||
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) {
|
||||
for pair in st.windows.values() {
|
||||
let js = serde_json::to_string(&serde_json::json!({
|
||||
"__from_node": true, "type": "window-focus", "id": id,
|
||||
})).unwrap();
|
||||
let _ = pair.webview.evaluate_script(&format!("window.postMessage({},'*');", js));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn create_initial_webview_window(
|
||||
event_loop_target: &tao::event_loop::EventLoopWindowTarget<()>,
|
||||
state: &Rc<RefCell<AppState>>,
|
||||
to_rust_tx: &Arc<mpsc::Sender<ToRust>>,
|
||||
init_script: &str,
|
||||
_control_flow: &mut ControlFlow,
|
||||
) {
|
||||
let window_id = 1u32;
|
||||
let wb = WindowBuilder::new()
|
||||
.with_title("Gelectron")
|
||||
.with_inner_size(tao::dpi::LogicalSize::new(800.0, 600.0))
|
||||
.with_visible(false);
|
||||
|
||||
if let Ok(window) = wb.build(event_loop_target) {
|
||||
let to_rust_tx_clone = to_rust_tx.clone();
|
||||
|
||||
match WebViewBuilder::new()
|
||||
.with_url("about:blank")
|
||||
.with_initialization_script(&init_script)
|
||||
.with_devtools(true)
|
||||
.with_ipc_handler(move |req| {
|
||||
let body = req.body().to_string();
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&body) {
|
||||
if let Ok(cmd) = serde_json::from_value::<ToRust>(val.clone()) {
|
||||
let _ = to_rust_tx_clone.send(cmd);
|
||||
} else if let Some(msg_type) = val.get("type").and_then(|v| v.as_str()) {
|
||||
if msg_type == "ipc-send" {
|
||||
let channel = val.get("channel").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let args = val.get("args").cloned().unwrap_or(serde_json::Value::Null);
|
||||
let _ = to_rust_tx_clone.send(ToRust::IpcMessage { id: window_id, channel: channel.to_string(), data: args });
|
||||
} else if msg_type == "quit" {
|
||||
let _ = to_rust_tx_clone.send(ToRust::Quit);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(&window)
|
||||
{
|
||||
Ok(webview) => {
|
||||
let wid = window.id();
|
||||
let mut st = state.borrow_mut();
|
||||
st.windows.insert(window_id, WindowPair { window, webview });
|
||||
st.window_wids.insert(wid, window_id);
|
||||
log::info!("Initial WebView window created (running compat layer)");
|
||||
}
|
||||
Err(e) => log::error!("WebView error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_to_rust(
|
||||
msg: ToRust,
|
||||
st: &mut AppState,
|
||||
event_loop_target: &tao::event_loop::EventLoopWindowTarget<()>,
|
||||
ipc_tx: &mpsc::Sender<ToNode>,
|
||||
control_flow: &mut ControlFlow,
|
||||
) {
|
||||
match msg {
|
||||
ToRust::CreateWindow { id, options } => {
|
||||
let mut wb = WindowBuilder::new()
|
||||
@@ -419,8 +616,7 @@ require('{}');
|
||||
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!(
|
||||
let _ = pair.webview.evaluate_script(&format!(
|
||||
"window.location.replace({});",
|
||||
serde_json::to_string(&url).unwrap()
|
||||
));
|
||||
@@ -486,25 +682,6 @@ require('{}');
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
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> {
|
||||
|
||||
Reference in New Issue
Block a user