janela 0.14.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mmamedel/janela/main/packages/janela/brand/janela-mark-dark.svg">
3
+ <img src="https://raw.githubusercontent.com/mmamedel/janela/main/packages/janela/brand/janela-mark-light.svg" alt="" width="88" align="right">
4
+ </picture>
5
+
1
6
  # janela
2
7
 
3
8
  > *janela* — Portuguese for **window**.
@@ -6,11 +11,12 @@ Desktop and mobile apps in pure TypeScript, compiled to native. No Rust, no
6
11
  Node, no Electron. The backend is TypeScript compiled to a native binary by
7
12
  [scriptc](https://scriptc.dev); the window is the OS webview via
8
13
  [webview/webview](https://github.com/webview/webview). A desktop binary comes
9
- out around 225420 KB — 225 KB for the smallest template — with no bundled
14
+ out around 190390 KB — 191 KB for the smallest template — with no bundled
10
15
  browser and no bundled runtime; iOS and Android bundles land around
11
- 228421 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
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
14
20
  [docs/frontend.md](../../docs/frontend.md).
15
21
 
16
22
  Five targets, one runtime — the same `main.ts`, the same typed contract and the
@@ -31,7 +37,11 @@ desktop-only for now; on mobile they report clearly when called.
31
37
  ## Quick start
32
38
 
33
39
  ```bash
34
- npm install -g janela # or: npx janela init my-app
40
+ pnpm create janela # the front door: prompts, scaffolds, installs
41
+ # (npm create janela@latest / yarn / bun also work)
42
+
43
+ # or drive the CLI yourself:
44
+ npm install -g janela
35
45
  janela --version # which janela you actually have
36
46
  janela init my-app # (or `jn init my-app`)
37
47
  cd my-app
@@ -663,4 +673,7 @@ gh release create v<version> --generate-notes --verify-tag
663
673
  ## License
664
674
 
665
675
  MIT. Bundles [webview/webview](https://github.com/webview/webview) headers
666
- (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.14.3",
3
+ "version": "0.16.0",
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": {
@@ -5,257 +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. 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. */
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. */
13
11
  :root {
14
12
  color-scheme: light dark;
15
13
  --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);
14
+ --ink: #18181b;
15
+ --ink-dim: #71717a;
16
+ --field: #ffffff;
17
+ --accent: #1c6fd4;
18
+ --shadow: 0 2px 2px rgb(0 0 0 / 0.08);
23
19
  }
24
20
  @media (prefers-color-scheme: dark) {
25
21
  :root {
26
22
  --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);
23
+ --ink: #fafafa;
24
+ --ink-dim: #a1a1aa;
25
+ --field: #0f0f11;
26
+ --accent: #58a6ff;
27
+ --shadow: 0 2px 2px rgb(0 0 0 / 0.3);
34
28
  }
35
29
  }
36
30
 
37
31
  * { box-sizing: border-box; }
38
32
  body {
39
33
  margin: 0;
40
- min-height: 100vh;
41
- display: flex;
42
- justify-content: center;
43
34
  background: var(--bg);
44
- color: var(--text);
45
- 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;
46
38
  -webkit-font-smoothing: antialiased;
47
39
  }
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
40
 
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;
41
+ .container {
42
+ padding: 10vh 20px 40px;
43
+ display: flex;
44
+ flex-direction: column;
45
+ align-items: center;
76
46
  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
47
  }
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);
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
- .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; }
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; }
104
77
 
105
78
  input, button {
106
- font: inherit;
107
79
  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;
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;
113
88
  }
114
- input { min-width: 0; }
115
- input[type="number"] { width: 5.5em; text-align: center; }
116
- 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); }
117
92
  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
93
 
94
+ /* Reserved so the layout does not jump when the answer arrives. */
95
+ .greeting { min-height: 1.5em; margin: 20px 0 0; }
143
96
  </style>
144
97
  </head>
145
98
  <body>
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>
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>
195
107
  </div>
108
+ <p class="hint">Click the janela logo to learn more.</p>
196
109
 
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>
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>
201
115
  </main>
202
116
 
203
117
  <script>
204
118
  // This template has no bundler, so it uses the `janela` global that the
205
119
  // host injects before the page loads. Projects with a build step should
206
- // `import { invoke, listen } from "janela/api"` instead — same functions,
207
- // but typed and resolvable by the bundler.
120
+ // `import { invoke } from "janela/api"` instead — same function, but
121
+ // typed and resolvable by the bundler.
208
122
  window.onload = async () => {
209
- const $ = (id) => document.getElementById(id);
210
- const show = (el, text) => {
211
- el.textContent = text;
212
- el.classList.add("filled");
213
- };
214
-
215
- janela.listen("added", (sum) => {
216
- const li = document.createElement("li");
217
- li.textContent = "host emitted 'added': " + sum;
218
- $("events").prepend(li);
219
- });
123
+ const out = document.getElementById("greeting");
124
+ const input = document.getElementById("greet-input");
220
125
 
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__" });
224
- await janela.invoke("log", "page loaded");
225
-
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 })}`);
230
- };
231
-
232
- $("wait").onclick = async (e) => {
233
- e.target.disabled = true;
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;
239
- 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 });
240
129
  };
241
130
 
242
- $("read").onclick = async () => {
243
- const path = $("path").value;
244
- const r = await janela.invoke("readFile", { path });
245
- show($("file"), r.ok ? `${path} — ${r.length} chars\n\n${r.text.slice(0, 400)}` : "error: " + r.error);
246
- };
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");
247
134
 
248
- // The native open dialog. The window keeps serving other commands
249
- // while the user is deciding.
250
- $("open").onclick = async () => {
251
- const r = await janela.invoke("openFile", {});
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)}`);
135
+ document.getElementById("greet-form").onsubmit = (e) => {
136
+ e.preventDefault();
137
+ greet(input.value || "__NAME__");
255
138
  };
256
-
257
- $("retitle").onclick = () => janela.invoke("setTitle", { title: $("title").value });
258
- $("quit").onclick = () => janela.invoke("quit");
259
139
  };
260
140
  </script>
261
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
  }