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 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 240420 KB — 241 KB for the smallest template — with no bundled
14
+ out around 190390 KB — 191 KB for the smallest template — with no bundled
15
15
  browser and no bundled runtime; iOS and Android bundles land around
16
- 244421 KB. Those are the *starter's* figures, and about 35 KB of each is the
17
- native file dialog and worker-thread reader it demonstrates: delete the "Files
18
- and the window" card and its two commands and the smallest binary is 212 KB. Per-template figures are in
16
+ 195388 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Desktop, iOS and Android apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
5
5
  "type": "module",
6
6
  "bin": {
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();
@@ -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
- /* 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. */
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
- --card: #ffffff;
15
- --text: #18181b;
16
- --muted: #71717a;
17
- --line: #e4e4e7;
14
+ --ink: #18181b;
15
+ --ink-dim: #71717a;
16
+ --field: #ffffff;
18
17
  --accent: #1c6fd4;
19
- --accent-soft: #eff4ff;
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
- --card: #202024;
26
- --text: #fafafa;
27
- --muted: #a1a1aa;
28
- --line: #2e2e34;
23
+ --ink: #fafafa;
24
+ --ink-dim: #a1a1aa;
25
+ --field: #0f0f11;
29
26
  --accent: #58a6ff;
30
- --accent-soft: #1d2433;
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(--text);
43
- font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
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
- /* The one line that proves the whole stack: this string was produced by
62
- TypeScript compiled to machine code, and handed to the page. */
63
- .greeting {
64
- margin: 0 0 20px;
65
- padding: 11px 16px;
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
- .card.wide { grid-column: 1 / -1; }
92
- .card p.why { margin: 0 0 11px; color: var(--muted); font-size: 12.5px; line-height: 1.45; }
93
- .card h3 {
94
- margin: 16px 0 6px; font-size: 11px; font-weight: 600;
95
- letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted);
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
- .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
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 var(--line);
103
- padding: 8px 12px;
104
- background: var(--card);
105
- color: var(--text);
106
- transition: border-color 0.15s, background 0.15s;
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
- input { min-width: 0; }
109
- input[type="number"] { width: 5.5em; text-align: center; }
110
- input.grow { flex: 1 1 12em; }
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
- <header>
142
- <svg class="mark" 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)" d="M858.27,438.09h-325V114.09c166.54,20.41,304.86,157.06,325,324Z"/><path fill="var(--accent)" d="M858.27,533.09c-20.52,167.37-159.03,301.49-325,324v-324h325Z"/><path fill="var(--accent)" d="M437.27,533.09v323c-165.54-19.9-305.05-156.97-325-323h325Z"/><path fill="var(--accent)" 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)" 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)" 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)" 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)" 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>
143
- <h1>__NAME__</h1>
144
- <p class="sub">TypeScript, compiled to a native binary. No Rust, no Node, no Electron.</p>
145
- </header>
146
-
147
- <p class="greeting" id="out">booting…</p>
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
- <footer>
189
- <span>Edit <code>index.html</code> for the page and <code>src-host/main.ts</code> for the commands.</span>
190
- <button id="quit">Quit</button>
191
- </footer>
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, listen } from "janela/api"` instead — same functions,
198
- // but typed and resolvable by the bundler.
120
+ // `import { invoke } from "janela/api"` instead — same function, but
121
+ // typed and resolvable by the bundler.
199
122
  window.onload = async () => {
200
- const $ = (id) => document.getElementById(id);
201
- const show = (el, text) => {
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
- // The greeting is a round trip: the string is built in compiled
213
- // TypeScript and returned to the page.
214
- $("out").textContent = await janela.invoke("greet", { name: "__NAME__" });
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
- $("read").onclick = async () => {
234
- const path = $("path").value;
235
- const r = await janela.invoke("readFile", { path });
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
- // The native open dialog. The window keeps serving other commands
240
- // while the user is deciding.
241
- $("open").onclick = async () => {
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 + " from the native TS binary";
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
  }