janela 0.15.0 → 0.16.1
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 +9 -5
- package/package.json +1 -1
- package/shim/wvshim.cc +142 -0
- package/templates/index.html +87 -198
- package/templates/main.ts +6 -68
- package/templates/react/files/src/App.tsx +44 -148
- package/templates/react/files/src/styles.css +61 -99
- package/templates/react/files/src-host/main.ts +10 -111
- package/templates/solid/files/src/App.tsx +39 -141
- package/templates/solid/files/src/styles.css +61 -99
- package/templates/solid/files/src-host/main.ts +10 -111
- package/templates/svelte/files/src/App.svelte +25 -110
- package/templates/svelte/files/src/styles.css +61 -99
- package/templates/svelte/files/src-host/main.ts +10 -111
- package/templates/vue/files/src/App.vue +27 -106
- package/templates/vue/files/src/styles.css +61 -99
- package/templates/vue/files/src-host/main.ts +10 -111
package/README.md
CHANGED
|
@@ -11,11 +11,12 @@ Desktop and mobile apps in pure TypeScript, compiled to native. No Rust, no
|
|
|
11
11
|
Node, no Electron. The backend is TypeScript compiled to a native binary by
|
|
12
12
|
[scriptc](https://scriptc.dev); the window is the OS webview via
|
|
13
13
|
[webview/webview](https://github.com/webview/webview). A desktop binary comes
|
|
14
|
-
out around
|
|
14
|
+
out around 190–390 KB — 191 KB for the smallest template — with no bundled
|
|
15
15
|
browser and no bundled runtime; iOS and Android bundles land around
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
195–388 KB. Those are the *starter's* figures: it calls one command, so it
|
|
17
|
+
links almost nothing beyond the runtime. Reaching for a platform API adds its
|
|
18
|
+
code — the native file dialog and the worker-thread reader together are about
|
|
19
|
+
35 KB — which is the shape worth remembering: what you pay is what you call. Per-template figures are in
|
|
19
20
|
[docs/frontend.md](../../docs/frontend.md).
|
|
20
21
|
|
|
21
22
|
Five targets, one runtime — the same `main.ts`, the same typed contract and the
|
|
@@ -672,4 +673,7 @@ gh release create v<version> --generate-notes --verify-tag
|
|
|
672
673
|
## License
|
|
673
674
|
|
|
674
675
|
MIT. Bundles [webview/webview](https://github.com/webview/webview) headers
|
|
675
|
-
(MIT, © Serge Zaitsev and contributors).
|
|
676
|
+
(MIT, © Serge Zaitsev and contributors). The starter page takes its shape from
|
|
677
|
+
[create-tauri-app](https://github.com/tauri-apps/create-tauri-app) (MIT), and
|
|
678
|
+
the Vite, Vue, React, Svelte and Solid marks it inlines are each project's
|
|
679
|
+
own.
|
package/package.json
CHANGED
package/shim/wvshim.cc
CHANGED
|
@@ -358,6 +358,137 @@ bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
|
358
358
|
|
|
359
359
|
#if defined(__APPLE__)
|
|
360
360
|
|
|
361
|
+
// A standard macOS main menu.
|
|
362
|
+
//
|
|
363
|
+
// On macOS a Command-key shortcut is a MENU key equivalent, not a
|
|
364
|
+
// window-manager gesture the way Alt+F4 is on Windows. With no main menu
|
|
365
|
+
// nothing claims Cmd+Q, Cmd+C, Cmd+V, Cmd+Z or Cmd+A, and a webview app simply
|
|
366
|
+
// appears to ignore them — the window had zero menu bars, which is exactly
|
|
367
|
+
// what that looks like from the outside.
|
|
368
|
+
//
|
|
369
|
+
// webview.h leaves this to the embedder on purpose: webview/webview#127 is
|
|
370
|
+
// still open, and #237, which added precisely this Edit menu, was closed with
|
|
371
|
+
// "anyone who is impatient can always take this code on their own". Tauri does
|
|
372
|
+
// the same thing one layer up, through muda.
|
|
373
|
+
//
|
|
374
|
+
// Every item here is a STANDARD AppKit selector travelling up the responder
|
|
375
|
+
// chain, so there is nothing to call back into TypeScript for and no new FFI
|
|
376
|
+
// surface. WKWebView answers the editing ones itself. Custom menus — a
|
|
377
|
+
// declarative table passed down from the host — are a separate feature.
|
|
378
|
+
static void install_main_menu() {
|
|
379
|
+
using namespace webview::detail;
|
|
380
|
+
objc::autoreleasepool arp;
|
|
381
|
+
|
|
382
|
+
id app = objc::msg_send<id>(objc::get_class("NSApplication"),
|
|
383
|
+
objc::selector("sharedApplication"));
|
|
384
|
+
if (!app) return;
|
|
385
|
+
// Idempotent: an embedder that already installed a menu keeps it.
|
|
386
|
+
if (objc::msg_send<id>(app, objc::selector("mainMenu"))) return;
|
|
387
|
+
|
|
388
|
+
// NSEventModifierFlags. The default for a key equivalent is Command alone,
|
|
389
|
+
// so only the combinations need naming.
|
|
390
|
+
const NSUInteger kShift = 1UL << 17;
|
|
391
|
+
const NSUInteger kControl = 1UL << 18;
|
|
392
|
+
const NSUInteger kOption = 1UL << 19;
|
|
393
|
+
const NSUInteger kCommand = 1UL << 20;
|
|
394
|
+
|
|
395
|
+
auto str = [](const std::string &v) {
|
|
396
|
+
return cocoa::NSString_stringWithUTF8String(v);
|
|
397
|
+
};
|
|
398
|
+
auto alloc_init = [](const char *cls) {
|
|
399
|
+
return objc::msg_send<id>(
|
|
400
|
+
objc::msg_send<id>(objc::get_class(cls), objc::selector("alloc")),
|
|
401
|
+
objc::selector("init"));
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// AppKit labels the first submenu from the bundle, not from this title, but
|
|
405
|
+
// "About <name>" and "Quit <name>" are ours to spell.
|
|
406
|
+
id process = objc::msg_send<id>(objc::get_class("NSProcessInfo"),
|
|
407
|
+
objc::selector("processInfo"));
|
|
408
|
+
const char *raw =
|
|
409
|
+
process ? objc::msg_send<const char *>(
|
|
410
|
+
objc::msg_send<id>(process, objc::selector("processName")),
|
|
411
|
+
objc::selector("UTF8String"))
|
|
412
|
+
: nullptr;
|
|
413
|
+
std::string name = raw ? raw : "App";
|
|
414
|
+
|
|
415
|
+
id menubar = alloc_init("NSMenu");
|
|
416
|
+
|
|
417
|
+
auto submenu = [&](const std::string &title) {
|
|
418
|
+
id holder = alloc_init("NSMenuItem");
|
|
419
|
+
id menu = objc::msg_send<id>(
|
|
420
|
+
objc::msg_send<id>(objc::get_class("NSMenu"), objc::selector("alloc")),
|
|
421
|
+
objc::selector("initWithTitle:"), str(title));
|
|
422
|
+
objc::msg_send<void>(holder, objc::selector("setSubmenu:"), menu);
|
|
423
|
+
objc::msg_send<void>(menubar, objc::selector("addItem:"), holder);
|
|
424
|
+
return menu;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
auto item = [&](id menu, const std::string &title, const char *sel,
|
|
428
|
+
const std::string &key, NSUInteger mods) {
|
|
429
|
+
id it = objc::msg_send<id>(
|
|
430
|
+
objc::msg_send<id>(objc::get_class("NSMenuItem"),
|
|
431
|
+
objc::selector("alloc")),
|
|
432
|
+
objc::selector("initWithTitle:action:keyEquivalent:"), str(title),
|
|
433
|
+
sel ? objc::selector(sel) : nullptr, str(key));
|
|
434
|
+
if (mods) {
|
|
435
|
+
objc::msg_send<void>(
|
|
436
|
+
it, objc::selector("setKeyEquivalentModifierMask:"), mods);
|
|
437
|
+
}
|
|
438
|
+
objc::msg_send<void>(menu, objc::selector("addItem:"), it);
|
|
439
|
+
};
|
|
440
|
+
auto separator = [&](id menu) {
|
|
441
|
+
objc::msg_send<void>(menu, objc::selector("addItem:"),
|
|
442
|
+
objc::msg_send<id>(objc::get_class("NSMenuItem"),
|
|
443
|
+
objc::selector("separatorItem")));
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
id app_menu = submenu(name);
|
|
447
|
+
item(app_menu, "About " + name, "orderFrontStandardAboutPanel:", "", 0);
|
|
448
|
+
separator(app_menu);
|
|
449
|
+
item(app_menu, "Hide " + name, "hide:", "h", 0);
|
|
450
|
+
item(app_menu, "Hide Others", "hideOtherApplications:", "h",
|
|
451
|
+
kOption | kCommand);
|
|
452
|
+
item(app_menu, "Show All", "unhideAllApplications:", "", 0);
|
|
453
|
+
separator(app_menu);
|
|
454
|
+
// performClose:, not terminate: — measured, not assumed. Both quit and
|
|
455
|
+
// both exit 0, but terminate: exits the process itself, so the host never
|
|
456
|
+
// returns from wv_run and anything after app.run() is skipped;
|
|
457
|
+
// performClose: goes through the same window-close path the red button
|
|
458
|
+
// uses, the run loop unwinds, and the host prints its own "run returned 0".
|
|
459
|
+
// One shutdown path instead of two. muda picks terminate: because Tauri's
|
|
460
|
+
// app logic is native and multi-window; janela is single-window, so closing
|
|
461
|
+
// the window IS quitting. If janela ever grows multiple windows this has to
|
|
462
|
+
// become a real Quit that closes all of them.
|
|
463
|
+
//
|
|
464
|
+
// (An earlier note here claimed performClose: did not fire at all. That was
|
|
465
|
+
// a bad test: posted key events go to the FRONTMOST app, and the app was not
|
|
466
|
+
// active. With it activated first, both actions work.)
|
|
467
|
+
item(app_menu, "Quit " + name, "performClose:", "q", 0);
|
|
468
|
+
|
|
469
|
+
id edit = submenu("Edit");
|
|
470
|
+
item(edit, "Undo", "undo:", "z", 0);
|
|
471
|
+
item(edit, "Redo", "redo:", "z", kShift | kCommand);
|
|
472
|
+
separator(edit);
|
|
473
|
+
item(edit, "Cut", "cut:", "x", 0);
|
|
474
|
+
item(edit, "Copy", "copy:", "c", 0);
|
|
475
|
+
item(edit, "Paste", "paste:", "v", 0);
|
|
476
|
+
item(edit, "Select All", "selectAll:", "a", 0);
|
|
477
|
+
|
|
478
|
+
id view = submenu("View");
|
|
479
|
+
item(view, "Enter Full Screen", "toggleFullScreen:", "f",
|
|
480
|
+
kControl | kCommand);
|
|
481
|
+
|
|
482
|
+
id window = submenu("Window");
|
|
483
|
+
item(window, "Minimize", "performMiniaturize:", "m", 0);
|
|
484
|
+
item(window, "Close", "performClose:", "w", 0);
|
|
485
|
+
|
|
486
|
+
objc::msg_send<void>(app, objc::selector("setMainMenu:"), menubar);
|
|
487
|
+
// Lets AppKit add the standard Window-menu bookkeeping (window list,
|
|
488
|
+
// Bring All to Front).
|
|
489
|
+
objc::msg_send<void>(app, objc::selector("setWindowsMenu:"), window);
|
|
490
|
+
}
|
|
491
|
+
|
|
361
492
|
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
362
493
|
std::string &error) {
|
|
363
494
|
(void)error;
|
|
@@ -608,6 +739,14 @@ bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
|
608
739
|
|
|
609
740
|
#endif
|
|
610
741
|
|
|
742
|
+
#if !defined(__APPLE__)
|
|
743
|
+
// Windows and Linux need no menu for these shortcuts: Alt+F4 is a
|
|
744
|
+
// window-manager message the win32 backend already answers as WM_CLOSE, and
|
|
745
|
+
// the editing keys are handled inside WebView2 and WebKitGTK. A menu here
|
|
746
|
+
// would be a feature, not a fix.
|
|
747
|
+
static void install_main_menu() {}
|
|
748
|
+
#endif
|
|
749
|
+
|
|
611
750
|
// Runs on the UI thread with no TS frame beneath it — see the note on the job
|
|
612
751
|
// pool above for why that matters.
|
|
613
752
|
void dialog_on_ui_thread(webview_t, void *arg) {
|
|
@@ -728,6 +867,9 @@ int32_t wv_create(int32_t debug) {
|
|
|
728
867
|
if (g_apps[i].used) continue;
|
|
729
868
|
webview_t w = webview_create(debug, nullptr);
|
|
730
869
|
if (!w) return -1;
|
|
870
|
+
// After webview_create, which is what brings NSApplication into being.
|
|
871
|
+
// A no-op off macOS.
|
|
872
|
+
install_main_menu();
|
|
731
873
|
// Field-wise reset: App holds a thread and atomics, so it is not
|
|
732
874
|
// copy-assignable from a temporary.
|
|
733
875
|
g_apps[i].binds.clear();
|
package/templates/index.html
CHANGED
|
@@ -5,248 +5,137 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>__NAME__</title>
|
|
7
7
|
<style>
|
|
8
|
-
/*
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/* The look of create-tauri-app's starter, in janela's own tokens: a
|
|
9
|
+
centred column, the logos with a hover glow in each project's colour,
|
|
10
|
+
and one round trip you can drive. All of it is yours to replace. */
|
|
11
11
|
:root {
|
|
12
12
|
color-scheme: light dark;
|
|
13
13
|
--bg: #f6f6f7;
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--line: #e4e4e7;
|
|
14
|
+
--ink: #18181b;
|
|
15
|
+
--ink-dim: #71717a;
|
|
16
|
+
--field: #ffffff;
|
|
18
17
|
--accent: #1c6fd4;
|
|
19
|
-
--
|
|
20
|
-
--shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 8px 24px -12px rgb(0 0 0 / 0.12);
|
|
18
|
+
--shadow: 0 2px 2px rgb(0 0 0 / 0.08);
|
|
21
19
|
}
|
|
22
20
|
@media (prefers-color-scheme: dark) {
|
|
23
21
|
:root {
|
|
24
22
|
--bg: #17171a;
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--line: #2e2e34;
|
|
23
|
+
--ink: #fafafa;
|
|
24
|
+
--ink-dim: #a1a1aa;
|
|
25
|
+
--field: #0f0f11;
|
|
29
26
|
--accent: #58a6ff;
|
|
30
|
-
--
|
|
31
|
-
--shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 8px 24px -12px rgb(0 0 0 / 0.5);
|
|
27
|
+
--shadow: 0 2px 2px rgb(0 0 0 / 0.3);
|
|
32
28
|
}
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
* { box-sizing: border-box; }
|
|
36
32
|
body {
|
|
37
33
|
margin: 0;
|
|
38
|
-
min-height: 100vh;
|
|
39
|
-
display: flex;
|
|
40
|
-
justify-content: center;
|
|
41
34
|
background: var(--bg);
|
|
42
|
-
color: var(--
|
|
43
|
-
font:
|
|
35
|
+
color: var(--ink);
|
|
36
|
+
font: 16px/1.5 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
37
|
+
text-rendering: optimizeLegibility;
|
|
44
38
|
-webkit-font-smoothing: antialiased;
|
|
45
39
|
}
|
|
46
|
-
main { width: 100%; max-width: 760px; padding: 26px 22px 32px; }
|
|
47
|
-
|
|
48
|
-
/* --- header ------------------------------------------------------- */
|
|
49
|
-
header { text-align: center; margin-bottom: 20px; }
|
|
50
|
-
/* The mark: a round window, which is what janela means. Inlined rather
|
|
51
|
-
than linked, because a janela frontend is embedded in the binary and
|
|
52
|
-
every asset has to be inlined anyway — and inline rather than a CSS
|
|
53
|
-
data URI so ONE copy serves both themes: the frame follows the text
|
|
54
|
-
colour, the glass follows the accent. Percent-encoding a data URI
|
|
55
|
-
would also have cost 4x the bytes. Delete it and the header still
|
|
56
|
-
works. */
|
|
57
|
-
.mark { display: block; width: 56px; height: 56px; margin: 0 auto 14px; }
|
|
58
|
-
h1 { margin: 0; font-size: 26px; letter-spacing: -0.02em; font-weight: 650; }
|
|
59
|
-
.sub { margin: 5px 0 0; color: var(--muted); font-size: 13.5px; }
|
|
60
40
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
border: 1px solid var(--line);
|
|
67
|
-
border-radius: 10px;
|
|
68
|
-
background: var(--accent-soft);
|
|
69
|
-
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
41
|
+
.container {
|
|
42
|
+
padding: 10vh 20px 40px;
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
align-items: center;
|
|
70
46
|
text-align: center;
|
|
71
|
-
overflow-wrap: anywhere;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/* --- cards -------------------------------------------------------- */
|
|
75
|
-
/* auto-fit rather than a fixed column count: the same page is the iOS and
|
|
76
|
-
Android frontend, where it must read as a single column. */
|
|
77
|
-
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 13px; }
|
|
78
|
-
.card {
|
|
79
|
-
background: var(--card);
|
|
80
|
-
border: 1px solid var(--line);
|
|
81
|
-
border-radius: 12px;
|
|
82
|
-
padding: 15px 17px;
|
|
83
|
-
box-shadow: var(--shadow);
|
|
84
|
-
}
|
|
85
|
-
.card h2 {
|
|
86
|
-
margin: 0 0 2px;
|
|
87
|
-
font-size: 13px; font-weight: 600;
|
|
88
|
-
letter-spacing: 0.04em; text-transform: uppercase;
|
|
89
|
-
color: var(--muted);
|
|
90
47
|
}
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
48
|
+
h1 { margin: 0; font-size: 2em; letter-spacing: -0.02em; }
|
|
49
|
+
.hint { color: var(--ink-dim); margin: 4px 0 18px; }
|
|
50
|
+
|
|
51
|
+
.row { display: flex; justify-content: center; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
52
|
+
.logos { margin: 26px 0 8px; }
|
|
53
|
+
.logos a { display: block; }
|
|
54
|
+
.logo {
|
|
55
|
+
/* content-box on purpose: the page sets border-box globally, which
|
|
56
|
+
would make 6em include the padding and shrink the mark by half. */
|
|
57
|
+
box-sizing: content-box;
|
|
58
|
+
height: 6em; padding: 1.5em;
|
|
59
|
+
will-change: filter; transition: filter 0.75s;
|
|
96
60
|
}
|
|
97
|
-
|
|
61
|
+
/* Each glow is the project's own colour — the same idea as Tauri's
|
|
62
|
+
starter, which is where the shape of this page comes from. */
|
|
63
|
+
/* The mark's frame is currentColor by design, so it follows the text.
|
|
64
|
+
Inside the link it would follow the LINK colour instead — hence the
|
|
65
|
+
explicit ink here. */
|
|
66
|
+
.logo.janela { color: var(--ink); }
|
|
67
|
+
.logo.janela:hover { filter: drop-shadow(0 0 2em var(--accent)); }
|
|
68
|
+
.logo.vite:hover { filter: drop-shadow(0 0 2em #747bff); }
|
|
69
|
+
.logo.vue:hover { filter: drop-shadow(0 0 2em #42b883); }
|
|
70
|
+
.logo.react:hover { filter: drop-shadow(0 0 2em #61dafb); }
|
|
71
|
+
.logo.svelte:hover { filter: drop-shadow(0 0 2em #ff3e00); }
|
|
72
|
+
.logo.solid:hover { filter: drop-shadow(0 0 2em #4f88c6); }
|
|
73
|
+
@media (prefers-reduced-motion: reduce) { .logo { transition: none; } }
|
|
74
|
+
|
|
75
|
+
a { color: var(--accent); text-decoration: none; font-weight: 500; }
|
|
76
|
+
a:hover { text-decoration: underline; }
|
|
98
77
|
|
|
99
78
|
input, button {
|
|
100
|
-
font: inherit;
|
|
101
79
|
border-radius: 8px;
|
|
102
|
-
border: 1px solid
|
|
103
|
-
padding:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
80
|
+
border: 1px solid transparent;
|
|
81
|
+
padding: 0.6em 1.2em;
|
|
82
|
+
font: inherit;
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
color: var(--ink);
|
|
85
|
+
background: var(--field);
|
|
86
|
+
box-shadow: var(--shadow);
|
|
87
|
+
transition: border-color 0.25s;
|
|
107
88
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
89
|
+
button { cursor: pointer; }
|
|
90
|
+
button:hover { border-color: var(--accent); }
|
|
91
|
+
button:active { border-color: var(--accent); filter: brightness(0.97); }
|
|
111
92
|
input:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
112
|
-
button { cursor: pointer; font-weight: 500; }
|
|
113
|
-
button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
|
|
114
|
-
button:disabled { opacity: 0.5; cursor: default; }
|
|
115
|
-
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
116
|
-
button.primary:hover:not(:disabled) { filter: brightness(1.08); color: #fff; }
|
|
117
|
-
|
|
118
|
-
.result { margin: 12px 0 0; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); }
|
|
119
|
-
.result.filled { color: var(--text); }
|
|
120
|
-
pre.result { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 11em; overflow: auto; margin-top: 12px; }
|
|
121
|
-
|
|
122
|
-
/* A dot that keeps moving while a 2s command is pending. If it stalls,
|
|
123
|
-
the host has blocked the UI thread — which is the bug this proves is
|
|
124
|
-
absent. */
|
|
125
|
-
.pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 1s ease-in-out infinite; }
|
|
126
|
-
@keyframes pulse { 0%, 100% { opacity: 0.25; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.15); } }
|
|
127
|
-
@media (prefers-reduced-motion: reduce) { .pulse { animation: none; opacity: 1; } }
|
|
128
|
-
|
|
129
|
-
ul.events { list-style: none; margin: 0; padding: 0; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
130
|
-
ul.events li { padding: 3px 0; border-bottom: 1px dashed var(--line); }
|
|
131
|
-
ul.events li:last-child { border-bottom: 0; }
|
|
132
|
-
ul.events:empty::after { content: "nothing yet — press add"; color: var(--muted); font-family: inherit; }
|
|
133
|
-
|
|
134
|
-
footer { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: center; color: var(--muted); font-size: 13px; }
|
|
135
|
-
footer code { background: var(--card); border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; font-size: 12px; }
|
|
136
93
|
|
|
94
|
+
/* Reserved so the layout does not jump when the answer arrives. */
|
|
95
|
+
.greeting { min-height: 1.5em; margin: 20px 0 0; }
|
|
137
96
|
</style>
|
|
138
97
|
</head>
|
|
139
98
|
<body>
|
|
140
|
-
<main>
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
<div class="grid">
|
|
150
|
-
<section class="card">
|
|
151
|
-
<h2>Typed commands</h2>
|
|
152
|
-
<p class="why">The name, the arguments and the result are all checked at compile time — no codegen, because both sides are TypeScript.</p>
|
|
153
|
-
<div class="row">
|
|
154
|
-
<input id="a" type="number" value="2" aria-label="first number" />
|
|
155
|
-
<span aria-hidden="true">+</span>
|
|
156
|
-
<input id="b" type="number" value="40" aria-label="second number" />
|
|
157
|
-
<button id="add" class="primary">add</button>
|
|
158
|
-
</div>
|
|
159
|
-
<p class="result" id="sum">—</p>
|
|
160
|
-
<h3>Events from the host</h3>
|
|
161
|
-
<ul class="events" id="events"></ul>
|
|
162
|
-
</section>
|
|
163
|
-
|
|
164
|
-
<section class="card">
|
|
165
|
-
<h2>Async without blocking</h2>
|
|
166
|
-
<p class="why">The window keeps answering while this is pending: the dot keeps moving and <strong>add</strong> still works.</p>
|
|
167
|
-
<div class="row">
|
|
168
|
-
<button id="wait">wait 2s</button>
|
|
169
|
-
<span id="spinner" hidden><span class="pulse"></span></span>
|
|
170
|
-
</div>
|
|
171
|
-
<p class="result" id="waited">—</p>
|
|
172
|
-
</section>
|
|
173
|
-
|
|
174
|
-
<section class="card wide">
|
|
175
|
-
<h2>Files and the window</h2>
|
|
176
|
-
<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>
|
|
177
|
-
<div class="row">
|
|
178
|
-
<input id="path" class="grow" type="text" value="janela.conf.json" aria-label="file to read" />
|
|
179
|
-
<button id="read">read</button>
|
|
180
|
-
<button id="open">pick a file…</button>
|
|
181
|
-
<input id="title" class="grow" type="text" value="__NAME__ — renamed" aria-label="window title" />
|
|
182
|
-
<button id="retitle">set title</button>
|
|
183
|
-
</div>
|
|
184
|
-
<pre class="result" id="file">—</pre>
|
|
185
|
-
</section>
|
|
99
|
+
<main class="container">
|
|
100
|
+
<h1>Welcome to janela</h1>
|
|
101
|
+
<p class="hint">TypeScript, compiled to a native binary. No Rust, no Node, no Electron.</p>
|
|
102
|
+
|
|
103
|
+
<div class="row logos">
|
|
104
|
+
<a href="https://mmamedel.github.io/janela/" target="_blank" rel="noreferrer">
|
|
105
|
+
<svg class="logo janela" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970.64 970.64" role="img" aria-label="janela"><path fill="currentColor" d="M970.64,485.32c0,268.03-217.28,485.32-485.32,485.32S0,753.35,0,485.32,217.28,0,485.32,0s485.32,217.28,485.32,485.32ZM938.11,485.34c0-250.08-202.73-452.8-452.8-452.8S32.51,235.26,32.51,485.34s202.73,452.8,452.8,452.8,452.8-202.73,452.8-452.8Z"/><path fill="currentColor" d="M892.79,485.25c0,225.05-182.44,407.49-407.49,407.49S77.82,710.3,77.82,485.25,260.26,77.77,485.31,77.77s407.49,182.44,407.49,407.49ZM470.27,112.09v335.5c0,1.05,13.11,12.99,15.36,14.43,1.65-2.06,14.64-13.4,14.64-14.43V112.59c0-1.1-2.44-1.43-3.45-1.55-8.42-.99-18.1.83-26.55,1.05ZM858.27,438.09c-20.14-166.94-158.46-303.59-325-324v324h325ZM437.27,115.09c-21.65,1.62-44.22,7.07-65,13.5-137.62,42.61-242.41,165.92-260,309.5h325V115.09ZM109.27,500.09h338.5c1,0,12.36-12.44,13.54-14.5,0-1-12.54-14.5-13.54-14.5H109.27c-1.81,9.94-.24,19.09,0,29ZM861.27,471.09h-338.5c-1,0-13.53,13.5-13.54,14.5.52,2.76,3.61,3.57,5.57,5.47,1.64,1.59,7.37,9.03,7.97,9.03h338.5c.9-9.78.9-19.22,0-29ZM500.27,860.09v-336.5c0-1.03-12.98-12.37-14.64-14.43-2.26,1.44-15.36,13.37-15.36,14.43v336.5h30ZM437.27,533.09H112.27c19.95,166.03,159.46,303.1,325,323v-323ZM858.27,533.09h-325v324c165.97-22.51,304.48-156.63,325-324Z"/><path fill="var(--accent, #1c6fd4)" d="M858.27,438.09h-325V114.09c166.54,20.41,304.86,157.06,325,324Z"/><path fill="var(--accent, #1c6fd4)" d="M858.27,533.09c-20.52,167.37-159.03,301.49-325,324v-324h325Z"/><path fill="var(--accent, #1c6fd4)" d="M437.27,533.09v323c-165.54-19.9-305.05-156.97-325-323h325Z"/><path fill="var(--accent, #1c6fd4)" d="M437.27,115.09v323H112.27c17.6-143.57,122.38-266.89,260-309.5,20.78-6.43,43.34-11.88,65-13.5ZM324.93,239.42c-5.22-5.22-14.48-5.8-20.62-1.79-23.51,27.2-56.28,50.77-79.07,77.93-3.21,3.83-6.62,7.81-7,13.05-.97,13.35,13.8,21.88,24.98,14.93,28.09-28.84,58.16-56.14,84.78-86.22,2.71-5.58,1.25-13.58-3.07-17.9ZM375.06,280.3c-4.15.41-7.65,2.76-10.8,5.27-2.42,1.93-17.06,17.64-18.19,19.81-7.64,14.76,7.17,29.85,21.55,22.56,2.83-1.44,19.3-18.18,21.64-21.36,8.21-11.17-.52-27.62-14.2-26.28ZM310.03,344.3c-2.88.42-6.45,2.49-8.78,4.26-4.52,3.45-32.55,31.28-34.83,35.17-8.84,15.07,8.47,31.23,23.59,21.59,4.35-2.77,34.16-32.7,36.28-36.72,6.36-12.04-2.95-26.26-16.27-24.31Z"/><path fill="var(--card, var(--bg, #f6f6f7))" d="M500.27,860.09h-30v-336.5c0-1.05,13.11-12.99,15.36-14.43,1.65,2.06,14.64,13.4,14.64,14.43v336.5Z"/><path fill="var(--card, var(--bg, #f6f6f7))" d="M470.27,112.09c8.45-.22,18.13-2.04,26.55-1.05,1.01.12,3.45.46,3.45,1.55v335c0,1.03-12.98,12.37-14.64,14.43-2.26-1.44-15.36-13.37-15.36-14.43V112.09Z"/><path fill="var(--card, var(--bg, #f6f6f7))" d="M109.27,500.09c-.24-9.91-1.81-19.06,0-29h338.5c1,0,13.54,13.5,13.54,14.5-1.17,2.06-12.54,14.5-13.54,14.5H109.27Z"/><path fill="var(--card, var(--bg, #f6f6f7))" d="M861.27,471.09c.9,9.78.9,19.22,0,29h-338.5c-.6,0-6.34-7.44-7.97-9.03-1.96-1.9-5.04-2.72-5.57-5.47,0-1,12.54-14.5,13.54-14.5h338.5Z"/><path fill="currentColor" d="M324.93,239.42c4.32,4.32,5.78,12.32,3.07,17.9-26.62,30.07-56.7,57.37-84.78,86.22-11.18,6.94-25.95-1.58-24.98-14.93.38-5.24,3.79-9.22,7-13.05,22.8-27.16,55.56-50.73,79.07-77.93,6.13-4.01,15.39-3.43,20.62,1.79Z"/><path fill="currentColor" d="M310.03,344.3c13.33-1.95,22.63,12.27,16.27,24.31-2.12,4.02-31.93,33.94-36.28,36.72-15.12,9.64-32.44-6.52-23.59-21.59,2.28-3.89,30.31-31.72,34.83-35.17,2.33-1.78,5.9-3.84,8.78-4.26Z"/><path fill="currentColor" d="M375.06,280.3c13.69-1.35,22.42,15.11,14.2,26.28-2.34,3.18-18.81,19.93-21.64,21.36-14.39,7.3-29.19-7.8-21.55-22.56,1.13-2.18,15.76-17.88,18.19-19.81,3.16-2.51,6.65-4.86,10.8-5.27Z"/></svg>
|
|
106
|
+
</a>
|
|
186
107
|
</div>
|
|
108
|
+
<p class="hint">Click the janela logo to learn more.</p>
|
|
187
109
|
|
|
188
|
-
<
|
|
189
|
-
<
|
|
190
|
-
<button
|
|
191
|
-
</
|
|
110
|
+
<form class="row" id="greet-form">
|
|
111
|
+
<input id="greet-input" placeholder="Enter a name..." aria-label="a name to greet" />
|
|
112
|
+
<button type="submit">Greet</button>
|
|
113
|
+
</form>
|
|
114
|
+
<p class="greeting" id="greeting"></p>
|
|
192
115
|
</main>
|
|
193
116
|
|
|
194
117
|
<script>
|
|
195
118
|
// This template has no bundler, so it uses the `janela` global that the
|
|
196
119
|
// host injects before the page loads. Projects with a build step should
|
|
197
|
-
// `import { invoke
|
|
198
|
-
//
|
|
120
|
+
// `import { invoke } from "janela/api"` instead — same function, but
|
|
121
|
+
// typed and resolvable by the bundler.
|
|
199
122
|
window.onload = async () => {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
el.textContent = text;
|
|
203
|
-
el.classList.add("filled");
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
janela.listen("added", (sum) => {
|
|
207
|
-
const li = document.createElement("li");
|
|
208
|
-
li.textContent = "host emitted 'added': " + sum;
|
|
209
|
-
$("events").prepend(li);
|
|
210
|
-
});
|
|
123
|
+
const out = document.getElementById("greeting");
|
|
124
|
+
const input = document.getElementById("greet-input");
|
|
211
125
|
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await janela.invoke("log", "page loaded");
|
|
216
|
-
|
|
217
|
-
$("add").onclick = async () => {
|
|
218
|
-
const a = Number($("a").value);
|
|
219
|
-
const b = Number($("b").value);
|
|
220
|
-
show($("sum"), `add(${a}, ${b}) → ${await janela.invoke("add", { a, b })}`);
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
$("wait").onclick = async (e) => {
|
|
224
|
-
e.target.disabled = true;
|
|
225
|
-
$("spinner").hidden = false;
|
|
226
|
-
show($("waited"), "waiting… try add, it still answers");
|
|
227
|
-
const answer = await janela.invoke("wait", { ms: 2000 });
|
|
228
|
-
show($("waited"), answer);
|
|
229
|
-
$("spinner").hidden = true;
|
|
230
|
-
e.target.disabled = false;
|
|
126
|
+
// `greet` is a command in src-host/main.ts, compiled to machine code.
|
|
127
|
+
const greet = async (name) => {
|
|
128
|
+
out.textContent = await janela.invoke("greet", { name });
|
|
231
129
|
};
|
|
232
130
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
show($("file"), r.ok ? `${path} — ${r.length} chars\n\n${r.text.slice(0, 400)}` : "error: " + r.error);
|
|
237
|
-
};
|
|
131
|
+
// Greeted once on load, so the round trip is visible without a click.
|
|
132
|
+
await greet("__NAME__");
|
|
133
|
+
await janela.invoke("log", "page loaded");
|
|
238
134
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const r = await janela.invoke("openFile", {});
|
|
243
|
-
if (!r.ok) show($("file"), "error: " + r.error);
|
|
244
|
-
else if (r.cancelled) show($("file"), "cancelled");
|
|
245
|
-
else show($("file"), `${r.path} — ${r.length} chars\n\n${r.text.slice(0, 400)}`);
|
|
135
|
+
document.getElementById("greet-form").onsubmit = (e) => {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
greet(input.value || "__NAME__");
|
|
246
138
|
};
|
|
247
|
-
|
|
248
|
-
$("retitle").onclick = () => janela.invoke("setTitle", { title: $("title").value });
|
|
249
|
-
$("quit").onclick = () => janela.invoke("quit");
|
|
250
139
|
};
|
|
251
140
|
</script>
|
|
252
141
|
</body>
|
package/templates/main.ts
CHANGED
|
@@ -3,83 +3,21 @@
|
|
|
3
3
|
// Register commands here; the page calls them with `await janela.invoke(name, args)`.
|
|
4
4
|
// Handlers take the arguments as a value and return a value — the runtime owns
|
|
5
5
|
// JSON at the boundary, so there is no parsing or stringifying to do here.
|
|
6
|
+
//
|
|
7
|
+
// This template has no bundler, so it uses the untyped `JanelaApp`. The
|
|
8
|
+
// framework templates declare a contract instead and the page is checked
|
|
9
|
+
// against it; see `janela init --template vue`.
|
|
6
10
|
|
|
7
11
|
import type { JanelaApp } from "janela/host";
|
|
8
12
|
|
|
9
13
|
export function setup(app: JanelaApp): void {
|
|
10
|
-
app.command("add", (args) => {
|
|
11
|
-
const a = args as { a: number; b: number };
|
|
12
|
-
const sum = a.a + a.b;
|
|
13
|
-
// Backend→frontend event, just to show the channel exists.
|
|
14
|
-
app.emit("added", sum);
|
|
15
|
-
return sum;
|
|
16
|
-
});
|
|
17
|
-
|
|
18
14
|
app.command("greet", (args) => {
|
|
19
15
|
const a = args as { name: string };
|
|
20
|
-
return "Hello, " + a.name + "
|
|
16
|
+
return "Hello, " + a.name + "! You've been greeted from a native TypeScript binary.";
|
|
21
17
|
});
|
|
22
18
|
|
|
19
|
+
// Anything the host logs goes to the terminal that `janela dev` runs in.
|
|
23
20
|
app.command("log", (args) => {
|
|
24
21
|
console.log("[host] page says:", args as string);
|
|
25
22
|
});
|
|
26
|
-
|
|
27
|
-
// An async command: answers later, without freezing the window. The page
|
|
28
|
-
// still just does `await janela.invoke("wait", { ms: 1000 })`.
|
|
29
|
-
app.commandAsync("wait", (args, resolve) => {
|
|
30
|
-
const a = args as { ms: number };
|
|
31
|
-
app.sleep(a.ms, () => {
|
|
32
|
-
resolve("waited " + a.ms + "ms without blocking the UI");
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// File I/O without freezing the window: the read happens on a worker thread
|
|
37
|
-
// in the shim. Use this instead of node:fs readFileSync, which blocks the
|
|
38
|
-
// host loop — and with it the whole UI — until the syscall returns.
|
|
39
|
-
app.commandAsync("readFile", (args, resolve) => {
|
|
40
|
-
const a = args as { path: string };
|
|
41
|
-
app.readFileAsync(a.path, (err, text) => {
|
|
42
|
-
if (err !== null) {
|
|
43
|
-
resolve({ ok: false, error: err });
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
resolve({ ok: true, length: text.length, text: text });
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// The native "open" dialog, paired with the reader above — picking a file is
|
|
51
|
-
// what makes readFileAsync useful. commandAsync is the right shape here: the
|
|
52
|
-
// page's promise stays parked while the user takes as long as they like, and
|
|
53
|
-
// the window carries on serving other calls meanwhile.
|
|
54
|
-
app.commandAsync("openFile", (_args, resolve) => {
|
|
55
|
-
app.openFileDialog(
|
|
56
|
-
{ title: "Pick a file", filters: [{ name: "Text", extensions: ["txt", "md"] }] },
|
|
57
|
-
(paths, err) => {
|
|
58
|
-
if (err !== undefined) {
|
|
59
|
-
resolve({ ok: false, error: err });
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (paths === null) {
|
|
63
|
-
resolve({ ok: true, cancelled: true });
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
app.readFileAsync(paths[0], (rerr, text) => {
|
|
67
|
-
resolve(
|
|
68
|
-
rerr !== null
|
|
69
|
-
? { ok: false, error: rerr }
|
|
70
|
-
: { ok: true, path: paths[0], length: text.length, text: text },
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// The window is yours to change at runtime, not just at startup.
|
|
78
|
-
app.command("setTitle", (args) => {
|
|
79
|
-
app.setTitle((args as { title: string }).title);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
app.command("quit", (_args) => {
|
|
83
|
-
app.quit();
|
|
84
|
-
});
|
|
85
23
|
}
|