janela 0.1.2 → 0.3.0
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/README.md +135 -26
- package/bin/janela.mjs +382 -46
- package/package.json +3 -3
- package/runtime/janela.ts +289 -122
- package/shim/wvshim.cc +726 -67
- package/templates/index.html +31 -0
- package/templates/main.ts +62 -19
- package/templates/react/deps.json +4 -0
- package/templates/react/files/index.html +11 -0
- package/templates/react/files/janela.conf.json +10 -0
- package/templates/react/files/src/App.css +3 -0
- package/templates/react/files/src/App.jsx +38 -0
- package/templates/react/files/src/main.jsx +9 -0
- package/templates/react/files/src-host/main.ts +42 -0
- package/templates/react/files/vite.config.js +6 -0
- package/templates/solid/deps.json +4 -0
- package/templates/solid/files/index.html +11 -0
- package/templates/solid/files/janela.conf.json +10 -0
- package/templates/solid/files/src/App.css +3 -0
- package/templates/solid/files/src/App.jsx +35 -0
- package/templates/solid/files/src/main.jsx +4 -0
- package/templates/solid/files/src-host/main.ts +42 -0
- package/templates/solid/files/vite.config.js +6 -0
- package/templates/svelte/deps.json +7 -0
- package/templates/svelte/files/index.html +11 -0
- package/templates/svelte/files/janela.conf.json +10 -0
- package/templates/svelte/files/src/App.svelte +33 -0
- package/templates/svelte/files/src/main.js +4 -0
- package/templates/svelte/files/src-host/main.ts +42 -0
- package/templates/svelte/files/svelte.config.js +3 -0
- package/templates/svelte/files/vite.config.js +6 -0
- package/templates/vue/deps.json +4 -0
- package/templates/vue/files/index.html +11 -0
- package/templates/vue/files/janela.conf.json +10 -0
- package/templates/vue/files/src/App.vue +39 -0
- package/templates/vue/files/src/main.js +4 -0
- package/templates/vue/files/src-host/main.ts +42 -0
- package/templates/vue/files/vite.config.js +6 -0
package/runtime/janela.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The Tauri-shaped surface: one `__invoke` binding carries every command as a
|
|
4
4
|
// (name, argsJson) envelope, dispatched to handlers registered on the app
|
|
5
|
-
// object.
|
|
5
|
+
// object. Handlers see decoded values — the runtime owns JSON at the boundary.
|
|
6
|
+
// Backend→frontend events ride wv_eval into the injected bootstrap.
|
|
6
7
|
//
|
|
7
8
|
// NOTE ON STYLE: every FFI call whose result initializes a variable is written
|
|
8
|
-
// `f(...) + 0`. scriptc
|
|
9
|
-
// initializer/assignment RHS
|
|
10
|
-
// the workaround.
|
|
9
|
+
// `f(...) + 0`. scriptc miscompiles a bare FFI call used as a complete
|
|
10
|
+
// initializer/assignment RHS — still true in 0.0.35, and reported upstream as
|
|
11
|
+
// vercel-labs/scriptc#21. Any enclosing expression is the workaround.
|
|
11
12
|
|
|
12
13
|
declare function wvCreate(debug: number): number;
|
|
13
14
|
declare function wvSetTitle(h: number, title: string): number;
|
|
@@ -16,19 +17,38 @@ declare function wvSetHtml(h: number, html: string): number;
|
|
|
16
17
|
declare function wvInit(h: number, js: string): number;
|
|
17
18
|
declare function wvEval(h: number, js: string): number;
|
|
18
19
|
declare function wvBind(h: number, name: string): number;
|
|
19
|
-
declare function
|
|
20
|
-
declare function
|
|
21
|
-
declare function
|
|
22
|
-
declare function
|
|
23
|
-
declare function wvRun(h: number, cb: (bindIndex: number, seq: number) => number): number;
|
|
20
|
+
declare function wvReply(h: number, body: string): number;
|
|
21
|
+
declare function wvOnInvoke(h: number, cb: (req: string) => number): number;
|
|
22
|
+
declare function wvOnTick(h: number, cb: () => void): number;
|
|
23
|
+
declare function wvRun(h: number): number;
|
|
24
24
|
declare function wvTerminate(h: number): number;
|
|
25
25
|
declare function wvDefer(h: number): number;
|
|
26
26
|
declare function wvResolve(h: number, id: number, status: number): number;
|
|
27
27
|
declare function wvTickStart(h: number, intervalMs: number): number;
|
|
28
28
|
declare function wvTickStop(h: number): number;
|
|
29
|
+
declare function wvFsRead(h: number, path: string): number;
|
|
30
|
+
declare function wvFsWrite(h: number, path: string, data: string): number;
|
|
31
|
+
declare function wvJobStatus(h: number, id: number): number;
|
|
32
|
+
declare function wvJobTake(h: number, id: number, sink: (text: string) => void): number;
|
|
33
|
+
declare function wvJobFree(h: number, id: number): number;
|
|
34
|
+
declare function wvDialog(
|
|
35
|
+
h: number,
|
|
36
|
+
kind: number,
|
|
37
|
+
flags: number,
|
|
38
|
+
title: string,
|
|
39
|
+
defaultPath: string,
|
|
40
|
+
defaultName: string,
|
|
41
|
+
filters: string,
|
|
42
|
+
): number;
|
|
43
|
+
declare function wvSetFullscreen(h: number, on: number): number;
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
const
|
|
45
|
+
const JOB_PENDING = 0;
|
|
46
|
+
const JOB_OK = 1;
|
|
47
|
+
|
|
48
|
+
const DLG_OPEN = 0;
|
|
49
|
+
const DLG_SAVE = 1;
|
|
50
|
+
const DLG_MULTIPLE = 1;
|
|
51
|
+
const DLG_DIRECTORY = 2;
|
|
32
52
|
|
|
33
53
|
// Injected into every page before it loads (webview_init).
|
|
34
54
|
const BOOTSTRAP =
|
|
@@ -47,23 +67,61 @@ const BOOTSTRAP =
|
|
|
47
67
|
" for (var i = 0; i < cbs.length; i++) cbs[i](payload);" +
|
|
48
68
|
"};";
|
|
49
69
|
|
|
50
|
-
// Handlers
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
70
|
+
// Handlers take the invoked arguments as a value and return a value; the
|
|
71
|
+
// runtime owns JSON at the boundary. `args` is whatever the page passed to
|
|
72
|
+
// janela.invoke(name, args) — cast it to the shape you expect. The return
|
|
73
|
+
// value is what the page's promise resolves with.
|
|
74
|
+
//
|
|
75
|
+
// Throwing is not supported by scriptc across the FFI boundary. Use
|
|
76
|
+
// commandAsync's `reject` to fail a call, or return an error value.
|
|
77
|
+
export type CommandHandler = (args: unknown) => unknown;
|
|
54
78
|
|
|
55
79
|
/**
|
|
56
80
|
* An async command: return immediately, answer later. `resolve`/`reject` take
|
|
57
|
-
*
|
|
81
|
+
* a value and settle the page's `await janela.invoke(...)` promise whenever
|
|
58
82
|
* they are called — from a later defer()/sleep() turn, or from another
|
|
59
83
|
* command. The window stays responsive for as long as the call is pending.
|
|
60
84
|
*/
|
|
61
85
|
export type AsyncCommandHandler = (
|
|
62
|
-
|
|
63
|
-
resolve: (
|
|
64
|
-
reject: (
|
|
86
|
+
args: unknown,
|
|
87
|
+
resolve: (value: unknown) => void,
|
|
88
|
+
reject: (reason: unknown) => void,
|
|
65
89
|
) => void;
|
|
66
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Completion of an async file operation. `err` is null on success; on failure
|
|
93
|
+
* it carries a Node-shaped message ("ENOENT: no such file or directory, open
|
|
94
|
+
* '/x'") and `text` is empty. Errors arrive as values, never as throws —
|
|
95
|
+
* scriptc cannot propagate an exception across the FFI boundary.
|
|
96
|
+
*/
|
|
97
|
+
export type FsCallback = (err: string | null, text: string) => void;
|
|
98
|
+
|
|
99
|
+
/** A named group of extensions offered in a dialog's file-type popup. */
|
|
100
|
+
export interface DialogFilter {
|
|
101
|
+
name: string;
|
|
102
|
+
/** Bare extensions, no dot and no glob: ["png", "jpg"]. */
|
|
103
|
+
extensions: string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface OpenDialogOptions {
|
|
107
|
+
title?: string;
|
|
108
|
+
/** Directory the dialog opens in. */
|
|
109
|
+
defaultPath?: string;
|
|
110
|
+
/** Allow picking more than one entry. */
|
|
111
|
+
multiple?: boolean;
|
|
112
|
+
/** Pick directories instead of files. Not supported on Windows. */
|
|
113
|
+
directory?: boolean;
|
|
114
|
+
filters?: DialogFilter[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface SaveDialogOptions {
|
|
118
|
+
title?: string;
|
|
119
|
+
defaultPath?: string;
|
|
120
|
+
/** Filename pre-filled in the name field. */
|
|
121
|
+
defaultName?: string;
|
|
122
|
+
filters?: DialogFilter[];
|
|
123
|
+
}
|
|
124
|
+
|
|
67
125
|
export interface WindowConfig {
|
|
68
126
|
title: string;
|
|
69
127
|
width: number;
|
|
@@ -83,85 +141,56 @@ export interface JanelaApp {
|
|
|
83
141
|
/** Run fn after at least ms. The host loop's timer; scriptc's setTimeout
|
|
84
142
|
* cannot fire while the window is open (its loop is parked inside run()). */
|
|
85
143
|
sleep: (ms: number, fn: () => void) => void;
|
|
86
|
-
/**
|
|
87
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Read a file without blocking the window. The syscall runs on a shim
|
|
146
|
+
* worker thread; the callback lands on the UI thread on a later turn.
|
|
147
|
+
* Prefer this over node:fs readFileSync inside a command — that one blocks
|
|
148
|
+
* the loop, and with it the whole window.
|
|
149
|
+
*/
|
|
150
|
+
readFileAsync: (path: string, cb: FsCallback) => void;
|
|
151
|
+
/** Write a file without blocking the window; cb(null) on success. */
|
|
152
|
+
writeFileAsync: (
|
|
153
|
+
path: string,
|
|
154
|
+
data: string,
|
|
155
|
+
cb: (err: string | null) => void,
|
|
156
|
+
) => void;
|
|
157
|
+
/**
|
|
158
|
+
* Show the native "open" dialog. `cb` gets the chosen paths, or null if the
|
|
159
|
+
* user cancelled. The modal runs on a later turn of the UI thread, so
|
|
160
|
+
* calling this from inside a command does not block that command's reply —
|
|
161
|
+
* pair it with commandAsync when the page is waiting for the result.
|
|
162
|
+
*/
|
|
163
|
+
openFileDialog: (
|
|
164
|
+
options: OpenDialogOptions,
|
|
165
|
+
cb: (paths: string[] | null, err?: string) => void,
|
|
166
|
+
) => void;
|
|
167
|
+
/** Show the native "save" dialog; cb gets the path, or null on cancel. */
|
|
168
|
+
saveFileDialog: (
|
|
169
|
+
options: SaveDialogOptions,
|
|
170
|
+
cb: (path: string | null, err?: string) => void,
|
|
171
|
+
) => void;
|
|
172
|
+
/** Change the window title at any time, not just at startup. */
|
|
173
|
+
setTitle: (title: string) => void;
|
|
174
|
+
/**
|
|
175
|
+
* Resize the window. `hint` is webview's sizing hint: 0 none, 1 minimum,
|
|
176
|
+
* 2 maximum, 3 fixed.
|
|
177
|
+
*/
|
|
178
|
+
setSize: (width: number, height: number, hint?: number) => void;
|
|
179
|
+
/** Enter or leave fullscreen. */
|
|
180
|
+
setFullscreen: (on: boolean) => void;
|
|
181
|
+
/** Fire an event into the page; the payload is delivered as a value. */
|
|
182
|
+
emit: (event: string, payload: unknown) => void;
|
|
88
183
|
/** Close the window and make run() return. */
|
|
89
184
|
quit: () => void;
|
|
90
185
|
/** Show the page and block until the window closes. Returns the run status. */
|
|
91
186
|
run: (html: string) => number;
|
|
92
187
|
}
|
|
93
188
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
function
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
HEX.charAt((unit >> 12) & 0xf) +
|
|
100
|
-
HEX.charAt((unit >> 8) & 0xf) +
|
|
101
|
-
HEX.charAt((unit >> 4) & 0xf) +
|
|
102
|
-
HEX.charAt(unit & 0xf)
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// The request arrives as UTF-8 JSON bytes. scriptc strings cannot hold lone
|
|
107
|
-
// surrogates (String.fromCharCode(0xd83d) yields a replacement char), so we
|
|
108
|
-
// never build non-ASCII chars directly: every code point >= 0x80 is re-emitted
|
|
109
|
-
// as a JSON \uXXXX escape (a surrogate PAIR of escapes for astral planes),
|
|
110
|
-
// which JSON.parse reconstructs correctly. Legal only because the payload is
|
|
111
|
-
// always JSON, where non-ASCII can only occur inside strings.
|
|
112
|
-
function readRequest(h: number): string {
|
|
113
|
-
let out = "";
|
|
114
|
-
const n = wvReqLen(h) + 0;
|
|
115
|
-
let i = 0;
|
|
116
|
-
while (i < n) {
|
|
117
|
-
const b0 = wvReqByte(h, i) + 0;
|
|
118
|
-
i = i + 1;
|
|
119
|
-
let cp = b0;
|
|
120
|
-
if ((b0 & 0xe0) === 0xc0 && i < n) {
|
|
121
|
-
cp = ((b0 & 0x1f) << 6) | (wvReqByte(h, i) & 0x3f);
|
|
122
|
-
i = i + 1;
|
|
123
|
-
} else if ((b0 & 0xf0) === 0xe0 && i + 1 < n) {
|
|
124
|
-
cp = ((b0 & 0x0f) << 12) | ((wvReqByte(h, i) & 0x3f) << 6) | (wvReqByte(h, i + 1) & 0x3f);
|
|
125
|
-
i = i + 2;
|
|
126
|
-
} else if ((b0 & 0xf8) === 0xf0 && i + 2 < n) {
|
|
127
|
-
cp =
|
|
128
|
-
((b0 & 0x07) << 18) |
|
|
129
|
-
((wvReqByte(h, i) & 0x3f) << 12) |
|
|
130
|
-
((wvReqByte(h, i + 1) & 0x3f) << 6) |
|
|
131
|
-
(wvReqByte(h, i + 2) & 0x3f);
|
|
132
|
-
i = i + 3;
|
|
133
|
-
}
|
|
134
|
-
if (cp < 0x80) {
|
|
135
|
-
out = out + String.fromCharCode(cp);
|
|
136
|
-
} else if (cp < 0x10000) {
|
|
137
|
-
out = out + uEscape(cp);
|
|
138
|
-
} else {
|
|
139
|
-
const v = cp - 0x10000;
|
|
140
|
-
out = out + uEscape(0xd800 + (v >> 10)) + uEscape(0xdc00 + (v & 0x3ff));
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return out;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// The reply must be valid JSON when it reaches the page. Non-ASCII chars can
|
|
147
|
-
// only legally occur inside JSON strings, where a \uXXXX escape is always
|
|
148
|
-
// equivalent — so escaping every char >127 keeps the byte channel ASCII-clean
|
|
149
|
-
// (surrogate halves escape individually, which JSON also permits).
|
|
150
|
-
function writeReply(h: number, body: string): void {
|
|
151
|
-
wvReplyReset(h);
|
|
152
|
-
for (let i = 0; i < body.length; i++) {
|
|
153
|
-
const c = body.charCodeAt(i);
|
|
154
|
-
if (c < 0x80) {
|
|
155
|
-
wvReplyPush(h, c);
|
|
156
|
-
} else {
|
|
157
|
-
wvReplyPush(h, 92); // backslash
|
|
158
|
-
wvReplyPush(h, 117); // 'u'
|
|
159
|
-
wvReplyPush(h, HEX.charCodeAt((c >> 12) & 0xf));
|
|
160
|
-
wvReplyPush(h, HEX.charCodeAt((c >> 8) & 0xf));
|
|
161
|
-
wvReplyPush(h, HEX.charCodeAt((c >> 4) & 0xf));
|
|
162
|
-
wvReplyPush(h, HEX.charCodeAt(c & 0xf));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
189
|
+
// JSON.stringify yields undefined for undefined; the wire always needs a
|
|
190
|
+
// value, and a command that returns nothing should read as null in the page.
|
|
191
|
+
function encode(value: unknown): string {
|
|
192
|
+
if (value === undefined) return "null";
|
|
193
|
+
return JSON.stringify(value);
|
|
165
194
|
}
|
|
166
195
|
|
|
167
196
|
export function createApp(cfg: WindowConfig): JanelaApp {
|
|
@@ -173,14 +202,16 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
173
202
|
// ---- the host loop -------------------------------------------------------
|
|
174
203
|
// scriptc's event loop is parked for as long as the program sits inside the
|
|
175
204
|
// wvRun() FFI call, so setTimeout/await never fire while the window is open.
|
|
176
|
-
// These queues are drained instead by
|
|
177
|
-
// ticker posts to the UI thread, and the ticker only runs while there
|
|
178
|
-
// work — an idle app costs nothing.
|
|
205
|
+
// These queues are drained instead by the retained tick handler that the
|
|
206
|
+
// shim's ticker posts to the UI thread, and the ticker only runs while there
|
|
207
|
+
// is work — an idle app costs nothing.
|
|
179
208
|
const asyncNames: string[] = [];
|
|
180
209
|
const asyncHandlers: AsyncCommandHandler[] = [];
|
|
181
210
|
let taskFns: (() => void)[] = [];
|
|
182
211
|
let timerFns: (() => void)[] = [];
|
|
183
212
|
let timerDue: number[] = [];
|
|
213
|
+
let jobIds: number[] = [];
|
|
214
|
+
let jobCbs: FsCallback[] = [];
|
|
184
215
|
let ticking = false;
|
|
185
216
|
|
|
186
217
|
const wake = (): void => {
|
|
@@ -191,7 +222,7 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
191
222
|
|
|
192
223
|
const idle = (): void => {
|
|
193
224
|
if (!ticking) return;
|
|
194
|
-
if (taskFns.length > 0 || timerFns.length > 0) return;
|
|
225
|
+
if (taskFns.length > 0 || timerFns.length > 0 || jobIds.length > 0) return;
|
|
195
226
|
ticking = false;
|
|
196
227
|
wvTickStop(h);
|
|
197
228
|
};
|
|
@@ -221,9 +252,96 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
221
252
|
timerDue = keptDue;
|
|
222
253
|
for (let i = 0; i < fire.length; i++) fire[i]();
|
|
223
254
|
}
|
|
255
|
+
|
|
256
|
+
// Finished file jobs: the worker thread has already done the blocking
|
|
257
|
+
// syscall, so all that happens on this (UI) thread is the drain.
|
|
258
|
+
if (jobIds.length > 0) {
|
|
259
|
+
const keptIds: number[] = [];
|
|
260
|
+
const keptCbs: FsCallback[] = [];
|
|
261
|
+
const doneIds: number[] = [];
|
|
262
|
+
const doneCbs: FsCallback[] = [];
|
|
263
|
+
const doneOk: boolean[] = [];
|
|
264
|
+
for (let i = 0; i < jobIds.length; i++) {
|
|
265
|
+
const st = wvJobStatus(h, jobIds[i]) + 0;
|
|
266
|
+
if (st === JOB_PENDING) {
|
|
267
|
+
keptIds.push(jobIds[i]);
|
|
268
|
+
keptCbs.push(jobCbs[i]);
|
|
269
|
+
} else {
|
|
270
|
+
doneIds.push(jobIds[i]);
|
|
271
|
+
doneCbs.push(jobCbs[i]);
|
|
272
|
+
doneOk.push(st === JOB_OK);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
jobIds = keptIds;
|
|
276
|
+
jobCbs = keptCbs;
|
|
277
|
+
for (let i = 0; i < doneIds.length; i++) {
|
|
278
|
+
// On failure the payload IS the error message, so one take serves both
|
|
279
|
+
// outcomes. The sink runs synchronously inside wvFsTake (the callback
|
|
280
|
+
// is lifetime:"call"), so `payload` is set by the time it returns.
|
|
281
|
+
let payload = "";
|
|
282
|
+
wvJobTake(h, doneIds[i], (text) => {
|
|
283
|
+
payload = text;
|
|
284
|
+
});
|
|
285
|
+
wvJobFree(h, doneIds[i]);
|
|
286
|
+
if (doneOk[i]) {
|
|
287
|
+
doneCbs[i](null, payload);
|
|
288
|
+
} else {
|
|
289
|
+
doneCbs[i](payload, "");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
224
293
|
idle();
|
|
225
294
|
};
|
|
226
295
|
|
|
296
|
+
// Filters cross as "Name|ext,ext|Name|ext" — the shim needs no JSON parser
|
|
297
|
+
// for what is always a short, flat list.
|
|
298
|
+
const encodeFilters = (filters: DialogFilter[] | undefined): string => {
|
|
299
|
+
if (filters === undefined || filters.length === 0) return "";
|
|
300
|
+
const parts: string[] = [];
|
|
301
|
+
for (let i = 0; i < filters.length; i++) {
|
|
302
|
+
parts.push(filters[i].name);
|
|
303
|
+
parts.push(filters[i].extensions.join(","));
|
|
304
|
+
}
|
|
305
|
+
return parts.join("|");
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Both dialog kinds share one path: start the job, then let the same drain
|
|
309
|
+
// that serves file I/O deliver the answer on a later turn.
|
|
310
|
+
const startDialog = (
|
|
311
|
+
kind: number,
|
|
312
|
+
flags: number,
|
|
313
|
+
title: string | undefined,
|
|
314
|
+
defaultPath: string | undefined,
|
|
315
|
+
defaultName: string | undefined,
|
|
316
|
+
filters: DialogFilter[] | undefined,
|
|
317
|
+
cb: (paths: string[] | null, err?: string) => void,
|
|
318
|
+
): void => {
|
|
319
|
+
const id = wvDialog(
|
|
320
|
+
h,
|
|
321
|
+
kind,
|
|
322
|
+
flags,
|
|
323
|
+
title === undefined ? "" : title,
|
|
324
|
+
defaultPath === undefined ? "" : defaultPath,
|
|
325
|
+
defaultName === undefined ? "" : defaultName,
|
|
326
|
+
encodeFilters(filters),
|
|
327
|
+
) + 0;
|
|
328
|
+
if (id < 0) {
|
|
329
|
+
taskFns.push(() => cb(null, "EAGAIN: could not open a dialog"));
|
|
330
|
+
wake();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
jobIds.push(id);
|
|
334
|
+
jobCbs.push((err, text) => {
|
|
335
|
+
if (err !== null) {
|
|
336
|
+
cb(null, err);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// "null" is a cancel; anything else is a JSON array of paths.
|
|
340
|
+
cb(JSON.parse(text) as string[] | null);
|
|
341
|
+
});
|
|
342
|
+
wake();
|
|
343
|
+
};
|
|
344
|
+
|
|
227
345
|
const app: JanelaApp = {
|
|
228
346
|
handle: h,
|
|
229
347
|
names: [],
|
|
@@ -250,10 +368,65 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
250
368
|
wake();
|
|
251
369
|
},
|
|
252
370
|
|
|
253
|
-
|
|
371
|
+
readFileAsync: (path, cb) => {
|
|
372
|
+
const id = wvFsRead(h, path) + 0;
|
|
373
|
+
if (id < 0) {
|
|
374
|
+
app.defer(() => cb("EAGAIN: could not start a read of '" + path + "'", ""));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
jobIds.push(id);
|
|
378
|
+
jobCbs.push(cb);
|
|
379
|
+
wake();
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
writeFileAsync: (path, data, cb) => {
|
|
383
|
+
const id = wvFsWrite(h, path, data) + 0;
|
|
384
|
+
if (id < 0) {
|
|
385
|
+
app.defer(() => cb("EAGAIN: could not start a write of '" + path + "'"));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
jobIds.push(id);
|
|
389
|
+
// The write payload is empty on success; the shared callback shape just
|
|
390
|
+
// ignores the text argument.
|
|
391
|
+
jobCbs.push((err, _text) => cb(err));
|
|
392
|
+
wake();
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
openFileDialog: (options, cb) => {
|
|
396
|
+
let flags = 0;
|
|
397
|
+
if (options.multiple === true) flags = flags + DLG_MULTIPLE;
|
|
398
|
+
if (options.directory === true) flags = flags + DLG_DIRECTORY;
|
|
399
|
+
startDialog(DLG_OPEN, flags, options.title, options.defaultPath, "",
|
|
400
|
+
options.filters, (paths, err) => cb(paths, err));
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
saveFileDialog: (options, cb) => {
|
|
404
|
+
startDialog(DLG_SAVE, 0, options.title, options.defaultPath,
|
|
405
|
+
options.defaultName, options.filters, (paths, err) => {
|
|
406
|
+
if (paths === null) {
|
|
407
|
+
cb(null, err);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
cb(paths.length > 0 ? paths[0] : null, err);
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
setTitle: (title) => {
|
|
415
|
+
wvSetTitle(h, title);
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
setSize: (width, height, hint) => {
|
|
419
|
+
wvSetSize(h, width, height, hint === undefined ? 0 : hint);
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
setFullscreen: (on) => {
|
|
423
|
+
wvSetFullscreen(h, on ? 1 : 0);
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
emit: (event, payload) => {
|
|
254
427
|
wvEval(
|
|
255
428
|
h,
|
|
256
|
-
"window.__wvEmit(" + JSON.stringify(event) + "," +
|
|
429
|
+
"window.__wvEmit(" + JSON.stringify(event) + "," + encode(payload) + ");",
|
|
257
430
|
);
|
|
258
431
|
},
|
|
259
432
|
|
|
@@ -262,26 +435,16 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
262
435
|
},
|
|
263
436
|
|
|
264
437
|
run: (html) => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// shim never calls webview_return for it.
|
|
271
|
-
if (bindIndex === TICK_BIND) {
|
|
272
|
-
turn();
|
|
273
|
-
return 0;
|
|
274
|
-
}
|
|
275
|
-
if (bindIndex !== INVOKE) {
|
|
276
|
-
writeReply(h, '"unknown binding"');
|
|
277
|
-
return 1;
|
|
278
|
-
}
|
|
279
|
-
const env = JSON.parse(readRequest(h)) as string[];
|
|
438
|
+
// Both handlers are retained: registered once here, called by the shim
|
|
439
|
+
// for as long as the window is open.
|
|
440
|
+
wvOnTick(h, turn);
|
|
441
|
+
wvOnInvoke(h, (req) => {
|
|
442
|
+
const env = JSON.parse(req) as string[];
|
|
280
443
|
const cmd = env[0];
|
|
281
|
-
const
|
|
444
|
+
const args = JSON.parse(env[1]) as unknown;
|
|
282
445
|
for (let i = 0; i < app.names.length; i++) {
|
|
283
446
|
if (app.names[i] === cmd) {
|
|
284
|
-
|
|
447
|
+
wvReply(h, encode(app.handlers[i](args)));
|
|
285
448
|
return 0;
|
|
286
449
|
}
|
|
287
450
|
}
|
|
@@ -292,25 +455,29 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
292
455
|
// that is. Meanwhile the loop is free to serve other calls.
|
|
293
456
|
const id = wvDefer(h) + 0;
|
|
294
457
|
if (id < 0) {
|
|
295
|
-
|
|
458
|
+
wvReply(h, encode("cannot defer command: " + cmd));
|
|
296
459
|
return 1;
|
|
297
460
|
}
|
|
298
|
-
const settle = (status: number): ((
|
|
461
|
+
const settle = (status: number): ((value: unknown) => void) => {
|
|
299
462
|
let done = false;
|
|
300
|
-
return (
|
|
463
|
+
return (value: unknown) => {
|
|
301
464
|
if (done) return; // a promise settles once
|
|
302
465
|
done = true;
|
|
303
|
-
|
|
466
|
+
wvReply(h, encode(value));
|
|
304
467
|
wvResolve(h, id, status);
|
|
305
468
|
};
|
|
306
469
|
};
|
|
307
|
-
asyncHandlers[i](
|
|
470
|
+
asyncHandlers[i](args, settle(0), settle(1));
|
|
308
471
|
return 0;
|
|
309
472
|
}
|
|
310
473
|
}
|
|
311
|
-
|
|
474
|
+
wvReply(h, encode("unknown command: " + cmd));
|
|
312
475
|
return 1; // rejects the frontend promise
|
|
313
|
-
})
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
wvBind(h, "__invoke");
|
|
479
|
+
wvSetHtml(h, html);
|
|
480
|
+
const rc = wvRun(h) + 0;
|
|
314
481
|
return rc;
|
|
315
482
|
},
|
|
316
483
|
};
|