mirinjs 0.0.1-alpha.0 → 0.0.1-alpha.10
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 +1 -1
- package/src/app.ts +70 -1
- package/src/config.ts +16 -0
- package/src/host.ts +9 -2
- package/src/index.ts +6 -2
- package/src/logger.ts +97 -0
- package/src/native.ts +10 -0
- package/src/runtime.ts +7 -2
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -32,6 +32,14 @@ export type AppEvents = {
|
|
|
32
32
|
"window-all-closed": void;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
/** A window's frame in screen points (bottom-left origin, like AppKit). */
|
|
36
|
+
export interface WindowFrame {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
type Listener<P> = (payload: P) => void;
|
|
36
44
|
|
|
37
45
|
class Emitter<Events extends Record<string, unknown>> {
|
|
@@ -127,6 +135,31 @@ export class WindowHandle extends Emitter<WindowEvents> {
|
|
|
127
135
|
runtime().core.windowSetMaterial(this.id, JSON.stringify(normalizeMaterial(material)));
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
/** Move the window's bottom-left origin to screen point (x, y), in points. */
|
|
139
|
+
setPosition(x: number, y: number): void {
|
|
140
|
+
runtime().core.windowSetPosition(this.id, x, y);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The latest known window frame (screen points, bottom-left origin). Tracked
|
|
144
|
+
* from `moved`/`resized` events, so it's current without a round-trip. */
|
|
145
|
+
getFrame(): WindowFrame {
|
|
146
|
+
return this.#frame ?? { x: 0, y: 0, width: 0, height: 0 };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Whether the window is currently zoomed (maximized). */
|
|
150
|
+
isMaximized(): boolean {
|
|
151
|
+
return this.#maximized;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#frame: WindowFrame | null = null;
|
|
155
|
+
#maximized = false;
|
|
156
|
+
|
|
157
|
+
/** @internal — fed from native window frame events. */
|
|
158
|
+
_setFrame(frame: WindowFrame, maximized: boolean): void {
|
|
159
|
+
this.#frame = frame;
|
|
160
|
+
this.#maximized = maximized;
|
|
161
|
+
}
|
|
162
|
+
|
|
130
163
|
#control(verb: string): void {
|
|
131
164
|
runtime().core.windowControl(this.id, verb);
|
|
132
165
|
}
|
|
@@ -235,9 +268,37 @@ function normalizeMaterial(
|
|
|
235
268
|
return typeof material === "string" ? { type: material } : material;
|
|
236
269
|
}
|
|
237
270
|
|
|
271
|
+
/** macOS Dock-icon controls (no-ops off macOS). */
|
|
272
|
+
export interface Dock {
|
|
273
|
+
/** Hide the Dock icon and menu-bar presence (agent/accessory app). */
|
|
274
|
+
hide(): void;
|
|
275
|
+
/** Restore the Dock icon and menu bar. */
|
|
276
|
+
show(): void;
|
|
277
|
+
}
|
|
278
|
+
|
|
238
279
|
class MirinApp extends Emitter<AppEvents> {
|
|
239
280
|
readonly windows = new Windows();
|
|
240
281
|
|
|
282
|
+
/** macOS Dock-icon controls. Hiding suits resident, hotkey-summoned apps. */
|
|
283
|
+
readonly dock: Dock = {
|
|
284
|
+
hide: () => this.#setDock(false),
|
|
285
|
+
show: () => this.#setDock(true),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/** Apply the Dock policy now if the core is up, else once it's ready. The
|
|
289
|
+
* native command needs the CEF UI thread, which only runs after `ready`. */
|
|
290
|
+
#setDock(visible: boolean): void {
|
|
291
|
+
const apply = () => runtime().core.appSetDockVisible(visible);
|
|
292
|
+
if (runtime().core.isReady()) {
|
|
293
|
+
apply();
|
|
294
|
+
} else {
|
|
295
|
+
const off = this.on("ready", () => {
|
|
296
|
+
off();
|
|
297
|
+
apply();
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
241
302
|
serve<R extends Router<any>>(router: R): ServeHandle<R> {
|
|
242
303
|
runtime().rpc.setRouter(router);
|
|
243
304
|
return { rpc: this.rpc as BroadcastEmitters<R> };
|
|
@@ -294,7 +355,15 @@ export function wireAppEvents(): void {
|
|
|
294
355
|
for (const kind of WINDOW_EVENTS) {
|
|
295
356
|
onNativeEvent(`window.${kind}`, (event: NativeEvent) => {
|
|
296
357
|
const id = event.id as number | undefined;
|
|
297
|
-
if (id
|
|
358
|
+
if (id == null) return;
|
|
359
|
+
const handle = app.windows._byId(id);
|
|
360
|
+
if (!handle) return;
|
|
361
|
+
// moved/resized carry the current frame + maximized state; track them so
|
|
362
|
+
// getFrame()/isMaximized() are answerable without a round-trip.
|
|
363
|
+
if (event.frame) {
|
|
364
|
+
handle._setFrame(event.frame as WindowFrame, Boolean(event.maximized));
|
|
365
|
+
}
|
|
366
|
+
handle._emit(kind, undefined);
|
|
298
367
|
});
|
|
299
368
|
}
|
|
300
369
|
}
|
package/src/config.ts
CHANGED
|
@@ -42,12 +42,22 @@ export interface WindowConfig {
|
|
|
42
42
|
title?: string;
|
|
43
43
|
width?: number;
|
|
44
44
|
height?: number;
|
|
45
|
+
/** Screen position (bottom-left origin, points). Centered when absent. */
|
|
46
|
+
x?: number;
|
|
47
|
+
y?: number;
|
|
45
48
|
/** "ready" (default) shows on first paint to avoid a white flash. */
|
|
46
49
|
show?: "ready" | "immediately";
|
|
47
50
|
/** "auto" (default) opens at launch; "manual" windows are templates for app.windows.open(name). */
|
|
48
51
|
open?: "auto" | "manual";
|
|
49
52
|
/** Custom title bar: hide it (content fills) or inset the traffic lights. */
|
|
50
53
|
titleBarStyle?: "hidden" | "hiddenInset";
|
|
54
|
+
/**
|
|
55
|
+
* Reposition the macOS traffic-light buttons for a custom title bar.
|
|
56
|
+
* `x` insets the leftmost button from the left edge; `y` sets the effective
|
|
57
|
+
* title-bar height the buttons are vertically centered in (so `y` ≈ your CSS
|
|
58
|
+
* title-bar height minus the button height). Re-applied automatically on resize.
|
|
59
|
+
*/
|
|
60
|
+
trafficLightPosition?: { x: number; y: number };
|
|
51
61
|
/** Non-opaque window (for transparent/blurred UIs). */
|
|
52
62
|
transparent?: boolean;
|
|
53
63
|
/**
|
|
@@ -70,6 +80,12 @@ export interface MirinConfig {
|
|
|
70
80
|
name: string;
|
|
71
81
|
/** Main-process entry, relative to the project root (runs in the Bun Worker). */
|
|
72
82
|
main: string;
|
|
83
|
+
/**
|
|
84
|
+
* App icon, relative to the project root (macOS). Accepts a `.icns`, a
|
|
85
|
+
* `.iconset` directory, or a single square `.png` (≥512px) that mirin renders
|
|
86
|
+
* into an `.icns`. Embedded in the bundle for the Dock and Finder.
|
|
87
|
+
*/
|
|
88
|
+
icon?: string;
|
|
73
89
|
windows: Record<string, WindowConfig>;
|
|
74
90
|
}
|
|
75
91
|
|
package/src/host.ts
CHANGED
|
@@ -36,16 +36,23 @@ const manifest = JSON.parse(
|
|
|
36
36
|
|
|
37
37
|
const coreConfig = JSON.parse(
|
|
38
38
|
process.env.MIRIN_CONFIG_JSON ??
|
|
39
|
-
JSON.stringify(
|
|
39
|
+
JSON.stringify(
|
|
40
|
+
process.env.MIRIN_DEV_URL ? { dev: true } : { resources_path: resourcesDir },
|
|
41
|
+
),
|
|
40
42
|
);
|
|
41
43
|
|
|
44
|
+
// Load the native core on the main thread FIRST. The Worker also dlopens the
|
|
45
|
+
// same dylib in its boot; doing the main-thread dlopen before spawning the
|
|
46
|
+
// Worker serializes the first-time load instead of racing two concurrent
|
|
47
|
+
// dlopens across threads.
|
|
48
|
+
const core = new Core(corePath);
|
|
49
|
+
|
|
42
50
|
const worker = new Worker(workerPath, {
|
|
43
51
|
workerData: { corePath, manifest, devUrl: process.env.MIRIN_DEV_URL },
|
|
44
52
|
});
|
|
45
53
|
worker.on("error", (err) => console.error("[mirin worker]", err));
|
|
46
54
|
|
|
47
55
|
// Hand the main thread to CEF. Blocks in the message loop until the app quits.
|
|
48
|
-
const core = new Core(corePath);
|
|
49
56
|
const exitCode = core.run(JSON.stringify(coreConfig));
|
|
50
57
|
|
|
51
58
|
void worker.terminate();
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Importing this module boots the runtime (loads libmirin_core, starts the RPC
|
|
5
5
|
* server, begins draining native events) and exposes the developer-facing API:
|
|
6
|
-
* the `app` singleton plus the `menu`, `Tray`, `dialog`, `clipboard`,
|
|
7
|
-
* `globalShortcut` features (docs/api-design.md).
|
|
6
|
+
* the `app` singleton plus the `menu`, `Tray`, `dialog`, `clipboard`,
|
|
7
|
+
* `globalShortcut`, and `logger` features (docs/api-design.md).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { app, wireAppEvents } from "./app.ts";
|
|
@@ -22,18 +22,22 @@ export { Tray } from "./tray.ts";
|
|
|
22
22
|
export { dialog } from "./dialog.ts";
|
|
23
23
|
export { clipboard } from "./clipboard.ts";
|
|
24
24
|
export { globalShortcut } from "./shortcut.ts";
|
|
25
|
+
export { logger, setLogLevel, getLogLevel, Logger } from "./logger.ts";
|
|
25
26
|
export { NotAttachedError } from "./runtime.ts";
|
|
26
27
|
|
|
27
28
|
export type {
|
|
28
29
|
WindowEvents,
|
|
29
30
|
WindowMaterialInfo,
|
|
31
|
+
WindowFrame,
|
|
30
32
|
AppEvents,
|
|
31
33
|
WindowHandle,
|
|
32
34
|
WindowOpenOptions,
|
|
33
35
|
ServeHandle,
|
|
34
36
|
BroadcastEmitters,
|
|
37
|
+
Dock,
|
|
35
38
|
} from "./app.ts";
|
|
36
39
|
export type { MenuItemTemplate, MenuRole } from "./menu.ts";
|
|
40
|
+
export type { LogLevel } from "./logger.ts";
|
|
37
41
|
export type { TrayOptions } from "./tray.ts";
|
|
38
42
|
export type { OpenDialogOptions, SaveDialogOptions, MessageDialogOptions } from "./dialog.ts";
|
|
39
43
|
export type {
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mirin/logger — a small leveled logger for the Bun main process.
|
|
3
|
+
*
|
|
4
|
+
* Exported from `mirinjs` so app code and mirin itself log consistently. The
|
|
5
|
+
* level defaults from the `MIRIN_LOG` env var (`debug` | `info` | `warn` |
|
|
6
|
+
* `error` | `silent`), falling back to `info`; change it at runtime with
|
|
7
|
+
* `logger.setLevel(...)`. `warn`/`error` go to stderr, `debug`/`info` to stdout.
|
|
8
|
+
*
|
|
9
|
+
* import { logger } from "mirinjs";
|
|
10
|
+
* logger.info("server listening", port);
|
|
11
|
+
* const db = logger.child("db");
|
|
12
|
+
* db.debug("query", sql); // → [mirin:db] debug query …
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
|
|
16
|
+
|
|
17
|
+
const ORDER: Record<LogLevel, number> = {
|
|
18
|
+
debug: 10,
|
|
19
|
+
info: 20,
|
|
20
|
+
warn: 30,
|
|
21
|
+
error: 40,
|
|
22
|
+
silent: 100,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const COLOR = {
|
|
26
|
+
debug: "\x1b[2m", // dim
|
|
27
|
+
info: "\x1b[36m", // cyan
|
|
28
|
+
warn: "\x1b[33m", // yellow
|
|
29
|
+
error: "\x1b[31m", // red
|
|
30
|
+
dim: "\x1b[2m",
|
|
31
|
+
reset: "\x1b[0m",
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const LEVELS: LogLevel[] = ["debug", "info", "warn", "error", "silent"];
|
|
35
|
+
|
|
36
|
+
function envLevel(): LogLevel {
|
|
37
|
+
const v = (process.env.MIRIN_LOG ?? "").toLowerCase() as LogLevel;
|
|
38
|
+
return LEVELS.includes(v) ? v : "info";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Shared across every logger instance so `setLevel` is global.
|
|
42
|
+
let currentLevel: LogLevel = envLevel();
|
|
43
|
+
const useColor = Boolean(process.stderr.isTTY) && process.env.NO_COLOR == null;
|
|
44
|
+
|
|
45
|
+
/** Set the global minimum level. Messages below it are dropped. */
|
|
46
|
+
export function setLogLevel(level: LogLevel): void {
|
|
47
|
+
currentLevel = level;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The current global log level. */
|
|
51
|
+
export function getLogLevel(): LogLevel {
|
|
52
|
+
return currentLevel;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class Logger {
|
|
56
|
+
/** @param scope optional dotted scope shown as `[mirin:scope]`. */
|
|
57
|
+
constructor(private readonly scope?: string) {}
|
|
58
|
+
|
|
59
|
+
/** Derive a scoped child logger (e.g. `logger.child("db")`). */
|
|
60
|
+
child(scope: string): Logger {
|
|
61
|
+
return new Logger(this.scope ? `${this.scope}:${scope}` : scope);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Set the global level (same as the exported `setLogLevel`). */
|
|
65
|
+
setLevel(level: LogLevel): void {
|
|
66
|
+
setLogLevel(level);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get level(): LogLevel {
|
|
70
|
+
return currentLevel;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
debug(...args: unknown[]): void {
|
|
74
|
+
this.#emit("debug", args);
|
|
75
|
+
}
|
|
76
|
+
info(...args: unknown[]): void {
|
|
77
|
+
this.#emit("info", args);
|
|
78
|
+
}
|
|
79
|
+
warn(...args: unknown[]): void {
|
|
80
|
+
this.#emit("warn", args);
|
|
81
|
+
}
|
|
82
|
+
error(...args: unknown[]): void {
|
|
83
|
+
this.#emit("error", args);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#emit(level: Exclude<LogLevel, "silent">, args: unknown[]): void {
|
|
87
|
+
if (ORDER[level] < ORDER[currentLevel]) return;
|
|
88
|
+
const name = this.scope ? `[mirin:${this.scope}]` : "[mirin]";
|
|
89
|
+
const prefix = useColor ? `${COLOR.dim}${name}${COLOR.reset}` : name;
|
|
90
|
+
const tag = useColor ? `${COLOR[level]}${level}${COLOR.reset}` : level;
|
|
91
|
+
const sink = level === "warn" || level === "error" ? console.error : console.log;
|
|
92
|
+
sink(`${prefix} ${tag}`, ...args);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The default mirin logger. Import and use directly, or derive children. */
|
|
97
|
+
export const logger = new Logger();
|
package/src/native.ts
CHANGED
|
@@ -23,7 +23,9 @@ 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 },
|
|
26
27
|
mirin_app_quit: { args: [], returns: FFIType.void },
|
|
28
|
+
mirin_app_set_dock_visible: { args: [FFIType.i32], returns: FFIType.void },
|
|
27
29
|
mirin_set_app_menu: { args: [FFIType.ptr], returns: FFIType.void },
|
|
28
30
|
mirin_popup_menu: { args: [FFIType.ptr], returns: FFIType.void },
|
|
29
31
|
mirin_tray_create: { args: [FFIType.ptr], returns: FFIType.void },
|
|
@@ -107,10 +109,18 @@ export class Core {
|
|
|
107
109
|
this.#lib.symbols.mirin_window_set_material(id, ptr(buf));
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
windowSetPosition(id: number, x: number, y: number): void {
|
|
113
|
+
this.#lib.symbols.mirin_window_set_position(id, x, y);
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
quit(): void {
|
|
111
117
|
this.#lib.symbols.mirin_app_quit();
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
appSetDockVisible(visible: boolean): void {
|
|
121
|
+
this.#lib.symbols.mirin_app_set_dock_visible(visible ? 1 : 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
setAppMenu(templateJson: string): void {
|
|
115
125
|
const buf = nullTerminated(templateJson);
|
|
116
126
|
this.#lib.symbols.mirin_set_app_menu(ptr(buf));
|
package/src/runtime.ts
CHANGED
|
@@ -35,9 +35,14 @@ export function runtime(): Runtime {
|
|
|
35
35
|
return current;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** Dev: every window loads the Vite server. Production: its manifest app:// URL.
|
|
38
|
+
/** Dev: every window loads the Vite server. Production: its manifest app:// URL.
|
|
39
|
+
* Any query/hash on the requested URL (e.g. "#devtools") is preserved so
|
|
40
|
+
* hash-routed sub-windows reach the right view through the dev server too. */
|
|
39
41
|
export function resolveUrl(url: string): string {
|
|
40
|
-
|
|
42
|
+
const devUrl = current?.devUrl;
|
|
43
|
+
if (!devUrl) return url;
|
|
44
|
+
const suffix = url.match(/[?#].*$/)?.[0] ?? "";
|
|
45
|
+
return devUrl + suffix;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
// ---- native event dispatch ----
|