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.
Files changed (38) hide show
  1. package/README.md +66 -12
  2. package/bin/janela.mjs +351 -39
  3. package/package.json +1 -1
  4. package/runtime/janela.ts +170 -25
  5. package/shim/wvshim.cc +539 -63
  6. package/templates/index.html +17 -0
  7. package/templates/main.ts +32 -0
  8. package/templates/react/deps.json +4 -0
  9. package/templates/react/files/index.html +11 -0
  10. package/templates/react/files/janela.conf.json +10 -0
  11. package/templates/react/files/src/App.css +3 -0
  12. package/templates/react/files/src/App.jsx +38 -0
  13. package/templates/react/files/src/main.jsx +9 -0
  14. package/templates/react/files/src-host/main.ts +42 -0
  15. package/templates/react/files/vite.config.js +6 -0
  16. package/templates/solid/deps.json +4 -0
  17. package/templates/solid/files/index.html +11 -0
  18. package/templates/solid/files/janela.conf.json +10 -0
  19. package/templates/solid/files/src/App.css +3 -0
  20. package/templates/solid/files/src/App.jsx +35 -0
  21. package/templates/solid/files/src/main.jsx +4 -0
  22. package/templates/solid/files/src-host/main.ts +42 -0
  23. package/templates/solid/files/vite.config.js +6 -0
  24. package/templates/svelte/deps.json +7 -0
  25. package/templates/svelte/files/index.html +11 -0
  26. package/templates/svelte/files/janela.conf.json +10 -0
  27. package/templates/svelte/files/src/App.svelte +33 -0
  28. package/templates/svelte/files/src/main.js +4 -0
  29. package/templates/svelte/files/src-host/main.ts +42 -0
  30. package/templates/svelte/files/svelte.config.js +3 -0
  31. package/templates/svelte/files/vite.config.js +6 -0
  32. package/templates/vue/deps.json +4 -0
  33. package/templates/vue/files/index.html +11 -0
  34. package/templates/vue/files/janela.conf.json +10 -0
  35. package/templates/vue/files/src/App.vue +39 -0
  36. package/templates/vue/files/src/main.js +4 -0
  37. package/templates/vue/files/src-host/main.ts +42 -0
  38. 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 wvFsStatus(h: number, id: number): number;
32
- declare function wvFsTake(h: number, id: number, sink: (text: string) => void): number;
33
- declare function wvFsFree(h: number, id: number): 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;
34
44
 
35
- const FS_PENDING = 0;
36
- const FS_OK = 1;
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 fsIds: number[] = [];
149
- let fsCbs: FsCallback[] = [];
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 || fsIds.length > 0) return;
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 (fsIds.length > 0) {
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 < fsIds.length; i++) {
200
- const st = wvFsStatus(h, fsIds[i]) + 0;
201
- if (st === FS_PENDING) {
202
- keptIds.push(fsIds[i]);
203
- keptCbs.push(fsCbs[i]);
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(fsIds[i]);
206
- doneCbs.push(fsCbs[i]);
207
- doneOk.push(st === FS_OK);
270
+ doneIds.push(jobIds[i]);
271
+ doneCbs.push(jobCbs[i]);
272
+ doneOk.push(st === JOB_OK);
208
273
  }
209
274
  }
210
- fsIds = keptIds;
211
- fsCbs = keptCbs;
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
- wvFsTake(h, doneIds[i], (text) => {
282
+ wvJobTake(h, doneIds[i], (text) => {
218
283
  payload = text;
219
284
  });
220
- wvFsFree(h, doneIds[i]);
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
- fsIds.push(id);
264
- fsCbs.push(cb);
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
- fsIds.push(id);
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
- fsCbs.push((err, _text) => cb(err));
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,