bun-app-window 0.7.0 → 0.8.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/CHANGELOG.md +264 -73
- package/README.md +247 -142
- package/README.zh-CN.md +238 -142
- package/README.zh-TW.md +241 -141
- package/package.json +9 -5
- package/src/shell/app.ts +255 -2
- package/src/shell/assets.d.ts +4 -4
- package/src/shell/embedded.ts +9 -10
- package/src/shell/index.ts +301 -318
- package/src/shell/ipc.ts +91 -33
- package/src/shell/native.ts +61 -64
- package/src/shell/pack.ts +247 -28
- package/src/shell/port.ts +37 -38
- package/src/shell/resolve.ts +121 -140
- package/src/shell/state.ts +8 -19
- package/src/shell/types.ts +79 -81
- package/src/ui/titlebar.ts +1 -2
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# bun-app-window
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/bun-app-window)
|
|
4
|
-
[](https://bun.sh)
|
|
5
5
|
[](#requirements--design-boundaries)
|
|
6
6
|
[](#requirements--design-boundaries)
|
|
7
7
|
[](#requirements--design-boundaries)
|
|
@@ -26,13 +26,13 @@ await openShell({ url: `http://127.0.0.1:${server.port}`, flags: FLAG_FRAMELESS
|
|
|
26
26
|
|
|
27
27
|
Before adopting this package, please review the following architectural constraints and trade-offs:
|
|
28
28
|
|
|
29
|
-
| Constraint
|
|
30
|
-
|
|
|
31
|
-
| Windows x64 only
|
|
32
|
-
| Bun runtime required
|
|
29
|
+
| Constraint | Implication |
|
|
30
|
+
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
31
|
+
| Windows x64 only | Enforced via `os` and `cpu` fields in `package.json` (the platform-neutral package name is reserved for future expansion). A prebuilt `libshell.dll` is shipped in the package; no install-time compilation is required |
|
|
32
|
+
| Bun runtime required | Ships raw TypeScript source files and requires Bun >= 1.4.4. While Node.js can install the package, it cannot execute it |
|
|
33
33
|
| TypeScript >= 7.0.2 to type-check it | The API is designed and checked on TypeScript 7.0.2, pinned exactly in `devDependencies`; no compatibility work is done for older compilers. Above 7.0.2 the outcome is unasserted — `bun-types` only opens its `typesVersions: ">= 7.1"` gate there, which changes what a `with { type: "text" }` import resolves to |
|
|
34
|
-
| Type-checking needs `@types/bun`
|
|
35
|
-
| Built on `bun:ffi`
|
|
34
|
+
| Type-checking needs `@types/bun` | Because the package ships `.ts` sources, your own `bun check` compiles them too. Add `bun add -d @types/bun`; without it the checker reports `TS2868: Cannot find name 'Bun'` from inside `node_modules` |
|
|
35
|
+
| Built on `bun:ffi` | `bun:ffi` is marked as experimental in Bun. While Bun recommends Node-API for production stability, this library chooses `bun:ffi` to achieve a zero-dependency footprint |
|
|
36
36
|
|
|
37
37
|
Runtime prerequisites and non-goals:
|
|
38
38
|
|
|
@@ -48,7 +48,7 @@ bun add bun-app-window
|
|
|
48
48
|
|
|
49
49
|
### What the tarball contains
|
|
50
50
|
|
|
51
|
-
The published package holds the prebuilt `dist/libshell.dll` plus the TypeScript sources — 22 files, 0.
|
|
51
|
+
The published package holds the prebuilt `dist/libshell.dll` plus the TypeScript sources — 22 files, 0.50 MB packed (1.47 MB unpacked). The native C++ layer (`native/`) and the vendored upstream headers (`vendor/`) are **not** shipped: they are only needed to rebuild `libshell.dll`, and rebuilding requires cloning the repository. `bun run build:native` and `build:exe` are therefore repository-only scripts that do nothing useful inside `node_modules`.
|
|
52
52
|
|
|
53
53
|
## Quick Start
|
|
54
54
|
|
|
@@ -56,22 +56,23 @@ The published package holds the prebuilt `dist/libshell.dll` plus the TypeScript
|
|
|
56
56
|
import { openShell, serveOnSafePort, FLAG_FRAMELESS, FLAG_CENTER } from "bun-app-window";
|
|
57
57
|
|
|
58
58
|
const server = serveOnSafePort({
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
fetch: () =>
|
|
60
|
+
new Response("<h1>Hello</h1>", {
|
|
61
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
62
|
+
}),
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
const shell = await openShell({
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
url: `http://127.0.0.1:${server.port}`,
|
|
67
|
+
title: "My App",
|
|
68
|
+
width: 1000,
|
|
69
|
+
height: 680,
|
|
70
|
+
flags: FLAG_FRAMELESS | FLAG_CENTER,
|
|
71
|
+
minWidth: 480,
|
|
72
|
+
minHeight: 320,
|
|
72
73
|
});
|
|
73
74
|
|
|
74
|
-
shell.post(JSON.stringify({ t: "ready" }));
|
|
75
|
+
shell.post(JSON.stringify({ t: "ready" })); // Host -> Page one-way message
|
|
75
76
|
shell.setTitle("Doing work…");
|
|
76
77
|
```
|
|
77
78
|
|
|
@@ -85,45 +86,45 @@ returns a stale message that looks like a fresh failure.
|
|
|
85
86
|
|
|
86
87
|
### Instance Methods & Properties
|
|
87
88
|
|
|
88
|
-
| Member
|
|
89
|
-
|
|
|
90
|
-
| `start()`
|
|
91
|
-
| `stop()`
|
|
92
|
-
| `show()`
|
|
93
|
-
| `isAlive()`
|
|
94
|
-
| `pollEvents(max?)`
|
|
95
|
-
| `state()`
|
|
96
|
-
| `lastError()`
|
|
97
|
-
| `setTitle` / `minimize` / `toggleMaximize` / `close` | Standard window control operations; each returns a status code `rc`
|
|
98
|
-
| `setMinSize(w, h)`
|
|
99
|
-
| `drag()`
|
|
100
|
-
| `beginResize(edge)`
|
|
101
|
-
| `post(json)`
|
|
102
|
-
| `setZoom(f)` / `getZoom()`
|
|
103
|
-
| `reload()` / `openDevTools()` / `toggleFullscreen()` | Equivalent to `Ctrl+R`, `F12`, and `F11` respectively
|
|
104
|
-
| `setFrameless(frameless)` / `isFrameless()`
|
|
105
|
-
| `setBrowserUi(mask)`
|
|
106
|
-
| `showContextMenu(x, y, items)`
|
|
107
|
-
| `pickFolder(title?)`
|
|
108
|
-
| `libInfo`
|
|
109
|
-
| `symbols`
|
|
89
|
+
| Member | Description |
|
|
90
|
+
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
91
|
+
| `start()` | Opens the window and returns `{ rc, error }`. Returns immediately while the UI message loop runs on a dedicated native thread |
|
|
92
|
+
| `stop()` | Destroys the window and releases native resources. Safe to call multiple times (idempotent) |
|
|
93
|
+
| `show()` | Reveals the window. Typically paired with `FLAG_DEFER_SHOW` to display the window only after the web UI is ready |
|
|
94
|
+
| `isAlive()` | Checks whether the native window still exists (unlike `state()`, which cannot distinguish between unstarted and destroyed states). To wait for close, use `pollEvents()` and watch for `CLOSED` instead of polling this |
|
|
95
|
+
| `pollEvents(max?)` | Drains window lifecycle events (`STATE` / `CLOSED`); synchronous and consuming. See "Window events" below |
|
|
96
|
+
| `state()` | Returns current window state: `{ maximized, minimized, focused, dark, raw }` |
|
|
97
|
+
| `lastError()` | Returns the most recent failure message, or an empty string if no error occurred |
|
|
98
|
+
| `setTitle` / `minimize` / `toggleMaximize` / `close` | Standard window control operations; each returns a status code `rc` |
|
|
99
|
+
| `setMinSize(w, h)` | Sets the minimum window dimensions in logical pixels |
|
|
100
|
+
| `drag()` | Initiates native window dragging from a page `pointerdown` event (preserves Windows Snap Layouts) |
|
|
101
|
+
| `beginResize(edge)` | Initiates native border resizing from a page `pointermove` event (`edge` uses `RESIZE_EDGE.*` constants) |
|
|
102
|
+
| `post(json)` | Sends a fire-and-forget JSON message from the host to the web page |
|
|
103
|
+
| `setZoom(f)` / `getZoom()` | Gets or sets the page zoom factor (clamped automatically to `0.25`–`3.0`) |
|
|
104
|
+
| `reload()` / `openDevTools()` / `toggleFullscreen()` | Equivalent to `Ctrl+R`, `F12`, and `F11` respectively |
|
|
105
|
+
| `setFrameless(frameless)` / `isFrameless()` | Switches the window form at runtime and reports the current one. Only a window **created** with `FLAG_FRAMELESS` can switch; a framed-created window refuses with `rc=-1` and a reason in `lastError()`. See "Window Flags" below |
|
|
106
|
+
| `setBrowserUi(mask)` | Disables specific default browser behaviors using a bitwise OR of `BROWSER_UI_*` flags |
|
|
107
|
+
| `showContextMenu(x, y, items)` | Displays a native Win32 context menu at the specified screen coordinates (see below) |
|
|
108
|
+
| `pickFolder(title?)` | Opens a native folder selection dialog (synchronously blocks the calling thread; **when exposed over IPC, pair it with `timeoutMs: 0`**, see below) |
|
|
109
|
+
| `libInfo` | Metadata of the resolved native library (file path, resolution source, SHA-256 hash) |
|
|
110
|
+
| `symbols` | Escape hatch providing direct access to the raw `dlopen` FFI symbols |
|
|
110
111
|
|
|
111
112
|
### Window Flags
|
|
112
113
|
|
|
113
114
|
Combine via bitwise OR (`|`) and pass to `opts.flags`:
|
|
114
115
|
|
|
115
|
-
| Flag
|
|
116
|
-
|
|
|
117
|
-
| `FLAG_FRAMELESS`
|
|
118
|
-
| `FLAG_RESIZABLE`
|
|
119
|
-
| `FLAG_CENTER`
|
|
120
|
-
| `FLAG_TOPMOST`
|
|
121
|
-
| `FLAG_DEFER_SHOW`
|
|
122
|
-
| `FLAG_ACCEPT_DROP`
|
|
123
|
-
| `FLAG_START_MAXIMIZED`
|
|
124
|
-
| `FLAG_ALLOW_EXTERNAL_NAV` | Opts out of external-navigation interception — for apps that genuinely are browsers
|
|
125
|
-
| `FLAG_TITLEBAR_RESERVE`
|
|
126
|
-
| `FLAG_NATIVE_CAPTIONS`
|
|
116
|
+
| Flag | Description |
|
|
117
|
+
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
118
|
+
| `FLAG_FRAMELESS` | Creates a frameless window without the default system title bar, delegating title bar rendering to the web UI. A window created this way can switch back to framed at runtime with `setFrameless(false)` |
|
|
119
|
+
| `FLAG_RESIZABLE` | Allows user resizing |
|
|
120
|
+
| `FLAG_CENTER` | Centers the window within the active monitor work area on startup (works in both standard and frameless modes) |
|
|
121
|
+
| `FLAG_TOPMOST` | Keeps the window always on top |
|
|
122
|
+
| `FLAG_DEFER_SHOW` | Creates the window hidden initially until `show()` is explicitly called |
|
|
123
|
+
| `FLAG_ACCEPT_DROP` | Enables native file and folder drag-and-drop from Windows Explorer |
|
|
124
|
+
| `FLAG_START_MAXIMIZED` | Opens the window directly in a maximized state without an intermediate normal-sized frame flash |
|
|
125
|
+
| `FLAG_ALLOW_EXTERNAL_NAV` | Opts out of external-navigation interception — for apps that genuinely are browsers |
|
|
126
|
+
| `FLAG_TITLEBAR_RESERVE` | In frameless mode, shifts the web content down by one title-bar height so the top strip belongs to the native window. Superseded by `FLAG_NATIVE_CAPTIONS`: the caption hit zone is pinned to client `y ∈ [0, 32)`, so this variant puts the page's own title bar 32 px away from where clicks are actually routed |
|
|
127
|
+
| `FLAG_NATIVE_CAPTIONS` | In frameless mode, draws the three caption buttons on a layered child window owned by the shell and answers `HTMAXBUTTON` there. This is the only way Snap Layouts can trigger: the WebView2 render child lives in another process and always returns `HTCLIENT`. Pair it with `mountTitleBar({ captions: "native" })` so the page reserves `46 × 3` instead of drawing its own buttons |
|
|
127
128
|
|
|
128
129
|
### External navigation is blocked by default
|
|
129
130
|
|
|
@@ -170,18 +171,18 @@ import htmlText from "./index.html" with { type: "text" };
|
|
|
170
171
|
const html = htmlText as unknown as string;
|
|
171
172
|
|
|
172
173
|
const app = await createApp({
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
174
|
+
html,
|
|
175
|
+
title: "My App",
|
|
176
|
+
flags: FLAG_FRAMELESS, // opt in to a self-drawn title bar
|
|
177
|
+
browserUi: BROWSER_UI_ALL,
|
|
178
|
+
service: {
|
|
179
|
+
greet: (name: string) => `hi ${name}`,
|
|
180
|
+
pickFolder: () => app.shell.pickFolder(),
|
|
181
|
+
},
|
|
181
182
|
});
|
|
182
183
|
|
|
183
|
-
await app.wait();
|
|
184
|
-
await app.stop();
|
|
184
|
+
await app.wait(); // resolves when the window closes
|
|
185
|
+
await app.stop(); // releases the event pump and the server; idempotent
|
|
185
186
|
```
|
|
186
187
|
|
|
187
188
|
Pass `page` to serve a bundled page script without writing a route yourself. `createApp` answers
|
|
@@ -209,7 +210,7 @@ Two more things on `App` that save a call and a subpath import:
|
|
|
209
210
|
```ts
|
|
210
211
|
// page.ts — inside index.html's bundle
|
|
211
212
|
import { connect } from "bun-app-window/ipc";
|
|
212
|
-
const api = await connect();
|
|
213
|
+
const api = await connect(); // token comes from the page URL's ?t=
|
|
213
214
|
await api.greet("Ada");
|
|
214
215
|
```
|
|
215
216
|
|
|
@@ -225,6 +226,20 @@ What it does for you:
|
|
|
225
226
|
`server.stop()`.
|
|
226
227
|
- Starts exactly one window-event pump: `app.events`. It is also what waits for close, so it is
|
|
227
228
|
the single consumer. Pass `{ events: app.events }` when you need `attachPlacementStore`.
|
|
229
|
+
- Reports page-side errors back to the host. Uncaught exceptions and unhandled rejections are
|
|
230
|
+
captured by an injected listener and posted over HTTP — not over the WebSocket, which does not
|
|
231
|
+
exist yet when a page script fails during evaluation. Pass `onPageError` to receive them; without
|
|
232
|
+
a callback they go to the host's stderr rather than being discarded.
|
|
233
|
+
- Checks `html` against `page` before starting. Supplying one without the other is a configuration
|
|
234
|
+
error whose only symptom is a window that renders and responds to nothing, so it throws. A
|
|
235
|
+
reference without `type="module"` produces a warning instead: the legal ways to write a
|
|
236
|
+
`<script>` tag are too varied for a string match to justify blocking startup.
|
|
237
|
+
- Serves extra static files with `assets`. A compiling framework usually emits more than one
|
|
238
|
+
artifact — typically a script plus a stylesheet — and `assets` is a path-to-text map with the
|
|
239
|
+
`content-type` inferred from the extension. Before this, every consumer hand-wrote a `routes`
|
|
240
|
+
callback for the stylesheet, and the usual mistake (a wrong MIME type) makes the browser drop the
|
|
241
|
+
stylesheet silently. Unrecognised extensions fall back to `application/octet-stream` rather than
|
|
242
|
+
guessing. A caller's `routes` still wins, and unregistered paths are 404.
|
|
228
243
|
- Startup failures leave nothing half-started: if `openShell` throws, the bound port is released.
|
|
229
244
|
|
|
230
245
|
Notes:
|
|
@@ -247,7 +262,7 @@ Notes:
|
|
|
247
262
|
- **Not re-exported from the main entry.** It is a subpath like `./state` and `./ipc`. It imports
|
|
248
263
|
`openShell` from `./index`; exporting it from `index.ts` would make that a cycle.
|
|
249
264
|
|
|
250
|
-
This is **not** a replacement for `Bun.WebView`. That one is a
|
|
265
|
+
This is **not** a replacement for `Bun.WebView`. That one is a _headless_ browser for automation,
|
|
251
266
|
testing and scraping — it has no window, and it can only screenshot its own page. This package
|
|
252
267
|
draws a real desktop window with a system title bar, DPI awareness, snap layouts and taskbar
|
|
253
268
|
identity. The two do not compete.
|
|
@@ -267,28 +282,46 @@ export type Events = { windowState: { maximized: boolean } };
|
|
|
267
282
|
import { createIpcServer } from "bun-app-window/ipc";
|
|
268
283
|
|
|
269
284
|
const ipc = createIpcServer({
|
|
270
|
-
|
|
285
|
+
service: { greet: (name: string) => `Hello ${name}` },
|
|
271
286
|
});
|
|
272
287
|
|
|
273
288
|
serveOnSafePort({
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
fetch(req) {
|
|
290
|
+
/* Serve assets & authenticate */
|
|
291
|
+
},
|
|
292
|
+
websocket: {
|
|
293
|
+
open: (ws) => ipc.attach(ws),
|
|
294
|
+
message: (ws, raw) => ipc.handle(ws, raw),
|
|
295
|
+
close: (ws) => ipc.detach(ws),
|
|
296
|
+
},
|
|
280
297
|
});
|
|
281
298
|
|
|
282
|
-
ipc.emit("windowState", { maximized: false });
|
|
299
|
+
ipc.emit("windowState", { maximized: false }); // Push event to connected pages
|
|
283
300
|
```
|
|
284
301
|
|
|
302
|
+
#### Delivering state to late connections
|
|
303
|
+
|
|
304
|
+
A page loads _after_ the window is created. Anything the host emits between `createApp` returning
|
|
305
|
+
and the page's WebSocket opening is delivered to zero connections and is not replayed — so a page
|
|
306
|
+
that reads its initial state from events alone stays empty, with no error on either side.
|
|
307
|
+
|
|
308
|
+
Mark state-shaped events as sticky and the server replays the last payload to each new connection:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
ipc.emit("windowState", { maximized: false }, { sticky: true }); // Replayed on attach()
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Sticky is **opt-in** and keeps one payload per event name, not a history. Events that are
|
|
315
|
+
notifications rather than state should stay non-sticky: a late connection would otherwise receive
|
|
316
|
+
one stale message that reads like a fresh one.
|
|
317
|
+
|
|
285
318
|
```ts
|
|
286
319
|
// Page
|
|
287
320
|
import { connect } from "bun-app-window/ipc";
|
|
288
321
|
import type { Api, Events } from "./api";
|
|
289
322
|
|
|
290
323
|
const api = await connect<Api, Events>();
|
|
291
|
-
await api.greet("Ada");
|
|
324
|
+
await api.greet("Ada"); // Inferred return type: string
|
|
292
325
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
293
326
|
```
|
|
294
327
|
|
|
@@ -319,17 +352,22 @@ The client is a `Proxy`: a handful of names resolve to the client itself and nev
|
|
|
319
352
|
method. If your service exposes a method under one of these names, **the server-side method becomes
|
|
320
353
|
unreachable** — the page calls the client's own member instead, and neither end reports an error.
|
|
321
354
|
|
|
322
|
-
| Name
|
|
323
|
-
|
|
|
324
|
-
| `on` / `off` | Event subscription helpers
|
|
325
|
-
| `close()`
|
|
326
|
-
| `connected`
|
|
327
|
-
| `pending`
|
|
328
|
-
| `then`
|
|
355
|
+
| Name | What the page actually gets |
|
|
356
|
+
| ------------ | ------------------------------------------------------------------------ |
|
|
357
|
+
| `on` / `off` | Event subscription helpers |
|
|
358
|
+
| `close()` | Closes the WebSocket channel — every later call is rejected |
|
|
359
|
+
| `connected` | `boolean`: whether the channel is open |
|
|
360
|
+
| `pending` | `number` of calls still awaiting a reply |
|
|
361
|
+
| `then` | `undefined`, so `await client` cannot hang on a never-resolving thenable |
|
|
329
362
|
|
|
330
363
|
Symbol keys are reserved as well. Pick a different service name (`winClose` instead of `close`) and
|
|
331
364
|
map it back on the page side — see the title bar section below.
|
|
332
365
|
|
|
366
|
+
Because the failure above is invisible at runtime, `createIpcServer` **throws at creation time** when
|
|
367
|
+
the service table contains any of these names. The error names the offending keys and suggests a
|
|
368
|
+
replacement. Pass `{ rejectReservedNames: false }` only when the table is generated from external
|
|
369
|
+
input and validated by other means.
|
|
370
|
+
|
|
333
371
|
### `bun-app-window/titlebar` — Windows 11 Title Bar
|
|
334
372
|
|
|
335
373
|
Mounts a customizable Windows 11 title bar inside frameless windows. The IPC service is **flat** —
|
|
@@ -339,12 +377,12 @@ so the host exposes `winClose` and the page maps it back:
|
|
|
339
377
|
```ts
|
|
340
378
|
// Host — flat service; `close` is reserved by the IPC client
|
|
341
379
|
createIpcServer({
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
380
|
+
service: {
|
|
381
|
+
minimize: () => shell.minimize(),
|
|
382
|
+
toggleMaximize: () => shell.toggleMaximize(),
|
|
383
|
+
winClose: () => shell.close(),
|
|
384
|
+
drag: () => shell.drag(),
|
|
385
|
+
},
|
|
348
386
|
});
|
|
349
387
|
```
|
|
350
388
|
|
|
@@ -353,14 +391,14 @@ createIpcServer({
|
|
|
353
391
|
import { mountTitleBar, bridgeFromWindowService } from "bun-app-window/titlebar";
|
|
354
392
|
|
|
355
393
|
const bar = mountTitleBar({
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
394
|
+
title: "My App",
|
|
395
|
+
icon: "/icon.svg",
|
|
396
|
+
bridge: bridgeFromWindowService({
|
|
397
|
+
minimize: () => api.minimize(),
|
|
398
|
+
toggleMaximize: () => api.toggleMaximize(),
|
|
399
|
+
close: () => api.winClose(), // mapped back to the bridge's own name
|
|
400
|
+
drag: () => api.drag(),
|
|
401
|
+
}),
|
|
364
402
|
});
|
|
365
403
|
|
|
366
404
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
@@ -388,7 +426,7 @@ if (saved) restorePlacement(shell, saved);
|
|
|
388
426
|
|
|
389
427
|
shell.show();
|
|
390
428
|
|
|
391
|
-
const store = attachPlacementStore(shell, FILE);
|
|
429
|
+
const store = attachPlacementStore(shell, FILE); // Subscribes to window events; writes once the drag settles
|
|
392
430
|
process.on("beforeExit", () => store.flush());
|
|
393
431
|
```
|
|
394
432
|
|
|
@@ -426,8 +464,10 @@ event stream directly:
|
|
|
426
464
|
import { EVENT_CLOSED, EVENT_STATE } from "bun-app-window/events";
|
|
427
465
|
|
|
428
466
|
for (const e of shell.pollEvents()) {
|
|
429
|
-
|
|
430
|
-
|
|
467
|
+
if (e.type === EVENT_CLOSED) break; // window is gone, the native singleton is released
|
|
468
|
+
if (e.type === EVENT_STATE && e.maximized) {
|
|
469
|
+
/* ... */
|
|
470
|
+
}
|
|
431
471
|
}
|
|
432
472
|
```
|
|
433
473
|
|
|
@@ -439,7 +479,9 @@ exactly one consumer. For a fixed cadence plus multi-subscriber fan-out, use
|
|
|
439
479
|
import { EVENT_CLOSED, attachEventPump } from "bun-app-window/events";
|
|
440
480
|
|
|
441
481
|
const pump = attachEventPump(shell, { intervalMs: 16 });
|
|
442
|
-
pump.on((e) => {
|
|
482
|
+
pump.on((e) => {
|
|
483
|
+
if (e.type === EVENT_CLOSED) pump.stop();
|
|
484
|
+
});
|
|
443
485
|
pump.onGap((missing) => console.warn(`missed ${missing} events`));
|
|
444
486
|
```
|
|
445
487
|
|
|
@@ -461,8 +503,8 @@ Exposes internal metrics and hit-testing utilities. Note: This module is exclude
|
|
|
461
503
|
import { debugOf } from "bun-app-window/debug";
|
|
462
504
|
|
|
463
505
|
const d = debugOf(shell);
|
|
464
|
-
d.metrics();
|
|
465
|
-
d.dpi();
|
|
506
|
+
d.metrics(); // → "pos=196,196 win=1960x1400 client=1960x1400 work=3072x1824 border=13x13 round=1"
|
|
507
|
+
d.dpi(); // → "awareness=-1 dpi=192 scale=200%"
|
|
466
508
|
d.hittest(x, y);
|
|
467
509
|
```
|
|
468
510
|
|
|
@@ -511,6 +553,60 @@ await writePageBundle({ entry: "src/ui/page.ts", out: "src/ui/page.bundle.txt" }
|
|
|
511
553
|
`bytes` is a UTF-8 byte count, not `string.length`; the two differ on any source containing non-ASCII
|
|
512
554
|
text, and a reported size that only looks right is worse than no reported size.
|
|
513
555
|
|
|
556
|
+
#### Compiling frameworks: `plugins`, `define`, `loader`
|
|
557
|
+
|
|
558
|
+
These three are passed straight through to `Bun.build`; nothing about them is specific to any
|
|
559
|
+
framework, and this package ships no plugins of its own.
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
const built = await bundlePage({
|
|
563
|
+
entry: "src/main.ts",
|
|
564
|
+
plugins: [myFrameworkPlugin()], // .vue / .svelte / .tsx — whatever your plugin handles
|
|
565
|
+
define: { __VUE_OPTIONS_API__: "false" }, // compile-time constants
|
|
566
|
+
loader: { ".thing": "ts" }, // custom extensions
|
|
567
|
+
});
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
#### `alias` is implemented here, because Bun has no such option
|
|
571
|
+
|
|
572
|
+
`BuildConfig` in Bun 1.4.4 has no `alias` property, and passing one is not an error — it is silently
|
|
573
|
+
ignored. Four forms were tried and all failed to resolve. So this package backs `alias` with its own
|
|
574
|
+
`onResolve` plugin:
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
await bundlePage({ entry, alias: { "@": "./src", lib: "./src/lib" } });
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
| Specifier | Rewritten? |
|
|
581
|
+
| ------------------------------------- | ---------- |
|
|
582
|
+
| equal to a key, or prefixed by `key/` | yes |
|
|
583
|
+
| starts with `.` or `/` | no |
|
|
584
|
+
| has a `scheme:` prefix | no |
|
|
585
|
+
|
|
586
|
+
**The default is empty.** `$lib` is a SvelteKit convention and `@/` is some project's `tsconfig`
|
|
587
|
+
alias; neither belongs here. Aliases merge with your plugins rather than replacing them, and the
|
|
588
|
+
plugin is only registered when you pass `alias`.
|
|
589
|
+
|
|
590
|
+
#### Multiple outputs
|
|
591
|
+
|
|
592
|
+
A compiling framework often produces more than one artifact — a script plus a stylesheet is the
|
|
593
|
+
common shape. `outputs` lists all of them with `path` / `kind` / `code` / `bytes`, and `code` /
|
|
594
|
+
`bytes` refer to the `entry-point` artifact specifically (not `outputs[0]`, because output order is
|
|
595
|
+
not a contract).
|
|
596
|
+
|
|
597
|
+
`writePageBundle` writes every artifact: the entry to `out`, the rest to the same directory with the
|
|
598
|
+
extension substituted. `page.bundle.txt` therefore yields `page.bundle.css`, **not** `main.css` — the
|
|
599
|
+
entry's filename must not leak into a path that HTML and `assets` reference. It returns
|
|
600
|
+
`written: Array<{ path, bytes }>` so cleanup removes every file it created.
|
|
601
|
+
|
|
602
|
+
```text
|
|
603
|
+
$ bun run bundle:page
|
|
604
|
+
109,888 B dist/page.bundle.txt ← entry-point
|
|
605
|
+
133 B dist/page.bundle.css ← asset
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Serve the extra files with `createApp({ assets })` — see the `app` section above.
|
|
609
|
+
|
|
514
610
|
Failure throws with Bun's full diagnostics. Both shapes are handled: compile-time problems come back
|
|
515
611
|
as `{ success: false, logs }`, while an unreachable entry point **throws an `AggregateError`** whose
|
|
516
612
|
`message` is only `Bundle failed` and whose detail lives on `.errors`.
|
|
@@ -541,13 +637,13 @@ const shell = await openShell({ url, browserUi: BROWSER_UI_ALL });
|
|
|
541
637
|
|
|
542
638
|
Individual flags can also be combined via bitwise OR:
|
|
543
639
|
|
|
544
|
-
| Flag
|
|
545
|
-
|
|
|
546
|
-
| `BROWSER_UI_MENU`
|
|
547
|
-
| `BROWSER_UI_KEYS`
|
|
548
|
-
| `BROWSER_UI_STATUSBAR` | Bottom-left URL hover status bar
|
|
549
|
-
| `BROWSER_UI_SWIPE`
|
|
550
|
-
| `BROWSER_UI_AUTOFILL`
|
|
640
|
+
| Flag | Disabled Behavior |
|
|
641
|
+
| ---------------------- | --------------------------------------------------------- |
|
|
642
|
+
| `BROWSER_UI_MENU` | Default Edge right-click context menu |
|
|
643
|
+
| `BROWSER_UI_KEYS` | Built-in browser shortcuts (`Ctrl+P`, `Ctrl+F`, `Ctrl+S`) |
|
|
644
|
+
| `BROWSER_UI_STATUSBAR` | Bottom-left URL hover status bar |
|
|
645
|
+
| `BROWSER_UI_SWIPE` | Trackpad horizontal swipe back/forward navigation |
|
|
646
|
+
| `BROWSER_UI_AUTOFILL` | Form autofill popups |
|
|
551
647
|
|
|
552
648
|
Note: `F11`, `F12`, and `Ctrl±` zoom shortcuts are handled directly by the native shell and remain active regardless of these flags.
|
|
553
649
|
|
|
@@ -558,26 +654,22 @@ After disabling the default browser menu, render native Win32 popup menus (`Trac
|
|
|
558
654
|
```ts
|
|
559
655
|
// Host: expose the menu capability on the flat service
|
|
560
656
|
createIpcServer({
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
657
|
+
service: {
|
|
658
|
+
showContextMenu: (x: number, y: number, items: string) => shell.showContextMenu(x, y, items),
|
|
659
|
+
},
|
|
564
660
|
});
|
|
565
661
|
```
|
|
566
662
|
|
|
567
663
|
```ts
|
|
568
664
|
// Page: trigger the native menu
|
|
569
665
|
document.addEventListener("contextmenu", async (e) => {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
Math.round(e.screenX),
|
|
573
|
-
Math.round(e.screenY),
|
|
574
|
-
"open|Open\nrefresh|Refresh\n-\nquit|Quit"
|
|
575
|
-
);
|
|
666
|
+
e.preventDefault();
|
|
667
|
+
await api.showContextMenu(Math.round(e.screenX), Math.round(e.screenY), "open|Open\nrefresh|Refresh\n-\nquit|Quit");
|
|
576
668
|
});
|
|
577
669
|
|
|
578
670
|
// Page: handle menu item selection
|
|
579
671
|
window.addEventListener("shell-msg", (e) => {
|
|
580
|
-
|
|
672
|
+
if (e.detail.t === "menu") console.log("Selected ID:", e.detail.id);
|
|
581
673
|
});
|
|
582
674
|
```
|
|
583
675
|
|
|
@@ -597,7 +689,7 @@ const shell = await openShell({ url, flags: FLAG_ACCEPT_DROP });
|
|
|
597
689
|
```js
|
|
598
690
|
// Page
|
|
599
691
|
window.addEventListener("shell-msg", (e) => {
|
|
600
|
-
|
|
692
|
+
if (e.detail.t === "drop") console.log("Dropped paths:", e.detail.paths);
|
|
601
693
|
});
|
|
602
694
|
```
|
|
603
695
|
|
|
@@ -605,10 +697,10 @@ Files **and folders** are forwarded by a script the shell injects, so the page d
|
|
|
605
697
|
|
|
606
698
|
```js
|
|
607
699
|
window.addEventListener("shell-msg", (e) => {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
700
|
+
if (e.detail.t !== "drop") return;
|
|
701
|
+
for (const p of e.detail.paths) {
|
|
702
|
+
console.log(e.detail.dirs.includes(p) ? "folder" : "file", p);
|
|
703
|
+
}
|
|
612
704
|
});
|
|
613
705
|
```
|
|
614
706
|
|
|
@@ -619,14 +711,27 @@ When running in an elevated administrator process (High Integrity Level) while E
|
|
|
619
711
|
The shell does not inject any implicit DOM nodes or stylesheets into your page. To make your web UI feel like a native desktop app, include the following baseline rules:
|
|
620
712
|
|
|
621
713
|
```css
|
|
622
|
-
html,
|
|
623
|
-
body {
|
|
624
|
-
|
|
625
|
-
|
|
714
|
+
html,
|
|
715
|
+
body {
|
|
716
|
+
cursor: default;
|
|
717
|
+
}
|
|
718
|
+
body {
|
|
719
|
+
user-select: none;
|
|
720
|
+
}
|
|
721
|
+
input,
|
|
722
|
+
textarea,
|
|
723
|
+
.log {
|
|
724
|
+
user-select: text;
|
|
725
|
+
cursor: auto;
|
|
726
|
+
}
|
|
727
|
+
button:focus-visible {
|
|
728
|
+
outline: 2px solid var(--accent);
|
|
729
|
+
outline-offset: 2px;
|
|
730
|
+
}
|
|
626
731
|
```
|
|
627
732
|
|
|
628
733
|
```html
|
|
629
|
-
<input type="text" spellcheck="false" autocomplete="off" autocapitalize="off"
|
|
734
|
+
<input type="text" spellcheck="false" autocomplete="off" autocapitalize="off" />
|
|
630
735
|
```
|
|
631
736
|
|
|
632
737
|
Setting `spellcheck="false"` is particularly important—red squiggly underlines beneath file paths and numeric inputs immediately give away a web view.
|
|
@@ -637,7 +742,7 @@ When bundling with `bun build --compile`, import `bun-app-window/embedded` to em
|
|
|
637
742
|
|
|
638
743
|
```ts
|
|
639
744
|
import { createShell } from "bun-app-window";
|
|
640
|
-
import { embeddedDll } from "bun-app-window/embedded";
|
|
745
|
+
import { embeddedDll } from "bun-app-window/embedded"; // Embeds the DLL into the binary
|
|
641
746
|
|
|
642
747
|
const shell = await createShell({ url, embedded: embeddedDll });
|
|
643
748
|
```
|
|
@@ -677,18 +782,18 @@ At runtime, `libshell.dll` is located in the following order:
|
|
|
677
782
|
|
|
678
783
|
### Built-in Keyboard & Mouse Shortcuts
|
|
679
784
|
|
|
680
|
-
| Input
|
|
681
|
-
|
|
|
682
|
-
| `Ctrl +` / `Ctrl -` / `Ctrl 0`
|
|
683
|
-
| `Ctrl + Mouse Wheel`
|
|
684
|
-
| `F11`
|
|
685
|
-
| `F5` / `Ctrl + R`
|
|
686
|
-
| `F12`
|
|
687
|
-
| `Alt + Space` / Right-click title bar | Open native Windows system menu
|
|
785
|
+
| Input | Behavior |
|
|
786
|
+
| ------------------------------------- | -------------------------------------------------------------------- |
|
|
787
|
+
| `Ctrl +` / `Ctrl -` / `Ctrl 0` | Zoom in / Zoom out / Reset zoom to 100% |
|
|
788
|
+
| `Ctrl + Mouse Wheel` | Zoom in / Zoom out |
|
|
789
|
+
| `F11` | Toggle fullscreen mode (hides window borders and covers the taskbar) |
|
|
790
|
+
| `F5` / `Ctrl + R` | Reload page |
|
|
791
|
+
| `F12` | Open WebView2 Developer Tools |
|
|
792
|
+
| `Alt + Space` / Right-click title bar | Open native Windows system menu |
|
|
688
793
|
|
|
689
794
|
## Development & Verification
|
|
690
795
|
|
|
691
|
-
Built with Bun 1.4.
|
|
796
|
+
Built with Bun 1.4.4 and TypeScript 7.0.2 with full strict type-checking enabled.
|
|
692
797
|
|
|
693
798
|
**This project has no remote CI.** GitHub Actions cannot allocate runners for this account (billing lock), and the project is a single-platform, single-developer package with no external service dependencies — so the gates run locally instead. The recommended entry point is:
|
|
694
799
|
|
|
@@ -715,10 +820,10 @@ Whenever `src/shell/` or `package.json` is modified, the consumer acceptance sta
|
|
|
715
820
|
|
|
716
821
|
For window enumeration (`EnumWindows`) or automated screenshot scripts, filter by the corresponding Win32 window class name:
|
|
717
822
|
|
|
718
|
-
| Window Mode
|
|
719
|
-
|
|
|
720
|
-
| Standard framed (without `FLAG_FRAMELESS`) | `webview`
|
|
721
|
-
| Frameless (with `FLAG_FRAMELESS`)
|
|
823
|
+
| Window Mode | Window Class Name |
|
|
824
|
+
| ------------------------------------------ | ----------------- |
|
|
825
|
+
| Standard framed (without `FLAG_FRAMELESS`) | `webview` |
|
|
826
|
+
| Frameless (with `FLAG_FRAMELESS`) | `webview_shell` |
|
|
722
827
|
|
|
723
828
|
## Acknowledgments
|
|
724
829
|
|