@n1h/sweetview 0.0.0-stage → 0.1.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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE-0BSD +12 -0
  3. package/README.md +268 -3
  4. package/docs/README.md +26 -0
  5. package/docs/app.md +523 -0
  6. package/docs/build.md +169 -0
  7. package/docs/ipc-ws.md +202 -0
  8. package/docs/ipc.md +323 -0
  9. package/docs/native.md +1066 -0
  10. package/docs/page.md +71 -0
  11. package/docs/settings-store.md +125 -0
  12. package/docs/webview-library.md +104 -0
  13. package/docs/window-chrome.md +221 -0
  14. package/docs/worker.md +72 -0
  15. package/file-imports.d.ts +19 -0
  16. package/package.json +66 -4
  17. package/src/app.ts +350 -0
  18. package/src/build.ts +118 -0
  19. package/src/html.ts +71 -0
  20. package/src/index.ts +8 -0
  21. package/src/ipc/bridge.ts +41 -0
  22. package/src/ipc/frame.ts +16 -0
  23. package/src/ipc/index.ts +16 -0
  24. package/src/ipc/memory.ts +33 -0
  25. package/src/ipc/service.ts +75 -0
  26. package/src/ipc-ws/client.ts +111 -0
  27. package/src/ipc-ws/index.ts +5 -0
  28. package/src/ipc-ws/server.ts +133 -0
  29. package/src/make-app.ts +93 -0
  30. package/src/messages.ts +16 -0
  31. package/src/native/app-icon.ts +23 -0
  32. package/src/native/app-menu.ts +18 -0
  33. package/src/native/darwin/app-icon.ts +10 -0
  34. package/src/native/darwin/app-menu.ts +88 -0
  35. package/src/native/darwin/folder-picker.ts +29 -0
  36. package/src/native/darwin/image.ts +47 -0
  37. package/src/native/darwin/menu.ts +89 -0
  38. package/src/native/darwin/notifier.ts +95 -0
  39. package/src/native/darwin/objc.ts +246 -0
  40. package/src/native/darwin/overlay.ts +352 -0
  41. package/src/native/darwin/tray.ts +129 -0
  42. package/src/native/darwin/window.ts +64 -0
  43. package/src/native/folder-picker.ts +15 -0
  44. package/src/native/hit-regions.ts +162 -0
  45. package/src/native/icon.ts +530 -0
  46. package/src/native/index.ts +38 -0
  47. package/src/native/menu.ts +43 -0
  48. package/src/native/notifications.ts +40 -0
  49. package/src/native/overlay.ts +182 -0
  50. package/src/native/platform.ts +25 -0
  51. package/src/native/tray.ts +41 -0
  52. package/src/native/win32/api.ts +234 -0
  53. package/src/native/win32/app-icon.ts +5 -0
  54. package/src/native/win32/app-menu.ts +4 -0
  55. package/src/native/win32/folder-picker.ts +121 -0
  56. package/src/native/win32/icon.ts +85 -0
  57. package/src/native/win32/menu.ts +107 -0
  58. package/src/native/win32/message-window.ts +72 -0
  59. package/src/native/win32/notifier.ts +28 -0
  60. package/src/native/win32/overlay.ts +499 -0
  61. package/src/native/win32/tray.ts +228 -0
  62. package/src/native/win32/window.ts +77 -0
  63. package/src/native/window.ts +40 -0
  64. package/src/page.ts +48 -0
  65. package/src/ring.ts +110 -0
  66. package/src/settings-store.ts +101 -0
  67. package/src/webview-lib.ts +60 -0
  68. package/src/window-chrome.ts +118 -0
  69. package/src/worker.ts +72 -0
@@ -0,0 +1,47 @@
1
+ // NSImage from the macOS half of an Icon: an SF Symbol by name, else PNG
2
+ // bytes. The one place the tray, menus, windows and the Dock icon get their
3
+ // images, so sizing and the template flag are decided the same way everywhere.
4
+
5
+ import type { Icon } from "../icon";
6
+ import { NIL, type Id, allocRaw, asId, cls, nsString, release, sel, sendBool, sendBytesLen, sendPtr, sendPtrPtr, sendSize } from "./objc";
7
+
8
+ export type MadeImage = {
9
+ img: Id;
10
+ /** We hold a +1 to release once the receiver has retained it. */
11
+ owned: boolean;
12
+ };
13
+
14
+ /**
15
+ * `points`: the size the image should report (18 for the menu bar, 16 for
16
+ * menu items); omitted leaves the PNG's own size, which is what the Dock
17
+ * wants. A PNG with 2x pixels renders crisp on Retina once its size is set
18
+ * in points. `templateDefault`: what `template` means when unset.
19
+ */
20
+ export function makeImage(icon: Icon["macos"] | undefined, points?: number, templateDefault = true): MadeImage {
21
+ const template = icon?.template ?? templateDefault;
22
+ if (icon?.symbol) {
23
+ const img = asId(sendPtrPtr(cls("NSImage"), sel("imageWithSystemSymbolName:accessibilityDescription:"), nsString(icon.symbol), NIL));
24
+ if (img !== NIL) {
25
+ sendBool(img, sel("setTemplate:"), template);
26
+ return { img, owned: false }; // +0 (autoreleased)
27
+ }
28
+ console.error(`sweetnative: no SF Symbol named "${icon.symbol}"${icon.png ? "; using the PNG" : ""}`);
29
+ }
30
+ if (icon?.png) {
31
+ const data = asId(sendBytesLen(cls("NSData"), sel("dataWithBytes:length:"), icon.png, BigInt(icon.png.length)));
32
+ const img = asId(sendPtr(allocRaw("NSImage"), sel("initWithData:"), data));
33
+ if (img !== NIL) {
34
+ sendBool(img, sel("setTemplate:"), template);
35
+ if (points !== undefined) sendSize(img, sel("setSize:"), points, points);
36
+ return { img, owned: true }; // +1 from alloc/init
37
+ }
38
+ console.error("sweetnative: NSImage could not decode the PNG bytes");
39
+ }
40
+ return { img: NIL, owned: false };
41
+ }
42
+
43
+ /** Sends `setter` (setImage:, setApplicationIconImage:, ...) with the made image, then drops our reference. */
44
+ export function applyImage(receiver: Id, setter: string, made: MadeImage): void {
45
+ sendPtr(receiver, sel(setter), made.img); // the receiver retains it
46
+ if (made.owned) release(made.img);
47
+ }
@@ -0,0 +1,89 @@
1
+ // NSMenu from a MenuTemplate. Every item targets one shared NSObject whose
2
+ // `menuAction:` looks the sender up in a map, so item handlers never need
3
+ // their own Objective-C class.
4
+
5
+ import { isSeparator, isSubmenu, type MenuTemplate } from "../menu";
6
+ import { applyImage, makeImage } from "./image";
7
+ import {
8
+ NIL,
9
+ type Id,
10
+ alloc,
11
+ allocRaw,
12
+ asId,
13
+ cls,
14
+ makeTarget,
15
+ nsString,
16
+ release,
17
+ sel,
18
+ send,
19
+ sendBool,
20
+ sendIntArg,
21
+ sendIntRetBool,
22
+ sendPtr,
23
+ sendPtrSelPtr,
24
+ } from "./objc";
25
+
26
+ const NSControlStateValueOn = 1n;
27
+ /** Menu item images are 16x16 points. */
28
+ const MENU_ITEM_POINTS = 16;
29
+ // macOS 27 hides menu item images by default (NSMenuItemImageVisibilityAutomatic = 0):
30
+ // symbol images for apps linked on the 26 SDK, every image on the 27 SDK. An
31
+ // item keeps its image only by asking for it. The selector is new in 27, so
32
+ // it is sent only where it exists.
33
+ const NSMenuItemImageVisibilityVisible = 1n;
34
+
35
+ /** NSMenuItem pointer -> click handler, for the menus currently alive. */
36
+ const handlers = new Map<Id, () => void>();
37
+
38
+ let target: Id | undefined;
39
+ const menuTarget = () => (target ??= makeTarget("NativeShellMenuTarget", { "menuAction:": (sender) => handlers.get(sender)?.() }));
40
+
41
+ export type BuiltMenu = { readonly menu: Id; readonly dispose: () => void };
42
+
43
+ /** Builds an NSMenu (+1, retained for the caller) from `template`. */
44
+ export function buildMenu(template: MenuTemplate): BuiltMenu {
45
+ const owned: Id[] = [];
46
+ const build = (items: MenuTemplate): Id => {
47
+ const menu = alloc("NSMenu");
48
+ sendBool(menu, sel("setAutoenablesItems:"), false);
49
+ for (const item of items) {
50
+ if (isSeparator(item)) {
51
+ sendPtr(menu, sel("addItem:"), asId(send(cls("NSMenuItem"), sel("separatorItem"))));
52
+ continue;
53
+ }
54
+ const it = allocRaw("NSMenuItem");
55
+ const inited = asId(sendPtrSelPtr(it, sel("initWithTitle:action:keyEquivalent:"), nsString(item.label), NIL, nsString("")));
56
+ sendBool(inited, sel("setEnabled:"), item.enabled !== false);
57
+ if (item.icon?.macos) {
58
+ const made = makeImage(item.icon.macos, MENU_ITEM_POINTS);
59
+ if (made.img === NIL) console.error(`sweetnative: menu item "${item.label}" has no usable macOS image`);
60
+ applyImage(inited, "setImage:", made);
61
+ if (sendIntRetBool(inited, sel("respondsToSelector:"), sel("setPreferredImageVisibility:"))) {
62
+ sendIntArg(inited, sel("setPreferredImageVisibility:"), NSMenuItemImageVisibilityVisible);
63
+ }
64
+ }
65
+ if (isSubmenu(item)) {
66
+ const sub = build(item.submenu);
67
+ sendPtr(inited, sel("setSubmenu:"), sub);
68
+ release(sub); // the item retains it
69
+ } else {
70
+ sendPtr(inited, sel("setTarget:"), menuTarget());
71
+ sendPtr(inited, sel("setAction:"), sel("menuAction:"));
72
+ sendIntArg(inited, sel("setState:"), item.checked ? NSControlStateValueOn : 0n);
73
+ handlers.set(inited, item.click ?? (() => {}));
74
+ owned.push(inited);
75
+ }
76
+ sendPtr(menu, sel("addItem:"), inited);
77
+ release(inited); // the menu retains it
78
+ }
79
+ return menu;
80
+ };
81
+ const menu = build(template);
82
+ return {
83
+ menu,
84
+ dispose() {
85
+ for (const id of owned) handlers.delete(id);
86
+ release(menu);
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,95 @@
1
+ // macOS notifications through UNUserNotificationCenter. It needs a bundle
2
+ // identifier, so this works from the .app and silently does nothing from the
3
+ // bare binary (`bun run dev`): currentNotificationCenter would raise there.
4
+ //
5
+ // The first notify() asks for permission (system prompt, once per bundle id).
6
+ // A delegate makes banners show while the app is frontmost and routes clicks.
7
+
8
+ import { FFIType } from "bun:ffi";
9
+ import type { Notifier, NotifierOptions } from "../notifications";
10
+ import {
11
+ NIL,
12
+ type Id,
13
+ allocRaw,
14
+ asId,
15
+ cls,
16
+ invokeBlock,
17
+ makeBlock,
18
+ makeTarget,
19
+ nsString,
20
+ release,
21
+ sel,
22
+ send,
23
+ sendPtr,
24
+ sendPtrPtr,
25
+ sendPtrPtrPtr,
26
+ sendUIntPtrVoid,
27
+ } from "./objc";
28
+
29
+ const UNAuthorizationOptionBadge = 1n;
30
+ const UNAuthorizationOptionSound = 2n;
31
+ const UNAuthorizationOptionAlert = 4n;
32
+ // UNNotificationPresentationOptions (macOS 11+): list | banner | sound
33
+ const PRESENT_BANNER_LIST_SOUND = 8n | 16n | 2n;
34
+
35
+ let nextId = 1;
36
+
37
+ const bundled = (): boolean => asId(send(asId(send(cls("NSBundle"), sel("mainBundle"))), sel("bundleIdentifier"))) !== NIL;
38
+
39
+ export function createNotifier(options: NotifierOptions = {}): Notifier {
40
+ let disposed = false;
41
+ let authorized = false; // asked, not necessarily granted; the answer arrives off-thread and is not awaited
42
+ let center: Id = NIL;
43
+
44
+ const delegate = makeTarget("NativeShellNotificationDelegate", {
45
+ // Called on the main thread while the app is frontmost: show it anyway.
46
+ "userNotificationCenter:willPresentNotification:withCompletionHandler:": {
47
+ args: 3,
48
+ impl: (_center, _notification, handler) => invokeBlock(handler, [FFIType.u64], PRESENT_BANNER_LIST_SOUND),
49
+ },
50
+ // Main thread: the user clicked (or dismissed) a notification.
51
+ "userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:": {
52
+ args: 3,
53
+ impl: (_center, _response, handler) => {
54
+ if (!disposed) options.onClick?.();
55
+ invokeBlock(handler, []);
56
+ },
57
+ },
58
+ });
59
+
60
+ const ensureCenter = (): boolean => {
61
+ if (center !== NIL) return true;
62
+ if (!bundled()) return false;
63
+ center = asId(send(cls("UNUserNotificationCenter"), sel("currentNotificationCenter")));
64
+ if (center === NIL) return false;
65
+ sendPtr(center, sel("setDelegate:"), delegate);
66
+ return true;
67
+ };
68
+
69
+ const ensureAuthorized = () => {
70
+ if (authorized) return;
71
+ authorized = true;
72
+ const done = makeBlock(() => {}, [FFIType.bool, FFIType.u64], true); // (BOOL granted, NSError*) on a background queue
73
+ sendUIntPtrVoid(center, sel("requestAuthorizationWithOptions:completionHandler:"), UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge, done);
74
+ };
75
+
76
+ return {
77
+ notify(n) {
78
+ if (disposed || !ensureCenter()) return false;
79
+ ensureAuthorized();
80
+ const content = asId(send(allocRaw("UNMutableNotificationContent"), sel("init")));
81
+ sendPtr(content, sel("setTitle:"), nsString(n.title));
82
+ if (n.body) sendPtr(content, sel("setBody:"), nsString(n.body));
83
+ if (!n.silent) sendPtr(content, sel("setSound:"), asId(send(cls("UNNotificationSound"), sel("defaultSound"))));
84
+ const request = asId(sendPtrPtrPtr(cls("UNNotificationRequest"), sel("requestWithIdentifier:content:trigger:"), nsString(`sweetnative-${nextId++}`), content, NIL));
85
+ sendPtrPtr(center, sel("addNotificationRequest:withCompletionHandler:"), request, NIL);
86
+ release(content); // the request retains it
87
+ return true;
88
+ },
89
+ dispose() {
90
+ if (disposed) return;
91
+ disposed = true;
92
+ if (center !== NIL && asId(send(center, sel("delegate"))) === delegate) sendPtr(center, sel("setDelegate:"), NIL); // a newer notifier's delegate stays
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,246 @@
1
+ // Minimal Objective-C runtime bridge over bun:ffi, shared by the macOS
2
+ // native pieces (folder picker, main menu).
3
+ //
4
+ // Every `id` and `SEL` is carried as a bigint (FFIType.u64), never as a JS
5
+ // number. Short NSStrings are tagged pointers with the top bit set
6
+ // (0x8000_0000_0000_0000 | ...), which a double cannot hold exactly; passing
7
+ // one through `Number()` or `FFIType.ptr` rounds the low bits and the next
8
+ // objc_msgSend dereferences garbage.
9
+
10
+ import { CFunction, CString, FFIType, JSCallback, dlopen, ptr, read, type Pointer } from "bun:ffi";
11
+
12
+ // Make sure AppKit classes exist even if nothing else has loaded the framework.
13
+ dlopen("/System/Library/Frameworks/AppKit.framework/AppKit", {
14
+ NSApplicationLoad: { args: [], returns: FFIType.bool },
15
+ });
16
+
17
+ const objc = dlopen("/usr/lib/libobjc.A.dylib", {
18
+ objc_getClass: { args: [FFIType.ptr], returns: FFIType.u64 },
19
+ sel_registerName: { args: [FFIType.ptr], returns: FFIType.u64 },
20
+ objc_msgSend: { args: [FFIType.u64, FFIType.u64], returns: FFIType.u64 },
21
+ objc_allocateClassPair: { args: [FFIType.u64, FFIType.ptr, FFIType.u64], returns: FFIType.u64 },
22
+ objc_registerClassPair: { args: [FFIType.u64], returns: FFIType.void },
23
+ class_addMethod: { args: [FFIType.u64, FFIType.u64, FFIType.ptr, FFIType.ptr], returns: FFIType.bool },
24
+ });
25
+
26
+ export type Id = bigint;
27
+ export const NIL: Id = 0n;
28
+ export const asId = (v: bigint | number | null): Id => (v === null ? NIL : BigInt(v));
29
+
30
+ // objc_msgSend is effectively variadic; one CFunction per signature we need.
31
+ const msgSendPtr = (objc.symbols.objc_msgSend as unknown as { ptr: Pointer }).ptr;
32
+ if (typeof msgSendPtr !== "number" || msgSendPtr === 0) throw new Error("objc_msgSend address unavailable");
33
+ const fn = (args: FFIType[], returns: FFIType) =>
34
+ CFunction({ ptr: msgSendPtr, args, returns }) as unknown as (...a: unknown[]) => bigint | number | null;
35
+
36
+ export const ID = FFIType.u64;
37
+ /** [obj sel] -> id */
38
+ export const send = fn([ID, ID], ID);
39
+ /** [obj sel] -> NSInteger */
40
+ export const sendInt = fn([ID, ID], FFIType.i64);
41
+ /** [obj sel:BOOL] -> void */
42
+ export const sendBool = fn([ID, ID, FFIType.bool], FFIType.void);
43
+ /** [obj sel:NSInteger] -> BOOL */
44
+ export const sendIntRetBool = fn([ID, ID, FFIType.i64], FFIType.bool);
45
+ /** [obj sel:id] -> id */
46
+ export const sendPtr = fn([ID, ID, ID], ID);
47
+ /** [obj sel:id :SEL :id] -> id (initWithTitle:action:keyEquivalent:) */
48
+ export const sendPtrSelPtr = fn([ID, ID, ID, ID, ID], ID);
49
+
50
+ /**
51
+ * NUL-terminated UTF-8. Pass the array itself to an FFIType.ptr parameter, never
52
+ * `ptr(cstr(..))`: Bun keeps a TypedArray argument alive for the call, while a
53
+ * bare pointer may be collected by a GC that runs while later arguments are built.
54
+ */
55
+ export const cstr = (s: string): Uint8Array => new TextEncoder().encode(s + "\0");
56
+ export const sel = (name: string) => asId(objc.symbols.sel_registerName(cstr(name)));
57
+ export function cls(name: string): Id {
58
+ const c = asId(objc.symbols.objc_getClass(cstr(name)));
59
+ if (c === NIL) throw new Error(`Objective-C class ${name} not found`);
60
+ return c;
61
+ }
62
+ /** [obj sel:const char*] -> id (stringWithUTF8String:) */
63
+ export const sendCStr = fn([ID, ID, FFIType.ptr], ID);
64
+ export const nsString = (s: string) => asId(sendCStr(cls("NSString"), sel("stringWithUTF8String:"), cstr(s)));
65
+ /** JS string from an NSString (UTF8String is a real heap pointer, never tagged). */
66
+ export const jsString = (nsstr: Id): string => {
67
+ const utf8 = asId(send(nsstr, sel("UTF8String")));
68
+ return utf8 === NIL ? "" : new CString(Number(utf8) as Pointer).toString();
69
+ };
70
+ /** [[Class alloc] init] */
71
+ export const alloc = (className: string) => asId(send(allocRaw(className), sel("init")));
72
+ /** [Class alloc] only, for objects that get an initWith...: next. */
73
+ export const allocRaw = (className: string) => asId(send(cls(className), sel("alloc")));
74
+ export const sharedApp = () => asId(send(cls("NSApplication"), sel("sharedApplication")));
75
+
76
+ // Callbacks the runtime may call for the life of the process; keep them alive.
77
+ const retained: JSCallback[] = [];
78
+
79
+ /** A method body: a one-argument `-(void)name:(id)sender`, or `{ args, impl }` for n object/pointer arguments. */
80
+ export type TargetMethod = ((sender: Id) => void) | { args: number; impl: (...args: Id[]) => void };
81
+
82
+ /**
83
+ * Defines an NSObject subclass whose instance methods are JS functions, and
84
+ * returns one instance of it. Every method returns void and takes object or
85
+ * pointer arguments (blocks included), which is what menu items, buttons and
86
+ * delegates call.
87
+ */
88
+ let targetClasses = 0;
89
+
90
+ /** Registers a fresh NSObject subclass whose selectors call into JavaScript; the name is made unique, so the same base name can be used again. */
91
+ export function makeTarget(baseName: string, methods: Record<string, TargetMethod>): Id {
92
+ const className = `${baseName}_${++targetClasses}`;
93
+ const klass = asId(objc.symbols.objc_allocateClassPair(cls("NSObject"), cstr(className), 0n));
94
+ if (klass === NIL) throw new Error(`objc_allocateClassPair(${className}) failed (already registered?)`);
95
+ for (const [selector, method] of Object.entries(methods)) {
96
+ const argc = typeof method === "function" ? 1 : method.args;
97
+ const impl = typeof method === "function" ? (sender: Id) => method(sender) : method.impl;
98
+ const cb = new JSCallback(
99
+ (_self: bigint, _cmd: bigint, ...args: bigint[]) => {
100
+ try {
101
+ impl(...args.map((a) => BigInt(a ?? 0)));
102
+ } catch (err) {
103
+ console.error(`sweetnative: ${className} ${selector} threw:`, err);
104
+ }
105
+ },
106
+ { args: [ID, ID, ...Array<FFIType>(argc).fill(ID)], returns: FFIType.void },
107
+ );
108
+ retained.push(cb);
109
+ if (!objc.symbols.class_addMethod(klass, sel(selector), cb.ptr, cstr("v@:" + "@".repeat(argc)))) {
110
+ throw new Error(`class_addMethod(${className}, ${selector}) failed`);
111
+ }
112
+ }
113
+ objc.symbols.objc_registerClassPair(klass);
114
+ return asId(send(asId(send(klass, sel("alloc"))), sel("init")));
115
+ }
116
+
117
+ /**
118
+ * Register a subclass whose listed selectors answer YES: `defineYesClass("NSWindow",
119
+ * "SweetnativeKeyWindow", ["canBecomeKeyWindow"])` makes a borderless window
120
+ * that can take keyboard focus. Once per class name; a second call is a no-op.
121
+ */
122
+ export function defineYesClass(superclass: string, className: string, selectors: string[]): void {
123
+ if (asId(objc.symbols.objc_getClass(cstr(className))) !== NIL) return;
124
+ const klass = asId(objc.symbols.objc_allocateClassPair(cls(superclass), cstr(className), 0n));
125
+ if (klass === NIL) throw new Error(`objc_allocateClassPair(${className}) failed`);
126
+ for (const selector of selectors) {
127
+ const cb = new JSCallback((_self: bigint, _cmd: bigint) => true, { args: [ID, ID], returns: FFIType.bool });
128
+ retained.push(cb);
129
+ if (!objc.symbols.class_addMethod(klass, sel(selector), cb.ptr, cstr("B@:"))) throw new Error(`class_addMethod(${className}, ${selector}) failed`);
130
+ }
131
+ objc.symbols.objc_registerClassPair(klass);
132
+ }
133
+
134
+ // ---- Data symbols and blocks ------------------------------------------------
135
+
136
+ const RTLD_NOW = 2;
137
+ const dl = dlopen("/usr/lib/libSystem.B.dylib", {
138
+ dlopen: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.ptr },
139
+ dlsym: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.ptr },
140
+ }).symbols;
141
+
142
+ /** Address of a data symbol (bun's dlopen binds functions only). */
143
+ export function dataSymbolAddress(library: string, name: string): bigint {
144
+ const image = dl.dlopen(cstr(library), RTLD_NOW);
145
+ if (image === null) throw new Error(`dlopen(${library}) failed`);
146
+ const address = dl.dlsym(image, cstr(name));
147
+ if (address === null) throw new Error(`dlsym(${name}) in ${library} returned null`);
148
+ return BigInt(address);
149
+ }
150
+
151
+ const BLOCK_IS_GLOBAL = 1 << 28;
152
+ const BLOCK_LITERAL_SIZE = 32;
153
+ let globalBlockIsa: bigint | undefined;
154
+ const blockDescriptor = new BigUint64Array([0n, BigInt(BLOCK_LITERAL_SIZE)]); // { reserved, size }
155
+ const retainedBlocks: Uint8Array[] = [];
156
+
157
+ /**
158
+ * A global Objective-C block (captures nothing, lives for the process) whose
159
+ * body is a JS function. `argTypes` are the block's own parameters, after the
160
+ * implicit block pointer. Object parameters must be FFIType.u64.
161
+ *
162
+ * `threadsafe` blocks are for completion handlers that AppKit calls on a
163
+ * background queue; Bun then queues the JS body for the event loop, which is
164
+ * blocked while the webview runs, so such a body effectively never runs and
165
+ * the block is fire-and-forget.
166
+ */
167
+ export function makeBlock(body: (...args: bigint[]) => void, argTypes: FFIType[], threadsafe = false): Id {
168
+ globalBlockIsa ??= dataSymbolAddress("/usr/lib/libSystem.B.dylib", "_NSConcreteGlobalBlock");
169
+ const cb = new JSCallback((_block: bigint, ...args: bigint[]) => body(...args), {
170
+ args: [FFIType.u64, ...argTypes],
171
+ returns: FFIType.void,
172
+ threadsafe,
173
+ });
174
+ if (cb.ptr === null) throw new Error("sweetnative: could not allocate a block callback");
175
+ retained.push(cb);
176
+ const literal = new Uint8Array(BLOCK_LITERAL_SIZE);
177
+ const dv = new DataView(literal.buffer);
178
+ dv.setBigUint64(0, globalBlockIsa, true); // isa
179
+ dv.setInt32(8, BLOCK_IS_GLOBAL, true); // flags
180
+ dv.setInt32(12, 0, true); // reserved
181
+ dv.setBigUint64(16, BigInt(cb.ptr), true); // invoke
182
+ dv.setBigUint64(24, BigInt(ptr(blockDescriptor)), true); // descriptor
183
+ retainedBlocks.push(literal);
184
+ return BigInt(ptr(literal));
185
+ }
186
+
187
+ /** Calls a block someone handed us (a completion handler): `invoke(block, ...args)`. */
188
+ export function invokeBlock(block: Id, argTypes: FFIType[], ...args: (bigint | number)[]): void {
189
+ if (block === NIL) return;
190
+ const invoke = read.ptr(Number(block) as Pointer, 16) as unknown as Pointer | bigint | null;
191
+ if (invoke === null) throw new Error("sweetnative: block has no invoke pointer");
192
+ const fn = CFunction({ ptr: Number(invoke) as Pointer, args: [FFIType.u64, ...argTypes], returns: FFIType.void }) as unknown as (...a: unknown[]) => void;
193
+ fn(block, ...args);
194
+ }
195
+
196
+ // ---- More objc_msgSend shapes (tray, menus, window) -------------------------
197
+
198
+ /** [obj sel:double] -> id (statusItemWithLength:) */
199
+ export const sendF64 = fn([ID, ID, FFIType.f64], ID);
200
+ /** [obj sel] -> double (doubleClickInterval) */
201
+ export const sendRetF64 = fn([ID, ID], FFIType.f64);
202
+ /** [obj sel] -> BOOL (isVisible) */
203
+ export const sendRetBool = fn([ID, ID], FFIType.bool);
204
+ /** [obj sel:NSInteger] -> void (setState:) */
205
+ export const sendIntArg = fn([ID, ID, FFIType.i64], FFIType.void);
206
+ /** [obj sel:NSUInteger :id] -> void (requestAuthorizationWithOptions:completionHandler:) */
207
+ export const sendUIntPtrVoid = fn([ID, ID, FFIType.u64, ID], FFIType.void);
208
+ /** [obj sel:id :id :id] -> id (requestWithIdentifier:content:trigger:) */
209
+ export const sendPtrPtrPtr = fn([ID, ID, ID, ID, ID], ID);
210
+ /** [obj sel:NSUInteger] -> id (standardWindowButton:) */
211
+ export const sendUIntRetPtr = fn([ID, ID, FFIType.u64], ID);
212
+ /** [obj sel:NSUInteger] -> NSInteger (sendActionOn:) */
213
+ export const sendUInt = fn([ID, ID, FFIType.u64], FFIType.i64);
214
+ /** [obj sel:id :id] -> id (imageWithSystemSymbolName:accessibilityDescription:) */
215
+ export const sendPtrPtr = fn([ID, ID, ID, ID], ID);
216
+ /** [obj sel:bytes :NSUInteger] -> id (dataWithBytes:length:) */
217
+ export const sendBytesLen = fn([ID, ID, FFIType.ptr, FFIType.u64], ID);
218
+ /** [obj sel:SEL :id :double] -> void (performSelector:withObject:afterDelay:) */
219
+ export const sendSelPtrF64 = fn([ID, ID, ID, ID, FFIType.f64], FFIType.void);
220
+ /** [obj sel:id :SEL :id] -> void (cancelPreviousPerformRequestsWithTarget:selector:object:) */
221
+ export const sendPtrSelPtrVoid = fn([ID, ID, ID, ID, ID], FFIType.void);
222
+
223
+ // Structs by value. On arm64 an NSRect (4 doubles) and an NSPoint (2 doubles)
224
+ // are passed in d0..d3 exactly like separate double arguments, and integer
225
+ // arguments after them still go in x2.. (Apple Silicon only, as is this package).
226
+ /** [obj sel:NSRect :NSUInteger :NSUInteger :BOOL] -> id (initWithContentRect:styleMask:backing:defer:) */
227
+ export const sendRectUIntUIntBool = fn([ID, ID, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.u64, FFIType.u64, FFIType.bool], ID);
228
+ /** [obj sel:NSRect :BOOL] -> void (setFrame:display:) */
229
+ export const sendRectBool = fn([ID, ID, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.bool], FFIType.void);
230
+ /** [obj sel:NSPoint] -> void (setFrameOrigin:) */
231
+ export const sendPoint = fn([ID, ID, FFIType.f64, FFIType.f64], FFIType.void);
232
+ /** [obj sel:NSSize] -> void (setSize:); NSSize is two doubles like NSPoint, so the same call. */
233
+ export const sendSize = sendPoint;
234
+ /** [obj sel:NSUInteger] -> void (setCollectionBehavior:, setStyleMask:) */
235
+ export const sendUIntArg = fn([ID, ID, FFIType.u64], FFIType.void);
236
+ /** [obj sel:double] -> void (setAlphaValue:) */
237
+ export const sendF64Void = fn([ID, ID, FFIType.f64], FFIType.void);
238
+ /** [obj sel:void*] -> id (getReturnValue:, getValue:) */
239
+ export const sendPtrArg = fn([ID, ID, FFIType.ptr], ID);
240
+ /** [obj sel:double :id :SEL :id :BOOL] -> id (scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:) */
241
+ export const sendF64PtrSelPtrBool = fn([ID, ID, FFIType.f64, ID, ID, ID, FFIType.bool], ID);
242
+
243
+ export const retain = (obj: Id): Id => asId(send(obj, sel("retain")));
244
+ export const release = (obj: Id): void => {
245
+ if (obj !== NIL) send(obj, sel("release"));
246
+ };