elit 3.5.6 → 3.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -1
- package/README.md +1 -1
- package/desktop/build.rs +83 -0
- package/desktop/icon.rs +106 -0
- package/desktop/lib.rs +2 -0
- package/desktop/main.rs +235 -0
- package/desktop/native_main.rs +128 -0
- package/desktop/native_renderer/action_widgets.rs +184 -0
- package/desktop/native_renderer/app_models.rs +171 -0
- package/desktop/native_renderer/app_runtime.rs +140 -0
- package/desktop/native_renderer/container_rendering.rs +610 -0
- package/desktop/native_renderer/content_widgets.rs +634 -0
- package/desktop/native_renderer/css_models.rs +371 -0
- package/desktop/native_renderer/embedded_surfaces.rs +414 -0
- package/desktop/native_renderer/form_controls.rs +516 -0
- package/desktop/native_renderer/interaction_dispatch.rs +89 -0
- package/desktop/native_renderer/runtime_support.rs +135 -0
- package/desktop/native_renderer/utilities.rs +495 -0
- package/desktop/native_renderer/vector_drawing.rs +491 -0
- package/desktop/native_renderer.rs +4122 -0
- package/desktop/runtime/external.rs +422 -0
- package/desktop/runtime/mod.rs +67 -0
- package/desktop/runtime/quickjs.rs +106 -0
- package/desktop/window.rs +383 -0
- package/package.json +6 -3
- package/dist/build.d.mts +0 -20
- package/dist/chokidar.d.mts +0 -134
- package/dist/cli.d.mts +0 -81
- package/dist/config.d.mts +0 -254
- package/dist/coverage.d.mts +0 -85
- package/dist/database.d.mts +0 -52
- package/dist/desktop.d.mts +0 -68
- package/dist/dom.d.mts +0 -87
- package/dist/el.d.mts +0 -208
- package/dist/fs.d.mts +0 -255
- package/dist/hmr.d.mts +0 -38
- package/dist/http.d.mts +0 -169
- package/dist/https.d.mts +0 -108
- package/dist/index.d.mts +0 -13
- package/dist/mime-types.d.mts +0 -48
- package/dist/native.d.mts +0 -136
- package/dist/path.d.mts +0 -163
- package/dist/router.d.mts +0 -49
- package/dist/runtime.d.mts +0 -97
- package/dist/server-D0Dp4R5z.d.mts +0 -449
- package/dist/server.d.mts +0 -7
- package/dist/state.d.mts +0 -117
- package/dist/style.d.mts +0 -232
- package/dist/test-reporter.d.mts +0 -77
- package/dist/test-runtime.d.mts +0 -122
- package/dist/test.d.mts +0 -39
- package/dist/types.d.mts +0 -586
- package/dist/universal.d.mts +0 -21
- package/dist/ws.d.mts +0 -200
- package/dist/wss.d.mts +0 -108
- package/src/build.ts +0 -362
- package/src/chokidar.ts +0 -427
- package/src/cli.ts +0 -1162
- package/src/config.ts +0 -509
- package/src/coverage.ts +0 -1479
- package/src/database.ts +0 -1410
- package/src/desktop-auto-render.ts +0 -317
- package/src/desktop-cli.ts +0 -1533
- package/src/desktop.ts +0 -99
- package/src/dev-build.ts +0 -340
- package/src/dom.ts +0 -901
- package/src/el.ts +0 -183
- package/src/fs.ts +0 -609
- package/src/hmr.ts +0 -149
- package/src/http.ts +0 -856
- package/src/https.ts +0 -411
- package/src/index.ts +0 -16
- package/src/mime-types.ts +0 -222
- package/src/mobile-cli.ts +0 -2313
- package/src/native-background.ts +0 -444
- package/src/native-border.ts +0 -343
- package/src/native-canvas.ts +0 -260
- package/src/native-cli.ts +0 -414
- package/src/native-color.ts +0 -904
- package/src/native-estimation.ts +0 -194
- package/src/native-grid.ts +0 -590
- package/src/native-interaction.ts +0 -1289
- package/src/native-layout.ts +0 -568
- package/src/native-link.ts +0 -76
- package/src/native-render-support.ts +0 -361
- package/src/native-spacing.ts +0 -231
- package/src/native-state.ts +0 -318
- package/src/native-strings.ts +0 -46
- package/src/native-transform.ts +0 -120
- package/src/native-types.ts +0 -439
- package/src/native-typography.ts +0 -254
- package/src/native-units.ts +0 -441
- package/src/native-vector.ts +0 -910
- package/src/native.ts +0 -5606
- package/src/path.ts +0 -493
- package/src/pm-cli.ts +0 -2498
- package/src/preview-build.ts +0 -294
- package/src/render-context.ts +0 -138
- package/src/router.ts +0 -260
- package/src/runtime.ts +0 -97
- package/src/server.ts +0 -2294
- package/src/state.ts +0 -556
- package/src/style.ts +0 -1790
- package/src/test-globals.d.ts +0 -184
- package/src/test-reporter.ts +0 -609
- package/src/test-runtime.ts +0 -1359
- package/src/test.ts +0 -368
- package/src/types.ts +0 -381
- package/src/universal.ts +0 -81
- package/src/wapk-cli.ts +0 -3213
- package/src/workspace-package.ts +0 -102
- package/src/ws.ts +0 -648
- package/src/wss.ts +0 -241
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
io::{BufRead, BufReader, Write},
|
|
3
|
+
path::PathBuf,
|
|
4
|
+
process::{Command, Stdio},
|
|
5
|
+
sync::mpsc::Receiver,
|
|
6
|
+
time::{SystemTime, UNIX_EPOCH},
|
|
7
|
+
};
|
|
8
|
+
#[cfg(target_os = "windows")]
|
|
9
|
+
use std::os::windows::process::CommandExt;
|
|
10
|
+
use tao::event_loop::EventLoopProxy;
|
|
11
|
+
|
|
12
|
+
use crate::runtime::{WindowCommand, WindowOptions};
|
|
13
|
+
|
|
14
|
+
#[cfg(target_os = "windows")]
|
|
15
|
+
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
|
16
|
+
|
|
17
|
+
#[cfg(target_os = "windows")]
|
|
18
|
+
fn child_process_path(path: &std::path::Path) -> String {
|
|
19
|
+
let value = path.to_string_lossy();
|
|
20
|
+
if let Some(rest) = value.strip_prefix(r"\\?\UNC\") {
|
|
21
|
+
format!(r"\\{}", rest)
|
|
22
|
+
} else if let Some(rest) = value.strip_prefix(r"\\?\") {
|
|
23
|
+
rest.to_string()
|
|
24
|
+
} else {
|
|
25
|
+
value.into_owned()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[cfg(not(target_os = "windows"))]
|
|
30
|
+
fn child_process_path(path: &std::path::Path) -> String {
|
|
31
|
+
path.to_string_lossy().into_owned()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn for_each_output_line<R>(reader: R, mut on_line: impl FnMut(String))
|
|
35
|
+
where
|
|
36
|
+
R: std::io::Read,
|
|
37
|
+
{
|
|
38
|
+
let mut reader = BufReader::new(reader);
|
|
39
|
+
let mut buffer = Vec::new();
|
|
40
|
+
|
|
41
|
+
loop {
|
|
42
|
+
buffer.clear();
|
|
43
|
+
match reader.read_until(b'\n', &mut buffer) {
|
|
44
|
+
Ok(0) => break,
|
|
45
|
+
Ok(_) => {
|
|
46
|
+
while matches!(buffer.last(), Some(b'\n' | b'\r')) {
|
|
47
|
+
buffer.pop();
|
|
48
|
+
}
|
|
49
|
+
on_line(String::from_utf8_lossy(&buffer).into_owned());
|
|
50
|
+
}
|
|
51
|
+
Err(_) => break,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Shim prepended to user script (written to a temp file).
|
|
57
|
+
/// JS writes commands to stderr with __WAG__ prefix.
|
|
58
|
+
const SHIM_NODELIKE: &str = r#"
|
|
59
|
+
const __wag = obj => process.stderr.write('__WAG__' + JSON.stringify(obj) + '\n');
|
|
60
|
+
|
|
61
|
+
globalThis.createWindow = opts => __wag({ cmd: 'createWindow', opts: opts ?? {} });
|
|
62
|
+
globalThis.windowEval = code => __wag({ cmd: 'windowEval', code });
|
|
63
|
+
globalThis.onMessage = fn => { globalThis.__onMessage = fn; };
|
|
64
|
+
|
|
65
|
+
// Window controls
|
|
66
|
+
globalThis.windowMinimize = () => __wag({ cmd: 'minimize' });
|
|
67
|
+
globalThis.windowMaximize = () => __wag({ cmd: 'maximize' });
|
|
68
|
+
globalThis.windowUnmaximize = () => __wag({ cmd: 'unmaximize' });
|
|
69
|
+
globalThis.windowSetTitle = title => __wag({ cmd: 'setTitle', title });
|
|
70
|
+
globalThis.windowDrag = () => __wag({ cmd: 'drag' });
|
|
71
|
+
globalThis.windowSetPosition = (x,y) => __wag({ cmd: 'setPosition', x, y });
|
|
72
|
+
globalThis.windowSetSize = (w,h) => __wag({ cmd: 'setSize', w, h });
|
|
73
|
+
globalThis.windowSetAlwaysOnTop = v => __wag({ cmd: 'setAlwaysOnTop', value: v });
|
|
74
|
+
globalThis.windowQuit = () => __wag({ cmd: 'quit' });
|
|
75
|
+
|
|
76
|
+
// ── createWindowServer(app, opts) ────────────────────────────────────────────────────
|
|
77
|
+
// Start an HTTP server and open a WebView pointing to it.
|
|
78
|
+
//
|
|
79
|
+
// exposePort: false (default) — server listens on a named pipe (Windows) or
|
|
80
|
+
// Unix socket. No TCP port is created at all, so no browser can reach it.
|
|
81
|
+
//
|
|
82
|
+
// exposePort: true — server binds to 0.0.0.0:port, reachable from the network.
|
|
83
|
+
//
|
|
84
|
+
// opts mirrors createWindow opts plus:
|
|
85
|
+
// port? number — fixed port (exposePort:true only; ignored for pipes)
|
|
86
|
+
// exposePort? bool — expose via TCP (default: false → pipe/socket)
|
|
87
|
+
//
|
|
88
|
+
// Returns: Promise<{ port, host, url }> for exposePort:true
|
|
89
|
+
// Promise<{ pipe }> for exposePort:false
|
|
90
|
+
globalThis.createWindowServer = async function(app, opts = {}) {
|
|
91
|
+
const httpMod = typeof require !== 'undefined' ? require('http') : await import('node:http');
|
|
92
|
+
const http = httpMod.default ?? httpMod;
|
|
93
|
+
const { exposePort: _e, port: _p, ...windowOpts } = opts;
|
|
94
|
+
|
|
95
|
+
if (opts.exposePort) {
|
|
96
|
+
// ── TCP, network-accessible — WebView still uses app:// ───────────────
|
|
97
|
+
const server = http.createServer(app);
|
|
98
|
+
await new Promise((resolve, reject) => {
|
|
99
|
+
server.listen(opts.port ?? 0, '0.0.0.0', resolve);
|
|
100
|
+
server.on('error', reject);
|
|
101
|
+
});
|
|
102
|
+
const port = server.address().port;
|
|
103
|
+
createWindow({ ...windowOpts, proxy_port: port });
|
|
104
|
+
return { port, host: '0.0.0.0', url: 'app://localhost/' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Named pipe / Unix socket — no TCP port at all ─────────────────────────
|
|
108
|
+
const cryptoMod = typeof require !== 'undefined' ? require('crypto') : await import('node:crypto');
|
|
109
|
+
const randomUUID = globalThis.crypto?.randomUUID?.bind(globalThis.crypto) ?? cryptoMod.randomUUID;
|
|
110
|
+
const secret = randomUUID();
|
|
111
|
+
const isWin = process.platform === 'win32';
|
|
112
|
+
const pipe = isWin
|
|
113
|
+
? '\\\\.\\pipe\\wag-' + secret
|
|
114
|
+
: '/tmp/wag-' + secret + '.sock';
|
|
115
|
+
|
|
116
|
+
// Guard: only Rust (which injects X-WAG-Internal) is allowed through
|
|
117
|
+
const server = http.createServer((req, res) => {
|
|
118
|
+
if (req.headers['x-wag-internal'] !== secret) {
|
|
119
|
+
res.writeHead(403); res.end('Forbidden'); return;
|
|
120
|
+
}
|
|
121
|
+
app(req, res);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
server.listen(pipe, resolve);
|
|
126
|
+
server.on('error', reject);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
createWindow({ ...windowOpts, proxy_pipe: pipe, proxy_secret: secret });
|
|
130
|
+
return { pipe };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// ── IPC: WebView → backend (via stdin) ───────────────────────────────────────
|
|
134
|
+
process.stdin.setEncoding('utf8');
|
|
135
|
+
if (typeof process.stdin.resume === 'function') process.stdin.resume();
|
|
136
|
+
let __stdinBuf = '';
|
|
137
|
+
process.stdin.on('data', chunk => {
|
|
138
|
+
__stdinBuf += chunk;
|
|
139
|
+
let nl;
|
|
140
|
+
while ((nl = __stdinBuf.indexOf('\n')) !== -1) {
|
|
141
|
+
const line = __stdinBuf.slice(0, nl).trim();
|
|
142
|
+
__stdinBuf = __stdinBuf.slice(nl + 1);
|
|
143
|
+
if (line && globalThis.__onMessage) {
|
|
144
|
+
try { globalThis.__onMessage(line); } catch(e) {}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
"#;
|
|
149
|
+
|
|
150
|
+
const SHIM_DENO: &str = r#"
|
|
151
|
+
const __encoder = new TextEncoder();
|
|
152
|
+
const __wag = obj => Deno.stderr.writeSync(__encoder.encode('__WAG__' + JSON.stringify(obj) + '\n'));
|
|
153
|
+
let __stdinBuf = '';
|
|
154
|
+
let __ipcStarted = false;
|
|
155
|
+
|
|
156
|
+
function __startIpcLoop() {
|
|
157
|
+
if (__ipcStarted) return;
|
|
158
|
+
__ipcStarted = true;
|
|
159
|
+
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
(async () => {
|
|
162
|
+
const reader = Deno.stdin.readable.pipeThrough(new TextDecoderStream()).getReader();
|
|
163
|
+
while (true) {
|
|
164
|
+
const { value, done } = await reader.read();
|
|
165
|
+
if (done) break;
|
|
166
|
+
__stdinBuf += value;
|
|
167
|
+
let nl;
|
|
168
|
+
while ((nl = __stdinBuf.indexOf('\n')) !== -1) {
|
|
169
|
+
const line = __stdinBuf.slice(0, nl).trim();
|
|
170
|
+
__stdinBuf = __stdinBuf.slice(nl + 1);
|
|
171
|
+
if (line && globalThis.__onMessage) {
|
|
172
|
+
try { globalThis.__onMessage(line); } catch(e) {}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
})();
|
|
177
|
+
}, 0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
globalThis.createWindow = opts => __wag({ cmd: 'createWindow', opts: opts ?? {} });
|
|
181
|
+
globalThis.windowEval = code => __wag({ cmd: 'windowEval', code });
|
|
182
|
+
globalThis.onMessage = fn => { globalThis.__onMessage = fn; __startIpcLoop(); };
|
|
183
|
+
|
|
184
|
+
// Window controls
|
|
185
|
+
globalThis.windowMinimize = () => __wag({ cmd: 'minimize' });
|
|
186
|
+
globalThis.windowMaximize = () => __wag({ cmd: 'maximize' });
|
|
187
|
+
globalThis.windowUnmaximize = () => __wag({ cmd: 'unmaximize' });
|
|
188
|
+
globalThis.windowSetTitle = title => __wag({ cmd: 'setTitle', title });
|
|
189
|
+
globalThis.windowDrag = () => __wag({ cmd: 'drag' });
|
|
190
|
+
globalThis.windowSetPosition = (x,y) => __wag({ cmd: 'setPosition', x, y });
|
|
191
|
+
globalThis.windowSetSize = (w,h) => __wag({ cmd: 'setSize', w, h });
|
|
192
|
+
globalThis.windowSetAlwaysOnTop = v => __wag({ cmd: 'setAlwaysOnTop', value: v });
|
|
193
|
+
globalThis.windowQuit = () => __wag({ cmd: 'quit' });
|
|
194
|
+
|
|
195
|
+
globalThis.createWindowServer = async function(app, opts = {}) {
|
|
196
|
+
const httpMod = await import('node:http');
|
|
197
|
+
const http = httpMod.default ?? httpMod;
|
|
198
|
+
const { exposePort: _e, port: _p, ...windowOpts } = opts;
|
|
199
|
+
|
|
200
|
+
if (opts.exposePort) {
|
|
201
|
+
const server = http.createServer(app);
|
|
202
|
+
await new Promise((resolve, reject) => {
|
|
203
|
+
server.listen(opts.port ?? 0, '0.0.0.0', resolve);
|
|
204
|
+
server.on('error', reject);
|
|
205
|
+
});
|
|
206
|
+
const port = server.address().port;
|
|
207
|
+
createWindow({ ...windowOpts, proxy_port: port });
|
|
208
|
+
return { port, host: '0.0.0.0', url: 'app://localhost/' };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const secret = globalThis.crypto.randomUUID();
|
|
212
|
+
const isWin = process.platform === 'win32';
|
|
213
|
+
const pipe = isWin
|
|
214
|
+
? '\\\\.\\pipe\\wag-' + secret
|
|
215
|
+
: '/tmp/wag-' + secret + '.sock';
|
|
216
|
+
|
|
217
|
+
const server = http.createServer((req, res) => {
|
|
218
|
+
if (req.headers['x-wag-internal'] !== secret) {
|
|
219
|
+
res.writeHead(403); res.end('Forbidden'); return;
|
|
220
|
+
}
|
|
221
|
+
app(req, res);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await new Promise((resolve, reject) => {
|
|
225
|
+
server.listen(pipe, resolve);
|
|
226
|
+
server.on('error', reject);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
createWindow({ ...windowOpts, proxy_pipe: pipe, proxy_secret: secret });
|
|
230
|
+
return { pipe };
|
|
231
|
+
};
|
|
232
|
+
"#;
|
|
233
|
+
|
|
234
|
+
/// Detect the executable name for the given runtime.
|
|
235
|
+
fn runtime_exe(name: &str) -> &'static str {
|
|
236
|
+
match name {
|
|
237
|
+
"bun" => "bun",
|
|
238
|
+
"node" | "nodejs" => "node",
|
|
239
|
+
"deno" => "deno",
|
|
240
|
+
_ => "bun",
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fn runtime_shim(name: &str) -> &'static str {
|
|
245
|
+
match name {
|
|
246
|
+
"deno" => SHIM_DENO,
|
|
247
|
+
_ => SHIM_NODELIKE,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fn configure_runtime_command(command: &mut Command, runtime_name: &str, shim: &str, script_path: &str) {
|
|
252
|
+
match runtime_name {
|
|
253
|
+
"bun" => {
|
|
254
|
+
command.arg("--preload").arg(shim).arg(script_path);
|
|
255
|
+
}
|
|
256
|
+
"node" | "nodejs" => {
|
|
257
|
+
command.arg("--require").arg(shim).arg(script_path);
|
|
258
|
+
}
|
|
259
|
+
"deno" => {
|
|
260
|
+
command.arg("run").arg("--preload").arg(shim).arg(script_path);
|
|
261
|
+
}
|
|
262
|
+
_ => {
|
|
263
|
+
command.arg("--preload").arg(shim).arg(script_path);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn embedded_script_path(runtime_name: &str) -> PathBuf {
|
|
269
|
+
let base_dir = std::env::current_exe()
|
|
270
|
+
.ok()
|
|
271
|
+
.and_then(|path| path.parent().map(|parent| parent.to_path_buf()))
|
|
272
|
+
.unwrap_or_else(std::env::temp_dir);
|
|
273
|
+
|
|
274
|
+
let stem = std::env::current_exe()
|
|
275
|
+
.ok()
|
|
276
|
+
.and_then(|path| path.file_stem().and_then(|name| name.to_str()).map(|name| name.to_string()))
|
|
277
|
+
.unwrap_or_else(|| String::from("elit-desktop"));
|
|
278
|
+
|
|
279
|
+
let stamp = SystemTime::now()
|
|
280
|
+
.duration_since(UNIX_EPOCH)
|
|
281
|
+
.unwrap_or_default()
|
|
282
|
+
.as_millis();
|
|
283
|
+
|
|
284
|
+
base_dir.join(format!(".{}-embedded-{}-{}-{}.js", stem, runtime_name, std::process::id(), stamp))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn run_script_file(
|
|
288
|
+
runtime_name: &str,
|
|
289
|
+
script_path: PathBuf,
|
|
290
|
+
cleanup_path: Option<PathBuf>,
|
|
291
|
+
proxy: EventLoopProxy<WindowCommand>,
|
|
292
|
+
ipc_rx: Receiver<String>,
|
|
293
|
+
) {
|
|
294
|
+
let shim_path = std::env::temp_dir().join("wag-shim.js");
|
|
295
|
+
std::fs::write(&shim_path, runtime_shim(runtime_name)).expect("Failed to write shim");
|
|
296
|
+
|
|
297
|
+
let script_path_buf = std::fs::canonicalize(&script_path).unwrap_or(script_path);
|
|
298
|
+
let script_path_str = child_process_path(&script_path_buf);
|
|
299
|
+
let script_dir = script_path_buf.parent().map(|p| p.to_path_buf());
|
|
300
|
+
|
|
301
|
+
let exe = runtime_exe(runtime_name);
|
|
302
|
+
let shim = child_process_path(&shim_path);
|
|
303
|
+
|
|
304
|
+
let mut command = Command::new(exe);
|
|
305
|
+
configure_runtime_command(&mut command, runtime_name, &shim, &script_path_str);
|
|
306
|
+
command.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
|
307
|
+
|
|
308
|
+
if let Some(dir) = script_dir {
|
|
309
|
+
command.current_dir(dir);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
#[cfg(target_os = "windows")]
|
|
313
|
+
command.creation_flags(CREATE_NO_WINDOW);
|
|
314
|
+
|
|
315
|
+
let mut child = match command.spawn() {
|
|
316
|
+
Ok(child) => child,
|
|
317
|
+
Err(_) => {
|
|
318
|
+
if let Some(path) = cleanup_path.as_ref() {
|
|
319
|
+
let _ = std::fs::remove_file(path);
|
|
320
|
+
}
|
|
321
|
+
panic!("Failed to spawn '{}'. Is it installed?", exe);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
let mut stdin = child.stdin.take().expect("Failed to get child stdin");
|
|
326
|
+
std::thread::spawn(move || {
|
|
327
|
+
for msg in ipc_rx {
|
|
328
|
+
let _ = writeln!(stdin, "{}", msg);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
let stdout = child.stdout.take().expect("Failed to capture stdout");
|
|
333
|
+
std::thread::spawn(move || {
|
|
334
|
+
for_each_output_line(stdout, |line| {
|
|
335
|
+
println!("{}", line);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
let stderr = child.stderr.take().expect("Failed to capture stderr");
|
|
340
|
+
|
|
341
|
+
for_each_output_line(stderr, |line| {
|
|
342
|
+
if let Some(json) = line.strip_prefix("__WAG__") {
|
|
343
|
+
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(json) {
|
|
344
|
+
let mut should_break = false;
|
|
345
|
+
match msg["cmd"].as_str() {
|
|
346
|
+
Some("createWindow") => {
|
|
347
|
+
let opts: WindowOptions =
|
|
348
|
+
serde_json::from_value(msg["opts"].clone()).unwrap_or_default();
|
|
349
|
+
proxy.send_event(WindowCommand::Create(opts)).ok();
|
|
350
|
+
}
|
|
351
|
+
Some("windowEval") => {
|
|
352
|
+
if let Some(code) = msg["code"].as_str() {
|
|
353
|
+
proxy.send_event(WindowCommand::Eval(code.to_string())).ok();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
Some("minimize") => { proxy.send_event(WindowCommand::Minimize).ok(); }
|
|
357
|
+
Some("maximize") => { proxy.send_event(WindowCommand::Maximize).ok(); }
|
|
358
|
+
Some("unmaximize") => { proxy.send_event(WindowCommand::Unmaximize).ok(); }
|
|
359
|
+
Some("drag") => { proxy.send_event(WindowCommand::Drag).ok(); }
|
|
360
|
+
Some("setTitle") => {
|
|
361
|
+
if let Some(t) = msg["title"].as_str() {
|
|
362
|
+
proxy.send_event(WindowCommand::SetTitle(t.to_string())).ok();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
Some("setPosition") => {
|
|
366
|
+
if let (Some(x), Some(y)) = (msg["x"].as_i64(), msg["y"].as_i64()) {
|
|
367
|
+
proxy.send_event(WindowCommand::SetPosition(x as i32, y as i32)).ok();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
Some("setSize") => {
|
|
371
|
+
if let (Some(w), Some(h)) = (msg["w"].as_u64(), msg["h"].as_u64()) {
|
|
372
|
+
proxy.send_event(WindowCommand::SetSize(w as u32, h as u32)).ok();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
Some("setAlwaysOnTop") => {
|
|
376
|
+
if let Some(v) = msg["value"].as_bool() {
|
|
377
|
+
proxy.send_event(WindowCommand::SetAlwaysOnTop(v)).ok();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
Some("quit") => {
|
|
381
|
+
proxy.send_event(WindowCommand::Quit).ok();
|
|
382
|
+
should_break = true;
|
|
383
|
+
}
|
|
384
|
+
_ => {}
|
|
385
|
+
}
|
|
386
|
+
if should_break {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
eprintln!("{}", line);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
let _ = child.wait();
|
|
396
|
+
|
|
397
|
+
if let Some(path) = cleanup_path {
|
|
398
|
+
let _ = std::fs::remove_file(path);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/// Run user JS using an external runtime (Bun / Node / Deno).
|
|
403
|
+
/// Communicates via stderr IPC protocol (Rust → child) and stdin (WebView IPC → child).
|
|
404
|
+
pub fn run(
|
|
405
|
+
runtime_name: &str,
|
|
406
|
+
script_path: &str,
|
|
407
|
+
proxy: EventLoopProxy<WindowCommand>,
|
|
408
|
+
ipc_rx: Receiver<String>,
|
|
409
|
+
) {
|
|
410
|
+
run_script_file(runtime_name, PathBuf::from(script_path), None, proxy, ipc_rx);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
pub fn run_embedded(
|
|
414
|
+
runtime_name: &str,
|
|
415
|
+
script_code: &str,
|
|
416
|
+
proxy: EventLoopProxy<WindowCommand>,
|
|
417
|
+
ipc_rx: Receiver<String>,
|
|
418
|
+
) {
|
|
419
|
+
let script_path = embedded_script_path(runtime_name);
|
|
420
|
+
std::fs::write(&script_path, script_code).expect("Failed to write embedded runtime script");
|
|
421
|
+
run_script_file(runtime_name, script_path.clone(), Some(script_path), proxy, ipc_rx);
|
|
422
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
|
|
3
|
+
/// Window configuration passed from JavaScript via createWindow({...})
|
|
4
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
5
|
+
pub struct WindowOptions {
|
|
6
|
+
pub title: Option<String>,
|
|
7
|
+
pub url: Option<String>,
|
|
8
|
+
pub html: Option<String>,
|
|
9
|
+
pub width: Option<u32>,
|
|
10
|
+
pub height: Option<u32>,
|
|
11
|
+
pub x: Option<i32>,
|
|
12
|
+
pub y: Option<i32>,
|
|
13
|
+
pub resizable: Option<bool>,
|
|
14
|
+
pub transparent: Option<bool>,
|
|
15
|
+
pub decorations: Option<bool>, // false = frameless window
|
|
16
|
+
pub always_on_top: Option<bool>,
|
|
17
|
+
pub maximized: Option<bool>,
|
|
18
|
+
pub center: Option<bool>, // center on screen at startup
|
|
19
|
+
pub minimizable: Option<bool>,
|
|
20
|
+
pub maximizable: Option<bool>,
|
|
21
|
+
pub closable: Option<bool>,
|
|
22
|
+
pub skip_taskbar: Option<bool>,
|
|
23
|
+
pub devtools: Option<bool>,
|
|
24
|
+
pub icon: Option<String>,
|
|
25
|
+
/// When set, WebView uses a custom `app://` protocol proxied to this
|
|
26
|
+
/// localhost port — the real port is never exposed to WebView JS code.
|
|
27
|
+
pub proxy_port: Option<u16>,
|
|
28
|
+
/// Rust injects this header into every proxied request so Express can
|
|
29
|
+
/// reject requests that arrive without it (e.g. from a browser).
|
|
30
|
+
pub proxy_secret: Option<String>,
|
|
31
|
+
/// Named pipe (Windows: \\.\pipe\...) or Unix socket path.
|
|
32
|
+
/// No TCP port is created — truly app-internal, unreachable from browsers.
|
|
33
|
+
pub proxy_pipe: Option<String>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Commands sent between the JS thread and the main (event loop) thread.
|
|
37
|
+
#[derive(Debug, Clone)]
|
|
38
|
+
#[allow(dead_code)]
|
|
39
|
+
pub enum WindowCommand {
|
|
40
|
+
/// Internal: start the runtime thread once the event loop is fully active.
|
|
41
|
+
StartRuntime,
|
|
42
|
+
/// JS called createWindow({...})
|
|
43
|
+
Create(WindowOptions),
|
|
44
|
+
/// Backend JS called windowEval(code) → run JS inside the WebView
|
|
45
|
+
Eval(String),
|
|
46
|
+
/// WebView sent an IPC message → forward to backend JS handler
|
|
47
|
+
IpcMessage(String),
|
|
48
|
+
|
|
49
|
+
// ── Window control commands ──────────────────────────────────────────────
|
|
50
|
+
Minimize,
|
|
51
|
+
Maximize,
|
|
52
|
+
Unmaximize,
|
|
53
|
+
SetTitle(String),
|
|
54
|
+
/// Start window drag — used for custom frameless titlebars
|
|
55
|
+
Drag,
|
|
56
|
+
SetPosition(i32, i32),
|
|
57
|
+
SetSize(u32, u32),
|
|
58
|
+
SetAlwaysOnTop(bool),
|
|
59
|
+
|
|
60
|
+
Quit,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[cfg(feature = "runtime-quickjs")]
|
|
64
|
+
pub mod quickjs;
|
|
65
|
+
|
|
66
|
+
#[cfg(feature = "runtime-external")]
|
|
67
|
+
pub mod external;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
use rquickjs::{Context, Function, Runtime};
|
|
2
|
+
use tao::event_loop::EventLoopProxy;
|
|
3
|
+
|
|
4
|
+
use crate::runtime::{WindowCommand, WindowOptions};
|
|
5
|
+
|
|
6
|
+
/// Shim injected before user code — provides the public JS API.
|
|
7
|
+
const SHIM: &str = r#"
|
|
8
|
+
// Internal dispatcher → calls Rust via __wagDispatch(json)
|
|
9
|
+
const __wag = obj => __wagDispatch(JSON.stringify(obj));
|
|
10
|
+
|
|
11
|
+
globalThis.createWindow = opts => __wag({ cmd: 'createWindow', opts: opts ?? {} });
|
|
12
|
+
globalThis.windowEval = code => __wag({ cmd: 'windowEval', code });
|
|
13
|
+
globalThis.onMessage = fn => { globalThis.__onMessage = fn; };
|
|
14
|
+
|
|
15
|
+
// Window controls
|
|
16
|
+
globalThis.windowMinimize = () => __wag({ cmd: 'minimize' });
|
|
17
|
+
globalThis.windowMaximize = () => __wag({ cmd: 'maximize' });
|
|
18
|
+
globalThis.windowUnmaximize = () => __wag({ cmd: 'unmaximize' });
|
|
19
|
+
globalThis.windowSetTitle = title => __wag({ cmd: 'setTitle', title });
|
|
20
|
+
globalThis.windowDrag = () => __wag({ cmd: 'drag' });
|
|
21
|
+
globalThis.windowSetPosition = (x,y) => __wag({ cmd: 'setPosition', x, y });
|
|
22
|
+
globalThis.windowSetSize = (w,h) => __wag({ cmd: 'setSize', w, h });
|
|
23
|
+
globalThis.windowSetAlwaysOnTop = v => __wag({ cmd: 'setAlwaysOnTop', value: v });
|
|
24
|
+
globalThis.windowQuit = () => __wag({ cmd: 'quit' });
|
|
25
|
+
"#;
|
|
26
|
+
|
|
27
|
+
pub fn run(
|
|
28
|
+
code: &str,
|
|
29
|
+
proxy: EventLoopProxy<WindowCommand>,
|
|
30
|
+
ipc_rx: std::sync::mpsc::Receiver<String>,
|
|
31
|
+
) {
|
|
32
|
+
let rt = Runtime::new().expect("QuickJS: failed to create runtime");
|
|
33
|
+
let ctx = Context::full(&rt).expect("QuickJS: failed to create context");
|
|
34
|
+
|
|
35
|
+
ctx.with(|ctx| {
|
|
36
|
+
let globals = ctx.globals();
|
|
37
|
+
let p = proxy.clone();
|
|
38
|
+
|
|
39
|
+
// Single dispatcher — handles all commands via JSON
|
|
40
|
+
globals
|
|
41
|
+
.set(
|
|
42
|
+
"__wagDispatch",
|
|
43
|
+
Function::new(ctx.clone(), move |json: String| {
|
|
44
|
+
dispatch(&p, &json);
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
.expect("QuickJS: failed to register __wagDispatch");
|
|
48
|
+
|
|
49
|
+
ctx.eval::<(), _>(SHIM).expect("QuickJS: shim failed");
|
|
50
|
+
ctx.eval::<(), _>(code).expect("QuickJS: user script failed");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Poll IPC messages from the WebView → call __onMessage(msg)
|
|
54
|
+
for msg in ipc_rx {
|
|
55
|
+
ctx.with(|ctx| {
|
|
56
|
+
let globals = ctx.globals();
|
|
57
|
+
if let Ok(handler) = globals.get::<_, rquickjs::Function>("__onMessage") {
|
|
58
|
+
handler.call::<(String,), ()>((msg,)).ok();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Parse a JSON command object and send the appropriate WindowCommand.
|
|
65
|
+
fn dispatch(proxy: &EventLoopProxy<WindowCommand>, json: &str) {
|
|
66
|
+
let Ok(msg) = serde_json::from_str::<serde_json::Value>(json) else { return };
|
|
67
|
+
|
|
68
|
+
match msg["cmd"].as_str() {
|
|
69
|
+
Some("createWindow") => {
|
|
70
|
+
let opts: WindowOptions =
|
|
71
|
+
serde_json::from_value(msg["opts"].clone()).unwrap_or_default();
|
|
72
|
+
proxy.send_event(WindowCommand::Create(opts)).ok();
|
|
73
|
+
}
|
|
74
|
+
Some("windowEval") => {
|
|
75
|
+
if let Some(code) = msg["code"].as_str() {
|
|
76
|
+
proxy.send_event(WindowCommand::Eval(code.to_string())).ok();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
Some("minimize") => { proxy.send_event(WindowCommand::Minimize).ok(); }
|
|
80
|
+
Some("maximize") => { proxy.send_event(WindowCommand::Maximize).ok(); }
|
|
81
|
+
Some("unmaximize") => { proxy.send_event(WindowCommand::Unmaximize).ok(); }
|
|
82
|
+
Some("drag") => { proxy.send_event(WindowCommand::Drag).ok(); }
|
|
83
|
+
Some("setTitle") => {
|
|
84
|
+
if let Some(t) = msg["title"].as_str() {
|
|
85
|
+
proxy.send_event(WindowCommand::SetTitle(t.to_string())).ok();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
Some("setPosition") => {
|
|
89
|
+
if let (Some(x), Some(y)) = (msg["x"].as_i64(), msg["y"].as_i64()) {
|
|
90
|
+
proxy.send_event(WindowCommand::SetPosition(x as i32, y as i32)).ok();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
Some("setSize") => {
|
|
94
|
+
if let (Some(w), Some(h)) = (msg["w"].as_u64(), msg["h"].as_u64()) {
|
|
95
|
+
proxy.send_event(WindowCommand::SetSize(w as u32, h as u32)).ok();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
Some("setAlwaysOnTop") => {
|
|
99
|
+
if let Some(v) = msg["value"].as_bool() {
|
|
100
|
+
proxy.send_event(WindowCommand::SetAlwaysOnTop(v)).ok();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
Some("quit") => { proxy.send_event(WindowCommand::Quit).ok(); }
|
|
104
|
+
_ => {}
|
|
105
|
+
}
|
|
106
|
+
}
|