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
@@ -20,6 +20,11 @@
20
20
  <p>
21
21
  <input id="path" type="text" value="janela.conf.json" size="32" />
22
22
  <button id="read">read file (async)</button>
23
+ <button id="open">pick a file…</button>
24
+ </p>
25
+ <p>
26
+ <input id="title" type="text" value="__NAME__ — renamed" size="24" />
27
+ <button id="retitle">set window title</button>
23
28
  </p>
24
29
  <pre id="out">booting…</pre>
25
30
  <ul id="events"></ul>
@@ -65,6 +70,18 @@
65
70
  : "error: " + r.error;
66
71
  };
67
72
 
73
+ // The native open dialog. The window keeps serving other commands
74
+ // while the user is deciding.
75
+ document.getElementById("open").onclick = async () => {
76
+ const r = await janela.invoke("openFile", {});
77
+ if (!r.ok) out.textContent = "error: " + r.error;
78
+ else if (r.cancelled) out.textContent = "cancelled";
79
+ else out.textContent = `${r.path}: ${r.length} chars\n\n` + r.text.slice(0, 400);
80
+ };
81
+
82
+ document.getElementById("retitle").onclick = () =>
83
+ janela.invoke("setTitle", { title: document.getElementById("title").value });
84
+
68
85
  document.getElementById("quit").onclick = () => janela.invoke("quit");
69
86
  };
70
87
  </script>
package/templates/main.ts CHANGED
@@ -51,6 +51,38 @@ export function setup(app: JanelaApp): void {
51
51
  });
52
52
  });
53
53
 
54
+ // The native "open" dialog, paired with the reader above — picking a file is
55
+ // what makes readFileAsync useful. commandAsync is the right shape here: the
56
+ // page's promise stays parked while the user takes as long as they like, and
57
+ // the window carries on serving other calls meanwhile.
58
+ app.commandAsync("openFile", (_args, resolve) => {
59
+ app.openFileDialog(
60
+ { title: "Pick a file", filters: [{ name: "Text", extensions: ["txt", "md"] }] },
61
+ (paths, err) => {
62
+ if (err !== undefined) {
63
+ resolve({ ok: false, error: err });
64
+ return;
65
+ }
66
+ if (paths === null) {
67
+ resolve({ ok: true, cancelled: true });
68
+ return;
69
+ }
70
+ app.readFileAsync(paths[0], (rerr, text) => {
71
+ resolve(
72
+ rerr !== null
73
+ ? { ok: false, error: rerr }
74
+ : { ok: true, path: paths[0], length: text.length, text: text },
75
+ );
76
+ });
77
+ },
78
+ );
79
+ });
80
+
81
+ // The window is yours to change at runtime, not just at startup.
82
+ app.command("setTitle", (args) => {
83
+ app.setTitle((args as { title: string }).title);
84
+ });
85
+
54
86
  app.command("quit", (_args) => {
55
87
  app.quit();
56
88
  });
@@ -0,0 +1,4 @@
1
+ {
2
+ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" },
3
+ "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "vite": "^6.0.7" }
4
+ }
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>__NAME__</title>
6
+ </head>
7
+ <body>
8
+ <div id="root"></div>
9
+ <script type="module" src="/src/main.jsx"></script>
10
+ </body>
11
+ </html>
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "__NAME__",
3
+ "identifier": "dev.janela.__NAME__",
4
+ "version": "0.1.0",
5
+ "window": {
6
+ "title": "__NAME__",
7
+ "width": 640,
8
+ "height": 420
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ body { font: 16px -apple-system, system-ui, sans-serif; padding: 2rem; }
2
+ input { width: 5em; }
3
+ ul { color: #666; font-size: 13px; }
@@ -0,0 +1,38 @@
1
+ import { useEffect, useState } from "react";
2
+ import "./App.css";
3
+
4
+ export default function App() {
5
+ const [greeting, setGreeting] = useState("…");
6
+ const [a, setA] = useState(2);
7
+ const [b, setB] = useState(40);
8
+ const [sum, setSum] = useState(null);
9
+ const [events, setEvents] = useState([]);
10
+
11
+ useEffect(() => {
12
+ // Backend→frontend events. The payload arrives as a value.
13
+ janela.listen("added", (value) =>
14
+ setEvents((prev) => [`host emitted: ${value}`, ...prev]),
15
+ );
16
+ janela.invoke("greet", { name: "__NAME__" }).then(setGreeting);
17
+ }, []);
18
+
19
+ const add = async () =>
20
+ setSum(await janela.invoke("add", { a: Number(a), b: Number(b) }));
21
+
22
+ return (
23
+ <>
24
+ <h1>{greeting}</h1>
25
+ <p>
26
+ <input type="number" value={a} onChange={(e) => setA(e.target.value)} /> +
27
+ <input type="number" value={b} onChange={(e) => setB(e.target.value)} />
28
+ <button onClick={add}>add</button>
29
+ {sum !== null && <span> = {sum}</span>}
30
+ </p>
31
+ <ul>
32
+ {events.map((e, i) => (
33
+ <li key={i}>{e}</li>
34
+ ))}
35
+ </ul>
36
+ </>
37
+ );
38
+ }
@@ -0,0 +1,9 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import App from "./App.jsx";
4
+
5
+ createRoot(document.getElementById("root")).render(
6
+ <StrictMode>
7
+ <App />
8
+ </StrictMode>,
9
+ );
@@ -0,0 +1,42 @@
1
+ // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
+ //
3
+ // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
+ // Handlers take the arguments as a value and return a value — the runtime owns
5
+ // JSON at the boundary, so there is no parsing or stringifying to do here.
6
+ //
7
+ // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
+ // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
+ // TypeScript like everything in this file is unaffected.
10
+
11
+ import type { JanelaApp } from "./janela";
12
+
13
+ export function setup(app: JanelaApp): void {
14
+ app.command("add", (args) => {
15
+ const a = args as { a: number; b: number };
16
+ const sum = a.a + a.b;
17
+ // Backend→frontend event: the page listens with janela.listen("added", …).
18
+ app.emit("added", sum);
19
+ return sum;
20
+ });
21
+
22
+ app.command("greet", (args) => {
23
+ const a = args as { name: string };
24
+ return "Hello, " + a.name + " — from the native TS binary";
25
+ });
26
+
27
+ app.command("log", (args) => {
28
+ console.log("[host] page says:", args as string);
29
+ });
30
+
31
+ // An async command: answers later, without freezing the window.
32
+ app.commandAsync("wait", (args, resolve) => {
33
+ const a = args as { ms: number };
34
+ app.sleep(a.ms, () => {
35
+ resolve("waited " + a.ms + "ms without blocking the UI");
36
+ });
37
+ });
38
+
39
+ app.command("quit", () => {
40
+ app.quit();
41
+ });
42
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ // janela flattens this build into one HTML document at `janela build` time,
5
+ // so there is no server and no base path to configure.
6
+ export default defineConfig({ plugins: [react()] });
@@ -0,0 +1,4 @@
1
+ {
2
+ "dependencies": { "solid-js": "^1.9.4" },
3
+ "devDependencies": { "vite": "^6.0.7", "vite-plugin-solid": "^2.11.0" }
4
+ }
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>__NAME__</title>
6
+ </head>
7
+ <body>
8
+ <div id="root"></div>
9
+ <script type="module" src="/src/main.jsx"></script>
10
+ </body>
11
+ </html>
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "__NAME__",
3
+ "identifier": "dev.janela.__NAME__",
4
+ "version": "0.1.0",
5
+ "window": {
6
+ "title": "__NAME__",
7
+ "width": 640,
8
+ "height": 420
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ body { font: 16px -apple-system, system-ui, sans-serif; padding: 2rem; }
2
+ input { width: 5em; }
3
+ ul { color: #666; font-size: 13px; }
@@ -0,0 +1,35 @@
1
+ import { createSignal, onMount, For, Show } from "solid-js";
2
+ import "./App.css";
3
+
4
+ export default function App() {
5
+ const [greeting, setGreeting] = createSignal("…");
6
+ const [a, setA] = createSignal(2);
7
+ const [b, setB] = createSignal(40);
8
+ const [sum, setSum] = createSignal(null);
9
+ const [events, setEvents] = createSignal([]);
10
+
11
+ // Backend→frontend events. The payload arrives as a value.
12
+ janela.listen("added", (value) =>
13
+ setEvents((prev) => [`host emitted: ${value}`, ...prev]),
14
+ );
15
+
16
+ onMount(async () => setGreeting(await janela.invoke("greet", { name: "__NAME__" })));
17
+
18
+ const add = async () =>
19
+ setSum(await janela.invoke("add", { a: Number(a()), b: Number(b()) }));
20
+
21
+ return (
22
+ <>
23
+ <h1>{greeting()}</h1>
24
+ <p>
25
+ <input type="number" value={a()} onInput={(e) => setA(e.currentTarget.value)} /> +
26
+ <input type="number" value={b()} onInput={(e) => setB(e.currentTarget.value)} />
27
+ <button onClick={add}>add</button>
28
+ <Show when={sum() !== null}><span> = {sum()}</span></Show>
29
+ </p>
30
+ <ul>
31
+ <For each={events()}>{(e) => <li>{e}</li>}</For>
32
+ </ul>
33
+ </>
34
+ );
35
+ }
@@ -0,0 +1,4 @@
1
+ import { render } from "solid-js/web";
2
+ import App from "./App.jsx";
3
+
4
+ render(() => <App />, document.getElementById("root"));
@@ -0,0 +1,42 @@
1
+ // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
+ //
3
+ // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
+ // Handlers take the arguments as a value and return a value — the runtime owns
5
+ // JSON at the boundary, so there is no parsing or stringifying to do here.
6
+ //
7
+ // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
+ // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
+ // TypeScript like everything in this file is unaffected.
10
+
11
+ import type { JanelaApp } from "./janela";
12
+
13
+ export function setup(app: JanelaApp): void {
14
+ app.command("add", (args) => {
15
+ const a = args as { a: number; b: number };
16
+ const sum = a.a + a.b;
17
+ // Backend→frontend event: the page listens with janela.listen("added", …).
18
+ app.emit("added", sum);
19
+ return sum;
20
+ });
21
+
22
+ app.command("greet", (args) => {
23
+ const a = args as { name: string };
24
+ return "Hello, " + a.name + " — from the native TS binary";
25
+ });
26
+
27
+ app.command("log", (args) => {
28
+ console.log("[host] page says:", args as string);
29
+ });
30
+
31
+ // An async command: answers later, without freezing the window.
32
+ app.commandAsync("wait", (args, resolve) => {
33
+ const a = args as { ms: number };
34
+ app.sleep(a.ms, () => {
35
+ resolve("waited " + a.ms + "ms without blocking the UI");
36
+ });
37
+ });
38
+
39
+ app.command("quit", () => {
40
+ app.quit();
41
+ });
42
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import solid from "vite-plugin-solid";
3
+
4
+ // janela flattens this build into one HTML document at `janela build` time,
5
+ // so there is no server and no base path to configure.
6
+ export default defineConfig({ plugins: [solid()] });
@@ -0,0 +1,7 @@
1
+ {
2
+ "devDependencies": {
3
+ "@sveltejs/vite-plugin-svelte": "^5.0.3",
4
+ "svelte": "^5.16.0",
5
+ "vite": "^6.0.7"
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>__NAME__</title>
6
+ </head>
7
+ <body>
8
+ <div id="app"></div>
9
+ <script type="module" src="/src/main.js"></script>
10
+ </body>
11
+ </html>
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "__NAME__",
3
+ "identifier": "dev.janela.__NAME__",
4
+ "version": "0.1.0",
5
+ "window": {
6
+ "title": "__NAME__",
7
+ "width": 640,
8
+ "height": 420
9
+ }
10
+ }
@@ -0,0 +1,33 @@
1
+ <script>
2
+ let greeting = $state("…");
3
+ let a = $state(2);
4
+ let b = $state(40);
5
+ let sum = $state(null);
6
+ let events = $state([]);
7
+
8
+ // Backend→frontend events. The payload arrives as a value.
9
+ janela.listen("added", (value) => (events = [`host emitted: ${value}`, ...events]));
10
+
11
+ janela.invoke("greet", { name: "__NAME__" }).then((g) => (greeting = g));
12
+
13
+ async function add() {
14
+ sum = await janela.invoke("add", { a: Number(a), b: Number(b) });
15
+ }
16
+ </script>
17
+
18
+ <h1>{greeting}</h1>
19
+ <p>
20
+ <input type="number" bind:value={a} /> +
21
+ <input type="number" bind:value={b} />
22
+ <button onclick={add}>add</button>
23
+ {#if sum !== null}<span> = {sum}</span>{/if}
24
+ </p>
25
+ <ul>
26
+ {#each events as e}<li>{e}</li>{/each}
27
+ </ul>
28
+
29
+ <style>
30
+ :global(body) { font: 16px -apple-system, system-ui, sans-serif; padding: 2rem; }
31
+ input { width: 5em; }
32
+ ul { color: #666; font-size: 13px; }
33
+ </style>
@@ -0,0 +1,4 @@
1
+ import { mount } from "svelte";
2
+ import App from "./App.svelte";
3
+
4
+ mount(App, { target: document.getElementById("app") });
@@ -0,0 +1,42 @@
1
+ // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
+ //
3
+ // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
+ // Handlers take the arguments as a value and return a value — the runtime owns
5
+ // JSON at the boundary, so there is no parsing or stringifying to do here.
6
+ //
7
+ // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
+ // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
+ // TypeScript like everything in this file is unaffected.
10
+
11
+ import type { JanelaApp } from "./janela";
12
+
13
+ export function setup(app: JanelaApp): void {
14
+ app.command("add", (args) => {
15
+ const a = args as { a: number; b: number };
16
+ const sum = a.a + a.b;
17
+ // Backend→frontend event: the page listens with janela.listen("added", …).
18
+ app.emit("added", sum);
19
+ return sum;
20
+ });
21
+
22
+ app.command("greet", (args) => {
23
+ const a = args as { name: string };
24
+ return "Hello, " + a.name + " — from the native TS binary";
25
+ });
26
+
27
+ app.command("log", (args) => {
28
+ console.log("[host] page says:", args as string);
29
+ });
30
+
31
+ // An async command: answers later, without freezing the window.
32
+ app.commandAsync("wait", (args, resolve) => {
33
+ const a = args as { ms: number };
34
+ app.sleep(a.ms, () => {
35
+ resolve("waited " + a.ms + "ms without blocking the UI");
36
+ });
37
+ });
38
+
39
+ app.command("quit", () => {
40
+ app.quit();
41
+ });
42
+ }
@@ -0,0 +1,3 @@
1
+ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
2
+
3
+ export default { preprocess: vitePreprocess() };
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
3
+
4
+ // janela flattens this build into one HTML document at `janela build` time,
5
+ // so there is no server and no base path to configure.
6
+ export default defineConfig({ plugins: [svelte()] });
@@ -0,0 +1,4 @@
1
+ {
2
+ "dependencies": { "vue": "^3.5.13" },
3
+ "devDependencies": { "@vitejs/plugin-vue": "^5.2.1", "vite": "^6.0.7" }
4
+ }
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>__NAME__</title>
6
+ </head>
7
+ <body>
8
+ <div id="app"></div>
9
+ <script type="module" src="/src/main.js"></script>
10
+ </body>
11
+ </html>
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "__NAME__",
3
+ "identifier": "dev.janela.__NAME__",
4
+ "version": "0.1.0",
5
+ "window": {
6
+ "title": "__NAME__",
7
+ "width": 640,
8
+ "height": 420
9
+ }
10
+ }
@@ -0,0 +1,39 @@
1
+ <script setup>
2
+ import { onMounted, ref } from "vue";
3
+
4
+ const greeting = ref("…");
5
+ const a = ref(2);
6
+ const b = ref(40);
7
+ const sum = ref(null);
8
+ const events = ref([]);
9
+
10
+ // Backend→frontend events. The payload arrives as a value, not a JSON string.
11
+ janela.listen("added", (value) => events.value.unshift(`host emitted: ${value}`));
12
+
13
+ onMounted(async () => {
14
+ greeting.value = await janela.invoke("greet", { name: "__NAME__" });
15
+ });
16
+
17
+ async function add() {
18
+ sum.value = await janela.invoke("add", { a: Number(a.value), b: Number(b.value) });
19
+ }
20
+ </script>
21
+
22
+ <template>
23
+ <h1>{{ greeting }}</h1>
24
+ <p>
25
+ <input v-model="a" type="number" /> +
26
+ <input v-model="b" type="number" />
27
+ <button @click="add">add</button>
28
+ <span v-if="sum !== null"> = {{ sum }}</span>
29
+ </p>
30
+ <ul>
31
+ <li v-for="(e, i) in events" :key="i">{{ e }}</li>
32
+ </ul>
33
+ </template>
34
+
35
+ <style>
36
+ body { font: 16px -apple-system, system-ui, sans-serif; padding: 2rem; }
37
+ input { width: 5em; }
38
+ ul { color: #666; font-size: 13px; }
39
+ </style>
@@ -0,0 +1,4 @@
1
+ import { createApp } from "vue";
2
+ import App from "./App.vue";
3
+
4
+ createApp(App).mount("#app");
@@ -0,0 +1,42 @@
1
+ // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
+ //
3
+ // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
+ // Handlers take the arguments as a value and return a value — the runtime owns
5
+ // JSON at the boundary, so there is no parsing or stringifying to do here.
6
+ //
7
+ // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
+ // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
+ // TypeScript like everything in this file is unaffected.
10
+
11
+ import type { JanelaApp } from "./janela";
12
+
13
+ export function setup(app: JanelaApp): void {
14
+ app.command("add", (args) => {
15
+ const a = args as { a: number; b: number };
16
+ const sum = a.a + a.b;
17
+ // Backend→frontend event: the page listens with janela.listen("added", …).
18
+ app.emit("added", sum);
19
+ return sum;
20
+ });
21
+
22
+ app.command("greet", (args) => {
23
+ const a = args as { name: string };
24
+ return "Hello, " + a.name + " — from the native TS binary";
25
+ });
26
+
27
+ app.command("log", (args) => {
28
+ console.log("[host] page says:", args as string);
29
+ });
30
+
31
+ // An async command: answers later, without freezing the window.
32
+ app.commandAsync("wait", (args, resolve) => {
33
+ const a = args as { ms: number };
34
+ app.sleep(a.ms, () => {
35
+ resolve("waited " + a.ms + "ms without blocking the UI");
36
+ });
37
+ });
38
+
39
+ app.command("quit", () => {
40
+ app.quit();
41
+ });
42
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import vue from "@vitejs/plugin-vue";
3
+
4
+ // janela flattens this build into one HTML document at `janela build` time,
5
+ // so there is no server and no base path to configure.
6
+ export default defineConfig({ plugins: [vue()] });