mirinjs 0.1.0-alpha.14 → 0.1.0-alpha.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirinjs",
3
- "version": "0.1.0-alpha.14",
3
+ "version": "0.1.0-alpha.16",
4
4
  "description": "Build desktop apps with Bun, TypeScript, and Chromium (CEF). The Bun-native Electron alternative.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "exports": {
25
25
  ".": "./src/index.ts",
26
- "./config": "./src/config.ts",
26
+ "./config": "./src/config/index.ts",
27
27
  "./rpc": "./src/rpc.ts",
28
28
  "./client": "./src/client.ts",
29
29
  "./codec": "./src/codec.ts",
@@ -3,95 +3,35 @@
3
3
  * (docs/api-design.md). Talks to the native core through the runtime.
4
4
  */
5
5
 
6
- import { runtime, onNativeEvent, resolveUrl, type NativeEvent } from "./runtime.ts";
7
- import type { Router, EventProc } from "./rpc.ts";
8
- import type { WindowConfig, WindowMaterial, WindowMaterialOptions } from "./config.ts";
9
- import { updater } from "./updater.ts";
10
- import type { Updater } from "./updater.ts";
11
- import { sidecar as spawnSidecar } from "./sidecar.ts";
12
- import type { SidecarOptions, SidecarProcess } from "./sidecar.ts";
13
-
14
- /** Which native backend a window's `material` resolved to. */
15
- export type WindowMaterialInfo = {
16
- /** The material that was requested. */
17
- requested: string;
18
- /** What actually rendered: real Liquid Glass, a vibrancy material, or none. */
19
- backend: "liquidGlass" | "vibrancy" | "none";
20
- /** Whether Apple's Liquid Glass (NSGlassEffectView, macOS 26+) is available. */
21
- liquidGlassAvailable: boolean;
22
- };
23
-
24
- export type WindowEvents = {
25
- focus: void;
26
- blur: void;
27
- moved: void;
28
- resized: void;
29
- closed: void;
30
- /** Fired when a native background material is applied (see setMaterial). */
31
- material: WindowMaterialInfo;
32
- };
33
-
34
- export type AppEvents = {
35
- ready: void;
36
- "window-all-closed": void;
37
- /** A deep-link URL (a registered `urlSchemes` scheme) opened the app, or
38
- * arrived while it was running. Includes the launch URL. */
39
- "open-url": string;
40
- };
41
-
42
- /** A window's frame in screen points (bottom-left origin, like AppKit). */
43
- export interface WindowFrame {
44
- x: number;
45
- y: number;
46
- width: number;
47
- height: number;
48
- }
49
-
50
- type Listener<P> = (payload: P) => void;
51
-
52
- class Emitter<Events extends Record<string, unknown>> {
53
- #listeners = new Map<keyof Events, Set<Listener<unknown>>>();
54
-
55
- on<K extends keyof Events>(type: K, listener: Listener<Events[K]>): () => void {
56
- let set = this.#listeners.get(type);
57
- if (!set) this.#listeners.set(type, (set = new Set()));
58
- set.add(listener as Listener<unknown>);
59
- return () => set!.delete(listener as Listener<unknown>);
60
- }
61
-
62
- async *events<K extends keyof Events>(type: K): AsyncIterableIterator<Events[K]> {
63
- const queue: Events[K][] = [];
64
- let wake: (() => void) | undefined;
65
- const off = this.on(type, (payload) => {
66
- queue.push(payload);
67
- wake?.();
68
- });
69
- try {
70
- while (true) {
71
- while (queue.length) yield queue.shift()!;
72
- await new Promise<void>((resolve) => (wake = resolve));
73
- }
74
- } finally {
75
- off();
76
- }
77
- }
78
-
79
- protected emit<K extends keyof Events>(type: K, payload: Events[K]): void {
80
- this.#listeners.get(type)?.forEach((fn) => fn(payload));
81
- }
82
- }
83
-
84
- export interface WindowOpenOptions extends WindowConfig {
85
- name?: string;
86
- }
6
+ import type { WindowMaterial, WindowMaterialOptions } from "../config/index.ts";
7
+ import type { Router } from "../rpc.ts";
8
+ import { type NativeEvent, onNativeEvent, resolveUrl, runtime } from "../runtime.ts";
9
+ import type { SidecarOptions, SidecarProcess } from "../sidecar.ts";
10
+ import { sidecar as spawnSidecar } from "../sidecar.ts";
11
+ import type { Updater } from "../updater/index.ts";
12
+ import { updater } from "../updater/index.ts";
13
+ import { Emitter } from "./lib/emitter.ts";
14
+ import { normalizeMaterial } from "./lib/material.ts";
15
+ import type {
16
+ AppEvents,
17
+ BroadcastEmitters,
18
+ Dock,
19
+ ServeHandle,
20
+ WindowEvents,
21
+ WindowFrame,
22
+ WindowMaterialInfo,
23
+ WindowOpenOptions,
24
+ } from "./types.ts";
87
25
 
88
26
  /** A live, typed handle to an open window. */
89
27
  export class WindowHandle extends Emitter<WindowEvents> {
90
- constructor(
91
- readonly id: number,
92
- readonly name: string | undefined,
93
- ) {
28
+ readonly id: number;
29
+ readonly name: string | undefined;
30
+
31
+ constructor(id: number, name: string | undefined) {
94
32
  super();
33
+ this.id = id;
34
+ this.name = name;
95
35
  }
96
36
 
97
37
  async setTitle(title: string): Promise<void> {
@@ -182,7 +122,10 @@ export class WindowHandle extends Emitter<WindowEvents> {
182
122
  }
183
123
  }
184
124
 
185
- type RpcEmitters = Record<string, { emit(payload: unknown): void; broadcast(payload: unknown): void }>;
125
+ type RpcEmitters = Record<
126
+ string,
127
+ { emit(payload: unknown): void; broadcast(payload: unknown): void }
128
+ >;
186
129
 
187
130
  function rpcEmitters(send: (method: string, payload: unknown) => void): RpcEmitters {
188
131
  return new Proxy({} as RpcEmitters, {
@@ -195,20 +138,6 @@ function rpcEmitters(send: (method: string, payload: unknown) => void): RpcEmitt
195
138
  });
196
139
  }
197
140
 
198
- /** Typed push emitters derived from a router's `event` procedures. */
199
- export type BroadcastEmitters<R extends Router<any>> =
200
- R extends Router<infer T>
201
- ? {
202
- [K in keyof T as T[K] extends EventProc<any> ? K : never]: T[K] extends EventProc<infer P>
203
- ? { broadcast(payload: P): void }
204
- : never;
205
- }
206
- : never;
207
-
208
- export interface ServeHandle<R extends Router<any>> {
209
- readonly rpc: BroadcastEmitters<R>;
210
- }
211
-
212
141
  class Windows {
213
142
  #byName = new Map<string, WindowHandle>();
214
143
  #byId = new Map<number, WindowHandle>();
@@ -267,26 +196,10 @@ function manifestWindow(name: string): WindowOpenOptions {
267
196
  return found;
268
197
  }
269
198
 
270
- /** Normalize the `material` option (name or object, or null) to native form. */
271
- function normalizeMaterial(
272
- material: WindowMaterial | WindowMaterialOptions | null | undefined,
273
- ): WindowMaterialOptions | null {
274
- if (!material) return null;
275
- return typeof material === "string" ? { type: material } : material;
276
- }
277
-
278
- /** macOS Dock-icon controls (no-ops off macOS). */
279
- export interface Dock {
280
- /** Hide the Dock icon and menu-bar presence (agent/accessory app). */
281
- hide(): void;
282
- /** Restore the Dock icon and menu bar. */
283
- show(): void;
284
- }
285
-
286
199
  class MirinApp extends Emitter<AppEvents> {
287
200
  readonly windows = new Windows();
288
201
 
289
- /** Auto-updater (macOS). Inert unless the app was built with `release` set. */
202
+ /** Cross-platform auto-updater. Inert unless the app was built with `release` set. */
290
203
  readonly updater: Updater = updater;
291
204
 
292
205
  /** This app's bundle id (mirin.config `id`). `undefined` when run detached. */
@@ -328,6 +241,7 @@ class MirinApp extends Emitter<AppEvents> {
328
241
  }
329
242
  }
330
243
 
244
+ /** Type-level `any` preserves the caller's concrete router shape. */
331
245
  serve<R extends Router<any>>(router: R): ServeHandle<R> {
332
246
  runtime().rpc.setRouter(router);
333
247
  return { rpc: this.rpc as BroadcastEmitters<R> };
@@ -0,0 +1,2 @@
1
+ export * from "./facade.ts";
2
+ export * from "./types.ts";
@@ -0,0 +1,40 @@
1
+ type Listener<P> = (payload: P) => void;
2
+
3
+ export class Emitter<Events extends Record<string, unknown>> {
4
+ #listeners = new Map<keyof Events, Set<Listener<unknown>>>();
5
+
6
+ on<K extends keyof Events>(type: K, listener: Listener<Events[K]>): () => void {
7
+ let set = this.#listeners.get(type);
8
+ if (!set) {
9
+ set = new Set();
10
+ this.#listeners.set(type, set);
11
+ }
12
+ set.add(listener as Listener<unknown>);
13
+ return () => {
14
+ set.delete(listener as Listener<unknown>);
15
+ };
16
+ }
17
+
18
+ async *events<K extends keyof Events>(type: K): AsyncIterableIterator<Events[K]> {
19
+ const queue: Events[K][] = [];
20
+ let wake: (() => void) | undefined;
21
+ const off = this.on(type, (payload) => {
22
+ queue.push(payload);
23
+ wake?.();
24
+ });
25
+ try {
26
+ while (true) {
27
+ while (queue.length) yield queue.shift()!;
28
+ await new Promise<void>((resolve) => {
29
+ wake = resolve;
30
+ });
31
+ }
32
+ } finally {
33
+ off();
34
+ }
35
+ }
36
+
37
+ protected emit<K extends keyof Events>(type: K, payload: Events[K]): void {
38
+ for (const fn of this.#listeners.get(type) ?? []) fn(payload);
39
+ }
40
+ }
@@ -0,0 +1,9 @@
1
+ import type { WindowMaterial, WindowMaterialOptions } from "../../config/index.ts";
2
+
3
+ /** Normalize the `material` option (name or object, or null) to native form. */
4
+ export function normalizeMaterial(
5
+ material: WindowMaterial | WindowMaterialOptions | null | undefined,
6
+ ): WindowMaterialOptions | null {
7
+ if (!material) return null;
8
+ return typeof material === "string" ? { type: material } : material;
9
+ }
@@ -0,0 +1,65 @@
1
+ import type { WindowConfig } from "../config/index.ts";
2
+ import type { EventProc, Router } from "../rpc.ts";
3
+
4
+ /** Which native backend a window's `material` resolved to. */
5
+ export type WindowMaterialInfo = {
6
+ /** The material that was requested. */
7
+ requested: string;
8
+ /** What actually rendered: real Liquid Glass, a vibrancy material, or none. */
9
+ backend: "liquidGlass" | "vibrancy" | "none";
10
+ /** Whether Apple's Liquid Glass (NSGlassEffectView, macOS 26+) is available. */
11
+ liquidGlassAvailable: boolean;
12
+ };
13
+
14
+ export type WindowEvents = {
15
+ focus: void;
16
+ blur: void;
17
+ moved: void;
18
+ resized: void;
19
+ closed: void;
20
+ /** Fired when a native background material is applied (see setMaterial). */
21
+ material: WindowMaterialInfo;
22
+ };
23
+
24
+ export type AppEvents = {
25
+ ready: void;
26
+ "window-all-closed": void;
27
+ /** A deep-link URL (a registered `urlSchemes` scheme) opened the app, or
28
+ * arrived while it was running. Includes the launch URL. */
29
+ "open-url": string;
30
+ };
31
+
32
+ /** A window's frame in screen points (bottom-left origin, like AppKit). */
33
+ export interface WindowFrame {
34
+ x: number;
35
+ y: number;
36
+ width: number;
37
+ height: number;
38
+ }
39
+
40
+ export interface WindowOpenOptions extends WindowConfig {
41
+ name?: string;
42
+ }
43
+
44
+ /** Typed push emitters derived from a router's `event` procedures.
45
+ * Type-level `any` keeps the caller's router payload inference intact. */
46
+ export type BroadcastEmitters<R extends Router<any>> = R extends Router<infer T>
47
+ ? {
48
+ [K in keyof T as T[K] extends EventProc<any> ? K : never]: T[K] extends EventProc<infer P>
49
+ ? { broadcast(payload: P): void }
50
+ : never;
51
+ }
52
+ : never;
53
+
54
+ /** Type-level `any` preserves the caller's concrete router shape. */
55
+ export interface ServeHandle<R extends Router<any>> {
56
+ readonly rpc: BroadcastEmitters<R>;
57
+ }
58
+
59
+ /** macOS Dock-icon controls (no-ops off macOS). */
60
+ export interface Dock {
61
+ /** Hide the Dock icon and menu-bar presence (agent/accessory app). */
62
+ hide(): void;
63
+ /** Restore the Dock icon and menu bar. */
64
+ show(): void;
65
+ }
package/src/client.ts CHANGED
@@ -7,18 +7,18 @@
7
7
 
8
8
  import type { EventProc, MutationProc, QueryProc, Router } from "./rpc.ts";
9
9
 
10
- export type ClientFor<R extends Router<any>> =
11
- R extends Router<infer T>
12
- ? {
13
- [K in keyof T]: T[K] extends QueryProc<infer I, infer O>
10
+ // Type-level `any` preserves the app router's concrete procedure inference for UI callers.
11
+ export type ClientFor<R extends Router<any>> = R extends Router<infer T>
12
+ ? {
13
+ [K in keyof T]: T[K] extends QueryProc<infer I, infer O>
14
+ ? (input: I) => Promise<Awaited<O>>
15
+ : T[K] extends MutationProc<infer I, infer O>
14
16
  ? (input: I) => Promise<Awaited<O>>
15
- : T[K] extends MutationProc<infer I, infer O>
16
- ? (input: I) => Promise<Awaited<O>>
17
- : T[K] extends EventProc<infer P>
18
- ? { on(listener: (payload: P) => void): () => void }
19
- : never;
20
- }
21
- : never;
17
+ : T[K] extends EventProc<infer P>
18
+ ? { on(listener: (payload: P) => void): () => void }
19
+ : never;
20
+ }
21
+ : never;
22
22
 
23
23
  interface MirinTransport {
24
24
  call(method: string, input: unknown): Promise<unknown>;
@@ -49,6 +49,7 @@ function transport(): MirinTransport {
49
49
  * Create the typed client for the app's router. Pass the Router *type* only —
50
50
  * never import the router value (and its handlers) into UI code.
51
51
  */
52
+ // Type-level `any` keeps `client<AppRouter>()` inference intact.
52
53
  export function client<R extends Router<any>>(): ClientFor<R> {
53
54
  const eventCache = new Map<string, { on(l: (p: unknown) => void): () => void }>();
54
55
 
@@ -0,0 +1 @@
1
+ export * from "./manifest.ts";
@@ -97,8 +97,10 @@ export interface MirinConfig {
97
97
  * `.ico` for the exe and window.
98
98
  */
99
99
  icon?: string;
100
+ /** CEF runtime packaging options. Omit to bundle every locale. */
101
+ cef?: CefConfig;
100
102
  /**
101
- * Auto-update configuration (macOS). Omit to disable updates entirely.
103
+ * Cross-platform auto-update configuration. Omit to disable updates entirely.
102
104
  * `baseUrl` is a flat directory of release artifacts (a GitHub Releases
103
105
  * `…/releases/latest/download` URL, an S3/R2 bucket, or any static host).
104
106
  * `channel` (default "stable") is baked into artifact filenames + the app's
@@ -140,7 +142,8 @@ export interface MirinConfig {
140
142
  /**
141
143
  * External binaries bundled into the `.app` and spawned at runtime with
142
144
  * `app.sidecar(name)`. Maps a logical name to a path (relative to the project
143
- * root) or a {@link SidecarSpec}. Each binary is copied into
145
+ * root) or a {@link SidecarSpec}. Names must be filename-safe (`A-Z`, `a-z`,
146
+ * `0-9`, `.`, `_`, `-`) and paths must stay under the project root. Each binary is copied into
144
147
  * `Contents/Resources/sidecars/<name>`, codesigned (hardened runtime), and —
145
148
  * when notary credentials are set — notarized with the rest of the app.
146
149
  *
@@ -151,7 +154,8 @@ export interface MirinConfig {
151
154
  sidecars?: Record<string, string | SidecarSpec>;
152
155
  /**
153
156
  * Extra Bun Worker entry files (relative to the project root) bundled next to
154
- * the main worker. Resolve one at runtime with `resolveWorker(name)` and pass
157
+ * the main worker. Names follow the same filename-safe rule as sidecars, and
158
+ * paths must stay under the project root. Resolve one at runtime with `resolveWorker(name)` and pass
155
159
  * it to `new Worker(...)` from `node:worker_threads`. These run off the main
156
160
  * thread for CPU/IO offload and must NOT call window/native APIs — only the
157
161
  * app worker owns AppKit/CEF (see docs/architecture.md).
@@ -307,6 +311,16 @@ export interface ReleaseConfig {
307
311
  baseUrl: string;
308
312
  /** Update channel; baked into artifact names + support dir. Default "stable". */
309
313
  channel?: string;
314
+ /** Optional markdown release notes embedded in the update manifest. */
315
+ notes?: string;
316
+ }
317
+
318
+ export interface CefConfig {
319
+ /**
320
+ * Locale packs bundled with production apps, as BCP 47 tags such as
321
+ * `en-US`, `es-419`, or `pt-BR`. Omit to bundle every CEF locale.
322
+ */
323
+ locales?: string[];
310
324
  }
311
325
 
312
326
  /** Identity function for typing/intellisense. `const` generic preserves window names as literal keys. */
package/src/dialog.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * back in a `dialog.result` event, which resolves the matching promise.
4
4
  */
5
5
 
6
- import { runtime, onNativeEvent } from "./runtime.ts";
6
+ import { onNativeEvent, runtime } from "./runtime.ts";
7
7
 
8
8
  export interface OpenDialogOptions {
9
9
  message?: string;
package/src/host.ts CHANGED
@@ -14,9 +14,9 @@
14
14
  * their `app://` URLs served from Contents/Resources.
15
15
  */
16
16
 
17
- import { Worker } from "node:worker_threads";
18
- import { dirname, join } from "node:path";
19
17
  import { existsSync, readFileSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ import { Worker } from "node:worker_threads";
20
20
  import { Core } from "./native.ts";
21
21
 
22
22
  // Bundle layout differs by platform: macOS `.app` puts the host in
@@ -45,15 +45,11 @@ if (!existsSync(corePath) || !existsSync(workerPath)) {
45
45
  process.exit(1);
46
46
  }
47
47
 
48
- const manifest = JSON.parse(
49
- process.env.MIRIN_MANIFEST_JSON ?? readManifestFromBundle() ?? "{}",
50
- );
48
+ const manifest = JSON.parse(process.env.MIRIN_MANIFEST_JSON ?? readManifestFromBundle() ?? "{}");
51
49
 
52
50
  const coreConfig = JSON.parse(
53
51
  process.env.MIRIN_CONFIG_JSON ??
54
- JSON.stringify(
55
- process.env.MIRIN_DEV_URL ? { dev: true } : { resources_path: resourcesDir },
56
- ),
52
+ JSON.stringify(process.env.MIRIN_DEV_URL ? { dev: true } : { resources_path: resourcesDir }),
57
53
  );
58
54
  // Opt out of single-instance (the core default) when the app allows multiple.
59
55
  if (manifest.singleInstance === false) coreConfig.single_instance = false;
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * `globalShortcut`, and `logger` features (docs/api-design.md).
8
8
  */
9
9
 
10
- import { app, wireAppEvents } from "./app.ts";
10
+ import { app, wireAppEvents } from "./app/index.ts";
11
11
  import { boot } from "./runtime.ts";
12
12
 
13
13
  // Side-effect imports: each feature subscribes its native-event handlers.
@@ -16,48 +16,50 @@ import "./tray.ts";
16
16
  import "./dialog.ts";
17
17
  import "./shortcut.ts";
18
18
 
19
- export { app } from "./app.ts";
20
- export { menu } from "./menu.ts";
21
- export { Tray } from "./tray.ts";
22
- export { dialog } from "./dialog.ts";
23
- export { clipboard } from "./clipboard.ts";
24
- export { globalShortcut } from "./shortcut.ts";
25
- export { logger, setLogLevel, getLogLevel, Logger } from "./logger.ts";
26
- export { updater, Updater } from "./updater.ts";
27
- export { sidecar } from "./sidecar.ts";
28
- export { NotAttachedError, resolveWorker } from "./runtime.ts";
29
-
30
19
  export type {
20
+ AppEvents,
21
+ BroadcastEmitters,
22
+ Dock,
23
+ ServeHandle,
31
24
  WindowEvents,
32
- WindowMaterialInfo,
33
25
  WindowFrame,
34
- AppEvents,
35
26
  WindowHandle,
27
+ WindowMaterialInfo,
36
28
  WindowOpenOptions,
37
- ServeHandle,
38
- BroadcastEmitters,
39
- Dock,
40
- } from "./app.ts";
41
- export type { MenuItemTemplate, MenuRole } from "./menu.ts";
42
- export type { LogLevel } from "./logger.ts";
43
- export type { UpdaterStatus, UpdateInfo, UpdateProgress, UpdaterEvents } from "./updater.ts";
44
- export type { SidecarOptions, SidecarProcess } from "./sidecar.ts";
45
- export type { TrayOptions } from "./tray.ts";
46
- export type { OpenDialogOptions, SaveDialogOptions, MessageDialogOptions } from "./dialog.ts";
29
+ } from "./app/index.ts";
30
+ export { app } from "./app/index.ts";
31
+ export { clipboard } from "./clipboard.ts";
47
32
  export type {
48
- MirinConfig,
49
- WindowConfig,
50
- WindowMaterial,
51
- WindowMaterialOptions,
52
- ReleaseConfig,
33
+ CefConfig,
53
34
  DmgConfig,
54
- NsisConfig,
55
35
  InnoConfig,
56
36
  LinuxConfig,
57
37
  LinuxPackageFormat,
58
- SidecarSpec,
38
+ MirinConfig,
39
+ NsisConfig,
40
+ ReleaseConfig,
59
41
  SidecarEntitlement,
60
- } from "./config.ts";
42
+ SidecarSpec,
43
+ WindowConfig,
44
+ WindowMaterial,
45
+ WindowMaterialOptions,
46
+ } from "./config/index.ts";
47
+ export type { MessageDialogOptions, OpenDialogOptions, SaveDialogOptions } from "./dialog.ts";
48
+ export { dialog } from "./dialog.ts";
49
+ export type { LogLevel } from "./logger.ts";
50
+ export { getLogLevel, Logger, logger, setLogLevel } from "./logger.ts";
51
+ export type { MenuItemTemplate, MenuRole } from "./menu.ts";
52
+ export { menu } from "./menu.ts";
53
+ export type { NotificationOptions } from "./notification.ts";
54
+ export { notification } from "./notification.ts";
55
+ export { NotAttachedError, resolveWorker } from "./runtime.ts";
56
+ export { globalShortcut } from "./shortcut.ts";
57
+ export type { SidecarOptions, SidecarProcess } from "./sidecar.ts";
58
+ export { resolveSidecar, sidecar } from "./sidecar.ts";
59
+ export type { TrayOptions } from "./tray.ts";
60
+ export { Tray } from "./tray.ts";
61
+ export type { UpdateInfo, UpdateProgress, UpdaterEvents, UpdaterStatus } from "./updater/index.ts";
62
+ export { Updater, updater } from "./updater/index.ts";
61
63
 
62
64
  wireAppEvents();
63
65
  boot();
package/src/logger.ts CHANGED
@@ -53,12 +53,16 @@ export function getLogLevel(): LogLevel {
53
53
  }
54
54
 
55
55
  export class Logger {
56
+ readonly #scope?: string;
57
+
56
58
  /** @param scope optional dotted scope shown as `[mirin:scope]`. */
57
- constructor(private readonly scope?: string) {}
59
+ constructor(scope?: string) {
60
+ this.#scope = scope;
61
+ }
58
62
 
59
63
  /** Derive a scoped child logger (e.g. `logger.child("db")`). */
60
64
  child(scope: string): Logger {
61
- return new Logger(this.scope ? `${this.scope}:${scope}` : scope);
65
+ return new Logger(this.#scope ? `${this.#scope}:${scope}` : scope);
62
66
  }
63
67
 
64
68
  /** Set the global level (same as the exported `setLogLevel`). */
@@ -85,7 +89,7 @@ export class Logger {
85
89
 
86
90
  #emit(level: Exclude<LogLevel, "silent">, args: unknown[]): void {
87
91
  if (ORDER[level] < ORDER[currentLevel]) return;
88
- const name = this.scope ? `[mirin:${this.scope}]` : "[mirin]";
92
+ const name = this.#scope ? `[mirin:${this.#scope}]` : "[mirin]";
89
93
  const prefix = useColor ? `${COLOR.dim}${name}${COLOR.reset}` : name;
90
94
  const tag = useColor ? `${COLOR[level]}${level}${COLOR.reset}` : level;
91
95
  const sink = level === "warn" || level === "error" ? console.error : console.log;
package/src/menu.ts CHANGED
@@ -4,12 +4,25 @@
4
4
  * carrying that id. `role` items map to AppKit's standard actions.
5
5
  */
6
6
 
7
- import { runtime, onNativeEvent } from "./runtime.ts";
7
+ import { onNativeEvent, runtime } from "./runtime.ts";
8
8
 
9
9
  export type MenuRole =
10
- | "quit" | "close" | "minimize" | "zoom" | "front" | "togglefullscreen"
11
- | "hide" | "hideothers" | "unhide"
12
- | "undo" | "redo" | "cut" | "copy" | "paste" | "selectall" | "delete";
10
+ | "quit"
11
+ | "close"
12
+ | "minimize"
13
+ | "zoom"
14
+ | "front"
15
+ | "togglefullscreen"
16
+ | "hide"
17
+ | "hideothers"
18
+ | "unhide"
19
+ | "undo"
20
+ | "redo"
21
+ | "cut"
22
+ | "copy"
23
+ | "paste"
24
+ | "selectall"
25
+ | "delete";
13
26
 
14
27
  export interface MenuItemTemplate {
15
28
  label?: string;
package/src/native.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * process, so the Rust statics behind these symbols are shared.
7
7
  */
8
8
 
9
- import { dlopen, FFIType, ptr, CString, type Pointer } from "bun:ffi";
9
+ import { CString, dlopen, FFIType, type Pointer, ptr } from "bun:ffi";
10
10
 
11
11
  function nullTerminated(s: string): Uint8Array {
12
12
  return new TextEncoder().encode(s + "\0");
@@ -23,8 +23,14 @@ const symbols = {
23
23
  mirin_window_set_title: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.void },
24
24
  mirin_window_control: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.void },
25
25
  mirin_window_set_material: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.void },
26
- mirin_window_set_position: { args: [FFIType.u32, FFIType.f64, FFIType.f64], returns: FFIType.void },
27
- mirin_window_maybe_start_drag: { args: [FFIType.u32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32], returns: FFIType.void },
26
+ mirin_window_set_position: {
27
+ args: [FFIType.u32, FFIType.f64, FFIType.f64],
28
+ returns: FFIType.void,
29
+ },
30
+ mirin_window_maybe_start_drag: {
31
+ args: [FFIType.u32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32],
32
+ returns: FFIType.void,
33
+ },
28
34
  mirin_app_quit: { args: [], returns: FFIType.void },
29
35
  mirin_app_set_dock_visible: { args: [FFIType.i32], returns: FFIType.void },
30
36
  mirin_set_app_menu: { args: [FFIType.ptr], returns: FFIType.void },
@@ -36,6 +42,7 @@ const symbols = {
36
42
  mirin_clipboard_read_text: { args: [], returns: FFIType.ptr },
37
43
  mirin_clipboard_write_text: { args: [FFIType.ptr], returns: FFIType.void },
38
44
  mirin_dialog_show: { args: [FFIType.ptr], returns: FFIType.void },
45
+ mirin_notification_show: { args: [FFIType.ptr], returns: FFIType.i32 },
39
46
  } as const;
40
47
 
41
48
  export type EventListener = (event: string) => void;
@@ -168,4 +175,9 @@ export class Core {
168
175
  const buf = nullTerminated(specJson);
169
176
  this.#lib.symbols.mirin_dialog_show(ptr(buf));
170
177
  }
178
+
179
+ notificationShow(specJson: string): boolean {
180
+ const buf = nullTerminated(specJson);
181
+ return this.#lib.symbols.mirin_notification_show(ptr(buf)) === 1;
182
+ }
171
183
  }
@@ -0,0 +1,19 @@
1
+ import { runtime } from "./runtime.ts";
2
+
3
+ export interface NotificationOptions {
4
+ title: string;
5
+ body?: string;
6
+ }
7
+
8
+ export const notification = {
9
+ /** Ask the operating system to display a desktop notification. */
10
+ show(options: NotificationOptions): boolean {
11
+ const title = options.title.trim();
12
+ if (!title || title.length > 128) return false;
13
+ if (options.body && options.body.length > 512) return false;
14
+
15
+ return runtime().core.notificationShow(
16
+ JSON.stringify({ ...options, title, appName: runtime().id }),
17
+ );
18
+ },
19
+ };