janela 0.14.1 → 0.14.3
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 +16 -3
- package/api/index.d.ts +11 -1
- package/bin/janela.mjs +72 -12
- package/bin/lib.mjs +27 -0
- package/package.json +1 -1
- package/templates/index.html +224 -53
- package/templates/janela.conf.json +2 -2
- package/templates/react/files/index.html +1 -0
- package/templates/react/files/janela.conf.json +2 -2
- package/templates/react/files/src/App.tsx +154 -37
- package/templates/react/files/src/main.tsx +1 -0
- package/templates/react/files/src/styles.css +137 -0
- package/templates/react/files/src-host/main.ts +82 -0
- package/templates/solid/files/index.html +1 -0
- package/templates/solid/files/janela.conf.json +2 -2
- package/templates/solid/files/src/App.tsx +145 -35
- package/templates/solid/files/src/main.tsx +1 -0
- package/templates/solid/files/src/styles.css +137 -0
- package/templates/solid/files/src-host/main.ts +82 -0
- package/templates/svelte/files/index.html +1 -0
- package/templates/svelte/files/janela.conf.json +2 -2
- package/templates/svelte/files/src/App.svelte +116 -27
- package/templates/svelte/files/src/main.ts +1 -0
- package/templates/svelte/files/src/styles.css +137 -0
- package/templates/svelte/files/src-host/main.ts +82 -0
- package/templates/vue/files/index.html +1 -0
- package/templates/vue/files/janela.conf.json +2 -2
- package/templates/vue/files/src/App.vue +99 -20
- package/templates/vue/files/src/main.ts +1 -0
- package/templates/vue/files/src/styles.css +137 -0
- package/templates/vue/files/src-host/main.ts +82 -0
- package/templates/react/files/src/App.css +0 -3
- package/templates/solid/files/src/App.css +0 -3
package/README.md
CHANGED
|
@@ -6,9 +6,11 @@ Desktop and mobile apps in pure TypeScript, compiled to native. No Rust, no
|
|
|
6
6
|
Node, no Electron. The backend is TypeScript compiled to a native binary by
|
|
7
7
|
[scriptc](https://scriptc.dev); the window is the OS webview via
|
|
8
8
|
[webview/webview](https://github.com/webview/webview). A desktop binary comes
|
|
9
|
-
out around
|
|
9
|
+
out around 225–420 KB — 225 KB for the smallest template — with no bundled
|
|
10
10
|
browser and no bundled runtime; iOS and Android bundles land around
|
|
11
|
-
|
|
11
|
+
228–421 KB. Those are the *starter's* figures, and about 35 KB of each is the
|
|
12
|
+
native file dialog and worker-thread reader it demonstrates: delete the "Files
|
|
13
|
+
and the window" card and its two commands and the smallest binary is 212 KB. Per-template figures are in
|
|
12
14
|
[docs/frontend.md](../../docs/frontend.md).
|
|
13
15
|
|
|
14
16
|
Five targets, one runtime — the same `main.ts`, the same typed contract and the
|
|
@@ -30,6 +32,7 @@ desktop-only for now; on mobile they report clearly when called.
|
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
34
|
npm install -g janela # or: npx janela init my-app
|
|
35
|
+
janela --version # which janela you actually have
|
|
33
36
|
janela init my-app # (or `jn init my-app`)
|
|
34
37
|
cd my-app
|
|
35
38
|
janela dev # build + run with logs in the terminal
|
|
@@ -40,10 +43,20 @@ Or start from a frontend framework:
|
|
|
40
43
|
|
|
41
44
|
```bash
|
|
42
45
|
janela init my-app --template vue # or react | svelte | solid | vanilla
|
|
43
|
-
cd my-app
|
|
46
|
+
cd my-app # deps are already installed
|
|
44
47
|
janela dev # Vite dev server + HMR, in a native window
|
|
45
48
|
```
|
|
46
49
|
|
|
50
|
+
`init` installs the template's dependencies with whichever package manager
|
|
51
|
+
ran it — `pnpm janela init` uses pnpm — so `janela dev` works straight
|
|
52
|
+
afterwards. Pass `--no-install` to skip that, and if the install fails the
|
|
53
|
+
project is still written; the message names the one command to retry.
|
|
54
|
+
|
|
55
|
+
A `--template` a globally installed CLI has never heard of is now an error
|
|
56
|
+
rather than a silent fallback, and `janela --version` says which one you are
|
|
57
|
+
running — worth checking first if a scaffolded project comes out looking
|
|
58
|
+
nothing like the screenshots.
|
|
59
|
+
|
|
47
60
|
`vanilla` is the default and needs no frontend toolchain at all. With a
|
|
48
61
|
framework, `janela dev` runs your Vite dev server and points the window at it,
|
|
49
62
|
and `janela build` flattens the production bundle into the binary.
|
package/api/index.d.ts
CHANGED
|
@@ -94,10 +94,20 @@ export interface JanelaClient<A> {
|
|
|
94
94
|
/**
|
|
95
95
|
* Call a declared command. Unknown names and wrong argument shapes are
|
|
96
96
|
* compile errors, and the result type comes from the contract.
|
|
97
|
+
*
|
|
98
|
+
* A command declared to take nothing — `quit: () => void` — normalises to
|
|
99
|
+
* `args: null`, and for those the argument is omitted rather than passed as
|
|
100
|
+
* an explicit `null`. That is what the rest-tuple is for: it makes the
|
|
101
|
+
* parameter optional for exactly those commands, and required with the
|
|
102
|
+
* declared shape for every other one. The bracketed `[X] extends [null]`
|
|
103
|
+
* form is deliberate — a bare `null extends X` would also match a command
|
|
104
|
+
* whose arguments are legitimately nullable.
|
|
97
105
|
*/
|
|
98
106
|
invoke<K extends keyof CommandsOf<A> & string>(
|
|
99
107
|
name: K,
|
|
100
|
-
args: CommandsOf<A>[K]["args"]
|
|
108
|
+
...args: [CommandsOf<A>[K]["args"]] extends [null]
|
|
109
|
+
? [args?: null]
|
|
110
|
+
: [args: CommandsOf<A>[K]["args"]]
|
|
101
111
|
): Promise<CommandsOf<A>[K]["result"]>;
|
|
102
112
|
/**
|
|
103
113
|
* Subscribe to a declared event; the payload type is inferred. Returns a
|
package/bin/janela.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// janela — the CLI (the tauri-cli analogue).
|
|
3
3
|
//
|
|
4
|
-
// janela init <name> scaffold a new project
|
|
4
|
+
// janela init <name> scaffold a new project (and install its dependencies)
|
|
5
5
|
// janela build compile the project to a native binary (+ .app on macOS)
|
|
6
6
|
// janela dev build, then run the binary with logs in the terminal
|
|
7
|
+
// janela --version which janela this is
|
|
7
8
|
//
|
|
8
9
|
// A project is: index.html (frontend), src-host/main.ts (commands),
|
|
9
10
|
// janela.conf.json (window + bundle config). Everything else — the C shim over
|
|
@@ -20,12 +21,15 @@ import { dirname, join, relative, resolve, sep } from "node:path";
|
|
|
20
21
|
import { fileURLToPath } from "node:url";
|
|
21
22
|
import {
|
|
22
23
|
ANDROID_ABI, ANDROID_TARGET_SDK, androidConf, ffiManifest, iosConf,
|
|
23
|
-
libraryProfile, mimeFor, NAME_RE,
|
|
24
|
-
rewriteHostSpecifier, suggestName,
|
|
24
|
+
installCommand, libraryProfile, mimeFor, NAME_RE, packageManager as pmFor,
|
|
25
|
+
patchPeSubsystem, PeError, rewriteHostSpecifier, suggestName,
|
|
25
26
|
} from "./lib.mjs";
|
|
26
27
|
|
|
27
28
|
const KIT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
29
|
const require = createRequire(join(KIT, "package.json"));
|
|
30
|
+
// createRequire is rooted AT the package.json, so "./package.json" is this
|
|
31
|
+
// package's own — not the bin/ directory's and not the workspace's.
|
|
32
|
+
const VERSION = require("./package.json").version;
|
|
29
33
|
|
|
30
34
|
function fail(msg) {
|
|
31
35
|
console.error(`janela: ${msg}`);
|
|
@@ -103,8 +107,9 @@ function viteCommand(root) {
|
|
|
103
107
|
if (existsSync(shim)) return { argv: [shim], shell: process.platform === "win32" };
|
|
104
108
|
|
|
105
109
|
fail(
|
|
106
|
-
"this project has a vite config but no local vite —
|
|
107
|
-
|
|
110
|
+
"this project has a vite config but no local vite — its dependencies are not " +
|
|
111
|
+
`installed yet:\n ${installCommand(pmFor(process.env)).join(" ")}\n` +
|
|
112
|
+
" (janela init installs them for you unless you pass --no-install)",
|
|
108
113
|
);
|
|
109
114
|
}
|
|
110
115
|
|
|
@@ -1156,7 +1161,29 @@ function copyTemplate(from, to, name) {
|
|
|
1156
1161
|
|
|
1157
1162
|
// Best-effort repair of a rejected name, so the error can suggest something
|
|
1158
1163
|
// that would have worked instead of only stating the rule.
|
|
1159
|
-
|
|
1164
|
+
/**
|
|
1165
|
+
* Install a scaffolded project's dependencies.
|
|
1166
|
+
*
|
|
1167
|
+
* Returns true on success. A failure is NOT fatal: the project is already
|
|
1168
|
+
* written, and the useful thing to do is name the one command to retry rather
|
|
1169
|
+
* than exit non-zero on a tree that is fine. Offline, an unreachable registry
|
|
1170
|
+
* and missing auth all land here.
|
|
1171
|
+
*
|
|
1172
|
+
* Windows needs a shell: npm/pnpm/yarn are .cmd shims, and Node has refused to
|
|
1173
|
+
* spawn those directly since the CVE-2024-27980 fix.
|
|
1174
|
+
*/
|
|
1175
|
+
function installDeps(dir, pm) {
|
|
1176
|
+
const cmd = installCommand(pm);
|
|
1177
|
+
console.log(`janela: installing dependencies with ${pm}…`);
|
|
1178
|
+
const r = spawnSync(cmd[0], cmd.slice(1), {
|
|
1179
|
+
stdio: "inherit",
|
|
1180
|
+
cwd: dir,
|
|
1181
|
+
shell: process.platform === "win32",
|
|
1182
|
+
});
|
|
1183
|
+
return r.status === 0;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function init(name, template, { install = true } = {}) {
|
|
1160
1187
|
if (!name) {
|
|
1161
1188
|
fail(
|
|
1162
1189
|
"no project name given.\n" +
|
|
@@ -1212,8 +1239,27 @@ function init(name, template) {
|
|
|
1212
1239
|
writeFileSync(join(dir, ".gitignore"), ".janela/\nnode_modules/\ndist/\n");
|
|
1213
1240
|
writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
1214
1241
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1242
|
+
// The no-framework template has no dependencies to install: it needs no
|
|
1243
|
+
// frontend toolchain, and `janela dev` works on it straight out of init.
|
|
1244
|
+
const hasDeps = template !== "vanilla";
|
|
1245
|
+
if (!hasDeps || !install) {
|
|
1246
|
+
// Only a skipped install on a template that HAS dependencies leaves the
|
|
1247
|
+
// caller something to run.
|
|
1248
|
+
const manual = hasDeps ? `${installCommand(pmFor(process.env)).join(" ")} && ` : "";
|
|
1249
|
+
console.log(`janela: created ${name}/ (${template}) — next: cd ${name} && ${manual}janela dev`);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const pm = pmFor(process.env);
|
|
1254
|
+
if (installDeps(dir, pm)) {
|
|
1255
|
+
console.log(`janela: created ${name}/ (${template}) — next: cd ${name} && janela dev`);
|
|
1256
|
+
} else {
|
|
1257
|
+
console.log(
|
|
1258
|
+
`janela: created ${name}/ (${template}), but installing its dependencies failed.\n` +
|
|
1259
|
+
` The project is written and fine — retry the install and you are set:\n` +
|
|
1260
|
+
` cd ${name} && ${installCommand(pm).join(" ")} && janela dev`,
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1217
1263
|
}
|
|
1218
1264
|
|
|
1219
1265
|
// ---- dev --------------------------------------------------------------------
|
|
@@ -1277,6 +1323,16 @@ async function dev(root) {
|
|
|
1277
1323
|
const argv = process.argv.slice(2);
|
|
1278
1324
|
const cmd = argv[0];
|
|
1279
1325
|
|
|
1326
|
+
// Answered before anything else, because the question it settles is "is the
|
|
1327
|
+
// janela running this command the one I think it is?". A globally installed
|
|
1328
|
+
// CLI several minor versions behind accepts `init` and quietly ignores flags it
|
|
1329
|
+
// has never heard of — `--template` did not exist before 0.3.0 — so the only
|
|
1330
|
+
// symptom is a project that came out looking wrong.
|
|
1331
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
1332
|
+
console.log(VERSION);
|
|
1333
|
+
process.exit(0);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1280
1336
|
function flag(name, fallback) {
|
|
1281
1337
|
const eq = argv.find((a) => a.startsWith(`--${name}=`));
|
|
1282
1338
|
if (eq) return eq.slice(name.length + 3);
|
|
@@ -1335,9 +1391,11 @@ function assertPositionals(max) {
|
|
|
1335
1391
|
|
|
1336
1392
|
switch (cmd) {
|
|
1337
1393
|
case "init":
|
|
1338
|
-
assertKnownFlags(["template"]);
|
|
1394
|
+
assertKnownFlags(["template", "no-install"]);
|
|
1339
1395
|
assertPositionals(1);
|
|
1340
|
-
init(positionals()[0], flag("template", "vanilla")
|
|
1396
|
+
init(positionals()[0], flag("template", "vanilla"), {
|
|
1397
|
+
install: !argv.includes("--no-install"),
|
|
1398
|
+
});
|
|
1341
1399
|
break;
|
|
1342
1400
|
case "build":
|
|
1343
1401
|
assertKnownFlags(["target"]);
|
|
@@ -1356,9 +1414,11 @@ switch (cmd) {
|
|
|
1356
1414
|
break;
|
|
1357
1415
|
default:
|
|
1358
1416
|
console.log(
|
|
1359
|
-
|
|
1417
|
+
`janela ${VERSION}\n\n` +
|
|
1418
|
+
"usage: janela init <name> [--template vanilla|vue|react|svelte|solid] [--no-install]\n" +
|
|
1360
1419
|
" janela build [--target desktop|ios|android]\n" +
|
|
1361
|
-
" janela dev [--target desktop|ios|android]"
|
|
1420
|
+
" janela dev [--target desktop|ios|android]\n" +
|
|
1421
|
+
" janela --version",
|
|
1362
1422
|
);
|
|
1363
1423
|
process.exit(cmd ? 1 : 0);
|
|
1364
1424
|
}
|
package/bin/lib.mjs
CHANGED
|
@@ -345,3 +345,30 @@ export function patchPeSubsystem(input) {
|
|
|
345
345
|
buf.writeUInt16LE(IMAGE_SUBSYSTEM_WINDOWS_GUI, subOff);
|
|
346
346
|
return { patched: true, buf };
|
|
347
347
|
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Which package manager to install a scaffolded project with.
|
|
351
|
+
*
|
|
352
|
+
* `npm_config_user_agent` is set by whichever manager ran janela, so
|
|
353
|
+
* `pnpm janela init` installs with pnpm. Run as a plain global binary it is
|
|
354
|
+
* unset, and npm is the only manager guaranteed to exist alongside node.
|
|
355
|
+
*
|
|
356
|
+
* Takes the environment rather than reading process.env, so it is testable.
|
|
357
|
+
*/
|
|
358
|
+
export function packageManager(env = {}) {
|
|
359
|
+
const ua = env.npm_config_user_agent ?? "";
|
|
360
|
+
for (const pm of ["pnpm", "yarn", "bun"]) if (ua.startsWith(`${pm}/`)) return pm;
|
|
361
|
+
return "npm";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The install invocation for a manager.
|
|
366
|
+
*
|
|
367
|
+
* yarn's is the bare command. npm gets --no-fund --no-audit: neither is useful
|
|
368
|
+
* on a freshly scaffolded tree, and the audit is the slowest part of it.
|
|
369
|
+
*/
|
|
370
|
+
export function installCommand(pm) {
|
|
371
|
+
if (pm === "yarn") return ["yarn"];
|
|
372
|
+
if (pm === "npm") return ["npm", "install", "--no-fund", "--no-audit"];
|
|
373
|
+
return [pm, "install"];
|
|
374
|
+
}
|
package/package.json
CHANGED
package/templates/index.html
CHANGED
|
@@ -1,33 +1,204 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>__NAME__</title>
|
|
5
7
|
<style>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
/* Everything here is yours to replace. It is deliberately one file with
|
|
9
|
+
no dependencies: this template has no bundler, so what you see is what
|
|
10
|
+
runs. The mark below is drawn in CSS rather than shipped as an image —
|
|
11
|
+
a janela frontend is embedded in the binary, so every asset has to be
|
|
12
|
+
inlined anyway, and a window needs no artwork. */
|
|
13
|
+
:root {
|
|
14
|
+
color-scheme: light dark;
|
|
15
|
+
--bg: #f6f6f7;
|
|
16
|
+
--card: #ffffff;
|
|
17
|
+
--text: #18181b;
|
|
18
|
+
--muted: #71717a;
|
|
19
|
+
--line: #e4e4e7;
|
|
20
|
+
--accent: #2563eb;
|
|
21
|
+
--accent-soft: #eff4ff;
|
|
22
|
+
--shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 8px 24px -12px rgb(0 0 0 / 0.12);
|
|
23
|
+
}
|
|
24
|
+
@media (prefers-color-scheme: dark) {
|
|
25
|
+
:root {
|
|
26
|
+
--bg: #17171a;
|
|
27
|
+
--card: #202024;
|
|
28
|
+
--text: #fafafa;
|
|
29
|
+
--muted: #a1a1aa;
|
|
30
|
+
--line: #2e2e34;
|
|
31
|
+
--accent: #60a5fa;
|
|
32
|
+
--accent-soft: #1d2433;
|
|
33
|
+
--shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 8px 24px -12px rgb(0 0 0 / 0.5);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
* { box-sizing: border-box; }
|
|
38
|
+
body {
|
|
39
|
+
margin: 0;
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
display: flex;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
background: var(--bg);
|
|
44
|
+
color: var(--text);
|
|
45
|
+
font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
46
|
+
-webkit-font-smoothing: antialiased;
|
|
47
|
+
}
|
|
48
|
+
main { width: 100%; max-width: 760px; padding: 26px 22px 32px; }
|
|
49
|
+
|
|
50
|
+
/* --- header ------------------------------------------------------- */
|
|
51
|
+
header { text-align: center; margin-bottom: 20px; }
|
|
52
|
+
.mark {
|
|
53
|
+
width: 56px; height: 44px; margin: 0 auto 14px;
|
|
54
|
+
border: 2px solid var(--text); border-radius: 8px;
|
|
55
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
56
|
+
}
|
|
57
|
+
.mark .bar {
|
|
58
|
+
height: 13px; flex: none;
|
|
59
|
+
border-bottom: 2px solid var(--text);
|
|
60
|
+
display: flex; align-items: center; gap: 3px; padding: 0 4px;
|
|
61
|
+
}
|
|
62
|
+
.mark .bar i { width: 3px; height: 3px; border-radius: 50%; background: var(--text); }
|
|
63
|
+
.mark .pane { flex: 1; background: linear-gradient(135deg, var(--accent), transparent 70%); opacity: 0.85; }
|
|
64
|
+
h1 { margin: 0; font-size: 26px; letter-spacing: -0.02em; font-weight: 650; }
|
|
65
|
+
.sub { margin: 5px 0 0; color: var(--muted); font-size: 13.5px; }
|
|
66
|
+
|
|
67
|
+
/* The one line that proves the whole stack: this string was produced by
|
|
68
|
+
TypeScript compiled to machine code, and handed to the page. */
|
|
69
|
+
.greeting {
|
|
70
|
+
margin: 0 0 20px;
|
|
71
|
+
padding: 11px 16px;
|
|
72
|
+
border: 1px solid var(--line);
|
|
73
|
+
border-radius: 10px;
|
|
74
|
+
background: var(--accent-soft);
|
|
75
|
+
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
76
|
+
text-align: center;
|
|
77
|
+
overflow-wrap: anywhere;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* --- cards -------------------------------------------------------- */
|
|
81
|
+
/* auto-fit rather than a fixed column count: the same page is the iOS and
|
|
82
|
+
Android frontend, where it must read as a single column. */
|
|
83
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 13px; }
|
|
84
|
+
.card {
|
|
85
|
+
background: var(--card);
|
|
86
|
+
border: 1px solid var(--line);
|
|
87
|
+
border-radius: 12px;
|
|
88
|
+
padding: 15px 17px;
|
|
89
|
+
box-shadow: var(--shadow);
|
|
90
|
+
}
|
|
91
|
+
.card h2 {
|
|
92
|
+
margin: 0 0 2px;
|
|
93
|
+
font-size: 13px; font-weight: 600;
|
|
94
|
+
letter-spacing: 0.04em; text-transform: uppercase;
|
|
95
|
+
color: var(--muted);
|
|
96
|
+
}
|
|
97
|
+
.card.wide { grid-column: 1 / -1; }
|
|
98
|
+
.card p.why { margin: 0 0 11px; color: var(--muted); font-size: 12.5px; line-height: 1.45; }
|
|
99
|
+
.card h3 {
|
|
100
|
+
margin: 16px 0 6px; font-size: 11px; font-weight: 600;
|
|
101
|
+
letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted);
|
|
102
|
+
}
|
|
103
|
+
.row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
104
|
+
|
|
105
|
+
input, button {
|
|
106
|
+
font: inherit;
|
|
107
|
+
border-radius: 8px;
|
|
108
|
+
border: 1px solid var(--line);
|
|
109
|
+
padding: 8px 12px;
|
|
110
|
+
background: var(--card);
|
|
111
|
+
color: var(--text);
|
|
112
|
+
transition: border-color 0.15s, background 0.15s;
|
|
113
|
+
}
|
|
114
|
+
input { min-width: 0; }
|
|
115
|
+
input[type="number"] { width: 5.5em; text-align: center; }
|
|
116
|
+
input.grow { flex: 1 1 12em; }
|
|
117
|
+
input:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
118
|
+
button { cursor: pointer; font-weight: 500; }
|
|
119
|
+
button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
|
|
120
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
121
|
+
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
122
|
+
button.primary:hover:not(:disabled) { filter: brightness(1.08); color: #fff; }
|
|
123
|
+
|
|
124
|
+
.result { margin: 12px 0 0; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); }
|
|
125
|
+
.result.filled { color: var(--text); }
|
|
126
|
+
pre.result { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 11em; overflow: auto; margin-top: 12px; }
|
|
127
|
+
|
|
128
|
+
/* A dot that keeps moving while a 2s command is pending. If it stalls,
|
|
129
|
+
the host has blocked the UI thread — which is the bug this proves is
|
|
130
|
+
absent. */
|
|
131
|
+
.pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 1s ease-in-out infinite; }
|
|
132
|
+
@keyframes pulse { 0%, 100% { opacity: 0.25; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.15); } }
|
|
133
|
+
@media (prefers-reduced-motion: reduce) { .pulse { animation: none; opacity: 1; } }
|
|
134
|
+
|
|
135
|
+
ul.events { list-style: none; margin: 0; padding: 0; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
136
|
+
ul.events li { padding: 3px 0; border-bottom: 1px dashed var(--line); }
|
|
137
|
+
ul.events li:last-child { border-bottom: 0; }
|
|
138
|
+
ul.events:empty::after { content: "nothing yet — press add"; color: var(--muted); font-family: inherit; }
|
|
139
|
+
|
|
140
|
+
footer { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: center; color: var(--muted); font-size: 13px; }
|
|
141
|
+
footer code { background: var(--card); border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; font-size: 12px; }
|
|
142
|
+
|
|
9
143
|
</style>
|
|
10
144
|
</head>
|
|
11
145
|
<body>
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
146
|
+
<main>
|
|
147
|
+
<header>
|
|
148
|
+
<div class="mark" aria-hidden="true">
|
|
149
|
+
<div class="bar"><i></i><i></i><i></i></div>
|
|
150
|
+
<div class="pane"></div>
|
|
151
|
+
</div>
|
|
152
|
+
<h1>__NAME__</h1>
|
|
153
|
+
<p class="sub">TypeScript, compiled to a native binary. No Rust, no Node, no Electron.</p>
|
|
154
|
+
</header>
|
|
155
|
+
|
|
156
|
+
<p class="greeting" id="out">booting…</p>
|
|
157
|
+
|
|
158
|
+
<div class="grid">
|
|
159
|
+
<section class="card">
|
|
160
|
+
<h2>Typed commands</h2>
|
|
161
|
+
<p class="why">The name, the arguments and the result are all checked at compile time — no codegen, because both sides are TypeScript.</p>
|
|
162
|
+
<div class="row">
|
|
163
|
+
<input id="a" type="number" value="2" aria-label="first number" />
|
|
164
|
+
<span aria-hidden="true">+</span>
|
|
165
|
+
<input id="b" type="number" value="40" aria-label="second number" />
|
|
166
|
+
<button id="add" class="primary">add</button>
|
|
167
|
+
</div>
|
|
168
|
+
<p class="result" id="sum">—</p>
|
|
169
|
+
<h3>Events from the host</h3>
|
|
170
|
+
<ul class="events" id="events"></ul>
|
|
171
|
+
</section>
|
|
172
|
+
|
|
173
|
+
<section class="card">
|
|
174
|
+
<h2>Async without blocking</h2>
|
|
175
|
+
<p class="why">The window keeps answering while this is pending: the dot keeps moving and <strong>add</strong> still works.</p>
|
|
176
|
+
<div class="row">
|
|
177
|
+
<button id="wait">wait 2s</button>
|
|
178
|
+
<span id="spinner" hidden><span class="pulse"></span></span>
|
|
179
|
+
</div>
|
|
180
|
+
<p class="result" id="waited">—</p>
|
|
181
|
+
</section>
|
|
182
|
+
|
|
183
|
+
<section class="card wide">
|
|
184
|
+
<h2>Files and the window</h2>
|
|
185
|
+
<p class="why">Reads run on a worker thread, so a large file never freezes the window. The dialog is the real native one.</p>
|
|
186
|
+
<div class="row">
|
|
187
|
+
<input id="path" class="grow" type="text" value="janela.conf.json" aria-label="file to read" />
|
|
188
|
+
<button id="read">read</button>
|
|
189
|
+
<button id="open">pick a file…</button>
|
|
190
|
+
<input id="title" class="grow" type="text" value="__NAME__ — renamed" aria-label="window title" />
|
|
191
|
+
<button id="retitle">set title</button>
|
|
192
|
+
</div>
|
|
193
|
+
<pre class="result" id="file">—</pre>
|
|
194
|
+
</section>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<footer>
|
|
198
|
+
<span>Edit <code>index.html</code> for the page and <code>src-host/main.ts</code> for the commands.</span>
|
|
199
|
+
<button id="quit">Quit</button>
|
|
200
|
+
</footer>
|
|
201
|
+
</main>
|
|
31
202
|
|
|
32
203
|
<script>
|
|
33
204
|
// This template has no bundler, so it uses the `janela` global that the
|
|
@@ -35,56 +206,56 @@
|
|
|
35
206
|
// `import { invoke, listen } from "janela/api"` instead — same functions,
|
|
36
207
|
// but typed and resolvable by the bundler.
|
|
37
208
|
window.onload = async () => {
|
|
38
|
-
const
|
|
39
|
-
const
|
|
209
|
+
const $ = (id) => document.getElementById(id);
|
|
210
|
+
const show = (el, text) => {
|
|
211
|
+
el.textContent = text;
|
|
212
|
+
el.classList.add("filled");
|
|
213
|
+
};
|
|
40
214
|
|
|
41
215
|
janela.listen("added", (sum) => {
|
|
42
216
|
const li = document.createElement("li");
|
|
43
|
-
li.textContent = "
|
|
44
|
-
events.prepend(li);
|
|
217
|
+
li.textContent = "host emitted 'added': " + sum;
|
|
218
|
+
$("events").prepend(li);
|
|
45
219
|
});
|
|
46
220
|
|
|
47
|
-
|
|
221
|
+
// The greeting is a round trip: the string is built in compiled
|
|
222
|
+
// TypeScript and returned to the page.
|
|
223
|
+
$("out").textContent = await janela.invoke("greet", { name: "__NAME__" });
|
|
48
224
|
await janela.invoke("log", "page loaded");
|
|
49
225
|
|
|
50
|
-
|
|
51
|
-
const a = Number(
|
|
52
|
-
const b = Number(
|
|
53
|
-
|
|
54
|
-
out.textContent = `add(${a}, ${b}) -> ${sum}`;
|
|
226
|
+
$("add").onclick = async () => {
|
|
227
|
+
const a = Number($("a").value);
|
|
228
|
+
const b = Number($("b").value);
|
|
229
|
+
show($("sum"), `add(${a}, ${b}) → ${await janela.invoke("add", { a, b })}`);
|
|
55
230
|
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
document.getElementById("wait").onclick = async (e) => {
|
|
231
|
+
|
|
232
|
+
$("wait").onclick = async (e) => {
|
|
59
233
|
e.target.disabled = true;
|
|
60
|
-
|
|
61
|
-
|
|
234
|
+
$("spinner").hidden = false;
|
|
235
|
+
show($("waited"), "waiting… try add, it still answers");
|
|
236
|
+
const answer = await janela.invoke("wait", { ms: 2000 });
|
|
237
|
+
show($("waited"), answer);
|
|
238
|
+
$("spinner").hidden = true;
|
|
62
239
|
e.target.disabled = false;
|
|
63
240
|
};
|
|
64
241
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
document.getElementById("read").onclick = async () => {
|
|
68
|
-
const path = document.getElementById("path").value;
|
|
242
|
+
$("read").onclick = async () => {
|
|
243
|
+
const path = $("path").value;
|
|
69
244
|
const r = await janela.invoke("readFile", { path });
|
|
70
|
-
|
|
71
|
-
? `${path}: ${r.length} chars\n\n` + r.text.slice(0, 400)
|
|
72
|
-
: "error: " + r.error;
|
|
245
|
+
show($("file"), r.ok ? `${path} — ${r.length} chars\n\n${r.text.slice(0, 400)}` : "error: " + r.error);
|
|
73
246
|
};
|
|
74
247
|
|
|
75
248
|
// The native open dialog. The window keeps serving other commands
|
|
76
249
|
// while the user is deciding.
|
|
77
|
-
|
|
250
|
+
$("open").onclick = async () => {
|
|
78
251
|
const r = await janela.invoke("openFile", {});
|
|
79
|
-
if (!r.ok)
|
|
80
|
-
else if (r.cancelled)
|
|
81
|
-
else
|
|
252
|
+
if (!r.ok) show($("file"), "error: " + r.error);
|
|
253
|
+
else if (r.cancelled) show($("file"), "cancelled");
|
|
254
|
+
else show($("file"), `${r.path} — ${r.length} chars\n\n${r.text.slice(0, 400)}`);
|
|
82
255
|
};
|
|
83
256
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
document.getElementById("quit").onclick = () => janela.invoke("quit");
|
|
257
|
+
$("retitle").onclick = () => janela.invoke("setTitle", { title: $("title").value });
|
|
258
|
+
$("quit").onclick = () => janela.invoke("quit");
|
|
88
259
|
};
|
|
89
260
|
</script>
|
|
90
261
|
</body>
|