janela 0.2.0 → 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 +66 -12
- package/bin/janela.mjs +351 -39
- package/package.json +1 -1
- package/runtime/janela.ts +170 -25
- package/shim/wvshim.cc +539 -63
- package/templates/index.html +17 -0
- package/templates/main.ts +32 -0
- 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
|
@@ -28,12 +28,27 @@ declare function wvTickStart(h: number, intervalMs: number): number;
|
|
|
28
28
|
declare function wvTickStop(h: number): number;
|
|
29
29
|
declare function wvFsRead(h: number, path: string): number;
|
|
30
30
|
declare function wvFsWrite(h: number, path: string, data: string): number;
|
|
31
|
-
declare function
|
|
32
|
-
declare function
|
|
33
|
-
declare function
|
|
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;
|
|
34
44
|
|
|
35
|
-
const
|
|
36
|
-
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;
|
|
37
52
|
|
|
38
53
|
// Injected into every page before it loads (webview_init).
|
|
39
54
|
const BOOTSTRAP =
|
|
@@ -81,6 +96,32 @@ export type AsyncCommandHandler = (
|
|
|
81
96
|
*/
|
|
82
97
|
export type FsCallback = (err: string | null, text: string) => void;
|
|
83
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
|
+
|
|
84
125
|
export interface WindowConfig {
|
|
85
126
|
title: string;
|
|
86
127
|
width: number;
|
|
@@ -113,6 +154,30 @@ export interface JanelaApp {
|
|
|
113
154
|
data: string,
|
|
114
155
|
cb: (err: string | null) => void,
|
|
115
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;
|
|
116
181
|
/** Fire an event into the page; the payload is delivered as a value. */
|
|
117
182
|
emit: (event: string, payload: unknown) => void;
|
|
118
183
|
/** Close the window and make run() return. */
|
|
@@ -145,8 +210,8 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
145
210
|
let taskFns: (() => void)[] = [];
|
|
146
211
|
let timerFns: (() => void)[] = [];
|
|
147
212
|
let timerDue: number[] = [];
|
|
148
|
-
let
|
|
149
|
-
let
|
|
213
|
+
let jobIds: number[] = [];
|
|
214
|
+
let jobCbs: FsCallback[] = [];
|
|
150
215
|
let ticking = false;
|
|
151
216
|
|
|
152
217
|
const wake = (): void => {
|
|
@@ -157,7 +222,7 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
157
222
|
|
|
158
223
|
const idle = (): void => {
|
|
159
224
|
if (!ticking) return;
|
|
160
|
-
if (taskFns.length > 0 || timerFns.length > 0 ||
|
|
225
|
+
if (taskFns.length > 0 || timerFns.length > 0 || jobIds.length > 0) return;
|
|
161
226
|
ticking = false;
|
|
162
227
|
wvTickStop(h);
|
|
163
228
|
};
|
|
@@ -190,34 +255,34 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
190
255
|
|
|
191
256
|
// Finished file jobs: the worker thread has already done the blocking
|
|
192
257
|
// syscall, so all that happens on this (UI) thread is the drain.
|
|
193
|
-
if (
|
|
258
|
+
if (jobIds.length > 0) {
|
|
194
259
|
const keptIds: number[] = [];
|
|
195
260
|
const keptCbs: FsCallback[] = [];
|
|
196
261
|
const doneIds: number[] = [];
|
|
197
262
|
const doneCbs: FsCallback[] = [];
|
|
198
263
|
const doneOk: boolean[] = [];
|
|
199
|
-
for (let i = 0; i <
|
|
200
|
-
const st =
|
|
201
|
-
if (st ===
|
|
202
|
-
keptIds.push(
|
|
203
|
-
keptCbs.push(
|
|
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]);
|
|
204
269
|
} else {
|
|
205
|
-
doneIds.push(
|
|
206
|
-
doneCbs.push(
|
|
207
|
-
doneOk.push(st ===
|
|
270
|
+
doneIds.push(jobIds[i]);
|
|
271
|
+
doneCbs.push(jobCbs[i]);
|
|
272
|
+
doneOk.push(st === JOB_OK);
|
|
208
273
|
}
|
|
209
274
|
}
|
|
210
|
-
|
|
211
|
-
|
|
275
|
+
jobIds = keptIds;
|
|
276
|
+
jobCbs = keptCbs;
|
|
212
277
|
for (let i = 0; i < doneIds.length; i++) {
|
|
213
278
|
// On failure the payload IS the error message, so one take serves both
|
|
214
279
|
// outcomes. The sink runs synchronously inside wvFsTake (the callback
|
|
215
280
|
// is lifetime:"call"), so `payload` is set by the time it returns.
|
|
216
281
|
let payload = "";
|
|
217
|
-
|
|
282
|
+
wvJobTake(h, doneIds[i], (text) => {
|
|
218
283
|
payload = text;
|
|
219
284
|
});
|
|
220
|
-
|
|
285
|
+
wvJobFree(h, doneIds[i]);
|
|
221
286
|
if (doneOk[i]) {
|
|
222
287
|
doneCbs[i](null, payload);
|
|
223
288
|
} else {
|
|
@@ -228,6 +293,55 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
228
293
|
idle();
|
|
229
294
|
};
|
|
230
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
|
+
|
|
231
345
|
const app: JanelaApp = {
|
|
232
346
|
handle: h,
|
|
233
347
|
names: [],
|
|
@@ -260,8 +374,8 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
260
374
|
app.defer(() => cb("EAGAIN: could not start a read of '" + path + "'", ""));
|
|
261
375
|
return;
|
|
262
376
|
}
|
|
263
|
-
|
|
264
|
-
|
|
377
|
+
jobIds.push(id);
|
|
378
|
+
jobCbs.push(cb);
|
|
265
379
|
wake();
|
|
266
380
|
},
|
|
267
381
|
|
|
@@ -271,13 +385,44 @@ export function createApp(cfg: WindowConfig): JanelaApp {
|
|
|
271
385
|
app.defer(() => cb("EAGAIN: could not start a write of '" + path + "'"));
|
|
272
386
|
return;
|
|
273
387
|
}
|
|
274
|
-
|
|
388
|
+
jobIds.push(id);
|
|
275
389
|
// The write payload is empty on success; the shared callback shape just
|
|
276
390
|
// ignores the text argument.
|
|
277
|
-
|
|
391
|
+
jobCbs.push((err, _text) => cb(err));
|
|
278
392
|
wake();
|
|
279
393
|
},
|
|
280
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
|
+
|
|
281
426
|
emit: (event, payload) => {
|
|
282
427
|
wvEval(
|
|
283
428
|
h,
|